Skip to content

Commit db42373

Browse files
author
mmiscool
committed
fix face metadata for bend faces to support radius PMI.
1 parent c7829ef commit db42373

4 files changed

Lines changed: 463 additions & 1 deletion

File tree

src/features/sheetMetal/sheetMetalEngineBridge/render.js

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,124 @@ function resamplePolyline3(points, sampleCount) {
272272
return out;
273273
}
274274

275+
function pointDistanceToAxis(point, axisPoint, axisDir) {
276+
if (!point || !axisPoint || !axisDir) return null;
277+
const offset = point.clone().sub(axisPoint);
278+
const axial = axisDir.clone().multiplyScalar(offset.dot(axisDir));
279+
return offset.sub(axial).length();
280+
}
281+
282+
function firstPointOnFaceGrid(points) {
283+
if (!Array.isArray(points)) return null;
284+
for (const point of points) {
285+
if (!Array.isArray(point) || point.length < 3) continue;
286+
return new THREE.Vector3(point[0], point[1], point[2]);
287+
}
288+
return null;
289+
}
290+
291+
function buildBendCylindricalMetadata({ facePoints, axisStart, axisEnd, axisDir, fallbackRadius = null }) {
292+
if (!axisStart?.isVector3 || !axisEnd?.isVector3 || !axisDir?.isVector3) return null;
293+
const axisCenter = axisStart.clone().add(axisEnd).multiplyScalar(0.5);
294+
const samplePoint = firstPointOnFaceGrid(facePoints);
295+
const sampledRadius = samplePoint ? pointDistanceToAxis(samplePoint, axisStart, axisDir) : null;
296+
const radius = Number.isFinite(sampledRadius) ? sampledRadius : fallbackRadius;
297+
const height = axisStart.distanceTo(axisEnd);
298+
if (!Number.isFinite(radius) || radius <= EPS) return null;
299+
return {
300+
type: "cylindrical",
301+
radius,
302+
height: Number.isFinite(height) ? height : 0,
303+
axis: [axisDir.x, axisDir.y, axisDir.z],
304+
center: [axisCenter.x, axisCenter.y, axisCenter.z],
305+
};
306+
}
307+
308+
function circumcenter2(a, b, c) {
309+
const ax = toFiniteNumber(a?.[0]);
310+
const ay = toFiniteNumber(a?.[1]);
311+
const bx = toFiniteNumber(b?.[0]);
312+
const by = toFiniteNumber(b?.[1]);
313+
const cx = toFiniteNumber(c?.[0]);
314+
const cy = toFiniteNumber(c?.[1]);
315+
const d = 2 * ((ax * (by - cy)) + (bx * (cy - ay)) + (cx * (ay - by)));
316+
if (Math.abs(d) <= EPS) return null;
317+
318+
const aSq = (ax * ax) + (ay * ay);
319+
const bSq = (bx * bx) + (by * by);
320+
const cSq = (cx * cx) + (cy * cy);
321+
return [
322+
((aSq * (by - cy)) + (bSq * (cy - ay)) + (cSq * (ay - by))) / d,
323+
((aSq * (cx - bx)) + (bSq * (ax - cx)) + (cSq * (bx - ax))) / d,
324+
];
325+
}
326+
327+
function fitCircularEdgePolyline2(polyline) {
328+
const points = Array.isArray(polyline) ? polyline : [];
329+
if (points.length < 3) return null;
330+
const first = points[0];
331+
const mid = points[(points.length / 2) | 0];
332+
const last = points[points.length - 1];
333+
const center = circumcenter2(first, mid, last);
334+
if (!center) return null;
335+
336+
const radius = Math.hypot(first[0] - center[0], first[1] - center[1]);
337+
if (!(Number.isFinite(radius) && radius > EPS)) return null;
338+
339+
const startAngle = Math.atan2(first[1] - center[1], first[0] - center[0]);
340+
const endAngle = Math.atan2(last[1] - center[1], last[0] - center[0]);
341+
let delta = endAngle - startAngle;
342+
while (delta <= -Math.PI) delta += Math.PI * 2;
343+
while (delta > Math.PI) delta -= Math.PI * 2;
344+
if (Math.abs(delta) <= THREE.MathUtils.degToRad(1)) return null;
345+
346+
const tolerance = Math.max(1e-4, radius * 1e-3);
347+
for (const point of points) {
348+
const sampleRadius = Math.hypot(point[0] - center[0], point[1] - center[1]);
349+
if (Math.abs(sampleRadius - radius) > tolerance) return null;
350+
}
351+
352+
return { center, radius };
353+
}
354+
355+
function buildFlatWallCylindricalMetadata({ edge, placementMatrix, thickness }) {
356+
const fit = fitCircularEdgePolyline2(edge?.polyline);
357+
if (!fit) return null;
358+
const axisDir = new THREE.Vector3(0, 0, 1).transformDirection(placementMatrix).normalize();
359+
const axisCenter = makeMidplaneWorldPoint(placementMatrix, fit.center);
360+
const height = Math.abs(toFiniteNumber(thickness, 0));
361+
if (!(height > EPS)) return null;
362+
return {
363+
type: "cylindrical",
364+
radius: fit.radius,
365+
height,
366+
axis: [axisDir.x, axisDir.y, axisDir.z],
367+
center: [axisCenter.x, axisCenter.y, axisCenter.z],
368+
};
369+
}
370+
371+
function addCylindricalFaceCenterline({ solid, faceName, metadata }) {
372+
if (!solid || !faceName || !metadata || metadata.type !== "cylindrical") return;
373+
const axis = Array.isArray(metadata.axis) ? metadata.axis : null;
374+
const center = Array.isArray(metadata.center) ? metadata.center : null;
375+
const height = Math.abs(toFiniteNumber(metadata.height, 0));
376+
if (!axis || axis.length !== 3 || !center || center.length !== 3 || !(height > EPS)) return;
377+
378+
const axisDir = new THREE.Vector3(axis[0], axis[1], axis[2]);
379+
if (axisDir.lengthSq() <= EPS) return;
380+
axisDir.normalize();
381+
382+
const axisCenter = new THREE.Vector3(center[0], center[1], center[2]);
383+
const halfHeight = height * 0.5;
384+
const start = axisCenter.clone().addScaledVector(axisDir, -halfHeight);
385+
const end = axisCenter.clone().addScaledVector(axisDir, halfHeight);
386+
solid.addAuxEdge(`${faceName}:CENTERLINE`, [quantizePoint3(start), quantizePoint3(end)], {
387+
centerline: true,
388+
materialKey: "OVERLAY",
389+
polylineWorld: false,
390+
});
391+
}
392+
275393
function buildBendLookup(tree) {
276394
const lookup = new Map();
277395
const visitFlat = (flat) => {
@@ -408,7 +526,13 @@ function addFlatPlacementToSolid({ solid, placement, featureID, thickness, edgeC
408526
const topB = outerOffset + next;
409527
addTriangleIfValid(solid, sideFace, topPoints[topA], bottomPoints[topA], topPoints[topB]);
410528
addTriangleIfValid(solid, sideFace, topPoints[topB], bottomPoints[topA], bottomPoints[topB]);
529+
const cylindricalMeta = buildFlatWallCylindricalMetadata({
530+
edge: mappedEdge,
531+
placementMatrix: placement.matrix,
532+
thickness,
533+
});
411534
solid.setFaceMetadata(sideFace, {
535+
...(cylindricalMeta || {}),
412536
flatId: flat.id,
413537
edgeId,
414538
edgeSignature: signature || null,
@@ -420,6 +544,7 @@ function addFlatPlacementToSolid({ solid, placement, featureID, thickness, edgeC
420544
edgeSignature: signature || null,
421545
},
422546
});
547+
if (cylindricalMeta) addCylindricalFaceCenterline({ solid, faceName: sideFace, metadata: cylindricalMeta });
423548
}
424549

425550
for (let holeIndex = 0; holeIndex < holeEntries.length; holeIndex += 1) {
@@ -520,7 +645,9 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
520645
const axis = bendPlacement.axisEnd.clone().sub(bendPlacement.axisStart);
521646
if (axis.lengthSq() <= EPS) return;
522647
const axisDir = axis.normalize();
523-
const axisOrigin = bendPlacement.axisStart.clone();
648+
const axisStart = bendPlacement.axisStart.clone();
649+
const axisEnd = bendPlacement.axisEnd.clone();
650+
const axisOrigin = axisStart.clone();
524651
const sampleCount = Math.max(2, parentEdgeWorld.length, childEdgeWorld.length);
525652
const parentEdge = resamplePolyline3(parentEdgeWorld, sampleCount);
526653
const childEdge = resamplePolyline3(childEdgeWorld, sampleCount);
@@ -561,6 +688,24 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
561688

562689
const lookup = bendLookup.get(String(bend.id)) || {};
563690
const parentEdgeId = lookup?.parentEdgeId ?? null;
691+
const midRadius = Math.max(EPS, toFiniteNumber(bendPlacement.midRadius, 0));
692+
const fallbackOuterRadius = midRadius + halfT;
693+
const fallbackInnerRadius = Math.max(EPS, midRadius - halfT);
694+
const isFaceAOuter = bendPlacement.angleRad >= 0;
695+
const faceAMetadata = buildBendCylindricalMetadata({
696+
facePoints: top,
697+
axisStart,
698+
axisEnd,
699+
axisDir,
700+
fallbackRadius: isFaceAOuter ? fallbackOuterRadius : fallbackInnerRadius,
701+
});
702+
const faceBMetadata = buildBendCylindricalMetadata({
703+
facePoints: bottom,
704+
axisStart,
705+
axisEnd,
706+
axisDir,
707+
fallbackRadius: isFaceAOuter ? fallbackInnerRadius : fallbackOuterRadius,
708+
});
564709

565710
const faceA = makeBendFaceName(featureID, bend.id, "A");
566711
const faceB = makeBendFaceName(featureID, bend.id, "B");
@@ -602,6 +747,7 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
602747
}
603748

604749
solid.setFaceMetadata(faceA, {
750+
...(faceAMetadata || {}),
605751
bendId: bend.id,
606752
sheetMetalFaceType: "A",
607753
sheetMetal: {
@@ -616,6 +762,7 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
616762
},
617763
});
618764
solid.setFaceMetadata(faceB, {
765+
...(faceBMetadata || {}),
619766
bendId: bend.id,
620767
sheetMetalFaceType: "B",
621768
sheetMetal: {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {
2+
__test_buildRenderableSheetModelFromTree,
3+
} from "../features/sheetMetal/sheetMetalEngineBridge.js";
4+
import {
5+
sheetMetalNonManifoldSmF18Fixture,
6+
} from "./fixtures/sheetMetal_nonManifold_sm_f18.js";
7+
8+
function approxEqual(a, b, tol = 1e-3) {
9+
return Math.abs(Number(a) - Number(b)) <= tol;
10+
}
11+
12+
function distancePointToAxis(point, center, axis) {
13+
const px = point[0] - center[0];
14+
const py = point[1] - center[1];
15+
const pz = point[2] - center[2];
16+
const dot = px * axis[0] + py * axis[1] + pz * axis[2];
17+
const rx = px - axis[0] * dot;
18+
const ry = py - axis[1] * dot;
19+
const rz = pz - axis[2] * dot;
20+
return Math.hypot(rx, ry, rz);
21+
}
22+
23+
function collectFaceVertexAxisDistances(solid, faceName, center, axis) {
24+
const faceId = solid?._faceNameToID?.get(faceName);
25+
const triVerts = Array.isArray(solid?._triVerts) ? solid._triVerts : [];
26+
const triIDs = Array.isArray(solid?._triIDs) ? solid._triIDs : [];
27+
const vertProperties = Array.isArray(solid?._vertProperties) ? solid._vertProperties : [];
28+
if (faceId == null || !triVerts.length || !triIDs.length || !vertProperties.length) return [];
29+
30+
const distances = [];
31+
for (let triIndex = 0; triIndex < triIDs.length; triIndex += 1) {
32+
if (triIDs[triIndex] !== faceId) continue;
33+
const base = triIndex * 3;
34+
for (let corner = 0; corner < 3; corner += 1) {
35+
const vertexIndex = triVerts[base + corner] * 3;
36+
const point = [
37+
vertProperties[vertexIndex],
38+
vertProperties[vertexIndex + 1],
39+
vertProperties[vertexIndex + 2],
40+
];
41+
distances.push(distancePointToAxis(point, center, axis));
42+
}
43+
}
44+
return distances;
45+
}
46+
47+
function validateBendFaceMetadata(solid, faceName, metadata) {
48+
if (!metadata || metadata.type !== "cylindrical") {
49+
throw new Error(`Expected cylindrical metadata on bend face "${faceName}".`);
50+
}
51+
if (!Number.isFinite(metadata.radius) || metadata.radius <= 0) {
52+
throw new Error(`Expected positive bend-face radius on "${faceName}".`);
53+
}
54+
if (!Number.isFinite(metadata.height) || metadata.height <= 0) {
55+
throw new Error(`Expected positive bend-face height on "${faceName}".`);
56+
}
57+
if (!Array.isArray(metadata.axis) || metadata.axis.length !== 3) {
58+
throw new Error(`Expected axis triplet on bend face "${faceName}".`);
59+
}
60+
if (!Array.isArray(metadata.center) || metadata.center.length !== 3) {
61+
throw new Error(`Expected center triplet on bend face "${faceName}".`);
62+
}
63+
64+
const axisLength = Math.hypot(metadata.axis[0], metadata.axis[1], metadata.axis[2]);
65+
if (!approxEqual(axisLength, 1, 1e-3)) {
66+
throw new Error(`Expected normalized bend-face axis on "${faceName}".`);
67+
}
68+
69+
const distances = collectFaceVertexAxisDistances(solid, faceName, metadata.center, metadata.axis);
70+
if (!distances.length) {
71+
throw new Error(`Could not sample bend-face vertices for "${faceName}".`);
72+
}
73+
74+
const avgRadius = distances.reduce((sum, value) => sum + value, 0) / distances.length;
75+
const minRadius = Math.min(...distances);
76+
const maxRadius = Math.max(...distances);
77+
if (!approxEqual(avgRadius, metadata.radius, 1e-3)) {
78+
throw new Error(`Bend-face radius metadata does not match geometry on "${faceName}".`);
79+
}
80+
if (maxRadius - minRadius > 2e-3) {
81+
throw new Error(`Bend-face vertices are not consistent with a cylindrical radius on "${faceName}".`);
82+
}
83+
}
84+
85+
export async function test_sheetMetal_bend_face_cylindrical_metadata(partHistory) {
86+
await partHistory.reset();
87+
partHistory.features = [];
88+
89+
const { featureID, tree, rootTransform } = sheetMetalNonManifoldSmF18Fixture;
90+
const result = __test_buildRenderableSheetModelFromTree({
91+
featureID,
92+
tree,
93+
rootMatrix: rootTransform,
94+
showFlatPattern: false,
95+
});
96+
97+
const solid = result?.root || null;
98+
const bends = Array.isArray(result?.evaluated?.bends3D) ? result.evaluated.bends3D : [];
99+
if (!solid || typeof solid.getFaceMetadata !== "function") {
100+
throw new Error("Sheet-metal bend metadata test did not produce a readable solid.");
101+
}
102+
if (!bends.length) {
103+
throw new Error("Sheet-metal bend metadata test did not produce any bends.");
104+
}
105+
106+
const bendPlacement = bends.find((entry) => entry?.bend?.id && Number.isFinite(entry?.midRadius));
107+
if (!bendPlacement) {
108+
throw new Error("Could not find a bend placement with radius data.");
109+
}
110+
111+
const faceA = `${featureID}:BEND:${bendPlacement.bend.id}:A`;
112+
const faceB = `${featureID}:BEND:${bendPlacement.bend.id}:B`;
113+
const metaA = solid.getFaceMetadata(faceA);
114+
const metaB = solid.getFaceMetadata(faceB);
115+
116+
validateBendFaceMetadata(solid, faceA, metaA);
117+
validateBendFaceMetadata(solid, faceB, metaB);
118+
119+
const thickness = Number(tree?.thickness);
120+
if (!Number.isFinite(thickness) || thickness <= 0) {
121+
throw new Error("Sheet-metal test fixture has invalid thickness.");
122+
}
123+
124+
const expectedRadii = [
125+
Math.max(1e-6, bendPlacement.midRadius - thickness * 0.5),
126+
bendPlacement.midRadius + thickness * 0.5,
127+
].sort((a, b) => a - b);
128+
const actualRadii = [metaA.radius, metaB.radius].sort((a, b) => a - b);
129+
130+
if (!approxEqual(actualRadii[0], expectedRadii[0], 1e-3) || !approxEqual(actualRadii[1], expectedRadii[1], 1e-3)) {
131+
throw new Error(`Bend-face metadata radii do not match inside/outside bend radii for bend "${bendPlacement.bend.id}".`);
132+
}
133+
134+
if (!approxEqual(Math.abs(metaA.radius - metaB.radius), thickness, 1e-3)) {
135+
throw new Error(`Bend-face metadata radii should differ by sheet thickness for bend "${bendPlacement.bend.id}".`);
136+
}
137+
138+
return partHistory;
139+
}

0 commit comments

Comments
 (0)