Skip to content

Commit be65b61

Browse files
authored
Merge pull request #18 from saa938/main
add wait function plus timer
2 parents 0d4b4fe + 9015798 commit be65b61

3 files changed

Lines changed: 235 additions & 61 deletions

File tree

src/App.svelte

Lines changed: 175 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,47 @@
225225
let robotInchesXY = getCurvePoint(linePercent, [_startPoint, ...currentLine.controlPoints, currentLine.endPoint]);
226226
robotXY = { x: x(robotInchesXY.x), y: y(robotInchesXY.y) };
227227
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 {
228269
switch (currentLine.endPoint.heading) {
229270
case "linear":
230271
robotHeading = -shortestRotation(
@@ -254,6 +295,7 @@
254295
255296
break;
256297
}
298+
}
257299
}
258300
259301
$: (() => {
@@ -276,47 +318,154 @@
276318
two.update();
277319
})();
278320
279-
let playing = false;
321+
let playing = false;
280322
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;
284326
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;
289335
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+
}
292340
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;
295370
} else {
296-
percent += (0.65 / lines.length) * (deltaTime * 0.1);
371+
sum += motionMsPerLine;
297372
}
298373
}
299374
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+
}
301405
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+
}
304421
}
422+
423+
// keep animating loop but do not advance percent while waiting
424+
animationFrame = requestAnimationFrame(animate);
425+
return;
305426
}
306427
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)');
313443
}
314444
}
315445
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);
319462
}
463+
}
464+
465+
function pause() {
466+
playing = false;
467+
cancelAnimationFrame(animationFrame);
468+
}
320469
321470
async function fpa(l: FPALine, s: FPASettings): Promise<Line> {
322471
let status = 'Starting optimization...';
@@ -749,6 +898,6 @@ hotkeys('s', function(event, handler){
749898
bind:robotHeading
750899
{x}
751900
{y}
752-
{fpa}
901+
{playElapsedMs}
753902
/>
754903
</div>

src/lib/ControlTab.svelte

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@
1212
export let robotHeight: number = 16;
1313
export let robotXY: BasePoint;
1414
export let robotHeading: number;
15-
export let fpa: (l: FPALine, s: FPASettings) => Promise<Line>;
1615
export let x: d3.ScaleLinear<number, number, number>;
1716
export let y: d3.ScaleLinear<number, number, number>;
1817
export let settings: FPASettings;
18+
export let playElapsedMs: number = 0;
19+
function formatMs(ms: number) {
20+
const s = Math.floor(ms / 1000);
21+
const rem = Math.floor(ms % 1000);
22+
return `${s}.${String(rem).padStart(3, '0')}s`;
23+
}
1924
</script>
2025

2126
<div class="flex-1 flex flex-shrink-0 flex-col justify-start items-center gap-2 h-full overflow-y-auto">
2227
<div
23-
class="flex flex-col justify-start items-start w-full rounded-lg bg-neutral-50 dark:bg-neutral-900 shadow-md p-4 overflow-y-scroll overflow-x-hidden h-full gap-6"
28+
class="flex flex-col justify-start items-start w-full rounded-lg bg-neutral-50 dark:bg-neutral-900 shadow-md p-4 overflow-y-scroll overflow-x-hidden h-full gap-3"
2429
>
2530
<div class="flex flex-col w-full justify-start items-start gap-0.5 text-sm">
2631
<div class="font-semibold">Canvas Options</div>
@@ -162,6 +167,7 @@
162167
</div>
163168
</div>
164169
<div class={`h-[0.75px] w-full`} style={`background: ${line.color}`} />
170+
{#if line.waitMs === undefined}
165171
<div class="flex flex-col justify-start items-start">
166172
<div class="font-light">End Point:</div>
167173
<div class="flex flex-row justify-start items-center gap-2">
@@ -230,40 +236,25 @@ With tangential heading, the heading follows the direction of the line."
230236
<p class="text-sm font-extralight">Reverse:</p>
231237
<input type="checkbox" bind:checked={line.endPoint.reverse} title="Reverse the direction the robot faces along the tangential path" />
232238
{/if}
233-
<!--
234-
<button
235-
class="px-2 rounded-md bg-neutral-100 dark:bg-neutral-950 dark:border-neutral-700 border-[0.5px] focus:outline-none text-sm"
236-
title="Optimize"
237-
name="Optimize"
238-
on:click={async () => {
239-
try {
240-
const optimizedLine = await fpa(
241-
{
242-
startPoint: idx === 0 ? startPoint : lines[idx - 1].endPoint,
243-
endPoint: line.endPoint,
244-
controlPoints: line.controlPoints,
245-
interpolation: line.endPoint.heading,
246-
color: line.color,
247-
},
248-
settings
249-
);
250-
lines = lines.map((l, i) => i === idx ? optimizedLine : l);
251-
} catch (error) {
252-
console.error('Optimization failed:', error);
253-
254-
// Check if it's an offline error
255-
if (error.message && error.message.startsWith('OFFLINE:')) {
256-
const offlineMessage = error.message.replace('OFFLINE: ', '');
257-
alert(`🌐 ${offlineMessage}\n\nThe optimization feature requires an internet connection.`);
258-
} else {
259-
alert(`❌ Optimization failed: ${error.message}`);
260-
}
261-
}
262-
}}
263-
>Optimize</button>
264-
-->
265239
</div>
266240
</div>
241+
{:else}
242+
<div class="flex flex-col justify-start items-start">
243+
<div class="font-light">Wait</div>
244+
<div class="flex flex-row justify-start items-center gap-2">
245+
<div class="font-extralight">Duration (ms):</div>
246+
<input
247+
class="pl-1.5 rounded-md bg-neutral-100 dark:bg-neutral-950 dark:border-neutral-700 border-[0.5px] focus:outline-none w-28"
248+
step="10"
249+
type="number"
250+
min="0"
251+
bind:value={line.waitMs}
252+
on:change={() => { line.waitMs = Number(line.waitMs) || 0 }}
253+
/>
254+
<div class="font-extralight">(Robot pauses at previous point)</div>
255+
</div>
256+
</div>
257+
{/if}
267258
{#each line.controlPoints as point, idx1}
268259
<div class="flex flex-col justify-start items-start">
269260
<div class="font-light">Control Point {idx1 + 1}:</div>
@@ -313,6 +304,7 @@ With tangential heading, the heading follows the direction of the line."
313304
{/each}
314305
</div>
315306
{/each}
307+
<div class="flex flex-row gap-2">
316308
<button
317309
on:click={() => {
318310
lines = [
@@ -330,7 +322,7 @@ With tangential heading, the heading follows the direction of the line."
330322
},
331323
];
332324
}}
333-
class="font-semibold text-green-500 text-sm flex flex-row justify-start items-center gap-1"
325+
class="font-semibold text-green-500 text-sm flex flex-row justify-center items-center gap-1 h-8 px-3"
334326
>
335327
<svg
336328
xmlns="http://www.w3.org/2000/svg"
@@ -348,6 +340,29 @@ With tangential heading, the heading follows the direction of the line."
348340
</svg>
349341
<p>Add Line</p>
350342
</button>
343+
<button
344+
on:click={() => {
345+
// Add a wait entry: keep endPoint equal to last end point (or startPoint)
346+
const lastEnd = lines.length > 0 ? lines[lines.length - 1].endPoint : startPoint;
347+
lines = [
348+
...lines,
349+
{
350+
name: `Wait ${lines.length + 1}`,
351+
endPoint: { x: lastEnd.x, y: lastEnd.y, heading: "constant", degrees: 0 },
352+
controlPoints: [],
353+
color: '#888888',
354+
waitMs: 1000,
355+
},
356+
];
357+
}}
358+
class="font-semibold text-yellow-500 text-sm flex flex-row justify-center items-center gap-1 h-8 px-3"
359+
>
360+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={2} stroke="currentColor" class="size-5">
361+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12M6 12h12" />
362+
</svg>
363+
<p>Add Wait</p>
364+
</button>
365+
</div>
351366
</div>
352367
<div
353368
class="w-full bg-neutral-50 dark:bg-neutral-900 rounded-lg p-3 flex flex-row justify-start items-center gap-3 shadow-lg"
@@ -402,5 +417,12 @@ With tangential heading, the heading follows the direction of the line."
402417
step="0.000001"
403418
class="w-full appearance-none slider focus:outline-none"
404419
/>
420+
<div class="text-sm font-extralight ml-2">
421+
{#if playElapsedMs}
422+
{formatMs(playElapsedMs)}
423+
{:else}
424+
0.000s
425+
{/if}
426+
</div>
405427
</div>
406428
</div>

0 commit comments

Comments
 (0)