forgecad 0.9.4 → 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 (90) hide show
  1. package/dist/assets/{AdminPage-jwoEgwE_.js → AdminPage-Da6hhpJx.js} +1 -1
  2. package/dist/assets/{BlogPage-Ck7g3ue2.js → BlogPage-Bl_sKeWb.js} +1 -1
  3. package/dist/assets/{DocsPage-9WaRC14b.js → DocsPage-Blz3Tp4j.js} +1 -6
  4. package/dist/assets/EditorApp-CuiPbtn5.js +12754 -0
  5. package/dist/assets/{EmbedViewer-37_PfMwv.js → EmbedViewer-BFG6-Ufm.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CO8WL0CY.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
  7. package/dist/assets/{PricingPage-DADKGuOa.js → PricingPage-BMxYT_F0.js} +1 -1
  8. package/dist/assets/{SettingsPage-DKKI4W49.js → SettingsPage-VVQNrCAg.js} +1 -1
  9. package/dist/assets/{app-CwI02pTA.js → app-Dl9ymBWC.js} +355 -36
  10. package/dist/assets/cli/{render-Kw5hLEcL.js → render-CFtwKCCY.js} +203 -862
  11. package/dist/assets/{sectionPlaneMath-C8N0w8o3.js → distance-BEC2RjJi.js} +4150 -801
  12. package/dist/assets/{evalWorker-D6ub3kfS.js → evalWorker-CRvbzTXm.js} +2611 -528
  13. package/dist/assets/{manifold-CwDdMKyc.js → manifold-B9QSr-qP.js} +1 -1
  14. package/dist/assets/{manifold-DTvmxSDf.js → manifold-DpBXFS2K.js} +1 -1
  15. package/dist/assets/{manifold-lru0jwVw.js → manifold-DzZ4VRPs.js} +2 -2
  16. package/dist/assets/{renderSceneState-tvtNKNRi.js → renderSceneState-BuAXF2jh.js} +1 -1
  17. package/dist/assets/{reportWorker-DeqktDGt.js → reportWorker-BNWEnRg1.js} +2606 -525
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +2 -2
  20. package/dist/docs-raw/AI/usage.md +0 -1
  21. package/dist/docs-raw/API/core/concepts.md +11 -1
  22. package/dist/docs-raw/CLI.md +64 -13
  23. package/dist/docs-raw/beta-operations.md +4 -0
  24. package/dist/docs-raw/deployment.md +38 -23
  25. package/dist/docs-raw/generated/assembly.md +8 -3
  26. package/dist/docs-raw/generated/concepts.md +126 -46
  27. package/dist/docs-raw/generated/core.md +97 -47
  28. package/dist/docs-raw/generated/curves.md +113 -595
  29. package/dist/docs-raw/generated/lib.md +40 -3
  30. package/dist/docs-raw/generated/output.md +6 -1
  31. package/dist/docs-raw/generated/sdf.md +50 -4
  32. package/dist/docs-raw/generated/sketch.md +9 -1
  33. package/dist/docs-raw/generated/viewport.md +1 -9
  34. package/dist/docs-raw/guides/inspection-bundles.md +40 -9
  35. package/dist/docs-raw/runbook.md +3 -3
  36. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -0
  37. package/dist/docs-raw/skills/forgecad-image-replicator.md +3 -1
  38. package/dist/docs-raw/skills/forgecad-make-a-model.md +48 -4
  39. package/dist/docs-raw/skills/forgecad-render-inspect.md +3 -1
  40. package/dist/docs-raw/skills/forgecad-visual-spec.md +2 -0
  41. package/dist/docs-raw/skills/forgecad.md +2 -1
  42. package/dist/docs-raw/skills/index.md +0 -1
  43. package/dist/index.html +1 -1
  44. package/dist/sitemap.xml +6 -6
  45. package/dist-cli/blender/render.py +43 -8
  46. package/dist-cli/forgecad.js +5729 -2015
  47. package/dist-cli/forgecad.js.map +1 -1
  48. package/dist-skill/CONTEXT.md +372 -667
  49. package/dist-skill/SKILL-dev.md +2 -1
  50. package/dist-skill/SKILL.md +2 -1
  51. package/dist-skill/docs/API/core/concepts.md +11 -1
  52. package/dist-skill/docs/CLI.md +64 -13
  53. package/dist-skill/docs/generated/assembly.md +8 -3
  54. package/dist-skill/docs/generated/core.md +97 -47
  55. package/dist-skill/docs/generated/curves.md +113 -595
  56. package/dist-skill/docs/generated/lib.md +40 -3
  57. package/dist-skill/docs/generated/output.md +6 -1
  58. package/dist-skill/docs/generated/sdf.md +50 -4
  59. package/dist-skill/docs/generated/sketch.md +9 -1
  60. package/dist-skill/docs/generated/viewport.md +1 -9
  61. package/dist-skill/docs/guides/inspection-bundles.md +40 -9
  62. package/dist-skill/docs-dev/API/core/concepts.md +11 -1
  63. package/dist-skill/docs-dev/CLI.md +64 -13
  64. package/dist-skill/docs-dev/generated/assembly.md +8 -3
  65. package/dist-skill/docs-dev/generated/core.md +97 -47
  66. package/dist-skill/docs-dev/generated/curves.md +113 -595
  67. package/dist-skill/docs-dev/generated/lib.md +40 -3
  68. package/dist-skill/docs-dev/generated/output.md +6 -1
  69. package/dist-skill/docs-dev/generated/sdf.md +50 -4
  70. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  71. package/dist-skill/docs-dev/generated/viewport.md +1 -9
  72. package/dist-skill/docs-dev/guides/inspection-bundles.md +40 -9
  73. package/dist-skill/library/README.md +0 -1
  74. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -0
  75. package/dist-skill/library/forgecad-image-replicator/SKILL.md +3 -1
  76. package/dist-skill/library/forgecad-make-a-model/SKILL.md +48 -4
  77. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  78. package/dist-skill/library/forgecad-visual-spec/SKILL.md +2 -0
  79. package/examples/api/drive-wheel-regions.forge.js +43 -0
  80. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  81. package/examples/api/sdf-circular-array-knurling.forge.js +19 -0
  82. package/examples/api/sdf-pattern2d-ceramic-ripple-set.forge.js +83 -0
  83. package/examples/api/sdf-pattern2d-grip-tread.forge.js +72 -0
  84. package/examples/api/sdf-pattern2d-orbital-jewelry.forge.js +62 -0
  85. package/examples/api/sdf-surface-basket-weave.forge.js +67 -0
  86. package/examples/api/sector-gear-body.forge.js +34 -0
  87. package/package.json +20 -2
  88. package/dist/assets/EditorApp-Dja2jMmW.js +0 -12509
  89. package/dist/docs-raw/skills/forgecad-api-dogfood.md +0 -130
  90. package/dist-skill/library/forgecad-api-dogfood/SKILL.md +0 -125
@@ -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, g as Vector3, e as Color, aC as resolveForgeRenderStyle, b9 as getRenderStylePreset, ax as setParamOverrides, b6 as runScript, a0 as MathUtils, G as Box3, 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, aU as BufferAttribute, bP as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-C8N0w8o3.js";
5
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-tvtNKNRi.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,
@@ -457,617 +454,37 @@ 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
457
+ const CAMERA_TOKEN_DIRECTIONS = {
458
+ front: [0, -1, 0.2],
459
+ back: [0, 1, 0.2],
460
+ side: [1, 0, 0.2],
461
+ right: [1, 0, 0.2],
462
+ top: [0, -0.01, 1],
463
+ iso: [0.6, -0.6, 0.4]
597
464
  };
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
- };
465
+ function normalizeCameraDirection(dir) {
466
+ const len = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2) || 1;
467
+ return [dir[0] / len, dir[1] / len, dir[2] / len];
769
468
  }
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
- };
469
+ function sphericalToCameraDirection(azimuthDeg, elevationDeg) {
470
+ const az = azimuthDeg * Math.PI / 180;
471
+ const el = elevationDeg * Math.PI / 180;
472
+ const cosEl = Math.cos(el);
473
+ return [cosEl * Math.sin(az), -cosEl * Math.cos(az), Math.sin(el)];
889
474
  }
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
- }
475
+ function parseCameraToken(token) {
476
+ const normalized = token.trim();
477
+ const preset = CAMERA_TOKEN_DIRECTIONS[normalized];
478
+ if (preset) return { label: normalized, dir: preset };
479
+ const parts = normalized.split(":").map((s) => Number.parseFloat(s));
480
+ if (parts.length >= 2 && parts.length <= 3 && parts.every((n) => Number.isFinite(n))) {
481
+ const dir = sphericalToCameraDirection(parts[0], parts[1]);
482
+ const label = `az${parts[0]}_el${parts[1]}`;
483
+ return { label, dir, distance: parts[2] };
1004
484
  }
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
- };
485
+ throw new Error(
486
+ `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
487
+ );
1071
488
  }
1072
489
  function formatAvailableViews(views) {
1073
490
  const names = Object.keys(views ?? {}).sort();
@@ -1172,136 +589,6 @@ ${body}
1172
589
  pathCount
1173
590
  };
1174
591
  }
1175
- const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
1176
- minThickness: 1.2,
1177
- warnThickness: 2,
1178
- maxThickness: 6,
1179
- maxSamplesPerObject: 5e3
1180
- };
1181
- const THICKNESS_COLORS = {
1182
- critical: [255, 28, 28],
1183
- warning: [255, 150, 0],
1184
- ok: [60, 220, 90],
1185
- thick: [70, 145, 255],
1186
- unknown: [90, 90, 90]
1187
- };
1188
- function finitePositive(value, fallback, label) {
1189
- if (value === void 0) return fallback;
1190
- if (!Number.isFinite(value) || value <= 0) {
1191
- throw new Error(`${label} must be a positive finite number.`);
1192
- }
1193
- return value;
1194
- }
1195
- function resolveThicknessInspectionOptions(raw = {}) {
1196
- const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
1197
- const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
1198
- const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
1199
- const maxSamplesPerObject = finitePositive(
1200
- raw.maxSamplesPerObject,
1201
- DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
1202
- "maxSamplesPerObject"
1203
- );
1204
- if (minThickness > warnThickness) {
1205
- throw new Error("minThickness must be less than or equal to warnThickness.");
1206
- }
1207
- if (warnThickness > maxThickness) {
1208
- throw new Error("warnThickness must be less than or equal to maxThickness.");
1209
- }
1210
- return {
1211
- minThickness,
1212
- warnThickness,
1213
- maxThickness,
1214
- maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
1215
- };
1216
- }
1217
- function lerp(a, b, t) {
1218
- return a + (b - a) * Math.max(0, Math.min(1, t));
1219
- }
1220
- function lerpColor$1(a, b, t) {
1221
- 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))];
1222
- }
1223
- function thicknessClass(thickness, options) {
1224
- if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
1225
- if (thickness <= options.minThickness) return "critical";
1226
- if (thickness <= options.warnThickness) return "warning";
1227
- if (thickness <= options.maxThickness) return "ok";
1228
- return "thick";
1229
- }
1230
- function thicknessColor(thickness, options) {
1231
- const cls = thicknessClass(thickness, options);
1232
- if (cls === "unknown") return THICKNESS_COLORS.unknown;
1233
- if (cls === "critical") return THICKNESS_COLORS.critical;
1234
- if (cls === "warning") {
1235
- const span = Math.max(1e-9, options.warnThickness - options.minThickness);
1236
- return lerpColor$1(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
1237
- }
1238
- if (cls === "ok") {
1239
- const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
1240
- return lerpColor$1(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
1241
- }
1242
- return THICKNESS_COLORS.thick;
1243
- }
1244
- function sampleArea(sample) {
1245
- const area = sample.area ?? 1;
1246
- return Number.isFinite(area) && area > 0 ? area : 1;
1247
- }
1248
- function weightedQuantile(samples, q) {
1249
- if (samples.length === 0) return null;
1250
- const sorted = [...samples].sort((a, b) => a.thickness - b.thickness);
1251
- const totalArea = sorted.reduce((sum, sample) => sum + sample.area, 0);
1252
- const target = totalArea * Math.max(0, Math.min(1, q));
1253
- let cumulative = 0;
1254
- for (const sample of sorted) {
1255
- cumulative += sample.area;
1256
- if (cumulative >= target) return sample.thickness;
1257
- }
1258
- return sorted[sorted.length - 1].thickness;
1259
- }
1260
- function percent(part, total) {
1261
- if (total <= 0) return 0;
1262
- return part / total * 100;
1263
- }
1264
- function summarizeThicknessSamples(samples, options) {
1265
- const resolved = [];
1266
- let totalArea = 0;
1267
- let resolvedArea = 0;
1268
- let unresolvedArea = 0;
1269
- let criticalArea = 0;
1270
- let warningArea = 0;
1271
- let weightedSum = 0;
1272
- for (const sample of samples) {
1273
- const area = sampleArea(sample);
1274
- totalArea += area;
1275
- const value = sample.thickness;
1276
- if (value == null || !Number.isFinite(value) || value <= 0) {
1277
- unresolvedArea += area;
1278
- continue;
1279
- }
1280
- resolved.push({ thickness: value, area });
1281
- resolvedArea += area;
1282
- weightedSum += value * area;
1283
- if (value <= options.minThickness) {
1284
- criticalArea += area;
1285
- } else if (value <= options.warnThickness) {
1286
- warningArea += area;
1287
- }
1288
- }
1289
- const values = resolved.map((sample) => sample.thickness);
1290
- return {
1291
- sampleCount: samples.length,
1292
- resolvedCount: resolved.length,
1293
- unresolvedCount: samples.length - resolved.length,
1294
- minThickness: values.length > 0 ? Math.min(...values) : null,
1295
- p05Thickness: weightedQuantile(resolved, 0.05),
1296
- medianThickness: weightedQuantile(resolved, 0.5),
1297
- meanThickness: resolvedArea > 0 ? weightedSum / resolvedArea : null,
1298
- maxThickness: values.length > 0 ? Math.max(...values) : null,
1299
- criticalAreaPercent: percent(criticalArea, resolvedArea),
1300
- warningAreaPercent: percent(warningArea, resolvedArea),
1301
- belowWarnAreaPercent: percent(criticalArea + warningArea, resolvedArea),
1302
- unresolvedAreaPercent: percent(unresolvedArea, totalArea)
1303
- };
1304
- }
1305
592
  const canvas = document.getElementById("canvas");
1306
593
  const exportCanvas = document.createElement("canvas");
1307
594
  const exportCtx = exportCanvas.getContext("2d");
@@ -1569,39 +856,8 @@ function updateSdfRaymarchUniforms(session) {
1569
856
  sdf.material.uniforms.uIsOrthographic.value = isOrthographic;
1570
857
  }
1571
858
  }
1572
- const ANGLE_DIRS = {
1573
- front: [0, -1, 0.2],
1574
- back: [0, 1, 0.2],
1575
- side: [1, 0, 0.2],
1576
- right: [1, 0, 0.2],
1577
- top: [0, -0.01, 1],
1578
- iso: [0.6, -0.6, 0.4]
1579
- };
1580
- function normalizeVector(dir) {
1581
- const len = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2) || 1;
1582
- return [dir[0] / len, dir[1] / len, dir[2] / len];
1583
- }
1584
- function sphericalToDir(azimuthDeg, elevationDeg) {
1585
- const az = azimuthDeg * Math.PI / 180;
1586
- const el = elevationDeg * Math.PI / 180;
1587
- const cosEl = Math.cos(el);
1588
- return [cosEl * Math.sin(az), -cosEl * Math.cos(az), Math.sin(el)];
1589
- }
1590
- function parseCameraToken(token) {
1591
- const preset = ANGLE_DIRS[token];
1592
- if (preset) return { label: token, dir: preset };
1593
- const parts = token.split(":").map((s) => Number.parseFloat(s));
1594
- if (parts.length >= 2 && parts.length <= 3 && parts.every((n) => Number.isFinite(n))) {
1595
- const dir = sphericalToDir(parts[0], parts[1]);
1596
- const label = `az${parts[0]}_el${parts[1]}`;
1597
- return { label, dir, distance: parts[2] };
1598
- }
1599
- throw new Error(
1600
- `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
1601
- );
1602
- }
1603
859
  function applyDirectionCamera(session, dir, distanceOverride) {
1604
- const d = normalizeVector(dir);
860
+ const d = normalizeCameraDirection(dir);
1605
861
  const dist = distanceOverride ?? session.distance;
1606
862
  session.camera.position.set(session.center.x + d[0] * dist, session.center.y + d[1] * dist, session.center.z + d[2] * dist);
1607
863
  session.camera.up.set(0, 0, 1);
@@ -1782,92 +1038,6 @@ function distanceColorForRootDistance(distance, maxDistance) {
1782
1038
  if (t <= 0.5) return lerpColor(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2);
1783
1039
  return lerpColor(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2);
1784
1040
  }
1785
- function cloneGeometryForFaceColors(geometry) {
1786
- return geometry.index ? geometry.toNonIndexed() : geometry.clone();
1787
- }
1788
- function geometryMaxDimension(geometry) {
1789
- geometry.computeBoundingBox();
1790
- const box = geometry.boundingBox;
1791
- if (!box) return 1;
1792
- const size = new Vector3();
1793
- box.getSize(size);
1794
- return Math.max(1, size.x, size.y, size.z);
1795
- }
1796
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
1797
- const origin = point.clone().addScaledVector(direction, epsilon);
1798
- raycaster.set(origin, direction);
1799
- raycaster.near = epsilon;
1800
- raycaster.far = far;
1801
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
1802
- return hit ? hit.distance + epsilon : null;
1803
- }
1804
- function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
1805
- const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
1806
- const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
1807
- if (forward == null) return backward;
1808
- if (backward == null) return forward;
1809
- return Math.min(forward, backward);
1810
- }
1811
- function analyzeThicknessGeometry(sourceGeometry, options) {
1812
- const geometry = cloneGeometryForFaceColors(sourceGeometry);
1813
- const position = geometry.getAttribute("position");
1814
- if (!position || position.count < 3) {
1815
- return { geometry, samples: [], triangleCount: 0, sampledTriangleCount: 0, sampleStride: 1, warnings: ["No triangle geometry."] };
1816
- }
1817
- const triangleCount = Math.floor(position.count / 3);
1818
- const sampleStride = Math.max(1, Math.ceil(triangleCount / options.maxSamplesPerObject));
1819
- const maxDim = geometryMaxDimension(geometry);
1820
- const epsilon = Math.max(1e-4, maxDim * 1e-6);
1821
- const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1822
- const colors = new Float32Array(position.count * 3);
1823
- const samples = [];
1824
- const warnings = [];
1825
- const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1826
- const rayMesh = new Mesh(geometry, rayMaterial);
1827
- const raycaster = new Raycaster();
1828
- if (sampleStride > 1) {
1829
- warnings.push(`Triangle sampling stride ${sampleStride}; increase --thickness-samples for denser analysis.`);
1830
- }
1831
- const a = new Vector3();
1832
- const b = new Vector3();
1833
- const c = new Vector3();
1834
- const normal = new Vector3();
1835
- const centroid = new Vector3();
1836
- let sampledTriangleCount = 0;
1837
- let lastThickness = null;
1838
- for (let tri = 0; tri < triangleCount; tri += 1) {
1839
- const offset = tri * 3;
1840
- a.fromBufferAttribute(position, offset);
1841
- b.fromBufferAttribute(position, offset + 1);
1842
- c.fromBufferAttribute(position, offset + 2);
1843
- normal.subVectors(b, a).cross(new Vector3().subVectors(c, a));
1844
- const areaTwice = normal.length();
1845
- const area = areaTwice * 0.5;
1846
- let thickness = lastThickness;
1847
- if (tri % sampleStride === 0) {
1848
- sampledTriangleCount += 1;
1849
- if (areaTwice <= 1e-12) {
1850
- thickness = null;
1851
- } else {
1852
- normal.multiplyScalar(1 / areaTwice);
1853
- centroid.copy(a).add(b).add(c).multiplyScalar(1 / 3);
1854
- thickness = triangleThickness(raycaster, rayMesh, centroid, normal, epsilon, far);
1855
- }
1856
- lastThickness = thickness;
1857
- samples.push({ thickness, area });
1858
- }
1859
- const color = thicknessColor(thickness, options);
1860
- for (let vertex = 0; vertex < 3; vertex += 1) {
1861
- const colorOffset = (offset + vertex) * 3;
1862
- colors[colorOffset] = color[0] / 255;
1863
- colors[colorOffset + 1] = color[1] / 255;
1864
- colors[colorOffset + 2] = color[2] / 255;
1865
- }
1866
- }
1867
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
1868
- rayMaterial.dispose();
1869
- return { geometry, samples, triangleCount, sampledTriangleCount, sampleStride, warnings };
1870
- }
1871
1041
  function buildMaskEntries(session) {
1872
1042
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
1873
1043
  return session.renderables.map((renderable, idx) => {
@@ -1957,6 +1127,7 @@ function decorateConnectivityReport(report) {
1957
1127
  return {
1958
1128
  method: report.method,
1959
1129
  options: report.options,
1130
+ broadphase: report.broadphase,
1960
1131
  objectCount: report.objectCount,
1961
1132
  componentCount: report.componentCount,
1962
1133
  objects,
@@ -2045,6 +1216,7 @@ function decorateDistanceReport(report) {
2045
1216
  maxRootDistance: report.maxRootDistance,
2046
1217
  objects,
2047
1218
  components,
1219
+ gapEdgeCount: report.gapEdgeCount,
2048
1220
  gapEdges: report.gapEdges,
2049
1221
  connectivity: report.connectivity,
2050
1222
  warnings: report.warnings,
@@ -2274,6 +1446,130 @@ function renderCurrentThickness(session, rawOptions) {
2274
1446
  material.dispose();
2275
1447
  }
2276
1448
  }
1449
+ const ROUGHNESS_SMOOTH_OPACITY = 0.16;
1450
+ const ROUGHNESS_HARSH_OPACITY = 1;
1451
+ function createRoughnessMaterial(clippingPlanes) {
1452
+ const material = new ShaderMaterial({
1453
+ transparent: true,
1454
+ depthTest: true,
1455
+ depthWrite: true,
1456
+ clippingPlanes: clippingPlanes ?? void 0,
1457
+ uniforms: {
1458
+ smoothOpacity: { value: ROUGHNESS_SMOOTH_OPACITY },
1459
+ harshOpacity: { value: ROUGHNESS_HARSH_OPACITY }
1460
+ },
1461
+ vertexShader: `
1462
+ attribute vec3 color;
1463
+ attribute float roughnessScore;
1464
+ varying vec3 vColor;
1465
+ varying float vRoughnessScore;
1466
+ varying vec3 vViewNormal;
1467
+
1468
+ void main() {
1469
+ vColor = color;
1470
+ vRoughnessScore = roughnessScore;
1471
+ vViewNormal = normalize(normalMatrix * normal);
1472
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1473
+ }
1474
+ `,
1475
+ fragmentShader: `
1476
+ precision highp float;
1477
+ uniform float smoothOpacity;
1478
+ uniform float harshOpacity;
1479
+ varying vec3 vColor;
1480
+ varying float vRoughnessScore;
1481
+ varying vec3 vViewNormal;
1482
+
1483
+ void main() {
1484
+ vec3 shadowLight = normalize(vec3(0.32, 0.42, 0.84));
1485
+ float shade = 0.34 + 0.48 * max(dot(normalize(vViewNormal), shadowLight), 0.0);
1486
+ vec3 smoothShadow = vec3(0.26, 0.30, 0.35) * shade;
1487
+ float colorMix = smoothstep(0.04, 0.35, vRoughnessScore);
1488
+ vec3 finalColor = mix(smoothShadow, vColor, colorMix);
1489
+ float alpha = mix(smoothOpacity, harshOpacity, smoothstep(0.02, 0.55, vRoughnessScore));
1490
+ gl_FragColor = vec4(finalColor, alpha);
1491
+ }
1492
+ `
1493
+ });
1494
+ material.toneMapped = false;
1495
+ return material;
1496
+ }
1497
+ function renderCurrentRoughness(session, rawOptions) {
1498
+ const r = getRenderer(session.size, session.pixelRatio);
1499
+ const options = resolveRoughnessInspectionOptions(rawOptions);
1500
+ const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
1501
+ const warnings = [];
1502
+ const objects = [];
1503
+ const replacements = session.renderables.map((renderable, index) => {
1504
+ const previousMaterial = renderable.solid.material;
1505
+ const previousGeometry = renderable.solid.geometry;
1506
+ if (renderable.sdfRaymarch) {
1507
+ const material2 = new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
1508
+ material2.toneMapped = false;
1509
+ renderable.solid.material = material2;
1510
+ warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh roughness inspection.`);
1511
+ return { renderable, previousMaterial, previousGeometry, material: material2, geometry: null };
1512
+ }
1513
+ const analysis = analyzeRoughnessGeometry(previousGeometry, options);
1514
+ const bbox = analysis.geometry.boundingBox;
1515
+ const sourceObject = byId.get(renderable.id);
1516
+ if (analysis.warnings.length > 0) {
1517
+ analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
1518
+ }
1519
+ objects.push({
1520
+ index: index + 1,
1521
+ id: renderable.id,
1522
+ name: renderable.name,
1523
+ groupName: renderable.groupName,
1524
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
1525
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
1526
+ bbox: {
1527
+ min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
1528
+ max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
1529
+ },
1530
+ ...analysis.summary
1531
+ });
1532
+ const material = createRoughnessMaterial(renderable.solidMaterial.clippingPlanes);
1533
+ renderable.solid.geometry = analysis.geometry;
1534
+ renderable.solid.material = material;
1535
+ return { renderable, previousMaterial, previousGeometry, material, geometry: analysis.geometry };
1536
+ });
1537
+ try {
1538
+ const png = withSolidOnlyVisibility(
1539
+ session,
1540
+ () => withTemporarySceneBackground(session, new Color(0), () => {
1541
+ updateSdfRaymarchUniforms(session);
1542
+ r.render(session.scene, session.camera);
1543
+ return captureRenderedPng(session.size);
1544
+ })
1545
+ );
1546
+ return {
1547
+ png,
1548
+ report: {
1549
+ method: "mesh-dihedral-angle",
1550
+ options,
1551
+ objectCount: objects.length,
1552
+ objects,
1553
+ warnings,
1554
+ style: {
1555
+ smoothColor: ROUGHNESS_COLORS.smooth,
1556
+ moderateColor: ROUGHNESS_COLORS.moderate,
1557
+ sharpColor: ROUGHNESS_COLORS.sharp,
1558
+ harshColor: ROUGHNESS_COLORS.harsh,
1559
+ smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
1560
+ harshOpacity: ROUGHNESS_HARSH_OPACITY
1561
+ }
1562
+ }
1563
+ };
1564
+ } finally {
1565
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, material, geometry }) => {
1566
+ renderable.solid.geometry = previousGeometry;
1567
+ renderable.solid.material = previousMaterial;
1568
+ geometry == null ? void 0 : geometry.dispose();
1569
+ material.dispose();
1570
+ });
1571
+ }
1572
+ }
2277
1573
  function emptySectionSvg() {
2278
1574
  return {
2279
1575
  svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 2 2" width="400" height="400"><rect x="-1" y="-1" width="2" height="2" fill="#2a2a2a"/></svg>',
@@ -2756,7 +2052,7 @@ function applyOrbitPose(session, turn, pitchDeg) {
2756
2052
  const pitch = MathUtils.degToRad(clampedPitch);
2757
2053
  const cosPitch = Math.cos(pitch);
2758
2054
  const dir = [Math.sin(yaw) * cosPitch, -Math.cos(yaw) * cosPitch, Math.sin(pitch)];
2759
- const d = normalizeVector(dir);
2055
+ const d = normalizeCameraDirection(dir);
2760
2056
  session.camera.position.set(
2761
2057
  session.orbitTarget.x + d[0] * session.orbitRadius,
2762
2058
  session.orbitTarget.y + d[1] * session.orbitRadius,
@@ -3190,6 +2486,12 @@ function createSession(code, opts) {
3190
2486
  const scene = new Scene();
3191
2487
  const sceneConfig = result.sceneConfig ?? null;
3192
2488
  try {
2489
+ if ((opts == null ? void 0 : opts.viewName) && (opts == null ? void 0 : opts.cameraToken)) {
2490
+ return {
2491
+ ok: false,
2492
+ error: "Cannot use --view with --camera. Choose either a model-declared view or an explicit camera."
2493
+ };
2494
+ }
3193
2495
  if (opts == null ? void 0 : opts.viewName) {
3194
2496
  if (requestedSceneState == null ? void 0 : requestedSceneState.camera) {
3195
2497
  return {
@@ -3227,6 +2529,30 @@ function createSession(code, opts) {
3227
2529
  const fov = 45;
3228
2530
  const distance = maxDim / (2 * Math.tan(fov * Math.PI / 360)) * 1.6;
3229
2531
  const cameraFov = ((_c = requestedSceneState == null ? void 0 : requestedSceneState.camera) == null ? void 0 : _c.fov) ?? ((_d = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _d.fov) ?? fov;
2532
+ try {
2533
+ if (opts == null ? void 0 : opts.cameraToken) {
2534
+ if (requestedSceneState == null ? void 0 : requestedSceneState.camera) {
2535
+ return {
2536
+ ok: false,
2537
+ error: "Cannot use camera presets/angles with an explicit render camera. Remove --camera-json or the camera field from --scene."
2538
+ };
2539
+ }
2540
+ const parsed = parseCameraToken(opts.cameraToken);
2541
+ const dir = normalizeCameraDirection(parsed.dir);
2542
+ const tokenDistance = parsed.distance ?? distance;
2543
+ requestedSceneState = mergeViewportRenderSceneStates(requestedSceneState, {
2544
+ camera: {
2545
+ projectionMode: "perspective",
2546
+ position: [center.x + dir[0] * tokenDistance, center.y + dir[1] * tokenDistance, center.z + dir[2] * tokenDistance],
2547
+ target: [center.x, center.y, center.z],
2548
+ up: [0, 0, 1],
2549
+ fov: cameraFov
2550
+ }
2551
+ });
2552
+ }
2553
+ } catch (err) {
2554
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2555
+ }
3230
2556
  const joints = ((_e = result.jointsView) == null ? void 0 : _e.enabled) === false ? [] : ((_f = result.jointsView) == null ? void 0 : _f.joints) ?? [];
3231
2557
  const jointCouplings = ((_g = result.jointsView) == null ? void 0 : _g.enabled) === false ? [] : ((_h = result.jointsView) == null ? void 0 : _h.couplings) ?? [];
3232
2558
  const animationClips = ((_i = result.jointsView) == null ? void 0 : _i.enabled) === false ? [] : ((_j = result.jointsView) == null ? void 0 : _j.animations) ?? [];
@@ -3525,7 +2851,7 @@ window.__forgeRender = async (code, opts) => {
3525
2851
  const session = built.session;
3526
2852
  await emitInspectProgress(opts, { type: "session-done", objectCount: session.objects.length });
3527
2853
  const renderMode = (opts == null ? void 0 : opts.renderMode) === "wireframe" ? "wireframe" : "solid";
3528
- const edgePreset = (opts == null ? void 0 : opts.edges) ?? "thin";
2854
+ const edgePreset = (opts == null ? void 0 : opts.edges) ?? "off";
3529
2855
  setSessionMode(session, renderMode);
3530
2856
  if (renderMode === "solid") {
3531
2857
  session.renderables.forEach((r) => {
@@ -3547,11 +2873,13 @@ window.__forgeRender = async (code, opts) => {
3547
2873
  const distanceRenders = {};
3548
2874
  const collisionRenders = {};
3549
2875
  const thicknessRenders = {};
2876
+ const roughnessRenders = {};
3550
2877
  let maskObjects = [];
3551
2878
  let connectivityReport = null;
3552
2879
  let distanceReport = null;
3553
2880
  let collisionReport = null;
3554
2881
  let thicknessReport = null;
2882
+ let roughnessReport = null;
3555
2883
  let sectionAtlas = null;
3556
2884
  const channelViewCounts = /* @__PURE__ */ new Map();
3557
2885
  const markChannelViewStart = async (channel, view) => {
@@ -3592,6 +2920,13 @@ window.__forgeRender = async (code, opts) => {
3592
2920
  normalRenders[label] = renderCurrentNormals(session);
3593
2921
  await markChannelViewDone("normals", label);
3594
2922
  }
2923
+ if (requestedChannels.has("roughness")) {
2924
+ await markChannelViewStart("roughness", label);
2925
+ const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
2926
+ roughnessRenders[label] = roughness.png;
2927
+ roughnessReport = roughness.report;
2928
+ await markChannelViewDone("roughness", label);
2929
+ }
3595
2930
  if (requestedChannels.has("mask")) {
3596
2931
  await markChannelViewStart("mask", label);
3597
2932
  const mask = renderCurrentMask(session);
@@ -3652,6 +2987,12 @@ window.__forgeRender = async (code, opts) => {
3652
2987
  renders,
3653
2988
  depth: depthRenders,
3654
2989
  normals: normalRenders,
2990
+ roughness: roughnessReport ? {
2991
+ ...roughnessReport,
2992
+ views: roughnessRenders
2993
+ } : {
2994
+ views: roughnessRenders
2995
+ },
3655
2996
  mask: {
3656
2997
  views: maskRenders,
3657
2998
  objects: maskObjects