@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/dist/index.js CHANGED
@@ -89,6 +89,54 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
89
89
  function _arrayWithHoles(r) {
90
90
  if (Array.isArray(r)) return r;
91
91
  }
92
+ function _createForOfIteratorHelper(r, e) {
93
+ var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
94
+ if (!t) {
95
+ if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
96
+ t && (r = t);
97
+ var n = 0,
98
+ F = function () {};
99
+ return {
100
+ s: F,
101
+ n: function () {
102
+ return n >= r.length ? {
103
+ done: !0
104
+ } : {
105
+ done: !1,
106
+ value: r[n++]
107
+ };
108
+ },
109
+ e: function (r) {
110
+ throw r;
111
+ },
112
+ f: F
113
+ };
114
+ }
115
+ throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
116
+ }
117
+ var o,
118
+ a = !0,
119
+ u = !1;
120
+ return {
121
+ s: function () {
122
+ t = t.call(r);
123
+ },
124
+ n: function () {
125
+ var r = t.next();
126
+ return a = r.done, r;
127
+ },
128
+ e: function (r) {
129
+ u = !0, o = r;
130
+ },
131
+ f: function () {
132
+ try {
133
+ a || null == t.return || t.return();
134
+ } finally {
135
+ if (u) throw o;
136
+ }
137
+ }
138
+ };
139
+ }
92
140
  function _defineProperty(e, r, t) {
93
141
  return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
94
142
  value: t,
@@ -761,6 +809,344 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
761
809
  return o.setColor(c);
762
810
  }
763
811
 
812
+ /* globals jscadUtilsAssertValidCSG jscadUtilsAssertValidCSGWarnings */
813
+ /**
814
+ * @typedef {Object} CSG
815
+ * @property {Array<{vertices: Array<{pos: any}>}>} polygons
816
+ * @property {function(): CSG} [canonicalized]
817
+ * @property {function(): CSG} [reTesselated]
818
+ * @property {function(): CSG} [fixTJunctions]
819
+ * @property {function(): Array} [getBounds]
820
+ * @property {Object} [properties]
821
+ */
822
+ /**
823
+ * Validate that a CSG object represents a solid, watertight mesh
824
+ * without degenerate faces. Returns an object with an `ok` boolean
825
+ * and an `errors` array describing any problems found.
826
+ *
827
+ * Checks performed:
828
+ * - **No empty mesh** – the object must contain at least one polygon.
829
+ * - **No degenerate polygons** – every polygon must have ≥ 3 vertices
830
+ * and a computable area greater than `EPS²`.
831
+ * - **Watertight / manifold edges** – every directed edge A→B in the
832
+ * mesh must be matched by exactly one reverse edge B→A in another
833
+ * polygon. Unmatched edges indicate holes; edges shared more than
834
+ * twice indicate non-manifold geometry.
835
+ *
836
+ * By default, the mesh is canonicalized and T-junctions are repaired
837
+ * before validation so that results from boolean operations (union,
838
+ * subtract, intersect) can be validated successfully. Pass
839
+ * `{ fixTJunctions: false }` to skip this step and validate the raw
840
+ * mesh.
841
+ *
842
+ * @param {CSG} csg The CSG object to validate.
843
+ * @param {object} [options] Validation options.
844
+ * @param {boolean} [options.fixTJunctions=true] Whether to canonicalize and fix T-junctions before validation.
845
+ * @return {{ ok: boolean, errors: string[], warnings: string[] }} Validation result.
846
+ * @function validateCSG
847
+ */
848
+ function validateCSG(csg, options) {
849
+ /** @type {string[]} */
850
+ var errors = [];
851
+ /** @type {string[]} */
852
+ var warnings = [];
853
+ if (!csg || !csg.polygons || csg.polygons.length === 0) {
854
+ errors.push('Empty mesh: no polygons');
855
+ return {
856
+ ok: false,
857
+ errors: errors,
858
+ warnings: warnings
859
+ };
860
+ }
861
+ var opts = _objectSpread2({
862
+ fixTJunctions: true
863
+ }, options);
864
+
865
+ // Optionally canonicalize and fix T-junctions so that boolean-op
866
+ // output can pass the watertight check.
867
+ if (opts.fixTJunctions && typeof csg.canonicalized === 'function') {
868
+ csg = csg.canonicalized();
869
+ if (typeof csg.reTesselated === 'function') {
870
+ csg = csg.reTesselated();
871
+ }
872
+ if (typeof csg.fixTJunctions === 'function') {
873
+ csg = csg.fixTJunctions();
874
+ }
875
+ }
876
+ var AREA_EPS = 1e-10;
877
+ var KEY_EPS = 1e-5;
878
+ var degenerateCount = 0;
879
+ var invalidVertexCount = 0;
880
+
881
+ // Check for NaN/Infinity vertex coordinates which cause WebGL errors
882
+ // (GL_INVALID_VALUE: glVertexAttribPointer: Vertex attribute size must be 1, 2, 3, or 4)
883
+ var _iterator = _createForOfIteratorHelper(csg.polygons),
884
+ _step;
885
+ try {
886
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
887
+ var npoly = _step.value;
888
+ var _iterator4 = _createForOfIteratorHelper(npoly.vertices),
889
+ _step4;
890
+ try {
891
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
892
+ var nvert = _step4.value;
893
+ var np = nvert.pos;
894
+ if (!Number.isFinite(np.x) || !Number.isFinite(np.y) || !Number.isFinite(np.z) || Number.isNaN(np.x) || Number.isNaN(np.y) || Number.isNaN(np.z)) {
895
+ invalidVertexCount++;
896
+ break;
897
+ }
898
+ }
899
+ } catch (err) {
900
+ _iterator4.e(err);
901
+ } finally {
902
+ _iterator4.f();
903
+ }
904
+ }
905
+ } catch (err) {
906
+ _iterator.e(err);
907
+ } finally {
908
+ _iterator.f();
909
+ }
910
+ if (invalidVertexCount > 0) {
911
+ errors.push(invalidVertexCount + ' polygon(s) with invalid vertex coordinates (NaN or Infinity)');
912
+ }
913
+
914
+ // Position-based vertex key (shared vertices across polygons have different
915
+ // object tags but the same position, so we round coordinates to match them).
916
+ /** @param {{ pos: { x: number, y: number, z: number } }} v */
917
+ function vtxKey(v) {
918
+ var p = v.pos;
919
+ return Math.round(p.x / KEY_EPS) + ',' + Math.round(p.y / KEY_EPS) + ',' + Math.round(p.z / KEY_EPS);
920
+ }
921
+
922
+ // First pass: identify degenerate polygons
923
+ var validPolygons = [];
924
+ var _iterator2 = _createForOfIteratorHelper(csg.polygons),
925
+ _step2;
926
+ try {
927
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
928
+ var poly = _step2.value;
929
+ var verts = poly.vertices;
930
+ var nv = verts.length;
931
+ if (nv < 3) {
932
+ degenerateCount++;
933
+ continue;
934
+ }
935
+
936
+ // Skip polygons with invalid vertex coordinates
937
+ var hasInvalid = false;
938
+ var _iterator5 = _createForOfIteratorHelper(verts),
939
+ _step5;
940
+ try {
941
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
942
+ var vert = _step5.value;
943
+ var ip = vert.pos;
944
+ if (!Number.isFinite(ip.x) || !Number.isFinite(ip.y) || !Number.isFinite(ip.z)) {
945
+ hasInvalid = true;
946
+ break;
947
+ }
948
+ }
949
+ } catch (err) {
950
+ _iterator5.e(err);
951
+ } finally {
952
+ _iterator5.f();
953
+ }
954
+ if (hasInvalid) continue;
955
+
956
+ // Check degenerate area using cross-product summation
957
+ var area = 0;
958
+ for (var ai = 0; ai < nv - 2; ai++) {
959
+ area += verts[ai + 1].pos.minus(verts[0].pos).cross(verts[ai + 2].pos.minus(verts[ai + 1].pos)).length();
960
+ }
961
+ area *= 0.5;
962
+ if (area < AREA_EPS) {
963
+ degenerateCount++;
964
+ continue;
965
+ }
966
+ validPolygons.push(poly);
967
+ }
968
+ } catch (err) {
969
+ _iterator2.e(err);
970
+ } finally {
971
+ _iterator2.f();
972
+ }
973
+ if (degenerateCount > 0) {
974
+ warnings.push(degenerateCount + ' degenerate polygon(s) (fewer than 3 vertices or near-zero area)');
975
+
976
+ // Rebuild the CSG from valid polygons only and re-run the repair
977
+ // pipeline so that fixTJunctions can close gaps left by the removed
978
+ // degenerate faces.
979
+ /* eslint-disable no-undef */
980
+ // @ts-ignore — CSG is a runtime global injected by the JSCAD compat layer
981
+ if (opts.fixTJunctions && typeof CSG !== 'undefined') {
982
+ // @ts-ignore
983
+ var cleaned = CSG.fromPolygons(validPolygons);
984
+ /* eslint-enable no-undef */
985
+ cleaned = cleaned.canonicalized();
986
+ if (typeof cleaned.reTesselated === 'function') {
987
+ cleaned = cleaned.reTesselated();
988
+ }
989
+ if (typeof cleaned.fixTJunctions === 'function') {
990
+ cleaned = cleaned.fixTJunctions();
991
+ }
992
+ // Re-scan for valid polygons after second repair pass
993
+ validPolygons = [];
994
+ var _iterator3 = _createForOfIteratorHelper(cleaned.polygons),
995
+ _step3;
996
+ try {
997
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
998
+ var cpoly = _step3.value;
999
+ var cverts = cpoly.vertices;
1000
+ var cnv = cverts.length;
1001
+ if (cnv < 3) continue;
1002
+ var carea = 0;
1003
+ for (var cai = 0; cai < cnv - 2; cai++) {
1004
+ carea += cverts[cai + 1].pos.minus(cverts[0].pos).cross(cverts[cai + 2].pos.minus(cverts[cai + 1].pos)).length();
1005
+ }
1006
+ carea *= 0.5;
1007
+ if (carea < AREA_EPS) continue;
1008
+ validPolygons.push(cpoly);
1009
+ }
1010
+ } catch (err) {
1011
+ _iterator3.e(err);
1012
+ } finally {
1013
+ _iterator3.f();
1014
+ }
1015
+ }
1016
+ }
1017
+
1018
+ // Edge map: key = "vtxKeyA/vtxKeyB", value = count
1019
+ /** @type {Record<string, number>} */
1020
+ var edgeCounts = {};
1021
+
1022
+ // Accumulate directed edges from valid polygons only
1023
+ for (var _i = 0, _validPolygons = validPolygons; _i < _validPolygons.length; _i++) {
1024
+ var vpoly = _validPolygons[_i];
1025
+ var vverts = vpoly.vertices;
1026
+ var vnv = vverts.length;
1027
+ for (var ei = 0; ei < vnv; ei++) {
1028
+ var v0 = vverts[ei];
1029
+ var v1 = vverts[(ei + 1) % vnv];
1030
+ var edgeKey = vtxKey(v0) + '/' + vtxKey(v1);
1031
+ edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1;
1032
+ }
1033
+ }
1034
+
1035
+ // Check edge manifoldness: every edge A→B should be cancelled by B→A
1036
+ var unmatchedEdges = 0;
1037
+ var nonManifoldEdges = 0;
1038
+ /** @type {Record<string, boolean>} */
1039
+ var checked = {};
1040
+ for (var _i2 = 0, _Object$keys = Object.keys(edgeCounts); _i2 < _Object$keys.length; _i2++) {
1041
+ var _edgeKey = _Object$keys[_i2];
1042
+ if (checked[_edgeKey]) continue;
1043
+ var parts = _edgeKey.split('/');
1044
+ var reverseKey = parts[1] + '/' + parts[0];
1045
+ var forwardCount = edgeCounts[_edgeKey] || 0;
1046
+ var reverseCount = edgeCounts[reverseKey] || 0;
1047
+ checked[_edgeKey] = true;
1048
+ checked[reverseKey] = true;
1049
+ if (forwardCount !== reverseCount) {
1050
+ unmatchedEdges += Math.abs(forwardCount - reverseCount);
1051
+ }
1052
+ if (forwardCount > 1 || reverseCount > 1) {
1053
+ nonManifoldEdges++;
1054
+ }
1055
+ }
1056
+ if (unmatchedEdges > 0) {
1057
+ errors.push(unmatchedEdges + ' unmatched edge(s): mesh is not watertight');
1058
+ }
1059
+ if (nonManifoldEdges > 0) {
1060
+ errors.push(nonManifoldEdges + ' non-manifold edge(s): edge shared by more than 2 polygons');
1061
+ }
1062
+ return {
1063
+ ok: errors.length === 0,
1064
+ errors: errors,
1065
+ warnings: warnings
1066
+ };
1067
+ }
1068
+
1069
+ /** @param {any} csg @returns {any} */
1070
+ function _noOp(csg) {
1071
+ return csg;
1072
+ }
1073
+
1074
+ /**
1075
+ * @param {boolean} warnEnabled
1076
+ * @returns {function(*, string=, string=): *}
1077
+ */
1078
+ function _makeAssertFn(warnEnabled) {
1079
+ return function _assert(csg) {
1080
+ var functionName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'unknown';
1081
+ var moduleName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'unknown';
1082
+ // Only validate CSG-like objects (they have a polygons array).
1083
+ // CAG objects (2D cross-sections) have `sides` instead and are passed through.
1084
+ if (!csg || csg.polygons === undefined) return csg;
1085
+ var result = validateCSG(csg);
1086
+ if (!result.ok) {
1087
+ throw new Error(moduleName + ':' + functionName + ': ' + 'invalid CSG: ' + result.errors.join(', '));
1088
+ }
1089
+ if (warnEnabled && result.warnings.length > 0) {
1090
+ throw new Error(moduleName + ':' + functionName + ': ' + 'CSG warnings: ' + result.warnings.join(', '));
1091
+ }
1092
+ return csg;
1093
+ };
1094
+ }
1095
+
1096
+ // Live pointer that all returned closures call through — swap this and all
1097
+ // existing closures immediately pick up the change.
1098
+ /** @type {function(*, string=, string=): *} */
1099
+ var _assertFn = _noOp;
1100
+
1101
+ // Read compat globals set by initJscadutils — mirrors the Debug() pattern
1102
+ // in debug.js. Returns _noOp when globals are absent (ESM / test context).
1103
+ function _resolveFromGlobals() {
1104
+ /* eslint-disable no-undef */
1105
+ // @ts-ignore — globals set by the JSCAD compat layer before bundle injection
1106
+ var enabled = typeof jscadUtilsAssertValidCSG !== 'undefined' && !!jscadUtilsAssertValidCSG;
1107
+ // @ts-ignore
1108
+ var warnEnabled = typeof jscadUtilsAssertValidCSGWarnings !== 'undefined' && !!jscadUtilsAssertValidCSGWarnings;
1109
+ /* eslint-enable no-undef */
1110
+ return enabled ? _makeAssertFn(warnEnabled) : _noOp;
1111
+ }
1112
+
1113
+ /**
1114
+ * Returns an asserter function bound to `moduleName`. Call the returned
1115
+ * function with a CSG object and the calling function's name to validate it
1116
+ * (when enabled) or pass it through unchanged (when disabled).
1117
+ *
1118
+ * Best practice is to call `AssertValidCSG` once per module at load time and
1119
+ * capture the result as a module-level constant so that `moduleName` appears
1120
+ * consistently in every error message thrown from that module.
1121
+ *
1122
+ * On creation, reads compat globals (jscadUtilsAssertValidCSG /
1123
+ * jscadUtilsAssertValidCSGWarnings) if setValidationEnabled() has not been
1124
+ * called explicitly — identical to how Debug('name') reads jscadUtilsDebug.
1125
+ *
1126
+ * Error message format: `moduleName:functionName: invalid CSG: <errors>`
1127
+ *
1128
+ * @example
1129
+ * // Once at the top of your module:
1130
+ * const assertValidCSG = AssertValidCSG('myModule');
1131
+ *
1132
+ * export function enlarge(object, ...) {
1133
+ * // ...
1134
+ * return assertValidCSG(new_object.translate(delta), 'enlarge');
1135
+ * }
1136
+ *
1137
+ * @param {string} [moduleName='unknown'] Module name, included in error messages.
1138
+ * @return {function(CSG, string=): CSG}
1139
+ */
1140
+ function AssertValidCSG() {
1141
+ var moduleName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown';
1142
+ {
1143
+ _assertFn = _resolveFromGlobals();
1144
+ }
1145
+ return function (csg, name) {
1146
+ return _assertFn(csg, name, moduleName);
1147
+ };
1148
+ }
1149
+
764
1150
  /** @typedef {object} ExtendedCSG
765
1151
  * @property {object} prototype
766
1152
  * @property {function} prototype.color
@@ -833,7 +1219,6 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
833
1219
  * @property {function} stackTrace
834
1220
  * @property {function} getConnector
835
1221
  */
836
-
837
1222
  /**
838
1223
  * Initialize `jscad-utils` and add utilities to the `proto` object.
839
1224
  * @param {CSG} proto The global `proto` object
@@ -943,6 +1328,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
943
1328
  proto.prototype.subtractIf = function subtractIf(object, condition) {
944
1329
  return condition ? this.subtract(result(this, object)) : this;
945
1330
  };
1331
+ proto.prototype.validate = function validate(options) {
1332
+ return validateCSG(this, options);
1333
+ };
946
1334
  proto.prototype._translate = proto.prototype.translate;
947
1335
 
948
1336
  /**
@@ -994,16 +1382,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
994
1382
  'default': init
995
1383
  });
996
1384
 
997
- var CSG = jsCadCSG__default["default"].CSG,
1385
+ var CSG$1 = jsCadCSG__default["default"].CSG,
998
1386
  CAG = jsCadCSG__default["default"].CAG;
999
1387
  var rectangular_extrude = scadApi__default["default"].extrusions.rectangular_extrude;
1000
1388
  var _scadApi$text = scadApi__default["default"].text,
1001
1389
  vector_text = _scadApi$text.vector_text,
1002
1390
  vector_char = _scadApi$text.vector_char;
1003
1391
  var union = scadApi__default["default"].booleanOps.union;
1004
- init(CSG);
1392
+ init(CSG$1);
1005
1393
 
1006
1394
  var debug$3 = Debug('jscadUtils:group');
1395
+ var assertValidCSG$2 = AssertValidCSG('group');
1007
1396
 
1008
1397
  /**
1009
1398
  * @function JsCadUtilsGroup
@@ -1082,7 +1471,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1082
1471
  debug$3('combine mapPick', value, key, object);
1083
1472
  return map ? map(value, key, index, object) : identity(value);
1084
1473
  }, self.name));
1085
- return g.subtractIf(self.holes && Array.isArray(self.holes) ? union(self.holes) : self.holes, self.holes && !options.noholes);
1474
+ return assertValidCSG$2(g.subtractIf(self.holes && Array.isArray(self.holes) ? union(self.holes) : self.holes, self.holes && !options.noholes), 'combine');
1086
1475
  } catch (err) {
1087
1476
  debug$3('combine error', this, pieces, options, err);
1088
1477
  throw error("group::combine error \"".concat(err.message || err.toString(), "\"\nthis: ").concat(this, "\npieces: \"").concat(pieces, "\"\noptions: ").concat(JSON.stringify(options, null, 2), "\nstack: ").concat(err.stack, "\n"), 'JSCAD_UTILS_GROUP_ERROR');
@@ -1144,7 +1533,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1144
1533
  });
1145
1534
  if (self.holes) {
1146
1535
  group.holes = toArray(self.holes).map(function (part) {
1147
- return map(CSG.fromPolygons(part.toPolygons()), 'holes');
1536
+ return assertValidCSG$2(map(CSG$1.fromPolygons(part.toPolygons()), 'holes'), 'clone');
1148
1537
  });
1149
1538
  }
1150
1539
  return group;
@@ -1172,7 +1561,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1172
1561
  var rotationCenter = solid.centroid();
1173
1562
  var rotationAxis = axes[axis];
1174
1563
  self.map(function (part) {
1175
- return part.rotate(rotationCenter, rotationAxis, angle);
1564
+ return assertValidCSG$2(part.rotate(rotationCenter, rotationAxis, angle), 'rotate');
1176
1565
  });
1177
1566
  return self;
1178
1567
  };
@@ -1206,7 +1595,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1206
1595
  // debug(', self);
1207
1596
  var t = calcSnap(self.combine(part), to, axis, orientation, delta);
1208
1597
  self.map(function (part) {
1209
- return part.translate(t);
1598
+ return assertValidCSG$2(part.translate(t), 'snap');
1210
1599
  });
1211
1600
  return self;
1212
1601
  } catch (err) {
@@ -1232,7 +1621,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1232
1621
  noholes: true
1233
1622
  }), axis, to, delta);
1234
1623
  self.map(function (part /*, name */) {
1235
- return part.translate(t);
1624
+ return assertValidCSG$2(part.translate(t), 'align');
1236
1625
  });
1237
1626
 
1238
1627
  // if (self.holes)
@@ -1270,14 +1659,14 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1270
1659
  var myConnector = connectorName.split('.').reduce(function (a, v) {
1271
1660
  return a[v];
1272
1661
  }, self.parts[partName].properties);
1273
- debug$3('toConnector', to instanceof CSG.Connector);
1662
+ debug$3('toConnector', to instanceof CSG$1.Connector);
1274
1663
  var toConnector = toConnectorName.split('.').reduce(function (a, v) {
1275
1664
  return a[v];
1276
1665
  }, to.properties);
1277
1666
  var matrix = myConnector.getTransformationTo(toConnector, mirror, normalrotation);
1278
1667
  debug$3('connectTo', matrix);
1279
1668
  self.map(function (part) {
1280
- return part.transform(matrix);
1669
+ return assertValidCSG$2(part.transform(matrix), 'connectTo');
1281
1670
  });
1282
1671
  return self;
1283
1672
  };
@@ -1298,7 +1687,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1298
1687
  // debug(' part, t);
1299
1688
  // var t = util.calcCenterWith(self.combine(part), axis, to, delta);
1300
1689
  self.map(function (part) {
1301
- return part.translate(t);
1690
+ return assertValidCSG$2(part.translate(t), 'midlineTo');
1302
1691
  });
1303
1692
 
1304
1693
  // if (self.holes)
@@ -1322,7 +1711,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1322
1711
  var t = Array.isArray(x) ? x : [x, y, z];
1323
1712
  debug$3('translate', t);
1324
1713
  self.map(function (part) {
1325
- return part.translate(t);
1714
+ return assertValidCSG$2(part.translate(t), 'translate');
1326
1715
  });
1327
1716
 
1328
1717
  // if (self.holes)
@@ -1346,7 +1735,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1346
1735
  if (!map) map = identity;
1347
1736
  var g = Group();
1348
1737
  p.forEach(function (name) {
1349
- g.add(map(CSG.fromPolygons(self.parts[name].toPolygons()), name), name);
1738
+ g.add(assertValidCSG$2(map(CSG$1.fromPolygons(self.parts[name].toPolygons()), name), 'pick'), name);
1350
1739
  });
1351
1740
  return g;
1352
1741
  };
@@ -1370,7 +1759,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1370
1759
  debug$3('array error', _this, parts);
1371
1760
  throw error("group::array error \"".concat(name, "\" not found.\nthis: ").concat(_this, "\nparts: \"").concat(parts, "\"\n"), 'JSCAD_UTILS_GROUP_ERROR');
1372
1761
  }
1373
- a.push(map(CSG.fromPolygons(self.parts[name].toPolygons()), name));
1762
+ a.push(assertValidCSG$2(map(CSG$1.fromPolygons(self.parts[name].toPolygons()), name), 'array'));
1374
1763
  });
1375
1764
  return a;
1376
1765
  // } catch (err) {
@@ -1439,7 +1828,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1439
1828
  self.names = names && names.length > 0 && names.split(',') || [];
1440
1829
  if (Array.isArray(objects)) {
1441
1830
  self.parts = zipObject(self.names, objects);
1442
- } else if (objects instanceof CSG) {
1831
+ } else if (objects instanceof CSG$1) {
1443
1832
  self.parts = zipObject(self.names, [objects]);
1444
1833
  } else {
1445
1834
  self.parts = objects || {};
@@ -1464,6 +1853,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1464
1853
  }
1465
1854
 
1466
1855
  var debug$2 = Debug('jscadUtils:util');
1856
+ var assertValidCSG$1 = AssertValidCSG('util');
1857
+
1467
1858
  // import utilInit from '../src/add-prototype';
1468
1859
  // utilInit(CSG);
1469
1860
  // console.trace('CSG', CSG.prototype);
@@ -1627,12 +2018,12 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1627
2018
  // console.trace('label', Object.getPrototypeOf(union(o)));
1628
2019
  // var foo = union(o);
1629
2020
  // console.trace('typeof', typeof foo);
1630
- return center(union(o));
2021
+ return assertValidCSG$1(center(union(o)), 'label');
1631
2022
  }
1632
2023
  function text(text) {
1633
2024
  var l = vector_char(0, 0, text); // l contains a list of polylines to draw
1634
2025
  var _char = l.segments.reduce(function (result, segment) {
1635
- var path = new CSG.Path2D(segment);
2026
+ var path = new CSG$1.Path2D(segment);
1636
2027
  var cag = path.expandToCAG(2);
1637
2028
  // debug('reduce', result, segment, path, cag);
1638
2029
  return result ? result.union(cag) : cag;
@@ -1641,17 +2032,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1641
2032
  }
1642
2033
  function unitCube(length, radius) {
1643
2034
  radius = radius || 0.5;
1644
- return CSG.cube({
2035
+ return assertValidCSG$1(CSG$1.cube({
1645
2036
  center: [0, 0, 0],
1646
2037
  radius: [radius, radius, length || 0.5]
1647
- });
2038
+ }), 'unitCube');
1648
2039
  }
1649
2040
  function unitAxis(length, radius, centroid) {
1650
2041
  debug$2('unitAxis', length, radius, centroid);
1651
2042
  centroid = centroid || [0, 0, 0];
1652
2043
  var unitaxis = unitCube(length, radius).setColor(1, 0, 0).union([unitCube(length, radius).rotateY(90).setColor(0, 1, 0), unitCube(length, radius).rotateX(90).setColor(0, 0, 1)]);
1653
- unitaxis.properties.origin = new CSG.Connector([0, 0, 0], [1, 0, 0], [0, 1, 0]);
1654
- return unitaxis.translate(centroid);
2044
+ unitaxis.properties.origin = new CSG$1.Connector([0, 0, 0], [1, 0, 0], [0, 1, 0]);
2045
+ return assertValidCSG$1(unitaxis.translate(centroid), 'unitAxis');
1655
2046
  }
1656
2047
  function toArray(a) {
1657
2048
  return Array.isArray(a) ? a : [a];
@@ -1775,15 +2166,15 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1775
2166
  }
1776
2167
  function center(object, objectSize) {
1777
2168
  objectSize = objectSize || size(object.getBounds());
1778
- return centerY(centerX(object, objectSize), objectSize);
2169
+ return assertValidCSG$1(centerY(centerX(object, objectSize), objectSize), 'center');
1779
2170
  }
1780
2171
  function centerY(object, objectSize) {
1781
2172
  objectSize = objectSize || size(object.getBounds());
1782
- return object.translate([0, -objectSize.y / 2, 0]);
2173
+ return assertValidCSG$1(object.translate([0, -objectSize.y / 2, 0]), 'centerY');
1783
2174
  }
1784
2175
  function centerX(object, objectSize) {
1785
2176
  objectSize = objectSize || size(object.getBounds());
1786
- return object.translate([-objectSize.x / 2, 0, 0]);
2177
+ return assertValidCSG$1(object.translate([-objectSize.x / 2, 0, 0]), 'centerX');
1787
2178
  }
1788
2179
 
1789
2180
  /**
@@ -1815,7 +2206,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1815
2206
 
1816
2207
  /// Calculate the difference between the original centroid and the new
1817
2208
  var delta = new_centroid.minus(objectCentroid).times(-1);
1818
- return new_object.translate(delta);
2209
+ return assertValidCSG$1(new_object.translate(delta), 'enlarge');
1819
2210
  }
1820
2211
 
1821
2212
  /**
@@ -1847,10 +2238,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1847
2238
  }
1848
2239
  var s = [scale(objectSize.x, x), scale(objectSize.y, y), scale(objectSize.z, z)];
1849
2240
  var min$1 = min(s);
1850
- return centerWith(object.scale(s.map(function (d, i) {
2241
+ return assertValidCSG$1(centerWith(object.scale(s.map(function (d, i) {
1851
2242
  if (a[i] === 0) return 1; // don't scale when value is zero
1852
2243
  return keep_aspect_ratio ? min$1 : d;
1853
- })), 'xyz', object);
2244
+ })), 'xyz', object), 'fit');
1854
2245
  }
1855
2246
  function shift(object, x, y, z) {
1856
2247
  var hsize = this.div(this.size(object.getBounds()), 2);
@@ -1858,10 +2249,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1858
2249
  }
1859
2250
  function zero(object) {
1860
2251
  var bounds = object.getBounds();
1861
- return object.translate([0, 0, -bounds[0].z]);
2252
+ return assertValidCSG$1(object.translate([0, 0, -bounds[0].z]), 'zero');
1862
2253
  }
1863
2254
  function mirrored4(x) {
1864
- return x.union([x.mirroredY(90), x.mirroredX(90), x.mirroredY(90).mirroredX(90)]);
2255
+ return assertValidCSG$1(x.union([x.mirroredY(90), x.mirroredX(90), x.mirroredY(90).mirroredX(90)]), 'mirrored4');
1865
2256
  }
1866
2257
  var flushSide = {
1867
2258
  'above-outside': [1, 0],
@@ -1927,7 +2318,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1927
2318
  function snap(moveobj, withobj, axis, orientation, delta) {
1928
2319
  debug$2('snap', moveobj, withobj, axis, orientation, delta);
1929
2320
  var t = calcSnap(moveobj, withobj, axis, orientation, delta);
1930
- return moveobj.translate(t);
2321
+ return assertValidCSG$1(moveobj.translate(t), 'snap');
1931
2322
  }
1932
2323
 
1933
2324
  /**
@@ -1940,23 +2331,35 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1940
2331
  * @return {CSG} [description]
1941
2332
  */
1942
2333
  function flush(moveobj, withobj, axis, mside, wside) {
1943
- return moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside));
2334
+ return assertValidCSG$1(moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside)), 'flush');
1944
2335
  }
2336
+
2337
+ /**
2338
+ *
2339
+ * @param {AxisStrings} axes
2340
+ * @param {function(number, string): number} valfun
2341
+ * @param {Array<number>} [a] In initial array to apply the values to, if not provided a new array will be created.
2342
+ * @returns {Array<number>} The resulting array after applying the function to the specified axes.
2343
+ */
1945
2344
  function axisApply(axes, valfun, a) {
1946
2345
  debug$2('axisApply', axes, valfun, a);
2346
+
2347
+ /** @type {Array<number>} */
1947
2348
  var retval = a || [0, 0, 0];
2349
+
2350
+ /** @type {Record<AxisString, number>} */
1948
2351
  var lookup = {
1949
2352
  x: 0,
1950
2353
  y: 1,
1951
2354
  z: 2
1952
2355
  };
1953
2356
  axes.split('').forEach(function (axis) {
1954
- retval[lookup[axis]] = valfun(lookup[axis], axis);
2357
+ retval[lookup[(/** @type {AxisString} */axis)]] = valfun(lookup[(/** @type {AxisString} */axis)], axis);
1955
2358
  });
1956
2359
  return retval;
1957
2360
  }
1958
2361
  function axis2array(axes, valfun) {
1959
- depreciated('axis2array');
2362
+ depreciated('axis2array', false, 'Use axisApply instead.');
1960
2363
  var a = [0, 0, 0];
1961
2364
  var lookup = {
1962
2365
  x: 0,
@@ -1997,7 +2400,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1997
2400
  });
1998
2401
  }
1999
2402
  function midlineTo(o, axis, to) {
2000
- return o.translate(calcmidlineTo(o, axis, to));
2403
+ return assertValidCSG$1(o.translate(calcmidlineTo(o, axis, to)), 'midlineTo');
2001
2404
  }
2002
2405
  function translator(o, axis, withObj) {
2003
2406
  var objectCentroid = centroid(o);
@@ -2018,7 +2421,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2018
2421
  return delta ? add(t, delta) : t;
2019
2422
  }
2020
2423
  function centerWith(o, axis, withObj) {
2021
- return o.translate(calcCenterWith(o, axis, withObj));
2424
+ return assertValidCSG$1(o.translate(calcCenterWith(o, axis, withObj)), 'centerWith');
2022
2425
  }
2023
2426
 
2024
2427
  /**
@@ -2048,6 +2451,109 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2048
2451
  return bounds[0][a] + (isEmpty(dist) ? size[axis] / 2 : dist);
2049
2452
  });
2050
2453
  }
2454
+ var EPS = 1e-5;
2455
+
2456
+ /**
2457
+ * Split a CSG object into two halves along a plane by directly
2458
+ * splitting polygons. This avoids BSP-tree-based boolean operations
2459
+ * which can fail on geometry produced by stretchAtPlane.
2460
+ * @param {CSG} csg The object to split
2461
+ * @param {CSG.Plane} plane The splitting plane
2462
+ * @return {{ front: CSG, back: CSG }} front (positive normal side) and back (negative normal side)
2463
+ */
2464
+ function splitCSGByPlane(csg, plane) {
2465
+ var frontPolys = [];
2466
+ var backPolys = [];
2467
+ csg.polygons.forEach(function (poly) {
2468
+ var vertices = poly.vertices;
2469
+ var numVerts = vertices.length;
2470
+ var hasfront = false;
2471
+ var hasback = false;
2472
+ var vertexIsBack = [];
2473
+ for (var i = 0; i < numVerts; i++) {
2474
+ var t = plane.normal.dot(vertices[i].pos) - plane.w;
2475
+ vertexIsBack.push(t < 0);
2476
+ if (t > EPS) hasfront = true;
2477
+ if (t < -EPS) hasback = true;
2478
+ }
2479
+ if (!hasfront && !hasback) {
2480
+ // coplanar — assign based on normal alignment
2481
+ var d = plane.normal.dot(poly.plane.normal);
2482
+ if (d >= 0) {
2483
+ frontPolys.push(poly);
2484
+ } else {
2485
+ backPolys.push(poly);
2486
+ }
2487
+ } else if (!hasback) {
2488
+ frontPolys.push(poly);
2489
+ } else if (!hasfront) {
2490
+ backPolys.push(poly);
2491
+ } else {
2492
+ // spanning — split the polygon
2493
+ var fv = [];
2494
+ var bv = [];
2495
+ for (var vi = 0; vi < numVerts; vi++) {
2496
+ var vertex = vertices[vi];
2497
+ var nextVi = (vi + 1) % numVerts;
2498
+ var isback = vertexIsBack[vi];
2499
+ var nextisback = vertexIsBack[nextVi];
2500
+ if (isback === nextisback) {
2501
+ if (isback) {
2502
+ bv.push(vertex);
2503
+ } else {
2504
+ fv.push(vertex);
2505
+ }
2506
+ } else {
2507
+ var point = vertex.pos;
2508
+ var nextpoint = vertices[nextVi].pos;
2509
+ var ip = plane.splitLineBetweenPoints(point, nextpoint);
2510
+ var iv = new CSG$1.Vertex(ip);
2511
+ if (isback) {
2512
+ bv.push(vertex);
2513
+ bv.push(iv);
2514
+ fv.push(iv);
2515
+ } else {
2516
+ fv.push(vertex);
2517
+ fv.push(iv);
2518
+ bv.push(iv);
2519
+ }
2520
+ }
2521
+ }
2522
+ // Remove degenerate (near-duplicate) adjacent vertices that arise
2523
+ // when the cut plane passes very close to existing vertices.
2524
+ // This matches the cleanup done by the BSP-tree splitter.
2525
+ var EPSEPS = EPS * EPS;
2526
+ if (fv.length >= 3) {
2527
+ var prev = fv[fv.length - 1];
2528
+ for (var fi = 0; fi < fv.length; fi++) {
2529
+ var curr = fv[fi];
2530
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
2531
+ fv.splice(fi, 1);
2532
+ fi--;
2533
+ }
2534
+ prev = curr;
2535
+ }
2536
+ }
2537
+ if (bv.length >= 3) {
2538
+ var prev = bv[bv.length - 1];
2539
+ for (var bi = 0; bi < bv.length; bi++) {
2540
+ var curr = bv[bi];
2541
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
2542
+ bv.splice(bi, 1);
2543
+ bi--;
2544
+ }
2545
+ prev = curr;
2546
+ }
2547
+ }
2548
+ if (fv.length >= 3) frontPolys.push(new CSG$1.Polygon(fv, poly.shared, poly.plane));
2549
+ if (bv.length >= 3) backPolys.push(new CSG$1.Polygon(bv, poly.shared, poly.plane));
2550
+ }
2551
+ });
2552
+ return {
2553
+ front: CSG$1.fromPolygons(frontPolys),
2554
+ back: CSG$1.fromPolygons(backPolys)
2555
+ };
2556
+ }
2051
2557
 
2052
2558
  /**
2053
2559
  * Cut an object into two pieces, along a given axis. The offset
@@ -2058,7 +2564,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2058
2564
  *
2059
2565
  * You can angle the cut plane and position the rotation point.
2060
2566
  *
2061
- * ![bisect example](./images/bisect.png)
2567
+ * ![bisect example](../test/images/bisect%20object%20positive.snap.png)
2062
2568
  * @param {CSG} object object to bisect
2063
2569
  * @param {string} axis axis to cut along
2064
2570
  * @param {number} [offset] offset to cut at
@@ -2136,13 +2642,13 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2136
2642
  }[[axis, rotateaxis].sort().join('')];
2137
2643
  var centroid = object.centroid();
2138
2644
  var rotateDelta = getDelta(objectSize, bounds, rotateOffsetAxis, rotateoffset);
2139
- var rotationCenter = options.rotationCenter || new CSG.Vector3D(axisApply('xyz', function (i, a) {
2645
+ var rotationCenter = options.rotationCenter || new CSG$1.Vector3D(axisApply('xyz', function (i, a) {
2140
2646
  if (a == axis) return cutDelta[i];
2141
2647
  if (a == rotateOffsetAxis) return rotateDelta[i];
2142
2648
  return centroid[a];
2143
2649
  }));
2144
2650
  var theRotationAxis = rotationAxes[rotateaxis];
2145
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1]).translate(cutDelta).rotate(rotationCenter, theRotationAxis, angle);
2651
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1]).translate(cutDelta).rotate(rotationCenter, theRotationAxis, angle);
2146
2652
  debug$2('bisect', debug$2.enabled && {
2147
2653
  axis: axis,
2148
2654
  offset: offset,
@@ -2155,7 +2661,33 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2155
2661
  cutplane: cutplane,
2156
2662
  options: options
2157
2663
  });
2158
- var g = Group('negative,positive', [object.cutByPlane(cutplane.plane).color(options.color && 'red'), object.cutByPlane(cutplane.plane.flipped()).color(options.color && 'blue')]);
2664
+ var negative = object.cutByPlane(cutplane.plane);
2665
+ var positive = object.cutByPlane(cutplane.plane.flipped());
2666
+
2667
+ // Detect cutByPlane failure: if a half's bounding box in the cut axis
2668
+ // is not smaller than the original, the BSP-tree-based cut failed.
2669
+ // Fall back to direct polygon splitting which is more robust, then
2670
+ // apply cutByPlane to the simpler half to add cap faces.
2671
+ var negSize = size(negative);
2672
+ var posSize = size(positive);
2673
+ if (negSize[axis] >= objectSize[axis] - EPS || posSize[axis] >= objectSize[axis] - EPS) {
2674
+ var halves = splitCSGByPlane(object, cutplane.plane);
2675
+ if (negSize[axis] >= objectSize[axis] - EPS) {
2676
+ negative = halves.back;
2677
+ // Cap the open cut face
2678
+ try {
2679
+ negative = negative.cutByPlane(cutplane.plane);
2680
+ } catch (e) {/* keep uncapped */}
2681
+ }
2682
+ if (posSize[axis] >= objectSize[axis] - EPS) {
2683
+ positive = halves.front;
2684
+ // Cap the open cut face
2685
+ try {
2686
+ positive = positive.cutByPlane(cutplane.plane.flipped());
2687
+ } catch (e) {/* keep uncapped */}
2688
+ }
2689
+ }
2690
+ var g = Group('negative,positive', [negative.color(options.color && 'red'), positive.color(options.color && 'blue')]);
2159
2691
  if (options.addRotationCenter) g.add(unitAxis(objectSize.length() + 10, 0.1, rotationCenter), 'rotationCenter');
2160
2692
  return g;
2161
2693
  }
@@ -2180,9 +2712,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2180
2712
  addRotationCenter: true
2181
2713
  };
2182
2714
  var info = normalVector(axis);
2183
- var rotationCenter = options.rotationCenter || new CSG.Vector3D(0, 0, 0);
2715
+ var rotationCenter = options.rotationCenter || new CSG$1.Vector3D(0, 0, 0);
2184
2716
  var theRotationAxis = rotationAxes[rotateaxis];
2185
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1])
2717
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1])
2186
2718
  // .translate(cutDelta)
2187
2719
  .rotate(rotationCenter, theRotationAxis, angle);
2188
2720
  var g = Group('negative,positive', [object.cutByPlane(cutplane.plane).color(options.color && 'red'), object.cutByPlane(cutplane.plane.flipped()).color(options.color && 'blue')]);
@@ -2197,7 +2729,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2197
2729
  * Creates a `JsCadUtilsGroup` object that has `body` and `wedge` objects. The `wedge` object
2198
2730
  * is created by radially cutting the object from the `start` to the `end` angle.
2199
2731
  *
2200
- * ![wedge example](./images/wedge.png)
2732
+ * ![wedge example](../test/images/wedge.snap.png)
2201
2733
  *
2202
2734
  *
2203
2735
  * @example
@@ -2253,7 +2785,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2253
2785
  var objectSize = size(object);
2254
2786
  var cutDelta = getDelta(objectSize, bounds, axis, offset, true);
2255
2787
  // debug('stretch.cutDelta', cutDelta, normal[axis]);
2256
- return object.stretchAtPlane(normal[axis], cutDelta, distance);
2788
+ return assertValidCSG$1(object.stretchAtPlane(normal[axis], cutDelta, distance), 'stretch');
2257
2789
  }
2258
2790
 
2259
2791
  /**
@@ -2268,11 +2800,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2268
2800
  function poly2solid(top, bottom, height) {
2269
2801
  if (top.sides.length == 0) {
2270
2802
  // empty!
2271
- return new CSG();
2803
+ return new CSG$1();
2272
2804
  }
2273
2805
  // var offsetVector = CSG.parseOptionAs3DVector(options, "offset", [0, 0, 10]);
2274
- var offsetVector = CSG.Vector3D.Create(0, 0, height);
2275
- var normalVector = CSG.Vector3D.Create(0, 1, 0);
2806
+ var offsetVector = CSG$1.Vector3D.Create(0, 0, height);
2807
+ var normalVector = CSG$1.Vector3D.Create(0, 1, 0);
2276
2808
  var polygons = [];
2277
2809
  // bottom and top
2278
2810
  polygons = polygons.concat(bottom._toPlanePolygons({
@@ -2286,8 +2818,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2286
2818
  flipped: offsetVector.z < 0
2287
2819
  }));
2288
2820
  // walls
2289
- var c1 = new CSG.Connector(offsetVector.times(0), [0, 0, offsetVector.z], normalVector);
2290
- var c2 = new CSG.Connector(offsetVector, [0, 0, offsetVector.z], normalVector);
2821
+ var c1 = new CSG$1.Connector(offsetVector.times(0), [0, 0, offsetVector.z], normalVector);
2822
+ var c2 = new CSG$1.Connector(offsetVector, [0, 0, offsetVector.z], normalVector);
2291
2823
  polygons = polygons.concat(bottom._toWallPolygons({
2292
2824
  cag: top,
2293
2825
  toConnector1: c1,
@@ -2295,7 +2827,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2295
2827
  }));
2296
2828
  // }
2297
2829
 
2298
- return CSG.fromPolygons(polygons);
2830
+ return assertValidCSG$1(CSG$1.fromPolygons(polygons), 'poly2solid');
2299
2831
  }
2300
2832
  function slices2poly(slices, options, axis) {
2301
2833
  debug$2('slices2poly', slices, options, axis);
@@ -2304,7 +2836,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2304
2836
  twiststeps: 0
2305
2837
  }, options);
2306
2838
  var twistangle = options && parseFloat(options.twistangle) || 0;
2307
- options && parseInt(options.twiststeps) || CSG.defaultResolution3D;
2839
+ options && parseInt(options.twiststeps) || CSG$1.defaultResolution3D;
2308
2840
  var normalVector = options.si.normalVector;
2309
2841
  var polygons = [];
2310
2842
 
@@ -2343,8 +2875,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2343
2875
  var nextidx = idx + 1;
2344
2876
  var top = !up ? slices[nextidx] : slice;
2345
2877
  var bottom = up ? slices[nextidx] : slice;
2346
- var c1 = new CSG.Connector(bottom.offset, connectorAxis, rotate(normalVector, twistangle, idx / slices.length));
2347
- var c2 = new CSG.Connector(top.offset, connectorAxis, rotate(normalVector, twistangle, nextidx / slices.length));
2878
+ var c1 = new CSG$1.Connector(bottom.offset, connectorAxis, rotate(normalVector, twistangle, idx / slices.length));
2879
+ var c2 = new CSG$1.Connector(top.offset, connectorAxis, rotate(normalVector, twistangle, nextidx / slices.length));
2348
2880
 
2349
2881
  // debug('slices2poly.slices', c1.point, c2.point);
2350
2882
  polygons = polygons.concat(bottom.poly._toWallPolygons({
@@ -2354,21 +2886,21 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2354
2886
  }));
2355
2887
  }
2356
2888
  });
2357
- return CSG.fromPolygons(polygons);
2889
+ return assertValidCSG$1(CSG$1.fromPolygons(polygons), 'slices2poly');
2358
2890
  }
2359
2891
  function normalVector(axis) {
2360
2892
  var axisInfo = {
2361
2893
  z: {
2362
2894
  orthoNormalCartesian: ['X', 'Y'],
2363
- normalVector: CSG.Vector3D.Create(0, 1, 0)
2895
+ normalVector: CSG$1.Vector3D.Create(0, 1, 0)
2364
2896
  },
2365
2897
  x: {
2366
2898
  orthoNormalCartesian: ['Y', 'Z'],
2367
- normalVector: CSG.Vector3D.Create(0, 0, 1)
2899
+ normalVector: CSG$1.Vector3D.Create(0, 0, 1)
2368
2900
  },
2369
2901
  y: {
2370
- orthoNormalCartesian: ['X', 'Z'],
2371
- normalVector: CSG.Vector3D.Create(0, 0, 1)
2902
+ orthoNormalCartesian: ['Z', 'X'],
2903
+ normalVector: CSG$1.Vector3D.Create(0, 0, 1)
2372
2904
  }
2373
2905
  };
2374
2906
  if (!axisInfo[axis]) error('normalVector: invalid axis ' + axis);
@@ -2420,7 +2952,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2420
2952
  var si = sliceParams(orientation, radius, b);
2421
2953
  debug$2('reShape', absoluteRadius, si);
2422
2954
  if (si.axis !== 'z') throw new Error('reShape error: CAG._toPlanePolygons only uses the "z" axis. You must use the "z" axis for now.');
2423
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(si.orthoNormalCartesian[0], si.orthoNormalCartesian[1]).translate(si.cutDelta);
2955
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(si.orthoNormalCartesian[0], si.orthoNormalCartesian[1]).translate(si.cutDelta);
2424
2956
  var slice = object.sectionCut(cutplane);
2425
2957
  var first = axisApply(si.axis, function () {
2426
2958
  return si.positive ? 0 : absoluteRadius;
@@ -2435,25 +2967,25 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2435
2967
  si: si
2436
2968
  }), si.axis).color(options.color);
2437
2969
  var remainder = object.cutByPlane(plane);
2438
- return union([options.unionOriginal ? object : remainder, delta.translate(si.moveDelta)]);
2970
+ return assertValidCSG$1(union([options.unionOriginal ? object : remainder, delta.translate(si.moveDelta)]), 'reShape');
2439
2971
  }
2440
2972
  function chamfer(object, radius, orientation, options) {
2441
- return reShape(object, radius, orientation, options, function (first, last, slice) {
2973
+ return assertValidCSG$1(reShape(object, radius, orientation, options, function (first, last, slice) {
2442
2974
  return [{
2443
2975
  poly: slice,
2444
- offset: new CSG.Vector3D(first)
2976
+ offset: new CSG$1.Vector3D(first)
2445
2977
  }, {
2446
2978
  poly: enlarge(slice, [-radius * 2, -radius * 2]),
2447
- offset: new CSG.Vector3D(last)
2979
+ offset: new CSG$1.Vector3D(last)
2448
2980
  }];
2449
- });
2981
+ }), 'chamfer');
2450
2982
  }
2451
2983
  function fillet(object, radius, orientation, options) {
2452
2984
  options = options || {};
2453
- return reShape(object, radius, orientation, options, function (first, last, slice) {
2454
- var v1 = new CSG.Vector3D(first);
2455
- var v2 = new CSG.Vector3D(last);
2456
- var res = options.resolution || CSG.defaultResolution3D;
2985
+ return assertValidCSG$1(reShape(object, radius, orientation, options, function (first, last, slice) {
2986
+ var v1 = new CSG$1.Vector3D(first);
2987
+ var v2 = new CSG$1.Vector3D(last);
2988
+ var res = options.resolution || CSG$1.defaultResolution3D;
2457
2989
  var slices = range(0, res).map(function (i) {
2458
2990
  var p = i > 0 ? i / (res - 1) : 0;
2459
2991
  var v = v1.lerp(v2, p);
@@ -2464,7 +2996,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2464
2996
  };
2465
2997
  });
2466
2998
  return slices;
2467
- });
2999
+ }), 'fillet');
2468
3000
  }
2469
3001
  function calcRotate(part, solid, axis /* , angle */) {
2470
3002
  var axes = {
@@ -2483,7 +3015,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2483
3015
  var _calcRotate = calcRotate(part, solid, axis),
2484
3016
  rotationCenter = _calcRotate.rotationCenter,
2485
3017
  rotationAxis = _calcRotate.rotationAxis;
2486
- return part.rotate(rotationCenter, rotationAxis, angle);
3018
+ return assertValidCSG$1('rotateAround')(part.rotate(rotationCenter, rotationAxis, angle));
2487
3019
  }
2488
3020
  function cloneProperties(from, to) {
2489
3021
  return Object.entries(from).reduce(function (props, _ref) {
@@ -2495,10 +3027,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2495
3027
  }, to);
2496
3028
  }
2497
3029
  function clone(o) {
2498
- var c = CSG.fromPolygons(o.toPolygons());
3030
+ var c = CSG$1.fromPolygons(o.toPolygons());
2499
3031
  cloneProperties(o, c);
2500
- debug$2('clone', o, c, CSG);
2501
- return c;
3032
+ debug$2('clone', o, c, CSG$1);
3033
+ return assertValidCSG$1(c, 'clone');
2502
3034
  }
2503
3035
 
2504
3036
  /**
@@ -2514,11 +3046,12 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2514
3046
  var point = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0, 0];
2515
3047
  var axis = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [1, 0, 0];
2516
3048
  var normal = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [0, 0, 1];
2517
- object.properties[name] = new CSG.Connector(point, axis, normal);
2518
- return object;
3049
+ object.properties[name] = new CSG$1.Connector(point, axis, normal);
3050
+ return assertValidCSG$1('addConnector')(object);
2519
3051
  }
2520
3052
 
2521
3053
  var debug$1 = Debug('jscadUtils:parts');
3054
+ var assertValidCSG = AssertValidCSG('parts');
2522
3055
  var parts = {
2523
3056
  BBox: BBox$1,
2524
3057
  Cube: Cube,
@@ -2536,7 +3069,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2536
3069
  */
2537
3070
  function BBox$1() {
2538
3071
  function box(object) {
2539
- return CSG.cube({
3072
+ return CSG$1.cube({
2540
3073
  center: object.centroid(),
2541
3074
  radius: object.size().dividedBy(2)
2542
3075
  });
@@ -2544,17 +3077,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2544
3077
  for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) {
2545
3078
  objects[_key] = arguments[_key];
2546
3079
  }
2547
- return objects.reduce(function (bbox, part) {
3080
+ return assertValidCSG(objects.reduce(function (bbox, part) {
2548
3081
  var object = bbox ? union([bbox, box(part)]) : part;
2549
3082
  return box(object);
2550
- }, undefined);
3083
+ }, undefined), 'BBox');
2551
3084
  }
2552
3085
  function Cube(width) {
2553
3086
  var r = div$1(fromxyz(width), 2);
2554
- return CSG.cube({
3087
+ return assertValidCSG(CSG$1.cube({
2555
3088
  center: r,
2556
3089
  radius: r
2557
- });
3090
+ }), 'Cube');
2558
3091
  }
2559
3092
 
2560
3093
  // export function Sphere(diameter) {
@@ -2588,11 +3121,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2588
3121
  center: [r[0], r[1], 0],
2589
3122
  radius: r,
2590
3123
  roundradius: corner_radius,
2591
- resolution: CSG.defaultResolution2D
3124
+ resolution: CSG$1.defaultResolution2D
2592
3125
  }).extrude({
2593
3126
  offset: [0, 0, thickness || 1.62]
2594
3127
  });
2595
- return roundedcube;
3128
+ return assertValidCSG(roundedcube, 'RoundedCube');
2596
3129
  }
2597
3130
 
2598
3131
  /**
@@ -2610,9 +3143,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2610
3143
  start: [0, 0, 0],
2611
3144
  end: [0, 0, height],
2612
3145
  radius: diameter / 2,
2613
- resolution: CSG.defaultResolution2D
3146
+ resolution: CSG$1.defaultResolution2D
2614
3147
  }, options);
2615
- return CSG.cylinder(options);
3148
+ return assertValidCSG(CSG$1.cylinder(options), 'Cylinder');
2616
3149
  }
2617
3150
 
2618
3151
  /**
@@ -2627,13 +3160,13 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2627
3160
  function Cone(diameter1, diameter2, height) {
2628
3161
  var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
2629
3162
  debug$1('parts.Cone', diameter1, diameter2, height, options);
2630
- return CSG.cylinder(Object.assign({
3163
+ return assertValidCSG(CSG$1.cylinder(Object.assign({
2631
3164
  start: [0, 0, 0],
2632
3165
  end: [0, 0, height],
2633
3166
  radiusStart: diameter1 / 2,
2634
3167
  radiusEnd: diameter2 / 2,
2635
- resolution: CSG.defaultResolution2D
2636
- }, options));
3168
+ resolution: CSG$1.defaultResolution2D
3169
+ }, options)), 'Cone');
2637
3170
  }
2638
3171
 
2639
3172
  /**
@@ -2646,9 +3179,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2646
3179
  var radius = diameter / 2;
2647
3180
  var sqrt3 = Math.sqrt(3) / 2;
2648
3181
  var hex = CAG.fromPoints([[radius, 0], [radius / 2, radius * sqrt3], [-radius / 2, radius * sqrt3], [-radius, 0], [-radius / 2, -radius * sqrt3], [radius / 2, -radius * sqrt3]]);
2649
- return hex.extrude({
3182
+ return assertValidCSG(hex.extrude({
2650
3183
  offset: [0, 0, height]
2651
- });
3184
+ }), 'Hexagon');
2652
3185
  }
2653
3186
 
2654
3187
  /**
@@ -2661,9 +3194,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2661
3194
  function Triangle(base, height) {
2662
3195
  var radius = base / 2;
2663
3196
  var tri = CAG.fromPoints([[-radius, 0], [radius, 0], [0, Math.sin(30) * radius]]);
2664
- return tri.extrude({
3197
+ return assertValidCSG(tri.extrude({
2665
3198
  offset: [0, 0, height]
2666
- });
3199
+ }), 'Triangle');
2667
3200
  }
2668
3201
 
2669
3202
  /**
@@ -2678,7 +3211,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2678
3211
  * @returns {CSG} A CSG Tube
2679
3212
  */
2680
3213
  function Tube(outsideDiameter, insideDiameter, height, outsideOptions, insideOptions) {
2681
- return Cylinder(outsideDiameter, height, outsideOptions).subtract(Cylinder(insideDiameter, height, insideOptions || outsideOptions));
3214
+ return assertValidCSG(Cylinder(outsideDiameter, height, outsideOptions).subtract(Cylinder(insideDiameter, height, insideOptions || outsideOptions)), 'Tube');
2682
3215
  }
2683
3216
 
2684
3217
  /**
@@ -2702,11 +3235,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2702
3235
  center: [r[0], r[1], 0],
2703
3236
  radius: r,
2704
3237
  roundradius: corner_radius,
2705
- resolution: CSG.defaultResolution2D
3238
+ resolution: CSG$1.defaultResolution2D
2706
3239
  }).extrude({
2707
3240
  offset: [0, 0, thickness || 1.62]
2708
3241
  });
2709
- return board;
3242
+ return assertValidCSG(board, 'Board');
2710
3243
  }
2711
3244
  var Hardware = {
2712
3245
  Orientation: {
@@ -2856,7 +3389,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2856
3389
  * This will bisect an object using a rabett join. Returns a
2857
3390
  * `group` object with `positive` and `negative` parts.
2858
3391
  *
2859
- * * ![parts example](./images/rabett.png)
3392
+ * ![rabett example](../test/images/boxes-Rabett.snap.png)
2860
3393
  * @example
2861
3394
  *include('dist/jscad-utils.jscad');
2862
3395
  *
@@ -2886,6 +3419,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2886
3419
  gap = gap || 0.25;
2887
3420
  var inside = thickness - gap;
2888
3421
  var outside = -thickness + gap;
3422
+ var boxHeight = box.size().z;
3423
+ if (Math.abs(height) >= boxHeight) {
3424
+ throw new Error("Rabett: height (".concat(height, ") must be less than the object height (").concat(boxHeight, ")"));
3425
+ }
3426
+
2889
3427
  // options.color = true;
2890
3428
  debug('inside', inside, 'outside', outside);
2891
3429
  var group = Group();
@@ -2915,7 +3453,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2915
3453
  * Used on a hollow object, this will rabett out the top and/or
2916
3454
  * bottom of the object.
2917
3455
  *
2918
- * ![A hollow hexagon with removable top and bottom](../images/rabett-tb.png)
3456
+ * ![A hollow hexagon with removable top and bottom](../test/images/boxes-RabettTopBottom.snap.png)
2919
3457
  *
2920
3458
  * @example
2921
3459
  *include('dist/jscad-utils.jscad');
@@ -3002,10 +3540,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3002
3540
  thickness = thickness || 2;
3003
3541
  var s = div$1(xyz2array(size), 2);
3004
3542
  var r = add(s, thickness);
3005
- var box = CSG.cube({
3543
+ var box = CSG$1.cube({
3006
3544
  center: r,
3007
3545
  radius: r
3008
- }).subtract(CSG.cube({
3546
+ }).subtract(CSG$1.cube({
3009
3547
  center: r,
3010
3548
  radius: s
3011
3549
  }));
@@ -3020,7 +3558,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3020
3558
  * wall thickness. This is done by reducing the object by half the
3021
3559
  * thickness and subtracting the reduced version from the original object.
3022
3560
  *
3023
- * ![A hollowed out cylinder](../images/rabett.png)
3561
+ * ![A hollowed out cylinder](../test/images/boxes-Rabett.snap.png)
3024
3562
  *
3025
3563
  * @param {CSG} object A CSG object
3026
3564
  * @param {Number} [thickness=2] The thickness of the walls.
@@ -3048,7 +3586,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3048
3586
  var BBox = function BBox(o) {
3049
3587
  depreciated('BBox', true, "Use 'parts.BBox' instead");
3050
3588
  var s = div$1(xyz2array(o.size()), 2);
3051
- return CSG.cube({
3589
+ return CSG$1.cube({
3052
3590
  center: s,
3053
3591
  radius: s
3054
3592
  }).align(o, 'xyz');
@@ -3060,7 +3598,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3060
3598
  var gap = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.25;
3061
3599
  var r = add(getRadius(box), -thickness / 2);
3062
3600
  r[2] = thickness / 2;
3063
- var cutter = CSG.cube({
3601
+ var cutter = CSG$1.cube({
3064
3602
  center: r,
3065
3603
  radius: r
3066
3604
  }).align(box, 'xy').color('green');