diff --git a/src/BoardGeomBuilder.ts b/src/BoardGeomBuilder.ts index 791be7d5..ee0bc698 100644 --- a/src/BoardGeomBuilder.ts +++ b/src/BoardGeomBuilder.ts @@ -17,6 +17,7 @@ import type { PcbCopperPour, PcbPanel, } from "circuit-json" +import type { PcbFabricationNoteRect } from "circuit-json" import { su } from "@tscircuit/circuit-json-util" import { translate, rotateZ } from "@jscad/modeling/src/operations/transforms" import { @@ -52,6 +53,7 @@ import { createSilkscreenPathGeom } from "./geoms/create-geoms-for-silkscreen-pa import { createSilkscreenLineGeom } from "./geoms/create-geoms-for-silkscreen-line" import { createSilkscreenRectGeom } from "./geoms/create-geoms-for-silkscreen-rect" import { createSilkscreenCircleGeom } from "./geoms/create-geoms-for-silkscreen-circle" +import { createFabricationNoteRectGeom } from "./geoms/create-geoms-for-fabrication-note-rect" import { createGeom2FromBRep } from "./geoms/brep-converter" import type { GeomContext } from "./GeomContext" import { @@ -97,6 +99,7 @@ type BuilderState = | "processing_silkscreen_circles" | "processing_silkscreen_rects" | "processing_silkscreen_paths" + | "processing_fabrication_note_rects" | "processing_cutouts" | "processing_copper_pours" | "finalizing" @@ -118,6 +121,7 @@ const buildStateOrder: BuilderState[] = [ "processing_silkscreen_circles", "processing_silkscreen_rects", "processing_silkscreen_paths", + "processing_fabrication_note_rects", "finalizing", "done", ] @@ -135,6 +139,7 @@ export class BoardGeomBuilder { private silkscreenLines: PcbSilkscreenLine[] private silkscreenCircles: PcbSilkscreenCircle[] private silkscreenRects: PcbSilkscreenRect[] + private fabricationNoteRects: PcbFabricationNoteRect[] private pcb_cutouts: PcbCutout[] private pcb_copper_pours: PcbCopperPour[] @@ -149,6 +154,7 @@ export class BoardGeomBuilder { private silkscreenLineGeoms: Geom3[] = [] private silkscreenCircleGeoms: Geom3[] = [] private silkscreenRectGeoms: Geom3[] = [] + private fabricationNoteRectGeoms: Geom3[] = [] private copperPourGeoms: Geom3[] = [] private boardClipGeom: Geom3 | null = null @@ -225,6 +231,7 @@ export class BoardGeomBuilder { this.silkscreenLines = su(circuitJson).pcb_silkscreen_line.list() this.silkscreenCircles = su(circuitJson).pcb_silkscreen_circle.list() this.silkscreenRects = su(circuitJson).pcb_silkscreen_rect.list() + this.fabricationNoteRects = su(circuitJson).pcb_fabrication_note_rect.list() this.pcb_cutouts = su(circuitJson).pcb_cutout.list() this.pcb_copper_pours = circuitJson.filter( (e) => e.type === "pcb_copper_pour", @@ -379,6 +386,17 @@ export class BoardGeomBuilder { } break + case "processing_fabrication_note_rects": + if (this.currentIndex < this.fabricationNoteRects.length) { + this.processFabricationNoteRect( + this.fabricationNoteRects[this.currentIndex]!, + ) + this.currentIndex++ + } else { + this.goToNextState() + } + break + case "processing_cutouts": if (this.currentIndex < this.pcb_cutouts.length) { this.processCutout(this.pcb_cutouts[this.currentIndex]!) @@ -1063,6 +1081,13 @@ export class BoardGeomBuilder { } } + private processFabricationNoteRect(fnr: PcbFabricationNoteRect) { + const rectGeom = createFabricationNoteRectGeom(fnr, this.ctx) + if (rectGeom) { + this.fabricationNoteRectGeoms.push(rectGeom) + } + } + private finalize() { if (!this.boardGeom) return // Colorize the final board geometry @@ -1082,6 +1107,7 @@ export class BoardGeomBuilder { ...this.silkscreenCircleGeoms, ...this.silkscreenRectGeoms, ...this.silkscreenPathGeoms, + ...this.fabricationNoteRectGeoms, ] if (this.onCompleteCallback) { diff --git a/src/geoms/create-geoms-for-fabrication-note-rect.ts b/src/geoms/create-geoms-for-fabrication-note-rect.ts new file mode 100644 index 00000000..288794ca --- /dev/null +++ b/src/geoms/create-geoms-for-fabrication-note-rect.ts @@ -0,0 +1,133 @@ +import type { Geom3 } from "@jscad/modeling/src/geometries/types" +import { roundedRectangle } from "@jscad/modeling/src/primitives" +import { extrudeLinear } from "@jscad/modeling/src/operations/extrusions" +import { translate } from "@jscad/modeling/src/operations/transforms" +import { colorize } from "@jscad/modeling/src/colors" +import { subtract, union } from "@jscad/modeling/src/operations/booleans" +import type { GeomContext } from "../GeomContext" +import { coerceDimensionToMm, parseDimensionToMm } from "../utils/units" +import { + clampRectBorderRadius, + extractRectBorderRadius, +} from "../utils/rect-border-radius" +import { M } from "./constants" + +const RECT_SEGMENTS = 64 + +// Helper function to parse color string to RGB array for JSCAD colorize +function parseFabricationNoteColorToRgb( + colorString: string | undefined, +): [number, number, number] { + if (!colorString) { + // Default fabrication note color: light yellow/orange rgb(255, 243, 204) + return [255 / 255, 243 / 255, 204 / 255] + } + + // Handle hex colors like "#FF0000" or "FF0000" + let hex = colorString + if (hex.startsWith("#")) { + hex = hex.slice(1) + } + if (hex.length === 6) { + const r = parseInt(hex.slice(0, 2), 16) / 255 + const g = parseInt(hex.slice(2, 4), 16) / 255 + const b = parseInt(hex.slice(4, 6), 16) / 255 + return [r, g, b] + } + + // Handle rgb() format like "rgb(255, 243, 204)" + if (colorString.startsWith("rgb")) { + const matches = colorString.match(/\d+/g) + if (matches && matches.length >= 3) { + return [ + parseInt(matches[0]!) / 255, + parseInt(matches[1]!) / 255, + parseInt(matches[2]!) / 255, + ] + } + } + + // Default fallback + return [255 / 255, 243 / 255, 204 / 255] +} + +export function createFabricationNoteRectGeom( + rect: any, // Fabrication note rect type + ctx: GeomContext, +): Geom3 | undefined { + const width = coerceDimensionToMm(rect.width, 0) + const height = coerceDimensionToMm(rect.height, 0) + if (width <= 0 || height <= 0) return undefined + + const centerX = parseDimensionToMm(rect.center?.x) ?? 0 + const centerY = parseDimensionToMm(rect.center?.y) ?? 0 + + const rawBorderRadius = extractRectBorderRadius(rect) + const borderRadius = clampRectBorderRadius( + width, + height, + typeof rawBorderRadius === "string" + ? parseDimensionToMm(rawBorderRadius) + : rawBorderRadius, + ) + + const createRectGeom = ( + rectWidth: number, + rectHeight: number, + radius: number, + ) => + extrudeLinear( + { height: 0.012 }, + roundedRectangle({ + size: [rectWidth, rectHeight], + roundRadius: radius, + segments: RECT_SEGMENTS, + }), + ) + + const isFilled = rect.is_filled ?? true + const hasStroke = rect.has_stroke ?? false + const strokeWidth = hasStroke + ? coerceDimensionToMm(rect.stroke_width, 0.1) + : 0 + + let fillGeom: Geom3 | undefined + if (isFilled) { + fillGeom = createRectGeom(width, height, borderRadius) + } + + let strokeGeom: Geom3 | undefined + if (hasStroke && strokeWidth > 0) { + const outerGeom = createRectGeom(width, height, borderRadius) + const innerWidth = width - strokeWidth * 2 + const innerHeight = height - strokeWidth * 2 + + if (innerWidth > 0 && innerHeight > 0) { + const innerRadius = clampRectBorderRadius( + innerWidth, + innerHeight, + Math.max(borderRadius - strokeWidth, 0), + ) + const innerGeom = createRectGeom(innerWidth, innerHeight, innerRadius) + strokeGeom = subtract(outerGeom, innerGeom) + } else { + strokeGeom = outerGeom + } + } + + let rectGeom = fillGeom + if (strokeGeom) { + rectGeom = rectGeom ? union(rectGeom, strokeGeom) : strokeGeom + } + + if (!rectGeom) return undefined + + const layerSign = rect.layer === "bottom" ? -1 : 1 + const zPos = (layerSign * ctx.pcbThickness) / 2 + layerSign * M * 1.5 + + rectGeom = translate([centerX, centerY, zPos], rectGeom) + + // Parse color for fabrication notes (default to light yellow/orange) + const colorRgb = parseFabricationNoteColorToRgb(rect.color) + return colorize(colorRgb, rectGeom) +}