forgecad 0.10.4 → 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-B3L3W1Uo.js → AdminPage-raksfnNA.js} +1 -1
- package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-DP3RxhPs.js} +2 -2
- package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-D7Dos-vl.js} +1 -1
- package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-DO1kvBns.js} +7 -1
- package/dist/assets/{EditorApp-BWUGCdD5.js → EditorApp-DQJmcmRT.js} +9 -8
- package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-DFDUhOma.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-DbE_tp8-.js} +2 -2
- package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-CominSso.js} +2 -2
- package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-CcVIN9yj.js} +2 -2
- package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-DLWcP289.js} +1 -1
- package/dist/assets/{app-BsRYSfxY.js → app-xW3hOdq9.js} +1135 -320
- package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-mDHk97u7.js} +6630 -2493
- package/dist/assets/cli/{render-XXol_ET7.js → render--SIU27W_.js} +1263 -112
- package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-uEe_Q7Kg.js} +1861 -610
- package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-BqyDHDcI.js} +6254 -2177
- 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-ymhBV4Ll.js → inspectWorker-UXMxlcR8.js} +2738 -742
- package/dist/assets/{jointPose-B0blBj9A.js → jointPose-bYMlwU3v.js} +1 -1
- package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
- package/dist/assets/{manifold-B_7QXpGB.js → manifold-BR7UYI4P.js} +1 -1
- package/dist/assets/{manifold-CYlIm-M6.js → manifold-CyOV5B9S.js} +2 -2
- package/dist/assets/{manifold-CNShmpEJ.js → manifold-D4d5NQst.js} +1 -1
- package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-DsaICZsn.js} +6010 -2032
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/CLI.md +4 -2
- package/dist/docs-raw/generated/assembly.md +76 -3
- package/dist/docs-raw/generated/concepts.md +31 -4
- package/dist/docs-raw/generated/core.md +159 -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 +4 -2
- package/dist/docs-raw/guides/structural-fea.md +224 -0
- package/dist/docs-raw/skills/forgecad.md +1 -0
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +15 -15
- package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-7YAHVXYM.js} +1 -1
- package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-ZRR6IOJW.js} +1 -1
- package/dist-cli/{chunk-UHBRMYA6.js → chunk-VNM67DIV.js} +6489 -2333
- package/dist-cli/forgecad.js +5258 -717
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +827 -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 +159 -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 +4 -2
- package/dist-skill/docs/guides/structural-fea.md +224 -0
- package/dist-skill/website/skills/forgecad.md +1 -0
- 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/package.json +4 -1
- /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +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,17 +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 makeThicknessRaycastTarget(sourceGeometry, rayMaterial
|
|
1327
|
+
function makeThicknessRaycastTarget(sourceGeometry, rayMaterial) {
|
|
1328
1328
|
const geometry = sourceGeometry.clone();
|
|
1329
1329
|
geometry.boundsTree = new MeshBVH(geometry);
|
|
1330
1330
|
const mesh = new Mesh(geometry, rayMaterial);
|
|
1331
1331
|
mesh.raycast = acceleratedRaycast;
|
|
1332
|
-
return { mesh,
|
|
1332
|
+
return { mesh, geometry };
|
|
1333
1333
|
}
|
|
1334
|
-
function geometryMaxDimension(geometry) {
|
|
1334
|
+
function geometryMaxDimension$1(geometry) {
|
|
1335
1335
|
geometry.computeBoundingBox();
|
|
1336
1336
|
const box = geometry.boundingBox;
|
|
1337
1337
|
if (!box) return 1;
|
|
@@ -1339,34 +1339,47 @@ function geometryMaxDimension(geometry) {
|
|
|
1339
1339
|
box.getSize(size);
|
|
1340
1340
|
return Math.max(1, size.x, size.y, size.z);
|
|
1341
1341
|
}
|
|
1342
|
-
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) {
|
|
1343
1366
|
const origin = point.clone().addScaledVector(direction, epsilon);
|
|
1344
1367
|
raycaster.set(origin, direction);
|
|
1345
1368
|
raycaster.near = epsilon;
|
|
1346
1369
|
raycaster.far = far;
|
|
1347
1370
|
const hits = raycaster.intersectObjects(rayTargetMeshes, false);
|
|
1371
|
+
const minDistance = Math.max(epsilon, contactTolerance);
|
|
1348
1372
|
for (const hit of hits) {
|
|
1349
|
-
if (hit.distance <=
|
|
1350
|
-
if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
|
|
1373
|
+
if (hit.distance <= minDistance) continue;
|
|
1351
1374
|
return hit.distance + epsilon;
|
|
1352
1375
|
}
|
|
1353
1376
|
return null;
|
|
1354
1377
|
}
|
|
1355
|
-
function triangleThickness(raycaster, rayTargetMeshes,
|
|
1356
|
-
const forward = firstOppositeSurfaceDistance(
|
|
1357
|
-
raycaster,
|
|
1358
|
-
rayTargetMeshes,
|
|
1359
|
-
jumpableMeshes,
|
|
1360
|
-
centroid,
|
|
1361
|
-
normal,
|
|
1362
|
-
epsilon,
|
|
1363
|
-
far,
|
|
1364
|
-
contactTolerance
|
|
1365
|
-
);
|
|
1378
|
+
function triangleThickness(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance) {
|
|
1379
|
+
const forward = firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance);
|
|
1366
1380
|
const backward = firstOppositeSurfaceDistance(
|
|
1367
1381
|
raycaster,
|
|
1368
1382
|
rayTargetMeshes,
|
|
1369
|
-
jumpableMeshes,
|
|
1370
1383
|
centroid,
|
|
1371
1384
|
normal.clone().negate(),
|
|
1372
1385
|
epsilon,
|
|
@@ -1377,9 +1390,20 @@ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid,
|
|
|
1377
1390
|
if (backward == null) return forward;
|
|
1378
1391
|
return Math.min(forward, backward);
|
|
1379
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
|
+
}
|
|
1380
1404
|
function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
|
|
1381
1405
|
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
1382
|
-
const geometry = cloneGeometryForFaceColors(sourceGeometry);
|
|
1406
|
+
const geometry = cloneGeometryForFaceColors$1(sourceGeometry);
|
|
1383
1407
|
const position = geometry.getAttribute("position");
|
|
1384
1408
|
if (!position || position.count < 3) {
|
|
1385
1409
|
return {
|
|
@@ -1395,24 +1419,13 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
|
|
|
1395
1419
|
const triangleCount = Math.floor(position.count / 3);
|
|
1396
1420
|
const surfaceTriangles = readSurfaceTriangles(position);
|
|
1397
1421
|
const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
|
|
1398
|
-
const connectedGeometries = context.connectedGeometries ?? [];
|
|
1399
|
-
const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
|
|
1400
|
-
const epsilon = Math.max(1e-4, maxDim * 1e-6);
|
|
1401
|
-
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
1402
1422
|
const colors = new Float32Array(position.count * 3);
|
|
1403
1423
|
const triangleThicknessValues = new Array(triangleCount).fill(void 0);
|
|
1404
1424
|
const samples = [];
|
|
1405
1425
|
const pointSamples = [];
|
|
1406
1426
|
const warnings = [];
|
|
1407
|
-
const
|
|
1408
|
-
const rayTargets = [
|
|
1409
|
-
makeThicknessRaycastTarget(geometry, rayMaterial, false),
|
|
1410
|
-
...connectedGeometries.map((connectedGeometry) => makeThicknessRaycastTarget(connectedGeometry, rayMaterial, true))
|
|
1411
|
-
];
|
|
1427
|
+
const rayContext = createThicknessRaycastContext(geometry, options, context);
|
|
1412
1428
|
try {
|
|
1413
|
-
const rayTargetMeshes = rayTargets.map((target) => target.mesh);
|
|
1414
|
-
const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
|
|
1415
|
-
const raycaster = new Raycaster();
|
|
1416
1429
|
if (surfaceTriangles.length === 0) {
|
|
1417
1430
|
warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
|
|
1418
1431
|
} else if (surfaceSamples.length < surfaceTriangles.length) {
|
|
@@ -1423,16 +1436,7 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
|
|
|
1423
1436
|
const sampledTriangleIndexes = /* @__PURE__ */ new Set();
|
|
1424
1437
|
for (const sample of surfaceSamples) {
|
|
1425
1438
|
sampledTriangleIndexes.add(sample.triangle.index);
|
|
1426
|
-
const thickness =
|
|
1427
|
-
raycaster,
|
|
1428
|
-
rayTargetMeshes,
|
|
1429
|
-
jumpableMeshes,
|
|
1430
|
-
sample.position,
|
|
1431
|
-
sample.normal,
|
|
1432
|
-
epsilon,
|
|
1433
|
-
far,
|
|
1434
|
-
options.contactTolerance
|
|
1435
|
-
);
|
|
1439
|
+
const thickness = measureThicknessAtPoint(rayContext, sample.position, sample.normal);
|
|
1436
1440
|
samples.push({ thickness, area: sample.area });
|
|
1437
1441
|
const previous = triangleThicknessValues[sample.triangle.index];
|
|
1438
1442
|
if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
|
|
@@ -1468,14 +1472,743 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
|
|
|
1468
1472
|
warnings
|
|
1469
1473
|
};
|
|
1470
1474
|
} finally {
|
|
1471
|
-
|
|
1472
|
-
|
|
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);
|
|
2077
|
+
const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
|
|
2078
|
+
const targets = [
|
|
2079
|
+
makeThroughThicknessTarget(sourceGeometry, rayMaterial),
|
|
2080
|
+
...connectedGeometries.map((geometry) => makeThroughThicknessTarget(geometry, rayMaterial))
|
|
2081
|
+
];
|
|
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();
|
|
2092
|
+
}
|
|
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
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
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();
|
|
1473
2205
|
}
|
|
1474
2206
|
}
|
|
1475
2207
|
const DEFAULT_VERTEX_CAP = 2e6;
|
|
1476
2208
|
const DEFAULT_TARGET_EDGE_SPACING_FACTOR = 2;
|
|
1477
2209
|
const DEFAULT_K = 8;
|
|
1478
2210
|
const DEFAULT_GATE_DOT = 0.3;
|
|
2211
|
+
const CREASE_WELD_DOT = Math.SQRT1_2;
|
|
1479
2212
|
const SCATTER_CELL_SPACING_FACTOR = 1.5;
|
|
1480
2213
|
const SCATTER_RADIUS_SPACING_FACTOR = 3;
|
|
1481
2214
|
const IDW_DISTANCE_FLOOR = 1e-9;
|
|
@@ -1486,6 +2219,9 @@ function subVec(a, b) {
|
|
|
1486
2219
|
function crossVec(a, b) {
|
|
1487
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]];
|
|
1488
2221
|
}
|
|
2222
|
+
function dotVec(a, b) {
|
|
2223
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2224
|
+
}
|
|
1489
2225
|
function lenVec(a) {
|
|
1490
2226
|
return Math.hypot(a[0], a[1], a[2]);
|
|
1491
2227
|
}
|
|
@@ -1511,9 +2247,7 @@ function maxEdge(positions, a, b, c) {
|
|
|
1511
2247
|
}
|
|
1512
2248
|
function weld(positions) {
|
|
1513
2249
|
if (positions.length % 9 !== 0) {
|
|
1514
|
-
throw new Error(
|
|
1515
|
-
`weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`
|
|
1516
|
-
);
|
|
2250
|
+
throw new Error(`weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`);
|
|
1517
2251
|
}
|
|
1518
2252
|
positions.length / 3;
|
|
1519
2253
|
let minX = Infinity;
|
|
@@ -1541,6 +2275,7 @@ function weld(positions) {
|
|
|
1541
2275
|
const remap = /* @__PURE__ */ new Map();
|
|
1542
2276
|
const out = [];
|
|
1543
2277
|
const tris = [];
|
|
2278
|
+
const weldNormals = [];
|
|
1544
2279
|
const inv = 1 / tolerance;
|
|
1545
2280
|
const quantKey = (i) => {
|
|
1546
2281
|
const qx = Math.round(positions[i] * inv);
|
|
@@ -1549,15 +2284,31 @@ function weld(positions) {
|
|
|
1549
2284
|
return `${qx},${qy},${qz}`;
|
|
1550
2285
|
};
|
|
1551
2286
|
for (let tri = 0; tri < positions.length; tri += 9) {
|
|
2287
|
+
const faceNormal = rawTriangleNormal(positions, tri);
|
|
1552
2288
|
const idx = [];
|
|
1553
2289
|
for (let corner = 0; corner < 3; corner += 1) {
|
|
1554
2290
|
const o = tri + corner * 3;
|
|
1555
2291
|
const key = quantKey(o);
|
|
1556
|
-
|
|
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
|
+
}
|
|
1557
2303
|
if (vi === void 0) {
|
|
1558
2304
|
vi = out.length / 3;
|
|
1559
2305
|
out.push(positions[o], positions[o + 1], positions[o + 2]);
|
|
1560
|
-
|
|
2306
|
+
weldNormals.push(faceNormal);
|
|
2307
|
+
if (bucket) {
|
|
2308
|
+
bucket.push(vi);
|
|
2309
|
+
} else {
|
|
2310
|
+
remap.set(key, [vi]);
|
|
2311
|
+
}
|
|
1561
2312
|
}
|
|
1562
2313
|
idx.push(vi);
|
|
1563
2314
|
}
|
|
@@ -1569,6 +2320,29 @@ function weld(positions) {
|
|
|
1569
2320
|
const degenerate = distinctVertexCount < 3 || tris.length === 0;
|
|
1570
2321
|
return { positions: out, tris, degenerate };
|
|
1571
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
|
+
}
|
|
1572
2346
|
function adaptiveSubdivide(mesh, targetEdge, vertexCap) {
|
|
1573
2347
|
requirePositiveFiniteNumber(targetEdge, "adaptiveSubdivide targetEdge");
|
|
1574
2348
|
requireIntegerAtLeast(vertexCap, "adaptiveSubdivide vertexCap", 3);
|
|
@@ -1674,7 +2448,7 @@ function gatherSamples(sampleGrid, p, rings, out) {
|
|
|
1674
2448
|
}
|
|
1675
2449
|
}
|
|
1676
2450
|
}
|
|
1677
|
-
function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
|
|
2451
|
+
function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot, normalGateMode = "signed") {
|
|
1678
2452
|
const cell = spacing * SCATTER_CELL_SPACING_FACTOR;
|
|
1679
2453
|
const sampleGrid = buildSampleGrid(samples.positions, cell);
|
|
1680
2454
|
const radius = spacing * SCATTER_RADIUS_SPACING_FACTOR;
|
|
@@ -1709,7 +2483,8 @@ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
|
|
|
1709
2483
|
const dz = samples.positions[so + 2] - pz;
|
|
1710
2484
|
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1711
2485
|
if (d2 > radius2) continue;
|
|
1712
|
-
|
|
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;
|
|
1713
2488
|
const val = samples.values[si];
|
|
1714
2489
|
if (count < k) {
|
|
1715
2490
|
bestD2[count] = d2;
|
|
@@ -1737,17 +2512,17 @@ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
|
|
|
1737
2512
|
continue;
|
|
1738
2513
|
}
|
|
1739
2514
|
let w = 0;
|
|
1740
|
-
let
|
|
2515
|
+
let accum = 0;
|
|
1741
2516
|
for (let t = 0; t < count; t += 1) {
|
|
1742
2517
|
const ww = 1 / Math.max(bestD2[t], IDW_DISTANCE_FLOOR);
|
|
1743
2518
|
w += ww;
|
|
1744
|
-
|
|
2519
|
+
accum += ww * bestVal[t];
|
|
1745
2520
|
}
|
|
1746
|
-
values[vi] =
|
|
2521
|
+
values[vi] = accum / w;
|
|
1747
2522
|
}
|
|
1748
2523
|
return { values, holeCount };
|
|
1749
2524
|
}
|
|
1750
|
-
function
|
|
2525
|
+
function backfillScalarFieldHoles(values, mesh) {
|
|
1751
2526
|
const vertexCount = values.length;
|
|
1752
2527
|
const adjacency = Array.from({ length: vertexCount }, () => []);
|
|
1753
2528
|
for (let t = 0; t < mesh.tris.length; t += 3) {
|
|
@@ -1813,6 +2588,10 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
|
|
|
1813
2588
|
const targetEdgeSpacingFactor = options.targetEdgeSpacingFactor === void 0 ? DEFAULT_TARGET_EDGE_SPACING_FACTOR : requirePositiveFiniteNumber(options.targetEdgeSpacingFactor, "targetEdgeSpacingFactor");
|
|
1814
2589
|
const k = options.k === void 0 ? DEFAULT_K : requireIntegerAtLeast(options.k, "k", 1);
|
|
1815
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
|
+
}
|
|
1816
2595
|
const welded = weld(trianglePositions);
|
|
1817
2596
|
let surfaceArea = 0;
|
|
1818
2597
|
for (let t = 0; t < welded.tris.length; t += 3) {
|
|
@@ -1828,7 +2607,7 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
|
|
|
1828
2607
|
}
|
|
1829
2608
|
const normals = vertexNormals(subdivided);
|
|
1830
2609
|
const effectiveSpacing = sampleSpacing > 0 ? sampleSpacing : 1;
|
|
1831
|
-
const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot);
|
|
2610
|
+
const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot, normalGateMode);
|
|
1832
2611
|
let valueMin = Infinity;
|
|
1833
2612
|
let valueMax = -Infinity;
|
|
1834
2613
|
for (let i = 0; i < scatter.values.length; i += 1) {
|
|
@@ -1841,7 +2620,7 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
|
|
|
1841
2620
|
valueMin = 0;
|
|
1842
2621
|
valueMax = 0;
|
|
1843
2622
|
}
|
|
1844
|
-
|
|
2623
|
+
backfillScalarFieldHoles(scatter.values, subdivided);
|
|
1845
2624
|
const vertexCount = subdivided.positions.length / 3;
|
|
1846
2625
|
return {
|
|
1847
2626
|
positions: Float32Array.from(subdivided.positions),
|
|
@@ -1856,6 +2635,22 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
|
|
|
1856
2635
|
vertexCount
|
|
1857
2636
|
};
|
|
1858
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
|
+
}
|
|
1859
2654
|
const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
|
|
1860
2655
|
smoothAngleDeg: 5,
|
|
1861
2656
|
sharpAngleDeg: 30,
|
|
@@ -1997,13 +2792,15 @@ function percentile(sorted, q) {
|
|
|
1997
2792
|
return Number(sorted[index].toFixed(2));
|
|
1998
2793
|
}
|
|
1999
2794
|
const DEG_PER_RAD = 180 / Math.PI;
|
|
2795
|
+
const ROUGHNESS_HOTSPOT_LIMIT = 16;
|
|
2796
|
+
const ROUGHNESS_HOTSPOT_RADIUS_FRACTION = 0.03;
|
|
2000
2797
|
function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
2001
2798
|
const options = resolveRoughnessInspectionOptions(rawOptions);
|
|
2002
2799
|
const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
|
|
2003
2800
|
const position = geometry.getAttribute("position");
|
|
2004
2801
|
const warnings = [];
|
|
2005
2802
|
if (!position || position.count < 3) {
|
|
2006
|
-
return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
2803
|
+
return { geometry, pointSamples: [], hotSpots: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
2007
2804
|
}
|
|
2008
2805
|
const triangleCount = Math.floor(position.count / 3);
|
|
2009
2806
|
const normals = new Array(triangleCount);
|
|
@@ -2076,16 +2873,66 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
|
2076
2873
|
area: sample.area
|
|
2077
2874
|
};
|
|
2078
2875
|
});
|
|
2876
|
+
const hotSpots = selectRoughnessHotSpots(pointSamples, bbox, options.sharpAngleDeg);
|
|
2079
2877
|
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
2080
2878
|
geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
|
|
2081
2879
|
geometry.computeBoundingBox();
|
|
2082
2880
|
return {
|
|
2083
2881
|
geometry,
|
|
2084
2882
|
pointSamples,
|
|
2883
|
+
hotSpots,
|
|
2085
2884
|
summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
|
|
2086
2885
|
warnings
|
|
2087
2886
|
};
|
|
2088
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
|
+
}
|
|
2089
2936
|
function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
|
|
2090
2937
|
const edgeAngles = [];
|
|
2091
2938
|
let boundaryEdgeCount = 0;
|
|
@@ -4130,14 +4977,47 @@ function mergeScalarFieldParams(override) {
|
|
|
4130
4977
|
}
|
|
4131
4978
|
return merged;
|
|
4132
4979
|
}
|
|
4133
|
-
function
|
|
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 = {}) {
|
|
4134
5014
|
const started = performance.now();
|
|
4135
|
-
const scalarSamples = scalarSamplesFromPointSamples(samples);
|
|
4136
5015
|
if (scalarSamples.values.length === 0) return null;
|
|
4137
5016
|
const reconstructStarted = performance.now();
|
|
4138
5017
|
const field = reconstructSurfaceScalarField(trianglePositions, scalarSamples, {
|
|
4139
5018
|
vertexCap: SCALAR_SURFACE_VERTEX_CAP,
|
|
4140
|
-
targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR
|
|
5019
|
+
targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR,
|
|
5020
|
+
...fieldOptions
|
|
4141
5021
|
});
|
|
4142
5022
|
const reconstructMs = performance.now() - reconstructStarted;
|
|
4143
5023
|
inspectProfileLog("scalar-overlay", {
|
|
@@ -4150,24 +5030,11 @@ function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domai
|
|
|
4150
5030
|
reconstructMs: Number(reconstructMs.toFixed(1)),
|
|
4151
5031
|
totalMs: Number((performance.now() - started).toFixed(1))
|
|
4152
5032
|
});
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
return {
|
|
4159
|
-
overlay: {
|
|
4160
|
-
renderable,
|
|
4161
|
-
positions: field.positions,
|
|
4162
|
-
normals: field.normals,
|
|
4163
|
-
index: field.index,
|
|
4164
|
-
aValue: field.values,
|
|
4165
|
-
colorScale,
|
|
4166
|
-
fieldParams
|
|
4167
|
-
},
|
|
4168
|
-
capped: field.capped,
|
|
4169
|
-
holeCount: field.holeCount
|
|
4170
|
-
};
|
|
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);
|
|
4171
5038
|
}
|
|
4172
5039
|
function renderScalarSurfaceOverlays(session, overlays) {
|
|
4173
5040
|
const started = performance.now();
|
|
@@ -4187,20 +5054,37 @@ function renderScalarSurfaceOverlays(session, overlays) {
|
|
|
4187
5054
|
ghostMaterial.toneMapped = false;
|
|
4188
5055
|
renderable.solid.material = ghostMaterial;
|
|
4189
5056
|
const overlay = overlayById.get(renderable.id);
|
|
5057
|
+
const previousVisible = renderable.solid.visible;
|
|
5058
|
+
if (overlay) renderable.solid.visible = false;
|
|
4190
5059
|
if (!overlay || overlay.positions.length === 0) {
|
|
4191
|
-
return {
|
|
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
|
+
};
|
|
4192
5071
|
}
|
|
4193
5072
|
const geometry = new BufferGeometry();
|
|
4194
5073
|
geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
|
|
4195
5074
|
geometry.setAttribute("normal", new BufferAttribute(overlay.normals, 3));
|
|
4196
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));
|
|
4197
5077
|
geometry.setIndex(new BufferAttribute(overlay.index, 1));
|
|
4198
5078
|
const lut = makeColorScaleTexture(colorScaleLUT(overlay.colorScale.colormap));
|
|
4199
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);
|
|
4200
5082
|
const uniforms = makeInspectScalarUniforms({
|
|
4201
5083
|
colorScale: lut,
|
|
5084
|
+
scalarTexture,
|
|
4202
5085
|
domainMin: overlay.colorScale.domainMin,
|
|
4203
5086
|
domainMax: overlay.colorScale.domainMax,
|
|
5087
|
+
colorScaleReversed: overlay.colorScale.reversed === true,
|
|
4204
5088
|
quantizeBands: overlay.fieldParams.quantizeBands,
|
|
4205
5089
|
isoEnabled: overlay.fieldParams.isoEnabled,
|
|
4206
5090
|
isoSpacing: overlay.fieldParams.isoSpacing,
|
|
@@ -4226,7 +5110,7 @@ function renderScalarSurfaceOverlays(session, overlays) {
|
|
|
4226
5110
|
mesh.renderOrder = 5;
|
|
4227
5111
|
mesh.raycast = () => null;
|
|
4228
5112
|
renderable.root.add(mesh);
|
|
4229
|
-
return { renderable, previousMaterial, ghostMaterial, mesh, geometry, material, lut };
|
|
5113
|
+
return { renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture };
|
|
4230
5114
|
});
|
|
4231
5115
|
try {
|
|
4232
5116
|
const png = withSolidOnlyVisibility(
|
|
@@ -4242,21 +5126,25 @@ function renderScalarSurfaceOverlays(session, overlays) {
|
|
|
4242
5126
|
});
|
|
4243
5127
|
return png;
|
|
4244
5128
|
} finally {
|
|
4245
|
-
replacements.forEach(
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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
|
+
);
|
|
4253
5141
|
}
|
|
4254
5142
|
}
|
|
4255
5143
|
function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
|
|
4256
5144
|
var _a;
|
|
4257
5145
|
const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
|
|
4258
5146
|
const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
|
|
4259
|
-
const fieldParams = mergeScalarFieldParams(fieldOverride);
|
|
5147
|
+
const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
|
|
4260
5148
|
const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
|
|
4261
5149
|
if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
|
|
4262
5150
|
const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
|
|
@@ -4265,7 +5153,8 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
|
|
|
4265
5153
|
const thicknessColorScale = {
|
|
4266
5154
|
colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
|
|
4267
5155
|
domainMin: options.colorMinThickness,
|
|
4268
|
-
domainMax: options.colorMaxThickness
|
|
5156
|
+
domainMax: options.colorMaxThickness,
|
|
5157
|
+
...{ reversed: true }
|
|
4269
5158
|
};
|
|
4270
5159
|
const objects = [];
|
|
4271
5160
|
const cloudObjects = [];
|
|
@@ -4278,7 +5167,6 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
|
|
|
4278
5167
|
neighborSets: raycastConnectivity.neighborIdsByObjectId.size
|
|
4279
5168
|
});
|
|
4280
5169
|
session.renderables.forEach((renderable, index) => {
|
|
4281
|
-
var _a2;
|
|
4282
5170
|
const sourceObject = byId.get(renderable.id);
|
|
4283
5171
|
const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
|
|
4284
5172
|
const analysisStarted = performance.now();
|
|
@@ -4327,25 +5215,32 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
|
|
|
4327
5215
|
...sample
|
|
4328
5216
|
});
|
|
4329
5217
|
});
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
const
|
|
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(
|
|
4333
5229
|
renderable,
|
|
4334
|
-
|
|
4335
|
-
analysis.pointSamples,
|
|
5230
|
+
field,
|
|
4336
5231
|
{ min: thicknessColorScale.domainMin, max: thicknessColorScale.domainMax },
|
|
4337
5232
|
fieldParams
|
|
4338
5233
|
);
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
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
|
+
);
|
|
4349
5244
|
}
|
|
4350
5245
|
}
|
|
4351
5246
|
analysis.geometry.dispose();
|
|
@@ -4384,12 +5279,212 @@ function renderCurrentThickness(session, rawOptions, fieldOverride) {
|
|
|
4384
5279
|
const state = getSessionThicknessInspection(session, rawOptions, fieldOverride);
|
|
4385
5280
|
return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
4386
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 };
|
|
5417
|
+
}
|
|
4387
5418
|
const ROUGHNESS_SMOOTH_OPACITY = 0.16;
|
|
4388
5419
|
const ROUGHNESS_HARSH_OPACITY = 1;
|
|
4389
5420
|
function renderCurrentRoughness(session, rawOptions, fieldOverride) {
|
|
4390
5421
|
const state = getSessionRoughnessInspection(session, rawOptions, fieldOverride);
|
|
4391
5422
|
return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
|
|
4392
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
|
+
};
|
|
5487
|
+
}
|
|
4393
5488
|
function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
4394
5489
|
var _a;
|
|
4395
5490
|
const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
|
|
@@ -4401,6 +5496,7 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
|
4401
5496
|
const warnings = [];
|
|
4402
5497
|
maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
|
|
4403
5498
|
const objects = [];
|
|
5499
|
+
const hotSpotCandidates = [];
|
|
4404
5500
|
const cloudObjects = [];
|
|
4405
5501
|
const points = [];
|
|
4406
5502
|
const overlays = [];
|
|
@@ -4455,6 +5551,16 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
|
4455
5551
|
...sample
|
|
4456
5552
|
});
|
|
4457
5553
|
});
|
|
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
|
+
});
|
|
4458
5564
|
const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
|
|
4459
5565
|
if (trianglePositions instanceof Float32Array) {
|
|
4460
5566
|
const built = buildScalarSurfaceOverlay(renderable, trianglePositions, analysis.pointSamples, null, fieldParams);
|
|
@@ -4472,6 +5578,15 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
|
4472
5578
|
}
|
|
4473
5579
|
analysis.geometry.dispose();
|
|
4474
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 }));
|
|
4475
5590
|
const state = {
|
|
4476
5591
|
optionsKey,
|
|
4477
5592
|
overlays,
|
|
@@ -4490,6 +5605,7 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
|
|
|
4490
5605
|
sampleBudget,
|
|
4491
5606
|
objectCount: objects.length,
|
|
4492
5607
|
objects,
|
|
5608
|
+
hotSpots,
|
|
4493
5609
|
warnings,
|
|
4494
5610
|
style: {
|
|
4495
5611
|
// Legacy class colors kept for back-compat; colorScale is the truth the
|
|
@@ -6180,6 +7296,7 @@ Fix one:
|
|
|
6180
7296
|
collisionEntries,
|
|
6181
7297
|
collisionReport: null,
|
|
6182
7298
|
thicknessInspection: null,
|
|
7299
|
+
throughThicknessInspection: null,
|
|
6183
7300
|
roughnessInspection: null,
|
|
6184
7301
|
joints,
|
|
6185
7302
|
jointCouplings,
|
|
@@ -6264,7 +7381,7 @@ window.__forgeRender = async (code, opts) => {
|
|
|
6264
7381
|
scanGranularity: opts == null ? void 0 : opts.scanGranularity,
|
|
6265
7382
|
respectAuthoredSceneStyle: opts == null ? void 0 : opts.respectAuthoredSceneStyle,
|
|
6266
7383
|
cutaway: (opts == null ? void 0 : opts.cutaway) ?? null,
|
|
6267
|
-
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"),
|
|
6268
7385
|
includeCollisions: requestedChannels.has("collisions"),
|
|
6269
7386
|
capture: "orbit"
|
|
6270
7387
|
});
|
|
@@ -6343,7 +7460,9 @@ window.__forgeRender = async (code, opts) => {
|
|
|
6343
7460
|
const comparisonRenders = {};
|
|
6344
7461
|
const collisionRenders = {};
|
|
6345
7462
|
const thicknessRenders = {};
|
|
7463
|
+
const throughThicknessRenders = {};
|
|
6346
7464
|
const roughnessRenders = {};
|
|
7465
|
+
const stressRenders = {};
|
|
6347
7466
|
let maskObjects = [];
|
|
6348
7467
|
let connectivityReport = null;
|
|
6349
7468
|
let floatingReport = null;
|
|
@@ -6353,8 +7472,11 @@ window.__forgeRender = async (code, opts) => {
|
|
|
6353
7472
|
let comparisonPointCloud = null;
|
|
6354
7473
|
let collisionReport = null;
|
|
6355
7474
|
let thicknessReport = null;
|
|
7475
|
+
let throughThicknessReport = null;
|
|
6356
7476
|
let roughnessReport = null;
|
|
7477
|
+
let stressReport = null;
|
|
6357
7478
|
let thicknessPointCloud = null;
|
|
7479
|
+
let throughThicknessPointCloud = null;
|
|
6358
7480
|
let roughnessPointCloud = null;
|
|
6359
7481
|
let sectionEvidence = null;
|
|
6360
7482
|
const framingViews = {};
|
|
@@ -6494,6 +7616,22 @@ window.__forgeRender = async (code, opts) => {
|
|
|
6494
7616
|
thicknessPointCloud = thickness.pointCloud;
|
|
6495
7617
|
await markChannelViewDone("thickness", label);
|
|
6496
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
|
+
}
|
|
6497
7635
|
} catch (e) {
|
|
6498
7636
|
if (comparisonSession) disposeSession(comparisonSession);
|
|
6499
7637
|
disposeSession(session);
|
|
@@ -6602,6 +7740,19 @@ window.__forgeRender = async (code, opts) => {
|
|
|
6602
7740
|
} : {
|
|
6603
7741
|
views: thicknessRenders
|
|
6604
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
|
+
},
|
|
6605
7756
|
section: sectionEvidence,
|
|
6606
7757
|
bbox: session.bbox,
|
|
6607
7758
|
volume: session.volume,
|