void update boundary provenance
This commit is contained in:
parent
f6bb8c1569
commit
6948e7ef25
5 changed files with 481 additions and 35 deletions
|
|
@ -15,16 +15,21 @@ Completed:
|
||||||
5. Segment/surface/region ID labels (overlay text).
|
5. Segment/surface/region ID labels (overlay text).
|
||||||
6. Fixed world/local debug overlay transform bug:
|
6. Fixed world/local debug overlay transform bug:
|
||||||
7. GeometryStore points are world-space; line geometry parented under solids root must convert world -> root local because `space.WORLD` is rotated -90deg on X.
|
7. GeometryStore points are world-space; line geometry parented under solids root must convert world -> root local because `space.WORLD` is rotated -90deg on X.
|
||||||
|
8. Manifold relation passthrough wired through kernel/worker/rebuild (`runIndex`, `runOriginalID`, `faceID`, and source-run solid mapping).
|
||||||
|
9. GeometryStore now emits provenance-partitioned `surface_patches` from per-face triangle run attribution (not just per-face-loop seeds).
|
||||||
|
10. Topology now records `patch_to_tris` and `tri_to_patch` during snapshot build for downstream hover/selection cutover.
|
||||||
|
11. Debug boundary rendering now prefers patch boundaries when present, so visualization aligns with sketch-derived/provenance splits.
|
||||||
|
|
||||||
In progress:
|
In progress:
|
||||||
|
|
||||||
1. Converting this plan into code-level milestone execution with strict acceptance checks.
|
1. Cut over hover/selection resolvers from face-loop heuristics to patch-first entities (`surface_patch_id` canonical path).
|
||||||
|
2. Improve partition quality from triangle boundary approximation to robust boundary arrangement where needed.
|
||||||
|
|
||||||
Next up:
|
Next up:
|
||||||
|
|
||||||
1. Add first `surface_patch_id` selection plumbing (read-only pass-through).
|
1. Add explicit `surface_patch_id` in hit/canonical selection entities.
|
||||||
2. Thread Manifold relation metadata through kernel adapters.
|
2. Bind extrude-profile hover/select directly to patch/source-region maps.
|
||||||
3. Start mixed-face partition scaffold from seeded `surface_patches`.
|
3. Add regression fixtures for boolean unions/subtracts with mixed curved + planar outputs.
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,24 @@ function createSolidsApi(getApi) {
|
||||||
? rec.indices
|
? rec.indices
|
||||||
: new Uint32Array(rec.indices || []);
|
: new Uint32Array(rec.indices || []);
|
||||||
if (!positions.length || !indices.length) continue;
|
if (!positions.length || !indices.length) continue;
|
||||||
map.set(id, { positions, indices });
|
const mesh = { positions, indices };
|
||||||
|
const optionalUint = ['mergeFromVert', 'mergeToVert', 'runIndex', 'runOriginalID', 'faceID'];
|
||||||
|
for (const key of optionalUint) {
|
||||||
|
if (!rec?.[key]?.length) continue;
|
||||||
|
mesh[key] = rec[key] instanceof Uint32Array ? rec[key] : new Uint32Array(rec[key]);
|
||||||
|
}
|
||||||
|
const optionalFloat = ['halfedgeTangent', 'runTransform'];
|
||||||
|
for (const key of optionalFloat) {
|
||||||
|
if (!rec?.[key]?.length) continue;
|
||||||
|
mesh[key] = rec[key] instanceof Float32Array ? rec[key] : new Float32Array(rec[key]);
|
||||||
|
}
|
||||||
|
if (rec?.run_source_solid_ids && typeof rec.run_source_solid_ids === 'object') {
|
||||||
|
mesh.run_source_solid_ids = rec.run_source_solid_ids;
|
||||||
|
}
|
||||||
|
if (Array.isArray(rec?.source_solid_ids)) {
|
||||||
|
mesh.source_solid_ids = rec.source_solid_ids.map(id => String(id || '')).filter(Boolean);
|
||||||
|
}
|
||||||
|
map.set(id, mesh);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +377,7 @@ function edgeKey(a, b) {
|
||||||
|
|
||||||
groups.set(groupId, {
|
groups.set(groupId, {
|
||||||
id: groupId,
|
id: groupId,
|
||||||
|
tris: tris.slice(),
|
||||||
geometry: faceGeom,
|
geometry: faceGeom,
|
||||||
center,
|
center,
|
||||||
normal,
|
normal,
|
||||||
|
|
@ -637,6 +655,209 @@ function edgeKey(a, b) {
|
||||||
return `${x}:${y}:${z}`;
|
return `${x}:${y}:${z}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRegionIds(ids = [], fallback = []) {
|
||||||
|
const next = new Set();
|
||||||
|
for (const id of ids || []) {
|
||||||
|
const raw = String(id || '').trim();
|
||||||
|
if (raw) next.add(raw);
|
||||||
|
}
|
||||||
|
if (!next.size) {
|
||||||
|
for (const id of fallback || []) {
|
||||||
|
const raw = String(id || '').trim();
|
||||||
|
if (raw) next.add(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!next.size) next.add('region:unknown');
|
||||||
|
return Array.from(next).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionKey(ids = []) {
|
||||||
|
return normalizeRegionIds(ids).join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runResolver(meshData = null) {
|
||||||
|
const runIndex = meshData?.runIndex;
|
||||||
|
const runOriginalID = meshData?.runOriginalID;
|
||||||
|
if (!runIndex?.length || runIndex.length < 2 || !runOriginalID?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (triIndex) => {
|
||||||
|
const tri = Number(triIndex);
|
||||||
|
if (!Number.isFinite(tri) || tri < 0) return null;
|
||||||
|
let lo = 0;
|
||||||
|
let hi = runIndex.length - 2;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const mid = (lo + hi) >> 1;
|
||||||
|
const a = Number(runIndex[mid]);
|
||||||
|
const b = Number(runIndex[mid + 1]);
|
||||||
|
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
|
||||||
|
if (tri < a) {
|
||||||
|
hi = mid - 1;
|
||||||
|
} else if (tri >= b) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
return Number(runOriginalID[mid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function facePatchesFromTriProvenance({
|
||||||
|
faceMeta = null,
|
||||||
|
mesh = null,
|
||||||
|
meshData = null,
|
||||||
|
sourceProfileKeys = [],
|
||||||
|
solidsById = new Map()
|
||||||
|
} = {}) {
|
||||||
|
const geometry = faceMeta?.geometry || null;
|
||||||
|
const indexAttr = geometry?.getIndex?.() || null;
|
||||||
|
const posAttr = geometry?.getAttribute?.('position') || null;
|
||||||
|
const triIndex = indexAttr?.array || null;
|
||||||
|
const triCount = Math.floor(Number(triIndex?.length || 0) / 3);
|
||||||
|
if (!posAttr || !triCount) return [];
|
||||||
|
|
||||||
|
const faceTris = Array.isArray(faceMeta?.tris) ? faceMeta.tris : [];
|
||||||
|
const resolveRun = runResolver(meshData);
|
||||||
|
const runSourceSolidIds = meshData?.run_source_solid_ids || {};
|
||||||
|
const fallbackSolidIds = Array.isArray(meshData?.source_solid_ids) ? meshData.source_solid_ids : [];
|
||||||
|
|
||||||
|
const triRegionIds = new Array(triCount);
|
||||||
|
const triRegionKeys = new Array(triCount);
|
||||||
|
const sourceRegionIdsSet = new Set();
|
||||||
|
|
||||||
|
for (let ti = 0; ti < triCount; ti++) {
|
||||||
|
const globalTri = Number(faceTris[ti]);
|
||||||
|
const sourceSolidIds = new Set();
|
||||||
|
const runOriginal = resolveRun ? resolveRun(globalTri) : null;
|
||||||
|
if (Number.isFinite(runOriginal)) {
|
||||||
|
const runSources = runSourceSolidIds[String(runOriginal)];
|
||||||
|
if (Array.isArray(runSources) && runSources.length) {
|
||||||
|
for (const sid of runSources) {
|
||||||
|
const id = String(sid || '').trim();
|
||||||
|
if (id) sourceSolidIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sourceSolidIds.size) {
|
||||||
|
for (const sid of fallbackSolidIds) {
|
||||||
|
const id = String(sid || '').trim();
|
||||||
|
if (id) sourceSolidIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fromSolids = [];
|
||||||
|
for (const sid of sourceSolidIds) {
|
||||||
|
const solid = solidsById.get(String(sid || ''));
|
||||||
|
const keys = Array.isArray(solid?.source?.profile_keys) ? solid.source.profile_keys : [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const kid = String(key || '').trim();
|
||||||
|
if (kid) fromSolids.push(kid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ids = normalizeRegionIds(fromSolids, sourceProfileKeys);
|
||||||
|
triRegionIds[ti] = ids;
|
||||||
|
triRegionKeys[ti] = regionKey(ids);
|
||||||
|
for (const id of ids) sourceRegionIdsSet.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const triNeighbors = Array.from({ length: triCount }, () => []);
|
||||||
|
const firstByEdge = new Map();
|
||||||
|
for (let ti = 0; ti < triCount; ti++) {
|
||||||
|
const a = Number(triIndex[ti * 3]);
|
||||||
|
const b = Number(triIndex[ti * 3 + 1]);
|
||||||
|
const c = Number(triIndex[ti * 3 + 2]);
|
||||||
|
const edges = [[a, b], [b, c], [c, a]];
|
||||||
|
for (const [v0, v1] of edges) {
|
||||||
|
const key = edgeKey(v0, v1);
|
||||||
|
if (!firstByEdge.has(key)) {
|
||||||
|
firstByEdge.set(key, ti);
|
||||||
|
} else {
|
||||||
|
const other = firstByEdge.get(key);
|
||||||
|
if (Number.isFinite(other) && other !== ti) {
|
||||||
|
triNeighbors[ti].push(other);
|
||||||
|
triNeighbors[other].push(ti);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const localToWorld = new Map();
|
||||||
|
const worldVertex = (vi) => {
|
||||||
|
const key = Number(vi);
|
||||||
|
if (localToWorld.has(key)) return localToWorld.get(key);
|
||||||
|
const p = new THREE.Vector3().fromBufferAttribute(posAttr, key);
|
||||||
|
const out = mesh?.matrixWorld ? p.applyMatrix4(mesh.matrixWorld) : p;
|
||||||
|
localToWorld.set(key, out);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const visited = new Uint8Array(triCount);
|
||||||
|
const groups = [];
|
||||||
|
for (let seed = 0; seed < triCount; seed++) {
|
||||||
|
if (visited[seed]) continue;
|
||||||
|
const key = triRegionKeys[seed];
|
||||||
|
const queue = [seed];
|
||||||
|
const localTris = [];
|
||||||
|
visited[seed] = 1;
|
||||||
|
while (queue.length) {
|
||||||
|
const cur = queue.pop();
|
||||||
|
localTris.push(cur);
|
||||||
|
const nbs = triNeighbors[cur] || [];
|
||||||
|
for (const nb of nbs) {
|
||||||
|
if (visited[nb]) continue;
|
||||||
|
if (triRegionKeys[nb] !== key) continue;
|
||||||
|
visited[nb] = 1;
|
||||||
|
queue.push(nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups.push({
|
||||||
|
key,
|
||||||
|
source_region_ids: triRegionIds[seed].slice(),
|
||||||
|
local_tris: localTris
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
const boundaryEdges = new Map();
|
||||||
|
const triIdsGlobal = [];
|
||||||
|
for (const localTri of group.local_tris) {
|
||||||
|
const i0 = Number(triIndex[localTri * 3]);
|
||||||
|
const i1 = Number(triIndex[localTri * 3 + 1]);
|
||||||
|
const i2 = Number(triIndex[localTri * 3 + 2]);
|
||||||
|
const globalTri = Number(faceTris[localTri]);
|
||||||
|
if (Number.isFinite(globalTri)) triIdsGlobal.push(globalTri);
|
||||||
|
const edges = [[i0, i1], [i1, i2], [i2, i0]];
|
||||||
|
for (const [a, b] of edges) {
|
||||||
|
const ek = edgeKey(a, b);
|
||||||
|
const rec = boundaryEdges.get(ek);
|
||||||
|
if (!rec) {
|
||||||
|
boundaryEdges.set(ek, { a, b, count: 1 });
|
||||||
|
} else {
|
||||||
|
rec.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loopSegments = [];
|
||||||
|
for (const rec of boundaryEdges.values()) {
|
||||||
|
if (Number(rec?.count) !== 1) continue;
|
||||||
|
const a = worldVertex(rec.a);
|
||||||
|
const b = worldVertex(rec.b);
|
||||||
|
if (!a || !b || a.distanceToSquared?.(b) <= 1e-16) continue;
|
||||||
|
loopSegments.push({ a: a.clone(), b: b.clone() });
|
||||||
|
}
|
||||||
|
const loops = buildBoundaryLoopsFromSegments(loopSegments);
|
||||||
|
if (!loops.length) continue;
|
||||||
|
out.push({
|
||||||
|
key: group.key,
|
||||||
|
source_region_ids: group.source_region_ids.slice(),
|
||||||
|
tri_ids: triIdsGlobal,
|
||||||
|
loops
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_rebuildTimer: null,
|
_rebuildTimer: null,
|
||||||
_rebuilding: false,
|
_rebuilding: false,
|
||||||
|
|
@ -935,7 +1156,18 @@ function edgeKey(a, b) {
|
||||||
this._debugGroup.add(lines);
|
this._debugGroup.add(lines);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const boundary of boundaries) {
|
const preferredBoundaryIds = new Set();
|
||||||
|
for (const patch of patches) {
|
||||||
|
const ids = Array.isArray(patch?.boundary_ids) ? patch.boundary_ids : [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const bid = String(id || '').trim();
|
||||||
|
if (bid) preferredBoundaryIds.add(bid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toDraw = preferredBoundaryIds.size
|
||||||
|
? boundaries.filter(boundary => preferredBoundaryIds.has(String(boundary?.id || '')))
|
||||||
|
: boundaries;
|
||||||
|
for (const boundary of toDraw) {
|
||||||
const segmentIds = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
const segmentIds = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
||||||
if (!segmentIds.length) continue;
|
if (!segmentIds.length) continue;
|
||||||
const positions = [];
|
const positions = [];
|
||||||
|
|
@ -1127,6 +1359,7 @@ function edgeKey(a, b) {
|
||||||
patch_to_tris: {},
|
patch_to_tris: {},
|
||||||
tri_to_patch: {}
|
tri_to_patch: {}
|
||||||
};
|
};
|
||||||
|
const solidsById = new Map((this.list() || []).map(item => [String(item?.id || ''), item]));
|
||||||
|
|
||||||
const getPointId = (p, role = 'boundary-vertex') => {
|
const getPointId = (p, role = 'boundary-vertex') => {
|
||||||
const key = quantPointKey(p);
|
const key = quantPointKey(p);
|
||||||
|
|
@ -1146,7 +1379,7 @@ function edgeKey(a, b) {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [solidId, view] of this._meshViews.entries()) {
|
for (const [solidId, view] of this._meshViews.entries()) {
|
||||||
const solid = this.list().find(item => String(item?.id || '') === String(solidId)) || null;
|
const solid = solidsById.get(String(solidId)) || null;
|
||||||
const sourceProfileKeys = Array.isArray(solid?.source?.profile_keys)
|
const sourceProfileKeys = Array.isArray(solid?.source?.profile_keys)
|
||||||
? solid.source.profile_keys.map(key => String(key || '')).filter(Boolean)
|
? solid.source.profile_keys.map(key => String(key || '')).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
@ -1190,6 +1423,31 @@ function edgeKey(a, b) {
|
||||||
});
|
});
|
||||||
this._geomSurfaceIdByFaceKey.set(faceKey, surfaceId);
|
this._geomSurfaceIdByFaceKey.set(faceKey, surfaceId);
|
||||||
|
|
||||||
|
const meshData = this._meshCache?.get?.(String(solidId)) || null;
|
||||||
|
const facePatches = facePatchesFromTriProvenance({
|
||||||
|
faceMeta: meta,
|
||||||
|
mesh,
|
||||||
|
meshData,
|
||||||
|
sourceProfileKeys,
|
||||||
|
solidsById
|
||||||
|
});
|
||||||
|
const faceSourceRegionSet = new Set();
|
||||||
|
for (const patch of facePatches) {
|
||||||
|
for (const id of patch?.source_region_ids || []) {
|
||||||
|
const rid = String(id || '').trim();
|
||||||
|
if (rid) faceSourceRegionSet.add(rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!faceSourceRegionSet.size) {
|
||||||
|
for (const rid of normalizeRegionIds(sourceProfileKeys)) {
|
||||||
|
faceSourceRegionSet.add(rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const faceSourceRegionIds = Array.from(faceSourceRegionSet);
|
||||||
|
const facePrimarySourceRegion = faceSourceRegionIds.length === 1
|
||||||
|
? faceSourceRegionIds[0]
|
||||||
|
: (primarySourceRegion || null);
|
||||||
|
|
||||||
const surfaceSegmentIds = [];
|
const surfaceSegmentIds = [];
|
||||||
for (let li = 0; li < loops.length; li++) {
|
for (let li = 0; li < loops.length; li++) {
|
||||||
const loop = loops[li];
|
const loop = loops[li];
|
||||||
|
|
@ -1260,24 +1518,125 @@ function edgeKey(a, b) {
|
||||||
loop_index: li
|
loop_index: li
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const patchId = `surface-patch:${faceKey}:${li}`;
|
}
|
||||||
const sourceRegionIds = sourceProfileKeys.slice();
|
|
||||||
surface_patches.push({
|
let emittedPatchCount = 0;
|
||||||
id: patchId,
|
if (facePatches.length) {
|
||||||
surface_id: surfaceId,
|
for (let pi = 0; pi < facePatches.length; pi++) {
|
||||||
boundary_ids: [boundaryId],
|
const patch = facePatches[pi];
|
||||||
source_region_id: primarySourceRegion,
|
const patchId = `surface-patch:${faceKey}:${pi}`;
|
||||||
source_region_ids: sourceRegionIds,
|
const patchBoundaryIds = [];
|
||||||
solid_id: solidId,
|
const sourceRegionIds = normalizeRegionIds(patch?.source_region_ids, faceSourceRegionIds);
|
||||||
face_id: faceId,
|
const patchPrimarySourceRegion = sourceRegionIds.length === 1
|
||||||
status: 'seed',
|
? sourceRegionIds[0]
|
||||||
source: {
|
: facePrimarySourceRegion;
|
||||||
type: 'solid-face-loop',
|
const patchLoops = Array.isArray(patch?.loops) ? patch.loops : [];
|
||||||
face_key: faceKey,
|
for (let pli = 0; pli < patchLoops.length; pli++) {
|
||||||
loop_index: li,
|
const loop = patchLoops[pli];
|
||||||
feature_id: solid?.source?.feature_id || null
|
const points = Array.isArray(loop?.points) ? loop.points : [];
|
||||||
|
if (points.length < 2) continue;
|
||||||
|
const boundaryId = `boundary:patch:${faceKey}:${pi}:${pli}`;
|
||||||
|
const boundarySegmentIds = [];
|
||||||
|
const closed = !!loop?.closed;
|
||||||
|
const stepCount = closed ? points.length : (points.length - 1);
|
||||||
|
for (let si = 0; si < stepCount; si++) {
|
||||||
|
const a = points[si];
|
||||||
|
const b = points[(si + 1) % points.length];
|
||||||
|
if (!a || !b || a.distanceToSquared?.(b) <= 1e-16) continue;
|
||||||
|
const segmentId = `segment:patch:${faceKey}:${pi}:${pli}:${si}`;
|
||||||
|
const aId = getPointId(a, 'patch-boundary-vertex');
|
||||||
|
const bId = getPointId(b, 'patch-boundary-vertex');
|
||||||
|
const mid = a.clone().add(b).multiplyScalar(0.5);
|
||||||
|
const midId = getPointId(mid, 'patch-boundary-midpoint');
|
||||||
|
segments.push({
|
||||||
|
id: segmentId,
|
||||||
|
boundary_id: boundaryId,
|
||||||
|
kind: 'line',
|
||||||
|
a: { x: Number(a.x || 0), y: Number(a.y || 0), z: Number(a.z || 0) },
|
||||||
|
b: { x: Number(b.x || 0), y: Number(b.y || 0), z: Number(b.z || 0) },
|
||||||
|
mid: { x: Number(mid.x || 0), y: Number(mid.y || 0), z: Number(mid.z || 0) },
|
||||||
|
point_ids: [aId, bId, midId],
|
||||||
|
source: {
|
||||||
|
type: 'surface-patch-boundary',
|
||||||
|
patch_id: patchId,
|
||||||
|
face_key: faceKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
boundarySegmentIds.push(segmentId);
|
||||||
|
surfaceSegmentIds.push(segmentId);
|
||||||
|
topology.segment_to_surfaces[segmentId] = [surfaceId];
|
||||||
|
}
|
||||||
|
if (!boundarySegmentIds.length) continue;
|
||||||
|
boundaries.push({
|
||||||
|
id: boundaryId,
|
||||||
|
surface_id: surfaceId,
|
||||||
|
segment_ids: boundarySegmentIds,
|
||||||
|
closed,
|
||||||
|
source: {
|
||||||
|
type: 'surface-patch-loop',
|
||||||
|
patch_id: patchId,
|
||||||
|
face_key: faceKey,
|
||||||
|
loop_index: pli
|
||||||
|
}
|
||||||
|
});
|
||||||
|
patchBoundaryIds.push(boundaryId);
|
||||||
}
|
}
|
||||||
});
|
if (!patchBoundaryIds.length) continue;
|
||||||
|
surface_patches.push({
|
||||||
|
id: patchId,
|
||||||
|
surface_id: surfaceId,
|
||||||
|
boundary_ids: patchBoundaryIds,
|
||||||
|
source_region_id: patchPrimarySourceRegion,
|
||||||
|
source_region_ids: sourceRegionIds,
|
||||||
|
solid_id: solidId,
|
||||||
|
face_id: faceId,
|
||||||
|
status: facePatches.length > 1 ? 'partitioned' : 'single-source',
|
||||||
|
source: {
|
||||||
|
type: 'tri-provenance',
|
||||||
|
face_key: faceKey,
|
||||||
|
feature_id: solid?.source?.feature_id || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emittedPatchCount++;
|
||||||
|
const patchTriIds = Array.isArray(patch?.tri_ids) ? patch.tri_ids : [];
|
||||||
|
topology.patch_to_tris[patchId] = patchTriIds.slice();
|
||||||
|
for (const triId of patchTriIds) {
|
||||||
|
if (!Number.isFinite(Number(triId))) continue;
|
||||||
|
topology.tri_to_patch[`${solidId}:${Number(triId)}`] = patchId;
|
||||||
|
}
|
||||||
|
regions.push({
|
||||||
|
id: `region:patch:${faceKey}:${pi}`,
|
||||||
|
surface_id: surfaceId,
|
||||||
|
boundary_ids: patchBoundaryIds.slice(),
|
||||||
|
source: {
|
||||||
|
type: 'surface-patch-region',
|
||||||
|
patch_id: patchId,
|
||||||
|
face_key: faceKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!emittedPatchCount) {
|
||||||
|
for (let li = 0; li < loops.length; li++) {
|
||||||
|
const boundaryId = `boundary:${faceKey}:${li}`;
|
||||||
|
const patchId = `surface-patch:${faceKey}:${li}`;
|
||||||
|
surface_patches.push({
|
||||||
|
id: patchId,
|
||||||
|
surface_id: surfaceId,
|
||||||
|
boundary_ids: [boundaryId],
|
||||||
|
source_region_id: facePrimarySourceRegion,
|
||||||
|
source_region_ids: faceSourceRegionIds.slice(),
|
||||||
|
solid_id: solidId,
|
||||||
|
face_id: faceId,
|
||||||
|
status: 'seed',
|
||||||
|
source: {
|
||||||
|
type: 'solid-face-loop',
|
||||||
|
face_key: faceKey,
|
||||||
|
loop_index: li,
|
||||||
|
feature_id: solid?.source?.feature_id || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
topology.surface_to_segments[surfaceId] = surfaceSegmentIds;
|
topology.surface_to_segments[surfaceId] = surfaceSegmentIds;
|
||||||
}
|
}
|
||||||
|
|
@ -2539,7 +2898,7 @@ function edgeKey(a, b) {
|
||||||
Number(preferredFrame.x_axis.y || 0),
|
Number(preferredFrame.x_axis.y || 0),
|
||||||
Number(preferredFrame.x_axis.z || 0)
|
Number(preferredFrame.x_axis.z || 0)
|
||||||
)
|
)
|
||||||
: new THREE.Vector3(1, 0, 0);
|
: (meta?.xAxis?.clone?.() || new THREE.Vector3(1, 0, 0));
|
||||||
if (xAxis.lengthSq() <= 1e-12) {
|
if (xAxis.lengthSq() <= 1e-12) {
|
||||||
xAxis.set(1, 0, 0);
|
xAxis.set(1, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,19 @@ function toKernelMesh(inst, meshData) {
|
||||||
const vertCount = Math.floor(positions.length / 3);
|
const vertCount = Math.floor(positions.length / 3);
|
||||||
const props = new Float32Array(vertCount * 3);
|
const props = new Float32Array(vertCount * 3);
|
||||||
props.set(positions);
|
props.set(positions);
|
||||||
return new inst.Mesh({
|
const rec = {
|
||||||
numProp: 3,
|
numProp: 3,
|
||||||
vertProperties: props,
|
vertProperties: props,
|
||||||
triVerts: Uint32Array.from(indices)
|
triVerts: Uint32Array.from(indices)
|
||||||
});
|
};
|
||||||
|
if (meshData?.mergeFromVert?.length) rec.mergeFromVert = Uint32Array.from(meshData.mergeFromVert);
|
||||||
|
if (meshData?.mergeToVert?.length) rec.mergeToVert = Uint32Array.from(meshData.mergeToVert);
|
||||||
|
if (meshData?.runIndex?.length) rec.runIndex = Uint32Array.from(meshData.runIndex);
|
||||||
|
if (meshData?.runOriginalID?.length) rec.runOriginalID = Uint32Array.from(meshData.runOriginalID);
|
||||||
|
if (meshData?.faceID?.length) rec.faceID = Uint32Array.from(meshData.faceID);
|
||||||
|
if (meshData?.halfedgeTangent?.length) rec.halfedgeTangent = Float32Array.from(meshData.halfedgeTangent);
|
||||||
|
if (meshData?.runTransform?.length) rec.runTransform = Float32Array.from(meshData.runTransform);
|
||||||
|
return new inst.Mesh(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromKernelMesh(mesh) {
|
function fromKernelMesh(mesh) {
|
||||||
|
|
@ -75,10 +83,18 @@ function fromKernelMesh(mesh) {
|
||||||
positions[dst + 1] = Number(verts[src + 1] || 0);
|
positions[dst + 1] = Number(verts[src + 1] || 0);
|
||||||
positions[dst + 2] = Number(verts[src + 2] || 0);
|
positions[dst + 2] = Number(verts[src + 2] || 0);
|
||||||
}
|
}
|
||||||
return {
|
const out = {
|
||||||
positions,
|
positions,
|
||||||
indices: Uint32Array.from(triVerts)
|
indices: Uint32Array.from(triVerts)
|
||||||
};
|
};
|
||||||
|
if (mesh?.mergeFromVert?.length) out.mergeFromVert = Uint32Array.from(mesh.mergeFromVert);
|
||||||
|
if (mesh?.mergeToVert?.length) out.mergeToVert = Uint32Array.from(mesh.mergeToVert);
|
||||||
|
if (mesh?.runIndex?.length) out.runIndex = Uint32Array.from(mesh.runIndex);
|
||||||
|
if (mesh?.runOriginalID?.length) out.runOriginalID = Uint32Array.from(mesh.runOriginalID);
|
||||||
|
if (mesh?.faceID?.length) out.faceID = Uint32Array.from(mesh.faceID);
|
||||||
|
if (mesh?.halfedgeTangent?.length) out.halfedgeTangent = Float32Array.from(mesh.halfedgeTangent);
|
||||||
|
if (mesh?.runTransform?.length) out.runTransform = Float32Array.from(mesh.runTransform);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function booleanMeshes(input, mode = 'add') {
|
async function booleanMeshes(input, mode = 'add') {
|
||||||
|
|
@ -94,11 +110,27 @@ async function booleanMeshes(input, mode = 'add') {
|
||||||
const targetMeshes = Array.isArray(options.targets) ? options.targets : null;
|
const targetMeshes = Array.isArray(options.targets) ? options.targets : null;
|
||||||
const toolMeshes = Array.isArray(options.tools) ? options.tools : null;
|
const toolMeshes = Array.isArray(options.tools) ? options.tools : null;
|
||||||
const manifolds = [];
|
const manifolds = [];
|
||||||
|
const runSourceSolidIdsByOriginal = {};
|
||||||
let result = null;
|
let result = null;
|
||||||
try {
|
try {
|
||||||
const toManifold = meshData => {
|
const toManifold = meshData => {
|
||||||
const kernelMesh = toKernelMesh(inst, meshData);
|
const kernelMesh = toKernelMesh(inst, meshData);
|
||||||
return kernelMesh ? new inst.Manifold(kernelMesh) : null;
|
const manifold = kernelMesh ? new inst.Manifold(kernelMesh) : null;
|
||||||
|
if (!manifold) return null;
|
||||||
|
try {
|
||||||
|
const infoMesh = manifold.getMesh?.();
|
||||||
|
const runOriginalID = Array.isArray(infoMesh?.runOriginalID)
|
||||||
|
? infoMesh.runOriginalID
|
||||||
|
: (infoMesh?.runOriginalID ? Array.from(infoMesh.runOriginalID) : []);
|
||||||
|
const sourceIds = Array.isArray(meshData?.source_solid_ids)
|
||||||
|
? meshData.source_solid_ids.map(id => String(id || '')).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const first = Number(runOriginalID?.[0]);
|
||||||
|
if (Number.isFinite(first) && sourceIds.length) {
|
||||||
|
runSourceSolidIdsByOriginal[String(first)] = sourceIds;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return manifold;
|
||||||
};
|
};
|
||||||
const combine = (list, kind = 'add') => {
|
const combine = (list, kind = 'add') => {
|
||||||
if (!Array.isArray(list) || !list.length) return null;
|
if (!Array.isArray(list) || !list.length) return null;
|
||||||
|
|
@ -137,7 +169,12 @@ async function booleanMeshes(input, mode = 'add') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const mesh = result?.getMesh?.();
|
const mesh = result?.getMesh?.();
|
||||||
return mesh ? { mesh: fromKernelMesh(mesh) } : null;
|
if (!mesh) return null;
|
||||||
|
const outMesh = fromKernelMesh(mesh);
|
||||||
|
if (Object.keys(runSourceSolidIdsByOriginal).length) {
|
||||||
|
outMesh.run_source_solid_ids = runSourceSolidIdsByOriginal;
|
||||||
|
}
|
||||||
|
return { mesh: outMesh };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('void.solid.kernel boolean failed', error);
|
console.warn('void.solid.kernel boolean failed', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
};
|
};
|
||||||
body.extrude = { depth, direction, symmetric };
|
body.extrude = { depth, direction, symmetric };
|
||||||
if (meshWorld) {
|
if (meshWorld) {
|
||||||
|
meshWorld.source_solid_ids = [id];
|
||||||
meshCache.set(id, meshWorld);
|
meshCache.set(id, meshWorld);
|
||||||
createdBodyIds.push(id);
|
createdBodyIds.push(id);
|
||||||
}
|
}
|
||||||
|
|
@ -358,8 +359,19 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
: [];
|
: [];
|
||||||
const createdSolids = createdBodyIds.map(id => solids.find(s => s?.id === id)).filter(Boolean);
|
const createdSolids = createdBodyIds.map(id => solids.find(s => s?.id === id)).filter(Boolean);
|
||||||
const targetSolids = targetIds.map(id => solids.find(s => s?.id === id)).filter(Boolean);
|
const targetSolids = targetIds.map(id => solids.find(s => s?.id === id)).filter(Boolean);
|
||||||
const toolMeshes = createdSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
const withSourceIds = (mesh, sid) => {
|
||||||
const targetMeshes = targetSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
if (!mesh) return null;
|
||||||
|
const source = Array.isArray(mesh.source_solid_ids) && mesh.source_solid_ids.length
|
||||||
|
? mesh.source_solid_ids
|
||||||
|
: [sid];
|
||||||
|
return { ...mesh, source_solid_ids: source.map(id => String(id || '')).filter(Boolean) };
|
||||||
|
};
|
||||||
|
const toolMeshes = createdSolids
|
||||||
|
.map(s => withSourceIds(meshCache.get(s.id), s.id))
|
||||||
|
.filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
||||||
|
const targetMeshes = targetSolids
|
||||||
|
.map(s => withSourceIds(meshCache.get(s.id), s.id))
|
||||||
|
.filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
||||||
let merge = null;
|
let merge = null;
|
||||||
if (operation === 'add') {
|
if (operation === 'add') {
|
||||||
const meshes = [...targetMeshes, ...toolMeshes];
|
const meshes = [...targetMeshes, ...toolMeshes];
|
||||||
|
|
@ -371,6 +383,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
}
|
}
|
||||||
if (merge?.mesh?.positions?.length && merge?.mesh?.indices?.length) {
|
if (merge?.mesh?.positions?.length && merge?.mesh?.indices?.length) {
|
||||||
const consumed = new Set([...targetSolids.map(s => s.id), ...createdSolids.map(s => s.id)]);
|
const consumed = new Set([...targetSolids.map(s => s.id), ...createdSolids.map(s => s.id)]);
|
||||||
|
const consumedSourceSolidIds = Array.from(consumed);
|
||||||
const sketchIds = new Set();
|
const sketchIds = new Set();
|
||||||
for (const solid of [...targetSolids, ...createdSolids]) {
|
for (const solid of [...targetSolids, ...createdSolids]) {
|
||||||
for (const sid of getSketchIdsForSolid(solid)) {
|
for (const sid of getSketchIdsForSolid(solid)) {
|
||||||
|
|
@ -411,6 +424,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
},
|
},
|
||||||
status: 'manifold_extrude_boolean_ready'
|
status: 'manifold_extrude_boolean_ready'
|
||||||
};
|
};
|
||||||
|
merge.mesh.source_solid_ids = consumedSourceSolidIds;
|
||||||
meshCache.set(id, merge.mesh);
|
meshCache.set(id, merge.mesh);
|
||||||
solids.push(body);
|
solids.push(body);
|
||||||
}
|
}
|
||||||
|
|
@ -438,8 +452,19 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
} else if (targetSolids.length < 2) {
|
} else if (targetSolids.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const targetMeshes = targetSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
const withSourceIds = (mesh, sid) => {
|
||||||
const toolMeshes = toolSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
if (!mesh) return null;
|
||||||
|
const source = Array.isArray(mesh.source_solid_ids) && mesh.source_solid_ids.length
|
||||||
|
? mesh.source_solid_ids
|
||||||
|
: [sid];
|
||||||
|
return { ...mesh, source_solid_ids: source.map(id => String(id || '')).filter(Boolean) };
|
||||||
|
};
|
||||||
|
const targetMeshes = targetSolids
|
||||||
|
.map(s => withSourceIds(meshCache.get(s.id), s.id))
|
||||||
|
.filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
||||||
|
const toolMeshes = toolSolids
|
||||||
|
.map(s => withSourceIds(meshCache.get(s.id), s.id))
|
||||||
|
.filter(mesh => mesh?.positions?.length && mesh?.indices?.length);
|
||||||
if (mode === 'subtract') {
|
if (mode === 'subtract') {
|
||||||
if (!targetMeshes.length || !toolMeshes.length) continue;
|
if (!targetMeshes.length || !toolMeshes.length) continue;
|
||||||
} else if (targetMeshes.length < 2) {
|
} else if (targetMeshes.length < 2) {
|
||||||
|
|
@ -489,6 +514,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) {
|
||||||
},
|
},
|
||||||
status: 'manifold_boolean_ready'
|
status: 'manifold_boolean_ready'
|
||||||
};
|
};
|
||||||
|
result.mesh.source_solid_ids = selectedIds.slice();
|
||||||
meshCache.set(id, result.mesh);
|
meshCache.set(id, result.mesh);
|
||||||
solids.push(body);
|
solids.push(body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,26 @@ function serializeMeshCache(meshCache) {
|
||||||
: new Uint32Array(mesh.indices);
|
: new Uint32Array(mesh.indices);
|
||||||
meshes.push({ id, positions, indices });
|
meshes.push({ id, positions, indices });
|
||||||
transfer.push(positions.buffer, indices.buffer);
|
transfer.push(positions.buffer, indices.buffer);
|
||||||
|
const optionalUint = ['mergeFromVert', 'mergeToVert', 'runIndex', 'runOriginalID', 'faceID'];
|
||||||
|
for (const key of optionalUint) {
|
||||||
|
if (!mesh?.[key]?.length) continue;
|
||||||
|
const arr = mesh[key] instanceof Uint32Array ? mesh[key] : new Uint32Array(mesh[key]);
|
||||||
|
meshes[meshes.length - 1][key] = arr;
|
||||||
|
transfer.push(arr.buffer);
|
||||||
|
}
|
||||||
|
const optionalFloat = ['halfedgeTangent', 'runTransform'];
|
||||||
|
for (const key of optionalFloat) {
|
||||||
|
if (!mesh?.[key]?.length) continue;
|
||||||
|
const arr = mesh[key] instanceof Float32Array ? mesh[key] : new Float32Array(mesh[key]);
|
||||||
|
meshes[meshes.length - 1][key] = arr;
|
||||||
|
transfer.push(arr.buffer);
|
||||||
|
}
|
||||||
|
if (mesh?.run_source_solid_ids && typeof mesh.run_source_solid_ids === 'object') {
|
||||||
|
meshes[meshes.length - 1].run_source_solid_ids = mesh.run_source_solid_ids;
|
||||||
|
}
|
||||||
|
if (Array.isArray(mesh?.source_solid_ids)) {
|
||||||
|
meshes[meshes.length - 1].source_solid_ids = mesh.source_solid_ids;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { meshes, transfer };
|
return { meshes, transfer };
|
||||||
}
|
}
|
||||||
|
|
@ -56,4 +76,3 @@ self.onmessage = async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue