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, 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, }, }; }