forgecad 0.9.5 → 0.9.6

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.
Files changed (39) hide show
  1. package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-Da6hhpJx.js} +1 -1
  2. package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-Bl_sKeWb.js} +1 -1
  3. package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-Blz3Tp4j.js} +1 -1
  4. package/dist/assets/{EditorApp-DNH1TEz1.js → EditorApp-CuiPbtn5.js} +32 -7
  5. package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-BFG6-Ufm.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
  7. package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-BMxYT_F0.js} +1 -1
  8. package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-VVQNrCAg.js} +1 -1
  9. package/dist/assets/{app-CFy7g5WP.js → app-Dl9ymBWC.js} +293 -36
  10. package/dist/assets/cli/{render-BrVVdj_T.js → render-CFtwKCCY.js} +10 -1081
  11. package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → distance-BEC2RjJi.js} +1897 -288
  12. package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-CRvbzTXm.js} +555 -83
  13. package/dist/assets/{manifold-Cjk7WhRs.js → manifold-B9QSr-qP.js} +1 -1
  14. package/dist/assets/{manifold-Dp6pvFr6.js → manifold-DpBXFS2K.js} +1 -1
  15. package/dist/assets/{manifold-CRoBhJKH.js → manifold-DzZ4VRPs.js} +2 -2
  16. package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-BuAXF2jh.js} +1 -1
  17. package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-BNWEnRg1.js} +555 -83
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +1 -1
  20. package/dist/docs-raw/beta-operations.md +4 -0
  21. package/dist/docs-raw/deployment.md +38 -23
  22. package/dist/docs-raw/generated/concepts.md +82 -5
  23. package/dist/docs-raw/generated/curves.md +97 -5
  24. package/dist/docs-raw/generated/sketch.md +9 -1
  25. package/dist/docs-raw/guides/inspection-bundles.md +9 -3
  26. package/dist/docs-raw/runbook.md +3 -3
  27. package/dist/index.html +1 -1
  28. package/dist/sitemap.xml +6 -6
  29. package/dist-cli/forgecad.js +828 -297
  30. package/dist-cli/forgecad.js.map +1 -1
  31. package/dist-skill/CONTEXT.md +115 -9
  32. package/dist-skill/docs/generated/curves.md +97 -5
  33. package/dist-skill/docs/generated/sketch.md +9 -1
  34. package/dist-skill/docs/guides/inspection-bundles.md +9 -3
  35. package/dist-skill/docs-dev/generated/curves.md +97 -5
  36. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  37. package/dist-skill/docs-dev/guides/inspection-bundles.md +9 -3
  38. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  39. package/package.json +20 -2
@@ -1,8 +1,5 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { D as DoubleSide, bH as initSolverWasm, bG as initKernel, S as Scene, bI as BoxGeometry, bd as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bJ as localAabbPlaneRelation, h as Vector2, bK as ShapeUtils, a0 as MathUtils, g as Vector3, G as Box3, aU as BufferAttribute, e as Color, aC as resolveForgeRenderStyle, b9 as getRenderStylePreset, ax as setParamOverrides, b6 as runScript, bL as Group, b3 as shapeToGeometry, b7 as MeshPhysicalMaterial, ba as AdditiveBlending, aH as LineBasicMaterial, b8 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bM as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bN as parseCameraCliSpec, bO as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bz as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bg as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bf as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bA as resolveJointAnimation, bB as resolveJointViewValues, R as Raycaster, bP as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-CykEnkvQ.js";
5
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-3DfsSASX.js";
1
+ import { D as DoubleSide, bM as initSolverWasm, bL as initKernel, S as Scene, bN as BoxGeometry, bg as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bO as localAabbPlaneRelation, h as Vector2, bP as ShapeUtils, g as Vector3, e as Color, aC as resolveForgeRenderStyle, bc as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, a0 as MathUtils, G as Box3, bQ as Group, b3 as shapeToGeometry, b8 as MeshPhysicalMaterial, bd as AdditiveBlending, aH as LineBasicMaterial, b9 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bR as resolveRoughnessInspectionOptions, bb as analyzeRoughnessGeometry, bS as ROUGHNESS_COLORS, bJ as analyzePhysicalConnectivity, bT as resolveThicknessInspectionOptions, ba as analyzeThicknessGeometry, bU as summarizeThicknessSamples, bV as THICKNESS_COLORS, bW as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bX as parseCameraCliSpec, bY as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bC as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bj as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bi as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bD as resolveJointAnimation, bE as resolveJointViewValues, bK as analyzeDistanceInspection, b2 as analyzeCollisionIntersections, bZ as serializeCollisionFinding, b_ as worldAuthorPlaneToLocal, a$ as SpotLight, aU as BufferAttribute } from "../distance-BEC2RjJi.js";
2
+ import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-BuAXF2jh.js";
6
3
  const CAD_MATERIAL_PROPS = {
7
4
  color: 6003669,
8
5
  metalness: 0.05,
@@ -269,17 +266,17 @@ function stitchLoops(points, edges) {
269
266
  const warnings = [];
270
267
  const adjacency = /* @__PURE__ */ new Map();
271
268
  const unusedEdges = /* @__PURE__ */ new Set();
272
- const edgeKey2 = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
269
+ const edgeKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
273
270
  for (const [a, b] of edges) {
274
271
  if (!adjacency.has(a)) adjacency.set(a, []);
275
272
  if (!adjacency.has(b)) adjacency.set(b, []);
276
273
  (_a = adjacency.get(a)) == null ? void 0 : _a.push(b);
277
274
  (_b = adjacency.get(b)) == null ? void 0 : _b.push(a);
278
- unusedEdges.add(edgeKey2(a, b));
275
+ unusedEdges.add(edgeKey(a, b));
279
276
  }
280
277
  const loops = [];
281
278
  for (const [edgeA, edgeB] of edges) {
282
- const firstKey = edgeKey2(edgeA, edgeB);
279
+ const firstKey = edgeKey(edgeA, edgeB);
283
280
  if (!unusedEdges.has(firstKey)) continue;
284
281
  const loop = [edgeA, edgeB];
285
282
  unusedEdges.delete(firstKey);
@@ -288,12 +285,12 @@ function stitchLoops(points, edges) {
288
285
  let closed = false;
289
286
  for (let guard = 0; guard < points.length + edges.length + 8; guard += 1) {
290
287
  const neighbors = adjacency.get(current) ?? [];
291
- const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey2(current, candidate)));
288
+ const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey(current, candidate)));
292
289
  if (next === void 0) {
293
290
  if (current === edgeA) closed = true;
294
291
  break;
295
292
  }
296
- unusedEdges.delete(edgeKey2(current, next));
293
+ unusedEdges.delete(edgeKey(current, next));
297
294
  if (next === edgeA) {
298
295
  closed = true;
299
296
  break;
@@ -457,618 +454,6 @@ function computeMeshSectionCap(mesh, planeInput) {
457
454
  warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
458
455
  };
459
456
  }
460
- const DEFAULT_COLLISION_INSPECTION_OPTIONS = {
461
- minOverlapVolume: 0.1
462
- };
463
- function cloneVec3$1(value) {
464
- return [value[0], value[1], value[2]];
465
- }
466
- function isIdentityTransform(matrix) {
467
- if (!matrix) return true;
468
- const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
469
- return identity.every((value, index) => Math.abs(matrix[index] - value) <= 1e-12);
470
- }
471
- function transformPoint(matrix, point) {
472
- const [x, y, z] = point;
473
- return [
474
- matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
475
- matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
476
- matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
477
- ];
478
- }
479
- function transformBBox(min, max, matrix) {
480
- const corners = [
481
- [min[0], min[1], min[2]],
482
- [min[0], min[1], max[2]],
483
- [min[0], max[1], min[2]],
484
- [min[0], max[1], max[2]],
485
- [max[0], min[1], min[2]],
486
- [max[0], min[1], max[2]],
487
- [max[0], max[1], min[2]],
488
- [max[0], max[1], max[2]]
489
- ];
490
- const outMin = [Infinity, Infinity, Infinity];
491
- const outMax = [-Infinity, -Infinity, -Infinity];
492
- for (const corner of corners) {
493
- const transformed = transformPoint(matrix, corner);
494
- for (let axis = 0; axis < 3; axis += 1) {
495
- outMin[axis] = Math.min(outMin[axis], transformed[axis]);
496
- outMax[axis] = Math.max(outMax[axis], transformed[axis]);
497
- }
498
- }
499
- return { min: outMin, max: outMax };
500
- }
501
- function prepareEntry$1(entry) {
502
- if (isIdentityTransform(entry.transform)) {
503
- return {
504
- ...entry,
505
- min: cloneVec3$1(entry.min),
506
- max: cloneVec3$1(entry.max)
507
- };
508
- }
509
- const bbox = transformBBox(entry.min, entry.max, entry.transform);
510
- return {
511
- ...entry,
512
- shape: entry.shape.transform(entry.transform),
513
- min: bbox.min,
514
- max: bbox.max
515
- };
516
- }
517
- function bboxOverlaps(a, b) {
518
- return [0, 1, 2].every((axis) => a.min[axis] < b.max[axis] && a.max[axis] > b.min[axis]);
519
- }
520
- function collisionId(a, b) {
521
- return `${a.id}__${b.id}`;
522
- }
523
- function serializeCollisionFinding(finding) {
524
- return {
525
- index: finding.index,
526
- id: finding.id,
527
- sourceIndex: finding.sourceIndex,
528
- targetIndex: finding.targetIndex,
529
- sourceId: finding.sourceId,
530
- targetId: finding.targetId,
531
- sourceName: finding.sourceName,
532
- targetName: finding.targetName,
533
- overlapVolume: finding.overlapVolume
534
- };
535
- }
536
- function analyzeCollisionIntersections(entries, rawOptions = {}) {
537
- const options = {
538
- minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume
539
- };
540
- const warnings = [];
541
- const collisions = [];
542
- const preparedEntries = entries.map((entry) => prepareEntry$1(entry));
543
- for (let i = 0; i < preparedEntries.length; i += 1) {
544
- for (let j = i + 1; j < preparedEntries.length; j += 1) {
545
- const a = preparedEntries[i];
546
- const b = preparedEntries[j];
547
- if (!bboxOverlaps(a, b)) continue;
548
- try {
549
- const hit = a.shape.intersect(b.shape);
550
- if (hit.isEmpty()) continue;
551
- const overlapVolume = hit.volume();
552
- if (!Number.isFinite(overlapVolume) || overlapVolume <= options.minOverlapVolume) continue;
553
- collisions.push({
554
- index: collisions.length + 1,
555
- id: collisionId(a, b),
556
- sourceIndex: i,
557
- targetIndex: j,
558
- sourceId: a.id,
559
- targetId: b.id,
560
- sourceName: a.name,
561
- targetName: b.name,
562
- overlapVolume,
563
- shape: hit
564
- });
565
- } catch (err) {
566
- const message = err instanceof Error ? err.message : String(err);
567
- warnings.push(`Could not boolean-test ${a.name} against ${b.name}: ${message}`);
568
- }
569
- }
570
- }
571
- const objects = preparedEntries.map((entry, index) => ({
572
- index,
573
- id: entry.id,
574
- name: entry.name,
575
- groupName: entry.groupName,
576
- treePath: entry.treePath,
577
- mock: entry.mock === true,
578
- bbox: {
579
- min: cloneVec3$1(entry.min),
580
- max: cloneVec3$1(entry.max)
581
- }
582
- }));
583
- return {
584
- method: "boolean-intersection",
585
- options,
586
- objectCount: objects.length,
587
- collisionCount: collisions.length,
588
- objects,
589
- collisions,
590
- warnings
591
- };
592
- }
593
- const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
594
- contactTolerance: 0.05,
595
- minOverlapVolume: 0.1,
596
- exactGeometry: false
597
- };
598
- const AXIS_NAMES = ["x", "y", "z"];
599
- class UnionFind {
600
- constructor(size) {
601
- __publicField(this, "parent");
602
- __publicField(this, "rank");
603
- this.parent = Array.from({ length: size }, (_, index) => index);
604
- this.rank = Array.from({ length: size }, () => 0);
605
- }
606
- find(value) {
607
- const parent = this.parent[value];
608
- if (parent === value) return value;
609
- const root = this.find(parent);
610
- this.parent[value] = root;
611
- return root;
612
- }
613
- union(a, b) {
614
- const rootA = this.find(a);
615
- const rootB = this.find(b);
616
- if (rootA === rootB) return;
617
- if (this.rank[rootA] < this.rank[rootB]) {
618
- this.parent[rootA] = rootB;
619
- return;
620
- }
621
- if (this.rank[rootA] > this.rank[rootB]) {
622
- this.parent[rootB] = rootA;
623
- return;
624
- }
625
- this.parent[rootB] = rootA;
626
- this.rank[rootA] += 1;
627
- }
628
- }
629
- function cloneVec3(value) {
630
- return [value[0], value[1], value[2]];
631
- }
632
- function emptyBBox() {
633
- return {
634
- min: [Infinity, Infinity, Infinity],
635
- max: [-Infinity, -Infinity, -Infinity]
636
- };
637
- }
638
- function expandBBox(target, min, max) {
639
- for (let axis = 0; axis < 3; axis += 1) {
640
- target.min[axis] = Math.min(target.min[axis], min[axis]);
641
- target.max[axis] = Math.max(target.max[axis], max[axis]);
642
- }
643
- }
644
- function intervalGap$1(aMin, aMax, bMin, bMax) {
645
- if (aMax < bMin) return bMin - aMax;
646
- if (bMax < aMin) return aMin - bMax;
647
- return 0;
648
- }
649
- function nearestBoundaryGap(a, b, axis) {
650
- return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
651
- }
652
- function bboxGaps(a, b) {
653
- return [
654
- intervalGap$1(a.min[0], a.max[0], b.min[0], b.max[0]),
655
- intervalGap$1(a.min[1], a.max[1], b.min[1], b.max[1]),
656
- intervalGap$1(a.min[2], a.max[2], b.min[2], b.max[2])
657
- ];
658
- }
659
- function maxGap(gaps) {
660
- return Math.max(gaps[0], gaps[1], gaps[2]);
661
- }
662
- function hasPositiveGap(gaps) {
663
- return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
664
- }
665
- function bboxInteriorOverlaps(a, b) {
666
- for (let axis = 0; axis < 3; axis += 1) {
667
- if (Math.min(a.max[axis], b.max[axis]) - Math.max(a.min[axis], b.min[axis]) <= 0) return false;
668
- }
669
- return true;
670
- }
671
- function bboxOverlapVolume(a, b) {
672
- let volume = 1;
673
- for (let axis = 0; axis < 3; axis += 1) {
674
- volume *= Math.max(0, Math.min(a.max[axis], b.max[axis]) - Math.max(a.min[axis], b.min[axis]));
675
- }
676
- return volume;
677
- }
678
- function estimateSweepPairCount(entries, axis, tolerance) {
679
- const ordered = entries.map((entry) => ({ min: entry.min[axis], max: entry.max[axis] })).sort((a, b) => a.min - b.min || a.max - b.max);
680
- const endValues = ordered.map((entry) => entry.max + tolerance).sort((a, b) => a - b);
681
- let expired = 0;
682
- let count = 0;
683
- for (let seen = 0; seen < ordered.length; seen += 1) {
684
- const currentMin = ordered[seen].min;
685
- while (expired < seen && endValues[expired] < currentMin) expired += 1;
686
- count += seen - expired;
687
- }
688
- return count;
689
- }
690
- function chooseSweepAxis(entries, tolerance) {
691
- let bestAxis = 0;
692
- let bestCount = estimateSweepPairCount(entries, bestAxis, tolerance);
693
- for (const axis of [1, 2]) {
694
- const count = estimateSweepPairCount(entries, axis, tolerance);
695
- if (count < bestCount) {
696
- bestAxis = axis;
697
- bestCount = count;
698
- }
699
- }
700
- return bestAxis;
701
- }
702
- function collectCandidatePairs(entries, tolerance) {
703
- if (entries.length < 2) return [];
704
- const axis = chooseSweepAxis(entries, tolerance);
705
- const ordered = entries.map((entry, index) => ({ entry, index })).sort((a, b) => a.entry.min[axis] - b.entry.min[axis] || a.entry.max[axis] - b.entry.max[axis] || a.index - b.index);
706
- let active = [];
707
- const pairs = [];
708
- for (const current of ordered) {
709
- active = active.filter((candidate) => candidate.entry.max[axis] + tolerance >= current.entry.min[axis]);
710
- for (const candidate of active) {
711
- const gaps = bboxGaps(candidate.entry, current.entry);
712
- if (maxGap(gaps) > tolerance) continue;
713
- const sourceIndex = Math.min(candidate.index, current.index);
714
- const targetIndex = Math.max(candidate.index, current.index);
715
- pairs.push({ sourceIndex, targetIndex, gaps });
716
- }
717
- active.push(current);
718
- }
719
- pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
720
- return pairs;
721
- }
722
- function contactFromBBoxes(a, b, tolerance) {
723
- const gaps = bboxGaps(a, b);
724
- const largestGap = maxGap(gaps);
725
- if (largestGap > tolerance) return { touching: false, gap: largestGap };
726
- const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
727
- if (separatedAxes.length > 0) {
728
- const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
729
- return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
730
- }
731
- const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
732
- axis,
733
- axisName,
734
- gap: nearestBoundaryGap(a, b, axis)
735
- })).filter((entry) => entry.gap <= tolerance);
736
- if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
737
- const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
738
- return { touching: true, gap: nearest.gap, axis: nearest.axisName };
739
- }
740
- function intersectionVolume(a, b) {
741
- try {
742
- const hit = a.shape.intersect(b.shape);
743
- if (hit.isEmpty()) return { volume: 0 };
744
- const volume = hit.volume();
745
- return { volume: Number.isFinite(volume) ? volume : 0 };
746
- } catch (err) {
747
- const message = err instanceof Error ? err.message : String(err);
748
- return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
749
- }
750
- }
751
- function bodyCountForEntry(entry) {
752
- if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
753
- return Math.max(0, Math.round(entry.bodyCount));
754
- }
755
- return 1;
756
- }
757
- function makeEdge(entries, sourceIndex, targetIndex, edge) {
758
- const source = entries[sourceIndex];
759
- const target = entries[targetIndex];
760
- return {
761
- sourceIndex,
762
- targetIndex,
763
- sourceId: source.id,
764
- targetId: target.id,
765
- sourceName: source.name,
766
- targetName: target.name,
767
- ...edge
768
- };
769
- }
770
- function analyzePhysicalConnectivity(entries, rawOptions = {}) {
771
- const options = {
772
- contactTolerance: rawOptions.contactTolerance ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance,
773
- minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.minOverlapVolume,
774
- exactGeometry: rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry
775
- };
776
- const warnings = [];
777
- const edges = [];
778
- const unionFind = new UnionFind(entries.length);
779
- for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
780
- const i = pair.sourceIndex;
781
- const j = pair.targetIndex;
782
- const a = entries[i];
783
- const b = entries[j];
784
- const bboxOverlaps2 = !hasPositiveGap(pair.gaps) && bboxInteriorOverlaps(a, b);
785
- if (options.exactGeometry && bboxOverlaps2) {
786
- const overlap = intersectionVolume(a, b);
787
- if (overlap.warning) warnings.push(overlap.warning);
788
- if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
789
- unionFind.union(i, j);
790
- edges.push(
791
- makeEdge(entries, i, j, {
792
- kind: "overlap",
793
- method: "boolean-intersection",
794
- gap: 0,
795
- overlapVolume: overlap.volume
796
- })
797
- );
798
- continue;
799
- }
800
- if (overlap.volume != null && overlap.volume > 0) {
801
- unionFind.union(i, j);
802
- edges.push(
803
- makeEdge(entries, i, j, {
804
- kind: "touching",
805
- method: "boolean-intersection",
806
- gap: 0,
807
- overlapVolume: overlap.volume
808
- })
809
- );
810
- continue;
811
- }
812
- }
813
- if (bboxOverlaps2) {
814
- unionFind.union(i, j);
815
- edges.push(
816
- makeEdge(entries, i, j, {
817
- kind: "overlap",
818
- method: "bbox-overlap",
819
- gap: 0,
820
- overlapVolume: bboxOverlapVolume(a, b)
821
- })
822
- );
823
- } else {
824
- const contact = contactFromBBoxes(a, b, options.contactTolerance);
825
- if (!contact.touching) continue;
826
- unionFind.union(i, j);
827
- edges.push(
828
- makeEdge(entries, i, j, {
829
- kind: "touching",
830
- method: "bbox-contact",
831
- gap: contact.gap,
832
- axis: contact.axis
833
- })
834
- );
835
- }
836
- }
837
- const objects = entries.map((entry, index) => ({
838
- index,
839
- id: entry.id,
840
- name: entry.name,
841
- groupName: entry.groupName,
842
- treePath: entry.treePath,
843
- mock: entry.mock === true,
844
- bodyCount: bodyCountForEntry(entry),
845
- bbox: {
846
- min: cloneVec3(entry.min),
847
- max: cloneVec3(entry.max)
848
- },
849
- componentIndex: 0
850
- }));
851
- const componentByRoot = /* @__PURE__ */ new Map();
852
- const rootToComponentIndex = /* @__PURE__ */ new Map();
853
- for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
854
- const root = unionFind.find(objectIndex);
855
- let component = componentByRoot.get(root);
856
- if (!component) {
857
- component = {
858
- index: componentByRoot.size + 1,
859
- objectIndexes: [],
860
- objectIds: [],
861
- objectNames: [],
862
- objectCount: 0,
863
- bodyCount: 0,
864
- bbox: emptyBBox()
865
- };
866
- componentByRoot.set(root, component);
867
- rootToComponentIndex.set(root, component.index);
868
- }
869
- const object = objects[objectIndex];
870
- object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
871
- component.objectIndexes.push(object.index);
872
- component.objectIds.push(object.id);
873
- component.objectNames.push(object.name);
874
- component.objectCount += 1;
875
- component.bodyCount += object.bodyCount;
876
- expandBBox(component.bbox, object.bbox.min, object.bbox.max);
877
- }
878
- const components = [...componentByRoot.values()];
879
- return {
880
- method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
881
- options,
882
- objectCount: objects.length,
883
- componentCount: components.length,
884
- objects,
885
- components,
886
- edges,
887
- warnings
888
- };
889
- }
890
- const EPSILON = 1e-9;
891
- function intervalGap(aMin, aMax, bMin, bMax) {
892
- if (aMax < bMin) return bMin - aMax;
893
- if (bMax < aMin) return aMin - bMax;
894
- return 0;
895
- }
896
- function bboxGap(a, b) {
897
- const axisGaps = [
898
- intervalGap(a.bbox.min[0], a.bbox.max[0], b.bbox.min[0], b.bbox.max[0]),
899
- intervalGap(a.bbox.min[1], a.bbox.max[1], b.bbox.min[1], b.bbox.max[1]),
900
- intervalGap(a.bbox.min[2], a.bbox.max[2], b.bbox.min[2], b.bbox.max[2])
901
- ];
902
- const gap = Math.sqrt(axisGaps[0] ** 2 + axisGaps[1] ** 2 + axisGaps[2] ** 2);
903
- return { gap, axisGaps };
904
- }
905
- function bboxVolume(component) {
906
- const dx = Math.max(0, component.bbox.max[0] - component.bbox.min[0]);
907
- const dy = Math.max(0, component.bbox.max[1] - component.bbox.min[1]);
908
- const dz = Math.max(0, component.bbox.max[2] - component.bbox.min[2]);
909
- return dx * dy * dz;
910
- }
911
- function compareDefaultRoot(a, b) {
912
- if (a.bodyCount !== b.bodyCount) return a.bodyCount - b.bodyCount;
913
- if (a.objectCount !== b.objectCount) return a.objectCount - b.objectCount;
914
- const volumeDelta = bboxVolume(a) - bboxVolume(b);
915
- if (Math.abs(volumeDelta) > EPSILON) return volumeDelta;
916
- return b.index - a.index;
917
- }
918
- function defaultRootComponentIndex(components) {
919
- if (components.length === 0) return null;
920
- return components.reduce((best, component) => compareDefaultRoot(component, best) > 0 ? component : best, components[0]).index;
921
- }
922
- function buildGapEdges(components) {
923
- const edges = [];
924
- for (let i = 0; i < components.length; i += 1) {
925
- for (let j = i + 1; j < components.length; j += 1) {
926
- const source = components[i];
927
- const target = components[j];
928
- const gap = bboxGap(source, target);
929
- if (!Number.isFinite(gap.gap)) continue;
930
- edges.push({
931
- sourceComponentIndex: source.index,
932
- targetComponentIndex: target.index,
933
- sourceObjectNames: [...source.objectNames],
934
- targetObjectNames: [...target.objectNames],
935
- gap: gap.gap,
936
- axisGaps: gap.axisGaps
937
- });
938
- }
939
- }
940
- return edges;
941
- }
942
- function componentPositionByIndex(components) {
943
- return new Map(components.map((component, position) => [component.index, position]));
944
- }
945
- function computeNearestComponents(components, gapEdges) {
946
- const nearest = components.map(() => ({ nearestGap: null, nearestComponentIndex: null }));
947
- const positions = componentPositionByIndex(components);
948
- for (const edge of gapEdges) {
949
- const sourcePosition = positions.get(edge.sourceComponentIndex);
950
- const targetPosition = positions.get(edge.targetComponentIndex);
951
- if (sourcePosition == null || targetPosition == null) continue;
952
- const source = nearest[sourcePosition];
953
- if (source.nearestGap == null || edge.gap < source.nearestGap - EPSILON || Math.abs(edge.gap - source.nearestGap) <= EPSILON && edge.targetComponentIndex < (source.nearestComponentIndex ?? Infinity)) {
954
- source.nearestGap = edge.gap;
955
- source.nearestComponentIndex = edge.targetComponentIndex;
956
- }
957
- const target = nearest[targetPosition];
958
- if (target.nearestGap == null || edge.gap < target.nearestGap - EPSILON || Math.abs(edge.gap - target.nearestGap) <= EPSILON && edge.sourceComponentIndex < (target.nearestComponentIndex ?? Infinity)) {
959
- target.nearestGap = edge.gap;
960
- target.nearestComponentIndex = edge.sourceComponentIndex;
961
- }
962
- }
963
- return nearest;
964
- }
965
- function computeRootDistances(components, gapEdges, rootComponentIndex) {
966
- if (rootComponentIndex == null) return [];
967
- const positions = componentPositionByIndex(components);
968
- const rootPosition = positions.get(rootComponentIndex);
969
- if (rootPosition == null) {
970
- throw new Error(`rootComponentIndex ${rootComponentIndex} does not match any physical component`);
971
- }
972
- const adjacency = components.map(() => []);
973
- for (const edge of gapEdges) {
974
- const sourcePosition = positions.get(edge.sourceComponentIndex);
975
- const targetPosition = positions.get(edge.targetComponentIndex);
976
- if (sourcePosition == null || targetPosition == null) continue;
977
- adjacency[sourcePosition].push({ to: targetPosition, gap: edge.gap });
978
- adjacency[targetPosition].push({ to: sourcePosition, gap: edge.gap });
979
- }
980
- const visited = components.map(() => false);
981
- const distances = components.map(() => Infinity);
982
- const parents = components.map(() => null);
983
- const parentGaps = components.map(() => null);
984
- distances[rootPosition] = 0;
985
- for (; ; ) {
986
- let current = -1;
987
- for (let i = 0; i < components.length; i += 1) {
988
- if (visited[i]) continue;
989
- if (current === -1 || distances[i] < distances[current] - EPSILON || Math.abs(distances[i] - distances[current]) <= EPSILON && components[i].index < components[current].index) {
990
- current = i;
991
- }
992
- }
993
- if (current === -1 || !Number.isFinite(distances[current])) break;
994
- visited[current] = true;
995
- for (const edge of adjacency[current]) {
996
- if (visited[edge.to]) continue;
997
- const nextDistance = distances[current] + edge.gap;
998
- if (nextDistance < distances[edge.to] - EPSILON || Math.abs(nextDistance - distances[edge.to]) <= EPSILON && components[current].index < (parents[edge.to] ?? Infinity)) {
999
- distances[edge.to] = nextDistance;
1000
- parents[edge.to] = components[current].index;
1001
- parentGaps[edge.to] = edge.gap;
1002
- }
1003
- }
1004
- }
1005
- return components.map((_, position) => ({
1006
- rootDistance: distances[position],
1007
- parentComponentIndex: parents[position],
1008
- parentGap: parentGaps[position]
1009
- }));
1010
- }
1011
- function analyzeDistanceInspection(entries, rawOptions = {}) {
1012
- const connectivity = analyzePhysicalConnectivity(entries, rawOptions);
1013
- const rootComponentIndex = rawOptions.rootComponentIndex ?? defaultRootComponentIndex(connectivity.components);
1014
- const gapEdges = buildGapEdges(connectivity.components);
1015
- const nearest = computeNearestComponents(connectivity.components, gapEdges);
1016
- const rooted = computeRootDistances(connectivity.components, gapEdges, rootComponentIndex);
1017
- const componentByIndex = /* @__PURE__ */ new Map();
1018
- const components = connectivity.components.map((component, position) => {
1019
- var _a, _b;
1020
- const rootData = rooted[position] ?? {
1021
- rootDistance: rootComponentIndex === component.index ? 0 : Infinity,
1022
- parentComponentIndex: null,
1023
- parentGap: null
1024
- };
1025
- const decorated = {
1026
- ...component,
1027
- isRoot: component.index === rootComponentIndex,
1028
- rootDistance: rootData.rootDistance,
1029
- nearestGap: ((_a = nearest[position]) == null ? void 0 : _a.nearestGap) ?? null,
1030
- nearestComponentIndex: ((_b = nearest[position]) == null ? void 0 : _b.nearestComponentIndex) ?? null,
1031
- parentComponentIndex: rootData.parentComponentIndex,
1032
- parentGap: rootData.parentGap
1033
- };
1034
- componentByIndex.set(component.index, decorated);
1035
- return decorated;
1036
- });
1037
- const objects = connectivity.objects.map((object) => {
1038
- const component = componentByIndex.get(object.componentIndex);
1039
- return {
1040
- ...object,
1041
- rootDistance: (component == null ? void 0 : component.rootDistance) ?? Infinity,
1042
- nearestGap: (component == null ? void 0 : component.nearestGap) ?? null,
1043
- nearestComponentIndex: (component == null ? void 0 : component.nearestComponentIndex) ?? null,
1044
- parentComponentIndex: (component == null ? void 0 : component.parentComponentIndex) ?? null,
1045
- parentGap: (component == null ? void 0 : component.parentGap) ?? null
1046
- };
1047
- });
1048
- const finiteDistances = components.map((component) => component.rootDistance).filter(Number.isFinite);
1049
- const maxRootDistance = finiteDistances.length > 0 ? Math.max(...finiteDistances) : 0;
1050
- return {
1051
- method: "physical-component-bbox-gap-graph",
1052
- distanceMethod: "axis-aligned-bbox-gap",
1053
- options: {
1054
- contactTolerance: connectivity.options.contactTolerance,
1055
- minOverlapVolume: connectivity.options.minOverlapVolume,
1056
- rootComponentIndex
1057
- },
1058
- objectCount: connectivity.objectCount,
1059
- componentCount: connectivity.componentCount,
1060
- rootComponentIndex,
1061
- maxRootDistance,
1062
- objects,
1063
- components,
1064
- gapEdges,
1065
- connectivity: {
1066
- method: connectivity.method,
1067
- edges: connectivity.edges
1068
- },
1069
- warnings: [...connectivity.warnings]
1070
- };
1071
- }
1072
457
  const CAMERA_TOKEN_DIRECTIONS = {
1073
458
  front: [0, -1, 0.2],
1074
459
  back: [0, 1, 0.2],
@@ -1204,374 +589,6 @@ ${body}
1204
589
  pathCount
1205
590
  };
1206
591
  }
1207
- const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
1208
- minThickness: 1.2,
1209
- warnThickness: 2,
1210
- maxThickness: 6,
1211
- maxSamplesPerObject: 5e3
1212
- };
1213
- const THICKNESS_COLORS = {
1214
- critical: [255, 28, 28],
1215
- warning: [255, 150, 0],
1216
- ok: [60, 220, 90],
1217
- thick: [70, 145, 255],
1218
- unknown: [90, 90, 90]
1219
- };
1220
- function finitePositive(value, fallback, label) {
1221
- if (value === void 0) return fallback;
1222
- if (!Number.isFinite(value) || value <= 0) {
1223
- throw new Error(`${label} must be a positive finite number.`);
1224
- }
1225
- return value;
1226
- }
1227
- function resolveThicknessInspectionOptions(raw = {}) {
1228
- const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
1229
- const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
1230
- const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
1231
- const maxSamplesPerObject = finitePositive(
1232
- raw.maxSamplesPerObject,
1233
- DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
1234
- "maxSamplesPerObject"
1235
- );
1236
- if (minThickness > warnThickness) {
1237
- throw new Error("minThickness must be less than or equal to warnThickness.");
1238
- }
1239
- if (warnThickness > maxThickness) {
1240
- throw new Error("warnThickness must be less than or equal to maxThickness.");
1241
- }
1242
- return {
1243
- minThickness,
1244
- warnThickness,
1245
- maxThickness,
1246
- maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
1247
- };
1248
- }
1249
- function lerp(a, b, t) {
1250
- return a + (b - a) * Math.max(0, Math.min(1, t));
1251
- }
1252
- function lerpColor$1(a, b, t) {
1253
- return [Math.round(lerp(a[0], b[0], t)), Math.round(lerp(a[1], b[1], t)), Math.round(lerp(a[2], b[2], t))];
1254
- }
1255
- function thicknessClass(thickness, options) {
1256
- if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
1257
- if (thickness <= options.minThickness) return "critical";
1258
- if (thickness <= options.warnThickness) return "warning";
1259
- if (thickness <= options.maxThickness) return "ok";
1260
- return "thick";
1261
- }
1262
- function thicknessColor(thickness, options) {
1263
- const cls = thicknessClass(thickness, options);
1264
- if (cls === "unknown") return THICKNESS_COLORS.unknown;
1265
- if (cls === "critical") return THICKNESS_COLORS.critical;
1266
- if (cls === "warning") {
1267
- const span = Math.max(1e-9, options.warnThickness - options.minThickness);
1268
- return lerpColor$1(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
1269
- }
1270
- if (cls === "ok") {
1271
- const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
1272
- return lerpColor$1(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
1273
- }
1274
- return THICKNESS_COLORS.thick;
1275
- }
1276
- function sampleArea(sample) {
1277
- const area = sample.area ?? 1;
1278
- return Number.isFinite(area) && area > 0 ? area : 1;
1279
- }
1280
- function weightedQuantile(samples, q) {
1281
- if (samples.length === 0) return null;
1282
- const sorted = [...samples].sort((a, b) => a.thickness - b.thickness);
1283
- const totalArea = sorted.reduce((sum, sample) => sum + sample.area, 0);
1284
- const target = totalArea * Math.max(0, Math.min(1, q));
1285
- let cumulative = 0;
1286
- for (const sample of sorted) {
1287
- cumulative += sample.area;
1288
- if (cumulative >= target) return sample.thickness;
1289
- }
1290
- return sorted[sorted.length - 1].thickness;
1291
- }
1292
- function percent(part, total) {
1293
- if (total <= 0) return 0;
1294
- return part / total * 100;
1295
- }
1296
- function summarizeThicknessSamples(samples, options) {
1297
- const resolved = [];
1298
- let totalArea = 0;
1299
- let resolvedArea = 0;
1300
- let unresolvedArea = 0;
1301
- let criticalArea = 0;
1302
- let warningArea = 0;
1303
- let weightedSum = 0;
1304
- for (const sample of samples) {
1305
- const area = sampleArea(sample);
1306
- totalArea += area;
1307
- const value = sample.thickness;
1308
- if (value == null || !Number.isFinite(value) || value <= 0) {
1309
- unresolvedArea += area;
1310
- continue;
1311
- }
1312
- resolved.push({ thickness: value, area });
1313
- resolvedArea += area;
1314
- weightedSum += value * area;
1315
- if (value <= options.minThickness) {
1316
- criticalArea += area;
1317
- } else if (value <= options.warnThickness) {
1318
- warningArea += area;
1319
- }
1320
- }
1321
- const values = resolved.map((sample) => sample.thickness);
1322
- return {
1323
- sampleCount: samples.length,
1324
- resolvedCount: resolved.length,
1325
- unresolvedCount: samples.length - resolved.length,
1326
- minThickness: values.length > 0 ? Math.min(...values) : null,
1327
- p05Thickness: weightedQuantile(resolved, 0.05),
1328
- medianThickness: weightedQuantile(resolved, 0.5),
1329
- meanThickness: resolvedArea > 0 ? weightedSum / resolvedArea : null,
1330
- maxThickness: values.length > 0 ? Math.max(...values) : null,
1331
- criticalAreaPercent: percent(criticalArea, resolvedArea),
1332
- warningAreaPercent: percent(warningArea, resolvedArea),
1333
- belowWarnAreaPercent: percent(criticalArea + warningArea, resolvedArea),
1334
- unresolvedAreaPercent: percent(unresolvedArea, totalArea)
1335
- };
1336
- }
1337
- const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1338
- smoothAngleDeg: 5,
1339
- sharpAngleDeg: 30,
1340
- harshAngleDeg: 90
1341
- };
1342
- const ROUGHNESS_COLORS = {
1343
- smooth: [62, 72, 84],
1344
- moderate: [255, 214, 0],
1345
- sharp: [255, 124, 34],
1346
- harsh: [255, 42, 96]
1347
- };
1348
- function resolveRoughnessInspectionOptions(raw = {}) {
1349
- const options = {
1350
- smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
1351
- sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
1352
- harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg
1353
- };
1354
- if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
1355
- throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
1356
- }
1357
- if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
1358
- throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
1359
- }
1360
- if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
1361
- throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
1362
- }
1363
- return options;
1364
- }
1365
- function roughnessClassForAngle(angleDeg, options) {
1366
- if (angleDeg >= options.harshAngleDeg) return "harsh";
1367
- if (angleDeg >= options.sharpAngleDeg) return "sharp";
1368
- if (angleDeg >= options.smoothAngleDeg) return "moderate";
1369
- return "smooth";
1370
- }
1371
- function roughnessScoreForAngle(angleDeg, options) {
1372
- if (angleDeg < options.sharpAngleDeg) return 0;
1373
- if (angleDeg < options.harshAngleDeg) {
1374
- return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
1375
- }
1376
- return 1;
1377
- }
1378
- function roughnessColorForAngle(angleDeg, options) {
1379
- const cls = roughnessClassForAngle(angleDeg, options);
1380
- if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
1381
- if (cls === "moderate") {
1382
- return lerpRgb(
1383
- ROUGHNESS_COLORS.moderate,
1384
- ROUGHNESS_COLORS.sharp,
1385
- (angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
1386
- );
1387
- }
1388
- return lerpRgb(
1389
- ROUGHNESS_COLORS.sharp,
1390
- ROUGHNESS_COLORS.harsh,
1391
- (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
1392
- );
1393
- }
1394
- function lerpRgb(a, b, t) {
1395
- const clamped = MathUtils.clamp(t, 0, 1);
1396
- return [
1397
- Math.round(MathUtils.lerp(a[0], b[0], clamped)),
1398
- Math.round(MathUtils.lerp(a[1], b[1], clamped)),
1399
- Math.round(MathUtils.lerp(a[2], b[2], clamped))
1400
- ];
1401
- }
1402
- function emptyRoughnessSummary() {
1403
- return {
1404
- triangleCount: 0,
1405
- edgeCount: 0,
1406
- boundaryEdgeCount: 0,
1407
- nonManifoldEdgeCount: 0,
1408
- smoothAreaPercent: 0,
1409
- moderateAreaPercent: 0,
1410
- sharpAreaPercent: 0,
1411
- harshAreaPercent: 0,
1412
- roughAreaPercent: 0,
1413
- meanAngleDeg: null,
1414
- p50AngleDeg: null,
1415
- p90AngleDeg: null,
1416
- p95AngleDeg: null,
1417
- p99AngleDeg: null,
1418
- maxAngleDeg: null,
1419
- qualityScore: 0
1420
- };
1421
- }
1422
- function summarizeRoughnessTriangles(triangles, edgeAngles, edgeCount, boundaryEdgeCount, nonManifoldEdgeCount, options) {
1423
- const areaByClass = {
1424
- smooth: 0,
1425
- moderate: 0,
1426
- sharp: 0,
1427
- harsh: 0
1428
- };
1429
- let totalArea = 0;
1430
- for (const tri of triangles) {
1431
- const area = Number.isFinite(tri.area) ? tri.area : 0;
1432
- totalArea += area;
1433
- areaByClass[roughnessClassForAngle(tri.maxAngleDeg, options)] += area;
1434
- }
1435
- const sortedAngles = [...edgeAngles].sort((lhs, rhs) => lhs - rhs);
1436
- const meanAngleDeg = sortedAngles.length > 0 ? Number((sortedAngles.reduce((sum, angle) => sum + angle, 0) / sortedAngles.length).toFixed(2)) : null;
1437
- const qualityScore = totalArea > 0 ? Math.round(
1438
- MathUtils.clamp(
1439
- 100 * (areaByClass.smooth + areaByClass.moderate * 0.9) / totalArea - 50 * areaByClass.harsh / totalArea,
1440
- 0,
1441
- 100
1442
- )
1443
- ) : 0;
1444
- const safePercent = (value) => totalArea > 0 ? Number((value / totalArea * 100).toFixed(2)) : 0;
1445
- return {
1446
- triangleCount: triangles.length,
1447
- edgeCount,
1448
- boundaryEdgeCount,
1449
- nonManifoldEdgeCount,
1450
- smoothAreaPercent: safePercent(areaByClass.smooth),
1451
- moderateAreaPercent: safePercent(areaByClass.moderate),
1452
- sharpAreaPercent: safePercent(areaByClass.sharp),
1453
- harshAreaPercent: safePercent(areaByClass.harsh),
1454
- roughAreaPercent: safePercent(areaByClass.sharp + areaByClass.harsh),
1455
- meanAngleDeg,
1456
- p50AngleDeg: percentile(sortedAngles, 0.5),
1457
- p90AngleDeg: percentile(sortedAngles, 0.9),
1458
- p95AngleDeg: percentile(sortedAngles, 0.95),
1459
- p99AngleDeg: percentile(sortedAngles, 0.99),
1460
- maxAngleDeg: sortedAngles.length > 0 ? Number(sortedAngles[sortedAngles.length - 1].toFixed(2)) : null,
1461
- qualityScore
1462
- };
1463
- }
1464
- function percentile(sorted, q) {
1465
- if (sorted.length === 0) return null;
1466
- const index = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
1467
- return Number(sorted[index].toFixed(2));
1468
- }
1469
- const DEG_PER_RAD = 180 / Math.PI;
1470
- function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1471
- const options = resolveRoughnessInspectionOptions(rawOptions);
1472
- const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
1473
- const position = geometry.getAttribute("position");
1474
- const warnings = [];
1475
- if (!position || position.count < 3) {
1476
- return { geometry, summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1477
- }
1478
- const triangleCount = Math.floor(position.count / 3);
1479
- const normals = new Array(triangleCount);
1480
- const triangles = new Array(triangleCount);
1481
- const edges = /* @__PURE__ */ new Map();
1482
- const colors = new Float32Array(position.count * 3);
1483
- const scores = new Float32Array(position.count);
1484
- const a = new Vector3();
1485
- const b = new Vector3();
1486
- const c = new Vector3();
1487
- const ac = new Vector3();
1488
- const normal = new Vector3();
1489
- const bbox = new Box3().setFromBufferAttribute(position);
1490
- const snap = Math.max(1e-6, bbox.getSize(new Vector3()).length() * 1e-8);
1491
- for (let tri = 0; tri < triangleCount; tri += 1) {
1492
- const offset = tri * 3;
1493
- readVertex(position, offset, a);
1494
- readVertex(position, offset + 1, b);
1495
- readVertex(position, offset + 2, c);
1496
- normal.subVectors(b, a).cross(ac.subVectors(c, a));
1497
- const areaTwice = normal.length();
1498
- triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
1499
- normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
1500
- const keys = [vertexKey(a, snap), vertexKey(b, snap), vertexKey(c, snap)];
1501
- for (let edge = 0; edge < 3; edge += 1) {
1502
- const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
1503
- let record = edges.get(key);
1504
- if (!record) {
1505
- record = { triangles: [] };
1506
- edges.set(key, record);
1507
- }
1508
- record.triangles.push(tri);
1509
- }
1510
- }
1511
- const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals);
1512
- if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
1513
- if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
1514
- for (let tri = 0; tri < triangleCount; tri += 1) {
1515
- const { maxAngleDeg } = triangles[tri];
1516
- const color = roughnessColorForAngle(maxAngleDeg, options);
1517
- const score = roughnessScoreForAngle(maxAngleDeg, options);
1518
- const offset = tri * 3;
1519
- for (let vertex = 0; vertex < 3; vertex += 1) {
1520
- const colorOffset = (offset + vertex) * 3;
1521
- colors[colorOffset] = color[0] / 255;
1522
- colors[colorOffset + 1] = color[1] / 255;
1523
- colors[colorOffset + 2] = color[2] / 255;
1524
- scores[offset + vertex] = score;
1525
- }
1526
- }
1527
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
1528
- geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
1529
- geometry.computeBoundingBox();
1530
- return {
1531
- geometry,
1532
- summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
1533
- warnings
1534
- };
1535
- }
1536
- function markTriangleRoughness(edges, triangles, normals) {
1537
- const edgeAngles = [];
1538
- let boundaryEdgeCount = 0;
1539
- let nonManifoldEdgeCount = 0;
1540
- for (const edge of edges.values()) {
1541
- if (edge.triangles.length === 1) {
1542
- boundaryEdgeCount += 1;
1543
- markTriangles(edge.triangles, triangles, 180);
1544
- edgeAngles.push(180);
1545
- continue;
1546
- }
1547
- if (edge.triangles.length > 2) {
1548
- nonManifoldEdgeCount += 1;
1549
- markTriangles(edge.triangles, triangles, 180);
1550
- edgeAngles.push(180);
1551
- continue;
1552
- }
1553
- const [first, second] = edge.triangles;
1554
- const dot = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
1555
- const angleDeg = Math.acos(dot) * DEG_PER_RAD;
1556
- markTriangles(edge.triangles, triangles, angleDeg);
1557
- edgeAngles.push(angleDeg);
1558
- }
1559
- return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
1560
- }
1561
- function readVertex(position, index, target) {
1562
- target.set(position.getX(index), position.getY(index), position.getZ(index));
1563
- }
1564
- function vertexKey(point, snap) {
1565
- return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
1566
- }
1567
- function edgeKey(a, b) {
1568
- return a < b ? `${a}|${b}` : `${b}|${a}`;
1569
- }
1570
- function markTriangles(indices, triangles, angleDeg) {
1571
- for (const index of indices) {
1572
- triangles[index].maxAngleDeg = Math.max(triangles[index].maxAngleDeg, angleDeg);
1573
- }
1574
- }
1575
592
  const canvas = document.getElementById("canvas");
1576
593
  const exportCanvas = document.createElement("canvas");
1577
594
  const exportCtx = exportCanvas.getContext("2d");
@@ -2021,92 +1038,6 @@ function distanceColorForRootDistance(distance, maxDistance) {
2021
1038
  if (t <= 0.5) return lerpColor(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2);
2022
1039
  return lerpColor(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2);
2023
1040
  }
2024
- function cloneGeometryForFaceColors(geometry) {
2025
- return geometry.index ? geometry.toNonIndexed() : geometry.clone();
2026
- }
2027
- function geometryMaxDimension(geometry) {
2028
- geometry.computeBoundingBox();
2029
- const box = geometry.boundingBox;
2030
- if (!box) return 1;
2031
- const size = new Vector3();
2032
- box.getSize(size);
2033
- return Math.max(1, size.x, size.y, size.z);
2034
- }
2035
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
2036
- const origin = point.clone().addScaledVector(direction, epsilon);
2037
- raycaster.set(origin, direction);
2038
- raycaster.near = epsilon;
2039
- raycaster.far = far;
2040
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
2041
- return hit ? hit.distance + epsilon : null;
2042
- }
2043
- function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
2044
- const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
2045
- const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
2046
- if (forward == null) return backward;
2047
- if (backward == null) return forward;
2048
- return Math.min(forward, backward);
2049
- }
2050
- function analyzeThicknessGeometry(sourceGeometry, options) {
2051
- const geometry = cloneGeometryForFaceColors(sourceGeometry);
2052
- const position = geometry.getAttribute("position");
2053
- if (!position || position.count < 3) {
2054
- return { geometry, samples: [], triangleCount: 0, sampledTriangleCount: 0, sampleStride: 1, warnings: ["No triangle geometry."] };
2055
- }
2056
- const triangleCount = Math.floor(position.count / 3);
2057
- const sampleStride = Math.max(1, Math.ceil(triangleCount / options.maxSamplesPerObject));
2058
- const maxDim = geometryMaxDimension(geometry);
2059
- const epsilon = Math.max(1e-4, maxDim * 1e-6);
2060
- const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
2061
- const colors = new Float32Array(position.count * 3);
2062
- const samples = [];
2063
- const warnings = [];
2064
- const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
2065
- const rayMesh = new Mesh(geometry, rayMaterial);
2066
- const raycaster = new Raycaster();
2067
- if (sampleStride > 1) {
2068
- warnings.push(`Triangle sampling stride ${sampleStride}; increase --thickness-samples for denser analysis.`);
2069
- }
2070
- const a = new Vector3();
2071
- const b = new Vector3();
2072
- const c = new Vector3();
2073
- const normal = new Vector3();
2074
- const centroid = new Vector3();
2075
- let sampledTriangleCount = 0;
2076
- let lastThickness = null;
2077
- for (let tri = 0; tri < triangleCount; tri += 1) {
2078
- const offset = tri * 3;
2079
- a.fromBufferAttribute(position, offset);
2080
- b.fromBufferAttribute(position, offset + 1);
2081
- c.fromBufferAttribute(position, offset + 2);
2082
- normal.subVectors(b, a).cross(new Vector3().subVectors(c, a));
2083
- const areaTwice = normal.length();
2084
- const area = areaTwice * 0.5;
2085
- let thickness = lastThickness;
2086
- if (tri % sampleStride === 0) {
2087
- sampledTriangleCount += 1;
2088
- if (areaTwice <= 1e-12) {
2089
- thickness = null;
2090
- } else {
2091
- normal.multiplyScalar(1 / areaTwice);
2092
- centroid.copy(a).add(b).add(c).multiplyScalar(1 / 3);
2093
- thickness = triangleThickness(raycaster, rayMesh, centroid, normal, epsilon, far);
2094
- }
2095
- lastThickness = thickness;
2096
- samples.push({ thickness, area });
2097
- }
2098
- const color = thicknessColor(thickness, options);
2099
- for (let vertex = 0; vertex < 3; vertex += 1) {
2100
- const colorOffset = (offset + vertex) * 3;
2101
- colors[colorOffset] = color[0] / 255;
2102
- colors[colorOffset + 1] = color[1] / 255;
2103
- colors[colorOffset + 2] = color[2] / 255;
2104
- }
2105
- }
2106
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
2107
- rayMaterial.dispose();
2108
- return { geometry, samples, triangleCount, sampledTriangleCount, sampleStride, warnings };
2109
- }
2110
1041
  function buildMaskEntries(session) {
2111
1042
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2112
1043
  return session.renderables.map((renderable, idx) => {
@@ -2196,6 +1127,7 @@ function decorateConnectivityReport(report) {
2196
1127
  return {
2197
1128
  method: report.method,
2198
1129
  options: report.options,
1130
+ broadphase: report.broadphase,
2199
1131
  objectCount: report.objectCount,
2200
1132
  componentCount: report.componentCount,
2201
1133
  objects,
@@ -2284,6 +1216,7 @@ function decorateDistanceReport(report) {
2284
1216
  maxRootDistance: report.maxRootDistance,
2285
1217
  objects,
2286
1218
  components,
1219
+ gapEdgeCount: report.gapEdgeCount,
2287
1220
  gapEdges: report.gapEdges,
2288
1221
  connectivity: report.connectivity,
2289
1222
  warnings: report.warnings,
@@ -3610,11 +2543,7 @@ function createSession(code, opts) {
3610
2543
  requestedSceneState = mergeViewportRenderSceneStates(requestedSceneState, {
3611
2544
  camera: {
3612
2545
  projectionMode: "perspective",
3613
- position: [
3614
- center.x + dir[0] * tokenDistance,
3615
- center.y + dir[1] * tokenDistance,
3616
- center.z + dir[2] * tokenDistance
3617
- ],
2546
+ position: [center.x + dir[0] * tokenDistance, center.y + dir[1] * tokenDistance, center.z + dir[2] * tokenDistance],
3618
2547
  target: [center.x, center.y, center.z],
3619
2548
  up: [0, 0, 1],
3620
2549
  fov: cameraFov