@pixerate/editor 0.137.0 → 0.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.cjs +2 -2
- package/dist/agent/index.d.cts +3 -3
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/index.cjs +57 -57
- package/dist/animation-editor/index.d.cts +6 -5
- package/dist/animation-editor/index.d.ts +6 -5
- package/dist/animation-editor/index.js +4 -4
- package/dist/{arrange-CKXK_hf7.d.cts → arrange-j_mLiFMO.d.cts} +1 -1
- package/dist/{arrange-BUREmgD9.d.ts → arrange-q5HQvnnB.d.ts} +1 -1
- package/dist/{bezier-Dywv3Bw8.d.ts → bezier-B6cK8SR6.d.ts} +2 -2
- package/dist/{bezier-CZ2AhjID.d.cts → bezier-CEJ8Uw4O.d.cts} +2 -2
- package/dist/{brushTypes-C7N3EBK7.d.cts → brushTypes-MX5UVs_m.d.cts} +27 -2
- package/dist/{brushTypes-C7N3EBK7.d.ts → brushTypes-MX5UVs_m.d.ts} +27 -2
- package/dist/{brushes-DJmUbIgz.d.ts → brushes-CQpL7_Lc.d.ts} +2 -107
- package/dist/{brushes-CnvZSYYU.d.cts → brushes-aEnjqoji.d.cts} +2 -107
- package/dist/{brushstrokeRender-IMFY3FF6.cjs → brushstrokeRender-PP6NTF6M.cjs} +52 -24
- package/dist/brushstrokeRender-PP6NTF6M.cjs.map +1 -0
- package/dist/{brushstrokeRender-2FAXA3XC.js → brushstrokeRender-QGEYJ4OZ.js} +40 -12
- package/dist/brushstrokeRender-QGEYJ4OZ.js.map +1 -0
- package/dist/{builtins-Cc-bH-3U.d.cts → builtins-BakDiKWf.d.cts} +3 -2
- package/dist/{builtins-ZU1GJnUA.d.ts → builtins-CE33vyv2.d.ts} +3 -2
- package/dist/canvas/index.cjs +16 -9
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +60 -14
- package/dist/canvas/index.d.ts +60 -14
- package/dist/canvas/index.js +10 -3
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-PRSRYHQD.cjs → chunk-3QHPFSQM.cjs} +36 -36
- package/dist/{chunk-PRSRYHQD.cjs.map → chunk-3QHPFSQM.cjs.map} +1 -1
- package/dist/{chunk-QBWEH3IM.cjs → chunk-3T5O225T.cjs} +391 -76
- package/dist/chunk-3T5O225T.cjs.map +1 -0
- package/dist/{chunk-EQQMGO42.js → chunk-C2J7GOMJ.js} +18 -5
- package/dist/chunk-C2J7GOMJ.js.map +1 -0
- package/dist/{chunk-2XS6LTUP.cjs → chunk-C46AZEBE.cjs} +104 -339
- package/dist/chunk-C46AZEBE.cjs.map +1 -0
- package/dist/{chunk-HUO3TI4C.js → chunk-CPSMARTX.js} +6 -241
- package/dist/chunk-CPSMARTX.js.map +1 -0
- package/dist/{chunk-KEENUAFN.cjs → chunk-DLLUOVQX.cjs} +18 -5
- package/dist/chunk-DLLUOVQX.cjs.map +1 -0
- package/dist/{chunk-LSXXAJG3.js → chunk-J7DSKTKB.js} +5 -4
- package/dist/chunk-J7DSKTKB.js.map +1 -0
- package/dist/{chunk-6S6362D2.js → chunk-NI6RORDS.js} +2 -2
- package/dist/{chunk-XN2NBLV5.cjs → chunk-PMEHFHSP.cjs} +82 -82
- package/dist/{chunk-XN2NBLV5.cjs.map → chunk-PMEHFHSP.cjs.map} +1 -1
- package/dist/{chunk-5FSRTP6E.js → chunk-PQFVLOMR.js} +319 -4
- package/dist/chunk-PQFVLOMR.js.map +1 -0
- package/dist/{chunk-OIBIRXRG.js → chunk-PSPLRQL6.js} +2 -2
- package/dist/{chunk-SYT2TLQF.js → chunk-QEIN6JQO.js} +2 -2
- package/dist/{chunk-3D77LOBM.js → chunk-TJBYBSTN.js} +51 -6
- package/dist/chunk-TJBYBSTN.js.map +1 -0
- package/dist/{chunk-DFSZO2B5.cjs → chunk-UFL5N4DK.cjs} +5 -4
- package/dist/chunk-UFL5N4DK.cjs.map +1 -0
- package/dist/{chunk-TLCTTQFF.cjs → chunk-VEHVXRJU.cjs} +17 -17
- package/dist/{chunk-TLCTTQFF.cjs.map → chunk-VEHVXRJU.cjs.map} +1 -1
- package/dist/{chunk-5OOIUXNJ.cjs → chunk-XFXHA5MU.cjs} +55 -10
- package/dist/chunk-XFXHA5MU.cjs.map +1 -0
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.d.cts +3 -3
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +1 -1
- package/dist/effects/index.d.cts +5 -4
- package/dist/effects/index.d.ts +5 -4
- package/dist/{export-DwnZ_hvQ.d.ts → export-0ZaTK__4.d.ts} +4 -4
- package/dist/{export-41rL6Cne.d.ts → export-C45v6MYq.d.ts} +1 -1
- package/dist/{export-CyJdfP_Z.d.cts → export-D-7WCDJ9.d.cts} +4 -4
- package/dist/{export-D9mXp05Q.d.cts → export-DSdCfd6O.d.cts} +1 -1
- package/dist/grain-BWmGZxP4.d.cts +58 -0
- package/dist/grain-BWmGZxP4.d.ts +58 -0
- package/dist/{helpers-v6GMazzg.d.cts → helpers-Cavy1TB7.d.cts} +21 -1
- package/dist/{helpers-C-RgeyvH.d.ts → helpers-LZQ4cpg4.d.ts} +21 -1
- package/dist/image-editor/index.cjs +695 -677
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +12 -11
- package/dist/image-editor/index.d.ts +12 -11
- package/dist/image-editor/index.js +23 -5
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{opentype-KR_jj6b_.d.cts → opentype-BpIJhGpj.d.cts} +1 -1
- package/dist/{opentype-a1KAW-qs.d.ts → opentype-CyxDScVz.d.ts} +1 -1
- package/dist/paint/index.cjs +21 -3
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +72 -5
- package/dist/paint/index.d.ts +72 -5
- package/dist/paint/index.js +20 -2
- package/dist/paint-editor/index.cjs +171 -171
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +77 -29
- package/dist/paint-editor/index.d.ts +77 -29
- package/dist/paint-editor/index.js +12 -12
- package/dist/{pathfinder-BHS4vQNm.d.cts → pathfinder-BMQEcS0k.d.cts} +2 -2
- package/dist/{pathfinder-BVvTIBMo.d.ts → pathfinder-CQIxubuP.d.ts} +2 -2
- package/dist/{pool-CZQOrSXZ.d.ts → pool-Cynuj89K.d.ts} +1 -1
- package/dist/{pool-FjZZmZ80.d.cts → pool-DedjxDKo.d.cts} +1 -1
- package/dist/{rand-8xbUb6ur.d.ts → rand-COS4_E1g.d.ts} +17 -1
- package/dist/{rand-Cw9VLrVC.d.cts → rand-D1JqpR-p.d.cts} +17 -1
- package/dist/{svg-DTb40ICU.d.cts → svg-RcixtBFR.d.cts} +2 -2
- package/dist/{svg-CSz3mzkw.d.ts → svg-nWBJEpnk.d.ts} +2 -2
- package/dist/{tools-Ca0kI1qo.d.cts → tools-CL82d7nw.d.cts} +1 -1
- package/dist/{tools-B0wXP5cF.d.ts → tools-DrxgDnXY.d.ts} +1 -1
- package/dist/vector/index.cjs +995 -955
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +44 -18
- package/dist/vector/index.d.ts +44 -18
- package/dist/vector/index.js +55 -15
- package/dist/vector/index.js.map +1 -1
- package/dist/video-editor/index.d.cts +5 -4
- package/dist/video-editor/index.d.ts +5 -4
- package/dist/{warp-DTOp0lON.d.cts → warp-CHi_P_G6.d.cts} +1 -1
- package/dist/{warp-pd7hrH4T.d.ts → warp-CPaHULUL.d.ts} +1 -1
- package/package.json +1 -1
- package/skills/pixerate-canvas/SKILL.md +12 -5
- package/skills/pixerate-vector/SKILL.md +4 -2
- package/dist/brushstrokeRender-2FAXA3XC.js.map +0 -1
- package/dist/brushstrokeRender-IMFY3FF6.cjs.map +0 -1
- package/dist/chunk-2XS6LTUP.cjs.map +0 -1
- package/dist/chunk-3D77LOBM.js.map +0 -1
- package/dist/chunk-5FSRTP6E.js.map +0 -1
- package/dist/chunk-5OOIUXNJ.cjs.map +0 -1
- package/dist/chunk-DFSZO2B5.cjs.map +0 -1
- package/dist/chunk-EQQMGO42.js.map +0 -1
- package/dist/chunk-HUO3TI4C.js.map +0 -1
- package/dist/chunk-KEENUAFN.cjs.map +0 -1
- package/dist/chunk-LSXXAJG3.js.map +0 -1
- package/dist/chunk-QBWEH3IM.cjs.map +0 -1
- /package/dist/{chunk-6S6362D2.js.map → chunk-NI6RORDS.js.map} +0 -0
- /package/dist/{chunk-OIBIRXRG.js.map → chunk-PSPLRQL6.js.map} +0 -0
- /package/dist/{chunk-SYT2TLQF.js.map → chunk-QEIN6JQO.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/builtins.ts"],"sourcesContent":["import type { HistoryState } from '../history/types.js';\nimport type { CanvasArrangeAction } from '../canvas/arrangeShortcuts.js';\nimport type { AlignEdge, DistributeAxis, DistributeMode, ZOrderOperation } from '../canvas/arrange.js';\nimport { DEFAULT_ARRANGE_SHORTCUTS } from '../canvas/arrangeShortcuts.js';\nimport type { CanvasBrush, CanvasBrushPaint, CanvasBrushPreset } from '../canvas/brushTypes.js';\nimport type { CommandParamsSchema, CommandSpec } from './registry.js';\nimport { ShortcutMap, type ShortcutBinding } from './shortcuts.js';\nimport type { KeyboardEventLike, Platform } from './chords.js';\n\n// ---------------------------------------------------------------------------\n// History\n// ---------------------------------------------------------------------------\n\n/** Minimal history surface (`HistoryManager` satisfies it). */\nexport interface HistoryCommandTarget {\n\tundo(): boolean | void;\n\tredo(): boolean | void;\n\tgetState(): Pick<HistoryState, 'canUndo' | 'canRedo'> & Partial<Pick<HistoryState, 'undoName' | 'redoName'>>;\n}\n\n/** Default history bindings: `Mod+Z` undo; `Mod+Shift+Z` redo, plus `Mod+Y` off macOS. */\nexport const DEFAULT_HISTORY_SHORTCUTS: readonly ShortcutBinding[] = [\n\t{ command: 'undo', chord: 'Mod+Z' },\n\t{ command: 'redo', chord: 'Mod+Shift+Z' },\n\t{ command: 'redo', chord: 'Mod+Y', platforms: ['windows', 'linux'] }\n];\n\nexport interface HistoryCommandOptions {\n\t/** Id prefix (default `history.`). */\n\tprefix?: string;\n\tgroup?: string;\n\tscope?: string;\n}\n\n/** `history.undo` / `history.redo` commands for a `HistoryManager` (disabled with a reason when empty). */\nexport function createHistoryCommands(history: HistoryCommandTarget, options: HistoryCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'history.';\n\tconst group = options.group ?? 'Edit';\n\tconst shortcutsOf = (command: 'undo' | 'redo') =>\n\t\tDEFAULT_HISTORY_SHORTCUTS.filter((b) => b.command === command).map(({ command: _c, ...rest }) => rest);\n\treturn [\n\t\t{\n\t\t\tid: `${prefix}undo`,\n\t\t\tlabel: 'Undo',\n\t\t\tgroup,\n\t\t\tkeywords: ['revert', 'back'],\n\t\t\tdescription: 'Reverts the last change.',\n\t\t\tshortcut: shortcutsOf('undo'),\n\t\t\t...(options.scope ? { scope: options.scope } : {}),\n\t\t\tenabled: () => (history.getState().canUndo ? true : 'Nothing to undo.'),\n\t\t\trun: () => {\n\t\t\t\thistory.undo();\n\t\t\t\treturn { undone: true, state: history.getState() };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tid: `${prefix}redo`,\n\t\t\tlabel: 'Redo',\n\t\t\tgroup,\n\t\t\tkeywords: ['repeat', 'forward'],\n\t\t\tdescription: 'Re-applies the last undone change.',\n\t\t\tshortcut: shortcutsOf('redo'),\n\t\t\t...(options.scope ? { scope: options.scope } : {}),\n\t\t\tenabled: () => (history.getState().canRedo ? true : 'Nothing to redo.'),\n\t\t\trun: () => {\n\t\t\t\thistory.redo();\n\t\t\t\treturn { redone: true, state: history.getState() };\n\t\t\t}\n\t\t}\n\t];\n}\n\nlet historyShortcutMap: ShortcutMap | null = null;\n\n/**\n * Resolves a keydown event to `undo` / `redo` with the default history bindings\n * (`Mod` is ⌘ on macOS and Ctrl elsewhere). Events from text inputs and\n * contenteditable elements are ignored unless `allowInEditable`.\n */\nexport function matchHistoryShortcut(\n\tevent: KeyboardEventLike,\n\toptions: { platform?: Platform; allowInEditable?: boolean } = {}\n): 'undo' | 'redo' | null {\n\thistoryShortcutMap ??= new ShortcutMap(DEFAULT_HISTORY_SHORTCUTS);\n\treturn (historyShortcutMap.resolve(event, options)?.command as 'undo' | 'redo' | undefined) ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Canvas arrange\n// ---------------------------------------------------------------------------\n\n/** Minimal arrange controller surface (`createCanvasArrangeController` satisfies it). */\nexport interface ArrangeCommandTarget {\n\talign(edge: AlignEdge, ids?: Iterable<string>): unknown;\n\tdistribute(axis: DistributeAxis, mode?: DistributeMode, ids?: Iterable<string>): unknown;\n\treorder(operation: ZOrderOperation, ids?: Iterable<string>): unknown;\n\tlock(ids?: Iterable<string>): unknown;\n\tunlock(ids?: Iterable<string>): unknown;\n}\n\nexport interface ArrangeCommandOptions {\n\t/** Id prefix (default `canvas.`). */\n\tprefix?: string;\n\tgroup?: string;\n\t/** Scope of the commands and their shortcuts (default `canvas`). Pass `null` for global. */\n\tscope?: string | null;\n\t/** Current selection, used for enablement (\"Select at least two nodes\"). */\n\tgetSelectedIds?: () => string[];\n\t/** Scene navigation (`nextScene` / `previousScene`); those commands are only added when given. */\n\tscenes?: { next(): unknown; previous(): unknown };\n\t/** Use these bindings instead of `DEFAULT_ARRANGE_SHORTCUTS`. */\n\tbindings?: readonly { action: CanvasArrangeAction; label: string }[];\n}\n\nconst ARRANGE_LABELS: Record<CanvasArrangeAction, string> = {\n\talignLeft: 'Align Left',\n\talignCenter: 'Align Horizontal Centers',\n\talignRight: 'Align Right',\n\talignTop: 'Align Top',\n\talignMiddle: 'Align Vertical Centers',\n\talignBottom: 'Align Bottom',\n\tdistributeHorizontal: 'Distribute Horizontally',\n\tdistributeVertical: 'Distribute Vertically',\n\tlock: 'Lock',\n\tunlock: 'Unlock',\n\tbringForward: 'Bring Forward',\n\tsendBackward: 'Send Backward',\n\tbringToFront: 'Bring to Front',\n\tsendToBack: 'Send to Back',\n\tnextScene: 'Next Scene',\n\tpreviousScene: 'Previous Scene'\n};\n\nconst IDS_PARAMS: CommandParamsSchema = {\n\ttype: 'object',\n\tproperties: {\n\t\tids: {\n\t\t\ttype: 'array',\n\t\t\titems: { type: 'string' },\n\t\t\tdescription: 'Node ids to act on (default: the selection).'\n\t\t}\n\t}\n};\n\n/**\n * Commands for the canvas arrange actions (`canvas.alignLeft`, `canvas.bringToFront`, …)\n * with the default arrange shortcuts. Each accepts `{ ids? }`; enablement checks the\n * named ids, or the selection when none are named.\n */\nexport function createArrangeCommands(controller: ArrangeCommandTarget, options: ArrangeCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'canvas.';\n\tconst group = options.group ?? 'Arrange';\n\tconst scope = options.scope === null ? undefined : options.scope ?? 'canvas';\n\tconst bindings = options.bindings ?? DEFAULT_ARRANGE_SHORTCUTS;\n\tconst count = (params?: { ids?: string[] }) => params?.ids?.length ?? options.getSelectedIds?.().length;\n\tconst needs = (min: number, what: string) => (_: unknown, params?: { ids?: string[] }) => {\n\t\tconst n = count(params);\n\t\tif (n === undefined || n >= min) return true;\n\t\treturn min === 1 ? `Select a ${what} first.` : `Select at least ${min} ${what}s.`;\n\t};\n\n\tconst run: Record<CanvasArrangeAction, (ids?: string[]) => unknown> = {\n\t\talignLeft: (ids) => controller.align('left', ids),\n\t\talignCenter: (ids) => controller.align('center', ids),\n\t\talignRight: (ids) => controller.align('right', ids),\n\t\talignTop: (ids) => controller.align('top', ids),\n\t\talignMiddle: (ids) => controller.align('middle', ids),\n\t\talignBottom: (ids) => controller.align('bottom', ids),\n\t\tdistributeHorizontal: (ids) => controller.distribute('horizontal', 'gaps', ids),\n\t\tdistributeVertical: (ids) => controller.distribute('vertical', 'gaps', ids),\n\t\tlock: (ids) => controller.lock(ids),\n\t\tunlock: (ids) => controller.unlock(ids),\n\t\tbringForward: (ids) => controller.reorder('bringForward', ids),\n\t\tsendBackward: (ids) => controller.reorder('sendBackward', ids),\n\t\tbringToFront: (ids) => controller.reorder('bringToFront', ids),\n\t\tsendToBack: (ids) => controller.reorder('sendToBack', ids),\n\t\tnextScene: () => options.scenes?.next(),\n\t\tpreviousScene: () => options.scenes?.previous()\n\t};\n\n\tconst actions = Object.keys(ARRANGE_LABELS) as CanvasArrangeAction[];\n\treturn actions\n\t\t.filter((action) => options.scenes || (action !== 'nextScene' && action !== 'previousScene'))\n\t\t.map((action): CommandSpec => {\n\t\t\tconst isScene = action === 'nextScene' || action === 'previousScene';\n\t\t\tconst enabled = isScene\n\t\t\t\t? undefined\n\t\t\t\t: action.startsWith('align')\n\t\t\t\t\t? needs(2, 'node')\n\t\t\t\t\t: action.startsWith('distribute')\n\t\t\t\t\t\t? needs(3, 'node')\n\t\t\t\t\t\t: needs(1, 'node');\n\t\t\treturn {\n\t\t\t\tid: `${prefix}${action}`,\n\t\t\t\tlabel: ARRANGE_LABELS[action],\n\t\t\t\tgroup: isScene ? 'Scenes' : group,\n\t\t\t\tkeywords: isScene ? ['scene', 'slide', 'page'] : action.startsWith('align') ? ['align'] : ['arrange', 'layer', 'order'],\n\t\t\t\t...(scope ? { scope } : {}),\n\t\t\t\tshortcut: bindings.filter((b) => b.action === action).map((b) => b.label),\n\t\t\t\t...(isScene ? {} : { params: IDS_PARAMS }),\n\t\t\t\t...(enabled ? { enabled } : {}),\n\t\t\t\trun: (_ctx: unknown, params: { ids?: string[] }) => run[action](params?.ids)\n\t\t\t};\n\t\t});\n}\n\n// ---------------------------------------------------------------------------\n// Canvas shape operations\n// ---------------------------------------------------------------------------\n\n/** Shape operations surface of the arrange controller (`createCanvasArrangeController` satisfies it). */\nexport interface ShapeCommandTarget {\n\tcombineShapes(operation: 'unite' | 'intersect' | 'subtract' | 'exclude' | 'divide', ids?: Iterable<string>): unknown;\n\tcanCombineShapes(ids?: Iterable<string>): boolean;\n\toffsetShapes(distance: number, ids?: Iterable<string>, options?: { join?: 'miter' | 'round' | 'bevel'; miterLimit?: number }): unknown;\n\tcanOffsetShapes(ids?: Iterable<string>): boolean;\n\toutlineStroke(\n\t\tids?: Iterable<string>,\n\t\toptions?: { join?: 'miter' | 'round' | 'bevel'; cap?: 'butt' | 'round' | 'square'; miterLimit?: number }\n\t): unknown;\n\tcanOutlineStroke(ids?: Iterable<string>): boolean;\n}\n\nexport interface ShapeCommandOptions {\n\t/** Id prefix (default `canvas.`). */\n\tprefix?: string;\n\tgroup?: string;\n\t/** Scope (default `canvas`). Pass `null` for global. */\n\tscope?: string | null;\n\t/** Offset distance used when `canvas.offsetPath` is run without one (default 10). */\n\tdefaultOffset?: number;\n}\n\nconst SHAPE_COMMANDS = [\n\t{ op: 'unite', label: 'Unite Shapes', keywords: ['union', 'merge', 'boolean', 'combine'] },\n\t{ op: 'intersect', label: 'Intersect Shapes', keywords: ['intersection', 'boolean', 'combine'] },\n\t{ op: 'subtract', label: 'Subtract Shapes', keywords: ['difference', 'minus front', 'boolean', 'combine'] },\n\t{ op: 'exclude', label: 'Exclude Shapes', keywords: ['xor', 'boolean', 'combine'] },\n\t{ op: 'divide', label: 'Divide Shapes', keywords: ['split', 'pathfinder', 'boolean'] }\n] as const;\n\nconst JOIN_PARAM = { type: 'string', enum: ['miter', 'round', 'bevel'], description: 'Corner style (default miter).' } as const;\n\n/**\n * Palette / context-menu / agent commands for the shape operations: `canvas.unite`,\n * `canvas.intersect`, `canvas.subtract`, `canvas.exclude`, `canvas.divide`,\n * `canvas.offsetPath` (`{ distance, join?, miterLimit?, ids? }`) and `canvas.outlineStroke`\n * (`{ join?, cap?, miterLimit?, ids? }`). Each is one undoable step when the controller has a\n * history; enablement explains why a command cannot run.\n */\nexport function createShapeCommands(controller: ShapeCommandTarget, options: ShapeCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'canvas.';\n\tconst group = options.group ?? 'Shape';\n\tconst scope = options.scope === null ? undefined : options.scope ?? 'canvas';\n\tconst base = { group, ...(scope ? { scope } : {}) };\n\tconst combine = SHAPE_COMMANDS.map(\n\t\t({ op, label, keywords }): CommandSpec => ({\n\t\t\t...base,\n\t\t\tid: `${prefix}${op}`,\n\t\t\tlabel,\n\t\t\tkeywords: [...keywords],\n\t\t\tdescription: `${label.split(' ')[0]}s the selected shapes into a new path (curves are kept).`,\n\t\t\tparams: IDS_PARAMS,\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) =>\n\t\t\t\tcontroller.canCombineShapes(params?.ids) ? true : 'Select at least two unlocked shapes.',\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] }) => controller.combineShapes(op, params?.ids)\n\t\t})\n\t);\n\treturn [\n\t\t...combine,\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}offsetPath`,\n\t\t\tlabel: 'Offset Path…',\n\t\t\tkeywords: ['inset', 'outset', 'grow', 'shrink', 'expand', 'contract'],\n\t\t\tdescription: 'Grows (positive distance) or shrinks (negative) the selected shapes.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tdistance: { type: 'number', description: `Offset in canvas units (default ${options.defaultOffset ?? 10}).` },\n\t\t\t\t\tjoin: JOIN_PARAM,\n\t\t\t\t\tmiterLimit: { type: 'number', description: 'Miter limit (default 4).' },\n\t\t\t\t\tids: IDS_PARAMS.properties!.ids\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) =>\n\t\t\t\tcontroller.canOffsetShapes(params?.ids) ? true : 'Select an unlocked shape.',\n\t\t\trun: (_ctx: unknown, params: { distance?: number; join?: 'miter' | 'round' | 'bevel'; miterLimit?: number; ids?: string[] }) =>\n\t\t\t\tcontroller.offsetShapes(params?.distance ?? options.defaultOffset ?? 10, params?.ids, {\n\t\t\t\t\t...(params?.join ? { join: params.join } : {}),\n\t\t\t\t\t...(params?.miterLimit !== undefined ? { miterLimit: params.miterLimit } : {})\n\t\t\t\t})\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}outlineStroke`,\n\t\t\tlabel: 'Outline Stroke',\n\t\t\tkeywords: ['expand stroke', 'stroke to path', 'convert stroke'],\n\t\t\tdescription: 'Converts the strokes of the selected shapes to filled shapes.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tjoin: JOIN_PARAM,\n\t\t\t\t\tcap: { type: 'string', enum: ['butt', 'round', 'square'], description: 'Line cap (default butt).' },\n\t\t\t\t\tmiterLimit: { type: 'number', description: 'Miter limit (default 4).' },\n\t\t\t\t\tids: IDS_PARAMS.properties!.ids\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) =>\n\t\t\t\tcontroller.canOutlineStroke(params?.ids) ? true : 'Select an unlocked shape with a stroke.',\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: { join?: 'miter' | 'round' | 'bevel'; cap?: 'butt' | 'round' | 'square'; miterLimit?: number; ids?: string[] }\n\t\t\t) =>\n\t\t\t\tcontroller.outlineStroke(params?.ids, {\n\t\t\t\t\t...(params?.join ? { join: params.join } : {}),\n\t\t\t\t\t...(params?.cap ? { cap: params.cap } : {}),\n\t\t\t\t\t...(params?.miterLimit !== undefined ? { miterLimit: params.miterLimit } : {})\n\t\t\t\t})\n\t\t}\n\t];\n}\n\n// ---------------------------------------------------------------------------\n// Canvas freehand drawing\n// ---------------------------------------------------------------------------\n\n/** Draw surface of the freehand draw controller (`createCanvasDrawController` satisfies it). */\nexport interface DrawCommandTarget {\n\tdrawStroke(input: {\n\t\tpoints: readonly (readonly [number, number])[];\n\t\ttool?: 'pen' | 'highlighter';\n\t\tstyle?: { color?: string; width?: number; opacity?: number; brush?: CanvasBrush; paint?: CanvasBrushPaint };\n\t\tpressure?: readonly number[];\n\t\ttiltX?: readonly number[];\n\t\ttiltY?: readonly number[];\n\t\tstreamline?: boolean;\n\t}): Promise<{ id: string; bounds: { x: number; y: number; width: number; height: number }; brush?: unknown }>;\n\t/** The dry paint presets a stroke can be brushed with (loads the paint engine). */\n\tlistBrushes(): Promise<CanvasBrushPreset[]>;\n}\n\nexport interface DrawCommandOptions {\n\t/** Id prefix (default `canvas.`). */\n\tprefix?: string;\n\tgroup?: string;\n\t/** Scope of the commands (default `canvas`). Pass `null` for global. */\n\tscope?: string | null;\n\t/** Refuse drawing with this reason (e.g. a read-only canvas); `canvas.listBrushes` still runs. */\n\treadOnlyReason?: () => string | null | undefined;\n}\n\nconst PROFILE_PARAM = (what: string, lo: number, hi: number) =>\n\t({\n\t\ttype: 'array',\n\t\tminItems: 1,\n\t\tmaxItems: 256,\n\t\titems: { type: 'number', minimum: lo, maximum: hi },\n\t\tdescription: `${what} spread evenly along the stroke's length: the first value at the start, the last at the end, linearly in between; one value is constant. Only with a brush.`\n\t}) as const;\n\nconst BRUSH_SETTINGS_PARAM = {\n\ttype: 'object',\n\tadditionalProperties: false,\n\tdescription: 'Tip settings instead of a preset.',\n\tproperties: {\n\t\tsize: { type: 'number', minimum: 1, maximum: 2500, description: 'Brush diameter in canvas units (default: the stroke width).' },\n\t\thardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness, 0 soft to 1 hard (default 1).' },\n\t\tspacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n\t\tangle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n\t\troundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness, 1 round to 0.01 flat (default 1).' },\n\t\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity ceiling of the stroke 0..1 (default 1).' },\n\t\tflow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab 0..1 (default 1).' },\n\t\tpressureSize: { type: 'boolean', description: 'Pressure scales the size (default true).' },\n\t\tpressureOpacity: { type: 'boolean', description: 'Pressure scales the opacity (default false).' }\n\t}\n} as const;\n\nconst HEX_PARAM = '^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$';\n\n/**\n * Gradient paint for a brushed stroke: the paint engine's `PaintSource` described structurally,\n * so commands never import the paint module (see GOTCHAS: the DTS build runs out of memory).\n * The canvas renderer validates it again (sorted stops, defaults) before anything is drawn.\n */\nconst BRUSH_PAINT_PARAM = {\n\tdescription:\n\t\t'Paint the brushed stroke with gradient paint (or one colour) instead of `color`, sampled dab by dab along the path; `color` still gives the alpha and fills \"paint\" stops. Only with `brush`. Example (blue to yellow over 200 canvas units, and back): { \"type\": \"gradient\", \"stops\": [{ \"offset\": 0, \"color\": \"#1d4ed8\" }, { \"offset\": 1, \"color\": \"#facc15\" }], \"map\": { \"by\": \"distance\", \"length\": 200, \"repeat\": \"mirror\" } }.',\n\toneOf: [\n\t\t{\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\trequired: ['type', 'color'],\n\t\t\tdescription: 'One colour.',\n\t\t\tproperties: {\n\t\t\t\ttype: { type: 'string', enum: ['color'], description: 'Always \"color\".' },\n\t\t\t\tcolor: { type: 'string', pattern: HEX_PARAM, description: 'The colour as #rrggbb (or #rgb).' }\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\trequired: ['type', 'stops'],\n\t\t\tdescription: 'Gradient paint: the brush samples it dab by dab as the stroke goes.',\n\t\t\tproperties: {\n\t\t\t\ttype: { type: 'string', enum: ['gradient'], description: 'Always \"gradient\".' },\n\t\t\t\tstops: {\n\t\t\t\t\ttype: 'array',\n\t\t\t\t\tminItems: 2,\n\t\t\t\t\tmaxItems: 16,\n\t\t\t\t\tdescription: '2..16 stops sorted by offset.',\n\t\t\t\t\titems: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\t\trequired: ['offset', 'color'],\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\toffset: { type: 'number', minimum: 0, maximum: 1, description: 'Position 0..1 along the gradient.' },\n\t\t\t\t\t\t\tcolor: {\n\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\tdescription: 'The stop colour as #rrggbb, or \"paint\" (the stroke colour) or \"secondary\" (white).',\n\t\t\t\t\t\t\t\tanyOf: [{ pattern: HEX_PARAM }, { enum: ['paint', 'secondary'] }]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Multiplies the brush flow where this colour is laid, 0..1 (default 1).' },\n\t\t\t\t\t\t\tmidpoint: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'Where between this stop and the next the colours are half mixed, 0.05..0.95 (default 0.5).' }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tspace: { type: 'string', enum: ['pigment', 'oklab', 'srgb'], description: 'How neighbouring stops mix: pigment (default; like paint, blue to yellow passes green), oklab (perceptual) or srgb (plain numbers).' },\n\t\t\t\tmap: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\trequired: ['by'],\n\t\t\t\t\tdescription: 'What moves the stroke along the gradient (default distance, 300 canvas units, mirror).',\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tby: {\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\tenum: ['distance', 'pressure', 'tilt', 'direction', 'across-tip', 'random'],\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t'distance: canvas units (or dabs) travelled along the path; pressure: the pressure profile 0..1; tilt: pen lean from the tilt profiles, upright 0 .. flat 1; direction: direction of travel, once round per 360°; across-tip: left to right across the tip (a brush loaded with several colours); random: a seeded random point per dab. Canvas strokes carry no timing, so velocity and time are not available.'\n\t\t\t\t\t\t},\n\t\t\t\t\t\tlength: { type: 'number', minimum: 1, maximum: 100000, description: 'distance: canvas units (or dabs, with unit \"dabs\") per run through the gradient (default 300).' },\n\t\t\t\t\t\trepeat: { type: 'string', enum: ['clamp', 'repeat', 'mirror'], description: 'distance: past the end, clamp (hold the last colour), repeat (start again) or mirror (run back; default).' },\n\t\t\t\t\t\tunit: { type: 'string', enum: ['px', 'dabs'], description: 'distance: canvas units (\"px\", default) or dabs.' },\n\t\t\t\t\t\tevery: { type: 'integer', minimum: 1, maximum: 500, description: 'random: dabs per random pick (default 1).' }\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tsoftness: { type: 'number', minimum: 0, maximum: 1, description: 'How softly neighbouring stops blend: 1 smooth (default) .. 0 hard steps.' }\n\t\t\t}\n\t\t}\n\t]\n} as const;\n\n/**\n * Agent / palette commands for freehand drawing: `canvas.drawFreehand` (a pen or highlighter\n * stroke through given points, optionally brushed with a dry paint preset and a pressure / tilt\n * profile; one undo step) and the read-only `canvas.listBrushes` (the dry presets it accepts).\n */\nexport function createDrawCommands(target: DrawCommandTarget, options: DrawCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'canvas.';\n\tconst group = options.group ?? 'Draw';\n\tconst scope = options.scope === null ? undefined : options.scope ?? 'canvas';\n\tconst base = { group, ...(scope ? { scope } : {}) };\n\treturn [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}drawFreehand`,\n\t\t\tlabel: 'Draw Freehand Stroke',\n\t\t\tkeywords: ['pen', 'highlighter', 'brush', 'sketch', 'draw', 'stroke', 'pencil', 'ink'],\n\t\t\tdescription:\n\t\t\t\t'Draws a freehand pen or highlighter stroke through points (canvas coordinates) as one path shape node, in one undo step. With `brush` it is painted with a dry paint brush (a preset id from canvas.listBrushes, or tip settings) and `pressure` / `tiltX` / `tiltY` profiles shape it along its length; `paint` gives it gradient paint that changes colour along the stroke. Wet brushes are refused. Returns the new node `id`, its `bounds` and the stored `brush`.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tadditionalProperties: false,\n\t\t\t\trequired: ['points'],\n\t\t\t\tproperties: {\n\t\t\t\t\tpoints: {\n\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\tminItems: 1,\n\t\t\t\t\t\tmaxItems: 5000,\n\t\t\t\t\t\titems: { type: 'array', minItems: 2, maxItems: 2, items: { type: 'number' } },\n\t\t\t\t\t\tdescription: 'Stroke points as [x, y] pairs in canvas (flow) units, in drawing order. Use a point every few units along curves.'\n\t\t\t\t\t},\n\t\t\t\t\ttool: { type: 'string', enum: ['pen', 'highlighter'], description: 'Pen (default: blue, width 3) or highlighter (yellow, width 24, opacity 0.35); sets the defaults below.' },\n\t\t\t\t\tcolor: { type: 'string', description: 'Stroke colour, any CSS colour (default from the tool).' },\n\t\t\t\t\twidth: { type: 'number', exclusiveMinimum: 0, maximum: 2500, description: 'Stroke width in canvas units; the brush size for brushed strokes (default from the tool).' },\n\t\t\t\t\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity 0..1 (default from the tool).' },\n\t\t\t\t\tbrush: {\n\t\t\t\t\t\tdescription: 'Paint the stroke with a dry paint brush: a preset id such as \"ink-pen\", \"pencil-hb\", \"brush-pen\", \"charcoal-stick\", \"soft-pastel\", \"marker\" or \"airbrush\" (all ids: canvas.listBrushes), or tip settings. Omit for a plain vector stroke.',\n\t\t\t\t\t\toneOf: [{ type: 'string', minLength: 1 }, BRUSH_SETTINGS_PARAM]\n\t\t\t\t\t},\n\t\t\t\t\tpaint: BRUSH_PAINT_PARAM,\n\t\t\t\t\tpressure: PROFILE_PARAM('Pen pressure 0..1', 0, 1),\n\t\t\t\t\ttiltX: PROFILE_PARAM('Pen tilt along x in degrees (−90..90)', -90, 90),\n\t\t\t\t\ttiltY: PROFILE_PARAM('Pen tilt along y in degrees (−90..90)', -90, 90),\n\t\t\t\t\tstreamline: { type: 'boolean', description: 'Smooth jittery points before fitting the path (default false).' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: () => options.readOnlyReason?.() || true,\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: {\n\t\t\t\t\tpoints: [number, number][];\n\t\t\t\t\ttool?: 'pen' | 'highlighter';\n\t\t\t\t\tcolor?: string;\n\t\t\t\t\twidth?: number;\n\t\t\t\t\topacity?: number;\n\t\t\t\t\tbrush?: CanvasBrush;\n\t\t\t\t\tpaint?: CanvasBrushPaint;\n\t\t\t\t\tpressure?: number[];\n\t\t\t\t\ttiltX?: number[];\n\t\t\t\t\ttiltY?: number[];\n\t\t\t\t\tstreamline?: boolean;\n\t\t\t\t}\n\t\t\t) =>\n\t\t\t\ttarget.drawStroke({\n\t\t\t\t\tpoints: params.points,\n\t\t\t\t\t...(params.tool ? { tool: params.tool } : {}),\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\t...(params.color !== undefined ? { color: params.color } : {}),\n\t\t\t\t\t\t...(params.width !== undefined ? { width: params.width } : {}),\n\t\t\t\t\t\t...(params.opacity !== undefined ? { opacity: params.opacity } : {}),\n\t\t\t\t\t\t...(params.brush !== undefined ? { brush: params.brush } : {}),\n\t\t\t\t\t\t...(params.paint !== undefined ? { paint: params.paint } : {})\n\t\t\t\t\t},\n\t\t\t\t\t...(params.pressure ? { pressure: params.pressure } : {}),\n\t\t\t\t\t...(params.tiltX ? { tiltX: params.tiltX } : {}),\n\t\t\t\t\t...(params.tiltY ? { tiltY: params.tiltY } : {}),\n\t\t\t\t\t...(params.streamline ? { streamline: true } : {})\n\t\t\t\t})\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listBrushes`,\n\t\t\tlabel: 'List Freehand Brushes',\n\t\t\tkeywords: ['brushes', 'presets', 'paint', 'pencil', 'ink'],\n\t\t\tdescription: 'Read-only: the dry paint presets canvas.drawFreehand accepts as `brush`, as { id, name, category, size } (size is the preset\\'s own diameter; the stroke width replaces it).',\n\t\t\tparams: { type: 'object', additionalProperties: false, properties: {} },\n\t\t\treadOnly: true,\n\t\t\trun: async () => ({ brushes: await target.listBrushes() })\n\t\t}\n\t];\n}\n\n// ---------------------------------------------------------------------------\n// Canvas components and instances\n// ---------------------------------------------------------------------------\n\ntype ComponentActionResult = { ok: true; ids: string[] } | { ok: false; reason: string };\n\n/** Components surface of the components controller (`createCanvasComponentsController` satisfies it). */\nexport interface ComponentCommandTarget {\n\tcreateComponent(ids?: Iterable<string>): ComponentActionResult;\n\tcanCreateComponent(ids?: Iterable<string>): true | string;\n\tcombineAsVariants(ids?: Iterable<string>): ComponentActionResult;\n\tcanCombineAsVariants(ids?: Iterable<string>): true | string;\n\tcreateInstance(componentId: string, options?: { parentId?: string; position?: { x: number; y: number }; properties?: Record<string, boolean | string> }): ComponentActionResult;\n\tcanCreateInstance(componentId?: string, parentId?: string): true | string;\n\tdetachInstance(ids?: Iterable<string>): ComponentActionResult;\n\tcanDetachInstance(ids?: Iterable<string>): true | string;\n\tresetOverrides(id?: string, group?: string): ComponentActionResult;\n\tcanResetOverrides(id?: string): true | string;\n\tpushOverrides(id?: string): ComponentActionResult;\n\tcanPushOverrides(id?: string): true | string;\n\tswapInstance(componentId: string, ids?: Iterable<string>): ComponentActionResult;\n\tcanSwapInstance(componentId?: string, ids?: Iterable<string>): true | string;\n\tsetInstanceProperties(properties: Record<string, unknown>, ids?: Iterable<string>): ComponentActionResult;\n\tcanSetInstanceProperties(ids?: Iterable<string>): true | string;\n\trestoreMainComponent(id?: string): ComponentActionResult;\n\tcanRestoreMainComponent(id?: string): true | string;\n\tgoToMainComponent(id?: string): string | undefined;\n\tcanGoToMainComponent(id?: string): true | string;\n\tlist(): unknown;\n\tinspect(id?: string): unknown;\n\t// Variant editing (optional so older targets keep type-checking; the controller has them all).\n\taddVariant?(\n\t\tsetId?: string,\n\t\toptions?: { from?: string; variant?: Record<string, string>; position?: { x: number; y: number }; defaultValue?: string }\n\t): ComponentActionResult;\n\tcanAddVariant?(setId?: string, options?: { from?: string; variant?: Record<string, string> }): true | string;\n\tremoveVariant?(variantId?: string): ComponentActionResult;\n\tcanRemoveVariant?(variantId?: string): true | string;\n\tsetVariantProperties?(values: Record<string, string>, variantId?: string, options?: { defaultValue?: string }): ComponentActionResult;\n\tcanSetVariantProperties?(values?: Record<string, string>, variantId?: string): true | string;\n\trenameVariantProperty?(name: string, newName: string, setId?: string): ComponentActionResult;\n\tcanRenameVariantProperty?(name?: string, newName?: string, setId?: string): true | string;\n\trenameVariantValue?(property: string, value: string, newValue: string, setId?: string): ComponentActionResult;\n\tcanRenameVariantValue?(property?: string, value?: string, newValue?: string, setId?: string): true | string;\n}\n\n/** The variant-editing part of a `ComponentCommandTarget` (present on the components controller). */\ntype VariantCommandTarget = Required<\n\tPick<\n\t\tComponentCommandTarget,\n\t\t| 'addVariant'\n\t\t| 'canAddVariant'\n\t\t| 'removeVariant'\n\t\t| 'canRemoveVariant'\n\t\t| 'setVariantProperties'\n\t\t| 'canSetVariantProperties'\n\t\t| 'renameVariantProperty'\n\t\t| 'canRenameVariantProperty'\n\t\t| 'renameVariantValue'\n\t\t| 'canRenameVariantValue'\n\t>\n>;\n\n/** Whether a command target can edit variants. */\nexport function hasVariantCommands(controller: ComponentCommandTarget): controller is ComponentCommandTarget & VariantCommandTarget {\n\treturn typeof controller.addVariant === 'function' && typeof controller.removeVariant === 'function';\n}\n\nconst SET_ID_PARAM = { type: 'string', description: 'Component set (or one of its variants) to act on (default: the selection’s set).' } as const;\nconst VARIANT_ID_PARAM = { type: 'string', description: 'Variant to act on (default: the first selected variant).' } as const;\nconst VARIANT_VALUES_PARAM = {\n\ttype: 'object',\n\tdescription: 'Variant property → value, e.g. { \"Size\": \"Medium\" }. New property names are added to every variant.',\n\tadditionalProperties: { type: 'string' }\n} as const;\n\nfunction variantCommands(controller: ComponentCommandTarget & VariantCommandTarget, prefix: string, base: Partial<CommandSpec>): CommandSpec[] {\n\treturn [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addVariant`,\n\t\t\tlabel: 'Add Variant',\n\t\t\tkeywords: ['variant', 'new variant', 'duplicate variant', 'component set'],\n\t\t\tdescription: 'Adds a variant to a component set by duplicating one (default: the selected or default variant) with new property values.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tsetId: SET_ID_PARAM,\n\t\t\t\t\tfrom: { type: 'string', description: 'Variant to duplicate (default: the selected variant, else the default one).' },\n\t\t\t\t\tvariant: VARIANT_VALUES_PARAM,\n\t\t\t\t\tx: { type: 'number', description: 'X in the set’s space (default: next to the source).' },\n\t\t\t\t\ty: { type: 'number', description: 'Y in the set’s space.' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { setId?: string; from?: string; variant?: Record<string, string> }) =>\n\t\t\t\tcontroller.canAddVariant(params?.setId, {\n\t\t\t\t\t...(params?.from !== undefined ? { from: params.from } : {}),\n\t\t\t\t\t...(params?.variant ? { variant: params.variant } : {})\n\t\t\t\t}),\n\t\t\trun: (_ctx: unknown, params?: { setId?: string; from?: string; variant?: Record<string, string>; x?: number; y?: number }) =>\n\t\t\t\toutcome(\n\t\t\t\t\tcontroller.addVariant(params?.setId, {\n\t\t\t\t\t\t...(params?.from !== undefined ? { from: params.from } : {}),\n\t\t\t\t\t\t...(params?.variant ? { variant: params.variant } : {}),\n\t\t\t\t\t\t...(params?.x !== undefined || params?.y !== undefined ? { position: { x: params?.x ?? 0, y: params?.y ?? 0 } } : {})\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t} as CommandSpec,\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeVariant`,\n\t\t\tlabel: 'Remove Variant',\n\t\t\tkeywords: ['delete variant', 'variant'],\n\t\t\tdescription: 'Removes a variant; its instances switch to the closest remaining variant, keeping overrides.',\n\t\t\tparams: { type: 'object', properties: { id: VARIANT_ID_PARAM } },\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => controller.canRemoveVariant(params?.id),\n\t\t\trun: (_ctx: unknown, params?: { id?: string }) => outcome(controller.removeVariant(params?.id))\n\t\t} as CommandSpec,\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setVariantProperties`,\n\t\t\tlabel: 'Edit Variant Properties…',\n\t\t\tkeywords: ['rename variant', 'variant values', 'variant'],\n\t\t\tdescription: 'Sets a variant’s property values (refusing a combination another variant has).',\n\t\t\tparams: { type: 'object', properties: { values: VARIANT_VALUES_PARAM, id: VARIANT_ID_PARAM }, required: ['values'] },\n\t\t\tenabled: (_ctx: unknown, params?: { values?: Record<string, string>; id?: string }) => controller.canSetVariantProperties(params?.values, params?.id),\n\t\t\trun: (_ctx: unknown, params: { values: Record<string, string>; id?: string }) => outcome(controller.setVariantProperties(params.values, params?.id))\n\t\t} as CommandSpec,\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}renameVariantProperty`,\n\t\t\tlabel: 'Rename Variant Property…',\n\t\t\tkeywords: ['rename property', 'variant property'],\n\t\t\tdescription: 'Renames a variant property across a component set.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { name: { type: 'string' }, newName: { type: 'string' }, setId: SET_ID_PARAM },\n\t\t\t\trequired: ['name', 'newName']\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { name?: string; newName?: string; setId?: string }) =>\n\t\t\t\tcontroller.canRenameVariantProperty(params?.name, params?.newName, params?.setId),\n\t\t\trun: (_ctx: unknown, params: { name: string; newName: string; setId?: string }) =>\n\t\t\t\toutcome(controller.renameVariantProperty(params.name, params.newName, params?.setId))\n\t\t} as CommandSpec,\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}renameVariantValue`,\n\t\t\tlabel: 'Rename Variant Value…',\n\t\t\tkeywords: ['rename value', 'variant value'],\n\t\t\tdescription: 'Renames a value of a variant property across a component set.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { property: { type: 'string' }, value: { type: 'string' }, newValue: { type: 'string' }, setId: SET_ID_PARAM },\n\t\t\t\trequired: ['property', 'value', 'newValue']\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { property?: string; value?: string; newValue?: string; setId?: string }) =>\n\t\t\t\tcontroller.canRenameVariantValue(params?.property, params?.value, params?.newValue, params?.setId),\n\t\t\trun: (_ctx: unknown, params: { property: string; value: string; newValue: string; setId?: string }) =>\n\t\t\t\toutcome(controller.renameVariantValue(params.property, params.value, params.newValue, params?.setId))\n\t\t} as CommandSpec\n\t];\n}\n\nexport interface ComponentCommandOptions {\n\t/** Id prefix (default `canvas.`). */\n\tprefix?: string;\n\tgroup?: string;\n\t/** Scope (default `canvas`). Pass `null` for global. */\n\tscope?: string | null;\n}\n\nconst outcome = (result: ComponentActionResult) => {\n\tif (!result.ok) throw new Error(result.reason);\n\treturn { ids: result.ids };\n};\n\nconst ID_PARAM = { type: 'string', description: 'Instance (or layer inside one) to act on (default: the first selected node).' } as const;\n\n/**\n * Palette / context-menu / agent commands for components and instances:\n * `canvas.createComponent`, `canvas.combineAsVariants`, `canvas.createInstance`\n * (`{ componentId, parentId?, x?, y?, properties? }`), `canvas.detachInstance`,\n * `canvas.resetOverrides` (`{ id?, group? }`), `canvas.pushOverrides`, `canvas.goToMainComponent`,\n * `canvas.swapInstance` (`{ componentId, ids? }`), `canvas.setInstanceProperties`\n * (`{ properties, ids? }`: variant and component property values), `canvas.restoreMainComponent`,\n * the variant editors `canvas.addVariant` (`{ setId?, from?, variant?, x?, y? }`),\n * `canvas.removeVariant` (`{ id? }`), `canvas.setVariantProperties` (`{ values, id? }`),\n * `canvas.renameVariantProperty` (`{ name, newName, setId? }`) and `canvas.renameVariantValue`\n * (`{ property, value, newValue, setId? }`) when the target has them, and the read-only `canvas.listComponents` / `canvas.inspectInstance`. Each action is one\n * undoable step when the controller has a history; enablement explains why it cannot run.\n */\nexport function createComponentCommands(controller: ComponentCommandTarget, options: ComponentCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'canvas.';\n\tconst group = options.group ?? 'Components';\n\tconst scope = options.scope === null ? undefined : options.scope ?? 'canvas';\n\tconst base = { group, ...(scope ? { scope } : {}) };\n\tconst idParams: CommandParamsSchema = { type: 'object', properties: { id: ID_PARAM } };\n\treturn [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}createComponent`,\n\t\t\tlabel: 'Create Component',\n\t\t\tkeywords: ['component', 'symbol', 'main component', 'reusable'],\n\t\t\tdescription: 'Turns the selected frame into a main component, or wraps the selection in one.',\n\t\t\tparams: IDS_PARAMS,\n\t\t\tshortcut: 'Mod+Alt+K',\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) => controller.canCreateComponent(params?.ids),\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] }) => outcome(controller.createComponent(params?.ids))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}combineAsVariants`,\n\t\t\tlabel: 'Combine as Variants',\n\t\t\tkeywords: ['component set', 'variant', 'variants'],\n\t\t\tdescription: 'Wraps the selected components in a component set.',\n\t\t\tparams: IDS_PARAMS,\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) => controller.canCombineAsVariants(params?.ids),\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] }) => outcome(controller.combineAsVariants(params?.ids))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}createInstance`,\n\t\t\tlabel: 'Create Instance',\n\t\t\tkeywords: ['instance', 'insert component', 'use component'],\n\t\t\tdescription: 'Places an instance of a component (or a component set’s default variant).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tcomponentId: { type: 'string', description: 'Component or component set id.' },\n\t\t\t\t\tparentId: { type: 'string', description: 'Frame to place it in (default: the canvas).' },\n\t\t\t\t\tx: { type: 'number', description: 'X in the parent’s space (default: next to the main).' },\n\t\t\t\t\ty: { type: 'number', description: 'Y in the parent’s space.' },\n\t\t\t\t\tproperties: { type: 'object', description: 'Variant and component property values.', additionalProperties: true }\n\t\t\t\t},\n\t\t\t\trequired: ['componentId']\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { componentId?: string; parentId?: string }) =>\n\t\t\t\tparams?.componentId === undefined ? true : controller.canCreateInstance(params.componentId, params.parentId),\n\t\t\trun: (_ctx: unknown, params: { componentId: string; parentId?: string; x?: number; y?: number; properties?: Record<string, boolean | string> }) =>\n\t\t\t\toutcome(\n\t\t\t\t\tcontroller.createInstance(params.componentId, {\n\t\t\t\t\t\t...(params.parentId !== undefined ? { parentId: params.parentId } : {}),\n\t\t\t\t\t\t...(params.x !== undefined || params.y !== undefined ? { position: { x: params.x ?? 0, y: params.y ?? 0 } } : {}),\n\t\t\t\t\t\t...(params.properties ? { properties: params.properties } : {})\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}detachInstance`,\n\t\t\tlabel: 'Detach Instance',\n\t\t\tkeywords: ['unlink', 'break instance'],\n\t\t\tdescription: 'Turns the selected instances into plain frames that keep their look.',\n\t\t\tparams: IDS_PARAMS,\n\t\t\tshortcut: 'Mod+Alt+B',\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) => controller.canDetachInstance(params?.ids),\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] }) => outcome(controller.detachInstance(params?.ids))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}resetOverrides`,\n\t\t\tlabel: 'Reset All Changes',\n\t\t\tkeywords: ['reset instance', 'reset overrides', 'revert'],\n\t\t\tdescription: 'Resets the overrides of an instance (or of a layer, or one override group) to its main.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tid: ID_PARAM,\n\t\t\t\t\tgroup: { type: 'string', description: 'One override group to reset, e.g. \"data.label\" or \"shape.fill\" (default: all).' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => controller.canResetOverrides(params?.id),\n\t\t\trun: (_ctx: unknown, params: { id?: string; group?: string }) => outcome(controller.resetOverrides(params?.id, params?.group))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}pushOverrides`,\n\t\t\tlabel: 'Push Changes to Main Component',\n\t\t\tkeywords: ['push overrides', 'update main component'],\n\t\t\tdescription: 'Copies the instance’s overrides to its main component and every other instance.',\n\t\t\tparams: idParams,\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => controller.canPushOverrides(params?.id),\n\t\t\trun: (_ctx: unknown, params: { id?: string }) => outcome(controller.pushOverrides(params?.id))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}goToMainComponent`,\n\t\t\tlabel: 'Go to Main Component',\n\t\t\tkeywords: ['main component', 'find main', 'reveal'],\n\t\t\tdescription: 'Selects the main component of the selected instance.',\n\t\t\tparams: idParams,\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => controller.canGoToMainComponent(params?.id),\n\t\t\trun: (_ctx: unknown, params: { id?: string }) => {\n\t\t\t\tconst main = controller.goToMainComponent(params?.id);\n\t\t\t\tif (!main) throw new Error('The main component was not found.');\n\t\t\t\treturn { id: main };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}swapInstance`,\n\t\t\tlabel: 'Swap Instance…',\n\t\t\tkeywords: ['replace instance', 'swap component'],\n\t\t\tdescription: 'Makes the selected instances show another component, keeping matching overrides.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tcomponentId: { type: 'string', description: 'Component or component set to swap to.' },\n\t\t\t\t\tids: IDS_PARAMS.properties!.ids\n\t\t\t\t},\n\t\t\t\trequired: ['componentId']\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { componentId?: string; ids?: string[] }) => controller.canSwapInstance(params?.componentId, params?.ids),\n\t\t\trun: (_ctx: unknown, params: { componentId: string; ids?: string[] }) => outcome(controller.swapInstance(params.componentId, params?.ids))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setInstanceProperties`,\n\t\t\tlabel: 'Set Instance Properties…',\n\t\t\tkeywords: ['variant', 'switch variant', 'component property', 'toggle'],\n\t\t\tdescription: 'Sets variant properties (switching variants) and component property values of the selected instances.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tproperties: { type: 'object', description: 'Property name → value (variant value, boolean, text or component id).', additionalProperties: true },\n\t\t\t\t\tids: IDS_PARAMS.properties!.ids\n\t\t\t\t},\n\t\t\t\trequired: ['properties']\n\t\t\t},\n\t\t\tenabled: (_ctx: unknown, params?: { ids?: string[] }) => controller.canSetInstanceProperties(params?.ids),\n\t\t\trun: (_ctx: unknown, params: { properties: Record<string, unknown>; ids?: string[] }) =>\n\t\t\t\toutcome(controller.setInstanceProperties(params.properties, params?.ids))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}restoreMainComponent`,\n\t\t\tlabel: 'Restore Main Component',\n\t\t\tkeywords: ['missing main', 'recover component'],\n\t\t\tdescription: 'Turns an instance whose main was deleted back into the main component.',\n\t\t\tparams: idParams,\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => controller.canRestoreMainComponent(params?.id),\n\t\t\trun: (_ctx: unknown, params: { id?: string }) => outcome(controller.restoreMainComponent(params?.id))\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listComponents`,\n\t\t\tlabel: 'List Components',\n\t\t\tkeywords: ['components', 'variants', 'assets'],\n\t\t\tdescription: 'Lists the components and component sets (with their variant options).',\n\t\t\treadOnly: true,\n\t\t\trun: () => controller.list()\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}inspectInstance`,\n\t\t\tlabel: 'Inspect Instance',\n\t\t\tkeywords: ['instance', 'overrides', 'properties'],\n\t\t\tdescription: 'Describes an instance: its main, variant, property values and overrides.',\n\t\t\tparams: idParams,\n\t\t\treadOnly: true,\n\t\t\tenabled: (_ctx: unknown, params?: { id?: string }) => (controller.inspect(params?.id) ? true : 'Select an instance.'),\n\t\t\trun: (_ctx: unknown, params: { id?: string }) => controller.inspect(params?.id) ?? null\n\t\t},\n\t\t...(hasVariantCommands(controller) ? variantCommands(controller, prefix, base) : [])\n\t];\n}\n\n// ---------------------------------------------------------------------------\n// Spreadsheet table actions\n// ---------------------------------------------------------------------------\n\n/** Spreadsheet controller surface used by `createSpreadsheetCommands`. */\nexport interface SpreadsheetCommandTarget {\n\tselectedRange: unknown;\n\tgetFilterState(): { enabled: boolean; rules: readonly unknown[] };\n\ttoggleFilters(): void;\n\tclearFilters(): void;\n\tunhideAll(): void;\n\tmergeCells(): boolean;\n\tunmergeCells(): boolean;\n\ttransposeRange(): boolean;\n}\n\nexport interface SpreadsheetCommandOptions {\n\t/** Id prefix (default `spreadsheet.`). */\n\tprefix?: string;\n\t/** Scope (default `spreadsheet`). Pass `null` for global. */\n\tscope?: string | null;\n}\n\n/** Turns a controller's `false` (nothing changed) into a failure the palette and agents see. */\nfunction orFail(changed: boolean, message: string): true {\n\tif (!changed) throw new Error(message);\n\treturn true;\n}\n\n/**\n * Palette / agent commands for common spreadsheet table actions (no default shortcuts).\n * Merge, unmerge and transpose act on the user's selection; agents should use the\n * `merge_cells` / `unmerge_cells` / `transpose_range` tools, which take a range.\n */\nexport function createSpreadsheetCommands(\n\tcontroller: SpreadsheetCommandTarget,\n\toptions: SpreadsheetCommandOptions = {}\n): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'spreadsheet.';\n\tconst scope = options.scope === null ? undefined : options.scope ?? 'spreadsheet';\n\tconst hasRange = () => (controller.selectedRange ? true : 'Select a range first.');\n\tconst base = { group: 'Table', ...(scope ? { scope } : {}) };\n\treturn [\n\t\t{ ...base, id: `${prefix}toggleFilters`, label: 'Toggle Filters', keywords: ['filter'], run: () => controller.toggleFilters() },\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}clearFilters`,\n\t\t\tlabel: 'Clear Filters',\n\t\t\tkeywords: ['filter', 'reset'],\n\t\t\tenabled: () => (controller.getFilterState().rules.length > 0 ? true : 'No filters to clear.'),\n\t\t\trun: () => controller.clearFilters()\n\t\t},\n\t\t{ ...base, id: `${prefix}unhideAll`, label: 'Unhide All Rows and Columns', keywords: ['show', 'hidden'], run: () => controller.unhideAll() },\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}mergeCells`,\n\t\t\tlabel: 'Merge Cells',\n\t\t\tkeywords: ['combine'],\n\t\t\tenabled: hasRange,\n\t\t\trun: () =>\n\t\t\t\torFail(controller.mergeCells(), 'Cannot merge the selection: it is a single cell, or it partly overlaps a merge or a spill range.')\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}unmergeCells`,\n\t\t\tlabel: 'Unmerge Cells',\n\t\t\tkeywords: ['split'],\n\t\t\tenabled: hasRange,\n\t\t\trun: () => orFail(controller.unmergeCells(), 'There are no merged cells in the selection.')\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}transposeRange`,\n\t\t\tlabel: 'Transpose Range',\n\t\t\tkeywords: ['rotate', 'swap rows columns'],\n\t\t\tenabled: hasRange,\n\t\t\trun: () =>\n\t\t\t\torFail(controller.transposeRange(), 'Cannot transpose the selection: it is a single cell, or a merge or a spill range is in the way.')\n\t\t}\n\t];\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-QBWEH3IM.cjs","../src/paint/rand.ts","../src/paint/gradient-paint.ts","../src/paint/brush.ts","../src/paint/path-stroke.ts","../src/paint/brushstroke.ts","../src/paint/exchange.ts","../src/paint/blending.ts","../src/paint/mixer.ts"],"names":["unit","i","finite"],"mappings":"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,IAAA;AACT,EAAA;AAEsC,EAAA;AACV,IAAA;AACJ,IAAA;AACS,MAAA;AACD,MAAA;AACH,MAAA;AACJ,MAAA;AAEO,MAAA;AACP,MAAA;AACN,MAAA;AACU,MAAA;AACpB,MAAA;AACP,IAAA;AACO,IAAA;AACT,EAAA;AAE4D,EAAA;AAC1C,IAAA;AAEa,IAAA;AACD,IAAA;AACb,IAAA;AACgB,MAAA;AACJ,MAAA;AAC3B,IAAA;AACS,IAAA;AACX,EAAA;AACF;AAEuD;AAC/B,EAAA;AACK,EAAA;AACA,EAAA;AACM,EAAA;AACnC;ARiyDmC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-QBWEH3IM.cjs","sourcesContent":[null,"// Portions ported from photocraft/crates/paint/src/rng.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Counter-based seeded randomness on 32-bit integer hashes: every value is a pure function of its\n * inputs, so strokes and papers regenerate identically on every machine. Shared by the paint\n * engine (`/paint`) and the paint editor (which re-exports it from `noise.ts`).\n */\n\n/** A 32-bit finaliser (murmur3's `fmix32`). */\nexport function mix32(h: number): number {\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\n/** Hash of integer coordinates and a salt to [0, 1). */\nexport function hash2(x: number, y: number, salt: number): number {\n const h = mix32((Math.imul(x | 0, 0x27d4eb2d) ^ mix32((y | 0) + Math.imul(salt | 0, 0x165667b1))) >>> 0);\n return h / 4294967296;\n}\n\n/** Uniform in [0, 1) from a seed, an index and a stream (one stream per random quantity). */\nexport function rand01(seed: number, index: number, stream: number): number {\n return hash2(index, stream, seed);\n}\n\n","/**\n * Gradient paint: a colour slot (the paint colour, a swatch, a stroke's colour) holds a\n * `PaintSource`, either one colour or a gradient that the brush samples dab by dab as the stroke\n * goes: by distance travelled, pen pressure, tilt, speed, direction, time, across the tip, or at\n * random. Shared by the paint editor and the image editor.\n *\n * - **Stops** have the vector and image gradients' shape (`offset`, `color`, `opacity`,\n * `midpoint`); opacity multiplies the brush's flow. `softness` (1, smooth, by default) turns\n * each blend into a harder step towards 0.\n * - **Space**: `pigment` (default) mixes neighbouring stops like paint (Kubelka-Munk, so blue to\n * yellow passes through green), `oklab` perceptually, `srgb` as plain numbers.\n * - **Mapping**: what moves along the gradient. `distance` (default: 300 px, mirrored) is the\n * \"drag it out\" paint: the colour changes as the stroke travels. It never stretches over the\n * whole stroke, whose length is unknown until the pen lifts.\n *\n * Everything is a pure function of the stroke's recorded samples and seed, so replays, time-lapses,\n * re-rendered exports and drying stay exact.\n */\n\nimport { mixPigment8 } from '../raster/blend';\nimport { rand01 } from './rand';\n\n/** sRGB 0..1. */\nexport type PaintRgb = [number, number, number];\n\n/** A gradient stop: the vector and image gradients' shape. */\nexport interface PaintGradientStop {\n /** 0..1 along the gradient. */\n offset: number;\n /**\n * #rrggbb, or `paint` / `secondary`: the editor's paint and second colours when the stroke\n * starts (a gradient between whatever two colours are chosen).\n */\n color: string;\n /** 0..1 (default 1): multiplies the brush's flow where this colour is laid. */\n opacity?: number;\n /** Where between this stop and the next (0.05..0.95 of the way) the colours are half mixed (default 0.5). */\n midpoint?: number;\n}\n\n/** How neighbouring stops mix. */\nexport type PaintGradientSpace = 'pigment' | 'oklab' | 'srgb';\nexport const PAINT_GRADIENT_SPACES: readonly PaintGradientSpace[] = ['pigment', 'oklab', 'srgb'];\n\n/** Past the end of the gradient: hold the last colour, start again, or run back. */\nexport type PaintGradientRepeat = 'clamp' | 'repeat' | 'mirror';\nexport const PAINT_GRADIENT_REPEATS: readonly PaintGradientRepeat[] = ['clamp', 'repeat', 'mirror'];\n\n/** What moves a stroke along its gradient. */\nexport type PaintGradientMapping =\n /** The stroke's travel: `length` px (or dabs, `unit` 'dabs') per run through the gradient. */\n | { by: 'distance'; length: number; repeat: PaintGradientRepeat; unit?: 'px' | 'dabs' }\n /** Pen pressure 0..1; tilt 0 (upright) .. 1 (flat); speed 0 .. 2.5 px/ms; direction of travel 0..360°. */\n | { by: 'pressure' }\n | { by: 'tilt' }\n | { by: 'velocity' }\n | { by: 'direction' }\n /** Time since the stroke began: `duration` ms per run through the gradient (needs timed samples). */\n | { by: 'time'; duration: number; repeat?: PaintGradientRepeat }\n /** Across the tip, left to right looking along the stroke: a brush loaded with several colours. */\n | { by: 'across-tip' }\n /** A random point of the gradient per dab (or per `every` dabs), seeded so replays match. */\n | { by: 'random'; every?: number };\n\nexport type PaintGradientMappingKind = PaintGradientMapping['by'];\nexport const PAINT_GRADIENT_MAPPINGS: readonly PaintGradientMappingKind[] = ['distance', 'pressure', 'tilt', 'velocity', 'direction', 'time', 'across-tip', 'random'];\n\nexport interface PaintColorSource {\n type: 'color';\n /** #rrggbb */\n color: string;\n}\n\nexport interface PaintGradientSource {\n type: 'gradient';\n /** 2..16 stops, sorted by offset. */\n stops: PaintGradientStop[];\n space: PaintGradientSpace;\n map: PaintGradientMapping;\n /** 0 (hard steps) .. 1 (smooth blends, default). */\n softness?: number;\n}\n\n/** What a colour slot holds: one colour or a gradient. */\nexport type PaintSource = PaintColorSource | PaintGradientSource;\n\n/** \"Drag it out\": 300 px of travel per run, mirrored back and forth. */\nexport const DEFAULT_PAINT_GRADIENT_MAPPING: Readonly<PaintGradientMapping> = { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' };\n\n/** Speed (px/ms) at which `velocity` reaches the end of the gradient (the brush dynamics' full speed). */\nexport const PAINT_GRADIENT_FULL_SPEED = 2.5;\n\n/** Seed stream of `random` (the multi-colour brush's, so migrated brushes pick the same colours). */\nconst RANDOM_STREAM = 11;\n\nexport class PaintSourceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PaintSourceError';\n }\n}\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\n/** `#rgb` / `#rrggbb` → lowercase `#rrggbb`, or null. */\nexport function normalizeHexColor(v: unknown): string | null {\n if (typeof v !== 'string' || !HEX.test(v.trim())) return null;\n const h = v.trim().toLowerCase();\n return h.length === 4 ? `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}` : h;\n}\n\n/** `#rrggbb` → sRGB 0..1. */\nexport function hexToPaintRgb(hex: string): PaintRgb {\n const n = parseInt(hex.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst describe = (v: unknown) => (typeof v === 'string' ? `\"${v}\"` : JSON.stringify(v));\n\nfunction parseMapping(raw: unknown, at: string): PaintGradientMapping {\n if (raw === undefined) return { ...DEFAULT_PAINT_GRADIENT_MAPPING } as PaintGradientMapping;\n if (!raw || typeof raw !== 'object') throw new PaintSourceError(`${at} must be an object like { \"by\": \"distance\", \"length\": 300, \"repeat\": \"mirror\" }.`);\n const r = raw as Record<string, unknown>;\n const by = r.by as PaintGradientMappingKind;\n if (!PAINT_GRADIENT_MAPPINGS.includes(by)) throw new PaintSourceError(`${at}.by ${describe(r.by)} is not a mapping; use one of ${PAINT_GRADIENT_MAPPINGS.join(', ')}.`);\n const repeat = (key: string, def: PaintGradientRepeat): PaintGradientRepeat => {\n if (r[key] === undefined) return def;\n if (!PAINT_GRADIENT_REPEATS.includes(r[key] as PaintGradientRepeat)) throw new PaintSourceError(`${at}.${key} ${describe(r[key])} is not a repeat; use ${PAINT_GRADIENT_REPEATS.join(', ')}.`);\n return r[key] as PaintGradientRepeat;\n };\n const range = (key: string, lo: number, hi: number, def: number, unit: string): number => {\n if (r[key] === undefined) return def;\n if (!finite(r[key]) || (r[key] as number) < lo || (r[key] as number) > hi) throw new PaintSourceError(`${at}.${key} ${describe(r[key])} is out of range; use ${lo}..${hi} ${unit}.`);\n return r[key] as number;\n };\n switch (by) {\n case 'distance': {\n const unit = r.unit === undefined ? 'px' : r.unit;\n if (unit !== 'px' && unit !== 'dabs') throw new PaintSourceError(`${at}.unit ${describe(r.unit)} is not a unit; use px or dabs.`);\n return { by, length: range('length', 1, unit === 'px' ? 100000 : 10000, 300, unit), repeat: repeat('repeat', 'mirror'), unit };\n }\n case 'time':\n return { by, duration: range('duration', 1, 600000, 2000, 'ms'), repeat: repeat('repeat', 'clamp') };\n case 'random':\n return { by, every: Math.round(range('every', 1, 500, 1, 'dabs')) };\n default:\n return { by };\n }\n}\n\n/**\n * Reads a paint source strictly: what tools and APIs take. Throws `PaintSourceError` saying what\n * is wrong (unknown type, an unreadable colour, fewer than 2 or more than 16 stops, offsets\n * outside 0..1 or not sorted, a bad mapping) and how to fix it. Missing optional fields get their\n * defaults (space `pigment`, the `distance` mapping, softness 1).\n */\nexport function parsePaintSource(raw: unknown, at = 'paint'): PaintSource {\n if (typeof raw === 'string') {\n const color = normalizeHexColor(raw);\n if (!color) throw new PaintSourceError(`${at} ${describe(raw)} is not a colour; use #rrggbb or a gradient object.`);\n return { type: 'color', color };\n }\n if (!raw || typeof raw !== 'object') throw new PaintSourceError(`${at} must be { \"type\": \"color\", \"color\": \"#rrggbb\" } or { \"type\": \"gradient\", \"stops\": [...] }.`);\n const r = raw as Record<string, unknown>;\n if (r.type === 'color') {\n const color = normalizeHexColor(r.color);\n if (!color) throw new PaintSourceError(`${at}.color ${describe(r.color)} is not a colour; use #rrggbb.`);\n return { type: 'color', color };\n }\n if (r.type !== 'gradient') throw new PaintSourceError(`${at}.type ${describe(r.type)} is not a paint; use \"color\" or \"gradient\".`);\n if (!Array.isArray(r.stops) || r.stops.length < 2 || r.stops.length > 16) throw new PaintSourceError(`${at}.stops needs 2..16 stops like { \"offset\": 0, \"color\": \"#1d4ed8\" }; got ${Array.isArray(r.stops) ? r.stops.length : describe(r.stops)}.`);\n const stops: PaintGradientStop[] = r.stops.map((s, i) => {\n const sat = `${at}.stops[${i}]`;\n if (!s || typeof s !== 'object') throw new PaintSourceError(`${sat} must be { \"offset\": 0..1, \"color\": \"#rrggbb\" }.`);\n const o = s as Record<string, unknown>;\n if (!finite(o.offset) || o.offset < 0 || o.offset > 1) throw new PaintSourceError(`${sat}.offset ${describe(o.offset)} must be a number 0..1.`);\n const color = o.color === 'paint' || o.color === 'secondary' ? o.color : normalizeHexColor(o.color);\n if (!color) throw new PaintSourceError(`${sat}.color ${describe(o.color)} is not a colour; use #rrggbb, \"paint\" or \"secondary\".`);\n const stop: PaintGradientStop = { offset: o.offset, color };\n if (o.opacity !== undefined) {\n if (!finite(o.opacity) || o.opacity < 0 || o.opacity > 1) throw new PaintSourceError(`${sat}.opacity ${describe(o.opacity)} must be 0..1.`);\n if (o.opacity !== 1) stop.opacity = o.opacity;\n }\n if (o.midpoint !== undefined) {\n if (!finite(o.midpoint) || o.midpoint < 0.05 || o.midpoint > 0.95) throw new PaintSourceError(`${sat}.midpoint ${describe(o.midpoint)} must be 0.05..0.95.`);\n if (o.midpoint !== 0.5) stop.midpoint = o.midpoint;\n }\n return stop;\n });\n for (let i = 1; i < stops.length; i++) {\n if (stops[i].offset < stops[i - 1].offset) throw new PaintSourceError(`${at}.stops must be sorted by offset: stop ${i} (${stops[i].offset}) comes before stop ${i - 1} (${stops[i - 1].offset}).`);\n }\n const space = r.space === undefined ? 'pigment' : (r.space as PaintGradientSpace);\n if (!PAINT_GRADIENT_SPACES.includes(space)) throw new PaintSourceError(`${at}.space ${describe(r.space)} is not a space; use ${PAINT_GRADIENT_SPACES.join(', ')}.`);\n const out: PaintGradientSource = { type: 'gradient', stops, space, map: parseMapping(r.map, `${at}.map`) };\n if (r.softness !== undefined) {\n if (!finite(r.softness) || r.softness < 0 || r.softness > 1) throw new PaintSourceError(`${at}.softness ${describe(r.softness)} must be 0..1.`);\n if (r.softness !== 1) out.softness = r.softness;\n }\n return out;\n}\n\n/** Reads a paint source leniently (documents, presets): `null` when it is not one. */\nexport function readPaintSource(raw: unknown): PaintSource | null {\n try {\n return raw === undefined || raw === null ? null : parsePaintSource(raw);\n } catch {\n return null;\n }\n}\n\n/**\n * The gradient with its `paint` / `secondary` stops replaced by those colours (#rrggbb): what a\n * stroke records and samples.\n */\nexport function resolvePaintGradient(g: PaintGradientSource, colors: { paint: string; secondary: string }): PaintGradientSource {\n if (!g.stops.some((s) => s.color === 'paint' || s.color === 'secondary')) return g;\n return { ...g, stops: g.stops.map((s) => (s.color === 'paint' ? { ...s, color: colors.paint } : s.color === 'secondary' ? { ...s, color: colors.secondary } : s)) };\n}\n\n/** The gradient for a stroke scaled by `k` (a high-resolution re-render): pixel distances scale with it. */\nexport function scalePaintGradient(g: PaintGradientSource, k: number): PaintGradientSource {\n if (k === 1 || g.map.by !== 'distance' || g.map.unit === 'dabs') return g;\n return { ...g, map: { ...g.map, length: Math.min(100000, Math.max(1, g.map.length * k)) } };\n}\n\n/** A colour source. */\nexport function solidPaint(color: string): PaintColorSource {\n return { type: 'color', color: normalizeHexColor(color) ?? '#000000' };\n}\n\n/** Whether a source is a gradient. */\nexport function isGradientPaint(s: PaintSource | null | undefined): s is PaintGradientSource {\n return Boolean(s && s.type === 'gradient');\n}\n\n/** One colour that stands for the source (the colour, or a gradient's first stop): what a plain colour slot shows. */\nexport function paintSourceColor(s: PaintSource, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): string {\n if (s.type === 'color') return s.color;\n const c = s.stops[0].color;\n return c === 'paint' ? colors.paint : c === 'secondary' ? colors.secondary : c;\n}\n\n/** The source as a CSS `linear-gradient` (or colour) for colour wells and swatches. */\nexport function paintSourceCss(s: PaintSource, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): string {\n if (s.type === 'color') return s.color;\n const parts = resolvePaintGradient(s, colors).stops.map((st) => {\n const [r, g, b] = hexToPaintRgb(st.color).map((v) => Math.round(v * 255));\n return `rgba(${r}, ${g}, ${b}, ${st.opacity ?? 1}) ${Math.round(st.offset * 1000) / 10}%`;\n });\n return `linear-gradient(90deg, ${parts.join(', ')})`;\n}\n\n// --- Sampling -------------------------------------------------------------------\n\n/** Where `u` (0..1 of a segment) lands with the midpoint `m` (the vector gradients' remap). */\nfunction midpointRemap(u: number, m: number): number {\n if (Math.abs(m - 0.5) < 1e-6) return u;\n return u <= m ? (0.5 * u) / m : 0.5 + (0.5 * (u - m)) / (1 - m);\n}\n\n/** Softness 1 keeps the blend; towards 0 it becomes a step at ½ (the multi-colour brush's curve). */\nfunction soften(f: number, softness: number): number {\n if (softness >= 1) return f;\n const w = Math.max(1e-3, softness);\n return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));\n}\n\nconst q8 = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255);\nconst mixScratch = [0, 0, 0];\n\nfunction srgbToLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\nfunction linearToSrgb(v: number): number {\n return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\nfunction toOklab(c: PaintRgb): PaintRgb {\n const r = srgbToLinear(c[0]);\n const g = srgbToLinear(c[1]);\n const b = srgbToLinear(c[2]);\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];\n}\nfunction fromOklab(c: PaintRgb): PaintRgb {\n const l = (c[0] + 0.3963377774 * c[1] + 0.2158037573 * c[2]) ** 3;\n const m = (c[0] - 0.1055613458 * c[1] - 0.0638541728 * c[2]) ** 3;\n const s = (c[0] - 0.0894841775 * c[1] - 1.291485548 * c[2]) ** 3;\n const out: PaintRgb = [\n linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),\n linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),\n linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),\n ];\n // Rounded to 16 bits so last-bit differences between JavaScript engines' pow / cbrt never show.\n return out.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 65535) / 65535) as PaintRgb;\n}\n\ninterface RampStop {\n t: number;\n rgb: PaintRgb;\n lab: PaintRgb | null;\n alpha: number;\n mid: number;\n}\n\n/** A gradient's colour at a dab: sRGB 0..1 and the opacity to multiply the flow by. */\nexport interface PaintGradientSample {\n rgb: PaintRgb;\n alpha: number;\n}\n\n/** What a dab offers a mapping: all pure functions of the stroke's recorded samples and seed. */\nexport interface PaintDabParams {\n /** Path length (px) the stroke has travelled at this dab. */\n travel: number;\n /** The dab's index in its copy of the stroke (0, 1, 2 …). */\n index: number;\n /** 0..1 */\n pressure: number;\n /** 0 upright .. 1 flat. */\n lean: number;\n /** px/ms, or `null` without timed samples. */\n speed: number | null;\n /** Direction of travel, radians counter-clockwise with y up. */\n direction: number;\n /** ms since the stroke's first timed sample, or `null` without timed samples. */\n time: number | null;\n}\n\n/**\n * A gradient prepared for sampling. `dab` gives the colour of one dab; `across-tip` gradients give\n * a table across the tip instead (`tipTable`).\n */\nexport class PaintGradientSampler {\n private readonly stops: RampStop[];\n private readonly softness: number;\n /** Stops at exactly k/(n − 1): segments found by arithmetic, as the multi-colour brush did. */\n private readonly uniform: boolean;\n readonly acrossTip: boolean;\n\n constructor(\n readonly source: PaintGradientSource,\n private readonly seed = 0\n ) {\n this.stops = source.stops.map((s) => {\n // Unresolved `paint` / `secondary` stops (call `resolvePaintGradient` first) read as black and white.\n const rgb = s.color === 'paint' ? ([0, 0, 0] as PaintRgb) : s.color === 'secondary' ? ([1, 1, 1] as PaintRgb) : hexToPaintRgb(s.color);\n return { t: s.offset, rgb, lab: source.space === 'oklab' ? toOklab(rgb) : null, alpha: s.opacity ?? 1, mid: s.midpoint ?? 0.5 };\n });\n this.softness = source.softness ?? 1;\n const n = this.stops.length;\n this.uniform = this.stops.every((s, k) => s.t === k / (n - 1) && s.mid === 0.5);\n this.acrossTip = source.map.by === 'across-tip';\n }\n\n /** The two stops' mix `u` of the way (0..1), in the gradient's space. */\n private mix(a: RampStop, b: RampStop, u: number, out: PaintGradientSample): PaintGradientSample {\n const v = soften(u, this.softness);\n out.alpha = a.alpha + (b.alpha - a.alpha) * v;\n const ar = a.rgb;\n const br = b.rgb;\n if (v <= 0 || (ar[0] === br[0] && ar[1] === br[1] && ar[2] === br[2])) {\n out.rgb = [ar[0], ar[1], ar[2]];\n return out;\n }\n if (v >= 1) {\n out.rgb = [br[0], br[1], br[2]];\n return out;\n }\n if (this.source.space === 'pigment') {\n mixPigment8(q8(ar[0]), q8(ar[1]), q8(ar[2]), q8(br[0]), q8(br[1]), q8(br[2]), v, mixScratch);\n out.rgb = [mixScratch[0], mixScratch[1], mixScratch[2]];\n } else if (this.source.space === 'oklab') {\n const la = a.lab!;\n const lb = b.lab!;\n out.rgb = fromOklab([la[0] + (lb[0] - la[0]) * v, la[1] + (lb[1] - la[1]) * v, la[2] + (lb[2] - la[2]) * v]);\n } else out.rgb = [ar[0] + (br[0] - ar[0]) * v, ar[1] + (br[1] - ar[1]) * v, ar[2] + (br[2] - ar[2]) * v];\n return out;\n }\n\n /** The colour at `t` (clamped to 0..1). */\n sample(t: number, out: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 }): PaintGradientSample {\n const st = this.stops;\n const n = st.length;\n if (this.uniform) {\n const f = Math.min(1, Math.max(0, t)) * (n - 1);\n const i = Math.min(n - 2, Math.floor(f));\n return this.mix(st[i], st[i + 1], f - i, out);\n }\n if (!(t > st[0].t)) return this.mix(st[0], st[0], 0, out);\n if (t >= st[n - 1].t) return this.mix(st[n - 1], st[n - 1], 0, out);\n let i = 0;\n while (i < n - 2 && !(t < st[i + 1].t)) i++;\n const a = st[i];\n const b = st[i + 1];\n const h = b.t - a.t;\n return this.mix(a, b, h > 0 ? midpointRemap((t - a.t) / h, a.mid) : 1, out);\n }\n\n /** Sampled `travel / length` runs along, past the end as `repeat` says. */\n private along(travel: number, length: number, repeat: PaintGradientRepeat, out: PaintGradientSample): PaintGradientSample {\n const n = this.stops.length;\n if (repeat === 'repeat' && this.uniform) {\n // Whole segments: the multi-colour brush's `(index / length) % colours`, to the bit.\n const segs = n - 1;\n const f = (travel / (length / segs)) % segs;\n const i = Math.min(segs - 1, Math.floor(f));\n return this.mix(this.stops[i], this.stops[i + 1], f - i, out);\n }\n const x = travel / length;\n let t: number;\n if (repeat === 'clamp') t = x;\n else if (repeat === 'repeat') t = x - Math.floor(x);\n else {\n const m = x - 2 * Math.floor(x / 2);\n t = m <= 1 ? m : 2 - m;\n }\n return this.sample(t, out);\n }\n\n /** The colour of one dab (not for `across-tip`: see `tipTable`). */\n dab(d: PaintDabParams, out: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 }): PaintGradientSample {\n const map = this.source.map;\n switch (map.by) {\n case 'distance':\n return this.along(map.unit === 'dabs' ? d.index : d.travel, map.length, map.repeat, out);\n case 'pressure':\n return this.sample(d.pressure, out);\n case 'tilt':\n return this.sample(d.lean, out);\n case 'velocity':\n return this.sample(d.speed === null ? 0 : Math.min(1, d.speed / PAINT_GRADIENT_FULL_SPEED), out);\n case 'direction': {\n const turn = d.direction / (2 * Math.PI);\n return this.sample(turn - Math.floor(turn), out);\n }\n case 'time':\n return this.along(d.time ?? 0, map.duration, map.repeat ?? 'clamp', out);\n case 'random':\n return this.sample(rand01(this.seed, Math.floor(d.index / (map.every ?? 1)), RANDOM_STREAM), out);\n default:\n return this.sample(0.5, out);\n }\n }\n\n /** `steps` colours from the left edge of the tip to the right (`across-tip`). */\n tipTable(steps: number): PaintGradientSample[] {\n const out: PaintGradientSample[] = [];\n for (let j = 0; j < steps; j++) out.push(this.sample(j / (steps - 1)));\n return out;\n }\n}\n\n// --- JSON Schema (for agent tools) -------------------------------------------------\n\nconst STOP_SCHEMA = {\n type: 'object',\n description: 'A gradient stop.',\n properties: {\n offset: { type: 'number', minimum: 0, maximum: 1, description: 'Where the stop is, 0..1 along the gradient. Stops must be sorted by offset.' },\n color: { type: 'string', pattern: '^(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|paint|secondary)$', description: '#rrggbb, or \"paint\" / \"secondary\": the editor\\'s paint or second colour when the stroke starts.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: \"0..1 (default 1): multiplies the brush's flow where this colour is laid.\" },\n midpoint: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'Where between this stop and the next the colours are half mixed, 0.05..0.95 of the way (default 0.5).' },\n },\n required: ['offset', 'color'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a gradient `PaintSource` (what tools accept for gradient paint). */\nexport const PAINT_GRADIENT_JSON_SCHEMA = {\n type: 'object',\n description:\n 'Gradient paint: the brush samples it dab by dab as the stroke goes. Example (blue to yellow over 300 px of stroke, and back): { \"type\": \"gradient\", \"stops\": [{ \"offset\": 0, \"color\": \"#1d4ed8\" }, { \"offset\": 1, \"color\": \"#facc15\" }], \"map\": { \"by\": \"distance\", \"length\": 300, \"repeat\": \"mirror\" } }.',\n properties: {\n type: { type: 'string', enum: ['gradient'], description: 'Always \"gradient\".' },\n stops: { type: 'array', minItems: 2, maxItems: 16, items: STOP_SCHEMA, description: '2..16 stops sorted by offset.' },\n space: { type: 'string', enum: [...PAINT_GRADIENT_SPACES], description: 'How neighbouring stops mix: pigment (default; like paint, blue to yellow passes green), oklab (perceptual) or srgb (plain numbers).' },\n map: {\n type: 'object',\n description: 'What moves the stroke along the gradient (default distance, 300 px, mirror).',\n properties: {\n by: {\n type: 'string',\n enum: [...PAINT_GRADIENT_MAPPINGS],\n description:\n 'distance: px (or dabs) of stroke travelled; pressure: pen pressure 0..1; tilt: pen lean, upright 0 .. flat 1; velocity: pen speed, 0 .. 2.5 px/ms; direction: direction of travel, once round per 360°; time: ms since the stroke began (needs timed points); across-tip: left to right across the tip, looking along the stroke (a brush loaded with several colours); random: a random point per dab (seeded, so replays match).',\n },\n length: { type: 'number', minimum: 1, maximum: 100000, description: 'distance: px of travel (or dabs, with unit \"dabs\") per run through the gradient (default 300).' },\n repeat: { type: 'string', enum: [...PAINT_GRADIENT_REPEATS], description: 'distance and time: past the end, clamp (hold the last colour), repeat (start again) or mirror (run back; default for distance, clamp for time).' },\n unit: { type: 'string', enum: ['px', 'dabs'], description: 'distance: px (default) or dabs.' },\n duration: { type: 'number', minimum: 1, maximum: 600000, description: 'time: ms per run through the gradient (default 2000).' },\n every: { type: 'integer', minimum: 1, maximum: 500, description: 'random: dabs per random pick (default 1).' },\n },\n required: ['by'],\n additionalProperties: false,\n },\n softness: { type: 'number', minimum: 0, maximum: 1, description: 'How softly neighbouring stops blend: 1 smooth (default) .. 0 hard steps.' },\n },\n required: ['type', 'stops'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a colour `PaintSource`. */\nexport const PAINT_COLOR_JSON_SCHEMA = {\n type: 'object',\n description: 'One colour: { \"type\": \"color\", \"color\": \"#rrggbb\" }.',\n properties: {\n type: { type: 'string', enum: ['color'], description: 'Always \"color\".' },\n color: { type: 'string', pattern: '^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$', description: '#rrggbb' },\n },\n required: ['type', 'color'],\n additionalProperties: false,\n} as const;\n\n/** JSON Schema of a `PaintSource`: a colour or gradient paint. */\nexport const PAINT_SOURCE_JSON_SCHEMA = {\n description: 'What to paint with: one colour, or gradient paint.',\n oneOf: [PAINT_COLOR_JSON_SCHEMA, PAINT_GRADIENT_JSON_SCHEMA],\n} as const;\n\n// --- Editing (for gradient editors in any framework) -------------------------------\n\nconst toHex8 = (rgb: readonly number[]) => '#' + rgb.slice(0, 3).map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('');\n\n/** Labels for the mapping picker. */\nexport const PAINT_GRADIENT_MAPPING_LABELS: Readonly<Record<PaintGradientMappingKind, string>> = {\n distance: 'Distance',\n pressure: 'Pressure',\n tilt: 'Tilt',\n velocity: 'Speed',\n direction: 'Direction',\n time: 'Time',\n 'across-tip': 'Across the tip',\n random: 'Random',\n};\nexport const PAINT_GRADIENT_SPACE_LABELS: Readonly<Record<PaintGradientSpace, string>> = { pigment: 'Pigment', oklab: 'Perceptual', srgb: 'RGB' };\nexport const PAINT_GRADIENT_REPEAT_LABELS: Readonly<Record<PaintGradientRepeat, string>> = { clamp: 'Hold', repeat: 'Repeat', mirror: 'Mirror' };\n\n/** The paint colour to the second colour, changing as the stroke is dragged out: where a gradient editor starts. */\nexport const DEFAULT_PAINT_GRADIENT: Readonly<PaintGradientSource> = {\n type: 'gradient',\n stops: [\n { offset: 0, color: 'paint' },\n { offset: 1, color: 'secondary' },\n ],\n space: 'pigment',\n map: { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' },\n};\n\n/** A few starting gradients for gradient editors. */\nexport const PAINT_GRADIENT_PRESETS: readonly { name: string; paint: PaintGradientSource }[] = [\n { name: 'Paint to second colour', paint: DEFAULT_PAINT_GRADIENT },\n {\n name: 'Sky to sun',\n paint: { type: 'gradient', stops: [{ offset: 0, color: '#1d4ed8' }, { offset: 1, color: '#facc15' }], space: 'pigment', map: { by: 'distance', length: 300, repeat: 'mirror', unit: 'px' } },\n },\n {\n name: 'Sunset',\n paint: { type: 'gradient', stops: [{ offset: 0, color: '#7c3aed' }, { offset: 0.5, color: '#e11d48' }, { offset: 1, color: '#f59e0b' }], space: 'oklab', map: { by: 'distance', length: 400, repeat: 'mirror', unit: 'px' } },\n },\n {\n name: 'Rainbow',\n paint: {\n type: 'gradient',\n stops: ['#e11d48', '#f59e0b', '#facc15', '#22c55e', '#3b82f6', '#8b5cf6'].map((color, k) => ({ offset: k / 5, color })),\n space: 'oklab',\n map: { by: 'distance', length: 600, repeat: 'repeat', unit: 'px' },\n },\n },\n {\n name: 'Pressure: light to dark',\n paint: { type: 'gradient', stops: [{ offset: 0, color: 'secondary' }, { offset: 1, color: 'paint' }], space: 'pigment', map: { by: 'pressure' } },\n },\n];\n\n/** Adds a stop at `offset` with the gradient's colour there. Returns the gradient and the new stop's index. */\nexport function addPaintGradientStop(g: PaintGradientSource, offset: number, colors: { paint: string; secondary: string } = { paint: '#000000', secondary: '#ffffff' }): { paint: PaintGradientSource; index: number } {\n if (g.stops.length >= 16) return { paint: g, index: -1 };\n const t = Math.min(1, Math.max(0, offset));\n const s = new PaintGradientSampler({ ...resolvePaintGradient(g, colors), map: { by: 'pressure' } }).sample(t);\n const stop: PaintGradientStop = { offset: t, color: toHex8(s.rgb) };\n if (s.alpha < 1) stop.opacity = Math.round(s.alpha * 1000) / 1000;\n let index = g.stops.findIndex((x) => x.offset > t);\n if (index < 0) index = g.stops.length;\n const stops = [...g.stops.slice(0, index), stop, ...g.stops.slice(index)];\n return { paint: { ...g, stops }, index };\n}\n\n/** Removes stop `index` (a gradient keeps at least 2). */\nexport function removePaintGradientStop(g: PaintGradientSource, index: number): PaintGradientSource {\n if (g.stops.length <= 2 || index < 0 || index >= g.stops.length) return g;\n return { ...g, stops: g.stops.filter((_, i) => i !== index) };\n}\n\n/** Changes stop `index` (moving it keeps the stops sorted). Returns the gradient and where the stop is now. */\nexport function updatePaintGradientStop(g: PaintGradientSource, index: number, patch: Partial<PaintGradientStop>): { paint: PaintGradientSource; index: number } {\n const target = g.stops[index];\n if (!target) return { paint: g, index };\n const next: PaintGradientStop = { ...target, ...patch };\n next.offset = Math.min(1, Math.max(0, next.offset));\n if (next.opacity === undefined || next.opacity >= 1) delete next.opacity;\n else next.opacity = Math.max(0, next.opacity);\n if (next.midpoint === undefined || next.midpoint === 0.5) delete next.midpoint;\n else next.midpoint = Math.min(0.95, Math.max(0.05, next.midpoint));\n const rest = g.stops.filter((_, i) => i !== index);\n let at = rest.findIndex((x) => x.offset > next.offset);\n if (at < 0) at = rest.length;\n return { paint: { ...g, stops: [...rest.slice(0, at), next, ...rest.slice(at)] }, index: at };\n}\n\n/** The gradient with its stops reversed (midpoints measured from the other side). */\nexport function reversePaintGradient(g: PaintGradientSource): PaintGradientSource {\n const st = g.stops;\n const stops = st\n .map((s, i) => {\n const out: PaintGradientStop = { offset: 1 - s.offset, color: s.color };\n if (s.opacity !== undefined) out.opacity = s.opacity;\n // The midpoint of the segment that now follows this stop was the previous stop's.\n const prev = st[i - 1];\n if (prev?.midpoint !== undefined) out.midpoint = 1 - prev.midpoint;\n return out;\n })\n .reverse();\n return { ...g, stops };\n}\n\n/** The mapping for `by`, keeping what carries over from `current` (length, repeat, duration …). */\nexport function paintGradientMappingFor(by: PaintGradientMappingKind, current: PaintGradientMapping): PaintGradientMapping {\n const prev = current as Partial<Record<'length' | 'repeat' | 'unit' | 'duration' | 'every', unknown>>;\n switch (by) {\n case 'distance':\n return { by, length: typeof prev.length === 'number' ? prev.length : 300, repeat: (prev.repeat as PaintGradientRepeat) ?? 'mirror', unit: (prev.unit as 'px' | 'dabs') ?? 'px' };\n case 'time':\n return { by, duration: typeof prev.duration === 'number' ? prev.duration : 2000, repeat: (prev.repeat as PaintGradientRepeat) ?? 'clamp' };\n case 'random':\n return { by, every: typeof prev.every === 'number' ? prev.every : 1 };\n default:\n return { by };\n }\n}\n\n/** Everything a gradient-paint editor UI needs, in one object (framework bindings take it from their own entry). */\nexport const PAINT_GRADIENT_KIT = {\n PAINT_GRADIENT_MAPPINGS,\n PAINT_GRADIENT_MAPPING_LABELS,\n PAINT_GRADIENT_PRESETS,\n PAINT_GRADIENT_REPEATS,\n PAINT_GRADIENT_REPEAT_LABELS,\n PAINT_GRADIENT_SPACES,\n PAINT_GRADIENT_SPACE_LABELS,\n addPaintGradientStop,\n paintGradientMappingFor,\n paintSourceCss,\n removePaintGradientStop,\n reversePaintGradient,\n updatePaintGradientStop,\n} as const;\nexport type PaintGradientKit = typeof PAINT_GRADIENT_KIT;\n","// Portions ported from photocraft/crates/paint/src/lib.rs, dynamics.rs and render.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The brush engine (a subset of photocraft's `crates/paint`): a computed round tip with size,\n * hardness, angle, roundness and spacing; pressure to size and opacity; smoothing; and Photoshop's\n * opacity-versus-flow build-up. Strokes are data, so one replays the same way for undo, agents\n * and tests.\n *\n * - Points are walked into dabs at `spacing × diameter` (`walkDabs`).\n * - Each dab's coverage (`dabCoverage`, photocraft's measured Photoshop profile) builds up in a\n * stroke coverage map with flow towards the dab's opacity ceiling: `c ← c + v·(ceil − c)`.\n * - The stroke composites once at the stroke opacity over the layer as it was before the stroke,\n * so overlapping dabs never exceed the opacity.\n */\n\nimport { blendPixel, mixPigment8, mixPigmentRgb, type BlendMode } from '../raster/blend';\nimport { srgbDecode, srgbEncode } from '../raster/deep';\nimport type { RasterRect as CropBox } from '../raster/surface';\nimport { TILE, tileKey, tileCoord, type TileKey } from '../raster/surface';\nimport { PaintGradientSampler, type PaintDabParams, type PaintGradientSample, type PaintGradientSource, type PaintRgb } from './gradient-paint';\n\n/** One input sample. Missing pressure is a mouse (1). */\nexport interface StrokePoint {\n x: number;\n y: number;\n /** 0..1 (default 1). */\n pressure?: number;\n /** Milliseconds (optional; kept for replays). */\n t?: number;\n}\n\nexport interface BrushSettings {\n /** Diameter in pixels (1..2500, default 20). */\n size: number;\n /** 0 (soft) .. 1 (hard, default). */\n hardness: number;\n /** Dab spacing as a fraction of the diameter (0.01..10, default 0.1). */\n spacing: number;\n /** Tip angle in degrees (default 0). */\n angle: number;\n /** Tip roundness 0.01..1 (default 1). */\n roundness: number;\n /** Stroke opacity ceiling 0..1 (default 1). */\n opacity: number;\n /** Build-up per dab 0..1 (default 1). */\n flow: number;\n /** Input smoothing 0..0.95 (default 0). */\n smoothing: number;\n /** Pressure controls size (default true). */\n pressureSize: boolean;\n /** Pressure controls opacity (default false). */\n pressureOpacity: boolean;\n /** How the stroke paints onto the layer (default normal). */\n blend: BlendMode;\n}\n\nexport const DEFAULT_BRUSH: Readonly<BrushSettings> = {\n size: 20,\n hardness: 1,\n spacing: 0.1,\n angle: 0,\n roundness: 1,\n opacity: 1,\n flow: 1,\n smoothing: 0,\n pressureSize: true,\n pressureOpacity: false,\n blend: 'normal',\n};\n\nexport const MAX_BRUSH_SIZE = 2500;\n\n/** A brush with every field in range (missing fields from `DEFAULT_BRUSH`). */\nexport function normalizeBrush(raw: Partial<BrushSettings> = {}): BrushSettings {\n const num = (v: unknown, lo: number, hi: number, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d);\n return {\n size: num(raw.size, 1, MAX_BRUSH_SIZE, DEFAULT_BRUSH.size),\n hardness: num(raw.hardness, 0, 1, DEFAULT_BRUSH.hardness),\n spacing: num(raw.spacing, 0.01, 10, DEFAULT_BRUSH.spacing),\n angle: num(raw.angle, -360, 360, DEFAULT_BRUSH.angle),\n roundness: num(raw.roundness, 0.01, 1, DEFAULT_BRUSH.roundness),\n opacity: num(raw.opacity, 0, 1, DEFAULT_BRUSH.opacity),\n flow: num(raw.flow, 0, 1, DEFAULT_BRUSH.flow),\n smoothing: num(raw.smoothing, 0, 0.95, DEFAULT_BRUSH.smoothing),\n pressureSize: typeof raw.pressureSize === 'boolean' ? raw.pressureSize : DEFAULT_BRUSH.pressureSize,\n pressureOpacity: typeof raw.pressureOpacity === 'boolean' ? raw.pressureOpacity : DEFAULT_BRUSH.pressureOpacity,\n blend: raw.blend ?? 'normal',\n };\n}\n\n// ---------------------------------------------------------------------------\n// The round tip (photocraft's measured Photoshop profile)\n// ---------------------------------------------------------------------------\n\n/**\n * Photoshop's round tip, measured by photocraft: a solid core of radius `r0 · R`, then a Gaussian\n * shoulder `exp(-((d − r0) / s)²)` of width `s · R`, both by hardness (0, 0.1, … 1).\n */\nconst TIP_SHAPE: ReadonlyArray<[number, number]> = [\n [0.0, 0.664], [0.14, 0.58], [0.28, 0.5], [0.4, 0.44], [0.52, 0.36], [0.63, 0.29],\n [0.7, 0.24], [0.78, 0.19], [0.84, 0.15], [0.89, 0.11], [1.0, 0.0],\n];\n/** The anti-aliased edge: the shoulder never narrows below this many pixels. */\nconst TIP_EDGE_PX = 1.2;\nconst SMALL_TIP_RADIUS = 3;\nconst SUBPIXEL = [-0.375, -0.125, 0.125, 0.375];\n\n/** The core radius and shoulder width (pixels) of a round tip of `radius` and `hardness`. */\nexport function tipShape(radius: number, hardness: number): [number, number] {\n const h = (Number.isNaN(hardness) ? 0 : Math.min(1, Math.max(0, hardness))) * 10;\n const i = Math.min(9, Math.floor(h));\n const t = h - i;\n const a = TIP_SHAPE[i];\n const b = TIP_SHAPE[i + 1];\n const r0 = a[0] + (b[0] - a[0]) * t;\n const s = a[1] + (b[1] - a[1]) * t;\n const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;\n const edge = Math.max(0.25, Math.min(TIP_EDGE_PX, 0.3 * r));\n return [Math.max(0, Math.min(r0 * r, r - edge)), Math.max(1e-3, s * r, edge)];\n}\n\n/** `exp(-u)` as `(1 − u/64)^64` by six squarings: exactly 0 from u = 64. */\nfunction softExp(u: number): number {\n let g = Math.min(1, Math.max(0, 1 - u / 64));\n for (let k = 0; k < 6; k++) g *= g;\n return g;\n}\n\n/** Opacity of a round tip at distance `d` from its centre. */\nexport function tipFalloff(d: number, radius: number, hardness: number): number {\n const [r0, s] = tipShape(radius, hardness);\n if (d <= r0) return 1;\n const x = (d - r0) / s;\n return softExp(x * x);\n}\n\n/** How far from its centre a round tip still paints. */\nexport function tipReach(radius: number, hardness: number): number {\n const [r0, s] = tipShape(radius, hardness);\n return r0 + 2.6 * s;\n}\n\n/** Coverage of a round dab at distance `d` from its centre. */\nexport function dabCoverage(d: number, radius: number, hardness: number): number {\n if (Number.isNaN(d) || d >= tipReach(radius, hardness) + 0.5) return 0;\n return tipFalloff(d, radius, hardness);\n}\n\n// ---------------------------------------------------------------------------\n// Points → dabs\n// ---------------------------------------------------------------------------\n\nexport interface Dab {\n x: number;\n y: number;\n radius: number;\n /** Flow (per-dab build-up). */\n alpha: number;\n /** Opacity ceiling this dab builds up to. */\n opacity: number;\n /** Radians. */\n angle: number;\n roundness: number;\n /** What gradient paint maps by (travel, index, pressure, time …); set by the dab walkers. */\n params?: PaintDabParams;\n}\n\n/** Exponential smoothing of input points; the first and last point are kept. */\nexport function smoothPoints(points: readonly StrokePoint[], amount: number): StrokePoint[] {\n const a = 1 - Math.min(0.95, Math.max(0, amount));\n if (a >= 1 || points.length < 3) return points.slice();\n const out: StrokePoint[] = [];\n let cur: StrokePoint | null = null;\n for (const p of points) {\n const n: StrokePoint = cur ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n out.push(n);\n cur = n;\n }\n out[out.length - 1] = points[points.length - 1];\n return out;\n}\n\nconst pressureOf = (p: StrokePoint) => (typeof p.pressure === 'number' && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1);\n\n/**\n * Walks points into dabs (incrementally: feed points in any chunks and get the same dabs). The\n * first point is a dab; then one every `spacing × diameter` along the path.\n */\nexport class DabWalker {\n private last: StrokePoint | null = null;\n private nextAt = 0;\n private pendingFirst: StrokePoint | null = null;\n private index = 0;\n private travelled = 0;\n private dir = 0;\n private firstT: number | null = null;\n private lastSpeedT: number | null = null;\n private lastSpeedAt: StrokePoint | null = null;\n private speed: number | null = null;\n\n constructor(readonly brush: BrushSettings) {}\n\n private diameter(p: StrokePoint): number {\n return this.brush.pressureSize ? this.brush.size * pressureOf(p) : this.brush.size;\n }\n\n private stepLen(p: StrokePoint): number {\n return Math.max(0.5, Math.max(1, this.diameter(p)) * Math.max(0.01, this.brush.spacing));\n }\n\n private dab(p: StrokePoint, travel: number): Dab {\n const b = this.brush;\n const time = typeof p.t === 'number' && Number.isFinite(p.t) && this.firstT !== null ? p.t - this.firstT : null;\n return {\n x: p.x,\n y: p.y,\n radius: Math.max(0.5, this.diameter(p) / 2),\n alpha: Math.min(1, Math.max(0, b.flow)),\n opacity: b.pressureOpacity ? pressureOf(p) : 1,\n angle: (b.angle * Math.PI) / 180,\n roundness: Math.min(1, Math.max(0.01, b.roundness)),\n params: { travel, index: this.index++, pressure: pressureOf(p), lean: 0, speed: time === null ? null : (this.speed ?? 0), direction: this.dir, time },\n };\n }\n\n /** Tracks time and a smoothed speed from timed samples (for gradient paint). */\n private timeOf(p: StrokePoint): void {\n if (typeof p.t !== 'number' || !Number.isFinite(p.t)) return;\n if (this.firstT === null) this.firstT = p.t;\n const a = this.lastSpeedAt;\n if (a && this.lastSpeedT !== null && p.t > this.lastSpeedT) {\n const v = Math.hypot(p.x - a.x, p.y - a.y) / (p.t - this.lastSpeedT);\n this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;\n }\n this.lastSpeedT = p.t;\n this.lastSpeedAt = p;\n }\n\n push(p: StrokePoint, out: Dab[]): void {\n const a = this.last;\n this.timeOf(p);\n if (!a) {\n this.last = p;\n this.pendingFirst = p;\n this.nextAt = this.stepLen(p);\n return;\n }\n const dx = p.x - a.x;\n const dy = p.y - a.y;\n const len = Math.hypot(dx, dy);\n if (len > 1e-9) this.dir = Math.atan2(-dy, dx);\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst, 0));\n this.pendingFirst = null;\n }\n if (len > 1e-9) {\n while (this.nextAt <= len + 1e-9) {\n const f = Math.min(1, this.nextAt / len);\n const q: StrokePoint = {\n x: a.x + dx * f,\n y: a.y + dy * f,\n pressure: pressureOf(a) + (pressureOf(p) - pressureOf(a)) * f,\n };\n if (typeof a.t === 'number' && typeof p.t === 'number') q.t = a.t + (p.t - a.t) * f;\n out.push(this.dab(q, this.travelled + this.nextAt));\n this.nextAt += this.stepLen(q);\n }\n this.nextAt -= len;\n this.travelled += len;\n }\n this.last = p;\n }\n\n finish(out: Dab[]): void {\n if (this.pendingFirst) {\n out.push(this.dab(this.pendingFirst, 0));\n this.pendingFirst = null;\n }\n }\n}\n\n/** All dabs of a point list (smoothing applied). */\nexport function walkDabs(points: readonly StrokePoint[], brush: BrushSettings): Dab[] {\n const w = new DabWalker(brush);\n const out: Dab[] = [];\n for (const p of smoothPoints(points, brush.smoothing)) w.push(p, out);\n w.finish(out);\n return out;\n}\n\n/** The pixel rectangle a dab can touch. */\nexport function dabRect(d: Dab, hardness: number): CropBox {\n // As `rasterizeDab`: the tip reaches `reach` across its minor axis and `reach / roundness` along it.\n const ro = Math.min(1, Math.max(0.5 / d.radius, d.roundness));\n const rm = Math.max(0.5, d.radius * ro);\n const r = Math.max(rm, tipReach(rm, hardness)) / ro + 1.5;\n const x0 = Math.floor(d.x - r);\n const y0 = Math.floor(d.y - r);\n return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };\n}\n\n/**\n * Rasterizes one dab over `rect` into `out` (rect.width × rect.height values, coverage × flow).\n * Small tips (radius below 3 px) take each pixel's covered area from a 4×4 grid.\n */\nexport function rasterizeDab(d: Dab, hardness: number, rect: CropBox, out: Float32Array): void {\n const w = rect.width;\n const h = rect.height;\n out.fill(0, 0, w * h);\n const r = d.radius;\n const ro = Math.min(1, Math.max(0.5 / r, d.roundness));\n const rm = Math.max(0.5, r * ro);\n const reach = Math.max(rm, tipReach(rm, hardness));\n const sn = Math.sin(d.angle);\n const cs = Math.cos(d.angle);\n const toTip = (dx: number, dy: number): [number, number] => {\n const ux = dx;\n const uy = -dy;\n return [ux * cs + uy * sn, -ux * sn + uy * cs];\n };\n const small = rm < SMALL_TIP_RADIUS;\n const reach2 = (reach + (small ? 0.75 : 0.5)) ** 2;\n for (let yy = 0; yy < h; yy++) {\n const dy = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < w; xx++) {\n const dx = rect.x + xx + 0.5 - d.x;\n const [u, v] = toTip(dx, dy);\n const d2 = (u * ro) ** 2 + v * v;\n if (d2 >= reach2) continue;\n let val: number;\n if (small) {\n let sum = 0;\n for (const sy of SUBPIXEL) {\n for (const sx of SUBPIXEL) {\n const [su, sv] = toTip(dx + sx, dy + sy);\n sum += tipFalloff(Math.sqrt((su * ro) ** 2 + sv * sv), rm, hardness);\n }\n }\n val = sum / 16;\n } else {\n val = dabCoverage(Math.sqrt(d2), rm, hardness);\n }\n if (val > 0) out[yy * w + xx] = val * d.alpha;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stroke coverage\n// ---------------------------------------------------------------------------\n\nconst TILE_PIXELS = TILE * TILE;\n\n/** A stroke's accumulated coverage, in 256 px tiles over document space. */\nexport class CoverageMap {\n readonly tiles = new Map<TileKey, Float32Array>();\n /**\n * Gradient paint: the colour laid at each pixel (sRGB 0..1, 3 per pixel), when dabs carry their\n * own colours. Each dab blends its colour in by the share of coverage it adds. Missing: the\n * stroke's one colour (`StrokeApply.color`).\n */\n readonly colors = new Map<TileKey, Float32Array>();\n bounds: CropBox | null = null;\n private dirty = new Set<TileKey>();\n\n /** The colour at a pixel (gradient paint), or `null` when the stroke has one colour there. */\n colorAt(x: number, y: number, out: number[]): number[] | null {\n const tx = Math.floor(x / TILE);\n const ty = Math.floor(y / TILE);\n const t = this.colors.get(tileKey(tx, ty));\n if (!t) return null;\n const i = ((y - ty * TILE) * TILE + (x - tx * TILE)) * 3;\n out[0] = t[i];\n out[1] = t[i + 1];\n out[2] = t[i + 2];\n return out;\n }\n\n get(x: number, y: number): number {\n const tx = Math.floor(x / TILE);\n const ty = Math.floor(y / TILE);\n const t = this.tiles.get(tileKey(tx, ty));\n return t ? t[(y - ty * TILE) * TILE + (x - tx * TILE)] : 0;\n }\n\n /**\n * Builds up `vals` (over `rect`) with flow towards `ceil`: `c ← c + v·(ceil − c)`. With `color`\n * (one sRGB colour for the dab) or `field` (sRGB per pixel of `rect`, 3 each), the pixels' colours\n * move towards it by the share of coverage this dab adds (gradient paint).\n */\n accumulate(rect: CropBox, vals: Float32Array, ceil: number, color: readonly number[] | null = null, field: Float32Array | null = null): void {\n if (rect.width <= 0 || rect.height <= 0) return;\n const w = rect.width;\n const tx0 = Math.floor(rect.x / TILE);\n const ty0 = Math.floor(rect.y / TILE);\n const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);\n const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);\n let touched = false;\n for (let ty = ty0; ty <= ty1; ty++) {\n for (let tx = tx0; tx <= tx1; tx++) {\n const x0 = Math.max(rect.x, tx * TILE);\n const y0 = Math.max(rect.y, ty * TILE);\n const x1 = Math.min(rect.x + rect.width, tx * TILE + TILE);\n const y1 = Math.min(rect.y + rect.height, ty * TILE + TILE);\n let tile: Float32Array | undefined;\n for (let y = y0; y < y1; y++) {\n const row = (y - rect.y) * w;\n const trow = (y - ty * TILE) * TILE;\n for (let x = x0; x < x1; x++) {\n const v = vals[row + (x - rect.x)];\n if (v <= 0) continue;\n if (!tile) {\n const key = tileKey(tx, ty);\n tile = this.tiles.get(key);\n if (!tile) {\n tile = new Float32Array(TILE_PIXELS);\n this.tiles.set(key, tile);\n }\n this.dirty.add(key);\n touched = true;\n }\n const ti = trow + (x - tx * TILE);\n const c = tile[ti];\n if (ceil > c) {\n const next = c + v * (ceil - c);\n tile[ti] = next;\n if (color || field) {\n const key = tileKey(tx, ty);\n let ct = this.colors.get(key);\n if (!ct) {\n ct = new Float32Array(TILE_PIXELS * 3);\n this.colors.set(key, ct);\n }\n const w = c > 0 ? (next - c) / next : 1;\n const fi = (row + (x - rect.x)) * 3;\n const ci = ti * 3;\n for (let ch = 0; ch < 3; ch++) {\n const col = field ? field[fi + ch] : color![ch];\n ct[ci + ch] += (col - ct[ci + ch]) * w;\n }\n }\n }\n }\n }\n }\n }\n if (touched) this.bounds = unionRect(this.bounds, rect);\n }\n\n /** Tiles changed since the last call. */\n takeDirty(): TileKey[] {\n const out = [...this.dirty];\n this.dirty.clear();\n return out;\n }\n}\n\nfunction unionRect(a: CropBox | null, b: CropBox): CropBox {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\n/** Paints a stroke's dabs into a coverage map, incrementally. */\nexport class StrokeCoverage {\n readonly coverage = new CoverageMap();\n private walker: DabWalker;\n private smoothing: number;\n private smoothPos: StrokePoint | null = null;\n private buf = new Float32Array(0);\n private field = new Float32Array(0);\n private readonly sampler: PaintGradientSampler | null;\n private readonly tipTable: PaintGradientSample[] | null;\n private readonly sample: PaintGradientSample = { rgb: [0, 0, 0], alpha: 1 };\n dabCount = 0;\n\n /** `paint`: a gradient source (resolved: no `paint` / `secondary` stops) for gradient paint; `seed` for its `random` mapping. */\n constructor(\n readonly brush: BrushSettings,\n paint: PaintGradientSource | null = null,\n seed = 0\n ) {\n this.walker = new DabWalker(brush);\n this.smoothing = brush.smoothing;\n this.sampler = paint ? new PaintGradientSampler(paint, seed) : null;\n this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;\n }\n\n /** Feeds input points; returns the document rectangles the new dabs touched. */\n push(points: readonly StrokePoint[]): CropBox[] {\n const dabs: Dab[] = [];\n for (const p of points) this.walker.push(this.smooth(p), dabs);\n return this.paint(dabs);\n }\n\n /** Ends the stroke (a single click still paints one dab). */\n finish(last?: StrokePoint): CropBox[] {\n const dabs: Dab[] = [];\n if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);\n this.walker.finish(dabs);\n return this.paint(dabs);\n }\n\n private smooth(p: StrokePoint): StrokePoint {\n const a = 1 - Math.min(0.95, Math.max(0, this.smoothing));\n const cur = this.smoothPos;\n const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n this.smoothPos = n;\n return n;\n }\n\n private paint(dabs: Dab[]): CropBox[] {\n const rects: CropBox[] = [];\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.hardness);\n const n = rect.width * rect.height;\n if (this.buf.length < n) this.buf = new Float32Array(n);\n const vals = this.buf.subarray(0, n);\n rasterizeDab(d, this.brush.hardness, rect, vals);\n if (this.sampler) this.paintDab(d, rect, vals);\n else this.coverage.accumulate(rect, vals, d.opacity);\n rects.push(rect);\n this.dabCount++;\n }\n return rects;\n }\n\n private paintDab(d: Dab, rect: CropBox, vals: Float32Array): void {\n const scratch = { field: this.field, sample: this.sample };\n const { color, field } = gradientDabColor(this.sampler!, this.tipTable, d, d.params!, rect, vals, scratch);\n this.field = scratch.field;\n this.coverage.accumulate(rect, vals, d.opacity, color, field);\n }\n}\n\n/** Colours across the tip of an `across-tip` gradient. */\nexport const TIP_GRADIENT_STEPS = 32;\n\n/**\n * Gradient paint for one dab: the dab's colour (or, across the tip, a colour per pixel) and its\n * opacity, which multiplies the flow. Shared by every coverage stroke.\n */\nexport function gradientDabColor(\n sampler: PaintGradientSampler,\n tipTable: readonly PaintGradientSample[] | null,\n d: Dab,\n params: PaintDabParams,\n rect: CropBox,\n vals: Float32Array,\n scratch: { field: Float32Array; sample: PaintGradientSample }\n): { color: PaintRgb | null; field: Float32Array | null } {\n if (!tipTable) {\n const s = sampler.dab(params, scratch.sample);\n if (s.alpha < 1) for (let k = 0; k < vals.length; k++) vals[k] *= s.alpha;\n return { color: s.rgb, field: null };\n }\n const n = rect.width * rect.height;\n if (scratch.field.length < n * 3) scratch.field = new Float32Array(n * 3);\n const f = scratch.field;\n // Across the stroke: the normal to the direction of travel (document y points down).\n const nx = Math.sin(params.direction);\n const ny = Math.cos(params.direction);\n const r = Math.max(0.5, d.radius);\n const last = tipTable.length - 1;\n for (let yy = 0, k = 0; yy < rect.height; yy++) {\n const py = rect.y + yy + 0.5 - d.y;\n for (let xx = 0; xx < rect.width; xx++, k++) {\n const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;\n const e = tipTable[Math.min(last, Math.max(0, Math.round(((u + 1) / 2) * last)))];\n f[k * 3] = e.rgb[0];\n f[k * 3 + 1] = e.rgb[1];\n f[k * 3 + 2] = e.rgb[2];\n if (e.alpha < 1) vals[k] *= e.alpha;\n }\n }\n return { color: null, field: f };\n}\n\n/** How a stroke changes pixels. */\nexport interface StrokeApply {\n /** `paint` puts colour down; `erase` removes alpha (or, with transparency locked, paints `background`). */\n mode: 'paint' | 'erase';\n /** RGBA 0..255 (paint colour, or the mask grey in `r`). */\n color: readonly [number, number, number, number];\n /** RGBA 0..255 the eraser paints on a transparency-locked layer (and on masks). */\n background: readonly [number, number, number, number];\n opacity: number;\n blend: BlendMode;\n lockTransparency: boolean;\n /** Selection coverage (document space, canvas-sized), or `null`. */\n selection: { width: number; height: number; data: ArrayLike<number> } | null;\n /** Layer offset (target pixels are in layer space). */\n offset: { x: number; y: number };\n}\n\n/**\n * Composites the stroke's coverage over `tileData` (the target's tile (tx, ty) as it was before\n * the stroke, in layer space; RGBA or one byte for masks) and returns the new tile data. With\n * `into` (the tile's previous result) and `region` (document space), only `region` is recomputed\n * and the rest of `into` is kept.\n */\nexport function applyStrokeToTile(\n coverage: CoverageMap,\n tileData: Uint8ClampedArray,\n channels: 1 | 4,\n tx: number,\n ty: number,\n s: StrokeApply,\n options: { into?: Uint8ClampedArray; region?: CropBox } = {}\n): Uint8ClampedArray {\n const out = options.into ? new Uint8ClampedArray(options.into) : new Uint8ClampedArray(tileData);\n const ox = tx * TILE + s.offset.x;\n const oy = ty * TILE + s.offset.y;\n let x0 = 0;\n let y0 = 0;\n let x1 = TILE;\n let y1 = TILE;\n if (options.region) {\n const r = options.region;\n x0 = Math.max(0, Math.floor(r.x) - ox);\n y0 = Math.max(0, Math.floor(r.y) - oy);\n x1 = Math.min(TILE, Math.ceil(r.x + r.width) - ox);\n y1 = Math.min(TILE, Math.ceil(r.y + r.height) - oy);\n if (x1 <= x0 || y1 <= y0) return out;\n // Start the region from the pre-stroke pixels: coverage is cumulative.\n if (options.into) {\n for (let y = y0; y < y1; y++) out.set(tileData.subarray((y * TILE + x0) * channels, (y * TILE + x1) * channels), (y * TILE + x0) * channels);\n }\n }\n const sel = s.selection;\n const cb = [0, 0, 0, 0];\n const cs = [s.color[0] / 255, s.color[1] / 255, s.color[2] / 255, s.color[3] / 255];\n const bg = [s.background[0] / 255, s.background[1] / 255, s.background[2] / 255, s.background[3] / 255];\n // With a tile-aligned offset the layer tile is one coverage tile: read it directly.\n const aligned = s.offset.x % TILE === 0 && s.offset.y % TILE === 0;\n const covTile = aligned ? coverage.tiles.get(tileKey(Math.floor(ox / TILE), Math.floor(oy / TILE))) : undefined;\n if (aligned && !covTile) return out;\n // Gradient paint: each pixel's own colour (alpha stays the stroke's).\n const colored = s.mode === 'paint' && coverage.colors.size > 0;\n const own = [0, 0, 0, cs[3]];\n const ownBytes: [number, number, number, number] = [0, 0, 0, s.color[3]];\n for (let y = y0; y < y1; y++) {\n const dy = oy + y;\n for (let x = x0; x < x1; x++) {\n const dx = ox + x;\n const c = covTile ? covTile[y * TILE + x] : coverage.get(dx, dy);\n if (c <= 0) continue;\n const pixelColor = colored ? coverage.colorAt(dx, dy, own) : null;\n if (pixelColor) for (let ch = 0; ch < 3; ch++) ownBytes[ch] = Math.round(Math.min(1, Math.max(0, own[ch])) * 255);\n let k = c * s.opacity;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n }\n if (k > 1) k = 1;\n if (k <= 0) continue;\n const p = y * TILE + x;\n if (channels === 1) {\n const target = s.mode === 'erase' ? s.background[0] : pixelColor ? Math.round(0.299 * ownBytes[0] + 0.587 * ownBytes[1] + 0.114 * ownBytes[2]) : s.color[0];\n out[p] = Math.round(out[p] + (target - out[p]) * k);\n continue;\n }\n const i = p * 4;\n if (s.mode === 'erase' && !s.lockTransparency) {\n out[i + 3] = Math.round(out[i + 3] * (1 - k));\n continue;\n }\n if (s.lockTransparency && out[i + 3] === 0) continue;\n const src = s.mode === 'erase' ? bg : pixelColor ? own : cs;\n if (s.blend === 'pigment') {\n // `blendPixel`'s paint mix on the 8-bit colours, with their spectra cached.\n const bytes = s.mode === 'erase' ? s.background : pixelColor ? ownBytes : s.color;\n const as = src[3] * k;\n if (!(as > 0)) continue;\n const ao = as + (out[i + 3] / 255) * (1 - as);\n mixPigment8(out[i], out[i + 1], out[i + 2], bytes[0], bytes[1], bytes[2], as / ao, cb);\n out[i] = Math.round(cb[0] * 255);\n out[i + 1] = Math.round(cb[1] * 255);\n out[i + 2] = Math.round(cb[2] * 255);\n if (!s.lockTransparency) out[i + 3] = Math.round(ao * 255);\n continue;\n }\n for (let ch = 0; ch < 4; ch++) cb[ch] = out[i + ch] / 255;\n const o = blendPixel(s.blend, cb, src, k);\n out[i] = Math.round(o[0] * 255);\n out[i + 1] = Math.round(o[1] * 255);\n out[i + 2] = Math.round(o[2] * 255);\n if (!s.lockTransparency) out[i + 3] = Math.round(o[3] * 255);\n }\n }\n return out;\n}\n\n/**\n * `applyStrokeToTile` at high bit depth: composites the stroke's coverage over deep RGBA\n * samples (`Uint16Array` sRGB or `Float32Array` linear light, see `raster/deep`) of the target's\n * tile (tx, ty) as it was before the stroke, and returns new samples of the same type. Blending\n * runs in float in the samples' own space (sRGB at 16 bits, linear light at 32 bits); the paint\n * colours are 8-bit sRGB and converted. With `options.channels` 1, a mask's coverage samples\n * (16-bit, or float 0..1) move towards the paint's grey, as `applyStrokeToTile` does at 8 bits.\n */\nexport function applyStrokeToDeepTile(\n coverage: CoverageMap,\n samples: Uint16Array | Float32Array,\n tx: number,\n ty: number,\n s: StrokeApply,\n options: { into?: Uint16Array | Float32Array; region?: CropBox; channels?: 1 | 4 } = {}\n): Uint16Array | Float32Array {\n const linear = samples instanceof Float32Array;\n const scale = linear ? 1 : 65535;\n const n = options.channels ?? 4;\n const out = (options.into ?? samples).slice() as Uint16Array | Float32Array;\n const ox = tx * TILE + s.offset.x;\n const oy = ty * TILE + s.offset.y;\n let x0 = 0;\n let y0 = 0;\n let x1 = TILE;\n let y1 = TILE;\n if (options.region) {\n const r = options.region;\n x0 = Math.max(0, Math.floor(r.x) - ox);\n y0 = Math.max(0, Math.floor(r.y) - oy);\n x1 = Math.min(TILE, Math.ceil(r.x + r.width) - ox);\n y1 = Math.min(TILE, Math.ceil(r.y + r.height) - oy);\n if (x1 <= x0 || y1 <= y0) return out;\n if (options.into) for (let y = y0; y < y1; y++) out.set(samples.subarray((y * TILE + x0) * n, (y * TILE + x1) * n), (y * TILE + x0) * n);\n }\n const grey = ((s.mode === 'erase' ? s.background[0] : s.color[0]) / 255) * scale;\n const colour = (c: readonly number[]) => {\n const v = [c[0] / 255, c[1] / 255, c[2] / 255, c[3] / 255];\n if (linear) for (let k = 0; k < 3; k++) v[k] = srgbDecode(v[k]);\n return v;\n };\n const src0 = colour(s.mode === 'erase' ? s.background : s.color);\n let src = src0;\n // Gradient paint: each pixel's own colour (alpha stays the stroke's).\n const colored = s.mode === 'paint' && coverage.colors.size > 0;\n const own = [0, 0, 0, src0[3]];\n const ownSrc = [0, 0, 0, src0[3]];\n const sel = s.selection;\n const cb = [0, 0, 0, 0];\n const mix = [0, 0, 0];\n const encCb = [0, 0, 0];\n const encSrc0 = linear ? [srgbEncode(src0[0]), srgbEncode(src0[1]), srgbEncode(src0[2])] : src0;\n const put = (i: number, v: number) => {\n out[i] = linear ? v : v <= 0 ? 0 : v >= 1 ? 65535 : Math.round(v * 65535);\n };\n for (let y = y0; y < y1; y++) {\n const dy = oy + y;\n for (let x = x0; x < x1; x++) {\n const dx = ox + x;\n const c = coverage.get(dx, dy);\n if (c <= 0) continue;\n const pixelColor = colored ? coverage.colorAt(dx, dy, own) : null;\n let k = c * s.opacity;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n }\n if (k > 1) k = 1;\n if (k <= 0) continue;\n src = src0;\n if (pixelColor) {\n for (let ch = 0; ch < 3; ch++) ownSrc[ch] = linear ? srgbDecode(own[ch]) : own[ch];\n src = ownSrc;\n }\n if (n === 1) {\n // Mask coverage: towards the paint's grey (masks take the colour's grey value).\n const p = y * TILE + x;\n const g = pixelColor ? (0.299 * own[0] + 0.587 * own[1] + 0.114 * own[2]) * scale : grey;\n const v = out[p] + (g - out[p]) * k;\n out[p] = linear ? v : Math.round(v);\n continue;\n }\n const i = (y * TILE + x) * 4;\n if (s.mode === 'erase' && !s.lockTransparency) {\n put(i + 3, (out[i + 3] / scale) * (1 - k));\n // A fully erased pixel keeps no colour, so a cleared tile stores nothing.\n if (out[i + 3] === 0) out[i] = out[i + 1] = out[i + 2] = 0;\n continue;\n }\n if (s.lockTransparency && out[i + 3] === 0) continue;\n for (let ch = 0; ch < 4; ch++) cb[ch] = out[i + ch] / scale;\n if (s.blend === 'pigment') {\n const as = src[3] * k;\n if (!(as > 0)) continue;\n const ao = as + cb[3] * (1 - as);\n for (let ch = 0; ch < 3; ch++) encCb[ch] = linear ? srgbEncode(cb[ch]) : cb[ch];\n mixPigmentRgb(encCb, pixelColor ? own : encSrc0, as / ao, mix);\n for (let ch = 0; ch < 3; ch++) put(i + ch, linear ? srgbDecode(mix[ch]) : mix[ch]);\n if (!s.lockTransparency) put(i + 3, ao);\n continue;\n }\n const o = blendPixel(s.blend, cb, src, k);\n put(i, o[0]);\n put(i + 1, o[1]);\n put(i + 2, o[2]);\n if (!s.lockTransparency) put(i + 3, o[3]);\n }\n }\n return out;\n}\n\n/** Tile coordinates of coverage tile keys (for callers that track dirty tiles). */\nexport function coverageTileCoord(key: TileKey): { tx: number; ty: number } {\n return tileCoord(key);\n}\n","/**\n * A brush stroke along a path: polyline samples, each with an optional pressure and pen tilt, are\n * walked at the brush's spacing (pressure and tilt interpolated between samples) into dabs, built\n * up in a coverage map and composited onto a layer. This is what a vector editor calls to draw a\n * path with a natural-media brush (\"brushstrokes on vector paths\"): the path is the data, the\n * pixels are rendered from it.\n *\n * - Pressure scales size and / or opacity as the brush says.\n * - Tilt (`tiltX` / `tiltY` in degrees, or `altitude` / `azimuth` in radians, as Pointer Events give\n * them) widens the tip (`tilt.size`), flattens it along the lean (`tilt.roundness`), and with\n * `tilt.followAzimuth` turns the tip to the lean, as a pen laid on its side does.\n * - With `followPath` the tip turns with the path (a calligraphic nib along a curve).\n *\n * Deterministic and DOM-free: the same samples and brush give the same pixels everywhere.\n */\n\nimport { applyStrokeToTile, CoverageMap, dabRect, gradientDabColor, normalizeBrush, rasterizeDab, TIP_GRADIENT_STEPS, type BrushSettings, type Dab } from './brush';\nimport { hexToPaintRgb, PaintGradientSampler, resolvePaintGradient, type PaintGradientSample, type PaintSource } from './gradient-paint';\nimport { TiledSurface, finishTile, tileKey, tilesInRect, TILE, type RasterRect } from '../raster/surface';\nimport type { BlendMode } from '../raster/blend';\n\n/** One sample of the path (document pixels). */\nexport interface PathSample {\n x: number;\n y: number;\n /** 0..1 (default 1). */\n pressure?: number;\n /** Pointer Events tilt in degrees (−90..90). */\n tiltX?: number;\n tiltY?: number;\n /** Radians above the surface (π/2 upright); overrides tiltX / tiltY. */\n altitude?: number;\n /** Radians clockwise from +x: where the pen leans (with altitude). */\n azimuth?: number;\n /** Milliseconds (gradient paint mapped by speed or time reads it). */\n t?: number;\n}\n\nexport interface PathStrokeOptions {\n brush: Partial<BrushSettings>;\n /** How tilt shapes the tip (each 0..1; default none). */\n tilt?: { size?: number; roundness?: number; followAzimuth?: boolean };\n /** Turn the tip with the direction of the path (added to the brush angle). */\n followPath?: boolean;\n /** Size at zero pressure as a fraction of the full size, when pressure controls size (default 0). */\n minSize?: number;\n /** Join the last sample back to the first. */\n closed?: boolean;\n}\n\n/** A pen's lean from a sample: 0 upright .. 1 flat, and the direction it leans (radians, clockwise from +x). */\nfunction lean(s: PathSample): { amount: number; azimuth: number } | null {\n if (typeof s.altitude === 'number' && Number.isFinite(s.altitude)) {\n return { amount: 1 - Math.min(1, Math.max(0, s.altitude / (Math.PI / 2))), azimuth: s.azimuth ?? 0 };\n }\n if (typeof s.tiltX === 'number' || typeof s.tiltY === 'number') {\n const tx = ((s.tiltX ?? 0) * Math.PI) / 180;\n const ty = ((s.tiltY ?? 0) * Math.PI) / 180;\n if (tx === 0 && ty === 0) return { amount: 0, azimuth: 0 };\n const az = Math.atan2(Math.tan(ty), Math.tan(tx));\n const alt = Math.atan(1 / Math.hypot(Math.tan(tx), Math.tan(ty)));\n return { amount: 1 - alt / (Math.PI / 2), azimuth: az };\n }\n return null;\n}\n\n/** The dabs of a brush stroke along `samples`, with pressure and tilt interpolated between them. */\nexport function pathStrokeDabs(samples: readonly PathSample[], options: PathStrokeOptions): Dab[] {\n const brush = normalizeBrush(options.brush);\n const pts = options.closed && samples.length > 2 ? [...samples, samples[0]] : [...samples];\n if (!pts.length) return [];\n const tilt = options.tilt ?? {};\n const minSize = Math.min(1, Math.max(0, options.minSize ?? 0));\n let index = 0;\n const t0 = pts.find((s) => typeof s.t === 'number' && Number.isFinite(s.t))?.t ?? null;\n const dab = (x: number, y: number, p: number, l: { amount: number; azimuth: number } | null, dir: number, travel = 0, time: number | null = null, speed: number | null = null): Dab => {\n const pr = Math.min(1, Math.max(0, p));\n const lean0 = l?.amount ?? 0;\n const size = brush.size * (brush.pressureSize ? minSize + (1 - minSize) * pr : 1) * (1 + 1.5 * (tilt.size ?? 0) * lean0);\n let angle = (brush.angle * Math.PI) / 180;\n if (options.followPath) angle += dir;\n if (tilt.followAzimuth && l) angle = -l.azimuth;\n const roundness = Math.max(0.05, brush.roundness * (1 - 0.85 * (tilt.roundness ?? 0) * lean0));\n const op = brush.pressureOpacity ? pr : 1;\n const params = { travel, index: index++, pressure: pr, lean: lean0, speed, direction: dir, time: time === null || t0 === null ? null : time - t0 };\n return { x, y, radius: Math.max(0.5, size / 2), alpha: brush.flow, opacity: brush.opacity * op, angle, roundness, params };\n };\n const out: Dab[] = [];\n const pressure = (s: PathSample) => (typeof s.pressure === 'number' && Number.isFinite(s.pressure) ? s.pressure : 1);\n if (pts.length === 1) {\n out.push(dab(pts[0].x, pts[0].y, pressure(pts[0]), lean(pts[0]), 0));\n return out;\n }\n let carry = 0;\n let along = 0;\n const timeOf = (s: PathSample) => (typeof s.t === 'number' && Number.isFinite(s.t) ? s.t : null);\n out.push(dab(pts[0].x, pts[0].y, pressure(pts[0]), lean(pts[0]), Math.atan2(-(pts[1].y - pts[0].y), pts[1].x - pts[0].x), 0, timeOf(pts[0])));\n for (let i = 0; i + 1 < pts.length; i++) {\n const a = pts[i];\n const b = pts[i + 1];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy);\n if (len === 0) continue;\n // Dab angles are counter-clockwise with y up, as the shared rasterizer reads them.\n const dir = Math.atan2(-dy, dx);\n const la = lean(a);\n const lb = lean(b);\n const ta = timeOf(a);\n const tb = timeOf(b);\n const speed = ta !== null && tb !== null && tb > ta ? len / (tb - ta) : null;\n let t = carry;\n for (;;) {\n const f = t / len;\n if (f > 1) break;\n const p = pressure(a) + (pressure(b) - pressure(a)) * f;\n const l = la && lb ? { amount: la.amount + (lb.amount - la.amount) * f, azimuth: la.azimuth + (lb.azimuth - la.azimuth) * f } : (la ?? lb);\n const size = brush.size * (brush.pressureSize ? minSize + (1 - minSize) * p : 1);\n const step = Math.max(0.5, size * brush.spacing);\n if (t > 0 || i > 0) out.push(dab(a.x + dx * f, a.y + dy * f, p, l, dir, along + t, ta !== null && tb !== null ? ta + (tb - ta) * f : null, speed));\n t += step;\n }\n carry = t - len;\n along += len;\n }\n return out;\n}\n\n/**\n * Gradient paint for a path stroke: a `PaintSource` gradient sampled dab by dab (by distance along\n * the path, pressure, tilt, speed, direction, time, across the tip or at random). `paint` /\n * `secondary` stops resolve to `colors` (default: the stroke colour and white).\n */\nexport interface PathStrokePaint {\n paint: PaintSource;\n /** Seed of the `random` mapping (default 0). */\n seed?: number;\n colors?: { paint: string; secondary: string };\n}\n\n/** The coverage of a brush stroke along `samples` (build-up towards each dab's opacity) and its bounds. */\nexport function pathStrokeCoverage(samples: readonly PathSample[], options: PathStrokeOptions & { gradient?: PathStrokePaint | null }): { coverage: CoverageMap; bounds: RasterRect | null; dabs: number } {\n const brush = normalizeBrush(options.brush);\n const coverage = new CoverageMap();\n const dabs = pathStrokeDabs(samples, options);\n const g = options.gradient?.paint.type === 'gradient' ? options.gradient : null;\n const sampler = g ? new PaintGradientSampler(resolvePaintGradient(g.paint as Extract<PaintSource, { type: 'gradient' }>, g.colors ?? { paint: '#000000', secondary: '#ffffff' }), g.seed ?? 0) : null;\n const tipTable = sampler?.acrossTip ? sampler.tipTable(TIP_GRADIENT_STEPS) : null;\n const scratch: { field: Float32Array; sample: PaintGradientSample } = { field: new Float32Array(0), sample: { rgb: [0, 0, 0], alpha: 1 } };\n let buf = new Float32Array(0);\n let bounds: RasterRect | null = null;\n for (const d of dabs) {\n const rect = dabRect(d, brush.hardness);\n const n = rect.width * rect.height;\n if (n <= 0) continue;\n if (buf.length < n) buf = new Float32Array(n);\n const vals = buf.subarray(0, n);\n rasterizeDab(d, brush.hardness, rect, vals);\n if (sampler) {\n const { color, field } = gradientDabColor(sampler, tipTable, d, d.params!, rect, vals, scratch);\n coverage.accumulate(rect, vals, d.opacity, color, field);\n } else coverage.accumulate(rect, vals, d.opacity);\n if (!bounds) bounds = { ...rect };\n else {\n const x = Math.min(bounds.x, rect.x);\n const y = Math.min(bounds.y, rect.y);\n bounds = { x, y, width: Math.max(bounds.x + bounds.width, rect.x + rect.width) - x, height: Math.max(bounds.y + bounds.height, rect.y + rect.height) - y };\n }\n }\n return { coverage, bounds, dabs: dabs.length };\n}\n\nexport interface PaintPathOptions extends PathStrokeOptions {\n /** RGBA 0..255 of the paint (with gradient paint: its alpha, and the colour of `paint` stops). */\n color: readonly [number, number, number, number];\n /** Gradient paint (a `PaintSource`); a colour source replaces `color`'s RGB. */\n paint?: PathStrokePaint | null;\n /** `erase` removes paint instead (default `paint`). */\n mode?: 'paint' | 'erase';\n /** Blend mode (default the brush's). */\n blend?: BlendMode;\n /** Layer offset: layer pixel = document pixel − offset (default 0, 0). */\n offset?: { x: number; y: number };\n lockTransparency?: boolean;\n /** Where paint may land (canvas-sized coverage 0..255), or none. */\n selection?: { width: number; height: number; data: ArrayLike<number> } | null;\n}\n\n/**\n * Paints a brush stroke along `samples` onto an RGBA layer surface and returns the new surface\n * (the input is not changed; untouched tiles are shared) with the bounds it touched.\n */\nexport function paintPathStroke(surface: TiledSurface, samples: readonly PathSample[], options: PaintPathOptions): { surface: TiledSurface; bounds: RasterRect | null; dabs: number } {\n if (surface.channels !== 4) throw new Error('paintPathStroke paints RGBA surfaces (4 channels).');\n const brush = normalizeBrush(options.brush);\n const hex = (c: readonly number[]) => '#' + [0, 1, 2].map((i) => Math.round(Math.min(255, Math.max(0, c[i]))).toString(16).padStart(2, '0')).join('');\n let color = options.color;\n if (options.paint?.paint.type === 'color') {\n const rgb = hexToPaintRgb(options.paint.paint.color).map((v) => Math.round(v * 255));\n color = [rgb[0], rgb[1], rgb[2], color[3]];\n }\n const gradient = options.paint?.paint.type === 'gradient' ? { ...options.paint, colors: options.paint.colors ?? { paint: hex(color), secondary: '#ffffff' } } : null;\n const { coverage, bounds, dabs } = pathStrokeCoverage(samples, { ...options, gradient });\n if (!bounds) return { surface, bounds: null, dabs };\n const offset = options.offset ?? { x: 0, y: 0 };\n const apply = {\n mode: options.mode ?? 'paint',\n color,\n background: [0, 0, 0, 0] as const,\n opacity: brush.opacity,\n blend: options.blend ?? brush.blend,\n lockTransparency: options.lockTransparency ?? false,\n selection: options.selection ?? null,\n offset,\n };\n const tiles = new Map(surface.tiles);\n for (const { tx, ty } of tilesInRect({ x: bounds.x - offset.x, y: bounds.y - offset.y, width: bounds.width, height: bounds.height })) {\n const key = tileKey(tx, ty);\n const src = surface.tiles.get(key)?.data;\n const base = src && src.length === TILE * TILE * 4 ? src : new Uint8ClampedArray(TILE * TILE * 4);\n const t = finishTile(applyStrokeToTile(coverage, base, 4, tx, ty, apply), 4, surface.defaultValue);\n if (t) tiles.set(key, t);\n else tiles.delete(key);\n }\n return { surface: new TiledSurface(4, tiles, surface.defaultValue), bounds, dabs };\n}\n","/**\n * A brushstroke rendered on its own: a path (polyline samples with pressure, tilt and time, or\n * points plus pressure / tilt profiles along the length) and a dry brush in, an RGBA image and its\n * bounds out. The canvas renders brushed freehand strokes with it, and the vector editor\n * brushstrokes on vector paths; both keep the path as the data and the pixels as a cache.\n *\n * - Built on {@link paintPathStroke}: the same dabs, coverage and build-up as everywhere else.\n * - Rendered at `scale` image pixels per path unit (both the samples and the brush size scale), so\n * a stroke looks the same at any zoom with more or fewer pixels. Snap zooms to\n * {@link brushstrokeScaleBucket} so panning and small zoom changes reuse the same image.\n * - Dry media only. A wet brush needs an area to flow in (the paint editor's wet layer); mixer,\n * blender and smudge brushes need paint under them. Those are refused with an error.\n * - Deterministic and DOM-free: the image is plain RGBA bytes. Turn it into a PNG data URL with\n * {@link brushstrokeDataUrl}, or into a canvas through an injected factory\n * ({@link brushstrokeToCanvas}).\n */\n\nimport { normalizeBrush, type BrushSettings } from './brush';\nimport { paintPathStroke, type PathSample, type PathStrokeOptions } from './path-stroke';\nimport { scalePaintGradient, type PaintSource } from './gradient-paint';\nimport { emptySurface, readRegion, type RasterRect } from '../raster/surface';\nimport { pngDataUrl } from '../raster/png';\nimport { BRUSHSTROKE_MAX_SCALE as MAX_SCALE, BRUSHSTROKE_MIN_SCALE as MIN_SCALE } from './brushstroke-scale';\n\nexport { brushstrokeScaleBucket, BRUSHSTROKE_MAX_SCALE, BRUSHSTROKE_MIN_SCALE } from './brushstroke-scale';\n\n/** One sample of a brushstroke: a {@link PathSample} plus an optional time stamp (milliseconds). */\nexport interface BrushstrokeSample extends PathSample {\n t?: number;\n}\n\n/**\n * A path plus pen profiles along it. Each profile is a list of values spread evenly over the\n * path's length (first at the start, last at the end) and interpolated in between; one value is\n * constant. Missing profiles leave the samples upright at full pressure.\n */\nexport interface BrushstrokeProfilePath {\n points: ReadonlyArray<{ x: number; y: number }>;\n /** Pressure 0..1 along the length. */\n pressure?: readonly number[];\n /** Pointer Events tilt in degrees (−90..90) along the length. */\n tiltX?: readonly number[];\n tiltY?: readonly number[];\n}\n\nexport type BrushstrokeInput = readonly BrushstrokeSample[] | BrushstrokeProfilePath;\n\n/**\n * A brush for a brushstroke: plain tip settings, or a paint-editor brush (`PaintBrush` matches this\n * shape structurally). Only dab-engine brushes render; `rotation: 'stroke'` turns the tip with the\n * path, and `dynamics` give the size at zero pressure and how tilt shapes the tip. Grain, jitter and\n * velocity dynamics are not applied (there is no paper, and the result must be deterministic).\n */\nexport type BrushstrokeBrush =\n | Partial<BrushSettings>\n | {\n id?: string;\n name?: string;\n engine?: string;\n tip: Partial<BrushSettings>;\n rotation?: string;\n dynamics?: { minSize?: number; tiltSize?: number; tiltRoundness?: number };\n };\n\n/**\n * What the stroke is painted with: a solid RGBA colour, or a `PaintSource` (one `#rrggbb` colour,\n * or gradient paint sampled dab by dab along the path: by distance, pressure, tilt, speed,\n * direction, time, across the tip or at random).\n */\nexport type BrushstrokePaint = { color: readonly [number, number, number, number] } | PaintSource;\n\nexport interface BrushstrokeOptions {\n /** Image pixels per path unit (default 1). Use {@link brushstrokeScaleBucket} for a zoom. */\n scale?: number;\n /** RGBA 0..255 (default opaque black). Shorthand for `paint: { color }`. */\n color?: readonly [number, number, number, number];\n /** What to paint with (wins over `color`; with gradient paint, `color` fills `paint` stops and gives the alpha). */\n paint?: BrushstrokePaint;\n /** Seed of gradient paint's `random` mapping (default 0). */\n seed?: number;\n /** Join the last sample back to the first. */\n closed?: boolean;\n /**\n * The largest image to render, in pixels (default 16 777 216, 4096²). A stroke that would be\n * bigger at `scale` renders at a lower scale instead; the result's `scale` says which.\n */\n maxPixels?: number;\n}\n\n/** A rendered brushstroke. */\nexport interface BrushstrokeImage {\n /** Straight-alpha RGBA bytes, `width × height × 4` (the same layout as `ImageData`). */\n image: { width: number; height: number; data: Uint8ClampedArray };\n /** Where the image goes, in path units: draw `image` stretched over this rectangle. Null for an empty stroke. */\n bounds: { x: number; y: number; width: number; height: number } | null;\n /** Image pixels per path unit it was rendered at. */\n scale: number;\n /** How many dabs the stroke laid down. */\n dabs: number;\n}\n\nconst DEFAULT_MAX_PIXELS = 4096 * 4096;\n\nconst isProfilePath = (input: BrushstrokeInput): input is BrushstrokeProfilePath => !Array.isArray(input) && typeof input === 'object' && input !== null && 'points' in input;\n\n/** A profile's value at `f` (0..1 along the length), or undefined without one. */\nfunction profileAt(profile: readonly number[] | undefined, f: number): number | undefined {\n if (!profile || !profile.length) return undefined;\n if (profile.length === 1) return profile[0];\n const u = Math.min(1, Math.max(0, f)) * (profile.length - 1);\n const i = Math.min(profile.length - 2, Math.floor(u));\n return profile[i] + (profile[i + 1] - profile[i]) * (u - i);\n}\n\n/** The samples of a brushstroke input: profile paths are sampled at their points by arc length. */\nexport function brushstrokeSamples(input: BrushstrokeInput): BrushstrokeSample[] {\n if (!isProfilePath(input)) return input.map((s) => ({ ...s }));\n const pts = input.points;\n const lengths = [0];\n for (let i = 1; i < pts.length; i++) lengths.push(lengths[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));\n const total = lengths.at(-1) ?? 0;\n return pts.map((p, i) => {\n const f = total > 0 ? lengths[i] / total : pts.length > 1 ? i / (pts.length - 1) : 0;\n const s: BrushstrokeSample = { x: p.x, y: p.y };\n const pressure = profileAt(input.pressure, f);\n const tiltX = profileAt(input.tiltX, f);\n const tiltY = profileAt(input.tiltY, f);\n if (pressure !== undefined) s.pressure = pressure;\n if (tiltX !== undefined) s.tiltX = tiltX;\n if (tiltY !== undefined) s.tiltY = tiltY;\n return s;\n });\n}\n\n/** Engines that cannot render a brushstroke on its own, and why. */\nconst REFUSED_ENGINES: Record<string, string> = {\n wet: 'is a wet brush, and wet paint needs an area to flow in (paint it on a wet layer in the paint editor)',\n mixer: 'is a Mixer Brush, which picks up paint from under the tip and has none on a standalone stroke',\n blender: 'is a blender, which only moves paint that is already there',\n smudge: 'is a smudge brush, which only moves paint that is already there',\n eraser: 'is an eraser, which has nothing to remove on a standalone stroke',\n};\n\n/**\n * The tip settings and path-stroke options of a brushstroke brush. Throws for brushes that are not\n * dry dab brushes (wet, mixer, blender, smudge, eraser, impasto and the procedural engines).\n */\nexport function brushstrokeStrokeOptions(brush: BrushstrokeBrush): PathStrokeOptions {\n if (!brush || typeof brush !== 'object') throw new Error('A brushstroke needs a brush: tip settings ({ size, hardness, … }) or a paint brush preset.');\n if (!('tip' in brush) || !brush.tip) return { brush: normalizeBrush(brush as Partial<BrushSettings>) };\n const engine = brush.engine ?? 'dab';\n if (engine !== 'dab') {\n const label = brush.name ?? brush.id ?? 'This brush';\n const why = REFUSED_ENGINES[engine] ?? `uses the '${engine}' engine, which renderBrushstroke does not draw`;\n throw new Error(`Brushstrokes render dry media only: '${label}' ${why}. Pick a dry brush (a dab-engine preset such as a pencil, pastel, charcoal, marker, ink or airbrush) or plain tip settings.`);\n }\n const d = brush.dynamics ?? {};\n const tilt = { size: d.tiltSize ?? 0, roundness: d.tiltRoundness ?? 0 };\n return {\n brush: normalizeBrush(brush.tip),\n followPath: brush.rotation === 'stroke',\n minSize: d.minSize ?? 0,\n ...(tilt.size || tilt.roundness ? { tilt } : {}),\n };\n}\n\n/** Whether a brush renders as a brushstroke (a dry dab brush, or plain tip settings). */\nexport function isBrushstrokeBrush(brush: BrushstrokeBrush): boolean {\n return !('tip' in brush) || !brush.tip || (brush.engine ?? 'dab') === 'dab';\n}\n\nconst finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\n/**\n * Renders a dry brushstroke along `input` with `brush` to an RGBA image. The same input, brush and\n * options always give the same bytes. Throws for a wet (or other non-dry) brush and for samples\n * that are not finite.\n */\nexport function renderBrushstroke(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): BrushstrokeImage {\n const strokeOptions = brushstrokeStrokeOptions(brush);\n const samples = brushstrokeSamples(input);\n for (const s of samples) if (!finite(s.x) || !finite(s.y)) throw new Error('Brushstroke samples need finite x and y.');\n const solid = options.paint && !('type' in options.paint) ? options.paint.color : null;\n const source = options.paint && 'type' in options.paint ? options.paint : null;\n const color = solid ?? options.color ?? [0, 0, 0, 255];\n let scale = finite(options.scale) && options.scale > 0 ? Math.min(MAX_SCALE, Math.max(MIN_SCALE, options.scale)) : 1;\n if (!samples.length) return { image: { width: 0, height: 0, data: new Uint8ClampedArray(0) }, bounds: null, scale, dabs: 0 };\n\n // Keep the image under maxPixels: estimate it from the samples and the widest the tip can get.\n const tip = strokeOptions.brush as BrushSettings;\n const reach = tip.size * (1 + 1.5 * (strokeOptions.tilt?.size ?? 0)) + 4;\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n for (const s of samples) {\n x0 = Math.min(x0, s.x); y0 = Math.min(y0, s.y); x1 = Math.max(x1, s.x); y1 = Math.max(y1, s.y);\n }\n const maxPixels = finite(options.maxPixels) && options.maxPixels > 0 ? options.maxPixels : DEFAULT_MAX_PIXELS;\n const area = (x1 - x0 + reach) * (y1 - y0 + reach);\n if (area * scale * scale > maxPixels) scale = Math.max(MIN_SCALE, Math.sqrt(maxPixels / area));\n\n const scaled = samples.map((s) => ({ ...s, x: s.x * scale, y: s.y * scale }));\n const { surface, bounds, dabs } = paintPathStroke(emptySurface(4), scaled, {\n ...strokeOptions,\n brush: { ...tip, size: tip.size * scale },\n closed: options.closed ?? strokeOptions.closed,\n color,\n // Gradient lengths are in path units: scale them with the image.\n ...(source ? { paint: { paint: source.type === 'gradient' ? scalePaintGradient(source, scale) : source, seed: options.seed ?? 0 } } : {}),\n });\n if (!bounds) return { image: { width: 0, height: 0, data: new Uint8ClampedArray(0) }, bounds: null, scale, dabs };\n const rect: RasterRect = bounds;\n const data = readRegion(surface, rect);\n return {\n image: { width: rect.width, height: rect.height, data },\n bounds: { x: rect.x / scale, y: rect.y / scale, width: rect.width / scale, height: rect.height / scale },\n scale,\n dabs,\n };\n}\n\n/** A rendered brushstroke as a PNG data URL (for an SVG `<image>`, an `<img>` or an export). Empty strokes give ''. */\nexport function brushstrokeDataUrl(stroke: BrushstrokeImage): string {\n const { width, height, data } = stroke.image;\n return width && height ? pngDataUrl(width, height, data) : '';\n}\n\n/** The 2D-canvas surface {@link brushstrokeToCanvas} draws on (an `HTMLCanvasElement` or `OffscreenCanvas`). */\nexport interface BrushstrokeCanvasLike {\n getContext(type: '2d'): { createImageData(w: number, h: number): { data: Uint8ClampedArray }; putImageData(img: never, x: number, y: number): void } | null;\n}\n\n/**\n * A rendered brushstroke on a canvas made by `createCanvas(width, height)` (injected, so this\n * module stays DOM-free). Returns null for an empty stroke or a canvas without a 2D context.\n */\nexport function brushstrokeToCanvas<C extends BrushstrokeCanvasLike>(stroke: BrushstrokeImage, createCanvas: (width: number, height: number) => C): C | null {\n const { width, height, data } = stroke.image;\n if (!width || !height) return null;\n const canvas = createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n const img = ctx.createImageData(width, height);\n img.data.set(data);\n ctx.putImageData(img as never, 0, 0);\n return canvas;\n}\n\n// ---------------------------------------------------------------------------\n// Cache\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string, as 8 hex digits. */\nfunction hashString(s: string, h = 0x811c9dc5): number {\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n}\n\n/**\n * A key for a brushstroke: the path (samples or profiles), the brush, the colour and the scale.\n * Two calls with equal arguments give equal keys; any change to them changes the key.\n */\nexport function brushstrokeKey(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): string {\n const path = JSON.stringify(input);\n const { brush: tip, ...rest } = brushstrokeStrokeOptions(brush);\n const rest2 = JSON.stringify([tip, rest, options.paint ?? options.color ?? null, options.seed ?? 0, options.closed ?? false, options.maxPixels ?? null]);\n const scale = finite(options.scale) ? options.scale : 1;\n return `${hashString(path).toString(16)}:${path.length}:${hashString(rest2).toString(16)}:${scale}`;\n}\n\n/**\n * A small least-recently-used cache of rendered brushstrokes keyed by {@link brushstrokeKey}: path,\n * brush, colour and scale. Pass a bucketed scale ({@link brushstrokeScaleBucket}) so zooming inside\n * a bucket hits the cache, and a changed path, brush or bucket re-renders.\n */\nexport class BrushstrokeCache {\n private readonly entries = new Map<string, BrushstrokeImage>();\n /** Renders that missed the cache (for tests and stats). */\n misses = 0;\n\n constructor(readonly capacity = 64) {}\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The cached stroke for these arguments, rendering (and caching) it on a miss. */\n render(input: BrushstrokeInput, brush: BrushstrokeBrush, options: BrushstrokeOptions = {}): BrushstrokeImage {\n const key = brushstrokeKey(input, brush, options);\n const hit = this.entries.get(key);\n if (hit) {\n this.entries.delete(key);\n this.entries.set(key, hit);\n return hit;\n }\n this.misses++;\n const out = renderBrushstroke(input, brush, options);\n this.entries.set(key, out);\n while (this.entries.size > Math.max(1, this.capacity)) this.entries.delete(this.entries.keys().next().value as string);\n return out;\n }\n\n clear(): void {\n this.entries.clear();\n }\n}\n","// Portions ported from photocraft/crates/paint/src/mixer.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The pick-up / mix exchange between a loaded brush and the paint under it: the one mechanism\n * every wet-capable brush shares. The Mixer Brush (`MixerStroke`) and the paint editor's\n * wet-capable brushes run on it now; the wet layer (phase 3) plugs in as another `PaintMedium`.\n *\n * Two halves:\n *\n * - `PaintExchange` is what the brush carries: a **reservoir** of loaded paint (with an amount\n * that drains per dab) and a **well** of paint picked up from the canvas. Per dab it\n * 1. picks up: the well moves towards the colour under the dab by the pick-up rate times how\n * much of the footprint holds paint (`pickUp`);\n * 2. mixes the dab's paint: reservoir weight `amount × (1 − mix)`, well weight `mix`\n * (`paint`);\n * 3. drains the reservoir (`drain`).\n * Every mix is RGB or Kubelka-Munk pigment (`mixing`), so a blue brush through yellow goes\n * green.\n * - `PaintMedium` is what the brush touches: `sample` (the colour and contact under a footprint)\n * and `deposit` (lay paint at per-pixel amounts). `TileMedium` is the raster layer's pixels.\n * A wet layer implements the same two calls over its wet pigment and water (sampling only wet\n * paint, so pick-up lifts what is still wet), and nothing in `PaintExchange` changes.\n *\n * The ratios a user sets are `pickup` (pick-up ratio, 0..1) and `mix` (mix ratio, 0..1);\n * `exchangeRates` turns them into per-dab rates with the Mixer Brush's constants, so a Mixer\n * Brush's `wet` and `mix` are exactly these two ratios. Everything is data in, pixels out.\n */\n\nimport { blendPixel, mixPigment8, type BlendMode } from '../raster/blend';\nimport type { RasterRect } from '../raster/surface';\nimport { TILE } from '../raster/surface';\nimport { createPigment, lerpPigmentInto, setPigmentColor, type Pigment } from './pigment';\n\n/** How colours mix: straight sRGB averages, or like paint (Kubelka-Munk). */\nexport type MixingModel = 'rgb' | 'pigment';\n\nexport const MIXING_MODELS: readonly MixingModel[] = ['rgb', 'pigment'];\n\n/** An sRGB colour, 0..1 per channel. */\nexport type RgbTriple = [number, number, number];\n\n/** What a brush carries between dabs and strokes. Colours are straight sRGB, 0..1. */\nexport interface MixerLoad {\n /** Loaded paint, or `null` when the brush is unloaded. */\n readonly reservoir: readonly [number, number, number] | null;\n /** Reservoir left, 0..1. */\n readonly amount: number;\n /** Colour picked up from the canvas (the well), or `null` when the brush is clean. */\n readonly pickup: readonly [number, number, number] | null;\n}\n\nexport const EMPTY_MIXER_LOAD: MixerLoad = { reservoir: null, amount: 0, pickup: null };\n\n/** Fraction of the way the well moves towards the canvas colour per dab at a pick-up ratio of 1. */\nexport const MIXER_PICKUP_RATE = 0.25;\n/** Fraction of the reservoir that drains per dab at a load of 0. */\nexport const MIXER_DRAIN = 0.08;\n\n/** Per-dab rates of an exchange. */\nexport interface ExchangeRates {\n /** How far the well moves towards the canvas colour per dab at full contact (0: lifts nothing). */\n pickup: number;\n /** The well's weight in each dab's paint; the reservoir gets `amount × (1 − mix)`. */\n mix: number;\n /** Fraction of the reservoir lost per dab. */\n drain: number;\n mixing: MixingModel;\n}\n\n/**\n * Per-dab rates from the ratios a user sets: pick-up ratio `pickup` (0 dry .. 1), mix ratio\n * `mix` (0 .. 1, scaled by the pick-up ratio: a brush that lifts nothing has nothing to mix) and\n * `load` (1 never drains). The Mixer Brush passes its `wet`, `mix` and `load`.\n */\nexport function exchangeRates(pickup: number, mix: number, load: number, mixing: MixingModel): ExchangeRates {\n return { pickup: MIXER_PICKUP_RATE * pickup, mix: mix * pickup, drain: MIXER_DRAIN * (1 - load), mixing };\n}\n\n/** What a footprint holds: its average paint colour and how much of it holds paint (0..1). */\nexport interface PaintSample {\n rgb: RgbTriple;\n /** Paint-weighted share of the footprint (alpha × coverage over coverage). */\n contact: number;\n}\n\n/** The brush side of the exchange: reservoir, amount and well, mixed per dab. */\nexport class PaintExchange {\n private reservoir: RgbTriple | null;\n private amount: number;\n private well: RgbTriple | null;\n private readonly pig: { res: Pigment; well: Pigment; paint: Pigment; canvas: Pigment } | null;\n private readonly out: RgbTriple = [0, 0, 0];\n\n constructor(\n readonly rates: ExchangeRates,\n load: MixerLoad\n ) {\n this.reservoir = load.reservoir ? [load.reservoir[0], load.reservoir[1], load.reservoir[2]] : null;\n this.amount = this.reservoir ? Math.min(1, Math.max(0, load.amount)) : 0;\n this.well = load.pickup ? [load.pickup[0], load.pickup[1], load.pickup[2]] : null;\n this.pig =\n rates.mixing === 'pigment'\n ? { res: createPigment(this.reservoir ?? [0, 0, 0]), well: createPigment(this.well ?? [0, 0, 0]), paint: createPigment([0, 0, 0]), canvas: createPigment([0, 0, 0]) }\n : null;\n }\n\n /** What the brush carries now. */\n get load(): MixerLoad {\n return { reservoir: this.reservoir ? [...this.reservoir] : null, amount: this.amount, pickup: this.well ? [...this.well] : null };\n }\n\n /** The colour picked up so far, or `null`. */\n get pickup(): Readonly<RgbTriple> | null {\n return this.well;\n }\n\n /** The prepared pigment of the last `paint` colour (pigment mixing only), for `deposit`. */\n get paintPigment(): Pigment | null {\n return this.pig?.paint ?? null;\n }\n\n /** Recolours the reservoir without refilling it (a multi-coloured brush, per dab). */\n setReservoirColor(r: number, g: number, b: number): void {\n if (!this.reservoir) {\n this.reservoir = [r, g, b];\n this.amount = 1;\n } else {\n this.reservoir[0] = r;\n this.reservoir[1] = g;\n this.reservoir[2] = b;\n }\n if (this.pig) setPigmentColor(this.pig.res, r, g, b);\n }\n\n /** `a` mixed `t` of the way to `b` into `this.out`, by the mixing model. */\n private mix(a: RgbTriple, pa: Pigment | null, b: RgbTriple, pb: Pigment | null, t: number): RgbTriple {\n const o = this.out;\n if (pa && pb) lerpPigmentInto(pa, pb, t, o, 0);\n else for (let c = 0; c < 3; c++) o[c] = a[c] + (b[c] - a[c]) * t;\n return o;\n }\n\n /** `a` mixed `t` of the way to `b` (sRGB 0..1) by the mixing model, into `out`. */\n mixColors(a: readonly number[], b: readonly number[], t: number, out: number[]): number[] {\n if (this.pig) {\n setPigmentColor(this.pig.canvas, a[0], a[1], a[2]);\n setPigmentColor(this.pig.paint, b[0], b[1], b[2]);\n lerpPigmentInto(this.pig.canvas, this.pig.paint, t, out, 0);\n } else for (let c = 0; c < 3; c++) out[c] = a[c] + (b[c] - a[c]) * t;\n return out;\n }\n\n /** 1. Pick-up: the well moves towards the sampled colour (or takes it, when clean). */\n pickUp(sample: PaintSample | null): void {\n if (!sample || !(this.rates.pickup > 0)) return;\n const pig = this.pig;\n if (!this.well) this.well = [sample.rgb[0], sample.rgb[1], sample.rgb[2]];\n else {\n if (pig) setPigmentColor(pig.canvas, sample.rgb[0], sample.rgb[1], sample.rgb[2]);\n const o = this.mix(this.well, pig?.well ?? null, sample.rgb, pig?.canvas ?? null, this.rates.pickup * sample.contact);\n this.well = [o[0], o[1], o[2]];\n }\n if (pig) setPigmentColor(pig.well, this.well[0], this.well[1], this.well[2]);\n }\n\n /**\n * 2. The dab's paint: the reservoir mixed with the well, written to `out`. Returns the total\n * weight (≤ 0: nothing to lay down); deposit `min(1, weight)` of it. With pigment mixing,\n * `paintPigment` is the result's pigment afterwards.\n */\n paint(out: RgbTriple): number {\n const res = this.reservoir;\n const wr = res ? this.amount * (1 - this.rates.mix) : 0;\n const wp = this.well ? this.rates.mix : 0;\n const total = wr + wp;\n if (!(total > 1e-6)) return 0;\n const pig = this.pig;\n if (res && this.well) {\n const m = this.mix(res, pig?.res ?? null, this.well, pig?.well ?? null, wp / total);\n out[0] = m[0];\n out[1] = m[1];\n out[2] = m[2];\n } else {\n const c = res ?? this.well!;\n out[0] = c[0];\n out[1] = c[1];\n out[2] = c[2];\n }\n if (pig) setPigmentColor(pig.paint, out[0], out[1], out[2]);\n return total;\n }\n\n /** 3. The reservoir drains. */\n drain(): void {\n this.amount *= 1 - this.rates.drain;\n }\n}\n\n/**\n * A brush's working tile: 8-bit bytes, or (on a 16- or 32-bit document) floats on the same\n * 0..255 sRGB-encoded scale, which keep the fractions an 8-bit tile would round away.\n */\nexport type PaintPixels = Uint8ClampedArray | Float32Array;\n\n/** Writes `v` (0..1) into a working tile: rounded for bytes, kept exact (not below 0) for floats. */\nexport function put255(px: PaintPixels, i: number, v: number): void {\n if (px instanceof Float32Array) px[i] = v > 0 ? v * 255 : 0;\n else px[i] = Math.round(v * 255);\n}\n\n/** The pixels a brush reads and writes (RGBA tiles in layer space). */\nexport interface PaintTileTarget {\n /** The tile (TILE × TILE × 4) at (tx, ty) as it is now, or `null` when it is fully transparent. */\n readTile(tx: number, ty: number): PaintPixels | null;\n /** A writable tile at (tx, ty), returned by `readTile` from then on. */\n writeTile(tx: number, ty: number): PaintPixels;\n /** Layer offset: layer pixel = document pixel − offset. */\n offset: { x: number; y: number };\n /** Keep alpha and paint only where the layer has pixels. */\n lockTransparency: boolean;\n /** Selection coverage (document space, canvas-sized), or `null`. */\n selection: { width: number; height: number; data: ArrayLike<number> } | null;\n}\n\n/** The canvas side of the exchange: where a brush picks paint up and lays it down. */\nexport interface PaintMedium {\n /** The paint under a footprint (`cov` over `rect`, document space), or `null` where there is none. */\n sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null;\n /**\n * Lays paint over the footprint at `dep × cov` per pixel. The colour is `paint` (with its\n * prepared pigment for pigment mixing), or per pixel from `field` (3 floats per pixel of\n * `rect`, sRGB 0..1) when given.\n */\n deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field?: Float32Array | null): void;\n}\n\nexport interface TileMediumOptions {\n /**\n * How deposits combine with the pixels. Missing: the exchange's mixing (source-over, or a\n * pigment mix with pigment mixing), as the Mixer Brush paints. Otherwise any blend mode, as a\n * brush tip's `blend`.\n */\n blend?: BlendMode;\n}\n\n/** A raster layer's pixels as a `PaintMedium` (through live tiles). */\nexport class TileMedium implements PaintMedium {\n // The tile last looked up by `locate` (and whether it came from `writeTile`).\n private tx = NaN;\n private ty = NaN;\n private tile: PaintPixels | null = null;\n private writable = false;\n private readonly canvasPig = createPigment([0, 0, 0]);\n private readonly paintPig = createPigment([0, 0, 0]);\n private paintKey = -1;\n private readonly cb = [0, 0, 0, 0];\n private readonly src = [0, 0, 0, 1];\n private readonly o: number[] = [0, 0, 0];\n\n constructor(\n readonly target: PaintTileTarget,\n private readonly options: TileMediumOptions = {}\n ) {}\n\n /** Forgets the cached tile (call when another writer may have replaced tiles). */\n reset(): void {\n this.tx = NaN;\n }\n\n /**\n * The tile holding document pixel (dx, dy) (`pixels`) and the pixel's byte index in it, or -1\n * when reading an empty tile.\n */\n locate(dx: number, dy: number, write: boolean): number {\n const t = this.target;\n const lx = dx - t.offset.x;\n const ly = dy - t.offset.y;\n const tx = Math.floor(lx / TILE);\n const ty = Math.floor(ly / TILE);\n if (tx !== this.tx || ty !== this.ty || (write && !this.writable)) {\n this.tx = tx;\n this.ty = ty;\n this.tile = write ? t.writeTile(tx, ty) : t.readTile(tx, ty);\n this.writable = write;\n }\n return this.tile ? ((ly - ty * TILE) * TILE + (lx - tx * TILE)) * 4 : -1;\n }\n\n /** The tile `locate` last found. */\n get pixels(): PaintPixels {\n return this.tile!;\n }\n\n /** Selection coverage at a document pixel, 0..1 (1 without a selection). */\n selectionAt(dx: number, dy: number): number {\n const sel = this.target.selection;\n if (!sel) return 1;\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) return 0;\n return sel.data[dy * sel.width + dx] / 255;\n }\n\n sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null {\n const w = rect.width;\n this.tx = NaN;\n let ar = 0;\n let ag = 0;\n let ab = 0;\n let wsum = 0;\n let asum = 0;\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const k = cov[yy * w + xx];\n if (k <= 0) continue;\n wsum += k;\n const i = this.locate(rect.x + xx, rect.y + yy, false);\n if (i < 0) continue;\n const px = this.tile!;\n const al = px[i + 3] / 255;\n if (al <= 0) continue;\n const ka = k * al;\n ar += (px[i] / 255) * ka;\n ag += (px[i + 1] / 255) * ka;\n ab += (px[i + 2] / 255) * ka;\n asum += ka;\n }\n }\n if (!(asum > 1e-6)) return null;\n return { rgb: [ar / asum, ag / asum, ab / asum], contact: asum / Math.max(1e-6, wsum) };\n }\n\n deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field: Float32Array | null = null): void {\n const t = this.target;\n const sel = t.selection;\n const w = rect.width;\n const o = this.o;\n const blend = this.options.blend;\n // Pigment where the exchange mixes pigment (no blend given) or the blend is `pigment`.\n const pigment = blend === undefined ? paintPigment !== null : blend === 'pigment';\n const general = blend !== undefined && blend !== 'normal' && blend !== 'pigment';\n // Pigment mode keeps the last canvas colour's spectrum: runs of equal pixels are common.\n let lastKey = -1;\n this.tx = NaN;\n for (let yy = 0; yy < rect.height; yy++) {\n const dy = rect.y + yy;\n for (let xx = 0; xx < w; xx++) {\n const p = yy * w + xx;\n let k = cov[p] * dep;\n if (k <= 0) continue;\n const dx = rect.x + xx;\n if (sel) {\n if (dx < 0 || dy < 0 || dx >= sel.width || dy >= sel.height) continue;\n k *= sel.data[dy * sel.width + dx] / 255;\n if (k <= 0) continue;\n }\n if (k > 1) k = 1;\n // A locked layer keeps its transparent pixels (and leaves their tiles unwritten).\n if (t.lockTransparency) {\n const ri = this.locate(dx, dy, false);\n if (ri < 0 || this.tile![ri + 3] === 0) continue;\n }\n const i = this.locate(dx, dy, true);\n const px = this.tile!;\n const pr = field ? field[p * 3] : paint[0];\n const pg = field ? field[p * 3 + 1] : paint[1];\n const pb = field ? field[p * 3 + 2] : paint[2];\n if (general) {\n const cb = this.cb;\n for (let c = 0; c < 4; c++) cb[c] = px[i + c] / 255;\n const src = this.src;\n src[0] = pr;\n src[1] = pg;\n src[2] = pb;\n const r = blendPixel(blend!, cb, src, k);\n put255(px, i, r[0]);\n put255(px, i + 1, r[1]);\n put255(px, i + 2, r[2]);\n if (!t.lockTransparency) put255(px, i + 3, r[3]);\n continue;\n }\n const a = px[i + 3] / 255;\n const na = t.lockTransparency ? a : a + (1 - a) * k;\n // The new paint's share of the result (source-over); a locked layer mixes by `k` alone.\n const share = t.lockTransparency ? k : na > 0 ? k / na : 1;\n if (a <= 0 || share >= 1) {\n o[0] = pr;\n o[1] = pg;\n o[2] = pb;\n } else if (pigment && field) {\n mixPigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2]), Math.round(pr * 255), Math.round(pg * 255), Math.round(pb * 255), share, o);\n } else if (pigment) {\n // Float tiles (16 / 32 bits) hold fractions the 8-bit key would merge: no cache there.\n const key = px instanceof Float32Array ? -2 : (px[i] << 16) | (px[i + 1] << 8) | px[i + 2];\n if (key !== lastKey || key === -2) {\n setPigmentColor(this.canvasPig, px[i] / 255, px[i + 1] / 255, px[i + 2] / 255);\n lastKey = key;\n }\n let pp = paintPigment;\n if (!pp) {\n // A pigment-blend tip over an RGB exchange: prepare the paint colour once per colour.\n const pkey = (Math.round(pr * 255) << 16) | (Math.round(pg * 255) << 8) | Math.round(pb * 255);\n if (pkey !== this.paintKey) {\n setPigmentColor(this.paintPig, pr, pg, pb);\n this.paintKey = pkey;\n }\n pp = this.paintPig;\n }\n lerpPigmentInto(this.canvasPig, pp, share, o, 0);\n } else {\n o[0] = px[i] / 255 + (pr - px[i] / 255) * share;\n o[1] = px[i + 1] / 255 + (pg - px[i + 1] / 255) * share;\n o[2] = px[i + 2] / 255 + (pb - px[i + 2] / 255) * share;\n }\n put255(px, i, o[0]);\n put255(px, i + 1, o[1]);\n put255(px, i + 2, o[2]);\n put255(px, i + 3, na);\n }\n }\n }\n}\n","/**\n * Blending tools on raster tiles, dab by dab (any dab walker can drive them):\n *\n * - **Blender** (`blenderDab`): every pixel under the tip moves towards the mix of the paint\n * within `distance` of it, so neighbouring colours run together. The mix is taken through the\n * pigment model: the neighbourhood's K/S spectra are averaged with Kubelka-Munk weights\n * (alpha × scattering), so blue next to yellow blends into green, not grey. `rgb` averages\n * straight colours instead.\n * - **Smudge** (`SmudgeTip`): a finger that carries a patch of paint. Each dab lays the patch it\n * carries over the pixels under the tip (`strength`) and picks up some of the paint that was\n * there (`1 − length`), so the paint's pattern is dragged along the stroke and fades with distance.\n * Optional pigment mixing for both steps.\n *\n * Both blend alpha too: blending next to bare canvas softens an edge, smudging drags paint out\n * into it. A transparency-locked layer keeps its alpha. Pure data in, pixels out.\n */\n\nimport { mixPigment8, pigment8 } from '../raster/blend';\nimport { PIGMENT_BANDS, lerpPigmentInto, type Pigment } from '../raster/pigment';\nimport type { RasterRect } from '../raster/surface';\nimport { put255, type MixingModel, type TileMedium } from './exchange';\n\n/** Blender options. */\nexport interface BlenderSettings {\n /** How far each pixel moves towards the neighbourhood mix per dab at full coverage, 0..1 (default 0.5). */\n strength: number;\n /** Blend distance: the neighbourhood's radius as a fraction of the tip diameter, 0..1 (default 0.35). */\n distance: number;\n /** `pigment` (default: Kubelka-Munk, blue next to yellow blends green) or `rgb`. */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_BLENDER: Readonly<BlenderSettings> = { strength: 0.5, distance: 0.35, mixing: 'pigment' };\n\n/** Smudge options. */\nexport interface SmudgeSettings {\n /** How strongly the carried paint covers the pixels under the tip per dab, 0..1 (default 0.7). */\n strength: number;\n /** How far paint is dragged: the share of the carried patch kept per dab, 0..1 (default 0.85). */\n length: number;\n /** `rgb` (default) or `pigment` (smeared colours mix like paint). */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_SMUDGE: Readonly<SmudgeSettings> = { strength: 0.7, length: 0.85, mixing: 'rgb' };\n\nconst unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\n\nexport function normalizeBlender(raw: Partial<BlenderSettings> = {}, base: BlenderSettings = DEFAULT_BLENDER): BlenderSettings {\n return { strength: unit(raw.strength, base.strength), distance: unit(raw.distance, base.distance), mixing: raw.mixing === 'rgb' || raw.mixing === 'pigment' ? raw.mixing : base.mixing };\n}\n\nexport function normalizeSmudge(raw: Partial<SmudgeSettings> = {}, base: SmudgeSettings = DEFAULT_SMUDGE): SmudgeSettings {\n return { strength: unit(raw.strength, base.strength), length: unit(raw.length, base.length), mixing: raw.mixing === 'rgb' || raw.mixing === 'pigment' ? raw.mixing : base.mixing };\n}\n\n/** The neighbourhood radius in pixels of a blender dab of `radius`. */\nexport function blendRadius(settings: BlenderSettings, radius: number): number {\n return Math.max(1, Math.round(settings.distance * 2 * radius));\n}\n\n/** Reusable buffers for `blenderDab` (one per stroke). */\nexport interface BlenderScratch {\n sums: Float64Array;\n mix: Pigment;\n out: number[];\n}\n\nexport function createBlenderScratch(): BlenderScratch {\n return { sums: new Float64Array(0), mix: { rgb: [0, 0, 0], strength: 1, ks: new Float32Array(PIGMENT_BANDS), scatter: 1 }, out: [0, 0, 0] };\n}\n\nconst N = PIGMENT_BANDS;\n\n/**\n * One blender dab: `cov` is the tip's coverage over `rect` (document space, 0..1), `opacity` the\n * dab's (pressure) opacity, `radius` its radius in pixels. Reads the neighbourhood as it was\n * before the dab, then writes every covered pixel.\n */\nexport function blenderDab(medium: TileMedium, rect: RasterRect, cov: ArrayLike<number>, opacity: number, radius: number, settings: BlenderSettings, scratch: BlenderScratch): void {\n const R = blendRadius(settings, radius);\n const W = rect.width + 2 * R;\n const H = rect.height + 2 * R;\n const pigment = settings.mixing === 'pigment';\n // Channels per pixel: pigment: S·a·K/S per band, S·a, a, r·a, g·a, b·a; rgb: r·a, g·a, b·a, a.\n const C = pigment ? N + 2 : 4;\n const stride = (W + 1) * C;\n const size = stride * (H + 1);\n if (scratch.sums.length < size) scratch.sums = new Float64Array(size);\n const I = scratch.sums;\n I.fill(0, 0, size);\n medium.reset();\n // Summed-area table over the region (row 0 and column 0 stay zero).\n const row = new Float64Array(C);\n for (let y = 0; y < H; y++) {\n row.fill(0);\n const dy = rect.y - R + y;\n for (let x = 0; x < W; x++) {\n const dx = rect.x - R + x;\n const i = medium.locate(dx, dy, false);\n if (i >= 0) {\n const px = medium.pixels;\n const a = px[i + 3] / 255;\n if (a > 0) {\n if (pigment) {\n const p = pigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2]));\n const sa = p.scatter * p.strength * a;\n for (let k = 0; k < N; k++) row[k] += sa * p.ks[k];\n row[N] += sa;\n row[N + 1] += a;\n } else {\n row[0] += (px[i] / 255) * a;\n row[1] += (px[i + 1] / 255) * a;\n row[2] += (px[i + 2] / 255) * a;\n row[3] += a;\n }\n }\n }\n const o = (y + 1) * stride + (x + 1) * C;\n const up = y * stride + (x + 1) * C;\n for (let c = 0; c < C; c++) I[o + c] = I[up + c] + row[c];\n }\n }\n const area = (2 * R + 1) * (2 * R + 1);\n const strength = settings.strength * opacity;\n const lock = medium.target.lockTransparency;\n const mixPig = scratch.mix;\n const out = scratch.out;\n const sum = (c: number, x1: number, y1: number, x2: number, y2: number) => I[y2 * stride + x2 * C + c] - I[y1 * stride + x2 * C + c] - I[y2 * stride + x1 * C + c] + I[y1 * stride + x1 * C + c];\n const w = rect.width;\n medium.reset();\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const k = cov[yy * w + xx];\n if (k <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n let t = strength * k * medium.selectionAt(dx, dy);\n if (t <= 0) continue;\n if (t > 1) t = 1;\n // The window [x − R, x + R] in region coordinates, as summed-area bounds.\n const x1 = xx;\n const y1 = yy;\n const x2 = xx + 2 * R + 1;\n const y2 = yy + 2 * R + 1;\n const asum = sum(pigment ? N + 1 : 3, x1, y1, x2, y2);\n const ri = medium.locate(dx, dy, false);\n const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;\n if (lock && a <= 0) continue;\n const avgA = asum / area;\n const na = lock ? a : (1 - t) * a + t * avgA;\n if (asum <= 1e-9) {\n // Only bare canvas nearby: the paint thins out (edges soften).\n if (lock || a <= 0) continue;\n const i = medium.locate(dx, dy, true);\n put255(medium.pixels, i + 3, na);\n continue;\n }\n // The neighbourhood's share of the paint in the result.\n const share = lock ? t : na > 0 ? (t * avgA) / na : 1;\n const i = medium.locate(dx, dy, true);\n const px = medium.pixels;\n if (pigment) {\n const sa = sum(N, x1, y1, x2, y2);\n for (let c = 0; c < N; c++) mixPig.ks[c] = sum(c, x1, y1, x2, y2) / sa;\n mixPig.scatter = sa / asum;\n if (a <= 0 || share >= 1) lerpPigmentInto(mixPig, mixPig, 0, out, 0);\n else lerpPigmentInto(pigment8(Math.round(px[i]), Math.round(px[i + 1]), Math.round(px[i + 2])), mixPig, share, out, 0);\n } else {\n for (let c = 0; c < 3; c++) {\n const m = sum(c, x1, y1, x2, y2) / asum;\n out[c] = a <= 0 || share >= 1 ? m : px[i + c] / 255 + (m - px[i + c] / 255) * share;\n }\n }\n put255(px, i, out[0]);\n put255(px, i + 1, out[1]);\n put255(px, i + 2, out[2]);\n if (!lock) put255(px, i + 3, na);\n }\n }\n}\n\n/** A dab as the smudge needs it: centre, radius (px) and pressure opacity. */\nexport interface SmudgeDab {\n x: number;\n y: number;\n radius: number;\n opacity: number;\n}\n\n/**\n * A smudging finger: carries a patch of paint (straight RGBA, `size × size` cells over the tip,\n * in canvas orientation) from dab to dab. The first dab only picks the patch up.\n */\nexport class SmudgeTip {\n private readonly size: number;\n private readonly patch: Float32Array;\n private loaded = false;\n private readonly out = [0, 0, 0];\n\n constructor(\n readonly settings: SmudgeSettings,\n /** The largest tip diameter of the stroke, in pixels (the patch resolution). */\n diameter: number\n ) {\n this.size = Math.max(2, Math.min(512, Math.ceil(diameter)));\n this.patch = new Float32Array(this.size * this.size * 4);\n }\n\n private cell(d: SmudgeDab, px: number, py: number): number {\n const s = this.size;\n const r = Math.max(0.5, d.radius);\n const bx = Math.min(s - 1, Math.max(0, Math.floor((((px - d.x) / r + 1) / 2) * s)));\n const by = Math.min(s - 1, Math.max(0, Math.floor((((py - d.y) / r + 1) / 2) * s)));\n return (by * s + bx) * 4;\n }\n\n /** Fills the patch from the pixels under the dab. */\n private load(medium: TileMedium, d: SmudgeDab): void {\n const s = this.size;\n const r = Math.max(0.5, d.radius);\n medium.reset();\n for (let by = 0; by < s; by++) {\n for (let bx = 0; bx < s; bx++) {\n const dx = Math.floor(d.x + (((bx + 0.5) / s) * 2 - 1) * r);\n const dy = Math.floor(d.y + (((by + 0.5) / s) * 2 - 1) * r);\n const i = medium.locate(dx, dy, false);\n const c = (by * s + bx) * 4;\n if (i < 0) {\n this.patch.fill(0, c, c + 4);\n continue;\n }\n const px = medium.pixels;\n this.patch[c] = px[i] / 255;\n this.patch[c + 1] = px[i + 1] / 255;\n this.patch[c + 2] = px[i + 2] / 255;\n this.patch[c + 3] = px[i + 3] / 255;\n }\n }\n this.loaded = true;\n }\n\n /** `a` mixed `t` of the way to `b` (both 0..1) into `out`. */\n private mix(a0: number, a1: number, a2: number, b0: number, b1: number, b2: number, t: number): number[] {\n const o = this.out;\n if (this.settings.mixing === 'pigment') {\n mixPigment8(Math.round(a0 * 255), Math.round(a1 * 255), Math.round(a2 * 255), Math.round(b0 * 255), Math.round(b1 * 255), Math.round(b2 * 255), t, o);\n } else {\n o[0] = a0 + (b0 - a0) * t;\n o[1] = a1 + (b1 - a1) * t;\n o[2] = a2 + (b2 - a2) * t;\n }\n return o;\n }\n\n /** One dab: lays the carried patch over the tip's footprint (`cov` over `rect`), then picks up. */\n dab(medium: TileMedium, d: SmudgeDab, rect: RasterRect, cov: ArrayLike<number>): void {\n if (!this.loaded) {\n this.load(medium, d);\n return;\n }\n const { strength, length } = this.settings;\n const lock = medium.target.lockTransparency;\n const patch = this.patch;\n const w = rect.width;\n medium.reset();\n for (let yy = 0; yy < rect.height; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const cv = cov[yy * w + xx];\n if (cv <= 0) continue;\n const dx = rect.x + xx;\n const dy = rect.y + yy;\n let k = strength * cv * d.opacity * medium.selectionAt(dx, dy);\n if (k <= 0) continue;\n if (k > 1) k = 1;\n const c = this.cell(d, dx + 0.5, dy + 0.5);\n const ba = patch[c + 3];\n const ri = medium.locate(dx, dy, false);\n const a = ri < 0 ? 0 : medium.pixels[ri + 3] / 255;\n if (lock && a <= 0) continue;\n // The pixel as it was: what the finger lays paint over, and what it picks up afterwards.\n const px0 = ri >= 0 ? medium.pixels : null;\n const o0 = px0 ? px0[ri] / 255 : 0;\n const o1 = px0 ? px0[ri + 1] / 255 : 0;\n const o2 = px0 ? px0[ri + 2] / 255 : 0;\n if (a > 0 || ba > 0) {\n const na = lock ? a : a + (ba - a) * k;\n const share = lock ? k * ba : na > 0 ? (k * ba) / na : 0;\n let r = o0;\n let g = o1;\n let b = o2;\n if (a <= 0 || share >= 1) {\n r = patch[c];\n g = patch[c + 1];\n b = patch[c + 2];\n } else if (share > 0) {\n const m = this.mix(o0, o1, o2, patch[c], patch[c + 1], patch[c + 2], share);\n r = m[0];\n g = m[1];\n b = m[2];\n }\n const i = medium.locate(dx, dy, true);\n const out = medium.pixels;\n put255(out, i, r);\n put255(out, i + 1, g);\n put255(out, i + 2, b);\n put255(out, i + 3, na);\n }\n // Pick up: the patch takes `1 − length` of the paint that was under it (alpha-weighted).\n const pick = (1 - length) * Math.min(1, cv * d.opacity);\n if (pick > 0) {\n const pa = ba + (a - ba) * pick;\n const s = pa > 0 ? (a * pick) / pa : 0;\n if (s > 0) {\n const m = ba <= 0 || s >= 1 ? [o0, o1, o2] : this.mix(patch[c], patch[c + 1], patch[c + 2], o0, o1, o2, s);\n patch[c] = m[0];\n patch[c + 1] = m[1];\n patch[c + 2] = m[2];\n }\n patch[c + 3] = pa;\n }\n }\n }\n }\n}\n","// Portions ported from photocraft/crates/paint/src/mixer.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The Mixer Brush: a reservoir of loaded paint plus a pickup well that takes up colour from the\n * canvas under each dab (photocraft's behavioural model):\n *\n * - Each dab samples the canvas under its footprint (coverage- and alpha-weighted). The pickup\n * colour `P` moves towards that sample by `PICKUP_RATE × wet` (scaled by the sample's coverage).\n * - The dab's paint mixes the reservoir `R` (weight `amount × (1 − mix·wet)`) with the pickup\n * (weight `mix·wet`) and lands at `flow × opacity` times the total weight, over the pixels as\n * they are now, so earlier dabs of the same stroke are picked up again (smearing).\n * - The reservoir drains per dab unless `load` is 1, so low-load strokes run dry. A dry brush\n * (`wet` 0) paints only reservoir colour; an empty, wet brush smears canvas colour.\n *\n * `mixing: 'pigment'` does all three mixes (pickup, paint and deposit) with the Kubelka-Munk model\n * of `./pigment`, so a blue brush dragged through yellow makes green instead of grey. Everything\n * is data in, pixels out: the same canvas, points, settings and load replay identically.\n */\n\nimport { DabWalker, dabRect, rasterizeDab, type BrushSettings, type Dab, type StrokePoint } from './brush';\nimport {\n EMPTY_MIXER_LOAD,\n exchangeRates,\n MIXER_DRAIN,\n MIXER_PICKUP_RATE,\n MIXING_MODELS,\n PaintExchange,\n TileMedium,\n type ExchangeRates,\n type MixerLoad,\n type MixingModel,\n type PaintTileTarget,\n type RgbTriple,\n} from './exchange';\nimport type { RasterRect as CropBox } from '../raster/surface';\n\n\nexport { EMPTY_MIXER_LOAD, MIXER_DRAIN, MIXER_PICKUP_RATE, MIXING_MODELS, type MixerLoad, type MixingModel };\n\n/** Mixer Brush options. */\nexport interface MixerSettings {\n /** How much colour the brush picks up from the canvas (the pick-up ratio), 0 (dry) .. 1 (default 0.5). */\n wet: number;\n /** How much paint the reservoir holds, 0 .. 1 (default 0.5): below 1 it drains per dab and the stroke runs dry. */\n load: number;\n /** How far each dab's paint moves from the reservoir towards the picked-up colour (the mix ratio), 0 .. 1 (default 0.5). */\n mix: number;\n /** Refill the reservoir with the paint colour at the start of each stroke (default true). */\n loadEachStroke: boolean;\n /** Empty the pickup well after each stroke (default true); off keeps the brush dirty for the next stroke. */\n cleanEachStroke: boolean;\n /** `rgb` (default) or `pigment` (blue and yellow make green). */\n mixing: MixingModel;\n}\n\nexport const DEFAULT_MIXER: Readonly<MixerSettings> = {\n wet: 0.5,\n load: 0.5,\n mix: 0.5,\n loadEachStroke: true,\n cleanEachStroke: true,\n mixing: 'rgb',\n};\n\n/** Mixer settings with every field in range (missing fields from `DEFAULT_MIXER`). */\nexport function normalizeMixer(raw: Partial<MixerSettings> = {}): MixerSettings {\n const unit = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d);\n const bool = (v: unknown, d: boolean) => (typeof v === 'boolean' ? v : d);\n return {\n wet: unit(raw.wet, DEFAULT_MIXER.wet),\n load: unit(raw.load, DEFAULT_MIXER.load),\n mix: unit(raw.mix, DEFAULT_MIXER.mix),\n loadEachStroke: bool(raw.loadEachStroke, DEFAULT_MIXER.loadEachStroke),\n cleanEachStroke: bool(raw.cleanEachStroke, DEFAULT_MIXER.cleanEachStroke),\n mixing: raw.mixing === 'pigment' ? 'pigment' : 'rgb',\n };\n}\n\n/** The per-dab exchange rates of Mixer Brush settings (`wet` is the pick-up ratio, `mix` the mix ratio). */\nexport function mixerRates(settings: MixerSettings): ExchangeRates {\n return exchangeRates(settings.wet, settings.mix, settings.load, settings.mixing);\n}\n\n/** The brush loaded full with `color` (sRGB 0..1); the pickup well is kept. */\nexport function loadMixer(load: MixerLoad, color: readonly [number, number, number]): MixerLoad {\n const c = (v: number) => Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0));\n return { reservoir: [c(color[0]), c(color[1]), c(color[2])], amount: 1, pickup: load.pickup };\n}\n\n/** The brush with its pickup well emptied; the reservoir is kept. */\nexport function cleanMixer(load: MixerLoad): MixerLoad {\n return { reservoir: load.reservoir, amount: load.amount, pickup: null };\n}\n\n/** The pixels a mixer stroke reads and writes (RGBA tiles in layer space). */\nexport type MixerTarget = PaintTileTarget;\n\n/**\n * One Mixer Brush stroke. Feed points with `push` (any chunks give the same dabs), end with\n * `finish`; `load` is the brush load after the dabs so far. Each call returns the document\n * rectangles it touched. Runs on the shared pick-up / mix exchange (`./exchange`).\n */\nexport class MixerStroke {\n dabCount = 0;\n bounds: CropBox | null = null;\n private walker: DabWalker;\n private smoothPos: StrokePoint | null = null;\n private buf = new Float32Array(0);\n private readonly exchange: PaintExchange;\n private readonly medium: TileMedium;\n private readonly paintColor: RgbTriple = [0, 0, 0];\n\n constructor(\n readonly brush: BrushSettings,\n settings: Partial<MixerSettings>,\n load: MixerLoad,\n target: MixerTarget\n ) {\n this.walker = new DabWalker(brush);\n this.exchange = new PaintExchange(mixerRates(normalizeMixer(settings)), load);\n this.medium = new TileMedium(target);\n }\n\n /** The brush load after the dabs so far. */\n get load(): MixerLoad {\n return this.exchange.load;\n }\n\n push(points: readonly StrokePoint[]): CropBox[] {\n const dabs: Dab[] = [];\n for (const p of points) this.walker.push(this.smooth(p), dabs);\n return this.paint(dabs);\n }\n\n finish(last?: StrokePoint): CropBox[] {\n const dabs: Dab[] = [];\n if (last && this.smoothPos && (this.smoothPos.x !== last.x || this.smoothPos.y !== last.y)) this.walker.push(last, dabs);\n this.walker.finish(dabs);\n return this.paint(dabs);\n }\n\n private smooth(p: StrokePoint): StrokePoint {\n const a = 1 - Math.min(0.95, Math.max(0, this.brush.smoothing));\n const cur = this.smoothPos;\n const n = cur && a < 1 ? { ...p, x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a } : p;\n this.smoothPos = n;\n return n;\n }\n\n private paint(dabs: Dab[]): CropBox[] {\n const rects: CropBox[] = [];\n for (const d of dabs) {\n const rect = dabRect(d, this.brush.hardness);\n const n = rect.width * rect.height;\n if (this.buf.length < n) this.buf = new Float32Array(n);\n const cov = this.buf.subarray(0, n);\n // Pure tip coverage: flow scales the deposit, not the sampling weights.\n rasterizeDab({ ...d, alpha: 1 }, this.brush.hardness, rect, cov);\n this.dab(d, rect, cov);\n rects.push(rect);\n this.bounds = union(this.bounds, rect);\n this.dabCount++;\n }\n return rects;\n }\n\n private dab(d: Dab, rect: CropBox, cov: Float32Array): void {\n const ex = this.exchange;\n // 1. Pick up from the canvas under the dab; 2. mix the dab's paint and lay it; 3. drain.\n ex.pickUp(this.medium.sample(rect, cov));\n const total = ex.paint(this.paintColor);\n if (total > 0) {\n const dep = Math.min(1, total) * this.brush.flow * this.brush.opacity * d.opacity;\n if (dep > 0) this.medium.deposit(rect, cov, dep, this.paintColor, ex.paintPigment);\n }\n ex.drain();\n }\n}\n\nfunction union(a: CropBox | null, b: CropBox): CropBox {\n if (!a) return { ...b };\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|