@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/compat.js CHANGED
@@ -38,6 +38,9 @@ function initJscadutils(_CSG, options = {}) {
38
38
  { enabled: [], disabled: [] }
39
39
  );
40
40
 
41
+ var jscadUtilsAssertValidCSGWarnings = options.assertValidCSGWarnings || false;
42
+ var jscadUtilsAssertValidCSG = options.assertValidCSG || false;
43
+
41
44
  // include:compat
42
45
  // ../dist/index.js
43
46
  var jscadUtils = (function (exports, jsCadCSG, scadApi) {
@@ -131,6 +134,54 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
131
134
  function _arrayWithHoles(r) {
132
135
  if (Array.isArray(r)) return r;
133
136
  }
137
+ function _createForOfIteratorHelper(r, e) {
138
+ var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
139
+ if (!t) {
140
+ if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
141
+ t && (r = t);
142
+ var n = 0,
143
+ F = function () {};
144
+ return {
145
+ s: F,
146
+ n: function () {
147
+ return n >= r.length ? {
148
+ done: !0
149
+ } : {
150
+ done: !1,
151
+ value: r[n++]
152
+ };
153
+ },
154
+ e: function (r) {
155
+ throw r;
156
+ },
157
+ f: F
158
+ };
159
+ }
160
+ throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
161
+ }
162
+ var o,
163
+ a = !0,
164
+ u = !1;
165
+ return {
166
+ s: function () {
167
+ t = t.call(r);
168
+ },
169
+ n: function () {
170
+ var r = t.next();
171
+ return a = r.done, r;
172
+ },
173
+ e: function (r) {
174
+ u = !0, o = r;
175
+ },
176
+ f: function () {
177
+ try {
178
+ a || null == t.return || t.return();
179
+ } finally {
180
+ if (u) throw o;
181
+ }
182
+ }
183
+ };
184
+ }
134
185
  function _defineProperty(e, r, t) {
135
186
  return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
136
187
  value: t,
@@ -803,6 +854,344 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
803
854
  return o.setColor(c);
804
855
  }
805
856
 
857
+ /* globals jscadUtilsAssertValidCSG jscadUtilsAssertValidCSGWarnings */
858
+ /**
859
+ * @typedef {Object} CSG
860
+ * @property {Array<{vertices: Array<{pos: any}>}>} polygons
861
+ * @property {function(): CSG} [canonicalized]
862
+ * @property {function(): CSG} [reTesselated]
863
+ * @property {function(): CSG} [fixTJunctions]
864
+ * @property {function(): Array} [getBounds]
865
+ * @property {Object} [properties]
866
+ */
867
+ /**
868
+ * Validate that a CSG object represents a solid, watertight mesh
869
+ * without degenerate faces. Returns an object with an `ok` boolean
870
+ * and an `errors` array describing any problems found.
871
+ *
872
+ * Checks performed:
873
+ * - **No empty mesh** – the object must contain at least one polygon.
874
+ * - **No degenerate polygons** – every polygon must have ≥ 3 vertices
875
+ * and a computable area greater than `EPS²`.
876
+ * - **Watertight / manifold edges** – every directed edge A→B in the
877
+ * mesh must be matched by exactly one reverse edge B→A in another
878
+ * polygon. Unmatched edges indicate holes; edges shared more than
879
+ * twice indicate non-manifold geometry.
880
+ *
881
+ * By default, the mesh is canonicalized and T-junctions are repaired
882
+ * before validation so that results from boolean operations (union,
883
+ * subtract, intersect) can be validated successfully. Pass
884
+ * `{ fixTJunctions: false }` to skip this step and validate the raw
885
+ * mesh.
886
+ *
887
+ * @param {CSG} csg The CSG object to validate.
888
+ * @param {object} [options] Validation options.
889
+ * @param {boolean} [options.fixTJunctions=true] Whether to canonicalize and fix T-junctions before validation.
890
+ * @return {{ ok: boolean, errors: string[], warnings: string[] }} Validation result.
891
+ * @function validateCSG
892
+ */
893
+ function validateCSG(csg, options) {
894
+ /** @type {string[]} */
895
+ var errors = [];
896
+ /** @type {string[]} */
897
+ var warnings = [];
898
+ if (!csg || !csg.polygons || csg.polygons.length === 0) {
899
+ errors.push('Empty mesh: no polygons');
900
+ return {
901
+ ok: false,
902
+ errors: errors,
903
+ warnings: warnings
904
+ };
905
+ }
906
+ var opts = _objectSpread2({
907
+ fixTJunctions: true
908
+ }, options);
909
+
910
+ // Optionally canonicalize and fix T-junctions so that boolean-op
911
+ // output can pass the watertight check.
912
+ if (opts.fixTJunctions && typeof csg.canonicalized === 'function') {
913
+ csg = csg.canonicalized();
914
+ if (typeof csg.reTesselated === 'function') {
915
+ csg = csg.reTesselated();
916
+ }
917
+ if (typeof csg.fixTJunctions === 'function') {
918
+ csg = csg.fixTJunctions();
919
+ }
920
+ }
921
+ var AREA_EPS = 1e-10;
922
+ var KEY_EPS = 1e-5;
923
+ var degenerateCount = 0;
924
+ var invalidVertexCount = 0;
925
+
926
+ // Check for NaN/Infinity vertex coordinates which cause WebGL errors
927
+ // (GL_INVALID_VALUE: glVertexAttribPointer: Vertex attribute size must be 1, 2, 3, or 4)
928
+ var _iterator = _createForOfIteratorHelper(csg.polygons),
929
+ _step;
930
+ try {
931
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
932
+ var npoly = _step.value;
933
+ var _iterator4 = _createForOfIteratorHelper(npoly.vertices),
934
+ _step4;
935
+ try {
936
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
937
+ var nvert = _step4.value;
938
+ var np = nvert.pos;
939
+ 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)) {
940
+ invalidVertexCount++;
941
+ break;
942
+ }
943
+ }
944
+ } catch (err) {
945
+ _iterator4.e(err);
946
+ } finally {
947
+ _iterator4.f();
948
+ }
949
+ }
950
+ } catch (err) {
951
+ _iterator.e(err);
952
+ } finally {
953
+ _iterator.f();
954
+ }
955
+ if (invalidVertexCount > 0) {
956
+ errors.push(invalidVertexCount + ' polygon(s) with invalid vertex coordinates (NaN or Infinity)');
957
+ }
958
+
959
+ // Position-based vertex key (shared vertices across polygons have different
960
+ // object tags but the same position, so we round coordinates to match them).
961
+ /** @param {{ pos: { x: number, y: number, z: number } }} v */
962
+ function vtxKey(v) {
963
+ var p = v.pos;
964
+ return Math.round(p.x / KEY_EPS) + ',' + Math.round(p.y / KEY_EPS) + ',' + Math.round(p.z / KEY_EPS);
965
+ }
966
+
967
+ // First pass: identify degenerate polygons
968
+ var validPolygons = [];
969
+ var _iterator2 = _createForOfIteratorHelper(csg.polygons),
970
+ _step2;
971
+ try {
972
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
973
+ var poly = _step2.value;
974
+ var verts = poly.vertices;
975
+ var nv = verts.length;
976
+ if (nv < 3) {
977
+ degenerateCount++;
978
+ continue;
979
+ }
980
+
981
+ // Skip polygons with invalid vertex coordinates
982
+ var hasInvalid = false;
983
+ var _iterator5 = _createForOfIteratorHelper(verts),
984
+ _step5;
985
+ try {
986
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
987
+ var vert = _step5.value;
988
+ var ip = vert.pos;
989
+ if (!Number.isFinite(ip.x) || !Number.isFinite(ip.y) || !Number.isFinite(ip.z)) {
990
+ hasInvalid = true;
991
+ break;
992
+ }
993
+ }
994
+ } catch (err) {
995
+ _iterator5.e(err);
996
+ } finally {
997
+ _iterator5.f();
998
+ }
999
+ if (hasInvalid) continue;
1000
+
1001
+ // Check degenerate area using cross-product summation
1002
+ var area = 0;
1003
+ for (var ai = 0; ai < nv - 2; ai++) {
1004
+ area += verts[ai + 1].pos.minus(verts[0].pos).cross(verts[ai + 2].pos.minus(verts[ai + 1].pos)).length();
1005
+ }
1006
+ area *= 0.5;
1007
+ if (area < AREA_EPS) {
1008
+ degenerateCount++;
1009
+ continue;
1010
+ }
1011
+ validPolygons.push(poly);
1012
+ }
1013
+ } catch (err) {
1014
+ _iterator2.e(err);
1015
+ } finally {
1016
+ _iterator2.f();
1017
+ }
1018
+ if (degenerateCount > 0) {
1019
+ warnings.push(degenerateCount + ' degenerate polygon(s) (fewer than 3 vertices or near-zero area)');
1020
+
1021
+ // Rebuild the CSG from valid polygons only and re-run the repair
1022
+ // pipeline so that fixTJunctions can close gaps left by the removed
1023
+ // degenerate faces.
1024
+ /* eslint-disable no-undef */
1025
+ // @ts-ignore — CSG is a runtime global injected by the JSCAD compat layer
1026
+ if (opts.fixTJunctions && typeof CSG !== 'undefined') {
1027
+ // @ts-ignore
1028
+ var cleaned = CSG.fromPolygons(validPolygons);
1029
+ /* eslint-enable no-undef */
1030
+ cleaned = cleaned.canonicalized();
1031
+ if (typeof cleaned.reTesselated === 'function') {
1032
+ cleaned = cleaned.reTesselated();
1033
+ }
1034
+ if (typeof cleaned.fixTJunctions === 'function') {
1035
+ cleaned = cleaned.fixTJunctions();
1036
+ }
1037
+ // Re-scan for valid polygons after second repair pass
1038
+ validPolygons = [];
1039
+ var _iterator3 = _createForOfIteratorHelper(cleaned.polygons),
1040
+ _step3;
1041
+ try {
1042
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
1043
+ var cpoly = _step3.value;
1044
+ var cverts = cpoly.vertices;
1045
+ var cnv = cverts.length;
1046
+ if (cnv < 3) continue;
1047
+ var carea = 0;
1048
+ for (var cai = 0; cai < cnv - 2; cai++) {
1049
+ carea += cverts[cai + 1].pos.minus(cverts[0].pos).cross(cverts[cai + 2].pos.minus(cverts[cai + 1].pos)).length();
1050
+ }
1051
+ carea *= 0.5;
1052
+ if (carea < AREA_EPS) continue;
1053
+ validPolygons.push(cpoly);
1054
+ }
1055
+ } catch (err) {
1056
+ _iterator3.e(err);
1057
+ } finally {
1058
+ _iterator3.f();
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ // Edge map: key = "vtxKeyA/vtxKeyB", value = count
1064
+ /** @type {Record<string, number>} */
1065
+ var edgeCounts = {};
1066
+
1067
+ // Accumulate directed edges from valid polygons only
1068
+ for (var _i = 0, _validPolygons = validPolygons; _i < _validPolygons.length; _i++) {
1069
+ var vpoly = _validPolygons[_i];
1070
+ var vverts = vpoly.vertices;
1071
+ var vnv = vverts.length;
1072
+ for (var ei = 0; ei < vnv; ei++) {
1073
+ var v0 = vverts[ei];
1074
+ var v1 = vverts[(ei + 1) % vnv];
1075
+ var edgeKey = vtxKey(v0) + '/' + vtxKey(v1);
1076
+ edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1;
1077
+ }
1078
+ }
1079
+
1080
+ // Check edge manifoldness: every edge A→B should be cancelled by B→A
1081
+ var unmatchedEdges = 0;
1082
+ var nonManifoldEdges = 0;
1083
+ /** @type {Record<string, boolean>} */
1084
+ var checked = {};
1085
+ for (var _i2 = 0, _Object$keys = Object.keys(edgeCounts); _i2 < _Object$keys.length; _i2++) {
1086
+ var _edgeKey = _Object$keys[_i2];
1087
+ if (checked[_edgeKey]) continue;
1088
+ var parts = _edgeKey.split('/');
1089
+ var reverseKey = parts[1] + '/' + parts[0];
1090
+ var forwardCount = edgeCounts[_edgeKey] || 0;
1091
+ var reverseCount = edgeCounts[reverseKey] || 0;
1092
+ checked[_edgeKey] = true;
1093
+ checked[reverseKey] = true;
1094
+ if (forwardCount !== reverseCount) {
1095
+ unmatchedEdges += Math.abs(forwardCount - reverseCount);
1096
+ }
1097
+ if (forwardCount > 1 || reverseCount > 1) {
1098
+ nonManifoldEdges++;
1099
+ }
1100
+ }
1101
+ if (unmatchedEdges > 0) {
1102
+ errors.push(unmatchedEdges + ' unmatched edge(s): mesh is not watertight');
1103
+ }
1104
+ if (nonManifoldEdges > 0) {
1105
+ errors.push(nonManifoldEdges + ' non-manifold edge(s): edge shared by more than 2 polygons');
1106
+ }
1107
+ return {
1108
+ ok: errors.length === 0,
1109
+ errors: errors,
1110
+ warnings: warnings
1111
+ };
1112
+ }
1113
+
1114
+ /** @param {any} csg @returns {any} */
1115
+ function _noOp(csg) {
1116
+ return csg;
1117
+ }
1118
+
1119
+ /**
1120
+ * @param {boolean} warnEnabled
1121
+ * @returns {function(*, string=, string=): *}
1122
+ */
1123
+ function _makeAssertFn(warnEnabled) {
1124
+ return function _assert(csg) {
1125
+ var functionName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'unknown';
1126
+ var moduleName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'unknown';
1127
+ // Only validate CSG-like objects (they have a polygons array).
1128
+ // CAG objects (2D cross-sections) have `sides` instead and are passed through.
1129
+ if (!csg || csg.polygons === undefined) return csg;
1130
+ var result = validateCSG(csg);
1131
+ if (!result.ok) {
1132
+ throw new Error(moduleName + ':' + functionName + ': ' + 'invalid CSG: ' + result.errors.join(', '));
1133
+ }
1134
+ if (warnEnabled && result.warnings.length > 0) {
1135
+ throw new Error(moduleName + ':' + functionName + ': ' + 'CSG warnings: ' + result.warnings.join(', '));
1136
+ }
1137
+ return csg;
1138
+ };
1139
+ }
1140
+
1141
+ // Live pointer that all returned closures call through — swap this and all
1142
+ // existing closures immediately pick up the change.
1143
+ /** @type {function(*, string=, string=): *} */
1144
+ var _assertFn = _noOp;
1145
+
1146
+ // Read compat globals set by initJscadutils — mirrors the Debug() pattern
1147
+ // in debug.js. Returns _noOp when globals are absent (ESM / test context).
1148
+ function _resolveFromGlobals() {
1149
+ /* eslint-disable no-undef */
1150
+ // @ts-ignore — globals set by the JSCAD compat layer before bundle injection
1151
+ var enabled = typeof jscadUtilsAssertValidCSG !== 'undefined' && !!jscadUtilsAssertValidCSG;
1152
+ // @ts-ignore
1153
+ var warnEnabled = typeof jscadUtilsAssertValidCSGWarnings !== 'undefined' && !!jscadUtilsAssertValidCSGWarnings;
1154
+ /* eslint-enable no-undef */
1155
+ return enabled ? _makeAssertFn(warnEnabled) : _noOp;
1156
+ }
1157
+
1158
+ /**
1159
+ * Returns an asserter function bound to `moduleName`. Call the returned
1160
+ * function with a CSG object and the calling function's name to validate it
1161
+ * (when enabled) or pass it through unchanged (when disabled).
1162
+ *
1163
+ * Best practice is to call `AssertValidCSG` once per module at load time and
1164
+ * capture the result as a module-level constant so that `moduleName` appears
1165
+ * consistently in every error message thrown from that module.
1166
+ *
1167
+ * On creation, reads compat globals (jscadUtilsAssertValidCSG /
1168
+ * jscadUtilsAssertValidCSGWarnings) if setValidationEnabled() has not been
1169
+ * called explicitly — identical to how Debug('name') reads jscadUtilsDebug.
1170
+ *
1171
+ * Error message format: `moduleName:functionName: invalid CSG: <errors>`
1172
+ *
1173
+ * @example
1174
+ * // Once at the top of your module:
1175
+ * const assertValidCSG = AssertValidCSG('myModule');
1176
+ *
1177
+ * export function enlarge(object, ...) {
1178
+ * // ...
1179
+ * return assertValidCSG(new_object.translate(delta), 'enlarge');
1180
+ * }
1181
+ *
1182
+ * @param {string} [moduleName='unknown'] Module name, included in error messages.
1183
+ * @return {function(CSG, string=): CSG}
1184
+ */
1185
+ function AssertValidCSG() {
1186
+ var moduleName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown';
1187
+ {
1188
+ _assertFn = _resolveFromGlobals();
1189
+ }
1190
+ return function (csg, name) {
1191
+ return _assertFn(csg, name, moduleName);
1192
+ };
1193
+ }
1194
+
806
1195
  /** @typedef {object} ExtendedCSG
807
1196
  * @property {object} prototype
808
1197
  * @property {function} prototype.color
@@ -875,7 +1264,6 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
875
1264
  * @property {function} stackTrace
876
1265
  * @property {function} getConnector
877
1266
  */
878
-
879
1267
  /**
880
1268
  * Initialize `jscad-utils` and add utilities to the `proto` object.
881
1269
  * @param {CSG} proto The global `proto` object
@@ -985,6 +1373,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
985
1373
  proto.prototype.subtractIf = function subtractIf(object, condition) {
986
1374
  return condition ? this.subtract(result(this, object)) : this;
987
1375
  };
1376
+ proto.prototype.validate = function validate(options) {
1377
+ return validateCSG(this, options);
1378
+ };
988
1379
  proto.prototype._translate = proto.prototype.translate;
989
1380
 
990
1381
  /**
@@ -1036,16 +1427,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1036
1427
  'default': init
1037
1428
  });
1038
1429
 
1039
- var CSG = jsCadCSG__default["default"].CSG,
1430
+ var CSG$1 = jsCadCSG__default["default"].CSG,
1040
1431
  CAG = jsCadCSG__default["default"].CAG;
1041
1432
  var rectangular_extrude = scadApi__default["default"].extrusions.rectangular_extrude;
1042
1433
  var _scadApi$text = scadApi__default["default"].text,
1043
1434
  vector_text = _scadApi$text.vector_text,
1044
1435
  vector_char = _scadApi$text.vector_char;
1045
1436
  var union = scadApi__default["default"].booleanOps.union;
1046
- init(CSG);
1437
+ init(CSG$1);
1047
1438
 
1048
1439
  var debug$3 = Debug('jscadUtils:group');
1440
+ var assertValidCSG$2 = AssertValidCSG('group');
1049
1441
 
1050
1442
  /**
1051
1443
  * @function JsCadUtilsGroup
@@ -1124,7 +1516,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1124
1516
  debug$3('combine mapPick', value, key, object);
1125
1517
  return map ? map(value, key, index, object) : identity(value);
1126
1518
  }, self.name));
1127
- return g.subtractIf(self.holes && Array.isArray(self.holes) ? union(self.holes) : self.holes, self.holes && !options.noholes);
1519
+ return assertValidCSG$2(g.subtractIf(self.holes && Array.isArray(self.holes) ? union(self.holes) : self.holes, self.holes && !options.noholes), 'combine');
1128
1520
  } catch (err) {
1129
1521
  debug$3('combine error', this, pieces, options, err);
1130
1522
  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');
@@ -1186,7 +1578,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1186
1578
  });
1187
1579
  if (self.holes) {
1188
1580
  group.holes = toArray(self.holes).map(function (part) {
1189
- return map(CSG.fromPolygons(part.toPolygons()), 'holes');
1581
+ return assertValidCSG$2(map(CSG$1.fromPolygons(part.toPolygons()), 'holes'), 'clone');
1190
1582
  });
1191
1583
  }
1192
1584
  return group;
@@ -1214,7 +1606,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1214
1606
  var rotationCenter = solid.centroid();
1215
1607
  var rotationAxis = axes[axis];
1216
1608
  self.map(function (part) {
1217
- return part.rotate(rotationCenter, rotationAxis, angle);
1609
+ return assertValidCSG$2(part.rotate(rotationCenter, rotationAxis, angle), 'rotate');
1218
1610
  });
1219
1611
  return self;
1220
1612
  };
@@ -1248,7 +1640,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1248
1640
  // debug(', self);
1249
1641
  var t = calcSnap(self.combine(part), to, axis, orientation, delta);
1250
1642
  self.map(function (part) {
1251
- return part.translate(t);
1643
+ return assertValidCSG$2(part.translate(t), 'snap');
1252
1644
  });
1253
1645
  return self;
1254
1646
  } catch (err) {
@@ -1274,7 +1666,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1274
1666
  noholes: true
1275
1667
  }), axis, to, delta);
1276
1668
  self.map(function (part /*, name */) {
1277
- return part.translate(t);
1669
+ return assertValidCSG$2(part.translate(t), 'align');
1278
1670
  });
1279
1671
 
1280
1672
  // if (self.holes)
@@ -1312,14 +1704,14 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1312
1704
  var myConnector = connectorName.split('.').reduce(function (a, v) {
1313
1705
  return a[v];
1314
1706
  }, self.parts[partName].properties);
1315
- debug$3('toConnector', to instanceof CSG.Connector);
1707
+ debug$3('toConnector', to instanceof CSG$1.Connector);
1316
1708
  var toConnector = toConnectorName.split('.').reduce(function (a, v) {
1317
1709
  return a[v];
1318
1710
  }, to.properties);
1319
1711
  var matrix = myConnector.getTransformationTo(toConnector, mirror, normalrotation);
1320
1712
  debug$3('connectTo', matrix);
1321
1713
  self.map(function (part) {
1322
- return part.transform(matrix);
1714
+ return assertValidCSG$2(part.transform(matrix), 'connectTo');
1323
1715
  });
1324
1716
  return self;
1325
1717
  };
@@ -1340,7 +1732,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1340
1732
  // debug(' part, t);
1341
1733
  // var t = util.calcCenterWith(self.combine(part), axis, to, delta);
1342
1734
  self.map(function (part) {
1343
- return part.translate(t);
1735
+ return assertValidCSG$2(part.translate(t), 'midlineTo');
1344
1736
  });
1345
1737
 
1346
1738
  // if (self.holes)
@@ -1364,7 +1756,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1364
1756
  var t = Array.isArray(x) ? x : [x, y, z];
1365
1757
  debug$3('translate', t);
1366
1758
  self.map(function (part) {
1367
- return part.translate(t);
1759
+ return assertValidCSG$2(part.translate(t), 'translate');
1368
1760
  });
1369
1761
 
1370
1762
  // if (self.holes)
@@ -1388,7 +1780,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1388
1780
  if (!map) map = identity;
1389
1781
  var g = Group();
1390
1782
  p.forEach(function (name) {
1391
- g.add(map(CSG.fromPolygons(self.parts[name].toPolygons()), name), name);
1783
+ g.add(assertValidCSG$2(map(CSG$1.fromPolygons(self.parts[name].toPolygons()), name), 'pick'), name);
1392
1784
  });
1393
1785
  return g;
1394
1786
  };
@@ -1412,7 +1804,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1412
1804
  debug$3('array error', _this, parts);
1413
1805
  throw error("group::array error \"".concat(name, "\" not found.\nthis: ").concat(_this, "\nparts: \"").concat(parts, "\"\n"), 'JSCAD_UTILS_GROUP_ERROR');
1414
1806
  }
1415
- a.push(map(CSG.fromPolygons(self.parts[name].toPolygons()), name));
1807
+ a.push(assertValidCSG$2(map(CSG$1.fromPolygons(self.parts[name].toPolygons()), name), 'array'));
1416
1808
  });
1417
1809
  return a;
1418
1810
  // } catch (err) {
@@ -1481,7 +1873,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1481
1873
  self.names = names && names.length > 0 && names.split(',') || [];
1482
1874
  if (Array.isArray(objects)) {
1483
1875
  self.parts = zipObject(self.names, objects);
1484
- } else if (objects instanceof CSG) {
1876
+ } else if (objects instanceof CSG$1) {
1485
1877
  self.parts = zipObject(self.names, [objects]);
1486
1878
  } else {
1487
1879
  self.parts = objects || {};
@@ -1506,6 +1898,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1506
1898
  }
1507
1899
 
1508
1900
  var debug$2 = Debug('jscadUtils:util');
1901
+ var assertValidCSG$1 = AssertValidCSG('util');
1902
+
1509
1903
  // import utilInit from '../src/add-prototype';
1510
1904
  // utilInit(CSG);
1511
1905
  // console.trace('CSG', CSG.prototype);
@@ -1669,12 +2063,12 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1669
2063
  // console.trace('label', Object.getPrototypeOf(union(o)));
1670
2064
  // var foo = union(o);
1671
2065
  // console.trace('typeof', typeof foo);
1672
- return center(union(o));
2066
+ return assertValidCSG$1(center(union(o)), 'label');
1673
2067
  }
1674
2068
  function text(text) {
1675
2069
  var l = vector_char(0, 0, text); // l contains a list of polylines to draw
1676
2070
  var _char = l.segments.reduce(function (result, segment) {
1677
- var path = new CSG.Path2D(segment);
2071
+ var path = new CSG$1.Path2D(segment);
1678
2072
  var cag = path.expandToCAG(2);
1679
2073
  // debug('reduce', result, segment, path, cag);
1680
2074
  return result ? result.union(cag) : cag;
@@ -1683,17 +2077,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1683
2077
  }
1684
2078
  function unitCube(length, radius) {
1685
2079
  radius = radius || 0.5;
1686
- return CSG.cube({
2080
+ return assertValidCSG$1(CSG$1.cube({
1687
2081
  center: [0, 0, 0],
1688
2082
  radius: [radius, radius, length || 0.5]
1689
- });
2083
+ }), 'unitCube');
1690
2084
  }
1691
2085
  function unitAxis(length, radius, centroid) {
1692
2086
  debug$2('unitAxis', length, radius, centroid);
1693
2087
  centroid = centroid || [0, 0, 0];
1694
2088
  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)]);
1695
- unitaxis.properties.origin = new CSG.Connector([0, 0, 0], [1, 0, 0], [0, 1, 0]);
1696
- return unitaxis.translate(centroid);
2089
+ unitaxis.properties.origin = new CSG$1.Connector([0, 0, 0], [1, 0, 0], [0, 1, 0]);
2090
+ return assertValidCSG$1(unitaxis.translate(centroid), 'unitAxis');
1697
2091
  }
1698
2092
  function toArray(a) {
1699
2093
  return Array.isArray(a) ? a : [a];
@@ -1817,15 +2211,15 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1817
2211
  }
1818
2212
  function center(object, objectSize) {
1819
2213
  objectSize = objectSize || size(object.getBounds());
1820
- return centerY(centerX(object, objectSize), objectSize);
2214
+ return assertValidCSG$1(centerY(centerX(object, objectSize), objectSize), 'center');
1821
2215
  }
1822
2216
  function centerY(object, objectSize) {
1823
2217
  objectSize = objectSize || size(object.getBounds());
1824
- return object.translate([0, -objectSize.y / 2, 0]);
2218
+ return assertValidCSG$1(object.translate([0, -objectSize.y / 2, 0]), 'centerY');
1825
2219
  }
1826
2220
  function centerX(object, objectSize) {
1827
2221
  objectSize = objectSize || size(object.getBounds());
1828
- return object.translate([-objectSize.x / 2, 0, 0]);
2222
+ return assertValidCSG$1(object.translate([-objectSize.x / 2, 0, 0]), 'centerX');
1829
2223
  }
1830
2224
 
1831
2225
  /**
@@ -1857,7 +2251,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1857
2251
 
1858
2252
  /// Calculate the difference between the original centroid and the new
1859
2253
  var delta = new_centroid.minus(objectCentroid).times(-1);
1860
- return new_object.translate(delta);
2254
+ return assertValidCSG$1(new_object.translate(delta), 'enlarge');
1861
2255
  }
1862
2256
 
1863
2257
  /**
@@ -1889,10 +2283,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1889
2283
  }
1890
2284
  var s = [scale(objectSize.x, x), scale(objectSize.y, y), scale(objectSize.z, z)];
1891
2285
  var min$1 = min(s);
1892
- return centerWith(object.scale(s.map(function (d, i) {
2286
+ return assertValidCSG$1(centerWith(object.scale(s.map(function (d, i) {
1893
2287
  if (a[i] === 0) return 1; // don't scale when value is zero
1894
2288
  return keep_aspect_ratio ? min$1 : d;
1895
- })), 'xyz', object);
2289
+ })), 'xyz', object), 'fit');
1896
2290
  }
1897
2291
  function shift(object, x, y, z) {
1898
2292
  var hsize = this.div(this.size(object.getBounds()), 2);
@@ -1900,10 +2294,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1900
2294
  }
1901
2295
  function zero(object) {
1902
2296
  var bounds = object.getBounds();
1903
- return object.translate([0, 0, -bounds[0].z]);
2297
+ return assertValidCSG$1(object.translate([0, 0, -bounds[0].z]), 'zero');
1904
2298
  }
1905
2299
  function mirrored4(x) {
1906
- return x.union([x.mirroredY(90), x.mirroredX(90), x.mirroredY(90).mirroredX(90)]);
2300
+ return assertValidCSG$1(x.union([x.mirroredY(90), x.mirroredX(90), x.mirroredY(90).mirroredX(90)]), 'mirrored4');
1907
2301
  }
1908
2302
  var flushSide = {
1909
2303
  'above-outside': [1, 0],
@@ -1969,7 +2363,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1969
2363
  function snap(moveobj, withobj, axis, orientation, delta) {
1970
2364
  debug$2('snap', moveobj, withobj, axis, orientation, delta);
1971
2365
  var t = calcSnap(moveobj, withobj, axis, orientation, delta);
1972
- return moveobj.translate(t);
2366
+ return assertValidCSG$1(moveobj.translate(t), 'snap');
1973
2367
  }
1974
2368
 
1975
2369
  /**
@@ -1982,23 +2376,35 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
1982
2376
  * @return {CSG} [description]
1983
2377
  */
1984
2378
  function flush(moveobj, withobj, axis, mside, wside) {
1985
- return moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside));
2379
+ return assertValidCSG$1(moveobj.translate(calcFlush(moveobj, withobj, axis, mside, wside)), 'flush');
1986
2380
  }
2381
+
2382
+ /**
2383
+ *
2384
+ * @param {AxisStrings} axes
2385
+ * @param {function(number, string): number} valfun
2386
+ * @param {Array<number>} [a] In initial array to apply the values to, if not provided a new array will be created.
2387
+ * @returns {Array<number>} The resulting array after applying the function to the specified axes.
2388
+ */
1987
2389
  function axisApply(axes, valfun, a) {
1988
2390
  debug$2('axisApply', axes, valfun, a);
2391
+
2392
+ /** @type {Array<number>} */
1989
2393
  var retval = a || [0, 0, 0];
2394
+
2395
+ /** @type {Record<AxisString, number>} */
1990
2396
  var lookup = {
1991
2397
  x: 0,
1992
2398
  y: 1,
1993
2399
  z: 2
1994
2400
  };
1995
2401
  axes.split('').forEach(function (axis) {
1996
- retval[lookup[axis]] = valfun(lookup[axis], axis);
2402
+ retval[lookup[(/** @type {AxisString} */axis)]] = valfun(lookup[(/** @type {AxisString} */axis)], axis);
1997
2403
  });
1998
2404
  return retval;
1999
2405
  }
2000
2406
  function axis2array(axes, valfun) {
2001
- depreciated('axis2array');
2407
+ depreciated('axis2array', false, 'Use axisApply instead.');
2002
2408
  var a = [0, 0, 0];
2003
2409
  var lookup = {
2004
2410
  x: 0,
@@ -2039,7 +2445,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2039
2445
  });
2040
2446
  }
2041
2447
  function midlineTo(o, axis, to) {
2042
- return o.translate(calcmidlineTo(o, axis, to));
2448
+ return assertValidCSG$1(o.translate(calcmidlineTo(o, axis, to)), 'midlineTo');
2043
2449
  }
2044
2450
  function translator(o, axis, withObj) {
2045
2451
  var objectCentroid = centroid(o);
@@ -2060,7 +2466,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2060
2466
  return delta ? add(t, delta) : t;
2061
2467
  }
2062
2468
  function centerWith(o, axis, withObj) {
2063
- return o.translate(calcCenterWith(o, axis, withObj));
2469
+ return assertValidCSG$1(o.translate(calcCenterWith(o, axis, withObj)), 'centerWith');
2064
2470
  }
2065
2471
 
2066
2472
  /**
@@ -2090,6 +2496,109 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2090
2496
  return bounds[0][a] + (isEmpty(dist) ? size[axis] / 2 : dist);
2091
2497
  });
2092
2498
  }
2499
+ var EPS = 1e-5;
2500
+
2501
+ /**
2502
+ * Split a CSG object into two halves along a plane by directly
2503
+ * splitting polygons. This avoids BSP-tree-based boolean operations
2504
+ * which can fail on geometry produced by stretchAtPlane.
2505
+ * @param {CSG} csg The object to split
2506
+ * @param {CSG.Plane} plane The splitting plane
2507
+ * @return {{ front: CSG, back: CSG }} front (positive normal side) and back (negative normal side)
2508
+ */
2509
+ function splitCSGByPlane(csg, plane) {
2510
+ var frontPolys = [];
2511
+ var backPolys = [];
2512
+ csg.polygons.forEach(function (poly) {
2513
+ var vertices = poly.vertices;
2514
+ var numVerts = vertices.length;
2515
+ var hasfront = false;
2516
+ var hasback = false;
2517
+ var vertexIsBack = [];
2518
+ for (var i = 0; i < numVerts; i++) {
2519
+ var t = plane.normal.dot(vertices[i].pos) - plane.w;
2520
+ vertexIsBack.push(t < 0);
2521
+ if (t > EPS) hasfront = true;
2522
+ if (t < -EPS) hasback = true;
2523
+ }
2524
+ if (!hasfront && !hasback) {
2525
+ // coplanar — assign based on normal alignment
2526
+ var d = plane.normal.dot(poly.plane.normal);
2527
+ if (d >= 0) {
2528
+ frontPolys.push(poly);
2529
+ } else {
2530
+ backPolys.push(poly);
2531
+ }
2532
+ } else if (!hasback) {
2533
+ frontPolys.push(poly);
2534
+ } else if (!hasfront) {
2535
+ backPolys.push(poly);
2536
+ } else {
2537
+ // spanning — split the polygon
2538
+ var fv = [];
2539
+ var bv = [];
2540
+ for (var vi = 0; vi < numVerts; vi++) {
2541
+ var vertex = vertices[vi];
2542
+ var nextVi = (vi + 1) % numVerts;
2543
+ var isback = vertexIsBack[vi];
2544
+ var nextisback = vertexIsBack[nextVi];
2545
+ if (isback === nextisback) {
2546
+ if (isback) {
2547
+ bv.push(vertex);
2548
+ } else {
2549
+ fv.push(vertex);
2550
+ }
2551
+ } else {
2552
+ var point = vertex.pos;
2553
+ var nextpoint = vertices[nextVi].pos;
2554
+ var ip = plane.splitLineBetweenPoints(point, nextpoint);
2555
+ var iv = new CSG$1.Vertex(ip);
2556
+ if (isback) {
2557
+ bv.push(vertex);
2558
+ bv.push(iv);
2559
+ fv.push(iv);
2560
+ } else {
2561
+ fv.push(vertex);
2562
+ fv.push(iv);
2563
+ bv.push(iv);
2564
+ }
2565
+ }
2566
+ }
2567
+ // Remove degenerate (near-duplicate) adjacent vertices that arise
2568
+ // when the cut plane passes very close to existing vertices.
2569
+ // This matches the cleanup done by the BSP-tree splitter.
2570
+ var EPSEPS = EPS * EPS;
2571
+ if (fv.length >= 3) {
2572
+ var prev = fv[fv.length - 1];
2573
+ for (var fi = 0; fi < fv.length; fi++) {
2574
+ var curr = fv[fi];
2575
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
2576
+ fv.splice(fi, 1);
2577
+ fi--;
2578
+ }
2579
+ prev = curr;
2580
+ }
2581
+ }
2582
+ if (bv.length >= 3) {
2583
+ var prev = bv[bv.length - 1];
2584
+ for (var bi = 0; bi < bv.length; bi++) {
2585
+ var curr = bv[bi];
2586
+ if (curr.pos.distanceToSquared(prev.pos) < EPSEPS) {
2587
+ bv.splice(bi, 1);
2588
+ bi--;
2589
+ }
2590
+ prev = curr;
2591
+ }
2592
+ }
2593
+ if (fv.length >= 3) frontPolys.push(new CSG$1.Polygon(fv, poly.shared, poly.plane));
2594
+ if (bv.length >= 3) backPolys.push(new CSG$1.Polygon(bv, poly.shared, poly.plane));
2595
+ }
2596
+ });
2597
+ return {
2598
+ front: CSG$1.fromPolygons(frontPolys),
2599
+ back: CSG$1.fromPolygons(backPolys)
2600
+ };
2601
+ }
2093
2602
 
2094
2603
  /**
2095
2604
  * Cut an object into two pieces, along a given axis. The offset
@@ -2100,7 +2609,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2100
2609
  *
2101
2610
  * You can angle the cut plane and position the rotation point.
2102
2611
  *
2103
- * ![bisect example](./images/bisect.png)
2612
+ * ![bisect example](../test/images/bisect%20object%20positive.snap.png)
2104
2613
  * @param {CSG} object object to bisect
2105
2614
  * @param {string} axis axis to cut along
2106
2615
  * @param {number} [offset] offset to cut at
@@ -2178,13 +2687,13 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2178
2687
  }[[axis, rotateaxis].sort().join('')];
2179
2688
  var centroid = object.centroid();
2180
2689
  var rotateDelta = getDelta(objectSize, bounds, rotateOffsetAxis, rotateoffset);
2181
- var rotationCenter = options.rotationCenter || new CSG.Vector3D(axisApply('xyz', function (i, a) {
2690
+ var rotationCenter = options.rotationCenter || new CSG$1.Vector3D(axisApply('xyz', function (i, a) {
2182
2691
  if (a == axis) return cutDelta[i];
2183
2692
  if (a == rotateOffsetAxis) return rotateDelta[i];
2184
2693
  return centroid[a];
2185
2694
  }));
2186
2695
  var theRotationAxis = rotationAxes[rotateaxis];
2187
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1]).translate(cutDelta).rotate(rotationCenter, theRotationAxis, angle);
2696
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1]).translate(cutDelta).rotate(rotationCenter, theRotationAxis, angle);
2188
2697
  debug$2('bisect', debug$2.enabled && {
2189
2698
  axis: axis,
2190
2699
  offset: offset,
@@ -2197,7 +2706,33 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2197
2706
  cutplane: cutplane,
2198
2707
  options: options
2199
2708
  });
2200
- var g = Group('negative,positive', [object.cutByPlane(cutplane.plane).color(options.color && 'red'), object.cutByPlane(cutplane.plane.flipped()).color(options.color && 'blue')]);
2709
+ var negative = object.cutByPlane(cutplane.plane);
2710
+ var positive = object.cutByPlane(cutplane.plane.flipped());
2711
+
2712
+ // Detect cutByPlane failure: if a half's bounding box in the cut axis
2713
+ // is not smaller than the original, the BSP-tree-based cut failed.
2714
+ // Fall back to direct polygon splitting which is more robust, then
2715
+ // apply cutByPlane to the simpler half to add cap faces.
2716
+ var negSize = size(negative);
2717
+ var posSize = size(positive);
2718
+ if (negSize[axis] >= objectSize[axis] - EPS || posSize[axis] >= objectSize[axis] - EPS) {
2719
+ var halves = splitCSGByPlane(object, cutplane.plane);
2720
+ if (negSize[axis] >= objectSize[axis] - EPS) {
2721
+ negative = halves.back;
2722
+ // Cap the open cut face
2723
+ try {
2724
+ negative = negative.cutByPlane(cutplane.plane);
2725
+ } catch (e) {/* keep uncapped */}
2726
+ }
2727
+ if (posSize[axis] >= objectSize[axis] - EPS) {
2728
+ positive = halves.front;
2729
+ // Cap the open cut face
2730
+ try {
2731
+ positive = positive.cutByPlane(cutplane.plane.flipped());
2732
+ } catch (e) {/* keep uncapped */}
2733
+ }
2734
+ }
2735
+ var g = Group('negative,positive', [negative.color(options.color && 'red'), positive.color(options.color && 'blue')]);
2201
2736
  if (options.addRotationCenter) g.add(unitAxis(objectSize.length() + 10, 0.1, rotationCenter), 'rotationCenter');
2202
2737
  return g;
2203
2738
  }
@@ -2222,9 +2757,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2222
2757
  addRotationCenter: true
2223
2758
  };
2224
2759
  var info = normalVector(axis);
2225
- var rotationCenter = options.rotationCenter || new CSG.Vector3D(0, 0, 0);
2760
+ var rotationCenter = options.rotationCenter || new CSG$1.Vector3D(0, 0, 0);
2226
2761
  var theRotationAxis = rotationAxes[rotateaxis];
2227
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1])
2762
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(info.orthoNormalCartesian[0], info.orthoNormalCartesian[1])
2228
2763
  // .translate(cutDelta)
2229
2764
  .rotate(rotationCenter, theRotationAxis, angle);
2230
2765
  var g = Group('negative,positive', [object.cutByPlane(cutplane.plane).color(options.color && 'red'), object.cutByPlane(cutplane.plane.flipped()).color(options.color && 'blue')]);
@@ -2239,7 +2774,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2239
2774
  * Creates a `JsCadUtilsGroup` object that has `body` and `wedge` objects. The `wedge` object
2240
2775
  * is created by radially cutting the object from the `start` to the `end` angle.
2241
2776
  *
2242
- * ![wedge example](./images/wedge.png)
2777
+ * ![wedge example](../test/images/wedge.snap.png)
2243
2778
  *
2244
2779
  *
2245
2780
  * @example
@@ -2295,7 +2830,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2295
2830
  var objectSize = size(object);
2296
2831
  var cutDelta = getDelta(objectSize, bounds, axis, offset, true);
2297
2832
  // debug('stretch.cutDelta', cutDelta, normal[axis]);
2298
- return object.stretchAtPlane(normal[axis], cutDelta, distance);
2833
+ return assertValidCSG$1(object.stretchAtPlane(normal[axis], cutDelta, distance), 'stretch');
2299
2834
  }
2300
2835
 
2301
2836
  /**
@@ -2310,11 +2845,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2310
2845
  function poly2solid(top, bottom, height) {
2311
2846
  if (top.sides.length == 0) {
2312
2847
  // empty!
2313
- return new CSG();
2848
+ return new CSG$1();
2314
2849
  }
2315
2850
  // var offsetVector = CSG.parseOptionAs3DVector(options, "offset", [0, 0, 10]);
2316
- var offsetVector = CSG.Vector3D.Create(0, 0, height);
2317
- var normalVector = CSG.Vector3D.Create(0, 1, 0);
2851
+ var offsetVector = CSG$1.Vector3D.Create(0, 0, height);
2852
+ var normalVector = CSG$1.Vector3D.Create(0, 1, 0);
2318
2853
  var polygons = [];
2319
2854
  // bottom and top
2320
2855
  polygons = polygons.concat(bottom._toPlanePolygons({
@@ -2328,8 +2863,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2328
2863
  flipped: offsetVector.z < 0
2329
2864
  }));
2330
2865
  // walls
2331
- var c1 = new CSG.Connector(offsetVector.times(0), [0, 0, offsetVector.z], normalVector);
2332
- var c2 = new CSG.Connector(offsetVector, [0, 0, offsetVector.z], normalVector);
2866
+ var c1 = new CSG$1.Connector(offsetVector.times(0), [0, 0, offsetVector.z], normalVector);
2867
+ var c2 = new CSG$1.Connector(offsetVector, [0, 0, offsetVector.z], normalVector);
2333
2868
  polygons = polygons.concat(bottom._toWallPolygons({
2334
2869
  cag: top,
2335
2870
  toConnector1: c1,
@@ -2337,7 +2872,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2337
2872
  }));
2338
2873
  // }
2339
2874
 
2340
- return CSG.fromPolygons(polygons);
2875
+ return assertValidCSG$1(CSG$1.fromPolygons(polygons), 'poly2solid');
2341
2876
  }
2342
2877
  function slices2poly(slices, options, axis) {
2343
2878
  debug$2('slices2poly', slices, options, axis);
@@ -2346,7 +2881,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2346
2881
  twiststeps: 0
2347
2882
  }, options);
2348
2883
  var twistangle = options && parseFloat(options.twistangle) || 0;
2349
- options && parseInt(options.twiststeps) || CSG.defaultResolution3D;
2884
+ options && parseInt(options.twiststeps) || CSG$1.defaultResolution3D;
2350
2885
  var normalVector = options.si.normalVector;
2351
2886
  var polygons = [];
2352
2887
 
@@ -2385,8 +2920,8 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2385
2920
  var nextidx = idx + 1;
2386
2921
  var top = !up ? slices[nextidx] : slice;
2387
2922
  var bottom = up ? slices[nextidx] : slice;
2388
- var c1 = new CSG.Connector(bottom.offset, connectorAxis, rotate(normalVector, twistangle, idx / slices.length));
2389
- var c2 = new CSG.Connector(top.offset, connectorAxis, rotate(normalVector, twistangle, nextidx / slices.length));
2923
+ var c1 = new CSG$1.Connector(bottom.offset, connectorAxis, rotate(normalVector, twistangle, idx / slices.length));
2924
+ var c2 = new CSG$1.Connector(top.offset, connectorAxis, rotate(normalVector, twistangle, nextidx / slices.length));
2390
2925
 
2391
2926
  // debug('slices2poly.slices', c1.point, c2.point);
2392
2927
  polygons = polygons.concat(bottom.poly._toWallPolygons({
@@ -2396,21 +2931,21 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2396
2931
  }));
2397
2932
  }
2398
2933
  });
2399
- return CSG.fromPolygons(polygons);
2934
+ return assertValidCSG$1(CSG$1.fromPolygons(polygons), 'slices2poly');
2400
2935
  }
2401
2936
  function normalVector(axis) {
2402
2937
  var axisInfo = {
2403
2938
  z: {
2404
2939
  orthoNormalCartesian: ['X', 'Y'],
2405
- normalVector: CSG.Vector3D.Create(0, 1, 0)
2940
+ normalVector: CSG$1.Vector3D.Create(0, 1, 0)
2406
2941
  },
2407
2942
  x: {
2408
2943
  orthoNormalCartesian: ['Y', 'Z'],
2409
- normalVector: CSG.Vector3D.Create(0, 0, 1)
2944
+ normalVector: CSG$1.Vector3D.Create(0, 0, 1)
2410
2945
  },
2411
2946
  y: {
2412
- orthoNormalCartesian: ['X', 'Z'],
2413
- normalVector: CSG.Vector3D.Create(0, 0, 1)
2947
+ orthoNormalCartesian: ['Z', 'X'],
2948
+ normalVector: CSG$1.Vector3D.Create(0, 0, 1)
2414
2949
  }
2415
2950
  };
2416
2951
  if (!axisInfo[axis]) error('normalVector: invalid axis ' + axis);
@@ -2462,7 +2997,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2462
2997
  var si = sliceParams(orientation, radius, b);
2463
2998
  debug$2('reShape', absoluteRadius, si);
2464
2999
  if (si.axis !== 'z') throw new Error('reShape error: CAG._toPlanePolygons only uses the "z" axis. You must use the "z" axis for now.');
2465
- var cutplane = CSG.OrthoNormalBasis.GetCartesian(si.orthoNormalCartesian[0], si.orthoNormalCartesian[1]).translate(si.cutDelta);
3000
+ var cutplane = CSG$1.OrthoNormalBasis.GetCartesian(si.orthoNormalCartesian[0], si.orthoNormalCartesian[1]).translate(si.cutDelta);
2466
3001
  var slice = object.sectionCut(cutplane);
2467
3002
  var first = axisApply(si.axis, function () {
2468
3003
  return si.positive ? 0 : absoluteRadius;
@@ -2477,25 +3012,25 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2477
3012
  si: si
2478
3013
  }), si.axis).color(options.color);
2479
3014
  var remainder = object.cutByPlane(plane);
2480
- return union([options.unionOriginal ? object : remainder, delta.translate(si.moveDelta)]);
3015
+ return assertValidCSG$1(union([options.unionOriginal ? object : remainder, delta.translate(si.moveDelta)]), 'reShape');
2481
3016
  }
2482
3017
  function chamfer(object, radius, orientation, options) {
2483
- return reShape(object, radius, orientation, options, function (first, last, slice) {
3018
+ return assertValidCSG$1(reShape(object, radius, orientation, options, function (first, last, slice) {
2484
3019
  return [{
2485
3020
  poly: slice,
2486
- offset: new CSG.Vector3D(first)
3021
+ offset: new CSG$1.Vector3D(first)
2487
3022
  }, {
2488
3023
  poly: enlarge(slice, [-radius * 2, -radius * 2]),
2489
- offset: new CSG.Vector3D(last)
3024
+ offset: new CSG$1.Vector3D(last)
2490
3025
  }];
2491
- });
3026
+ }), 'chamfer');
2492
3027
  }
2493
3028
  function fillet(object, radius, orientation, options) {
2494
3029
  options = options || {};
2495
- return reShape(object, radius, orientation, options, function (first, last, slice) {
2496
- var v1 = new CSG.Vector3D(first);
2497
- var v2 = new CSG.Vector3D(last);
2498
- var res = options.resolution || CSG.defaultResolution3D;
3030
+ return assertValidCSG$1(reShape(object, radius, orientation, options, function (first, last, slice) {
3031
+ var v1 = new CSG$1.Vector3D(first);
3032
+ var v2 = new CSG$1.Vector3D(last);
3033
+ var res = options.resolution || CSG$1.defaultResolution3D;
2499
3034
  var slices = range(0, res).map(function (i) {
2500
3035
  var p = i > 0 ? i / (res - 1) : 0;
2501
3036
  var v = v1.lerp(v2, p);
@@ -2506,7 +3041,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2506
3041
  };
2507
3042
  });
2508
3043
  return slices;
2509
- });
3044
+ }), 'fillet');
2510
3045
  }
2511
3046
  function calcRotate(part, solid, axis /* , angle */) {
2512
3047
  var axes = {
@@ -2525,7 +3060,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2525
3060
  var _calcRotate = calcRotate(part, solid, axis),
2526
3061
  rotationCenter = _calcRotate.rotationCenter,
2527
3062
  rotationAxis = _calcRotate.rotationAxis;
2528
- return part.rotate(rotationCenter, rotationAxis, angle);
3063
+ return assertValidCSG$1('rotateAround')(part.rotate(rotationCenter, rotationAxis, angle));
2529
3064
  }
2530
3065
  function cloneProperties(from, to) {
2531
3066
  return Object.entries(from).reduce(function (props, _ref) {
@@ -2537,10 +3072,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2537
3072
  }, to);
2538
3073
  }
2539
3074
  function clone(o) {
2540
- var c = CSG.fromPolygons(o.toPolygons());
3075
+ var c = CSG$1.fromPolygons(o.toPolygons());
2541
3076
  cloneProperties(o, c);
2542
- debug$2('clone', o, c, CSG);
2543
- return c;
3077
+ debug$2('clone', o, c, CSG$1);
3078
+ return assertValidCSG$1(c, 'clone');
2544
3079
  }
2545
3080
 
2546
3081
  /**
@@ -2556,11 +3091,12 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2556
3091
  var point = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0, 0];
2557
3092
  var axis = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [1, 0, 0];
2558
3093
  var normal = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [0, 0, 1];
2559
- object.properties[name] = new CSG.Connector(point, axis, normal);
2560
- return object;
3094
+ object.properties[name] = new CSG$1.Connector(point, axis, normal);
3095
+ return assertValidCSG$1('addConnector')(object);
2561
3096
  }
2562
3097
 
2563
3098
  var debug$1 = Debug('jscadUtils:parts');
3099
+ var assertValidCSG = AssertValidCSG('parts');
2564
3100
  var parts = {
2565
3101
  BBox: BBox$1,
2566
3102
  Cube: Cube,
@@ -2578,7 +3114,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2578
3114
  */
2579
3115
  function BBox$1() {
2580
3116
  function box(object) {
2581
- return CSG.cube({
3117
+ return CSG$1.cube({
2582
3118
  center: object.centroid(),
2583
3119
  radius: object.size().dividedBy(2)
2584
3120
  });
@@ -2586,17 +3122,17 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2586
3122
  for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) {
2587
3123
  objects[_key] = arguments[_key];
2588
3124
  }
2589
- return objects.reduce(function (bbox, part) {
3125
+ return assertValidCSG(objects.reduce(function (bbox, part) {
2590
3126
  var object = bbox ? union([bbox, box(part)]) : part;
2591
3127
  return box(object);
2592
- }, undefined);
3128
+ }, undefined), 'BBox');
2593
3129
  }
2594
3130
  function Cube(width) {
2595
3131
  var r = div$1(fromxyz(width), 2);
2596
- return CSG.cube({
3132
+ return assertValidCSG(CSG$1.cube({
2597
3133
  center: r,
2598
3134
  radius: r
2599
- });
3135
+ }), 'Cube');
2600
3136
  }
2601
3137
 
2602
3138
  // export function Sphere(diameter) {
@@ -2630,11 +3166,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2630
3166
  center: [r[0], r[1], 0],
2631
3167
  radius: r,
2632
3168
  roundradius: corner_radius,
2633
- resolution: CSG.defaultResolution2D
3169
+ resolution: CSG$1.defaultResolution2D
2634
3170
  }).extrude({
2635
3171
  offset: [0, 0, thickness || 1.62]
2636
3172
  });
2637
- return roundedcube;
3173
+ return assertValidCSG(roundedcube, 'RoundedCube');
2638
3174
  }
2639
3175
 
2640
3176
  /**
@@ -2652,9 +3188,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2652
3188
  start: [0, 0, 0],
2653
3189
  end: [0, 0, height],
2654
3190
  radius: diameter / 2,
2655
- resolution: CSG.defaultResolution2D
3191
+ resolution: CSG$1.defaultResolution2D
2656
3192
  }, options);
2657
- return CSG.cylinder(options);
3193
+ return assertValidCSG(CSG$1.cylinder(options), 'Cylinder');
2658
3194
  }
2659
3195
 
2660
3196
  /**
@@ -2669,13 +3205,13 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2669
3205
  function Cone(diameter1, diameter2, height) {
2670
3206
  var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
2671
3207
  debug$1('parts.Cone', diameter1, diameter2, height, options);
2672
- return CSG.cylinder(Object.assign({
3208
+ return assertValidCSG(CSG$1.cylinder(Object.assign({
2673
3209
  start: [0, 0, 0],
2674
3210
  end: [0, 0, height],
2675
3211
  radiusStart: diameter1 / 2,
2676
3212
  radiusEnd: diameter2 / 2,
2677
- resolution: CSG.defaultResolution2D
2678
- }, options));
3213
+ resolution: CSG$1.defaultResolution2D
3214
+ }, options)), 'Cone');
2679
3215
  }
2680
3216
 
2681
3217
  /**
@@ -2688,9 +3224,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2688
3224
  var radius = diameter / 2;
2689
3225
  var sqrt3 = Math.sqrt(3) / 2;
2690
3226
  var hex = CAG.fromPoints([[radius, 0], [radius / 2, radius * sqrt3], [-radius / 2, radius * sqrt3], [-radius, 0], [-radius / 2, -radius * sqrt3], [radius / 2, -radius * sqrt3]]);
2691
- return hex.extrude({
3227
+ return assertValidCSG(hex.extrude({
2692
3228
  offset: [0, 0, height]
2693
- });
3229
+ }), 'Hexagon');
2694
3230
  }
2695
3231
 
2696
3232
  /**
@@ -2703,9 +3239,9 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2703
3239
  function Triangle(base, height) {
2704
3240
  var radius = base / 2;
2705
3241
  var tri = CAG.fromPoints([[-radius, 0], [radius, 0], [0, Math.sin(30) * radius]]);
2706
- return tri.extrude({
3242
+ return assertValidCSG(tri.extrude({
2707
3243
  offset: [0, 0, height]
2708
- });
3244
+ }), 'Triangle');
2709
3245
  }
2710
3246
 
2711
3247
  /**
@@ -2720,7 +3256,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2720
3256
  * @returns {CSG} A CSG Tube
2721
3257
  */
2722
3258
  function Tube(outsideDiameter, insideDiameter, height, outsideOptions, insideOptions) {
2723
- return Cylinder(outsideDiameter, height, outsideOptions).subtract(Cylinder(insideDiameter, height, insideOptions || outsideOptions));
3259
+ return assertValidCSG(Cylinder(outsideDiameter, height, outsideOptions).subtract(Cylinder(insideDiameter, height, insideOptions || outsideOptions)), 'Tube');
2724
3260
  }
2725
3261
 
2726
3262
  /**
@@ -2744,11 +3280,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2744
3280
  center: [r[0], r[1], 0],
2745
3281
  radius: r,
2746
3282
  roundradius: corner_radius,
2747
- resolution: CSG.defaultResolution2D
3283
+ resolution: CSG$1.defaultResolution2D
2748
3284
  }).extrude({
2749
3285
  offset: [0, 0, thickness || 1.62]
2750
3286
  });
2751
- return board;
3287
+ return assertValidCSG(board, 'Board');
2752
3288
  }
2753
3289
  var Hardware = {
2754
3290
  Orientation: {
@@ -2898,7 +3434,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2898
3434
  * This will bisect an object using a rabett join. Returns a
2899
3435
  * `group` object with `positive` and `negative` parts.
2900
3436
  *
2901
- * * ![parts example](./images/rabett.png)
3437
+ * ![rabett example](../test/images/boxes-Rabett.snap.png)
2902
3438
  * @example
2903
3439
  *include('dist/jscad-utils.jscad');
2904
3440
  *
@@ -2928,6 +3464,11 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2928
3464
  gap = gap || 0.25;
2929
3465
  var inside = thickness - gap;
2930
3466
  var outside = -thickness + gap;
3467
+ var boxHeight = box.size().z;
3468
+ if (Math.abs(height) >= boxHeight) {
3469
+ throw new Error("Rabett: height (".concat(height, ") must be less than the object height (").concat(boxHeight, ")"));
3470
+ }
3471
+
2931
3472
  // options.color = true;
2932
3473
  debug('inside', inside, 'outside', outside);
2933
3474
  var group = Group();
@@ -2957,7 +3498,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
2957
3498
  * Used on a hollow object, this will rabett out the top and/or
2958
3499
  * bottom of the object.
2959
3500
  *
2960
- * ![A hollow hexagon with removable top and bottom](../images/rabett-tb.png)
3501
+ * ![A hollow hexagon with removable top and bottom](../test/images/boxes-RabettTopBottom.snap.png)
2961
3502
  *
2962
3503
  * @example
2963
3504
  *include('dist/jscad-utils.jscad');
@@ -3044,10 +3585,10 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3044
3585
  thickness = thickness || 2;
3045
3586
  var s = div$1(xyz2array(size), 2);
3046
3587
  var r = add(s, thickness);
3047
- var box = CSG.cube({
3588
+ var box = CSG$1.cube({
3048
3589
  center: r,
3049
3590
  radius: r
3050
- }).subtract(CSG.cube({
3591
+ }).subtract(CSG$1.cube({
3051
3592
  center: r,
3052
3593
  radius: s
3053
3594
  }));
@@ -3062,7 +3603,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3062
3603
  * wall thickness. This is done by reducing the object by half the
3063
3604
  * thickness and subtracting the reduced version from the original object.
3064
3605
  *
3065
- * ![A hollowed out cylinder](../images/rabett.png)
3606
+ * ![A hollowed out cylinder](../test/images/boxes-Rabett.snap.png)
3066
3607
  *
3067
3608
  * @param {CSG} object A CSG object
3068
3609
  * @param {Number} [thickness=2] The thickness of the walls.
@@ -3090,7 +3631,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3090
3631
  var BBox = function BBox(o) {
3091
3632
  depreciated('BBox', true, "Use 'parts.BBox' instead");
3092
3633
  var s = div$1(xyz2array(o.size()), 2);
3093
- return CSG.cube({
3634
+ return CSG$1.cube({
3094
3635
  center: s,
3095
3636
  radius: s
3096
3637
  }).align(o, 'xyz');
@@ -3102,7 +3643,7 @@ var jscadUtils = (function (exports, jsCadCSG, scadApi) {
3102
3643
  var gap = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.25;
3103
3644
  var r = add(getRadius(box), -thickness / 2);
3104
3645
  r[2] = thickness / 2;
3105
- var cutter = CSG.cube({
3646
+ var cutter = CSG$1.cube({
3106
3647
  center: r,
3107
3648
  radius: r
3108
3649
  }).align(box, 'xy').color('green');