forgecad 0.10.3 → 0.10.5
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-CK7ObBz3.js → AdminPage-raksfnNA.js} +1 -1
- package/dist/assets/{BenchmarkPage-Ds7Z2doN.js → BenchmarkPage-DP3RxhPs.js} +2 -2
- package/dist/assets/{BlogPage-DlPbpt6A.js → BlogPage-D7Dos-vl.js} +1 -1
- package/dist/assets/{DocsPage-vZb3b3Y0.js → DocsPage-DO1kvBns.js} +34 -43
- package/dist/assets/{EditorApp-HLoKfe15.js → EditorApp-DQJmcmRT.js} +51 -17
- package/dist/assets/{EmbedViewer--KnqBKrJ.js → EmbedViewer-DFDUhOma.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-C_LssmnA.js → LandingPageProofDriven-DbE_tp8-.js} +54 -36
- package/dist/assets/{LegalPage-DGsyo4n1.js → LegalPage-CominSso.js} +2 -2
- package/dist/assets/{PricingPage-BOE27B-R.js → PricingPage-CcVIN9yj.js} +2 -2
- package/dist/assets/{SettingsPage-f47cnk39.js → SettingsPage-DLWcP289.js} +1 -1
- package/dist/assets/{app-D6ccu2Xx.js → app-xW3hOdq9.js} +1343 -4004
- package/dist/assets/{backendInit-DbTkQN9J.js → backendInit-mDHk97u7.js} +12346 -3803
- package/dist/assets/cli/{render-BsngirjC.js → render--SIU27W_.js} +1909 -146
- package/dist/assets/{constructionHistoryWorker-PCwXrTDB.js → constructionHistoryWorker-uEe_Q7Kg.js} +2362 -835
- package/dist/assets/{evalWorker-CS63PfZu.js → evalWorker-BqyDHDcI.js} +7755 -3127
- package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
- package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
- package/dist/assets/{inspectWorker-Y4cOzNyA.js → inspectWorker-UXMxlcR8.js} +6550 -2943
- package/dist/assets/{jointPose-AMvCywzS.js → jointPose-bYMlwU3v.js} +1 -1
- package/dist/assets/{landing-proof-driven-ORyigZ6p.css → landing-proof-driven-_u4v_xQb.css} +71 -11
- package/dist/assets/{manifold-Crd_F2qx.js → manifold-BR7UYI4P.js} +1 -1
- package/dist/assets/{manifold-CBry38ly.js → manifold-CyOV5B9S.js} +2 -2
- package/dist/assets/{manifold-k2kRcc85.js → manifold-D4d5NQst.js} +1 -1
- package/dist/assets/{reportWorker-CWvn0CEv.js → reportWorker-DsaICZsn.js} +7320 -2827
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/usage.md +17 -15
- package/dist/docs-raw/CLI.md +4 -2
- package/dist/docs-raw/component-model.md +2 -2
- package/dist/docs-raw/generated/assembly.md +76 -3
- package/dist/docs-raw/generated/concepts.md +36 -5
- package/dist/docs-raw/generated/core.md +185 -21
- package/dist/docs-raw/generated/curves.md +344 -6
- package/dist/docs-raw/generated/runtime-names.md +12 -12
- package/dist/docs-raw/generated/sketch.md +16 -3
- package/dist/docs-raw/guides/inspection-bundles.md +5 -3
- package/dist/docs-raw/guides/structural-fea.md +224 -0
- package/dist/docs-raw/simulation-workflow.md +1 -1
- package/dist/docs-raw/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
- package/dist/docs-raw/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
- package/dist/docs-raw/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
- package/{dist-skill/website/skills/forgecad-visual-spec.md → dist/docs-raw/skills/forgecad-image-prompt.md} +7 -7
- package/dist/docs-raw/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
- package/{dist-skill/website/skills/forgecad-project.md → dist/docs-raw/skills/forgecad-project-sync.md} +5 -5
- package/dist/docs-raw/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
- package/dist/docs-raw/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
- package/dist/docs-raw/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
- package/dist/docs-raw/skills/forgecad.md +1 -0
- package/dist/docs-raw/skills/index.md +9 -12
- package/dist/index.html +9 -9
- package/dist/llms.txt +7 -7
- package/dist/sitemap.xml +16 -16
- package/dist-cli/{check-compiler-HPF2T2FS.js → check-compiler-7YAHVXYM.js} +1 -1
- package/dist-cli/{check-query-propagation-HYSLTXAB.js → check-query-propagation-ZRR6IOJW.js} +1 -1
- package/dist-cli/{chunk-WLUKAW3H.js → chunk-VNM67DIV.js} +29671 -24865
- package/dist-cli/forgecad.js +5906 -714
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +853 -45
- package/dist-skill/SKILL.md +1 -0
- package/dist-skill/docs/CLI.md +4 -2
- package/dist-skill/docs/generated/assembly.md +73 -3
- package/dist-skill/docs/generated/core.md +185 -21
- package/dist-skill/docs/generated/curves.md +343 -6
- package/dist-skill/docs/generated/runtime-names.md +12 -12
- package/dist-skill/docs/generated/sketch.md +16 -3
- package/dist-skill/docs/guides/inspection-bundles.md +5 -3
- package/dist-skill/docs/guides/structural-fea.md +224 -0
- package/dist-skill/library/README.md +9 -12
- package/dist-skill/library/{forgecad-make-a-model → forgecad-build-model}/SKILL.md +16 -6
- package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/SKILL.md +4 -4
- package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/master-prompt.md +1 -1
- package/dist-skill/library/{forgecad-model-grader → forgecad-grade-model}/SKILL.md +6 -4
- package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +4 -0
- package/dist-skill/library/{forgecad-visual-spec → forgecad-image-prompt}/SKILL.md +5 -5
- package/dist-skill/library/forgecad-image-prompt/agents/openai.yaml +4 -0
- package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/SKILL.md +4 -4
- package/dist-skill/library/{forgecad-project → forgecad-project-sync}/SKILL.md +3 -3
- package/dist-skill/library/{forgecad-3d-reconstruction → forgecad-reconstruct-cad-file}/SKILL.md +5 -5
- package/dist-skill/library/forgecad-reconstruct-cad-file/agents/openai.yaml +4 -0
- package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/SKILL.md +10 -10
- package/dist-skill/library/forgecad-reconstruct-from-images/agents/openai.yaml +4 -0
- package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/SKILL.md +4 -4
- package/dist-skill/website/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
- package/dist-skill/website/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
- package/dist-skill/website/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
- package/{dist/docs-raw/skills/forgecad-visual-spec.md → dist-skill/website/skills/forgecad-image-prompt.md} +7 -7
- package/dist-skill/website/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
- package/{dist/docs-raw/skills/forgecad-project.md → dist-skill/website/skills/forgecad-project-sync.md} +5 -5
- package/dist-skill/website/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
- package/dist-skill/website/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
- package/dist-skill/website/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
- package/dist-skill/website/skills/forgecad.md +1 -0
- package/dist-skill/website/skills/index.md +9 -12
- package/examples/analysis/structural-stress-fea.forge.js +19 -0
- package/examples/api/blend-full-round.forge.js +37 -0
- package/examples/api/blend-variable-radius.forge.js +51 -0
- package/examples/api/curve-project-and-intersect.forge.js +59 -0
- package/examples/api/extrude-up-to-face.forge.js +47 -0
- package/examples/api/spoon-full-tang-handle.forge.js +148 -0
- package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
- package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
- package/examples/api/texture-projection.forge.js +75 -0
- package/examples/assets/uv-grid.png +0 -0
- package/package.json +4 -1
- package/dist/docs-raw/skills/forgecad-blockout-model.md +0 -49
- package/dist/docs-raw/skills/forgecad-component-model.md +0 -53
- package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +0 -60
- package/dist-skill/library/forgecad-3d-reconstruction/agents/openai.yaml +0 -4
- package/dist-skill/library/forgecad-blockout-model/SKILL.md +0 -42
- package/dist-skill/library/forgecad-component-model/SKILL.md +0 -46
- package/dist-skill/library/forgecad-image-replicator/agents/openai.yaml +0 -4
- package/dist-skill/library/forgecad-model-grader/agents/openai.yaml +0 -4
- package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +0 -48
- package/dist-skill/library/forgecad-reconstruction-benchmark/agents/openai.yaml +0 -4
- package/dist-skill/library/forgecad-visual-spec/agents/openai.yaml +0 -4
- package/dist-skill/website/skills/forgecad-blockout-model.md +0 -49
- package/dist-skill/website/skills/forgecad-component-model.md +0 -53
- package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +0 -60
- /package/dist/assets/{landing-proof-driven-DiGqdtWa.js → landing-proof-driven-DNPRKL_p.js} +0 -0
- /package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/default-profiles.md +0 -0
- /package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/summarize_manifest.py +0 -0
- /package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/scripts/compare_images.py +0 -0
- /package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/scripts/mujoco_verify.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
import { D as DoubleSide, a as Scene,
|
|
5
|
-
import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-
|
|
4
|
+
import { D as DoubleSide, a as Scene, bJ as BoxGeometry, cp as MeshStandardMaterial, a5 as BackSide, bd as PointLight, M as Mesh, ab as MeshBasicMaterial, d2 as localAabbPlaneRelation, i as Vector2, d3 as ShapeUtils, d4 as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, d5 as Frustum, J as Box3, a1 as MathUtils, d6 as meshContactDataFor, d7 as AabbSpatialIndex, d8 as detectPhysicalContact, d9 as resolveThicknessInspectionOptions, da as thicknessColor, db as thicknessClass, bf as BufferAttribute, R as Raycaster, bS as MeshBVH, c1 as acceleratedRaycast, dc as requireFiniteNumber, dd as requireIntegerAtLeast, de as requirePositiveFiniteNumber, d1 as initBackendForEvaluation, f as Color, bh as COMPARISON_COLORS, aB as resolveForgeRenderStyle, b_ as getRenderStylePreset, az as setParamOverrides, bw as runScript, cN as scanProxyGridForBounds, df as Group, bl as shapeToGeometry, bx as MeshPhysicalMaterial, bQ as AdditiveBlending, c4 as scanMaterialShellColor, bY as descriptorToThreeTexture, bZ as applyProjectedTexture, dg as createScanProxyGeometry, b3 as LineBasicMaterial, c0 as NormalBlending, by as LineSegments, P as PerspectiveCamera, cK as DEFAULT_VIEW_CONFIG, bF as worldAuthorPlaneToLocal, dh as resolveSectionHatchMetrics, cT as buildGeometryComparisonPointCloud, cR as triangleSoupFromMeshes, O as OrthographicCamera, l as ShaderMaterial, ca as ZEBRA_STRIPE_FRAGMENT_SHADER, cb as ZEBRA_STRIPE_VERTEX_SHADER, c5 as ZEBRA_STRIPE_SOFTNESS, c6 as ZEBRA_STRIPE_SCALE, c7 as ZEBRA_LIGHT_COLOR, c8 as ZEBRA_DARK_COLOR, c9 as ZEBRA_ACCENT_COLOR, bR as geometryWithVisibleVertexColors, di as intersectWithPlane, dj as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, dk as parseCameraCliSpec, dl as PMREMGenerator, b6 as CanvasTexture, b7 as Object3D, b8 as FogExp2, b9 as Fog, ba as AmbientLight, be as DirectionalLight, bb as HemisphereLight, aW as findJointAnimationClip, q as Plane, ce as SURFACE_FIELD_FRAGMENT_SHADER, cf as SURFACE_FIELD_VERTEX_SHADER, cd as scanMaterialLayerStyles, cc as SCAN_PROXY_LAYER_STYLES, aX as resolveJointAnimation, aY as resolveJointViewValues, b2 as BufferGeometry, dm as DEFAULT_ROUGHNESS_COLOR_SCALE, bT as makeColorScaleTexture, bU as colorScaleLUT, bV as makeScalarValueTexture, bW as makeInspectScalarUniforms, c2 as INSPECT_SCALAR_FRAGMENT_SHADER, c3 as INSPECT_SCALAR_VERTEX_SHADER, bg as heatPointsForSide, dn as analyzeCollisionIntersections, dp as serializeCollisionFinding, dq as summarizeThicknessSamples, dr as THICKNESS_COLORS, ds as DEFAULT_THICKNESS_COLOR_SCALE, dt as DEFAULT_INSPECT_ISOLINE_SPACING, bc as SpotLight, ct as CylinderGeometry, du as TorusGeometry, ch as CatmullRomCurve3, ci as TubeGeometry, cX as resolveScalarSceneSampleBudget, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, ck as DEFAULT_COLORMAP, dv as SphereGeometry, dw as ConeGeometry, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, c$ as comparisonCandidateContextOpacity, dx as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-mDHk97u7.js";
|
|
5
|
+
import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-bYMlwU3v.js";
|
|
6
6
|
const CAD_MATERIAL_PROPS = {
|
|
7
7
|
color: 6003669,
|
|
8
8
|
metalness: 0.05,
|
|
@@ -1187,12 +1187,12 @@ function planeKey(triangle, snap) {
|
|
|
1187
1187
|
}
|
|
1188
1188
|
function triangleEdgeKeys(triangle, snap) {
|
|
1189
1189
|
const vertices = [vertexKey$2(triangle.a, snap), vertexKey$2(triangle.b, snap), vertexKey$2(triangle.c, snap)];
|
|
1190
|
-
return [edgeKey$
|
|
1190
|
+
return [edgeKey$2(vertices[0], vertices[1]), edgeKey$2(vertices[1], vertices[2]), edgeKey$2(vertices[2], vertices[0])];
|
|
1191
1191
|
}
|
|
1192
1192
|
function vertexKey$2(point, snap) {
|
|
1193
1193
|
return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
|
|
1194
1194
|
}
|
|
1195
|
-
function edgeKey$
|
|
1195
|
+
function edgeKey$2(a, b) {
|
|
1196
1196
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
1197
1197
|
}
|
|
1198
1198
|
const MIN_TRIANGLE_AREA = 1e-12;
|
|
@@ -1321,10 +1321,17 @@ function fract(value) {
|
|
|
1321
1321
|
function clampUnit(value) {
|
|
1322
1322
|
return Math.min(1 - 1e-9, Math.max(1e-9, value));
|
|
1323
1323
|
}
|
|
1324
|
-
function cloneGeometryForFaceColors(geometry) {
|
|
1324
|
+
function cloneGeometryForFaceColors$1(geometry) {
|
|
1325
1325
|
return geometry.index ? geometry.toNonIndexed() : geometry.clone();
|
|
1326
1326
|
}
|
|
1327
|
-
function
|
|
1327
|
+
function makeThicknessRaycastTarget(sourceGeometry, rayMaterial) {
|
|
1328
|
+
const geometry = sourceGeometry.clone();
|
|
1329
|
+
geometry.boundsTree = new MeshBVH(geometry);
|
|
1330
|
+
const mesh = new Mesh(geometry, rayMaterial);
|
|
1331
|
+
mesh.raycast = acceleratedRaycast;
|
|
1332
|
+
return { mesh, geometry };
|
|
1333
|
+
}
|
|
1334
|
+
function geometryMaxDimension$1(geometry) {
|
|
1328
1335
|
geometry.computeBoundingBox();
|
|
1329
1336
|
const box = geometry.boundingBox;
|
|
1330
1337
|
if (!box) return 1;
|
|
@@ -1332,34 +1339,47 @@ function geometryMaxDimension(geometry) {
|
|
|
1332
1339
|
box.getSize(size);
|
|
1333
1340
|
return Math.max(1, size.x, size.y, size.z);
|
|
1334
1341
|
}
|
|
1335
|
-
function
|
|
1342
|
+
function createThicknessRaycastContext(sourceGeometry, rawOptions = {}, context = {}) {
|
|
1343
|
+
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
1344
|
+
const connectedGeometries = context.connectedGeometries ?? [];
|
|
1345
|
+
const maxDim = Math.max(geometryMaxDimension$1(sourceGeometry), ...connectedGeometries.map(geometryMaxDimension$1));
|
|
1346
|
+
const epsilon = Math.max(1e-4, maxDim * 1e-6);
|
|
1347
|
+
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
1348
|
+
const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
|
|
1349
|
+
const rayTargets = [
|
|
1350
|
+
makeThicknessRaycastTarget(sourceGeometry, rayMaterial),
|
|
1351
|
+
...connectedGeometries.map((connectedGeometry) => makeThicknessRaycastTarget(connectedGeometry, rayMaterial))
|
|
1352
|
+
];
|
|
1353
|
+
return {
|
|
1354
|
+
raycaster: new Raycaster(),
|
|
1355
|
+
rayTargetMeshes: rayTargets.map((target) => target.mesh),
|
|
1356
|
+
epsilon,
|
|
1357
|
+
far,
|
|
1358
|
+
contactTolerance: options.contactTolerance,
|
|
1359
|
+
dispose: () => {
|
|
1360
|
+
rayTargets.forEach((target) => target.geometry.dispose());
|
|
1361
|
+
rayMaterial.dispose();
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, point, direction, epsilon, far, contactTolerance) {
|
|
1336
1366
|
const origin = point.clone().addScaledVector(direction, epsilon);
|
|
1337
1367
|
raycaster.set(origin, direction);
|
|
1338
1368
|
raycaster.near = epsilon;
|
|
1339
1369
|
raycaster.far = far;
|
|
1340
1370
|
const hits = raycaster.intersectObjects(rayTargetMeshes, false);
|
|
1371
|
+
const minDistance = Math.max(epsilon, contactTolerance);
|
|
1341
1372
|
for (const hit of hits) {
|
|
1342
|
-
if (hit.distance <=
|
|
1343
|
-
if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
|
|
1373
|
+
if (hit.distance <= minDistance) continue;
|
|
1344
1374
|
return hit.distance + epsilon;
|
|
1345
1375
|
}
|
|
1346
1376
|
return null;
|
|
1347
1377
|
}
|
|
1348
|
-
function triangleThickness(raycaster, rayTargetMeshes,
|
|
1349
|
-
const forward = firstOppositeSurfaceDistance(
|
|
1350
|
-
raycaster,
|
|
1351
|
-
rayTargetMeshes,
|
|
1352
|
-
jumpableMeshes,
|
|
1353
|
-
centroid,
|
|
1354
|
-
normal,
|
|
1355
|
-
epsilon,
|
|
1356
|
-
far,
|
|
1357
|
-
contactTolerance
|
|
1358
|
-
);
|
|
1378
|
+
function triangleThickness(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance) {
|
|
1379
|
+
const forward = firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance);
|
|
1359
1380
|
const backward = firstOppositeSurfaceDistance(
|
|
1360
1381
|
raycaster,
|
|
1361
1382
|
rayTargetMeshes,
|
|
1362
|
-
jumpableMeshes,
|
|
1363
1383
|
centroid,
|
|
1364
1384
|
normal.clone().negate(),
|
|
1365
1385
|
epsilon,
|
|
@@ -1370,9 +1390,20 @@ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid,
|
|
|
1370
1390
|
if (backward == null) return forward;
|
|
1371
1391
|
return Math.min(forward, backward);
|
|
1372
1392
|
}
|
|
1393
|
+
function measureThicknessAtPoint(context, point, normal) {
|
|
1394
|
+
return triangleThickness(
|
|
1395
|
+
context.raycaster,
|
|
1396
|
+
context.rayTargetMeshes,
|
|
1397
|
+
point,
|
|
1398
|
+
normal,
|
|
1399
|
+
context.epsilon,
|
|
1400
|
+
context.far,
|
|
1401
|
+
context.contactTolerance
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1373
1404
|
function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
|
|
1374
1405
|
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
1375
|
-
const geometry = cloneGeometryForFaceColors(sourceGeometry);
|
|
1406
|
+
const geometry = cloneGeometryForFaceColors$1(sourceGeometry);
|
|
1376
1407
|
const position = geometry.getAttribute("position");
|
|
1377
1408
|
if (!position || position.count < 3) {
|
|
1378
1409
|
return {
|
|
@@ -1388,82 +1419,1238 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
|
|
|
1388
1419
|
const triangleCount = Math.floor(position.count / 3);
|
|
1389
1420
|
const surfaceTriangles = readSurfaceTriangles(position);
|
|
1390
1421
|
const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
|
|
1391
|
-
const connectedGeometries = context.connectedGeometries ?? [];
|
|
1392
|
-
const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
|
|
1393
|
-
const epsilon = Math.max(1e-4, maxDim * 1e-6);
|
|
1394
|
-
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
1395
1422
|
const colors = new Float32Array(position.count * 3);
|
|
1396
1423
|
const triangleThicknessValues = new Array(triangleCount).fill(void 0);
|
|
1397
1424
|
const samples = [];
|
|
1398
1425
|
const pointSamples = [];
|
|
1399
1426
|
const warnings = [];
|
|
1427
|
+
const rayContext = createThicknessRaycastContext(geometry, options, context);
|
|
1428
|
+
try {
|
|
1429
|
+
if (surfaceTriangles.length === 0) {
|
|
1430
|
+
warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
|
|
1431
|
+
} else if (surfaceSamples.length < surfaceTriangles.length) {
|
|
1432
|
+
warnings.push(
|
|
1433
|
+
`Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const sampledTriangleIndexes = /* @__PURE__ */ new Set();
|
|
1437
|
+
for (const sample of surfaceSamples) {
|
|
1438
|
+
sampledTriangleIndexes.add(sample.triangle.index);
|
|
1439
|
+
const thickness = measureThicknessAtPoint(rayContext, sample.position, sample.normal);
|
|
1440
|
+
samples.push({ thickness, area: sample.area });
|
|
1441
|
+
const previous = triangleThicknessValues[sample.triangle.index];
|
|
1442
|
+
if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
|
|
1443
|
+
triangleThicknessValues[sample.triangle.index] = thickness;
|
|
1444
|
+
}
|
|
1445
|
+
pointSamples.push({
|
|
1446
|
+
position: [sample.position.x, sample.position.y, sample.position.z],
|
|
1447
|
+
normal: [sample.normal.x, sample.normal.y, sample.normal.z],
|
|
1448
|
+
value: thickness,
|
|
1449
|
+
className: thicknessClass(thickness, options),
|
|
1450
|
+
color: thicknessColor(thickness, options),
|
|
1451
|
+
area: sample.area
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1455
|
+
const color = thicknessColor(triangleThicknessValues[tri], options);
|
|
1456
|
+
const offset = tri * 3;
|
|
1457
|
+
for (let vertex = 0; vertex < 3; vertex += 1) {
|
|
1458
|
+
const colorOffset = (offset + vertex) * 3;
|
|
1459
|
+
colors[colorOffset] = color[0] / 255;
|
|
1460
|
+
colors[colorOffset + 1] = color[1] / 255;
|
|
1461
|
+
colors[colorOffset + 2] = color[2] / 255;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
1465
|
+
return {
|
|
1466
|
+
geometry,
|
|
1467
|
+
samples,
|
|
1468
|
+
pointSamples,
|
|
1469
|
+
triangleCount,
|
|
1470
|
+
sampledTriangleCount: sampledTriangleIndexes.size,
|
|
1471
|
+
sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
|
|
1472
|
+
warnings
|
|
1473
|
+
};
|
|
1474
|
+
} finally {
|
|
1475
|
+
rayContext.dispose();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
const DEFAULT_TEXTURE_SIZE_CAP = 4096;
|
|
1479
|
+
const DEFAULT_TARGET_TEXEL_SPACING_FACTOR = 0.5;
|
|
1480
|
+
const DEFAULT_PLANAR_TARGET_TEXEL_SPACING_FACTOR = 0.12;
|
|
1481
|
+
const PATCH_GUTTER = 2;
|
|
1482
|
+
const MIN_PATCH_SIDE = 4;
|
|
1483
|
+
const MAX_PACK_ATTEMPTS = 8;
|
|
1484
|
+
const CREASE_NORMAL_DOT = Math.SQRT1_2;
|
|
1485
|
+
const PLANAR_COMPONENT_NORMAL_DOT = Math.cos(Math.PI / 180);
|
|
1486
|
+
function nextPowerOfTwo(value) {
|
|
1487
|
+
return 2 ** Math.ceil(Math.log2(Math.max(1, value)));
|
|
1488
|
+
}
|
|
1489
|
+
function clamp(value, min, max) {
|
|
1490
|
+
return Math.min(max, Math.max(min, value));
|
|
1491
|
+
}
|
|
1492
|
+
function vertexAt(position, index, out) {
|
|
1493
|
+
return out.fromBufferAttribute(position, index);
|
|
1494
|
+
}
|
|
1495
|
+
function triangleMetrics(position, tri, a, b, c) {
|
|
1496
|
+
vertexAt(position, tri * 3, a);
|
|
1497
|
+
vertexAt(position, tri * 3 + 1, b);
|
|
1498
|
+
vertexAt(position, tri * 3 + 2, c);
|
|
1499
|
+
const ab = b.distanceTo(a);
|
|
1500
|
+
const bc = c.distanceTo(b);
|
|
1501
|
+
const ca = a.distanceTo(c);
|
|
1502
|
+
const area = b.clone().sub(a).cross(c.clone().sub(a)).length() * 0.5;
|
|
1503
|
+
return { area, maxEdge: Math.max(ab, bc, ca) };
|
|
1504
|
+
}
|
|
1505
|
+
function buildTriangleInfos(position) {
|
|
1506
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1507
|
+
const infos = [];
|
|
1508
|
+
const box = new Box3();
|
|
1509
|
+
const a = new Vector3();
|
|
1510
|
+
const b = new Vector3();
|
|
1511
|
+
const c = new Vector3();
|
|
1512
|
+
const ab = new Vector3();
|
|
1513
|
+
const ac = new Vector3();
|
|
1514
|
+
for (let i = 0; i < position.count; i += 1) {
|
|
1515
|
+
box.expandByPoint(vertexAt(position, i, a));
|
|
1516
|
+
}
|
|
1517
|
+
const size = new Vector3();
|
|
1518
|
+
box.getSize(size);
|
|
1519
|
+
const tolerance = Math.max(size.length() * 1e-5, Number.EPSILON);
|
|
1520
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1521
|
+
const va = vertexAt(position, tri * 3, a).clone();
|
|
1522
|
+
const vb = vertexAt(position, tri * 3 + 1, b).clone();
|
|
1523
|
+
const vc = vertexAt(position, tri * 3 + 2, c).clone();
|
|
1524
|
+
const abLen = va.distanceTo(vb);
|
|
1525
|
+
const bcLen = vb.distanceTo(vc);
|
|
1526
|
+
const caLen = vc.distanceTo(va);
|
|
1527
|
+
ab.subVectors(vb, va);
|
|
1528
|
+
ac.subVectors(vc, va);
|
|
1529
|
+
const normal = ab.clone().cross(ac);
|
|
1530
|
+
const area = normal.length() * 0.5;
|
|
1531
|
+
normal.normalize();
|
|
1532
|
+
infos.push({
|
|
1533
|
+
tri,
|
|
1534
|
+
a: va,
|
|
1535
|
+
b: vb,
|
|
1536
|
+
c: vc,
|
|
1537
|
+
normal,
|
|
1538
|
+
area,
|
|
1539
|
+
maxEdge: Math.max(abLen, bcLen, caLen)
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return { infos, tolerance };
|
|
1543
|
+
}
|
|
1544
|
+
function quantizedPointKey(point, tolerance) {
|
|
1545
|
+
const inv = 1 / tolerance;
|
|
1546
|
+
return `${Math.round(point.x * inv)},${Math.round(point.y * inv)},${Math.round(point.z * inv)}`;
|
|
1547
|
+
}
|
|
1548
|
+
function edgeKey$1(left, right) {
|
|
1549
|
+
return left < right ? `${left}|${right}` : `${right}|${left}`;
|
|
1550
|
+
}
|
|
1551
|
+
function triangleVertexKeys(info, tolerance) {
|
|
1552
|
+
return [quantizedPointKey(info.a, tolerance), quantizedPointKey(info.b, tolerance), quantizedPointKey(info.c, tolerance)];
|
|
1553
|
+
}
|
|
1554
|
+
function buildTriangleAdjacency(infos, tolerance) {
|
|
1555
|
+
const edgeToTris = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const info of infos) {
|
|
1557
|
+
const keys = triangleVertexKeys(info, tolerance);
|
|
1558
|
+
for (const [left, right] of [
|
|
1559
|
+
[keys[0], keys[1]],
|
|
1560
|
+
[keys[1], keys[2]],
|
|
1561
|
+
[keys[2], keys[0]]
|
|
1562
|
+
]) {
|
|
1563
|
+
const key = edgeKey$1(left, right);
|
|
1564
|
+
const tris = edgeToTris.get(key) ?? [];
|
|
1565
|
+
tris.push(info.tri);
|
|
1566
|
+
edgeToTris.set(key, tris);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const adjacency = Array.from({ length: infos.length }, () => /* @__PURE__ */ new Set());
|
|
1570
|
+
for (const tris of edgeToTris.values()) {
|
|
1571
|
+
for (let i = 0; i < tris.length; i += 1) {
|
|
1572
|
+
for (let j = i + 1; j < tris.length; j += 1) {
|
|
1573
|
+
adjacency[tris[i]].add(tris[j]);
|
|
1574
|
+
adjacency[tris[j]].add(tris[i]);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return adjacency.map((neighbors) => [...neighbors]);
|
|
1579
|
+
}
|
|
1580
|
+
function triangleOnPlane(info, origin, normal, tolerance) {
|
|
1581
|
+
return Math.abs(info.a.clone().sub(origin).dot(normal)) <= tolerance && Math.abs(info.b.clone().sub(origin).dot(normal)) <= tolerance && Math.abs(info.c.clone().sub(origin).dot(normal)) <= tolerance;
|
|
1582
|
+
}
|
|
1583
|
+
function detectPlanarComponents(infos, tolerance) {
|
|
1584
|
+
const adjacency = buildTriangleAdjacency(infos, tolerance);
|
|
1585
|
+
const assigned = new Array(infos.length).fill(false);
|
|
1586
|
+
const components = [];
|
|
1587
|
+
for (const seed of infos) {
|
|
1588
|
+
if (assigned[seed.tri] || seed.area <= 0) continue;
|
|
1589
|
+
const queue = [seed.tri];
|
|
1590
|
+
const component = [];
|
|
1591
|
+
assigned[seed.tri] = true;
|
|
1592
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
1593
|
+
const tri = queue[cursor];
|
|
1594
|
+
component.push(tri);
|
|
1595
|
+
for (const neighborTri of adjacency[tri]) {
|
|
1596
|
+
if (assigned[neighborTri]) continue;
|
|
1597
|
+
const neighbor = infos[neighborTri];
|
|
1598
|
+
if (neighbor.area <= 0) continue;
|
|
1599
|
+
if (neighbor.normal.dot(seed.normal) < PLANAR_COMPONENT_NORMAL_DOT) continue;
|
|
1600
|
+
if (!triangleOnPlane(neighbor, seed.a, seed.normal, tolerance)) continue;
|
|
1601
|
+
assigned[neighborTri] = true;
|
|
1602
|
+
queue.push(neighborTri);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (component.length > 1) components.push(component);
|
|
1606
|
+
}
|
|
1607
|
+
return components;
|
|
1608
|
+
}
|
|
1609
|
+
function planarBasis(component, infos) {
|
|
1610
|
+
const origin = infos[component[0]].a.clone();
|
|
1611
|
+
const normal = new Vector3();
|
|
1612
|
+
let bestEdge = new Vector3(1, 0, 0);
|
|
1613
|
+
let bestEdgeLength = 0;
|
|
1614
|
+
for (const tri of component) {
|
|
1615
|
+
const info = infos[tri];
|
|
1616
|
+
normal.addScaledVector(info.normal, info.area);
|
|
1617
|
+
for (const [left, right] of [
|
|
1618
|
+
[info.a, info.b],
|
|
1619
|
+
[info.b, info.c],
|
|
1620
|
+
[info.c, info.a]
|
|
1621
|
+
]) {
|
|
1622
|
+
const edge = right.clone().sub(left);
|
|
1623
|
+
const length = edge.length();
|
|
1624
|
+
if (length > bestEdgeLength) {
|
|
1625
|
+
bestEdgeLength = length;
|
|
1626
|
+
bestEdge = edge;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
normal.normalize();
|
|
1631
|
+
const uAxis = bestEdge.sub(normal.clone().multiplyScalar(bestEdge.dot(normal)));
|
|
1632
|
+
if (uAxis.lengthSq() < 1e-12) {
|
|
1633
|
+
uAxis.copy(Math.abs(normal.z) < 0.9 ? new Vector3(0, 0, 1) : new Vector3(1, 0, 0));
|
|
1634
|
+
uAxis.sub(normal.clone().multiplyScalar(uAxis.dot(normal)));
|
|
1635
|
+
}
|
|
1636
|
+
uAxis.normalize();
|
|
1637
|
+
const vAxis = normal.clone().cross(uAxis).normalize();
|
|
1638
|
+
return { origin, normal, uAxis, vAxis };
|
|
1639
|
+
}
|
|
1640
|
+
function planarCoordinate(point, patch) {
|
|
1641
|
+
const relative = point.clone().sub(patch.origin);
|
|
1642
|
+
return [relative.dot(patch.uAxis), relative.dot(patch.vAxis)];
|
|
1643
|
+
}
|
|
1644
|
+
function buildPlanarPatch(component, infos, targetTexel) {
|
|
1645
|
+
const basis = planarBasis(component, infos);
|
|
1646
|
+
let minU = Infinity;
|
|
1647
|
+
let maxU = -Infinity;
|
|
1648
|
+
let minV = Infinity;
|
|
1649
|
+
let maxV = -Infinity;
|
|
1650
|
+
for (const tri of component) {
|
|
1651
|
+
const info = infos[tri];
|
|
1652
|
+
for (const point of [info.a, info.b, info.c]) {
|
|
1653
|
+
const [u, v] = planarCoordinate(point, basis);
|
|
1654
|
+
minU = Math.min(minU, u);
|
|
1655
|
+
maxU = Math.max(maxU, u);
|
|
1656
|
+
minV = Math.min(minV, v);
|
|
1657
|
+
maxV = Math.max(maxV, v);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const spanU = Math.max(maxU - minU, targetTexel);
|
|
1661
|
+
const spanV = Math.max(maxV - minV, targetTexel);
|
|
1662
|
+
const innerU = Math.max(1, Math.ceil(spanU / targetTexel));
|
|
1663
|
+
const innerV = Math.max(1, Math.ceil(spanV / targetTexel));
|
|
1664
|
+
return {
|
|
1665
|
+
kind: "planar",
|
|
1666
|
+
tris: component,
|
|
1667
|
+
width: innerU + PATCH_GUTTER * 2 + 1,
|
|
1668
|
+
height: innerV + PATCH_GUTTER * 2 + 1,
|
|
1669
|
+
innerU,
|
|
1670
|
+
innerV,
|
|
1671
|
+
x: 0,
|
|
1672
|
+
y: 0,
|
|
1673
|
+
...basis,
|
|
1674
|
+
minU,
|
|
1675
|
+
maxU,
|
|
1676
|
+
minV,
|
|
1677
|
+
maxV
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function buildScalarPatches(infos, planarComponents, targetTexel, planarTargetTexel) {
|
|
1681
|
+
const planarTriSet = new Set(planarComponents.flat());
|
|
1682
|
+
const patches = planarComponents.map((component) => buildPlanarPatch(component, infos, planarTargetTexel));
|
|
1683
|
+
for (const info of infos) {
|
|
1684
|
+
if (planarTriSet.has(info.tri)) continue;
|
|
1685
|
+
const inner = Math.max(1, Math.ceil(info.maxEdge / targetTexel));
|
|
1686
|
+
const side = Math.max(MIN_PATCH_SIDE, inner + PATCH_GUTTER * 2 + 1);
|
|
1687
|
+
patches.push({
|
|
1688
|
+
kind: "triangle",
|
|
1689
|
+
tri: info.tri,
|
|
1690
|
+
width: side,
|
|
1691
|
+
height: side,
|
|
1692
|
+
side,
|
|
1693
|
+
inner: side - PATCH_GUTTER * 2 - 1,
|
|
1694
|
+
x: 0,
|
|
1695
|
+
y: 0
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
return patches;
|
|
1699
|
+
}
|
|
1700
|
+
function packPatches(position, sampleSpacing, options) {
|
|
1701
|
+
const textureSizeCap = Math.max(64, Math.floor(options.textureSizeCap ?? DEFAULT_TEXTURE_SIZE_CAP));
|
|
1702
|
+
const targetFactor = options.targetTexelSpacingFactor ?? DEFAULT_TARGET_TEXEL_SPACING_FACTOR;
|
|
1703
|
+
const planarTargetFactor = options.planarTargetTexelSpacingFactor ?? DEFAULT_PLANAR_TARGET_TEXEL_SPACING_FACTOR;
|
|
1704
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1705
|
+
const { infos, tolerance } = buildTriangleInfos(position);
|
|
1706
|
+
const planarComponents = detectPlanarComponents(infos, tolerance);
|
|
1707
|
+
let targetTexel = Math.max(1e-6, sampleSpacing * targetFactor);
|
|
1708
|
+
let planarTargetTexel = Math.max(1e-6, sampleSpacing * planarTargetFactor);
|
|
1709
|
+
let capped = false;
|
|
1710
|
+
for (let attempt = 0; attempt < MAX_PACK_ATTEMPTS; attempt += 1) {
|
|
1711
|
+
const patches = buildScalarPatches(infos, planarComponents, targetTexel, planarTargetTexel);
|
|
1712
|
+
const patchArea = patches.reduce((sum, patch) => sum + patch.width * patch.height, 0);
|
|
1713
|
+
let width = nextPowerOfTwo(Math.ceil(Math.sqrt(patchArea)));
|
|
1714
|
+
width = clamp(width, 256, textureSizeCap);
|
|
1715
|
+
const maxPatchWidth = patches.reduce((max, patch) => Math.max(max, patch.width), 0);
|
|
1716
|
+
const maxPatchHeight = patches.reduce((max, patch) => Math.max(max, patch.height), 0);
|
|
1717
|
+
const maxPatchDimension = Math.max(maxPatchWidth, maxPatchHeight);
|
|
1718
|
+
width = Math.max(width, Math.min(textureSizeCap, nextPowerOfTwo(maxPatchWidth)));
|
|
1719
|
+
let x = 0;
|
|
1720
|
+
let y = 0;
|
|
1721
|
+
let rowH = 0;
|
|
1722
|
+
for (const patch of [...patches].sort((left, right) => right.height - left.height || right.width - left.width)) {
|
|
1723
|
+
if (x + patch.width > width) {
|
|
1724
|
+
x = 0;
|
|
1725
|
+
y += rowH;
|
|
1726
|
+
rowH = 0;
|
|
1727
|
+
}
|
|
1728
|
+
patch.x = x;
|
|
1729
|
+
patch.y = y;
|
|
1730
|
+
x += patch.width;
|
|
1731
|
+
rowH = Math.max(rowH, patch.height);
|
|
1732
|
+
}
|
|
1733
|
+
const height = nextPowerOfTwo(y + rowH);
|
|
1734
|
+
if (maxPatchDimension <= textureSizeCap && height <= textureSizeCap) {
|
|
1735
|
+
const byTri = new Array(triangleCount);
|
|
1736
|
+
patches.forEach((patch) => {
|
|
1737
|
+
if (patch.kind === "planar") {
|
|
1738
|
+
for (const tri of patch.tris) byTri[tri] = patch;
|
|
1739
|
+
} else {
|
|
1740
|
+
byTri[patch.tri] = patch;
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
return { patches, patchByTri: byTri, width, height, capped };
|
|
1744
|
+
}
|
|
1745
|
+
capped = true;
|
|
1746
|
+
const grow = Math.sqrt(Math.max(height / textureSizeCap, maxPatchDimension / textureSizeCap)) * 1.05;
|
|
1747
|
+
targetTexel *= grow;
|
|
1748
|
+
planarTargetTexel *= grow;
|
|
1749
|
+
}
|
|
1750
|
+
throw new Error(`buildThicknessSurfaceScalarField: could not pack scalar texture atlas within ${textureSizeCap}px`);
|
|
1751
|
+
}
|
|
1752
|
+
function projectToRightTriangle(u, v) {
|
|
1753
|
+
let uu = clamp(u, 0, 1);
|
|
1754
|
+
let vv = clamp(v, 0, 1);
|
|
1755
|
+
const sum = uu + vv;
|
|
1756
|
+
if (sum > 1) {
|
|
1757
|
+
uu /= sum;
|
|
1758
|
+
vv /= sum;
|
|
1759
|
+
}
|
|
1760
|
+
return [uu, vv];
|
|
1761
|
+
}
|
|
1762
|
+
function triangleParamForTexel(px, py, inner) {
|
|
1763
|
+
return projectToRightTriangle((px - PATCH_GUTTER) / inner, (py - PATCH_GUTTER) / inner);
|
|
1764
|
+
}
|
|
1765
|
+
function barycentric2d(px, py, ax, ay, bx, by, cx, cy) {
|
|
1766
|
+
const v0x = bx - ax;
|
|
1767
|
+
const v0y = by - ay;
|
|
1768
|
+
const v1x = cx - ax;
|
|
1769
|
+
const v1y = cy - ay;
|
|
1770
|
+
const v2x = px - ax;
|
|
1771
|
+
const v2y = py - ay;
|
|
1772
|
+
const d00 = v0x * v0x + v0y * v0y;
|
|
1773
|
+
const d01 = v0x * v1x + v0y * v1y;
|
|
1774
|
+
const d11 = v1x * v1x + v1y * v1y;
|
|
1775
|
+
const d20 = v2x * v0x + v2y * v0y;
|
|
1776
|
+
const d21 = v2x * v1x + v2y * v1y;
|
|
1777
|
+
const denom = d00 * d11 - d01 * d01;
|
|
1778
|
+
if (Math.abs(denom) < 1e-12) return null;
|
|
1779
|
+
const v = (d11 * d20 - d01 * d21) / denom;
|
|
1780
|
+
const w = (d00 * d21 - d01 * d20) / denom;
|
|
1781
|
+
return [1 - v - w, v, w];
|
|
1782
|
+
}
|
|
1783
|
+
function setPlanarVertexUv(uvs, vertexIndex, point, patch, textureWidth, textureHeight) {
|
|
1784
|
+
const [u, v] = planarCoordinate(point, patch);
|
|
1785
|
+
const spanU = Math.max(patch.maxU - patch.minU, 1e-9);
|
|
1786
|
+
const spanV = Math.max(patch.maxV - patch.minV, 1e-9);
|
|
1787
|
+
const gridU = PATCH_GUTTER + (u - patch.minU) / spanU * patch.innerU;
|
|
1788
|
+
const gridV = PATCH_GUTTER + (v - patch.minV) / spanV * patch.innerV;
|
|
1789
|
+
uvs[vertexIndex * 2] = (patch.x + gridU + 0.5) / textureWidth;
|
|
1790
|
+
uvs[vertexIndex * 2 + 1] = (patch.y + gridV + 0.5) / textureHeight;
|
|
1791
|
+
}
|
|
1792
|
+
function rasterizePlanarPatch(values, textureWidth, textureHeight, patch, position, measurement, measurementContext) {
|
|
1793
|
+
const spanU = Math.max(patch.maxU - patch.minU, 1e-9);
|
|
1794
|
+
const spanV = Math.max(patch.maxV - patch.minV, 1e-9);
|
|
1795
|
+
const a = new Vector3();
|
|
1796
|
+
const b = new Vector3();
|
|
1797
|
+
const c = new Vector3();
|
|
1798
|
+
const point = new Vector3();
|
|
1799
|
+
for (const tri of patch.tris) {
|
|
1800
|
+
vertexAt(position, tri * 3, a);
|
|
1801
|
+
vertexAt(position, tri * 3 + 1, b);
|
|
1802
|
+
vertexAt(position, tri * 3 + 2, c);
|
|
1803
|
+
const [au, av] = planarCoordinate(a, patch);
|
|
1804
|
+
const [bu, bv] = planarCoordinate(b, patch);
|
|
1805
|
+
const [cu, cv] = planarCoordinate(c, patch);
|
|
1806
|
+
const ax = PATCH_GUTTER + (au - patch.minU) / spanU * patch.innerU;
|
|
1807
|
+
const ay = PATCH_GUTTER + (av - patch.minV) / spanV * patch.innerV;
|
|
1808
|
+
const bx = PATCH_GUTTER + (bu - patch.minU) / spanU * patch.innerU;
|
|
1809
|
+
const by = PATCH_GUTTER + (bv - patch.minV) / spanV * patch.innerV;
|
|
1810
|
+
const cx = PATCH_GUTTER + (cu - patch.minU) / spanU * patch.innerU;
|
|
1811
|
+
const cy = PATCH_GUTTER + (cv - patch.minV) / spanV * patch.innerV;
|
|
1812
|
+
const minX = clamp(Math.floor(Math.min(ax, bx, cx)) - 1, 0, patch.width - 1);
|
|
1813
|
+
const maxX = clamp(Math.ceil(Math.max(ax, bx, cx)) + 1, 0, patch.width - 1);
|
|
1814
|
+
const minY = clamp(Math.floor(Math.min(ay, by, cy)) - 1, 0, patch.height - 1);
|
|
1815
|
+
const maxY = clamp(Math.ceil(Math.max(ay, by, cy)) + 1, 0, patch.height - 1);
|
|
1816
|
+
for (let py = minY; py <= maxY; py += 1) {
|
|
1817
|
+
for (let px = minX; px <= maxX; px += 1) {
|
|
1818
|
+
const bary = barycentric2d(px, py, ax, ay, bx, by, cx, cy);
|
|
1819
|
+
if (!bary) continue;
|
|
1820
|
+
const [wa, wb, wc] = bary;
|
|
1821
|
+
if (wa < -1e-6 || wb < -1e-6 || wc < -1e-6) continue;
|
|
1822
|
+
point.copy(a).multiplyScalar(wa).addScaledVector(b, wb).addScaledVector(c, wc);
|
|
1823
|
+
const value = measurement.measureAtPoint(measurementContext, point, patch.normal);
|
|
1824
|
+
if (value == null) continue;
|
|
1825
|
+
const texelX = patch.x + px;
|
|
1826
|
+
const texelY = patch.y + py;
|
|
1827
|
+
if (texelX < 0 || texelY < 0 || texelX >= textureWidth || texelY >= textureHeight) continue;
|
|
1828
|
+
values[texelY * textureWidth + texelX] = value;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function fillPatchHoles(values, width, patch, fill) {
|
|
1834
|
+
const minX = patch.x;
|
|
1835
|
+
const minY = patch.y;
|
|
1836
|
+
const maxX = patch.x + patch.width;
|
|
1837
|
+
const maxY = patch.y + patch.height;
|
|
1838
|
+
for (let pass = 0; pass < Math.max(8, patch.width, patch.height); pass += 1) {
|
|
1839
|
+
let changed = false;
|
|
1840
|
+
for (let y = minY; y < maxY; y += 1) {
|
|
1841
|
+
for (let x = minX; x < maxX; x += 1) {
|
|
1842
|
+
const idx = y * width + x;
|
|
1843
|
+
if (Number.isFinite(values[idx])) continue;
|
|
1844
|
+
let sum = 0;
|
|
1845
|
+
let count = 0;
|
|
1846
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
1847
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
1848
|
+
if (dx === 0 && dy === 0) continue;
|
|
1849
|
+
const xx = x + dx;
|
|
1850
|
+
const yy = y + dy;
|
|
1851
|
+
if (xx < minX || yy < minY || xx >= maxX || yy >= maxY) continue;
|
|
1852
|
+
const value = values[yy * width + xx];
|
|
1853
|
+
if (!Number.isFinite(value)) continue;
|
|
1854
|
+
sum += value;
|
|
1855
|
+
count += 1;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (count > 0) {
|
|
1859
|
+
values[idx] = sum / count;
|
|
1860
|
+
changed = true;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (!changed) break;
|
|
1865
|
+
}
|
|
1866
|
+
for (let y = minY; y < maxY; y += 1) {
|
|
1867
|
+
for (let x = minX; x < maxX; x += 1) {
|
|
1868
|
+
const idx = y * width + x;
|
|
1869
|
+
if (!Number.isFinite(values[idx])) values[idx] = fill;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function fillUnusedTextureCells(values, fill) {
|
|
1874
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1875
|
+
if (!Number.isFinite(values[i])) values[i] = fill;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function valueRange(values) {
|
|
1879
|
+
let min = Infinity;
|
|
1880
|
+
let max = -Infinity;
|
|
1881
|
+
for (const value of values) {
|
|
1882
|
+
if (!Number.isFinite(value)) continue;
|
|
1883
|
+
if (value < min) min = value;
|
|
1884
|
+
if (value > max) max = value;
|
|
1885
|
+
}
|
|
1886
|
+
return Number.isFinite(min) && Number.isFinite(max) ? { min, max } : { min: 0, max: 0 };
|
|
1887
|
+
}
|
|
1888
|
+
function sampleNearest(values, width, height, u, v) {
|
|
1889
|
+
const x = clamp(Math.round(u * width - 0.5), 0, width - 1);
|
|
1890
|
+
const y = clamp(Math.round(v * height - 0.5), 0, height - 1);
|
|
1891
|
+
return values[y * width + x];
|
|
1892
|
+
}
|
|
1893
|
+
function buildCreaseAwareRenderNormals(position) {
|
|
1894
|
+
const normals = new Float32Array(position.count * 3);
|
|
1895
|
+
const a = new Vector3();
|
|
1896
|
+
const b = new Vector3();
|
|
1897
|
+
const c = new Vector3();
|
|
1898
|
+
const face = new Vector3();
|
|
1899
|
+
const box = new Box3();
|
|
1900
|
+
for (let i = 0; i < position.count; i += 1) {
|
|
1901
|
+
box.expandByPoint(vertexAt(position, i, a));
|
|
1902
|
+
}
|
|
1903
|
+
const size = new Vector3();
|
|
1904
|
+
box.getSize(size);
|
|
1905
|
+
const tolerance = Math.max(size.length() * 1e-5, Number.EPSILON);
|
|
1906
|
+
const inv = 1 / tolerance;
|
|
1907
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1908
|
+
for (let tri = 0; tri < Math.floor(position.count / 3); tri += 1) {
|
|
1909
|
+
vertexAt(position, tri * 3, a);
|
|
1910
|
+
vertexAt(position, tri * 3 + 1, b);
|
|
1911
|
+
vertexAt(position, tri * 3 + 2, c);
|
|
1912
|
+
face.subVectors(b, a).cross(c.clone().sub(a)).normalize();
|
|
1913
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
1914
|
+
const vi = tri * 3 + corner;
|
|
1915
|
+
vertexAt(position, vi, a);
|
|
1916
|
+
const key = `${Math.round(a.x * inv)},${Math.round(a.y * inv)},${Math.round(a.z * inv)}`;
|
|
1917
|
+
let groups = buckets.get(key);
|
|
1918
|
+
if (!groups) {
|
|
1919
|
+
groups = [];
|
|
1920
|
+
buckets.set(key, groups);
|
|
1921
|
+
}
|
|
1922
|
+
let group = groups.find((candidate) => candidate.normal.dot(face) >= CREASE_NORMAL_DOT);
|
|
1923
|
+
if (!group) {
|
|
1924
|
+
group = { normal: face.clone(), vertices: [] };
|
|
1925
|
+
groups.push(group);
|
|
1926
|
+
} else {
|
|
1927
|
+
group.normal.add(face).normalize();
|
|
1928
|
+
}
|
|
1929
|
+
group.vertices.push(vi);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
for (const groups of buckets.values()) {
|
|
1933
|
+
for (const group of groups) {
|
|
1934
|
+
for (const vi of group.vertices) {
|
|
1935
|
+
normals[vi * 3] = group.normal.x;
|
|
1936
|
+
normals[vi * 3 + 1] = group.normal.y;
|
|
1937
|
+
normals[vi * 3 + 2] = group.normal.z;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return normals;
|
|
1942
|
+
}
|
|
1943
|
+
function buildMeasuredSurfaceScalarField(sourceGeometry, rawOptions, fieldOptions = {}, context = {}, measurement) {
|
|
1944
|
+
const options = measurement.resolveOptions(rawOptions);
|
|
1945
|
+
const fieldGeometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry;
|
|
1946
|
+
try {
|
|
1947
|
+
const position = fieldGeometry.getAttribute("position");
|
|
1948
|
+
if (!position || position.count < 3) {
|
|
1949
|
+
throw new Error(`${measurement.errorPrefix}: sourceGeometry must contain triangle positions`);
|
|
1950
|
+
}
|
|
1951
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
1952
|
+
const a = new Vector3();
|
|
1953
|
+
const b = new Vector3();
|
|
1954
|
+
const c = new Vector3();
|
|
1955
|
+
let surfaceArea = 0;
|
|
1956
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1957
|
+
surfaceArea += triangleMetrics(position, tri, a, b, c).area;
|
|
1958
|
+
}
|
|
1959
|
+
const sampleSpacing = surfaceArea > 0 ? Math.sqrt(surfaceArea / Math.max(1, options.maxSamplesPerObject)) : 1;
|
|
1960
|
+
const { patches, patchByTri, width, height, capped } = packPatches(position, sampleSpacing, fieldOptions);
|
|
1961
|
+
const values = new Float32Array(width * height);
|
|
1962
|
+
values.fill(NaN);
|
|
1963
|
+
const positions = new Float32Array(position.count * 3);
|
|
1964
|
+
const normals = buildCreaseAwareRenderNormals(position);
|
|
1965
|
+
const uvs = new Float32Array(position.count * 2);
|
|
1966
|
+
const index = new Uint32Array(position.count);
|
|
1967
|
+
const aValue = new Float32Array(position.count);
|
|
1968
|
+
const measurementContext = measurement.createContext(fieldGeometry, rawOptions, context);
|
|
1969
|
+
const point = new Vector3();
|
|
1970
|
+
const normal = new Vector3();
|
|
1971
|
+
const ab = new Vector3();
|
|
1972
|
+
const ac = new Vector3();
|
|
1973
|
+
try {
|
|
1974
|
+
for (let i = 0; i < position.count; i += 1) {
|
|
1975
|
+
vertexAt(position, i, point);
|
|
1976
|
+
positions[i * 3] = point.x;
|
|
1977
|
+
positions[i * 3 + 1] = point.y;
|
|
1978
|
+
positions[i * 3 + 2] = point.z;
|
|
1979
|
+
index[i] = i;
|
|
1980
|
+
}
|
|
1981
|
+
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
1982
|
+
const patch = patchByTri[tri];
|
|
1983
|
+
vertexAt(position, tri * 3, a);
|
|
1984
|
+
vertexAt(position, tri * 3 + 1, b);
|
|
1985
|
+
vertexAt(position, tri * 3 + 2, c);
|
|
1986
|
+
if (patch.kind === "planar") {
|
|
1987
|
+
setPlanarVertexUv(uvs, tri * 3, a, patch, width, height);
|
|
1988
|
+
setPlanarVertexUv(uvs, tri * 3 + 1, b, patch, width, height);
|
|
1989
|
+
setPlanarVertexUv(uvs, tri * 3 + 2, c, patch, width, height);
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
ab.subVectors(b, a);
|
|
1993
|
+
ac.subVectors(c, a);
|
|
1994
|
+
normal.copy(ab).cross(ac).normalize();
|
|
1995
|
+
uvs[tri * 6] = (patch.x + PATCH_GUTTER + 0.5) / width;
|
|
1996
|
+
uvs[tri * 6 + 1] = (patch.y + PATCH_GUTTER + 0.5) / height;
|
|
1997
|
+
uvs[tri * 6 + 2] = (patch.x + PATCH_GUTTER + patch.inner + 0.5) / width;
|
|
1998
|
+
uvs[tri * 6 + 3] = (patch.y + PATCH_GUTTER + 0.5) / height;
|
|
1999
|
+
uvs[tri * 6 + 4] = (patch.x + PATCH_GUTTER + 0.5) / width;
|
|
2000
|
+
uvs[tri * 6 + 5] = (patch.y + PATCH_GUTTER + patch.inner + 0.5) / height;
|
|
2001
|
+
for (let py = 0; py < patch.side; py += 1) {
|
|
2002
|
+
for (let px = 0; px < patch.side; px += 1) {
|
|
2003
|
+
const [u, v] = triangleParamForTexel(px, py, patch.inner);
|
|
2004
|
+
point.copy(a).addScaledVector(ab, u).addScaledVector(ac, v);
|
|
2005
|
+
const value = measurement.measureAtPoint(measurementContext, point, normal);
|
|
2006
|
+
const texel = (patch.y + py) * width + patch.x + px;
|
|
2007
|
+
if (value != null) values[texel] = value;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
for (const patch of patches) {
|
|
2012
|
+
if (patch.kind === "planar") rasterizePlanarPatch(values, width, height, patch, position, measurement, measurementContext);
|
|
2013
|
+
}
|
|
2014
|
+
} finally {
|
|
2015
|
+
measurementContext.dispose();
|
|
2016
|
+
}
|
|
2017
|
+
const range = valueRange(values);
|
|
2018
|
+
for (const patch of patches) fillPatchHoles(values, width, patch, range.min);
|
|
2019
|
+
const finalRange = valueRange(values);
|
|
2020
|
+
fillUnusedTextureCells(values, finalRange.min);
|
|
2021
|
+
for (let i = 0; i < position.count; i += 1) {
|
|
2022
|
+
aValue[i] = sampleNearest(values, width, height, uvs[i * 2], uvs[i * 2 + 1]);
|
|
2023
|
+
}
|
|
2024
|
+
return {
|
|
2025
|
+
positions,
|
|
2026
|
+
normals,
|
|
2027
|
+
index,
|
|
2028
|
+
values: aValue,
|
|
2029
|
+
valueMin: finalRange.min,
|
|
2030
|
+
valueMax: finalRange.max,
|
|
2031
|
+
capped,
|
|
2032
|
+
degenerate: false,
|
|
2033
|
+
holeCount: 0,
|
|
2034
|
+
vertexCount: position.count,
|
|
2035
|
+
uvs,
|
|
2036
|
+
textureValues: values,
|
|
2037
|
+
textureWidth: width,
|
|
2038
|
+
textureHeight: height
|
|
2039
|
+
};
|
|
2040
|
+
} finally {
|
|
2041
|
+
if (fieldGeometry !== sourceGeometry) fieldGeometry.dispose();
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function buildThicknessSurfaceScalarField(sourceGeometry, rawOptions = {}, fieldOptions = {}, context = {}) {
|
|
2045
|
+
return buildMeasuredSurfaceScalarField(sourceGeometry, rawOptions, fieldOptions, context, {
|
|
2046
|
+
errorPrefix: "buildThicknessSurfaceScalarField",
|
|
2047
|
+
resolveOptions: resolveThicknessInspectionOptions,
|
|
2048
|
+
createContext: createThicknessRaycastContext,
|
|
2049
|
+
measureAtPoint: measureThicknessAtPoint
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
function makeThroughThicknessTarget(sourceGeometry, rayMaterial) {
|
|
2053
|
+
const geometry = sourceGeometry.clone();
|
|
2054
|
+
const bvh = new MeshBVH(geometry);
|
|
2055
|
+
geometry.boundsTree = bvh;
|
|
2056
|
+
const mesh = new Mesh(geometry, rayMaterial);
|
|
2057
|
+
mesh.raycast = acceleratedRaycast;
|
|
2058
|
+
return { mesh, geometry, bvh };
|
|
2059
|
+
}
|
|
2060
|
+
function geometryMaxDimension(geometry) {
|
|
2061
|
+
geometry.computeBoundingBox();
|
|
2062
|
+
const box = geometry.boundingBox;
|
|
2063
|
+
if (!box) return 1;
|
|
2064
|
+
const size = new Vector3();
|
|
2065
|
+
box.getSize(size);
|
|
2066
|
+
return Math.max(1, size.x, size.y, size.z);
|
|
2067
|
+
}
|
|
2068
|
+
function resolveThroughThicknessInspectionOptions(raw = {}) {
|
|
2069
|
+
return resolveThicknessInspectionOptions(raw);
|
|
2070
|
+
}
|
|
2071
|
+
function createThroughThicknessContext(sourceGeometry, rawOptions = {}, context = {}) {
|
|
2072
|
+
const options = resolveThroughThicknessInspectionOptions(rawOptions);
|
|
2073
|
+
const connectedGeometries = context.connectedGeometries ?? [];
|
|
2074
|
+
const maxDim = Math.max(geometryMaxDimension(sourceGeometry), ...connectedGeometries.map(geometryMaxDimension));
|
|
2075
|
+
const epsilon = Math.max(1e-4, maxDim * 1e-6);
|
|
2076
|
+
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
1400
2077
|
const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
...connectedGeometries.map((
|
|
1404
|
-
mesh: new Mesh(connectedGeometry, rayMaterial),
|
|
1405
|
-
jumpable: true
|
|
1406
|
-
}))
|
|
2078
|
+
const targets = [
|
|
2079
|
+
makeThroughThicknessTarget(sourceGeometry, rayMaterial),
|
|
2080
|
+
...connectedGeometries.map((geometry) => makeThroughThicknessTarget(geometry, rayMaterial))
|
|
1407
2081
|
];
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
const sampledTriangleIndexes = /* @__PURE__ */ new Set();
|
|
1419
|
-
for (const sample of surfaceSamples) {
|
|
1420
|
-
sampledTriangleIndexes.add(sample.triangle.index);
|
|
1421
|
-
const thickness = triangleThickness(
|
|
1422
|
-
raycaster,
|
|
1423
|
-
rayTargetMeshes,
|
|
1424
|
-
jumpableMeshes,
|
|
1425
|
-
sample.position,
|
|
1426
|
-
sample.normal,
|
|
1427
|
-
epsilon,
|
|
1428
|
-
far,
|
|
1429
|
-
options.contactTolerance
|
|
1430
|
-
);
|
|
1431
|
-
samples.push({ thickness, area: sample.area });
|
|
1432
|
-
const previous = triangleThicknessValues[sample.triangle.index];
|
|
1433
|
-
if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
|
|
1434
|
-
triangleThicknessValues[sample.triangle.index] = thickness;
|
|
2082
|
+
return {
|
|
2083
|
+
raycaster: new Raycaster(),
|
|
2084
|
+
targets,
|
|
2085
|
+
epsilon,
|
|
2086
|
+
far,
|
|
2087
|
+
contactTolerance: options.contactTolerance,
|
|
2088
|
+
minInwardDot: 1e-5,
|
|
2089
|
+
dispose: () => {
|
|
2090
|
+
targets.forEach((target) => target.geometry.dispose());
|
|
2091
|
+
rayMaterial.dispose();
|
|
1435
2092
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function firstRayHitDistance(context, point, direction) {
|
|
2096
|
+
const origin = point.clone().addScaledVector(direction, context.epsilon);
|
|
2097
|
+
context.raycaster.set(origin, direction);
|
|
2098
|
+
context.raycaster.near = context.epsilon;
|
|
2099
|
+
context.raycaster.far = context.far;
|
|
2100
|
+
const hits = context.raycaster.intersectObjects(
|
|
2101
|
+
context.targets.map((target) => target.mesh),
|
|
2102
|
+
false
|
|
2103
|
+
);
|
|
2104
|
+
const minDistance = Math.max(context.epsilon, context.contactTolerance);
|
|
2105
|
+
for (const hit of hits) {
|
|
2106
|
+
if (hit.distance <= minDistance) continue;
|
|
2107
|
+
return hit.distance + context.epsilon;
|
|
2108
|
+
}
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
function boxDistanceToPointSq(box, point) {
|
|
2112
|
+
const x = point.x < box.min.x ? box.min.x - point.x : point.x > box.max.x ? point.x - box.max.x : 0;
|
|
2113
|
+
const y = point.y < box.min.y ? box.min.y - point.y : point.y > box.max.y ? point.y - box.max.y : 0;
|
|
2114
|
+
const z = point.z < box.min.z ? box.min.z - point.z : point.z > box.max.z ? point.z - box.max.z : 0;
|
|
2115
|
+
return x * x + y * y + z * z;
|
|
2116
|
+
}
|
|
2117
|
+
function candidateTravelsThroughMaterial(context, point, normal, candidate, candidateDistance) {
|
|
2118
|
+
if (candidateDistance <= Math.max(context.epsilon, context.contactTolerance)) return false;
|
|
2119
|
+
const direction = candidate.clone().sub(point).multiplyScalar(1 / candidateDistance);
|
|
2120
|
+
if (direction.dot(normal) >= -1e-5) return false;
|
|
2121
|
+
const firstHit = firstRayHitDistance(context, point, direction);
|
|
2122
|
+
if (firstHit == null) return false;
|
|
2123
|
+
const tolerance = Math.max(context.contactTolerance, context.epsilon * 8);
|
|
2124
|
+
return Math.abs(firstHit - candidateDistance) <= tolerance;
|
|
2125
|
+
}
|
|
2126
|
+
function measureThroughThicknessAtPoint(context, point, normal) {
|
|
2127
|
+
let bestDistanceSq = Infinity;
|
|
2128
|
+
const closest = new Vector3();
|
|
2129
|
+
const candidate = new Vector3();
|
|
2130
|
+
for (const target of context.targets) {
|
|
2131
|
+
target.bvh.shapecast({
|
|
2132
|
+
boundsTraverseOrder: (box) => boxDistanceToPointSq(box, point),
|
|
2133
|
+
intersectsBounds: (box) => boxDistanceToPointSq(box, point) < bestDistanceSq,
|
|
2134
|
+
intersectsTriangle: (triangle) => {
|
|
2135
|
+
triangle.closestPointToPoint(point, candidate);
|
|
2136
|
+
const distanceSq = candidate.distanceToSquared(point);
|
|
2137
|
+
if (distanceSq >= bestDistanceSq) return;
|
|
2138
|
+
const distance = Math.sqrt(distanceSq);
|
|
2139
|
+
if (!candidateTravelsThroughMaterial(context, point, normal, candidate, distance)) return;
|
|
2140
|
+
bestDistanceSq = distanceSq;
|
|
2141
|
+
closest.copy(candidate);
|
|
2142
|
+
}
|
|
1443
2143
|
});
|
|
1444
2144
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
2145
|
+
return Number.isFinite(bestDistanceSq) ? closest.distanceTo(point) : null;
|
|
2146
|
+
}
|
|
2147
|
+
function cloneGeometryForFaceColors(geometry) {
|
|
2148
|
+
return geometry.index ? geometry.toNonIndexed() : geometry.clone();
|
|
2149
|
+
}
|
|
2150
|
+
function analyzeThroughThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
|
|
2151
|
+
const options = resolveThroughThicknessInspectionOptions(rawOptions);
|
|
2152
|
+
const geometry = cloneGeometryForFaceColors(sourceGeometry);
|
|
2153
|
+
const position = geometry.getAttribute("position");
|
|
2154
|
+
if (!position || position.count < 3) {
|
|
2155
|
+
return {
|
|
2156
|
+
geometry,
|
|
2157
|
+
samples: [],
|
|
2158
|
+
pointSamples: [],
|
|
2159
|
+
triangleCount: 0,
|
|
2160
|
+
sampledTriangleCount: 0,
|
|
2161
|
+
sampleStride: 1,
|
|
2162
|
+
warnings: ["No triangle geometry."]
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
const triangleCount = Math.floor(position.count / 3);
|
|
2166
|
+
const surfaceTriangles = readSurfaceTriangles(position);
|
|
2167
|
+
const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
|
|
2168
|
+
const samples = [];
|
|
2169
|
+
const pointSamples = [];
|
|
2170
|
+
const warnings = [];
|
|
2171
|
+
const throughContext = createThroughThicknessContext(geometry, options, context);
|
|
2172
|
+
try {
|
|
2173
|
+
if (surfaceTriangles.length === 0) {
|
|
2174
|
+
warnings.push("No non-degenerate triangle surface was available for through-thickness sampling.");
|
|
2175
|
+
} else if (surfaceSamples.length < surfaceTriangles.length) {
|
|
2176
|
+
warnings.push(
|
|
2177
|
+
`Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
const sampledTriangleIndexes = /* @__PURE__ */ new Set();
|
|
2181
|
+
for (const sample of surfaceSamples) {
|
|
2182
|
+
sampledTriangleIndexes.add(sample.triangle.index);
|
|
2183
|
+
const throughThickness = measureThroughThicknessAtPoint(throughContext, sample.position, sample.normal);
|
|
2184
|
+
samples.push({ thickness: throughThickness, area: sample.area });
|
|
2185
|
+
pointSamples.push({
|
|
2186
|
+
position: [sample.position.x, sample.position.y, sample.position.z],
|
|
2187
|
+
normal: [sample.normal.x, sample.normal.y, sample.normal.z],
|
|
2188
|
+
value: throughThickness,
|
|
2189
|
+
className: thicknessClass(throughThickness, options),
|
|
2190
|
+
color: thicknessColor(throughThickness, options),
|
|
2191
|
+
area: sample.area
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
return {
|
|
2195
|
+
geometry,
|
|
2196
|
+
samples,
|
|
2197
|
+
pointSamples,
|
|
2198
|
+
triangleCount,
|
|
2199
|
+
sampledTriangleCount: sampledTriangleIndexes.size,
|
|
2200
|
+
sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
|
|
2201
|
+
warnings
|
|
2202
|
+
};
|
|
2203
|
+
} finally {
|
|
2204
|
+
throughContext.dispose();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const DEFAULT_VERTEX_CAP = 2e6;
|
|
2208
|
+
const DEFAULT_TARGET_EDGE_SPACING_FACTOR = 2;
|
|
2209
|
+
const DEFAULT_K = 8;
|
|
2210
|
+
const DEFAULT_GATE_DOT = 0.3;
|
|
2211
|
+
const CREASE_WELD_DOT = Math.SQRT1_2;
|
|
2212
|
+
const SCATTER_CELL_SPACING_FACTOR = 1.5;
|
|
2213
|
+
const SCATTER_RADIUS_SPACING_FACTOR = 3;
|
|
2214
|
+
const IDW_DISTANCE_FLOOR = 1e-9;
|
|
2215
|
+
const MAX_SUBDIVISION_PASSES = 24;
|
|
2216
|
+
function subVec(a, b) {
|
|
2217
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
2218
|
+
}
|
|
2219
|
+
function crossVec(a, b) {
|
|
2220
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
2221
|
+
}
|
|
2222
|
+
function dotVec(a, b) {
|
|
2223
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2224
|
+
}
|
|
2225
|
+
function lenVec(a) {
|
|
2226
|
+
return Math.hypot(a[0], a[1], a[2]);
|
|
2227
|
+
}
|
|
2228
|
+
function getVert(positions, i) {
|
|
2229
|
+
const o = i * 3;
|
|
2230
|
+
return [positions[o], positions[o + 1], positions[o + 2]];
|
|
2231
|
+
}
|
|
2232
|
+
function triArea(positions, a, b, c) {
|
|
2233
|
+
const va = getVert(positions, a);
|
|
2234
|
+
return 0.5 * lenVec(crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va)));
|
|
2235
|
+
}
|
|
2236
|
+
function triNormal(positions, a, b, c) {
|
|
2237
|
+
const va = getVert(positions, a);
|
|
2238
|
+
const n = crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va));
|
|
2239
|
+
const l = lenVec(n) || 1;
|
|
2240
|
+
return [n[0] / l, n[1] / l, n[2] / l];
|
|
2241
|
+
}
|
|
2242
|
+
function edgeLen(positions, a, b) {
|
|
2243
|
+
return lenVec(subVec(getVert(positions, a), getVert(positions, b)));
|
|
2244
|
+
}
|
|
2245
|
+
function maxEdge(positions, a, b, c) {
|
|
2246
|
+
return Math.max(edgeLen(positions, a, b), edgeLen(positions, b, c), edgeLen(positions, c, a));
|
|
2247
|
+
}
|
|
2248
|
+
function weld(positions) {
|
|
2249
|
+
if (positions.length % 9 !== 0) {
|
|
2250
|
+
throw new Error(`weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`);
|
|
2251
|
+
}
|
|
2252
|
+
positions.length / 3;
|
|
2253
|
+
let minX = Infinity;
|
|
2254
|
+
let minY = Infinity;
|
|
2255
|
+
let minZ = Infinity;
|
|
2256
|
+
let maxX = -Infinity;
|
|
2257
|
+
let maxY = -Infinity;
|
|
2258
|
+
let maxZ = -Infinity;
|
|
2259
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2260
|
+
const x = positions[i];
|
|
2261
|
+
const y = positions[i + 1];
|
|
2262
|
+
const z = positions[i + 2];
|
|
2263
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
2264
|
+
throw new Error(`weld: non-finite vertex position at float index ${i}`);
|
|
2265
|
+
}
|
|
2266
|
+
if (x < minX) minX = x;
|
|
2267
|
+
if (y < minY) minY = y;
|
|
2268
|
+
if (z < minZ) minZ = z;
|
|
2269
|
+
if (x > maxX) maxX = x;
|
|
2270
|
+
if (y > maxY) maxY = y;
|
|
2271
|
+
if (z > maxZ) maxZ = z;
|
|
2272
|
+
}
|
|
2273
|
+
const diagonal = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ);
|
|
2274
|
+
const tolerance = Math.max(diagonal * 1e-5, Number.EPSILON);
|
|
2275
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2276
|
+
const out = [];
|
|
2277
|
+
const tris = [];
|
|
2278
|
+
const weldNormals = [];
|
|
2279
|
+
const inv = 1 / tolerance;
|
|
2280
|
+
const quantKey = (i) => {
|
|
2281
|
+
const qx = Math.round(positions[i] * inv);
|
|
2282
|
+
const qy = Math.round(positions[i + 1] * inv);
|
|
2283
|
+
const qz = Math.round(positions[i + 2] * inv);
|
|
2284
|
+
return `${qx},${qy},${qz}`;
|
|
2285
|
+
};
|
|
2286
|
+
for (let tri = 0; tri < positions.length; tri += 9) {
|
|
2287
|
+
const faceNormal = rawTriangleNormal(positions, tri);
|
|
2288
|
+
const idx = [];
|
|
2289
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
2290
|
+
const o = tri + corner * 3;
|
|
2291
|
+
const key = quantKey(o);
|
|
2292
|
+
const bucket = remap.get(key);
|
|
2293
|
+
let vi;
|
|
2294
|
+
if (bucket) {
|
|
2295
|
+
for (const candidate of bucket) {
|
|
2296
|
+
if (dotVec(weldNormals[candidate], faceNormal) >= CREASE_WELD_DOT) {
|
|
2297
|
+
vi = candidate;
|
|
2298
|
+
weldNormals[candidate] = normalizedSum(weldNormals[candidate], faceNormal);
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (vi === void 0) {
|
|
2304
|
+
vi = out.length / 3;
|
|
2305
|
+
out.push(positions[o], positions[o + 1], positions[o + 2]);
|
|
2306
|
+
weldNormals.push(faceNormal);
|
|
2307
|
+
if (bucket) {
|
|
2308
|
+
bucket.push(vi);
|
|
2309
|
+
} else {
|
|
2310
|
+
remap.set(key, [vi]);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
idx.push(vi);
|
|
2314
|
+
}
|
|
2315
|
+
if (idx[0] !== idx[1] && idx[1] !== idx[2] && idx[2] !== idx[0]) {
|
|
2316
|
+
tris.push(idx[0], idx[1], idx[2]);
|
|
1453
2317
|
}
|
|
1454
2318
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
2319
|
+
const distinctVertexCount = out.length / 3;
|
|
2320
|
+
const degenerate = distinctVertexCount < 3 || tris.length === 0;
|
|
2321
|
+
return { positions: out, tris, degenerate };
|
|
2322
|
+
}
|
|
2323
|
+
function rawTriangleNormal(positions, triOffset) {
|
|
2324
|
+
const ax = positions[triOffset];
|
|
2325
|
+
const ay = positions[triOffset + 1];
|
|
2326
|
+
const az = positions[triOffset + 2];
|
|
2327
|
+
const bx = positions[triOffset + 3];
|
|
2328
|
+
const by = positions[triOffset + 4];
|
|
2329
|
+
const bz = positions[triOffset + 5];
|
|
2330
|
+
const cx = positions[triOffset + 6];
|
|
2331
|
+
const cy = positions[triOffset + 7];
|
|
2332
|
+
const cz = positions[triOffset + 8];
|
|
2333
|
+
const ab = [bx - ax, by - ay, bz - az];
|
|
2334
|
+
const ac = [cx - ax, cy - ay, cz - az];
|
|
2335
|
+
const n = crossVec(ab, ac);
|
|
2336
|
+
const length = lenVec(n);
|
|
2337
|
+
if (!(length > 0)) return [0, 0, 0];
|
|
2338
|
+
return [n[0] / length, n[1] / length, n[2] / length];
|
|
2339
|
+
}
|
|
2340
|
+
function normalizedSum(a, b) {
|
|
2341
|
+
const sum = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
2342
|
+
const length = lenVec(sum);
|
|
2343
|
+
if (!(length > 0)) return a;
|
|
2344
|
+
return [sum[0] / length, sum[1] / length, sum[2] / length];
|
|
2345
|
+
}
|
|
2346
|
+
function adaptiveSubdivide(mesh, targetEdge, vertexCap) {
|
|
2347
|
+
requirePositiveFiniteNumber(targetEdge, "adaptiveSubdivide targetEdge");
|
|
2348
|
+
requireIntegerAtLeast(vertexCap, "adaptiveSubdivide vertexCap", 3);
|
|
2349
|
+
const positions = mesh.positions.slice();
|
|
2350
|
+
let tris = mesh.tris.slice();
|
|
2351
|
+
let capped = false;
|
|
2352
|
+
const keyOf = (a, b) => a < b ? `${a}_${b}` : `${b}_${a}`;
|
|
2353
|
+
for (let pass = 0; pass < MAX_SUBDIVISION_PASSES; pass += 1) {
|
|
2354
|
+
const next = [];
|
|
2355
|
+
const midCache = /* @__PURE__ */ new Map();
|
|
2356
|
+
let changed = false;
|
|
2357
|
+
const getMid = (a, b) => {
|
|
2358
|
+
const key = keyOf(a, b);
|
|
2359
|
+
let mi = midCache.get(key);
|
|
2360
|
+
if (mi === void 0) {
|
|
2361
|
+
mi = positions.length / 3;
|
|
2362
|
+
const ao = a * 3;
|
|
2363
|
+
const bo = b * 3;
|
|
2364
|
+
positions.push(
|
|
2365
|
+
(positions[ao] + positions[bo]) / 2,
|
|
2366
|
+
(positions[ao + 1] + positions[bo + 1]) / 2,
|
|
2367
|
+
(positions[ao + 2] + positions[bo + 2]) / 2
|
|
2368
|
+
);
|
|
2369
|
+
midCache.set(key, mi);
|
|
2370
|
+
}
|
|
2371
|
+
return mi;
|
|
2372
|
+
};
|
|
2373
|
+
for (let t = 0; t < tris.length; t += 3) {
|
|
2374
|
+
const a = tris[t];
|
|
2375
|
+
const b = tris[t + 1];
|
|
2376
|
+
const c = tris[t + 2];
|
|
2377
|
+
const vertexCount = positions.length / 3;
|
|
2378
|
+
if (maxEdge(positions, a, b, c) > targetEdge && vertexCount < vertexCap) {
|
|
2379
|
+
const ab = getMid(a, b);
|
|
2380
|
+
const bc = getMid(b, c);
|
|
2381
|
+
const ca = getMid(c, a);
|
|
2382
|
+
next.push(a, ab, ca, ab, b, bc, ca, bc, c, ab, bc, ca);
|
|
2383
|
+
changed = true;
|
|
2384
|
+
} else {
|
|
2385
|
+
if (positions.length / 3 >= vertexCap) capped = true;
|
|
2386
|
+
next.push(a, b, c);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
tris = next;
|
|
2390
|
+
if (!changed) break;
|
|
2391
|
+
}
|
|
2392
|
+
return { positions, tris, capped };
|
|
2393
|
+
}
|
|
2394
|
+
function vertexNormals(mesh) {
|
|
2395
|
+
const vertexCount = mesh.positions.length / 3;
|
|
2396
|
+
const acc = new Float32Array(vertexCount * 3);
|
|
2397
|
+
for (let t = 0; t < mesh.tris.length; t += 3) {
|
|
2398
|
+
const a = mesh.tris[t];
|
|
2399
|
+
const b = mesh.tris[t + 1];
|
|
2400
|
+
const c = mesh.tris[t + 2];
|
|
2401
|
+
const n = triNormal(mesh.positions, a, b, c);
|
|
2402
|
+
const area = triArea(mesh.positions, a, b, c);
|
|
2403
|
+
const nx = n[0] * area;
|
|
2404
|
+
const ny = n[1] * area;
|
|
2405
|
+
const nz = n[2] * area;
|
|
2406
|
+
for (const vi of [a, b, c]) {
|
|
2407
|
+
acc[vi * 3] += nx;
|
|
2408
|
+
acc[vi * 3 + 1] += ny;
|
|
2409
|
+
acc[vi * 3 + 2] += nz;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
for (let i = 0; i < vertexCount; i += 1) {
|
|
2413
|
+
const o = i * 3;
|
|
2414
|
+
const l = Math.hypot(acc[o], acc[o + 1], acc[o + 2]) || 1;
|
|
2415
|
+
acc[o] /= l;
|
|
2416
|
+
acc[o + 1] /= l;
|
|
2417
|
+
acc[o + 2] /= l;
|
|
2418
|
+
}
|
|
2419
|
+
return acc;
|
|
2420
|
+
}
|
|
2421
|
+
function buildSampleGrid(positions, cell) {
|
|
2422
|
+
const grid = /* @__PURE__ */ new Map();
|
|
2423
|
+
const sampleCount = positions.length / 3;
|
|
2424
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
2425
|
+
const o = i * 3;
|
|
2426
|
+
const key = `${Math.floor(positions[o] / cell)},${Math.floor(positions[o + 1] / cell)},${Math.floor(positions[o + 2] / cell)}`;
|
|
2427
|
+
let arr = grid.get(key);
|
|
2428
|
+
if (!arr) {
|
|
2429
|
+
arr = [];
|
|
2430
|
+
grid.set(key, arr);
|
|
2431
|
+
}
|
|
2432
|
+
arr.push(i);
|
|
2433
|
+
}
|
|
2434
|
+
return { grid, cell };
|
|
2435
|
+
}
|
|
2436
|
+
function gatherSamples(sampleGrid, p, rings, out) {
|
|
2437
|
+
out.length = 0;
|
|
2438
|
+
const { cell, grid } = sampleGrid;
|
|
2439
|
+
const bx = Math.floor(p[0] / cell);
|
|
2440
|
+
const by = Math.floor(p[1] / cell);
|
|
2441
|
+
const bz = Math.floor(p[2] / cell);
|
|
2442
|
+
for (let dx = -rings; dx <= rings; dx += 1) {
|
|
2443
|
+
for (let dy = -rings; dy <= rings; dy += 1) {
|
|
2444
|
+
for (let dz = -rings; dz <= rings; dz += 1) {
|
|
2445
|
+
const arr = grid.get(`${bx + dx},${by + dy},${bz + dz}`);
|
|
2446
|
+
if (arr) for (const i of arr) out.push(i);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot, normalGateMode = "signed") {
|
|
2452
|
+
const cell = spacing * SCATTER_CELL_SPACING_FACTOR;
|
|
2453
|
+
const sampleGrid = buildSampleGrid(samples.positions, cell);
|
|
2454
|
+
const radius = spacing * SCATTER_RADIUS_SPACING_FACTOR;
|
|
2455
|
+
const radius2 = radius * radius;
|
|
2456
|
+
const vertexCount = positions.length / 3;
|
|
2457
|
+
const values = new Float32Array(vertexCount);
|
|
2458
|
+
let holeCount = 0;
|
|
2459
|
+
const candidates = [];
|
|
2460
|
+
const scratchP = [0, 0, 0];
|
|
2461
|
+
const bestD2 = new Float64Array(k);
|
|
2462
|
+
const bestVal = new Float64Array(k);
|
|
2463
|
+
for (let vi = 0; vi < vertexCount; vi += 1) {
|
|
2464
|
+
const pi = vi * 3;
|
|
2465
|
+
const px = positions[pi];
|
|
2466
|
+
const py = positions[pi + 1];
|
|
2467
|
+
const pz = positions[pi + 2];
|
|
2468
|
+
const nx = vnormals[pi];
|
|
2469
|
+
const ny = vnormals[pi + 1];
|
|
2470
|
+
const nz = vnormals[pi + 2];
|
|
2471
|
+
scratchP[0] = px;
|
|
2472
|
+
scratchP[1] = py;
|
|
2473
|
+
scratchP[2] = pz;
|
|
2474
|
+
gatherSamples(sampleGrid, scratchP, 3, candidates);
|
|
2475
|
+
let count = 0;
|
|
2476
|
+
let worst = 0;
|
|
2477
|
+
let worstD2 = -Infinity;
|
|
2478
|
+
for (let c = 0; c < candidates.length; c += 1) {
|
|
2479
|
+
const si = candidates[c];
|
|
2480
|
+
const so = si * 3;
|
|
2481
|
+
const dx = samples.positions[so] - px;
|
|
2482
|
+
const dy = samples.positions[so + 1] - py;
|
|
2483
|
+
const dz = samples.positions[so + 2] - pz;
|
|
2484
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
2485
|
+
if (d2 > radius2) continue;
|
|
2486
|
+
const normalDot = samples.normals[so] * nx + samples.normals[so + 1] * ny + samples.normals[so + 2] * nz;
|
|
2487
|
+
if ((normalGateMode === "absolute" ? Math.abs(normalDot) : normalDot) < gateDot) continue;
|
|
2488
|
+
const val = samples.values[si];
|
|
2489
|
+
if (count < k) {
|
|
2490
|
+
bestD2[count] = d2;
|
|
2491
|
+
bestVal[count] = val;
|
|
2492
|
+
if (d2 > worstD2) {
|
|
2493
|
+
worstD2 = d2;
|
|
2494
|
+
worst = count;
|
|
2495
|
+
}
|
|
2496
|
+
count += 1;
|
|
2497
|
+
} else if (d2 < worstD2) {
|
|
2498
|
+
bestD2[worst] = d2;
|
|
2499
|
+
bestVal[worst] = val;
|
|
2500
|
+
worstD2 = -Infinity;
|
|
2501
|
+
for (let t = 0; t < k; t += 1) {
|
|
2502
|
+
if (bestD2[t] > worstD2) {
|
|
2503
|
+
worstD2 = bestD2[t];
|
|
2504
|
+
worst = t;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (count === 0) {
|
|
2510
|
+
holeCount += 1;
|
|
2511
|
+
values[vi] = NaN;
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
let w = 0;
|
|
2515
|
+
let accum = 0;
|
|
2516
|
+
for (let t = 0; t < count; t += 1) {
|
|
2517
|
+
const ww = 1 / Math.max(bestD2[t], IDW_DISTANCE_FLOOR);
|
|
2518
|
+
w += ww;
|
|
2519
|
+
accum += ww * bestVal[t];
|
|
2520
|
+
}
|
|
2521
|
+
values[vi] = accum / w;
|
|
2522
|
+
}
|
|
2523
|
+
return { values, holeCount };
|
|
2524
|
+
}
|
|
2525
|
+
function backfillScalarFieldHoles(values, mesh) {
|
|
2526
|
+
const vertexCount = values.length;
|
|
2527
|
+
const adjacency = Array.from({ length: vertexCount }, () => []);
|
|
2528
|
+
for (let t = 0; t < mesh.tris.length; t += 3) {
|
|
2529
|
+
const a = mesh.tris[t];
|
|
2530
|
+
const b = mesh.tris[t + 1];
|
|
2531
|
+
const c = mesh.tris[t + 2];
|
|
2532
|
+
adjacency[a].push(b, c);
|
|
2533
|
+
adjacency[b].push(a, c);
|
|
2534
|
+
adjacency[c].push(a, b);
|
|
2535
|
+
}
|
|
2536
|
+
let frontier = [];
|
|
2537
|
+
for (let i = 0; i < vertexCount; i += 1) {
|
|
2538
|
+
if (Number.isFinite(values[i])) frontier.push(i);
|
|
2539
|
+
}
|
|
2540
|
+
if (frontier.length === 0) {
|
|
2541
|
+
values.fill(0);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
while (frontier.length > 0) {
|
|
2545
|
+
const nextFrontier = [];
|
|
2546
|
+
for (const v of frontier) {
|
|
2547
|
+
const val = values[v];
|
|
2548
|
+
for (const n of adjacency[v]) {
|
|
2549
|
+
if (!Number.isFinite(values[n])) {
|
|
2550
|
+
values[n] = val;
|
|
2551
|
+
nextFrontier.push(n);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
frontier = nextFrontier;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function reconstructSurfaceScalarField(trianglePositions, samples, options = {}) {
|
|
2559
|
+
if (!(trianglePositions instanceof Float32Array)) {
|
|
2560
|
+
throw new Error("reconstructSurfaceScalarField: trianglePositions must be a Float32Array");
|
|
2561
|
+
}
|
|
2562
|
+
if (trianglePositions.length === 0 || trianglePositions.length % 9 !== 0) {
|
|
2563
|
+
throw new Error(
|
|
2564
|
+
`reconstructSurfaceScalarField: trianglePositions length must be a positive multiple of 9, got ${trianglePositions.length}`
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
if (!(samples.positions instanceof Float32Array) || !(samples.values instanceof Float32Array) || !(samples.normals instanceof Float32Array)) {
|
|
2568
|
+
throw new Error("reconstructSurfaceScalarField: samples positions/values/normals must be Float32Arrays");
|
|
2569
|
+
}
|
|
2570
|
+
const sampleCount = samples.values.length;
|
|
2571
|
+
if (sampleCount === 0) {
|
|
2572
|
+
throw new Error("reconstructSurfaceScalarField: samples must be non-empty");
|
|
2573
|
+
}
|
|
2574
|
+
if (samples.positions.length !== sampleCount * 3) {
|
|
2575
|
+
throw new Error(
|
|
2576
|
+
`reconstructSurfaceScalarField: samples.positions length ${samples.positions.length} must equal values*3 (${sampleCount * 3})`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
if (samples.normals.length !== sampleCount * 3) {
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
`reconstructSurfaceScalarField: samples.normals length ${samples.normals.length} must equal values*3 (${sampleCount * 3})`
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
2585
|
+
requireFiniteNumber(samples.values[i], `samples.values[${i}]`);
|
|
2586
|
+
}
|
|
2587
|
+
const vertexCap = options.vertexCap === void 0 ? DEFAULT_VERTEX_CAP : requireIntegerAtLeast(options.vertexCap, "vertexCap", 3);
|
|
2588
|
+
const targetEdgeSpacingFactor = options.targetEdgeSpacingFactor === void 0 ? DEFAULT_TARGET_EDGE_SPACING_FACTOR : requirePositiveFiniteNumber(options.targetEdgeSpacingFactor, "targetEdgeSpacingFactor");
|
|
2589
|
+
const k = options.k === void 0 ? DEFAULT_K : requireIntegerAtLeast(options.k, "k", 1);
|
|
2590
|
+
const gateDot = options.gateDot === void 0 ? DEFAULT_GATE_DOT : requireFiniteNumber(options.gateDot, "gateDot");
|
|
2591
|
+
const normalGateMode = options.normalGateMode ?? "signed";
|
|
2592
|
+
if (normalGateMode !== "signed" && normalGateMode !== "absolute") {
|
|
2593
|
+
throw new Error(`reconstructSurfaceScalarField: normalGateMode must be 'signed' or 'absolute', got '${normalGateMode}'`);
|
|
2594
|
+
}
|
|
2595
|
+
const welded = weld(trianglePositions);
|
|
2596
|
+
let surfaceArea = 0;
|
|
2597
|
+
for (let t = 0; t < welded.tris.length; t += 3) {
|
|
2598
|
+
surfaceArea += triArea(welded.positions, welded.tris[t], welded.tris[t + 1], welded.tris[t + 2]);
|
|
2599
|
+
}
|
|
2600
|
+
const sampleSpacing = surfaceArea > 0 ? Math.sqrt(surfaceArea / sampleCount) : 0;
|
|
2601
|
+
let subdivided;
|
|
2602
|
+
if (welded.degenerate || !(sampleSpacing > 0)) {
|
|
2603
|
+
subdivided = { positions: welded.positions, tris: welded.tris, capped: false };
|
|
2604
|
+
} else {
|
|
2605
|
+
const targetEdge = targetEdgeSpacingFactor * sampleSpacing;
|
|
2606
|
+
subdivided = adaptiveSubdivide(welded, targetEdge, vertexCap);
|
|
2607
|
+
}
|
|
2608
|
+
const normals = vertexNormals(subdivided);
|
|
2609
|
+
const effectiveSpacing = sampleSpacing > 0 ? sampleSpacing : 1;
|
|
2610
|
+
const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot, normalGateMode);
|
|
2611
|
+
let valueMin = Infinity;
|
|
2612
|
+
let valueMax = -Infinity;
|
|
2613
|
+
for (let i = 0; i < scatter.values.length; i += 1) {
|
|
2614
|
+
const v = scatter.values[i];
|
|
2615
|
+
if (!Number.isFinite(v)) continue;
|
|
2616
|
+
if (v < valueMin) valueMin = v;
|
|
2617
|
+
if (v > valueMax) valueMax = v;
|
|
2618
|
+
}
|
|
2619
|
+
if (!Number.isFinite(valueMin) || !Number.isFinite(valueMax)) {
|
|
2620
|
+
valueMin = 0;
|
|
2621
|
+
valueMax = 0;
|
|
2622
|
+
}
|
|
2623
|
+
backfillScalarFieldHoles(scatter.values, subdivided);
|
|
2624
|
+
const vertexCount = subdivided.positions.length / 3;
|
|
1457
2625
|
return {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
2626
|
+
positions: Float32Array.from(subdivided.positions),
|
|
2627
|
+
normals,
|
|
2628
|
+
index: Uint32Array.from(subdivided.tris),
|
|
2629
|
+
values: scatter.values,
|
|
2630
|
+
valueMin,
|
|
2631
|
+
valueMax,
|
|
2632
|
+
capped: subdivided.capped,
|
|
2633
|
+
degenerate: welded.degenerate,
|
|
2634
|
+
holeCount: scatter.holeCount,
|
|
2635
|
+
vertexCount
|
|
1465
2636
|
};
|
|
1466
2637
|
}
|
|
2638
|
+
function stressSurfaceToScalarSamples(surface) {
|
|
2639
|
+
const positions = new Float32Array(surface.samples.length * 3);
|
|
2640
|
+
const normals = new Float32Array(surface.samples.length * 3);
|
|
2641
|
+
const values = new Float32Array(surface.samples.length);
|
|
2642
|
+
surface.samples.forEach((sample, index) => {
|
|
2643
|
+
const base = index * 3;
|
|
2644
|
+
positions[base] = sample.position[0];
|
|
2645
|
+
positions[base + 1] = sample.position[1];
|
|
2646
|
+
positions[base + 2] = sample.position[2];
|
|
2647
|
+
normals[base] = sample.normal[0];
|
|
2648
|
+
normals[base + 1] = sample.normal[1];
|
|
2649
|
+
normals[base + 2] = sample.normal[2];
|
|
2650
|
+
values[index] = sample.value;
|
|
2651
|
+
});
|
|
2652
|
+
return { positions, normals, values };
|
|
2653
|
+
}
|
|
1467
2654
|
const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
|
|
1468
2655
|
smoothAngleDeg: 5,
|
|
1469
2656
|
sharpAngleDeg: 30,
|
|
@@ -1605,13 +2792,15 @@ function percentile(sorted, q) {
|
|
|
1605
2792
|
return Number(sorted[index].toFixed(2));
|
|
1606
2793
|
}
|
|
1607
2794
|
const DEG_PER_RAD = 180 / Math.PI;
|
|
2795
|
+
const ROUGHNESS_HOTSPOT_LIMIT = 16;
|
|
2796
|
+
const ROUGHNESS_HOTSPOT_RADIUS_FRACTION = 0.03;
|
|
1608
2797
|
function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
1609
2798
|
const options = resolveRoughnessInspectionOptions(rawOptions);
|
|
1610
2799
|
const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
|
|
1611
2800
|
const position = geometry.getAttribute("position");
|
|
1612
2801
|
const warnings = [];
|
|
1613
2802
|
if (!position || position.count < 3) {
|
|
1614
|
-
return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
2803
|
+
return { geometry, pointSamples: [], hotSpots: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
1615
2804
|
}
|
|
1616
2805
|
const triangleCount = Math.floor(position.count / 3);
|
|
1617
2806
|
const normals = new Array(triangleCount);
|
|
@@ -1684,16 +2873,66 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
|
1684
2873
|
area: sample.area
|
|
1685
2874
|
};
|
|
1686
2875
|
});
|
|
2876
|
+
const hotSpots = selectRoughnessHotSpots(pointSamples, bbox, options.sharpAngleDeg);
|
|
1687
2877
|
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
1688
2878
|
geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
|
|
1689
2879
|
geometry.computeBoundingBox();
|
|
1690
2880
|
return {
|
|
1691
2881
|
geometry,
|
|
1692
2882
|
pointSamples,
|
|
2883
|
+
hotSpots,
|
|
1693
2884
|
summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
|
|
1694
2885
|
warnings
|
|
1695
2886
|
};
|
|
1696
2887
|
}
|
|
2888
|
+
function selectRoughnessHotSpots(samples, bbox, minAngleDeg) {
|
|
2889
|
+
const size = bbox.getSize(new Vector3());
|
|
2890
|
+
const clusterRadius = Math.max(size.length() * ROUGHNESS_HOTSPOT_RADIUS_FRACTION, 1e-6);
|
|
2891
|
+
const clusterRadiusSq = clusterRadius * clusterRadius;
|
|
2892
|
+
const candidates = samples.filter((sample) => sample.value !== null && Number.isFinite(sample.value) && sample.value >= minAngleDeg).sort((lhs, rhs) => {
|
|
2893
|
+
const valueDelta = (rhs.value ?? -Infinity) - (lhs.value ?? -Infinity);
|
|
2894
|
+
if (Math.abs(valueDelta) > 1e-9) return valueDelta;
|
|
2895
|
+
return (rhs.area ?? 0) - (lhs.area ?? 0);
|
|
2896
|
+
});
|
|
2897
|
+
const clusters = [];
|
|
2898
|
+
for (const candidate of candidates) {
|
|
2899
|
+
const cluster = clusters.find((entry) => squaredDistance(candidate.position, entry.representative.position) <= clusterRadiusSq);
|
|
2900
|
+
if (cluster) {
|
|
2901
|
+
cluster.count += 1;
|
|
2902
|
+
cluster.area += candidate.area ?? 0;
|
|
2903
|
+
const value = candidate.value ?? -Infinity;
|
|
2904
|
+
if (value > cluster.peakValue) {
|
|
2905
|
+
cluster.peakValue = value;
|
|
2906
|
+
cluster.representative = candidate;
|
|
2907
|
+
}
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
clusters.push({
|
|
2911
|
+
representative: candidate,
|
|
2912
|
+
peakValue: candidate.value ?? 0,
|
|
2913
|
+
count: 1,
|
|
2914
|
+
area: candidate.area ?? 0
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
return clusters.sort((lhs, rhs) => {
|
|
2918
|
+
const valueDelta = rhs.peakValue - lhs.peakValue;
|
|
2919
|
+
if (Math.abs(valueDelta) > 1e-9) return valueDelta;
|
|
2920
|
+
if (rhs.count !== lhs.count) return rhs.count - lhs.count;
|
|
2921
|
+
return rhs.area - lhs.area;
|
|
2922
|
+
}).slice(0, ROUGHNESS_HOTSPOT_LIMIT).map((cluster, index) => ({
|
|
2923
|
+
...cluster.representative,
|
|
2924
|
+
rank: index + 1,
|
|
2925
|
+
clusterRadius,
|
|
2926
|
+
nearbySampleCount: cluster.count,
|
|
2927
|
+
nearbyArea: Number(cluster.area.toFixed(6))
|
|
2928
|
+
}));
|
|
2929
|
+
}
|
|
2930
|
+
function squaredDistance(a, b) {
|
|
2931
|
+
const dx = a[0] - b[0];
|
|
2932
|
+
const dy = a[1] - b[1];
|
|
2933
|
+
const dz = a[2] - b[2];
|
|
2934
|
+
return dx * dx + dy * dy + dz * dz;
|
|
2935
|
+
}
|
|
1697
2936
|
function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
|
|
1698
2937
|
const edgeAngles = [];
|
|
1699
2938
|
let boundaryEdgeCount = 0;
|
|
@@ -2001,6 +3240,16 @@ function createCaptureDebugLogger(enabled) {
|
|
|
2001
3240
|
console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
|
|
2002
3241
|
};
|
|
2003
3242
|
}
|
|
3243
|
+
function setInspectProfileEnabled(enabled) {
|
|
3244
|
+
window.__forgeInspectProfile = enabled === true;
|
|
3245
|
+
}
|
|
3246
|
+
function inspectProfileEnabled() {
|
|
3247
|
+
return window.__forgeInspectProfile === true;
|
|
3248
|
+
}
|
|
3249
|
+
function inspectProfileLog(phase, detail) {
|
|
3250
|
+
if (!inspectProfileEnabled()) return;
|
|
3251
|
+
console.info(`[forge-inspect-profile] ${phase} ${JSON.stringify(detail)}`);
|
|
3252
|
+
}
|
|
2004
3253
|
class EmptyInspectionShape {
|
|
2005
3254
|
intersect() {
|
|
2006
3255
|
return {
|
|
@@ -3675,28 +4924,120 @@ function bboxFromGeometry(geometry) {
|
|
|
3675
4924
|
max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
|
|
3676
4925
|
};
|
|
3677
4926
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
samples.
|
|
4927
|
+
const SCALAR_SURFACE_VERTEX_CAP = 2e6;
|
|
4928
|
+
const SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR = 8;
|
|
4929
|
+
function scalarSamplesFromPointSamples(samples) {
|
|
4930
|
+
const finite = samples.filter((sample) => sample.value != null && Number.isFinite(sample.value));
|
|
4931
|
+
const positions = new Float32Array(finite.length * 3);
|
|
4932
|
+
const values = new Float32Array(finite.length);
|
|
4933
|
+
const normals = new Float32Array(finite.length * 3);
|
|
4934
|
+
finite.forEach((sample, index) => {
|
|
3682
4935
|
const base = index * 3;
|
|
3683
|
-
positions[base] = sample.position[0]
|
|
3684
|
-
positions[base + 1] = sample.position[1]
|
|
3685
|
-
positions[base + 2] = sample.position[2]
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
4936
|
+
positions[base] = sample.position[0];
|
|
4937
|
+
positions[base + 1] = sample.position[1];
|
|
4938
|
+
positions[base + 2] = sample.position[2];
|
|
4939
|
+
normals[base] = sample.normal[0];
|
|
4940
|
+
normals[base + 1] = sample.normal[1];
|
|
4941
|
+
normals[base + 2] = sample.normal[2];
|
|
4942
|
+
values[index] = sample.value;
|
|
4943
|
+
});
|
|
4944
|
+
return { positions, values, normals };
|
|
4945
|
+
}
|
|
4946
|
+
const DEFAULT_SCALAR_FIELD_PARAMS = {
|
|
4947
|
+
quantizeBands: 0,
|
|
4948
|
+
isoEnabled: false,
|
|
4949
|
+
isoSpacing: 0,
|
|
4950
|
+
isoLineWidthPx: 1.5,
|
|
4951
|
+
criticalEnabled: false,
|
|
4952
|
+
criticalThreshold: 0,
|
|
4953
|
+
shadingEnabled: true
|
|
4954
|
+
};
|
|
4955
|
+
function mergeScalarFieldParams(override) {
|
|
4956
|
+
if (!override) return DEFAULT_SCALAR_FIELD_PARAMS;
|
|
4957
|
+
const merged = { ...DEFAULT_SCALAR_FIELD_PARAMS };
|
|
4958
|
+
if (override.quantizeBands !== void 0) {
|
|
4959
|
+
const bands = override.quantizeBands;
|
|
4960
|
+
if (!Number.isInteger(bands) || bands < 0) {
|
|
4961
|
+
throw new RangeError(`Heatmap band count must be a non-negative integer, got ${bands}.`);
|
|
4962
|
+
}
|
|
4963
|
+
merged.quantizeBands = bands;
|
|
4964
|
+
}
|
|
4965
|
+
if (override.isoEnabled !== void 0) {
|
|
4966
|
+
merged.isoEnabled = override.isoEnabled;
|
|
4967
|
+
}
|
|
4968
|
+
if (merged.isoEnabled && merged.isoSpacing <= 0) {
|
|
4969
|
+
merged.isoSpacing = DEFAULT_INSPECT_ISOLINE_SPACING;
|
|
4970
|
+
}
|
|
4971
|
+
if (override.isoLineWidthPx !== void 0) {
|
|
4972
|
+
const width = override.isoLineWidthPx;
|
|
4973
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
4974
|
+
throw new RangeError(`Heatmap isoline width must be a finite positive number of pixels, got ${width}.`);
|
|
3695
4975
|
}
|
|
4976
|
+
merged.isoLineWidthPx = width;
|
|
4977
|
+
}
|
|
4978
|
+
return merged;
|
|
4979
|
+
}
|
|
4980
|
+
function scalarFieldColorOverride(override) {
|
|
4981
|
+
return {
|
|
4982
|
+
colormap: override == null ? void 0 : override.colormap,
|
|
4983
|
+
reversed: (override == null ? void 0 : override.reversed) === true
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
function scalarSurfaceOverlayFromField(renderable, field, domain, fieldParams, colorOverride = {}) {
|
|
4987
|
+
const colormap = colorOverride.colormap ?? DEFAULT_COLORMAP;
|
|
4988
|
+
const reversed = colorOverride.reversed === true ? { reversed: true } : {};
|
|
4989
|
+
const colorScale = domain ? { colormap, domainMin: domain.min, domainMax: domain.max, ...reversed } : {
|
|
4990
|
+
colormap,
|
|
4991
|
+
domainMin: field.valueMin,
|
|
4992
|
+
domainMax: field.valueMax > field.valueMin ? field.valueMax : field.valueMin + 1,
|
|
4993
|
+
...reversed
|
|
4994
|
+
};
|
|
4995
|
+
return {
|
|
4996
|
+
overlay: {
|
|
4997
|
+
renderable,
|
|
4998
|
+
positions: field.positions,
|
|
4999
|
+
normals: field.normals,
|
|
5000
|
+
index: field.index,
|
|
5001
|
+
aValue: field.values,
|
|
5002
|
+
uvs: "uvs" in field ? field.uvs : void 0,
|
|
5003
|
+
textureValues: "textureValues" in field ? field.textureValues : void 0,
|
|
5004
|
+
textureWidth: "textureWidth" in field ? field.textureWidth : void 0,
|
|
5005
|
+
textureHeight: "textureHeight" in field ? field.textureHeight : void 0,
|
|
5006
|
+
colorScale,
|
|
5007
|
+
fieldParams
|
|
5008
|
+
},
|
|
5009
|
+
capped: field.capped,
|
|
5010
|
+
holeCount: field.holeCount
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
function buildScalarSurfaceOverlayFromSamples(renderable, trianglePositions, scalarSamples, domain, fieldParams, fieldOptions = {}, colorOverride = {}) {
|
|
5014
|
+
const started = performance.now();
|
|
5015
|
+
if (scalarSamples.values.length === 0) return null;
|
|
5016
|
+
const reconstructStarted = performance.now();
|
|
5017
|
+
const field = reconstructSurfaceScalarField(trianglePositions, scalarSamples, {
|
|
5018
|
+
vertexCap: SCALAR_SURFACE_VERTEX_CAP,
|
|
5019
|
+
targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR,
|
|
5020
|
+
...fieldOptions
|
|
3696
5021
|
});
|
|
3697
|
-
|
|
5022
|
+
const reconstructMs = performance.now() - reconstructStarted;
|
|
5023
|
+
inspectProfileLog("scalar-overlay", {
|
|
5024
|
+
object: renderable.name,
|
|
5025
|
+
samples: scalarSamples.values.length,
|
|
5026
|
+
inputTriangles: trianglePositions.length / 9,
|
|
5027
|
+
vertices: field.vertexCount,
|
|
5028
|
+
capped: field.capped,
|
|
5029
|
+
holes: field.holeCount,
|
|
5030
|
+
reconstructMs: Number(reconstructMs.toFixed(1)),
|
|
5031
|
+
totalMs: Number((performance.now() - started).toFixed(1))
|
|
5032
|
+
});
|
|
5033
|
+
return scalarSurfaceOverlayFromField(renderable, field, domain, fieldParams, colorOverride);
|
|
5034
|
+
}
|
|
5035
|
+
function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domain, fieldParams) {
|
|
5036
|
+
const scalarSamples = scalarSamplesFromPointSamples(samples);
|
|
5037
|
+
return buildScalarSurfaceOverlayFromSamples(renderable, trianglePositions, scalarSamples, domain, fieldParams);
|
|
3698
5038
|
}
|
|
3699
|
-
function
|
|
5039
|
+
function renderScalarSurfaceOverlays(session, overlays) {
|
|
5040
|
+
const started = performance.now();
|
|
3700
5041
|
const r = getRenderer(session.size, session.pixelRatio);
|
|
3701
5042
|
const overlayById = new Map(overlays.map((overlay) => [overlay.renderable.id, overlay]));
|
|
3702
5043
|
const replacements = session.renderables.map((renderable) => {
|
|
@@ -3713,49 +5054,98 @@ function renderScalarPointOverlays(session, overlays) {
|
|
|
3713
5054
|
ghostMaterial.toneMapped = false;
|
|
3714
5055
|
renderable.solid.material = ghostMaterial;
|
|
3715
5056
|
const overlay = overlayById.get(renderable.id);
|
|
3716
|
-
|
|
3717
|
-
|
|
5057
|
+
const previousVisible = renderable.solid.visible;
|
|
5058
|
+
if (overlay) renderable.solid.visible = false;
|
|
5059
|
+
if (!overlay || overlay.positions.length === 0) {
|
|
5060
|
+
return {
|
|
5061
|
+
renderable,
|
|
5062
|
+
previousMaterial,
|
|
5063
|
+
previousVisible,
|
|
5064
|
+
ghostMaterial,
|
|
5065
|
+
mesh: null,
|
|
5066
|
+
geometry: null,
|
|
5067
|
+
material: null,
|
|
5068
|
+
lut: null,
|
|
5069
|
+
scalarTexture: null
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
3718
5072
|
const geometry = new BufferGeometry();
|
|
3719
5073
|
geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
|
|
3720
|
-
geometry.setAttribute("
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
5074
|
+
geometry.setAttribute("normal", new BufferAttribute(overlay.normals, 3));
|
|
5075
|
+
geometry.setAttribute("aValue", new BufferAttribute(overlay.aValue, 1));
|
|
5076
|
+
geometry.setAttribute("uv", new BufferAttribute(overlay.uvs ?? new Float32Array(overlay.positions.length / 3 * 2), 2));
|
|
5077
|
+
geometry.setIndex(new BufferAttribute(overlay.index, 1));
|
|
5078
|
+
const lut = makeColorScaleTexture(colorScaleLUT(overlay.colorScale.colormap));
|
|
5079
|
+
trackTextureDecode(lut);
|
|
5080
|
+
const scalarTexture = overlay.textureValues && overlay.textureWidth && overlay.textureHeight ? makeScalarValueTexture(overlay.textureValues, overlay.textureWidth, overlay.textureHeight) : null;
|
|
5081
|
+
if (scalarTexture) trackTextureDecode(scalarTexture);
|
|
5082
|
+
const uniforms = makeInspectScalarUniforms({
|
|
5083
|
+
colorScale: lut,
|
|
5084
|
+
scalarTexture,
|
|
5085
|
+
domainMin: overlay.colorScale.domainMin,
|
|
5086
|
+
domainMax: overlay.colorScale.domainMax,
|
|
5087
|
+
colorScaleReversed: overlay.colorScale.reversed === true,
|
|
5088
|
+
quantizeBands: overlay.fieldParams.quantizeBands,
|
|
5089
|
+
isoEnabled: overlay.fieldParams.isoEnabled,
|
|
5090
|
+
isoSpacing: overlay.fieldParams.isoSpacing,
|
|
5091
|
+
isoLineWidthPx: overlay.fieldParams.isoLineWidthPx,
|
|
5092
|
+
criticalEnabled: overlay.fieldParams.criticalEnabled,
|
|
5093
|
+
criticalThreshold: overlay.fieldParams.criticalThreshold,
|
|
5094
|
+
shadingEnabled: overlay.fieldParams.shadingEnabled
|
|
5095
|
+
});
|
|
5096
|
+
const clippingPlanes = renderable.solidMaterial.clippingPlanes ?? void 0;
|
|
5097
|
+
const material = new ShaderMaterial({
|
|
5098
|
+
vertexShader: INSPECT_SCALAR_VERTEX_SHADER,
|
|
5099
|
+
fragmentShader: INSPECT_SCALAR_FRAGMENT_SHADER,
|
|
5100
|
+
uniforms,
|
|
5101
|
+
side: DoubleSide,
|
|
5102
|
+
clippingPlanes,
|
|
5103
|
+
// Engage the shader's `#include <clipping_planes_*>` chunks only when section
|
|
5104
|
+
// planes are actually present (NUM_CLIPPING_PLANES define is gated on this),
|
|
5105
|
+
// so heatmap surfaces honor section cuts the same as the ghosted solid does.
|
|
5106
|
+
clipping: Boolean(clippingPlanes && clippingPlanes.length > 0)
|
|
3728
5107
|
});
|
|
3729
5108
|
material.toneMapped = false;
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
renderable.root.add(
|
|
3734
|
-
return { renderable, previousMaterial, ghostMaterial,
|
|
5109
|
+
const mesh = new Mesh(geometry, material);
|
|
5110
|
+
mesh.renderOrder = 5;
|
|
5111
|
+
mesh.raycast = () => null;
|
|
5112
|
+
renderable.root.add(mesh);
|
|
5113
|
+
return { renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture };
|
|
3735
5114
|
});
|
|
3736
5115
|
try {
|
|
3737
|
-
|
|
5116
|
+
const png = withSolidOnlyVisibility(
|
|
3738
5117
|
session,
|
|
3739
5118
|
() => withTemporarySceneBackground(session, new Color(0), () => {
|
|
3740
5119
|
r.render(session.scene, session.camera);
|
|
3741
5120
|
return captureRenderedPng(session.size);
|
|
3742
5121
|
})
|
|
3743
5122
|
);
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
renderable.solid.material = previousMaterial;
|
|
3748
|
-
ghostMaterial.dispose();
|
|
3749
|
-
geometry == null ? void 0 : geometry.dispose();
|
|
3750
|
-
material == null ? void 0 : material.dispose();
|
|
5123
|
+
inspectProfileLog("scalar-render", {
|
|
5124
|
+
overlays: overlays.length,
|
|
5125
|
+
totalMs: Number((performance.now() - started).toFixed(1))
|
|
3751
5126
|
});
|
|
5127
|
+
return png;
|
|
5128
|
+
} finally {
|
|
5129
|
+
replacements.forEach(
|
|
5130
|
+
({ renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture }) => {
|
|
5131
|
+
if (mesh) renderable.root.remove(mesh);
|
|
5132
|
+
renderable.solid.material = previousMaterial;
|
|
5133
|
+
renderable.solid.visible = previousVisible;
|
|
5134
|
+
ghostMaterial.dispose();
|
|
5135
|
+
geometry == null ? void 0 : geometry.dispose();
|
|
5136
|
+
material == null ? void 0 : material.dispose();
|
|
5137
|
+
lut == null ? void 0 : lut.dispose();
|
|
5138
|
+
scalarTexture == null ? void 0 : scalarTexture.dispose();
|
|
5139
|
+
}
|
|
5140
|
+
);
|
|
3752
5141
|
}
|
|
3753
5142
|
}
|
|
3754
|
-
function getSessionThicknessInspection(session, rawOptions) {
|
|
5143
|
+
function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
|
|
3755
5144
|
var _a;
|
|
3756
5145
|
const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
|
|
3757
5146
|
const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
|
|
3758
|
-
const
|
|
5147
|
+
const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
|
|
5148
|
+
const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
|
|
3759
5149
|
if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
|
|
3760
5150
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
3761
5151
|
const warnings = [];
|
|
@@ -3763,17 +5153,30 @@ function getSessionThicknessInspection(session, rawOptions) {
|
|
|
3763
5153
|
const thicknessColorScale = {
|
|
3764
5154
|
colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
|
|
3765
5155
|
domainMin: options.colorMinThickness,
|
|
3766
|
-
domainMax: options.colorMaxThickness
|
|
5156
|
+
domainMax: options.colorMaxThickness,
|
|
5157
|
+
...{ reversed: true }
|
|
3767
5158
|
};
|
|
3768
5159
|
const objects = [];
|
|
3769
5160
|
const cloudObjects = [];
|
|
3770
5161
|
const points = [];
|
|
3771
5162
|
const overlays = [];
|
|
5163
|
+
const connectivityStarted = performance.now();
|
|
3772
5164
|
const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
|
|
5165
|
+
inspectProfileLog("thickness-connectivity", {
|
|
5166
|
+
ms: Number((performance.now() - connectivityStarted).toFixed(1)),
|
|
5167
|
+
neighborSets: raycastConnectivity.neighborIdsByObjectId.size
|
|
5168
|
+
});
|
|
3773
5169
|
session.renderables.forEach((renderable, index) => {
|
|
3774
5170
|
const sourceObject = byId.get(renderable.id);
|
|
3775
|
-
const
|
|
3776
|
-
|
|
5171
|
+
const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
|
|
5172
|
+
const analysisStarted = performance.now();
|
|
5173
|
+
const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, { connectedGeometries });
|
|
5174
|
+
inspectProfileLog("thickness-analysis", {
|
|
5175
|
+
object: renderable.name,
|
|
5176
|
+
samples: analysis.pointSamples.length,
|
|
5177
|
+
triangles: analysis.triangleCount,
|
|
5178
|
+
connectedGeometries: connectedGeometries.length,
|
|
5179
|
+
ms: Number((performance.now() - analysisStarted).toFixed(1))
|
|
3777
5180
|
});
|
|
3778
5181
|
const bbox = bboxFromGeometry(analysis.geometry);
|
|
3779
5182
|
const summary = summarizeThicknessSamples(analysis.samples, options);
|
|
@@ -3812,7 +5215,34 @@ function getSessionThicknessInspection(session, rawOptions) {
|
|
|
3812
5215
|
...sample
|
|
3813
5216
|
});
|
|
3814
5217
|
});
|
|
3815
|
-
|
|
5218
|
+
if (analysis.pointSamples.length > 0) {
|
|
5219
|
+
const scalarStarted = performance.now();
|
|
5220
|
+
const field = buildThicknessSurfaceScalarField(analysis.geometry, options, {}, { connectedGeometries });
|
|
5221
|
+
inspectProfileLog("thickness-scalar-overlay", {
|
|
5222
|
+
object: renderable.name,
|
|
5223
|
+
vertices: field.vertexCount,
|
|
5224
|
+
capped: field.capped,
|
|
5225
|
+
holes: field.holeCount,
|
|
5226
|
+
ms: Number((performance.now() - scalarStarted).toFixed(1))
|
|
5227
|
+
});
|
|
5228
|
+
const built = scalarSurfaceOverlayFromField(
|
|
5229
|
+
renderable,
|
|
5230
|
+
field,
|
|
5231
|
+
{ min: thicknessColorScale.domainMin, max: thicknessColorScale.domainMax },
|
|
5232
|
+
fieldParams
|
|
5233
|
+
);
|
|
5234
|
+
overlays.push(built.overlay);
|
|
5235
|
+
if (built.capped) {
|
|
5236
|
+
warnings.push(
|
|
5237
|
+
`${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
|
|
5238
|
+
);
|
|
5239
|
+
}
|
|
5240
|
+
if (built.holeCount > 0) {
|
|
5241
|
+
warnings.push(
|
|
5242
|
+
`${renderable.name}: ${built.holeCount} scalar texture samples had ambiguous edge thickness (filled from neighboring texels).`
|
|
5243
|
+
);
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
3816
5246
|
analysis.geometry.dispose();
|
|
3817
5247
|
});
|
|
3818
5248
|
const state = {
|
|
@@ -3835,9 +5265,6 @@ function getSessionThicknessInspection(session, rawOptions) {
|
|
|
3835
5265
|
objects,
|
|
3836
5266
|
warnings,
|
|
3837
5267
|
style: {
|
|
3838
|
-
// gradientColors is the legacy 'thickness-classic' rainbow, kept for
|
|
3839
|
-
// back-compat. colorScale is the truth the viewport/CLI now render with.
|
|
3840
|
-
gradientColors: THICKNESS_GRADIENT_COLORS.map((color) => [...color]),
|
|
3841
5268
|
colorScale: thicknessColorScale,
|
|
3842
5269
|
colorMinThickness: options.colorMinThickness,
|
|
3843
5270
|
colorMaxThickness: options.colorMaxThickness,
|
|
@@ -3848,26 +5275,228 @@ function getSessionThicknessInspection(session, rawOptions) {
|
|
|
3848
5275
|
session.thicknessInspection = state;
|
|
3849
5276
|
return state;
|
|
3850
5277
|
}
|
|
3851
|
-
function renderCurrentThickness(session, rawOptions) {
|
|
3852
|
-
const state = getSessionThicknessInspection(session, rawOptions);
|
|
3853
|
-
return { png:
|
|
5278
|
+
function renderCurrentThickness(session, rawOptions, fieldOverride) {
|
|
5279
|
+
const state = getSessionThicknessInspection(session, rawOptions, fieldOverride);
|
|
5280
|
+
return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
5281
|
+
}
|
|
5282
|
+
function getSessionThroughThicknessInspection(session, rawOptions, fieldOverride) {
|
|
5283
|
+
var _a;
|
|
5284
|
+
const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
|
|
5285
|
+
const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
|
|
5286
|
+
const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
|
|
5287
|
+
const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
|
|
5288
|
+
if (((_a = session.throughThicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.throughThicknessInspection;
|
|
5289
|
+
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
5290
|
+
const warnings = [];
|
|
5291
|
+
maybePushSceneSampleBudgetWarning(warnings, "Minimum solid span", sampleBudget);
|
|
5292
|
+
const colorScale = {
|
|
5293
|
+
colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
|
|
5294
|
+
domainMin: options.colorMinThickness,
|
|
5295
|
+
domainMax: options.colorMaxThickness,
|
|
5296
|
+
...{ reversed: true }
|
|
5297
|
+
};
|
|
5298
|
+
const objects = [];
|
|
5299
|
+
const cloudObjects = [];
|
|
5300
|
+
const points = [];
|
|
5301
|
+
const overlays = [];
|
|
5302
|
+
const connectivityStarted = performance.now();
|
|
5303
|
+
const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
|
|
5304
|
+
inspectProfileLog("through-thickness-connectivity", {
|
|
5305
|
+
ms: Number((performance.now() - connectivityStarted).toFixed(1)),
|
|
5306
|
+
neighborSets: raycastConnectivity.neighborIdsByObjectId.size
|
|
5307
|
+
});
|
|
5308
|
+
session.renderables.forEach((renderable, index) => {
|
|
5309
|
+
const sourceObject = byId.get(renderable.id);
|
|
5310
|
+
const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
|
|
5311
|
+
const analysisStarted = performance.now();
|
|
5312
|
+
const analysis = analyzeThroughThicknessGeometry(renderable.solid.geometry, options, { connectedGeometries });
|
|
5313
|
+
inspectProfileLog("through-thickness-analysis", {
|
|
5314
|
+
object: renderable.name,
|
|
5315
|
+
samples: analysis.pointSamples.length,
|
|
5316
|
+
triangles: analysis.triangleCount,
|
|
5317
|
+
connectedGeometries: connectedGeometries.length,
|
|
5318
|
+
ms: Number((performance.now() - analysisStarted).toFixed(1))
|
|
5319
|
+
});
|
|
5320
|
+
const bbox = bboxFromGeometry(analysis.geometry);
|
|
5321
|
+
const summary = summarizeThicknessSamples(analysis.samples, options);
|
|
5322
|
+
if (analysis.warnings.length > 0) {
|
|
5323
|
+
analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
|
|
5324
|
+
}
|
|
5325
|
+
const objectIndex = index + 1;
|
|
5326
|
+
objects.push({
|
|
5327
|
+
index: objectIndex,
|
|
5328
|
+
id: renderable.id,
|
|
5329
|
+
name: renderable.name,
|
|
5330
|
+
groupName: renderable.groupName,
|
|
5331
|
+
treePath: sourceObject == null ? void 0 : sourceObject.treePath,
|
|
5332
|
+
mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
|
|
5333
|
+
triangleCount: analysis.triangleCount,
|
|
5334
|
+
sampledTriangleCount: analysis.sampledTriangleCount,
|
|
5335
|
+
sampleStride: analysis.sampleStride,
|
|
5336
|
+
bbox,
|
|
5337
|
+
...summary
|
|
5338
|
+
});
|
|
5339
|
+
cloudObjects.push({
|
|
5340
|
+
index: objectIndex,
|
|
5341
|
+
id: renderable.id,
|
|
5342
|
+
name: renderable.name,
|
|
5343
|
+
groupName: renderable.groupName,
|
|
5344
|
+
treePath: sourceObject == null ? void 0 : sourceObject.treePath,
|
|
5345
|
+
mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
|
|
5346
|
+
sampleCount: analysis.pointSamples.length,
|
|
5347
|
+
bbox
|
|
5348
|
+
});
|
|
5349
|
+
analysis.pointSamples.forEach((sample) => {
|
|
5350
|
+
points.push({
|
|
5351
|
+
objectIndex,
|
|
5352
|
+
objectId: renderable.id,
|
|
5353
|
+
objectName: renderable.name,
|
|
5354
|
+
...sample
|
|
5355
|
+
});
|
|
5356
|
+
});
|
|
5357
|
+
if (analysis.pointSamples.length > 0) {
|
|
5358
|
+
const trianglePositions = cloneGeometryPositions(analysis.geometry);
|
|
5359
|
+
if (trianglePositions) {
|
|
5360
|
+
const built = buildScalarSurfaceOverlay(
|
|
5361
|
+
renderable,
|
|
5362
|
+
trianglePositions,
|
|
5363
|
+
analysis.pointSamples,
|
|
5364
|
+
{ min: colorScale.domainMin, max: colorScale.domainMax },
|
|
5365
|
+
fieldParams
|
|
5366
|
+
);
|
|
5367
|
+
if (built) {
|
|
5368
|
+
overlays.push(built.overlay);
|
|
5369
|
+
if (built.capped) {
|
|
5370
|
+
warnings.push(
|
|
5371
|
+
`${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
|
|
5372
|
+
);
|
|
5373
|
+
}
|
|
5374
|
+
if (built.holeCount > 0) {
|
|
5375
|
+
warnings.push(
|
|
5376
|
+
`${renderable.name}: ${built.holeCount} surface vertices had no finite minimum-solid-span sample nearby (filled by nearest neighbor).`
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
analysis.geometry.dispose();
|
|
5383
|
+
});
|
|
5384
|
+
const state = {
|
|
5385
|
+
optionsKey,
|
|
5386
|
+
overlays,
|
|
5387
|
+
pointCloud: {
|
|
5388
|
+
schemaVersion: 1,
|
|
5389
|
+
property: "throughThickness",
|
|
5390
|
+
coordinateSpace: "object-local",
|
|
5391
|
+
units: "model",
|
|
5392
|
+
sampleCount: points.length,
|
|
5393
|
+
objects: cloudObjects,
|
|
5394
|
+
points
|
|
5395
|
+
},
|
|
5396
|
+
report: {
|
|
5397
|
+
method: "mesh-nearest-through-boundary-bvh",
|
|
5398
|
+
options,
|
|
5399
|
+
sampleBudget,
|
|
5400
|
+
objectCount: objects.length,
|
|
5401
|
+
objects,
|
|
5402
|
+
warnings,
|
|
5403
|
+
style: {
|
|
5404
|
+
colorScale,
|
|
5405
|
+
colorMinThickness: options.colorMinThickness,
|
|
5406
|
+
colorMaxThickness: options.colorMaxThickness,
|
|
5407
|
+
unknownColor: THICKNESS_COLORS.unknown
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
};
|
|
5411
|
+
session.throughThicknessInspection = state;
|
|
5412
|
+
return state;
|
|
5413
|
+
}
|
|
5414
|
+
function renderCurrentThroughThickness(session, rawOptions, fieldOverride) {
|
|
5415
|
+
const state = getSessionThroughThicknessInspection(session, rawOptions, fieldOverride);
|
|
5416
|
+
return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
3854
5417
|
}
|
|
3855
5418
|
const ROUGHNESS_SMOOTH_OPACITY = 0.16;
|
|
3856
5419
|
const ROUGHNESS_HARSH_OPACITY = 1;
|
|
3857
|
-
function renderCurrentRoughness(session, rawOptions) {
|
|
3858
|
-
const state = getSessionRoughnessInspection(session, rawOptions);
|
|
3859
|
-
return { png:
|
|
5420
|
+
function renderCurrentRoughness(session, rawOptions, fieldOverride) {
|
|
5421
|
+
const state = getSessionRoughnessInspection(session, rawOptions, fieldOverride);
|
|
5422
|
+
return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
5423
|
+
}
|
|
5424
|
+
function stressSamplesForRenderable(surface, renderable, useAllSamples) {
|
|
5425
|
+
const samples = useAllSamples ? surface.samples : surface.samples.filter((sample) => sample.partName === renderable.name || sample.partName === renderable.groupName);
|
|
5426
|
+
if (samples.length === 0) return null;
|
|
5427
|
+
return { ...surface, samples };
|
|
5428
|
+
}
|
|
5429
|
+
function renderCurrentStressSurface(session, surface, fieldOverride) {
|
|
5430
|
+
var _a;
|
|
5431
|
+
const fieldParams = mergeScalarFieldParams(fieldOverride);
|
|
5432
|
+
const colorOverride = scalarFieldColorOverride(fieldOverride);
|
|
5433
|
+
const overlays = [];
|
|
5434
|
+
const warnings = [];
|
|
5435
|
+
const useAllSamples = session.renderables.length === 1;
|
|
5436
|
+
const domain = {
|
|
5437
|
+
min: surface.domain.min,
|
|
5438
|
+
max: surface.domain.max > surface.domain.min ? surface.domain.max : surface.domain.min + 1
|
|
5439
|
+
};
|
|
5440
|
+
for (const renderable of session.renderables) {
|
|
5441
|
+
const scopedSurface = stressSamplesForRenderable(surface, renderable, useAllSamples);
|
|
5442
|
+
if (!scopedSurface) continue;
|
|
5443
|
+
const trianglePositions = (_a = renderable.solid.geometry.getAttribute("position")) == null ? void 0 : _a.array;
|
|
5444
|
+
if (!(trianglePositions instanceof Float32Array)) {
|
|
5445
|
+
warnings.push(`${renderable.name}: rendered geometry has no Float32Array position buffer for stress heatmap.`);
|
|
5446
|
+
continue;
|
|
5447
|
+
}
|
|
5448
|
+
const built = buildScalarSurfaceOverlayFromSamples(
|
|
5449
|
+
renderable,
|
|
5450
|
+
trianglePositions,
|
|
5451
|
+
stressSurfaceToScalarSamples(scopedSurface),
|
|
5452
|
+
domain,
|
|
5453
|
+
fieldParams,
|
|
5454
|
+
{ normalGateMode: "absolute" },
|
|
5455
|
+
colorOverride
|
|
5456
|
+
);
|
|
5457
|
+
if (!built) continue;
|
|
5458
|
+
overlays.push(built.overlay);
|
|
5459
|
+
if (built.capped) {
|
|
5460
|
+
throw new Error(
|
|
5461
|
+
`${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; refusing to render an incomplete stress heatmap.`
|
|
5462
|
+
);
|
|
5463
|
+
}
|
|
5464
|
+
if (built.holeCount > 0) {
|
|
5465
|
+
throw new Error(
|
|
5466
|
+
`${renderable.name}: ${built.holeCount} surface vertices had no in-gate stress sample; refusing to render a filled stress heatmap.`
|
|
5467
|
+
);
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
if (overlays.length === 0) {
|
|
5471
|
+
throw new Error("Stress surface samples did not match any rendered model surface.");
|
|
5472
|
+
}
|
|
5473
|
+
return {
|
|
5474
|
+
png: renderScalarSurfaceOverlays(session, overlays),
|
|
5475
|
+
report: {
|
|
5476
|
+
method: "solver-stress-surface-v1",
|
|
5477
|
+
field: surface.field,
|
|
5478
|
+
unit: surface.unit,
|
|
5479
|
+
coordinateSpace: surface.coordinateSpace,
|
|
5480
|
+
sampleCount: surface.samples.length,
|
|
5481
|
+
valueMin: surface.valueMin,
|
|
5482
|
+
valueMax: surface.valueMax,
|
|
5483
|
+
hotSpots: surface.hotSpots,
|
|
5484
|
+
warnings
|
|
5485
|
+
}
|
|
5486
|
+
};
|
|
3860
5487
|
}
|
|
3861
|
-
function getSessionRoughnessInspection(session, rawOptions) {
|
|
5488
|
+
function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
3862
5489
|
var _a;
|
|
3863
5490
|
const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
|
|
3864
5491
|
const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
|
|
3865
|
-
const
|
|
5492
|
+
const fieldParams = mergeScalarFieldParams(fieldOverride);
|
|
5493
|
+
const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
|
|
3866
5494
|
if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
|
|
3867
5495
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
3868
5496
|
const warnings = [];
|
|
3869
5497
|
maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
|
|
3870
5498
|
const objects = [];
|
|
5499
|
+
const hotSpotCandidates = [];
|
|
3871
5500
|
const cloudObjects = [];
|
|
3872
5501
|
const points = [];
|
|
3873
5502
|
const overlays = [];
|
|
@@ -3886,6 +5515,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
|
|
|
3886
5515
|
});
|
|
3887
5516
|
const roughnessColorScale = Number.isFinite(roughnessMin) && roughnessMax > roughnessMin ? { colormap: DEFAULT_ROUGHNESS_COLOR_SCALE.colormap, domainMin: roughnessMin, domainMax: roughnessMax } : DEFAULT_ROUGHNESS_COLOR_SCALE;
|
|
3888
5517
|
session.renderables.forEach((renderable, index) => {
|
|
5518
|
+
var _a2;
|
|
3889
5519
|
const analysis = analysesByIndex[index];
|
|
3890
5520
|
const bbox = bboxFromGeometry(analysis.geometry);
|
|
3891
5521
|
const sourceObject = byId.get(renderable.id);
|
|
@@ -3921,9 +5551,42 @@ function getSessionRoughnessInspection(session, rawOptions) {
|
|
|
3921
5551
|
...sample
|
|
3922
5552
|
});
|
|
3923
5553
|
});
|
|
3924
|
-
|
|
5554
|
+
analysis.hotSpots.forEach((hotSpot) => {
|
|
5555
|
+
hotSpotCandidates.push({
|
|
5556
|
+
...hotSpot,
|
|
5557
|
+
rank: 0,
|
|
5558
|
+
objectRank: hotSpot.rank,
|
|
5559
|
+
objectIndex,
|
|
5560
|
+
objectId: renderable.id,
|
|
5561
|
+
objectName: renderable.name
|
|
5562
|
+
});
|
|
5563
|
+
});
|
|
5564
|
+
const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
|
|
5565
|
+
if (trianglePositions instanceof Float32Array) {
|
|
5566
|
+
const built = buildScalarSurfaceOverlay(renderable, trianglePositions, analysis.pointSamples, null, fieldParams);
|
|
5567
|
+
if (built) {
|
|
5568
|
+
overlays.push(built.overlay);
|
|
5569
|
+
if (built.capped) {
|
|
5570
|
+
warnings.push(
|
|
5571
|
+
`${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
|
|
5572
|
+
);
|
|
5573
|
+
}
|
|
5574
|
+
if (built.holeCount > 0) {
|
|
5575
|
+
warnings.push(`${renderable.name}: ${built.holeCount} surface vertices had no in-gate sample (filled by nearest neighbor).`);
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
3925
5579
|
analysis.geometry.dispose();
|
|
3926
5580
|
});
|
|
5581
|
+
const hotSpots = hotSpotCandidates.sort((lhs, rhs) => {
|
|
5582
|
+
const valueDelta = (rhs.value ?? -Infinity) - (lhs.value ?? -Infinity);
|
|
5583
|
+
if (Math.abs(valueDelta) > 1e-9) return valueDelta;
|
|
5584
|
+
if (rhs.nearbySampleCount !== lhs.nearbySampleCount) return rhs.nearbySampleCount - lhs.nearbySampleCount;
|
|
5585
|
+
const areaDelta = rhs.nearbyArea - lhs.nearbyArea;
|
|
5586
|
+
if (Math.abs(areaDelta) > 1e-9) return areaDelta;
|
|
5587
|
+
if (lhs.objectIndex !== rhs.objectIndex) return lhs.objectIndex - rhs.objectIndex;
|
|
5588
|
+
return lhs.objectRank - rhs.objectRank;
|
|
5589
|
+
}).map((hotSpot, index) => ({ ...hotSpot, rank: index + 1 }));
|
|
3927
5590
|
const state = {
|
|
3928
5591
|
optionsKey,
|
|
3929
5592
|
overlays,
|
|
@@ -3942,6 +5605,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
|
|
|
3942
5605
|
sampleBudget,
|
|
3943
5606
|
objectCount: objects.length,
|
|
3944
5607
|
objects,
|
|
5608
|
+
hotSpots,
|
|
3945
5609
|
warnings,
|
|
3946
5610
|
style: {
|
|
3947
5611
|
// Legacy class colors kept for back-compat; colorScale is the truth the
|
|
@@ -5073,6 +6737,60 @@ Available renderable objects: ${available}`;
|
|
|
5073
6737
|
}
|
|
5074
6738
|
return "No visible renderable objects found.";
|
|
5075
6739
|
}
|
|
6740
|
+
const PENDING_TEXTURE_DECODES = /* @__PURE__ */ new Set();
|
|
6741
|
+
function trackTextureDecode(texture) {
|
|
6742
|
+
PENDING_TEXTURE_DECODES.add(textureDecoded(texture));
|
|
6743
|
+
}
|
|
6744
|
+
function textureDecoded(texture) {
|
|
6745
|
+
return new Promise((resolve) => {
|
|
6746
|
+
var _a;
|
|
6747
|
+
const finish = () => {
|
|
6748
|
+
texture.needsUpdate = true;
|
|
6749
|
+
resolve();
|
|
6750
|
+
};
|
|
6751
|
+
const forgeDecoded = (_a = texture.userData) == null ? void 0 : _a.forgeDecoded;
|
|
6752
|
+
if (forgeDecoded) {
|
|
6753
|
+
forgeDecoded.then(
|
|
6754
|
+
() => finish(),
|
|
6755
|
+
() => finish()
|
|
6756
|
+
);
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
const image = texture.image;
|
|
6760
|
+
if (image && typeof ImageBitmap !== "undefined" && image instanceof ImageBitmap) {
|
|
6761
|
+
finish();
|
|
6762
|
+
return;
|
|
6763
|
+
}
|
|
6764
|
+
if (image && typeof image.decode === "function") {
|
|
6765
|
+
image.decode().then(finish, finish);
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
if (image && (image.naturalWidth > 0 || image.complete && image.width > 0)) {
|
|
6769
|
+
finish();
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
if (image && "onload" in image) {
|
|
6773
|
+
const prevLoad = image.onload;
|
|
6774
|
+
const prevError = image.onerror;
|
|
6775
|
+
image.onload = (ev) => {
|
|
6776
|
+
if (typeof prevLoad === "function") prevLoad.call(image, ev);
|
|
6777
|
+
finish();
|
|
6778
|
+
};
|
|
6779
|
+
image.onerror = (ev) => {
|
|
6780
|
+
if (typeof prevError === "function") prevError.call(image, ev);
|
|
6781
|
+
finish();
|
|
6782
|
+
};
|
|
6783
|
+
return;
|
|
6784
|
+
}
|
|
6785
|
+
finish();
|
|
6786
|
+
});
|
|
6787
|
+
}
|
|
6788
|
+
async function awaitPendingTextureDecodes() {
|
|
6789
|
+
if (PENDING_TEXTURE_DECODES.size === 0) return;
|
|
6790
|
+
const pending = Array.from(PENDING_TEXTURE_DECODES);
|
|
6791
|
+
PENDING_TEXTURE_DECODES.clear();
|
|
6792
|
+
await Promise.all(pending);
|
|
6793
|
+
}
|
|
5076
6794
|
function createSession(code, opts) {
|
|
5077
6795
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
|
|
5078
6796
|
const size = (opts == null ? void 0 : opts.size) ?? 1024;
|
|
@@ -5426,6 +7144,12 @@ Fix one:
|
|
|
5426
7144
|
depthWrite: materialOpacity >= 0.99 && materialTransmission <= 0,
|
|
5427
7145
|
clippingPlanes: applicableCutPlanes
|
|
5428
7146
|
});
|
|
7147
|
+
const textureDescriptor = mp == null ? void 0 : mp.texture;
|
|
7148
|
+
if (textureDescriptor && solidMaterial instanceof MeshPhysicalMaterial) {
|
|
7149
|
+
const projectedTexture = descriptorToThreeTexture(textureDescriptor.image, { colorSpace: "srgb" });
|
|
7150
|
+
applyProjectedTexture(solidMaterial, textureDescriptor.projection, projectedTexture);
|
|
7151
|
+
trackTextureDecode(projectedTexture);
|
|
7152
|
+
}
|
|
5429
7153
|
solid = new Mesh(geo.solid, solidMaterial);
|
|
5430
7154
|
if (isScanRenderStyle) {
|
|
5431
7155
|
const scanProxyGeometry = createScanProxyGeometry(geo.solid, { grid: scanProxyGrid ?? void 0 });
|
|
@@ -5572,6 +7296,7 @@ Fix one:
|
|
|
5572
7296
|
collisionEntries,
|
|
5573
7297
|
collisionReport: null,
|
|
5574
7298
|
thicknessInspection: null,
|
|
7299
|
+
throughThicknessInspection: null,
|
|
5575
7300
|
roughnessInspection: null,
|
|
5576
7301
|
joints,
|
|
5577
7302
|
jointCouplings,
|
|
@@ -5618,6 +7343,7 @@ async function emitInspectProgress(opts, event) {
|
|
|
5618
7343
|
}
|
|
5619
7344
|
window.__forgeRender = async (code, opts) => {
|
|
5620
7345
|
var _a, _b, _c;
|
|
7346
|
+
setInspectProfileEnabled(opts == null ? void 0 : opts.debug);
|
|
5621
7347
|
const requestedCameraTokens = (opts == null ? void 0 : opts.cameras) ?? (opts == null ? void 0 : opts.angles);
|
|
5622
7348
|
const hasDirectionalCameraTokens = Array.isArray(requestedCameraTokens) && requestedCameraTokens.length > 0;
|
|
5623
7349
|
if ((opts == null ? void 0 : opts.viewName) && hasDirectionalCameraTokens) {
|
|
@@ -5635,6 +7361,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5635
7361
|
const built = createSession(code, {
|
|
5636
7362
|
size: (opts == null ? void 0 : opts.size) || 1024,
|
|
5637
7363
|
pixelRatio: (opts == null ? void 0 : opts.pixelRatio) || 1,
|
|
7364
|
+
debug: opts == null ? void 0 : opts.debug,
|
|
5638
7365
|
quality: opts == null ? void 0 : opts.quality,
|
|
5639
7366
|
allFiles: opts == null ? void 0 : opts.allFiles,
|
|
5640
7367
|
binaryFiles: opts == null ? void 0 : opts.binaryFiles,
|
|
@@ -5654,7 +7381,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5654
7381
|
scanGranularity: opts == null ? void 0 : opts.scanGranularity,
|
|
5655
7382
|
respectAuthoredSceneStyle: opts == null ? void 0 : opts.respectAuthoredSceneStyle,
|
|
5656
7383
|
cutaway: (opts == null ? void 0 : opts.cutaway) ?? null,
|
|
5657
|
-
includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("thickness"),
|
|
7384
|
+
includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("throughThickness") || requestedChannels.has("thickness"),
|
|
5658
7385
|
includeCollisions: requestedChannels.has("collisions"),
|
|
5659
7386
|
capture: "orbit"
|
|
5660
7387
|
});
|
|
@@ -5697,6 +7424,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5697
7424
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
5698
7425
|
}
|
|
5699
7426
|
}
|
|
7427
|
+
await awaitPendingTextureDecodes();
|
|
5700
7428
|
await emitInspectProgress(opts, { type: "session-done", objectCount: session.objects.length });
|
|
5701
7429
|
const renderMode = (opts == null ? void 0 : opts.renderMode) === "wireframe" ? "wireframe" : "solid";
|
|
5702
7430
|
const edgePreset = (opts == null ? void 0 : opts.edges) ?? "off";
|
|
@@ -5732,7 +7460,9 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5732
7460
|
const comparisonRenders = {};
|
|
5733
7461
|
const collisionRenders = {};
|
|
5734
7462
|
const thicknessRenders = {};
|
|
7463
|
+
const throughThicknessRenders = {};
|
|
5735
7464
|
const roughnessRenders = {};
|
|
7465
|
+
const stressRenders = {};
|
|
5736
7466
|
let maskObjects = [];
|
|
5737
7467
|
let connectivityReport = null;
|
|
5738
7468
|
let floatingReport = null;
|
|
@@ -5742,8 +7472,11 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5742
7472
|
let comparisonPointCloud = null;
|
|
5743
7473
|
let collisionReport = null;
|
|
5744
7474
|
let thicknessReport = null;
|
|
7475
|
+
let throughThicknessReport = null;
|
|
5745
7476
|
let roughnessReport = null;
|
|
7477
|
+
let stressReport = null;
|
|
5746
7478
|
let thicknessPointCloud = null;
|
|
7479
|
+
let throughThicknessPointCloud = null;
|
|
5747
7480
|
let roughnessPointCloud = null;
|
|
5748
7481
|
let sectionEvidence = null;
|
|
5749
7482
|
const framingViews = {};
|
|
@@ -5822,7 +7555,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5822
7555
|
}
|
|
5823
7556
|
if (requestedChannels.has("roughness")) {
|
|
5824
7557
|
await markChannelViewStart("roughness", label);
|
|
5825
|
-
const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
|
|
7558
|
+
const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness, opts == null ? void 0 : opts.scalarFieldParams);
|
|
5826
7559
|
roughnessRenders[label] = roughness.png;
|
|
5827
7560
|
roughnessReport = roughness.report;
|
|
5828
7561
|
roughnessPointCloud = roughness.pointCloud;
|
|
@@ -5877,12 +7610,28 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5877
7610
|
}
|
|
5878
7611
|
if (requestedChannels.has("thickness")) {
|
|
5879
7612
|
await markChannelViewStart("thickness", label);
|
|
5880
|
-
const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
|
|
7613
|
+
const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness, opts == null ? void 0 : opts.scalarFieldParams);
|
|
5881
7614
|
thicknessRenders[label] = thickness.png;
|
|
5882
7615
|
thicknessReport = thickness.report;
|
|
5883
7616
|
thicknessPointCloud = thickness.pointCloud;
|
|
5884
7617
|
await markChannelViewDone("thickness", label);
|
|
5885
7618
|
}
|
|
7619
|
+
if (requestedChannels.has("stress")) {
|
|
7620
|
+
if (!(opts == null ? void 0 : opts.stressSurface)) throw new Error("stress channel requires solver-produced stressSurface evidence.");
|
|
7621
|
+
await markChannelViewStart("stress", label);
|
|
7622
|
+
const stress = renderCurrentStressSurface(session, opts.stressSurface, opts.scalarFieldParams);
|
|
7623
|
+
stressRenders[label] = stress.png;
|
|
7624
|
+
stressReport = stress.report;
|
|
7625
|
+
await markChannelViewDone("stress", label);
|
|
7626
|
+
}
|
|
7627
|
+
if (requestedChannels.has("throughThickness")) {
|
|
7628
|
+
await markChannelViewStart("throughThickness", label);
|
|
7629
|
+
const throughThickness = renderCurrentThroughThickness(session, opts == null ? void 0 : opts.thickness, opts == null ? void 0 : opts.scalarFieldParams);
|
|
7630
|
+
throughThicknessRenders[label] = throughThickness.png;
|
|
7631
|
+
throughThicknessReport = throughThickness.report;
|
|
7632
|
+
throughThicknessPointCloud = throughThickness.pointCloud;
|
|
7633
|
+
await markChannelViewDone("throughThickness", label);
|
|
7634
|
+
}
|
|
5886
7635
|
} catch (e) {
|
|
5887
7636
|
if (comparisonSession) disposeSession(comparisonSession);
|
|
5888
7637
|
disposeSession(session);
|
|
@@ -5991,6 +7740,19 @@ window.__forgeRender = async (code, opts) => {
|
|
|
5991
7740
|
} : {
|
|
5992
7741
|
views: thicknessRenders
|
|
5993
7742
|
},
|
|
7743
|
+
stress: stressReport ? {
|
|
7744
|
+
...stressReport,
|
|
7745
|
+
views: stressRenders
|
|
7746
|
+
} : {
|
|
7747
|
+
views: stressRenders
|
|
7748
|
+
},
|
|
7749
|
+
throughThickness: throughThicknessReport ? {
|
|
7750
|
+
...throughThicknessReport,
|
|
7751
|
+
pointCloud: throughThicknessPointCloud,
|
|
7752
|
+
views: throughThicknessRenders
|
|
7753
|
+
} : {
|
|
7754
|
+
views: throughThicknessRenders
|
|
7755
|
+
},
|
|
5994
7756
|
section: sectionEvidence,
|
|
5995
7757
|
bbox: session.bbox,
|
|
5996
7758
|
volume: session.volume,
|
|
@@ -6022,6 +7784,7 @@ window.__forgeCaptureInit = async (code, opts) => {
|
|
|
6022
7784
|
if (!built.ok) {
|
|
6023
7785
|
return built;
|
|
6024
7786
|
}
|
|
7787
|
+
await awaitPendingTextureDecodes();
|
|
6025
7788
|
captureSession = built.session;
|
|
6026
7789
|
return {
|
|
6027
7790
|
ok: true,
|