From de685095b8237eed9e926037f8914dd5050d4541 Mon Sep 17 00:00:00 2001 From: Rishabh Gupta Date: Wed, 2 Oct 2024 20:40:39 +0530 Subject: [PATCH 1/2] feat: Schematic kicad style --- bun.lockb | Bin 258727 -> 260032 bytes .../convert-circuit-json-to-schematic-svg.ts | 642 ++++++++++-------- lib/utils/colors.ts | 235 +++++++ package.json | 7 +- .../schematic-resistor-capacitor.stories.tsx | 367 ++++++++++ tests/fixtures/get-test-fixture.ts | 10 + .../__snapshots__/kicad-theme-demo.snap.svg | 10 + tests/sch/kicad-theme-demo.test.tsx | 54 ++ 8 files changed, 1046 insertions(+), 279 deletions(-) create mode 100644 lib/utils/colors.ts create mode 100644 stories/schematic-resistor-capacitor.stories.tsx create mode 100644 tests/fixtures/get-test-fixture.ts create mode 100644 tests/sch/__snapshots__/kicad-theme-demo.snap.svg create mode 100644 tests/sch/kicad-theme-demo.test.tsx diff --git a/bun.lockb b/bun.lockb index cdf56145586e99e66a110e0936726bd2364c859d..9ca52c8aa7867c02dbe874e6956fc94b7b7450a2 100755 GIT binary patch delta 20917 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 delta 20290 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 c3Pp+wU item.type === "schematic_port")) { - const flippedCenter = { x: port.center.x, y: flipY(port.center.y) } - const svg = createSchematicPort(flippedCenter) - svgChildren.push(svg) - - const component = componentMap.get(port.schematic_component_id) - if (component) { - const line = createPortToComponentLine( - flippedCenter, - component, - port.facing_direction || "right", - ) - svgChildren.push(line) - } - } - // Process schematic traces for (const trace of soup.filter((item) => item.type === "schematic_trace")) { const svg = createSchematicTrace(trace, flipY, portPositions) if (svg) svgChildren.push(svg) } - // Process text - for (const text of soup.filter((item) => item.type === "schematic_text")) { - const flippedPosition = { x: text.position.x, y: flipY(text.position.y) } - const svg = createSchematicText(text, flippedPosition) - svgChildren.push(svg) - } - const padding = 1 const width = maxX - minX + 2 * padding const viewBox = `${minX - padding} ${minY - padding} ${width} ${height + 2 * padding}` @@ -89,9 +74,9 @@ export function convertCircuitJsonToSchematicSvg( attributes: { xmlns: "http://www.w3.org/2000/svg", viewBox, - width: "1200", - height: "600", - style: "background-color: #fff;", + width: options?.width ?? "1200", + height: options?.height ?? "600", + style: `background-color: ${colorMap.schematic.background}`, }, children: [ { @@ -101,11 +86,14 @@ export function convertCircuitJsonToSchematicSvg( { type: "text", value: ` - .component { fill: none; stroke: red; stroke-width: 0.03; } - .component-pin { fill: none; stroke: red; stroke-width: 0.03; } - .trace { stroke: green; stroke-width: 0.03; fill: none; } - .text { font-family: Arial, sans-serif; font-size: 0.2px; } - .port { fill: none; stroke: blue; stroke-width: 0.03; } + .component { fill: none; stroke: ${colorMap.schematic.component_outline}; stroke-width: 0.03; } + .chip { fill: ${colorMap.schematic.component_body}; stroke: ${colorMap.schematic.component_outline}; stroke-width: 0.03; } + .component-pin { fill: none; stroke: ${colorMap.schematic.component_outline}; stroke-width: 0.02; } + .trace { stroke: ${colorMap.schematic.wire}; stroke-width: 0.02; fill: none; } + .text { font-family: Arial, sans-serif; font-size: 0.2px; fill: ${colorMap.schematic.wire}; } + .pin-number { font-size: 0.15px; fill: ${colorMap.schematic.pin_number}; } + .port-label { fill: ${colorMap.schematic.reference}; } + .component-name { font-size: 0.25px; fill: ${colorMap.schematic.reference}; } `, }, ], @@ -116,273 +104,375 @@ export function convertCircuitJsonToSchematicSvg( return stringify({ value: "", ...svgObject }) - function updateBounds(center: any, size: any, rotation: number) { - const corners = [ - { x: -size.width / 2, y: -size.height / 2 }, - { x: size.width / 2, y: -size.height / 2 }, - { x: size.width / 2, y: size.height / 2 }, - { x: -size.width / 2, y: size.height / 2 }, - ] - - for (const corner of corners) { - const rotatedX = - corner.x * Math.cos(rotation) - corner.y * Math.sin(rotation) + center.x - const rotatedY = - corner.x * Math.sin(rotation) + corner.y * Math.cos(rotation) + center.y - minX = Math.min(minX, rotatedX) - minY = Math.min(minY, rotatedY) - maxX = Math.max(maxX, rotatedX) - maxY = Math.max(maxY, rotatedY) - } - } -} - -function createSchematicComponent( - center: { x: number; y: number }, - size: { width: number; height: number }, - rotation: number, - symbolName?: string, -): any { - const transform = `translate(${center.x}, ${center.y}) rotate(${(rotation * 180) / Math.PI})` - - if (symbolName) { - const symbol = (symbols as any)[symbolName] - const paths = symbol.primitives.filter((p: any) => p.type === "path") - const updatedSymbol = { - ...symbol, - primitives: paths, - } - const svg = parseSync( - getSvg(updatedSymbol, { - width: size.width, - height: size.height, - }), + function createSchematicComponent( + center: { x: number; y: number }, + size: { width: number; height: number }, + rotation: number, + symbolName?: string, + portArrangement?: any, + portLabels?: any, + sourceComponentId?: string, + circuitJson?: AnyCircuitElement[], + ): any { + const transform = `translate(${center.x}, ${center.y}) rotate(${(rotation * 180) / Math.PI})` + + let children: any[] = [] + + // Find the source component and get its name + const sourceComponent = circuitJson?.find( + (item) => + item.type === "source_component" && + item.source_component_id === sourceComponentId, ) - - // Filter out non-path elements and modify path colors - const pathElements = svg.children - .filter( - (child: any) => - child.name === "path" && child.attributes.fill !== "green", + const componentName = sourceComponent ? sourceComponent.name : "" + const manufacturerNumber = sourceComponent?.manufacturer_part_number + const resistance = sourceComponent?.resistance + const capacitance = sourceComponent?.capacitance + + if (symbolName) { + const symbol = (symbols as any)[symbolName] + const paths = symbol.primitives.filter((p: any) => p.type === "path") + const updatedSymbol = { + ...symbol, + primitives: paths, + } + const svg = parseSync( + getSvg(updatedSymbol, { + width: size.width, + height: size.height, + }), ) - .map((path: any) => { - const currentStrokeWidth = Number.parseFloat( - path.attributes["stroke-width"] || "0.02", - ) - const newStrokeWidth = (currentStrokeWidth * 1.5).toString() - return { - ...path, - attributes: { - ...path.attributes, - stroke: - path.attributes.stroke === "black" - ? "red" - : path.attributes.stroke, - "stroke-width": newStrokeWidth, - }, - } + children = svg.children + .filter( + (child: any) => + child.name === "path" && child.attributes.fill !== "green", + ) + .map((path: any) => { + const currentStrokeWidth = Number.parseFloat( + path.attributes["stroke-width"] || "0.02", + ) + const newStrokeWidth = (currentStrokeWidth * 1.5).toString() + + return { + ...path, + attributes: { + ...path.attributes, + stroke: + path.attributes.stroke === "black" + ? `${colorMap.schematic.component_outline}` + : path.attributes.stroke, + "stroke-width": newStrokeWidth, + }, + } + }) + } else { + children.push({ + name: "rect", + type: "element", + attributes: { + class: "component chip", + x: -size.width / 2, + y: -size.height / 2, + width: size.width, + height: size.height, + }, }) - - // Check if viewBox attribute exists - const viewBoxAttr = svg.attributes.viewBox - if (typeof viewBoxAttr === "undefined") { - throw new Error("SVG does not have a viewBox attribute.") - } - - // Extract viewBox values - const viewBox = viewBoxAttr.split(" ").map(Number) - if (viewBox.length < 4) { - throw new Error("Invalid viewBox attribute.") } - const [minX, minY, width = 0, height = 0] = viewBox - - // Calculate scale factors - const scaleX = size.width / (width || 1) - const scaleY = size.height / (height || 1) - const scale = Math.min(scaleX, scaleY) - - // Adjust transformation to include scaling and centering - const adjustedTransform = `${transform} scale(${scale}) translate(${-(minX ?? 0) - width / 2}, ${-(minY ?? 0) - height / 2})` + if (manufacturerNumber) { + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 1.2, + y: -size.height / 2 - 0.4, // Position above the component + "text-anchor": "right", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: manufacturerNumber }], + }) - return { - name: "g", - type: "element", - attributes: { transform: adjustedTransform }, - children: pathElements, + // Add component name on top + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 1.2, + y: -size.height / 2 - 0.7, // Position above the component + "text-anchor": "right", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: componentName }], + }) } - } - return { - name: "g", - type: "element", - attributes: { transform }, - children: [ - { - name: "rect", + if (resistance || capacitance) { + children.push({ + name: "text", type: "element", attributes: { - class: "component", - x: (-size.width / 2).toString(), - y: (-size.height / 2).toString(), - width: size.width.toString(), - height: size.height.toString(), + class: "component-name", + x: 0, + y: (-size.height / 2) - 0.2, // Position above the component + "text-anchor": "middle", + "dominant-baseline": "auto", }, - }, - ], - } -} + children: [{ type: "text", value: resistance || capacitance }], + }) -function createSchematicPort(center: { x: number; y: number }): any { - const portSize = 0.2 - const x = center.x - portSize / 2 - const y = center.y - portSize / 2 + // Add component name on top + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 0, + y: (-size.height / 2) - 0.5, // Position above the component + "text-anchor": "middle", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: componentName }], + }) + } - return { - name: "rect", - type: "element", - attributes: { - class: "port", - x: x.toString(), - y: y.toString(), - width: portSize.toString(), - height: portSize.toString(), - }, - } -} + // Add ports if portArrangement is provided + if (portArrangement) { + const portLength = 0.2 // Length of the port line + const circleRadius = 0.05 // Radius of the port circle + const labelOffset = 0.1 // Offset for label positioning + + // console.log(portArrangement) + + for (const [side, arrangement] of Object.entries(portArrangement)) { + if(arrangement === undefined) continue + + const pins = arrangement.pins + const direction = arrangement.direction + + let getX: (index: number, total: number) => number + let getY: (index: number, total: number) => number + let getEndX: (x: number) => number + let getEndY: (y: number) => number + let getLabelX: (x: number) => number + let getLabelY: (y: number) => number + let labelAnchor: string + let isVertical = false + + switch (side) { + case "left_side": + getX = () => -size.width / 2 + getY = (index, total) => + -size.height / 2 + (size.height * (index + 1)) / (total + 1) + getEndX = (x) => x - portLength + getEndY = (y) => y + getLabelX = (x) => x + labelOffset + getLabelY = (y) => y + labelAnchor = "start" + break + case "right_side": + getX = () => size.width / 2 + getY = (index, total) => + -size.height / 2 + (size.height * (index + 1)) / (total + 1) + getEndX = (x) => x + portLength + getEndY = (y) => y + getLabelX = (x) => x - labelOffset + getLabelY = (y) => y + labelAnchor = "end" + break + case "top_side": + getX = (index, total) => + -size.width / 2 + (size.width * (index + 1)) / (total + 1) + getY = () => -size.height / 2 + getEndX = (x) => x + getEndY = (y) => y - portLength + getLabelX = (x) => x + getLabelY = (y) => y + labelOffset + 0.15 + labelAnchor = "middle" + isVertical = true + break + case "bottom_side": + getX = (index, total) => + -size.width / 2 + (size.width * (index + 1)) / (total + 1) + getY = () => size.height / 2 + getEndX = (x) => x + getEndY = (y) => y + portLength + getLabelX = (x) => x + getLabelY = (y) => y - labelOffset - 0.15 + labelAnchor = "middle" + isVertical = true + break + default: + continue // Skip unknown sides + } -function createPortToComponentLine( - portCenter: { x: number; y: number }, - component: any, - facingDirection: string, -): any { - const componentCenter = { x: component.center.x, y: portCenter.y } - const halfWidth = component.size.width / 2 - const halfHeight = component.size.height / 2 - - let endX = portCenter.x - let endY = portCenter.y - - switch (facingDirection) { - case "left": - endX = componentCenter.x - halfWidth - break - case "right": - endX = componentCenter.x + halfWidth - break - case "up": - endY = componentCenter.y - halfHeight - break - case "down": - endY = componentCenter.y + halfHeight - break - } + const totalPins = pins.length + + pins.forEach((pin: number, index: number) => { + let x = getX(index, totalPins) + let y = getY(index, totalPins) + + if (direction === "bottom-to-top" || direction === "right-to-left") { + ;[x, y] = [ + getX(totalPins - 1 - index, totalPins), + getY(totalPins - 1 - index, totalPins), + ] + } + + const endX = getEndX(x) + const endY = getEndY(y) + + children.push({ + name: "line", + type: "element", + attributes: { + class: "component-pin", + x1: x, + y1: y, + x2: endX, + y2: endY, + }, + }) + + children.push({ + name: "circle", + type: "element", + attributes: { + class: "component-pin", + cx: endX, + cy: endY, + r: circleRadius, + }, + }) + + // Add label if it exists in portLabels + const labelKey = `pin${pin}` + if (portLabels && labelKey in portLabels) { + let labelTransform = "" + if (isVertical) { + labelTransform = `rotate(${side === "top_side" ? -90 : 270}, ${getLabelX(x)}, ${getLabelY(y)})` + } + children.push({ + name: "text", + type: "element", + attributes: { + class: "port-label", + x: getLabelX(x), + y: getLabelY(y), + "text-anchor": labelAnchor, + "dominant-baseline": "middle", + "font-size": "0.2", + transform: labelTransform, + }, + children: [{ type: "text", value: portLabels[labelKey] }], + }) + } + // Add pin number + children.push({ + name: "text", + type: "element", + attributes: { + class: "pin-number", + x: endX, + y: endY + (side === "bottom_side" ? 0.15 : -0.15), + "text-anchor": "middle", + "dominant-baseline": "middle", + "font-size": "0.15", + }, + children: [{ type: "text", value: pin.toString() }], + }) + }) + } + } - return { - name: "line", - type: "element", - attributes: { - class: "component-pin", - x1: portCenter.x.toString(), - y1: portCenter.y.toString(), - x2: endX.toString(), - y2: endY.toString(), - }, + return { + name: "g", + type: "element", + attributes: { transform }, + children, + } } -} -function createSchematicTrace( - trace: any, - flipY: (y: number) => number, - portPositions: Map, -): any { - const edges = trace.edges - if (edges.length === 0) return null - - let path = "" - - // Process all edges - edges.forEach((edge: any, index: number) => { - const fromPoint = - edge.from.ti !== undefined ? portPositions.get(edge.from.ti) : edge.from - const toPoint = - edge.to.ti !== undefined ? portPositions.get(edge.to.ti) : edge.to - - if (!fromPoint || !toPoint) { - return - } + function createSchematicTrace( + trace: any, + flipY: (y: number) => number, + portPositions: Map, + ): any { + const edges = trace.edges + if (edges.length === 0) return null + + let path = "" + + // Process all edges + edges.forEach((edge: any, index: number) => { + const fromPoint = + edge.from.ti !== undefined ? portPositions.get(edge.from.ti) : edge.from + const toPoint = + edge.to.ti !== undefined ? portPositions.get(edge.to.ti) : edge.to + + if (!fromPoint || !toPoint) { + return + } - const fromCoord = `${fromPoint.x} ${flipY(fromPoint.y)}` - const toCoord = `${toPoint.x} ${flipY(toPoint.y)}` + const fromCoord = `${fromPoint.x - 0.15} ${flipY(fromPoint.y)}` + const toCoord = `${toPoint.x + 0.15} ${flipY(toPoint.y)}` - if (index === 0) { - path += `M ${fromCoord} L ${toCoord}` - } else { - path += ` L ${toCoord}` - } - }) - - // Handle connection to final port if needed - if (trace.to_schematic_port_id) { - const finalPort = portPositions.get(trace.to_schematic_port_id) - if (finalPort) { - const lastFromPoint = path.split("M")[1]?.split("L")[0] - const lastEdge = edges[edges.length - 1] - const lastPoint = - lastEdge.to.ti !== undefined - ? portPositions.get(lastEdge.to.ti) - : lastEdge.to - if (lastPoint.x !== finalPort.x || lastPoint.y !== finalPort.y) { - const finalCoord = `${finalPort.x} ${flipY(finalPort.y)}` - path += ` M ${lastFromPoint} L ${finalCoord}` + if (index === 0) { + path += `M ${fromCoord} L ${toCoord}` + } else { + path += ` L ${toCoord}` } - } - } - - return path - ? { - name: "path", - type: "element", - attributes: { - class: "trace", - d: path, - }, + }) + + // Handle connection to final port if needed + if (trace.to_schematic_port_id) { + const finalPort = portPositions.get(trace.to_schematic_port_id) + if (finalPort) { + const lastFromPoint = path.split("M")[1]?.split("L")[0] + const lastEdge = edges[edges.length - 1] + const lastPoint = + lastEdge.to.ti !== undefined + ? portPositions.get(lastEdge.to.ti) + : lastEdge.to + if (lastPoint.x !== finalPort.x || lastPoint.y !== finalPort.y) { + const finalCoord = `${finalPort.x} ${flipY(finalPort.y)}` + path += ` M ${lastFromPoint} L ${finalCoord}` + } } - : null -} + } -function createSchematicText( - text: any, - position: { x: number; y: number }, -): any { - return { - name: "text", - type: "element", - attributes: { - class: "text", - x: position.x.toString(), - y: position.y.toString(), - "text-anchor": getTextAnchor(text.anchor), - "dominant-baseline": "middle", - }, - children: [ - { - type: "text", - value: text.text ? text.text : "", - }, - ], + return path + ? { + name: "path", + type: "element", + attributes: { + class: "trace", + d: path, + }, + } + : null } -} -function getTextAnchor(anchor: string): string { - switch (anchor) { - case "left": - return "start" - case "right": - return "end" - default: - return "middle" + function updateBounds(center: any, size: any, rotation: number) { + const corners = [ + { x: -size.width / 2, y: -size.height / 2 }, + { x: size.width / 2, y: -size.height / 2 }, + { x: size.width / 2, y: size.height / 2 }, + { x: -size.width / 2, y: size.height / 2 }, + ] + + for (const corner of corners) { + const rotatedX = + corner.x * Math.cos(rotation) - corner.y * Math.sin(rotation) + center.x + const rotatedY = + corner.x * Math.sin(rotation) + corner.y * Math.cos(rotation) + center.y + minX = Math.min(minX, rotatedX) + minY = Math.min(minY, rotatedY) + maxX = Math.max(maxX, rotatedX) + maxY = Math.max(maxY, rotatedY) + } } } diff --git a/lib/utils/colors.ts b/lib/utils/colors.ts new file mode 100644 index 0000000..cfb20c5 --- /dev/null +++ b/lib/utils/colors.ts @@ -0,0 +1,235 @@ +// Kicad-2020 color scheme +export const colorMap = { + "3d_viewer": { + background_bottom: "rgb(102, 102, 128)", + background_top: "rgb(204, 204, 230)", + board: "rgb(51, 43, 23)", + copper: "rgb(179, 156, 0)", + silkscreen_bottom: "rgb(230, 230, 230)", + silkscreen_top: "rgb(230, 230, 230)", + soldermask: "rgb(20, 51, 36)", + solderpaste: "rgb(128, 128, 128)", + }, + board: { + anchor: "rgb(255, 38, 226)", + aux_items: "rgb(255, 255, 255)", + b_adhes: "rgb(0, 0, 132)", + b_crtyd: "rgb(255, 38, 226)", + b_fab: "rgb(88, 93, 132)", + b_mask: "rgba(2, 255, 238, 0.400)", + b_paste: "rgb(0, 194, 194)", + b_silks: "rgb(232, 178, 167)", + background: "rgb(0, 16, 35)", + cmts_user: "rgb(89, 148, 220)", + copper: { + b: "rgb(77, 127, 196)", + f: "rgb(200, 52, 52)", + in1: "rgb(127, 200, 127)", + in10: "rgb(237, 124, 51)", + in11: "rgb(91, 195, 235)", + in12: "rgb(247, 111, 142)", + in13: "rgb(167, 165, 198)", + in14: "rgb(40, 204, 217)", + in15: "rgb(232, 178, 167)", + in16: "rgb(242, 237, 161)", + in17: "rgb(237, 124, 51)", + in18: "rgb(91, 195, 235)", + in19: "rgb(247, 111, 142)", + in2: "rgb(206, 125, 44)", + in20: "rgb(167, 165, 198)", + in21: "rgb(40, 204, 217)", + in22: "rgb(232, 178, 167)", + in23: "rgb(242, 237, 161)", + in24: "rgb(237, 124, 51)", + in25: "rgb(91, 195, 235)", + in26: "rgb(247, 111, 142)", + in27: "rgb(167, 165, 198)", + in28: "rgb(40, 204, 217)", + in29: "rgb(232, 178, 167)", + in3: "rgb(79, 203, 203)", + in30: "rgb(242, 237, 161)", + in4: "rgb(219, 98, 139)", + in5: "rgb(167, 165, 198)", + in6: "rgb(40, 204, 217)", + in7: "rgb(232, 178, 167)", + in8: "rgb(242, 237, 161)", + in9: "rgb(141, 203, 129)", + }, + cursor: "rgb(255, 255, 255)", + drc: "rgb(194, 194, 194)", + drc_error: "rgba(215, 91, 107, 0.800)", + drc_exclusion: "rgb(255, 255, 255)", + drc_warning: "rgba(255, 208, 66, 0.902)", + dwgs_user: "rgb(194, 194, 194)", + eco1_user: "rgb(180, 219, 210)", + eco2_user: "rgb(216, 200, 82)", + edge_cuts: "rgb(208, 210, 205)", + f_adhes: "rgb(132, 0, 132)", + f_crtyd: "rgb(255, 0, 245)", + f_fab: "rgb(175, 175, 175)", + f_mask: "rgba(216, 100, 255, 0.400)", + f_paste: "rgba(180, 160, 154, 0.902)", + f_silks: "rgb(242, 237, 161)", + footprint_text_back: "rgb(0, 0, 132)", + footprint_text_front: "rgb(194, 194, 194)", + footprint_text_invisible: "rgb(132, 132, 132)", + grid: "rgb(132, 132, 132)", + grid_axes: "rgb(194, 194, 194)", + margin: "rgb(255, 38, 226)", + microvia: "rgb(0, 132, 132)", + no_connect: "rgb(0, 0, 132)", + pad_back: "rgb(77, 127, 196)", + pad_front: "rgb(200, 52, 52)", + pad_plated_hole: "rgb(194, 194, 0)", + pad_through_hole: "rgb(227, 183, 46)", + plated_hole: "rgb(26, 196, 210)", + ratsnest: "rgba(245, 255, 213, 0.702)", + select_overlay: "rgb(4, 255, 67)", + through_via: "rgb(236, 236, 236)", + user_1: "rgb(194, 194, 194)", + user_2: "rgb(89, 148, 220)", + user_3: "rgb(180, 219, 210)", + user_4: "rgb(216, 200, 82)", + user_5: "rgb(194, 194, 194)", + user_6: "rgb(89, 148, 220)", + user_7: "rgb(180, 219, 210)", + user_8: "rgb(216, 200, 82)", + user_9: "rgb(232, 178, 167)", + via_blind_buried: "rgb(187, 151, 38)", + via_hole: "rgb(227, 183, 46)", + via_micro: "rgb(0, 132, 132)", + via_through: "rgb(236, 236, 236)", + worksheet: "rgb(200, 114, 171)", + }, + gerbview: { + axes: "rgb(0, 0, 132)", + background: "rgb(0, 0, 0)", + dcodes: "rgb(255, 255, 255)", + grid: "rgb(132, 132, 132)", + layers: [ + "rgb(132, 0, 0)", + "rgb(194, 194, 0)", + "rgb(194, 0, 194)", + "rgb(194, 0, 0)", + "rgb(0, 132, 132)", + "rgb(0, 132, 0)", + "rgb(0, 0, 132)", + "rgb(132, 132, 132)", + "rgb(132, 0, 132)", + "rgb(194, 194, 194)", + "rgb(132, 0, 132)", + "rgb(132, 0, 0)", + "rgb(132, 132, 0)", + "rgb(194, 194, 194)", + "rgb(0, 0, 132)", + "rgb(0, 132, 0)", + "rgb(132, 0, 0)", + "rgb(194, 194, 0)", + "rgb(194, 0, 194)", + "rgb(194, 0, 0)", + "rgb(0, 132, 132)", + "rgb(0, 132, 0)", + "rgb(0, 0, 132)", + "rgb(132, 132, 132)", + "rgb(132, 0, 132)", + "rgb(194, 194, 194)", + "rgb(132, 0, 132)", + "rgb(132, 0, 0)", + "rgb(132, 132, 0)", + "rgb(194, 194, 194)", + "rgb(0, 0, 132)", + "rgb(0, 132, 0)", + "rgb(132, 0, 0)", + "rgb(194, 194, 0)", + "rgb(194, 0, 194)", + "rgb(194, 0, 0)", + "rgb(0, 132, 132)", + "rgb(0, 132, 0)", + "rgb(0, 0, 132)", + "rgb(132, 132, 132)", + "rgb(132, 0, 132)", + "rgb(194, 194, 194)", + "rgb(132, 0, 132)", + "rgb(132, 0, 0)", + "rgb(132, 132, 0)", + "rgb(194, 194, 194)", + "rgb(0, 0, 132)", + "rgb(0, 132, 0)", + "rgb(132, 0, 0)", + "rgb(194, 194, 0)", + "rgb(194, 0, 194)", + "rgb(194, 0, 0)", + "rgb(0, 132, 132)", + "rgb(0, 132, 0)", + "rgb(0, 0, 132)", + "rgb(132, 132, 132)", + "rgb(132, 0, 132)", + "rgb(194, 194, 194)", + "rgb(132, 0, 132)", + "rgb(132, 0, 0)", + ], + negative_objects: "rgb(132, 132, 132)", + worksheet: "rgb(0, 0, 132)", + }, + meta: { + filename: "kicad_2020", + name: "KiCad 2020", + version: 2, + }, + palette: [ + "rgb(132, 0, 0)", + "rgb(194, 194, 0)", + "rgb(194, 0, 194)", + "rgb(194, 0, 0)", + "rgb(0, 132, 132)", + "rgb(0, 132, 0)", + "rgb(0, 0, 132)", + "rgb(132, 132, 132)", + "rgb(132, 0, 132)", + "rgb(194, 194, 194)", + "rgb(132, 0, 132)", + "rgb(132, 0, 0)", + "rgb(132, 132, 0)", + "rgb(194, 194, 194)", + "rgb(0, 0, 132)", + "rgb(0, 132, 0)", + ], + schematic: { + aux_items: "rgb(46, 46, 46)", + background: "rgb(245, 241, 237)", + brightened: "rgb(255, 0, 255)", + bus: "rgb(0, 0, 132)", + bus_junction: "rgb(0, 0, 132)", + component_body: "rgb(255, 255, 194)", + component_outline: "rgb(132, 0, 0)", + cursor: "rgb(15, 15, 15)", + erc_error: "rgba(230, 9, 13, 0.800)", + erc_warning: "rgba(209, 146, 0, 0.800)", + fields: "rgb(132, 0, 132)", + grid: "rgb(181, 181, 181)", + grid_axes: "rgb(0, 0, 132)", + hidden: "rgb(194, 194, 194)", + junction: "rgb(0, 150, 0)", + label_global: "rgb(132, 0, 0)", + label_hier: "rgb(114, 86, 0)", + label_local: "rgb(15, 15, 15)", + net_name: "rgb(132, 132, 132)", + no_connect: "rgb(0, 0, 132)", + note: "rgb(0, 0, 194)", + override_item_colors: false, + pin: "rgb(132, 0, 0)", + pin_name: "rgb(0, 100, 100)", + pin_number: "rgb(169, 0, 0)", + reference: "rgb(0, 100, 100)", + shadow: "rgba(102, 179, 255, 0.800)", + sheet: "rgb(132, 0, 0)", + sheet_background: "rgba(253, 255, 231, 0.000)", + sheet_fields: "rgb(132, 0, 132)", + sheet_filename: "rgb(114, 86, 0)", + sheet_label: "rgb(0, 100, 100)", + sheet_name: "rgb(0, 100, 100)", + value: "rgb(0, 100, 100)", + wire: "rgb(0, 150, 0)", + worksheet: "rgb(132, 0, 0)", + }, +} diff --git a/package.json b/package.json index 173b1a4..df4b872 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dist" ], "scripts": { + "start": "storybook dev -p 6006", "prepublish": "npm run build", "build": "tsup-node ./lib/index.ts --format esm --dts --sourcemap", "format": "biome format . --write", @@ -26,7 +27,7 @@ "@storybook/react": "^8.2.5", "@storybook/react-vite": "^8.2.5", "@storybook/test": "^8.2.5", - "@tscircuit/core": "^0.0.71", + "@tscircuit/core": "^0.0.98", "@tscircuit/plop": "^0.0.10", "@types/bun": "^1.1.9", "bun-match-svg": "^0.0.6", @@ -44,10 +45,10 @@ "dependencies": { "@tscircuit/footprinter": "^0.0.57", "@tscircuit/routing": "^1.3.5", - "circuit-json": "*", "@tscircuit/soup-util": "^0.0.28", "@types/node": "^22.5.5", - "schematic-symbols": "^0.0.17", + "circuit-json": "*", + "schematic-symbols": "^0.0.32", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" } diff --git a/stories/schematic-resistor-capacitor.stories.tsx b/stories/schematic-resistor-capacitor.stories.tsx new file mode 100644 index 0000000..a71bd26 --- /dev/null +++ b/stories/schematic-resistor-capacitor.stories.tsx @@ -0,0 +1,367 @@ +import { convertCircuitJsonToSchematicSvg } from "../lib/index.js"; + +const soup: any = [ + { + type: "source_port", + source_port_id: "source_port_0", + name: "pin1", + pin_number: 1, + port_hints: ["anode", "pos", "pin1", "1"], + source_component_id: "source_component_0", + }, + { + type: "source_port", + source_port_id: "source_port_1", + name: "pin2", + pin_number: 2, + port_hints: ["cathode", "neg", "pin2", "2"], + source_component_id: "source_component_0", + }, + { + type: "source_component", + source_component_id: "source_component_0", + ftype: "simple_resistor", + name: "R1", + resistance: 100000, + }, + { + type: "source_port", + source_port_id: "source_port_2", + name: "pin1", + pin_number: 1, + port_hints: ["anode", "pos", "pin1", "1"], + source_component_id: "source_component_1", + }, + { + type: "source_port", + source_port_id: "source_port_3", + name: "pin2", + pin_number: 2, + port_hints: ["cathode", "neg", "pin2", "2"], + source_component_id: "source_component_1", + }, + { + type: "source_component", + source_component_id: "source_component_1", + ftype: "simple_capacitor", + name: "C1", + capacitance: 0.0001, + }, + { + type: "source_component", + source_component_id: "source_component_2", + ftype: "simple_chip", + name: "U2", + manufacturer_part_number: "ATmega8-16A", + }, + { + type: "source_trace", + source_trace_id: "source_trace_0", + connected_source_port_ids: ["source_port_1", "source_port_2"], + connected_source_net_ids: [], + }, + { + type: "schematic_component", + schematic_component_id: "schematic_component_0", + center: { + x: -2, + y: 0, + }, + rotation: 0, + size: { + width: 1.0583332999999997, + height: 1, + }, + source_component_id: "source_component_0", + symbol_name: "boxresistor_horz", + }, + { + type: "schematic_component", + schematic_component_id: "schematic_component_1", + center: { + x: 2, + y: 0, + }, + rotation: 0, + size: { + width: 1.0583333000000001, + height: 0.5291665999999999, + }, + source_component_id: "source_component_1", + symbol_name: "capacitor_horz", + }, + { + type: "schematic_component", + schematic_component_id: "schematic_component_2", + center: { + x: 7, + y: 0, + }, + rotation: 0, + size: { + width: 3, + height: 7, + }, + port_arrangement: { + left_side: { + pins: [29, 7, 8, 20, 19, 22], + direction: "top-to-bottom", + }, + right_side: { + pins: [12, 13, 14, 15, 16, 17, 23], + direction: "bottom-to-top", + }, + top_side: { + pins: [4, 18, 1, 2], + direction: "left-to-right", + }, + }, + pin_spacing: 0.2, + pin_styles: { + pin29: { + bottom_margin: 0.5, + }, + }, + port_labels: { + pin7: "GND", + pin8: "-V+", + }, + source_component_id: "source_component_2", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_0", + schematic_component_id: "schematic_component_0", + center: { + x: -2.551106550000001, + y: 0.0003562000000023602, + }, + source_port_id: "source_port_0", + facing_direction: "left", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_1", + schematic_component_id: "schematic_component_0", + center: { + x: -1.4835251500000002, + y: 0.0009027000000010332, + }, + source_port_id: "source_port_1", + facing_direction: "right", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_2", + schematic_component_id: "schematic_component_1", + center: { + x: 1.4550195499999996, + y: 0.0009026999999992569, + }, + source_port_id: "source_port_2", + facing_direction: "left", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_3", + schematic_component_id: "schematic_component_1", + center: { + x: 2.55743815, + y: 0.00035620000000058383, + }, + source_port_id: "source_port_3", + facing_direction: "right", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_0", + source_trace_id: "source_trace_0", + edges: [ + { + from: { + x: -1.33342515, + y: 0.0009027000000010332, + layer: "top", + }, + to: { + x: 1.3049195499999997, + y: 0.0009027000000010332, + layer: "top", + }, + }, + ], + }, + { + type: "pcb_component", + pcb_component_id: "pcb_component_0", + center: { + x: -2, + y: 0, + }, + width: 1.5999999999999999, + height: 0.6000000000000001, + layer: "top", + rotation: 0, + source_component_id: "source_component_0", + }, + { + type: "pcb_component", + pcb_component_id: "pcb_component_1", + center: { + x: 2, + y: 0, + }, + width: 1.5999999999999999, + height: 0.6000000000000001, + layer: "top", + rotation: 0, + source_component_id: "source_component_1", + }, + { + type: "pcb_component", + pcb_component_id: "pcb_component_2", + center: { + x: 0, + y: 0, + }, + width: 0, + height: 0, + layer: "top", + rotation: 0, + source_component_id: "source_component_2", + }, + { + type: "pcb_board", + pcb_board_id: "pcb_board_0", + center: { + x: 0, + y: 0, + }, + thickness: 1.4, + num_layers: 4, + width: 10, + height: 10, + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_0", + pcb_component_id: "pcb_component_0", + pcb_port_id: "pcb_port_0", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["1", "left"], + x: -2.5, + y: 0, + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_1", + pcb_component_id: "pcb_component_0", + pcb_port_id: "pcb_port_1", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["2", "right"], + x: -1.5, + y: 0, + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_2", + pcb_component_id: "pcb_component_1", + pcb_port_id: "pcb_port_2", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["1", "left"], + x: 1.5, + y: 0, + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_3", + pcb_component_id: "pcb_component_1", + pcb_port_id: "pcb_port_3", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["2", "right"], + x: 2.5, + y: 0, + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_0", + pcb_component_id: "pcb_component_0", + layers: ["top"], + x: -2.5, + y: 0, + source_port_id: "source_port_0", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_1", + pcb_component_id: "pcb_component_0", + layers: ["top"], + x: -1.5, + y: 0, + source_port_id: "source_port_1", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_2", + pcb_component_id: "pcb_component_1", + layers: ["top"], + x: 1.5, + y: 0, + source_port_id: "source_port_2", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_3", + pcb_component_id: "pcb_component_1", + layers: ["top"], + x: 2.5, + y: 0, + source_port_id: "source_port_3", + }, + { + type: "pcb_trace", + pcb_trace_id: "pcb_trace_0", + route: [ + { + route_type: "wire", + x: -1.5, + y: 0, + width: 0.16, + layer: "top", + start_pcb_port_id: "pcb_port_1", + }, + { + route_type: "wire", + x: 1.5, + y: 0, + width: 0.16, + layer: "top", + end_pcb_port_id: "pcb_port_2", + }, + ], + source_trace_id: "source_trace_0", + }, +]; + +export const ResistorCapacitorSch = () => { + const result = convertCircuitJsonToSchematicSvg(soup); + + return
; +}; + +export default { + title: "Resistor and Capacitor Schematic", + component: ResistorCapacitorSch, +}; diff --git a/tests/fixtures/get-test-fixture.ts b/tests/fixtures/get-test-fixture.ts new file mode 100644 index 0000000..3c0a32f --- /dev/null +++ b/tests/fixtures/get-test-fixture.ts @@ -0,0 +1,10 @@ +import { Circuit } from "@tscircuit/core" + +export const getTestFixture = () => { + const project = new Circuit() + + return { + project, + circuit: project, + } +} diff --git a/tests/sch/__snapshots__/kicad-theme-demo.snap.svg b/tests/sch/__snapshots__/kicad-theme-demo.snap.svg new file mode 100644 index 0000000..96f7a52 --- /dev/null +++ b/tests/sch/__snapshots__/kicad-theme-demo.snap.svg @@ -0,0 +1,10 @@ +10R10.1C1ATmega8-16AU2297820192212131415161723418 \ No newline at end of file diff --git a/tests/sch/kicad-theme-demo.test.tsx b/tests/sch/kicad-theme-demo.test.tsx new file mode 100644 index 0000000..e09aacd --- /dev/null +++ b/tests/sch/kicad-theme-demo.test.tsx @@ -0,0 +1,54 @@ +import { expect, it } from "bun:test"; +import { convertCircuitJsonToSchematicSvg } from "lib/index"; +import { getTestFixture } from "tests/fixtures/get-test-fixture"; + +it("example 4: kicad theme demo", async () => { + const { project } = getTestFixture(); + + project.add( + + + + + + + + ); + + expect( + convertCircuitJsonToSchematicSvg(project.getCircuitJson(), { + width: 30, + height: 8, + }) + ).toMatchSvgSnapshot(import.meta.path); +}); From bd9ce1f36c43de01117fe96ffdaee84e16869ced Mon Sep 17 00:00:00 2001 From: Rishabh Gupta Date: Wed, 2 Oct 2024 20:59:41 +0530 Subject: [PATCH 2/2] fix test and type check --- .../convert-circuit-json-to-schematic-svg.ts | 305 +----------------- .../create-svg-objects-from-sch-component.ts | 305 ++++++++++++++++++ tests/sch/__snapshots__/resistor.snap.svg | 17 +- 3 files changed, 326 insertions(+), 301 deletions(-) create mode 100644 lib/sch/svg-object-fns/create-svg-objects-from-sch-component.ts diff --git a/lib/sch/convert-circuit-json-to-schematic-svg.ts b/lib/sch/convert-circuit-json-to-schematic-svg.ts index 79dc20d..ce1eccb 100644 --- a/lib/sch/convert-circuit-json-to-schematic-svg.ts +++ b/lib/sch/convert-circuit-json-to-schematic-svg.ts @@ -1,7 +1,7 @@ import type { AnyCircuitElement } from "circuit-json" import { colorMap } from "lib/utils/colors" -import { getSvg, symbols } from "schematic-symbols" -import { parseSync, stringify } from "svgson" +import { stringify } from "svgson" +import { createSchematicComponent } from "./svg-object-fns/create-svg-objects-from-sch-component" interface Options { width?: number @@ -102,298 +102,15 @@ export function convertCircuitJsonToSchematicSvg( ], } - return stringify({ value: "", ...svgObject }) - - function createSchematicComponent( - center: { x: number; y: number }, - size: { width: number; height: number }, - rotation: number, - symbolName?: string, - portArrangement?: any, - portLabels?: any, - sourceComponentId?: string, - circuitJson?: AnyCircuitElement[], - ): any { - const transform = `translate(${center.x}, ${center.y}) rotate(${(rotation * 180) / Math.PI})` - - let children: any[] = [] - - // Find the source component and get its name - const sourceComponent = circuitJson?.find( - (item) => - item.type === "source_component" && - item.source_component_id === sourceComponentId, - ) - const componentName = sourceComponent ? sourceComponent.name : "" - const manufacturerNumber = sourceComponent?.manufacturer_part_number - const resistance = sourceComponent?.resistance - const capacitance = sourceComponent?.capacitance - - if (symbolName) { - const symbol = (symbols as any)[symbolName] - const paths = symbol.primitives.filter((p: any) => p.type === "path") - const updatedSymbol = { - ...symbol, - primitives: paths, - } - const svg = parseSync( - getSvg(updatedSymbol, { - width: size.width, - height: size.height, - }), - ) - - children = svg.children - .filter( - (child: any) => - child.name === "path" && child.attributes.fill !== "green", - ) - .map((path: any) => { - const currentStrokeWidth = Number.parseFloat( - path.attributes["stroke-width"] || "0.02", - ) - const newStrokeWidth = (currentStrokeWidth * 1.5).toString() - - return { - ...path, - attributes: { - ...path.attributes, - stroke: - path.attributes.stroke === "black" - ? `${colorMap.schematic.component_outline}` - : path.attributes.stroke, - "stroke-width": newStrokeWidth, - }, - } - }) - } else { - children.push({ - name: "rect", - type: "element", - attributes: { - class: "component chip", - x: -size.width / 2, - y: -size.height / 2, - width: size.width, - height: size.height, - }, - }) - } - - if (manufacturerNumber) { - children.push({ - name: "text", - type: "element", - attributes: { - class: "component-name", - x: 1.2, - y: -size.height / 2 - 0.4, // Position above the component - "text-anchor": "right", - "dominant-baseline": "auto", - }, - children: [{ type: "text", value: manufacturerNumber }], - }) - - // Add component name on top - children.push({ - name: "text", - type: "element", - attributes: { - class: "component-name", - x: 1.2, - y: -size.height / 2 - 0.7, // Position above the component - "text-anchor": "right", - "dominant-baseline": "auto", - }, - children: [{ type: "text", value: componentName }], - }) - } - - if (resistance || capacitance) { - children.push({ - name: "text", - type: "element", - attributes: { - class: "component-name", - x: 0, - y: (-size.height / 2) - 0.2, // Position above the component - "text-anchor": "middle", - "dominant-baseline": "auto", - }, - children: [{ type: "text", value: resistance || capacitance }], - }) - - // Add component name on top - children.push({ - name: "text", - type: "element", - attributes: { - class: "component-name", - x: 0, - y: (-size.height / 2) - 0.5, // Position above the component - "text-anchor": "middle", - "dominant-baseline": "auto", - }, - children: [{ type: "text", value: componentName }], - }) - } - - // Add ports if portArrangement is provided - if (portArrangement) { - const portLength = 0.2 // Length of the port line - const circleRadius = 0.05 // Radius of the port circle - const labelOffset = 0.1 // Offset for label positioning - - // console.log(portArrangement) - - for (const [side, arrangement] of Object.entries(portArrangement)) { - if(arrangement === undefined) continue - - const pins = arrangement.pins - const direction = arrangement.direction - - let getX: (index: number, total: number) => number - let getY: (index: number, total: number) => number - let getEndX: (x: number) => number - let getEndY: (y: number) => number - let getLabelX: (x: number) => number - let getLabelY: (y: number) => number - let labelAnchor: string - let isVertical = false - - switch (side) { - case "left_side": - getX = () => -size.width / 2 - getY = (index, total) => - -size.height / 2 + (size.height * (index + 1)) / (total + 1) - getEndX = (x) => x - portLength - getEndY = (y) => y - getLabelX = (x) => x + labelOffset - getLabelY = (y) => y - labelAnchor = "start" - break - case "right_side": - getX = () => size.width / 2 - getY = (index, total) => - -size.height / 2 + (size.height * (index + 1)) / (total + 1) - getEndX = (x) => x + portLength - getEndY = (y) => y - getLabelX = (x) => x - labelOffset - getLabelY = (y) => y - labelAnchor = "end" - break - case "top_side": - getX = (index, total) => - -size.width / 2 + (size.width * (index + 1)) / (total + 1) - getY = () => -size.height / 2 - getEndX = (x) => x - getEndY = (y) => y - portLength - getLabelX = (x) => x - getLabelY = (y) => y + labelOffset + 0.15 - labelAnchor = "middle" - isVertical = true - break - case "bottom_side": - getX = (index, total) => - -size.width / 2 + (size.width * (index + 1)) / (total + 1) - getY = () => size.height / 2 - getEndX = (x) => x - getEndY = (y) => y + portLength - getLabelX = (x) => x - getLabelY = (y) => y - labelOffset - 0.15 - labelAnchor = "middle" - isVertical = true - break - default: - continue // Skip unknown sides - } - - const totalPins = pins.length - - pins.forEach((pin: number, index: number) => { - let x = getX(index, totalPins) - let y = getY(index, totalPins) - - if (direction === "bottom-to-top" || direction === "right-to-left") { - ;[x, y] = [ - getX(totalPins - 1 - index, totalPins), - getY(totalPins - 1 - index, totalPins), - ] - } - - const endX = getEndX(x) - const endY = getEndY(y) - - children.push({ - name: "line", - type: "element", - attributes: { - class: "component-pin", - x1: x, - y1: y, - x2: endX, - y2: endY, - }, - }) - - children.push({ - name: "circle", - type: "element", - attributes: { - class: "component-pin", - cx: endX, - cy: endY, - r: circleRadius, - }, - }) - - // Add label if it exists in portLabels - const labelKey = `pin${pin}` - if (portLabels && labelKey in portLabels) { - let labelTransform = "" - if (isVertical) { - labelTransform = `rotate(${side === "top_side" ? -90 : 270}, ${getLabelX(x)}, ${getLabelY(y)})` - } - children.push({ - name: "text", - type: "element", - attributes: { - class: "port-label", - x: getLabelX(x), - y: getLabelY(y), - "text-anchor": labelAnchor, - "dominant-baseline": "middle", - "font-size": "0.2", - transform: labelTransform, - }, - children: [{ type: "text", value: portLabels[labelKey] }], - }) - } - // Add pin number - children.push({ - name: "text", - type: "element", - attributes: { - class: "pin-number", - x: endX, - y: endY + (side === "bottom_side" ? 0.15 : -0.15), - "text-anchor": "middle", - "dominant-baseline": "middle", - "font-size": "0.15", - }, - children: [{ type: "text", value: pin.toString() }], - }) - }) - } - } - - return { - name: "g", - type: "element", - attributes: { transform }, - children, - } - } + return stringify({ + value: "", + ...svgObject, + attributes: { + ...svgObject.attributes, + width: svgObject.attributes.width.toString(), + height: svgObject.attributes.height.toString(), + }, + }) function createSchematicTrace( trace: any, diff --git a/lib/sch/svg-object-fns/create-svg-objects-from-sch-component.ts b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component.ts new file mode 100644 index 0000000..785b051 --- /dev/null +++ b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component.ts @@ -0,0 +1,305 @@ +import type { AnyCircuitElement } from "circuit-json"; +import { colorMap } from "lib/utils/colors"; +import { getSvg, symbols } from "schematic-symbols"; +import { parseSync } from "svgson"; + +export function createSchematicComponent( + center: { x: number; y: number }, + size: { width: number; height: number }, + rotation: number, + symbolName?: string, + portArrangement?: any, + portLabels?: any, + sourceComponentId?: string, + circuitJson?: AnyCircuitElement[], + ): any { + const transform = `translate(${center.x}, ${center.y}) rotate(${(rotation * 180) / Math.PI})` + + let children: any[] = [] + + // Find the source component and get its name + const sourceComponent = circuitJson?.find( + (item) => + item.type === "source_component" && + item.source_component_id === sourceComponentId, + ) + const componentName = + sourceComponent && "name" in sourceComponent ? sourceComponent.name : "" + const manufacturerNumber = + sourceComponent && "manufacturer_part_number" in sourceComponent + ? sourceComponent.manufacturer_part_number + : "" + const resistance = + sourceComponent && "resistance" in sourceComponent + ? sourceComponent.resistance + : "" + const capacitance = + sourceComponent && "capacitance" in sourceComponent + ? sourceComponent.capacitance + : "" + + if (symbolName) { + const symbol = (symbols as any)[symbolName] + const paths = symbol.primitives.filter((p: any) => p.type === "path") + const updatedSymbol = { + ...symbol, + primitives: paths, + } + const svg = parseSync( + getSvg(updatedSymbol, { + width: size.width, + height: size.height, + }), + ) + + children = svg.children + .filter( + (child: any) => + child.name === "path" && child.attributes.fill !== "green", + ) + .map((path: any) => { + const currentStrokeWidth = Number.parseFloat( + path.attributes["stroke-width"] || "0.02", + ) + const newStrokeWidth = (currentStrokeWidth * 1.5).toString() + + return { + ...path, + attributes: { + ...path.attributes, + stroke: + path.attributes.stroke === "black" + ? `${colorMap.schematic.component_outline}` + : path.attributes.stroke, + "stroke-width": newStrokeWidth, + }, + } + }) + } else { + children.push({ + name: "rect", + type: "element", + attributes: { + class: "component chip", + x: -size.width / 2, + y: -size.height / 2, + width: size.width, + height: size.height, + }, + }) + } + + if (manufacturerNumber) { + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 1.2, + y: -size.height / 2 - 0.4, // Position above the component + "text-anchor": "right", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: manufacturerNumber }], + }) + + // Add component name on top + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 1.2, + y: -size.height / 2 - 0.7, // Position above the component + "text-anchor": "right", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: componentName }], + }) + } + + if (resistance || capacitance) { + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 0, + y: -size.height / 2 - 0.2, // Position above the component + "text-anchor": "middle", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: resistance || capacitance }], + }) + + // Add component name on top + children.push({ + name: "text", + type: "element", + attributes: { + class: "component-name", + x: 0, + y: -size.height / 2 - 0.5, // Position above the component + "text-anchor": "middle", + "dominant-baseline": "auto", + }, + children: [{ type: "text", value: componentName }], + }) + } + + // Add ports if portArrangement is provided + if (portArrangement) { + const portLength = 0.2 // Length of the port line + const circleRadius = 0.05 // Radius of the port circle + const labelOffset = 0.1 // Offset for label positioning + + // console.log(portArrangement) + + for (const [side, arrangement] of Object.entries(portArrangement)) { + if (!arrangement) continue + + const pins = (arrangement as any).pins + const direction = (arrangement as any).direction + + let getX: (index: number, total: number) => number + let getY: (index: number, total: number) => number + let getEndX: (x: number) => number + let getEndY: (y: number) => number + let getLabelX: (x: number) => number + let getLabelY: (y: number) => number + let labelAnchor: string + let isVertical = false + + switch (side) { + case "left_side": + getX = () => -size.width / 2 + getY = (index, total) => + -size.height / 2 + (size.height * (index + 1)) / (total + 1) + getEndX = (x) => x - portLength + getEndY = (y) => y + getLabelX = (x) => x + labelOffset + getLabelY = (y) => y + labelAnchor = "start" + break + case "right_side": + getX = () => size.width / 2 + getY = (index, total) => + -size.height / 2 + (size.height * (index + 1)) / (total + 1) + getEndX = (x) => x + portLength + getEndY = (y) => y + getLabelX = (x) => x - labelOffset + getLabelY = (y) => y + labelAnchor = "end" + break + case "top_side": + getX = (index, total) => + -size.width / 2 + (size.width * (index + 1)) / (total + 1) + getY = () => -size.height / 2 + getEndX = (x) => x + getEndY = (y) => y - portLength + getLabelX = (x) => x + getLabelY = (y) => y + labelOffset + 0.15 + labelAnchor = "middle" + isVertical = true + break + case "bottom_side": + getX = (index, total) => + -size.width / 2 + (size.width * (index + 1)) / (total + 1) + getY = () => size.height / 2 + getEndX = (x) => x + getEndY = (y) => y + portLength + getLabelX = (x) => x + getLabelY = (y) => y - labelOffset - 0.15 + labelAnchor = "middle" + isVertical = true + break + default: + continue // Skip unknown sides + } + + const totalPins = pins.length + + pins.forEach((pin: number, index: number) => { + let x = getX(index, totalPins) + let y = getY(index, totalPins) + + if (direction === "bottom-to-top" || direction === "right-to-left") { + ;[x, y] = [ + getX(totalPins - 1 - index, totalPins), + getY(totalPins - 1 - index, totalPins), + ] + } + + const endX = getEndX(x) + const endY = getEndY(y) + + children.push({ + name: "line", + type: "element", + attributes: { + class: "component-pin", + x1: x, + y1: y, + x2: endX, + y2: endY, + }, + }) + + children.push({ + name: "circle", + type: "element", + attributes: { + class: "component-pin", + cx: endX, + cy: endY, + r: circleRadius, + }, + }) + + // Add label if it exists in portLabels + const labelKey = `pin${pin}` + if (portLabels && labelKey in portLabels) { + let labelTransform = "" + if (isVertical) { + labelTransform = `rotate(${side === "top_side" ? -90 : 270}, ${getLabelX(x)}, ${getLabelY(y)})` + } + children.push({ + name: "text", + type: "element", + attributes: { + class: "port-label", + x: getLabelX(x), + y: getLabelY(y), + "text-anchor": labelAnchor, + "dominant-baseline": "middle", + "font-size": "0.2", + transform: labelTransform, + }, + children: [{ type: "text", value: portLabels[labelKey] }], + }) + } + // Add pin number + children.push({ + name: "text", + type: "element", + attributes: { + class: "pin-number", + x: endX, + y: endY + (side === "bottom_side" ? 0.15 : -0.15), + "text-anchor": "middle", + "dominant-baseline": "middle", + "font-size": "0.15", + }, + children: [{ type: "text", value: pin.toString() }], + }) + }) + } + } + + return { + name: "g", + type: "element", + attributes: { transform }, + children, + } + } \ No newline at end of file diff --git a/tests/sch/__snapshots__/resistor.snap.svg b/tests/sch/__snapshots__/resistor.snap.svg index c5bb4d6..03717c5 100644 --- a/tests/sch/__snapshots__/resistor.snap.svg +++ b/tests/sch/__snapshots__/resistor.snap.svg @@ -1,7 +1,10 @@ - \ No newline at end of file +10000R1 \ No newline at end of file