diff --git a/bun.lock b/bun.lock index e42b84e..d15f26e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@tscircuit/plop": "^0.0.72", "circuit-json": "^0.0.403", - "circuit-json-to-lbrn": "^0.0.85", + "circuit-json-to-lbrn": "^0.0.86", "circuit-to-svg": "^0.0.344", "kicad-to-circuit-json": "^0.0.88", "kicadts": "^0.0.45", @@ -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.85", "", { "dependencies": { "lbrnts": "^0.0.22", "manifold-3d": "^3.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-Ws0891TlqhKZ2RdVjh7lrss8e+GHA+sVXmtaNOr2n7iXe3ga7nYOWm2LWq/edWLonjYL6IkoVXk8MWjPMPQrgQ=="], + "circuit-json-to-lbrn": ["circuit-json-to-lbrn@0.0.86", "", { "dependencies": { "lbrnts": "^0.0.22", "manifold-3d": "^3.3.2" }, "peerDependencies": { "typescript": "^5" } }, "sha512-KVZwQxG2TQaHylpKsS4Ik8ghYL2gjYyBd+zCBXGHFZ3WEBO6ipE3rWTIEqLB0DZ9Ga1TQ/PIWCRLQvas+nBSWg=="], "circuit-json-to-simple-3d": ["circuit-json-to-simple-3d@0.0.9", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-thpCtDb9LpAQfGO+Z0hae19sxVAmJAMYM/It9UTMCtyXC3zoUHwRcp/oqtMO/ZIW+JDBhiR8AHmmtKScL2kW7Q=="], diff --git a/lib/components/workspace-context.tsx b/lib/components/workspace-context.tsx index 05a812d..2f83da3 100644 --- a/lib/components/workspace-context.tsx +++ b/lib/components/workspace-context.tsx @@ -14,6 +14,7 @@ import React, { } from "react" import conductivityPadsTemplateRaw from "../../assets/connectivity-test-pads.json?raw" import fiducialsTemplateRaw from "../../assets/fiducials.json?raw" +import { configureTopSoldermaskRemovalLayer } from "../helpers/configure-top-soldermask-removal-layer" export type FiducialConductivityOption = | "none" @@ -143,6 +144,8 @@ const defaultLbrnOptions: ConvertCircuitJsonToLbrnOptions = { includeSoldermask: true, includeSilkscreen: true, includeCopperCutFill: true, + includeSoldermaskAblation: true, + soldermaskAblationClearance: 0.5, includeOxidationCleaningLayer: true, includeSoldermaskCure: true, mirrorBottomLayer: true, @@ -258,20 +261,29 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) { const setLbrnOptions = ( options: Partial, ) => { - setLbrnOptionsState((prev) => ({ - ...prev, - ...options, - includeCopper: true, - includeSoldermask: true, - includeSilkscreen: true, - includeCopperCutFill: true, - includeOxidationCleaningLayer: - options.includeOxidationCleaningLayer ?? - prev.includeOxidationCleaningLayer ?? - true, - includeSoldermaskCure: true, - includeLayers: options.includeLayers ?? prev.includeLayers, - })) + setLbrnOptionsState((prev) => { + const copperCutFillMargin = + options.copperCutFillMargin ?? + prev.copperCutFillMargin ?? + defaultLbrnOptions.copperCutFillMargin + + return { + ...prev, + ...options, + includeCopper: true, + includeSoldermask: true, + includeSilkscreen: true, + includeCopperCutFill: true, + includeSoldermaskAblation: true, + soldermaskAblationClearance: copperCutFillMargin, + includeOxidationCleaningLayer: + options.includeOxidationCleaningLayer ?? + prev.includeOxidationCleaningLayer ?? + true, + includeSoldermaskCure: true, + includeLayers: options.includeLayers ?? prev.includeLayers, + } + }) } const resetSavedLbrnOptions = () => { @@ -280,6 +292,8 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) { laserSpotSize: defaultLbrnOptions.laserSpotSize, traceMargin: defaultLbrnOptions.traceMargin, copperCutFillMargin: defaultLbrnOptions.copperCutFillMargin, + soldermaskAblationClearance: + defaultLbrnOptions.soldermaskAblationClearance, globalCopperSoldermaskMarginAdjustment: defaultLbrnOptions.globalCopperSoldermaskMarginAdjustment, solderMaskMarginPercent: defaultLbrnOptions.solderMaskMarginPercent, @@ -460,9 +474,19 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) { setError(null) try { - const finalOptions = { ...lbrnOptions, ...options } + const copperCutFillMargin = + options?.copperCutFillMargin ?? + lbrnOptions.copperCutFillMargin ?? + defaultLbrnOptions.copperCutFillMargin + const finalOptions = { + ...lbrnOptions, + ...options, + includeSoldermaskAblation: true, + soldermaskAblationClearance: copperCutFillMargin, + } const project = await convertCircuitJsonToLbrn(circuitJson, finalOptions) + configureTopSoldermaskRemovalLayer(project) const xml = project.getString() setLbrnFileContent({ diff --git a/lib/helpers/configure-top-soldermask-removal-layer.ts b/lib/helpers/configure-top-soldermask-removal-layer.ts new file mode 100644 index 0000000..1806a2f --- /dev/null +++ b/lib/helpers/configure-top-soldermask-removal-layer.ts @@ -0,0 +1,20 @@ +import { CutSetting, type LightBurnProject } from "lbrnts" + +export const TOP_SOLDERMASK_REMOVAL_LAYER_INDEX = 16 +export const TOP_SOLDERMASK_REMOVAL_LAYER_NAME = "Top Soldermask Removal" + +export const configureTopSoldermaskRemovalLayer = ( + project: LightBurnProject, +): CutSetting | undefined => { + const cutSetting = project.children.find( + (child): child is CutSetting => + child instanceof CutSetting && + child.index === TOP_SOLDERMASK_REMOVAL_LAYER_INDEX, + ) + + if (cutSetting) { + cutSetting.name = TOP_SOLDERMASK_REMOVAL_LAYER_NAME + } + + return cutSetting +} diff --git a/tests/top-soldermask-removal.test.ts b/tests/top-soldermask-removal.test.ts new file mode 100644 index 0000000..a802b24 --- /dev/null +++ b/tests/top-soldermask-removal.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from "bun:test" +import type { CircuitJson } from "circuit-json" +import { convertCircuitJsonToLbrn } from "circuit-json-to-lbrn" +import circuitJson from "./examples/example01/1206x4_3216metric.json" with { + type: "json", +} +import { + configureTopSoldermaskRemovalLayer, + TOP_SOLDERMASK_REMOVAL_LAYER_INDEX, + TOP_SOLDERMASK_REMOVAL_LAYER_NAME, +} from "../lib/helpers/configure-top-soldermask-removal-layer" + +type ProjectNode = { + children?: ProjectNode[] + cutIndex?: number +} + +const flattenProject = (nodes: ProjectNode[]): ProjectNode[] => + nodes.flatMap((node) => [node, ...flattenProject(node.children ?? [])]) + +test("creates top soldermask removal from cut fill without interior lines", async () => { + const project = await convertCircuitJsonToLbrn(circuitJson as CircuitJson, { + includeLayers: ["top"], + includeCopper: true, + includeCopperCutFill: true, + includeSoldermaskAblation: true, + copperCutFillMargin: 0.5, + soldermaskAblationClearance: 0.5, + origin: { x: 0, y: 0 }, + }) + + const removalSetting = configureTopSoldermaskRemovalLayer(project) + const projectNodes = flattenProject(project.children as ProjectNode[]) + const copperCutFillShapes = projectNodes.filter((node) => node.cutIndex === 6) + const removalShapes = projectNodes.filter( + (node) => node.cutIndex === TOP_SOLDERMASK_REMOVAL_LAYER_INDEX, + ) + + expect(removalSetting?.name).toBe(TOP_SOLDERMASK_REMOVAL_LAYER_NAME) + expect(removalShapes).toHaveLength(1) + expect(copperCutFillShapes.length).toBeGreaterThan(removalShapes.length) + expect(project.getString()).toContain( + ``, + ) +})