some updates to effects
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
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' && target.fraction >= 0.45 && !hasHesitated) {
|
||||||
|
delay = 800; // Noticeably pause
|
||||||
|
newHasHesitated = true;
|
||||||
|
} 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(mode, maxDist, blendThreshold) {
|
||||||
|
if (mode === 'erratic') {
|
||||||
|
if (maxDist <= 5) return { action: 'wait', delay: 100 };
|
||||||
|
} else if (mode === 'jumpy') {
|
||||||
|
if (maxDist <= 5) return { action: 'wait', delay: 100 };
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
return Math.max(1, count);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called to apply per-waypoint visual perturbations
|
||||||
|
applyPerturbation(px, py, nx, ny, fraction, distance, mode) {
|
||||||
|
if (mode === 'snaky') {
|
||||||
|
const cycles = Math.max(1, Math.floor(distance / 50));
|
||||||
|
const offset = Math.sin(fraction * Math.PI * 2 * cycles) * 15;
|
||||||
|
px += nx * offset;
|
||||||
|
py += ny * offset;
|
||||||
|
} else if (mode === 'erratic') {
|
||||||
|
// Meaningful jumps
|
||||||
|
px += (Math.random() - 0.5) * 20;
|
||||||
|
py += (Math.random() - 0.5) * 20;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
+22
-81
@@ -2,6 +2,7 @@ import BLE from './ble.js';
|
|||||||
import IK from './kinematics.js';
|
import IK from './kinematics.js';
|
||||||
import UI from './ui.js';
|
import UI from './ui.js';
|
||||||
import Settings from './settings.js';
|
import Settings from './settings.js';
|
||||||
|
import { MotionEffects } from './motion-effects.js';
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
SEGMENT_SIZE_MM: 25, // Maximum distance between waypoints to prevent out-of-bounds joint-space arcs
|
SEGMENT_SIZE_MM: 25, // Maximum distance between waypoints to prevent out-of-bounds joint-space arcs
|
||||||
@@ -57,9 +58,7 @@ async function executeQueue() {
|
|||||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
||||||
|
|
||||||
// Restore speed if it was altered
|
// Restore speed if it was altered
|
||||||
if (BLE.isConnected() && ['jumpy', 'hesitant', 'overshoot'].includes(currentMotionMode)) {
|
await MotionEffects.onSequenceEnd(currentMotionMode);
|
||||||
try { await BLE.write(`SPD:${Settings.speed}`); } catch(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentResolve) currentResolve(true);
|
if (currentResolve) currentResolve(true);
|
||||||
currentResolve = null;
|
currentResolve = null;
|
||||||
@@ -69,20 +68,12 @@ async function executeQueue() {
|
|||||||
isMoving = true;
|
isMoving = true;
|
||||||
let target = motionQueue.shift();
|
let target = motionQueue.shift();
|
||||||
|
|
||||||
let delayBeforeSend = 0;
|
const { delay: delayBeforeSend, hasHesitated: newHasHesitated } = await MotionEffects.onWaypointSend(target, hasHesitated);
|
||||||
if (target.mode === 'hesitant' && target.fraction >= 0.45 && !hasHesitated) {
|
hasHesitated = newHasHesitated;
|
||||||
hasHesitated = true;
|
|
||||||
delayBeforeSend = 400;
|
|
||||||
} else if (target.mode === 'overshoot_settle') {
|
|
||||||
delayBeforeSend = 200; // brief pause before returning
|
|
||||||
}
|
|
||||||
|
|
||||||
const doSend = async () => {
|
const doSend = async () => {
|
||||||
const cmd = `X:${target.s1},${target.s2}`;
|
const cmd = `X:${target.s1},${target.s2}`;
|
||||||
if (BLE.isConnected()) {
|
if (BLE.isConnected()) {
|
||||||
if (target.mode === 'overshoot_settle') {
|
|
||||||
try { await BLE.write(`SPD:${Math.floor(Math.max(100, Settings.speed * 0.4))}`); } catch(e){}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await BLE.write(cmd);
|
await BLE.write(cmd);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -104,23 +95,13 @@ async function executeQueue() {
|
|||||||
const maxDist = Math.max(dist1, dist2);
|
const maxDist = Math.max(dist1, dist2);
|
||||||
|
|
||||||
if (motionQueue.length > 0) {
|
if (motionQueue.length > 0) {
|
||||||
// We have more waypoints. Blend or wait depending on mode.
|
const queueAction = MotionEffects.getQueueAction(target.mode, maxDist, CONFIG.BLEND_THRESHOLD_STEPS);
|
||||||
if (target.mode === 'erratic') {
|
if (queueAction.action === 'wait') {
|
||||||
if (maxDist <= 5) { // wait for full stop
|
clearInterval(queueCheckInterval);
|
||||||
clearInterval(queueCheckInterval);
|
setTimeout(executeQueue, queueAction.delay);
|
||||||
setTimeout(executeQueue, 50 + Math.random() * 100);
|
} else if (queueAction.action === 'blend') {
|
||||||
}
|
clearInterval(queueCheckInterval);
|
||||||
} else if (target.mode === 'jumpy') {
|
executeQueue();
|
||||||
if (maxDist <= 5) { // stop and go rhythm
|
|
||||||
clearInterval(queueCheckInterval);
|
|
||||||
setTimeout(executeQueue, 100);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// direct, snaky, hesitant, etc default to direct blending
|
|
||||||
if (maxDist <= CONFIG.BLEND_THRESHOLD_STEPS) {
|
|
||||||
clearInterval(queueCheckInterval);
|
|
||||||
executeQueue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Last waypoint. Wait until closely reached.
|
// Last waypoint. Wait until closely reached.
|
||||||
@@ -159,36 +140,16 @@ async function executeQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildSegments(startX, startY, endX, endY, mode) {
|
function buildSegments(startX, startY, endX, endY, mode) {
|
||||||
let actualEndX = endX;
|
const modDest = MotionEffects.modifyDestination(startX, startY, endX, endY, mode);
|
||||||
let actualEndY = endY;
|
const actualEndX = modDest.endX;
|
||||||
|
const actualEndY = modDest.endY;
|
||||||
if (mode === 'overshoot') {
|
|
||||||
const totalDx = endX - startX;
|
|
||||||
const totalDy = endY - startY;
|
|
||||||
const totalDist = Math.sqrt(totalDx * totalDx + totalDy * totalDy);
|
|
||||||
if (totalDist > 0) {
|
|
||||||
actualEndX = endX + (totalDx / totalDist) * 15;
|
|
||||||
actualEndY = endY + (totalDy / totalDist) * 15;
|
|
||||||
|
|
||||||
const wsCheck = IK.checkWorkspace(actualEndX, actualEndY);
|
|
||||||
if (!wsCheck.ok) {
|
|
||||||
actualEndX = endX;
|
|
||||||
actualEndY = endY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dx = actualEndX - startX;
|
const dx = actualEndX - startX;
|
||||||
const dy = actualEndY - startY;
|
const dy = actualEndY - startY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
let numSegments = Math.ceil(distance / CONFIG.SEGMENT_SIZE_MM);
|
let numSegments = MotionEffects.getSegmentCount(distance, CONFIG.SEGMENT_SIZE_MM, mode);
|
||||||
|
|
||||||
if (mode === 'snaky' || mode === 'erratic') {
|
|
||||||
numSegments = Math.max(numSegments, Math.ceil(distance / 10)); // 10mm segments max for finer resolution
|
|
||||||
}
|
|
||||||
if (numSegments === 0) numSegments = 1;
|
|
||||||
|
|
||||||
let segs = [];
|
let segs = [];
|
||||||
|
|
||||||
const nx = -dy / distance || 0;
|
const nx = -dy / distance || 0;
|
||||||
@@ -199,15 +160,9 @@ function buildSegments(startX, startY, endX, endY, mode) {
|
|||||||
let px = startX + dx * fraction;
|
let px = startX + dx * fraction;
|
||||||
let py = startY + dy * fraction;
|
let py = startY + dy * fraction;
|
||||||
|
|
||||||
if (mode === 'snaky') {
|
const perturbed = MotionEffects.applyPerturbation(px, py, nx, ny, fraction, distance, mode);
|
||||||
const cycles = Math.max(1, Math.floor(distance / 50));
|
px = perturbed.px;
|
||||||
const offset = Math.sin(fraction * Math.PI * 2 * cycles) * 15;
|
py = perturbed.py;
|
||||||
px += nx * offset;
|
|
||||||
py += ny * offset;
|
|
||||||
} else if (mode === 'erratic') {
|
|
||||||
px += (Math.random() - 0.5) * 10;
|
|
||||||
py += (Math.random() - 0.5) * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsCheck = IK.checkWorkspace(px, py);
|
const wsCheck = IK.checkWorkspace(px, py);
|
||||||
if (!wsCheck.ok) {
|
if (!wsCheck.ok) {
|
||||||
@@ -238,14 +193,8 @@ function buildSegments(startX, startY, endX, endY, mode) {
|
|||||||
segs.push({ s1, s2, mode, fraction });
|
segs.push({ s1, s2, mode, fraction });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'overshoot') {
|
const settlingSegs = MotionEffects.getSettlingWaypoints(endX, endY, mode);
|
||||||
const res = IK.solve(endX, endY);
|
segs = segs.concat(settlingSegs);
|
||||||
if (res.reachable && !IK.armsCrossed(res.theta1, res.theta2)) {
|
|
||||||
const s1 = IK.radToStepsAbsolute(res.theta1, 1);
|
|
||||||
const s2 = IK.radToStepsAbsolute(res.theta2, 2);
|
|
||||||
segs.push({ s1, s2, mode: 'overshoot_settle', fraction: 1.1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return segs;
|
return segs;
|
||||||
}
|
}
|
||||||
@@ -261,10 +210,7 @@ async function goto(targetX, targetY, mode = 'direct') {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'random') {
|
mode = MotionEffects.resolveMode(mode);
|
||||||
const modes = ['direct', 'snaky', 'erratic', 'jumpy', 'hesitant', 'overshoot'];
|
|
||||||
mode = modes[Math.floor(Math.random() * modes.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMotionMode = mode;
|
currentMotionMode = mode;
|
||||||
hasHesitated = false;
|
hasHesitated = false;
|
||||||
@@ -317,12 +263,7 @@ async function goto(targetX, targetY, mode = 'direct') {
|
|||||||
|
|
||||||
motionQueue = segments;
|
motionQueue = segments;
|
||||||
|
|
||||||
if (BLE.isConnected()) {
|
await MotionEffects.onSequenceStart(mode);
|
||||||
if (mode === 'jumpy' || mode === 'hesitant') {
|
|
||||||
const fastSpeed = Math.min(2000, Settings.speed * 1.5);
|
|
||||||
try { await BLE.write(`SPD:${Math.floor(fastSpeed)}`); } catch(e){}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
currentResolve = resolve;
|
currentResolve = resolve;
|
||||||
|
|||||||
Reference in New Issue
Block a user