void update boundary provenance

This commit is contained in:
Stewart Allen 2026-03-02 18:10:22 -05:00
commit 6948e7ef25
5 changed files with 481 additions and 35 deletions

View file

@ -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

View file

@ -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);
} }

View file

@ -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;

View file

@ -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);
} }

View file

@ -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) => {
}); });
} }
}; };