@tscircuit/3d-viewer 0.0.627 → 0.0.628

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.
Files changed (3) hide show
  1. package/README.md +8 -11
  2. package/dist/index.js +453 -135
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -82,7 +82,7 @@ viewer. The host can switch tabs and focus the schematic using the source ID.
82
82
  Omit the callback when schematic navigation is unavailable.
83
83
 
84
84
  ```tsx
85
- <CadViewer
85
+ ;<CadViewer
86
86
  circuitJson={circuitJson}
87
87
  onViewSchematicComponent={({ source_component_id }) => {
88
88
  // Find the schematic component with this source ID and focus it.
@@ -96,9 +96,9 @@ Omit the callback when schematic navigation is unavailable.
96
96
  When using the SVG converter in Node.js environments, you'll need to provide JSDOM:
97
97
 
98
98
  ```typescript
99
- import { JSDOM } from 'jsdom'
100
- import { convert3dCircuitToSvg } from '@tscircuit/3d-viewer/3d'
101
- import { applyJsdomShim } from '@tscircuit/3d-viewer/utils'
99
+ import { JSDOM } from "jsdom"
100
+ import { convert3dCircuitToSvg } from "@tscircuit/3d-viewer/3d"
101
+ import { applyJsdomShim } from "@tscircuit/3d-viewer/utils"
102
102
 
103
103
  // Setup JSDOM environment
104
104
  const dom = new JSDOM()
@@ -113,8 +113,8 @@ const options = {
113
113
  zoom: 50,
114
114
  camera: {
115
115
  position: { x: 0, y: 0, z: 100 },
116
- lookAt: { x: 0, y: 0, z: 0 }
117
- }
116
+ lookAt: { x: 0, y: 0, z: 0 },
117
+ },
118
118
  }
119
119
 
120
120
  const svgString = await convert3dCircuitToSvg(circuitJson, options)
@@ -137,7 +137,6 @@ The `convert3dCircuitToSvg` function accepts the following options:
137
137
  Main component for rendering the 3D PCB viewer.
138
138
 
139
139
  Props:
140
-
141
140
  - `circuit-json`: (optional) An array of AnyCircuitElement objects representing the PCB layout.
142
141
  - `children`: (optional) React children elements describing the PCB layout (alternative to using `circuit-json`).
143
142
  - `resolveStaticAsset`: (optional) Function that receives each component model URL (`obj`, `wrl`, `stl`, `gltf`, `glb`, `step`) and returns the resolved URL to load.
@@ -147,14 +146,12 @@ Props:
147
146
  Defines the PCB board dimensions.
148
147
 
149
148
  Props:
150
-
151
149
  - `width`: Width of the board (e.g., "20mm").
152
150
  - `height`: Height of the board (e.g., "20mm").
153
151
 
154
152
  ### Component Elements
155
153
 
156
154
  Various component elements can be used as children of the `<board>` element:
157
-
158
155
  - `<resistor>`
159
156
  - `<capacitor>`
160
157
  - `<chip>`
@@ -269,7 +266,7 @@ input remains a distinct diagnostic, not the mounted pose's zero-angle control.
269
266
  You can define custom 3D models for components using the `cadModel` prop:
270
267
 
271
268
  ```jsx
272
- <chip
269
+ ;<chip
273
270
  name="U1"
274
271
  footprint="soic8"
275
272
  cadModel={{
@@ -284,7 +281,7 @@ You can define custom 3D models for components using the `cadModel` prop:
284
281
  For more complex or programmatically defined models, you can use JSCAD:
285
282
 
286
283
  ```jsx
287
- <bug
284
+ ;<bug
288
285
  footprint="soic8"
289
286
  name="U1"
290
287
  cadModel={{
package/dist/index.js CHANGED
@@ -233,11 +233,11 @@ var require_constants = __commonJS({
233
233
  "node_modules/@jscad/modeling/src/maths/constants.js"(exports, module) {
234
234
  "use strict";
235
235
  var spatialResolution = 1e5;
236
- var EPS3 = 1e-5;
236
+ var EPS4 = 1e-5;
237
237
  var NEPS = 1e-13;
238
238
  var TAU = Math.PI * 2;
239
239
  module.exports = {
240
- EPS: EPS3,
240
+ EPS: EPS4,
241
241
  NEPS,
242
242
  TAU,
243
243
  spatialResolution
@@ -288,13 +288,13 @@ var require_identity = __commonJS({
288
288
  var require_fromRotation = __commonJS({
289
289
  "node_modules/@jscad/modeling/src/maths/mat4/fromRotation.js"(exports, module) {
290
290
  "use strict";
291
- var { EPS: EPS3 } = require_constants();
291
+ var { EPS: EPS4 } = require_constants();
292
292
  var { sin, cos } = require_trigonometry();
293
293
  var identity7 = require_identity();
294
294
  var fromRotation6 = (out, rad, axis) => {
295
295
  let [x, y, z] = axis;
296
296
  const lengthSquared = x * x + y * y + z * z;
297
- if (Math.abs(lengthSquared) < EPS3) {
297
+ if (Math.abs(lengthSquared) < EPS4) {
298
298
  return identity7(out);
299
299
  }
300
300
  const len6 = 1 / Math.sqrt(lengthSquared);
@@ -1221,13 +1221,13 @@ var require_multiply2 = __commonJS({
1221
1221
  var require_rotate = __commonJS({
1222
1222
  "node_modules/@jscad/modeling/src/maths/mat4/rotate.js"(exports, module) {
1223
1223
  "use strict";
1224
- var { EPS: EPS3 } = require_constants();
1224
+ var { EPS: EPS4 } = require_constants();
1225
1225
  var { sin, cos } = require_trigonometry();
1226
1226
  var copy10 = require_copy();
1227
1227
  var rotate7 = (out, matrix, radians, axis) => {
1228
1228
  let [x, y, z] = axis;
1229
1229
  const lengthSquared = x * x + y * y + z * z;
1230
- if (Math.abs(lengthSquared) < EPS3) {
1230
+ if (Math.abs(lengthSquared) < EPS4) {
1231
1231
  return copy10(out, matrix);
1232
1232
  }
1233
1233
  const len6 = 1 / Math.sqrt(lengthSquared);
@@ -3716,19 +3716,19 @@ var require_fromPoints3 = __commonJS({
3716
3716
  var require_fromPointsRandom = __commonJS({
3717
3717
  "node_modules/@jscad/modeling/src/maths/plane/fromPointsRandom.js"(exports, module) {
3718
3718
  "use strict";
3719
- var { EPS: EPS3 } = require_constants();
3719
+ var { EPS: EPS4 } = require_constants();
3720
3720
  var vec3 = require_vec3();
3721
3721
  var fromPointsRandom = (out, a, b, c) => {
3722
3722
  let ba = vec3.subtract(vec3.create(), b, a);
3723
3723
  let ca = vec3.subtract(vec3.create(), c, a);
3724
- if (vec3.length(ba) < EPS3) {
3724
+ if (vec3.length(ba) < EPS4) {
3725
3725
  ba = vec3.orthogonal(ba, ca);
3726
3726
  }
3727
- if (vec3.length(ca) < EPS3) {
3727
+ if (vec3.length(ca) < EPS4) {
3728
3728
  ca = vec3.orthogonal(ca, ba);
3729
3729
  }
3730
3730
  let normal = vec3.cross(vec3.create(), ba, ca);
3731
- if (vec3.length(normal) < EPS3) {
3731
+ if (vec3.length(normal) < EPS4) {
3732
3732
  ca = vec3.orthogonal(ca, ba);
3733
3733
  normal = vec3.cross(normal, ba, ca);
3734
3734
  }
@@ -4411,7 +4411,7 @@ var require_isA3 = __commonJS({
4411
4411
  var require_isConvex2 = __commonJS({
4412
4412
  "node_modules/@jscad/modeling/src/geometries/geom3/isConvex.js"(exports, module) {
4413
4413
  "use strict";
4414
- var { EPS: EPS3 } = require_constants();
4414
+ var { EPS: EPS4 } = require_constants();
4415
4415
  var vec3 = require_vec3();
4416
4416
  var geom3 = require_isA3();
4417
4417
  var toPolygons = require_toPolygons();
@@ -4442,7 +4442,7 @@ var require_isConvex2 = __commonJS({
4442
4442
  for (let j = 0; j < vertices.length; j++) {
4443
4443
  const v = vertices[j];
4444
4444
  const distance5 = vec3.dot(plane, v) - plane[3];
4445
- if (distance5 > EPS3) {
4445
+ if (distance5 > EPS4) {
4446
4446
  return false;
4447
4447
  }
4448
4448
  }
@@ -4634,7 +4634,7 @@ var require_clone8 = __commonJS({
4634
4634
  var require_close = __commonJS({
4635
4635
  "node_modules/@jscad/modeling/src/geometries/path2/close.js"(exports, module) {
4636
4636
  "use strict";
4637
- var { EPS: EPS3 } = require_constants();
4637
+ var { EPS: EPS4 } = require_constants();
4638
4638
  var vec2 = require_vec2();
4639
4639
  var clone10 = require_clone8();
4640
4640
  var close = (geometry) => {
@@ -4645,7 +4645,7 @@ var require_close = __commonJS({
4645
4645
  const points = cloned.points;
4646
4646
  const p0 = points[0];
4647
4647
  let pn = points[points.length - 1];
4648
- while (vec2.distance(p0, pn) < EPS3 * EPS3) {
4648
+ while (vec2.distance(p0, pn) < EPS4 * EPS4) {
4649
4649
  points.pop();
4650
4650
  if (points.length === 1) break;
4651
4651
  pn = points[points.length - 1];
@@ -4680,7 +4680,7 @@ var require_create8 = __commonJS({
4680
4680
  var require_fromPoints5 = __commonJS({
4681
4681
  "node_modules/@jscad/modeling/src/geometries/path2/fromPoints.js"(exports, module) {
4682
4682
  "use strict";
4683
- var { EPS: EPS3 } = require_constants();
4683
+ var { EPS: EPS4 } = require_constants();
4684
4684
  var vec2 = require_vec2();
4685
4685
  var close = require_close();
4686
4686
  var create10 = require_create8();
@@ -4692,7 +4692,7 @@ var require_fromPoints5 = __commonJS({
4692
4692
  if (created.points.length > 1) {
4693
4693
  const p0 = created.points[0];
4694
4694
  const pn = created.points[created.points.length - 1];
4695
- if (vec2.distance(p0, pn) < EPS3 * EPS3) {
4695
+ if (vec2.distance(p0, pn) < EPS4 * EPS4) {
4696
4696
  closed = true;
4697
4697
  }
4698
4698
  }
@@ -6519,12 +6519,12 @@ var require_fromPlanes = __commonJS({
6519
6519
  "use strict";
6520
6520
  var vec3 = require_vec3();
6521
6521
  var { solve2Linear } = require_utils();
6522
- var { EPS: EPS3 } = require_constants();
6522
+ var { EPS: EPS4 } = require_constants();
6523
6523
  var fromPointAndDirection = require_fromPointAndDirection();
6524
6524
  var fromPlanes = (out, plane1, plane2) => {
6525
6525
  let direction = vec3.cross(vec3.create(), plane1, plane2);
6526
6526
  let length6 = vec3.length(direction);
6527
- if (length6 < EPS3) {
6527
+ if (length6 < EPS4) {
6528
6528
  throw new Error("parallel planes do not intersect");
6529
6529
  }
6530
6530
  length6 = 1 / length6;
@@ -6863,13 +6863,13 @@ var require_measureAggregateBoundingBox = __commonJS({
6863
6863
  var require_calculateEpsilonFromBounds = __commonJS({
6864
6864
  "node_modules/@jscad/modeling/src/measurements/calculateEpsilonFromBounds.js"(exports, module) {
6865
6865
  "use strict";
6866
- var { EPS: EPS3 } = require_constants();
6866
+ var { EPS: EPS4 } = require_constants();
6867
6867
  var calculateEpsilonFromBounds = (bounds, dimensions) => {
6868
6868
  let total = 0;
6869
6869
  for (let i = 0; i < dimensions; i++) {
6870
6870
  total += bounds[1][i] - bounds[0][i];
6871
6871
  }
6872
- return EPS3 * total / dimensions;
6872
+ return EPS4 * total / dimensions;
6873
6873
  };
6874
6874
  module.exports = calculateEpsilonFromBounds;
6875
6875
  }
@@ -7238,7 +7238,7 @@ var require_commonChecks = __commonJS({
7238
7238
  var require_arc = __commonJS({
7239
7239
  "node_modules/@jscad/modeling/src/primitives/arc.js"(exports, module) {
7240
7240
  "use strict";
7241
- var { EPS: EPS3, TAU } = require_constants();
7241
+ var { EPS: EPS4, TAU } = require_constants();
7242
7242
  var vec2 = require_vec2();
7243
7243
  var path2 = require_path2();
7244
7244
  var { isGT, isGTE, isNumberArray } = require_commonChecks();
@@ -7266,7 +7266,7 @@ var require_arc = __commonJS({
7266
7266
  if (startAngle > endAngle) {
7267
7267
  rotation = endAngle + (TAU - startAngle);
7268
7268
  }
7269
- const minangle = Math.acos((radius * radius + radius * radius - EPS3 * EPS3) / (2 * radius * radius));
7269
+ const minangle = Math.acos((radius * radius + radius * radius - EPS4 * EPS4) / (2 * radius * radius));
7270
7270
  const centerv = vec2.clone(center);
7271
7271
  let point;
7272
7272
  const pointArray = [];
@@ -7304,7 +7304,7 @@ var require_arc = __commonJS({
7304
7304
  var require_ellipse = __commonJS({
7305
7305
  "node_modules/@jscad/modeling/src/primitives/ellipse.js"(exports, module) {
7306
7306
  "use strict";
7307
- var { EPS: EPS3, TAU } = require_constants();
7307
+ var { EPS: EPS4, TAU } = require_constants();
7308
7308
  var vec2 = require_vec2();
7309
7309
  var geom2 = require_geom2();
7310
7310
  var { sin, cos } = require_trigonometry();
@@ -7335,7 +7335,7 @@ var require_ellipse = __commonJS({
7335
7335
  rotation = endAngle + (TAU - startAngle);
7336
7336
  }
7337
7337
  const minradius = Math.min(radius[0], radius[1]);
7338
- const minangle = Math.acos((minradius * minradius + minradius * minradius - EPS3 * EPS3) / (2 * minradius * minradius));
7338
+ const minangle = Math.acos((minradius * minradius + minradius * minradius - EPS4 * EPS4) / (2 * minradius * minradius));
7339
7339
  if (rotation < minangle) throw new Error("startAngle and endAngle do not define a significant rotation");
7340
7340
  segments = Math.floor(segments * (rotation / TAU));
7341
7341
  const centerv = vec2.clone(center);
@@ -7447,7 +7447,7 @@ var require_cube = __commonJS({
7447
7447
  var require_cylinderElliptic = __commonJS({
7448
7448
  "node_modules/@jscad/modeling/src/primitives/cylinderElliptic.js"(exports, module) {
7449
7449
  "use strict";
7450
- var { EPS: EPS3, TAU } = require_constants();
7450
+ var { EPS: EPS4, TAU } = require_constants();
7451
7451
  var vec3 = require_vec3();
7452
7452
  var geom3 = require_geom3();
7453
7453
  var poly3 = require_poly3();
@@ -7484,7 +7484,7 @@ var require_cylinderElliptic = __commonJS({
7484
7484
  rotation = endAngle + (TAU - startAngle);
7485
7485
  }
7486
7486
  const minradius = Math.min(startRadius[0], startRadius[1], endRadius[0], endRadius[1]);
7487
- const minangle = Math.acos((minradius * minradius + minradius * minradius - EPS3 * EPS3) / (2 * minradius * minradius));
7487
+ const minangle = Math.acos((minradius * minradius + minradius * minradius - EPS4 * EPS4) / (2 * minradius * minradius));
7488
7488
  if (rotation < minangle) throw new Error("startAngle and endAngle do not define a significant rotation");
7489
7489
  const slices = Math.floor(segments * (rotation / TAU));
7490
7490
  const start = vec3.fromValues(0, 0, -(height / 2));
@@ -7933,7 +7933,7 @@ var require_rectangle = __commonJS({
7933
7933
  var require_roundedCuboid = __commonJS({
7934
7934
  "node_modules/@jscad/modeling/src/primitives/roundedCuboid.js"(exports, module) {
7935
7935
  "use strict";
7936
- var { EPS: EPS3, TAU } = require_constants();
7936
+ var { EPS: EPS4, TAU } = require_constants();
7937
7937
  var vec2 = require_vec2();
7938
7938
  var vec3 = require_vec3();
7939
7939
  var geom3 = require_geom3();
@@ -7949,7 +7949,7 @@ var require_roundedCuboid = __commonJS({
7949
7949
  let layerradius = radius * cospitch;
7950
7950
  let layeroffset = size4[2] - (radius - radius * sinpitch);
7951
7951
  if (!positive) layeroffset = radius - radius * sinpitch - size4[2];
7952
- layerradius = layerradius > EPS3 ? layerradius : 0;
7952
+ layerradius = layerradius > EPS4 ? layerradius : 0;
7953
7953
  const corner0 = vec3.add(vec3.create(), center, [size4[0] - radius, size4[1] - radius, layeroffset]);
7954
7954
  const corner1 = vec3.add(vec3.create(), center, [radius - size4[0], size4[1] - radius, layeroffset]);
7955
7955
  const corner2 = vec3.add(vec3.create(), center, [radius - size4[0], radius - size4[1], layeroffset]);
@@ -8044,7 +8044,7 @@ var require_roundedCuboid = __commonJS({
8044
8044
  if (size4[0] === 0 || size4[1] === 0 || size4[2] === 0) return geom3.create();
8045
8045
  if (roundRadius === 0) return cuboid5({ center, size: size4 });
8046
8046
  size4 = size4.map((v) => v / 2);
8047
- if (roundRadius > size4[0] - EPS3 || roundRadius > size4[1] - EPS3 || roundRadius > size4[2] - EPS3) throw new Error("roundRadius must be smaller than the radius of all dimensions");
8047
+ if (roundRadius > size4[0] - EPS4 || roundRadius > size4[1] - EPS4 || roundRadius > size4[2] - EPS4) throw new Error("roundRadius must be smaller than the radius of all dimensions");
8048
8048
  segments = Math.floor(segments / 4);
8049
8049
  let prevCornersPos = null;
8050
8050
  let prevCornersNeg = null;
@@ -8086,7 +8086,7 @@ var require_roundedCuboid = __commonJS({
8086
8086
  var require_roundedCylinder = __commonJS({
8087
8087
  "node_modules/@jscad/modeling/src/primitives/roundedCylinder.js"(exports, module) {
8088
8088
  "use strict";
8089
- var { EPS: EPS3, TAU } = require_constants();
8089
+ var { EPS: EPS4, TAU } = require_constants();
8090
8090
  var vec3 = require_vec3();
8091
8091
  var geom3 = require_geom3();
8092
8092
  var poly3 = require_poly3();
@@ -8114,7 +8114,7 @@ var require_roundedCylinder = __commonJS({
8114
8114
  const end = [0, 0, height / 2];
8115
8115
  const direction = vec3.subtract(vec3.create(), end, start);
8116
8116
  const length6 = vec3.length(direction);
8117
- if (2 * roundRadius > length6 - EPS3) throw new Error("height must be larger than twice roundRadius");
8117
+ if (2 * roundRadius > length6 - EPS4) throw new Error("height must be larger than twice roundRadius");
8118
8118
  let defaultnormal;
8119
8119
  if (Math.abs(direction[0]) > Math.abs(direction[1])) {
8120
8120
  defaultnormal = vec3.fromValues(0, 1, 0);
@@ -8199,7 +8199,7 @@ var require_roundedCylinder = __commonJS({
8199
8199
  var require_roundedRectangle = __commonJS({
8200
8200
  "node_modules/@jscad/modeling/src/primitives/roundedRectangle.js"(exports, module) {
8201
8201
  "use strict";
8202
- var { EPS: EPS3, TAU } = require_constants();
8202
+ var { EPS: EPS4, TAU } = require_constants();
8203
8203
  var vec2 = require_vec2();
8204
8204
  var geom2 = require_geom2();
8205
8205
  var { isGTE, isNumberArray } = require_commonChecks();
@@ -8220,7 +8220,7 @@ var require_roundedRectangle = __commonJS({
8220
8220
  if (size4[0] === 0 || size4[1] === 0) return geom2.create();
8221
8221
  if (roundRadius === 0) return rectangle({ center, size: size4 });
8222
8222
  size4 = size4.map((v) => v / 2);
8223
- if (roundRadius > size4[0] - EPS3 || roundRadius > size4[1] - EPS3) throw new Error("roundRadius must be smaller than the radius of all dimensions");
8223
+ if (roundRadius > size4[0] - EPS4 || roundRadius > size4[1] - EPS4) throw new Error("roundRadius must be smaller than the radius of all dimensions");
8224
8224
  const cornersegments = Math.floor(segments / 4);
8225
8225
  const corner0 = vec2.add(vec2.create(), center, [size4[0] - roundRadius, size4[1] - roundRadius]);
8226
8226
  const corner1 = vec2.add(vec2.create(), center, [roundRadius - size4[0], size4[1] - roundRadius]);
@@ -9284,7 +9284,7 @@ var require_repair = __commonJS({
9284
9284
  var require_extrudeWalls = __commonJS({
9285
9285
  "node_modules/@jscad/modeling/src/operations/extrusions/extrudeWalls.js"(exports, module) {
9286
9286
  "use strict";
9287
- var { EPS: EPS3 } = require_constants();
9287
+ var { EPS: EPS4 } = require_constants();
9288
9288
  var vec3 = require_vec3();
9289
9289
  var poly3 = require_poly3();
9290
9290
  var slice = require_slice();
@@ -9324,7 +9324,7 @@ var require_extrudeWalls = __commonJS({
9324
9324
  });
9325
9325
  return newEdges;
9326
9326
  };
9327
- var EPSAREA = EPS3 * EPS3 / 2 * Math.sin(Math.PI / 3);
9327
+ var EPSAREA = EPS4 * EPS4 / 2 * Math.sin(Math.PI / 3);
9328
9328
  var extrudeWalls = (slice0, slice1) => {
9329
9329
  let edges0 = slice.toEdges(slice0);
9330
9330
  let edges1 = slice.toEdges(slice1);
@@ -10307,7 +10307,7 @@ var require_OrthoNormalBasis = __commonJS({
10307
10307
  var require_reTesselateCoplanarPolygons = __commonJS({
10308
10308
  "node_modules/@jscad/modeling/src/operations/modifiers/reTesselateCoplanarPolygons.js"(exports, module) {
10309
10309
  "use strict";
10310
- var { EPS: EPS3 } = require_constants();
10310
+ var { EPS: EPS4 } = require_constants();
10311
10311
  var line2 = require_line2();
10312
10312
  var vec2 = require_vec2();
10313
10313
  var OrthoNormalBasis = require_OrthoNormalBasis();
@@ -10325,7 +10325,7 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10325
10325
  const topy2polygonindexes = /* @__PURE__ */ new Map();
10326
10326
  const ycoordinatetopolygonindexes = /* @__PURE__ */ new Map();
10327
10327
  const ycoordinatebins = /* @__PURE__ */ new Map();
10328
- const ycoordinateBinningFactor = 10 / EPS3;
10328
+ const ycoordinateBinningFactor = 10 / EPS4;
10329
10329
  for (let polygonindex = 0; polygonindex < numpolygons; polygonindex++) {
10330
10330
  const poly3d = sourcepolygons[polygonindex];
10331
10331
  let vertices2d = [];
@@ -10505,7 +10505,7 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10505
10505
  const prevoutpolygon = newoutpolygonrow[newoutpolygonrow.length - 1];
10506
10506
  const d1 = vec2.distance(outpolygon.topleft, prevoutpolygon.topright);
10507
10507
  const d2 = vec2.distance(outpolygon.bottomleft, prevoutpolygon.bottomright);
10508
- if (d1 < EPS3 && d2 < EPS3) {
10508
+ if (d1 < EPS4 && d2 < EPS4) {
10509
10509
  outpolygon.topleft = prevoutpolygon.topleft;
10510
10510
  outpolygon.leftline = prevoutpolygon.leftline;
10511
10511
  outpolygon.bottomleft = prevoutpolygon.bottomleft;
@@ -10522,8 +10522,8 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10522
10522
  for (let ii = 0; ii < prevoutpolygonrow.length; ii++) {
10523
10523
  if (!matchedindexes.has(ii)) {
10524
10524
  const prevpolygon = prevoutpolygonrow[ii];
10525
- if (vec2.distance(prevpolygon.bottomleft, thispolygon.topleft) < EPS3) {
10526
- if (vec2.distance(prevpolygon.bottomright, thispolygon.topright) < EPS3) {
10525
+ if (vec2.distance(prevpolygon.bottomleft, thispolygon.topleft) < EPS4) {
10526
+ if (vec2.distance(prevpolygon.bottomright, thispolygon.topright) < EPS4) {
10527
10527
  matchedindexes.add(ii);
10528
10528
  const v1 = line2.direction(thispolygon.leftline);
10529
10529
  const v2 = line2.direction(prevpolygon.leftline);
@@ -10531,8 +10531,8 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10531
10531
  const v3 = line2.direction(thispolygon.rightline);
10532
10532
  const v4 = line2.direction(prevpolygon.rightline);
10533
10533
  const d2 = v3[0] - v4[0];
10534
- const leftlinecontinues = Math.abs(d1) < EPS3;
10535
- const rightlinecontinues = Math.abs(d2) < EPS3;
10534
+ const leftlinecontinues = Math.abs(d1) < EPS4;
10535
+ const rightlinecontinues = Math.abs(d2) < EPS4;
10536
10536
  const leftlineisconvex = leftlinecontinues || d1 >= 0;
10537
10537
  const rightlineisconvex = rightlinecontinues || d2 >= 0;
10538
10538
  if (leftlineisconvex && rightlineisconvex) {
@@ -10551,7 +10551,7 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10551
10551
  if (!prevcontinuedindexes.has(ii)) {
10552
10552
  const prevpolygon = prevoutpolygonrow[ii];
10553
10553
  prevpolygon.outpolygon.rightpoints.push(prevpolygon.bottomright);
10554
- if (vec2.distance(prevpolygon.bottomright, prevpolygon.bottomleft) > EPS3) {
10554
+ if (vec2.distance(prevpolygon.bottomright, prevpolygon.bottomleft) > EPS4) {
10555
10555
  prevpolygon.outpolygon.leftpoints.push(prevpolygon.bottomleft);
10556
10556
  }
10557
10557
  prevpolygon.outpolygon.leftpoints.reverse();
@@ -10570,7 +10570,7 @@ var require_reTesselateCoplanarPolygons = __commonJS({
10570
10570
  rightpoints: []
10571
10571
  };
10572
10572
  thispolygon.outpolygon.leftpoints.push(thispolygon.topleft);
10573
- if (vec2.distance(thispolygon.topleft, thispolygon.topright) > EPS3) {
10573
+ if (vec2.distance(thispolygon.topleft, thispolygon.topright) > EPS4) {
10574
10574
  thispolygon.outpolygon.rightpoints.push(thispolygon.topright);
10575
10575
  }
10576
10576
  } else {
@@ -10671,7 +10671,7 @@ var require_retessellate = __commonJS({
10671
10671
  var require_mayOverlap = __commonJS({
10672
10672
  "node_modules/@jscad/modeling/src/operations/booleans/mayOverlap.js"(exports, module) {
10673
10673
  "use strict";
10674
- var { EPS: EPS3 } = require_constants();
10674
+ var { EPS: EPS4 } = require_constants();
10675
10675
  var measureBoundingBox2 = require_measureBoundingBox2();
10676
10676
  var mayOverlap = (geometry1, geometry2) => {
10677
10677
  if (geometry1.polygons.length === 0 || geometry2.polygons.length === 0) {
@@ -10683,12 +10683,12 @@ var require_mayOverlap = __commonJS({
10683
10683
  const bounds2 = measureBoundingBox2(geometry2);
10684
10684
  const min22 = bounds2[0];
10685
10685
  const max22 = bounds2[1];
10686
- if (min22[0] - max1[0] > EPS3) return false;
10687
- if (min1[0] - max22[0] > EPS3) return false;
10688
- if (min22[1] - max1[1] > EPS3) return false;
10689
- if (min1[1] - max22[1] > EPS3) return false;
10690
- if (min22[2] - max1[2] > EPS3) return false;
10691
- if (min1[2] - max22[2] > EPS3) return false;
10686
+ if (min22[0] - max1[0] > EPS4) return false;
10687
+ if (min1[0] - max22[0] > EPS4) return false;
10688
+ if (min22[1] - max1[1] > EPS4) return false;
10689
+ if (min1[1] - max22[1] > EPS4) return false;
10690
+ if (min22[2] - max1[2] > EPS4) return false;
10691
+ if (min1[2] - max22[2] > EPS4) return false;
10692
10692
  return true;
10693
10693
  };
10694
10694
  module.exports = mayOverlap;
@@ -10838,12 +10838,12 @@ var require_splitLineSegmentByPlane = __commonJS({
10838
10838
  var require_splitPolygonByPlane = __commonJS({
10839
10839
  "node_modules/@jscad/modeling/src/operations/booleans/trees/splitPolygonByPlane.js"(exports, module) {
10840
10840
  "use strict";
10841
- var { EPS: EPS3 } = require_constants();
10841
+ var { EPS: EPS4 } = require_constants();
10842
10842
  var plane = require_plane();
10843
10843
  var vec3 = require_vec3();
10844
10844
  var poly3 = require_poly3();
10845
10845
  var splitLineSegmentByPlane = require_splitLineSegmentByPlane();
10846
- var EPS_SQUARED = EPS3 * EPS3;
10846
+ var EPS_SQUARED = EPS4 * EPS4;
10847
10847
  var removeConsecutiveDuplicates = (vertices) => {
10848
10848
  const result = [];
10849
10849
  let prevvertex = vertices[vertices.length - 1];
@@ -10871,12 +10871,12 @@ var require_splitPolygonByPlane = __commonJS({
10871
10871
  let hasfront = false;
10872
10872
  let hasback = false;
10873
10873
  const vertexIsBack = [];
10874
- const MINEPS = -EPS3;
10874
+ const MINEPS = -EPS4;
10875
10875
  for (let i = 0; i < numvertices; i++) {
10876
10876
  const t = vec3.dot(splane, vertices[i]) - splane[3];
10877
10877
  const isback = t < MINEPS;
10878
10878
  vertexIsBack.push(isback);
10879
- if (t > EPS3) hasfront = true;
10879
+ if (t > EPS4) hasfront = true;
10880
10880
  if (t < MINEPS) hasback = true;
10881
10881
  }
10882
10882
  if (!hasfront && !hasback) {
@@ -10941,7 +10941,7 @@ var require_splitPolygonByPlane = __commonJS({
10941
10941
  var require_PolygonTreeNode = __commonJS({
10942
10942
  "node_modules/@jscad/modeling/src/operations/booleans/trees/PolygonTreeNode.js"(exports, module) {
10943
10943
  "use strict";
10944
- var { EPS: EPS3 } = require_constants();
10944
+ var { EPS: EPS4 } = require_constants();
10945
10945
  var vec3 = require_vec3();
10946
10946
  var poly3 = require_poly3();
10947
10947
  var splitPolygonByPlane = require_splitPolygonByPlane();
@@ -11044,7 +11044,7 @@ var require_PolygonTreeNode = __commonJS({
11044
11044
  const polygon2 = this.polygon;
11045
11045
  if (polygon2) {
11046
11046
  const bound = poly3.measureBoundingSphere(polygon2);
11047
- const sphereradius = bound[3] + EPS3;
11047
+ const sphereradius = bound[3] + EPS4;
11048
11048
  const spherecenter = bound;
11049
11049
  const d = vec3.dot(splane, spherecenter) - splane[3];
11050
11050
  if (d > sphereradius) {
@@ -11760,7 +11760,7 @@ var require_booleans = __commonJS({
11760
11760
  var require_offsetFromPoints = __commonJS({
11761
11761
  "node_modules/@jscad/modeling/src/operations/expansions/offsetFromPoints.js"(exports, module) {
11762
11762
  "use strict";
11763
- var { EPS: EPS3, TAU } = require_constants();
11763
+ var { EPS: EPS4, TAU } = require_constants();
11764
11764
  var intersect3 = require_intersect();
11765
11765
  var line2 = require_line2();
11766
11766
  var vec2 = require_vec2();
@@ -11773,7 +11773,7 @@ var require_offsetFromPoints = __commonJS({
11773
11773
  segments: 16
11774
11774
  };
11775
11775
  let { delta, corners, closed, segments } = Object.assign({}, defaults, options);
11776
- if (Math.abs(delta) < EPS3) return points;
11776
+ if (Math.abs(delta) < EPS4) return points;
11777
11777
  let rotation = options.closed ? area2(points) : 1;
11778
11778
  if (rotation === 0) rotation = 1;
11779
11779
  const orientation2 = rotation > 0 && delta >= 0 || rotation < 0 && delta < 0;
@@ -11960,7 +11960,7 @@ var require_extrudePolygon = __commonJS({
11960
11960
  var require_expandShell = __commonJS({
11961
11961
  "node_modules/@jscad/modeling/src/operations/expansions/expandShell.js"(exports, module) {
11962
11962
  "use strict";
11963
- var { EPS: EPS3, TAU } = require_constants();
11963
+ var { EPS: EPS4, TAU } = require_constants();
11964
11964
  var mat4 = require_mat4();
11965
11965
  var vec3 = require_vec3();
11966
11966
  var fnNumberSort = require_fnNumberSort();
@@ -12065,7 +12065,7 @@ var require_expandShell = __commonJS({
12065
12065
  const p2 = vec3.add(vec3.create(), endpoint, v1);
12066
12066
  let skip = false;
12067
12067
  if (i >= 0) {
12068
- if (vec3.distance(p1, prevp1) < EPS3) {
12068
+ if (vec3.distance(p1, prevp1) < EPS4) {
12069
12069
  skip = true;
12070
12070
  }
12071
12071
  }
@@ -20991,7 +20991,7 @@ import * as THREE27 from "three";
20991
20991
  // package.json
20992
20992
  var package_default = {
20993
20993
  name: "@tscircuit/3d-viewer",
20994
- version: "0.0.626",
20994
+ version: "0.0.627",
20995
20995
  repository: {
20996
20996
  type: "git",
20997
20997
  url: "https://github.com/tscircuit/3d-viewer"
@@ -21046,7 +21046,7 @@ var package_default = {
21046
21046
  "@tscircuit/circuit-json-util": "*"
21047
21047
  },
21048
21048
  devDependencies: {
21049
- "@tscircuit/flex-utils": "https://jscdn.tscircuit.com/@tscircuit/flex-utils/0.0.5.tgz",
21049
+ "@tscircuit/flex-utils": "https://jscdn.tscircuit.com/@tscircuit/flex-utils/0.0.8.tgz",
21050
21050
  "@biomejs/biome": "^2.1.4",
21051
21051
  "@chromatic-com/storybook": "^1.9.0",
21052
21052
  "@jscad/modeling": "^2.12.5",
@@ -28130,30 +28130,265 @@ function signedArea(data, start, end, dim) {
28130
28130
  }
28131
28131
 
28132
28132
  // node_modules/@tscircuit/flex-utils/dist/index.js
28133
+ var PcbFoldError = class extends Error {
28134
+ constructor(issue) {
28135
+ super(issue.message);
28136
+ this.issue = issue;
28137
+ this.name = "PcbFoldError";
28138
+ }
28139
+ issue;
28140
+ };
28133
28141
  var EPS = 1e-7;
28134
- function createPcbFold(records, thickness) {
28142
+ var samePoint = (a, b) => Math.hypot(a.x - b.x, a.y - b.y) < EPS;
28143
+ function pointInPolygon(point, polygon2, includeBoundary = true) {
28144
+ let inside = false;
28145
+ for (let i = 0; i < polygon2.length; i++) {
28146
+ const a = polygon2[i], b = polygon2[(i + 1) % polygon2.length];
28147
+ const cross4 = (point.x - a.x) * (b.y - a.y) - (point.y - a.y) * (b.x - a.x);
28148
+ if (Math.abs(cross4) < EPS * Math.max(1, Math.hypot(b.x - a.x, b.y - a.y)) && point.x >= Math.min(a.x, b.x) - EPS && point.x <= Math.max(a.x, b.x) + EPS && point.y >= Math.min(a.y, b.y) - EPS && point.y <= Math.max(a.y, b.y) + EPS)
28149
+ return includeBoundary;
28150
+ if (a.y > point.y !== b.y > point.y && point.x < a.x + (b.x - a.x) * (point.y - a.y) / (b.y - a.y))
28151
+ inside = !inside;
28152
+ }
28153
+ return inside;
28154
+ }
28155
+ function lineCrossings(outline, nx, ny, d) {
28156
+ const crossings = [];
28157
+ for (let edge = 0; edge < outline.length; edge++) {
28158
+ const a = outline[edge], b = outline[(edge + 1) % outline.length];
28159
+ const da = a.x * nx + a.y * ny - d, db = b.x * nx + b.y * ny - d;
28160
+ if (Math.abs(da) < EPS && Math.abs(db) < EPS) continue;
28161
+ if (da > EPS && db > EPS || da < -EPS && db < -EPS) continue;
28162
+ if (Math.abs(da - db) < EPS) continue;
28163
+ const fraction = Math.max(0, Math.min(1, da / (da - db)));
28164
+ const point = {
28165
+ x: a.x + (b.x - a.x) * fraction,
28166
+ y: a.y + (b.y - a.y) * fraction
28167
+ };
28168
+ if (crossings.some((crossing) => samePoint(crossing.point, point))) continue;
28169
+ crossings.push(
28170
+ fraction > 1 - EPS ? { point: b, edge: (edge + 1) % outline.length, fraction: 0 } : { point, edge, fraction }
28171
+ );
28172
+ }
28173
+ return crossings.sort(
28174
+ (a, b) => -ny * a.point.x + nx * a.point.y - (-ny * b.point.x + nx * b.point.y)
28175
+ );
28176
+ }
28177
+ function movingPolygon(outline, a, b, nx, ny) {
28178
+ const polygon2 = [];
28179
+ for (let edge = 0; edge < outline.length; edge++) {
28180
+ polygon2.push(outline[edge]);
28181
+ for (const crossing of [a, b].filter((c) => c.edge === edge).sort((a2, b2) => a2.fraction - b2.fraction))
28182
+ if (crossing.fraction > EPS) polygon2.push(crossing.point);
28183
+ }
28184
+ const start = polygon2.findIndex((p) => samePoint(p, a.point));
28185
+ const end = polygon2.findIndex((p) => samePoint(p, b.point));
28186
+ if (start < 0 || end < 0)
28187
+ throw new Error("Cannot resolve PCB bend boundary crossings");
28188
+ const path = (from, to) => {
28189
+ const points = [polygon2[from]];
28190
+ while (from !== to) {
28191
+ from = (from + 1) % polygon2.length;
28192
+ points.push(polygon2[from]);
28193
+ }
28194
+ return points;
28195
+ };
28196
+ const area2 = outline.reduce((sum, p, i) => {
28197
+ const next = outline[(i + 1) % outline.length];
28198
+ return sum + p.x * next.y - next.x * p.y;
28199
+ }, 0);
28200
+ const side = Math.sign(area2) * ((b.point.y - a.point.y) * nx - (b.point.x - a.point.x) * ny);
28201
+ return side > 0 ? path(start, end) : path(end, start);
28202
+ }
28203
+ function getFiniteBendRegion({
28204
+ bendId,
28205
+ outline,
28206
+ nx,
28207
+ ny,
28208
+ center,
28209
+ proximal,
28210
+ axisMin,
28211
+ axisMax
28212
+ }) {
28213
+ const finiteCrossings = lineCrossings(outline, nx, ny, center).filter(
28214
+ ({ point }) => {
28215
+ const along = -ny * point.x + nx * point.y;
28216
+ return along >= axisMin - EPS && along <= axisMax + EPS;
28217
+ }
28218
+ );
28219
+ if (finiteCrossings.length !== 2)
28220
+ throw new PcbFoldError({
28221
+ code: "invalid_finite_bend_region",
28222
+ message: "A finite PCB bend must cut one board cross-section from boundary to boundary",
28223
+ bendId
28224
+ });
28225
+ const [a, b] = finiteCrossings;
28226
+ const middle = {
28227
+ x: (a.point.x + b.point.x) / 2,
28228
+ y: (a.point.y + b.point.y) / 2
28229
+ };
28230
+ if (!pointInPolygon(middle, outline, false))
28231
+ throw new PcbFoldError({
28232
+ code: "invalid_finite_bend_region",
28233
+ message: "A finite PCB bend must pass through the board interior",
28234
+ bendId
28235
+ });
28236
+ const centerRegion = movingPolygon(outline, a, b, nx, ny);
28237
+ const alongMiddle = -ny * middle.x + nx * middle.y;
28238
+ const proximalCrossings = lineCrossings(outline, nx, ny, proximal);
28239
+ for (let i = 0; i + 1 < proximalCrossings.length; i++) {
28240
+ const left = proximalCrossings[i], right = proximalCrossings[i + 1];
28241
+ const lo = -ny * left.point.x + nx * left.point.y;
28242
+ const hi = -ny * right.point.x + nx * right.point.y;
28243
+ if (alongMiddle <= lo + EPS || alongMiddle >= hi - EPS) continue;
28244
+ const midpoint2 = {
28245
+ x: (left.point.x + right.point.x) / 2,
28246
+ y: (left.point.y + right.point.y) / 2
28247
+ };
28248
+ if (!pointInPolygon(midpoint2, outline, false)) continue;
28249
+ const region = movingPolygon(outline, left, right, nx, ny);
28250
+ if (region.some((p) => p.x * nx + p.y * ny < proximal - EPS) || centerRegion.some((p) => !pointInPolygon(p, region)) || region.some(
28251
+ (p) => p.x * nx + p.y * ny >= center - EPS && !pointInPolygon(p, centerRegion)
28252
+ ))
28253
+ break;
28254
+ return region;
28255
+ }
28256
+ throw new PcbFoldError({
28257
+ code: "invalid_finite_bend_region",
28258
+ message: "The finite PCB bend zone must remain in one connected board cross-section",
28259
+ bendId
28260
+ });
28261
+ }
28262
+ function polygonsTouch(a, b) {
28263
+ if (a.some((p) => pointInPolygon(p, b)) || b.some((p) => pointInPolygon(p, a)))
28264
+ return true;
28265
+ const cross4 = (p, q, r) => (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
28266
+ for (let i = 0; i < a.length; i++)
28267
+ for (let j = 0; j < b.length; j++) {
28268
+ const p = a[i], q = a[(i + 1) % a.length], r = b[j], s = b[(j + 1) % b.length];
28269
+ if (cross4(p, q, r) * cross4(p, q, s) < -EPS && cross4(r, s, p) * cross4(r, s, q) < -EPS)
28270
+ return true;
28271
+ }
28272
+ return false;
28273
+ }
28274
+ function polygonContainsPolygon(boundary, polygon2) {
28275
+ if (polygon2.some((p) => !pointInPolygon(p, boundary))) return false;
28276
+ for (let i = 0; i < polygon2.length; i++) {
28277
+ const a = polygon2[i], b = polygon2[(i + 1) % polygon2.length];
28278
+ const dx = b.x - a.x, dy = b.y - a.y;
28279
+ const lengthSquared = dx * dx + dy * dy;
28280
+ if (lengthSquared < EPS * EPS) continue;
28281
+ const crossings = [0, 1];
28282
+ for (let j = 0; j < boundary.length; j++) {
28283
+ const c = boundary[j], d = boundary[(j + 1) % boundary.length];
28284
+ const ex = d.x - c.x, ey = d.y - c.y;
28285
+ const denominator = dx * ey - dy * ex;
28286
+ const cx = c.x - a.x, cy = c.y - a.y;
28287
+ if (Math.abs(denominator) < EPS) {
28288
+ if (Math.abs(cx * dy - cy * dx) > EPS) continue;
28289
+ for (const p of [c, d]) {
28290
+ const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared;
28291
+ if (t > 0 && t < 1) crossings.push(t);
28292
+ }
28293
+ } else {
28294
+ const t = (cx * ey - cy * ex) / denominator;
28295
+ const u = (cx * dy - cy * dx) / denominator;
28296
+ if (t > 0 && t < 1 && u >= -EPS && u <= 1 + EPS) crossings.push(t);
28297
+ }
28298
+ }
28299
+ crossings.sort((a2, b2) => a2 - b2);
28300
+ for (let j = 1; j < crossings.length; j++) {
28301
+ const t = (crossings[j - 1] + crossings[j]) / 2;
28302
+ if (!pointInPolygon({ x: a.x + t * dx, y: a.y + t * dy }, boundary))
28303
+ return false;
28304
+ }
28305
+ }
28306
+ return true;
28307
+ }
28308
+ var EPS2 = 1e-7;
28309
+ function orderPcbBends(bends) {
28310
+ const sorted = [...bends].sort(
28311
+ (a, b) => a.start - b.start || a.nx - b.nx || a.ny - b.ny || a.id.localeCompare(b.id)
28312
+ );
28313
+ const parents = /* @__PURE__ */ new Map();
28314
+ const overlap = (bend) => {
28315
+ throw new PcbFoldError({
28316
+ code: "overlapping_bend_zones",
28317
+ message: "Overlapping PCB bend zones or incompatible moving regions are not supported",
28318
+ bendId: bend.id
28319
+ });
28320
+ };
28321
+ for (let i = 0; i < sorted.length; i++)
28322
+ for (let j = i + 1; j < sorted.length; j++) {
28323
+ const a = sorted[i], b = sorted[j];
28324
+ if (a.movingOutline && b.movingOutline) {
28325
+ if (!polygonsTouch(a.movingOutline, b.movingOutline)) continue;
28326
+ const aContainsB = polygonContainsPolygon(
28327
+ a.movingOutline,
28328
+ b.movingOutline
28329
+ );
28330
+ const bContainsA = polygonContainsPolygon(
28331
+ b.movingOutline,
28332
+ a.movingOutline
28333
+ );
28334
+ if (aContainsB === bContainsA) overlap(b);
28335
+ const parent = aContainsB ? a : b, child = aContainsB ? b : a;
28336
+ if (child.movingOutline.some(
28337
+ (p) => p.x * parent.nx + p.y * parent.ny < parent.end - EPS2
28338
+ ))
28339
+ overlap(child);
28340
+ parents.set(child, [...parents.get(child) ?? [], parent]);
28341
+ } else {
28342
+ if (Math.abs(a.nx - b.nx) > EPS2 || Math.abs(a.ny - b.ny) > EPS2)
28343
+ throw new PcbFoldError({
28344
+ code: "nonparallel_bends",
28345
+ message: "Nonparallel PCB bends or bends with opposite moving directions require a board outline to resolve their moving regions"
28346
+ });
28347
+ if (b.start < a.end - EPS2) overlap(b);
28348
+ parents.set(b, [...parents.get(b) ?? [], a]);
28349
+ }
28350
+ }
28351
+ const ordered = [], visited = /* @__PURE__ */ new Set();
28352
+ const visit = (bend) => {
28353
+ if (visited.has(bend)) return;
28354
+ for (const parent of parents.get(bend) ?? []) visit(parent);
28355
+ visited.add(bend);
28356
+ ordered.push(bend);
28357
+ };
28358
+ for (const bend of sorted) visit(bend);
28359
+ return ordered;
28360
+ }
28361
+ function createPcbFold(records, thickness, { outline } = {}) {
28135
28362
  if (!Number.isFinite(thickness) || thickness < 0)
28136
28363
  throw new Error("Board thickness must be finite and nonnegative");
28364
+ if (outline && (outline.length < 3 || outline.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))))
28365
+ throw new Error(
28366
+ "A finite board outline with at least three points is required"
28367
+ );
28137
28368
  const bends = records.map((b) => {
28138
28369
  const { start, end, bend_angle: degrees, bend_radius: radius } = b;
28139
28370
  const values = [start?.x, start?.y, end?.x, end?.y, degrees, radius];
28140
28371
  if (values.some((v) => !Number.isFinite(v)) || radius <= thickness / 2 || !["left", "right"].includes(b.bend_side)) {
28141
- throw new Error(
28142
- `Invalid PCB bend ${b.pcb_bend_id}: finite geometry and radius greater than half the board thickness are required`
28143
- );
28372
+ throw new PcbFoldError({
28373
+ code: "invalid_bend_geometry",
28374
+ message: `Invalid PCB bend ${b.pcb_bend_id}: finite geometry and radius greater than half the board thickness are required`,
28375
+ bendId: b.pcb_bend_id
28376
+ });
28144
28377
  }
28145
28378
  const length6 = Math.hypot(end.x - start.x, end.y - start.y);
28146
- if (length6 < EPS || Math.abs(degrees) > 180)
28147
- throw new Error(
28148
- `Unsupported PCB bend ${b.pcb_bend_id}: distinct endpoints and angles within \xB1180 degrees are required`
28149
- );
28379
+ if (length6 < EPS2 || Math.abs(degrees) > 180)
28380
+ throw new PcbFoldError({
28381
+ code: "unsupported_bend_geometry",
28382
+ message: `Unsupported PCB bend ${b.pcb_bend_id}: distinct endpoints and angles within \xB1180 degrees are required`,
28383
+ bendId: b.pcb_bend_id
28384
+ });
28150
28385
  const sign = b.bend_side === "right" ? 1 : -1;
28151
28386
  const nx = sign * (end.y - start.y) / length6;
28152
28387
  const ny = -sign * (end.x - start.x) / length6;
28153
28388
  const angle3 = degrees * Math.PI / 180;
28154
28389
  const width = radius * Math.abs(angle3);
28155
28390
  const center = start.x * nx + start.y * ny;
28156
- return {
28391
+ const geometry = {
28157
28392
  id: b.pcb_bend_id,
28158
28393
  nx,
28159
28394
  ny,
@@ -28170,21 +28405,21 @@ function createPcbFold(records, thickness) {
28170
28405
  -ny * end.x + nx * end.y
28171
28406
  )
28172
28407
  };
28173
- }).filter((b) => Math.abs(b.angle) > EPS);
28174
- const first = bends[0];
28175
- if (first && bends.some(
28176
- (b) => Math.abs(b.nx - first.nx) > EPS || Math.abs(b.ny - first.ny) > EPS
28177
- )) {
28178
- throw new Error(
28179
- "Folded PCB rendering currently requires parallel bends with the same moving direction"
28180
- );
28181
- }
28182
- bends.sort((a, b) => a.start - b.start);
28183
- for (let i = 1; i < bends.length; i++) {
28184
- if (bends[i].start < bends[i - 1].end - EPS)
28185
- throw new Error("Overlapping PCB bend zones are not supported");
28186
- }
28187
- const distalFirst = [...bends].reverse();
28408
+ if (outline && Math.abs(angle3) > EPS2)
28409
+ geometry.movingOutline = getFiniteBendRegion({
28410
+ bendId: b.pcb_bend_id,
28411
+ outline,
28412
+ nx,
28413
+ ny,
28414
+ center,
28415
+ proximal: geometry.start,
28416
+ axisMin: geometry.axisMin,
28417
+ axisMax: geometry.axisMax
28418
+ });
28419
+ return geometry;
28420
+ }).filter((b) => Math.abs(b.angle) > EPS2);
28421
+ const proximalFirst = orderPcbBends(bends);
28422
+ const distalFirst = [...proximalFirst].reverse();
28188
28423
  const transform = ({
28189
28424
  p,
28190
28425
  anchor,
@@ -28192,6 +28427,7 @@ function createPcbFold(records, thickness) {
28192
28427
  }) => {
28193
28428
  let result = { ...p };
28194
28429
  for (const b of distalFirst) {
28430
+ if (b.movingOutline && !pointInPolygon(anchor, b.movingOutline)) continue;
28195
28431
  const width = b.end - b.start;
28196
28432
  const s = anchor.x * b.nx + anchor.y * b.ny - b.start;
28197
28433
  const q = Math.max(0, Math.min(width, s));
@@ -28232,21 +28468,28 @@ function createPcbFold(records, thickness) {
28232
28468
  return { x: dot6(axes2[0]), y: dot6(axes2[1]), z: dot6(axes2[2]) };
28233
28469
  };
28234
28470
  return {
28235
- bends,
28471
+ bends: proximalFirst,
28236
28472
  inversePoint: (p, anchor) => inverse4({ p, anchor, direction: false }),
28237
28473
  inverseDirection: (p, anchor) => inverse4({ p, anchor, direction: true }),
28238
28474
  point: (p, anchor = p) => transform({ p, anchor, direction: false }),
28239
28475
  direction: (p, anchor) => transform({ p, anchor, direction: true }),
28240
28476
  assertRigid(points, label) {
28241
28477
  for (const b of bends) {
28242
- const ds = points.map((p) => p.x * b.nx + p.y * b.ny);
28243
- if (Math.max(...ds) > b.start + EPS && Math.min(...ds) < b.end - EPS)
28244
- throw new Error(`${label} intersects PCB bend zone ${b.id}`);
28478
+ const width = b.end - b.start;
28479
+ const progress = points.map(
28480
+ (p) => b.movingOutline && !pointInPolygon(p, b.movingOutline) ? 0 : Math.max(0, Math.min(width, p.x * b.nx + p.y * b.ny - b.start))
28481
+ );
28482
+ if (Math.max(...progress) > EPS2 && Math.min(...progress) < width - EPS2)
28483
+ throw new PcbFoldError({
28484
+ code: "rigid_bend_zone_intersection",
28485
+ message: `${label} intersects PCB bend zone ${b.id}`,
28486
+ bendId: b.id
28487
+ });
28245
28488
  }
28246
28489
  }
28247
28490
  };
28248
28491
  }
28249
- var EPS2 = 1e-7;
28492
+ var EPS3 = 1e-7;
28250
28493
  function boundsOfTriangles(triangles) {
28251
28494
  const points = triangles.flatMap((t) => t.vertices);
28252
28495
  if (!points.length)
@@ -28276,7 +28519,7 @@ function foldSurfaceMesh(mesh, fold) {
28276
28519
  d
28277
28520
  }) => {
28278
28521
  const distances = polygon2.map(({ p }) => nx * p.x + ny * p.y - d);
28279
- if (Math.min(...distances) >= -EPS2 || Math.max(...distances) <= EPS2)
28522
+ if (Math.min(...distances) >= -EPS3 || Math.max(...distances) <= EPS3)
28280
28523
  return [polygon2];
28281
28524
  const halves = [[], []];
28282
28525
  for (let side = 0; side < 2; side++) {
@@ -28284,8 +28527,8 @@ function foldSurfaceMesh(mesh, fold) {
28284
28527
  for (let i = 0; i < polygon2.length; i++) {
28285
28528
  const a = polygon2[i], next = (i + 1) % polygon2.length, b = polygon2[next];
28286
28529
  const da = distances[i] * sign, db = distances[next] * sign;
28287
- if (da >= -EPS2) halves[side].push(a);
28288
- if (da > EPS2 && db < -EPS2 || da < -EPS2 && db > EPS2) {
28530
+ if (da >= -EPS3) halves[side].push(a);
28531
+ if (da > EPS3 && db < -EPS3 || da < -EPS3 && db > EPS3) {
28289
28532
  const t = da / (da - db);
28290
28533
  halves[side].push({
28291
28534
  p: {
@@ -28321,14 +28564,17 @@ function foldSurfaceMesh(mesh, fold) {
28321
28564
  );
28322
28565
  for (const polygon2 of polygons) {
28323
28566
  for (const b of fold.bends) {
28567
+ if (b.movingOutline) continue;
28324
28568
  const mid = polygon2.reduce((sum, { p }) => sum + p.x * b.nx + p.y * b.ny, 0) / polygon2.length;
28325
- if (mid > b.start + EPS2 && mid < b.end - EPS2 && polygon2.some(({ p }) => {
28569
+ if (mid > b.start + EPS3 && mid < b.end - EPS3 && polygon2.some(({ p }) => {
28326
28570
  const along = -b.ny * p.x + b.nx * p.y;
28327
- return along < b.axisMin - EPS2 || along > b.axisMax + EPS2;
28571
+ return along < b.axisMin - EPS3 || along > b.axisMax + EPS3;
28328
28572
  }))
28329
- throw new Error(
28330
- `PCB bend ${b.id} must span the full board cross-section through its bend zone`
28331
- );
28573
+ throw new PcbFoldError({
28574
+ code: "incomplete_bend_cross_section",
28575
+ message: `PCB bend ${b.id} must span the full board cross-section through its bend zone`,
28576
+ bendId: b.id
28577
+ });
28332
28578
  }
28333
28579
  for (let i = 1; i + 1 < polygon2.length; i++) {
28334
28580
  const verts = [polygon2[0], polygon2[i], polygon2[i + 1]];
@@ -28454,7 +28700,12 @@ function getCadFoldContext(cadComponent, json) {
28454
28700
  (e) => e.type === "pcb_bend" && e.pcb_board_id === board.pcb_board_id
28455
28701
  );
28456
28702
  return {
28457
- fold: createPcbFold(bends, board.thickness ?? 1.6),
28703
+ fold: createPcbFold(bends, board.thickness ?? 1.6, {
28704
+ outline: board.outline?.map((p) => ({
28705
+ x: p.x - board.center.x,
28706
+ y: p.y - board.center.y
28707
+ }))
28708
+ }),
28458
28709
  boardCenter: board.center,
28459
28710
  flatMount: pcb.center,
28460
28711
  defaultRotation: {
@@ -28551,6 +28802,43 @@ function extrudePolygon({
28551
28802
  }
28552
28803
  return { triangles, boundingBox: boundsOfTriangles(triangles) };
28553
28804
  }
28805
+ function foldRigidMesh(mesh, fold, { flatAnchor, label = "Rigid mesh", mount }) {
28806
+ if (mount) fold.assertRigid([mount], `${label} mount`);
28807
+ fold.assertRigid(
28808
+ mesh.triangles.flatMap((t) => t.vertices),
28809
+ label
28810
+ );
28811
+ const triangles = mesh.triangles.map((t) => ({
28812
+ ...t,
28813
+ vertices: t.vertices.map(
28814
+ (p) => fold.point(p, flatAnchor)
28815
+ ),
28816
+ normal: fold.direction(t.normal, flatAnchor)
28817
+ }));
28818
+ return { ...mesh, triangles, boundingBox: boundsOfTriangles(triangles) };
28819
+ }
28820
+ function getStiffenerOutline(stiffener) {
28821
+ if (stiffener.shape === "polygon") return stiffener.outline;
28822
+ if (!(stiffener.width > 0 && stiffener.height > 0))
28823
+ throw new Error("Invalid stiffener dimensions");
28824
+ const a = (stiffener.rotation ?? 0) * Math.PI / 180;
28825
+ return [
28826
+ [-1, -1],
28827
+ [1, -1],
28828
+ [1, 1],
28829
+ [-1, 1]
28830
+ ].map(([x, y]) => ({
28831
+ x: stiffener.center.x + x * stiffener.width / 2 * Math.cos(a) - y * stiffener.height / 2 * Math.sin(a),
28832
+ y: stiffener.center.y + x * stiffener.width / 2 * Math.sin(a) + y * stiffener.height / 2 * Math.cos(a)
28833
+ }));
28834
+ }
28835
+ function getStiffenerAnchor(outline) {
28836
+ return {
28837
+ x: outline.reduce((s, p) => s + p.x, 0) / outline.length,
28838
+ y: outline.reduce((s, p) => s + p.y, 0) / outline.length,
28839
+ z: 0
28840
+ };
28841
+ }
28554
28842
  function createStiffenerMesh({
28555
28843
  stiffener,
28556
28844
  boardThickness,
@@ -28559,20 +28847,7 @@ function createStiffenerMesh({
28559
28847
  const adhesive = stiffener.adhesive_thickness ?? 0;
28560
28848
  if (!Number.isFinite(adhesive) || adhesive < 0 || !Number.isFinite(stiffener.thickness) || stiffener.thickness <= 0 || !["top", "bottom"].includes(stiffener.layer))
28561
28849
  throw new Error(`Invalid PCB stiffener ${stiffener.pcb_stiffener_id}`);
28562
- const outline = stiffener.shape === "polygon" ? stiffener.outline : (() => {
28563
- if (!(stiffener.width > 0 && stiffener.height > 0))
28564
- throw new Error("Invalid stiffener dimensions");
28565
- const a = (stiffener.rotation ?? 0) * Math.PI / 180;
28566
- return [
28567
- [-1, -1],
28568
- [1, -1],
28569
- [1, 1],
28570
- [-1, 1]
28571
- ].map(([x, y]) => ({
28572
- x: stiffener.center.x + x * stiffener.width / 2 * Math.cos(a) - y * stiffener.height / 2 * Math.sin(a),
28573
- y: stiffener.center.y + x * stiffener.width / 2 * Math.sin(a) + y * stiffener.height / 2 * Math.cos(a)
28574
- }));
28575
- })();
28850
+ const outline = getStiffenerOutline(stiffener);
28576
28851
  const near = boardThickness / 2 + adhesive, far = near + stiffener.thickness;
28577
28852
  const mesh = extrudePolygon({
28578
28853
  outline,
@@ -28580,21 +28855,10 @@ function createStiffenerMesh({
28580
28855
  top: stiffener.layer === "top" ? far : -near
28581
28856
  });
28582
28857
  if (!fold) return mesh;
28583
- fold.assertRigid(
28584
- mesh.triangles.flatMap((t) => t.vertices),
28585
- stiffener.pcb_stiffener_id
28586
- );
28587
- const anchor = {
28588
- x: outline.reduce((s, p) => s + p.x, 0) / outline.length,
28589
- y: outline.reduce((s, p) => s + p.y, 0) / outline.length,
28590
- z: 0
28591
- };
28592
- const triangles = mesh.triangles.map((t) => ({
28593
- ...t,
28594
- vertices: t.vertices.map((p) => fold.point(p, anchor)),
28595
- normal: fold.direction(t.normal, anchor)
28596
- }));
28597
- return { ...mesh, triangles, boundingBox: boundsOfTriangles(triangles) };
28858
+ return foldRigidMesh(mesh, fold, {
28859
+ flatAnchor: getStiffenerAnchor(outline),
28860
+ label: stiffener.pcb_stiffener_id
28861
+ });
28598
28862
  }
28599
28863
 
28600
28864
  // src/CadViewerManifold.tsx
@@ -29984,14 +30248,60 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson, visibility) => {
29984
30248
 
29985
30249
  // src/utils/flex-meshes.ts
29986
30250
  import * as THREE53 from "three";
29987
- function meshToSurface(mesh, center) {
30251
+ function restoreOutlinePrecision(point, outline, tolerance) {
30252
+ let distanceSquared = tolerance * tolerance;
30253
+ let closest;
30254
+ for (let i = 0; i < outline.length; i++) {
30255
+ const a = outline[i];
30256
+ const b = outline[(i + 1) % outline.length];
30257
+ const dx = b.x - a.x;
30258
+ const dy = b.y - a.y;
30259
+ const lengthSquared = dx * dx + dy * dy;
30260
+ if (lengthSquared === 0) continue;
30261
+ const t = Math.max(
30262
+ 0,
30263
+ Math.min(
30264
+ 1,
30265
+ ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared
30266
+ )
30267
+ );
30268
+ const x = a.x + t * dx;
30269
+ const y = a.y + t * dy;
30270
+ const distance5 = (x - point.x) ** 2 + (y - point.y) ** 2;
30271
+ if (distance5 <= distanceSquared) {
30272
+ distanceSquared = distance5;
30273
+ closest = { x, y };
30274
+ }
30275
+ }
30276
+ if (closest) point.set(closest.x, closest.y, point.z);
30277
+ }
30278
+ function meshToSurface(mesh, center, outline) {
29988
30279
  mesh.updateMatrix();
29989
30280
  const geometry = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
29990
30281
  const position = geometry.getAttribute("position"), uv = geometry.getAttribute("uv");
29991
30282
  const triangles = [];
29992
30283
  for (let i = 0; i < position.count; i += 3) {
29993
30284
  const vertices = [0, 1, 2].map((j) => {
29994
- const v = new THREE53.Vector3().fromBufferAttribute(position, i + j).applyMatrix4(mesh.matrix);
30285
+ const v = new THREE53.Vector3().fromBufferAttribute(position, i + j);
30286
+ const localMagnitude = Math.max(
30287
+ 1,
30288
+ Math.abs(v.x),
30289
+ Math.abs(v.y),
30290
+ Math.abs(v.z)
30291
+ );
30292
+ v.applyMatrix4(mesh.matrix);
30293
+ if (outline && position.array instanceof Float32Array) {
30294
+ const m = mesh.matrix.elements;
30295
+ const xyScale = Math.max(
30296
+ Math.abs(m[0]) + Math.abs(m[4]) + Math.abs(m[8]),
30297
+ Math.abs(m[1]) + Math.abs(m[5]) + Math.abs(m[9])
30298
+ );
30299
+ restoreOutlinePrecision(
30300
+ v,
30301
+ outline,
30302
+ localMagnitude * 2 ** -22 * xyScale + Math.max(1, Math.abs(v.x), Math.abs(v.y)) * Number.EPSILON * 4
30303
+ );
30304
+ }
29995
30305
  return { x: v.x - center.x, y: v.y - center.y, z: v.z };
29996
30306
  });
29997
30307
  const [a, b, c] = vertices;
@@ -30048,13 +30358,21 @@ function createFlexMeshes(geometryMeshes, textureMeshes, json, foldPcbs) {
30048
30358
  throw new Error(
30049
30359
  "Flex rendering requires one board, matching board references, and no panel"
30050
30360
  );
30051
- const fold = foldPcbs ? createPcbFold(bends, board.thickness ?? 1.6) : void 0;
30361
+ const fold = foldPcbs ? createPcbFold(bends, board.thickness ?? 1.6, {
30362
+ outline: board.outline?.map((point) => ({
30363
+ x: point.x - board.center.x,
30364
+ y: point.y - board.center.y
30365
+ }))
30366
+ }) : void 0;
30052
30367
  const body = geometryMeshes.find((m) => m.name === "board-geom");
30053
- const flatBody = body ? meshToSurface(body, board.center) : void 0;
30368
+ const flatBody = fold && body ? meshToSurface(body, board.center, board.outline) : void 0;
30054
30369
  const geometry = fold ? geometryMeshes.map(
30055
30370
  (mesh) => replaceGeometry(
30056
30371
  mesh,
30057
- foldSurfaceMesh(meshToSurface(mesh, board.center), fold),
30372
+ foldSurfaceMesh(
30373
+ mesh === body && flatBody ? flatBody : meshToSurface(mesh, board.center, board.outline),
30374
+ fold
30375
+ ),
30058
30376
  board
30059
30377
  )
30060
30378
  ) : [...geometryMeshes];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.627",
3
+ "version": "0.0.628",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/tscircuit/3d-viewer"
@@ -55,7 +55,7 @@
55
55
  "@tscircuit/circuit-json-util": "*"
56
56
  },
57
57
  "devDependencies": {
58
- "@tscircuit/flex-utils": "https://jscdn.tscircuit.com/@tscircuit/flex-utils/0.0.5.tgz",
58
+ "@tscircuit/flex-utils": "https://jscdn.tscircuit.com/@tscircuit/flex-utils/0.0.8.tgz",
59
59
  "@biomejs/biome": "^2.1.4",
60
60
  "@chromatic-com/storybook": "^1.9.0",
61
61
  "@jscad/modeling": "^2.12.5",