diff --git a/bun.lockb b/bun.lockb index cdf5614..9ca52c8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lib/sch/convert-circuit-json-to-schematic-svg.ts b/lib/sch/convert-circuit-json-to-schematic-svg.ts index 1550e8b..ce1eccb 100644 --- a/lib/sch/convert-circuit-json-to-schematic-svg.ts +++ b/lib/sch/convert-circuit-json-to-schematic-svg.ts @@ -1,9 +1,16 @@ import type { AnyCircuitElement } from "circuit-json" -import { getSvg, symbols } from "schematic-symbols" -import { parseSync, stringify } from "svgson" +import { colorMap } from "lib/utils/colors" +import { stringify } from "svgson" +import { createSchematicComponent } from "./svg-object-fns/create-svg-objects-from-sch-component" + +interface Options { + width?: number + height?: number +} export function convertCircuitJsonToSchematicSvg( soup: AnyCircuitElement[], + options?: Options, ): string { let minX = Number.POSITIVE_INFINITY let minY = Number.POSITIVE_INFINITY @@ -20,8 +27,6 @@ export function convertCircuitJsonToSchematicSvg( } else if (item.type === "schematic_port") { updateBounds(item.center, { width: portSize, height: portSize }, 0) portPositions.set(item.schematic_port_id, item.center) - } else if (item.type === "schematic_text") { - updateBounds(item.position, { width: 0, height: 0 }, 0) } } @@ -44,41 +49,21 @@ export function convertCircuitJsonToSchematicSvg( component.size, component.rotation || 0, (component as any).symbol_name, + (component as any).port_arrangement, + (component as any).port_labels, + (component as any).source_component_id, + soup, ) svgChildren.push(svg) componentMap.set(component.schematic_component_id, component) } - // Process ports and add lines to component edges - for (const port of soup.filter((item) => 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}; } `, }, ], @@ -114,7 +102,75 @@ export function convertCircuitJsonToSchematicSvg( ], } - return stringify({ value: "", ...svgObject }) + return stringify({ + value: "", + ...svgObject, + attributes: { + ...svgObject.attributes, + width: svgObject.attributes.width.toString(), + height: svgObject.attributes.height.toString(), + }, + }) + + 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 - 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}` + } + } + } + + return path + ? { + name: "path", + type: "element", + attributes: { + class: "trace", + d: path, + }, + } + : null + } function updateBounds(center: any, size: any, rotation: number) { const corners = [ @@ -137,255 +193,6 @@ export function convertCircuitJsonToSchematicSvg( } } -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, - }), - ) - - // Filter out non-path elements and modify path colors - const pathElements = 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" - ? "red" - : path.attributes.stroke, - "stroke-width": newStrokeWidth, - }, - } - }) - - // 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})` - - return { - name: "g", - type: "element", - attributes: { transform: adjustedTransform }, - children: pathElements, - } - } - - return { - name: "g", - type: "element", - attributes: { transform }, - children: [ - { - name: "rect", - type: "element", - attributes: { - class: "component", - x: (-size.width / 2).toString(), - y: (-size.height / 2).toString(), - width: size.width.toString(), - height: size.height.toString(), - }, - }, - ], - } -} - -function createSchematicPort(center: { x: number; y: number }): any { - const portSize = 0.2 - const x = center.x - portSize / 2 - const y = center.y - portSize / 2 - - return { - name: "rect", - type: "element", - attributes: { - class: "port", - x: x.toString(), - y: y.toString(), - width: portSize.toString(), - height: portSize.toString(), - }, - } -} - -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 - } - - return { - name: "line", - type: "element", - attributes: { - class: "component-pin", - x1: portCenter.x.toString(), - y1: portCenter.y.toString(), - x2: endX.toString(), - y2: endY.toString(), - }, - } -} - -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)}` - - 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}` - } - } - } - - return path - ? { - name: "path", - type: "element", - attributes: { - class: "trace", - d: path, - }, - } - : 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 : "", - }, - ], - } -} - -function getTextAnchor(anchor: string): string { - switch (anchor) { - case "left": - return "start" - case "right": - return "end" - default: - return "middle" - } -} - /** * @deprecated use `convertCircuitJsonToSchematicSvg` instead */ 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/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 80cac38..0723d57 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/__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 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); +});