diff --git a/docs/void/plan-face-provenance.md b/docs/void/plan-face-provenance.md index 794c8cab..0ad94e62 100644 --- a/docs/void/plan-face-provenance.md +++ b/docs/void/plan-face-provenance.md @@ -15,16 +15,21 @@ Completed: 5. Segment/surface/region ID labels (overlay text). 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. +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: -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: -1. Add first `surface_patch_id` selection plumbing (read-only pass-through). -2. Thread Manifold relation metadata through kernel adapters. -3. Start mixed-face partition scaffold from seeded `surface_patches`. +1. Add explicit `surface_patch_id` in hit/canonical selection entities. +2. Bind extrude-profile hover/select directly to patch/source-region maps. +3. Add regression fixtures for boolean unions/subtracts with mixed curved + planar outputs. ## Decisions diff --git a/src/void/api/solids.js b/src/void/api/solids.js index cb4b9c0d..de5c495e 100644 --- a/src/void/api/solids.js +++ b/src/void/api/solids.js @@ -102,7 +102,24 @@ function createSolidsApi(getApi) { ? rec.indices : new Uint32Array(rec.indices || []); 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; } @@ -360,6 +377,7 @@ function edgeKey(a, b) { groups.set(groupId, { id: groupId, + tris: tris.slice(), geometry: faceGeom, center, normal, @@ -637,6 +655,209 @@ function edgeKey(a, b) { 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 { _rebuildTimer: null, _rebuilding: false, @@ -935,7 +1156,18 @@ function edgeKey(a, b) { this._debugGroup.add(lines); } } 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 : []; if (!segmentIds.length) continue; const positions = []; @@ -1127,6 +1359,7 @@ function edgeKey(a, b) { patch_to_tris: {}, tri_to_patch: {} }; + const solidsById = new Map((this.list() || []).map(item => [String(item?.id || ''), item])); const getPointId = (p, role = 'boundary-vertex') => { const key = quantPointKey(p); @@ -1146,7 +1379,7 @@ function edgeKey(a, b) { }; 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) ? solid.source.profile_keys.map(key => String(key || '')).filter(Boolean) : []; @@ -1190,6 +1423,31 @@ function edgeKey(a, b) { }); 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 = []; for (let li = 0; li < loops.length; li++) { const loop = loops[li]; @@ -1260,24 +1518,125 @@ function edgeKey(a, b) { loop_index: li } }); - const patchId = `surface-patch:${faceKey}:${li}`; - const sourceRegionIds = sourceProfileKeys.slice(); - surface_patches.push({ - id: patchId, - surface_id: surfaceId, - boundary_ids: [boundaryId], - source_region_id: primarySourceRegion, - source_region_ids: sourceRegionIds, - 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 + } + + let emittedPatchCount = 0; + if (facePatches.length) { + for (let pi = 0; pi < facePatches.length; pi++) { + const patch = facePatches[pi]; + const patchId = `surface-patch:${faceKey}:${pi}`; + const patchBoundaryIds = []; + const sourceRegionIds = normalizeRegionIds(patch?.source_region_ids, faceSourceRegionIds); + const patchPrimarySourceRegion = sourceRegionIds.length === 1 + ? sourceRegionIds[0] + : facePrimarySourceRegion; + const patchLoops = Array.isArray(patch?.loops) ? patch.loops : []; + for (let pli = 0; pli < patchLoops.length; pli++) { + const loop = patchLoops[pli]; + 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; } @@ -2539,7 +2898,7 @@ function edgeKey(a, b) { Number(preferredFrame.x_axis.y || 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) { xAxis.set(1, 0, 0); } diff --git a/src/void/solid/kernel.js b/src/void/solid/kernel.js index c2c162a4..a046f814 100644 --- a/src/void/solid/kernel.js +++ b/src/void/solid/kernel.js @@ -54,11 +54,19 @@ function toKernelMesh(inst, meshData) { const vertCount = Math.floor(positions.length / 3); const props = new Float32Array(vertCount * 3); props.set(positions); - return new inst.Mesh({ + const rec = { numProp: 3, vertProperties: props, 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) { @@ -75,10 +83,18 @@ function fromKernelMesh(mesh) { positions[dst + 1] = Number(verts[src + 1] || 0); positions[dst + 2] = Number(verts[src + 2] || 0); } - return { + const out = { positions, 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') { @@ -94,11 +110,27 @@ async function booleanMeshes(input, mode = 'add') { const targetMeshes = Array.isArray(options.targets) ? options.targets : null; const toolMeshes = Array.isArray(options.tools) ? options.tools : null; const manifolds = []; + const runSourceSolidIdsByOriginal = {}; let result = null; try { const toManifold = 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') => { if (!Array.isArray(list) || !list.length) return null; @@ -137,7 +169,12 @@ async function booleanMeshes(input, mode = 'add') { } } 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) { console.warn('void.solid.kernel boolean failed', error); return null; diff --git a/src/void/solid/rebuild.js b/src/void/solid/rebuild.js index 09d9f203..6fdb8fa8 100644 --- a/src/void/solid/rebuild.js +++ b/src/void/solid/rebuild.js @@ -343,6 +343,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) { }; body.extrude = { depth, direction, symmetric }; if (meshWorld) { + meshWorld.source_solid_ids = [id]; meshCache.set(id, meshWorld); 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 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 targetMeshes = targetSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length); + const withSourceIds = (mesh, sid) => { + 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; if (operation === 'add') { const meshes = [...targetMeshes, ...toolMeshes]; @@ -371,6 +383,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) { } if (merge?.mesh?.positions?.length && merge?.mesh?.indices?.length) { const consumed = new Set([...targetSolids.map(s => s.id), ...createdSolids.map(s => s.id)]); + const consumedSourceSolidIds = Array.from(consumed); const sketchIds = new Set(); for (const solid of [...targetSolids, ...createdSolids]) { for (const sid of getSketchIdsForSolid(solid)) { @@ -411,6 +424,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) { }, status: 'manifold_extrude_boolean_ready' }; + merge.mesh.source_solid_ids = consumedSourceSolidIds; meshCache.set(id, merge.mesh); solids.push(body); } @@ -438,8 +452,19 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) { } else if (targetSolids.length < 2) { continue; } - const targetMeshes = targetSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length); - const toolMeshes = toolSolids.map(s => meshCache.get(s.id)).filter(mesh => mesh?.positions?.length && mesh?.indices?.length); + const withSourceIds = (mesh, sid) => { + 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 (!targetMeshes.length || !toolMeshes.length) continue; } else if (targetMeshes.length < 2) { @@ -489,6 +514,7 @@ async function rebuildGeneratedSolidsFromSnapshot(snapshot, options = {}) { }, status: 'manifold_boolean_ready' }; + result.mesh.source_solid_ids = selectedIds.slice(); meshCache.set(id, result.mesh); solids.push(body); } diff --git a/src/void/worker/solids_worker.js b/src/void/worker/solids_worker.js index f617a554..4d9b976c 100644 --- a/src/void/worker/solids_worker.js +++ b/src/void/worker/solids_worker.js @@ -24,6 +24,26 @@ function serializeMeshCache(meshCache) { : new Uint32Array(mesh.indices); meshes.push({ id, positions, indices }); 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 }; } @@ -56,4 +76,3 @@ self.onmessage = async (event) => { }); } }; -