circuit-json-to-gltf 0.0.71 → 0.0.73

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.d.ts CHANGED
@@ -196,6 +196,38 @@ declare const COORDINATE_TRANSFORMS: {
196
196
  readonly OBJ_Z_UP_TO_Y_UP: CoordinateTransformConfig;
197
197
  };
198
198
 
199
+ interface CameraFitOptions {
200
+ /**
201
+ * Target-to-camera direction vector used for solved camera position.
202
+ */
203
+ direction?: readonly [number, number, number];
204
+ /**
205
+ * Vertical field of view in degrees.
206
+ */
207
+ fov?: number;
208
+ /**
209
+ * Aspect ratio (width / height) used for horizontal fit calculations.
210
+ */
211
+ aspectRatio?: number;
212
+ /**
213
+ * Focal length in millimeters. If provided with sensorHeight,
214
+ * it is used instead of fov.
215
+ */
216
+ focalLength?: number;
217
+ /**
218
+ * Sensor height in millimeters for focalLength->fov conversion.
219
+ */
220
+ sensorHeight?: number;
221
+ }
222
+ /**
223
+ * Calculate optimal camera position for PCB viewing based on circuit dimensions
224
+ */
225
+ declare function getBestCameraPosition(circuitJson: CircuitJson): {
226
+ camPos: readonly [number, number, number];
227
+ lookAt: readonly [number, number, number];
228
+ fov: number;
229
+ };
230
+
199
231
  declare function convertCircuitJsonToGltf(circuitJson: CircuitJson, options?: ConversionOptions): Promise<ArrayBuffer | object>;
200
232
 
201
233
  interface Point {
@@ -208,4 +240,4 @@ interface BRepShape {
208
240
  is_negative?: boolean;
209
241
  }
210
242
 
211
- export { type BRepShape, type BoardRenderOptions, type BoundingBox, type Box3D, COORDINATE_TRANSFORMS, type Camera3D, type CircuitTo3DOptions, type Color, type ConversionOptions, type CoordinateTransformConfig, type GLTFExportOptions, type LayerRef, type Light3D, type OBJMaterial, type OBJMesh, type Point, type Point3, type STLMesh, type Scene3D, type Size3, type Triangle, applyCoordinateTransform, clearGLBCache, clearOBJCache, clearSTLCache, convertCircuitJsonTo3D, convertCircuitJsonToGltf, convertSceneToGLTF, loadGLB, loadOBJ, loadSTL, renderBoardLayer, renderBoardTextures, transformTriangles };
243
+ export { type BRepShape, type BoardRenderOptions, type BoundingBox, type Box3D, COORDINATE_TRANSFORMS, type Camera3D, type CameraFitOptions, type CircuitTo3DOptions, type Color, type ConversionOptions, type CoordinateTransformConfig, type GLTFExportOptions, type LayerRef, type Light3D, type OBJMaterial, type OBJMesh, type Point, type Point3, type STLMesh, type Scene3D, type Size3, type Triangle, applyCoordinateTransform, clearGLBCache, clearOBJCache, clearSTLCache, convertCircuitJsonTo3D, convertCircuitJsonToGltf, convertSceneToGLTF, getBestCameraPosition, loadGLB, loadOBJ, loadSTL, renderBoardLayer, renderBoardTextures, transformTriangles };
package/dist/index.js CHANGED
@@ -446,8 +446,8 @@ var require_add2 = __commonJS({
446
446
  var require_dot = __commonJS({
447
447
  "node_modules/@jscad/modeling/src/maths/vec3/dot.js"(exports, module) {
448
448
  "use strict";
449
- var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
450
- module.exports = dot;
449
+ var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
450
+ module.exports = dot2;
451
451
  }
452
452
  });
453
453
 
@@ -455,7 +455,7 @@ var require_dot = __commonJS({
455
455
  var require_angle = __commonJS({
456
456
  "node_modules/@jscad/modeling/src/maths/vec3/angle.js"(exports, module) {
457
457
  "use strict";
458
- var dot = require_dot();
458
+ var dot2 = require_dot();
459
459
  var angle = (a, b) => {
460
460
  const ax = a[0];
461
461
  const ay = a[1];
@@ -466,7 +466,7 @@ var require_angle = __commonJS({
466
466
  const mag1 = Math.sqrt(ax * ax + ay * ay + az * az);
467
467
  const mag2 = Math.sqrt(bx * bx + by * by + bz * bz);
468
468
  const mag = mag1 * mag2;
469
- const cosine = mag && dot(a, b) / mag;
469
+ const cosine = mag && dot2(a, b) / mag;
470
470
  return Math.acos(Math.min(Math.max(cosine, -1), 1));
471
471
  };
472
472
  module.exports = angle;
@@ -516,7 +516,7 @@ var require_copy2 = __commonJS({
516
516
  var require_cross = __commonJS({
517
517
  "node_modules/@jscad/modeling/src/maths/vec3/cross.js"(exports, module) {
518
518
  "use strict";
519
- var cross = (out, a, b) => {
519
+ var cross2 = (out, a, b) => {
520
520
  const ax = a[0];
521
521
  const ay = a[1];
522
522
  const az = a[2];
@@ -528,7 +528,7 @@ var require_cross = __commonJS({
528
528
  out[2] = ax * by - ay * bx;
529
529
  return out;
530
530
  };
531
- module.exports = cross;
531
+ module.exports = cross2;
532
532
  }
533
533
  });
534
534
 
@@ -724,13 +724,13 @@ var require_orthogonal = __commonJS({
724
724
  "use strict";
725
725
  var abs = require_abs();
726
726
  var create = require_create2();
727
- var cross = require_cross();
727
+ var cross2 = require_cross();
728
728
  var orthogonal = (out, vector) => {
729
729
  const bV = abs(create(), vector);
730
730
  const b0 = 0 + (bV[0] < bV[1] && bV[0] < bV[2]);
731
731
  const b1 = 0 + (bV[1] <= bV[0] && bV[1] < bV[2]);
732
732
  const b2 = 0 + (bV[2] <= bV[0] && bV[2] <= bV[1]);
733
- return cross(out, vector, [b0, b1, b2]);
733
+ return cross2(out, vector, [b0, b1, b2]);
734
734
  };
735
735
  module.exports = orthogonal;
736
736
  }
@@ -1650,13 +1650,13 @@ var require_copy3 = __commonJS({
1650
1650
  var require_cross2 = __commonJS({
1651
1651
  "node_modules/@jscad/modeling/src/maths/vec2/cross.js"(exports, module) {
1652
1652
  "use strict";
1653
- var cross = (out, a, b) => {
1653
+ var cross2 = (out, a, b) => {
1654
1654
  out[0] = 0;
1655
1655
  out[1] = 0;
1656
1656
  out[2] = a[0] * b[1] - a[1] * b[0];
1657
1657
  return out;
1658
1658
  };
1659
- module.exports = cross;
1659
+ module.exports = cross2;
1660
1660
  }
1661
1661
  });
1662
1662
 
@@ -1690,8 +1690,8 @@ var require_divide2 = __commonJS({
1690
1690
  var require_dot2 = __commonJS({
1691
1691
  "node_modules/@jscad/modeling/src/maths/vec2/dot.js"(exports, module) {
1692
1692
  "use strict";
1693
- var dot = (a, b) => a[0] * b[0] + a[1] * b[1];
1694
- module.exports = dot;
1693
+ var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1];
1694
+ module.exports = dot2;
1695
1695
  }
1696
1696
  });
1697
1697
 
@@ -2401,7 +2401,7 @@ var require_create5 = __commonJS({
2401
2401
  var require_point_line_distance = __commonJS({
2402
2402
  "node_modules/@jscad/modeling/src/operations/hulls/quickhull/point-line-distance.js"(exports, module) {
2403
2403
  "use strict";
2404
- var cross = require_cross();
2404
+ var cross2 = require_cross();
2405
2405
  var subtract3 = require_subtract();
2406
2406
  var squaredLength = require_squaredLength();
2407
2407
  var distanceSquared = (p, a, b) => {
@@ -2410,7 +2410,7 @@ var require_point_line_distance = __commonJS({
2410
2410
  const cr = [];
2411
2411
  subtract3(ab, b, a);
2412
2412
  subtract3(ap, p, a);
2413
- const area = squaredLength(cross(cr, ap, ab));
2413
+ const area = squaredLength(cross2(cr, ap, ab));
2414
2414
  const s = squaredLength(ab);
2415
2415
  if (s === 0) {
2416
2416
  throw Error("a and b are the same point");
@@ -2426,14 +2426,14 @@ var require_point_line_distance = __commonJS({
2426
2426
  var require_get_plane_normal = __commonJS({
2427
2427
  "node_modules/@jscad/modeling/src/operations/hulls/quickhull/get-plane-normal.js"(exports, module) {
2428
2428
  "use strict";
2429
- var cross = require_cross();
2429
+ var cross2 = require_cross();
2430
2430
  var normalize = require_normalize();
2431
2431
  var subtract3 = require_subtract();
2432
2432
  var planeNormal = (out, point1, point2, point3) => {
2433
2433
  const tmp = [0, 0, 0];
2434
2434
  subtract3(out, point1, point2);
2435
2435
  subtract3(tmp, point2, point3);
2436
- cross(out, out, tmp);
2436
+ cross2(out, out, tmp);
2437
2437
  return normalize(out, out);
2438
2438
  };
2439
2439
  module.exports = planeNormal;
@@ -2641,8 +2641,8 @@ var require_Face = __commonJS({
2641
2641
  "use strict";
2642
2642
  var add = require_add2();
2643
2643
  var copy = require_copy2();
2644
- var cross = require_cross();
2645
- var dot = require_dot();
2644
+ var cross2 = require_cross();
2645
+ var dot2 = require_dot();
2646
2646
  var length = require_length();
2647
2647
  var normalize = require_normalize();
2648
2648
  var scale = require_scale();
@@ -2688,7 +2688,7 @@ var require_Face = __commonJS({
2688
2688
  while (e2 !== e0) {
2689
2689
  copy(v1, v2);
2690
2690
  subtract3(v2, e2.head().point, e0.head().point);
2691
- add(this.normal, this.normal, cross(t, v1, v2));
2691
+ add(this.normal, this.normal, cross2(t, v1, v2));
2692
2692
  e2 = e2.next;
2693
2693
  this.nVertices += 1;
2694
2694
  }
@@ -2714,7 +2714,7 @@ var require_Face = __commonJS({
2714
2714
  const maxVector = subtract3([], p2, p1);
2715
2715
  const maxLength = Math.sqrt(maxSquaredLength);
2716
2716
  scale(maxVector, maxVector, 1 / maxLength);
2717
- const maxProjection = dot(this.normal, maxVector);
2717
+ const maxProjection = dot2(this.normal, maxVector);
2718
2718
  scale(maxVector, maxVector, -maxProjection);
2719
2719
  add(this.normal, this.normal, maxVector);
2720
2720
  normalize(this.normal, this.normal);
@@ -2736,10 +2736,10 @@ var require_Face = __commonJS({
2736
2736
  this.computeNormal();
2737
2737
  }
2738
2738
  this.computeCentroid();
2739
- this.offset = dot(this.normal, this.centroid);
2739
+ this.offset = dot2(this.normal, this.centroid);
2740
2740
  }
2741
2741
  distanceToPlane(point) {
2742
- return dot(this.normal, point) - this.offset;
2742
+ return dot2(this.normal, point) - this.offset;
2743
2743
  }
2744
2744
  /**
2745
2745
  * @private
@@ -2848,7 +2848,7 @@ var require_Face = __commonJS({
2848
2848
  var require_QuickHull = __commonJS({
2849
2849
  "node_modules/@jscad/modeling/src/operations/hulls/quickhull/QuickHull.js"(exports, module) {
2850
2850
  "use strict";
2851
- var dot = require_dot();
2851
+ var dot2 = require_dot();
2852
2852
  var pointLineDistance = require_point_line_distance();
2853
2853
  var getPlaneNormal = require_get_plane_normal();
2854
2854
  var VertexList = require_VertexList();
@@ -3059,12 +3059,12 @@ var require_QuickHull = __commonJS({
3059
3059
  }
3060
3060
  }
3061
3061
  const normal = getPlaneNormal([], v0.point, v1.point, v2.point);
3062
- const distPO = dot(v0.point, normal);
3062
+ const distPO = dot2(v0.point, normal);
3063
3063
  maxDistance = -1;
3064
3064
  for (i = 0; i < this.vertices.length; i += 1) {
3065
3065
  const vertex = this.vertices[i];
3066
3066
  if (vertex !== v0 && vertex !== v1 && vertex !== v2) {
3067
- const distance = Math.abs(dot(normal, vertex.point) - distPO);
3067
+ const distance = Math.abs(dot2(normal, vertex.point) - distPO);
3068
3068
  if (distance > maxDistance) {
3069
3069
  maxDistance = distance;
3070
3070
  v3 = vertex;
@@ -3072,7 +3072,7 @@ var require_QuickHull = __commonJS({
3072
3072
  }
3073
3073
  }
3074
3074
  const faces = [];
3075
- if (dot(v3.point, normal) - distPO < 0) {
3075
+ if (dot2(v3.point, normal) - distPO < 0) {
3076
3076
  faces.push(
3077
3077
  Face.createTriangle(v0, v1, v2),
3078
3078
  Face.createTriangle(v3, v1, v0),
@@ -4004,8 +4004,8 @@ var require_measureBoundingBox = __commonJS({
4004
4004
  var require_dot3 = __commonJS({
4005
4005
  "node_modules/@jscad/modeling/src/maths/vec4/dot.js"(exports, module) {
4006
4006
  "use strict";
4007
- var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
4008
- module.exports = dot;
4007
+ var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
4008
+ module.exports = dot2;
4009
4009
  }
4010
4010
  });
4011
4011
 
@@ -4112,10 +4112,10 @@ var require_measureSignedVolume = __commonJS({
4112
4112
  var measureSignedVolume = (polygon3) => {
4113
4113
  let signedVolume = 0;
4114
4114
  const vertices = polygon3.vertices;
4115
- const cross = vec3.create();
4115
+ const cross2 = vec3.create();
4116
4116
  for (let i = 0; i < vertices.length - 2; i++) {
4117
- vec3.cross(cross, vertices[i + 1], vertices[i + 2]);
4118
- signedVolume += vec3.dot(vertices[0], cross);
4117
+ vec3.cross(cross2, vertices[i + 1], vertices[i + 2]);
4118
+ signedVolume += vec3.dot(vertices[0], cross2);
4119
4119
  }
4120
4120
  signedVolume /= 6;
4121
4121
  return signedVolume;
@@ -11459,7 +11459,7 @@ var require_unionGeom3 = __commonJS({
11459
11459
  var flatten = require_flatten();
11460
11460
  var retessellate = require_retessellate();
11461
11461
  var unionSub = require_unionGeom3Sub();
11462
- var union = (...geometries) => {
11462
+ var union3 = (...geometries) => {
11463
11463
  geometries = flatten(geometries);
11464
11464
  let i;
11465
11465
  for (i = 1; i < geometries.length; i += 2) {
@@ -11469,7 +11469,7 @@ var require_unionGeom3 = __commonJS({
11469
11469
  newgeometry = retessellate(newgeometry);
11470
11470
  return newgeometry;
11471
11471
  };
11472
- module.exports = union;
11472
+ module.exports = union3;
11473
11473
  }
11474
11474
  });
11475
11475
 
@@ -11483,14 +11483,14 @@ var require_unionGeom2 = __commonJS({
11483
11483
  var fromFakePolygons = require_fromFakePolygons();
11484
11484
  var to3DWalls = require_to3DWalls();
11485
11485
  var unionGeom3 = require_unionGeom3();
11486
- var union = (...geometries) => {
11486
+ var union3 = (...geometries) => {
11487
11487
  geometries = flatten(geometries);
11488
11488
  const newgeometries = geometries.map((geometry) => to3DWalls({ z0: -1, z1: 1 }, geometry));
11489
11489
  const newgeom3 = unionGeom3(newgeometries);
11490
11490
  const epsilon = measureEpsilon(newgeom3);
11491
11491
  return fromFakePolygons(epsilon, geom33.toPolygons(newgeom3));
11492
11492
  };
11493
- module.exports = union;
11493
+ module.exports = union3;
11494
11494
  }
11495
11495
  });
11496
11496
 
@@ -11504,7 +11504,7 @@ var require_union = __commonJS({
11504
11504
  var geom33 = require_geom3();
11505
11505
  var unionGeom2 = require_unionGeom2();
11506
11506
  var unionGeom3 = require_unionGeom3();
11507
- var union = (...geometries) => {
11507
+ var union3 = (...geometries) => {
11508
11508
  geometries = flatten(geometries);
11509
11509
  if (geometries.length === 0) throw new Error("wrong number of arguments");
11510
11510
  if (!areAllShapesTheSameType(geometries)) {
@@ -11515,7 +11515,7 @@ var require_union = __commonJS({
11515
11515
  if (geom33.isA(geometry)) return unionGeom3(geometries);
11516
11516
  return geometry;
11517
11517
  };
11518
- module.exports = union;
11518
+ module.exports = union3;
11519
11519
  }
11520
11520
  });
11521
11521
 
@@ -11871,8 +11871,8 @@ var require_expandShell = __commonJS({
11871
11871
  let bestzaxisorthogonality = 0;
11872
11872
  for (let i = 1; i < planes.length; i++) {
11873
11873
  const normal = planes[i];
11874
- const cross = vec3.cross(v1, xaxis, normal);
11875
- const crosslength = vec3.length(cross);
11874
+ const cross2 = vec3.cross(v1, xaxis, normal);
11875
+ const crosslength = vec3.length(cross2);
11876
11876
  if (crosslength > 0.05) {
11877
11877
  if (crosslength > bestzaxisorthogonality) {
11878
11878
  bestzaxisorthogonality = crosslength;
@@ -11905,7 +11905,7 @@ var require_expandGeom3 = __commonJS({
11905
11905
  "node_modules/@jscad/modeling/src/operations/expansions/expandGeom3.js"(exports, module) {
11906
11906
  "use strict";
11907
11907
  var geom33 = require_geom3();
11908
- var union = require_union();
11908
+ var union3 = require_union();
11909
11909
  var expandShell = require_expandShell();
11910
11910
  var expandGeom3 = (options, geometry) => {
11911
11911
  const defaults = {
@@ -11921,7 +11921,7 @@ var require_expandGeom3 = __commonJS({
11921
11921
  if (polygons.length === 0) throw new Error("the given geometry cannot be empty");
11922
11922
  options = { delta, corners, segments };
11923
11923
  const expanded = expandShell(options, geometry);
11924
- return union(geometry, expanded);
11924
+ return union3(geometry, expanded);
11925
11925
  };
11926
11926
  module.exports = expandGeom3;
11927
11927
  }
@@ -12633,7 +12633,7 @@ var require_hullChain = __commonJS({
12633
12633
  "node_modules/@jscad/modeling/src/operations/hulls/hullChain.js"(exports, module) {
12634
12634
  "use strict";
12635
12635
  var flatten = require_flatten();
12636
- var union = require_union();
12636
+ var union3 = require_union();
12637
12637
  var hull = require_hull();
12638
12638
  var hullChain = (...geometries) => {
12639
12639
  geometries = flatten(geometries);
@@ -12642,7 +12642,7 @@ var require_hullChain = __commonJS({
12642
12642
  for (let i = 1; i < geometries.length; i++) {
12643
12643
  hulls.push(hull(geometries[i - 1], geometries[i]));
12644
12644
  }
12645
- return union(hulls);
12645
+ return union3(hulls);
12646
12646
  };
12647
12647
  module.exports = hullChain;
12648
12648
  }
@@ -15001,6 +15001,8 @@ var import_extrusions = __toESM(require_extrusions(), 1);
15001
15001
  var import_primitives = __toESM(require_primitives(), 1);
15002
15002
  var import_transforms = __toESM(require_transforms(), 1);
15003
15003
  var DEFAULT_SEGMENTS = 64;
15004
+ var REDUCED_SEGMENTS = 16;
15005
+ var HOLE_COUNT_THRESHOLD = 50;
15004
15006
  var toBoardSpaceVec2 = (point, center) => [point.x - center.x, -(point.y - center.y)];
15005
15007
  var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
15006
15008
  var arePointsClockwise = (points) => {
@@ -15013,11 +15015,11 @@ var arePointsClockwise = (points) => {
15013
15015
  const signedArea = area / 2;
15014
15016
  return signedArea <= 0;
15015
15017
  };
15016
- var createCircularHole = (x, y, radius, thickness) => (0, import_primitives.cylinder)({
15018
+ var createCircularHole = (x, y, radius, thickness, segments = DEFAULT_SEGMENTS) => (0, import_primitives.cylinder)({
15017
15019
  center: [x, y, 0],
15018
15020
  height: thickness + 1,
15019
15021
  radius,
15020
- segments: DEFAULT_SEGMENTS
15022
+ segments
15021
15023
  });
15022
15024
  var createCutoutGeoms = (boardCenter, thickness, cutouts = []) => {
15023
15025
  const geoms = [];
@@ -15129,9 +15131,36 @@ var filterCutoutsForBoard = (cutouts, board) => {
15129
15131
  var import_extrusions2 = __toESM(require_extrusions(), 1);
15130
15132
  var import_primitives2 = __toESM(require_primitives(), 1);
15131
15133
  var import_transforms2 = __toESM(require_transforms(), 1);
15132
- var import_booleans = __toESM(require_booleans(), 1);
15134
+ var import_booleans2 = __toESM(require_booleans(), 1);
15133
15135
  var geom3 = __toESM(require_geom3(), 1);
15134
15136
  var import_measureBoundingBox = __toESM(require_measureBoundingBox2(), 1);
15137
+
15138
+ // lib/utils/batched-union.ts
15139
+ var import_booleans = __toESM(require_booleans(), 1);
15140
+ var batchedUnion = (geoms, batchSize = 50) => {
15141
+ if (geoms.length === 0) {
15142
+ throw new Error("Cannot union empty array");
15143
+ }
15144
+ if (geoms.length === 1) {
15145
+ return geoms[0];
15146
+ }
15147
+ let results = [...geoms];
15148
+ while (results.length > 1) {
15149
+ const newResults = [];
15150
+ for (let i = 0; i < results.length; i += batchSize) {
15151
+ const batch = results.slice(i, i + batchSize);
15152
+ if (batch.length === 1) {
15153
+ newResults.push(batch[0]);
15154
+ } else {
15155
+ newResults.push((0, import_booleans.union)(...batch));
15156
+ }
15157
+ }
15158
+ results = newResults;
15159
+ }
15160
+ return results[0];
15161
+ };
15162
+
15163
+ // lib/utils/pcb-board-geometry.ts
15135
15164
  var RADIUS_EPSILON = 1e-4;
15136
15165
  var getNumberProperty = (obj, key) => {
15137
15166
  const value = obj[key];
@@ -15157,14 +15186,14 @@ var createBoardOutlineGeom = (board, center, thickness) => {
15157
15186
  geom = (0, import_transforms2.translate)([0, 0, -thickness / 2], geom);
15158
15187
  return geom;
15159
15188
  };
15160
- var createPillHole = (x, y, width, height, thickness, rotate) => {
15189
+ var createPillHoleWithSegments = (x, y, width, height, thickness, rotate, segments = DEFAULT_SEGMENTS) => {
15161
15190
  const minDimension = Math.min(width, height);
15162
15191
  const maxAllowedRadius = Math.max(0, minDimension / 2 - RADIUS_EPSILON);
15163
15192
  const roundRadius = maxAllowedRadius <= 0 ? 0 : Math.min(height / 2, maxAllowedRadius);
15164
15193
  const hole2d = (0, import_primitives2.roundedRectangle)({
15165
15194
  size: [width, height],
15166
15195
  roundRadius,
15167
- segments: DEFAULT_SEGMENTS
15196
+ segments
15168
15197
  });
15169
15198
  let hole3d = (0, import_extrusions2.extrudeLinear)({ height: thickness + 1 }, hole2d);
15170
15199
  hole3d = (0, import_transforms2.translate)([0, 0, -(thickness + 1) / 2], hole3d);
@@ -15173,7 +15202,7 @@ var createPillHole = (x, y, width, height, thickness, rotate) => {
15173
15202
  }
15174
15203
  return (0, import_transforms2.translate)([x, y, 0], hole3d);
15175
15204
  };
15176
- var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) => {
15205
+ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = [], segments = DEFAULT_SEGMENTS) => {
15177
15206
  const holeGeoms = [];
15178
15207
  for (const hole of holes) {
15179
15208
  const holeRecord = hole;
@@ -15187,13 +15216,14 @@ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) =>
15187
15216
  const rotate = holeHeight > holeWidth;
15188
15217
  const width = rotate ? holeHeight : holeWidth;
15189
15218
  const height = rotate ? holeWidth : holeHeight;
15190
- const pillHole = createPillHole(
15219
+ const pillHole = createPillHoleWithSegments(
15191
15220
  relX,
15192
15221
  relY,
15193
15222
  width,
15194
15223
  height,
15195
15224
  thickness,
15196
- rotate
15225
+ rotate,
15226
+ segments
15197
15227
  );
15198
15228
  holeGeoms.push(pillHole);
15199
15229
  continue;
@@ -15210,7 +15240,7 @@ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) =>
15210
15240
  const hole2d = (0, import_primitives2.roundedRectangle)({
15211
15241
  size: [holeWidth, holeHeight],
15212
15242
  roundRadius,
15213
- segments: DEFAULT_SEGMENTS
15243
+ segments
15214
15244
  });
15215
15245
  let hole3d = (0, import_extrusions2.extrudeLinear)({ height: thickness + 1 }, hole2d);
15216
15246
  hole3d = (0, import_transforms2.translate)([0, 0, -(thickness + 1) / 2], hole3d);
@@ -15224,7 +15254,7 @@ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) =>
15224
15254
  const diameter = getNumberProperty(holeRecord, "hole_diameter") ?? getNumberProperty(holeRecord, "diameter");
15225
15255
  if (!diameter) continue;
15226
15256
  const radius = diameter / 2;
15227
- holeGeoms.push(createCircularHole(relX, relY, radius, thickness));
15257
+ holeGeoms.push(createCircularHole(relX, relY, radius, thickness, segments));
15228
15258
  }
15229
15259
  for (const plated of platedHoles) {
15230
15260
  const platedRecord = plated;
@@ -15240,13 +15270,23 @@ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) =>
15240
15270
  const width = rotate ? holeHeight : holeWidth;
15241
15271
  const height = rotate ? holeWidth : holeHeight;
15242
15272
  holeGeoms.push(
15243
- createPillHole(relX, relY, width, height, thickness, rotate)
15273
+ createPillHoleWithSegments(
15274
+ relX,
15275
+ relY,
15276
+ width,
15277
+ height,
15278
+ thickness,
15279
+ rotate,
15280
+ segments
15281
+ )
15244
15282
  );
15245
15283
  continue;
15246
15284
  }
15247
15285
  const diameter = getNumberProperty(platedRecord, "hole_diameter") ?? getNumberProperty(platedRecord, "outer_diameter");
15248
15286
  if (!diameter) continue;
15249
- holeGeoms.push(createCircularHole(relX, relY, diameter / 2, thickness));
15287
+ holeGeoms.push(
15288
+ createCircularHole(relX, relY, diameter / 2, thickness, segments)
15289
+ );
15250
15290
  }
15251
15291
  return holeGeoms;
15252
15292
  };
@@ -15264,16 +15304,16 @@ var geom3ToTriangles = (geometry, polygons) => {
15264
15304
  next2[1] - base[1],
15265
15305
  next2[2] - base[2]
15266
15306
  ];
15267
- const cross = [
15307
+ const cross2 = [
15268
15308
  ab[1] * ac[2] - ab[2] * ac[1],
15269
15309
  ab[2] * ac[0] - ab[0] * ac[2],
15270
15310
  ab[0] * ac[1] - ab[1] * ac[0]
15271
15311
  ];
15272
- const length = Math.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2) || 1;
15312
+ const length = Math.sqrt(cross2[0] ** 2 + cross2[1] ** 2 + cross2[2] ** 2) || 1;
15273
15313
  const normal = {
15274
- x: cross[0] / length,
15275
- y: cross[1] / length,
15276
- z: cross[2] / length
15314
+ x: cross2[0] / length,
15315
+ y: cross2[1] / length,
15316
+ z: cross2[2] / length
15277
15317
  };
15278
15318
  for (let i = 1; i < poly.vertices.length - 1; i++) {
15279
15319
  const v1 = poly.vertices[i];
@@ -15302,11 +15342,21 @@ var createBoardMesh = (board, options) => {
15302
15342
  const { thickness, holes = [], platedHoles = [], cutouts = [] } = options;
15303
15343
  const center = board.center ?? { x: 0, y: 0 };
15304
15344
  let boardGeom = createBoardOutlineGeom(board, center, thickness);
15305
- const holeGeoms = createHoleGeoms(center, thickness, holes, platedHoles);
15345
+ const totalHoleCount = holes.length + platedHoles.length;
15346
+ const useReducedSegments = totalHoleCount > HOLE_COUNT_THRESHOLD;
15347
+ const segments = useReducedSegments ? REDUCED_SEGMENTS : DEFAULT_SEGMENTS;
15348
+ const holeGeoms = createHoleGeoms(
15349
+ center,
15350
+ thickness,
15351
+ holes,
15352
+ platedHoles,
15353
+ segments
15354
+ );
15306
15355
  const cutoutGeoms = createCutoutGeoms(center, thickness, cutouts);
15307
15356
  const subtractGeoms = [...holeGeoms, ...cutoutGeoms];
15308
15357
  if (subtractGeoms.length > 0) {
15309
- boardGeom = (0, import_booleans.subtract)(boardGeom, ...subtractGeoms);
15358
+ const unifiedHoles = batchedUnion(subtractGeoms);
15359
+ boardGeom = (0, import_booleans2.subtract)(boardGeom, unifiedHoles);
15310
15360
  }
15311
15361
  boardGeom = (0, import_transforms2.rotateX)(-Math.PI / 2, boardGeom);
15312
15362
  const polygons = geom3.toPolygons(boardGeom);
@@ -15321,7 +15371,7 @@ var createBoardMesh = (board, options) => {
15321
15371
 
15322
15372
  // lib/utils/pcb-panel-geometry.ts
15323
15373
  var import_transforms3 = __toESM(require_transforms(), 1);
15324
- var import_booleans2 = __toESM(require_booleans(), 1);
15374
+ var import_booleans3 = __toESM(require_booleans(), 1);
15325
15375
  var geom32 = __toESM(require_geom3(), 1);
15326
15376
  var import_measureBoundingBox2 = __toESM(require_measureBoundingBox2(), 1);
15327
15377
  var createPanelMesh = (panel, options) => {
@@ -15332,7 +15382,7 @@ var createPanelMesh = (panel, options) => {
15332
15382
  const cutoutGeoms = createCutoutGeoms(center, thickness, cutouts);
15333
15383
  const subtractGeoms = [...holeGeoms, ...cutoutGeoms];
15334
15384
  if (subtractGeoms.length > 0) {
15335
- panelGeom = (0, import_booleans2.subtract)(panelGeom, ...subtractGeoms);
15385
+ panelGeom = (0, import_booleans3.subtract)(panelGeom, ...subtractGeoms);
15336
15386
  }
15337
15387
  panelGeom = (0, import_transforms3.rotateX)(-Math.PI / 2, panelGeom);
15338
15388
  const polygons = geom32.toPolygons(panelGeom);
@@ -17190,6 +17240,130 @@ async function convertSceneToGLTF(scene, options = {}) {
17190
17240
  return result;
17191
17241
  }
17192
17242
 
17243
+ // lib/utils/camera-position.ts
17244
+ var DEFAULT_CAMERA_DIRECTION = [-0.7, 1.2, -0.8];
17245
+ function normalizeVector([x, y, z]) {
17246
+ const length = Math.hypot(x, y, z);
17247
+ if (length === 0) {
17248
+ return [0, 1, 0];
17249
+ }
17250
+ return [x / length, y / length, z / length];
17251
+ }
17252
+ function dot([ax, ay, az], [bx, by, bz]) {
17253
+ return ax * bx + ay * by + az * bz;
17254
+ }
17255
+ function cross([ax, ay, az], [bx, by, bz]) {
17256
+ return [ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx];
17257
+ }
17258
+ function getVerticalFovRadians(opts) {
17259
+ if (opts?.focalLength !== void 0 && opts.sensorHeight !== void 0 && opts.focalLength > 0 && opts.sensorHeight > 0) {
17260
+ return 2 * Math.atan(opts.sensorHeight / (2 * opts.focalLength));
17261
+ }
17262
+ const fovDegrees = opts?.fov ?? 50;
17263
+ const fovRadians = fovDegrees * Math.PI / 180;
17264
+ if (!Number.isFinite(fovRadians) || fovRadians <= 0) {
17265
+ return 50 * Math.PI / 180;
17266
+ }
17267
+ return Math.min(Math.max(fovRadians, 0.01), Math.PI - 0.01);
17268
+ }
17269
+ function getVerticalFovDegrees(opts) {
17270
+ return getVerticalFovRadians(opts) * 180 / Math.PI;
17271
+ }
17272
+ function getRequiredDistanceForFrustum(corners, cameraDirection, right, up, tanHalfHorizontal, tanHalfVertical) {
17273
+ let requiredDistance = 0;
17274
+ for (const corner of corners) {
17275
+ const u = [corner[0], corner[1], corner[2]];
17276
+ const un = dot(u, cameraDirection);
17277
+ const ur = Math.abs(dot(u, right));
17278
+ const uu = Math.abs(dot(u, up));
17279
+ const distanceForHorizontal = un + ur / tanHalfHorizontal;
17280
+ const distanceForVertical = un + uu / tanHalfVertical;
17281
+ requiredDistance = Math.max(
17282
+ requiredDistance,
17283
+ distanceForHorizontal,
17284
+ distanceForVertical
17285
+ );
17286
+ }
17287
+ return requiredDistance;
17288
+ }
17289
+ function getBestCameraPosition(circuitJson, opts) {
17290
+ const verticalFovDegrees = getVerticalFovDegrees(opts);
17291
+ const panel = circuitJson.find((item) => item.type === "pcb_panel");
17292
+ const board = circuitJson.find((item) => item.type === "pcb_board");
17293
+ const surface = panel || board;
17294
+ if (!surface) {
17295
+ return {
17296
+ camPos: [30, 30, 25],
17297
+ lookAt: [0, 0, 0],
17298
+ fov: verticalFovDegrees
17299
+ };
17300
+ }
17301
+ const { width, height, center } = surface;
17302
+ if (!width || !height || !center) {
17303
+ return {
17304
+ camPos: [30, 30, 25],
17305
+ lookAt: [0, 0, 0],
17306
+ fov: verticalFovDegrees
17307
+ };
17308
+ }
17309
+ const lookAtX = center.x;
17310
+ const lookAtZ = center.y;
17311
+ const cameraDirection = normalizeVector(
17312
+ opts?.direction ?? DEFAULT_CAMERA_DIRECTION
17313
+ );
17314
+ const forward = [
17315
+ -cameraDirection[0],
17316
+ -cameraDirection[1],
17317
+ -cameraDirection[2]
17318
+ ];
17319
+ const worldUp = [0, 1, 0];
17320
+ const right = normalizeVector(cross(forward, worldUp));
17321
+ const up = normalizeVector(cross(right, forward));
17322
+ const verticalFov = verticalFovDegrees * Math.PI / 180;
17323
+ const aspectRatio = opts?.aspectRatio !== void 0 && Number.isFinite(opts.aspectRatio) && opts.aspectRatio > 0 ? opts.aspectRatio : 4 / 3;
17324
+ const tanHalfVertical = Math.tan(verticalFov / 2);
17325
+ const tanHalfHorizontal = tanHalfVertical * aspectRatio;
17326
+ const halfWidth = width / 2;
17327
+ const halfHeight = height / 2;
17328
+ const boardCorners = [
17329
+ [halfWidth, 0, halfHeight],
17330
+ [halfWidth, 0, -halfHeight],
17331
+ [-halfWidth, 0, halfHeight],
17332
+ [-halfWidth, 0, -halfHeight]
17333
+ ];
17334
+ const requiredDistanceAssumingVerticalFov = getRequiredDistanceForFrustum(
17335
+ boardCorners,
17336
+ cameraDirection,
17337
+ right,
17338
+ up,
17339
+ tanHalfHorizontal,
17340
+ tanHalfVertical
17341
+ );
17342
+ const tanHalfHorizontalIfFovIsHorizontal = tanHalfVertical;
17343
+ const tanHalfVerticalIfFovIsHorizontal = tanHalfHorizontalIfFovIsHorizontal / aspectRatio;
17344
+ const requiredDistanceAssumingHorizontalFov = getRequiredDistanceForFrustum(
17345
+ boardCorners,
17346
+ cameraDirection,
17347
+ right,
17348
+ up,
17349
+ tanHalfHorizontalIfFovIsHorizontal,
17350
+ tanHalfVerticalIfFovIsHorizontal
17351
+ );
17352
+ const requiredDistance = Math.max(
17353
+ requiredDistanceAssumingVerticalFov,
17354
+ requiredDistanceAssumingHorizontalFov
17355
+ );
17356
+ const distance = Math.max(requiredDistance, 1);
17357
+ const camX = lookAtX + cameraDirection[0] * distance;
17358
+ const camY = cameraDirection[1] * distance;
17359
+ const camZ = lookAtZ + cameraDirection[2] * distance;
17360
+ return {
17361
+ camPos: [-camX, camY, camZ],
17362
+ lookAt: [-lookAtX, 0, lookAtZ],
17363
+ fov: verticalFovDegrees
17364
+ };
17365
+ }
17366
+
17193
17367
  // lib/index.ts
17194
17368
  async function convertCircuitJsonToGltf(circuitJson, options = {}) {
17195
17369
  const {
@@ -17227,6 +17401,7 @@ export {
17227
17401
  convertCircuitJsonTo3D,
17228
17402
  convertCircuitJsonToGltf,
17229
17403
  convertSceneToGLTF,
17404
+ getBestCameraPosition,
17230
17405
  loadGLB,
17231
17406
  loadOBJ,
17232
17407
  loadSTL,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "circuit-json-to-gltf",
3
3
  "main": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.0.71",
5
+ "version": "0.0.73",
6
6
  "scripts": {
7
7
  "test": "bun test tests/",
8
8
  "format": "biome format --write .",