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';
  }
})();