diff --git a/bun.lock b/bun.lock index 54213ef..e42b84e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,11 +8,11 @@ "@radix-ui/react-dialog": "^1.1.15", "@tscircuit/plop": "^0.0.72", "circuit-json": "^0.0.403", - "circuit-json-to-lbrn": "^0.0.82", + "circuit-json-to-lbrn": "^0.0.85", "circuit-to-svg": "^0.0.344", "kicad-to-circuit-json": "^0.0.88", "kicadts": "^0.0.45", - "lbrnts": "^0.0.19", + "lbrnts": "^0.0.22", "lucide-react": "^0.562.0", "react": "^19.1.0", "react-cosmos": "^7.1.0", @@ -595,7 +595,7 @@ "circuit-json-to-gltf": ["circuit-json-to-gltf@0.0.96", "", { "dependencies": { "@jscad/modeling": "^2.12.6", "earcut": "^3.0.2", "jscad-electronics": "^0.0.129", "jscad-to-gltf": "^0.0.5", "occt-import-js": "^0.0.23" }, "peerDependencies": { "@resvg/resvg-js": "2", "@resvg/resvg-wasm": "2", "@tscircuit/circuit-json-util": "*", "circuit-json": "*", "circuit-to-svg": "*", "typescript": "^5" }, "optionalPeers": ["@resvg/resvg-js", "@resvg/resvg-wasm"] }, "sha512-7V1cj+WhyPBRsVbgghSCnbJNyWkx/KAhvnZU1Pdp7lZH+iHFIqRyIL/vNRRo657JDGM4bTlVFMUugo+RrseoKw=="], - "circuit-json-to-lbrn": ["circuit-json-to-lbrn@0.0.82", "", { "dependencies": { "lbrnts": "^0.0.19", "manifold-3d": "^3.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-DEF0S7+j983BKDA6dqLYqBi7+lkiKax5PDTzBxf5Jg+uQrWxW7ddSDuDyPUTVkM2OE25DjNt02MBpsKV56upow=="], + "circuit-json-to-lbrn": ["circuit-json-to-lbrn@0.0.85", "", { "dependencies": { "lbrnts": "^0.0.22", "manifold-3d": "^3.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-Ws0891TlqhKZ2RdVjh7lrss8e+GHA+sVXmtaNOr2n7iXe3ga7nYOWm2LWq/edWLonjYL6IkoVXk8MWjPMPQrgQ=="], "circuit-json-to-simple-3d": ["circuit-json-to-simple-3d@0.0.9", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-thpCtDb9LpAQfGO+Z0hae19sxVAmJAMYM/It9UTMCtyXC3zoUHwRcp/oqtMO/ZIW+JDBhiR8AHmmtKScL2kW7Q=="], @@ -933,7 +933,7 @@ "ktx-parse": ["ktx-parse@1.1.0", "", {}, "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="], - "lbrnts": ["lbrnts@0.0.19", "", { "dependencies": { "fast-xml-parser": "^5.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-FaQJKMZaQNSbAbsFBkqEZvTA55sn4WOfrbJPgrQa8lR09nPtHLofXuA8ydOrTZggP/drgpamDao+q4cuGcL7OQ=="], + "lbrnts": ["lbrnts@0.0.22", "", { "dependencies": { "fast-xml-parser": "^5.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-XeUvQ91qOtD+QXKIQHUeFf+a/EiDAPt5hdmmIn+w0MqOyHnvYyJQQeoyYpuqiNvGlovOROA+8eBftF8OsW/RIA=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], diff --git a/lib/components/workspace-context.tsx b/lib/components/workspace-context.tsx index 8e4666d..05a812d 100644 --- a/lib/components/workspace-context.tsx +++ b/lib/components/workspace-context.tsx @@ -1,5 +1,8 @@ import type { CircuitJson } from "circuit-json" -import type { ConvertCircuitJsonToLbrnOptions } from "circuit-json-to-lbrn" +import { + convertCircuitJsonToLbrn, + type ConvertCircuitJsonToLbrnOptions, +} from "circuit-json-to-lbrn" import { KicadToCircuitJsonConverter } from "kicad-to-circuit-json" import React, { createContext, @@ -144,6 +147,12 @@ const defaultLbrnOptions: ConvertCircuitJsonToLbrnOptions = { includeSoldermaskCure: true, mirrorBottomLayer: true, includeLayers: ["top", "bottom"], + toolingLayerIncludeRefs: [ + "test_short_top_left_top_trace", + "test_short_top_right_top_trace", + "test_short_bottom_right_top_trace", + "test_short_bottom_left_top_trace", + ], laserSpotSize: 0.005, traceMargin: 0.5, copperCutFillMargin: 0.5, @@ -451,12 +460,10 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) { setError(null) try { - const { convertCircuitJsonToLbrn } = await import("circuit-json-to-lbrn") - const finalOptions = { ...lbrnOptions, ...options } - const rawXml = await convertCircuitJsonToLbrn(circuitJson, finalOptions) - const xml = formatLbrnXml(rawXml) + const project = await convertCircuitJsonToLbrn(circuitJson, finalOptions) + const xml = project.getString() setLbrnFileContent({ xml, @@ -500,44 +507,6 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) { ) } -const formatLbrnXml = (value: unknown): string => { - if (typeof value === "string") { - return value - } - - if (value && typeof value === "object") { - const candidate = value as { xml?: unknown; outerHTML?: unknown } - if (typeof candidate.xml === "string") { - return candidate.xml - } - if (typeof candidate.outerHTML === "string") { - return candidate.outerHTML - } - } - - if (typeof window !== "undefined" && value instanceof XMLDocument) { - return new XMLSerializer().serializeToString(value) - } - - if (typeof window !== "undefined" && value instanceof Element) { - return new XMLSerializer().serializeToString(value) - } - - if (Array.isArray(value)) { - return value.map(formatLbrnXml).join("") - } - - const fallback = String(value) - if (fallback.startsWith("[object ") && fallback.endsWith("]")) { - const inner = fallback.slice(8, -1) - if (inner.trim().startsWith(" { + const toolingShapes = project.children.filter( + (child): child is ShapeBase => + child instanceof ShapeBase && child.cutIndex === TOOL_1_INDEX, + ) + + // lbrnts 0.0.22 supports native tool layers in the project model but its SVG + // palette stops at C24. Borrow an unused preview color without changing the + // exported LightBurn project. + for (const shape of toolingShapes) { + shape.cutIndex = PREVIEW_INDEX + } + + try { + return generateLightBurnSvg(project).replaceAll(PREVIEW_COLOR, TOOL_1_COLOR) + } finally { + for (const shape of toolingShapes) { + shape.cutIndex = TOOL_1_INDEX + } + } +} diff --git a/lib/helpers/lightburn-cut-settings.ts b/lib/helpers/lightburn-cut-settings.ts index a34a7a8..0602f39 100644 --- a/lib/helpers/lightburn-cut-settings.ts +++ b/lib/helpers/lightburn-cut-settings.ts @@ -24,6 +24,7 @@ export const LIGHTBURN_COLORS: Record = { 22: "#FF8040", 23: "#FFC0FF", 24: "#FF80C0", + 30: "#F36926", } export type ExistingCutSetting = { diff --git a/lib/hooks/preview-hooks.tsx b/lib/hooks/preview-hooks.tsx index c0a3daf..6014a4a 100644 --- a/lib/hooks/preview-hooks.tsx +++ b/lib/hooks/preview-hooks.tsx @@ -1,9 +1,10 @@ import { distance, type CircuitJson } from "circuit-json" -import { convertCircuitJsonToLbrn } from "circuit-json-to-lbrn" -import type { ConvertCircuitJsonToLbrnOptions } from "circuit-json-to-lbrn" +import { + convertCircuitJsonToLbrn, + type ConvertCircuitJsonToLbrnOptions, +} from "circuit-json-to-lbrn" import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" import type { PcbSvgOptions } from "circuit-to-svg" -import { generateLightBurnSvg } from "lbrnts" import { type RefObject, useCallback, @@ -20,6 +21,7 @@ import { translate, } from "transformation-matrix" import { useMouseMatrixTransform } from "use-mouse-matrix-transform" +import { generateLightBurnSvgForPreview } from "../helpers/generate-lightburn-svg-for-preview" import { IDENTITY_MATRIX, computeFitTransform } from "../helpers/svg-transform" const pcbPreviewSvgOptions: PcbSvgOptions = { @@ -189,7 +191,7 @@ export function useSvgGeneration({ lbrnOptions, ) - const lbrnSvgResult = generateLightBurnSvg(lbrnProject) + const lbrnSvgResult = generateLightBurnSvgForPreview(lbrnProject) if (!cancelled) { setLbrnSvg(String(lbrnSvgResult)) lastLbrnInputs.current = { circuitJson, lbrnOptions } diff --git a/package.json b/package.json index 60960ab..e88967f 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "@radix-ui/react-dialog": "^1.1.15", "@tscircuit/plop": "^0.0.72", "circuit-json": "^0.0.403", - "circuit-json-to-lbrn": "^0.0.82", + "circuit-json-to-lbrn": "^0.0.86", "circuit-to-svg": "^0.0.344", "kicad-to-circuit-json": "^0.0.88", "kicadts": "^0.0.45", - "lbrnts": "^0.0.19", + "lbrnts": "^0.0.22", "lucide-react": "^0.562.0", "react": "^19.1.0", "react-cosmos": "^7.1.0", diff --git a/tests/conductivity-pads.test.ts b/tests/conductivity-pads.test.ts index 6af34ce..9dfe3d4 100644 --- a/tests/conductivity-pads.test.ts +++ b/tests/conductivity-pads.test.ts @@ -45,6 +45,12 @@ test("inserted conductivity pads are included in top and bottom copper cut fill" includeCopper: true, includeCopperCutFill: true, includeLayers: ["top", "bottom"], + toolingLayerIncludeRefs: [ + "test_short_top_left_top_trace", + "test_short_top_right_top_trace", + "test_short_bottom_right_top_trace", + "test_short_bottom_left_top_trace", + ], origin: { x: 0, y: 0 }, copperCutFillMargin: 0.5, }) diff --git a/tests/svg.test.ts b/tests/svg.test.ts index 9251566..689d522 100644 --- a/tests/svg.test.ts +++ b/tests/svg.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test" +import { LightBurnProject, ShapePath } from "lbrnts" +import { generateLightBurnSvgForPreview } from "../lib/helpers/generate-lightburn-svg-for-preview" const testSvg = ` @@ -9,3 +11,23 @@ test("svg snapshot example", async () => { // Subsequent runs will compare against the saved snapshot await expect(testSvg).toMatchSvgSnapshot(import.meta.path) }) + +test("renders native T1 shapes in the preview palette", () => { + const project = new LightBurnProject() + const toolingShape = new ShapePath({ + cutIndex: 30, + isClosed: true, + verts: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 0, y: 1 }, + { x: 0, y: 0 }, + ], + prims: Array.from({ length: 5 }, () => ({ type: 0 })), + }) + project.children.push(toolingShape) + + expect(generateLightBurnSvgForPreview(project)).toContain("#F36926") + expect(toolingShape.cutIndex).toBe(30) +})