|
225 | 225 | let robotInchesXY = getCurvePoint(linePercent, [_startPoint, ...currentLine.controlPoints, currentLine.endPoint]); |
226 | 226 | robotXY = { x: x(robotInchesXY.x), y: y(robotInchesXY.y) }; |
227 | 227 |
|
| 228 | + // If this line is a wait, compute heading from the previous non-wait line's end heading |
| 229 | + if ((currentLine as any).waitMs !== undefined) { |
| 230 | + let prevIdx = currentLineIdx - 1; |
| 231 | + while (prevIdx >= 0 && (lines[prevIdx] as any).waitMs !== undefined) { |
| 232 | + prevIdx -= 1; |
| 233 | + } |
| 234 | +
|
| 235 | + if (prevIdx >= 0) { |
| 236 | + const prevLine = lines[prevIdx]; |
| 237 | + const prevStart = prevIdx === 0 ? startPoint : lines[prevIdx - 1].endPoint; |
| 238 | + // determine heading at end of prevLine (t = 1) |
| 239 | + switch (prevLine.endPoint.heading) { |
| 240 | + case "linear": |
| 241 | + robotHeading = -shortestRotation( |
| 242 | + prevLine.endPoint.startDeg, |
| 243 | + prevLine.endPoint.endDeg, |
| 244 | + 1 |
| 245 | + ); |
| 246 | + break; |
| 247 | + case "constant": |
| 248 | + robotHeading = -prevLine.endPoint.degrees; |
| 249 | + break; |
| 250 | + case "tangential": { |
| 251 | + const pBefore = getCurvePoint(0.99, [prevStart, ...prevLine.controlPoints, prevLine.endPoint]); |
| 252 | + const pEnd = getCurvePoint(1, [prevStart, ...prevLine.controlPoints, prevLine.endPoint]); |
| 253 | + const pBeforePx = { x: x(pBefore.x), y: y(pBefore.y) }; |
| 254 | + const pEndPx = { x: x(pEnd.x), y: y(pEnd.y) }; |
| 255 | + const dx = pEndPx.x - pBeforePx.x; |
| 256 | + const dy = pEndPx.y - pBeforePx.y; |
| 257 | + if (dx !== 0 || dy !== 0) { |
| 258 | + robotHeading = radiansToDegrees(Math.atan2(dy, dx)); |
| 259 | + } |
| 260 | + break; |
| 261 | + } |
| 262 | + } |
| 263 | + } else { |
| 264 | + // no previous line, fall back to startPoint heading if available |
| 265 | + if (startPoint.heading === "constant") robotHeading = -((startPoint as any).degrees ?? 0); |
| 266 | + else if (startPoint.heading === "linear") robotHeading = -shortestRotation(startPoint.startDeg, startPoint.endDeg, 1); |
| 267 | + } |
| 268 | + } else { |
228 | 269 | switch (currentLine.endPoint.heading) { |
229 | 270 | case "linear": |
230 | 271 | robotHeading = -shortestRotation( |
|
254 | 295 |
|
255 | 296 | break; |
256 | 297 | } |
| 298 | + } |
257 | 299 | } |
258 | 300 |
|
259 | 301 | $: (() => { |
|
276 | 318 | two.update(); |
277 | 319 | })(); |
278 | 320 |
|
279 | | - let playing = false; |
| 321 | +let playing = false; |
280 | 322 |
|
281 | | - let animationFrame: number; |
282 | | - let startTime: number | null = null; |
283 | | - let previousTime: number | null = null; |
| 323 | +let animationFrame: number; |
| 324 | +let startTime: number | null = null; |
| 325 | +let previousTime: number | null = null; |
284 | 326 |
|
285 | | - function animate(timestamp: number) { |
286 | | - if (!startTime) { |
287 | | - startTime = timestamp; |
288 | | - } |
| 327 | +let waiting = false; |
| 328 | +let waitTimerRemaining = 0; |
| 329 | +let lastHandledLineIdx = -1; |
| 330 | +let waitEndTimestamp: number | null = null; |
| 331 | +let prevPercent = 0; |
| 332 | +let cycleTimerRunning = false; |
| 333 | +// Playback elapsed timer (ms) — resets when `play()` starts |
| 334 | +let playElapsedMs = 0; |
289 | 335 |
|
290 | | - if (previousTime !== null) { |
291 | | - const deltaTime = timestamp - previousTime; |
| 336 | +function normalizeWaitMs(value: any): number { |
| 337 | + const n = Number(value) || 0; |
| 338 | + return n <= 0 ? 0 : n; // treat value as milliseconds directly |
| 339 | +} |
292 | 340 |
|
293 | | - if (percent >= 100) { |
294 | | - percent = 0; |
| 341 | +function animate(timestamp: number) { |
| 342 | + // First frame init |
| 343 | + if (previousTime === null) { |
| 344 | + previousTime = timestamp; |
| 345 | + animationFrame = requestAnimationFrame(animate); |
| 346 | + return; |
| 347 | + } |
| 348 | +
|
| 349 | + // Calculate elapsed ms since last frame |
| 350 | + const deltaTime = timestamp - previousTime; |
| 351 | + // Move previousTime forward now (keeps delta strictly the time spent since last frame) |
| 352 | + previousTime = timestamp; |
| 353 | +
|
| 354 | + // compute playback elapsed deterministically from `percent` and waits |
| 355 | + function computeElapsedFromPercent(p: number) { |
| 356 | + if (!lines || lines.length === 0) return 0; |
| 357 | + const clamped = Math.max(0, Math.min(100, p)); |
| 358 | + const totalLineProgress = (lines.length * clamped) / 100; |
| 359 | + let idx = Math.min(Math.trunc(totalLineProgress), Math.max(0, lines.length - 1)); |
| 360 | + const frac = totalLineProgress - Math.floor(totalLineProgress); |
| 361 | +
|
| 362 | + // motion time per non-wait line (derived from existing speed formula) |
| 363 | + const motionMsPerLine = 100 / 0.065; // ~=1538.461538ms |
| 364 | +
|
| 365 | + let sum = 0; |
| 366 | + for (let j = 0; j < idx; j++) { |
| 367 | + const l = lines[j] as any; |
| 368 | + if (l && l.waitMs !== undefined) { |
| 369 | + sum += Number(l.waitMs) || 0; |
295 | 370 | } else { |
296 | | - percent += (0.65 / lines.length) * (deltaTime * 0.1); |
| 371 | + sum += motionMsPerLine; |
297 | 372 | } |
298 | 373 | } |
299 | 374 |
|
300 | | - previousTime = timestamp; |
| 375 | + // current line partial |
| 376 | + const cur = lines[idx] as any; |
| 377 | + if (cur) { |
| 378 | + if (cur.waitMs !== undefined) { |
| 379 | + sum += (Number(cur.waitMs) || 0) * frac; |
| 380 | + } else { |
| 381 | + sum += motionMsPerLine * frac; |
| 382 | + } |
| 383 | + } |
| 384 | +
|
| 385 | + return sum; |
| 386 | + } |
| 387 | +
|
| 388 | + playElapsedMs = computeElapsedFromPercent(percent); |
| 389 | +
|
| 390 | + // Detect current line based on percent |
| 391 | + const totalLineProgress = (lines.length * Math.min(percent, 99.999999)) / 100; |
| 392 | + const currentLineIdx = Math.min(Math.trunc(totalLineProgress), Math.max(0, lines.length - 1)); |
| 393 | + const currentLine = lines[currentLineIdx]; |
| 394 | +
|
| 395 | + // If we just entered a line that has a wait, start the wait timer (only once per line) |
| 396 | + if (currentLine && (currentLine as any).waitMs !== undefined && lastHandledLineIdx !== currentLineIdx) { |
| 397 | + waiting = true; |
| 398 | + const ms = normalizeWaitMs((currentLine as any).waitMs); |
| 399 | + // set an absolute end timestamp for the wait based on current frame timestamp |
| 400 | + waitEndTimestamp = timestamp + ms; |
| 401 | + waitTimerRemaining = ms; |
| 402 | + lastHandledLineIdx = currentLineIdx; |
| 403 | + console.log(`Entering wait on line ${currentLineIdx}: raw waitMs=${(currentLine as any).waitMs} normalized=${ms}ms, will end at ${waitEndTimestamp}`); |
| 404 | + } |
301 | 405 |
|
302 | | - if (playing) { |
303 | | - requestAnimationFrame(animate); |
| 406 | + // HANDLE WAIT: subtract elapsed time (ms). Do not advance percent while waiting. |
| 407 | + if (waiting) { |
| 408 | + // compute remaining based on absolute end timestamp if set |
| 409 | + if (waitEndTimestamp !== null) { |
| 410 | + const remaining = Math.max(0, waitEndTimestamp - timestamp); |
| 411 | + waitTimerRemaining = remaining; |
| 412 | + if (timestamp >= waitEndTimestamp) { |
| 413 | + // wait finished |
| 414 | + waiting = false; |
| 415 | + waitTimerRemaining = 0; |
| 416 | + waitEndTimestamp = null; |
| 417 | + // reset previousTime so the next frame's delta doesn't include the time that passed during the wait |
| 418 | + previousTime = timestamp; |
| 419 | + console.log(`Wait finished on line ${currentLineIdx}`); |
| 420 | + } |
304 | 421 | } |
| 422 | +
|
| 423 | + // keep animating loop but do not advance percent while waiting |
| 424 | + animationFrame = requestAnimationFrame(animate); |
| 425 | + return; |
305 | 426 | } |
306 | 427 |
|
307 | | - function play() { |
308 | | - if (!playing) { |
309 | | - playing = true; |
310 | | - startTime = null; |
311 | | - previousTime = null; |
312 | | - animationFrame = requestAnimationFrame(animate); |
| 428 | + // NORMAL PROGRESS |
| 429 | + if (percent >= 100) { |
| 430 | + percent = 0; |
| 431 | + lastHandledLineIdx = -1; // allow waits to re-trigger on next loop |
| 432 | + // cycle finished — stop cycle timer so it will restart on the next run |
| 433 | + cycleTimerRunning = false; |
| 434 | + } else { |
| 435 | + // Your original motion formula — kept similar, but uses real ms deltaTime |
| 436 | + const speed = 0.65 / Math.max(1, lines.length); |
| 437 | + percent += speed * deltaTime * 0.1; // you can tune this multiplier if you want motion faster/slower |
| 438 | + // If we transitioned from 0 -> >0, this is the very first path beginning — reset cycle timer |
| 439 | + if (!cycleTimerRunning && prevPercent === 0 && percent > 0) { |
| 440 | + playElapsedMs = 0; |
| 441 | + cycleTimerRunning = true; |
| 442 | + console.log('Cycle timer started (first path began)'); |
313 | 443 | } |
314 | 444 | } |
315 | 445 |
|
316 | | - function pause() { |
317 | | - playing = false; |
318 | | - cancelAnimationFrame(animationFrame); |
| 446 | + animationFrame = requestAnimationFrame(animate); |
| 447 | + // remember percent for next frame to detect 0->>0 transitions |
| 448 | + prevPercent = percent; |
| 449 | +} |
| 450 | +
|
| 451 | +function play() { |
| 452 | + if (!playing) { |
| 453 | + playing = true; |
| 454 | + // If we're starting from the very beginning (never played), reset elapsed timer. |
| 455 | + // If we're resuming (percent > 0 or waits already handled), don't reset timers or progress. |
| 456 | + startTime = null; |
| 457 | + previousTime = null; // force animate to initialize timing on next frame (prevents big delta) |
| 458 | + if (percent === 0 && lastHandledLineIdx === -1) { |
| 459 | + playElapsedMs = 0; |
| 460 | + } |
| 461 | + animationFrame = requestAnimationFrame(animate); |
319 | 462 | } |
| 463 | +} |
| 464 | +
|
| 465 | +function pause() { |
| 466 | + playing = false; |
| 467 | + cancelAnimationFrame(animationFrame); |
| 468 | +} |
320 | 469 |
|
321 | 470 | async function fpa(l: FPALine, s: FPASettings): Promise<Line> { |
322 | 471 | let status = 'Starting optimization...'; |
@@ -749,6 +898,6 @@ hotkeys('s', function(event, handler){ |
749 | 898 | bind:robotHeading |
750 | 899 | {x} |
751 | 900 | {y} |
752 | | - {fpa} |
| 901 | + {playElapsedMs} |
753 | 902 | /> |
754 | 903 | </div> |
0 commit comments