Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/BoardGeomBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand All @@ -118,6 +121,7 @@ const buildStateOrder: BuilderState[] = [
"processing_silkscreen_circles",
"processing_silkscreen_rects",
"processing_silkscreen_paths",
"processing_fabrication_note_rects",
"finalizing",
"done",
]
Expand All @@ -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[]

Expand All @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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]!)
Expand Down Expand Up @@ -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
Expand All @@ -1082,6 +1107,7 @@ export class BoardGeomBuilder {
...this.silkscreenCircleGeoms,
...this.silkscreenRectGeoms,
...this.silkscreenPathGeoms,
...this.fabricationNoteRectGeoms,
]

if (this.onCompleteCallback) {
Expand Down
133 changes: 133 additions & 0 deletions src/geoms/create-geoms-for-fabrication-note-rect.ts
Original file line number Diff line number Diff line change
@@ -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)
}