Vector art points have four types and the pen draws whole curves (#195)
The pen only pulled out the outgoing handle of a new point, because the opposite handle was mirrored only if it already existed. The first segment of every dragged point was therefore half a curve. Beyond that bug, a point had no way to say how its handles relate. - A vector point has an optional mode: straight, corner, smooth or symmetric. Missing means corner. The mode is not part of the fingerprint, because it changes no pixel, only the next drag. - Dragging a handle follows the mode. Corner leaves the other handle alone, smooth turns it along and keeps its length, symmetric mirrors it exactly. - The pen pulls out both handles mirrored and marks the point symmetric. A plain click still sets a point without handles. - The tab header has a "Point: ..." menu for the last clicked or set point. That point is drawn filled. Changing its type is one undo step. Text instead of four icons, because the UI kit has no tooltips. - Switching a point without handles to a curved type first grows handles along its neighbours. Smooth and symmetric line the handles up and keep the rough direction of the curve. - A point inserted on a curve is smooth. Symmetric neighbours become smooth, because the split shortens one of their handles. - Alt-click still toggles between straight and symmetric. Found on the way: the old Alt rounding took the point at the other end of an open line as a neighbour, so its handles came out skewed. Fixed and held by a unit test. Existing points with handles now count as corner. Before, every drag mirrored. No pixel changes, only the next drag behaves differently. Tests: - ./check.sh: 129 files, 1917 tests, exit 0 (before 1903) - GM_STEPS=47, freshly seeded: ALL PASSED, 39 checks (before 32) - Check sum 1195 in README and wiki is computed (1188 + 7), CI measures it - Counter-checks: pen without 'symmetric' fails 3 checks, no smooth branch fails 1 unit test and 1 e2e check Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90627ccf47
commit
3ff7e91dbf
11 files changed
+487
-65
No files matched your search
+118
-1
@@ -13,7 +13,8 @@ import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
* the pen puts points down, the arrow drags one and takes one away, and what comes out is read from
|
||||
* the model, which is the only place a point's coordinates exist exactly. The last block, 47f,
|
||||
* walks the way a user walks instead: the asset comes out of the + menu, its row opens the tab,
|
||||
* and a sprite is given the drawing through its Texture row.
|
||||
* and a sprite is given the drawing through its Texture row. 47g draws on that asset to measure the
|
||||
* four point types of #195.
|
||||
*/
|
||||
|
||||
const CANVAS = 256;
|
||||
@@ -473,4 +474,120 @@ export async function vectorArt(t) {
|
||||
}),
|
||||
);
|
||||
await shot('47-picture-picker');
|
||||
|
||||
// 47g (#195). The pen draws a curve with two handles, and the header's point type steers what
|
||||
// a dragged handle does to the other one: a corner leaves it, a smooth point turns it round
|
||||
// at its own length, straight takes both away. Drawn on the asset 47f made, which is empty.
|
||||
await evaluate(`__gm.openDocument('asset:${madeId}')`);
|
||||
await settle();
|
||||
const pointsOf = async () =>
|
||||
(await json(`__gm.vectorArt.shapes(${JSON.stringify(madeId)})`)).at(-1)?.rings[0]?.points ?? [];
|
||||
const screenAt = (x, y) => json(`__gm.vectorArt.pointAt(${x}, ${y})`);
|
||||
const pointType = () => json('__gm.vectorArt.pointButton()');
|
||||
const before195 = await pointType();
|
||||
check(
|
||||
'with no point picked the point type reads as none and cannot be opened',
|
||||
before195.text === 'Point: –' && !before195.enabled,
|
||||
JSON.stringify(before195),
|
||||
);
|
||||
|
||||
await clickText('Pen');
|
||||
const A = await screenAt(60, 160);
|
||||
const B = await screenAt(128, 80);
|
||||
const pulledTo = await screenAt(168, 80);
|
||||
const C = await screenAt(196, 160);
|
||||
await click(A.x, A.y);
|
||||
await drag(B.x, B.y, pulledTo.x, pulledTo.y);
|
||||
await click(C.x, C.y);
|
||||
await click(A.x, A.y);
|
||||
const penPoints = await pointsOf();
|
||||
const curved = penPoints[1];
|
||||
check(
|
||||
'a drag with the pen grows both handles of the point, mirrored – a symmetric point',
|
||||
penPoints.length === 3 &&
|
||||
curved?.mode === 'symmetric' &&
|
||||
near(curved.out?.x, 40) &&
|
||||
near(curved.out?.y, 0) &&
|
||||
near(curved.in?.x, -40) &&
|
||||
near(curved.in?.y, 0),
|
||||
JSON.stringify(penPoints),
|
||||
);
|
||||
|
||||
await clickText('Select');
|
||||
await click(B.x, B.y);
|
||||
const picked = await pointType();
|
||||
check(
|
||||
'a click on that point picks it, and the header names its type',
|
||||
picked.text === 'Point: Symmetric' && picked.enabled && picked.kind === 'symmetric',
|
||||
JSON.stringify(picked),
|
||||
);
|
||||
|
||||
// The menu ticks its entries in the text itself, so a row reads "☐ Corner" or "☑ Corner".
|
||||
const openPointMenu = async () => {
|
||||
const button = await pointType();
|
||||
await click(button.x, button.y);
|
||||
return (await json('__gm.menuRows()')) ?? [];
|
||||
};
|
||||
const choose = async (label) => {
|
||||
const rows = await openPointMenu();
|
||||
const text = rows.find((row) => row.endsWith(` ${label}`));
|
||||
const row = text && (await inOverlay(text));
|
||||
if (!row) throw new Error(`no "${label}" in the point type menu: ${JSON.stringify(rows)}`);
|
||||
await click(row.x, row.y);
|
||||
await settle();
|
||||
};
|
||||
|
||||
const offered = await openPointMenu();
|
||||
check(
|
||||
'the point type menu offers the four types and ticks the one the point is',
|
||||
offered.join('|') === '☐ Straight|☐ Corner|☐ Smooth|☑ Symmetric',
|
||||
JSON.stringify(offered),
|
||||
);
|
||||
await press('Escape');
|
||||
await settle();
|
||||
const dragOut = async (x, y) => {
|
||||
const point = (await pointsOf())[1];
|
||||
const from = await screenAt(point.x + point.out.x, point.y + point.out.y);
|
||||
const to = await screenAt(x, y);
|
||||
await drag(from.x, from.y, to.x, to.y);
|
||||
return (await pointsOf())[1];
|
||||
};
|
||||
|
||||
await choose('Corner');
|
||||
const cusp = await dragOut(128, 40);
|
||||
check(
|
||||
'on a corner a dragged handle leaves the other one where it was',
|
||||
cusp.mode === null &&
|
||||
near(cusp.out.x, 0) &&
|
||||
near(cusp.out.y, -40) &&
|
||||
near(cusp.in.x, -40) &&
|
||||
near(cusp.in.y, 0),
|
||||
JSON.stringify(cusp),
|
||||
);
|
||||
|
||||
await choose('Smooth');
|
||||
const lined = (await pointsOf())[1];
|
||||
const smoothDragged = await dragOut(128 + 60, 80);
|
||||
const inLength = Math.hypot(smoothDragged.in.x, smoothDragged.in.y);
|
||||
check(
|
||||
'Smooth lines the handles up, and dragging one turns the other round at its own length',
|
||||
lined.mode === 'smooth' &&
|
||||
smoothDragged.mode === 'smooth' &&
|
||||
near(smoothDragged.out.x, 60) &&
|
||||
near(smoothDragged.out.y, 0) &&
|
||||
smoothDragged.in.x < 0 &&
|
||||
near(smoothDragged.in.y, 0) &&
|
||||
near(inLength, Math.hypot(lined.in.x, lined.in.y)) &&
|
||||
!near(inLength, 60),
|
||||
JSON.stringify({ lined, smoothDragged }),
|
||||
);
|
||||
await shot('47-point-smooth');
|
||||
|
||||
await choose('Straight');
|
||||
const straight = (await pointsOf())[1];
|
||||
check(
|
||||
'Straight takes both handles away, and the header says so',
|
||||
straight.in === null && straight.out === null && (await pointType()).text === 'Point: Straight',
|
||||
JSON.stringify({ straight, header: await pointType() }),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user