|
33 | 33 | // derived from the real plot time (≈ x45–x60) |
34 | 34 | var INK = '#e8351f'; |
35 | 35 | var cur, len = 1, drawn = 0, painted = 0, playing = true, speed = 1, mmPerUnit = 1, totalMin = 1; |
| 36 | + var termBody = $('pfTermBody'); // command journal (homepage only) |
| 37 | + var polys = [], strokes = [], termIdx = -1; // parsed geometry + journal cursor |
36 | 38 | var vb = { x: 0, y: 0, w: 1, h: 1 }; |
37 | 39 | var tf = { s: 1, ox: 0, oy: 0, dpr: 1, ready: false }; |
38 | 40 | var prevPt = null; |
|
139 | 141 | len = ppath.getTotalLength(); if (!len || !isFinite(len)) len = 1; |
140 | 142 | var md = Math.max(vb.w, vb.h); |
141 | 143 | mmPerUnit = 420 / md; // longest side ≈ 420mm |
| 144 | + // parse the raw path once: polylines for fast full-draw, stroke table |
| 145 | + // (start/end/len/cumulative draw length) for the command journal |
| 146 | + polys = []; strokes = []; termIdx = -1; |
| 147 | + if (termBody) termBody.textContent = ''; |
| 148 | + (function () { |
| 149 | + var re = /([ML])\s*(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g, m, curP = null; |
| 150 | + while ((m = re.exec(cur.d)) !== null) { |
| 151 | + var x = +m[2], y = +m[3]; |
| 152 | + if (m[1] === 'M') { curP = [[x, y]]; polys.push(curP); } |
| 153 | + else if (curP) curP.push([x, y]); |
| 154 | + } |
| 155 | + var cum = 0; |
| 156 | + for (var i = 0; i < polys.length; i++) { |
| 157 | + var pl = polys[i], L = 0; |
| 158 | + for (var j = 0; j < pl.length - 1; j++) |
| 159 | + L += Math.hypot(pl[j+1][0]-pl[j][0], pl[j+1][1]-pl[j][1]); |
| 160 | + cum += L; |
| 161 | + strokes.push({ sx: pl[0][0], sy: pl[0][1], len: L, cum: cum }); |
| 162 | + } |
| 163 | + })(); |
142 | 164 | var st = pathStats(cur.d); |
143 | 165 | totalMin = (st.draw * mmPerUnit) / FEED |
144 | 166 | + (st.travel * mmPerUnit) / TRAVEL_FEED |
|
161 | 183 | if (!tf.ready) resetCanvas(); // self-heal if the bed wasn't laid out yet |
162 | 184 | var p = drawn / len; |
163 | 185 | paintTo(drawn); |
| 186 | + updateJournal(); |
164 | 187 | if (pbar) pbar.style.width = (p * 100) + '%'; |
165 | 188 | if (pct) pct.textContent = Math.round(p * 100); |
166 | 189 | if (ink) ink.textContent = (drawn * mmPerUnit / 1000).toFixed(2); |
|
176 | 199 | } |
177 | 200 | } |
178 | 201 |
|
| 202 | + // Draw every stroke directly from the parsed polylines in one canvas pass — |
| 203 | + // used by Skip and end-state refits, where sampling the whole path with |
| 204 | + // getPointAtLength would stall the frame for ~a second. |
| 205 | + function fastPaintAll() { |
| 206 | + if (!tf.ready) resetCanvas(); |
| 207 | + if (!tf.ready) return; |
| 208 | + ctx.beginPath(); |
| 209 | + for (var i = 0; i < polys.length; i++) { |
| 210 | + var pl = polys[i]; |
| 211 | + ctx.moveTo(mapX(pl[0][0]), mapY(pl[0][1])); |
| 212 | + for (var j = 1; j < pl.length; j++) ctx.lineTo(mapX(pl[j][0]), mapY(pl[j][1])); |
| 213 | + } |
| 214 | + ctx.stroke(); |
| 215 | + painted = len; |
| 216 | + var lastP = polys.length ? polys[polys.length-1][polys[polys.length-1].length-1] : null; |
| 217 | + prevPt = lastP ? { x: lastP[0], y: lastP[1] } : null; |
| 218 | + } |
| 219 | + |
| 220 | + // Command journal — emit pyaxidraw-style lines for the strokes just drawn. |
| 221 | + function pad4(n) { return String(n).padStart(4, '0'); } |
| 222 | + function updateJournal() { |
| 223 | + if (!termBody || !strokes.length) return; |
| 224 | + var lo = 0, hi = strokes.length - 1, idx = 0; // first stroke with cum >= drawn |
| 225 | + while (lo <= hi) { var mid = (lo + hi) >> 1; |
| 226 | + if (strokes[mid].cum < drawn) { lo = mid + 1; } else { idx = mid; hi = mid - 1; } } |
| 227 | + if (drawn >= len) idx = strokes.length - 1; |
| 228 | + if (idx === termIdx) return; |
| 229 | + termIdx = idx; |
| 230 | + var out = [], from = Math.max(0, idx - 2); |
| 231 | + for (var k = from; k <= idx; k++) { |
| 232 | + var st2 = strokes[k]; |
| 233 | + out.push('ad.penup() z=+1'); |
| 234 | + out.push('ad.moveto(' + (st2.sx * mmPerUnit).toFixed(2) + ', ' + (st2.sy * mmPerUnit).toFixed(2) + ') travel'); |
| 235 | + out.push('ad.pendown() z=-1 · nib 0.30 mm'); |
| 236 | + out.push('ad.lineto(…) stroke ' + pad4(k + 1) + '/' + strokes.length + ' · ' + (st2.len * mmPerUnit).toFixed(1) + ' mm'); |
| 237 | + } |
| 238 | + out.push(drawn >= len ? 'ad.penup() z=+1 · job complete' : '_'); |
| 239 | + termBody.textContent = out.join('\n'); |
| 240 | + } |
| 241 | + |
179 | 242 | // Re-fit + repaint the current progress when the bed changes size. |
180 | | - function refit() { resetCanvas(); paintTo(drawn); } |
| 243 | + function refit() { resetCanvas(); if (drawn >= len) fastPaintAll(); else paintTo(drawn); } |
181 | 244 | window.addEventListener('resize', refit); |
182 | 245 |
|
183 | 246 | function restart() { |
|
219 | 282 | } |
220 | 283 | if (playBtn) playBtn.onclick = toggle; |
221 | 284 | if ($('restart')) $('restart').onclick = restart; |
222 | | - if ($('skip')) $('skip').onclick = function () { drawn = len; playing = false; render(); if (replay) replay.classList.add('show'); if (playBtn) playBtn.textContent = 'Replay'; }; |
| 285 | + if ($('skip')) $('skip').onclick = function () { drawn = len; playing = false; fastPaintAll(); render(); if (replay) replay.classList.add('show'); if (playBtn) playBtn.textContent = 'Replay'; }; |
223 | 286 | if ($('speed')) $('speed').addEventListener('click', function (e) { |
224 | 287 | var b = e.target.closest('button'); if (!b) return; |
225 | 288 | [].forEach.call(e.currentTarget.children, function (x) { x.classList.remove('on'); }); |
|
0 commit comments