forgecad 0.9.6 → 0.9.7
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-Da6hhpJx.js → AdminPage-DX0mpSZT.js} +1 -1
- package/dist/assets/{BlogPage-Bl_sKeWb.js → BlogPage-CI_P0_Pf.js} +1 -1
- package/dist/assets/{DocsPage-Blz3Tp4j.js → DocsPage-DLhIIZyJ.js} +3 -3
- package/dist/assets/{EditorApp-CuiPbtn5.js → EditorApp-BujZvuwX.js} +140 -20
- package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
- package/dist/assets/{EmbedViewer-BFG6-Ufm.js → EmbedViewer-0S0qXKog.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-DB9fQd5P.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
- package/dist/assets/{PricingPage-BMxYT_F0.js → PricingPage-DGkX3Ahr.js} +1 -1
- package/dist/assets/{SettingsPage-VVQNrCAg.js → SettingsPage-DBsqTB_y.js} +82 -22
- package/dist/assets/{app-Dl9ymBWC.js → app-BE2nD6Yz.js} +1056 -258
- package/dist/assets/cli/{render-CFtwKCCY.js → render-iP9qh475.js} +1533 -207
- package/dist/assets/{evalWorker-CRvbzTXm.js → evalWorker-Ds5U4xtN.js} +2178 -30
- package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
- package/dist/assets/{manifold-DpBXFS2K.js → manifold-Bk26ViCr.js} +1 -1
- package/dist/assets/{manifold-DzZ4VRPs.js → manifold-DjYsd7A_.js} +2 -2
- package/dist/assets/{manifold-B9QSr-qP.js → manifold-sJ-axdXM.js} +1 -1
- package/dist/assets/{renderSceneState-BuAXF2jh.js → renderSceneState-Bngp5MrQ.js} +1 -1
- package/dist/assets/{reportWorker-BNWEnRg1.js → reportWorker-CU8RZ4O0.js} +2161 -30
- package/dist/assets/{distance-BEC2RjJi.js → sectionPlaneMath-BdTjyVfs.js} +2539 -1187
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +1 -1
- package/dist/docs-raw/AI/usage.md +7 -2
- package/dist/docs-raw/CLI.md +82 -53
- package/dist/docs-raw/beta-operations.md +5 -0
- package/dist/docs-raw/coding.md +1 -1
- package/dist/docs-raw/generated/concepts.md +59 -2
- package/dist/docs-raw/generated/core.md +206 -1
- package/dist/docs-raw/generated/lib.md +17 -1
- package/dist/docs-raw/generated/viewport.md +1 -1
- package/dist/docs-raw/guides/inspection-bundles.md +36 -13
- package/dist/docs-raw/platform/auth.md +2 -0
- package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
- package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
- package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
- package/dist/docs-raw/skills/index.md +2 -2
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +6 -6
- package/dist-cli/forgecad.js +7975 -4528
- package/dist-cli/forgecad.js.map +1 -1
- package/dist-skill/CONTEXT.md +260 -16
- package/dist-skill/docs/CLI.md +82 -53
- package/dist-skill/docs/generated/core.md +206 -1
- package/dist-skill/docs/generated/lib.md +17 -1
- package/dist-skill/docs/generated/viewport.md +1 -1
- package/dist-skill/docs/guides/inspection-bundles.md +36 -13
- package/dist-skill/docs-dev/CLI.md +82 -53
- package/dist-skill/docs-dev/coding.md +1 -1
- package/dist-skill/docs-dev/generated/core.md +206 -1
- package/dist-skill/docs-dev/generated/lib.md +17 -1
- package/dist-skill/docs-dev/generated/viewport.md +1 -1
- package/dist-skill/docs-dev/guides/inspection-bundles.md +36 -13
- package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
- package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
- package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
- package/examples/api/bolted-service-cover.forge.js +17 -0
- package/examples/api/cable-gland-anchor.forge.js +14 -0
- package/examples/api/captured-cartridge-guide.forge.js +14 -0
- package/examples/api/captured-linear-slide.forge.js +13 -0
- package/examples/api/clevis-pin-joint.forge.js +13 -0
- package/examples/api/datum-enclosure.forge.js +16 -0
- package/examples/api/hose-barb-port.forge.js +14 -0
- package/examples/api/intentional-overlap-overmold.forge.js +16 -0
- package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
- package/examples/api/living-hinge-cover.forge.js +14 -0
- package/examples/api/pcb-terminal-block.forge.js +22 -0
- package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
- package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
- package/examples/api/routed-tube-clip.forge.js +15 -0
- package/examples/api/seated-bearing-stack.forge.js +30 -0
- package/examples/api/snap-latch-cover.forge.js +14 -0
- package/examples/api/static-assembly-connectors.forge.js +14 -16
- package/examples/api/thumb-screw-clamp.forge.js +15 -0
- package/package.json +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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, bI as initSolverWasm, bH as initKernel, S as Scene, bJ as BoxGeometry, be as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bK as localAabbPlaneRelation, h as Vector2, bL as ShapeUtils, bM as aabbInteriorOverlaps, bN as aabbOverlapVolume, bO as AabbSpatialIndex, bP as aabbGaps, g as Vector3, R as Raycaster, aU as BufferAttribute, a0 as MathUtils, G as Box3, e as Color, aC as resolveForgeRenderStyle, ba as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, bQ as Group, b3 as shapeToGeometry, b8 as MeshPhysicalMaterial, bb as AdditiveBlending, aH as LineBasicMaterial, b9 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bR as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bS as parseCameraCliSpec, bT as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bA as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bh as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bg as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bB as resolveJointAnimation, bC as resolveJointViewValues, bU as PointsMaterial, bV as Points, b2 as analyzeCollisionIntersections, bW as serializeCollisionFinding, bX as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-BdTjyVfs.js";
|
|
5
|
+
import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-Bngp5MrQ.js";
|
|
3
6
|
const CAD_MATERIAL_PROPS = {
|
|
4
7
|
color: 6003669,
|
|
5
8
|
metalness: 0.05,
|
|
@@ -266,17 +269,17 @@ function stitchLoops(points, edges) {
|
|
|
266
269
|
const warnings = [];
|
|
267
270
|
const adjacency = /* @__PURE__ */ new Map();
|
|
268
271
|
const unusedEdges = /* @__PURE__ */ new Set();
|
|
269
|
-
const
|
|
272
|
+
const edgeKey2 = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
270
273
|
for (const [a, b] of edges) {
|
|
271
274
|
if (!adjacency.has(a)) adjacency.set(a, []);
|
|
272
275
|
if (!adjacency.has(b)) adjacency.set(b, []);
|
|
273
276
|
(_a = adjacency.get(a)) == null ? void 0 : _a.push(b);
|
|
274
277
|
(_b = adjacency.get(b)) == null ? void 0 : _b.push(a);
|
|
275
|
-
unusedEdges.add(
|
|
278
|
+
unusedEdges.add(edgeKey2(a, b));
|
|
276
279
|
}
|
|
277
280
|
const loops = [];
|
|
278
281
|
for (const [edgeA, edgeB] of edges) {
|
|
279
|
-
const firstKey =
|
|
282
|
+
const firstKey = edgeKey2(edgeA, edgeB);
|
|
280
283
|
if (!unusedEdges.has(firstKey)) continue;
|
|
281
284
|
const loop = [edgeA, edgeB];
|
|
282
285
|
unusedEdges.delete(firstKey);
|
|
@@ -285,12 +288,12 @@ function stitchLoops(points, edges) {
|
|
|
285
288
|
let closed = false;
|
|
286
289
|
for (let guard = 0; guard < points.length + edges.length + 8; guard += 1) {
|
|
287
290
|
const neighbors = adjacency.get(current) ?? [];
|
|
288
|
-
const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(
|
|
291
|
+
const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey2(current, candidate)));
|
|
289
292
|
if (next === void 0) {
|
|
290
293
|
if (current === edgeA) closed = true;
|
|
291
294
|
break;
|
|
292
295
|
}
|
|
293
|
-
unusedEdges.delete(
|
|
296
|
+
unusedEdges.delete(edgeKey2(current, next));
|
|
294
297
|
if (next === edgeA) {
|
|
295
298
|
closed = true;
|
|
296
299
|
break;
|
|
@@ -454,6 +457,425 @@ function computeMeshSectionCap(mesh, planeInput) {
|
|
|
454
457
|
warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
|
|
455
458
|
};
|
|
456
459
|
}
|
|
460
|
+
const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
|
|
461
|
+
contactTolerance: 0.05,
|
|
462
|
+
minOverlapVolume: 0.1,
|
|
463
|
+
exactGeometry: true
|
|
464
|
+
};
|
|
465
|
+
const AXIS_NAMES = ["x", "y", "z"];
|
|
466
|
+
let UnionFind$1 = class UnionFind {
|
|
467
|
+
constructor(size) {
|
|
468
|
+
__publicField(this, "parent");
|
|
469
|
+
__publicField(this, "rank");
|
|
470
|
+
this.parent = Array.from({ length: size }, (_, index) => index);
|
|
471
|
+
this.rank = Array.from({ length: size }, () => 0);
|
|
472
|
+
}
|
|
473
|
+
find(value) {
|
|
474
|
+
const parent = this.parent[value];
|
|
475
|
+
if (parent === value) return value;
|
|
476
|
+
const root = this.find(parent);
|
|
477
|
+
this.parent[value] = root;
|
|
478
|
+
return root;
|
|
479
|
+
}
|
|
480
|
+
union(a, b) {
|
|
481
|
+
const rootA = this.find(a);
|
|
482
|
+
const rootB = this.find(b);
|
|
483
|
+
if (rootA === rootB) return;
|
|
484
|
+
if (this.rank[rootA] < this.rank[rootB]) {
|
|
485
|
+
this.parent[rootA] = rootB;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (this.rank[rootA] > this.rank[rootB]) {
|
|
489
|
+
this.parent[rootB] = rootA;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this.parent[rootB] = rootA;
|
|
493
|
+
this.rank[rootA] += 1;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
function cloneVec3(value) {
|
|
497
|
+
return [value[0], value[1], value[2]];
|
|
498
|
+
}
|
|
499
|
+
function emptyBBox$1() {
|
|
500
|
+
return {
|
|
501
|
+
min: [Infinity, Infinity, Infinity],
|
|
502
|
+
max: [-Infinity, -Infinity, -Infinity]
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function expandBBox$1(target, min, max) {
|
|
506
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
507
|
+
target.min[axis] = Math.min(target.min[axis], min[axis]);
|
|
508
|
+
target.max[axis] = Math.max(target.max[axis], max[axis]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function nearestBoundaryGap(a, b, axis) {
|
|
512
|
+
return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
|
|
513
|
+
}
|
|
514
|
+
function hasPositiveGap(gaps) {
|
|
515
|
+
return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
|
|
516
|
+
}
|
|
517
|
+
function collectCandidatePairs(entries, tolerance) {
|
|
518
|
+
if (entries.length < 2) return [];
|
|
519
|
+
const index = new AabbSpatialIndex(entries);
|
|
520
|
+
const pairs = index.overlapPairs({ padding: tolerance }).pairs.map((pair) => ({
|
|
521
|
+
sourceIndex: pair.sourceIndex,
|
|
522
|
+
targetIndex: pair.targetIndex,
|
|
523
|
+
gaps: aabbGaps(entries[pair.sourceIndex], entries[pair.targetIndex])
|
|
524
|
+
})).filter((pair) => Math.max(pair.gaps[0], pair.gaps[1], pair.gaps[2]) <= tolerance);
|
|
525
|
+
pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
|
|
526
|
+
return pairs;
|
|
527
|
+
}
|
|
528
|
+
function contactFromBBoxes(a, b, tolerance) {
|
|
529
|
+
const gaps = aabbGaps(a, b);
|
|
530
|
+
const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
|
|
531
|
+
if (largestGap > tolerance) return { touching: false, gap: largestGap };
|
|
532
|
+
const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
|
|
533
|
+
if (separatedAxes.length > 0) {
|
|
534
|
+
const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
|
|
535
|
+
return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
|
|
536
|
+
}
|
|
537
|
+
const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
|
|
538
|
+
axis,
|
|
539
|
+
axisName,
|
|
540
|
+
gap: nearestBoundaryGap(a, b, axis)
|
|
541
|
+
})).filter((entry) => entry.gap <= tolerance);
|
|
542
|
+
if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
|
|
543
|
+
const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
|
|
544
|
+
return { touching: true, gap: nearest.gap, axis: nearest.axisName };
|
|
545
|
+
}
|
|
546
|
+
function intersectionVolume(a, b) {
|
|
547
|
+
try {
|
|
548
|
+
const hit = a.shape.intersect(b.shape);
|
|
549
|
+
if (hit.isEmpty()) return { volume: 0 };
|
|
550
|
+
const volume = hit.volume();
|
|
551
|
+
return { volume: Number.isFinite(volume) ? volume : 0 };
|
|
552
|
+
} catch (err) {
|
|
553
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
554
|
+
return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function bodyCountForEntry(entry) {
|
|
558
|
+
if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
|
|
559
|
+
return Math.max(0, Math.round(entry.bodyCount));
|
|
560
|
+
}
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
function makeEdge(entries, sourceIndex, targetIndex, edge) {
|
|
564
|
+
const source = entries[sourceIndex];
|
|
565
|
+
const target = entries[targetIndex];
|
|
566
|
+
return {
|
|
567
|
+
sourceIndex,
|
|
568
|
+
targetIndex,
|
|
569
|
+
sourceId: source.id,
|
|
570
|
+
targetId: target.id,
|
|
571
|
+
sourceName: source.name,
|
|
572
|
+
targetName: target.name,
|
|
573
|
+
...edge
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function analyzePhysicalConnectivity(entries, rawOptions = {}) {
|
|
577
|
+
const exactGeometry = rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry;
|
|
578
|
+
const options = {
|
|
579
|
+
contactTolerance: rawOptions.contactTolerance ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance,
|
|
580
|
+
minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.minOverlapVolume,
|
|
581
|
+
exactGeometry,
|
|
582
|
+
mergeOverlappingBBoxes: rawOptions.mergeOverlappingBBoxes ?? !exactGeometry,
|
|
583
|
+
mergeTouchingBBoxes: rawOptions.mergeTouchingBBoxes ?? !exactGeometry
|
|
584
|
+
};
|
|
585
|
+
const warnings = [];
|
|
586
|
+
const edges = [];
|
|
587
|
+
const unionFind = new UnionFind$1(entries.length);
|
|
588
|
+
for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
|
|
589
|
+
const i = pair.sourceIndex;
|
|
590
|
+
const j = pair.targetIndex;
|
|
591
|
+
const a = entries[i];
|
|
592
|
+
const b = entries[j];
|
|
593
|
+
const bboxOverlaps = !hasPositiveGap(pair.gaps) && aabbInteriorOverlaps(a, b);
|
|
594
|
+
if (options.exactGeometry && bboxOverlaps) {
|
|
595
|
+
const overlap = intersectionVolume(a, b);
|
|
596
|
+
if (overlap.warning) warnings.push(overlap.warning);
|
|
597
|
+
if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
|
|
598
|
+
unionFind.union(i, j);
|
|
599
|
+
edges.push(
|
|
600
|
+
makeEdge(entries, i, j, {
|
|
601
|
+
kind: "overlap",
|
|
602
|
+
method: "boolean-intersection",
|
|
603
|
+
gap: 0,
|
|
604
|
+
overlapVolume: overlap.volume
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (bboxOverlaps && options.mergeOverlappingBBoxes) {
|
|
612
|
+
unionFind.union(i, j);
|
|
613
|
+
edges.push(
|
|
614
|
+
makeEdge(entries, i, j, {
|
|
615
|
+
kind: "overlap",
|
|
616
|
+
method: "bbox-overlap",
|
|
617
|
+
gap: 0,
|
|
618
|
+
overlapVolume: aabbOverlapVolume(a, b)
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
} else {
|
|
622
|
+
const contact = contactFromBBoxes(a, b, options.contactTolerance);
|
|
623
|
+
if (!contact.touching || !options.mergeTouchingBBoxes) continue;
|
|
624
|
+
unionFind.union(i, j);
|
|
625
|
+
edges.push(
|
|
626
|
+
makeEdge(entries, i, j, {
|
|
627
|
+
kind: "touching",
|
|
628
|
+
method: "bbox-contact",
|
|
629
|
+
gap: contact.gap,
|
|
630
|
+
axis: contact.axis
|
|
631
|
+
})
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const objects = entries.map((entry, index) => ({
|
|
636
|
+
index,
|
|
637
|
+
id: entry.id,
|
|
638
|
+
name: entry.name,
|
|
639
|
+
groupName: entry.groupName,
|
|
640
|
+
treePath: entry.treePath,
|
|
641
|
+
mock: entry.mock === true,
|
|
642
|
+
bodyCount: bodyCountForEntry(entry),
|
|
643
|
+
bbox: {
|
|
644
|
+
min: cloneVec3(entry.min),
|
|
645
|
+
max: cloneVec3(entry.max)
|
|
646
|
+
},
|
|
647
|
+
componentIndex: 0
|
|
648
|
+
}));
|
|
649
|
+
const componentByRoot = /* @__PURE__ */ new Map();
|
|
650
|
+
const rootToComponentIndex = /* @__PURE__ */ new Map();
|
|
651
|
+
for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
|
|
652
|
+
const root = unionFind.find(objectIndex);
|
|
653
|
+
let component = componentByRoot.get(root);
|
|
654
|
+
if (!component) {
|
|
655
|
+
component = {
|
|
656
|
+
index: componentByRoot.size + 1,
|
|
657
|
+
objectIndexes: [],
|
|
658
|
+
objectIds: [],
|
|
659
|
+
objectNames: [],
|
|
660
|
+
objectCount: 0,
|
|
661
|
+
bodyCount: 0,
|
|
662
|
+
bbox: emptyBBox$1()
|
|
663
|
+
};
|
|
664
|
+
componentByRoot.set(root, component);
|
|
665
|
+
rootToComponentIndex.set(root, component.index);
|
|
666
|
+
}
|
|
667
|
+
const object = objects[objectIndex];
|
|
668
|
+
object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
|
|
669
|
+
component.objectIndexes.push(object.index);
|
|
670
|
+
component.objectIds.push(object.id);
|
|
671
|
+
component.objectNames.push(object.name);
|
|
672
|
+
component.objectCount += 1;
|
|
673
|
+
component.bodyCount += object.bodyCount;
|
|
674
|
+
expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
|
|
675
|
+
}
|
|
676
|
+
const components = [...componentByRoot.values()];
|
|
677
|
+
return {
|
|
678
|
+
method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
|
|
679
|
+
options,
|
|
680
|
+
objectCount: objects.length,
|
|
681
|
+
componentCount: components.length,
|
|
682
|
+
objects,
|
|
683
|
+
components,
|
|
684
|
+
edges,
|
|
685
|
+
warnings
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const EPSILON = 1e-9;
|
|
689
|
+
function intervalGap(aMin, aMax, bMin, bMax) {
|
|
690
|
+
if (aMax < bMin) return bMin - aMax;
|
|
691
|
+
if (bMax < aMin) return aMin - bMax;
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
function bboxGap(a, b) {
|
|
695
|
+
const axisGaps = [
|
|
696
|
+
intervalGap(a.bbox.min[0], a.bbox.max[0], b.bbox.min[0], b.bbox.max[0]),
|
|
697
|
+
intervalGap(a.bbox.min[1], a.bbox.max[1], b.bbox.min[1], b.bbox.max[1]),
|
|
698
|
+
intervalGap(a.bbox.min[2], a.bbox.max[2], b.bbox.min[2], b.bbox.max[2])
|
|
699
|
+
];
|
|
700
|
+
const gap = Math.sqrt(axisGaps[0] ** 2 + axisGaps[1] ** 2 + axisGaps[2] ** 2);
|
|
701
|
+
return { gap, axisGaps };
|
|
702
|
+
}
|
|
703
|
+
function bboxVolume(component) {
|
|
704
|
+
const dx = Math.max(0, component.bbox.max[0] - component.bbox.min[0]);
|
|
705
|
+
const dy = Math.max(0, component.bbox.max[1] - component.bbox.min[1]);
|
|
706
|
+
const dz = Math.max(0, component.bbox.max[2] - component.bbox.min[2]);
|
|
707
|
+
return dx * dy * dz;
|
|
708
|
+
}
|
|
709
|
+
function compareDefaultRoot(a, b) {
|
|
710
|
+
if (a.bodyCount !== b.bodyCount) return a.bodyCount - b.bodyCount;
|
|
711
|
+
if (a.objectCount !== b.objectCount) return a.objectCount - b.objectCount;
|
|
712
|
+
const volumeDelta = bboxVolume(a) - bboxVolume(b);
|
|
713
|
+
if (Math.abs(volumeDelta) > EPSILON) return volumeDelta;
|
|
714
|
+
return b.index - a.index;
|
|
715
|
+
}
|
|
716
|
+
function defaultRootComponentIndex(components) {
|
|
717
|
+
if (components.length === 0) return null;
|
|
718
|
+
return components.reduce((best, component) => compareDefaultRoot(component, best) > 0 ? component : best, components[0]).index;
|
|
719
|
+
}
|
|
720
|
+
function componentPositionByIndex(components) {
|
|
721
|
+
return new Map(components.map((component, position) => [component.index, position]));
|
|
722
|
+
}
|
|
723
|
+
function computeNearestComponents(components) {
|
|
724
|
+
const nearest = components.map(() => ({ nearestGap: null, nearestComponentIndex: null }));
|
|
725
|
+
for (let sourcePosition = 0; sourcePosition < components.length; sourcePosition += 1) {
|
|
726
|
+
for (let targetPosition = sourcePosition + 1; targetPosition < components.length; targetPosition += 1) {
|
|
727
|
+
const sourceComponent = components[sourcePosition];
|
|
728
|
+
const targetComponent = components[targetPosition];
|
|
729
|
+
const edge = bboxGap(sourceComponent, targetComponent);
|
|
730
|
+
if (!Number.isFinite(edge.gap)) continue;
|
|
731
|
+
const source = nearest[sourcePosition];
|
|
732
|
+
if (source.nearestGap == null || edge.gap < source.nearestGap - EPSILON || Math.abs(edge.gap - source.nearestGap) <= EPSILON && targetComponent.index < (source.nearestComponentIndex ?? Infinity)) {
|
|
733
|
+
source.nearestGap = edge.gap;
|
|
734
|
+
source.nearestComponentIndex = targetComponent.index;
|
|
735
|
+
}
|
|
736
|
+
const target = nearest[targetPosition];
|
|
737
|
+
if (target.nearestGap == null || edge.gap < target.nearestGap - EPSILON || Math.abs(edge.gap - target.nearestGap) <= EPSILON && sourceComponent.index < (target.nearestComponentIndex ?? Infinity)) {
|
|
738
|
+
target.nearestGap = edge.gap;
|
|
739
|
+
target.nearestComponentIndex = sourceComponent.index;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return nearest;
|
|
744
|
+
}
|
|
745
|
+
function computeRootDistances(components, rootComponentIndex) {
|
|
746
|
+
if (rootComponentIndex == null) return [];
|
|
747
|
+
const positions = componentPositionByIndex(components);
|
|
748
|
+
const rootPosition = positions.get(rootComponentIndex);
|
|
749
|
+
if (rootPosition == null) {
|
|
750
|
+
throw new Error(`rootComponentIndex ${rootComponentIndex} does not match any physical component`);
|
|
751
|
+
}
|
|
752
|
+
const visited = components.map(() => false);
|
|
753
|
+
const distances = components.map(() => Infinity);
|
|
754
|
+
const parents = components.map(() => null);
|
|
755
|
+
const parentGaps = components.map(() => null);
|
|
756
|
+
distances[rootPosition] = 0;
|
|
757
|
+
for (; ; ) {
|
|
758
|
+
let current = -1;
|
|
759
|
+
for (let i = 0; i < components.length; i += 1) {
|
|
760
|
+
if (visited[i]) continue;
|
|
761
|
+
if (current === -1 || distances[i] < distances[current] - EPSILON || Math.abs(distances[i] - distances[current]) <= EPSILON && components[i].index < components[current].index) {
|
|
762
|
+
current = i;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (current === -1 || !Number.isFinite(distances[current])) break;
|
|
766
|
+
visited[current] = true;
|
|
767
|
+
for (let targetPosition = 0; targetPosition < components.length; targetPosition += 1) {
|
|
768
|
+
if (visited[targetPosition] || targetPosition === current) continue;
|
|
769
|
+
const edge = bboxGap(components[current], components[targetPosition]);
|
|
770
|
+
if (!Number.isFinite(edge.gap)) continue;
|
|
771
|
+
const nextDistance = distances[current] + edge.gap;
|
|
772
|
+
if (nextDistance < distances[targetPosition] - EPSILON || Math.abs(nextDistance - distances[targetPosition]) <= EPSILON && components[current].index < (parents[targetPosition] ?? Infinity)) {
|
|
773
|
+
distances[targetPosition] = nextDistance;
|
|
774
|
+
parents[targetPosition] = components[current].index;
|
|
775
|
+
parentGaps[targetPosition] = edge.gap;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return components.map((_, position) => ({
|
|
780
|
+
rootDistance: distances[position],
|
|
781
|
+
parentComponentIndex: parents[position],
|
|
782
|
+
parentGap: parentGaps[position]
|
|
783
|
+
}));
|
|
784
|
+
}
|
|
785
|
+
function makeGapEdge(source, target) {
|
|
786
|
+
const gap = bboxGap(source, target);
|
|
787
|
+
return {
|
|
788
|
+
sourceComponentIndex: source.index,
|
|
789
|
+
targetComponentIndex: target.index,
|
|
790
|
+
sourceObjectNames: [...source.objectNames],
|
|
791
|
+
targetObjectNames: [...target.objectNames],
|
|
792
|
+
gap: gap.gap,
|
|
793
|
+
axisGaps: gap.axisGaps
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function compactGapEdges(components, nearest, rooted) {
|
|
797
|
+
const componentByIndex = new Map(components.map((component) => [component.index, component]));
|
|
798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
799
|
+
const edges = [];
|
|
800
|
+
const add = (sourceIndex, targetIndex) => {
|
|
801
|
+
if (targetIndex == null || sourceIndex === targetIndex) return;
|
|
802
|
+
const source = componentByIndex.get(Math.min(sourceIndex, targetIndex));
|
|
803
|
+
const target = componentByIndex.get(Math.max(sourceIndex, targetIndex));
|
|
804
|
+
if (!source || !target) return;
|
|
805
|
+
const key = `${source.index}:${target.index}`;
|
|
806
|
+
if (seen.has(key)) return;
|
|
807
|
+
seen.add(key);
|
|
808
|
+
edges.push(makeGapEdge(source, target));
|
|
809
|
+
};
|
|
810
|
+
components.forEach((component, position) => {
|
|
811
|
+
var _a, _b;
|
|
812
|
+
add(component.index, ((_a = nearest[position]) == null ? void 0 : _a.nearestComponentIndex) ?? null);
|
|
813
|
+
add(component.index, ((_b = rooted[position]) == null ? void 0 : _b.parentComponentIndex) ?? null);
|
|
814
|
+
});
|
|
815
|
+
return edges.sort((a, b) => a.sourceComponentIndex - b.sourceComponentIndex || a.targetComponentIndex - b.targetComponentIndex);
|
|
816
|
+
}
|
|
817
|
+
function analyzeDistanceInspection(entries, rawOptions = {}) {
|
|
818
|
+
const connectivity = analyzePhysicalConnectivity(entries, rawOptions);
|
|
819
|
+
const rootComponentIndex = rawOptions.rootComponentIndex ?? defaultRootComponentIndex(connectivity.components);
|
|
820
|
+
const nearest = computeNearestComponents(connectivity.components);
|
|
821
|
+
const rooted = computeRootDistances(connectivity.components, rootComponentIndex);
|
|
822
|
+
const gapEdges = compactGapEdges(connectivity.components, nearest, rooted);
|
|
823
|
+
const componentByIndex = /* @__PURE__ */ new Map();
|
|
824
|
+
const components = connectivity.components.map((component, position) => {
|
|
825
|
+
var _a, _b;
|
|
826
|
+
const rootData = rooted[position] ?? {
|
|
827
|
+
rootDistance: rootComponentIndex === component.index ? 0 : Infinity,
|
|
828
|
+
parentComponentIndex: null,
|
|
829
|
+
parentGap: null
|
|
830
|
+
};
|
|
831
|
+
const decorated = {
|
|
832
|
+
...component,
|
|
833
|
+
isRoot: component.index === rootComponentIndex,
|
|
834
|
+
rootDistance: rootData.rootDistance,
|
|
835
|
+
nearestGap: ((_a = nearest[position]) == null ? void 0 : _a.nearestGap) ?? null,
|
|
836
|
+
nearestComponentIndex: ((_b = nearest[position]) == null ? void 0 : _b.nearestComponentIndex) ?? null,
|
|
837
|
+
parentComponentIndex: rootData.parentComponentIndex,
|
|
838
|
+
parentGap: rootData.parentGap
|
|
839
|
+
};
|
|
840
|
+
componentByIndex.set(component.index, decorated);
|
|
841
|
+
return decorated;
|
|
842
|
+
});
|
|
843
|
+
const objects = connectivity.objects.map((object) => {
|
|
844
|
+
const component = componentByIndex.get(object.componentIndex);
|
|
845
|
+
return {
|
|
846
|
+
...object,
|
|
847
|
+
rootDistance: (component == null ? void 0 : component.rootDistance) ?? Infinity,
|
|
848
|
+
nearestGap: (component == null ? void 0 : component.nearestGap) ?? null,
|
|
849
|
+
nearestComponentIndex: (component == null ? void 0 : component.nearestComponentIndex) ?? null,
|
|
850
|
+
parentComponentIndex: (component == null ? void 0 : component.parentComponentIndex) ?? null,
|
|
851
|
+
parentGap: (component == null ? void 0 : component.parentGap) ?? null
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
const finiteDistances = components.map((component) => component.rootDistance).filter(Number.isFinite);
|
|
855
|
+
const maxRootDistance = finiteDistances.length > 0 ? Math.max(...finiteDistances) : 0;
|
|
856
|
+
return {
|
|
857
|
+
method: "physical-component-bbox-gap-graph",
|
|
858
|
+
distanceMethod: "axis-aligned-bbox-gap",
|
|
859
|
+
options: {
|
|
860
|
+
contactTolerance: connectivity.options.contactTolerance,
|
|
861
|
+
minOverlapVolume: connectivity.options.minOverlapVolume,
|
|
862
|
+
rootComponentIndex
|
|
863
|
+
},
|
|
864
|
+
objectCount: connectivity.objectCount,
|
|
865
|
+
componentCount: connectivity.componentCount,
|
|
866
|
+
rootComponentIndex,
|
|
867
|
+
maxRootDistance,
|
|
868
|
+
gapEdgeCount: connectivity.components.length * (connectivity.components.length - 1) / 2,
|
|
869
|
+
objects,
|
|
870
|
+
components,
|
|
871
|
+
gapEdges,
|
|
872
|
+
connectivity: {
|
|
873
|
+
method: connectivity.method,
|
|
874
|
+
edges: connectivity.edges
|
|
875
|
+
},
|
|
876
|
+
warnings: [...connectivity.warnings]
|
|
877
|
+
};
|
|
878
|
+
}
|
|
457
879
|
const CAMERA_TOKEN_DIRECTIONS = {
|
|
458
880
|
front: [0, -1, 0.2],
|
|
459
881
|
back: [0, 1, 0.2],
|
|
@@ -589,6 +1011,805 @@ ${body}
|
|
|
589
1011
|
pathCount
|
|
590
1012
|
};
|
|
591
1013
|
}
|
|
1014
|
+
const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
|
|
1015
|
+
minThickness: 1.2,
|
|
1016
|
+
warnThickness: 2,
|
|
1017
|
+
maxThickness: 6,
|
|
1018
|
+
maxSamplesPerObject: 5e3
|
|
1019
|
+
};
|
|
1020
|
+
const THICKNESS_COLORS = {
|
|
1021
|
+
critical: [255, 28, 28],
|
|
1022
|
+
warning: [255, 150, 0],
|
|
1023
|
+
ok: [60, 220, 90],
|
|
1024
|
+
thick: [70, 145, 255],
|
|
1025
|
+
unknown: [90, 90, 90]
|
|
1026
|
+
};
|
|
1027
|
+
function finitePositive(value, fallback, label) {
|
|
1028
|
+
if (value === void 0) return fallback;
|
|
1029
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1030
|
+
throw new Error(`${label} must be a positive finite number.`);
|
|
1031
|
+
}
|
|
1032
|
+
return value;
|
|
1033
|
+
}
|
|
1034
|
+
function resolveThicknessInspectionOptions(raw = {}) {
|
|
1035
|
+
const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
|
|
1036
|
+
const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
|
|
1037
|
+
const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
|
|
1038
|
+
const maxSamplesPerObject = finitePositive(
|
|
1039
|
+
raw.maxSamplesPerObject,
|
|
1040
|
+
DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
|
|
1041
|
+
"maxSamplesPerObject"
|
|
1042
|
+
);
|
|
1043
|
+
if (minThickness > warnThickness) {
|
|
1044
|
+
throw new Error("minThickness must be less than or equal to warnThickness.");
|
|
1045
|
+
}
|
|
1046
|
+
if (warnThickness > maxThickness) {
|
|
1047
|
+
throw new Error("warnThickness must be less than or equal to maxThickness.");
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
minThickness,
|
|
1051
|
+
warnThickness,
|
|
1052
|
+
maxThickness,
|
|
1053
|
+
maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function lerp(a, b, t) {
|
|
1057
|
+
return a + (b - a) * Math.max(0, Math.min(1, t));
|
|
1058
|
+
}
|
|
1059
|
+
function lerpColor$1(a, b, t) {
|
|
1060
|
+
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))];
|
|
1061
|
+
}
|
|
1062
|
+
function thicknessClass(thickness, options) {
|
|
1063
|
+
if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
|
|
1064
|
+
if (thickness <= options.minThickness) return "critical";
|
|
1065
|
+
if (thickness <= options.warnThickness) return "warning";
|
|
1066
|
+
if (thickness <= options.maxThickness) return "ok";
|
|
1067
|
+
return "thick";
|
|
1068
|
+
}
|
|
1069
|
+
function thicknessColor(thickness, options) {
|
|
1070
|
+
const cls = thicknessClass(thickness, options);
|
|
1071
|
+
if (cls === "unknown") return THICKNESS_COLORS.unknown;
|
|
1072
|
+
if (cls === "critical") return THICKNESS_COLORS.critical;
|
|
1073
|
+
if (cls === "warning") {
|
|
1074
|
+
const span = Math.max(1e-9, options.warnThickness - options.minThickness);
|
|
1075
|
+
return lerpColor$1(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
|
|
1076
|
+
}
|
|
1077
|
+
if (cls === "ok") {
|
|
1078
|
+
const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
|
|
1079
|
+
return lerpColor$1(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
|
|
1080
|
+
}
|
|
1081
|
+
return THICKNESS_COLORS.thick;
|
|
1082
|
+
}
|
|
1083
|
+
function sampleArea(sample) {
|
|
1084
|
+
const area = sample.area ?? 1;
|
|
1085
|
+
return Number.isFinite(area) && area > 0 ? area : 1;
|
|
1086
|
+
}
|
|
1087
|
+
function weightedQuantile(samples, q) {
|
|
1088
|
+
if (samples.length === 0) return null;
|
|
1089
|
+
const sorted = [...samples].sort((a, b) => a.thickness - b.thickness);
|
|
1090
|
+
const totalArea = sorted.reduce((sum, sample) => sum + sample.area, 0);
|
|
1091
|
+
const target = totalArea * Math.max(0, Math.min(1, q));
|
|
1092
|
+
let cumulative = 0;
|
|
1093
|
+
for (const sample of sorted) {
|
|
1094
|
+
cumulative += sample.area;
|
|
1095
|
+
if (cumulative >= target) return sample.thickness;
|
|
1096
|
+
}
|
|
1097
|
+
return sorted[sorted.length - 1].thickness;
|
|
1098
|
+
}
|
|
1099
|
+
function percent(part, total) {
|
|
1100
|
+
if (total <= 0) return 0;
|
|
1101
|
+
return part / total * 100;
|
|
1102
|
+
}
|
|
1103
|
+
function summarizeThicknessSamples(samples, options) {
|
|
1104
|
+
const resolved = [];
|
|
1105
|
+
let totalArea = 0;
|
|
1106
|
+
let resolvedArea = 0;
|
|
1107
|
+
let unresolvedArea = 0;
|
|
1108
|
+
let criticalArea = 0;
|
|
1109
|
+
let warningArea = 0;
|
|
1110
|
+
let weightedSum = 0;
|
|
1111
|
+
for (const sample of samples) {
|
|
1112
|
+
const area = sampleArea(sample);
|
|
1113
|
+
totalArea += area;
|
|
1114
|
+
const value = sample.thickness;
|
|
1115
|
+
if (value == null || !Number.isFinite(value) || value <= 0) {
|
|
1116
|
+
unresolvedArea += area;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
resolved.push({ thickness: value, area });
|
|
1120
|
+
resolvedArea += area;
|
|
1121
|
+
weightedSum += value * area;
|
|
1122
|
+
if (value <= options.minThickness) {
|
|
1123
|
+
criticalArea += area;
|
|
1124
|
+
} else if (value <= options.warnThickness) {
|
|
1125
|
+
warningArea += area;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const values = resolved.map((sample) => sample.thickness);
|
|
1129
|
+
return {
|
|
1130
|
+
sampleCount: samples.length,
|
|
1131
|
+
resolvedCount: resolved.length,
|
|
1132
|
+
unresolvedCount: samples.length - resolved.length,
|
|
1133
|
+
minThickness: values.length > 0 ? Math.min(...values) : null,
|
|
1134
|
+
p05Thickness: weightedQuantile(resolved, 0.05),
|
|
1135
|
+
medianThickness: weightedQuantile(resolved, 0.5),
|
|
1136
|
+
meanThickness: resolvedArea > 0 ? weightedSum / resolvedArea : null,
|
|
1137
|
+
maxThickness: values.length > 0 ? Math.max(...values) : null,
|
|
1138
|
+
criticalAreaPercent: percent(criticalArea, resolvedArea),
|
|
1139
|
+
warningAreaPercent: percent(warningArea, resolvedArea),
|
|
1140
|
+
belowWarnAreaPercent: percent(criticalArea + warningArea, resolvedArea),
|
|
1141
|
+
unresolvedAreaPercent: percent(unresolvedArea, totalArea)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
const MIN_TRIANGLE_AREA = 1e-12;
|
|
1145
|
+
const R2_ALPHA = 0.7548776662466927;
|
|
1146
|
+
const R2_BETA = 0.5698402909980532;
|
|
1147
|
+
function readSurfaceTriangles(position) {
|
|
1148
|
+
const triangles = [];
|
|
1149
|
+
const a = new Vector3();
|
|
1150
|
+
const b = new Vector3();
|
|
1151
|
+
const c = new Vector3();
|
|
1152
|
+
const ac = new Vector3();
|
|
1153
|
+
const normal = new Vector3();
|
|
1154
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1155
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1156
|
+
const offset = tri * 3;
|
|
1157
|
+
a.fromBufferAttribute(position, offset);
|
|
1158
|
+
b.fromBufferAttribute(position, offset + 1);
|
|
1159
|
+
c.fromBufferAttribute(position, offset + 2);
|
|
1160
|
+
normal.subVectors(b, a).cross(ac.subVectors(c, a));
|
|
1161
|
+
const areaTwice = normal.length();
|
|
1162
|
+
if (areaTwice <= MIN_TRIANGLE_AREA) continue;
|
|
1163
|
+
triangles.push({
|
|
1164
|
+
index: tri,
|
|
1165
|
+
a: a.clone(),
|
|
1166
|
+
b: b.clone(),
|
|
1167
|
+
c: c.clone(),
|
|
1168
|
+
normal: normal.clone().multiplyScalar(1 / areaTwice),
|
|
1169
|
+
area: areaTwice * 0.5
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
return triangles;
|
|
1173
|
+
}
|
|
1174
|
+
function allocateAreaSampleCounts(triangles, maxSamples) {
|
|
1175
|
+
const sampleBudget = Math.max(1, Math.floor(maxSamples));
|
|
1176
|
+
const counts = new Array(triangles.length).fill(0);
|
|
1177
|
+
if (triangles.length === 0) return counts;
|
|
1178
|
+
const totalArea = triangles.reduce((sum, triangle) => sum + triangle.area, 0);
|
|
1179
|
+
if (!(totalArea > 0)) return counts;
|
|
1180
|
+
const remainders = triangles.map((triangle, index) => {
|
|
1181
|
+
const quota = triangle.area / totalArea * sampleBudget;
|
|
1182
|
+
const whole = Math.floor(quota);
|
|
1183
|
+
counts[index] += whole;
|
|
1184
|
+
return { index, fraction: quota - whole, area: triangle.area };
|
|
1185
|
+
});
|
|
1186
|
+
let left = sampleBudget - counts.reduce((sum, count) => sum + count, 0);
|
|
1187
|
+
remainders.sort((a, b) => b.fraction - a.fraction || b.area - a.area || a.index - b.index);
|
|
1188
|
+
for (const entry of remainders) {
|
|
1189
|
+
if (left <= 0) break;
|
|
1190
|
+
counts[entry.index] += 1;
|
|
1191
|
+
left -= 1;
|
|
1192
|
+
}
|
|
1193
|
+
return counts;
|
|
1194
|
+
}
|
|
1195
|
+
function sampleSurfaceTriangles(triangles, maxSamples) {
|
|
1196
|
+
const counts = allocateAreaSampleCounts(triangles, maxSamples);
|
|
1197
|
+
const samples = [];
|
|
1198
|
+
const position = new Vector3();
|
|
1199
|
+
let sampleIndex = 0;
|
|
1200
|
+
triangles.forEach((triangle, triangleListIndex) => {
|
|
1201
|
+
const count = counts[triangleListIndex];
|
|
1202
|
+
if (count <= 0) return;
|
|
1203
|
+
for (let localIndex = 0; localIndex < count; localIndex += 1) {
|
|
1204
|
+
const barycentric = triangleBarycentricSample(triangle.index, localIndex, sampleIndex);
|
|
1205
|
+
position.copy(triangle.a).multiplyScalar(barycentric[0]).addScaledVector(triangle.b, barycentric[1]).addScaledVector(triangle.c, barycentric[2]);
|
|
1206
|
+
samples.push({
|
|
1207
|
+
triangle,
|
|
1208
|
+
position: position.clone(),
|
|
1209
|
+
normal: triangle.normal.clone(),
|
|
1210
|
+
area: triangle.area / count,
|
|
1211
|
+
barycentric,
|
|
1212
|
+
sampleIndex,
|
|
1213
|
+
triangleSampleIndex: localIndex,
|
|
1214
|
+
triangleSampleCount: count
|
|
1215
|
+
});
|
|
1216
|
+
sampleIndex += 1;
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
return samples;
|
|
1220
|
+
}
|
|
1221
|
+
function totalSurfaceArea(triangles) {
|
|
1222
|
+
return triangles.reduce((sum, triangle) => sum + triangle.area, 0);
|
|
1223
|
+
}
|
|
1224
|
+
function triangleBarycentricSample(triangleIndex, triangleSampleIndex, sampleIndex) {
|
|
1225
|
+
const seedA = hash01((triangleIndex + 1) * 1013);
|
|
1226
|
+
const seedB = hash01((triangleIndex + 1) * 9176);
|
|
1227
|
+
const u = clampUnit(fract(seedA + (sampleIndex + 1) * R2_ALPHA));
|
|
1228
|
+
const v = clampUnit(fract(seedB + (triangleSampleIndex + 1) * R2_BETA));
|
|
1229
|
+
const root = Math.sqrt(u);
|
|
1230
|
+
return [1 - root, root * (1 - v), root * v];
|
|
1231
|
+
}
|
|
1232
|
+
function hash01(value) {
|
|
1233
|
+
return fract(Math.sin(value * 12.9898) * 43758.5453123);
|
|
1234
|
+
}
|
|
1235
|
+
function fract(value) {
|
|
1236
|
+
return value - Math.floor(value);
|
|
1237
|
+
}
|
|
1238
|
+
function clampUnit(value) {
|
|
1239
|
+
return Math.min(1 - 1e-9, Math.max(1e-9, value));
|
|
1240
|
+
}
|
|
1241
|
+
function cloneGeometryForFaceColors(geometry) {
|
|
1242
|
+
return geometry.index ? geometry.toNonIndexed() : geometry.clone();
|
|
1243
|
+
}
|
|
1244
|
+
function geometryMaxDimension(geometry) {
|
|
1245
|
+
geometry.computeBoundingBox();
|
|
1246
|
+
const box = geometry.boundingBox;
|
|
1247
|
+
if (!box) return 1;
|
|
1248
|
+
const size = new Vector3();
|
|
1249
|
+
box.getSize(size);
|
|
1250
|
+
return Math.max(1, size.x, size.y, size.z);
|
|
1251
|
+
}
|
|
1252
|
+
function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
|
|
1253
|
+
const origin = point.clone().addScaledVector(direction, epsilon);
|
|
1254
|
+
raycaster.set(origin, direction);
|
|
1255
|
+
raycaster.near = epsilon;
|
|
1256
|
+
raycaster.far = far;
|
|
1257
|
+
const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
|
|
1258
|
+
return hit ? hit.distance + epsilon : null;
|
|
1259
|
+
}
|
|
1260
|
+
function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
|
|
1261
|
+
const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
|
|
1262
|
+
const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
|
|
1263
|
+
if (forward == null) return backward;
|
|
1264
|
+
if (backward == null) return forward;
|
|
1265
|
+
return Math.min(forward, backward);
|
|
1266
|
+
}
|
|
1267
|
+
function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
|
|
1268
|
+
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
1269
|
+
const geometry = cloneGeometryForFaceColors(sourceGeometry);
|
|
1270
|
+
const position = geometry.getAttribute("position");
|
|
1271
|
+
if (!position || position.count < 3) {
|
|
1272
|
+
return {
|
|
1273
|
+
geometry,
|
|
1274
|
+
samples: [],
|
|
1275
|
+
pointSamples: [],
|
|
1276
|
+
triangleCount: 0,
|
|
1277
|
+
sampledTriangleCount: 0,
|
|
1278
|
+
sampleStride: 1,
|
|
1279
|
+
warnings: ["No triangle geometry."]
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1283
|
+
const surfaceTriangles = readSurfaceTriangles(position);
|
|
1284
|
+
const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
|
|
1285
|
+
const maxDim = geometryMaxDimension(geometry);
|
|
1286
|
+
const epsilon = Math.max(1e-4, maxDim * 1e-6);
|
|
1287
|
+
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
1288
|
+
const colors = new Float32Array(position.count * 3);
|
|
1289
|
+
const triangleThicknessValues = new Array(triangleCount).fill(void 0);
|
|
1290
|
+
const samples = [];
|
|
1291
|
+
const pointSamples = [];
|
|
1292
|
+
const warnings = [];
|
|
1293
|
+
const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
|
|
1294
|
+
const rayMesh = new Mesh(geometry, rayMaterial);
|
|
1295
|
+
const raycaster = new Raycaster();
|
|
1296
|
+
if (surfaceTriangles.length === 0) {
|
|
1297
|
+
warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
|
|
1298
|
+
} else if (surfaceSamples.length < surfaceTriangles.length) {
|
|
1299
|
+
warnings.push(
|
|
1300
|
+
`Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const sampledTriangleIndexes = /* @__PURE__ */ new Set();
|
|
1304
|
+
for (const sample of surfaceSamples) {
|
|
1305
|
+
sampledTriangleIndexes.add(sample.triangle.index);
|
|
1306
|
+
const thickness = triangleThickness(raycaster, rayMesh, sample.position, sample.normal, epsilon, far);
|
|
1307
|
+
samples.push({ thickness, area: sample.area });
|
|
1308
|
+
const previous = triangleThicknessValues[sample.triangle.index];
|
|
1309
|
+
if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
|
|
1310
|
+
triangleThicknessValues[sample.triangle.index] = thickness;
|
|
1311
|
+
}
|
|
1312
|
+
pointSamples.push({
|
|
1313
|
+
position: [sample.position.x, sample.position.y, sample.position.z],
|
|
1314
|
+
normal: [sample.normal.x, sample.normal.y, sample.normal.z],
|
|
1315
|
+
value: thickness,
|
|
1316
|
+
className: thicknessClass(thickness, options),
|
|
1317
|
+
color: thicknessColor(thickness, options),
|
|
1318
|
+
area: sample.area
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1322
|
+
const color = thicknessColor(triangleThicknessValues[tri], options);
|
|
1323
|
+
const offset = tri * 3;
|
|
1324
|
+
for (let vertex = 0; vertex < 3; vertex += 1) {
|
|
1325
|
+
const colorOffset = (offset + vertex) * 3;
|
|
1326
|
+
colors[colorOffset] = color[0] / 255;
|
|
1327
|
+
colors[colorOffset + 1] = color[1] / 255;
|
|
1328
|
+
colors[colorOffset + 2] = color[2] / 255;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
1332
|
+
rayMaterial.dispose();
|
|
1333
|
+
return {
|
|
1334
|
+
geometry,
|
|
1335
|
+
samples,
|
|
1336
|
+
pointSamples,
|
|
1337
|
+
triangleCount,
|
|
1338
|
+
sampledTriangleCount: sampledTriangleIndexes.size,
|
|
1339
|
+
sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
|
|
1340
|
+
warnings
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
|
|
1344
|
+
smoothAngleDeg: 5,
|
|
1345
|
+
sharpAngleDeg: 30,
|
|
1346
|
+
harshAngleDeg: 90,
|
|
1347
|
+
maxSamplesPerObject: 5e3
|
|
1348
|
+
};
|
|
1349
|
+
const ROUGHNESS_COLORS = {
|
|
1350
|
+
smooth: [62, 72, 84],
|
|
1351
|
+
moderate: [255, 214, 0],
|
|
1352
|
+
sharp: [255, 124, 34],
|
|
1353
|
+
harsh: [255, 42, 96]
|
|
1354
|
+
};
|
|
1355
|
+
function resolveRoughnessInspectionOptions(raw = {}) {
|
|
1356
|
+
const options = {
|
|
1357
|
+
smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
|
|
1358
|
+
sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
|
|
1359
|
+
harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg,
|
|
1360
|
+
maxSamplesPerObject: raw.maxSamplesPerObject ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.maxSamplesPerObject
|
|
1361
|
+
};
|
|
1362
|
+
if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
|
|
1363
|
+
throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
|
|
1364
|
+
}
|
|
1365
|
+
if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
|
|
1366
|
+
throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
|
|
1367
|
+
}
|
|
1368
|
+
if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
|
|
1369
|
+
throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
|
|
1370
|
+
}
|
|
1371
|
+
if (!Number.isFinite(options.maxSamplesPerObject) || options.maxSamplesPerObject <= 0) {
|
|
1372
|
+
throw new Error(`maxSamplesPerObject must be a positive finite number (got ${options.maxSamplesPerObject}).`);
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
...options,
|
|
1376
|
+
maxSamplesPerObject: Math.max(1, Math.floor(options.maxSamplesPerObject))
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function roughnessClassForAngle(angleDeg, options) {
|
|
1380
|
+
if (angleDeg >= options.harshAngleDeg) return "harsh";
|
|
1381
|
+
if (angleDeg >= options.sharpAngleDeg) return "sharp";
|
|
1382
|
+
if (angleDeg >= options.smoothAngleDeg) return "moderate";
|
|
1383
|
+
return "smooth";
|
|
1384
|
+
}
|
|
1385
|
+
function roughnessScoreForAngle(angleDeg, options) {
|
|
1386
|
+
if (angleDeg < options.sharpAngleDeg) return 0;
|
|
1387
|
+
if (angleDeg < options.harshAngleDeg) {
|
|
1388
|
+
return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
|
|
1389
|
+
}
|
|
1390
|
+
return 1;
|
|
1391
|
+
}
|
|
1392
|
+
function roughnessColorForAngle(angleDeg, options) {
|
|
1393
|
+
const cls = roughnessClassForAngle(angleDeg, options);
|
|
1394
|
+
if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
|
|
1395
|
+
if (cls === "moderate") {
|
|
1396
|
+
return lerpRgb(
|
|
1397
|
+
ROUGHNESS_COLORS.moderate,
|
|
1398
|
+
ROUGHNESS_COLORS.sharp,
|
|
1399
|
+
(angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
return lerpRgb(
|
|
1403
|
+
ROUGHNESS_COLORS.sharp,
|
|
1404
|
+
ROUGHNESS_COLORS.harsh,
|
|
1405
|
+
(angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
function lerpRgb(a, b, t) {
|
|
1409
|
+
const clamped = MathUtils.clamp(t, 0, 1);
|
|
1410
|
+
return [
|
|
1411
|
+
Math.round(MathUtils.lerp(a[0], b[0], clamped)),
|
|
1412
|
+
Math.round(MathUtils.lerp(a[1], b[1], clamped)),
|
|
1413
|
+
Math.round(MathUtils.lerp(a[2], b[2], clamped))
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
function emptyRoughnessSummary() {
|
|
1417
|
+
return {
|
|
1418
|
+
triangleCount: 0,
|
|
1419
|
+
edgeCount: 0,
|
|
1420
|
+
boundaryEdgeCount: 0,
|
|
1421
|
+
nonManifoldEdgeCount: 0,
|
|
1422
|
+
smoothAreaPercent: 0,
|
|
1423
|
+
moderateAreaPercent: 0,
|
|
1424
|
+
sharpAreaPercent: 0,
|
|
1425
|
+
harshAreaPercent: 0,
|
|
1426
|
+
roughAreaPercent: 0,
|
|
1427
|
+
meanAngleDeg: null,
|
|
1428
|
+
p50AngleDeg: null,
|
|
1429
|
+
p90AngleDeg: null,
|
|
1430
|
+
p95AngleDeg: null,
|
|
1431
|
+
p99AngleDeg: null,
|
|
1432
|
+
maxAngleDeg: null,
|
|
1433
|
+
qualityScore: 0
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function summarizeRoughnessTriangles(triangles, edgeAngles, edgeCount, boundaryEdgeCount, nonManifoldEdgeCount, options) {
|
|
1437
|
+
const areaByClass = {
|
|
1438
|
+
smooth: 0,
|
|
1439
|
+
moderate: 0,
|
|
1440
|
+
sharp: 0,
|
|
1441
|
+
harsh: 0
|
|
1442
|
+
};
|
|
1443
|
+
let totalArea = 0;
|
|
1444
|
+
for (const tri of triangles) {
|
|
1445
|
+
const area = Number.isFinite(tri.area) ? tri.area : 0;
|
|
1446
|
+
totalArea += area;
|
|
1447
|
+
areaByClass[roughnessClassForAngle(tri.maxAngleDeg, options)] += area;
|
|
1448
|
+
}
|
|
1449
|
+
const sortedAngles = [...edgeAngles].sort((lhs, rhs) => lhs - rhs);
|
|
1450
|
+
const meanAngleDeg = sortedAngles.length > 0 ? Number((sortedAngles.reduce((sum, angle) => sum + angle, 0) / sortedAngles.length).toFixed(2)) : null;
|
|
1451
|
+
const qualityScore = totalArea > 0 ? Math.round(
|
|
1452
|
+
MathUtils.clamp(
|
|
1453
|
+
100 * (areaByClass.smooth + areaByClass.moderate * 0.9) / totalArea - 50 * areaByClass.harsh / totalArea,
|
|
1454
|
+
0,
|
|
1455
|
+
100
|
|
1456
|
+
)
|
|
1457
|
+
) : 0;
|
|
1458
|
+
const safePercent = (value) => totalArea > 0 ? Number((value / totalArea * 100).toFixed(2)) : 0;
|
|
1459
|
+
return {
|
|
1460
|
+
triangleCount: triangles.length,
|
|
1461
|
+
edgeCount,
|
|
1462
|
+
boundaryEdgeCount,
|
|
1463
|
+
nonManifoldEdgeCount,
|
|
1464
|
+
smoothAreaPercent: safePercent(areaByClass.smooth),
|
|
1465
|
+
moderateAreaPercent: safePercent(areaByClass.moderate),
|
|
1466
|
+
sharpAreaPercent: safePercent(areaByClass.sharp),
|
|
1467
|
+
harshAreaPercent: safePercent(areaByClass.harsh),
|
|
1468
|
+
roughAreaPercent: safePercent(areaByClass.sharp + areaByClass.harsh),
|
|
1469
|
+
meanAngleDeg,
|
|
1470
|
+
p50AngleDeg: percentile(sortedAngles, 0.5),
|
|
1471
|
+
p90AngleDeg: percentile(sortedAngles, 0.9),
|
|
1472
|
+
p95AngleDeg: percentile(sortedAngles, 0.95),
|
|
1473
|
+
p99AngleDeg: percentile(sortedAngles, 0.99),
|
|
1474
|
+
maxAngleDeg: sortedAngles.length > 0 ? Number(sortedAngles[sortedAngles.length - 1].toFixed(2)) : null,
|
|
1475
|
+
qualityScore
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function percentile(sorted, q) {
|
|
1479
|
+
if (sorted.length === 0) return null;
|
|
1480
|
+
const index = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
|
|
1481
|
+
return Number(sorted[index].toFixed(2));
|
|
1482
|
+
}
|
|
1483
|
+
const DEG_PER_RAD = 180 / Math.PI;
|
|
1484
|
+
function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
1485
|
+
const options = resolveRoughnessInspectionOptions(rawOptions);
|
|
1486
|
+
const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
|
|
1487
|
+
const position = geometry.getAttribute("position");
|
|
1488
|
+
const warnings = [];
|
|
1489
|
+
if (!position || position.count < 3) {
|
|
1490
|
+
return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
1491
|
+
}
|
|
1492
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1493
|
+
const normals = new Array(triangleCount);
|
|
1494
|
+
const triangles = new Array(triangleCount);
|
|
1495
|
+
const triangleEdgeAngles = Array.from({ length: triangleCount }, () => [0, 0, 0]);
|
|
1496
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1497
|
+
const colors = new Float32Array(position.count * 3);
|
|
1498
|
+
const scores = new Float32Array(position.count);
|
|
1499
|
+
const a = new Vector3();
|
|
1500
|
+
const b = new Vector3();
|
|
1501
|
+
const c = new Vector3();
|
|
1502
|
+
const ac = new Vector3();
|
|
1503
|
+
const normal = new Vector3();
|
|
1504
|
+
const bbox = new Box3().setFromBufferAttribute(position);
|
|
1505
|
+
const snap = Math.max(1e-6, bbox.getSize(new Vector3()).length() * 1e-8);
|
|
1506
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1507
|
+
const offset = tri * 3;
|
|
1508
|
+
readVertex(position, offset, a);
|
|
1509
|
+
readVertex(position, offset + 1, b);
|
|
1510
|
+
readVertex(position, offset + 2, c);
|
|
1511
|
+
normal.subVectors(b, a).cross(ac.subVectors(c, a));
|
|
1512
|
+
const areaTwice = normal.length();
|
|
1513
|
+
triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
|
|
1514
|
+
normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
|
|
1515
|
+
const keys = [vertexKey$1(a, snap), vertexKey$1(b, snap), vertexKey$1(c, snap)];
|
|
1516
|
+
for (let edge = 0; edge < 3; edge += 1) {
|
|
1517
|
+
const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
|
|
1518
|
+
let record = edges.get(key);
|
|
1519
|
+
if (!record) {
|
|
1520
|
+
record = { triangles: [], triangleEdges: [] };
|
|
1521
|
+
edges.set(key, record);
|
|
1522
|
+
}
|
|
1523
|
+
record.triangles.push(tri);
|
|
1524
|
+
record.triangleEdges.push({ triangle: tri, edge });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles);
|
|
1528
|
+
if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
|
|
1529
|
+
if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
|
|
1530
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1531
|
+
const { maxAngleDeg } = triangles[tri];
|
|
1532
|
+
const color = roughnessColorForAngle(maxAngleDeg, options);
|
|
1533
|
+
const score = roughnessScoreForAngle(maxAngleDeg, options);
|
|
1534
|
+
const offset = tri * 3;
|
|
1535
|
+
for (let vertex = 0; vertex < 3; vertex += 1) {
|
|
1536
|
+
const colorOffset = (offset + vertex) * 3;
|
|
1537
|
+
colors[colorOffset] = color[0] / 255;
|
|
1538
|
+
colors[colorOffset + 1] = color[1] / 255;
|
|
1539
|
+
colors[colorOffset + 2] = color[2] / 255;
|
|
1540
|
+
scores[offset + vertex] = score;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const surfaceTriangles = readSurfaceTriangles(position);
|
|
1544
|
+
const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
|
|
1545
|
+
const edgeInfluenceRadius = roughnessEdgeInfluenceRadius(surfaceTriangles, surfaceSamples.length);
|
|
1546
|
+
const pointSamples = surfaceSamples.map((sample) => {
|
|
1547
|
+
const angleDeg = roughnessAngleAtSample(
|
|
1548
|
+
sample.position,
|
|
1549
|
+
sample.triangle,
|
|
1550
|
+
triangleEdgeAngles[sample.triangle.index],
|
|
1551
|
+
edgeInfluenceRadius
|
|
1552
|
+
);
|
|
1553
|
+
const color = roughnessColorForAngle(angleDeg, options);
|
|
1554
|
+
return {
|
|
1555
|
+
position: [sample.position.x, sample.position.y, sample.position.z],
|
|
1556
|
+
normal: [sample.normal.x, sample.normal.y, sample.normal.z],
|
|
1557
|
+
value: angleDeg,
|
|
1558
|
+
className: roughnessClassForAngle(angleDeg, options),
|
|
1559
|
+
color,
|
|
1560
|
+
area: sample.area
|
|
1561
|
+
};
|
|
1562
|
+
});
|
|
1563
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
1564
|
+
geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
|
|
1565
|
+
geometry.computeBoundingBox();
|
|
1566
|
+
return {
|
|
1567
|
+
geometry,
|
|
1568
|
+
pointSamples,
|
|
1569
|
+
summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
|
|
1570
|
+
warnings
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
|
|
1574
|
+
const edgeAngles = [];
|
|
1575
|
+
let boundaryEdgeCount = 0;
|
|
1576
|
+
let nonManifoldEdgeCount = 0;
|
|
1577
|
+
for (const edge of edges.values()) {
|
|
1578
|
+
if (edge.triangles.length === 1) {
|
|
1579
|
+
boundaryEdgeCount += 1;
|
|
1580
|
+
markEdge(edge, triangles, triangleEdgeAngles, 180);
|
|
1581
|
+
edgeAngles.push(180);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (edge.triangles.length > 2) {
|
|
1585
|
+
nonManifoldEdgeCount += 1;
|
|
1586
|
+
markEdge(edge, triangles, triangleEdgeAngles, 180);
|
|
1587
|
+
edgeAngles.push(180);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
const [first, second] = edge.triangles;
|
|
1591
|
+
const dot = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
|
|
1592
|
+
const angleDeg = Math.acos(dot) * DEG_PER_RAD;
|
|
1593
|
+
markEdge(edge, triangles, triangleEdgeAngles, angleDeg);
|
|
1594
|
+
edgeAngles.push(angleDeg);
|
|
1595
|
+
}
|
|
1596
|
+
return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
|
|
1597
|
+
}
|
|
1598
|
+
function readVertex(position, index, target) {
|
|
1599
|
+
target.set(position.getX(index), position.getY(index), position.getZ(index));
|
|
1600
|
+
}
|
|
1601
|
+
function vertexKey$1(point, snap) {
|
|
1602
|
+
return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
|
|
1603
|
+
}
|
|
1604
|
+
function edgeKey(a, b) {
|
|
1605
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
1606
|
+
}
|
|
1607
|
+
function markEdge(edge, triangles, triangleEdgeAngles, angleDeg) {
|
|
1608
|
+
for (const { triangle, edge: edgeIndex } of edge.triangleEdges) {
|
|
1609
|
+
triangles[triangle].maxAngleDeg = Math.max(triangles[triangle].maxAngleDeg, angleDeg);
|
|
1610
|
+
triangleEdgeAngles[triangle][edgeIndex] = Math.max(triangleEdgeAngles[triangle][edgeIndex], angleDeg);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function roughnessEdgeInfluenceRadius(triangles, sampleCount) {
|
|
1614
|
+
if (triangles.length === 0 || sampleCount <= 0) return 0;
|
|
1615
|
+
return Math.sqrt(totalSurfaceArea(triangles) / sampleCount);
|
|
1616
|
+
}
|
|
1617
|
+
function roughnessAngleAtSample(point, triangle, edgeAngles, influenceRadius) {
|
|
1618
|
+
let angle = 0;
|
|
1619
|
+
for (let edge = 0; edge < 3; edge += 1) {
|
|
1620
|
+
const edgeAngle = edgeAngles[edge] ?? 0;
|
|
1621
|
+
if (edgeAngle <= 0) continue;
|
|
1622
|
+
const distance = pointDistanceToTriangleEdge(point, triangle, edge);
|
|
1623
|
+
if (distance <= influenceRadius) angle = Math.max(angle, edgeAngle);
|
|
1624
|
+
}
|
|
1625
|
+
return angle;
|
|
1626
|
+
}
|
|
1627
|
+
function pointDistanceToTriangleEdge(point, triangle, edge) {
|
|
1628
|
+
if (edge === 0) return pointSegmentDistance(point, triangle.a, triangle.b);
|
|
1629
|
+
if (edge === 1) return pointSegmentDistance(point, triangle.b, triangle.c);
|
|
1630
|
+
return pointSegmentDistance(point, triangle.c, triangle.a);
|
|
1631
|
+
}
|
|
1632
|
+
function pointSegmentDistance(point, start, end) {
|
|
1633
|
+
const segment = end.clone().sub(start);
|
|
1634
|
+
const lengthSq = segment.lengthSq();
|
|
1635
|
+
if (lengthSq <= 1e-18) return point.distanceTo(start);
|
|
1636
|
+
const t = MathUtils.clamp(point.clone().sub(start).dot(segment) / lengthSq, 0, 1);
|
|
1637
|
+
return point.distanceTo(segment.multiplyScalar(t).add(start));
|
|
1638
|
+
}
|
|
1639
|
+
const DEFAULT_VERTEX_TOLERANCE = 1e-5;
|
|
1640
|
+
class UnionFind2 {
|
|
1641
|
+
constructor(size) {
|
|
1642
|
+
__publicField(this, "parent");
|
|
1643
|
+
__publicField(this, "rank");
|
|
1644
|
+
this.parent = Array.from({ length: size }, (_, index) => index);
|
|
1645
|
+
this.rank = Array.from({ length: size }, () => 0);
|
|
1646
|
+
}
|
|
1647
|
+
find(value) {
|
|
1648
|
+
const parent = this.parent[value];
|
|
1649
|
+
if (parent === value) return value;
|
|
1650
|
+
const root = this.find(parent);
|
|
1651
|
+
this.parent[value] = root;
|
|
1652
|
+
return root;
|
|
1653
|
+
}
|
|
1654
|
+
union(a, b) {
|
|
1655
|
+
const rootA = this.find(a);
|
|
1656
|
+
const rootB = this.find(b);
|
|
1657
|
+
if (rootA === rootB) return;
|
|
1658
|
+
if (this.rank[rootA] < this.rank[rootB]) {
|
|
1659
|
+
this.parent[rootA] = rootB;
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (this.rank[rootA] > this.rank[rootB]) {
|
|
1663
|
+
this.parent[rootB] = rootA;
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
this.parent[rootB] = rootA;
|
|
1667
|
+
this.rank[rootA] += 1;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function emptyBBox() {
|
|
1671
|
+
return {
|
|
1672
|
+
min: [Infinity, Infinity, Infinity],
|
|
1673
|
+
max: [-Infinity, -Infinity, -Infinity]
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function expandBBox(bbox, x, y, z) {
|
|
1677
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return;
|
|
1678
|
+
bbox.min[0] = Math.min(bbox.min[0], x);
|
|
1679
|
+
bbox.min[1] = Math.min(bbox.min[1], y);
|
|
1680
|
+
bbox.min[2] = Math.min(bbox.min[2], z);
|
|
1681
|
+
bbox.max[0] = Math.max(bbox.max[0], x);
|
|
1682
|
+
bbox.max[1] = Math.max(bbox.max[1], y);
|
|
1683
|
+
bbox.max[2] = Math.max(bbox.max[2], z);
|
|
1684
|
+
}
|
|
1685
|
+
function vertexKey(positions, vertexIndex, tolerance) {
|
|
1686
|
+
const base = vertexIndex * 3;
|
|
1687
|
+
const x = positions[base];
|
|
1688
|
+
const y = positions[base + 1];
|
|
1689
|
+
const z = positions[base + 2];
|
|
1690
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
|
|
1691
|
+
return `${Math.round(x / tolerance)},${Math.round(y / tolerance)},${Math.round(z / tolerance)}`;
|
|
1692
|
+
}
|
|
1693
|
+
function analyzeMeshConnectedComponents(positions, vertexTolerance = DEFAULT_VERTEX_TOLERANCE) {
|
|
1694
|
+
const vertexCount = Math.floor(positions.length / 3);
|
|
1695
|
+
const triangleCount = Math.floor(vertexCount / 3);
|
|
1696
|
+
const vertexComponentIndexes = new Int32Array(vertexCount);
|
|
1697
|
+
if (triangleCount === 0) return { components: [], vertexComponentIndexes };
|
|
1698
|
+
const tolerance = Number.isFinite(vertexTolerance) && vertexTolerance > 0 ? vertexTolerance : DEFAULT_VERTEX_TOLERANCE;
|
|
1699
|
+
const unionFind = new UnionFind2(triangleCount);
|
|
1700
|
+
const vertexOwnerByKey = /* @__PURE__ */ new Map();
|
|
1701
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
1702
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
1703
|
+
const key = vertexKey(positions, triangleIndex * 3 + corner, tolerance);
|
|
1704
|
+
if (key == null) continue;
|
|
1705
|
+
const owner = vertexOwnerByKey.get(key);
|
|
1706
|
+
if (owner === void 0) {
|
|
1707
|
+
vertexOwnerByKey.set(key, triangleIndex);
|
|
1708
|
+
} else {
|
|
1709
|
+
unionFind.union(triangleIndex, owner);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const componentIndexByRoot = /* @__PURE__ */ new Map();
|
|
1714
|
+
const components = [];
|
|
1715
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
1716
|
+
const root = unionFind.find(triangleIndex);
|
|
1717
|
+
let componentIndex = componentIndexByRoot.get(root);
|
|
1718
|
+
if (componentIndex === void 0) {
|
|
1719
|
+
componentIndex = components.length;
|
|
1720
|
+
componentIndexByRoot.set(root, componentIndex);
|
|
1721
|
+
components.push({
|
|
1722
|
+
index: componentIndex,
|
|
1723
|
+
triangleIndexes: [],
|
|
1724
|
+
vertexCount: 0,
|
|
1725
|
+
bbox: emptyBBox()
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const component = components[componentIndex];
|
|
1729
|
+
component.triangleIndexes.push(triangleIndex);
|
|
1730
|
+
component.vertexCount += 3;
|
|
1731
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
1732
|
+
const vertexIndex = triangleIndex * 3 + corner;
|
|
1733
|
+
const base = vertexIndex * 3;
|
|
1734
|
+
vertexComponentIndexes[vertexIndex] = componentIndex;
|
|
1735
|
+
expandBBox(component.bbox, positions[base], positions[base + 1], positions[base + 2]);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return { components, vertexComponentIndexes };
|
|
1739
|
+
}
|
|
1740
|
+
function isFiniteComponent(component) {
|
|
1741
|
+
return component.bbox.min.every(Number.isFinite) && component.bbox.max.every(Number.isFinite);
|
|
1742
|
+
}
|
|
1743
|
+
function objectConnectivityEntry(object) {
|
|
1744
|
+
return {
|
|
1745
|
+
id: object.id,
|
|
1746
|
+
name: object.name,
|
|
1747
|
+
shape: object.shape,
|
|
1748
|
+
min: object.min,
|
|
1749
|
+
max: object.max,
|
|
1750
|
+
groupName: object.groupName,
|
|
1751
|
+
treePath: object.treePath,
|
|
1752
|
+
mock: object.mock,
|
|
1753
|
+
bodyCount: 1
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function buildMeshBodyConnectivityInput(objects, options) {
|
|
1757
|
+
const entries = [];
|
|
1758
|
+
const bodyIdsByObjectId = /* @__PURE__ */ new Map();
|
|
1759
|
+
const vertexComponentIndexesByObjectId = /* @__PURE__ */ new Map();
|
|
1760
|
+
for (const object of objects) {
|
|
1761
|
+
if (!object.positions || object.positions.length < 9) {
|
|
1762
|
+
entries.push(objectConnectivityEntry(object));
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const meshComponents = analyzeMeshConnectedComponents(object.positions);
|
|
1766
|
+
const finiteComponents = meshComponents.components.filter(isFiniteComponent);
|
|
1767
|
+
if (finiteComponents.length <= 1) {
|
|
1768
|
+
entries.push(objectConnectivityEntry(object));
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const bodyIds = Array.from({ length: meshComponents.components.length }, (_, index) => `${object.id}:body:${index + 1}`);
|
|
1772
|
+
for (const component of finiteComponents) {
|
|
1773
|
+
entries.push({
|
|
1774
|
+
id: bodyIds[component.index],
|
|
1775
|
+
name: `${object.name} body ${component.index + 1}`,
|
|
1776
|
+
shape: options.bodyShape(object, component),
|
|
1777
|
+
min: component.bbox.min,
|
|
1778
|
+
max: component.bbox.max,
|
|
1779
|
+
groupName: object.groupName,
|
|
1780
|
+
treePath: object.treePath,
|
|
1781
|
+
mock: object.mock,
|
|
1782
|
+
bodyCount: 1
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
bodyIdsByObjectId.set(object.id, bodyIds);
|
|
1786
|
+
vertexComponentIndexesByObjectId.set(object.id, meshComponents.vertexComponentIndexes);
|
|
1787
|
+
}
|
|
1788
|
+
return { entries, bodyIdsByObjectId, vertexComponentIndexesByObjectId };
|
|
1789
|
+
}
|
|
1790
|
+
function meshVertexColorBuffersFor(input, colorForEntryId) {
|
|
1791
|
+
const out = /* @__PURE__ */ new Map();
|
|
1792
|
+
const colorByEntryId = /* @__PURE__ */ new Map();
|
|
1793
|
+
for (const [objectId, vertexComponentIndexes] of input.vertexComponentIndexesByObjectId) {
|
|
1794
|
+
const bodyIds = input.bodyIdsByObjectId.get(objectId);
|
|
1795
|
+
if (!bodyIds) continue;
|
|
1796
|
+
const colors = new Float32Array(vertexComponentIndexes.length * 3);
|
|
1797
|
+
for (let vertexIndex = 0; vertexIndex < vertexComponentIndexes.length; vertexIndex += 1) {
|
|
1798
|
+
const bodyId = bodyIds[vertexComponentIndexes[vertexIndex]];
|
|
1799
|
+
let color = colorByEntryId.get(bodyId);
|
|
1800
|
+
if (!color) {
|
|
1801
|
+
color = colorForEntryId(bodyId);
|
|
1802
|
+
colorByEntryId.set(bodyId, color);
|
|
1803
|
+
}
|
|
1804
|
+
const offset = vertexIndex * 3;
|
|
1805
|
+
colors[offset] = color[0];
|
|
1806
|
+
colors[offset + 1] = color[1];
|
|
1807
|
+
colors[offset + 2] = color[2];
|
|
1808
|
+
}
|
|
1809
|
+
out.set(objectId, colors);
|
|
1810
|
+
}
|
|
1811
|
+
return out;
|
|
1812
|
+
}
|
|
592
1813
|
const canvas = document.getElementById("canvas");
|
|
593
1814
|
const exportCanvas = document.createElement("canvas");
|
|
594
1815
|
const exportCtx = exportCanvas.getContext("2d");
|
|
@@ -622,6 +1843,14 @@ function createCaptureDebugLogger(enabled) {
|
|
|
622
1843
|
console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
|
|
623
1844
|
};
|
|
624
1845
|
}
|
|
1846
|
+
class EmptyInspectionShape {
|
|
1847
|
+
intersect() {
|
|
1848
|
+
return {
|
|
1849
|
+
isEmpty: () => true,
|
|
1850
|
+
volume: () => 0
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
625
1854
|
const COLLISION_SOURCE_OPACITY = 0.22;
|
|
626
1855
|
const COLLISION_SOURCE_COLOR = [180, 200, 220];
|
|
627
1856
|
const COLLISION_HIGHLIGHT_COLOR = [255, 68, 16];
|
|
@@ -665,6 +1894,31 @@ const MASK_PALETTE = [
|
|
|
665
1894
|
[0, 0, 128],
|
|
666
1895
|
[128, 128, 128]
|
|
667
1896
|
];
|
|
1897
|
+
function cloneShapePositions(shape) {
|
|
1898
|
+
const geometry = shapeToGeometry(shape);
|
|
1899
|
+
try {
|
|
1900
|
+
const position = geometry.solid.getAttribute("position");
|
|
1901
|
+
if (!position) return void 0;
|
|
1902
|
+
return new Float32Array(position.array);
|
|
1903
|
+
} finally {
|
|
1904
|
+
geometry.solid.dispose();
|
|
1905
|
+
geometry.edges.dispose();
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function meshConnectivitySource(entry) {
|
|
1909
|
+
const bb = entry.shape.boundingBox();
|
|
1910
|
+
return {
|
|
1911
|
+
id: entry.source.id,
|
|
1912
|
+
name: entry.source.name,
|
|
1913
|
+
shape: entry.shape,
|
|
1914
|
+
min: [bb.min[0], bb.min[1], bb.min[2]],
|
|
1915
|
+
max: [bb.max[0], bb.max[1], bb.max[2]],
|
|
1916
|
+
groupName: entry.source.groupName,
|
|
1917
|
+
treePath: entry.source.treePath,
|
|
1918
|
+
mock: entry.source.mock,
|
|
1919
|
+
positions: cloneShapePositions(entry.shape)
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
668
1922
|
function summarizeSceneGeometry(entries) {
|
|
669
1923
|
const min = [Infinity, Infinity, Infinity];
|
|
670
1924
|
const max = [-Infinity, -Infinity, -Infinity];
|
|
@@ -1038,6 +2292,17 @@ function distanceColorForRootDistance(distance, maxDistance) {
|
|
|
1038
2292
|
if (t <= 0.5) return lerpColor(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2);
|
|
1039
2293
|
return lerpColor(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2);
|
|
1040
2294
|
}
|
|
2295
|
+
function rgbFloats(color) {
|
|
2296
|
+
return [color[0] / 255, color[1] / 255, color[2] / 255];
|
|
2297
|
+
}
|
|
2298
|
+
function geometryWithVertexColors(renderable, colors) {
|
|
2299
|
+
if (!colors) return null;
|
|
2300
|
+
const position = renderable.solid.geometry.getAttribute("position");
|
|
2301
|
+
if (!position || colors.length !== position.count * 3) return null;
|
|
2302
|
+
const geometry = renderable.solid.geometry.clone();
|
|
2303
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
2304
|
+
return geometry;
|
|
2305
|
+
}
|
|
1041
2306
|
function buildMaskEntries(session) {
|
|
1042
2307
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
1043
2308
|
return session.renderables.map((renderable, idx) => {
|
|
@@ -1140,30 +2405,26 @@ function renderCurrentConnectivity(session, rawReport) {
|
|
|
1140
2405
|
const r = getRenderer(session.size, session.pixelRatio);
|
|
1141
2406
|
const report = rawReport ?? analyzeSessionConnectivity(session);
|
|
1142
2407
|
const byId = new Map(report.objects.map((object) => [object.id, object]));
|
|
2408
|
+
const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
|
|
2409
|
+
const object = byId.get(entryId);
|
|
2410
|
+
return rgbFloats(object ? maskColorForIndex(object.componentIndex) : [128, 128, 128]);
|
|
2411
|
+
}) : /* @__PURE__ */ new Map();
|
|
1143
2412
|
const replacements = session.renderables.map((renderable) => {
|
|
1144
2413
|
const object = byId.get(renderable.id);
|
|
1145
2414
|
const color = object ? maskColorForIndex(object.componentIndex) : [128, 128, 128];
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
`,
|
|
1155
|
-
fragmentShader: `
|
|
1156
|
-
precision highp float;
|
|
1157
|
-
uniform vec3 connectivityColor;
|
|
1158
|
-
void main() {
|
|
1159
|
-
gl_FragColor = vec4(connectivityColor, 1.0);
|
|
1160
|
-
}
|
|
1161
|
-
`
|
|
2415
|
+
const previousMaterial = renderable.solid.material;
|
|
2416
|
+
const previousGeometry = renderable.solid.geometry;
|
|
2417
|
+
const vertexGeometry = geometryWithVertexColors(renderable, vertexColorsById.get(renderable.id));
|
|
2418
|
+
const material = new MeshBasicMaterial({
|
|
2419
|
+
color: vertexGeometry ? 16777215 : new Color(color[0] / 255, color[1] / 255, color[2] / 255),
|
|
2420
|
+
vertexColors: Boolean(vertexGeometry),
|
|
2421
|
+
side: DoubleSide,
|
|
2422
|
+
clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
|
|
1162
2423
|
});
|
|
1163
2424
|
material.toneMapped = false;
|
|
1164
|
-
|
|
2425
|
+
if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
|
|
1165
2426
|
renderable.solid.material = material;
|
|
1166
|
-
return { renderable, previousMaterial, material };
|
|
2427
|
+
return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
|
|
1167
2428
|
});
|
|
1168
2429
|
try {
|
|
1169
2430
|
const png = withSolidOnlyVisibility(
|
|
@@ -1176,8 +2437,10 @@ function renderCurrentConnectivity(session, rawReport) {
|
|
|
1176
2437
|
);
|
|
1177
2438
|
return { png, report: decorateConnectivityReport(report) };
|
|
1178
2439
|
} finally {
|
|
1179
|
-
replacements.forEach(({ renderable, previousMaterial, material }) => {
|
|
2440
|
+
replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
|
|
1180
2441
|
renderable.solid.material = previousMaterial;
|
|
2442
|
+
renderable.solid.geometry = previousGeometry;
|
|
2443
|
+
vertexGeometry == null ? void 0 : vertexGeometry.dispose();
|
|
1181
2444
|
material.dispose();
|
|
1182
2445
|
});
|
|
1183
2446
|
}
|
|
@@ -1236,30 +2499,26 @@ function renderCurrentDistance(session) {
|
|
|
1236
2499
|
const r = getRenderer(session.size, session.pixelRatio);
|
|
1237
2500
|
const report = decorateDistanceReport(analyzeSessionDistance(session));
|
|
1238
2501
|
const byId = new Map(report.objects.map((object) => [object.id, object]));
|
|
2502
|
+
const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
|
|
2503
|
+
var _a;
|
|
2504
|
+
return rgbFloats(((_a = byId.get(entryId)) == null ? void 0 : _a.color) ?? DISTANCE_UNKNOWN_COLOR);
|
|
2505
|
+
}) : /* @__PURE__ */ new Map();
|
|
1239
2506
|
const replacements = session.renderables.map((renderable) => {
|
|
1240
2507
|
const object = byId.get(renderable.id);
|
|
1241
2508
|
const color = (object == null ? void 0 : object.color) ?? DISTANCE_UNKNOWN_COLOR;
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
`,
|
|
1251
|
-
fragmentShader: `
|
|
1252
|
-
precision highp float;
|
|
1253
|
-
uniform vec3 distanceColor;
|
|
1254
|
-
void main() {
|
|
1255
|
-
gl_FragColor = vec4(distanceColor, 1.0);
|
|
1256
|
-
}
|
|
1257
|
-
`
|
|
2509
|
+
const previousMaterial = renderable.solid.material;
|
|
2510
|
+
const previousGeometry = renderable.solid.geometry;
|
|
2511
|
+
const vertexGeometry = geometryWithVertexColors(renderable, vertexColorsById.get(renderable.id));
|
|
2512
|
+
const material = new MeshBasicMaterial({
|
|
2513
|
+
color: vertexGeometry ? 16777215 : new Color(color[0] / 255, color[1] / 255, color[2] / 255),
|
|
2514
|
+
vertexColors: Boolean(vertexGeometry),
|
|
2515
|
+
side: DoubleSide,
|
|
2516
|
+
clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
|
|
1258
2517
|
});
|
|
1259
2518
|
material.toneMapped = false;
|
|
1260
|
-
|
|
2519
|
+
if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
|
|
1261
2520
|
renderable.solid.material = material;
|
|
1262
|
-
return { renderable, previousMaterial, material };
|
|
2521
|
+
return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
|
|
1263
2522
|
});
|
|
1264
2523
|
try {
|
|
1265
2524
|
const png = withSolidOnlyVisibility(
|
|
@@ -1272,8 +2531,10 @@ function renderCurrentDistance(session) {
|
|
|
1272
2531
|
);
|
|
1273
2532
|
return { png, report };
|
|
1274
2533
|
} finally {
|
|
1275
|
-
replacements.forEach(({ renderable, previousMaterial, material }) => {
|
|
2534
|
+
replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
|
|
1276
2535
|
renderable.solid.material = previousMaterial;
|
|
2536
|
+
renderable.solid.geometry = previousGeometry;
|
|
2537
|
+
vertexGeometry == null ? void 0 : vertexGeometry.dispose();
|
|
1277
2538
|
material.dispose();
|
|
1278
2539
|
});
|
|
1279
2540
|
}
|
|
@@ -1373,26 +2634,113 @@ function renderCurrentCollisions(session) {
|
|
|
1373
2634
|
collisionMaterials.forEach((material) => material.dispose());
|
|
1374
2635
|
}
|
|
1375
2636
|
}
|
|
1376
|
-
function
|
|
2637
|
+
function inspectionOptionsKey(value) {
|
|
2638
|
+
return JSON.stringify(value ?? {});
|
|
2639
|
+
}
|
|
2640
|
+
function bboxFromGeometry(geometry) {
|
|
2641
|
+
geometry.computeBoundingBox();
|
|
2642
|
+
const bbox = geometry.boundingBox;
|
|
2643
|
+
return {
|
|
2644
|
+
min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
|
|
2645
|
+
max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
function pointBuffersFromSamples(samples, offset = 0.025) {
|
|
2649
|
+
const positions = new Float32Array(samples.length * 3);
|
|
2650
|
+
const colors = new Float32Array(samples.length * 3);
|
|
2651
|
+
samples.forEach((sample, index) => {
|
|
2652
|
+
const base = index * 3;
|
|
2653
|
+
positions[base] = sample.position[0] + sample.normal[0] * offset;
|
|
2654
|
+
positions[base + 1] = sample.position[1] + sample.normal[1] * offset;
|
|
2655
|
+
positions[base + 2] = sample.position[2] + sample.normal[2] * offset;
|
|
2656
|
+
colors[base] = sample.color[0] / 255;
|
|
2657
|
+
colors[base + 1] = sample.color[1] / 255;
|
|
2658
|
+
colors[base + 2] = sample.color[2] / 255;
|
|
2659
|
+
});
|
|
2660
|
+
return { positions, colors };
|
|
2661
|
+
}
|
|
2662
|
+
function renderScalarPointOverlays(session, overlays) {
|
|
1377
2663
|
const r = getRenderer(session.size, session.pixelRatio);
|
|
2664
|
+
const overlayById = new Map(overlays.map((overlay) => [overlay.renderable.id, overlay]));
|
|
2665
|
+
const replacements = session.renderables.map((renderable) => {
|
|
2666
|
+
const previousMaterial = renderable.solid.material;
|
|
2667
|
+
const ghostMaterial = new MeshBasicMaterial({
|
|
2668
|
+
color: new Color(2502970),
|
|
2669
|
+
transparent: true,
|
|
2670
|
+
opacity: 0.24,
|
|
2671
|
+
depthWrite: true,
|
|
2672
|
+
depthTest: true,
|
|
2673
|
+
side: DoubleSide,
|
|
2674
|
+
clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
|
|
2675
|
+
});
|
|
2676
|
+
ghostMaterial.toneMapped = false;
|
|
2677
|
+
renderable.solid.material = ghostMaterial;
|
|
2678
|
+
const overlay = overlayById.get(renderable.id);
|
|
2679
|
+
if (!overlay || overlay.positions.length === 0)
|
|
2680
|
+
return { renderable, previousMaterial, ghostMaterial, points: null, geometry: null, material: null };
|
|
2681
|
+
const geometry = new BufferGeometry();
|
|
2682
|
+
geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
|
|
2683
|
+
geometry.setAttribute("color", new BufferAttribute(overlay.colors, 3));
|
|
2684
|
+
const material = new PointsMaterial({
|
|
2685
|
+
size: 3,
|
|
2686
|
+
sizeAttenuation: false,
|
|
2687
|
+
vertexColors: true,
|
|
2688
|
+
depthTest: true,
|
|
2689
|
+
depthWrite: false,
|
|
2690
|
+
clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
|
|
2691
|
+
});
|
|
2692
|
+
material.toneMapped = false;
|
|
2693
|
+
const points = new Points(geometry, material);
|
|
2694
|
+
points.renderOrder = 5;
|
|
2695
|
+
points.raycast = () => null;
|
|
2696
|
+
renderable.root.add(points);
|
|
2697
|
+
return { renderable, previousMaterial, ghostMaterial, points, geometry, material };
|
|
2698
|
+
});
|
|
2699
|
+
try {
|
|
2700
|
+
return withSolidOnlyVisibility(
|
|
2701
|
+
session,
|
|
2702
|
+
() => withTemporarySceneBackground(session, new Color(0), () => {
|
|
2703
|
+
updateSdfRaymarchUniforms(session);
|
|
2704
|
+
r.render(session.scene, session.camera);
|
|
2705
|
+
return captureRenderedPng(session.size);
|
|
2706
|
+
})
|
|
2707
|
+
);
|
|
2708
|
+
} finally {
|
|
2709
|
+
replacements.forEach(({ renderable, previousMaterial, ghostMaterial, points, geometry, material }) => {
|
|
2710
|
+
if (points) renderable.root.remove(points);
|
|
2711
|
+
renderable.solid.material = previousMaterial;
|
|
2712
|
+
ghostMaterial.dispose();
|
|
2713
|
+
geometry == null ? void 0 : geometry.dispose();
|
|
2714
|
+
material == null ? void 0 : material.dispose();
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function getSessionThicknessInspection(session, rawOptions) {
|
|
2719
|
+
var _a;
|
|
1378
2720
|
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
1379
|
-
const
|
|
1380
|
-
|
|
2721
|
+
const optionsKey = inspectionOptionsKey(options);
|
|
2722
|
+
if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
|
|
1381
2723
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
1382
2724
|
const warnings = [];
|
|
1383
2725
|
const objects = [];
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const bbox = analysis.geometry.boundingBox;
|
|
1389
|
-
const summary = summarizeThicknessSamples(analysis.samples, options);
|
|
2726
|
+
const cloudObjects = [];
|
|
2727
|
+
const points = [];
|
|
2728
|
+
const overlays = [];
|
|
2729
|
+
session.renderables.forEach((renderable, index) => {
|
|
1390
2730
|
const sourceObject = byId.get(renderable.id);
|
|
2731
|
+
if (renderable.sdfRaymarch) {
|
|
2732
|
+
warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh thickness inspection.`);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options);
|
|
2736
|
+
const bbox = bboxFromGeometry(analysis.geometry);
|
|
2737
|
+
const summary = summarizeThicknessSamples(analysis.samples, options);
|
|
1391
2738
|
if (analysis.warnings.length > 0) {
|
|
1392
2739
|
analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
|
|
1393
2740
|
}
|
|
2741
|
+
const objectIndex = index + 1;
|
|
1394
2742
|
objects.push({
|
|
1395
|
-
index:
|
|
2743
|
+
index: objectIndex,
|
|
1396
2744
|
id: renderable.id,
|
|
1397
2745
|
name: renderable.name,
|
|
1398
2746
|
groupName: renderable.groupName,
|
|
@@ -1401,174 +2749,154 @@ function renderCurrentThickness(session, rawOptions) {
|
|
|
1401
2749
|
triangleCount: analysis.triangleCount,
|
|
1402
2750
|
sampledTriangleCount: analysis.sampledTriangleCount,
|
|
1403
2751
|
sampleStride: analysis.sampleStride,
|
|
1404
|
-
bbox
|
|
1405
|
-
min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
|
|
1406
|
-
max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
|
|
1407
|
-
},
|
|
2752
|
+
bbox,
|
|
1408
2753
|
...summary
|
|
1409
2754
|
});
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
2755
|
+
cloudObjects.push({
|
|
2756
|
+
index: objectIndex,
|
|
2757
|
+
id: renderable.id,
|
|
2758
|
+
name: renderable.name,
|
|
2759
|
+
groupName: renderable.groupName,
|
|
2760
|
+
treePath: sourceObject == null ? void 0 : sourceObject.treePath,
|
|
2761
|
+
mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
|
|
2762
|
+
sampleCount: analysis.pointSamples.length,
|
|
2763
|
+
bbox
|
|
2764
|
+
});
|
|
2765
|
+
analysis.pointSamples.forEach((sample) => {
|
|
2766
|
+
points.push({
|
|
2767
|
+
objectIndex,
|
|
2768
|
+
objectId: renderable.id,
|
|
2769
|
+
objectName: renderable.name,
|
|
2770
|
+
...sample
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
|
|
2774
|
+
analysis.geometry.dispose();
|
|
1413
2775
|
});
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
2776
|
+
const state = {
|
|
2777
|
+
optionsKey,
|
|
2778
|
+
overlays,
|
|
2779
|
+
pointCloud: {
|
|
2780
|
+
schemaVersion: 1,
|
|
2781
|
+
property: "thickness",
|
|
2782
|
+
coordinateSpace: "object-local",
|
|
2783
|
+
units: "model",
|
|
2784
|
+
sampleCount: points.length,
|
|
2785
|
+
objects: cloudObjects,
|
|
2786
|
+
points
|
|
2787
|
+
},
|
|
2788
|
+
report: {
|
|
2789
|
+
method: "mesh-normal-raycast",
|
|
2790
|
+
options,
|
|
2791
|
+
objectCount: objects.length,
|
|
2792
|
+
objects,
|
|
2793
|
+
warnings,
|
|
2794
|
+
style: {
|
|
2795
|
+
criticalColor: THICKNESS_COLORS.critical,
|
|
2796
|
+
warningColor: THICKNESS_COLORS.warning,
|
|
2797
|
+
okColor: THICKNESS_COLORS.ok,
|
|
2798
|
+
thickColor: THICKNESS_COLORS.thick,
|
|
2799
|
+
unknownColor: THICKNESS_COLORS.unknown
|
|
1438
2800
|
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
session.thicknessInspection = state;
|
|
2804
|
+
return state;
|
|
2805
|
+
}
|
|
2806
|
+
function renderCurrentThickness(session, rawOptions) {
|
|
2807
|
+
const state = getSessionThicknessInspection(session, rawOptions);
|
|
2808
|
+
return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
1448
2809
|
}
|
|
1449
2810
|
const ROUGHNESS_SMOOTH_OPACITY = 0.16;
|
|
1450
2811
|
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
2812
|
function renderCurrentRoughness(session, rawOptions) {
|
|
1498
|
-
const
|
|
2813
|
+
const state = getSessionRoughnessInspection(session, rawOptions);
|
|
2814
|
+
return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
2815
|
+
}
|
|
2816
|
+
function getSessionRoughnessInspection(session, rawOptions) {
|
|
2817
|
+
var _a;
|
|
1499
2818
|
const options = resolveRoughnessInspectionOptions(rawOptions);
|
|
2819
|
+
const optionsKey = inspectionOptionsKey(options);
|
|
2820
|
+
if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
|
|
1500
2821
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
1501
2822
|
const warnings = [];
|
|
1502
2823
|
const objects = [];
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
2824
|
+
const cloudObjects = [];
|
|
2825
|
+
const points = [];
|
|
2826
|
+
const overlays = [];
|
|
2827
|
+
session.renderables.forEach((renderable, index) => {
|
|
1506
2828
|
if (renderable.sdfRaymarch) {
|
|
1507
|
-
const material2 = new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
|
|
1508
|
-
material2.toneMapped = false;
|
|
1509
|
-
renderable.solid.material = material2;
|
|
1510
2829
|
warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh roughness inspection.`);
|
|
1511
|
-
return
|
|
2830
|
+
return;
|
|
1512
2831
|
}
|
|
1513
|
-
const analysis = analyzeRoughnessGeometry(
|
|
1514
|
-
const bbox = analysis.geometry
|
|
2832
|
+
const analysis = analyzeRoughnessGeometry(renderable.solid.geometry, options);
|
|
2833
|
+
const bbox = bboxFromGeometry(analysis.geometry);
|
|
1515
2834
|
const sourceObject = byId.get(renderable.id);
|
|
1516
2835
|
if (analysis.warnings.length > 0) {
|
|
1517
2836
|
analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
|
|
1518
2837
|
}
|
|
2838
|
+
const objectIndex = index + 1;
|
|
1519
2839
|
objects.push({
|
|
1520
|
-
index:
|
|
2840
|
+
index: objectIndex,
|
|
1521
2841
|
id: renderable.id,
|
|
1522
2842
|
name: renderable.name,
|
|
1523
2843
|
groupName: renderable.groupName,
|
|
1524
2844
|
treePath: sourceObject == null ? void 0 : sourceObject.treePath,
|
|
1525
2845
|
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
|
-
},
|
|
2846
|
+
bbox,
|
|
1530
2847
|
...analysis.summary
|
|
1531
2848
|
});
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2849
|
+
cloudObjects.push({
|
|
2850
|
+
index: objectIndex,
|
|
2851
|
+
id: renderable.id,
|
|
2852
|
+
name: renderable.name,
|
|
2853
|
+
groupName: renderable.groupName,
|
|
2854
|
+
treePath: sourceObject == null ? void 0 : sourceObject.treePath,
|
|
2855
|
+
mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
|
|
2856
|
+
sampleCount: analysis.pointSamples.length,
|
|
2857
|
+
bbox
|
|
2858
|
+
});
|
|
2859
|
+
analysis.pointSamples.forEach((sample) => {
|
|
2860
|
+
points.push({
|
|
2861
|
+
objectIndex,
|
|
2862
|
+
objectId: renderable.id,
|
|
2863
|
+
objectName: renderable.name,
|
|
2864
|
+
...sample
|
|
2865
|
+
});
|
|
2866
|
+
});
|
|
2867
|
+
overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
|
|
2868
|
+
analysis.geometry.dispose();
|
|
1536
2869
|
});
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2870
|
+
const state = {
|
|
2871
|
+
optionsKey,
|
|
2872
|
+
overlays,
|
|
2873
|
+
pointCloud: {
|
|
2874
|
+
schemaVersion: 1,
|
|
2875
|
+
property: "roughness",
|
|
2876
|
+
coordinateSpace: "object-local",
|
|
2877
|
+
units: "degrees",
|
|
2878
|
+
sampleCount: points.length,
|
|
2879
|
+
objects: cloudObjects,
|
|
2880
|
+
points
|
|
2881
|
+
},
|
|
2882
|
+
report: {
|
|
2883
|
+
method: "mesh-dihedral-angle",
|
|
2884
|
+
options,
|
|
2885
|
+
objectCount: objects.length,
|
|
2886
|
+
objects,
|
|
2887
|
+
warnings,
|
|
2888
|
+
style: {
|
|
2889
|
+
smoothColor: ROUGHNESS_COLORS.smooth,
|
|
2890
|
+
moderateColor: ROUGHNESS_COLORS.moderate,
|
|
2891
|
+
sharpColor: ROUGHNESS_COLORS.sharp,
|
|
2892
|
+
harshColor: ROUGHNESS_COLORS.harsh,
|
|
2893
|
+
smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
|
|
2894
|
+
harshOpacity: ROUGHNESS_HARSH_OPACITY
|
|
1562
2895
|
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
renderable.solid.material = previousMaterial;
|
|
1568
|
-
geometry == null ? void 0 : geometry.dispose();
|
|
1569
|
-
material.dispose();
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
session.roughnessInspection = state;
|
|
2899
|
+
return state;
|
|
1572
2900
|
}
|
|
1573
2901
|
function emptySectionSvg() {
|
|
1574
2902
|
return {
|
|
@@ -2457,19 +3785,8 @@ function createSession(code, opts) {
|
|
|
2457
3785
|
name: entry.source.name,
|
|
2458
3786
|
shape: entry.shape
|
|
2459
3787
|
}));
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
return {
|
|
2463
|
-
id: entry.source.id,
|
|
2464
|
-
name: entry.source.name,
|
|
2465
|
-
shape: entry.shape,
|
|
2466
|
-
min: [bb2.min[0], bb2.min[1], bb2.min[2]],
|
|
2467
|
-
max: [bb2.max[0], bb2.max[1], bb2.max[2]],
|
|
2468
|
-
groupName: entry.source.groupName,
|
|
2469
|
-
treePath: entry.source.treePath,
|
|
2470
|
-
mock: entry.source.mock
|
|
2471
|
-
};
|
|
2472
|
-
}) : [];
|
|
3788
|
+
const connectivityBodyInput = (opts == null ? void 0 : opts.includeConnectivity) ? buildMeshBodyConnectivityInput(shapeVisibleObjs.map(meshConnectivitySource), { bodyShape: () => new EmptyInspectionShape() }) : null;
|
|
3789
|
+
const connectivityEntries = (connectivityBodyInput == null ? void 0 : connectivityBodyInput.entries) ?? [];
|
|
2473
3790
|
const collisionEntries = (opts == null ? void 0 : opts.includeCollisions) ? shapeVisibleObjs.map((entry) => {
|
|
2474
3791
|
const bb2 = entry.shape.boundingBox();
|
|
2475
3792
|
return {
|
|
@@ -2772,10 +4089,13 @@ function createSession(code, opts) {
|
|
|
2772
4089
|
renderables,
|
|
2773
4090
|
sectionShapes,
|
|
2774
4091
|
connectivityEntries,
|
|
4092
|
+
connectivityBodyInput,
|
|
2775
4093
|
physicalConnectivity: null,
|
|
2776
4094
|
distanceReport: null,
|
|
2777
4095
|
collisionEntries,
|
|
2778
4096
|
collisionReport: null,
|
|
4097
|
+
thicknessInspection: null,
|
|
4098
|
+
roughnessInspection: null,
|
|
2779
4099
|
joints,
|
|
2780
4100
|
jointCouplings,
|
|
2781
4101
|
animationClips,
|
|
@@ -2880,6 +4200,8 @@ window.__forgeRender = async (code, opts) => {
|
|
|
2880
4200
|
let collisionReport = null;
|
|
2881
4201
|
let thicknessReport = null;
|
|
2882
4202
|
let roughnessReport = null;
|
|
4203
|
+
let thicknessPointCloud = null;
|
|
4204
|
+
let roughnessPointCloud = null;
|
|
2883
4205
|
let sectionAtlas = null;
|
|
2884
4206
|
const channelViewCounts = /* @__PURE__ */ new Map();
|
|
2885
4207
|
const markChannelViewStart = async (channel, view) => {
|
|
@@ -2925,6 +4247,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
2925
4247
|
const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
|
|
2926
4248
|
roughnessRenders[label] = roughness.png;
|
|
2927
4249
|
roughnessReport = roughness.report;
|
|
4250
|
+
roughnessPointCloud = roughness.pointCloud;
|
|
2928
4251
|
await markChannelViewDone("roughness", label);
|
|
2929
4252
|
}
|
|
2930
4253
|
if (requestedChannels.has("mask")) {
|
|
@@ -2963,6 +4286,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
2963
4286
|
const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
|
|
2964
4287
|
thicknessRenders[label] = thickness.png;
|
|
2965
4288
|
thicknessReport = thickness.report;
|
|
4289
|
+
thicknessPointCloud = thickness.pointCloud;
|
|
2966
4290
|
await markChannelViewDone("thickness", label);
|
|
2967
4291
|
}
|
|
2968
4292
|
} catch (e) {
|
|
@@ -2989,6 +4313,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
2989
4313
|
normals: normalRenders,
|
|
2990
4314
|
roughness: roughnessReport ? {
|
|
2991
4315
|
...roughnessReport,
|
|
4316
|
+
pointCloud: roughnessPointCloud,
|
|
2992
4317
|
views: roughnessRenders
|
|
2993
4318
|
} : {
|
|
2994
4319
|
views: roughnessRenders
|
|
@@ -3017,6 +4342,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
3017
4342
|
},
|
|
3018
4343
|
thickness: thicknessReport ? {
|
|
3019
4344
|
...thicknessReport,
|
|
4345
|
+
pointCloud: thicknessPointCloud,
|
|
3020
4346
|
views: thicknessRenders
|
|
3021
4347
|
} : {
|
|
3022
4348
|
views: thicknessRenders
|