brepjs-verify 0.3.0 → 0.8.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.
@@ -164,6 +164,14 @@ var HINT_TABLE = {
164
164
  fix: "The boolean subtraction failed — often a tool that does not actually intersect the base, or tolerance issues.",
165
165
  nextStep: "Confirm the tool overlaps the base, optionally heal the inputs, then re-cut."
166
166
  },
167
+ FILLET_FAILED: {
168
+ fix: "The fillet could not be built — usually the radius is too large for an edge it touched (it cannot exceed the adjacent face/wall), or you filleted EVERY edge (the no-edge-list form) including ones too thin to round.",
169
+ nextStep: "Select only the edges you mean to round — e.g. edgeFinder().inDirection(\"Z\").findAll(solid) — and/or reduce the radius below the thinnest adjacent wall, then re-verify."
170
+ },
171
+ CHAMFER_FAILED: {
172
+ fix: "The chamfer could not be built — usually the distance is too large for an edge it touched, or you chamfered EVERY edge (the no-edge-list form) including ones too thin.",
173
+ nextStep: "Select only the target edges with an edge finder and/or reduce the distance below the shortest adjacent edge, then re-verify."
174
+ },
167
175
  BOOLEAN_HAS_ERRORS: {
168
176
  fix: "The boolean ran but the kernel reported errors (often coincident faces or near-tangent contact).",
169
177
  nextStep: "Perturb one operand slightly so contact is a clean overlap, or heal the inputs, then retry."
@@ -199,6 +207,10 @@ var HINT_TABLE = {
199
207
  TYPECHECK: {
200
208
  fix: "Fix the TypeScript type error before running the part — the API call or value does not match brepjs’s types.",
201
209
  nextStep: "Correct the flagged type (e.g. argument/return type or import), then re-verify."
210
+ },
211
+ EXPECTED_UNKNOWN_KEY: {
212
+ fix: "Your `expected` block has keys the CLI does not assert (so the intended check never ran). Bounds must be `{ xMin, xMax, yMin, yMax, zMin, zMax }` — not `{ min, max }` or `{ x, y, z }`.",
213
+ nextStep: "Rewrite `expected` using only volume, area, tolerancePct, and bounds.{xMin..zMax}, then re-verify."
202
214
  }
203
215
  };
204
216
  /** Synthetic code attached to validity-check failures (validSolid returns a plain string error). */
@@ -249,7 +261,7 @@ function shapeTypeOf(brep, s) {
249
261
  return "Unknown";
250
262
  }
251
263
  function runChecks(brep, shape) {
252
- const { isSolid, isShape3D, isFace, measureVolume, measureArea, getBounds, validSolid, isOk } = brep;
264
+ const { isSolid, isShape3D, isFace, measureVolumeProps, measureArea, getBounds, getFaces, getEdges, getWires, getVertices, getSolids, getShells, isManifoldShell, validSolid, isOk } = brep;
253
265
  const r = emptyReport();
254
266
  r.shapeType = shapeTypeOf(brep, shape);
255
267
  if (isSolid(shape)) {
@@ -266,19 +278,41 @@ function runChecks(brep, shape) {
266
278
  });
267
279
  }
268
280
  r.checks.push(validCheck);
281
+ } else {
282
+ const solids = getSolids(shape);
283
+ if (solids.length > 0) {
284
+ const failures = [];
285
+ solids.forEach((s, i) => {
286
+ const v = validSolid(s);
287
+ if (!isOk(v)) failures.push(`body ${i}: ${v.error}`);
288
+ });
289
+ const bodiesCheck = {
290
+ name: "allBodiesValid",
291
+ passed: failures.length === 0
292
+ };
293
+ if (failures.length > 0) {
294
+ bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
295
+ r.errorInfos.push({
296
+ message: `allBodiesValid: ${bodiesCheck.detail}`,
297
+ code: VALIDITY_FAILURE_CODE
298
+ });
299
+ }
300
+ r.checks.push(bodiesCheck);
301
+ }
269
302
  }
270
303
  if (isShape3D(shape)) {
271
- const vol = measureVolume(shape);
272
- if (isOk(vol)) {
273
- r.measurements.volume = vol.value;
304
+ const volProps = measureVolumeProps(shape);
305
+ if (isOk(volProps)) {
306
+ r.measurements.volume = volProps.value.volume;
307
+ r.measurements.centerOfMass = volProps.value.centerOfMass;
274
308
  r.checks.push({
275
309
  name: "positiveVolume",
276
- passed: vol.value > 0
310
+ passed: volProps.value.volume > 0
277
311
  });
278
312
  } else pushError(r, {
279
- message: `measureVolume: ${vol.error.message}`,
280
- code: vol.error.code,
281
- suggestion: vol.error.suggestion
313
+ message: `measureVolume: ${volProps.error.message}`,
314
+ code: volProps.error.code,
315
+ suggestion: volProps.error.suggestion
282
316
  });
283
317
  }
284
318
  if (isFace(shape) || isShape3D(shape)) {
@@ -290,6 +324,18 @@ function runChecks(brep, shape) {
290
324
  } catch (e) {
291
325
  pushError(r, { message: `getBounds: ${e.message}` });
292
326
  }
327
+ try {
328
+ r.topology = {
329
+ faceCount: getFaces(shape).length,
330
+ edgeCount: getEdges(shape).length,
331
+ wireCount: getWires(shape).length,
332
+ vertexCount: getVertices(shape).length
333
+ };
334
+ } catch {}
335
+ if (r.topology) try {
336
+ const shells = getShells(shape);
337
+ if (shells.length > 0) r.topology.manifold = shells.every((s) => isManifoldShell(s));
338
+ } catch {}
293
339
  r.hints = buildHints(r);
294
340
  return r;
295
341
  }
@@ -302,8 +348,38 @@ function pctDelta(actual, expected) {
302
348
  return Math.abs(actual - expected) / Math.abs(expected) * 100;
303
349
  }
304
350
  function withinTolerance(actual, expected, tolerancePct) {
351
+ if (Math.abs(actual - expected) <= 1e-6) return true;
305
352
  return pctDelta(actual, expected) <= tolerancePct;
306
353
  }
354
+ var TOP_LEVEL_KEYS = new Set([
355
+ "volume",
356
+ "area",
357
+ "bounds",
358
+ "tolerancePct"
359
+ ]);
360
+ var BOUND_KEYS = new Set([
361
+ "xMin",
362
+ "xMax",
363
+ "yMin",
364
+ "yMax",
365
+ "zMin",
366
+ "zMax"
367
+ ]);
368
+ /**
369
+ * Keys in an `expected` block that the CLI does not understand and would silently ignore — a
370
+ * `{ min: [...], max: [...] }` or `{ x: [...] }` bounds shape, or a misspelled top-level field.
371
+ * Surfaced as an error (not dropped) so a wrong `expected` shape fails loud instead of passing
372
+ * vacuously with the intended assertion never run.
373
+ */
374
+ function unknownExpectedKeys(expected) {
375
+ const bad = [];
376
+ for (const k of Object.keys(expected)) if (!TOP_LEVEL_KEYS.has(k)) bad.push(k);
377
+ const bounds = expected.bounds;
378
+ if (bounds && typeof bounds === "object") {
379
+ for (const k of Object.keys(bounds)) if (!BOUND_KEYS.has(k)) bad.push(`bounds.${k}`);
380
+ }
381
+ return bad;
382
+ }
307
383
  function isExpectedDims(v) {
308
384
  if (typeof v !== "object" || v === null) return false;
309
385
  const r = v;
@@ -427,6 +503,25 @@ var COMPILER_OPTIONS = {
427
503
  skipLibCheck: true,
428
504
  allowImportingTsExtensions: true
429
505
  };
506
+ /**
507
+ * Locate the `@types/node` declarations so a part may import Node built-ins (`node:fs` to load a
508
+ * font, `node:fs/promises` to read a STEP file, etc.) without `--check` failing on the import.
509
+ * Returns the `@types` directory to use as a `typeRoots` entry. Probes the tool's own install
510
+ * first (where `@types/node` ships as a dependency), then the part's directory.
511
+ */
512
+ function nodeTypesRoot(partPath, toolDir) {
513
+ const froms = [
514
+ {}.url,
515
+ toolDir ? (0, node_url.pathToFileURL)((0, node_path.resolve)(toolDir, "package.json")).href : void 0,
516
+ (0, node_url.pathToFileURL)(partPath).href
517
+ ];
518
+ for (const from of froms) {
519
+ if (!from) continue;
520
+ try {
521
+ return (0, node_path.dirname)((0, node_path.dirname)((0, node_module.createRequire)(from).resolve("@types/node/package.json")));
522
+ } catch {}
523
+ }
524
+ }
430
525
  function diagnosticToErrorInfo(d) {
431
526
  const text = typescript.default.flattenDiagnosticMessageText(d.messageText, "\n");
432
527
  let where = "";
@@ -451,6 +546,11 @@ function typecheckPart(partPath, toolDir) {
451
546
  const dts = resolveBrepjsTypes(partPath, toolDir);
452
547
  const options = { ...COMPILER_OPTIONS };
453
548
  if (dts) options.paths = { brepjs: [dts] };
549
+ const typesRoot = nodeTypesRoot(partPath, toolDir);
550
+ if (typesRoot) {
551
+ options.typeRoots = [typesRoot];
552
+ options.types = ["node"];
553
+ }
454
554
  const program = typescript.default.createProgram([partPath], options);
455
555
  const errors = [
456
556
  ...program.getSemanticDiagnostics(),
@@ -464,6 +564,39 @@ function typecheckPart(partPath, toolDir) {
464
564
  }
465
565
  //#endregion
466
566
  //#region src/verify/runPart.ts
567
+ /** Centroid of a face group's vertices, in part (Z-up, mm) coordinates. */
568
+ function faceCentroid(m, start, count) {
569
+ let x = 0;
570
+ let y = 0;
571
+ let z = 0;
572
+ for (let i = start; i < start + count; i++) {
573
+ const vi = (m.triangles[i] ?? 0) * 3;
574
+ x += m.vertices[vi] ?? 0;
575
+ y += m.vertices[vi + 1] ?? 0;
576
+ z += m.vertices[vi + 2] ?? 0;
577
+ }
578
+ const n = count || 1;
579
+ return [
580
+ x / n,
581
+ y / n,
582
+ z / n
583
+ ];
584
+ }
585
+ function buildMaterialMap(m, spec) {
586
+ if (spec === void 0 || spec === null) return {};
587
+ if (typeof spec !== "function" && (typeof spec !== "object" || Array.isArray(spec))) return { warning: "export const materials must be a function or a material object — ignored" };
588
+ const sel = spec;
589
+ const select = typeof sel === "function" ? sel : () => sel;
590
+ const map = /* @__PURE__ */ new Map();
591
+ for (const fg of m.faceGroups) {
592
+ const mat = select({
593
+ faceId: fg.faceId,
594
+ center: faceCentroid(m, fg.start, fg.count)
595
+ });
596
+ if (mat) map.set(fg.faceId, mat);
597
+ }
598
+ return map.size > 0 ? { map } : {};
599
+ }
467
600
  async function loadPart(modulePath) {
468
601
  try {
469
602
  return await import((0, node_url.pathToFileURL)(modulePath).href);
@@ -487,7 +620,13 @@ function toErrorInfo(prefix, e) {
487
620
  code: e.code,
488
621
  suggestion: e.suggestion
489
622
  };
490
- if (e instanceof Error) return { message: `${prefix}: ${e.message}` };
623
+ if (e instanceof Error) {
624
+ const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1];
625
+ return code ? {
626
+ message: `${prefix}: ${e.message}`,
627
+ code
628
+ } : { message: `${prefix}: ${e.message}` };
629
+ }
491
630
  return { message: `${prefix}: ${String(e)}` };
492
631
  }
493
632
  function finalize(result) {
@@ -567,11 +706,22 @@ async function runPart(modulePath, opts = {}) {
567
706
  if (isExpectedDims(mod.expected)) {
568
707
  const expected = mod.expected;
569
708
  result.assertions = evaluateExpected(expected, result.measurements);
709
+ const unknown = unknownExpectedKeys(expected);
710
+ if (unknown.length > 0) pushError(result, {
711
+ message: `expected has unrecognized keys (ignored): ${unknown.join(", ")}. Valid keys: volume, area, tolerancePct, bounds.{xMin,xMax,yMin,yMax,zMin,zMax}.`,
712
+ code: "EXPECTED_UNKNOWN_KEY"
713
+ });
570
714
  }
571
715
  let glb;
572
716
  let step;
573
717
  if (opts.glb) try {
574
- glb = exportGlb(mesh(shape));
718
+ const shapeMesh = mesh(shape);
719
+ const { map, warning } = buildMaterialMap(shapeMesh, mod.materials);
720
+ if (warning) pushError(result, {
721
+ message: `materials: ${warning}`,
722
+ code: "MATERIALS_IGNORED"
723
+ });
724
+ glb = map ? exportGlb(shapeMesh, { materials: map }) : exportGlb(shapeMesh);
575
725
  } catch (e) {
576
726
  pushError(result, toErrorInfo("exportGlb", e));
577
727
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { runPart, type RunPartOptions, type RunPartResult } from './verify/runPa
2
2
  export { runChecks } from './verify/checks.js';
3
3
  export { runMeasure, type MeasureReport } from './verify/measure.js';
4
4
  export { runDiff } from './verify/diff.js';
5
- export { serializeReport, emptyReport, type VerifyReport, type VerifyCheck, type VerifyMeasurements, type VerifyAssertion, type DiffReport, type BoundsDelta, } from './verify/report.js';
5
+ export { serializeReport, emptyReport, type VerifyReport, type VerifyCheck, type VerifyMeasurements, type VerifyTopology, type VerifyAssertion, type DiffReport, type BoundsDelta, } from './verify/report.js';
6
6
  export { typecheckPart, TYPECHECK_CODE, type TypecheckResult } from './verify/typecheck.js';
7
7
  export { evaluateExpected, isExpectedDims, pctDelta, DEFAULT_TOLERANCE_PCT, type ExpectedDims, type ExpectedBounds, } from './verify/expected.js';