@tscircuit/3d-viewer 0.0.451 → 0.0.453

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14229,7 +14229,7 @@ var require_browser = __commonJS({
14229
14229
 
14230
14230
  // src/CadViewer.tsx
14231
14231
  import { useState as useState36, useCallback as useCallback22, useRef as useRef26, useEffect as useEffect42, useMemo as useMemo28 } from "react";
14232
- import * as THREE29 from "three";
14232
+ import * as THREE30 from "three";
14233
14233
 
14234
14234
  // src/CadViewerJscad.tsx
14235
14235
  import { su as su6 } from "@tscircuit/circuit-json-util";
@@ -28509,7 +28509,7 @@ import * as THREE15 from "three";
28509
28509
  // package.json
28510
28510
  var package_default = {
28511
28511
  name: "@tscircuit/3d-viewer",
28512
- version: "0.0.450",
28512
+ version: "0.0.452",
28513
28513
  main: "./dist/index.js",
28514
28514
  module: "./dist/index.js",
28515
28515
  type: "module",
@@ -32356,13 +32356,74 @@ var CadViewerJscad = forwardRef3(
32356
32356
  );
32357
32357
 
32358
32358
  // src/CadViewerManifold.tsx
32359
- import { su as su15 } from "@tscircuit/circuit-json-util";
32359
+ import { su as su16 } from "@tscircuit/circuit-json-util";
32360
32360
  import { useEffect as useEffect23, useMemo as useMemo20, useState as useState16 } from "react";
32361
32361
 
32362
32362
  // src/hooks/useManifoldBoardBuilder.ts
32363
32363
  import { useState as useState15, useEffect as useEffect22, useMemo as useMemo19, useRef as useRef9 } from "react";
32364
- import { su as su14 } from "@tscircuit/circuit-json-util";
32365
- import * as THREE25 from "three";
32364
+ import { su as su15 } from "@tscircuit/circuit-json-util";
32365
+ import * as THREE26 from "three";
32366
+
32367
+ // src/utils/manifold/create-manifold-board.ts
32368
+ var arePointsClockwise3 = (points) => {
32369
+ let area = 0;
32370
+ for (let i = 0; i < points.length; i++) {
32371
+ const j = (i + 1) % points.length;
32372
+ if (points[i] && points[j]) {
32373
+ area += points[i][0] * points[j][1];
32374
+ area -= points[j][0] * points[i][1];
32375
+ }
32376
+ }
32377
+ const signedArea = area / 2;
32378
+ return signedArea <= 0;
32379
+ };
32380
+ function createManifoldBoard(Manifold, CrossSection, boardData, pcbThickness, manifoldInstancesForCleanup) {
32381
+ let boardOp;
32382
+ let outlineCrossSection = null;
32383
+ if (boardData.outline && boardData.outline.length >= 3) {
32384
+ let outlineVec2 = boardData.outline.map((p) => [
32385
+ p.x,
32386
+ p.y
32387
+ ]);
32388
+ if (arePointsClockwise3(outlineVec2)) {
32389
+ outlineVec2 = outlineVec2.reverse();
32390
+ }
32391
+ const crossSection = CrossSection.ofPolygons([outlineVec2]);
32392
+ manifoldInstancesForCleanup.push(crossSection);
32393
+ outlineCrossSection = crossSection;
32394
+ boardOp = Manifold.extrude(
32395
+ crossSection,
32396
+ pcbThickness,
32397
+ void 0,
32398
+ // nDivisions
32399
+ void 0,
32400
+ // twistDegrees
32401
+ void 0,
32402
+ // scaleTop
32403
+ true
32404
+ // center (for Z-axis)
32405
+ );
32406
+ manifoldInstancesForCleanup.push(boardOp);
32407
+ } else {
32408
+ if (boardData.outline && boardData.outline.length > 0) {
32409
+ console.warn(
32410
+ "Board outline has fewer than 3 points, falling back to rectangular board."
32411
+ );
32412
+ }
32413
+ boardOp = Manifold.cube(
32414
+ [boardData.width, boardData.height, pcbThickness],
32415
+ true
32416
+ // center (for all axes)
32417
+ );
32418
+ manifoldInstancesForCleanup.push(boardOp);
32419
+ boardOp = boardOp.translate([boardData.center.x, boardData.center.y, 0]);
32420
+ manifoldInstancesForCleanup.push(boardOp);
32421
+ }
32422
+ return { boardOp, outlineCrossSection };
32423
+ }
32424
+
32425
+ // src/utils/manifold/process-copper-pours.ts
32426
+ import * as THREE19 from "three";
32366
32427
 
32367
32428
  // src/utils/manifold-mesh-to-three-geometry.ts
32368
32429
  import * as THREE18 from "three";
@@ -32385,409 +32446,181 @@ function manifoldMeshToThreeGeometry(manifoldMesh) {
32385
32446
  return geometry;
32386
32447
  }
32387
32448
 
32388
- // src/utils/trace-texture.ts
32389
- import * as THREE19 from "three";
32390
- import { su as su7 } from "@tscircuit/circuit-json-util";
32391
- function isWireRoutePoint(point2) {
32392
- return point2 && point2.route_type === "wire" && typeof point2.layer === "string" && typeof point2.width === "number";
32393
- }
32394
- function createTraceTextureForLayer({
32395
- layer,
32396
- circuitJson,
32397
- boardData,
32398
- traceColor,
32399
- traceTextureResolution
32400
- }) {
32401
- const pcbTraces = su7(circuitJson).pcb_trace.list();
32402
- const allPcbVias = su7(circuitJson).pcb_via.list();
32403
- const allPcbPlatedHoles = su7(
32404
- circuitJson
32405
- ).pcb_plated_hole.list();
32406
- const tracesOnLayer = pcbTraces.filter(
32407
- (t) => t.route.some((p) => isWireRoutePoint(p) && p.layer === layer)
32449
+ // src/utils/manifold/process-copper-pours.ts
32450
+ var arePointsClockwise4 = (points) => {
32451
+ let area = 0;
32452
+ for (let i = 0; i < points.length; i++) {
32453
+ const j = (i + 1) % points.length;
32454
+ if (points[i] && points[j]) {
32455
+ area += points[i][0] * points[j][1];
32456
+ area -= points[j][0] * points[i][1];
32457
+ }
32458
+ }
32459
+ const signedArea = area / 2;
32460
+ return signedArea <= 0;
32461
+ };
32462
+ function segmentToPoints2(p1, p2, bulge, arcSegments) {
32463
+ if (!bulge || Math.abs(bulge) < 1e-9) {
32464
+ return [];
32465
+ }
32466
+ const theta = 4 * Math.atan(bulge);
32467
+ const dx = p2[0] - p1[0];
32468
+ const dy = p2[1] - p1[1];
32469
+ const dist = Math.sqrt(dx * dx + dy * dy);
32470
+ if (dist < 1e-9) return [];
32471
+ const radius = Math.abs(dist / (2 * Math.sin(theta / 2)));
32472
+ const m = Math.sqrt(Math.max(0, radius * radius - dist / 2 * (dist / 2)));
32473
+ const midPoint = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
32474
+ const ux = dx / dist;
32475
+ const uy = dy / dist;
32476
+ const nx = -uy;
32477
+ const ny = ux;
32478
+ const centerX = midPoint[0] + nx * m * Math.sign(bulge);
32479
+ const centerY = midPoint[1] + ny * m * Math.sign(bulge);
32480
+ const startAngle = Math.atan2(p1[1] - centerY, p1[0] - centerX);
32481
+ const points = [];
32482
+ const numSteps = Math.max(
32483
+ 2,
32484
+ Math.ceil(arcSegments * Math.abs(theta) / (Math.PI * 2) * 4)
32408
32485
  );
32409
- if (tracesOnLayer.length === 0) return null;
32410
- const canvas = document.createElement("canvas");
32411
- const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
32412
- const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
32413
- canvas.width = canvasWidth;
32414
- canvas.height = canvasHeight;
32415
- const ctx = canvas.getContext("2d");
32416
- if (!ctx) return null;
32417
- if (layer === "bottom") {
32418
- ctx.translate(0, canvasHeight);
32419
- ctx.scale(1, -1);
32486
+ const angleStep = theta / numSteps;
32487
+ for (let i = 1; i < numSteps; i++) {
32488
+ const angle = startAngle + angleStep * i;
32489
+ points.push([
32490
+ centerX + radius * Math.cos(angle),
32491
+ centerY + radius * Math.sin(angle)
32492
+ ]);
32420
32493
  }
32421
- tracesOnLayer.forEach((trace) => {
32422
- let firstPoint = true;
32423
- ctx.beginPath();
32424
- ctx.strokeStyle = traceColor;
32425
- ctx.lineCap = "round";
32426
- ctx.lineJoin = "round";
32427
- let currentLineWidth = 0;
32428
- for (const point2 of trace.route) {
32429
- if (!isWireRoutePoint(point2) || point2.layer !== layer) {
32430
- if (!firstPoint) ctx.stroke();
32431
- firstPoint = true;
32432
- continue;
32433
- }
32434
- const pcbX = point2.x;
32435
- const pcbY = point2.y;
32436
- currentLineWidth = point2.width * traceTextureResolution;
32437
- ctx.lineWidth = currentLineWidth;
32438
- const canvasX = (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
32439
- const canvasY = (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
32440
- if (firstPoint) {
32441
- ctx.moveTo(canvasX, canvasY);
32442
- firstPoint = false;
32443
- } else {
32444
- ctx.lineTo(canvasX, canvasY);
32445
- }
32446
- }
32447
- if (!firstPoint) {
32448
- ctx.stroke();
32449
- }
32450
- });
32451
- ctx.globalCompositeOperation = "destination-out";
32452
- ctx.fillStyle = "black";
32453
- allPcbVias.forEach((via) => {
32454
- const canvasX = (via.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
32455
- const canvasY = (-(via.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
32456
- const canvasRadius = via.outer_diameter / 2 * traceTextureResolution;
32457
- ctx.beginPath();
32458
- ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
32459
- ctx.fill();
32460
- });
32461
- allPcbPlatedHoles.forEach((ph) => {
32462
- if (ph.layers.includes(layer) && ph.shape === "circle") {
32463
- const canvasX = (ph.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
32464
- const canvasY = (-(ph.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
32465
- const canvasRadius = ph.outer_diameter / 2 * traceTextureResolution;
32466
- ctx.beginPath();
32467
- ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
32468
- ctx.fill();
32469
- }
32470
- });
32471
- ctx.globalCompositeOperation = "source-over";
32472
- const texture = new THREE19.CanvasTexture(canvas);
32473
- texture.generateMipmaps = true;
32474
- texture.minFilter = THREE19.LinearMipmapLinearFilter;
32475
- texture.magFilter = THREE19.LinearFilter;
32476
- texture.anisotropy = 16;
32477
- texture.needsUpdate = true;
32478
- return texture;
32494
+ return points;
32479
32495
  }
32480
-
32481
- // src/utils/silkscreen-texture.ts
32482
- var import_text2 = __toESM(require_text(), 1);
32483
- import * as THREE20 from "three";
32484
- import { su as su8 } from "@tscircuit/circuit-json-util";
32485
- function createSilkscreenTextureForLayer({
32486
- layer,
32487
- circuitJson,
32488
- boardData,
32489
- silkscreenColor = "rgb(255,255,255)",
32490
- traceTextureResolution
32491
- }) {
32492
- const pcbSilkscreenTexts = su8(circuitJson).pcb_silkscreen_text.list();
32493
- const pcbSilkscreenPaths = su8(circuitJson).pcb_silkscreen_path.list();
32494
- const pcbSilkscreenLines = su8(circuitJson).pcb_silkscreen_line.list();
32495
- const pcbSilkscreenRects = su8(circuitJson).pcb_silkscreen_rect.list();
32496
- const pcbSilkscreenCircles = su8(circuitJson).pcb_silkscreen_circle.list();
32497
- const textsOnLayer = pcbSilkscreenTexts.filter((t) => t.layer === layer);
32498
- const pathsOnLayer = pcbSilkscreenPaths.filter((p) => p.layer === layer);
32499
- const linesOnLayer = pcbSilkscreenLines.filter((l) => l.layer === layer);
32500
- const rectsOnLayer = pcbSilkscreenRects.filter((r) => r.layer === layer);
32501
- const circlesOnLayer = pcbSilkscreenCircles.filter((c) => c.layer === layer);
32502
- if (textsOnLayer.length === 0 && pathsOnLayer.length === 0 && linesOnLayer.length === 0 && rectsOnLayer.length === 0 && circlesOnLayer.length === 0) {
32503
- return null;
32504
- }
32505
- const canvas = document.createElement("canvas");
32506
- const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
32507
- const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
32508
- canvas.width = canvasWidth;
32509
- canvas.height = canvasHeight;
32510
- const ctx = canvas.getContext("2d");
32511
- if (!ctx) return null;
32512
- if (layer === "bottom") {
32513
- ctx.translate(0, canvasHeight);
32514
- ctx.scale(1, -1);
32496
+ function ringToPoints2(ring2, arcSegments) {
32497
+ const allPoints = [];
32498
+ const vertices = ring2.vertices;
32499
+ for (let i = 0; i < vertices.length; i++) {
32500
+ const p1 = vertices[i];
32501
+ const p2 = vertices[(i + 1) % vertices.length];
32502
+ allPoints.push([p1.x, p1.y]);
32503
+ if (p1.bulge) {
32504
+ const arcPoints = segmentToPoints2(
32505
+ [p1.x, p1.y],
32506
+ [p2.x, p2.y],
32507
+ p1.bulge,
32508
+ arcSegments
32509
+ );
32510
+ allPoints.push(...arcPoints);
32511
+ }
32515
32512
  }
32516
- ctx.strokeStyle = silkscreenColor;
32517
- ctx.fillStyle = silkscreenColor;
32518
- const canvasXFromPcb = (pcbX) => (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
32519
- const canvasYFromPcb = (pcbY) => (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
32520
- linesOnLayer.forEach((lineEl) => {
32521
- const startXmm = parseDimensionToMm(lineEl.x1) ?? 0;
32522
- const startYmm = parseDimensionToMm(lineEl.y1) ?? 0;
32523
- const endXmm = parseDimensionToMm(lineEl.x2) ?? 0;
32524
- const endYmm = parseDimensionToMm(lineEl.y2) ?? 0;
32525
- if (startXmm === endXmm && startYmm === endYmm) return;
32526
- ctx.beginPath();
32527
- ctx.lineWidth = coerceDimensionToMm(lineEl.stroke_width, 0.1) * traceTextureResolution;
32528
- ctx.lineCap = "round";
32529
- ctx.moveTo(canvasXFromPcb(startXmm), canvasYFromPcb(startYmm));
32530
- ctx.lineTo(canvasXFromPcb(endXmm), canvasYFromPcb(endYmm));
32531
- ctx.stroke();
32532
- });
32533
- pathsOnLayer.forEach((path) => {
32534
- if (path.route.length < 2) return;
32535
- ctx.beginPath();
32536
- ctx.lineWidth = coerceDimensionToMm(path.stroke_width, 0.1) * traceTextureResolution;
32537
- ctx.lineCap = "round";
32538
- ctx.lineJoin = "round";
32539
- path.route.forEach((point2, index2) => {
32540
- const canvasX = canvasXFromPcb(parseDimensionToMm(point2.x) ?? 0);
32541
- const canvasY = canvasYFromPcb(parseDimensionToMm(point2.y) ?? 0);
32542
- if (index2 === 0) ctx.moveTo(canvasX, canvasY);
32543
- else ctx.lineTo(canvasX, canvasY);
32544
- });
32545
- ctx.stroke();
32546
- });
32547
- circlesOnLayer.forEach((circleEl) => {
32548
- const radius = coerceDimensionToMm(circleEl.radius, 0);
32549
- if (radius <= 0) return;
32550
- const strokeWidth = coerceDimensionToMm(circleEl.stroke_width, 0.12);
32551
- const hasStroke = strokeWidth > 0;
32552
- const centerXmm = parseDimensionToMm(circleEl.center?.x) ?? 0;
32553
- const centerYmm = parseDimensionToMm(circleEl.center?.y) ?? 0;
32554
- const canvasCenterX = canvasXFromPcb(centerXmm);
32555
- const canvasCenterY = canvasYFromPcb(centerYmm);
32556
- const radiusPx = radius * traceTextureResolution;
32557
- ctx.save();
32558
- ctx.translate(canvasCenterX, canvasCenterY);
32559
- if (hasStroke) {
32560
- const outerRadiusPx = radiusPx + strokeWidth / 2 * traceTextureResolution;
32561
- const innerRadiusPx = Math.max(
32513
+ return allPoints;
32514
+ }
32515
+ function processCopperPoursForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup, boardMaterial, holeUnion, boardClipVolume) {
32516
+ const copperPourGeoms = [];
32517
+ const copperPours = circuitJson.filter(
32518
+ (e) => e.type === "pcb_copper_pour"
32519
+ );
32520
+ for (const pour of copperPours) {
32521
+ const pourThickness = DEFAULT_SMT_PAD_THICKNESS;
32522
+ const layerSign = pour.layer === "bottom" ? -1 : 1;
32523
+ const zPos = layerSign * (pcbThickness / 2 + pourThickness / 2 + MANIFOLD_Z_OFFSET);
32524
+ let pourOp;
32525
+ if (pour.shape === "rect") {
32526
+ pourOp = Manifold.cube([pour.width, pour.height, pourThickness], true);
32527
+ manifoldInstancesForCleanup.push(pourOp);
32528
+ if (pour.rotation) {
32529
+ const rotatedOp = pourOp.rotate([0, 0, pour.rotation]);
32530
+ manifoldInstancesForCleanup.push(rotatedOp);
32531
+ pourOp = rotatedOp;
32532
+ }
32533
+ pourOp = pourOp.translate([pour.center.x, pour.center.y, zPos]);
32534
+ manifoldInstancesForCleanup.push(pourOp);
32535
+ } else if (pour.shape === "polygon") {
32536
+ if (pour.points.length < 3) continue;
32537
+ let pointsVec2 = pour.points.map((p) => [
32538
+ p.x,
32539
+ p.y
32540
+ ]);
32541
+ if (arePointsClockwise4(pointsVec2)) {
32542
+ pointsVec2 = pointsVec2.reverse();
32543
+ }
32544
+ const crossSection = CrossSection.ofPolygons([pointsVec2]);
32545
+ manifoldInstancesForCleanup.push(crossSection);
32546
+ pourOp = Manifold.extrude(
32547
+ crossSection,
32548
+ pourThickness,
32562
32549
  0,
32563
- radiusPx - strokeWidth / 2 * traceTextureResolution
32550
+ // nDivisions
32551
+ 0,
32552
+ // twistDegrees
32553
+ [1, 1],
32554
+ // scaleTop
32555
+ true
32556
+ // center extrusion
32557
+ ).translate([0, 0, zPos]);
32558
+ manifoldInstancesForCleanup.push(pourOp);
32559
+ } else if (pour.shape === "brep") {
32560
+ const brepShape = pour.brep_shape;
32561
+ if (!brepShape || !brepShape.outer_ring) continue;
32562
+ let outerRingPoints = ringToPoints2(
32563
+ brepShape.outer_ring,
32564
+ SMOOTH_CIRCLE_SEGMENTS
32564
32565
  );
32565
- if (innerRadiusPx > 0) {
32566
- ctx.beginPath();
32567
- ctx.arc(0, 0, outerRadiusPx, 0, 2 * Math.PI);
32568
- ctx.arc(0, 0, innerRadiusPx, 0, 2 * Math.PI, true);
32569
- ctx.fill("evenodd");
32570
- } else {
32571
- ctx.beginPath();
32572
- ctx.arc(0, 0, outerRadiusPx, 0, 2 * Math.PI);
32573
- ctx.fill();
32566
+ if (arePointsClockwise4(outerRingPoints)) {
32567
+ outerRingPoints = outerRingPoints.reverse();
32574
32568
  }
32575
- } else {
32576
- ctx.beginPath();
32577
- ctx.arc(0, 0, radiusPx, 0, 2 * Math.PI);
32578
- ctx.fill();
32579
- }
32580
- ctx.restore();
32581
- });
32582
- rectsOnLayer.forEach((rect) => {
32583
- const width10 = coerceDimensionToMm(rect.width, 0);
32584
- const height10 = coerceDimensionToMm(rect.height, 0);
32585
- if (width10 <= 0 || height10 <= 0) return;
32586
- const centerXmm = parseDimensionToMm(rect.center?.x) ?? 0;
32587
- const centerYmm = parseDimensionToMm(rect.center?.y) ?? 0;
32588
- const canvasCenterX = canvasXFromPcb(centerXmm);
32589
- const canvasCenterY = canvasYFromPcb(centerYmm);
32590
- const rawRadius = extractRectBorderRadius(rect);
32591
- const borderRadiusInput = typeof rawRadius === "string" ? parseDimensionToMm(rawRadius) : rawRadius;
32592
- const borderRadiusMm = clampRectBorderRadius(
32593
- width10,
32594
- height10,
32595
- borderRadiusInput
32596
- );
32597
- ctx.save();
32598
- ctx.translate(canvasCenterX, canvasCenterY);
32599
- const halfWidthPx = width10 / 2 * traceTextureResolution;
32600
- const halfHeightPx = height10 / 2 * traceTextureResolution;
32601
- const borderRadiusPx = Math.min(
32602
- borderRadiusMm * traceTextureResolution,
32603
- halfWidthPx,
32604
- halfHeightPx
32605
- );
32606
- const hasStroke = rect.has_stroke ?? false;
32607
- const isFilled = rect.is_filled ?? true;
32608
- const isDashed = rect.is_stroke_dashed ?? false;
32609
- const strokeWidthPx = hasStroke ? coerceDimensionToMm(rect.stroke_width, 0.1) * traceTextureResolution : 0;
32610
- const drawRoundedRectPath = (x, y, rectWidth, rectHeight, radius) => {
32611
- ctx.beginPath();
32612
- if (radius <= 0) {
32613
- ctx.rect(x, y, rectWidth, rectHeight);
32614
- } else {
32615
- const r = radius;
32616
- const right = x + rectWidth;
32617
- const bottom = y + rectHeight;
32618
- ctx.moveTo(x + r, y);
32619
- ctx.lineTo(right - r, y);
32620
- ctx.quadraticCurveTo(right, y, right, y + r);
32621
- ctx.lineTo(right, bottom - r);
32622
- ctx.quadraticCurveTo(right, bottom, right - r, bottom);
32623
- ctx.lineTo(x + r, bottom);
32624
- ctx.quadraticCurveTo(x, bottom, x, bottom - r);
32625
- ctx.lineTo(x, y + r);
32626
- ctx.quadraticCurveTo(x, y, x + r, y);
32627
- ctx.closePath();
32569
+ const polygons = [outerRingPoints];
32570
+ if (brepShape.inner_rings) {
32571
+ const innerRingsPoints = brepShape.inner_rings.map((ring2) => {
32572
+ let points = ringToPoints2(ring2, SMOOTH_CIRCLE_SEGMENTS);
32573
+ if (!arePointsClockwise4(points)) {
32574
+ points = points.reverse();
32575
+ }
32576
+ return points;
32577
+ });
32578
+ polygons.push(...innerRingsPoints);
32628
32579
  }
32629
- };
32630
- drawRoundedRectPath(
32631
- -halfWidthPx,
32632
- -halfHeightPx,
32633
- halfWidthPx * 2,
32634
- halfHeightPx * 2,
32635
- borderRadiusPx
32636
- );
32637
- if (isFilled) {
32638
- ctx.fill();
32580
+ const crossSection = CrossSection.ofPolygons(polygons);
32581
+ manifoldInstancesForCleanup.push(crossSection);
32582
+ pourOp = Manifold.extrude(
32583
+ crossSection,
32584
+ pourThickness,
32585
+ 0,
32586
+ // nDivisions
32587
+ 0,
32588
+ // twistDegrees
32589
+ [1, 1],
32590
+ // scaleTop
32591
+ true
32592
+ // center extrusion
32593
+ ).translate([0, 0, zPos]);
32594
+ manifoldInstancesForCleanup.push(pourOp);
32639
32595
  }
32640
- if (hasStroke && strokeWidthPx > 0) {
32641
- ctx.lineWidth = strokeWidthPx;
32642
- if (isDashed) {
32643
- const dashLength = Math.max(strokeWidthPx * 2, 1);
32644
- ctx.setLineDash([dashLength, dashLength]);
32645
- }
32646
- ctx.stroke();
32647
- if (isDashed) {
32648
- ctx.setLineDash([]);
32596
+ if (pourOp) {
32597
+ if (holeUnion) {
32598
+ const withHoles = pourOp.subtract(holeUnion);
32599
+ manifoldInstancesForCleanup.push(withHoles);
32600
+ pourOp = withHoles;
32649
32601
  }
32650
- }
32651
- ctx.restore();
32652
- });
32653
- textsOnLayer.forEach((textS) => {
32654
- const fontSize = textS.font_size || 0.25;
32655
- const textStrokeWidth = Math.min(Math.max(0.01, fontSize * 0.1), fontSize * 0.05) * traceTextureResolution;
32656
- ctx.lineWidth = textStrokeWidth;
32657
- ctx.lineCap = "butt";
32658
- ctx.lineJoin = "miter";
32659
- const rawTextOutlines = (0, import_text2.vectorText)({
32660
- height: fontSize * 0.45,
32661
- input: textS.text
32662
- });
32663
- const processedTextOutlines = [];
32664
- rawTextOutlines.forEach((outline) => {
32665
- if (outline.length === 29) {
32666
- processedTextOutlines.push(
32667
- outline.slice(0, 15)
32668
- );
32669
- processedTextOutlines.push(
32670
- outline.slice(14, 29)
32671
- );
32672
- } else if (outline.length === 17) {
32673
- processedTextOutlines.push(
32674
- outline.slice(0, 10)
32675
- );
32676
- processedTextOutlines.push(
32677
- outline.slice(9, 17)
32678
- );
32679
- } else {
32680
- processedTextOutlines.push(outline);
32602
+ if (boardClipVolume) {
32603
+ const clipped = Manifold.intersection([pourOp, boardClipVolume]);
32604
+ manifoldInstancesForCleanup.push(clipped);
32605
+ pourOp = clipped;
32681
32606
  }
32682
- });
32683
- const points = processedTextOutlines.flat();
32684
- const textBounds = {
32685
- minX: points.length > 0 ? Math.min(...points.map((p) => p[0])) : 0,
32686
- maxX: points.length > 0 ? Math.max(...points.map((p) => p[0])) : 0,
32687
- minY: points.length > 0 ? Math.min(...points.map((p) => p[1])) : 0,
32688
- maxY: points.length > 0 ? Math.max(...points.map((p) => p[1])) : 0
32689
- };
32690
- const textCenterX = (textBounds.minX + textBounds.maxX) / 2;
32691
- const textCenterY = (textBounds.minY + textBounds.maxY) / 2;
32692
- let xOff = -textCenterX;
32693
- let yOff = -textCenterY;
32694
- const alignment = textS.anchor_alignment || "center";
32695
- if (alignment.includes("left")) {
32696
- xOff = -textBounds.minX;
32697
- } else if (alignment.includes("right")) {
32698
- xOff = -textBounds.maxX;
32699
- }
32700
- if (alignment.includes("top")) {
32701
- yOff = -textBounds.maxY;
32702
- } else if (alignment.includes("bottom")) {
32703
- yOff = -textBounds.minY;
32704
- }
32705
- const transformMatrices = [];
32706
- let rotationDeg = textS.ccw_rotation ?? 0;
32707
- if (textS.layer === "bottom") {
32708
- transformMatrices.push(
32709
- translate4(textCenterX, textCenterY),
32710
- { a: -1, b: 0, c: 0, d: 1, e: 0, f: 0 },
32711
- translate4(-textCenterX, -textCenterY)
32712
- );
32713
- rotationDeg = -rotationDeg;
32714
- }
32715
- if (rotationDeg) {
32716
- const rad = rotationDeg * Math.PI / 180;
32717
- transformMatrices.push(
32718
- translate4(textCenterX, textCenterY),
32719
- rotate2(rad),
32720
- translate4(-textCenterX, -textCenterY)
32721
- );
32722
- }
32723
- const finalTransformMatrix = transformMatrices.length > 0 ? compose(...transformMatrices) : void 0;
32724
- processedTextOutlines.forEach((segment) => {
32725
- ctx.beginPath();
32726
- segment.forEach((p, index2) => {
32727
- let transformedP = { x: p[0], y: p[1] };
32728
- if (finalTransformMatrix) {
32729
- transformedP = applyToPoint(finalTransformMatrix, transformedP);
32730
- }
32731
- const pcbX = transformedP.x + xOff + textS.anchor_position.x;
32732
- const pcbY = transformedP.y + yOff + textS.anchor_position.y;
32733
- const canvasX = canvasXFromPcb(pcbX);
32734
- const canvasY = canvasYFromPcb(pcbY);
32735
- if (index2 === 0) ctx.moveTo(canvasX, canvasY);
32736
- else ctx.lineTo(canvasX, canvasY);
32737
- });
32738
- ctx.stroke();
32739
- });
32740
- });
32741
- const texture = new THREE20.CanvasTexture(canvas);
32742
- texture.generateMipmaps = true;
32743
- texture.minFilter = THREE20.LinearMipmapLinearFilter;
32744
- texture.magFilter = THREE20.LinearFilter;
32745
- texture.anisotropy = 16;
32746
- texture.needsUpdate = true;
32747
- return texture;
32748
- }
32749
-
32750
- // src/utils/manifold/process-non-plated-holes.ts
32751
- import { su as su9 } from "@tscircuit/circuit-json-util";
32752
-
32753
- // src/utils/hole-geoms.ts
32754
- function createCircleHoleDrill({
32755
- Manifold,
32756
- x,
32757
- y,
32758
- diameter,
32759
- thickness,
32760
- segments = 32
32761
- }) {
32762
- const drill = Manifold.cylinder(
32763
- thickness * 1.2,
32764
- diameter / 2,
32765
- diameter / 2,
32766
- segments,
32767
- true
32768
- );
32769
- return drill.translate([x, y, 0]);
32770
- }
32771
- function createPlatedHoleDrill({
32772
- Manifold,
32773
- x,
32774
- y,
32775
- holeDiameter,
32776
- thickness,
32777
- zOffset = 1e-3,
32778
- segments = 32
32779
- }) {
32780
- const boardHoleRadius = holeDiameter / 2 + zOffset;
32781
- const drill = Manifold.cylinder(
32782
- thickness * 1.2,
32783
- boardHoleRadius,
32784
- boardHoleRadius,
32785
- segments,
32786
- true
32787
- );
32788
- return drill.translate([x, y, 0]);
32607
+ const covered = pour.covered_with_solder_mask !== false;
32608
+ const pourColorArr = covered ? tracesMaterialColors[boardMaterial] ?? colors.fr4GreenSolderWithMask : colors.copper;
32609
+ const pourColor = new THREE19.Color(...pourColorArr);
32610
+ const threeGeom = manifoldMeshToThreeGeometry(pourOp.getMesh());
32611
+ copperPourGeoms.push({
32612
+ key: `coppour-${pour.pcb_copper_pour_id}`,
32613
+ geometry: threeGeom,
32614
+ color: pourColor
32615
+ });
32616
+ }
32617
+ }
32618
+ return { copperPourGeoms };
32789
32619
  }
32790
32620
 
32621
+ // src/utils/manifold/process-cutouts.ts
32622
+ import { su as su7 } from "@tscircuit/circuit-json-util";
32623
+
32791
32624
  // src/utils/pad-geoms.ts
32792
32625
  var RECT_PAD_SEGMENTS2 = 64;
32793
32626
  function createRoundedRectPrism({
@@ -32863,6 +32696,156 @@ function createPadManifoldOp({
32863
32696
  return null;
32864
32697
  }
32865
32698
 
32699
+ // src/utils/manifold/process-cutouts.ts
32700
+ var arePointsClockwise5 = (points) => {
32701
+ let area = 0;
32702
+ for (let i = 0; i < points.length; i++) {
32703
+ const j = (i + 1) % points.length;
32704
+ if (points[i] && points[j]) {
32705
+ area += points[i][0] * points[j][1];
32706
+ area -= points[j][0] * points[i][1];
32707
+ }
32708
+ }
32709
+ const signedArea = area / 2;
32710
+ return signedArea <= 0;
32711
+ };
32712
+ function processCutoutsForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup) {
32713
+ const cutoutOps = [];
32714
+ const pcbCutouts = su7(circuitJson).pcb_cutout.list();
32715
+ for (const cutout of pcbCutouts) {
32716
+ let cutoutOp;
32717
+ const cutoutHeight = pcbThickness * 1.5;
32718
+ switch (cutout.shape) {
32719
+ case "rect": {
32720
+ const rectCornerRadius = extractRectBorderRadius(cutout);
32721
+ if (typeof rectCornerRadius === "number" && rectCornerRadius > 0) {
32722
+ cutoutOp = createRoundedRectPrism({
32723
+ Manifold,
32724
+ width: cutout.width,
32725
+ height: cutout.height,
32726
+ thickness: cutoutHeight,
32727
+ borderRadius: rectCornerRadius
32728
+ });
32729
+ } else {
32730
+ cutoutOp = Manifold.cube(
32731
+ [cutout.width, cutout.height, cutoutHeight],
32732
+ true
32733
+ // centered
32734
+ );
32735
+ }
32736
+ manifoldInstancesForCleanup.push(cutoutOp);
32737
+ if (cutout.rotation) {
32738
+ const rotatedOp = cutoutOp.rotate([0, 0, cutout.rotation]);
32739
+ manifoldInstancesForCleanup.push(rotatedOp);
32740
+ cutoutOp = rotatedOp;
32741
+ }
32742
+ cutoutOp = cutoutOp.translate([
32743
+ cutout.center.x,
32744
+ cutout.center.y,
32745
+ 0
32746
+ // Centered vertically by Manifold.cube, so Z is 0 for board plane
32747
+ ]);
32748
+ manifoldInstancesForCleanup.push(cutoutOp);
32749
+ break;
32750
+ }
32751
+ case "circle":
32752
+ cutoutOp = Manifold.cylinder(
32753
+ cutoutHeight,
32754
+ cutout.radius,
32755
+ -1,
32756
+ // default for radiusHigh
32757
+ SMOOTH_CIRCLE_SEGMENTS,
32758
+ true
32759
+ // centered
32760
+ );
32761
+ manifoldInstancesForCleanup.push(cutoutOp);
32762
+ cutoutOp = cutoutOp.translate([cutout.center.x, cutout.center.y, 0]);
32763
+ manifoldInstancesForCleanup.push(cutoutOp);
32764
+ break;
32765
+ case "polygon":
32766
+ if (cutout.points.length < 3) {
32767
+ console.warn(
32768
+ `PCB Cutout [${cutout.pcb_cutout_id}] polygon has fewer than 3 points, skipping.`
32769
+ );
32770
+ continue;
32771
+ }
32772
+ let pointsVec2 = cutout.points.map((p) => [
32773
+ p.x,
32774
+ p.y
32775
+ ]);
32776
+ if (arePointsClockwise5(pointsVec2)) {
32777
+ pointsVec2 = pointsVec2.reverse();
32778
+ }
32779
+ const crossSection = CrossSection.ofPolygons([pointsVec2]);
32780
+ manifoldInstancesForCleanup.push(crossSection);
32781
+ cutoutOp = Manifold.extrude(
32782
+ crossSection,
32783
+ cutoutHeight,
32784
+ 0,
32785
+ // nDivisions
32786
+ 0,
32787
+ // twistDegrees
32788
+ [1, 1],
32789
+ // scaleTop
32790
+ true
32791
+ // center extrusion
32792
+ );
32793
+ manifoldInstancesForCleanup.push(cutoutOp);
32794
+ break;
32795
+ default:
32796
+ console.warn(
32797
+ `Unsupported cutout shape: ${cutout.shape} for cutout ${cutout.pcb_cutout_id}`
32798
+ );
32799
+ continue;
32800
+ }
32801
+ if (cutoutOp) {
32802
+ cutoutOps.push(cutoutOp);
32803
+ }
32804
+ }
32805
+ return { cutoutOps };
32806
+ }
32807
+
32808
+ // src/utils/manifold/process-non-plated-holes.ts
32809
+ import { su as su8 } from "@tscircuit/circuit-json-util";
32810
+
32811
+ // src/utils/hole-geoms.ts
32812
+ function createCircleHoleDrill({
32813
+ Manifold,
32814
+ x,
32815
+ y,
32816
+ diameter,
32817
+ thickness,
32818
+ segments = 32
32819
+ }) {
32820
+ const drill = Manifold.cylinder(
32821
+ thickness * 1.2,
32822
+ diameter / 2,
32823
+ diameter / 2,
32824
+ segments,
32825
+ true
32826
+ );
32827
+ return drill.translate([x, y, 0]);
32828
+ }
32829
+ function createPlatedHoleDrill({
32830
+ Manifold,
32831
+ x,
32832
+ y,
32833
+ holeDiameter,
32834
+ thickness,
32835
+ zOffset = 1e-3,
32836
+ segments = 32
32837
+ }) {
32838
+ const boardHoleRadius = holeDiameter / 2 + zOffset;
32839
+ const drill = Manifold.cylinder(
32840
+ thickness * 1.2,
32841
+ boardHoleRadius,
32842
+ boardHoleRadius,
32843
+ segments,
32844
+ true
32845
+ );
32846
+ return drill.translate([x, y, 0]);
32847
+ }
32848
+
32866
32849
  // src/utils/manifold/process-non-plated-holes.ts
32867
32850
  function isCircleHole(hole) {
32868
32851
  return (hole.shape === "circle" || hole.hole_shape === "circle") && typeof hole.hole_diameter === "number";
@@ -32875,7 +32858,7 @@ function isRotatedPillHole(hole) {
32875
32858
  }
32876
32859
  function processNonPlatedHolesForManifold(Manifold, circuitJson, pcbThickness, manifoldInstancesForCleanup) {
32877
32860
  const nonPlatedHoleBoardDrills = [];
32878
- const pcbHoles = su9(circuitJson).pcb_hole.list();
32861
+ const pcbHoles = su8(circuitJson).pcb_hole.list();
32879
32862
  const createPillOp = (width10, height10, depth) => {
32880
32863
  const pillOp = createRoundedRectPrism({
32881
32864
  Manifold,
@@ -32924,9 +32907,9 @@ function processNonPlatedHolesForManifold(Manifold, circuitJson, pcbThickness, m
32924
32907
  }
32925
32908
 
32926
32909
  // src/utils/manifold/process-plated-holes.ts
32927
- import { su as su10 } from "@tscircuit/circuit-json-util";
32928
- import * as THREE21 from "three";
32929
- var arePointsClockwise3 = (points) => {
32910
+ import { su as su9 } from "@tscircuit/circuit-json-util";
32911
+ import * as THREE20 from "three";
32912
+ var arePointsClockwise6 = (points) => {
32930
32913
  let area = 0;
32931
32914
  for (let i = 0; i < points.length; i++) {
32932
32915
  const j = (i + 1) % points.length;
@@ -32946,11 +32929,11 @@ var createEllipsePoints = (width10, height10, segments) => {
32946
32929
  }
32947
32930
  return points;
32948
32931
  };
32949
- var COPPER_COLOR = new THREE21.Color(...colors.copper);
32932
+ var COPPER_COLOR = new THREE20.Color(...colors.copper);
32950
32933
  var PLATED_HOLE_LIP_HEIGHT = 0.05;
32951
32934
  function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup, boardClipVolume) {
32952
32935
  const platedHoleBoardDrills = [];
32953
- const pcbPlatedHoles = su10(circuitJson).pcb_plated_hole.list();
32936
+ const pcbPlatedHoles = su9(circuitJson).pcb_plated_hole.list();
32954
32937
  const platedHoleCopperGeoms = [];
32955
32938
  const platedHoleCopperOpsForSubtract = [];
32956
32939
  const createPillOp = (width10, height10, depth) => {
@@ -32973,7 +32956,7 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
32973
32956
  point2.x,
32974
32957
  point2.y
32975
32958
  ]);
32976
- if (arePointsClockwise3(points)) {
32959
+ if (arePointsClockwise6(points)) {
32977
32960
  points = points.reverse();
32978
32961
  }
32979
32962
  const crossSection = CrossSection.ofPolygons([points]);
@@ -33018,7 +33001,7 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
33018
33001
  const height10 = Math.max(baseHeight + sizeDelta, M);
33019
33002
  if (holeShape === "oval") {
33020
33003
  let points = createEllipsePoints(width10, height10, SMOOTH_CIRCLE_SEGMENTS);
33021
- if (arePointsClockwise3(points)) {
33004
+ if (arePointsClockwise6(points)) {
33022
33005
  points = points.reverse();
33023
33006
  }
33024
33007
  const crossSection = CrossSection.ofPolygons([points]);
@@ -33333,7 +33316,7 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
33333
33316
  drillH,
33334
33317
  SMOOTH_CIRCLE_SEGMENTS
33335
33318
  );
33336
- if (arePointsClockwise3(boardDrillPoints)) {
33319
+ if (arePointsClockwise6(boardDrillPoints)) {
33337
33320
  boardDrillPoints = boardDrillPoints.reverse();
33338
33321
  }
33339
33322
  const boardDrillCrossSection = CrossSection.ofPolygons([boardDrillPoints]);
@@ -33361,7 +33344,7 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
33361
33344
  outerH,
33362
33345
  SMOOTH_CIRCLE_SEGMENTS
33363
33346
  );
33364
- if (arePointsClockwise3(outerPoints)) {
33347
+ if (arePointsClockwise6(outerPoints)) {
33365
33348
  outerPoints = outerPoints.reverse();
33366
33349
  }
33367
33350
  const outerCrossSection = CrossSection.ofPolygons([outerPoints]);
@@ -33380,7 +33363,7 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
33380
33363
  holeH,
33381
33364
  SMOOTH_CIRCLE_SEGMENTS
33382
33365
  );
33383
- if (arePointsClockwise3(innerPoints)) {
33366
+ if (arePointsClockwise6(innerPoints)) {
33384
33367
  innerPoints = innerPoints.reverse();
33385
33368
  }
33386
33369
  const innerCrossSection = CrossSection.ofPolygons([innerPoints]);
@@ -33518,6 +33501,46 @@ function processPlatedHolesForManifold(Manifold, CrossSection, circuitJson, pcbT
33518
33501
  return { platedHoleBoardDrills, platedHoleCopperGeoms, platedHoleSubtractOp };
33519
33502
  }
33520
33503
 
33504
+ // src/utils/manifold/process-smt-pads.ts
33505
+ import { su as su10 } from "@tscircuit/circuit-json-util";
33506
+ import * as THREE21 from "three";
33507
+ var COPPER_COLOR2 = new THREE21.Color(...colors.copper);
33508
+ function processSmtPadsForManifold(Manifold, circuitJson, pcbThickness, manifoldInstancesForCleanup, holeUnion, boardClipVolume) {
33509
+ const smtPadGeoms = [];
33510
+ const smtPads = su10(circuitJson).pcb_smtpad.list();
33511
+ smtPads.forEach((pad2, index2) => {
33512
+ const padBaseThickness = DEFAULT_SMT_PAD_THICKNESS;
33513
+ const zPos = pad2.layer === "bottom" ? -pcbThickness / 2 - BOARD_SURFACE_OFFSET.copper : pcbThickness / 2 + BOARD_SURFACE_OFFSET.copper;
33514
+ let padManifoldOp = createPadManifoldOp({
33515
+ Manifold,
33516
+ pad: pad2,
33517
+ padBaseThickness
33518
+ });
33519
+ if (padManifoldOp) {
33520
+ manifoldInstancesForCleanup.push(padManifoldOp);
33521
+ const translatedPad = padManifoldOp.translate([pad2.x, pad2.y, zPos]);
33522
+ manifoldInstancesForCleanup.push(translatedPad);
33523
+ let finalPadOp = translatedPad;
33524
+ if (holeUnion) {
33525
+ finalPadOp = translatedPad.subtract(holeUnion);
33526
+ manifoldInstancesForCleanup.push(finalPadOp);
33527
+ }
33528
+ if (boardClipVolume) {
33529
+ const clipped = Manifold.intersection([finalPadOp, boardClipVolume]);
33530
+ manifoldInstancesForCleanup.push(clipped);
33531
+ finalPadOp = clipped;
33532
+ }
33533
+ const threeGeom = manifoldMeshToThreeGeometry(finalPadOp.getMesh());
33534
+ smtPadGeoms.push({
33535
+ key: `smt_pad-${pad2.layer || "top"}-${pad2.pcb_smtpad_id || index2}`,
33536
+ geometry: threeGeom,
33537
+ color: COPPER_COLOR2
33538
+ });
33539
+ }
33540
+ });
33541
+ return { smtPadGeoms };
33542
+ }
33543
+
33521
33544
  // src/utils/manifold/process-vias.ts
33522
33545
  import { su as su11 } from "@tscircuit/circuit-json-util";
33523
33546
  import * as THREE22 from "three";
@@ -33573,7 +33596,7 @@ function createViaCopper({
33573
33596
  }
33574
33597
 
33575
33598
  // src/utils/manifold/process-vias.ts
33576
- var COPPER_COLOR2 = new THREE22.Color(...colors.copper);
33599
+ var COPPER_COLOR3 = new THREE22.Color(...colors.copper);
33577
33600
  function processViasForManifold(Manifold, circuitJson, pcbThickness, manifoldInstancesForCleanup, boardClipVolume) {
33578
33601
  const viaBoardDrills = [];
33579
33602
  const pcbVias = su11(circuitJson).pcb_via.list();
@@ -33613,396 +33636,566 @@ function processViasForManifold(Manifold, circuitJson, pcbThickness, manifoldIns
33613
33636
  manifoldInstancesForCleanup.push(clipped);
33614
33637
  finalCopperOp = clipped;
33615
33638
  }
33616
- const threeGeom = manifoldMeshToThreeGeometry(finalCopperOp.getMesh());
33617
- viaCopperGeoms.push({
33618
- key: `via-${via.pcb_via_id || index2}`,
33619
- geometry: threeGeom,
33620
- color: COPPER_COLOR2
33621
- });
33639
+ const threeGeom = manifoldMeshToThreeGeometry(finalCopperOp.getMesh());
33640
+ viaCopperGeoms.push({
33641
+ key: `via-${via.pcb_via_id || index2}`,
33642
+ geometry: threeGeom,
33643
+ color: COPPER_COLOR3
33644
+ });
33645
+ }
33646
+ });
33647
+ return { viaBoardDrills, viaCopperGeoms };
33648
+ }
33649
+
33650
+ // src/utils/silkscreen-texture.ts
33651
+ var import_text2 = __toESM(require_text(), 1);
33652
+ import * as THREE23 from "three";
33653
+ import { su as su12 } from "@tscircuit/circuit-json-util";
33654
+ function createSilkscreenTextureForLayer({
33655
+ layer,
33656
+ circuitJson,
33657
+ boardData,
33658
+ silkscreenColor = "rgb(255,255,255)",
33659
+ traceTextureResolution
33660
+ }) {
33661
+ const pcbSilkscreenTexts = su12(circuitJson).pcb_silkscreen_text.list();
33662
+ const pcbSilkscreenPaths = su12(circuitJson).pcb_silkscreen_path.list();
33663
+ const pcbSilkscreenLines = su12(circuitJson).pcb_silkscreen_line.list();
33664
+ const pcbSilkscreenRects = su12(circuitJson).pcb_silkscreen_rect.list();
33665
+ const pcbSilkscreenCircles = su12(circuitJson).pcb_silkscreen_circle.list();
33666
+ const textsOnLayer = pcbSilkscreenTexts.filter((t) => t.layer === layer);
33667
+ const pathsOnLayer = pcbSilkscreenPaths.filter((p) => p.layer === layer);
33668
+ const linesOnLayer = pcbSilkscreenLines.filter((l) => l.layer === layer);
33669
+ const rectsOnLayer = pcbSilkscreenRects.filter((r) => r.layer === layer);
33670
+ const circlesOnLayer = pcbSilkscreenCircles.filter((c) => c.layer === layer);
33671
+ if (textsOnLayer.length === 0 && pathsOnLayer.length === 0 && linesOnLayer.length === 0 && rectsOnLayer.length === 0 && circlesOnLayer.length === 0) {
33672
+ return null;
33673
+ }
33674
+ const canvas = document.createElement("canvas");
33675
+ const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
33676
+ const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
33677
+ canvas.width = canvasWidth;
33678
+ canvas.height = canvasHeight;
33679
+ const ctx = canvas.getContext("2d");
33680
+ if (!ctx) return null;
33681
+ if (layer === "bottom") {
33682
+ ctx.translate(0, canvasHeight);
33683
+ ctx.scale(1, -1);
33684
+ }
33685
+ ctx.strokeStyle = silkscreenColor;
33686
+ ctx.fillStyle = silkscreenColor;
33687
+ const canvasXFromPcb = (pcbX) => (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
33688
+ const canvasYFromPcb = (pcbY) => (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
33689
+ linesOnLayer.forEach((lineEl) => {
33690
+ const startXmm = parseDimensionToMm(lineEl.x1) ?? 0;
33691
+ const startYmm = parseDimensionToMm(lineEl.y1) ?? 0;
33692
+ const endXmm = parseDimensionToMm(lineEl.x2) ?? 0;
33693
+ const endYmm = parseDimensionToMm(lineEl.y2) ?? 0;
33694
+ if (startXmm === endXmm && startYmm === endYmm) return;
33695
+ ctx.beginPath();
33696
+ ctx.lineWidth = coerceDimensionToMm(lineEl.stroke_width, 0.1) * traceTextureResolution;
33697
+ ctx.lineCap = "round";
33698
+ ctx.moveTo(canvasXFromPcb(startXmm), canvasYFromPcb(startYmm));
33699
+ ctx.lineTo(canvasXFromPcb(endXmm), canvasYFromPcb(endYmm));
33700
+ ctx.stroke();
33701
+ });
33702
+ pathsOnLayer.forEach((path) => {
33703
+ if (path.route.length < 2) return;
33704
+ ctx.beginPath();
33705
+ ctx.lineWidth = coerceDimensionToMm(path.stroke_width, 0.1) * traceTextureResolution;
33706
+ ctx.lineCap = "round";
33707
+ ctx.lineJoin = "round";
33708
+ path.route.forEach((point2, index2) => {
33709
+ const canvasX = canvasXFromPcb(parseDimensionToMm(point2.x) ?? 0);
33710
+ const canvasY = canvasYFromPcb(parseDimensionToMm(point2.y) ?? 0);
33711
+ if (index2 === 0) ctx.moveTo(canvasX, canvasY);
33712
+ else ctx.lineTo(canvasX, canvasY);
33713
+ });
33714
+ ctx.stroke();
33715
+ });
33716
+ circlesOnLayer.forEach((circleEl) => {
33717
+ const radius = coerceDimensionToMm(circleEl.radius, 0);
33718
+ if (radius <= 0) return;
33719
+ const strokeWidth = coerceDimensionToMm(circleEl.stroke_width, 0.12);
33720
+ const hasStroke = strokeWidth > 0;
33721
+ const centerXmm = parseDimensionToMm(circleEl.center?.x) ?? 0;
33722
+ const centerYmm = parseDimensionToMm(circleEl.center?.y) ?? 0;
33723
+ const canvasCenterX = canvasXFromPcb(centerXmm);
33724
+ const canvasCenterY = canvasYFromPcb(centerYmm);
33725
+ const radiusPx = radius * traceTextureResolution;
33726
+ ctx.save();
33727
+ ctx.translate(canvasCenterX, canvasCenterY);
33728
+ if (hasStroke) {
33729
+ const outerRadiusPx = radiusPx + strokeWidth / 2 * traceTextureResolution;
33730
+ const innerRadiusPx = Math.max(
33731
+ 0,
33732
+ radiusPx - strokeWidth / 2 * traceTextureResolution
33733
+ );
33734
+ if (innerRadiusPx > 0) {
33735
+ ctx.beginPath();
33736
+ ctx.arc(0, 0, outerRadiusPx, 0, 2 * Math.PI);
33737
+ ctx.arc(0, 0, innerRadiusPx, 0, 2 * Math.PI, true);
33738
+ ctx.fill("evenodd");
33739
+ } else {
33740
+ ctx.beginPath();
33741
+ ctx.arc(0, 0, outerRadiusPx, 0, 2 * Math.PI);
33742
+ ctx.fill();
33743
+ }
33744
+ } else {
33745
+ ctx.beginPath();
33746
+ ctx.arc(0, 0, radiusPx, 0, 2 * Math.PI);
33747
+ ctx.fill();
33622
33748
  }
33749
+ ctx.restore();
33623
33750
  });
33624
- return { viaBoardDrills, viaCopperGeoms };
33625
- }
33626
-
33627
- // src/utils/manifold/process-smt-pads.ts
33628
- import { su as su12 } from "@tscircuit/circuit-json-util";
33629
- import * as THREE23 from "three";
33630
- var COPPER_COLOR3 = new THREE23.Color(...colors.copper);
33631
- function processSmtPadsForManifold(Manifold, circuitJson, pcbThickness, manifoldInstancesForCleanup, holeUnion, boardClipVolume) {
33632
- const smtPadGeoms = [];
33633
- const smtPads = su12(circuitJson).pcb_smtpad.list();
33634
- smtPads.forEach((pad2, index2) => {
33635
- const padBaseThickness = DEFAULT_SMT_PAD_THICKNESS;
33636
- const zPos = pad2.layer === "bottom" ? -pcbThickness / 2 - BOARD_SURFACE_OFFSET.copper : pcbThickness / 2 + BOARD_SURFACE_OFFSET.copper;
33637
- let padManifoldOp = createPadManifoldOp({
33638
- Manifold,
33639
- pad: pad2,
33640
- padBaseThickness
33641
- });
33642
- if (padManifoldOp) {
33643
- manifoldInstancesForCleanup.push(padManifoldOp);
33644
- const translatedPad = padManifoldOp.translate([pad2.x, pad2.y, zPos]);
33645
- manifoldInstancesForCleanup.push(translatedPad);
33646
- let finalPadOp = translatedPad;
33647
- if (holeUnion) {
33648
- finalPadOp = translatedPad.subtract(holeUnion);
33649
- manifoldInstancesForCleanup.push(finalPadOp);
33751
+ rectsOnLayer.forEach((rect) => {
33752
+ const width10 = coerceDimensionToMm(rect.width, 0);
33753
+ const height10 = coerceDimensionToMm(rect.height, 0);
33754
+ if (width10 <= 0 || height10 <= 0) return;
33755
+ const centerXmm = parseDimensionToMm(rect.center?.x) ?? 0;
33756
+ const centerYmm = parseDimensionToMm(rect.center?.y) ?? 0;
33757
+ const canvasCenterX = canvasXFromPcb(centerXmm);
33758
+ const canvasCenterY = canvasYFromPcb(centerYmm);
33759
+ const rawRadius = extractRectBorderRadius(rect);
33760
+ const borderRadiusInput = typeof rawRadius === "string" ? parseDimensionToMm(rawRadius) : rawRadius;
33761
+ const borderRadiusMm = clampRectBorderRadius(
33762
+ width10,
33763
+ height10,
33764
+ borderRadiusInput
33765
+ );
33766
+ ctx.save();
33767
+ ctx.translate(canvasCenterX, canvasCenterY);
33768
+ const halfWidthPx = width10 / 2 * traceTextureResolution;
33769
+ const halfHeightPx = height10 / 2 * traceTextureResolution;
33770
+ const borderRadiusPx = Math.min(
33771
+ borderRadiusMm * traceTextureResolution,
33772
+ halfWidthPx,
33773
+ halfHeightPx
33774
+ );
33775
+ const hasStroke = rect.has_stroke ?? false;
33776
+ const isFilled = rect.is_filled ?? true;
33777
+ const isDashed = rect.is_stroke_dashed ?? false;
33778
+ const strokeWidthPx = hasStroke ? coerceDimensionToMm(rect.stroke_width, 0.1) * traceTextureResolution : 0;
33779
+ const drawRoundedRectPath = (x, y, rectWidth, rectHeight, radius) => {
33780
+ ctx.beginPath();
33781
+ if (radius <= 0) {
33782
+ ctx.rect(x, y, rectWidth, rectHeight);
33783
+ } else {
33784
+ const r = radius;
33785
+ const right = x + rectWidth;
33786
+ const bottom = y + rectHeight;
33787
+ ctx.moveTo(x + r, y);
33788
+ ctx.lineTo(right - r, y);
33789
+ ctx.quadraticCurveTo(right, y, right, y + r);
33790
+ ctx.lineTo(right, bottom - r);
33791
+ ctx.quadraticCurveTo(right, bottom, right - r, bottom);
33792
+ ctx.lineTo(x + r, bottom);
33793
+ ctx.quadraticCurveTo(x, bottom, x, bottom - r);
33794
+ ctx.lineTo(x, y + r);
33795
+ ctx.quadraticCurveTo(x, y, x + r, y);
33796
+ ctx.closePath();
33650
33797
  }
33651
- if (boardClipVolume) {
33652
- const clipped = Manifold.intersection([finalPadOp, boardClipVolume]);
33653
- manifoldInstancesForCleanup.push(clipped);
33654
- finalPadOp = clipped;
33798
+ };
33799
+ drawRoundedRectPath(
33800
+ -halfWidthPx,
33801
+ -halfHeightPx,
33802
+ halfWidthPx * 2,
33803
+ halfHeightPx * 2,
33804
+ borderRadiusPx
33805
+ );
33806
+ if (isFilled) {
33807
+ ctx.fill();
33808
+ }
33809
+ if (hasStroke && strokeWidthPx > 0) {
33810
+ ctx.lineWidth = strokeWidthPx;
33811
+ if (isDashed) {
33812
+ const dashLength = Math.max(strokeWidthPx * 2, 1);
33813
+ ctx.setLineDash([dashLength, dashLength]);
33814
+ }
33815
+ ctx.stroke();
33816
+ if (isDashed) {
33817
+ ctx.setLineDash([]);
33655
33818
  }
33656
- const threeGeom = manifoldMeshToThreeGeometry(finalPadOp.getMesh());
33657
- smtPadGeoms.push({
33658
- key: `smt_pad-${pad2.layer || "top"}-${pad2.pcb_smtpad_id || index2}`,
33659
- geometry: threeGeom,
33660
- color: COPPER_COLOR3
33661
- });
33662
33819
  }
33820
+ ctx.restore();
33663
33821
  });
33664
- return { smtPadGeoms };
33665
- }
33666
-
33667
- // src/utils/manifold/create-manifold-board.ts
33668
- var arePointsClockwise4 = (points) => {
33669
- let area = 0;
33670
- for (let i = 0; i < points.length; i++) {
33671
- const j = (i + 1) % points.length;
33672
- if (points[i] && points[j]) {
33673
- area += points[i][0] * points[j][1];
33674
- area -= points[j][0] * points[i][1];
33822
+ textsOnLayer.forEach((textS) => {
33823
+ const fontSize = textS.font_size || 0.25;
33824
+ const textStrokeWidth = Math.min(Math.max(0.01, fontSize * 0.1), fontSize * 0.05) * traceTextureResolution;
33825
+ ctx.lineWidth = textStrokeWidth;
33826
+ ctx.lineCap = "butt";
33827
+ ctx.lineJoin = "miter";
33828
+ const rawTextOutlines = (0, import_text2.vectorText)({
33829
+ height: fontSize * 0.45,
33830
+ input: textS.text
33831
+ });
33832
+ const processedTextOutlines = [];
33833
+ rawTextOutlines.forEach((outline) => {
33834
+ if (outline.length === 29) {
33835
+ processedTextOutlines.push(
33836
+ outline.slice(0, 15)
33837
+ );
33838
+ processedTextOutlines.push(
33839
+ outline.slice(14, 29)
33840
+ );
33841
+ } else if (outline.length === 17) {
33842
+ processedTextOutlines.push(
33843
+ outline.slice(0, 10)
33844
+ );
33845
+ processedTextOutlines.push(
33846
+ outline.slice(9, 17)
33847
+ );
33848
+ } else {
33849
+ processedTextOutlines.push(outline);
33850
+ }
33851
+ });
33852
+ const points = processedTextOutlines.flat();
33853
+ const textBounds = {
33854
+ minX: points.length > 0 ? Math.min(...points.map((p) => p[0])) : 0,
33855
+ maxX: points.length > 0 ? Math.max(...points.map((p) => p[0])) : 0,
33856
+ minY: points.length > 0 ? Math.min(...points.map((p) => p[1])) : 0,
33857
+ maxY: points.length > 0 ? Math.max(...points.map((p) => p[1])) : 0
33858
+ };
33859
+ const textCenterX = (textBounds.minX + textBounds.maxX) / 2;
33860
+ const textCenterY = (textBounds.minY + textBounds.maxY) / 2;
33861
+ let xOff = -textCenterX;
33862
+ let yOff = -textCenterY;
33863
+ const alignment = textS.anchor_alignment || "center";
33864
+ if (alignment.includes("left")) {
33865
+ xOff = -textBounds.minX;
33866
+ } else if (alignment.includes("right")) {
33867
+ xOff = -textBounds.maxX;
33675
33868
  }
33676
- }
33677
- const signedArea = area / 2;
33678
- return signedArea <= 0;
33679
- };
33680
- function createManifoldBoard(Manifold, CrossSection, boardData, pcbThickness, manifoldInstancesForCleanup) {
33681
- let boardOp;
33682
- let outlineCrossSection = null;
33683
- if (boardData.outline && boardData.outline.length >= 3) {
33684
- let outlineVec2 = boardData.outline.map((p) => [
33685
- p.x,
33686
- p.y
33687
- ]);
33688
- if (arePointsClockwise4(outlineVec2)) {
33689
- outlineVec2 = outlineVec2.reverse();
33869
+ if (alignment.includes("top")) {
33870
+ yOff = -textBounds.maxY;
33871
+ } else if (alignment.includes("bottom")) {
33872
+ yOff = -textBounds.minY;
33690
33873
  }
33691
- const crossSection = CrossSection.ofPolygons([outlineVec2]);
33692
- manifoldInstancesForCleanup.push(crossSection);
33693
- outlineCrossSection = crossSection;
33694
- boardOp = Manifold.extrude(
33695
- crossSection,
33696
- pcbThickness,
33697
- void 0,
33698
- // nDivisions
33699
- void 0,
33700
- // twistDegrees
33701
- void 0,
33702
- // scaleTop
33703
- true
33704
- // center (for Z-axis)
33705
- );
33706
- manifoldInstancesForCleanup.push(boardOp);
33707
- } else {
33708
- if (boardData.outline && boardData.outline.length > 0) {
33709
- console.warn(
33710
- "Board outline has fewer than 3 points, falling back to rectangular board."
33874
+ const transformMatrices = [];
33875
+ let rotationDeg = textS.ccw_rotation ?? 0;
33876
+ if (textS.layer === "bottom") {
33877
+ transformMatrices.push(
33878
+ translate4(textCenterX, textCenterY),
33879
+ { a: -1, b: 0, c: 0, d: 1, e: 0, f: 0 },
33880
+ translate4(-textCenterX, -textCenterY)
33711
33881
  );
33882
+ rotationDeg = -rotationDeg;
33712
33883
  }
33713
- boardOp = Manifold.cube(
33714
- [boardData.width, boardData.height, pcbThickness],
33715
- true
33716
- // center (for all axes)
33717
- );
33718
- manifoldInstancesForCleanup.push(boardOp);
33719
- boardOp = boardOp.translate([boardData.center.x, boardData.center.y, 0]);
33720
- manifoldInstancesForCleanup.push(boardOp);
33721
- }
33722
- return { boardOp, outlineCrossSection };
33884
+ if (rotationDeg) {
33885
+ const rad = rotationDeg * Math.PI / 180;
33886
+ transformMatrices.push(
33887
+ translate4(textCenterX, textCenterY),
33888
+ rotate2(rad),
33889
+ translate4(-textCenterX, -textCenterY)
33890
+ );
33891
+ }
33892
+ const finalTransformMatrix = transformMatrices.length > 0 ? compose(...transformMatrices) : void 0;
33893
+ processedTextOutlines.forEach((segment) => {
33894
+ ctx.beginPath();
33895
+ segment.forEach((p, index2) => {
33896
+ let transformedP = { x: p[0], y: p[1] };
33897
+ if (finalTransformMatrix) {
33898
+ transformedP = applyToPoint(finalTransformMatrix, transformedP);
33899
+ }
33900
+ const pcbX = transformedP.x + xOff + textS.anchor_position.x;
33901
+ const pcbY = transformedP.y + yOff + textS.anchor_position.y;
33902
+ const canvasX = canvasXFromPcb(pcbX);
33903
+ const canvasY = canvasYFromPcb(pcbY);
33904
+ if (index2 === 0) ctx.moveTo(canvasX, canvasY);
33905
+ else ctx.lineTo(canvasX, canvasY);
33906
+ });
33907
+ ctx.stroke();
33908
+ });
33909
+ });
33910
+ const texture = new THREE23.CanvasTexture(canvas);
33911
+ texture.generateMipmaps = true;
33912
+ texture.minFilter = THREE23.LinearMipmapLinearFilter;
33913
+ texture.magFilter = THREE23.LinearFilter;
33914
+ texture.anisotropy = 16;
33915
+ texture.needsUpdate = true;
33916
+ return texture;
33723
33917
  }
33724
33918
 
33725
- // src/utils/manifold/process-copper-pours.ts
33919
+ // src/utils/soldermask-texture.ts
33726
33920
  import * as THREE24 from "three";
33727
- var arePointsClockwise5 = (points) => {
33728
- let area = 0;
33729
- for (let i = 0; i < points.length; i++) {
33730
- const j = (i + 1) % points.length;
33731
- if (points[i] && points[j]) {
33732
- area += points[i][0] * points[j][1];
33733
- area -= points[j][0] * points[i][1];
33734
- }
33735
- }
33736
- const signedArea = area / 2;
33737
- return signedArea <= 0;
33738
- };
33739
- function segmentToPoints2(p1, p2, bulge, arcSegments) {
33740
- if (!bulge || Math.abs(bulge) < 1e-9) {
33741
- return [];
33742
- }
33743
- const theta = 4 * Math.atan(bulge);
33744
- const dx = p2[0] - p1[0];
33745
- const dy = p2[1] - p1[1];
33746
- const dist = Math.sqrt(dx * dx + dy * dy);
33747
- if (dist < 1e-9) return [];
33748
- const radius = Math.abs(dist / (2 * Math.sin(theta / 2)));
33749
- const m = Math.sqrt(Math.max(0, radius * radius - dist / 2 * (dist / 2)));
33750
- const midPoint = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
33751
- const ux = dx / dist;
33752
- const uy = dy / dist;
33753
- const nx = -uy;
33754
- const ny = ux;
33755
- const centerX = midPoint[0] + nx * m * Math.sign(bulge);
33756
- const centerY = midPoint[1] + ny * m * Math.sign(bulge);
33757
- const startAngle = Math.atan2(p1[1] - centerY, p1[0] - centerX);
33758
- const points = [];
33759
- const numSteps = Math.max(
33760
- 2,
33761
- Math.ceil(arcSegments * Math.abs(theta) / (Math.PI * 2) * 4)
33762
- );
33763
- const angleStep = theta / numSteps;
33764
- for (let i = 1; i < numSteps; i++) {
33765
- const angle = startAngle + angleStep * i;
33766
- points.push([
33767
- centerX + radius * Math.cos(angle),
33768
- centerY + radius * Math.sin(angle)
33769
- ]);
33921
+ import { su as su13 } from "@tscircuit/circuit-json-util";
33922
+ function createSoldermaskTextureForLayer({
33923
+ layer,
33924
+ circuitJson,
33925
+ boardData,
33926
+ soldermaskColor,
33927
+ traceTextureResolution
33928
+ }) {
33929
+ const canvas = document.createElement("canvas");
33930
+ const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
33931
+ const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
33932
+ canvas.width = canvasWidth;
33933
+ canvas.height = canvasHeight;
33934
+ const ctx = canvas.getContext("2d");
33935
+ if (!ctx) return null;
33936
+ if (layer === "bottom") {
33937
+ ctx.translate(0, canvasHeight);
33938
+ ctx.scale(1, -1);
33770
33939
  }
33771
- return points;
33772
- }
33773
- function ringToPoints2(ring2, arcSegments) {
33774
- const allPoints = [];
33775
- const vertices = ring2.vertices;
33776
- for (let i = 0; i < vertices.length; i++) {
33777
- const p1 = vertices[i];
33778
- const p2 = vertices[(i + 1) % vertices.length];
33779
- allPoints.push([p1.x, p1.y]);
33780
- if (p1.bulge) {
33781
- const arcPoints = segmentToPoints2(
33782
- [p1.x, p1.y],
33783
- [p2.x, p2.y],
33784
- p1.bulge,
33785
- arcSegments
33940
+ ctx.fillStyle = soldermaskColor;
33941
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
33942
+ const canvasXFromPcb = (pcbX) => (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
33943
+ const canvasYFromPcb = (pcbY) => (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
33944
+ ctx.globalCompositeOperation = "destination-out";
33945
+ ctx.fillStyle = "black";
33946
+ const pcbSmtPads = su13(circuitJson).pcb_smtpad.list();
33947
+ const smtPadsOnLayer = pcbSmtPads.filter((pad2) => pad2.layer === layer);
33948
+ smtPadsOnLayer.forEach((pad2) => {
33949
+ if (pad2.shape === "polygon" && pad2.points) {
33950
+ ctx.beginPath();
33951
+ pad2.points.forEach((point2, index2) => {
33952
+ const px = canvasXFromPcb(point2.x);
33953
+ const py = canvasYFromPcb(point2.y);
33954
+ if (index2 === 0) {
33955
+ ctx.moveTo(px, py);
33956
+ } else {
33957
+ ctx.lineTo(px, py);
33958
+ }
33959
+ });
33960
+ ctx.closePath();
33961
+ ctx.fill();
33962
+ return;
33963
+ }
33964
+ if (pad2.x === void 0 || pad2.y === void 0) return;
33965
+ const x = pad2.x;
33966
+ const y = pad2.y;
33967
+ const canvasX = canvasXFromPcb(x);
33968
+ const canvasY = canvasYFromPcb(y);
33969
+ if (pad2.shape === "rect") {
33970
+ const width10 = pad2.width * traceTextureResolution;
33971
+ const height10 = pad2.height * traceTextureResolution;
33972
+ ctx.fillRect(canvasX - width10 / 2, canvasY - height10 / 2, width10, height10);
33973
+ } else if (pad2.shape === "circle") {
33974
+ const radius = (pad2.radius ?? pad2.width / 2) * traceTextureResolution;
33975
+ ctx.beginPath();
33976
+ ctx.arc(canvasX, canvasY, radius, 0, 2 * Math.PI);
33977
+ ctx.fill();
33978
+ } else if (pad2.shape === "pill" || pad2.shape === "rotated_rect") {
33979
+ const width10 = pad2.width * traceTextureResolution;
33980
+ const height10 = pad2.height * traceTextureResolution;
33981
+ const radius = Math.min(width10, height10) / 2;
33982
+ ctx.beginPath();
33983
+ ctx.roundRect(
33984
+ canvasX - width10 / 2,
33985
+ canvasY - height10 / 2,
33986
+ width10,
33987
+ height10,
33988
+ radius
33786
33989
  );
33787
- allPoints.push(...arcPoints);
33990
+ ctx.fill();
33788
33991
  }
33789
- }
33790
- return allPoints;
33791
- }
33792
- function processCopperPoursForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup, boardMaterial, holeUnion, boardClipVolume) {
33793
- const copperPourGeoms = [];
33794
- const copperPours = circuitJson.filter(
33795
- (e) => e.type === "pcb_copper_pour"
33796
- );
33797
- for (const pour of copperPours) {
33798
- const pourThickness = DEFAULT_SMT_PAD_THICKNESS;
33799
- const layerSign = pour.layer === "bottom" ? -1 : 1;
33800
- const zPos = layerSign * (pcbThickness / 2 + pourThickness / 2 + MANIFOLD_Z_OFFSET);
33801
- let pourOp;
33802
- if (pour.shape === "rect") {
33803
- pourOp = Manifold.cube([pour.width, pour.height, pourThickness], true);
33804
- manifoldInstancesForCleanup.push(pourOp);
33805
- if (pour.rotation) {
33806
- const rotatedOp = pourOp.rotate([0, 0, pour.rotation]);
33807
- manifoldInstancesForCleanup.push(rotatedOp);
33808
- pourOp = rotatedOp;
33809
- }
33810
- pourOp = pourOp.translate([pour.center.x, pour.center.y, zPos]);
33811
- manifoldInstancesForCleanup.push(pourOp);
33812
- } else if (pour.shape === "polygon") {
33813
- if (pour.points.length < 3) continue;
33814
- let pointsVec2 = pour.points.map((p) => [
33815
- p.x,
33816
- p.y
33817
- ]);
33818
- if (arePointsClockwise5(pointsVec2)) {
33819
- pointsVec2 = pointsVec2.reverse();
33820
- }
33821
- const crossSection = CrossSection.ofPolygons([pointsVec2]);
33822
- manifoldInstancesForCleanup.push(crossSection);
33823
- pourOp = Manifold.extrude(
33824
- crossSection,
33825
- pourThickness,
33826
- 0,
33827
- // nDivisions
33828
- 0,
33829
- // twistDegrees
33830
- [1, 1],
33831
- // scaleTop
33832
- true
33833
- // center extrusion
33834
- ).translate([0, 0, zPos]);
33835
- manifoldInstancesForCleanup.push(pourOp);
33836
- } else if (pour.shape === "brep") {
33837
- const brepShape = pour.brep_shape;
33838
- if (!brepShape || !brepShape.outer_ring) continue;
33839
- let outerRingPoints = ringToPoints2(
33840
- brepShape.outer_ring,
33841
- SMOOTH_CIRCLE_SEGMENTS
33992
+ });
33993
+ const pcbVias = su13(circuitJson).pcb_via.list();
33994
+ pcbVias.forEach((via) => {
33995
+ const canvasX = canvasXFromPcb(via.x);
33996
+ const canvasY = canvasYFromPcb(via.y);
33997
+ const canvasRadius = via.outer_diameter / 2 * traceTextureResolution;
33998
+ ctx.beginPath();
33999
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI);
34000
+ ctx.fill();
34001
+ });
34002
+ const pcbPlatedHoles = su13(circuitJson).pcb_plated_hole.list();
34003
+ pcbPlatedHoles.forEach((hole) => {
34004
+ if (!hole.layers?.includes(layer)) return;
34005
+ const x = hole.x;
34006
+ const y = hole.y;
34007
+ const canvasX = canvasXFromPcb(x);
34008
+ const canvasY = canvasYFromPcb(y);
34009
+ if (hole.shape === "circle") {
34010
+ const outerDiameter = hole.outer_diameter;
34011
+ const canvasRadius = outerDiameter / 2 * traceTextureResolution;
34012
+ ctx.beginPath();
34013
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI);
34014
+ ctx.fill();
34015
+ } else if (hole.shape === "pill" || hole.shape === "oval") {
34016
+ const width10 = (hole.outer_width ?? hole.outer_diameter ?? hole.hole_width) * traceTextureResolution;
34017
+ const height10 = (hole.outer_height ?? hole.outer_diameter ?? hole.hole_height) * traceTextureResolution;
34018
+ const radius = Math.min(width10, height10) / 2;
34019
+ ctx.beginPath();
34020
+ ctx.roundRect(
34021
+ canvasX - width10 / 2,
34022
+ canvasY - height10 / 2,
34023
+ width10,
34024
+ height10,
34025
+ radius
33842
34026
  );
33843
- if (arePointsClockwise5(outerRingPoints)) {
33844
- outerRingPoints = outerRingPoints.reverse();
33845
- }
33846
- const polygons = [outerRingPoints];
33847
- if (brepShape.inner_rings) {
33848
- const innerRingsPoints = brepShape.inner_rings.map((ring2) => {
33849
- let points = ringToPoints2(ring2, SMOOTH_CIRCLE_SEGMENTS);
33850
- if (!arePointsClockwise5(points)) {
33851
- points = points.reverse();
33852
- }
33853
- return points;
33854
- });
33855
- polygons.push(...innerRingsPoints);
33856
- }
33857
- const crossSection = CrossSection.ofPolygons(polygons);
33858
- manifoldInstancesForCleanup.push(crossSection);
33859
- pourOp = Manifold.extrude(
33860
- crossSection,
33861
- pourThickness,
33862
- 0,
33863
- // nDivisions
33864
- 0,
33865
- // twistDegrees
33866
- [1, 1],
33867
- // scaleTop
33868
- true
33869
- // center extrusion
33870
- ).translate([0, 0, zPos]);
33871
- manifoldInstancesForCleanup.push(pourOp);
34027
+ ctx.fill();
33872
34028
  }
33873
- if (pourOp) {
33874
- if (holeUnion) {
33875
- const withHoles = pourOp.subtract(holeUnion);
33876
- manifoldInstancesForCleanup.push(withHoles);
33877
- pourOp = withHoles;
33878
- }
33879
- if (boardClipVolume) {
33880
- const clipped = Manifold.intersection([pourOp, boardClipVolume]);
33881
- manifoldInstancesForCleanup.push(clipped);
33882
- pourOp = clipped;
34029
+ });
34030
+ const pcbHoles = su13(circuitJson).pcb_hole.list();
34031
+ pcbHoles.forEach((hole) => {
34032
+ const x = hole.x;
34033
+ const y = hole.y;
34034
+ const canvasX = canvasXFromPcb(x);
34035
+ const canvasY = canvasYFromPcb(y);
34036
+ const holeShape = hole.hole_shape || hole.shape;
34037
+ if (holeShape === "circle" && typeof hole.hole_diameter === "number") {
34038
+ const canvasRadius = hole.hole_diameter / 2 * traceTextureResolution;
34039
+ ctx.beginPath();
34040
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI);
34041
+ ctx.fill();
34042
+ } else if (holeShape === "pill" && typeof hole.hole_width === "number" && typeof hole.hole_height === "number") {
34043
+ const width10 = hole.hole_width * traceTextureResolution;
34044
+ const height10 = hole.hole_height * traceTextureResolution;
34045
+ const radius = Math.min(width10, height10) / 2;
34046
+ ctx.beginPath();
34047
+ ctx.roundRect(
34048
+ canvasX - width10 / 2,
34049
+ canvasY - height10 / 2,
34050
+ width10,
34051
+ height10,
34052
+ radius
34053
+ );
34054
+ ctx.fill();
34055
+ } else if (holeShape === "rotated_pill" && typeof hole.hole_width === "number" && typeof hole.hole_height === "number") {
34056
+ const width10 = hole.hole_width * traceTextureResolution;
34057
+ const height10 = hole.hole_height * traceTextureResolution;
34058
+ const radius = Math.min(width10, height10) / 2;
34059
+ const rotation2 = (hole.ccw_rotation || 0) * (Math.PI / 180);
34060
+ ctx.save();
34061
+ ctx.translate(canvasX, canvasY);
34062
+ if (layer === "bottom") {
34063
+ ctx.rotate(-rotation2);
34064
+ } else {
34065
+ ctx.rotate(rotation2);
33883
34066
  }
33884
- const covered = pour.covered_with_solder_mask !== false;
33885
- const pourColorArr = covered ? tracesMaterialColors[boardMaterial] ?? colors.fr4GreenSolderWithMask : colors.copper;
33886
- const pourColor = new THREE24.Color(...pourColorArr);
33887
- const threeGeom = manifoldMeshToThreeGeometry(pourOp.getMesh());
33888
- copperPourGeoms.push({
33889
- key: `coppour-${pour.pcb_copper_pour_id}`,
33890
- geometry: threeGeom,
33891
- color: pourColor
34067
+ ctx.beginPath();
34068
+ ctx.roundRect(-width10 / 2, -height10 / 2, width10, height10, radius);
34069
+ ctx.fill();
34070
+ ctx.restore();
34071
+ }
34072
+ });
34073
+ const pcbCopperPours = su13(circuitJson).pcb_copper_pour.list();
34074
+ pcbCopperPours.forEach((pour) => {
34075
+ if (pour.layer !== layer) return;
34076
+ if (pour.covered_with_solder_mask !== false) return;
34077
+ if (pour.shape === "rect") {
34078
+ const centerX = canvasXFromPcb(pour.center.x);
34079
+ const centerY = canvasYFromPcb(pour.center.y);
34080
+ const width10 = pour.width * traceTextureResolution;
34081
+ const height10 = pour.height * traceTextureResolution;
34082
+ ctx.fillRect(centerX - width10 / 2, centerY - height10 / 2, width10, height10);
34083
+ } else if (pour.shape === "polygon" && pour.points) {
34084
+ ctx.beginPath();
34085
+ pour.points.forEach((point2, index2) => {
34086
+ const px = canvasXFromPcb(point2.x);
34087
+ const py = canvasYFromPcb(point2.y);
34088
+ if (index2 === 0) {
34089
+ ctx.moveTo(px, py);
34090
+ } else {
34091
+ ctx.lineTo(px, py);
34092
+ }
33892
34093
  });
34094
+ ctx.closePath();
34095
+ ctx.fill();
33893
34096
  }
33894
- }
33895
- return { copperPourGeoms };
34097
+ });
34098
+ ctx.globalCompositeOperation = "source-over";
34099
+ const texture = new THREE24.CanvasTexture(canvas);
34100
+ texture.generateMipmaps = true;
34101
+ texture.minFilter = THREE24.LinearMipmapLinearFilter;
34102
+ texture.magFilter = THREE24.LinearFilter;
34103
+ texture.anisotropy = 16;
34104
+ texture.needsUpdate = true;
34105
+ return texture;
33896
34106
  }
33897
34107
 
33898
- // src/utils/manifold/process-cutouts.ts
33899
- import { su as su13 } from "@tscircuit/circuit-json-util";
33900
- var arePointsClockwise6 = (points) => {
33901
- let area = 0;
33902
- for (let i = 0; i < points.length; i++) {
33903
- const j = (i + 1) % points.length;
33904
- if (points[i] && points[j]) {
33905
- area += points[i][0] * points[j][1];
33906
- area -= points[j][0] * points[i][1];
33907
- }
34108
+ // src/utils/trace-texture.ts
34109
+ import * as THREE25 from "three";
34110
+ import { su as su14 } from "@tscircuit/circuit-json-util";
34111
+ function isWireRoutePoint(point2) {
34112
+ return point2 && point2.route_type === "wire" && typeof point2.layer === "string" && typeof point2.width === "number";
34113
+ }
34114
+ function createTraceTextureForLayer({
34115
+ layer,
34116
+ circuitJson,
34117
+ boardData,
34118
+ traceColor,
34119
+ traceTextureResolution
34120
+ }) {
34121
+ const pcbTraces = su14(circuitJson).pcb_trace.list();
34122
+ const allPcbVias = su14(circuitJson).pcb_via.list();
34123
+ const allPcbPlatedHoles = su14(
34124
+ circuitJson
34125
+ ).pcb_plated_hole.list();
34126
+ const tracesOnLayer = pcbTraces.filter(
34127
+ (t) => t.route.some((p) => isWireRoutePoint(p) && p.layer === layer)
34128
+ );
34129
+ if (tracesOnLayer.length === 0) return null;
34130
+ const canvas = document.createElement("canvas");
34131
+ const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
34132
+ const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
34133
+ canvas.width = canvasWidth;
34134
+ canvas.height = canvasHeight;
34135
+ const ctx = canvas.getContext("2d");
34136
+ if (!ctx) return null;
34137
+ if (layer === "bottom") {
34138
+ ctx.translate(0, canvasHeight);
34139
+ ctx.scale(1, -1);
33908
34140
  }
33909
- const signedArea = area / 2;
33910
- return signedArea <= 0;
33911
- };
33912
- function processCutoutsForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup) {
33913
- const cutoutOps = [];
33914
- const pcbCutouts = su13(circuitJson).pcb_cutout.list();
33915
- for (const cutout of pcbCutouts) {
33916
- let cutoutOp;
33917
- const cutoutHeight = pcbThickness * 1.5;
33918
- switch (cutout.shape) {
33919
- case "rect": {
33920
- const rectCornerRadius = extractRectBorderRadius(cutout);
33921
- if (typeof rectCornerRadius === "number" && rectCornerRadius > 0) {
33922
- cutoutOp = createRoundedRectPrism({
33923
- Manifold,
33924
- width: cutout.width,
33925
- height: cutout.height,
33926
- thickness: cutoutHeight,
33927
- borderRadius: rectCornerRadius
33928
- });
33929
- } else {
33930
- cutoutOp = Manifold.cube(
33931
- [cutout.width, cutout.height, cutoutHeight],
33932
- true
33933
- // centered
33934
- );
33935
- }
33936
- manifoldInstancesForCleanup.push(cutoutOp);
33937
- if (cutout.rotation) {
33938
- const rotatedOp = cutoutOp.rotate([0, 0, cutout.rotation]);
33939
- manifoldInstancesForCleanup.push(rotatedOp);
33940
- cutoutOp = rotatedOp;
33941
- }
33942
- cutoutOp = cutoutOp.translate([
33943
- cutout.center.x,
33944
- cutout.center.y,
33945
- 0
33946
- // Centered vertically by Manifold.cube, so Z is 0 for board plane
33947
- ]);
33948
- manifoldInstancesForCleanup.push(cutoutOp);
33949
- break;
33950
- }
33951
- case "circle":
33952
- cutoutOp = Manifold.cylinder(
33953
- cutoutHeight,
33954
- cutout.radius,
33955
- -1,
33956
- // default for radiusHigh
33957
- SMOOTH_CIRCLE_SEGMENTS,
33958
- true
33959
- // centered
33960
- );
33961
- manifoldInstancesForCleanup.push(cutoutOp);
33962
- cutoutOp = cutoutOp.translate([cutout.center.x, cutout.center.y, 0]);
33963
- manifoldInstancesForCleanup.push(cutoutOp);
33964
- break;
33965
- case "polygon":
33966
- if (cutout.points.length < 3) {
33967
- console.warn(
33968
- `PCB Cutout [${cutout.pcb_cutout_id}] polygon has fewer than 3 points, skipping.`
33969
- );
33970
- continue;
33971
- }
33972
- let pointsVec2 = cutout.points.map((p) => [
33973
- p.x,
33974
- p.y
33975
- ]);
33976
- if (arePointsClockwise6(pointsVec2)) {
33977
- pointsVec2 = pointsVec2.reverse();
33978
- }
33979
- const crossSection = CrossSection.ofPolygons([pointsVec2]);
33980
- manifoldInstancesForCleanup.push(crossSection);
33981
- cutoutOp = Manifold.extrude(
33982
- crossSection,
33983
- cutoutHeight,
33984
- 0,
33985
- // nDivisions
33986
- 0,
33987
- // twistDegrees
33988
- [1, 1],
33989
- // scaleTop
33990
- true
33991
- // center extrusion
33992
- );
33993
- manifoldInstancesForCleanup.push(cutoutOp);
33994
- break;
33995
- default:
33996
- console.warn(
33997
- `Unsupported cutout shape: ${cutout.shape} for cutout ${cutout.pcb_cutout_id}`
33998
- );
34141
+ tracesOnLayer.forEach((trace) => {
34142
+ let firstPoint = true;
34143
+ ctx.beginPath();
34144
+ ctx.strokeStyle = traceColor;
34145
+ ctx.lineCap = "round";
34146
+ ctx.lineJoin = "round";
34147
+ let currentLineWidth = 0;
34148
+ for (const point2 of trace.route) {
34149
+ if (!isWireRoutePoint(point2) || point2.layer !== layer) {
34150
+ if (!firstPoint) ctx.stroke();
34151
+ firstPoint = true;
33999
34152
  continue;
34153
+ }
34154
+ const pcbX = point2.x;
34155
+ const pcbY = point2.y;
34156
+ currentLineWidth = point2.width * traceTextureResolution;
34157
+ ctx.lineWidth = currentLineWidth;
34158
+ const canvasX = (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34159
+ const canvasY = (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34160
+ if (firstPoint) {
34161
+ ctx.moveTo(canvasX, canvasY);
34162
+ firstPoint = false;
34163
+ } else {
34164
+ ctx.lineTo(canvasX, canvasY);
34165
+ }
34000
34166
  }
34001
- if (cutoutOp) {
34002
- cutoutOps.push(cutoutOp);
34167
+ if (!firstPoint) {
34168
+ ctx.stroke();
34003
34169
  }
34004
- }
34005
- return { cutoutOps };
34170
+ });
34171
+ ctx.globalCompositeOperation = "destination-out";
34172
+ ctx.fillStyle = "black";
34173
+ allPcbVias.forEach((via) => {
34174
+ const canvasX = (via.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34175
+ const canvasY = (-(via.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34176
+ const canvasRadius = via.outer_diameter / 2 * traceTextureResolution;
34177
+ ctx.beginPath();
34178
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
34179
+ ctx.fill();
34180
+ });
34181
+ allPcbPlatedHoles.forEach((ph) => {
34182
+ if (ph.layers.includes(layer) && ph.shape === "circle") {
34183
+ const canvasX = (ph.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34184
+ const canvasY = (-(ph.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34185
+ const canvasRadius = ph.outer_diameter / 2 * traceTextureResolution;
34186
+ ctx.beginPath();
34187
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
34188
+ ctx.fill();
34189
+ }
34190
+ });
34191
+ ctx.globalCompositeOperation = "source-over";
34192
+ const texture = new THREE25.CanvasTexture(canvas);
34193
+ texture.generateMipmaps = true;
34194
+ texture.minFilter = THREE25.LinearMipmapLinearFilter;
34195
+ texture.magFilter = THREE25.LinearFilter;
34196
+ texture.anisotropy = 16;
34197
+ texture.needsUpdate = true;
34198
+ return texture;
34006
34199
  }
34007
34200
 
34008
34201
  // src/hooks/useManifoldBoardBuilder.ts
@@ -34017,7 +34210,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34017
34210
  const panels = circuitJson.filter(
34018
34211
  (e) => e.type === "pcb_panel"
34019
34212
  );
34020
- const boards = su14(circuitJson).pcb_board.list();
34213
+ const boards = su15(circuitJson).pcb_board.list();
34021
34214
  if (panels.length > 0) {
34022
34215
  const panel = panels[0];
34023
34216
  return {
@@ -34036,7 +34229,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34036
34229
  return boardsNotInPanel.length > 0 ? boardsNotInPanel[0] : null;
34037
34230
  }, [circuitJson]);
34038
34231
  const isFauxBoard = useMemo19(() => {
34039
- const boards = su14(circuitJson).pcb_board.list();
34232
+ const boards = su15(circuitJson).pcb_board.list();
34040
34233
  return boards.length > 0 && boards[0].pcb_board_id === "faux-board";
34041
34234
  }, [circuitJson]);
34042
34235
  useEffect22(() => {
@@ -34171,7 +34364,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34171
34364
  {
34172
34365
  key: "plated-holes-union",
34173
34366
  geometry: cutPlatedGeom,
34174
- color: new THREE25.Color(
34367
+ color: new THREE26.Color(
34175
34368
  colors.copper[0],
34176
34369
  colors.copper[1],
34177
34370
  colors.copper[2]
@@ -34201,7 +34394,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34201
34394
  const matColorArray = boardMaterialColors[boardData.material] ?? colors.fr4Green;
34202
34395
  currentGeoms.board = {
34203
34396
  geometry: finalBoardGeom,
34204
- color: new THREE25.Color(
34397
+ color: new THREE26.Color(
34205
34398
  matColorArray[0],
34206
34399
  matColorArray[1],
34207
34400
  matColorArray[2]
@@ -34262,6 +34455,22 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34262
34455
  silkscreenColor,
34263
34456
  traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34264
34457
  });
34458
+ const soldermaskColorArr = tracesMaterialColors[boardData.material] ?? colors.fr4GreenSolderWithMask;
34459
+ const soldermaskColor = `rgb(${Math.round(soldermaskColorArr[0] * 255)}, ${Math.round(soldermaskColorArr[1] * 255)}, ${Math.round(soldermaskColorArr[2] * 255)})`;
34460
+ currentTextures.topSoldermask = createSoldermaskTextureForLayer({
34461
+ layer: "top",
34462
+ circuitJson,
34463
+ boardData,
34464
+ soldermaskColor,
34465
+ traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34466
+ });
34467
+ currentTextures.bottomSoldermask = createSoldermaskTextureForLayer({
34468
+ layer: "bottom",
34469
+ circuitJson,
34470
+ boardData,
34471
+ soldermaskColor,
34472
+ traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34473
+ });
34265
34474
  setTextures(currentTextures);
34266
34475
  } catch (e) {
34267
34476
  console.error("Error processing geometry with Manifold in hook:", e);
@@ -34290,11 +34499,11 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34290
34499
  };
34291
34500
 
34292
34501
  // src/utils/manifold/create-three-geometry-meshes.ts
34293
- import * as THREE27 from "three";
34502
+ import * as THREE28 from "three";
34294
34503
 
34295
34504
  // src/utils/create-board-material.ts
34296
- import * as THREE26 from "three";
34297
- var DEFAULT_SIDE = THREE26.DoubleSide;
34505
+ import * as THREE27 from "three";
34506
+ var DEFAULT_SIDE = THREE27.DoubleSide;
34298
34507
  var createBoardMaterial = ({
34299
34508
  material,
34300
34509
  color,
@@ -34302,7 +34511,7 @@ var createBoardMaterial = ({
34302
34511
  isFaux = false
34303
34512
  }) => {
34304
34513
  if (material === "fr4") {
34305
- return new THREE26.MeshPhysicalMaterial({
34514
+ return new THREE27.MeshPhysicalMaterial({
34306
34515
  color,
34307
34516
  side,
34308
34517
  metalness: 0,
@@ -34316,7 +34525,7 @@ var createBoardMaterial = ({
34316
34525
  flatShading: true
34317
34526
  });
34318
34527
  }
34319
- return new THREE26.MeshStandardMaterial({
34528
+ return new THREE27.MeshStandardMaterial({
34320
34529
  color,
34321
34530
  side,
34322
34531
  flatShading: true,
@@ -34332,12 +34541,12 @@ function createGeometryMeshes(geoms) {
34332
34541
  const meshes = [];
34333
34542
  if (!geoms) return meshes;
34334
34543
  if (geoms.board && geoms.board.geometry) {
34335
- const mesh = new THREE27.Mesh(
34544
+ const mesh = new THREE28.Mesh(
34336
34545
  geoms.board.geometry,
34337
34546
  createBoardMaterial({
34338
34547
  material: geoms.board.material,
34339
34548
  color: geoms.board.color,
34340
- side: THREE27.DoubleSide,
34549
+ side: THREE28.DoubleSide,
34341
34550
  isFaux: geoms.board.isFaux
34342
34551
  })
34343
34552
  );
@@ -34347,11 +34556,11 @@ function createGeometryMeshes(geoms) {
34347
34556
  const createMeshesFromArray = (geomArray) => {
34348
34557
  if (geomArray) {
34349
34558
  geomArray.forEach((comp) => {
34350
- const mesh = new THREE27.Mesh(
34559
+ const mesh = new THREE28.Mesh(
34351
34560
  comp.geometry,
34352
- new THREE27.MeshStandardMaterial({
34561
+ new THREE28.MeshStandardMaterial({
34353
34562
  color: comp.color,
34354
- side: THREE27.DoubleSide,
34563
+ side: THREE28.DoubleSide,
34355
34564
  flatShading: true,
34356
34565
  // Consistent with board
34357
34566
  polygonOffset: true,
@@ -34372,21 +34581,24 @@ function createGeometryMeshes(geoms) {
34372
34581
  }
34373
34582
 
34374
34583
  // src/utils/manifold/create-three-texture-meshes.ts
34375
- import * as THREE28 from "three";
34584
+ import * as THREE29 from "three";
34376
34585
  function createTextureMeshes(textures, boardData, pcbThickness) {
34377
34586
  const meshes = [];
34378
34587
  if (!textures || !boardData || pcbThickness === null) return meshes;
34379
- const createTexturePlane = (texture, yOffset, isBottomLayer, keySuffix) => {
34588
+ const createTexturePlane = (texture, yOffset, isBottomLayer, keySuffix, usePolygonOffset = false) => {
34380
34589
  if (!texture) return null;
34381
- const planeGeom = new THREE28.PlaneGeometry(boardData.width, boardData.height);
34382
- const material = new THREE28.MeshBasicMaterial({
34590
+ const planeGeom = new THREE29.PlaneGeometry(boardData.width, boardData.height);
34591
+ const material = new THREE29.MeshBasicMaterial({
34383
34592
  map: texture,
34384
34593
  transparent: true,
34385
- side: THREE28.DoubleSide,
34386
- depthWrite: false
34594
+ side: THREE29.DoubleSide,
34595
+ depthWrite: false,
34387
34596
  // Important for layers to avoid z-fighting issues with board itself
34597
+ polygonOffset: usePolygonOffset,
34598
+ polygonOffsetFactor: usePolygonOffset ? -1 : 0,
34599
+ polygonOffsetUnits: usePolygonOffset ? -1 : 0
34388
34600
  });
34389
- const mesh = new THREE28.Mesh(planeGeom, material);
34601
+ const mesh = new THREE29.Mesh(planeGeom, material);
34390
34602
  mesh.position.set(boardData.center.x, boardData.center.y, yOffset);
34391
34603
  if (isBottomLayer) {
34392
34604
  mesh.rotation.set(Math.PI, 0, 0);
@@ -34425,6 +34637,26 @@ function createTextureMeshes(textures, boardData, pcbThickness) {
34425
34637
  "silkscreen"
34426
34638
  );
34427
34639
  if (bottomSilkscreenMesh) meshes.push(bottomSilkscreenMesh);
34640
+ const topSoldermaskMesh = createTexturePlane(
34641
+ textures.topSoldermask,
34642
+ pcbThickness / 2 + 8e-4,
34643
+ // Just above board surface, below traces
34644
+ false,
34645
+ "soldermask",
34646
+ true
34647
+ // Enable polygon offset
34648
+ );
34649
+ if (topSoldermaskMesh) meshes.push(topSoldermaskMesh);
34650
+ const bottomSoldermaskMesh = createTexturePlane(
34651
+ textures.bottomSoldermask,
34652
+ -pcbThickness / 2 - 8e-4,
34653
+ // Just below board surface (bottom side)
34654
+ true,
34655
+ "soldermask",
34656
+ true
34657
+ // Enable polygon offset
34658
+ );
34659
+ if (bottomSoldermaskMesh) meshes.push(bottomSoldermaskMesh);
34428
34660
  return meshes;
34429
34661
  }
34430
34662
 
@@ -34469,6 +34701,10 @@ var BoardMeshes = ({
34469
34701
  shouldShow = visibility.topSilkscreen;
34470
34702
  } else if (mesh.name.includes("bottom-silkscreen")) {
34471
34703
  shouldShow = visibility.bottomSilkscreen;
34704
+ } else if (mesh.name.includes("top-soldermask")) {
34705
+ shouldShow = visibility.topMask;
34706
+ } else if (mesh.name.includes("bottom-soldermask")) {
34707
+ shouldShow = visibility.bottomMask;
34472
34708
  }
34473
34709
  if (shouldShow) {
34474
34710
  rootObject.add(mesh);
@@ -34581,7 +34817,7 @@ try {
34581
34817
  [textures, boardData, pcbThickness]
34582
34818
  );
34583
34819
  const cadComponents = useMemo20(
34584
- () => su15(circuitJson).cad_component.list(),
34820
+ () => su16(circuitJson).cad_component.list(),
34585
34821
  [circuitJson]
34586
34822
  );
34587
34823
  const boardDimensions = useMemo20(() => {
@@ -40696,6 +40932,48 @@ var AppearanceMenu = () => {
40696
40932
  ]
40697
40933
  }
40698
40934
  ),
40935
+ /* @__PURE__ */ jsxs8(
40936
+ Item22,
40937
+ {
40938
+ style: {
40939
+ ...itemStyles,
40940
+ backgroundColor: hoveredItem === "topMask" ? "#404040" : "transparent"
40941
+ },
40942
+ onSelect: (e) => e.preventDefault(),
40943
+ onPointerDown: (e) => {
40944
+ e.preventDefault();
40945
+ setLayerVisibility("topMask", !visibility.topMask);
40946
+ },
40947
+ onMouseEnter: () => setHoveredItem("topMask"),
40948
+ onMouseLeave: () => setHoveredItem(null),
40949
+ onTouchStart: () => setHoveredItem("topMask"),
40950
+ children: [
40951
+ /* @__PURE__ */ jsx34("span", { style: iconContainerStyles, children: visibility.topMask && /* @__PURE__ */ jsx34(CheckIcon, {}) }),
40952
+ /* @__PURE__ */ jsx34("span", { style: { display: "flex", alignItems: "center" }, children: "Top Soldermask" })
40953
+ ]
40954
+ }
40955
+ ),
40956
+ /* @__PURE__ */ jsxs8(
40957
+ Item22,
40958
+ {
40959
+ style: {
40960
+ ...itemStyles,
40961
+ backgroundColor: hoveredItem === "bottomMask" ? "#404040" : "transparent"
40962
+ },
40963
+ onSelect: (e) => e.preventDefault(),
40964
+ onPointerDown: (e) => {
40965
+ e.preventDefault();
40966
+ setLayerVisibility("bottomMask", !visibility.bottomMask);
40967
+ },
40968
+ onMouseEnter: () => setHoveredItem("bottomMask"),
40969
+ onMouseLeave: () => setHoveredItem(null),
40970
+ onTouchStart: () => setHoveredItem("bottomMask"),
40971
+ children: [
40972
+ /* @__PURE__ */ jsx34("span", { style: iconContainerStyles, children: visibility.bottomMask && /* @__PURE__ */ jsx34(CheckIcon, {}) }),
40973
+ /* @__PURE__ */ jsx34("span", { style: { display: "flex", alignItems: "center" }, children: "Bottom Soldermask" })
40974
+ ]
40975
+ }
40976
+ ),
40699
40977
  /* @__PURE__ */ jsxs8(
40700
40978
  Item22,
40701
40979
  {
@@ -41535,7 +41813,7 @@ var CadViewerInner = (props) => {
41535
41813
  );
41536
41814
  };
41537
41815
  var CadViewer = (props) => {
41538
- const defaultTarget = useMemo28(() => new THREE29.Vector3(0, 0, 0), []);
41816
+ const defaultTarget = useMemo28(() => new THREE30.Vector3(0, 0, 0), []);
41539
41817
  const initialCameraPosition = useMemo28(
41540
41818
  () => [5, -5, 5],
41541
41819
  []
@@ -41552,12 +41830,12 @@ var CadViewer = (props) => {
41552
41830
 
41553
41831
  // src/convert-circuit-json-to-3d-svg.ts
41554
41832
  var import_debug = __toESM(require_browser(), 1);
41555
- import { su as su16 } from "@tscircuit/circuit-json-util";
41556
- import * as THREE33 from "three";
41833
+ import { su as su17 } from "@tscircuit/circuit-json-util";
41834
+ import * as THREE34 from "three";
41557
41835
  import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
41558
41836
 
41559
41837
  // src/utils/create-geometry-from-polygons.ts
41560
- import * as THREE30 from "three";
41838
+ import * as THREE31 from "three";
41561
41839
  import { BufferGeometry as BufferGeometry3, Float32BufferAttribute as Float32BufferAttribute2 } from "three";
41562
41840
  function createGeometryFromPolygons(polygons) {
41563
41841
  const geometry = new BufferGeometry3();
@@ -41571,12 +41849,12 @@ function createGeometryFromPolygons(polygons) {
41571
41849
  ...polygon3.vertices[i + 1]
41572
41850
  // Third vertex
41573
41851
  );
41574
- const v1 = new THREE30.Vector3(...polygon3.vertices[0]);
41575
- const v2 = new THREE30.Vector3(...polygon3.vertices[i]);
41576
- const v3 = new THREE30.Vector3(...polygon3.vertices[i + 1]);
41577
- const normal = new THREE30.Vector3().crossVectors(
41578
- new THREE30.Vector3().subVectors(v2, v1),
41579
- new THREE30.Vector3().subVectors(v3, v1)
41852
+ const v1 = new THREE31.Vector3(...polygon3.vertices[0]);
41853
+ const v2 = new THREE31.Vector3(...polygon3.vertices[i]);
41854
+ const v3 = new THREE31.Vector3(...polygon3.vertices[i + 1]);
41855
+ const normal = new THREE31.Vector3().crossVectors(
41856
+ new THREE31.Vector3().subVectors(v2, v1),
41857
+ new THREE31.Vector3().subVectors(v3, v1)
41580
41858
  ).normalize();
41581
41859
  normals.push(
41582
41860
  normal.x,
@@ -41600,10 +41878,10 @@ function createGeometryFromPolygons(polygons) {
41600
41878
  var import_modeling2 = __toESM(require_src(), 1);
41601
41879
  var import_jscad_planner2 = __toESM(require_dist(), 1);
41602
41880
  var jscadModeling2 = __toESM(require_src(), 1);
41603
- import * as THREE32 from "three";
41881
+ import * as THREE33 from "three";
41604
41882
 
41605
41883
  // src/utils/load-model.ts
41606
- import * as THREE31 from "three";
41884
+ import * as THREE32 from "three";
41607
41885
  import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
41608
41886
  import { OBJLoader as OBJLoader2 } from "three/examples/jsm/loaders/OBJLoader.js";
41609
41887
  import { STLLoader as STLLoader2 } from "three/examples/jsm/loaders/STLLoader.js";
@@ -41611,12 +41889,12 @@ async function load3DModel(url) {
41611
41889
  if (url.endsWith(".stl")) {
41612
41890
  const loader = new STLLoader2();
41613
41891
  const geometry = await loader.loadAsync(url);
41614
- const material = new THREE31.MeshStandardMaterial({
41892
+ const material = new THREE32.MeshStandardMaterial({
41615
41893
  color: 8947848,
41616
41894
  metalness: 0.5,
41617
41895
  roughness: 0.5
41618
41896
  });
41619
- return new THREE31.Mesh(geometry, material);
41897
+ return new THREE32.Mesh(geometry, material);
41620
41898
  }
41621
41899
  if (url.endsWith(".obj")) {
41622
41900
  const loader = new OBJLoader2();
@@ -41649,9 +41927,9 @@ async function renderComponent(component, scene) {
41649
41927
  }
41650
41928
  if (component.rotation) {
41651
41929
  model.rotation.set(
41652
- THREE32.MathUtils.degToRad(component.rotation.x ?? 0),
41653
- THREE32.MathUtils.degToRad(component.rotation.y ?? 0),
41654
- THREE32.MathUtils.degToRad(component.rotation.z ?? 0)
41930
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
41931
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
41932
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41655
41933
  );
41656
41934
  }
41657
41935
  scene.add(model);
@@ -41665,13 +41943,13 @@ async function renderComponent(component, scene) {
41665
41943
  );
41666
41944
  if (jscadObject && (jscadObject.polygons || jscadObject.sides)) {
41667
41945
  const threeGeom = convertCSGToThreeGeom(jscadObject);
41668
- const material2 = new THREE32.MeshStandardMaterial({
41946
+ const material2 = new THREE33.MeshStandardMaterial({
41669
41947
  color: 8947848,
41670
41948
  metalness: 0.5,
41671
41949
  roughness: 0.5,
41672
- side: THREE32.DoubleSide
41950
+ side: THREE33.DoubleSide
41673
41951
  });
41674
- const mesh2 = new THREE32.Mesh(threeGeom, material2);
41952
+ const mesh2 = new THREE33.Mesh(threeGeom, material2);
41675
41953
  if (component.position) {
41676
41954
  mesh2.position.set(
41677
41955
  component.position.x ?? 0,
@@ -41681,9 +41959,9 @@ async function renderComponent(component, scene) {
41681
41959
  }
41682
41960
  if (component.rotation) {
41683
41961
  mesh2.rotation.set(
41684
- THREE32.MathUtils.degToRad(component.rotation.x ?? 0),
41685
- THREE32.MathUtils.degToRad(component.rotation.y ?? 0),
41686
- THREE32.MathUtils.degToRad(component.rotation.z ?? 0)
41962
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
41963
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
41964
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41687
41965
  );
41688
41966
  }
41689
41967
  scene.add(mesh2);
@@ -41700,17 +41978,17 @@ async function renderComponent(component, scene) {
41700
41978
  if (!geom || !geom.polygons && !geom.sides) {
41701
41979
  continue;
41702
41980
  }
41703
- const color = new THREE32.Color(geomInfo.color);
41981
+ const color = new THREE33.Color(geomInfo.color);
41704
41982
  color.convertLinearToSRGB();
41705
41983
  const geomWithColor = { ...geom, color: [color.r, color.g, color.b] };
41706
41984
  const threeGeom = convertCSGToThreeGeom(geomWithColor);
41707
- const material2 = new THREE32.MeshStandardMaterial({
41985
+ const material2 = new THREE33.MeshStandardMaterial({
41708
41986
  vertexColors: true,
41709
41987
  metalness: 0.2,
41710
41988
  roughness: 0.8,
41711
- side: THREE32.DoubleSide
41989
+ side: THREE33.DoubleSide
41712
41990
  });
41713
- const mesh2 = new THREE32.Mesh(threeGeom, material2);
41991
+ const mesh2 = new THREE33.Mesh(threeGeom, material2);
41714
41992
  if (component.position) {
41715
41993
  mesh2.position.set(
41716
41994
  component.position.x ?? 0,
@@ -41720,22 +41998,22 @@ async function renderComponent(component, scene) {
41720
41998
  }
41721
41999
  if (component.rotation) {
41722
42000
  mesh2.rotation.set(
41723
- THREE32.MathUtils.degToRad(component.rotation.x ?? 0),
41724
- THREE32.MathUtils.degToRad(component.rotation.y ?? 0),
41725
- THREE32.MathUtils.degToRad(component.rotation.z ?? 0)
42001
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
42002
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
42003
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41726
42004
  );
41727
42005
  }
41728
42006
  scene.add(mesh2);
41729
42007
  }
41730
42008
  return;
41731
42009
  }
41732
- const geometry = new THREE32.BoxGeometry(0.5, 0.5, 0.5);
41733
- const material = new THREE32.MeshStandardMaterial({
42010
+ const geometry = new THREE33.BoxGeometry(0.5, 0.5, 0.5);
42011
+ const material = new THREE33.MeshStandardMaterial({
41734
42012
  color: 16711680,
41735
42013
  transparent: true,
41736
42014
  opacity: 0.25
41737
42015
  });
41738
- const mesh = new THREE32.Mesh(geometry, material);
42016
+ const mesh = new THREE33.Mesh(geometry, material);
41739
42017
  if (component.position) {
41740
42018
  mesh.position.set(
41741
42019
  component.position.x ?? 0,
@@ -41756,11 +42034,11 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41756
42034
  padding = 20,
41757
42035
  zoom = 1.5
41758
42036
  } = options;
41759
- const scene = new THREE33.Scene();
42037
+ const scene = new THREE34.Scene();
41760
42038
  const renderer = new SVGRenderer();
41761
42039
  renderer.setSize(width10, height10);
41762
- renderer.setClearColor(new THREE33.Color(backgroundColor), 1);
41763
- const camera = new THREE33.OrthographicCamera();
42040
+ renderer.setClearColor(new THREE34.Color(backgroundColor), 1);
42041
+ const camera = new THREE34.OrthographicCamera();
41764
42042
  const aspect = width10 / height10;
41765
42043
  const frustumSize = 100;
41766
42044
  const halfFrustumSize = frustumSize / 2 / zoom;
@@ -41774,25 +42052,25 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41774
42052
  camera.position.set(position.x, position.y, position.z);
41775
42053
  camera.up.set(0, 1, 0);
41776
42054
  const lookAt = options.camera?.lookAt ?? { x: 0, y: 0, z: 0 };
41777
- camera.lookAt(new THREE33.Vector3(lookAt.x, lookAt.y, lookAt.z));
42055
+ camera.lookAt(new THREE34.Vector3(lookAt.x, lookAt.y, lookAt.z));
41778
42056
  camera.updateProjectionMatrix();
41779
- const ambientLight = new THREE33.AmbientLight(16777215, Math.PI / 2);
42057
+ const ambientLight = new THREE34.AmbientLight(16777215, Math.PI / 2);
41780
42058
  scene.add(ambientLight);
41781
- const pointLight = new THREE33.PointLight(16777215, Math.PI / 4);
42059
+ const pointLight = new THREE34.PointLight(16777215, Math.PI / 4);
41782
42060
  pointLight.position.set(-10, -10, 10);
41783
42061
  scene.add(pointLight);
41784
- const components = su16(circuitJson).cad_component.list();
42062
+ const components = su17(circuitJson).cad_component.list();
41785
42063
  for (const component of components) {
41786
42064
  await renderComponent(component, scene);
41787
42065
  }
41788
- const boardData = su16(circuitJson).pcb_board.list()[0];
42066
+ const boardData = su17(circuitJson).pcb_board.list()[0];
41789
42067
  const boardGeom = createBoardGeomFromCircuitJson(circuitJson);
41790
42068
  if (boardGeom) {
41791
42069
  for (const geom of boardGeom) {
41792
42070
  const g = geom;
41793
42071
  if (!g.polygons || g.polygons.length === 0) continue;
41794
42072
  const geometry = createGeometryFromPolygons(g.polygons);
41795
- const baseColor = new THREE33.Color(
42073
+ const baseColor = new THREE34.Color(
41796
42074
  g.color?.[0] ?? 0,
41797
42075
  g.color?.[1] ?? 0,
41798
42076
  g.color?.[2] ?? 0
@@ -41800,18 +42078,18 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41800
42078
  const material = createBoardMaterial({
41801
42079
  material: boardData?.material,
41802
42080
  color: baseColor,
41803
- side: THREE33.DoubleSide
42081
+ side: THREE34.DoubleSide
41804
42082
  });
41805
- const mesh = new THREE33.Mesh(geometry, material);
42083
+ const mesh = new THREE34.Mesh(geometry, material);
41806
42084
  scene.add(mesh);
41807
42085
  }
41808
42086
  }
41809
- const gridHelper = new THREE33.GridHelper(100, 100);
42087
+ const gridHelper = new THREE34.GridHelper(100, 100);
41810
42088
  gridHelper.rotation.x = Math.PI / 2;
41811
42089
  scene.add(gridHelper);
41812
- const box = new THREE33.Box3().setFromObject(scene);
41813
- const center = box.getCenter(new THREE33.Vector3());
41814
- const size5 = box.getSize(new THREE33.Vector3());
42090
+ const box = new THREE34.Box3().setFromObject(scene);
42091
+ const center = box.getCenter(new THREE34.Vector3());
42092
+ const size5 = box.getSize(new THREE34.Vector3());
41815
42093
  scene.position.sub(center);
41816
42094
  const maxDim = Math.max(size5.x, size5.y, size5.z);
41817
42095
  if (maxDim > 0) {