Add loop mode (--loop): repeat movement until user activity
Introduce a continuous "loop" setting so a triggered sweep keeps the
cursor moving until the user moves the mouse (or Ctrl+C), instead of
firing a single sweep.
- strategies.ts: add optional `loopPath` to MovementStrategy; give `line`
and `diagonal` infinite loop generators that pick a direction once and
ramp forever (4px/step). Their finite `path` and declared `bounds` are
unchanged, so single-sweep behavior is identical.
- executor.ts: add ExecuteOptions { restore?, bounds?, loop? }. Omitting
options reproduces the original single-sweep contract exactly.
- keeper.ts: in loop mode, run an infinite loopPath once (stopped only by
interruption) or chain a finite path cycle after cycle; force `reflect`
bounds for every pattern and suppress the between-cycle restore, so
line/diagonal bounce edge-to-edge instead of stopping at the first edge.
- config plumbing: new boolean `loop` through config.default.json,
config.ts, configFile.ts, cli.ts (-l/--loop), and move.ts, mirroring
the existing `verbose` precedence.
- docs: README loop-mode section + usage/validation updates; CHANGELOG
Unreleased entry.
- tests: loopPath generators, executor options (bounds override, loop
selection, restore suppression), config/configFile loop plumbing, and
keeper-level loop behavior (ramps far vs. bounded single-sweep, chained
cycles). 79 pass.
This commit is contained in:
@@ -133,6 +133,85 @@ describe("executePath — bounds policies", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("executePath — options", () => {
|
||||
test("restore:false leaves the cursor at the last step, no snap-back", async () => {
|
||||
const dev = new FakeDevice();
|
||||
const start = { x: 500, y: 500 };
|
||||
const pts = [
|
||||
{ x: 501, y: 500 },
|
||||
{ x: 502, y: 500 },
|
||||
];
|
||||
const outcome = await executePath(
|
||||
fixed(pts, "clamp"),
|
||||
ctxOf(start, dev.w, dev.h),
|
||||
dev,
|
||||
noopLog,
|
||||
cfgOf(),
|
||||
{ restore: false },
|
||||
);
|
||||
expect(outcome).toBe("completed");
|
||||
// No trailing restore-to-start command.
|
||||
expect(dev.commanded).toEqual(pts);
|
||||
});
|
||||
|
||||
test("the default (no options) still restores to start", async () => {
|
||||
const dev = new FakeDevice();
|
||||
const start = { x: 500, y: 500 };
|
||||
const pts = [{ x: 501, y: 500 }];
|
||||
await executePath(fixed(pts, "clamp"), ctxOf(start, dev.w, dev.h), dev, noopLog, cfgOf());
|
||||
expect(dev.commanded).toEqual([...pts, start]);
|
||||
});
|
||||
|
||||
test("bounds override supersedes the strategy's declared policy", async () => {
|
||||
const dev = new FakeDevice(100, 100);
|
||||
// Declared 'abort' would stop before this out-of-bounds point; the
|
||||
// 'reflect' override folds it back inside instead (span [2,97]:
|
||||
// x=120 -> 74) and the sweep completes.
|
||||
const strat = fixed([{ x: 120, y: 50 }], "abort");
|
||||
const outcome = await executePath(
|
||||
strat,
|
||||
ctxOf({ x: 50, y: 50 }, 100, 100),
|
||||
dev,
|
||||
noopLog,
|
||||
cfgOf(),
|
||||
{ bounds: "reflect", restore: false },
|
||||
);
|
||||
expect(outcome).toBe("completed");
|
||||
expect(dev.commanded[0]).toEqual({ x: 74, y: 50 });
|
||||
});
|
||||
|
||||
test("loop:true runs loopPath when present, path otherwise", async () => {
|
||||
const dev = new FakeDevice();
|
||||
// A strategy whose loopPath differs from its path, both finite here.
|
||||
const strat: MovementStrategy = {
|
||||
name: "dual",
|
||||
bounds: "clamp",
|
||||
*path(): Generator<Point> {
|
||||
yield { x: 1, y: 1 };
|
||||
},
|
||||
*loopPath(): Generator<Point> {
|
||||
yield { x: 10, y: 10 };
|
||||
yield { x: 20, y: 20 };
|
||||
},
|
||||
};
|
||||
await executePath(strat, ctxOf({ x: 0, y: 0 }, dev.w, dev.h), dev, noopLog, cfgOf(), {
|
||||
loop: true,
|
||||
restore: false,
|
||||
});
|
||||
expect(dev.commanded).toEqual([{ x: 10, y: 10 }, { x: 20, y: 20 }]);
|
||||
});
|
||||
|
||||
test("loop:true falls back to path when the strategy has no loopPath", async () => {
|
||||
const dev = new FakeDevice();
|
||||
const strat = fixed([{ x: 3, y: 3 }], "clamp");
|
||||
await executePath(strat, ctxOf({ x: 0, y: 0 }, dev.w, dev.h), dev, noopLog, cfgOf(), {
|
||||
loop: true,
|
||||
restore: false,
|
||||
});
|
||||
expect(dev.commanded).toEqual([{ x: 3, y: 3 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executePath — readback tolerance", () => {
|
||||
test("a readback within tolerance is not treated as interruption", async () => {
|
||||
const dev = new FakeDevice();
|
||||
|
||||
Reference in New Issue
Block a user