@tscircuit/3d-viewer 0.0.451 → 0.0.452

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.451",
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(
32562
- 0,
32563
- radiusPx - strokeWidth / 2 * traceTextureResolution
32564
- );
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();
32574
- }
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();
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;
32628
32532
  }
32629
- };
32630
- drawRoundedRectPath(
32631
- -halfWidthPx,
32632
- -halfHeightPx,
32633
- halfWidthPx * 2,
32634
- halfHeightPx * 2,
32635
- borderRadiusPx
32636
- );
32637
- if (isFilled) {
32638
- ctx.fill();
32639
- }
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]);
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();
32645
32543
  }
32646
- ctx.stroke();
32647
- if (isDashed) {
32648
- ctx.setLineDash([]);
32544
+ const crossSection = CrossSection.ofPolygons([pointsVec2]);
32545
+ manifoldInstancesForCleanup.push(crossSection);
32546
+ pourOp = Manifold.extrude(
32547
+ crossSection,
32548
+ pourThickness,
32549
+ 0,
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
32565
+ );
32566
+ if (arePointsClockwise4(outerRingPoints)) {
32567
+ outerRingPoints = outerRingPoints.reverse();
32649
32568
  }
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);
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);
32681
32579
  }
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
- );
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);
32722
32595
  }
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]);
32596
+ if (pourOp) {
32597
+ if (holeUnion) {
32598
+ const withHoles = pourOp.subtract(holeUnion);
32599
+ manifoldInstancesForCleanup.push(withHoles);
32600
+ pourOp = withHoles;
32601
+ }
32602
+ if (boardClipVolume) {
32603
+ const clipped = Manifold.intersection([pourOp, boardClipVolume]);
32604
+ manifoldInstancesForCleanup.push(clipped);
32605
+ pourOp = clipped;
32606
+ }
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,523 @@ 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
- });
33622
- }
33623
- });
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);
33650
- }
33651
- if (boardClipVolume) {
33652
- const clipped = Manifold.intersection([finalPadOp, boardClipVolume]);
33653
- manifoldInstancesForCleanup.push(clipped);
33654
- finalPadOp = clipped;
33655
- }
33656
- const threeGeom = manifoldMeshToThreeGeometry(finalPadOp.getMesh());
33657
- smtPadGeoms.push({
33658
- key: `smt_pad-${pad2.layer || "top"}-${pad2.pcb_smtpad_id || index2}`,
33639
+ const threeGeom = manifoldMeshToThreeGeometry(finalCopperOp.getMesh());
33640
+ viaCopperGeoms.push({
33641
+ key: `via-${via.pcb_via_id || index2}`,
33659
33642
  geometry: threeGeom,
33660
33643
  color: COPPER_COLOR3
33661
33644
  });
33662
33645
  }
33663
33646
  });
33664
- return { smtPadGeoms };
33647
+ return { viaBoardDrills, viaCopperGeoms };
33665
33648
  }
33666
33649
 
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];
33675
- }
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;
33676
33673
  }
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();
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();
33690
33748
  }
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)
33749
+ ctx.restore();
33750
+ });
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
33705
33765
  );
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."
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();
33797
+ }
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([]);
33818
+ }
33819
+ }
33820
+ ctx.restore();
33821
+ });
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;
33868
+ }
33869
+ if (alignment.includes("top")) {
33870
+ yOff = -textBounds.maxY;
33871
+ } else if (alignment.includes("bottom")) {
33872
+ yOff = -textBounds.minY;
33873
+ }
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;
33883
- }
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
34029
+ });
34030
+ const pcbCopperPours = su13(circuitJson).pcb_copper_pour.list();
34031
+ pcbCopperPours.forEach((pour) => {
34032
+ if (pour.layer !== layer) return;
34033
+ if (pour.covered_with_solder_mask !== false) return;
34034
+ if (pour.shape === "rect") {
34035
+ const centerX = canvasXFromPcb(pour.center.x);
34036
+ const centerY = canvasYFromPcb(pour.center.y);
34037
+ const width10 = pour.width * traceTextureResolution;
34038
+ const height10 = pour.height * traceTextureResolution;
34039
+ ctx.fillRect(centerX - width10 / 2, centerY - height10 / 2, width10, height10);
34040
+ } else if (pour.shape === "polygon" && pour.points) {
34041
+ ctx.beginPath();
34042
+ pour.points.forEach((point2, index2) => {
34043
+ const px = canvasXFromPcb(point2.x);
34044
+ const py = canvasYFromPcb(point2.y);
34045
+ if (index2 === 0) {
34046
+ ctx.moveTo(px, py);
34047
+ } else {
34048
+ ctx.lineTo(px, py);
34049
+ }
33892
34050
  });
34051
+ ctx.closePath();
34052
+ ctx.fill();
33893
34053
  }
33894
- }
33895
- return { copperPourGeoms };
34054
+ });
34055
+ ctx.globalCompositeOperation = "source-over";
34056
+ const texture = new THREE24.CanvasTexture(canvas);
34057
+ texture.generateMipmaps = true;
34058
+ texture.minFilter = THREE24.LinearMipmapLinearFilter;
34059
+ texture.magFilter = THREE24.LinearFilter;
34060
+ texture.anisotropy = 16;
34061
+ texture.needsUpdate = true;
34062
+ return texture;
33896
34063
  }
33897
34064
 
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
- }
34065
+ // src/utils/trace-texture.ts
34066
+ import * as THREE25 from "three";
34067
+ import { su as su14 } from "@tscircuit/circuit-json-util";
34068
+ function isWireRoutePoint(point2) {
34069
+ return point2 && point2.route_type === "wire" && typeof point2.layer === "string" && typeof point2.width === "number";
34070
+ }
34071
+ function createTraceTextureForLayer({
34072
+ layer,
34073
+ circuitJson,
34074
+ boardData,
34075
+ traceColor,
34076
+ traceTextureResolution
34077
+ }) {
34078
+ const pcbTraces = su14(circuitJson).pcb_trace.list();
34079
+ const allPcbVias = su14(circuitJson).pcb_via.list();
34080
+ const allPcbPlatedHoles = su14(
34081
+ circuitJson
34082
+ ).pcb_plated_hole.list();
34083
+ const tracesOnLayer = pcbTraces.filter(
34084
+ (t) => t.route.some((p) => isWireRoutePoint(p) && p.layer === layer)
34085
+ );
34086
+ if (tracesOnLayer.length === 0) return null;
34087
+ const canvas = document.createElement("canvas");
34088
+ const canvasWidth = Math.floor(boardData.width * traceTextureResolution);
34089
+ const canvasHeight = Math.floor(boardData.height * traceTextureResolution);
34090
+ canvas.width = canvasWidth;
34091
+ canvas.height = canvasHeight;
34092
+ const ctx = canvas.getContext("2d");
34093
+ if (!ctx) return null;
34094
+ if (layer === "bottom") {
34095
+ ctx.translate(0, canvasHeight);
34096
+ ctx.scale(1, -1);
33908
34097
  }
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
- );
34098
+ tracesOnLayer.forEach((trace) => {
34099
+ let firstPoint = true;
34100
+ ctx.beginPath();
34101
+ ctx.strokeStyle = traceColor;
34102
+ ctx.lineCap = "round";
34103
+ ctx.lineJoin = "round";
34104
+ let currentLineWidth = 0;
34105
+ for (const point2 of trace.route) {
34106
+ if (!isWireRoutePoint(point2) || point2.layer !== layer) {
34107
+ if (!firstPoint) ctx.stroke();
34108
+ firstPoint = true;
33999
34109
  continue;
34110
+ }
34111
+ const pcbX = point2.x;
34112
+ const pcbY = point2.y;
34113
+ currentLineWidth = point2.width * traceTextureResolution;
34114
+ ctx.lineWidth = currentLineWidth;
34115
+ const canvasX = (pcbX - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34116
+ const canvasY = (-(pcbY - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34117
+ if (firstPoint) {
34118
+ ctx.moveTo(canvasX, canvasY);
34119
+ firstPoint = false;
34120
+ } else {
34121
+ ctx.lineTo(canvasX, canvasY);
34122
+ }
34000
34123
  }
34001
- if (cutoutOp) {
34002
- cutoutOps.push(cutoutOp);
34124
+ if (!firstPoint) {
34125
+ ctx.stroke();
34003
34126
  }
34004
- }
34005
- return { cutoutOps };
34127
+ });
34128
+ ctx.globalCompositeOperation = "destination-out";
34129
+ ctx.fillStyle = "black";
34130
+ allPcbVias.forEach((via) => {
34131
+ const canvasX = (via.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34132
+ const canvasY = (-(via.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34133
+ const canvasRadius = via.outer_diameter / 2 * traceTextureResolution;
34134
+ ctx.beginPath();
34135
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
34136
+ ctx.fill();
34137
+ });
34138
+ allPcbPlatedHoles.forEach((ph) => {
34139
+ if (ph.layers.includes(layer) && ph.shape === "circle") {
34140
+ const canvasX = (ph.x - boardData.center.x + boardData.width / 2) * traceTextureResolution;
34141
+ const canvasY = (-(ph.y - boardData.center.y) + boardData.height / 2) * traceTextureResolution;
34142
+ const canvasRadius = ph.outer_diameter / 2 * traceTextureResolution;
34143
+ ctx.beginPath();
34144
+ ctx.arc(canvasX, canvasY, canvasRadius, 0, 2 * Math.PI, false);
34145
+ ctx.fill();
34146
+ }
34147
+ });
34148
+ ctx.globalCompositeOperation = "source-over";
34149
+ const texture = new THREE25.CanvasTexture(canvas);
34150
+ texture.generateMipmaps = true;
34151
+ texture.minFilter = THREE25.LinearMipmapLinearFilter;
34152
+ texture.magFilter = THREE25.LinearFilter;
34153
+ texture.anisotropy = 16;
34154
+ texture.needsUpdate = true;
34155
+ return texture;
34006
34156
  }
34007
34157
 
34008
34158
  // src/hooks/useManifoldBoardBuilder.ts
@@ -34017,7 +34167,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34017
34167
  const panels = circuitJson.filter(
34018
34168
  (e) => e.type === "pcb_panel"
34019
34169
  );
34020
- const boards = su14(circuitJson).pcb_board.list();
34170
+ const boards = su15(circuitJson).pcb_board.list();
34021
34171
  if (panels.length > 0) {
34022
34172
  const panel = panels[0];
34023
34173
  return {
@@ -34036,7 +34186,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34036
34186
  return boardsNotInPanel.length > 0 ? boardsNotInPanel[0] : null;
34037
34187
  }, [circuitJson]);
34038
34188
  const isFauxBoard = useMemo19(() => {
34039
- const boards = su14(circuitJson).pcb_board.list();
34189
+ const boards = su15(circuitJson).pcb_board.list();
34040
34190
  return boards.length > 0 && boards[0].pcb_board_id === "faux-board";
34041
34191
  }, [circuitJson]);
34042
34192
  useEffect22(() => {
@@ -34171,7 +34321,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34171
34321
  {
34172
34322
  key: "plated-holes-union",
34173
34323
  geometry: cutPlatedGeom,
34174
- color: new THREE25.Color(
34324
+ color: new THREE26.Color(
34175
34325
  colors.copper[0],
34176
34326
  colors.copper[1],
34177
34327
  colors.copper[2]
@@ -34201,7 +34351,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34201
34351
  const matColorArray = boardMaterialColors[boardData.material] ?? colors.fr4Green;
34202
34352
  currentGeoms.board = {
34203
34353
  geometry: finalBoardGeom,
34204
- color: new THREE25.Color(
34354
+ color: new THREE26.Color(
34205
34355
  matColorArray[0],
34206
34356
  matColorArray[1],
34207
34357
  matColorArray[2]
@@ -34262,6 +34412,22 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34262
34412
  silkscreenColor,
34263
34413
  traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34264
34414
  });
34415
+ const soldermaskColorArr = tracesMaterialColors[boardData.material] ?? colors.fr4GreenSolderWithMask;
34416
+ const soldermaskColor = `rgb(${Math.round(soldermaskColorArr[0] * 255)}, ${Math.round(soldermaskColorArr[1] * 255)}, ${Math.round(soldermaskColorArr[2] * 255)})`;
34417
+ currentTextures.topSoldermask = createSoldermaskTextureForLayer({
34418
+ layer: "top",
34419
+ circuitJson,
34420
+ boardData,
34421
+ soldermaskColor,
34422
+ traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34423
+ });
34424
+ currentTextures.bottomSoldermask = createSoldermaskTextureForLayer({
34425
+ layer: "bottom",
34426
+ circuitJson,
34427
+ boardData,
34428
+ soldermaskColor,
34429
+ traceTextureResolution: TRACE_TEXTURE_RESOLUTION
34430
+ });
34265
34431
  setTextures(currentTextures);
34266
34432
  } catch (e) {
34267
34433
  console.error("Error processing geometry with Manifold in hook:", e);
@@ -34290,11 +34456,11 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
34290
34456
  };
34291
34457
 
34292
34458
  // src/utils/manifold/create-three-geometry-meshes.ts
34293
- import * as THREE27 from "three";
34459
+ import * as THREE28 from "three";
34294
34460
 
34295
34461
  // src/utils/create-board-material.ts
34296
- import * as THREE26 from "three";
34297
- var DEFAULT_SIDE = THREE26.DoubleSide;
34462
+ import * as THREE27 from "three";
34463
+ var DEFAULT_SIDE = THREE27.DoubleSide;
34298
34464
  var createBoardMaterial = ({
34299
34465
  material,
34300
34466
  color,
@@ -34302,7 +34468,7 @@ var createBoardMaterial = ({
34302
34468
  isFaux = false
34303
34469
  }) => {
34304
34470
  if (material === "fr4") {
34305
- return new THREE26.MeshPhysicalMaterial({
34471
+ return new THREE27.MeshPhysicalMaterial({
34306
34472
  color,
34307
34473
  side,
34308
34474
  metalness: 0,
@@ -34316,7 +34482,7 @@ var createBoardMaterial = ({
34316
34482
  flatShading: true
34317
34483
  });
34318
34484
  }
34319
- return new THREE26.MeshStandardMaterial({
34485
+ return new THREE27.MeshStandardMaterial({
34320
34486
  color,
34321
34487
  side,
34322
34488
  flatShading: true,
@@ -34332,12 +34498,12 @@ function createGeometryMeshes(geoms) {
34332
34498
  const meshes = [];
34333
34499
  if (!geoms) return meshes;
34334
34500
  if (geoms.board && geoms.board.geometry) {
34335
- const mesh = new THREE27.Mesh(
34501
+ const mesh = new THREE28.Mesh(
34336
34502
  geoms.board.geometry,
34337
34503
  createBoardMaterial({
34338
34504
  material: geoms.board.material,
34339
34505
  color: geoms.board.color,
34340
- side: THREE27.DoubleSide,
34506
+ side: THREE28.DoubleSide,
34341
34507
  isFaux: geoms.board.isFaux
34342
34508
  })
34343
34509
  );
@@ -34347,11 +34513,11 @@ function createGeometryMeshes(geoms) {
34347
34513
  const createMeshesFromArray = (geomArray) => {
34348
34514
  if (geomArray) {
34349
34515
  geomArray.forEach((comp) => {
34350
- const mesh = new THREE27.Mesh(
34516
+ const mesh = new THREE28.Mesh(
34351
34517
  comp.geometry,
34352
- new THREE27.MeshStandardMaterial({
34518
+ new THREE28.MeshStandardMaterial({
34353
34519
  color: comp.color,
34354
- side: THREE27.DoubleSide,
34520
+ side: THREE28.DoubleSide,
34355
34521
  flatShading: true,
34356
34522
  // Consistent with board
34357
34523
  polygonOffset: true,
@@ -34372,21 +34538,24 @@ function createGeometryMeshes(geoms) {
34372
34538
  }
34373
34539
 
34374
34540
  // src/utils/manifold/create-three-texture-meshes.ts
34375
- import * as THREE28 from "three";
34541
+ import * as THREE29 from "three";
34376
34542
  function createTextureMeshes(textures, boardData, pcbThickness) {
34377
34543
  const meshes = [];
34378
34544
  if (!textures || !boardData || pcbThickness === null) return meshes;
34379
- const createTexturePlane = (texture, yOffset, isBottomLayer, keySuffix) => {
34545
+ const createTexturePlane = (texture, yOffset, isBottomLayer, keySuffix, usePolygonOffset = false) => {
34380
34546
  if (!texture) return null;
34381
- const planeGeom = new THREE28.PlaneGeometry(boardData.width, boardData.height);
34382
- const material = new THREE28.MeshBasicMaterial({
34547
+ const planeGeom = new THREE29.PlaneGeometry(boardData.width, boardData.height);
34548
+ const material = new THREE29.MeshBasicMaterial({
34383
34549
  map: texture,
34384
34550
  transparent: true,
34385
- side: THREE28.DoubleSide,
34386
- depthWrite: false
34551
+ side: THREE29.DoubleSide,
34552
+ depthWrite: false,
34387
34553
  // Important for layers to avoid z-fighting issues with board itself
34554
+ polygonOffset: usePolygonOffset,
34555
+ polygonOffsetFactor: usePolygonOffset ? -1 : 0,
34556
+ polygonOffsetUnits: usePolygonOffset ? -1 : 0
34388
34557
  });
34389
- const mesh = new THREE28.Mesh(planeGeom, material);
34558
+ const mesh = new THREE29.Mesh(planeGeom, material);
34390
34559
  mesh.position.set(boardData.center.x, boardData.center.y, yOffset);
34391
34560
  if (isBottomLayer) {
34392
34561
  mesh.rotation.set(Math.PI, 0, 0);
@@ -34425,6 +34594,26 @@ function createTextureMeshes(textures, boardData, pcbThickness) {
34425
34594
  "silkscreen"
34426
34595
  );
34427
34596
  if (bottomSilkscreenMesh) meshes.push(bottomSilkscreenMesh);
34597
+ const topSoldermaskMesh = createTexturePlane(
34598
+ textures.topSoldermask,
34599
+ pcbThickness / 2 + 8e-4,
34600
+ // Just above board surface, below traces
34601
+ false,
34602
+ "soldermask",
34603
+ true
34604
+ // Enable polygon offset
34605
+ );
34606
+ if (topSoldermaskMesh) meshes.push(topSoldermaskMesh);
34607
+ const bottomSoldermaskMesh = createTexturePlane(
34608
+ textures.bottomSoldermask,
34609
+ -pcbThickness / 2 - 8e-4,
34610
+ // Just below board surface (bottom side)
34611
+ true,
34612
+ "soldermask",
34613
+ true
34614
+ // Enable polygon offset
34615
+ );
34616
+ if (bottomSoldermaskMesh) meshes.push(bottomSoldermaskMesh);
34428
34617
  return meshes;
34429
34618
  }
34430
34619
 
@@ -34469,6 +34658,10 @@ var BoardMeshes = ({
34469
34658
  shouldShow = visibility.topSilkscreen;
34470
34659
  } else if (mesh.name.includes("bottom-silkscreen")) {
34471
34660
  shouldShow = visibility.bottomSilkscreen;
34661
+ } else if (mesh.name.includes("top-soldermask")) {
34662
+ shouldShow = visibility.topMask;
34663
+ } else if (mesh.name.includes("bottom-soldermask")) {
34664
+ shouldShow = visibility.bottomMask;
34472
34665
  }
34473
34666
  if (shouldShow) {
34474
34667
  rootObject.add(mesh);
@@ -34581,7 +34774,7 @@ try {
34581
34774
  [textures, boardData, pcbThickness]
34582
34775
  );
34583
34776
  const cadComponents = useMemo20(
34584
- () => su15(circuitJson).cad_component.list(),
34777
+ () => su16(circuitJson).cad_component.list(),
34585
34778
  [circuitJson]
34586
34779
  );
34587
34780
  const boardDimensions = useMemo20(() => {
@@ -40696,6 +40889,48 @@ var AppearanceMenu = () => {
40696
40889
  ]
40697
40890
  }
40698
40891
  ),
40892
+ /* @__PURE__ */ jsxs8(
40893
+ Item22,
40894
+ {
40895
+ style: {
40896
+ ...itemStyles,
40897
+ backgroundColor: hoveredItem === "topMask" ? "#404040" : "transparent"
40898
+ },
40899
+ onSelect: (e) => e.preventDefault(),
40900
+ onPointerDown: (e) => {
40901
+ e.preventDefault();
40902
+ setLayerVisibility("topMask", !visibility.topMask);
40903
+ },
40904
+ onMouseEnter: () => setHoveredItem("topMask"),
40905
+ onMouseLeave: () => setHoveredItem(null),
40906
+ onTouchStart: () => setHoveredItem("topMask"),
40907
+ children: [
40908
+ /* @__PURE__ */ jsx34("span", { style: iconContainerStyles, children: visibility.topMask && /* @__PURE__ */ jsx34(CheckIcon, {}) }),
40909
+ /* @__PURE__ */ jsx34("span", { style: { display: "flex", alignItems: "center" }, children: "Top Soldermask" })
40910
+ ]
40911
+ }
40912
+ ),
40913
+ /* @__PURE__ */ jsxs8(
40914
+ Item22,
40915
+ {
40916
+ style: {
40917
+ ...itemStyles,
40918
+ backgroundColor: hoveredItem === "bottomMask" ? "#404040" : "transparent"
40919
+ },
40920
+ onSelect: (e) => e.preventDefault(),
40921
+ onPointerDown: (e) => {
40922
+ e.preventDefault();
40923
+ setLayerVisibility("bottomMask", !visibility.bottomMask);
40924
+ },
40925
+ onMouseEnter: () => setHoveredItem("bottomMask"),
40926
+ onMouseLeave: () => setHoveredItem(null),
40927
+ onTouchStart: () => setHoveredItem("bottomMask"),
40928
+ children: [
40929
+ /* @__PURE__ */ jsx34("span", { style: iconContainerStyles, children: visibility.bottomMask && /* @__PURE__ */ jsx34(CheckIcon, {}) }),
40930
+ /* @__PURE__ */ jsx34("span", { style: { display: "flex", alignItems: "center" }, children: "Bottom Soldermask" })
40931
+ ]
40932
+ }
40933
+ ),
40699
40934
  /* @__PURE__ */ jsxs8(
40700
40935
  Item22,
40701
40936
  {
@@ -41535,7 +41770,7 @@ var CadViewerInner = (props) => {
41535
41770
  );
41536
41771
  };
41537
41772
  var CadViewer = (props) => {
41538
- const defaultTarget = useMemo28(() => new THREE29.Vector3(0, 0, 0), []);
41773
+ const defaultTarget = useMemo28(() => new THREE30.Vector3(0, 0, 0), []);
41539
41774
  const initialCameraPosition = useMemo28(
41540
41775
  () => [5, -5, 5],
41541
41776
  []
@@ -41552,12 +41787,12 @@ var CadViewer = (props) => {
41552
41787
 
41553
41788
  // src/convert-circuit-json-to-3d-svg.ts
41554
41789
  var import_debug = __toESM(require_browser(), 1);
41555
- import { su as su16 } from "@tscircuit/circuit-json-util";
41556
- import * as THREE33 from "three";
41790
+ import { su as su17 } from "@tscircuit/circuit-json-util";
41791
+ import * as THREE34 from "three";
41557
41792
  import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
41558
41793
 
41559
41794
  // src/utils/create-geometry-from-polygons.ts
41560
- import * as THREE30 from "three";
41795
+ import * as THREE31 from "three";
41561
41796
  import { BufferGeometry as BufferGeometry3, Float32BufferAttribute as Float32BufferAttribute2 } from "three";
41562
41797
  function createGeometryFromPolygons(polygons) {
41563
41798
  const geometry = new BufferGeometry3();
@@ -41571,12 +41806,12 @@ function createGeometryFromPolygons(polygons) {
41571
41806
  ...polygon3.vertices[i + 1]
41572
41807
  // Third vertex
41573
41808
  );
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)
41809
+ const v1 = new THREE31.Vector3(...polygon3.vertices[0]);
41810
+ const v2 = new THREE31.Vector3(...polygon3.vertices[i]);
41811
+ const v3 = new THREE31.Vector3(...polygon3.vertices[i + 1]);
41812
+ const normal = new THREE31.Vector3().crossVectors(
41813
+ new THREE31.Vector3().subVectors(v2, v1),
41814
+ new THREE31.Vector3().subVectors(v3, v1)
41580
41815
  ).normalize();
41581
41816
  normals.push(
41582
41817
  normal.x,
@@ -41600,10 +41835,10 @@ function createGeometryFromPolygons(polygons) {
41600
41835
  var import_modeling2 = __toESM(require_src(), 1);
41601
41836
  var import_jscad_planner2 = __toESM(require_dist(), 1);
41602
41837
  var jscadModeling2 = __toESM(require_src(), 1);
41603
- import * as THREE32 from "three";
41838
+ import * as THREE33 from "three";
41604
41839
 
41605
41840
  // src/utils/load-model.ts
41606
- import * as THREE31 from "three";
41841
+ import * as THREE32 from "three";
41607
41842
  import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
41608
41843
  import { OBJLoader as OBJLoader2 } from "three/examples/jsm/loaders/OBJLoader.js";
41609
41844
  import { STLLoader as STLLoader2 } from "three/examples/jsm/loaders/STLLoader.js";
@@ -41611,12 +41846,12 @@ async function load3DModel(url) {
41611
41846
  if (url.endsWith(".stl")) {
41612
41847
  const loader = new STLLoader2();
41613
41848
  const geometry = await loader.loadAsync(url);
41614
- const material = new THREE31.MeshStandardMaterial({
41849
+ const material = new THREE32.MeshStandardMaterial({
41615
41850
  color: 8947848,
41616
41851
  metalness: 0.5,
41617
41852
  roughness: 0.5
41618
41853
  });
41619
- return new THREE31.Mesh(geometry, material);
41854
+ return new THREE32.Mesh(geometry, material);
41620
41855
  }
41621
41856
  if (url.endsWith(".obj")) {
41622
41857
  const loader = new OBJLoader2();
@@ -41649,9 +41884,9 @@ async function renderComponent(component, scene) {
41649
41884
  }
41650
41885
  if (component.rotation) {
41651
41886
  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)
41887
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
41888
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
41889
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41655
41890
  );
41656
41891
  }
41657
41892
  scene.add(model);
@@ -41665,13 +41900,13 @@ async function renderComponent(component, scene) {
41665
41900
  );
41666
41901
  if (jscadObject && (jscadObject.polygons || jscadObject.sides)) {
41667
41902
  const threeGeom = convertCSGToThreeGeom(jscadObject);
41668
- const material2 = new THREE32.MeshStandardMaterial({
41903
+ const material2 = new THREE33.MeshStandardMaterial({
41669
41904
  color: 8947848,
41670
41905
  metalness: 0.5,
41671
41906
  roughness: 0.5,
41672
- side: THREE32.DoubleSide
41907
+ side: THREE33.DoubleSide
41673
41908
  });
41674
- const mesh2 = new THREE32.Mesh(threeGeom, material2);
41909
+ const mesh2 = new THREE33.Mesh(threeGeom, material2);
41675
41910
  if (component.position) {
41676
41911
  mesh2.position.set(
41677
41912
  component.position.x ?? 0,
@@ -41681,9 +41916,9 @@ async function renderComponent(component, scene) {
41681
41916
  }
41682
41917
  if (component.rotation) {
41683
41918
  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)
41919
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
41920
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
41921
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41687
41922
  );
41688
41923
  }
41689
41924
  scene.add(mesh2);
@@ -41700,17 +41935,17 @@ async function renderComponent(component, scene) {
41700
41935
  if (!geom || !geom.polygons && !geom.sides) {
41701
41936
  continue;
41702
41937
  }
41703
- const color = new THREE32.Color(geomInfo.color);
41938
+ const color = new THREE33.Color(geomInfo.color);
41704
41939
  color.convertLinearToSRGB();
41705
41940
  const geomWithColor = { ...geom, color: [color.r, color.g, color.b] };
41706
41941
  const threeGeom = convertCSGToThreeGeom(geomWithColor);
41707
- const material2 = new THREE32.MeshStandardMaterial({
41942
+ const material2 = new THREE33.MeshStandardMaterial({
41708
41943
  vertexColors: true,
41709
41944
  metalness: 0.2,
41710
41945
  roughness: 0.8,
41711
- side: THREE32.DoubleSide
41946
+ side: THREE33.DoubleSide
41712
41947
  });
41713
- const mesh2 = new THREE32.Mesh(threeGeom, material2);
41948
+ const mesh2 = new THREE33.Mesh(threeGeom, material2);
41714
41949
  if (component.position) {
41715
41950
  mesh2.position.set(
41716
41951
  component.position.x ?? 0,
@@ -41720,22 +41955,22 @@ async function renderComponent(component, scene) {
41720
41955
  }
41721
41956
  if (component.rotation) {
41722
41957
  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)
41958
+ THREE33.MathUtils.degToRad(component.rotation.x ?? 0),
41959
+ THREE33.MathUtils.degToRad(component.rotation.y ?? 0),
41960
+ THREE33.MathUtils.degToRad(component.rotation.z ?? 0)
41726
41961
  );
41727
41962
  }
41728
41963
  scene.add(mesh2);
41729
41964
  }
41730
41965
  return;
41731
41966
  }
41732
- const geometry = new THREE32.BoxGeometry(0.5, 0.5, 0.5);
41733
- const material = new THREE32.MeshStandardMaterial({
41967
+ const geometry = new THREE33.BoxGeometry(0.5, 0.5, 0.5);
41968
+ const material = new THREE33.MeshStandardMaterial({
41734
41969
  color: 16711680,
41735
41970
  transparent: true,
41736
41971
  opacity: 0.25
41737
41972
  });
41738
- const mesh = new THREE32.Mesh(geometry, material);
41973
+ const mesh = new THREE33.Mesh(geometry, material);
41739
41974
  if (component.position) {
41740
41975
  mesh.position.set(
41741
41976
  component.position.x ?? 0,
@@ -41756,11 +41991,11 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41756
41991
  padding = 20,
41757
41992
  zoom = 1.5
41758
41993
  } = options;
41759
- const scene = new THREE33.Scene();
41994
+ const scene = new THREE34.Scene();
41760
41995
  const renderer = new SVGRenderer();
41761
41996
  renderer.setSize(width10, height10);
41762
- renderer.setClearColor(new THREE33.Color(backgroundColor), 1);
41763
- const camera = new THREE33.OrthographicCamera();
41997
+ renderer.setClearColor(new THREE34.Color(backgroundColor), 1);
41998
+ const camera = new THREE34.OrthographicCamera();
41764
41999
  const aspect = width10 / height10;
41765
42000
  const frustumSize = 100;
41766
42001
  const halfFrustumSize = frustumSize / 2 / zoom;
@@ -41774,25 +42009,25 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41774
42009
  camera.position.set(position.x, position.y, position.z);
41775
42010
  camera.up.set(0, 1, 0);
41776
42011
  const lookAt = options.camera?.lookAt ?? { x: 0, y: 0, z: 0 };
41777
- camera.lookAt(new THREE33.Vector3(lookAt.x, lookAt.y, lookAt.z));
42012
+ camera.lookAt(new THREE34.Vector3(lookAt.x, lookAt.y, lookAt.z));
41778
42013
  camera.updateProjectionMatrix();
41779
- const ambientLight = new THREE33.AmbientLight(16777215, Math.PI / 2);
42014
+ const ambientLight = new THREE34.AmbientLight(16777215, Math.PI / 2);
41780
42015
  scene.add(ambientLight);
41781
- const pointLight = new THREE33.PointLight(16777215, Math.PI / 4);
42016
+ const pointLight = new THREE34.PointLight(16777215, Math.PI / 4);
41782
42017
  pointLight.position.set(-10, -10, 10);
41783
42018
  scene.add(pointLight);
41784
- const components = su16(circuitJson).cad_component.list();
42019
+ const components = su17(circuitJson).cad_component.list();
41785
42020
  for (const component of components) {
41786
42021
  await renderComponent(component, scene);
41787
42022
  }
41788
- const boardData = su16(circuitJson).pcb_board.list()[0];
42023
+ const boardData = su17(circuitJson).pcb_board.list()[0];
41789
42024
  const boardGeom = createBoardGeomFromCircuitJson(circuitJson);
41790
42025
  if (boardGeom) {
41791
42026
  for (const geom of boardGeom) {
41792
42027
  const g = geom;
41793
42028
  if (!g.polygons || g.polygons.length === 0) continue;
41794
42029
  const geometry = createGeometryFromPolygons(g.polygons);
41795
- const baseColor = new THREE33.Color(
42030
+ const baseColor = new THREE34.Color(
41796
42031
  g.color?.[0] ?? 0,
41797
42032
  g.color?.[1] ?? 0,
41798
42033
  g.color?.[2] ?? 0
@@ -41800,18 +42035,18 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41800
42035
  const material = createBoardMaterial({
41801
42036
  material: boardData?.material,
41802
42037
  color: baseColor,
41803
- side: THREE33.DoubleSide
42038
+ side: THREE34.DoubleSide
41804
42039
  });
41805
- const mesh = new THREE33.Mesh(geometry, material);
42040
+ const mesh = new THREE34.Mesh(geometry, material);
41806
42041
  scene.add(mesh);
41807
42042
  }
41808
42043
  }
41809
- const gridHelper = new THREE33.GridHelper(100, 100);
42044
+ const gridHelper = new THREE34.GridHelper(100, 100);
41810
42045
  gridHelper.rotation.x = Math.PI / 2;
41811
42046
  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());
42047
+ const box = new THREE34.Box3().setFromObject(scene);
42048
+ const center = box.getCenter(new THREE34.Vector3());
42049
+ const size5 = box.getSize(new THREE34.Vector3());
41815
42050
  scene.position.sub(center);
41816
42051
  const maxDim = Math.max(size5.x, size5.y, size5.z);
41817
42052
  if (maxDim > 0) {