Implementing effects. Closes #2 #19
@@ -190,7 +190,8 @@ hamburger (`☰`) button. It contains `<button data-route="...">` nav items that
|
||||
Centralised trajectory generation and streaming module.
|
||||
- **Why**: Moving side-to-side linearly in joint space causes the arms to arc dangerously out-of-bounds at the top.
|
||||
- **How it works**: Intercepts `Motion.goto(x, y)`, slices the Cartesian straight line into small segments (max `25 mm` by default, configurable in `Motion.CONFIG.SEGMENT_SIZE_MM`), and streams them to the ESP32.
|
||||
- **Dynamic Blending**: It monitors `P:` positional updates from the ESP32 and fires the next waypoint *before* `AccelStepper` decelerates (when within `BLEND_THRESHOLD_STEPS`). This produces a seamless Cartesian trajectory with no MCU code changes and allows for future "visual flavor" movement effects (e.g. snaky, erratic).
|
||||
- **Dynamic Blending**: It monitors `P:` positional updates from the ESP32 and fires the next waypoint *before* `AccelStepper` decelerates (when within `BLEND_THRESHOLD_STEPS`). This produces a seamless Cartesian trajectory with no MCU code changes.
|
||||
- **Motion Effects (`web/js/motion-effects.js`)**: Visual flavor modes (snaky, erratic, jumpy, hesitant, overshoot, random, direct) are encapsulated in their own module to keep the core orchestrator clean. The `MotionEffects` singleton intercepts trajectory generation (`applyPerturbation`) and queue execution (`onWaypointSend`, `getQueueAction`) to dynamically inject BLE speed commands (`SPD:`) or alter queue blending logic (e.g. lowering the blend threshold to force physical synchronization of mid-move speed changes without stuttering).
|
||||
- **Simulation Sync**: In simulation mode, `Motion.goto()` instantly updates its internal steps and manually dispatches a fake `ble:status` event containing `P:s1,s2`. This allows all UI components to effortlessly sync their visual state using their existing BLE listeners.
|
||||
|
||||
---
|
||||
@@ -405,7 +406,7 @@ The router currently only has three routes. These sections need to be created/ex
|
||||
### `board-control` section (implemented)
|
||||
- Uses predefined background maps (SVG/PNG) with corresponding JSON config files specifying clickable coordinates in physical mm.
|
||||
- Click a spot on the map or select from a compact list → IK move to coordinates.
|
||||
- Supports movement modes (e.g., Direct, Erratic, Random, Snaky).
|
||||
- Supports movement modes (e.g., Direct, Snaky, Erratic, Jumpy, Hesitant, Overshoot, Random).
|
||||
- Requires arm to be homed; homing state is persisted across sessions via `localStorage`.
|
||||
|
||||
### `input-text` section (implemented)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import IK from './kinematics.js';
|
||||
import Settings from './settings.js';
|
||||
import BLE from './ble.js';
|
||||
|
||||
export const MotionEffects = {
|
||||
// Returns true if the mode should randomize
|
||||
resolveMode(mode) {
|
||||
if (mode === 'random') {
|
||||
const modes = ['direct', 'snaky', 'erratic', 'jumpy', 'hesitant', 'overshoot'];
|
||||
return modes[Math.floor(Math.random() * modes.length)];
|
||||
}
|
||||
return mode || 'direct';
|
||||
},
|
||||
|
||||
// Called before a movement sequence starts
|
||||
async onSequenceStart(mode) {
|
||||
if (!BLE.isConnected()) return;
|
||||
if (mode === 'jumpy' || mode === 'overshoot') {
|
||||
const fastSpeed = Math.min(2000, Settings.speed * 1.5);
|
||||
try { await BLE.write(`SPD:${Math.floor(fastSpeed)}`); } catch(e){}
|
||||
}
|
||||
},
|
||||
|
||||
// Called after a movement sequence finishes
|
||||
async onSequenceEnd(mode) {
|
||||
if (!BLE.isConnected()) return;
|
||||
if (mode !== 'direct') {
|
||||
try { await BLE.write(`SPD:${Settings.speed}`); } catch(e){}
|
||||
}
|
||||
},
|
||||
|
||||
// Called right before sending a waypoint to BLE
|
||||
async onWaypointSend(target, hasHesitated) {
|
||||
let delay = 0;
|
||||
let newHasHesitated = hasHesitated;
|
||||
|
||||
if (target.mode === 'hesitant') {
|
||||
if (target.fraction >= 0.35 && hasHesitated === false) {
|
||||
// Slow down massively instead of stopping
|
||||
if (BLE.isConnected()) {
|
||||
try { await BLE.write(`SPD:${Math.floor(Math.max(30, Settings.speed * 0.1))}`); } catch(e){}
|
||||
}
|
||||
newHasHesitated = true; // Started hesitation
|
||||
} else if (target.fraction >= 0.55 && hasHesitated === true) {
|
||||
// Speed back up aggressively
|
||||
if (BLE.isConnected()) {
|
||||
try { await BLE.write(`SPD:${Math.floor(Settings.speed * 1.2)}`); } catch(e){}
|
||||
}
|
||||
newHasHesitated = 2; // Finished hesitation
|
||||
}
|
||||
} else if (target.mode === 'erratic') {
|
||||
// Aggressive speed changes randomly during the move
|
||||
if (Math.random() > 0.4) {
|
||||
if (BLE.isConnected()) {
|
||||
const spd = Math.floor(Math.max(30, Settings.speed * (0.1 + Math.random() * 1.5)));
|
||||
try { await BLE.write(`SPD:${spd}`); } catch(e){}
|
||||
}
|
||||
}
|
||||
} else if (target.mode === 'overshoot_settle') {
|
||||
delay = 200; // Brief pause before returning
|
||||
if (BLE.isConnected()) {
|
||||
try { await BLE.write(`SPD:${Math.floor(Math.max(100, Settings.speed * 0.4))}`); } catch(e){}
|
||||
}
|
||||
}
|
||||
return { delay, hasHesitated: newHasHesitated };
|
||||
},
|
||||
|
||||
// Called to determine blending behaviour during queue checks
|
||||
getQueueAction(targetMode, nextTargetMode, maxDist, blendThreshold) {
|
||||
if (nextTargetMode === 'overshoot_settle') {
|
||||
// Must physically reach the overshoot point before popping the settle command
|
||||
if (maxDist <= 10) return { action: 'blend' };
|
||||
return { action: 'continue' };
|
||||
} else if (targetMode === 'jumpy') {
|
||||
if (maxDist <= 5) return { action: 'wait', delay: 100 };
|
||||
} else if (targetMode === 'hesitant') {
|
||||
// Lower blend threshold prevents blasting all speed-change waypoints instantly
|
||||
if (maxDist <= 30) return { action: 'blend' };
|
||||
} else {
|
||||
if (maxDist <= blendThreshold) return { action: 'blend' };
|
||||
}
|
||||
return { action: 'continue' };
|
||||
},
|
||||
|
||||
// Called to modify the final destination before slicing (e.g. overshoot)
|
||||
modifyDestination(startX, startY, endX, endY, mode) {
|
||||
if (mode === 'overshoot') {
|
||||
const dx = endX - startX;
|
||||
const dy = endY - startY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > 0) {
|
||||
let nx = endX + (dx / dist) * 20; // Overshoot 20mm
|
||||
let ny = endY + (dy / dist) * 20;
|
||||
if (IK.checkWorkspace(nx, ny).ok) {
|
||||
return { endX: nx, endY: ny };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { endX, endY };
|
||||
},
|
||||
|
||||
// Called to determine segment count
|
||||
getSegmentCount(distance, baseSegmentSize, mode) {
|
||||
let count = Math.ceil(distance / baseSegmentSize);
|
||||
if (mode === 'snaky') {
|
||||
count = Math.max(count, Math.ceil(distance / 10)); // fine resolution for sine
|
||||
} else if (mode === 'erratic') {
|
||||
count = Math.max(count, Math.ceil(distance / 20)); // fewer, larger jumps
|
||||
} else if (mode === 'hesitant') {
|
||||
count = Math.max(count, Math.ceil(distance / 15)); // ensure enough waypoints for speed changes
|
||||
}
|
||||
return Math.max(1, count);
|
||||
},
|
||||
|
||||
// Called to apply per-waypoint visual perturbations
|
||||
applyPerturbation(px, py, nx, ny, fraction, distance, mode) {
|
||||
if (fraction === 1) return { px, py }; // Always hit exact target at the end
|
||||
|
||||
if (mode === 'snaky') {
|
||||
const cycles = Math.max(1, Math.floor(distance / 50));
|
||||
const offset = Math.sin(fraction * Math.PI * 2 * cycles) * 30; // 30mm amplitude
|
||||
px += nx * offset;
|
||||
py += ny * offset;
|
||||
} else if (mode === 'erratic') {
|
||||
// Meaningful, aggressive jumps
|
||||
px += (Math.random() - 0.5) * 40;
|
||||
py += (Math.random() - 0.5) * 40;
|
||||
}
|
||||
return { px, py };
|
||||
},
|
||||
|
||||
// Generate extra waypoints at the end (e.g. settle from overshoot)
|
||||
getSettlingWaypoints(endX, endY, mode) {
|
||||
const segs = [];
|
||||
if (mode === 'overshoot') {
|
||||
const res = IK.solve(endX, endY);
|
||||
if (res.reachable && !IK.armsCrossed(res.theta1, res.theta2)) {
|
||||
segs.push({
|
||||
s1: IK.radToStepsAbsolute(res.theta1, 1),
|
||||
s2: IK.radToStepsAbsolute(res.theta2, 2),
|
||||
mode: 'overshoot_settle',
|
||||
fraction: 1.1
|
||||
});
|
||||
}
|
||||
}
|
||||
return segs;
|
||||
}
|
||||
};
|
||||
+118
-70
@@ -1,6 +1,8 @@
|
||||
import BLE from './ble.js';
|
||||
import IK from './kinematics.js';
|
||||
import UI from './ui.js';
|
||||
import Settings from './settings.js';
|
||||
import { MotionEffects } from './motion-effects.js';
|
||||
|
||||
const CONFIG = {
|
||||
SEGMENT_SIZE_MM: 25, // Maximum distance between waypoints to prevent out-of-bounds joint-space arcs
|
||||
@@ -47,11 +49,17 @@ let motionQueue = [];
|
||||
let isMoving = false;
|
||||
let currentResolve = null;
|
||||
let queueCheckInterval = null;
|
||||
let currentMotionMode = 'direct';
|
||||
let hasHesitated = false;
|
||||
|
||||
async function executeQueue() {
|
||||
if (motionQueue.length === 0) {
|
||||
isMoving = false;
|
||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||
|
||||
// Restore speed if it was altered
|
||||
await MotionEffects.onSequenceEnd(currentMotionMode);
|
||||
|
||||
if (currentResolve) currentResolve(true);
|
||||
currentResolve = null;
|
||||
return;
|
||||
@@ -60,102 +68,135 @@ async function executeQueue() {
|
||||
isMoving = true;
|
||||
let target = motionQueue.shift();
|
||||
|
||||
const cmd = `X:${target.s1},${target.s2}`;
|
||||
if (BLE.isConnected()) {
|
||||
try {
|
||||
await BLE.write(cmd);
|
||||
} catch (e) {
|
||||
UI.log(`BLE error: ${e.message}`, 'error');
|
||||
motionQueue = []; // clear queue on error
|
||||
isMoving = false;
|
||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||
if (currentResolve) currentResolve(false);
|
||||
currentResolve = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup interval to monitor progress
|
||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||
queueCheckInterval = setInterval(async () => {
|
||||
// Calculate distance to target in steps
|
||||
const dist1 = Math.abs(target.s1 - steps1);
|
||||
const dist2 = Math.abs(target.s2 - steps2);
|
||||
const maxDist = Math.max(dist1, dist2);
|
||||
const { delay: delayBeforeSend, hasHesitated: newHasHesitated } = await MotionEffects.onWaypointSend(target, hasHesitated);
|
||||
hasHesitated = newHasHesitated;
|
||||
|
||||
if (motionQueue.length > 0) {
|
||||
// We have more waypoints. Blend!
|
||||
if (target.mode === 'direct') {
|
||||
if (maxDist <= CONFIG.BLEND_THRESHOLD_STEPS) {
|
||||
clearInterval(queueCheckInterval);
|
||||
executeQueue(); // start next waypoint early
|
||||
}
|
||||
} else if (target.mode === 'erratic') {
|
||||
if (maxDist <= 5) { // wait for full stop
|
||||
clearInterval(queueCheckInterval);
|
||||
setTimeout(executeQueue, 150); // slight pause for erratic effect
|
||||
const doSend = async () => {
|
||||
const cmd = `X:${target.s1},${target.s2}`;
|
||||
if (BLE.isConnected()) {
|
||||
try {
|
||||
await BLE.write(cmd);
|
||||
} catch (e) {
|
||||
UI.log(`BLE error: ${e.message}`, 'error');
|
||||
motionQueue = []; // clear queue on error
|
||||
isMoving = false;
|
||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||
if (currentResolve) currentResolve(false);
|
||||
currentResolve = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup interval to monitor progress
|
||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||
queueCheckInterval = setInterval(async () => {
|
||||
// Calculate distance to target in steps
|
||||
const dist1 = Math.abs(target.s1 - steps1);
|
||||
const dist2 = Math.abs(target.s2 - steps2);
|
||||
const maxDist = Math.max(dist1, dist2);
|
||||
|
||||
if (motionQueue.length > 0) {
|
||||
const nextTarget = motionQueue[0];
|
||||
const queueAction = MotionEffects.getQueueAction(target.mode, nextTarget.mode, maxDist, CONFIG.BLEND_THRESHOLD_STEPS);
|
||||
if (queueAction.action === 'wait') {
|
||||
clearInterval(queueCheckInterval);
|
||||
setTimeout(executeQueue, queueAction.delay);
|
||||
} else if (queueAction.action === 'blend') {
|
||||
clearInterval(queueCheckInterval);
|
||||
executeQueue();
|
||||
}
|
||||
} else {
|
||||
// Default to direct blending
|
||||
if (maxDist <= CONFIG.BLEND_THRESHOLD_STEPS) {
|
||||
clearInterval(queueCheckInterval);
|
||||
executeQueue();
|
||||
// Last waypoint. Wait until closely reached.
|
||||
if (maxDist <= 10) {
|
||||
clearInterval(queueCheckInterval);
|
||||
motionQueue = [];
|
||||
executeQueue(); // Will trigger resolve
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Last waypoint. Wait until closely reached.
|
||||
if (maxDist <= 10) {
|
||||
clearInterval(queueCheckInterval);
|
||||
motionQueue = [];
|
||||
executeQueue(); // Will trigger resolve
|
||||
}
|
||||
}, 40); // check ~25 times a second
|
||||
|
||||
} else {
|
||||
// Simulation: instantly jump to final target
|
||||
const finalTarget = motionQueue.length > 0 ? motionQueue[motionQueue.length - 1] : target;
|
||||
steps1 = finalTarget.s1;
|
||||
steps2 = finalTarget.s2;
|
||||
console.log(`[sim] Jumped to X:${steps1},${steps2} via trajectory`);
|
||||
|
||||
// Trigger pseudo-status event for the UI to update markers
|
||||
document.dispatchEvent(new CustomEvent('ble:status', { detail: `P:${steps1},${steps2}` }));
|
||||
|
||||
motionQueue = [];
|
||||
isMoving = false;
|
||||
if (currentResolve) {
|
||||
currentResolve(true);
|
||||
currentResolve = null;
|
||||
}
|
||||
}, 40); // check ~25 times a second
|
||||
|
||||
} else {
|
||||
// Simulation: instantly jump to final target
|
||||
const finalTarget = motionQueue.length > 0 ? motionQueue[motionQueue.length - 1] : target;
|
||||
steps1 = finalTarget.s1;
|
||||
steps2 = finalTarget.s2;
|
||||
console.log(`[sim] Jumped to X:${steps1},${steps2} via trajectory`);
|
||||
|
||||
// Trigger pseudo-status event for the UI to update markers
|
||||
document.dispatchEvent(new CustomEvent('ble:status', { detail: `P:${steps1},${steps2}` }));
|
||||
|
||||
motionQueue = [];
|
||||
isMoving = false;
|
||||
if (currentResolve) {
|
||||
currentResolve(true);
|
||||
currentResolve = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (delayBeforeSend > 0) {
|
||||
setTimeout(doSend, delayBeforeSend);
|
||||
} else {
|
||||
doSend();
|
||||
}
|
||||
}
|
||||
|
||||
function buildSegments(startX, startY, endX, endY, mode) {
|
||||
const dx = endX - startX;
|
||||
const dy = endY - startY;
|
||||
const modDest = MotionEffects.modifyDestination(startX, startY, endX, endY, mode);
|
||||
const actualEndX = modDest.endX;
|
||||
const actualEndY = modDest.endY;
|
||||
|
||||
const dx = actualEndX - startX;
|
||||
const dy = actualEndY - startY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
let numSegments = Math.ceil(distance / CONFIG.SEGMENT_SIZE_MM);
|
||||
if (numSegments === 0) numSegments = 1;
|
||||
|
||||
let numSegments = MotionEffects.getSegmentCount(distance, CONFIG.SEGMENT_SIZE_MM, mode);
|
||||
|
||||
let segs = [];
|
||||
|
||||
const nx = -dy / distance || 0;
|
||||
const ny = dx / distance || 0;
|
||||
|
||||
for (let i = 1; i <= numSegments; i++) {
|
||||
const fraction = i / numSegments;
|
||||
const px = startX + dx * fraction;
|
||||
const py = startY + dy * fraction;
|
||||
let px = startX + dx * fraction;
|
||||
let py = startY + dy * fraction;
|
||||
|
||||
const perturbed = MotionEffects.applyPerturbation(px, py, nx, ny, fraction, distance, mode);
|
||||
px = perturbed.px;
|
||||
py = perturbed.py;
|
||||
|
||||
const wsCheck = IK.checkWorkspace(px, py);
|
||||
if (!wsCheck.ok) return null;
|
||||
if (!wsCheck.ok) {
|
||||
px = startX + dx * fraction;
|
||||
py = startY + dy * fraction;
|
||||
}
|
||||
|
||||
const res = IK.solve(px, py);
|
||||
if (!res.reachable || IK.armsCrossed(res.theta1, res.theta2)) return null;
|
||||
if (!res.reachable || IK.armsCrossed(res.theta1, res.theta2)) {
|
||||
px = startX + dx * fraction;
|
||||
py = startY + dy * fraction;
|
||||
const fallbackRes = IK.solve(px, py);
|
||||
if (!fallbackRes.reachable || IK.armsCrossed(fallbackRes.theta1, fallbackRes.theta2)) {
|
||||
return null;
|
||||
}
|
||||
segs.push({
|
||||
s1: IK.radToStepsAbsolute(fallbackRes.theta1, 1),
|
||||
s2: IK.radToStepsAbsolute(fallbackRes.theta2, 2),
|
||||
mode,
|
||||
fraction
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const s1 = IK.radToStepsAbsolute(res.theta1, 1);
|
||||
const s2 = IK.radToStepsAbsolute(res.theta2, 2);
|
||||
|
||||
segs.push({ s1, s2, mode });
|
||||
segs.push({ s1, s2, mode, fraction });
|
||||
}
|
||||
|
||||
const settlingSegs = MotionEffects.getSettlingWaypoints(endX, endY, mode);
|
||||
segs = segs.concat(settlingSegs);
|
||||
|
||||
return segs;
|
||||
}
|
||||
|
||||
@@ -170,6 +211,11 @@ async function goto(targetX, targetY, mode = 'direct') {
|
||||
return false;
|
||||
}
|
||||
|
||||
mode = MotionEffects.resolveMode(mode);
|
||||
|
||||
currentMotionMode = mode;
|
||||
hasHesitated = false;
|
||||
|
||||
// Quick check on final destination
|
||||
const destCheck = IK.checkWorkspace(targetX, targetY);
|
||||
if (!destCheck.ok) {
|
||||
@@ -218,6 +264,8 @@ async function goto(targetX, targetY, mode = 'direct') {
|
||||
|
||||
motionQueue = segments;
|
||||
|
||||
await MotionEffects.onSequenceStart(mode);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
currentResolve = resolve;
|
||||
executeQueue();
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import SequenceRunner from './sequence-runner.js';
|
||||
import UI from './ui.js';
|
||||
|
||||
function getGlobalMode() {
|
||||
const m1 = document.getElementById('mode-select');
|
||||
if (m1) return m1.value;
|
||||
const m2 = document.getElementById('seq-mode-select');
|
||||
if (m2) return m2.value;
|
||||
const m3 = document.getElementById('text-mode-select');
|
||||
if (m3) return m3.value;
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
export function buildQuickSequencesHTML() {
|
||||
return `
|
||||
<div class="card" id="quick-sequences-card" style="margin-bottom: 16px;">
|
||||
@@ -49,7 +59,7 @@ export async function initQuickSequences(bgName, getSpots, getSteps, onStepCompl
|
||||
if (seq) {
|
||||
stopBtn.style.display = 'block';
|
||||
const { steps1, steps2 } = getSteps();
|
||||
await SequenceRunner.run(seq, getSpots(), steps1, steps2, onStepComplete);
|
||||
await SequenceRunner.run(seq, getSpots(), steps1, steps2, getGlobalMode(), onStepComplete);
|
||||
stopBtn.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,9 +132,12 @@ function buildHTML() {
|
||||
<label class="form-label">Movement Mode</label>
|
||||
<select id="mode-select" class="form-input" style="width:100%;">
|
||||
<option value="direct">Direct (Normal)</option>
|
||||
<option value="erratic">Erratic (Coming Soon)</option>
|
||||
<option value="random">Random (Coming Soon)</option>
|
||||
<option value="snaky">Snaky (Coming Soon)</option>
|
||||
<option value="snaky">Snaky</option>
|
||||
<option value="erratic">Erratic</option>
|
||||
<option value="jumpy">Jumpy</option>
|
||||
<option value="hesitant">Hesitant</option>
|
||||
<option value="overshoot">Overshoot</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -128,6 +128,19 @@ function buildHTML() {
|
||||
<option value="portrait-right">Right Side Test (Portrait)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 12px;">
|
||||
<label class="form-label">Movement Mode</label>
|
||||
<select id="text-mode-select" class="form-input" style="width:100%;">
|
||||
<option value="direct">Direct (Normal)</option>
|
||||
<option value="snaky">Snaky</option>
|
||||
<option value="erratic">Erratic</option>
|
||||
<option value="jumpy">Jumpy</option>
|
||||
<option value="hesitant">Hesitant</option>
|
||||
<option value="overshoot">Overshoot</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
|
||||
@@ -264,9 +277,11 @@ function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function executeMove(spot) {
|
||||
async function executeMove(spot, forceDirect = false) {
|
||||
UI.log(`Spelling: ${spot.label}`, 'info');
|
||||
return await Motion.goto(spot.x, spot.y);
|
||||
const modeEl = document.getElementById('text-mode-select');
|
||||
const mode = forceDirect ? 'direct' : (modeEl ? modeEl.value : 'direct');
|
||||
return await Motion.goto(spot.x, spot.y, mode);
|
||||
}
|
||||
|
||||
async function spellText(text) {
|
||||
@@ -298,7 +313,7 @@ async function spellText(text) {
|
||||
const spot = spots.find(s => s.id.toUpperCase() === char);
|
||||
|
||||
if (spot) {
|
||||
await executeMove(spot);
|
||||
await executeMove(spot, i === 0);
|
||||
// Wait for delay
|
||||
await delay(Settings.delay);
|
||||
} else {
|
||||
|
||||
@@ -261,6 +261,19 @@ function buildHTML() {
|
||||
</select>
|
||||
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 4px;">Provides available spots for the editor.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 12px;">
|
||||
<label class="form-label">Global Movement Mode</label>
|
||||
<select id="seq-mode-select" class="form-input" style="width:100%;">
|
||||
<option value="direct">Direct (Normal)</option>
|
||||
<option value="snaky">Snaky</option>
|
||||
<option value="erratic">Erratic</option>
|
||||
<option value="jumpy">Jumpy</option>
|
||||
<option value="hesitant">Hesitant</option>
|
||||
<option value="overshoot">Overshoot</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm);">
|
||||
<div class="setting-row">
|
||||
@@ -671,8 +684,11 @@ export default {
|
||||
btnPlay.style.display = 'none';
|
||||
btnStop.style.display = 'inline-block';
|
||||
|
||||
const modeEl = document.getElementById('seq-mode-select');
|
||||
const mode = modeEl ? modeEl.value : 'direct';
|
||||
|
||||
await sendSettings();
|
||||
await SequenceRunner.run(sequence, spots, steps1, steps2, (s1, s2) => {
|
||||
await SequenceRunner.run(sequence, spots, steps1, steps2, mode, (s1, s2) => {
|
||||
steps1 = s1; steps2 = s2;
|
||||
updatePositionReadout();
|
||||
});
|
||||
|
||||
@@ -195,7 +195,10 @@ async function moveToXY(targetX, targetY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await Motion.goto(targetX, targetY);
|
||||
const modeEl = document.getElementById('test-mode-select');
|
||||
const mode = modeEl ? modeEl.value : 'direct';
|
||||
|
||||
return await Motion.goto(targetX, targetY, mode);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -526,6 +529,17 @@ function buildHTML() {
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Move to XY</span></div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div class="form-group" style="margin-bottom: 4px;">
|
||||
<select id="test-mode-select" class="form-input" style="width:100%; font-size: 0.9rem;">
|
||||
<option value="direct">Effect: Direct</option>
|
||||
<option value="snaky">Effect: Snaky</option>
|
||||
<option value="erratic">Effect: Erratic</option>
|
||||
<option value="jumpy">Effect: Jumpy</option>
|
||||
<option value="hesitant">Effect: Hesitant</option>
|
||||
<option value="overshoot">Effect: Overshoot</option>
|
||||
<option value="random">Effect: Random</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<label class="form-label" style="width:18px">X</label>
|
||||
<input class="form-input" type="number" id="input-x"
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
get isRunning() { return isRunning; },
|
||||
stop() { isRunning = false; },
|
||||
|
||||
async run(sequence, spotsData, currentSteps1, currentSteps2, onStepComplete) {
|
||||
async run(sequence, spotsData, currentSteps1, currentSteps2, mode, onStepComplete) {
|
||||
if (isRunning) {
|
||||
UI.log('A sequence is already running.', 'warn');
|
||||
return false;
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
|
||||
UI.log(`Seq step: ${label}`, 'info');
|
||||
|
||||
const success = await Motion.goto(targetX, targetY, step.mode || 'direct');
|
||||
const success = await Motion.goto(targetX, targetY, mode || 'direct');
|
||||
if (!success) {
|
||||
isRunning = false;
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user