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