初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
BRUSH_MODE_NORMAL,
|
||||
BRUSH_MODE_PIXEL,
|
||||
PixelUndoHistory,
|
||||
brushModeSpec,
|
||||
brushStampPoints,
|
||||
clampCanvasView,
|
||||
clampFloatingButton,
|
||||
linePoints,
|
||||
normalizeBrushSize,
|
||||
restoreCanvasToolState,
|
||||
zoomCanvasView,
|
||||
} from "../app/static/canvas-tools-model.js";
|
||||
|
||||
test("normal and pixel modes expose independent ranges and presets", () => {
|
||||
assert.deepEqual(brushModeSpec(BRUSH_MODE_NORMAL).presets, [1, 2, 4, 8]);
|
||||
assert.deepEqual(brushModeSpec(BRUSH_MODE_PIXEL).presets, [1, 2, 3, 4]);
|
||||
assert.equal(normalizeBrushSize(99, BRUSH_MODE_NORMAL), 16);
|
||||
assert.equal(normalizeBrushSize(99, BRUSH_MODE_PIXEL), 4);
|
||||
assert.equal(normalizeBrushSize("bad", BRUSH_MODE_PIXEL), 1);
|
||||
});
|
||||
|
||||
test("pixel stamps are exact squares from 1x1 through 4x4", () => {
|
||||
for (let size = 1; size <= 4; size += 1) {
|
||||
const points = brushStampPoints(32, 32, size, BRUSH_MODE_PIXEL);
|
||||
assert.equal(points.length, size * size);
|
||||
assert.equal(new Set(points.map(({ x }) => x)).size, size);
|
||||
assert.equal(new Set(points.map(({ y }) => y)).size, size);
|
||||
}
|
||||
});
|
||||
|
||||
test("pixel stamps clip at every canvas edge", () => {
|
||||
const points = brushStampPoints(0, 0, 4, BRUSH_MODE_PIXEL);
|
||||
assert.equal(points.length, 9);
|
||||
assert.ok(points.every(({ x, y }) => x >= 0 && y >= 0 && x < 64 && y < 64));
|
||||
const opposite = brushStampPoints(63, 63, 4, BRUSH_MODE_PIXEL);
|
||||
assert.equal(opposite.length, 4);
|
||||
assert.ok(opposite.every(({ x, y }) => x >= 0 && y >= 0 && x < 64 && y < 64));
|
||||
assert.equal(brushStampPoints(0, 32, 4, BRUSH_MODE_PIXEL).length, 12);
|
||||
assert.equal(brushStampPoints(32, 0, 4, BRUSH_MODE_PIXEL).length, 12);
|
||||
});
|
||||
|
||||
test("normal circle geometry remains unchanged", () => {
|
||||
assert.equal(brushStampPoints(32, 32, 1, BRUSH_MODE_NORMAL).length, 1);
|
||||
assert.equal(brushStampPoints(32, 32, 2, BRUSH_MODE_NORMAL).length, 4);
|
||||
assert.equal(brushStampPoints(32, 32, 4, BRUSH_MODE_NORMAL).length, 12);
|
||||
});
|
||||
|
||||
test("line interpolation includes endpoints without coordinate gaps", () => {
|
||||
const points = linePoints({ x: 2, y: 3 }, { x: 19, y: 11 });
|
||||
assert.deepEqual(points[0], { x: 2, y: 3 });
|
||||
assert.deepEqual(points.at(-1), { x: 19, y: 11 });
|
||||
for (let index = 1; index < points.length; index += 1) {
|
||||
assert.ok(Math.abs(points[index].x - points[index - 1].x) <= 1);
|
||||
assert.ok(Math.abs(points[index].y - points[index - 1].y) <= 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("v1 tools migrate into normal mode while v2 restores both sizes", () => {
|
||||
assert.deepEqual(restoreCanvasToolState(null, {
|
||||
paintColor: "#123456",
|
||||
backgroundColor: "#654321",
|
||||
tool: "eraser",
|
||||
brushSize: 8,
|
||||
}), {
|
||||
paintColor: "#123456",
|
||||
backgroundColor: "#654321",
|
||||
tool: "eraser",
|
||||
pixelEditEnabled: false,
|
||||
normalBrushSize: 8,
|
||||
pixelBrushSize: 1,
|
||||
});
|
||||
assert.deepEqual(restoreCanvasToolState({
|
||||
tool: "brush",
|
||||
pixelEditEnabled: true,
|
||||
normalBrushSize: 12,
|
||||
pixelBrushSize: 4,
|
||||
}), {
|
||||
paintColor: null,
|
||||
backgroundColor: null,
|
||||
tool: "brush",
|
||||
pixelEditEnabled: true,
|
||||
normalBrushSize: 12,
|
||||
pixelBrushSize: 4,
|
||||
});
|
||||
assert.equal(restoreCanvasToolState("damaged", { brushSize: "bad" }).normalBrushSize, 1);
|
||||
});
|
||||
|
||||
test("pixel mode markup and grid stay accessible and non-interactive", async () => {
|
||||
const [index, style, view] = await Promise.all([
|
||||
readFile(new URL("../app/static/index.html", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/style.css", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/views/canvas.js", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(index, /id="pixel-edit-toggle"[^>]+aria-pressed="false"/);
|
||||
assert.match(index, /id="pixel-edit-status"[^>]+aria-live="polite"/);
|
||||
assert.match(index, /id="pixel-grid"[^>]+aria-hidden="true"[^>]+hidden/);
|
||||
assert.match(style, /\.pixel-grid\s*\{[^}]*pointer-events:\s*none/s);
|
||||
assert.match(style, /background-size:\s*calc\(100% \/ 64\) calc\(100% \/ 64\)/);
|
||||
assert.match(view, /pixelGrid\.hidden = !\(pixelEditEnabled && getInteractionMode\(\) === "pixels"\)/);
|
||||
});
|
||||
|
||||
test("pixel undo history stores changed snapshots, caps entries, and clears", () => {
|
||||
const history = new PixelUndoHistory(2);
|
||||
const first = Uint8Array.from([0, 0, 0]);
|
||||
assert.equal(history.record(first, first), false);
|
||||
assert.equal(history.size, 0);
|
||||
assert.equal(history.record(first, Uint8Array.from([1, 0, 0])), true);
|
||||
first[0] = 9;
|
||||
history.record(Uint8Array.from([1, 0, 0]), Uint8Array.from([2, 0, 0]));
|
||||
history.record(Uint8Array.from([2, 0, 0]), Uint8Array.from([3, 0, 0]));
|
||||
assert.equal(history.size, 2);
|
||||
assert.deepEqual(history.undo(), Uint8Array.from([2, 0, 0]));
|
||||
assert.deepEqual(history.undo(), Uint8Array.from([1, 0, 0]));
|
||||
assert.equal(history.undo(), null);
|
||||
history.record(Uint8Array.from([3]), Uint8Array.from([4]));
|
||||
history.clear();
|
||||
assert.equal(history.size, 0);
|
||||
});
|
||||
|
||||
test("canvas view clamps scale and keeps a visible section in the viewport", () => {
|
||||
const bounds = { viewportWidth: 320, viewportHeight: 568, baseSize: 296, minVisible: 48 };
|
||||
assert.deepEqual(clampCanvasView({ scale: 99, x: 9999, y: -9999 }, bounds), {
|
||||
scale: 8,
|
||||
x: 1296,
|
||||
y: -1420,
|
||||
});
|
||||
assert.deepEqual(clampCanvasView({ scale: 0, x: 0, y: 0 }, bounds), { scale: 1, x: 0, y: 0 });
|
||||
});
|
||||
|
||||
test("zooming around an anchor preserves its logical position", () => {
|
||||
const bounds = { viewportWidth: 400, viewportHeight: 600, baseSize: 360, minVisible: 48 };
|
||||
const view = { scale: 1, x: 0, y: 0 };
|
||||
const zoomed = zoomCanvasView(view, 2, { x: 300, y: 250 }, bounds);
|
||||
assert.deepEqual(zoomed, { scale: 2, x: -100, y: 50 });
|
||||
const logicalBefore = {
|
||||
x: (300 - 200 - view.x) / view.scale,
|
||||
y: (250 - 300 - view.y) / view.scale,
|
||||
};
|
||||
const logicalAfter = {
|
||||
x: (300 - 200 - zoomed.x) / zoomed.scale,
|
||||
y: (250 - 300 - zoomed.y) / zoomed.scale,
|
||||
};
|
||||
assert.deepEqual(logicalAfter, logicalBefore);
|
||||
});
|
||||
|
||||
test("floating editor button stays inside its safe viewport bounds", () => {
|
||||
assert.deepEqual(clampFloatingButton({ x: -20, y: 999 }, {
|
||||
viewportWidth: 320,
|
||||
viewportHeight: 568,
|
||||
size: 56,
|
||||
inset: 8,
|
||||
}), { x: 36, y: 532 });
|
||||
});
|
||||
|
||||
test("canvas editor markup, guarded input, and checkpoint events stay wired", async () => {
|
||||
const [index, style, canvasView, sceneBoard, templates, animations] = await Promise.all([
|
||||
readFile(new URL("../app/static/index.html", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/style.css", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/views/canvas.js", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/scene-board.js", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/views/templates.js", import.meta.url), "utf8"),
|
||||
readFile(new URL("../app/static/views/animations.js", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(index, /id="start-canvas-edit"[^>]*>开始编辑<\/button>/);
|
||||
assert.match(index, /id="canvas-assistive-toggle"[^>]+aria-expanded="false"/);
|
||||
assert.match(index, /id="undo-canvas-edit"[^>]+aria-disabled="true"/);
|
||||
assert.match(index, /id="move-canvas-view"/);
|
||||
assert.match(index, /id="continue-canvas-edit"/);
|
||||
assert.match(style, /body\.canvas-edit-active #board-shell\s*\{[^}]*position:\s*fixed/s);
|
||||
assert.match(style, /#scene-overlay\[data-interaction="pixels"\]:not\(\[data-canvas-editing="true"\]\)\s*\{[^}]*pointer-events:\s*none/s);
|
||||
assert.match(canvasView, /if \(!editorActive \|\| getInteractionMode\(\) !== "pixels"\) return/);
|
||||
assert.match(canvasView, /new PixelUndoHistory\(\)/);
|
||||
assert.match(sceneBoard, /matrix:scene-replaced/);
|
||||
assert.match(templates, /matrix:canvas-history-checkpoint/);
|
||||
assert.match(animations, /matrix:canvas-history-checkpoint/);
|
||||
});
|
||||
Reference in New Issue
Block a user