diff --git a/bun.lock b/bun.lock index f66b17de..428dc744 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@tscircuit/pcb-viewer", @@ -8,9 +9,9 @@ "@tscircuit/alphabet": "^0.0.20", "@tscircuit/math-utils": "^0.0.29", "@vitejs/plugin-react": "^5.0.2", - "circuit-json": "^0.0.356", - "circuit-to-canvas": "^0.0.62", - "circuit-to-svg": "^0.0.271", + "circuit-json": "^0.0.374", + "circuit-to-canvas": "^0.0.66", + "circuit-to-svg": "^0.0.323", "color": "^4.2.3", "react-supergrid": "^1.0.10", "react-toastify": "^10.0.5", @@ -401,6 +402,8 @@ "@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="], + "@thednp/dommatrix": ["@thednp/dommatrix@2.0.12", "", {}, "sha512-eOshhlSShBXLfrMQqqhA450TppJXhKriaQdN43mmniOCMn9sD60QKF1Axsj7bKl339WH058LuGFS6H84njYH5w=="], + "@tscircuit/alphabet": ["@tscircuit/alphabet@0.0.20", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-Z7xNzU5MVRtzq0sRkIX89Cdz7LQi5rrfWGTOdyytfsxnnvTyanbkr0Tan6eUFY3ujvWcvA78Ox5dDOrwpJJGxw=="], "@tscircuit/capacity-autorouter": ["@tscircuit/capacity-autorouter@0.0.264", "", { "dependencies": { "bun-match-svg": "^0.0.14", "fast-json-stable-stringify": "^2.1.0", "object-hash": "^3.0.0" } }, "sha512-XUsiE0hkvwxYCXlytPstFidhaBRy+6whREot5SBpoo4OMwJkbKu3q1kfxuGnH6XJl633I/7rk7mnAIpMWgfjaQ=="], @@ -623,7 +626,7 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "circuit-json": ["circuit-json@0.0.356", "", {}, "sha512-Jutbw2KmHx9rlAe4mCjPkVng1I07yu7JC7nMTqI8tyTGuovtdVKz8VyWw3W/HenwRQNywp5fDBObo/+WZkIGjA=="], + "circuit-json": ["circuit-json@0.0.374", "", {}, "sha512-l3TbGx3qHRmtey24oCs/gyONi3+lrf23VQJQnHQXFCfz0J37LlM7ouNUO+3FEUeEGsxgnCCTmtPO9DrbVELYOw=="], "circuit-json-to-bpc": ["circuit-json-to-bpc@0.0.13", "", { "peerDependencies": { "bpc-graph": "*", "circuit-json": "*", "typescript": "^5" } }, "sha512-3wSMtPa6tJkiBQN4tsm7f0Mb7Wp90X2c8dNbULoDVE4mGGoFqP1DXqBlyvvZZl+4SjqznzQQ0EioLe2SCQTOcg=="], @@ -635,9 +638,9 @@ "circuit-json-to-spice": ["circuit-json-to-spice@0.0.33", "", { "dependencies": { "circuit-json-to-connectivity-map": "^0.0.22" }, "peerDependencies": { "@tscircuit/circuit-json-util": "*", "circuit-json": "*", "typescript": "^5.0.0" } }, "sha512-K5Z2Su53ySQ46Fo2oZvOgGNU2+PKsK/d558QJoWoQl0tZ2GspXFONeCZ2cj0zMSIj6pYscQIMwSoZ+IKrtKygg=="], - "circuit-to-canvas": ["circuit-to-canvas@0.0.62", "", { "dependencies": { "transformation-matrix": "^3.1.0" }, "peerDependencies": { "@tscircuit/alphabet": "*", "typescript": "^5" } }, "sha512-J+9z0YCdzEDH6+UWTGkCVKxSt7SQVxbWYVk6rKIvpEURJrUKpPK1MlRMyAvJVSNd7xWFZE9ugScLJApeEN1HQQ=="], + "circuit-to-canvas": ["circuit-to-canvas@0.0.66", "", { "dependencies": { "transformation-matrix": "^3.1.0" }, "peerDependencies": { "@tscircuit/alphabet": "*", "typescript": "^5" } }, "sha512-t1tug7HfH00Tupwf+gL/hX0TLzaOgpNPCk64Z++ZjFVGYnE5fbZs/Qkpz05ULyfQbS3nwqZPzXNhnQVjs2Y5cA=="], - "circuit-to-svg": ["circuit-to-svg@0.0.271", "", { "dependencies": { "@types/node": "^22.5.5", "bun-types": "^1.1.40", "calculate-elbow": "0.0.12", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" } }, "sha512-6g57xJT5lGiiSr29NIZztSwWhXq50ZXgwYbU2NCDLooZNXIz3d+sDrwhHltax53czFrt6w8q1om2XGPZHzAq6g=="], + "circuit-to-svg": ["circuit-to-svg@0.0.323", "", { "dependencies": { "@types/node": "^22.5.5", "bun-types": "^1.1.40", "calculate-elbow": "0.0.12", "debug": "^4.4.3", "svg-path-commander": "^2.1.11", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "@tscircuit/alphabet": "*" } }, "sha512-KtJJnvp75aTBPZZHqyFRTKciPDqEhJvFBqEg1ZvBrZy+TqPv+anUnJqWP4hg+2pW7lK/fYs9v4M6qsUiiBke0Q=="], "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], @@ -1553,6 +1556,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svg-path-commander": ["svg-path-commander@2.1.11", "", { "dependencies": { "@thednp/dommatrix": "^2.0.12" } }, "sha512-wmQ6QA3Od+HOcpIzLjPlbv59+x3yd3V5W6xitUOvAHmqZpP7wVrRM2CHqEm5viHUbZu6PjzFsjbTEFtIeUxaNA=="], + "svgson": ["svgson@5.3.1", "", { "dependencies": { "deep-rename-keys": "^0.2.1", "xml-reader": "2.4.3" } }, "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA=="], "tar-fs": ["tar-fs@3.0.8", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg=="], @@ -1789,6 +1794,8 @@ "circuit-to-svg/@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], + "circuit-to-svg/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], diff --git a/package.json b/package.json index 572c8870..1548997b 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "@tscircuit/alphabet": "^0.0.20", "@tscircuit/math-utils": "^0.0.29", "@vitejs/plugin-react": "^5.0.2", - "circuit-json": "^0.0.356", - "circuit-to-canvas": "^0.0.62", - "circuit-to-svg": "^0.0.271", + "circuit-json": "^0.0.374", + "circuit-to-canvas": "^0.0.66", + "circuit-to-svg": "^0.0.323", "color": "^4.2.3", "react-supergrid": "^1.0.10", "react-toastify": "^10.0.5", diff --git a/src/components/CanvasPrimitiveRenderer.tsx b/src/components/CanvasPrimitiveRenderer.tsx index dea74f60..99c4a3b2 100644 --- a/src/components/CanvasPrimitiveRenderer.tsx +++ b/src/components/CanvasPrimitiveRenderer.tsx @@ -16,6 +16,7 @@ import { drawPrimitives } from "lib/draw-primitives" import { drawSoldermaskElementsForLayer } from "lib/draw-soldermask" import { drawSilkscreenElementsForLayer } from "lib/draw-silkscreen" import { drawPcbViaElementsForLayer } from "lib/draw-via" +import { drawCourtyardElementsForLayer } from "lib/draw-courtyard" import type { GridConfig, Primitive } from "lib/types" import React, { useEffect, useRef } from "react" import { SuperGrid, toMMSI } from "react-supergrid" @@ -55,6 +56,8 @@ const orderedLayers = [ "top_notes", "top_silkscreen", "bottom_silkscreen", + "top_courtyard", + "bottom_courtyard", "other", ] @@ -100,7 +103,10 @@ export const CanvasPrimitiveRenderer = ({ .filter((p) => p._element?.type !== "pcb_smtpad") .filter((p) => p._element?.type !== "pcb_plated_hole") .filter((p) => p._element?.type !== "pcb_via") + .filter((p) => p._element?.type !== "pcb_via") .filter((p) => p._element?.type !== "pcb_trace") + .filter((p) => p._element?.type !== "pcb_courtyard_circle") + .filter((p) => p._element?.type !== "pcb_courtyard_rect") drawPrimitives(drawer, filteredPrimitives) @@ -355,6 +361,29 @@ export const CanvasPrimitiveRenderer = ({ realToCanvasMat: transform, }) } + + // Draw top courtyard + const topCourtyardCanvas = canvasRefs.current.top_courtyard + if (topCourtyardCanvas) { + drawCourtyardElementsForLayer({ + canvas: topCourtyardCanvas, + elements, + layers: ["top_courtyard" as PcbRenderLayer], + realToCanvasMat: transform, + }) + } + + // Draw bottom courtyard + const bottomCourtyardCanvas = canvasRefs.current.bottom_courtyard + if (bottomCourtyardCanvas) { + drawCourtyardElementsForLayer({ + canvas: bottomCourtyardCanvas, + elements, + layers: ["bottom_courtyard" as PcbRenderLayer], + realToCanvasMat: transform, + }) + } + // Draw board outline using circuit-to-canvas const boardCanvas = canvasRefs.current.board if (boardCanvas) { diff --git a/src/examples/2025/simple/courtyard-circle.fixture.tsx b/src/examples/2025/simple/courtyard-circle.fixture.tsx new file mode 100644 index 00000000..1d5da2a0 --- /dev/null +++ b/src/examples/2025/simple/courtyard-circle.fixture.tsx @@ -0,0 +1,35 @@ +import type React from "react" +import { PCBViewer } from "../../../PCBViewer" +import { AnyCircuitElement } from "circuit-json" + +export const CourtyardCircleExample: React.FC = () => { + return ( +
+ +
+ ) +} + +export default CourtyardCircleExample diff --git a/src/lib/Drawer.ts b/src/lib/Drawer.ts index a5c01ce6..ce7f06f8 100644 --- a/src/lib/Drawer.ts +++ b/src/lib/Drawer.ts @@ -49,6 +49,9 @@ export const LAYER_NAME_TO_COLOR = { tkeepout: colors.board.b_crtyd, tplace: colors.board.b_silks, + top_courtyard: colors.board.f_crtyd, + bottom_courtyard: colors.board.b_crtyd, + top_silkscreen: colors.board.f_silks, bottom_silkscreen: colors.board.b_silks, @@ -82,7 +85,9 @@ export const DEFAULT_DRAW_ORDER = [ "soldermask_top", "soldermask_with_copper_bottom", "soldermask_with_copper_top", + "bottom_courtyard", "bottom_fabrication", + "top_courtyard", "top_fabrication", "edge_cuts", "top_silkscreen", @@ -370,12 +375,27 @@ export class Drawer { ctx.restore() } - circle(x: number, y: number, r: number, mesh_fill?: boolean) { + circle( + x: number, + y: number, + r: number, + mesh_fill?: boolean, + is_filled = true, + ) { const r$ = scaleOnly(this.transform, r) const [x$, y$] = applyToPoint(this.transform, [x, y]) this.applyAperture() const ctx = this.getLayerCtx() + // Ensure we have a stroke if not filled + if (!is_filled) { + const originalLineWidth = ctx.lineWidth + // Set a minimum visible stroke width if calculated width is 0 or too small + // or rely on what applyAperture set. + // If the aperture size was 0, we might want a hairline. + // But typically applyAperture sets lineWidth based on aperture.size. + } + if (mesh_fill) { ctx.save() ctx.beginPath() @@ -395,7 +415,11 @@ export class Drawer { } else { ctx.beginPath() ctx.arc(x$, y$, r$, 0, 2 * Math.PI) - ctx.fill() + if (is_filled !== false) { + ctx.fill() + } else { + ctx.stroke() + } } } diff --git a/src/lib/colors.ts b/src/lib/colors.ts index b0c438ed..4752dc22 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -16,7 +16,7 @@ export default { anchor: "rgb(255, 38, 226)", aux_items: "rgb(255, 255, 255)", b_adhes: "rgb(0, 0, 132)", - b_crtyd: "rgb(255, 38, 226)", + b_crtyd: "rgb(38, 233, 255)", b_fab: "rgb(88, 93, 132)", b_mask: "rgba(2, 255, 238, 0.400)", b_paste: "rgb(0, 194, 194)", diff --git a/src/lib/convert-element-to-primitive.ts b/src/lib/convert-element-to-primitive.ts index 75cd5d79..c55fee68 100644 --- a/src/lib/convert-element-to-primitive.ts +++ b/src/lib/convert-element-to-primitive.ts @@ -10,6 +10,7 @@ import { convertSmtpadPill, convertSmtpadRotatedPill, } from "./element-to-primitive-converters/convert-smtpad-pill" + import { convertPcbCopperTextToPrimitive } from "./element-to-primitive/convert-pcb-copper-text-to-primitive" type MetaData = { diff --git a/src/lib/draw-courtyard.ts b/src/lib/draw-courtyard.ts new file mode 100644 index 00000000..845bcea5 --- /dev/null +++ b/src/lib/draw-courtyard.ts @@ -0,0 +1,47 @@ +import type { AnyCircuitElement, PcbRenderLayer } from "circuit-json" +import { + CircuitToCanvasDrawer, + DEFAULT_PCB_COLOR_MAP, + type PcbColorMap, +} from "circuit-to-canvas" +import type { Matrix } from "transformation-matrix" +import colors from "./colors" + +const PCB_VIEWER_COLOR_MAP: PcbColorMap = { + ...DEFAULT_PCB_COLOR_MAP, + courtyard: { + top: colors.board.f_crtyd, + bottom: colors.board.b_crtyd, + }, +} + +export function isCourtyardElement(element: AnyCircuitElement) { + return ( + element.type === "pcb_courtyard_circle" || + element.type === "pcb_courtyard_rect" + ) +} + +export function drawCourtyardElementsForLayer({ + canvas, + elements, + layers, + realToCanvasMat, +}: { + canvas: HTMLCanvasElement + elements: AnyCircuitElement[] + layers: PcbRenderLayer[] + realToCanvasMat: Matrix +}) { + const drawer = new CircuitToCanvasDrawer(canvas) + + drawer.configure({ + colorOverrides: PCB_VIEWER_COLOR_MAP, + }) + + drawer.realToCanvasMat = realToCanvasMat + + const courtyardElements = elements.filter(isCourtyardElement) + + drawer.drawElements(courtyardElements, { layers }) +} diff --git a/src/lib/draw-primitives.ts b/src/lib/draw-primitives.ts index 18196504..eb3a0bdb 100644 --- a/src/lib/draw-primitives.ts +++ b/src/lib/draw-primitives.ts @@ -309,7 +309,13 @@ export const drawCircle = (drawer: Drawer, circle: Circle) => { color: getColor(circle), layer: circle.layer, }) - drawer.circle(circle.x, circle.y, circle.r, circle.mesh_fill) + drawer.circle( + circle.x, + circle.y, + circle.r, + circle.mesh_fill, + circle.is_filled, + ) } export const drawOval = (drawer: Drawer, oval: Oval) => { diff --git a/src/lib/types.ts b/src/lib/types.ts index 9c36c1b0..a1c738cf 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -102,6 +102,7 @@ export interface Circle extends PCBDrawingObject { y: number r: number mesh_fill?: boolean + is_filled?: boolean } export interface Oval extends PCBDrawingObject {