Files
feuerwerk/src/editor/debug/vectorArtHooks.ts
T
adminandClaude Opus 5.5 a5ae21481b
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m9s
Type point values, snap and scrub the preview in the curve tab (#248)
Building a curve by mouse alone was fiddly: there was no way to put a
point on an exact value, no grid, and the running preview could not be
stopped at a chosen moment. The hand cursor over points also looked
like a link instead of a drawing tool.

- Number fields t and v in the header show the selected point. Typing
  or dragging on them goes through the same rules as a mouse drag, so
  points never overtake and handles stay clamped. One undo step each.
  t is greyed out on the two end points.
- The magnet button from Vector art snaps points and handles to a
  0.05 grid in t and v, when placing on free space and when dragging.
- Points and handles keep the default arrow in the curve tab and in
  Vector art. The pixel art palette dropped pointer as well, which is
  beyond the issue. Collider corners never had it.
- The preview now starts off. A vertical time line with a knob under
  the t axis sets t when dragged sideways. Dragging the dot on the
  rail jumps to the first t where the curve reaches that value, or to
  the closest one if it never does.
- The rules are pure functions in curveView.ts (withCurvePointSet,
  snappedToGrid, shareAtValue, previewSecondsAt), the canvas only
  calls them.

Tests:
- ./check.sh: 180 files, 2616 tests, CHECK EXIT 0
- GM_STEPS=1,47,53: ALL PASSED, 87 checks
- 4 new unit tests in curve-view.test.ts, 4 new e2e checks in 53
  (53h-53k) and 1 in 47, all with real mouse and keyboard
- Counter-probes bite: without "first t" 1 unit test fails, with
  pointer, no snap, no field commit, no scrub or preview on 5 browser
  checks fail, pointer in Vector art fails the new 47 check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:45:53 +02:00

82 lines
4.2 KiB
TypeScript

import { findAsset } from '../../core/model/Project';
import { currentGradientBar, currentShapeList } from '../inspector/shapeSection';
import { centreOf, snapButton, type HookContext } from './hookHelpers';
/** The Vector art tab (#129). */
export function vectorArtHooks({ state, editor }: HookContext) {
const { vectorArtEditor } = editor;
return {
/** The Vector art tab (#129, stage 3): where a canvas pixel is, and which tool a press uses. */
vectorArt: {
current: () => vectorArtEditor.currentAssetId,
info: () => vectorArtEditor.shownInfo,
pointAt: (x: number, y: number) => vectorArtEditor.canvasToScreen(x, y),
rect: () => vectorArtEditor.canvasRect(),
tool: () => vectorArtEditor.canvas.currentTool,
snapping: () => vectorArtEditor.canvas.snapping,
cursor: () => vectorArtEditor.canvas.cursor,
snapButton: () => snapButton(vectorArtEditor.snap),
/** Whether the pen has a line open that the next click would add a point to. */
drawing: () => vectorArtEditor.canvas.drawing,
/** The header's point type (#195): where to click it, what it says, and the type it reads. */
pointButton: () => ({
...centreOf(vectorArtEditor.pointButton),
text: vectorArtEditor.pointButton.text,
enabled: vectorArtEditor.pointButton.enabled,
kind: vectorArtEditor.canvas.pickedKind,
}),
/** The shapes as the model holds them, so a check can read points and handles exactly. */
shapes: (assetId: string) => {
const asset = findAsset(state.project, assetId);
if (!asset || asset.type !== 'vectorart') return null;
return asset.shapes.map((shape) => ({
id: shape.id,
name: shape.name,
fill: shape.fill,
opacity: shape.opacity,
stroke: shape.stroke ? (JSON.parse(JSON.stringify(shape.stroke)) as unknown) : null,
paint: shape.paint ? (JSON.parse(JSON.stringify(shape.paint)) as unknown) : null,
rings: shape.rings.map((ring) => ({
closed: ring.closed,
points: ring.points.map((point) => ({
x: point.x,
y: point.y,
in: point.in ? { ...point.in } : null,
out: point.out ? { ...point.out } : null,
mode: point.mode ?? null,
})),
})),
}));
},
selected: () => (state.selection?.kind === 'shape' ? state.selection.shapeId : ''),
selectedIds: () => state.selectedShapeIds,
/**
* The shape list in the inspector (#129, stage 4): the order it shows, top first, and where a
* row sits. A row carries a name that also stands in the tab, so a check cannot click it by text.
*/
shapeRows: () => currentShapeList()?.shownIds ?? null,
shapeRowAt: (shapeId: string) => currentShapeList()?.rowGlobal(shapeId) ?? null,
/** The cross on a shape's row. */
shapeRemoveAt: (shapeId: string) => currentShapeList()?.removeGlobal(shapeId) ?? null,
/** The duplicate mark on a shape's row, and where a double-click edits its name. */
shapeDuplicateAt: (shapeId: string) => currentShapeList()?.duplicateGlobal(shapeId) ?? null,
shapeNameAt: (shapeId: string) => currentShapeList()?.nameGlobal(shapeId) ?? null,
/** The field a shape's name is being edited in, with what it holds; null while none is. */
shapeRenaming: () => {
const field = currentShapeList()?.renameField;
return field ? { ...centreOf(field), value: field.value } : null;
},
/** The Paint tool (#186): its button, the "Handles" menu, and which part it shows. */
paintButton: () => centreOf(vectorArtEditor.paintButton),
paintTarget: () => ({
...centreOf(vectorArtEditor.paintTargetButton),
text: vectorArtEditor.paintTargetButton.text,
target: vectorArtEditor.canvas.paintTarget,
}),
/** A colour stop's marker, or the bar at an offset, in the inspector's fill or outline bar. */
stopAt: (part: 'fill' | 'stroke', index: number) => currentGradientBar(part)?.stopGlobal(index) ?? null,
barAt: (part: 'fill' | 'stroke', offset: number) => currentGradientBar(part)?.barGlobal(offset) ?? null,
},
};
}