@jwc/jscad-utils 5.1.0 → 5.5.0

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/src/util.js CHANGED
@@ -1,5 +1,14 @@
1
+ /**
2
+ * Generates a detailed documentation comment for the function.
3
+ * @param {any} o - The object to convert to a string representation
4
+ * @returns {string} A string representation of the object, including polygon count and properties for CSG-like objects, or the object's toString() result
5
+ * @function jscadToString
6
+ */
1
7
  /** @typedef { import('@jscad/csg').CSG } CSG */
2
8
 
9
+ /** @typedef {"x"|"y"|"z"|"xy"|"yz"|"xz"|"xyz"} AxisStrings Composed axis strings*/
10
+ /** @typedef {"x"|"y"|"z"} AxisString Individual axis strings */
11
+
3
12
  import * as array from './array';
4
13
  import { Debug } from './debug';
5
14
  // import jsCadCSG from '@jscad/csg';
@@ -16,7 +25,10 @@ import {
16
25
  vector_char,
17
26
  vector_text
18
27
  } from './jscad';
28
+ import { AssertValidCSG } from './validate';
19
29
  const debug = Debug('jscadUtils:util');
30
+ const assertValidCSG = AssertValidCSG('util');
31
+
20
32
  // import utilInit from '../src/add-prototype';
21
33
  // utilInit(CSG);
22
34
  // console.trace('CSG', CSG.prototype);
@@ -193,7 +205,7 @@ export function label(text, x, y, width, height) {
193
205
  // console.trace('label', Object.getPrototypeOf(union(o)));
194
206
  // var foo = union(o);
195
207
  // console.trace('typeof', typeof foo);
196
- return center(union(o));
208
+ return assertValidCSG(center(union(o)), 'label');
197
209
  }
198
210
 
199
211
  export function text(text) {
@@ -209,10 +221,10 @@ export function text(text) {
209
221
 
210
222
  export function unitCube(length, radius) {
211
223
  radius = radius || 0.5;
212
- return CSG.cube({
224
+ return assertValidCSG(CSG.cube({
213
225
  center: [0, 0, 0],
214
226
  radius: [radius, radius, length || 0.5]
215
- });
227
+ }), 'unitCube');
216
228
  }
217
229
 
218
230
  export function unitAxis(length, radius, centroid) {
@@ -230,7 +242,7 @@ export function unitAxis(length, radius, centroid) {
230
242
  [1, 0, 0],
231
243
  [0, 1, 0]
232
244
  );
233
- return unitaxis.translate(centroid);
245
+ return assertValidCSG(unitaxis.translate(centroid), 'unitAxis');
234
246
  }
235
247
 
236
248
  export function toArray(a) {
@@ -372,17 +384,17 @@ export function scale(size, value) {
372
384
 
373
385
  export function center(object, objectSize) {
374
386
  objectSize = objectSize || size(object.getBounds());
375
- return centerY(centerX(object, objectSize), objectSize);
387
+ return assertValidCSG(centerY(centerX(object, objectSize), objectSize), 'center');
376
388
  }
377
389
 
378
390
  export function centerY(object, objectSize) {
379
391
  objectSize = objectSize || size(object.getBounds());
380
- return object.translate([0, -objectSize.y / 2, 0]);
392
+ return assertValidCSG(object.translate([0, -objectSize.y / 2, 0]), 'centerY');
381
393
  }
382
394
 
383
395
  export function centerX(object, objectSize) {
384
396
  objectSize = objectSize || size(object.getBounds());
385
- return object.translate([-objectSize.x / 2, 0, 0]);
397
+ return assertValidCSG(object.translate([-objectSize.x / 2, 0, 0]), 'centerX');
386
398
  }
387
399
 
388
400
  /**
@@ -419,7 +431,7 @@ export function enlarge(object, x, y, z) {
419
431
  /// Calculate the difference between the original centroid and the new
420
432
  var delta = new_centroid.minus(objectCentroid).times(-1);
421
433
 
422
- return new_object.translate(delta);
434
+ return assertValidCSG(new_object.translate(delta), 'enlarge');
423
435
  }
424
436
 
425
437
  /**
@@ -458,7 +470,7 @@ export function fit(object, x, y, z, keep_aspect_ratio) {
458
470
  scale(objectSize.z, z)
459
471
  ];
460
472
  var min = array.min(s);
461
- return centerWith(
473
+ return assertValidCSG(centerWith(
462
474
  object.scale(
463
475
  s.map(function (d, i) {
464
476
  if (a[i] === 0) return 1; // don't scale when value is zero
@@ -467,7 +479,7 @@ export function fit(object, x, y, z, keep_aspect_ratio) {
467
479
  ),
468
480
  'xyz',
469
481
  object
470
- );
482
+ ), 'fit');
471
483
  }
472
484
 
473
485
  export function shift(object, x, y, z) {
@@ -477,15 +489,15 @@ export function shift(object, x, y, z) {
477
489
 
478
490
  export function zero(object) {
479
491
  var bounds = object.getBounds();
480
- return object.translate([0, 0, -bounds[0].z]);
492
+ return assertValidCSG(object.translate([0, 0, -bounds[0].z]), 'zero');
481
493
  }
482
494
 
483
495
  export function mirrored4(x) {
484
- return x.union([
496
+ return assertValidCSG(x.union([
485
497
  x.mirroredY(90),
486
498
  x.mirroredX(90),
487
499
  x.mirroredY(90).mirroredX(90)
488
- ]);
500
+ ]), 'mirrored4');
489
501
  }
490
502
 
491
503
  export const flushSide = {
@@ -570,7 +582,7 @@ export function calcSnap(moveobj, withobj, axes, orientation, delta = 0) {
570
582
  export function snap(moveobj, withobj, axis, orientation, delta) {
571
583
  debug('snap', moveobj, withobj, axis, orientation, delta);
572
584
  var t = calcSnap(moveobj, withobj, axis, orientation, delta);
573
- return moveobj.translate(t);
585
+ return assertValidCSG(moveobj.translate(t), 'snap');
574
586
  }
575
587
 
576
588
  /**
@@ -583,26 +595,40 @@ export function snap(moveobj, withobj, axis, orientation, delta) {
583
595
  * @return {CSG} [description]
584
596
  */
585
597
  export function flush(moveobj, withobj, axis, mside, wside) {
586
- return moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside));
598
+ return assertValidCSG(moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside)), 'flush');
587
599
  }
588
600
 
601
+ /**
602
+ *
603
+ * @param {AxisStrings} axes
604
+ * @param {function(number, string): number} valfun
605
+ * @param {Array<number>} [a] In initial array to apply the values to, if not provided a new array will be created.
606
+ * @returns {Array<number>} The resulting array after applying the function to the specified axes.
607
+ */
589
608
  export function axisApply(axes, valfun, a) {
590
609
  debug('axisApply', axes, valfun, a);
610
+
611
+ /** @type {Array<number>} */
591
612
  var retval = a || [0, 0, 0];
613
+
614
+ /** @type {Record<AxisString, number>} */
592
615
  var lookup = {
593
616
  x: 0,
594
617
  y: 1,
595
618
  z: 2
596
619
  };
597
620
  axes.split('').forEach(function (axis) {
598
- retval[lookup[axis]] = valfun(lookup[axis], axis);
621
+ retval[lookup[/** @type {AxisString} */ (axis)]] = valfun(
622
+ lookup[/** @type {AxisString} */ (axis)],
623
+ axis
624
+ );
599
625
  });
600
626
 
601
627
  return retval;
602
628
  }
603
629
 
604
630
  export function axis2array(axes, valfun) {
605
- depreciated('axis2array');
631
+ depreciated('axis2array', false, 'Use axisApply instead.');
606
632
  var a = [0, 0, 0];
607
633
  var lookup = {
608
634
  x: 0,
@@ -652,7 +678,7 @@ export function calcmidlineTo(o, axis, to) {
652
678
  }
653
679
 
654
680
  export function midlineTo(o, axis, to) {
655
- return o.translate(calcmidlineTo(o, axis, to));
681
+ return assertValidCSG(o.translate(calcmidlineTo(o, axis, to)), 'midlineTo');
656
682
  }
657
683
 
658
684
  export function translator(o, axis, withObj) {
@@ -678,7 +704,7 @@ export function calcCenterWith(o, axes, withObj, delta = 0) {
678
704
  }
679
705
 
680
706
  export function centerWith(o, axis, withObj) {
681
- return o.translate(calcCenterWith(o, axis, withObj));
707
+ return assertValidCSG(o.translate(calcCenterWith(o, axis, withObj)), 'centerWith');
682
708
  }
683
709
 
684
710
  /**
@@ -709,6 +735,118 @@ export function getDelta(size, bounds, axis, offset, nonzero) {
709
735
  });
710
736
  }
711
737
 
738
+
739
+ var EPS = 1e-5;
740
+
741
+ /**
742
+ * Split a CSG object into two halves along a plane by directly
743
+ * splitting polygons. This avoids BSP-tree-based boolean operations
744
+ * which can fail on geometry produced by stretchAtPlane.
745
+ * @param {CSG} csg The object to split
746
+ * @param {CSG.Plane} plane The splitting plane
747
+ * @return {{ front: CSG, back: CSG }} front (positive normal side) and back (negative normal side)
748
+ */
749
+ function splitCSGByPlane(csg, plane) {
750
+ var frontPolys = [];
751
+ var backPolys = [];
752
+
753
+ csg.polygons.forEach(function (poly) {
754
+ var vertices = poly.vertices;
755
+ var numVerts = vertices.length;
756
+ var hasfront = false;
757
+ var hasback = false;
758
+ var vertexIsBack = [];
759
+
760
+ for (var i = 0; i < numVerts; i++) {
761
+ var t = plane.normal.dot(vertices[i].pos) - plane.w;
762
+ vertexIsBack.push(t < 0);
763
+ if (t > EPS) hasfront = true;
764
+ if (t < -EPS) hasback = true;
765
+ }
766
+
767
+ if (!hasfront && !hasback) {
768
+ // coplanar — assign based on normal alignment
769
+ var d = plane.normal.dot(poly.plane.normal);
770
+ if (d >= 0) {
771
+ frontPolys.push(poly);
772
+ } else {
773
+ backPolys.push(poly);
774
+ }
775
+ } else if (!hasback) {
776
+ frontPolys.push(poly);
777
+ } else if (!hasfront) {
778
+ backPolys.push(poly);
779
+ } else {
780
+ // spanning — split the polygon
781
+ var fv = [];
782
+ var bv = [];
783
+ for (var vi = 0; vi < numVerts; vi++) {
784
+ var vertex = vertices[vi];
785
+ var nextVi = (vi + 1) % numVerts;
786
+ var isback = vertexIsBack[vi];
787
+ var nextisback = vertexIsBack[nextVi];
788
+
789
+ if (isback === nextisback) {
790
+ if (isback) {
791
+ bv.push(vertex);
792
+ } else {
793
+ fv.push(vertex);
794
+ }
795
+ } else {
796
+ var point = vertex.pos;
797
+ var nextpoint = vertices[nextVi].pos;
798
+ var ip = plane.splitLineBetweenPoints(point, nextpoint);
799
+ var iv = new CSG.Vertex(ip);
800
+ if (isback) {
801
+ bv.push(vertex);
802
+ bv.push(iv);
803
+ fv.push(iv);
804
+ } else {
805
+ fv.push(vertex);
806
+ fv.push(iv);
807
+ bv.push(iv);
808
+ }
809
+ }
810
+ }
811
+ // Remove degenerate (near-duplicate) adjacent vertices that arise
812
+ // when the cut plane passes very close to existing vertices.
813
+ // This matches the cleanup done by the BSP-tree splitter.
814
+ var EPSEPS = EPS * EPS;
815
+ if (fv.length >= 3) {
816
+ var prev = fv[fv.length - 1];
817
+ for (var fi = 0; fi < fv.length; fi++) {
818
+ var curr = fv[fi];
819
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
820
+ fv.splice(fi, 1);
821
+ fi--;
822
+ }
823
+ prev = curr;
824
+ }
825
+ }
826
+ if (bv.length >= 3) {
827
+ var prev = bv[bv.length - 1];
828
+ for (var bi = 0; bi < bv.length; bi++) {
829
+ var curr = bv[bi];
830
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
831
+ bv.splice(bi, 1);
832
+ bi--;
833
+ }
834
+ prev = curr;
835
+ }
836
+ }
837
+ if (fv.length >= 3)
838
+ frontPolys.push(new CSG.Polygon(fv, poly.shared, poly.plane));
839
+ if (bv.length >= 3)
840
+ backPolys.push(new CSG.Polygon(bv, poly.shared, poly.plane));
841
+ }
842
+ });
843
+
844
+ return {
845
+ front: CSG.fromPolygons(frontPolys),
846
+ back: CSG.fromPolygons(backPolys)
847
+ };
848
+ }
849
+
712
850
  /**
713
851
  * Cut an object into two pieces, along a given axis. The offset
714
852
  * allows you to move the cut plane along the cut axis. For example,
@@ -718,7 +856,7 @@ export function getDelta(size, bounds, axis, offset, nonzero) {
718
856
  *
719
857
  * You can angle the cut plane and position the rotation point.
720
858
  *
721
- * ![bisect example](./images/bisect.png)
859
+ * ![bisect example](../test/images/bisect%20object%20positive.snap.png)
722
860
  * @param {CSG} object object to bisect
723
861
  * @param {string} axis axis to cut along
724
862
  * @param {number} [offset] offset to cut at
@@ -841,9 +979,33 @@ export function bisect(...args) {
841
979
  }
842
980
  );
843
981
 
982
+ var negative = object.cutByPlane(cutplane.plane);
983
+ var positive = object.cutByPlane(cutplane.plane.flipped());
984
+
985
+ // Detect cutByPlane failure: if a half's bounding box in the cut axis
986
+ // is not smaller than the original, the BSP-tree-based cut failed.
987
+ // Fall back to direct polygon splitting which is more robust, then
988
+ // apply cutByPlane to the simpler half to add cap faces.
989
+ var negSize = size(negative);
990
+ var posSize = size(positive);
991
+ if (negSize[axis] >= objectSize[axis] - EPS ||
992
+ posSize[axis] >= objectSize[axis] - EPS) {
993
+ var halves = splitCSGByPlane(object, cutplane.plane);
994
+ if (negSize[axis] >= objectSize[axis] - EPS) {
995
+ negative = halves.back;
996
+ // Cap the open cut face
997
+ try { negative = negative.cutByPlane(cutplane.plane); } catch (e) { /* keep uncapped */ }
998
+ }
999
+ if (posSize[axis] >= objectSize[axis] - EPS) {
1000
+ positive = halves.front;
1001
+ // Cap the open cut face
1002
+ try { positive = positive.cutByPlane(cutplane.plane.flipped()); } catch (e) { /* keep uncapped */ }
1003
+ }
1004
+ }
1005
+
844
1006
  var g = Group('negative,positive', [
845
- object.cutByPlane(cutplane.plane).color(options.color && 'red'),
846
- object.cutByPlane(cutplane.plane.flipped()).color(options.color && 'blue')
1007
+ negative.color(options.color && 'red'),
1008
+ positive.color(options.color && 'blue')
847
1009
  ]);
848
1010
 
849
1011
  if (options.addRotationCenter)
@@ -905,7 +1067,7 @@ export function slice(
905
1067
  * Creates a `JsCadUtilsGroup` object that has `body` and `wedge` objects. The `wedge` object
906
1068
  * is created by radially cutting the object from the `start` to the `end` angle.
907
1069
  *
908
- * ![wedge example](./images/wedge.png)
1070
+ * ![wedge example](../test/images/wedge.snap.png)
909
1071
  *
910
1072
  *
911
1073
  * @example
@@ -961,7 +1123,7 @@ export function stretch(object, axis, distance, offset) {
961
1123
  var objectSize = size(object);
962
1124
  var cutDelta = getDelta(objectSize, bounds, axis, offset, true);
963
1125
  // debug('stretch.cutDelta', cutDelta, normal[axis]);
964
- return object.stretchAtPlane(normal[axis], cutDelta, distance);
1126
+ return assertValidCSG(object.stretchAtPlane(normal[axis], cutDelta, distance), 'stretch');
965
1127
  }
966
1128
 
967
1129
  /**
@@ -1018,7 +1180,7 @@ export function poly2solid(top, bottom, height) {
1018
1180
  );
1019
1181
  // }
1020
1182
 
1021
- return CSG.fromPolygons(polygons);
1183
+ return assertValidCSG(CSG.fromPolygons(polygons), 'poly2solid');
1022
1184
  }
1023
1185
 
1024
1186
  export function slices2poly(slices, options, axis) {
@@ -1102,7 +1264,7 @@ export function slices2poly(slices, options, axis) {
1102
1264
  }
1103
1265
  });
1104
1266
 
1105
- return CSG.fromPolygons(polygons);
1267
+ return assertValidCSG(CSG.fromPolygons(polygons), 'slices2poly');
1106
1268
  }
1107
1269
 
1108
1270
  export function normalVector(axis) {
@@ -1116,7 +1278,7 @@ export function normalVector(axis) {
1116
1278
  normalVector: CSG.Vector3D.Create(0, 0, 1)
1117
1279
  },
1118
1280
  y: {
1119
- orthoNormalCartesian: ['X', 'Z'],
1281
+ orthoNormalCartesian: ['Z', 'X'],
1120
1282
  normalVector: CSG.Vector3D.Create(0, 0, 1)
1121
1283
  }
1122
1284
  };
@@ -1170,6 +1332,7 @@ export function sliceParams(orientation, radius, bounds) {
1170
1332
  // };
1171
1333
  // },
1172
1334
 
1335
+
1173
1336
  export function reShape(object, radius, orientation, options, slicer) {
1174
1337
  options = options || {};
1175
1338
  var b = object.getBounds();
@@ -1211,57 +1374,61 @@ export function reShape(object, radius, orientation, options, slicer) {
1211
1374
  ).color(options.color);
1212
1375
 
1213
1376
  var remainder = object.cutByPlane(plane);
1214
- return union([
1377
+ return assertValidCSG(union([
1215
1378
  options.unionOriginal ? object : remainder,
1216
1379
  delta.translate(si.moveDelta)
1217
- ]);
1380
+ ]), 'reShape');
1218
1381
  }
1219
1382
 
1220
1383
  export function chamfer(object, radius, orientation, options) {
1221
- return reShape(object, radius, orientation, options, function (
1222
- first,
1223
- last,
1224
- slice
1225
- ) {
1226
- return [
1227
- {
1228
- poly: slice,
1229
- offset: new CSG.Vector3D(first)
1230
- },
1231
- {
1232
- poly: enlarge(slice, [-radius * 2, -radius * 2]),
1233
- offset: new CSG.Vector3D(last)
1234
- }
1235
- ];
1236
- });
1384
+ return assertValidCSG(reShape(
1385
+ object,
1386
+ radius,
1387
+ orientation,
1388
+ options,
1389
+ function (first, last, slice) {
1390
+ return [
1391
+ {
1392
+ poly: slice,
1393
+ offset: new CSG.Vector3D(first)
1394
+ },
1395
+ {
1396
+ poly: enlarge(slice, [-radius * 2, -radius * 2]),
1397
+ offset: new CSG.Vector3D(last)
1398
+ }
1399
+ ];
1400
+ }
1401
+ ), 'chamfer');
1237
1402
  }
1238
1403
 
1239
1404
  export function fillet(object, radius, orientation, options) {
1240
1405
  options = options || {};
1241
- return reShape(object, radius, orientation, options, function (
1242
- first,
1243
- last,
1244
- slice
1245
- ) {
1246
- var v1 = new CSG.Vector3D(first);
1247
- var v2 = new CSG.Vector3D(last);
1248
-
1249
- var res = options.resolution || CSG.defaultResolution3D;
1250
-
1251
- var slices = array.range(0, res).map(function (i) {
1252
- var p = i > 0 ? i / (res - 1) : 0;
1253
- var v = v1.lerp(v2, p);
1254
-
1255
- var size = -radius * 2 - Math.cos(Math.asin(p)) * (-radius * 2);
1256
-
1257
- return {
1258
- poly: enlarge(slice, [size, size]),
1259
- offset: v
1260
- };
1261
- });
1262
-
1263
- return slices;
1264
- });
1406
+ return assertValidCSG(reShape(
1407
+ object,
1408
+ radius,
1409
+ orientation,
1410
+ options,
1411
+ function (first, last, slice) {
1412
+ var v1 = new CSG.Vector3D(first);
1413
+ var v2 = new CSG.Vector3D(last);
1414
+
1415
+ var res = options.resolution || CSG.defaultResolution3D;
1416
+
1417
+ var slices = array.range(0, res).map(function (i) {
1418
+ var p = i > 0 ? i / (res - 1) : 0;
1419
+ var v = v1.lerp(v2, p);
1420
+
1421
+ var size = -radius * 2 - Math.cos(Math.asin(p)) * (-radius * 2);
1422
+
1423
+ return {
1424
+ poly: enlarge(slice, [size, size]),
1425
+ offset: v
1426
+ };
1427
+ });
1428
+
1429
+ return slices;
1430
+ }
1431
+ ), 'fillet');
1265
1432
  }
1266
1433
 
1267
1434
  export function calcRotate(part, solid, axis /* , angle */) {
@@ -1278,7 +1445,7 @@ export function calcRotate(part, solid, axis /* , angle */) {
1278
1445
  export function rotateAround(part, solid, axis, angle) {
1279
1446
  var { rotationCenter, rotationAxis } = calcRotate(part, solid, axis, angle);
1280
1447
 
1281
- return part.rotate(rotationCenter, rotationAxis, angle);
1448
+ return assertValidCSG('rotateAround')(part.rotate(rotationCenter, rotationAxis, angle));
1282
1449
  }
1283
1450
  function cloneProperties(from, to, depth = 0) {
1284
1451
  return Object.entries(from).reduce((props, [key, value]) => {
@@ -1293,7 +1460,7 @@ export function clone(o) {
1293
1460
  cloneProperties(o, c);
1294
1461
 
1295
1462
  debug('clone', o, c, CSG);
1296
- return c;
1463
+ return assertValidCSG(c, 'clone');
1297
1464
  }
1298
1465
 
1299
1466
  /**
@@ -1313,5 +1480,5 @@ export function addConnector(
1313
1480
  normal = [0, 0, 1]
1314
1481
  ) {
1315
1482
  object.properties[name] = new CSG.Connector(point, axis, normal);
1316
- return object;
1483
+ return assertValidCSG('addConnector')(object);
1317
1484
  }