import { preset, subdivide, decimate } from "./topology.js"; import { hydrate, snapshot, ensureDocument, splitPiece, combinePieces, } from "./document.js"; function bounds(mesh, piece) { const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity]; for ( let v = piece.vertexStart; v < piece.vertexStart + piece.vertexCount; v++ ) for (let k = 0; k < 3; k++) { min[k] = Math.min(min[k], mesh.positions[v * 3 + k]); max[k] = Math.max(max[k], mesh.positions[v * 3 + k]); } return { min, max }; } // Runs on owned document copies in a disposable worker. Cancel terminates it. export function operate(data, action, activeId, options = {}) { const mesh = hydrate(data); const active = mesh.pieces.find((p) => p.id === activeId); let items = mesh.pieces.map((piece) => ({ mesh, piece })); let note = ""; if (action === "add") { if (items.length >= 16) throw new Error("Keep up to 16 pieces in a project."); const added = ensureDocument(preset(options.preset)); const piece = added.pieces[0]; Object.assign(piece, { id: options.id, name: options.preset[0].toUpperCase() + options.preset.slice(1), color: options.color, }); const boxes = mesh.pieces.map((p) => bounds(mesh, p)); const shape = bounds(added, piece); // Compact deterministic placement; keep new clay separate and within bounds. const candidates = []; if (active) { const b = bounds(mesh, active); candidates.push([ b.max[0] - shape.min[0] + 0.15, (b.min[1] + b.max[1]) / 2, (b.min[2] + b.max[2]) / 2, ]); } for (let y = 0; y <= 6; y += 2.5) for (let x = 0; x <= 6; x += 2.5) for (const sx of x ? [1, -1] : [1]) for (const sy of y ? [1, -1] : [1]) candidates.push([x * sx, y * sy, 0]); const offset = candidates.find((o) => { const lo = shape.min.map((x, k) => x + o[k]), hi = shape.max.map((x, k) => x + o[k]); return ( lo.every((x) => x >= -8) && hi.every((x) => x <= 8) && boxes.every((b) => lo.some((x, k) => x > b.max[k] + 0.1 || hi[k] < b.min[k] - 0.1), ) ); }); if (!offset) throw new Error("No room for another piece. Delete a piece first."); for (let i = 0; i < added.positions.length; i++) added.positions[i] += offset[i % 3]; piece.symmetryX = Math.fround(offset[0]); items.push({ mesh: added, piece }); activeId = piece.id; } else { if (!active) throw new Error("Select a piece of clay first."); if (action === "delete") items = items.filter((i) => i.piece.id !== activeId); else if (action === "subdivide" || action === "decimate") { const source = splitPiece(mesh, active); const result = action === "subdivide" ? subdivide(source) : decimate(source); result.level = mesh.level; const piece = { ...active, vertexStart: 0, indexStart: 0, vertexCount: result.positions.length / 3, indexCount: result.indices.length, }; result.pieces = [piece]; items = items.map((i) => i.piece.id === activeId ? { mesh: result, piece } : i, ); if (action === "decimate") note = `${Math.round((1 - result.indices.length / source.indices.length) * 100)}% fewer triangles`; } else throw new Error("Unknown clay operation."); } const combined = combinePieces(items); combined.level = mesh.level; return { document: snapshot(combined), activeId: combined.pieces.some((p) => p.id === activeId) ? activeId : (combined.pieces[0]?.id ?? null), note, }; } if (typeof self !== "undefined" && typeof document === "undefined") self.onmessage = ({ data }) => { try { const result = operate( data.document, data.action, data.activeId, data.options, ); self.postMessage(result, [ result.document.positions.buffer, result.document.indices.buffer, result.document.masks.buffer, ]); } catch (error) { self.postMessage({ error: error.message }); } };