Spaces:
Sleeping
Sleeping
File size: 7,442 Bytes
9032518 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | // assembly_seq.js — workshop-style DISASSEMBLY / ASSEMBLY sequence for the
// chat_cad car viewer ("human labor doing assembly").
//
// Uses the window.__cadGesture hook (THREE/scene/camera + getParts()). On first
// run each removable part mesh is split into its CONNECTED COMPONENTS (each
// wheel, each lamp, each glass pane becomes its own mesh), then the parts are
// removed ONE AT A TIME in a mechanic's order — wheels, rims, grille, lamps,
// glass, underbody — each arcing out and landing in a labelled row on the
// floor beside the car. Assembly runs the same sequence in reverse.
//
// Public API: window.__toggleDisassembly() -> 'disassembling'|'assembling'|'busy'|null
(function () {
let state = 'assembled'; // assembled | busy | disassembled
let plan = null; // [{mesh, offset:Vector3, lift}]
let THREE = null, H = null;
const ORDER = ['wheel_tyre', 'wheel_rim', 'grille', 'headlight', 'taillight',
'glass', 'underbody'];
const orderOf = (name) => {
for (let i = 0; i < ORDER.length; i++) if (name.includes(ORDER[i])) return i;
return ORDER.length;
};
const isBody = (name) => name.includes('body') && !name.includes('under');
// ---- split an indexed mesh into connected components (union-find) ----
function splitComponents(mesh) {
const g = mesh.geometry;
const idx = g.getIndex();
if (!idx) return [mesh];
const ia = idx.array;
// union-find over the vertex ids that this part actually uses
const parent = new Map();
const find = (a) => { let r = a; while (parent.get(r) !== r) r = parent.get(r);
while (parent.get(a) !== r) { const nx = parent.get(a); parent.set(a, r); a = nx; } return r; };
const uni = (a, b) => { const ra = find(a), rb = find(b); if (ra !== rb) parent.set(ra, rb); };
for (let i = 0; i < ia.length; i++) if (!parent.has(ia[i])) parent.set(ia[i], ia[i]);
for (let i = 0; i < ia.length; i += 3) { uni(ia[i], ia[i + 1]); uni(ia[i], ia[i + 2]); }
// bucket faces by component root
const buckets = new Map();
for (let i = 0; i < ia.length; i += 3) {
const r = find(ia[i]);
let b = buckets.get(r); if (!b) { b = []; buckets.set(r, b); }
b.push(ia[i], ia[i + 1], ia[i + 2]);
}
if (buckets.size <= 1) return [mesh];
const out = [];
let k = 0;
for (const faces of buckets.values()) {
// skip debris fragments (< 30 faces) — keep them attached to component 0
if (faces.length / 3 < 30 && buckets.size > 2) { if (out[0]) { /* merge later */ } }
const ng = new THREE.BufferGeometry();
ng.setAttribute('position', g.getAttribute('position'));
if (g.getAttribute('normal')) ng.setAttribute('normal', g.getAttribute('normal'));
ng.setIndex(faces);
if (!g.getAttribute('normal')) ng.computeVertexNormals();
const nm = new THREE.Mesh(ng, mesh.material);
nm.name = mesh.name + '_' + (k++);
out.push(nm);
}
const parent3 = mesh.parent;
out.forEach(m => parent3.add(m));
parent3.remove(mesh);
return out;
}
function buildPlan() {
const parts = H.getParts();
if (!parts || parts.length < 2) return null;
// split every removable part into components (idempotent: split parts have '_N' suffix)
let all = [];
for (const m of parts) {
if (isBody(m.name) || /_\d+$/.test(m.name)) { all.push(m); continue; }
all = all.concat(splitComponents(m));
}
const movers = all.filter(m => !isBody(m.name));
if (!movers.length) return null;
// assembly bounds (from everything)
const box = new THREE.Box3(); all.forEach(m => box.expandByObject(m));
const size = box.getSize(new THREE.Vector3());
const ground = box.min.z;
const sideY = box.max.y + size.y * 0.55; // lay-down row beside the car
// mechanic's order, then front-to-back within the same class
const info = movers.map(m => {
const b = new THREE.Box3().setFromObject(m);
return { mesh: m, c: b.getCenter(new THREE.Vector3()), b };
});
info.sort((p, q) => (orderOf(p.mesh.name) - orderOf(q.mesh.name)) || (p.c.x - q.c.x));
// floor slots: two rows along X beside the car
const n = info.length;
const perRow = Math.ceil(n / 2);
const stepX = (size.x * 1.15) / Math.max(perRow - 1, 1);
plan = info.map((p, i) => {
const row = Math.floor(i / perRow), col = i % perRow;
const slot = new THREE.Vector3(
box.min.x + col * stepX,
sideY + row * size.y * 0.45,
ground + (p.c.z - p.b.min.z)); // rest ON the floor
return { mesh: p.mesh,
offset: slot.sub(p.c), // translation that puts it in its slot
lift: size.z * (0.5 + 0.25 * Math.random()) };
});
return plan;
}
const easeInOut = t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
// rAF with a setTimeout watchdog: embedded/background tabs throttle rAF to
// zero, which would freeze the sequence forever. Whichever fires first wins.
function tick(cb) {
let fired = false;
const raf = requestAnimationFrame(t => { if (!fired) { fired = true; clearTimeout(to); cb(t); } });
const to = setTimeout(() => { if (!fired) { fired = true; cancelAnimationFrame(raf); cb(performance.now()); } }, 34);
}
function animateSeq(items, dir, done) { // dir +1 = remove, -1 = install
const DUR = 650, GAP = 130; // ms per part / stagger
const t0 = performance.now();
const list = dir > 0 ? items : [...items].reverse();
function frame(now) {
let busy = false;
for (let i = 0; i < list.length; i++) {
const p = list[i];
const lt = (now - t0 - i * (DUR * 0.55 + GAP)) / DUR; // overlapped stagger
if (lt < 0) { busy = true; continue; }
const t = Math.min(1, lt);
const e = easeInOut(t);
const f = dir > 0 ? e : 1 - e;
p.mesh.position.set(p.offset.x * f, p.offset.y * f,
p.offset.z * f + p.lift * Math.sin(Math.PI * f));
if (t < 1) busy = true;
}
if (busy) tick(frame);
else { list.forEach(p => { const f = dir > 0 ? 1 : 0;
p.mesh.position.set(p.offset.x * f, p.offset.y * f, p.offset.z * f); });
done(); }
}
tick(frame);
}
window.__toggleDisassembly = async function () {
H = window.__cadGesture;
if (!H) { alert('Viewer not ready.'); return null; }
THREE = H.THREE;
if (state === 'busy') return 'busy';
if (state === 'assembled') {
if (!plan) plan = buildPlan();
if (!plan && H.showParts) { // auto-load the labelled parts split
state = 'busy'; updateBtn();
await H.showParts();
state = 'assembled';
plan = buildPlan();
}
if (!plan) { alert('No multi-part car loaded yet — generate a car first (e.g. "draw me a car").'); updateBtn(); return null; }
state = 'busy';
animateSeq(plan, +1, () => { state = 'disassembled'; updateBtn(); });
} else {
state = 'busy';
animateSeq(plan, -1, () => { state = 'assembled'; updateBtn(); });
}
updateBtn();
return state;
};
function updateBtn() {
const b = document.getElementById('disasmBtn');
if (!b) return;
b.textContent = state === 'busy' ? 'Working…'
: state === 'disassembled' ? 'Assemble' : 'Disassemble';
}
})();
|