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.
- package/dist/assets/{AdminPage-jwoEgwE_.js → AdminPage-Da6hhpJx.js} +1 -1
- package/dist/assets/{BlogPage-Ck7g3ue2.js → BlogPage-Bl_sKeWb.js} +1 -1
- package/dist/assets/{DocsPage-9WaRC14b.js → DocsPage-Blz3Tp4j.js} +1 -6
- package/dist/assets/EditorApp-CuiPbtn5.js +12754 -0
- package/dist/assets/{EmbedViewer-37_PfMwv.js → EmbedViewer-BFG6-Ufm.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-CO8WL0CY.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
- package/dist/assets/{PricingPage-DADKGuOa.js → PricingPage-BMxYT_F0.js} +1 -1
- package/dist/assets/{SettingsPage-DKKI4W49.js → SettingsPage-VVQNrCAg.js} +1 -1
- package/dist/assets/{app-CwI02pTA.js → app-Dl9ymBWC.js} +355 -36
- package/dist/assets/cli/{render-Kw5hLEcL.js → render-CFtwKCCY.js} +203 -862
- package/dist/assets/{sectionPlaneMath-C8N0w8o3.js → distance-BEC2RjJi.js} +4150 -801
- package/dist/assets/{evalWorker-D6ub3kfS.js → evalWorker-CRvbzTXm.js} +2611 -528
- package/dist/assets/{manifold-CwDdMKyc.js → manifold-B9QSr-qP.js} +1 -1
- package/dist/assets/{manifold-DTvmxSDf.js → manifold-DpBXFS2K.js} +1 -1
- package/dist/assets/{manifold-lru0jwVw.js → manifold-DzZ4VRPs.js} +2 -2
- package/dist/assets/{renderSceneState-tvtNKNRi.js → renderSceneState-BuAXF2jh.js} +1 -1
- package/dist/assets/{reportWorker-DeqktDGt.js → reportWorker-BNWEnRg1.js} +2606 -525
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/usage.md +0 -1
- package/dist/docs-raw/API/core/concepts.md +11 -1
- package/dist/docs-raw/CLI.md +64 -13
- package/dist/docs-raw/beta-operations.md +4 -0
- package/dist/docs-raw/deployment.md +38 -23
- package/dist/docs-raw/generated/assembly.md +8 -3
- package/dist/docs-raw/generated/concepts.md +126 -46
- package/dist/docs-raw/generated/core.md +97 -47
- package/dist/docs-raw/generated/curves.md +113 -595
- package/dist/docs-raw/generated/lib.md +40 -3
- package/dist/docs-raw/generated/output.md +6 -1
- package/dist/docs-raw/generated/sdf.md +50 -4
- package/dist/docs-raw/generated/sketch.md +9 -1
- package/dist/docs-raw/generated/viewport.md +1 -9
- package/dist/docs-raw/guides/inspection-bundles.md +40 -9
- package/dist/docs-raw/runbook.md +3 -3
- package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -0
- package/dist/docs-raw/skills/forgecad-image-replicator.md +3 -1
- package/dist/docs-raw/skills/forgecad-make-a-model.md +48 -4
- package/dist/docs-raw/skills/forgecad-render-inspect.md +3 -1
- package/dist/docs-raw/skills/forgecad-visual-spec.md +2 -0
- package/dist/docs-raw/skills/forgecad.md +2 -1
- package/dist/docs-raw/skills/index.md +0 -1
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +6 -6
- package/dist-cli/blender/render.py +43 -8
- package/dist-cli/forgecad.js +5729 -2015
- package/dist-cli/forgecad.js.map +1 -1
- package/dist-skill/CONTEXT.md +372 -667
- package/dist-skill/SKILL-dev.md +2 -1
- package/dist-skill/SKILL.md +2 -1
- package/dist-skill/docs/API/core/concepts.md +11 -1
- package/dist-skill/docs/CLI.md +64 -13
- package/dist-skill/docs/generated/assembly.md +8 -3
- package/dist-skill/docs/generated/core.md +97 -47
- package/dist-skill/docs/generated/curves.md +113 -595
- package/dist-skill/docs/generated/lib.md +40 -3
- package/dist-skill/docs/generated/output.md +6 -1
- package/dist-skill/docs/generated/sdf.md +50 -4
- package/dist-skill/docs/generated/sketch.md +9 -1
- package/dist-skill/docs/generated/viewport.md +1 -9
- package/dist-skill/docs/guides/inspection-bundles.md +40 -9
- package/dist-skill/docs-dev/API/core/concepts.md +11 -1
- package/dist-skill/docs-dev/CLI.md +64 -13
- package/dist-skill/docs-dev/generated/assembly.md +8 -3
- package/dist-skill/docs-dev/generated/core.md +97 -47
- package/dist-skill/docs-dev/generated/curves.md +113 -595
- package/dist-skill/docs-dev/generated/lib.md +40 -3
- package/dist-skill/docs-dev/generated/output.md +6 -1
- package/dist-skill/docs-dev/generated/sdf.md +50 -4
- package/dist-skill/docs-dev/generated/sketch.md +9 -1
- package/dist-skill/docs-dev/generated/viewport.md +1 -9
- package/dist-skill/docs-dev/guides/inspection-bundles.md +40 -9
- package/dist-skill/library/README.md +0 -1
- package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -0
- package/dist-skill/library/forgecad-image-replicator/SKILL.md +3 -1
- package/dist-skill/library/forgecad-make-a-model/SKILL.md +48 -4
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
- package/dist-skill/library/forgecad-visual-spec/SKILL.md +2 -0
- package/examples/api/drive-wheel-regions.forge.js +43 -0
- package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
- package/examples/api/sdf-circular-array-knurling.forge.js +19 -0
- package/examples/api/sdf-pattern2d-ceramic-ripple-set.forge.js +83 -0
- package/examples/api/sdf-pattern2d-grip-tread.forge.js +72 -0
- package/examples/api/sdf-pattern2d-orbital-jewelry.forge.js +62 -0
- package/examples/api/sdf-surface-basket-weave.forge.js +67 -0
- package/examples/api/sector-gear-body.forge.js +34 -0
- package/package.json +20 -2
- package/dist/assets/EditorApp-Dja2jMmW.js +0 -12509
- package/dist/docs-raw/skills/forgecad-api-dogfood.md +0 -130
- package/dist-skill/library/forgecad-api-dogfood/SKILL.md +0 -125
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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 =
|
|
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 =
|
|
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) ?? "
|
|
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
|