@pixerate/editor 0.252.0 → 0.254.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.
Files changed (73) hide show
  1. package/dist/animation-editor/index.cjs +53 -53
  2. package/dist/animation-editor/index.js +2 -2
  3. package/dist/brushes/index.cjs +229 -2014
  4. package/dist/brushes/index.d.cts +5 -1
  5. package/dist/brushes/index.d.ts +5 -1
  6. package/dist/brushes/index.js +236 -2021
  7. package/dist/brushes/patch.d.cts +23 -0
  8. package/dist/brushes/patch.d.ts +23 -0
  9. package/dist/brushes/ui.d.cts +105 -0
  10. package/dist/brushes/ui.d.ts +105 -0
  11. package/dist/canvas/index.cjs +7 -7
  12. package/dist/canvas/index.js +1 -1
  13. package/dist/{chunk-QKXO6JPP.cjs → chunk-2OQGPEHH.cjs} +14 -14
  14. package/dist/chunk-34KLWUH4.cjs +2284 -0
  15. package/dist/{chunk-Z3BRH4JK.cjs → chunk-557BDWKO.cjs} +6 -6
  16. package/dist/{chunk-ZBCT5MEA.cjs → chunk-CBVNCWJM.cjs} +7 -7
  17. package/dist/{chunk-226QDRO3.js → chunk-DHQPRKXF.js} +1 -1
  18. package/dist/{chunk-ABL7IWFZ.js → chunk-GBEF2D4P.js} +1 -1
  19. package/dist/{chunk-ISIO63Q5.cjs → chunk-L2JTLW6B.cjs} +150 -25
  20. package/dist/{chunk-JA57JIT5.js → chunk-MV7CYFMJ.js} +133 -8
  21. package/dist/{chunk-5E2Q7ZHI.js → chunk-MWUZCEFT.js} +1 -1
  22. package/dist/chunk-QPHNVZHO.js +2284 -0
  23. package/dist/effects/conformance.d.cts +1 -0
  24. package/dist/effects/conformance.d.ts +1 -0
  25. package/dist/effects/fake.d.cts +7 -1
  26. package/dist/effects/fake.d.ts +7 -1
  27. package/dist/effects/helpers.d.cts +36 -4
  28. package/dist/effects/helpers.d.ts +36 -4
  29. package/dist/effects/index.cjs +331 -102
  30. package/dist/effects/index.js +238 -9
  31. package/dist/effects/types.d.cts +84 -5
  32. package/dist/effects/types.d.ts +84 -5
  33. package/dist/image-editor/index.cjs +15 -15
  34. package/dist/image-editor/index.js +1 -1
  35. package/dist/paint-editor/brush-controls.d.cts +57 -0
  36. package/dist/paint-editor/brush-controls.d.ts +57 -0
  37. package/dist/paint-editor/brush-library-tools.d.cts +15 -0
  38. package/dist/paint-editor/brush-library-tools.d.ts +15 -0
  39. package/dist/paint-editor/brush-library.d.cts +29 -0
  40. package/dist/paint-editor/brush-library.d.ts +29 -0
  41. package/dist/paint-editor/controller.d.cts +76 -17
  42. package/dist/paint-editor/controller.d.ts +76 -17
  43. package/dist/paint-editor/index.cjs +543 -858
  44. package/dist/paint-editor/index.d.cts +1 -0
  45. package/dist/paint-editor/index.d.ts +1 -0
  46. package/dist/paint-editor/index.js +329 -644
  47. package/dist/paint-editor/serialize.d.cts +7 -4
  48. package/dist/paint-editor/serialize.d.ts +7 -4
  49. package/dist/paint-editor/sticker-tools.d.cts +1 -1
  50. package/dist/paint-editor/sticker-tools.d.ts +1 -1
  51. package/dist/paint-editor/strokes.d.cts +10 -1
  52. package/dist/paint-editor/strokes.d.ts +10 -1
  53. package/dist/paint-editor/tools.d.cts +1 -1
  54. package/dist/paint-editor/tools.d.ts +1 -1
  55. package/dist/paint-editor/ui.d.cts +3 -51
  56. package/dist/paint-editor/ui.d.ts +3 -51
  57. package/dist/paint-editor/wet-tools.d.cts +1 -1
  58. package/dist/paint-editor/wet-tools.d.ts +1 -1
  59. package/dist/vector/index.cjs +6 -6
  60. package/dist/vector/index.js +1 -1
  61. package/dist/video-editor/index.cjs +344 -344
  62. package/dist/video-editor/index.js +4 -4
  63. package/package.json +1 -1
  64. package/skills/pixerate-brushes/SKILL.md +5 -3
  65. package/skills/pixerate-paint-editor/SKILL.md +26 -24
  66. package/skills/pixerate-paint-editor/references/brush-facets.md +30 -0
  67. package/skills/pixerate-paint-editor/references/gradient-paint.md +1 -1
  68. package/skills/pixerate-paint-editor/references/light-paint.md +2 -2
  69. package/skills/pixerate-paint-editor/references/oils.md +1 -1
  70. package/skills/pixerate-paint-editor/references/particle-brushes.md +8 -5
  71. package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -1
  72. package/skills/pixerate-stickers/SKILL.md +1 -1
  73. package/skills/pixerate-video-editor/SKILL.md +1 -1
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
3
 
4
4
 
@@ -11,2045 +11,252 @@
11
11
 
12
12
 
13
13
 
14
- var _chunkHS6QDWDFcjs = require('../chunk-HS6QDWDF.cjs');
15
14
 
16
15
 
17
16
 
18
17
 
19
- var _chunkJSDND7DFcjs = require('../chunk-JSDND7DF.cjs');
20
- require('../chunk-U67EN7GG.cjs');
21
18
 
22
19
 
23
- var _chunkWOFXZ3XUcjs = require('../chunk-WOFXZ3XU.cjs');
24
20
 
25
21
 
26
22
 
27
23
 
28
- var _chunkFQPUF2W2cjs = require('../chunk-FQPUF2W2.cjs');
29
- require('../chunk-3K6HWEXZ.cjs');
30
24
 
31
25
 
32
- var _chunk2VX55NFMcjs = require('../chunk-2VX55NFM.cjs');
33
- require('../chunk-474IQG74.cjs');
34
26
 
35
- // src/brushes/types.ts
36
- var BRUSH_KINDS = ["mark", "tool"];
37
- var BRUSH_MEDIA = ["paint", "ink", "dry", "spray"];
38
- var BRUSH_TOOLS = ["eraser", "blender", "smudge", "water", "dryer", "blow", "blot", "wet-blend", "wet-drag"];
39
- var BRUSH_TIP_SHAPES = ["round", "flat", "chisel", "bristle", "stamp"];
40
- var BRUSH_MARKS_KINDS = ["continuous", "particles", "objects"];
41
- var BRUSH_SURFACES = ["dry", "wet", "thick"];
42
-
43
- // src/brushes/schema.ts
44
- var MAX_BRUSH_PALETTE = 16;
45
- var BRUSH_WORD = /^[a-z0-9][a-z0-9-]{0,39}$/;
46
- var unit = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
47
- var num = (minimum, maximum, description) => ({ type: "number", minimum, maximum, description });
48
- var int = (minimum, maximum, description) => ({ type: "integer", minimum, maximum, description });
49
- var bool = (description) => ({ type: "boolean", description });
50
- var oneOf = (values, description) => ({ type: "string", enum: [...values], description });
51
- var object = (description, properties, required) => ({
52
- type: "object",
53
- description,
54
- properties,
55
- ...required ? { required } : {},
56
- additionalProperties: false
57
- });
58
- var MIXING = oneOf(["pigment", "rgb"], "How colours mix: pigment (Kubelka-Munk, blue and yellow make green) or rgb (straight averages). Default pigment; a Mixer Brush and the smudge tool default to rgb.");
59
- var BRUSH_STAMP_PATTERN = `^(?:[^:\\s]+:\\S+|${_chunkJSDND7DFcjs.STAMP_KINDS.join("|")})$`;
60
- var STAMP = {
61
- type: "string",
62
- pattern: BRUSH_STAMP_PATTERN,
63
- description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${_chunkJSDND7DFcjs.STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}). A bare editor stamp (star) means editor:star.`
64
- };
65
- var BRISTLES = object("Bristle tips: the tuft. Missing fields take their defaults.", {
66
- count: int(3, 128, "Bristles in the tuft (default 32)."),
67
- stiffness: unit("Stiff bristles follow the tip; soft ones lag and bend into curves at turns (default 0.5)."),
68
- splay: unit("How far pressure spreads the tuft (default 0.5)."),
69
- thickness: unit("Each bristle's line width as a fraction of the tip size (default 0.06)."),
70
- load: unit("Paint per bristle: 1 never runs dry; lower, bristles run out along the stroke, a dry brush (default 0.6)."),
71
- jitter: unit("Random variation between bristles: length, paint, position (default 0.4).")
72
- });
73
- var TIP = object(
74
- "The tip: its shape and the dab it lays.",
75
- {
76
- shape: oneOf(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: textured stamps (see stamps), laid as objects or particles."),
77
- size: num(1, _chunkFQPUF2W2cjs.MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
78
- hardness: unit("Edge hardness, 0 soft .. 1 hard (default 1)."),
79
- spacing: num(0.01, 10, "Dab spacing as a fraction of the diameter: 0.05 smooth, 1 dotted (default 0.1)."),
80
- angle: num(-360, 360, "Tip angle in degrees, counter-clockwise; added to the stroke direction or barrel with dynamics.rotation (default 0)."),
81
- roundness: num(0.01, 1, "Roundness: 1 round (default), lower a flat ellipse (pastel, chisel)."),
82
- opacity: unit("Stroke opacity ceiling: overlapping dabs never exceed it (default 1)."),
83
- flow: unit("Build-up per dab towards the opacity: low flow builds softly, like an airbrush (default 1)."),
84
- blend: oneOf(
85
- _chunk2VX55NFMcjs.BLEND_MODES.map((m) => m.id),
86
- "How the stroke paints onto the layer (default normal). pigment mixes like real paint; multiply darkens like marker ink; watercolor glazes; tint, highlight and shadow recolour, light and shade."
87
- ),
88
- bristle: BRISTLES,
89
- stamps: {
90
- type: "array",
91
- minItems: 1,
92
- maxItems: _chunkHS6QDWDFcjs.MAX_STICKER_STAMPS,
93
- items: STAMP,
94
- description: `Stamp tips: the stickers. Objects take any sticker and pick one at random per mark (default editor:star; repeats count once); particles take only the editor's stamps, use the first ${_chunkHS6QDWDFcjs.MAX_DOT_TEXTURES} in marks.stampOrder, and repeats weight the order.`
95
- }
96
- },
97
- ["shape"]
98
- );
99
- var CONTINUOUS = object("Dabs laid along the path (the default).", { kind: { type: "string", const: "continuous", description: 'Always "continuous".' } }, ["kind"]);
100
- var PARTICLES = object(
101
- "Particles that move with the pen and draw as they go. Missing fields take their defaults.",
102
- {
103
- kind: { type: "string", const: "particles", description: 'Always "particles".' },
104
- physics: oneOf(
105
- _chunkHS6QDWDFcjs.PARTICLE_KINDS,
106
- "How the particles move (default spring). string: strands dragged behind the pen (hair, fur); spring: on springs to the pen, overshooting; orbit: circling the pen (curls); flow: thrown into a swirling flow until they fade; elastic: links that stretch and snap back; fuzzy: winding strings waving out of step."
107
- ),
108
- render: oneOf(_chunkHS6QDWDFcjs.PARTICLE_RENDERS, "How they draw (default line). dot: a mark per step; line: a continuous line each; sequence: beads spaced along each path; ribbon: the band between neighbouring paths filled."),
109
- count: int(1, 64, "Particles (flow: thrown per tip of travel). Default 12."),
110
- size: unit("Mark size as a fraction of the tip size; ribbon: its edge width (default 0.08)."),
111
- spread: unit("How far apart they start, as a fraction of the tip size; ribbon: its width (default 0.5)."),
112
- stiffness: unit("Spring, string and elastic stiffness: stiff follows closely, soft swings wide (default 0.4)."),
113
- damping: unit("How quickly motion dies down (default 0.25)."),
114
- speed: unit("Orbit and flow speed (default 0.5)."),
115
- gravity: num(-1, 1, "Pull downwards; negative pulls up (default 0)."),
116
- life: unit("Flow particles: how long they live (default 0.5)."),
117
- interval: unit("Sequence: spacing of the beads (default 0.3)."),
118
- ribbonFill: unit("Ribbon: fill opacity between the paths, relative to their edges (default 0.5; 0 only edges)."),
119
- ribbonPairs: oneOf(_chunkHS6QDWDFcjs.RIBBON_PAIRS, "Ribbon: adjacent (default) joins each particle to its neighbour; first-last makes one band between the outermost two."),
120
- waveAmplitude: unit("Wave: sideways swing as a fraction of the tip size (default 0: no wave)."),
121
- waveLength: num(4, 2e3, "Wave: wavelength in px of pen travel (default 60)."),
122
- wavePhaseJitter: unit("Wave: how far out of step the particles' waves are; 0 in step, 1 random phases (default 0)."),
123
- stampOrder: oneOf(_chunkHS6QDWDFcjs.DOT_TEXTURE_ORDERS, "Stamp tips: sequence (default) uses the stamps in turn; random picks one per dot, seeded."),
124
- stampRotation: oneOf(_chunkHS6QDWDFcjs.DOT_ROTATIONS, "Stamp tips: follow turns each with its particle's motion; random (default); fixed uses the tip angle.")
125
- },
126
- ["kind"]
127
- );
128
- var OBJECTS = object(
129
- "Each mark stays an editable object (a sticker) on a sticker layer; needs a stamp tip. Missing fields take their defaults.",
130
- {
131
- kind: { type: "string", const: "objects", description: 'Always "objects".' },
132
- density: unit("About density \xD7 4 marks per tip diameter of travel; 0.25 \u2248 one per diameter (default 0.4)."),
133
- sizeJitter: unit("Random size change (default 0.4)."),
134
- rotationJitter: unit("Random turn as a fraction of \xB1180\xB0 (default 1)."),
135
- scatter: num(0, 2, "How far from the path marks land, in tip diameters (default 0.5)."),
136
- colourJitter: unit("Random colour change: at 1 up to \xB140\xB0 of hue and \xB10.15 OKLab lightness (default 0.2).")
137
- },
138
- ["kind"]
139
- );
140
- var DYNAMICS = object("Pen feel: pressure, tilt, velocity, jitter, rotation and smoothing. Every amount 0 (off) .. 1 unless noted; missing fields take their defaults.", {
141
- pressureSize: bool("Pressure scales the size, down to minSize (default true)."),
142
- pressureOpacity: bool("Pressure scales the opacity (default false)."),
143
- smoothing: num(0, 0.95, "Position smoothing of the input (default 0)."),
144
- pressureSmoothing: num(0, 0.95, "Pressure smoothing, separate from position smoothing (default 0)."),
145
- rotation: oneOf(_chunkHS6QDWDFcjs.BRUSH_ROTATIONS, "What turns the tip: fixed (tip.angle, default), stroke (direction of travel) or barrel (pen barrel rotation)."),
146
- minSize: unit("Size at zero pressure as a fraction of the full size, with pressureSize (default 0)."),
147
- pressureFlow: unit("How much pressure scales flow (default 0)."),
148
- tiltSize: unit("Laying the pen down widens the tip, up to 2.5\xD7 (default 0)."),
149
- tiltRoundness: unit("Laying the pen down flattens the tip along the tilt, side-of-the-pencil shading (default 0)."),
150
- tiltOpacity: unit("Laying the pen down lightens the mark (default 0)."),
151
- velocitySize: num(-1, 1, "Positive grows the tip on fast strokes, negative thins it; needs timed points (default 0)."),
152
- velocityOpacity: num(-1, 1, "Positive darkens fast marks, negative lightens them; needs timed points (default 0)."),
153
- sizeJitter: unit("Random size change per dab (default 0)."),
154
- angleJitter: unit("Random angle per dab, as a fraction of \xB1180\xB0 (default 0)."),
155
- opacityJitter: unit("Random opacity drop per dab (default 0)."),
156
- scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0).")
157
- });
158
- var EXCHANGE = object("Paint exchange with the canvas: picking up paint and mixing it into what the brush lays. Paint brushes on a dry, wet or thick surface and Mixer Brushes.", {
159
- pickup: unit("Pick-up ratio: how much of the paint under each dab the brush lifts and carries (default 0; with a reservoir 0.5)."),
160
- mix: unit("Mix ratio: how much of the carried paint goes into what the brush lays, against its own colour (default 0.5)."),
161
- dirty: bool("Keep the carried paint after the stroke, so the next one starts with the last colour touched (default false)."),
162
- reservoir: object("A paint reservoir makes this a Mixer Brush (an empty object takes every default).", {
163
- load: unit("How much paint the reservoir holds; below 1 it drains per dab and long strokes run dry (default 0.5)."),
164
- refill: bool("Refill with the paint colour before each stroke (default true)."),
165
- loadsColour: bool("Load the current colour at all (default true; false: it only moves paint already down).")
166
- })
167
- });
168
- var WET = object("Wet surface (watercolour, gouache, wet oil) and the wet tools. Missing fields take their defaults.", {
169
- medium: oneOf(_chunkHS6QDWDFcjs.WET_MEDIA, "The wet medium; a layer takes the medium's wet settings the first time it holds wet paint (default: the variety when it is watercolor, gouache or oil, else watercolor)."),
170
- water: unit("Wetness: water laid, 0.2 a damp brush .. 1 dripping (default 0.5)."),
171
- pigment: unit("Pigment concentration: 0 clear water .. 1 straight from the tube (default 0.5)."),
172
- body: unit("Body: 0 transparent watercolour .. 1 opaque gouache (default 0)."),
173
- rewet: unit("How much dried paint the water lifts again: 0 staining (default 0)."),
174
- load: unit("Paint loading: 1 never runs out; lower, the brush runs dry along a long stroke (default 1)."),
175
- thinner: unit("Thinner (water or solvent): more transparent, runnier paint (default 0).")
176
- });
177
- var THICK = object("Thick surface (impasto: paint with relief, lit by the canvas lighting). Missing fields take their defaults.", {
178
- applicator: oneOf(_chunkHS6QDWDFcjs.IMPASTO_TOOLS, "What lays the paint (default brush). brush: bristle ridges; tube: blobs squeezed from the tube; knife: scrapes the relief flat and smears; roller: a flat stippled band; gloop: a thick glossy bead."),
179
- thickness: unit("How far the paint stands up (default 0.6)."),
180
- gloss: unit("Shine: 0 matte .. 1 wet-looking gloss (default 0.4)."),
181
- metallic: unit("Metallic: highlights take the paint colour, gold, copper, silver (default 0)."),
182
- load: unit("Paint loading: 1 never runs out; lower, the stroke thins as the brush empties (default 0.9)."),
183
- thinner: unit("Thinner: flatter, more transparent paint (default 0)."),
184
- drag: unit("Knife and brush: how much of the paint under the tool is dragged along (default 0.4)."),
185
- rim: unit("Light paint: light is stronger where the bead turns away, as on backlit glass (default 0)."),
186
- clarity: unit("Glassy paint: the middle of a bead lets the layers below show through (default 0).")
187
- });
188
- var BEHAVIOUR = object("What the material does on the surface, and how tools work. Missing fields take their defaults.", {
189
- surface: oneOf(BRUSH_SURFACES, "dry (default): stays where it lands; wet: flows, pools and dries (see wet); thick: stands up in relief (see thick)."),
190
- grain: unit("Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Default 0."),
191
- mixing: MIXING,
192
- exchange: EXCHANGE,
193
- wet: WET,
194
- thick: THICK,
195
- strength: unit("Tools: how strongly the blender, smudge or wet tool works per dab (default 0.5; smudge 0.7)."),
196
- distance: unit("Blender: how far away colours are mixed in, as a fraction of the tip diameter (default 0.35)."),
197
- length: unit("Smudge: how far paint is dragged, the share of carried paint kept per dab (default 0.85).")
198
- });
199
- var LIGHT = object("Light the brush gives off with its colour (glow and spill). Give light brushes dark paper.", {
200
- amount: unit("Light per dab, scaled by its coverage and flow (default 0: none)."),
201
- pressure: bool("Pressure scales the light (default false)."),
202
- spill: object("Suggested spill for the layer, applied when it has none of its own.", {
203
- radius: num(0, 512, "How far the light reaches, px; 0 turns spill off (a highlighter)."),
204
- strength: num(0, 2, "How strong the spill is.")
205
- }),
206
- glow: bool("Suggest a glow layer effect for the layer (glow stays a layer effect).")
207
- });
208
- var MOTION = object("Animation after painting.", {
209
- live: bool("Effect paint keeps playing on stroke layers (default false)."),
210
- fps: int(1, 60, "Suggested playback rate in frames per second."),
211
- waveSpeed: unit("Particle waves: how fast they run by stroke time; 0 they stand still (default 0), 1 about four a second.")
212
- });
213
- var SOLID = {
214
- type: "string",
215
- pattern: "^(#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*(,\\s*(0|1|0?\\.\\d+)\\s*)?\\))$",
216
- description: "A solid colour: #rrggbb, #rrggbbaa or rgba(r, g, b, a)."
217
- };
218
- var DUAL = object("A colour for each colour scheme; the host picks one when the stroke starts and records it.", { light: SOLID, dark: SOLID }, ["light", "dark"]);
219
- var EFFECT = object(
220
- "Effect paint: the brush paints an effect (search_effects).",
221
- {
222
- type: { type: "string", const: "effect", description: 'Always "effect".' },
223
- effect: { type: "string", minLength: 1, description: "The effect id." },
224
- provider: { type: "string", description: "The provider's id when it was chosen." },
225
- values: { type: "object", description: "Every control value (number, string, boolean or a number list)." },
226
- time: { type: "number", minimum: 0, description: "Which moment of an animated effect, ms." },
227
- map: object(
228
- "Lay the effect along each stroke instead of fixing it to the canvas.",
229
- {
230
- by: { type: "string", const: "stroke", description: 'Always "stroke".' },
231
- length: num(16, 4096, "Strip length: px of stroke per repeat of the effect."),
232
- repeat: oneOf(["repeat", "mirror", "clamp"], "Past the strip end: start again, run back or hold.")
233
- },
234
- ["by", "length", "repeat"]
235
- ),
236
- live: bool("On a stroke layer the stroke keeps playing the effect.")
237
- },
238
- ["type", "effect", "provider", "values", "time"]
239
- );
240
- var PAINT = {
241
- description: "A colour a brush can carry: a solid colour, a light / dark pair, a gradient or effect paint.",
242
- anyOf: [SOLID, DUAL, _chunkFQPUF2W2cjs.PAINT_GRADIENT_JSON_SCHEMA, EFFECT]
243
- };
244
- var COLOUR = object("The brush's colours. The user's colour still wins unless own is set.", {
245
- default: {
246
- ...PAINT,
247
- description: "The colour the brush starts with. A gradient is always painted (its paint / secondary stops take the user's colours); a solid colour or effect paint is suggested to the host."
248
- },
249
- palette: { type: "array", maxItems: MAX_BRUSH_PALETTE, items: PAINT, description: `Up to ${MAX_BRUSH_PALETTE} suggested colours.` },
250
- own: bool("The brush always paints its own colour (a rainbow marker); default false.")
251
- });
252
- var BRUSH_JSON_SCHEMA = object(
253
- "A brush made of facets. Only id, name, kind and tip.shape are required; every other field takes the default for its engine.",
254
- {
255
- id: { type: "string", pattern: _chunkHS6QDWDFcjs.BRUSH_ID.source, description: "Library id: lowercase letters, digits and dashes, up to 64 characters." },
256
- name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
257
- description: { type: "string", maxLength: 500, description: "What the brush is for, in a sentence." },
258
- tags: { type: "array", maxItems: 16, uniqueItems: true, items: { type: "string", pattern: BRUSH_WORD.source, description: "A tag: lowercase letters, digits and dashes." }, description: "Search tags." },
259
- kind: oneOf(BRUSH_KINDS, "mark: lays a material (see medium); tool: works on paint already down (see tool)."),
260
- medium: oneOf(BRUSH_MEDIA, "Marks: what it lays. paint (dry, wet or thick), ink, dry (pencil, pastel, charcoal) or spray (airbrush). Descriptive: the tip, marks and behaviour decide how it renders."),
261
- variety: { type: "string", pattern: BRUSH_WORD.source, description: "What kind of the medium: oil, watercolor, gouache, pencil, pastel, charcoal, airbrush, marker, highlighter\u2026 A wet brush whose variety is watercolor, gouache or oil paints that medium." },
262
- tool: oneOf(
263
- BRUSH_TOOLS,
264
- "Tools: eraser removes paint; blender mixes the colours under the tip; smudge drags paint along; water lays clear water (lifts dried paint with behaviour.wet.rewet); dryer dries wet paint; blow pushes water along; blot takes wet paint up; wet-blend evens out wet pigment; wet-drag drags wet paint."
265
- ),
266
- tip: TIP,
267
- marks: { description: "How the tip is laid along the path (default continuous).", oneOf: [CONTINUOUS, PARTICLES, OBJECTS] },
268
- dynamics: DYNAMICS,
269
- behaviour: BEHAVIOUR,
270
- light: LIGHT,
271
- motion: MOTION,
272
- colour: COLOUR
273
- },
274
- ["id", "name", "kind", "tip"]
275
- );
276
- var BRUSH_FACET_SCHEMAS = {
277
- tip: TIP,
278
- marks: { continuous: CONTINUOUS, particles: PARTICLES, objects: OBJECTS },
279
- dynamics: DYNAMICS,
280
- behaviour: BEHAVIOUR,
281
- light: LIGHT,
282
- motion: MOTION,
283
- colour: COLOUR,
284
- paint: PAINT
285
- };
286
27
 
287
- // src/brushes/normalize.ts
288
- var WET_BRUSH_TOOLS = ["water", "dryer", "blow", "blot", "wet-blend", "wet-drag"];
289
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
290
- var clone = (v) => JSON.parse(JSON.stringify(v));
291
- var SOLID2 = new RegExp(String(BRUSH_FACET_SCHEMAS.paint.anyOf[0].pattern));
292
- function read(value, schema, path, problems) {
293
- if (schema.const !== void 0) {
294
- if (value === schema.const) return value;
295
- problems.push(`${path} must be ${JSON.stringify(schema.const)}; dropped.`);
296
- return void 0;
297
- }
298
- switch (schema.type) {
299
- case "number":
300
- case "integer": {
301
- if (typeof value !== "number" || !Number.isFinite(value)) {
302
- problems.push(`${path} must be a number; dropped.`);
303
- return void 0;
304
- }
305
- let n = schema.type === "integer" ? Math.round(value) : value;
306
- if (schema.minimum !== void 0 && n < schema.minimum) n = schema.minimum;
307
- if (schema.maximum !== void 0 && n > schema.maximum) n = schema.maximum;
308
- if (n !== value) problems.push(`${path} ${value} is out of range (${schema.minimum}..${schema.maximum}${schema.type === "integer" ? ", whole" : ""}); set to ${n}.`);
309
- return n;
310
- }
311
- case "boolean":
312
- if (typeof value === "boolean") return value;
313
- problems.push(`${path} must be true or false; dropped.`);
314
- return void 0;
315
- case "string": {
316
- const ok = typeof value === "string" && (!schema.enum || schema.enum.includes(value)) && (!schema.pattern || new RegExp(schema.pattern).test(value)) && value.length >= (_nullishCoalesce(schema.minLength, () => ( 0)));
317
- if (!ok) {
318
- problems.push(`${path} ${JSON.stringify(value)} is not valid${schema.enum ? `; use one of ${schema.enum.join(", ")}` : ""}; dropped.`);
319
- return void 0;
320
- }
321
- return schema.maxLength !== void 0 ? value.slice(0, schema.maxLength) : value;
322
- }
323
- case "array": {
324
- if (!Array.isArray(value)) {
325
- problems.push(`${path} must be a list; dropped.`);
326
- return void 0;
327
- }
328
- let items = value.map((v, i) => schema.items ? read(v, schema.items, `${path}[${i}]`, problems) : v).filter((v) => v !== void 0);
329
- if (schema.uniqueItems) items = [...new Set(items)];
330
- if (schema.maxItems !== void 0 && items.length > schema.maxItems) {
331
- problems.push(`${path} has more than ${schema.maxItems} items; the rest were dropped.`);
332
- items = items.slice(0, schema.maxItems);
333
- }
334
- if (schema.minItems !== void 0 && items.length < schema.minItems) {
335
- problems.push(`${path} needs at least ${schema.minItems} valid item(s); dropped.`);
336
- return void 0;
337
- }
338
- return items;
339
- }
340
- case "object":
341
- return readObject(value, schema, path, problems);
342
- default:
343
- return value;
344
- }
345
- }
346
- function readObject(value, schema, path, problems) {
347
- if (!isObject(value)) {
348
- problems.push(`${path} must be an object; dropped.`);
349
- return void 0;
350
- }
351
- const out = {};
352
- for (const [key, v] of Object.entries(value)) {
353
- if (v === void 0) continue;
354
- const child = _optionalChain([schema, 'access', _ => _.properties, 'optionalAccess', _2 => _2[key]]);
355
- if (!child) {
356
- problems.push(`${path}.${key} is not a brush field; dropped.`);
357
- continue;
358
- }
359
- const r = read(v, child, `${path}.${key}`, problems);
360
- if (r !== void 0) out[key] = r;
361
- }
362
- return out;
363
- }
364
- function readPaint(value, path, problems) {
365
- if (typeof value === "string") {
366
- if (SOLID2.test(value.trim())) return value.trim().toLowerCase();
367
- } else if (isObject(value) && value.type === "gradient") {
368
- const g = _chunkFQPUF2W2cjs.readPaintSource.call(void 0, value);
369
- if (g && g.type === "gradient") return g;
370
- } else if (isObject(value) && value.type === "effect") {
371
- if (!_chunkWOFXZ3XUcjs.validateJsonSchema.call(void 0, value, BRUSH_FACET_SCHEMAS.paint.anyOf[3]).length) return clone(value);
372
- } else if (isObject(value) && typeof value.light === "string" && typeof value.dark === "string" && Object.keys(value).length === 2) {
373
- if (SOLID2.test(value.light.trim()) && SOLID2.test(value.dark.trim())) return { light: value.light.trim().toLowerCase(), dark: value.dark.trim().toLowerCase() };
374
- }
375
- problems.push(`${path} is not a colour (#rrggbb, #rrggbbaa, rgba(), { light, dark }, a gradient or effect paint); dropped.`);
376
- return void 0;
377
- }
378
- function normalizeBrushSpec(raw, problems = []) {
379
- if (!isObject(raw)) throw new Error("A brush must be an object with id, name, kind and tip.");
380
- if (typeof raw.id !== "string" || !_chunkHS6QDWDFcjs.BRUSH_ID.test(raw.id)) throw new Error(`Brush id ${JSON.stringify(raw.id)} is not valid: use lowercase letters, digits and dashes (up to 64), starting with a letter or digit.`);
381
- const name = typeof raw.name === "string" ? raw.name.trim().slice(0, 80) : "";
382
- if (!name) throw new Error(`Brush "${raw.id}" needs a name.`);
383
- const tool = BRUSH_TOOLS.includes(raw.tool) ? raw.tool : void 0;
384
- if (raw.tool !== void 0 && !tool) problems.push(`tool ${JSON.stringify(raw.tool)} is not a tool; use one of ${BRUSH_TOOLS.join(", ")}.`);
385
- let kind;
386
- if (raw.kind === "mark" || raw.kind === "tool") kind = raw.kind;
387
- else if (raw.kind === void 0) kind = tool ? "tool" : "mark";
388
- else throw new Error(`Brush "${raw.id}" kind ${JSON.stringify(raw.kind)} is not valid: use mark (lays a material) or tool (works on paint already down).`);
389
- if (kind === "tool" && !tool) throw new Error(`Brush "${raw.id}" is a tool and needs tool: one of ${BRUSH_TOOLS.join(", ")}.`);
390
- const { colour: rawColour, marks: rawMarks, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
391
- const out = readObject(rest, BRUSH_JSON_SCHEMA, "brush", problems);
392
- const brush = { id: raw.id, name, ...out, kind, ...tool ? { tool } : {}, tip: _nullishCoalesce(out.tip, () => ( { shape: "round" })) };
393
- const ordered = () => {
394
- const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, motion, colour } = brush;
395
- const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, motion, colour };
396
- return Object.fromEntries(Object.entries(all).filter(([, v]) => v !== void 0));
397
- };
398
- if (!out.tip) problems.push("tip is missing; a round tip is used.");
399
- if (!brush.tip.shape) {
400
- problems.push("tip.shape is missing; round is used.");
401
- brush.tip = { ...brush.tip, shape: "round" };
402
- }
403
- if (rawMarks !== void 0) {
404
- const k = isObject(rawMarks) ? rawMarks.kind : void 0;
405
- const schema = BRUSH_FACET_SCHEMAS.marks[k];
406
- if (!schema) problems.push(`marks.kind ${JSON.stringify(k)} is not valid; use continuous, particles or objects. marks dropped.`);
407
- else brush.marks = readObject(rawMarks, schema, "brush.marks", problems);
408
- }
409
- if (rawColour !== void 0) {
410
- const c = readObject(rawColour, { ...BRUSH_FACET_SCHEMAS.colour, properties: { own: BRUSH_FACET_SCHEMAS.colour.properties.own, default: {}, palette: {} } }, "brush.colour", problems);
411
- if (c) {
412
- const colour = {};
413
- if (c.default !== void 0) {
414
- const p = readPaint(c.default, "brush.colour.default", problems);
415
- if (p !== void 0) colour.default = p;
416
- }
417
- if (c.palette !== void 0) {
418
- if (!Array.isArray(c.palette)) problems.push("brush.colour.palette must be a list; dropped.");
419
- else {
420
- const list2 = c.palette.map((p, i) => readPaint(p, `brush.colour.palette[${i}]`, problems)).filter((p) => p !== void 0);
421
- if (list2.length > MAX_BRUSH_PALETTE) problems.push(`brush.colour.palette has more than ${MAX_BRUSH_PALETTE} colours; the rest were dropped.`);
422
- colour.palette = list2.slice(0, MAX_BRUSH_PALETTE);
423
- }
424
- }
425
- if (typeof c.own === "boolean") colour.own = c.own;
426
- brush.colour = colour;
427
- }
428
- }
429
- const drop = (what, why) => problems.push(`${what} dropped: ${why}.`);
430
- if (kind === "mark" && brush.tool) {
431
- drop("tool", "a mark brush has no tool (set kind to tool)");
432
- delete brush.tool;
433
- }
434
- if (kind === "tool") {
435
- if (brush.medium) {
436
- drop("medium", "tools lay no medium");
437
- delete brush.medium;
438
- }
439
- if (brush.marks) {
440
- drop("marks", "tools lay no marks");
441
- delete brush.marks;
442
- }
443
- }
444
- if (_optionalChain([brush, 'access', _3 => _3.marks, 'optionalAccess', _4 => _4.kind]) === "objects" && brush.tip.shape !== "stamp") {
445
- problems.push("marks objects lay stamps: tip.shape set to stamp.");
446
- brush.tip.shape = "stamp";
447
- }
448
- if (brush.tip.bristle && brush.tip.shape !== "bristle") {
449
- drop("tip.bristle", "only bristle tips have a tuft");
450
- delete brush.tip.bristle;
451
- }
452
- if (brush.tip.stamps && brush.tip.shape !== "stamp") {
453
- drop("tip.stamps", "only stamp tips lay stamps");
454
- delete brush.tip.stamps;
455
- }
456
- if (brush.tip.stamps) {
457
- const particles = _optionalChain([brush, 'access', _5 => _5.marks, 'optionalAccess', _6 => _6.kind]) === "particles";
458
- const stamps = brush.tip.stamps.flatMap((s) => {
459
- const ref = _chunkJSDND7DFcjs.normalizeStickerRef.call(void 0, s);
460
- if (particles && !_chunkJSDND7DFcjs.editorStampOf.call(void 0, ref)) {
461
- problems.push(`brush.tip.stamps "${ref}" dropped: particle marks lay only the editor's stamps (editor:star, editor:dot\u2026).`);
462
- return [];
463
- }
464
- return [ref];
465
- });
466
- if (stamps.length) brush.tip.stamps = stamps;
467
- else {
468
- drop("tip.stamps", "none of the stamps can be laid");
469
- delete brush.tip.stamps;
470
- }
471
- }
472
- const b = brush.behaviour;
473
- if (b) {
474
- const wetTool = kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool);
475
- if (kind === "tool" && b.surface !== void 0) {
476
- drop("behaviour.surface", "a tool works on the surface it finds");
477
- delete b.surface;
478
- }
479
- if (b.wet && b.surface !== "wet" && !wetTool) {
480
- drop("behaviour.wet", "it needs surface wet or a wet tool");
481
- delete b.wet;
482
- }
483
- if (b.thick && b.surface !== "thick") {
484
- drop("behaviour.thick", "it needs surface thick");
485
- delete b.thick;
486
- }
487
- if (b.strength !== void 0 && !(kind === "tool" && brush.tool !== "eraser")) {
488
- drop("behaviour.strength", "only the blender, smudge and wet tools have a strength");
489
- delete b.strength;
490
- }
491
- if (b.distance !== void 0 && brush.tool !== "blender") {
492
- drop("behaviour.distance", "only the blender has a blend distance");
493
- delete b.distance;
494
- }
495
- if (b.length !== void 0 && brush.tool !== "smudge") {
496
- drop("behaviour.length", "only the smudge tool has a drag length");
497
- delete b.length;
498
- }
499
- if (b.exchange && kind === "tool" && !wetTool) {
500
- drop("behaviour.exchange", `the ${brush.tool} does not pick paint up`);
501
- delete b.exchange;
502
- } else if (b.exchange && (_optionalChain([brush, 'access', _7 => _7.marks, 'optionalAccess', _8 => _8.kind]) === "particles" || _optionalChain([brush, 'access', _9 => _9.marks, 'optionalAccess', _10 => _10.kind]) === "objects" || brush.tip.shape === "bristle")) {
503
- drop("behaviour.exchange", "particle, object and bristle marks do not pick paint up");
504
- delete b.exchange;
505
- }
506
- }
507
- return ordered();
508
- }
509
28
 
510
- // src/brushes/resolve.ts
511
- var ALL = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
512
- var EXCHANGE2 = ["dab", "wet", "impasto"];
513
- var NOT = { toEngine: (v) => !v, toFacet: (v) => !v };
514
- var EDITOR_STAMPS = {
515
- toEngine: (v) => v.flatMap((ref) => {
516
- try {
517
- const stamp = _chunkJSDND7DFcjs.editorStampOf.call(void 0, ref);
518
- return stamp ? [stamp] : [];
519
- } catch (e2) {
520
- return [];
521
- }
522
- }),
523
- toFacet: (v) => v.map((stamp) => `editor:${stamp}`)
524
- };
525
- var f = (facet, engine, engines = ALL, extra = {}) => ({ facet, engine, engines, ...extra });
526
- var each = (facetPrefix, enginePrefix, keys, engines) => keys.map((k) => f(`${facetPrefix}.${k}`, `${enginePrefix}.${k}`, engines));
527
- var BRUSH_FIELDS = [
528
- ...each("tip", "tip", ["size", "hardness", "spacing", "angle", "roundness", "opacity", "flow", "blend"]),
529
- ...each("dynamics", "tip", ["smoothing", "pressureSize", "pressureOpacity"]),
530
- f("dynamics.pressureSmoothing", "pressureSmoothing"),
531
- f("dynamics.rotation", "rotation"),
532
- ...each("dynamics", "dynamics", ["minSize", "pressureFlow", "tiltSize", "tiltRoundness", "tiltOpacity", "velocitySize", "velocityOpacity", "sizeJitter", "angleJitter", "opacityJitter", "scatter"]),
533
- f("behaviour.grain", "grain"),
534
- ...each("light", "light", ["amount", "pressure"]),
535
- // Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
536
- f("behaviour.exchange.pickup", "behavior.pickup", EXCHANGE2),
537
- f("behaviour.exchange.mix", "behavior.mix", EXCHANGE2),
538
- f("behaviour.exchange.dirty", "behavior.dirty", EXCHANGE2),
539
- f("behaviour.mixing", "behavior.mixing", EXCHANGE2),
540
- f("behaviour.exchange.pickup", "mixer.wet", ["mixer"]),
541
- f("behaviour.exchange.mix", "mixer.mix", ["mixer"]),
542
- f("behaviour.exchange.dirty", "mixer.cleanEachStroke", ["mixer"], { map: NOT }),
543
- f("behaviour.mixing", "mixer.mixing", ["mixer"]),
544
- f("behaviour.exchange.reservoir.load", "mixer.load", ["mixer"]),
545
- f("behaviour.exchange.reservoir.refill", "mixer.loadEachStroke", ["mixer"]),
546
- f("behaviour.exchange.reservoir.loadsColour", "loadsColor", ["mixer"]),
547
- // Tools.
548
- f("behaviour.strength", "blender.strength", ["blender"]),
549
- f("behaviour.distance", "blender.distance", ["blender"]),
550
- f("behaviour.mixing", "blender.mixing", ["blender"]),
551
- f("behaviour.strength", "smudge.strength", ["smudge"]),
552
- f("behaviour.length", "smudge.length", ["smudge"]),
553
- f("behaviour.mixing", "smudge.mixing", ["smudge"]),
554
- f("behaviour.strength", "wet.strength", ["wet"]),
555
- // Surfaces.
556
- ...each("behaviour.wet", "wet", ["medium", "water", "pigment", "body", "rewet", "load", "thinner"], ["wet"]),
557
- f("behaviour.thick.applicator", "impasto.tool", ["impasto"]),
558
- ...each("behaviour.thick", "impasto", ["thickness", "gloss", "metallic", "load", "thinner", "drag", "rim", "clarity"], ["impasto"]),
559
- // Marks.
560
- f("marks.physics", "particle.kind", ["particle"]),
561
- ...each("marks", "particle", ["render", "count", "size", "spread", "stiffness", "damping", "speed", "gravity", "life", "interval", "ribbonFill", "ribbonPairs", "waveAmplitude", "waveLength", "wavePhaseJitter"], ["particle"]),
562
- f("marks.stampOrder", "particle.dotTextureOrder", ["particle"]),
563
- f("marks.stampRotation", "particle.dotRotation", ["particle"]),
564
- f("tip.stamps", "particle.dotTextures", ["particle"], { shapes: ["stamp"], map: EDITOR_STAMPS }),
565
- f("motion.waveSpeed", "particle.waveSpeed", ["particle"]),
566
- f("tip.stamps", "sticker.stamps", ["sticker"], { shapes: ["stamp"] }),
567
- ...each("marks", "sticker", ["density", "sizeJitter", "rotationJitter", "scatter"], ["sticker"]),
568
- f("marks.colourJitter", "sticker.colorJitter", ["sticker"]),
569
- ...each("tip.bristle", "bristle", ["count", "stiffness", "splay", "thickness", "load", "jitter"], ["bristle"])
570
- ];
571
- var WET_TOOL_OF = { water: "water", dryer: "dry", blow: "blow", blot: "blot", "wet-blend": "blend", "wet-drag": "smudge" };
572
- function getPath(o, path) {
573
- let v = o;
574
- for (const k of path.split(".")) {
575
- if (v === null || typeof v !== "object") return void 0;
576
- v = v[k];
577
- }
578
- return v;
579
- }
580
- function setPath(o, path, value) {
581
- const keys = path.split(".");
582
- let node = o;
583
- for (const k of keys.slice(0, -1)) {
584
- if (node[k] === null || typeof node[k] !== "object") node[k] = {};
585
- node = node[k];
586
- }
587
- node[keys[keys.length - 1]] = value;
588
- }
589
- var copy = (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v;
590
- function resolveEngine(brush) {
591
- if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet";
592
- if (_optionalChain([brush, 'access', _11 => _11.marks, 'optionalAccess', _12 => _12.kind]) === "particles") return "particle";
593
- if (_optionalChain([brush, 'access', _13 => _13.marks, 'optionalAccess', _14 => _14.kind]) === "objects") return "sticker";
594
- if (brush.tip.shape === "bristle") return "bristle";
595
- const surface = _optionalChain([brush, 'access', _15 => _15.behaviour, 'optionalAccess', _16 => _16.surface]);
596
- if (surface === "thick") return "impasto";
597
- if (surface === "wet") return "wet";
598
- if (_optionalChain([brush, 'access', _17 => _17.behaviour, 'optionalAccess', _18 => _18.exchange, 'optionalAccess', _19 => _19.reservoir])) return "mixer";
599
- return "dab";
600
- }
601
- var VARIETY_CATEGORY = {
602
- pencil: "pencil",
603
- pastel: "pastel",
604
- "soft-pastel": "pastel",
605
- "oil-pastel": "pastel",
606
- charcoal: "charcoal",
607
- airbrush: "airbrush",
608
- marker: "marker"
609
- };
610
- var MEDIUM_CATEGORY = { paint: "paint", ink: "ink", dry: "pencil", spray: "airbrush" };
611
- function resolveWetMedium(brush) {
612
- return _nullishCoalesce(_optionalChain([brush, 'access', _20 => _20.behaviour, 'optionalAccess', _21 => _21.wet, 'optionalAccess', _22 => _22.medium]), () => ( (_chunkHS6QDWDFcjs.WET_MEDIA.includes(brush.variety) ? brush.variety : void 0)));
613
- }
614
- function resolveCategory(brush, engine = resolveEngine(brush)) {
615
- if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet-tool";
616
- if ((_nullishCoalesce(_optionalChain([brush, 'access', _23 => _23.light, 'optionalAccess', _24 => _24.amount]), () => ( 0))) > 0) return "light";
617
- switch (engine) {
618
- case "particle":
619
- return "procedural";
620
- case "sticker":
621
- return "sticker";
622
- case "bristle":
623
- return "bristle";
624
- case "impasto":
625
- return "impasto";
626
- case "wet":
627
- return resolveWetMedium(brush) === "oil" ? "oil" : "watercolor";
628
- case "mixer":
629
- return "mixer";
630
- default:
631
- return brush.variety && VARIETY_CATEGORY[brush.variety] || brush.medium && MEDIUM_CATEGORY[brush.medium] || "custom";
632
- }
633
- }
634
- function resolveBrush(brush, options = {}) {
635
- const engine = resolveEngine(brush);
636
- const input = { id: brush.id, name: brush.name, category: resolveCategory(brush, engine), engine };
637
- for (const field of BRUSH_FIELDS) {
638
- if (!field.engines.includes(engine) || field.shapes && !field.shapes.includes(brush.tip.shape)) continue;
639
- const v = getPath(brush, field.facet);
640
- if (v !== void 0) setPath(input, field.engine, field.map ? field.map.toEngine(v) : copy(v));
641
- }
642
- if (engine === "wet") {
643
- setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
644
- const medium = resolveWetMedium(brush);
645
- if (medium) setPath(input, "wet.medium", medium);
646
- }
647
- if (engine === "blender" || engine === "smudge") input.loadsColor = false;
648
- const paint = _optionalChain([brush, 'access', _25 => _25.colour, 'optionalAccess', _26 => _26.default]);
649
- if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") input.paint = copy(paint);
650
- return _chunkHS6QDWDFcjs.normalizePaintBrush.call(void 0, { ...input, ...options.builtin ? { builtin: true } : {} });
651
- }
652
29
 
653
- // src/brushes/migrate.ts
654
- var equal = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
655
- var copy2 = (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v;
656
- var CATEGORY_MEDIUM = {
657
- pencil: ["dry", "pencil"],
658
- pastel: ["dry", "pastel"],
659
- charcoal: ["dry", "charcoal"],
660
- airbrush: ["spray", "airbrush"],
661
- marker: ["ink", "marker"],
662
- ink: ["ink"],
663
- paint: ["paint"]
664
- };
665
- function tipShape(b) {
666
- if (b.engine === "bristle") return "bristle";
667
- if (b.engine === "sticker" || b.engine === "particle" && b.particle.dotTextures.length > 0) return "stamp";
668
- if (b.tip.roundness >= 1) return "round";
669
- return Math.abs(Math.abs(b.tip.angle % 180) - 45) < 1e-9 || Math.abs(Math.abs(b.tip.angle % 180) - 135) < 1e-9 ? "chisel" : "flat";
670
- }
671
- function skeleton(b) {
672
- const brush = { id: b.id, name: b.name, kind: "mark", tip: { shape: tipShape(b) } };
673
- if (b.engine === "eraser" || b.engine === "blender" || b.engine === "smudge") return { ...brush, kind: "tool", tool: b.engine };
674
- if (b.engine === "wet" && b.wet.tool !== "paint") {
675
- const tool = Object.keys(WET_TOOL_OF).find((t) => WET_TOOL_OF[t] === b.wet.tool);
676
- return { ...brush, kind: "tool", tool };
677
- }
678
- switch (b.engine) {
679
- case "wet":
680
- return { ...brush, medium: "paint", variety: b.wet.medium, behaviour: { surface: "wet" } };
681
- case "impasto":
682
- return { ...brush, medium: "paint", behaviour: { surface: "thick" } };
683
- case "mixer":
684
- return { ...brush, medium: "paint", variety: "mixer", behaviour: { exchange: { reservoir: {} } } };
685
- case "bristle":
686
- return { ...brush, medium: "paint" };
687
- case "particle":
688
- return { ...brush, marks: { kind: "particles" } };
689
- case "sticker":
690
- return { ...brush, marks: { kind: "objects" } };
691
- default: {
692
- const [medium, variety] = _nullishCoalesce(CATEGORY_MEDIUM[b.category], () => ( []));
693
- return { ...brush, ...medium ? { medium } : {}, ...variety ? { variety } : {} };
694
- }
695
- }
696
- }
697
- var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
698
- function migrateBrushV1(raw) {
699
- const b = _chunkHS6QDWDFcjs.normalizePaintBrush.call(void 0, raw);
700
- const brush = skeleton(b);
701
- const defaults = resolveBrush(brush);
702
- for (const field of BRUSH_FIELDS) {
703
- if (!field.engines.includes(b.engine) || field.shapes && !field.shapes.includes(brush.tip.shape)) continue;
704
- const v = getPath(b, field.engine);
705
- if (!equal(v, getPath(defaults, field.engine))) setPath(brush, field.facet, field.map ? field.map.toFacet(v) : copy2(v));
706
- }
707
- if (b.paint) setPath(brush, "colour.default", copy2(b.paint));
708
- return Object.fromEntries(ORDER.filter((k) => brush[k] !== void 0).map((k) => [k, brush[k]]));
709
- }
710
30
 
711
- // src/brushes/presets.ts
712
- var BRUSH_PRESETS = [
713
- {
714
- id: "pencil-hb",
715
- name: "HB pencil",
716
- kind: "mark",
717
- medium: "dry",
718
- variety: "pencil",
719
- tip: { shape: "round", size: 4, hardness: 0.7, spacing: 0.08, opacity: 0.85, flow: 0.55 },
720
- dynamics: { smoothing: 0.25, pressureOpacity: true, pressureSmoothing: 0.3, minSize: 0.55, tiltSize: 0.9, tiltRoundness: 0.7, tiltOpacity: 0.5 },
721
- behaviour: { grain: 0.85 }
722
- },
723
- {
724
- id: "pencil-6b",
725
- name: "6B pencil",
726
- kind: "mark",
727
- medium: "dry",
728
- variety: "pencil",
729
- tip: { shape: "round", size: 7, hardness: 0.55, spacing: 0.07, flow: 0.75 },
730
- dynamics: { smoothing: 0.2, pressureOpacity: true, pressureSmoothing: 0.3, minSize: 0.5, tiltSize: 1, tiltRoundness: 0.75, tiltOpacity: 0.35 },
731
- behaviour: { grain: 0.7 }
732
- },
733
- {
734
- id: "soft-pastel",
735
- name: "Soft pastel",
736
- kind: "mark",
737
- medium: "dry",
738
- variety: "soft-pastel",
739
- tip: { shape: "flat", size: 32, hardness: 0.55, spacing: 0.05, angle: 90, roundness: 0.55, flow: 0.7 },
740
- dynamics: { smoothing: 0.15, pressureSize: false, pressureOpacity: true, pressureSmoothing: 0.25, rotation: "stroke", sizeJitter: 0.06, angleJitter: 0.02 },
741
- behaviour: { grain: 1 }
742
- },
743
- {
744
- id: "oil-pastel",
745
- name: "Oil pastel",
746
- kind: "mark",
747
- medium: "dry",
748
- variety: "oil-pastel",
749
- tip: { shape: "flat", size: 26, hardness: 0.8, spacing: 0.05, angle: 90, roundness: 0.7, flow: 0.9, blend: "pigment" },
750
- dynamics: { smoothing: 0.15, pressureSmoothing: 0.25, rotation: "stroke", minSize: 0.6 },
751
- behaviour: { grain: 0.5 }
752
- },
753
- {
754
- id: "charcoal-stick",
755
- name: "Charcoal stick",
756
- kind: "mark",
757
- medium: "dry",
758
- variety: "charcoal",
759
- tip: { shape: "flat", size: 26, hardness: 0.4, spacing: 0.05, angle: 90, roundness: 0.35, opacity: 0.95, flow: 0.5 },
760
- dynamics: { smoothing: 0.15, pressureSize: false, pressureOpacity: true, pressureSmoothing: 0.2, rotation: "stroke", tiltSize: 0.6, sizeJitter: 0.1, angleJitter: 0.03 },
761
- behaviour: { grain: 0.95 }
762
- },
763
- {
764
- id: "vine-charcoal",
765
- name: "Vine charcoal",
766
- kind: "mark",
767
- medium: "dry",
768
- variety: "charcoal",
769
- tip: { shape: "round", size: 10, hardness: 0.3, spacing: 0.06, opacity: 0.75, flow: 0.45 },
770
- dynamics: { smoothing: 0.2, pressureOpacity: true, pressureSmoothing: 0.2, minSize: 0.4, tiltSize: 0.8, tiltRoundness: 0.5, tiltOpacity: 0.3, sizeJitter: 0.08 },
771
- behaviour: { grain: 0.9 }
772
- },
773
- {
774
- id: "airbrush",
775
- name: "Airbrush",
776
- kind: "mark",
777
- medium: "spray",
778
- variety: "airbrush",
779
- tip: { shape: "round", size: 90, hardness: 0, spacing: 0.04, flow: 0.06 },
780
- dynamics: { smoothing: 0.3, pressureSize: false, pressureSmoothing: 0.3, pressureFlow: 1, tiltSize: 0.5, tiltRoundness: 0.6 }
781
- },
782
- {
783
- id: "marker",
784
- name: "Chisel marker",
785
- kind: "mark",
786
- medium: "ink",
787
- variety: "marker",
788
- tip: { shape: "chisel", size: 24, hardness: 0.85, spacing: 0.04, angle: 45, roundness: 0.35, opacity: 0.6, blend: "multiply" },
789
- dynamics: { smoothing: 0.3, pressureSize: false },
790
- behaviour: { grain: 0.1 }
791
- },
792
- {
793
- id: "ink-pen",
794
- name: "Ink pen",
795
- kind: "mark",
796
- medium: "ink",
797
- variety: "pen",
798
- tip: { shape: "round", size: 4, spacing: 0.05 },
799
- dynamics: { smoothing: 0.5, pressureSmoothing: 0.4, minSize: 0.25, velocitySize: -0.45 }
800
- },
801
- {
802
- id: "brush-pen",
803
- name: "Brush pen",
804
- kind: "mark",
805
- medium: "ink",
806
- variety: "brush-pen",
807
- tip: { shape: "flat", size: 14, hardness: 0.9, spacing: 0.04, roundness: 0.8 },
808
- dynamics: { smoothing: 0.5, pressureSmoothing: 0.45, rotation: "stroke", minSize: 0.05, velocitySize: -0.3 }
809
- },
810
- {
811
- id: "eraser-soft",
812
- name: "Soft eraser",
813
- kind: "tool",
814
- tool: "eraser",
815
- tip: { shape: "round", size: 40, hardness: 0.2, spacing: 0.08, flow: 0.6 },
816
- dynamics: { pressureSize: false, pressureOpacity: true }
817
- },
818
- {
819
- id: "eraser-hard",
820
- name: "Hard eraser",
821
- kind: "tool",
822
- tool: "eraser",
823
- tip: { shape: "round", size: 16, hardness: 0.9, spacing: 0.06 },
824
- dynamics: { minSize: 0.5 },
825
- behaviour: { grain: 0.3 }
826
- },
827
- {
828
- id: "blender",
829
- name: "Blender",
830
- kind: "tool",
831
- tool: "blender",
832
- tip: { shape: "round", size: 34, hardness: 0.4 },
833
- dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true }
834
- },
835
- {
836
- id: "smudge",
837
- name: "Smudge",
838
- kind: "tool",
839
- tool: "smudge",
840
- tip: { shape: "round", size: 26, hardness: 0.6, spacing: 0.06 },
841
- dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true },
842
- behaviour: { strength: 0.75 }
843
- },
844
- {
845
- id: "wet-smudge",
846
- name: "Pigment smudge",
847
- kind: "tool",
848
- tool: "smudge",
849
- tip: { shape: "round", size: 30, hardness: 0.5, spacing: 0.06 },
850
- dynamics: { smoothing: 0.2, pressureOpacity: true, minSize: 0.6 },
851
- behaviour: { length: 0.9, mixing: "pigment" }
852
- },
853
- {
854
- id: "dirty-round",
855
- name: "Dirty round",
856
- kind: "mark",
857
- medium: "paint",
858
- variety: "acrylic",
859
- tip: { shape: "round", size: 22, hardness: 0.65, spacing: 0.06, flow: 0.7 },
860
- dynamics: { smoothing: 0.2, minSize: 0.5 },
861
- behaviour: { exchange: { pickup: 0.55, mix: 0.6, dirty: true } }
862
- },
863
- // The paint and second colours side by side across the tip.
864
- {
865
- id: "multicolor-flat",
866
- name: "Multi-colour flat",
867
- kind: "mark",
868
- medium: "paint",
869
- variety: "acrylic",
870
- tip: { shape: "flat", size: 30, hardness: 0.75, spacing: 0.05, angle: 90, roundness: 0.3, flow: 0.85 },
871
- dynamics: { smoothing: 0.2, rotation: "stroke", minSize: 0.6 },
872
- behaviour: { exchange: { pickup: 0.15, mix: 0.4 } },
873
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "across-tip" }, softness: 0.7 } }
874
- },
875
- // From the paint colour to the second colour and back, every 6 dabs.
876
- {
877
- id: "palette-dabber",
878
- name: "Palette dabber",
879
- kind: "mark",
880
- medium: "paint",
881
- variety: "acrylic",
882
- tip: { shape: "round", size: 18, hardness: 0.7, spacing: 0.35, flow: 0.9 },
883
- dynamics: { smoothing: 0.1, minSize: 0.5, sizeJitter: 0.15, scatter: 0.3 },
884
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 0.5, color: "secondary" }, { offset: 1, color: "paint" }], space: "pigment", map: { by: "distance", length: 12, repeat: "repeat", unit: "dabs" }, softness: 0.4 } }
885
- },
886
- // Changes colour as it is dragged out: paint → second colour over 300 px, and back.
887
- {
888
- id: "gradient-round",
889
- name: "Gradient round",
890
- kind: "mark",
891
- medium: "paint",
892
- variety: "gradient",
893
- tip: { shape: "round", size: 24, hardness: 0.8, spacing: 0.05, flow: 0.9 },
894
- dynamics: { smoothing: 0.2, minSize: 0.5 },
895
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 300, repeat: "mirror", unit: "px" } } }
896
- },
897
- // Light pressure lays the second colour, full pressure the paint colour.
898
- {
899
- id: "pressure-ombre",
900
- name: "Pressure ombr\xE9",
901
- kind: "mark",
902
- medium: "paint",
903
- variety: "gradient",
904
- tip: { shape: "round", size: 26, hardness: 0.6, spacing: 0.05, flow: 0.8 },
905
- dynamics: { smoothing: 0.2, pressureSize: false },
906
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "secondary" }, { offset: 1, color: "paint" }], space: "pigment", map: { by: "pressure" } } }
907
- },
908
- {
909
- id: "rainbow-marker",
910
- name: "Rainbow marker",
911
- kind: "mark",
912
- medium: "ink",
913
- variety: "marker",
914
- tip: { shape: "flat", size: 18, hardness: 0.9, spacing: 0.04, angle: 90, roundness: 0.4 },
915
- dynamics: { smoothing: 0.25, pressureSize: false, rotation: "stroke" },
916
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "#e11d48" }, { offset: 0.2, color: "#f59e0b" }, { offset: 0.4, color: "#facc15" }, { offset: 0.6, color: "#22c55e" }, { offset: 0.8, color: "#3b82f6" }, { offset: 1, color: "#8b5cf6" }], space: "oklab", map: { by: "distance", length: 600, repeat: "repeat", unit: "px" } }, own: true }
917
- },
918
- {
919
- id: "mixer-brush",
920
- name: "Mixer Brush",
921
- kind: "mark",
922
- medium: "paint",
923
- variety: "mixer",
924
- tip: { shape: "round", size: 30, hardness: 0.7, flow: 0.5 },
925
- dynamics: { smoothing: 0.2, minSize: 0.5 },
926
- behaviour: { exchange: { reservoir: { load: 0.95 } }, mixing: "pigment" }
927
- },
928
- {
929
- id: "watercolor-round",
930
- name: "Watercolour round",
931
- kind: "mark",
932
- medium: "paint",
933
- variety: "watercolor",
934
- tip: { shape: "round", size: 28, hardness: 0.6, spacing: 0.12 },
935
- dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.35 },
936
- behaviour: { surface: "wet", exchange: { pickup: 0.55 }, wet: { water: 0.55, rewet: 0.2 } }
937
- },
938
- {
939
- id: "watercolor-wash",
940
- name: "Wash brush",
941
- kind: "mark",
942
- medium: "paint",
943
- variety: "watercolor",
944
- tip: { shape: "flat", size: 90, hardness: 0.35, angle: 90, roundness: 0.45 },
945
- dynamics: { smoothing: 0.3, pressureSize: false, pressureOpacity: true, rotation: "stroke" },
946
- behaviour: { surface: "wet", exchange: { pickup: 0.45, mix: 0.45 }, wet: { water: 0.8, pigment: 0.45, rewet: 0.2 } }
947
- },
948
- // A graded wash: the paint colour runs into the second colour as the brush travels.
949
- {
950
- id: "gradient-wash",
951
- name: "Gradient wash",
952
- kind: "mark",
953
- medium: "paint",
954
- variety: "watercolor",
955
- tip: { shape: "flat", size: 80, hardness: 0.35, angle: 90, roundness: 0.5 },
956
- dynamics: { smoothing: 0.3, pressureSize: false, pressureOpacity: true, rotation: "stroke" },
957
- behaviour: { surface: "wet", exchange: { pickup: 0.4, mix: 0.4 }, wet: { water: 0.8, pigment: 0.45, rewet: 0.2 } },
958
- colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 500, repeat: "mirror", unit: "px" } } }
959
- },
960
- {
961
- id: "watercolor-detail",
962
- name: "Watercolour rigger",
963
- kind: "mark",
964
- medium: "paint",
965
- variety: "watercolor",
966
- tip: { shape: "round", size: 7, hardness: 0.75 },
967
- dynamics: { smoothing: 0.4, minSize: 0.2 },
968
- behaviour: { surface: "wet", exchange: { pickup: 0.4, mix: 0.45 }, wet: { water: 0.4, pigment: 0.7, rewet: 0.2 } }
969
- },
970
- {
971
- id: "gouache",
972
- name: "Gouache",
973
- kind: "mark",
974
- medium: "paint",
975
- variety: "gouache",
976
- tip: { shape: "round", size: 26, hardness: 0.7 },
977
- dynamics: { smoothing: 0.25, minSize: 0.5 },
978
- behaviour: { surface: "wet", exchange: { pickup: 0.65, mix: 0.6 }, wet: { water: 0.35, pigment: 0.8, body: 1, rewet: 0.3 } }
979
- },
980
- {
981
- id: "oil-round",
982
- name: "Wet oil round",
983
- kind: "mark",
984
- medium: "paint",
985
- variety: "oil",
986
- tip: { shape: "round", size: 24, hardness: 0.75, spacing: 0.08 },
987
- dynamics: { smoothing: 0.25, minSize: 0.5 },
988
- behaviour: { surface: "wet", exchange: { pickup: 0.6, mix: 0.55 }, wet: { water: 0.4, pigment: 0.9, body: 1, load: 0.97 } }
989
- },
990
- {
991
- id: "oil-flat",
992
- name: "Wet oil flat",
993
- kind: "mark",
994
- medium: "paint",
995
- variety: "oil",
996
- tip: { shape: "flat", size: 40, hardness: 0.85, spacing: 0.06, angle: 90, roundness: 0.35 },
997
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.6 },
998
- behaviour: { surface: "wet", exchange: { pickup: 0.65, mix: 0.6 }, wet: { water: 0.45, pigment: 0.9, body: 1, load: 0.95 } }
999
- },
1000
- {
1001
- id: "impasto-brush",
1002
- name: "Impasto brush",
1003
- kind: "mark",
1004
- medium: "paint",
1005
- variety: "oil",
1006
- tip: { shape: "flat", size: 36, hardness: 0.8, spacing: 0.06, angle: 90, roundness: 0.55 },
1007
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.55 },
1008
- behaviour: { surface: "thick", exchange: { pickup: 0.4 }, thick: { gloss: 0.45, load: 0.92 } }
1009
- },
1010
- {
1011
- id: "paint-tube",
1012
- name: "Paint tube",
1013
- kind: "mark",
1014
- medium: "paint",
1015
- variety: "oil",
1016
- tip: { shape: "round", size: 22, hardness: 0.9, spacing: 0.05 },
1017
- dynamics: { smoothing: 0.35, minSize: 0.4 },
1018
- behaviour: { surface: "thick", thick: { applicator: "tube", thickness: 0.9, gloss: 0.5, load: 1, drag: 0.1 } }
1019
- },
1020
- {
1021
- id: "palette-knife",
1022
- name: "Palette knife",
1023
- kind: "mark",
1024
- medium: "paint",
1025
- variety: "oil",
1026
- tip: { shape: "flat", size: 48, spacing: 0.05, angle: 90, roundness: 0.18 },
1027
- dynamics: { smoothing: 0.3, pressureSize: false, rotation: "stroke" },
1028
- behaviour: { surface: "thick", thick: { applicator: "knife", thickness: 0.3, gloss: 0.55, load: 1, drag: 0.85 } }
1029
- },
1030
- {
1031
- id: "paint-roller",
1032
- name: "Roller",
1033
- kind: "mark",
1034
- medium: "paint",
1035
- variety: "acrylic",
1036
- tip: { shape: "flat", size: 70, hardness: 0.95, spacing: 0.04, angle: 90, roundness: 0.12 },
1037
- dynamics: { smoothing: 0.4, pressureSize: false, rotation: "stroke" },
1038
- behaviour: { surface: "thick", thick: { applicator: "roller", thickness: 0.25, gloss: 0.3, drag: 0.15 } }
1039
- },
1040
- {
1041
- id: "gloop-pen",
1042
- name: "Gloop pen",
1043
- kind: "mark",
1044
- medium: "paint",
1045
- variety: "gloop",
1046
- tip: { shape: "round", size: 14, hardness: 0.9, spacing: 0.05 },
1047
- dynamics: { smoothing: 0.5, minSize: 0.5 },
1048
- behaviour: { surface: "thick", thick: { applicator: "gloop", thickness: 1, gloss: 0.9, load: 1, drag: 0 } }
1049
- },
1050
- {
1051
- id: "metallic-paint",
1052
- name: "Metallic paint",
1053
- kind: "mark",
1054
- medium: "paint",
1055
- variety: "metallic",
1056
- tip: { shape: "flat", size: 30, hardness: 0.8, spacing: 0.06, angle: 90, roundness: 0.6 },
1057
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.5 },
1058
- behaviour: { surface: "thick", exchange: { pickup: 0.2, mix: 0.4 }, thick: { thickness: 0.45, gloss: 0.8, metallic: 1, load: 0.95, drag: 0.3 } }
1059
- },
1060
- {
1061
- id: "hair-strands",
1062
- name: "Hair strands",
1063
- kind: "mark",
1064
- medium: "ink",
1065
- variety: "hair",
1066
- tip: { shape: "round", size: 40, hardness: 0.8, opacity: 0.9, flow: 0.6 },
1067
- marks: { kind: "particles", physics: "string", count: 22, size: 0.025, spread: 1, stiffness: 0.08, damping: 0.3 },
1068
- dynamics: { smoothing: 0.2, pressureOpacity: true }
1069
- },
1070
- {
1071
- id: "spring-ink",
1072
- name: "Spring ink",
1073
- kind: "mark",
1074
- medium: "ink",
1075
- variety: "spring",
1076
- tip: { shape: "round", size: 30, hardness: 0.9, flow: 0.8 },
1077
- marks: { kind: "particles", count: 5, spread: 0.6, stiffness: 0.2, damping: 0.1 },
1078
- dynamics: { smoothing: 0.1 }
1079
- },
1080
- {
1081
- id: "orbit-curls",
1082
- name: "Orbit curls",
1083
- kind: "mark",
1084
- medium: "ink",
1085
- variety: "curls",
1086
- tip: { shape: "round", size: 36, hardness: 0.85, opacity: 0.9, flow: 0.7 },
1087
- marks: { kind: "particles", physics: "orbit", count: 4, size: 0.05, spread: 0.6, speed: 0.45 },
1088
- dynamics: { smoothing: 0.2 }
1089
- },
1090
- {
1091
- id: "flow-fur",
1092
- name: "Flow fur",
1093
- kind: "mark",
1094
- medium: "ink",
1095
- variety: "fur",
1096
- tip: { shape: "round", size: 50, hardness: 0.7, opacity: 0.8, flow: 0.5 },
1097
- marks: { kind: "particles", physics: "flow", count: 10, size: 0.03, spread: 0.6, life: 0.25 },
1098
- dynamics: { smoothing: 0.2, pressureOpacity: true }
1099
- },
1100
- {
1101
- id: "bead-trail",
1102
- name: "Bead trail",
1103
- kind: "mark",
1104
- medium: "paint",
1105
- variety: "beads",
1106
- tip: { shape: "round", size: 34, hardness: 0.9 },
1107
- marks: { kind: "particles", render: "sequence", count: 3, size: 0.18, stiffness: 0.3, damping: 0.3, interval: 0.4 },
1108
- dynamics: { smoothing: 0.25 }
1109
- },
1110
- {
1111
- id: "sparkle-dust",
1112
- name: "Sparkle dust",
1113
- kind: "mark",
1114
- medium: "paint",
1115
- variety: "glitter",
1116
- tip: { shape: "round", size: 60, hardness: 0.6, flow: 0.9 },
1117
- marks: { kind: "particles", physics: "flow", render: "dot", count: 6, size: 0.05, spread: 0.9, speed: 0.7, life: 0.4 },
1118
- dynamics: { smoothing: 0.1, pressureOpacity: true }
1119
- },
1120
- {
1121
- id: "bristle-round",
1122
- name: "Bristle round",
1123
- kind: "mark",
1124
- medium: "paint",
1125
- variety: "oil",
1126
- tip: { shape: "bristle", size: 30, hardness: 0.85, flow: 0.9, bristle: { count: 28, stiffness: 0.45, splay: 0.6, thickness: 0.035, load: 0.7, jitter: 0.5 } },
1127
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.4, tiltSize: 0.6, tiltRoundness: 0.6 }
1128
- },
1129
- {
1130
- id: "bristle-flat",
1131
- name: "Bristle flat",
1132
- kind: "mark",
1133
- medium: "paint",
1134
- variety: "oil",
1135
- tip: { shape: "bristle", size: 46, hardness: 0.85, angle: 90, roundness: 0.3, flow: 0.9, bristle: { count: 40, stiffness: 0.6, splay: 0.4, thickness: 0.025, load: 0.65, jitter: 0.45 } },
1136
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.5, tiltSize: 0.5 }
1137
- },
1138
- {
1139
- id: "dry-brush",
1140
- name: "Dry brush",
1141
- kind: "mark",
1142
- medium: "paint",
1143
- variety: "dry-brush",
1144
- tip: { shape: "bristle", size: 44, hardness: 0.9, angle: 90, roundness: 0.45, flow: 0.8, bristle: { count: 48, stiffness: 0.35, splay: 0.7, thickness: 0.05, load: 0.15, jitter: 0.8 } },
1145
- dynamics: { smoothing: 0.2, rotation: "stroke" },
1146
- behaviour: { grain: 0.6 }
1147
- },
1148
- // Light paint: brushes that lay light with their colour (glow and spill; see light.ts).
1149
- {
1150
- id: "glow-gel",
1151
- name: "Glow gel",
1152
- kind: "mark",
1153
- medium: "paint",
1154
- variety: "gel",
1155
- tip: { shape: "round", size: 16, hardness: 0.9, spacing: 0.05 },
1156
- dynamics: { smoothing: 0.5, minSize: 0.6 },
1157
- behaviour: { surface: "thick", thick: { applicator: "gloop", thickness: 1, gloss: 0.95, load: 1, drag: 0, rim: 0.8, clarity: 0.5 } },
1158
- light: { amount: 0.85 }
1159
- },
1160
- {
1161
- id: "neon-marker",
1162
- name: "Neon marker",
1163
- kind: "mark",
1164
- medium: "ink",
1165
- variety: "neon",
1166
- tip: { shape: "round", size: 10, hardness: 0.9, spacing: 0.04 },
1167
- dynamics: { smoothing: 0.45, pressureSize: false },
1168
- light: { amount: 1 }
1169
- },
1170
- {
1171
- id: "light-airbrush",
1172
- name: "Light airbrush",
1173
- kind: "mark",
1174
- medium: "spray",
1175
- variety: "airbrush",
1176
- tip: { shape: "round", size: 90, hardness: 0, spacing: 0.04, flow: 0.06 },
1177
- dynamics: { smoothing: 0.3, pressureSize: false, pressureSmoothing: 0.3, pressureFlow: 1 },
1178
- light: { amount: 0.7, pressure: true }
1179
- },
1180
- {
1181
- id: "spark-trail",
1182
- name: "Spark trail",
1183
- kind: "mark",
1184
- medium: "paint",
1185
- variety: "sparks",
1186
- tip: { shape: "round", size: 50, hardness: 0.7 },
1187
- marks: { kind: "particles", physics: "flow", render: "dot", count: 8, size: 0.06, spread: 0.7, speed: 0.9, life: 0.3 },
1188
- dynamics: { smoothing: 0.1 },
1189
- light: { amount: 1 }
1190
- },
1191
- // Silk of light: strands dragged behind the pen on slow waves, the bands between them filled.
1192
- {
1193
- id: "light-ribbon",
1194
- name: "Light ribbon",
1195
- kind: "mark",
1196
- medium: "paint",
1197
- variety: "ribbon",
1198
- tip: { shape: "round", size: 80, hardness: 0.8, opacity: 0.9 },
1199
- marks: { kind: "particles", physics: "string", render: "ribbon", count: 5, size: 0.015, spread: 1, stiffness: 0.05, damping: 0.3, ribbonFill: 0.45, waveAmplitude: 0.25, waveLength: 220, wavePhaseJitter: 0.25 },
1200
- dynamics: { smoothing: 0.35, pressureSize: false },
1201
- light: { amount: 0.8 }
1202
- },
1203
- // Flames: short-lived bands thrown off the pen, rising and curling in the flow.
1204
- {
1205
- id: "flame-ribbon",
1206
- name: "Flame ribbon",
1207
- kind: "mark",
1208
- medium: "paint",
1209
- variety: "flame",
1210
- tip: { shape: "round", size: 50, hardness: 0.6, opacity: 0.85 },
1211
- marks: { kind: "particles", physics: "flow", render: "ribbon", count: 4, size: 0.012, speed: 0.6, gravity: -0.6, life: 0.2 },
1212
- dynamics: { smoothing: 0.25 },
1213
- light: { amount: 1 }
1214
- },
1215
- // A neon tube that lags and snaps back, with a slow shimmer.
1216
- {
1217
- id: "neon-strand",
1218
- name: "Neon strand",
1219
- kind: "mark",
1220
- medium: "ink",
1221
- variety: "neon",
1222
- tip: { shape: "round", size: 40, hardness: 0.9 },
1223
- marks: { kind: "particles", physics: "elastic", count: 3, size: 0.05, spread: 0.25, stiffness: 0.6, damping: 0.3, waveAmplitude: 0.06, waveLength: 120 },
1224
- dynamics: { smoothing: 0.4, pressureSize: false },
1225
- light: { amount: 1 }
1226
- },
1227
- // Smoke lit from within: thin bands thrown into a slow, rising flow.
1228
- {
1229
- id: "smoke-ribbon",
1230
- name: "Smoke ribbon",
1231
- kind: "mark",
1232
- medium: "paint",
1233
- variety: "smoke",
1234
- tip: { shape: "round", size: 70, hardness: 0.3, opacity: 0.45 },
1235
- marks: { kind: "particles", physics: "flow", render: "ribbon", count: 3, size: 5e-3, speed: 0.15, gravity: -0.15, life: 0.35, ribbonFill: 0.25, waveAmplitude: 0.1, waveLength: 140, wavePhaseJitter: 1 },
1236
- dynamics: { smoothing: 0.3 },
1237
- light: { amount: 0.3 }
1238
- },
1239
- // Out-of-focus lights: soft discs and rings spaced along springy paths.
1240
- {
1241
- id: "bokeh-dots",
1242
- name: "Bokeh dots",
1243
- kind: "mark",
1244
- medium: "paint",
1245
- variety: "bokeh",
1246
- tip: { shape: "stamp", size: 80, hardness: 0.7, opacity: 0.6, stamps: ["editor:dot", "editor:dot", "editor:ring"] },
1247
- marks: { kind: "particles", render: "sequence", count: 3, size: 0.3, spread: 1, stiffness: 0.15, damping: 0.4, interval: 1, stampOrder: "random" },
1248
- dynamics: { smoothing: 0.3 },
1249
- light: { amount: 0.9 }
1250
- },
1251
- // Sparks: twinkles and stars thrown into the flow, spaced along it.
1252
- {
1253
- id: "twinkle-sparks",
1254
- name: "Twinkle sparks",
1255
- kind: "mark",
1256
- medium: "paint",
1257
- variety: "sparks",
1258
- tip: { shape: "stamp", size: 50, hardness: 0.8, stamps: ["editor:sparkle", "editor:sparkle", "editor:star"] },
1259
- marks: { kind: "particles", physics: "flow", render: "sequence", count: 3, size: 0.25, spread: 0.8, speed: 0.9, life: 0.35, interval: 0.6, stampOrder: "random" },
1260
- dynamics: { smoothing: 0.1 },
1261
- light: { amount: 1 }
1262
- },
1263
- {
1264
- id: "glow-wash",
1265
- name: "Glow wash",
1266
- kind: "mark",
1267
- medium: "paint",
1268
- variety: "watercolor",
1269
- tip: { shape: "round", size: 40, hardness: 0.55, spacing: 0.12 },
1270
- dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.35 },
1271
- behaviour: { surface: "wet", exchange: { pickup: 0.4 }, wet: { water: 0.65, pigment: 0.75, rewet: 0.1 } },
1272
- light: { amount: 0.7 }
1273
- },
1274
- // Fluorescent ink: a transparent chisel marker that multiplies (text under it stays dark) and glows a little
1275
- // brighter than the paper. Its light spills nowhere (a spill hint of 0), and it comes in highlighter colours.
1276
- {
1277
- id: "highlighter",
1278
- name: "Highlighter",
1279
- kind: "mark",
1280
- medium: "ink",
1281
- variety: "highlighter",
1282
- tip: { shape: "chisel", size: 30, hardness: 0.85, spacing: 0.04, angle: 45, roundness: 0.35, opacity: 0.6, blend: "multiply" },
1283
- dynamics: { smoothing: 0.3, pressureSize: false },
1284
- behaviour: { grain: 0.05 },
1285
- light: { amount: 0.5, spill: { radius: 0 } },
1286
- colour: { default: "#fff200", palette: ["#fff200", "#ff6ec7", "#7cff4f", "#ffa630"] }
1287
- },
1288
- {
1289
- id: "sticker-stars",
1290
- name: "Star stickers",
1291
- kind: "mark",
1292
- variety: "stars",
1293
- tip: { shape: "stamp", size: 36, stamps: ["editor:star", "editor:sparkle"] },
1294
- marks: { kind: "objects", density: 0.35, sizeJitter: 0.5, scatter: 0.6, colourJitter: 0.3 },
1295
- dynamics: { smoothing: 0.2, minSize: 0.5 }
1296
- },
1297
- {
1298
- id: "sticker-leaves",
1299
- name: "Leaf stickers",
1300
- kind: "mark",
1301
- variety: "leaves",
1302
- tip: { shape: "stamp", size: 40, stamps: ["editor:leaf"] },
1303
- marks: { kind: "objects", density: 0.5, rotationJitter: 0.35, scatter: 0.4, colourJitter: 0.35 },
1304
- dynamics: { smoothing: 0.25, rotation: "stroke", minSize: 0.5 }
1305
- },
1306
- {
1307
- id: "sticker-confetti",
1308
- name: "Confetti",
1309
- kind: "mark",
1310
- variety: "confetti",
1311
- tip: { shape: "stamp", size: 22, stamps: ["editor:confetti", "editor:dot", "editor:ring"] },
1312
- marks: { kind: "objects", density: 0.7, sizeJitter: 0.5, scatter: 1.5, colourJitter: 1 },
1313
- dynamics: { smoothing: 0.1, pressureSize: false }
1314
- },
1315
- {
1316
- id: "sticker-flowers",
1317
- name: "Flower stickers",
1318
- kind: "mark",
1319
- variety: "flowers",
1320
- tip: { shape: "stamp", size: 44, stamps: ["editor:flower", "editor:heart"] },
1321
- marks: { kind: "objects", density: 0.3, sizeJitter: 0.5, scatter: 0.7, colourJitter: 0.4 },
1322
- dynamics: { smoothing: 0.2 }
1323
- },
1324
- {
1325
- id: "sticker-splats",
1326
- name: "Paint splats",
1327
- kind: "mark",
1328
- variety: "splats",
1329
- tip: { shape: "stamp", size: 50, opacity: 0.95, stamps: ["editor:splat"] },
1330
- marks: { kind: "objects", density: 0.25, sizeJitter: 0.6, scatter: 0.8, colourJitter: 0.15 },
1331
- dynamics: { smoothing: 0.1 }
1332
- },
1333
- {
1334
- id: "add-water",
1335
- name: "Add water",
1336
- kind: "tool",
1337
- tool: "water",
1338
- tip: { shape: "round", size: 40, hardness: 0.4, spacing: 0.12 },
1339
- dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.5 },
1340
- behaviour: { wet: { water: 0.6, pigment: 0, rewet: 0.3 } }
1341
- },
1342
- {
1343
- id: "hair-dryer",
1344
- name: "Dry",
1345
- kind: "tool",
1346
- tool: "dryer",
1347
- tip: { shape: "round", size: 80, hardness: 0.1, spacing: 0.15 },
1348
- dynamics: { smoothing: 0.3, pressureSize: false, pressureOpacity: true }
1349
- },
1350
- {
1351
- id: "blow",
1352
- name: "Blow",
1353
- kind: "tool",
1354
- tool: "blow",
1355
- tip: { shape: "round", size: 50, hardness: 0.2 },
1356
- dynamics: { smoothing: 0.3, pressureSize: false, pressureOpacity: true },
1357
- behaviour: { strength: 0.6 }
1358
- },
1359
- {
1360
- id: "wet-blend",
1361
- name: "Wet blend",
1362
- kind: "tool",
1363
- tool: "wet-blend",
1364
- tip: { shape: "round", size: 36, hardness: 0.3 },
1365
- dynamics: { smoothing: 0.25, pressureSize: false, pressureOpacity: true },
1366
- behaviour: { strength: 0.4 }
1367
- },
1368
- {
1369
- id: "wet-drag",
1370
- name: "Wet smudge",
1371
- kind: "tool",
1372
- tool: "wet-drag",
1373
- tip: { shape: "round", size: 30, hardness: 0.5, spacing: 0.08 },
1374
- dynamics: { smoothing: 0.25, pressureSize: false, pressureOpacity: true },
1375
- behaviour: { strength: 0.6 }
1376
- },
1377
- {
1378
- id: "blot",
1379
- name: "Blot (erase wet)",
1380
- kind: "tool",
1381
- tool: "blot",
1382
- tip: { shape: "round", size: 40, hardness: 0.5, spacing: 0.12 },
1383
- dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true },
1384
- behaviour: { strength: 0.7 }
1385
- }
1386
- ];
1387
-
1388
- // src/brushes/profiles.ts
1389
- var ALL_COLOURS = ["solid", "dual", "gradient", "effect"];
1390
- var ELLIPSES = ["round", "flat", "chisel"];
1391
- var PAINT_PROFILE = {
1392
- id: "paint",
1393
- name: "Paint editor",
1394
- kinds: ["mark", "tool"],
1395
- tools: BRUSH_TOOLS,
1396
- tipShapes: BRUSH_TIP_SHAPES,
1397
- marks: BRUSH_MARKS_KINDS,
1398
- surfaces: ["dry", "wet", "thick"],
1399
- exchange: "full",
1400
- reservoir: "full",
1401
- grain: "full",
1402
- light: "full",
1403
- motion: "full",
1404
- colours: ALL_COLOURS,
1405
- velocity: "full",
1406
- tilt: "full"
1407
- };
1408
- var IMAGE_PROFILE = {
1409
- id: "image",
1410
- name: "Image editor",
1411
- kinds: ["mark", "tool"],
1412
- tools: ["eraser"],
1413
- tipShapes: ELLIPSES,
1414
- marks: ["continuous"],
1415
- surfaces: ["dry"],
1416
- exchange: "ignored",
1417
- reservoir: "full",
1418
- grain: "ignored",
1419
- light: "none",
1420
- motion: "none",
1421
- colours: ["solid", "dual", "gradient"],
1422
- velocity: "ignored",
1423
- tilt: "ignored"
1424
- };
1425
- var CANVAS_PROFILE = {
1426
- id: "canvas",
1427
- name: "Canvas",
1428
- kinds: ["mark"],
1429
- tools: [],
1430
- tipShapes: ELLIPSES,
1431
- marks: ["continuous"],
1432
- surfaces: ["dry"],
1433
- exchange: "ignored",
1434
- reservoir: "none",
1435
- grain: "full",
1436
- light: "none",
1437
- motion: "none",
1438
- colours: ["solid", "dual", "gradient"],
1439
- velocity: "ignored",
1440
- tilt: "full"
1441
- };
1442
- var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", light: "ignored" };
1443
- var BRUSH_HOST_PROFILES = { paint: PAINT_PROFILE, image: IMAGE_PROFILE, canvas: CANVAS_PROFILE, vector: VECTOR_PROFILE };
1444
- function brushColourKind(paint) {
1445
- if (typeof paint === "string") return "solid";
1446
- if ("type" in paint) return paint.type === "gradient" ? "gradient" : "effect";
1447
- return "dual";
1448
- }
1449
- var list = (values) => values.join(", ");
1450
- var nonZero = (v) => v !== void 0 && v !== 0;
1451
- function checkBrush(brush, profile) {
1452
- const issues = [];
1453
- const host = profile.name;
1454
- const add = (facet, severity, reason, fix) => issues.push({ facet, severity, reason, fix });
1455
- const feature = (support, used, facet, what, fix) => {
1456
- if (!used || support === "full") return;
1457
- if (support === "none") add(facet, "error", `${host} can't paint ${what}.`, fix);
1458
- else add(facet, "warning", `${host} paints this brush without ${what}.`, `Nothing to do; or ${fix.charAt(0).toLowerCase()}${fix.slice(1)}`);
1459
- };
1460
- if (!profile.kinds.includes(brush.kind)) add("kind", "error", `${host} has no ${brush.kind} brushes.`, `Use a ${list(profile.kinds)} brush.`);
1461
- else if (brush.kind === "tool" && brush.tool && !profile.tools.includes(brush.tool)) {
1462
- add("tool", "error", `${host} has no ${brush.tool} tool.`, profile.tools.length ? `Use one of: ${list(profile.tools)}.` : `${host} has no tools; use a mark brush.`);
1463
- }
1464
- if (!profile.tipShapes.includes(brush.tip.shape)) add("tip.shape", "error", `${host} can't lay a ${brush.tip.shape} tip.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
1465
- const marks = _nullishCoalesce(_optionalChain([brush, 'access', _27 => _27.marks, 'optionalAccess', _28 => _28.kind]), () => ( "continuous"));
1466
- if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
1467
- const surface = _nullishCoalesce(_optionalChain([brush, 'access', _29 => _29.behaviour, 'optionalAccess', _30 => _30.surface]), () => ( "dry"));
1468
- if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
1469
- const ex = _optionalChain([brush, 'access', _31 => _31.behaviour, 'optionalAccess', _32 => _32.exchange]);
1470
- feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _33 => _33.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
1471
- feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _34 => _34.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _35 => _35.pickup])) || !!_optionalChain([ex, 'optionalAccess', _36 => _36.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
1472
- feature(profile.grain, nonZero(_optionalChain([brush, 'access', _37 => _37.behaviour, 'optionalAccess', _38 => _38.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
1473
- feature(profile.light, nonZero(_optionalChain([brush, 'access', _39 => _39.light, 'optionalAccess', _40 => _40.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
1474
- const m = brush.motion;
1475
- feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
1476
- const d = _nullishCoalesce(brush.dynamics, () => ( {}));
1477
- feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
1478
- feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
1479
- const colour = brush.colour;
1480
- if (_optionalChain([colour, 'optionalAccess', _41 => _41.default]) !== void 0) {
1481
- const kind = brushColourKind(colour.default);
1482
- if (!profile.colours.includes(kind)) add("colour.default", "error", `${host} can't paint ${kind} colours.`, `Use a ${list(profile.colours)} colour, or remove colour.default.`);
1483
- }
1484
- const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _42 => _42.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
1485
- if (foreign.length) add("colour.palette", "warning", `${host} skips the palette's ${list(foreign)} colours.`, `Nothing to do; or keep to ${list(profile.colours)} colours.`);
1486
- return { ok: !issues.some((i) => i.severity === "error"), issues };
1487
- }
1488
- function listPresets(profile, presets = BRUSH_PRESETS) {
1489
- return presets.filter((b) => checkBrush(b, profile).ok);
1490
- }
1491
- function degrade(brush, profile) {
1492
- const before = checkBrush(brush, profile).issues.filter((i) => i.severity === "error");
1493
- const b = JSON.parse(JSON.stringify(brush));
1494
- const dropped = [];
1495
- for (const issue of before) {
1496
- switch (issue.facet) {
1497
- case "tip.shape":
1498
- b.tip = { ...b.tip, shape: profile.tipShapes.includes("round") ? "round" : profile.tipShapes[0] };
1499
- delete b.tip.bristle;
1500
- delete b.tip.stamps;
1501
- break;
1502
- case "marks":
1503
- delete b.marks;
1504
- if (b.tip.shape === "stamp") {
1505
- b.tip.shape = "round";
1506
- delete b.tip.stamps;
1507
- }
1508
- break;
1509
- case "behaviour.surface":
1510
- delete b.behaviour.surface;
1511
- delete b.behaviour.wet;
1512
- delete b.behaviour.thick;
1513
- break;
1514
- case "behaviour.exchange.reservoir":
1515
- delete b.behaviour.exchange.reservoir;
1516
- break;
1517
- case "behaviour.exchange":
1518
- delete b.behaviour.exchange;
1519
- break;
1520
- case "behaviour.grain":
1521
- delete b.behaviour.grain;
1522
- break;
1523
- case "light":
1524
- delete b.light;
1525
- break;
1526
- case "motion":
1527
- delete b.motion;
1528
- break;
1529
- case "dynamics.velocity":
1530
- delete b.dynamics.velocitySize;
1531
- delete b.dynamics.velocityOpacity;
1532
- break;
1533
- case "dynamics.tilt":
1534
- delete b.dynamics.tiltSize;
1535
- delete b.dynamics.tiltRoundness;
1536
- delete b.dynamics.tiltOpacity;
1537
- break;
1538
- case "colour.default":
1539
- delete b.colour.default;
1540
- break;
1541
- default:
1542
- continue;
1543
- }
1544
- dropped.push(issue);
1545
- }
1546
- if (_optionalChain([b, 'access', _43 => _43.behaviour, 'optionalAccess', _44 => _44.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
1547
- const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
1548
- return { brush: b, dropped, issues: after };
1549
- }
1550
31
 
1551
- // src/brushes/library.ts
1552
- var BrushLibraryError = class extends Error {
1553
- constructor(code, message) {
1554
- super(message);
1555
- this.code = code;
1556
- this.name = "BrushLibraryError";
1557
- }
1558
-
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+ var _chunk34KLWUH4cjs = require('../chunk-34KLWUH4.cjs');
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+ var _chunkHS6QDWDFcjs = require('../chunk-HS6QDWDF.cjs');
57
+
58
+
59
+ var _chunkJSDND7DFcjs = require('../chunk-JSDND7DF.cjs');
60
+ require('../chunk-U67EN7GG.cjs');
61
+ require('../chunk-WOFXZ3XU.cjs');
62
+
63
+
64
+ var _chunkFQPUF2W2cjs = require('../chunk-FQPUF2W2.cjs');
65
+ require('../chunk-3K6HWEXZ.cjs');
66
+
67
+
68
+ var _chunk2VX55NFMcjs = require('../chunk-2VX55NFM.cjs');
69
+ require('../chunk-474IQG74.cjs');
70
+
71
+ // src/brushes/ui.ts
72
+ var title = (s) => s.charAt(0).toUpperCase() + s.slice(1).replace(/-/g, " ");
73
+ var options = (values, labels = {}) => values.map((value) => ({ value, label: _nullishCoalesce(labels[value], () => ( title(value))) }));
74
+ var MEDIUM_LABELS = { paint: "Paint", ink: "Ink", dry: "Dry media", spray: "Spray" };
75
+ var TOOL_LABELS = { eraser: "Eraser", blender: "Blender", smudge: "Smudge", water: "Add water", dryer: "Dry", blow: "Blow", blot: "Blot", "wet-blend": "Wet blend", "wet-drag": "Wet smudge" };
76
+ var SHAPE_LABELS = { round: "Round", flat: "Flat", chisel: "Chisel", bristle: "Bristle", stamp: "Stamp" };
77
+ var MARKS_LABELS = { continuous: "Continuous", particles: "Particles", objects: "Stickers" };
78
+ var SURFACE_LABELS = { dry: "Dry", wet: "Wet (flows)", thick: "Thick (impasto)" };
79
+ var ROTATION_LABELS = { fixed: "Fixed", stroke: "Follow stroke", barrel: "Pen barrel" };
80
+ var PHYSICS_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow", elastic: "Elastic", fuzzy: "Fuzzy" };
81
+ var RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ribbon: "Ribbon" };
82
+ var APPLICATOR_LABELS = { brush: "Brush", tube: "Paint tube", knife: "Palette knife", roller: "Roller", gloop: "Gloop pen" };
83
+ var WET_MEDIUM_LABELS = { watercolor: "Watercolour", gouache: "Gouache", oil: "Oil" };
84
+ var STAMP_OPTIONS = _chunkJSDND7DFcjs.STAMP_KINDS.map((k) => ({ value: `editor:${k}`, label: title(k) }));
85
+ var ARIA = {
86
+ "dynamics.rotation": "Brush rotation",
87
+ "tip.blend": "Brush blend",
88
+ "behaviour.mixing": "Brush mixing",
89
+ "behaviour.exchange.dirty": "Dirty brush",
90
+ "marks.physics": "Particle kind",
91
+ "marks.render": "Particle render",
92
+ "marks.ribbonPairs": "Ribbon pairs",
93
+ "marks.stampOrder": "Dot texture order",
94
+ "marks.stampRotation": "Dot rotation",
95
+ "behaviour.wet.medium": "Wet medium",
96
+ "behaviour.thick.applicator": "Impasto tool",
97
+ "light.pressure": "Light follows pressure"
1559
98
  };
1560
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1561
- var isV1 = (item) => "engine" in item || "category" in item || !("kind" in item);
1562
- function parseBrushLibrary(raw, problems = []) {
1563
- if (typeof raw === "string") {
1564
- try {
1565
- raw = JSON.parse(raw);
1566
- } catch (e3) {
1567
- throw new BrushLibraryError("invalid", "Not a brush library: the text is not JSON.");
1568
- }
1569
- }
1570
- const file = isObject2(raw) ? raw : null;
1571
- const items = Array.isArray(raw) ? raw : file && Array.isArray(file.brushes) ? file.brushes : null;
1572
- if (!items) throw new BrushLibraryError("invalid", 'Not a brush library: expected {"format":"pixerate-brushes","version":2,"brushes":[...]} or an array of brushes.');
1573
- if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
1574
- throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
1575
- }
1576
- const v1File = _optionalChain([file, 'optionalAccess', _45 => _45.version]) === 1;
1577
- const out = [];
1578
- items.forEach((item, i) => {
1579
- const at = `brushes[${i}]`;
1580
- if (!isObject2(item)) return void problems.push(`${at} is not an object; skipped.`);
1581
- if (typeof item.id !== "string" || !_chunkHS6QDWDFcjs.BRUSH_ID.test(item.id)) return void problems.push(`${at} has no valid id (lowercase letters, digits and dashes); skipped.`);
1582
- if (typeof item.name !== "string" || !item.name.trim()) return void problems.push(`${at} ("${item.id}") has no name; skipped.`);
1583
- const { builtin: _b, ...rest } = item;
1584
- const own = [];
1585
- try {
1586
- out.push(normalizeBrushSpec(v1File || isV1(rest) ? migrateBrushV1(rest) : rest, own));
1587
- } catch (e) {
1588
- own.push(`${e.message} Skipped.`);
1589
- }
1590
- problems.push(...own.map((p) => `${at} ("${item.id}"): ${p}`));
1591
- });
1592
- return out;
1593
- }
1594
- var words = (s) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
1595
- function matches(entry, f2) {
1596
- const b = entry.brush;
1597
- if (f2.source && f2.source === "builtin" !== entry.builtin) return false;
1598
- if (f2.kind && b.kind !== f2.kind) return false;
1599
- if (f2.medium && b.medium !== f2.medium) return false;
1600
- if (f2.variety && b.variety !== f2.variety) return false;
1601
- if (f2.tool && b.tool !== f2.tool) return false;
1602
- if (f2.tag && !(_nullishCoalesce(b.tags, () => ( []))).includes(f2.tag)) return false;
1603
- if (f2.text) {
1604
- const text = words([b.id, b.name, _nullishCoalesce(b.description, () => ( "")), _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
1605
- if (!words(f2.text).every((w) => text.some((t) => t.startsWith(w)))) return false;
1606
- }
1607
- if (f2.profile && !checkBrush(b, f2.profile).ok) return false;
1608
- return true;
1609
- }
1610
- var copy3 = (v) => JSON.parse(JSON.stringify(v));
1611
- function createBrushLibrary(options = {}) {
1612
- const presets = (_nullishCoalesce(options.presets, () => ( BRUSH_PRESETS))).map(copy3);
1613
- const builtinIds = new Set(presets.map((b) => b.id));
1614
- const key = `${_nullishCoalesce(options.namespace, () => ( "pixerate.brushes"))}:user`;
1615
- const listeners = /* @__PURE__ */ new Set();
1616
- let user = null;
1617
- let queue = Promise.resolve();
1618
- const run = (task) => {
1619
- const result = queue.then(task, task);
1620
- queue = result.catch(() => void 0);
1621
- return result;
1622
- };
1623
- const notify = () => listeners.forEach((listener) => listener());
1624
- const load = async () => {
1625
- if (user) return user;
1626
- const raw = options.storage ? await options.storage.get(key) : null;
1627
- let brushes = [];
1628
- if (raw) {
1629
- try {
1630
- brushes = parseBrushLibrary(raw).filter((b) => !builtinIds.has(b.id));
1631
- } catch (e4) {
1632
- }
1633
- }
1634
- user = brushes;
1635
- return user;
1636
- };
1637
- const store = async (brushes) => {
1638
- const file = { format: "pixerate-brushes", version: 2, brushes };
1639
- if (options.storage) await options.storage.set(key, JSON.stringify(file));
1640
- user = brushes;
1641
- };
1642
- const entries = async () => [...presets.map((brush) => ({ brush, builtin: true })), ...(await load()).map((brush) => ({ brush, builtin: false }))];
1643
- const find = async (id) => {
1644
- const entry = (await entries()).find((e) => e.brush.id === id);
1645
- if (!entry) throw new BrushLibraryError("unknown_brush", `No brush "${id}". ${closest(id, (await entries()).map((e) => e.brush.id))}list_brush_presets lists them.`);
1646
- return entry;
1647
- };
1648
- const refuseBuiltin = (id, what) => {
1649
- if (builtinIds.has(id)) throw new BrushLibraryError("builtin", `"${id}" is a built-in brush and can't be ${what}; duplicate it first (duplicate_brush) and change the copy.`);
1650
- };
1651
- const freeId = async (base) => {
1652
- const taken = new Set((await entries()).map((e) => e.brush.id));
1653
- const stem = base.slice(0, 58);
1654
- for (let n = 1; ; n++) {
1655
- const id = n === 1 ? `${stem}-copy` : `${stem}-copy-${n}`;
1656
- if (!taken.has(id)) return id;
1657
- }
99
+ var aria = (path, label) => _nullishCoalesce(ARIA[path], () => ( `Brush ${label.toLowerCase()}`));
100
+ function brushEditorSections(brush, profile = _chunk34KLWUH4cjs.PAINT_PROFILE) {
101
+ const resolved = _chunk34KLWUH4cjs.resolveBrush.call(void 0, brush);
102
+ const engine = _chunk34KLWUH4cjs.resolveEngine.call(void 0, brush);
103
+ const fieldOf = (facet) => _chunk34KLWUH4cjs.BRUSH_FIELDS.find((f) => f.facet === facet && f.engines.includes(engine));
104
+ const facetOf = (enginePath) => _optionalChain([(_nullishCoalesce(_chunk34KLWUH4cjs.BRUSH_FIELDS.find((f) => f.engine === enginePath && f.engines.includes(engine)), () => ( _chunk34KLWUH4cjs.BRUSH_FIELDS.find((f) => f.engine === enginePath)))), 'optionalAccess', _ => _.facet]);
105
+ const value = (path) => {
106
+ const own = _chunk34KLWUH4cjs.getPath.call(void 0, brush, path);
107
+ if (own !== void 0) return own;
108
+ const field = fieldOf(path);
109
+ if (!field) return void 0;
110
+ const v = _chunk34KLWUH4cjs.getPath.call(void 0, resolved, field.engine);
111
+ return field.map ? field.map.toFacet(v) : v;
1658
112
  };
1659
- const saveLocked = async (raw, overwrite) => {
1660
- const problems = [];
1661
- let brush;
1662
- try {
1663
- brush = normalizeBrushSpec(raw, problems);
1664
- } catch (e) {
1665
- throw new BrushLibraryError("invalid", e.message);
1666
- }
1667
- refuseBuiltin(brush.id, "overwritten");
1668
- const brushes = await load();
1669
- const at = brushes.findIndex((b) => b.id === brush.id);
1670
- if (at !== -1 && !overwrite) throw new BrushLibraryError("exists", `A brush "${brush.id}" exists already; pick another id, or update it (update_brush).`);
1671
- await store(at === -1 ? [...brushes, brush] : brushes.map((b, i) => i === at ? brush : b));
1672
- return { brush: copy3(brush), problems, replaced: at !== -1 };
113
+ const warned = new Map(_chunk34KLWUH4cjs.checkBrush.call(void 0, brush, profile).issues.filter((i) => i.severity === "warning").map((i) => [i.facet, i.reason]));
114
+ const noteFor = (path) => _optionalChain([[...warned], 'access', _2 => _2.find, 'call', _3 => _3(([facet]) => path === facet || path.startsWith(`${facet}.`)), 'optionalAccess', _4 => _4[1]]);
115
+ const withNote = (c) => {
116
+ const note = noteFor(c.path);
117
+ return note ? { ...c, note } : c;
1673
118
  };
1674
- return {
1675
- list: (filter = {}) => run(async () => (await entries()).filter((e) => matches(e, filter)).map(copy3)),
1676
- get: (id) => run(async () => {
1677
- const entry = (await entries()).find((e) => e.brush.id === id);
1678
- return entry ? copy3(entry) : null;
1679
- }),
1680
- isBuiltin: (id) => builtinIds.has(id),
1681
- save: (brush, o = {}) => run(async () => {
1682
- const result = await saveLocked(brush, !!o.overwrite);
1683
- notify();
1684
- return result;
1685
- }),
1686
- remove: (id) => run(async () => {
1687
- refuseBuiltin(id, "removed");
1688
- const brushes = await load();
1689
- if (!brushes.some((b) => b.id === id)) return false;
1690
- await store(brushes.filter((b) => b.id !== id));
1691
- notify();
1692
- return true;
1693
- }),
1694
- duplicate: (id, o = {}) => run(async () => {
1695
- const { brush } = await find(id);
1696
- const newId = await _asyncNullishCoalesce(o.id, async () => ( await freeId(id)));
1697
- const { brush: saved } = await saveLocked({ ...copy3(brush), id: newId, name: _nullishCoalesce(o.name, () => ( `${brush.name} copy`.slice(0, 80))) }, false);
1698
- notify();
1699
- return saved;
1700
- }),
1701
- exportFile: (ids) => run(async () => {
1702
- const brushes = ids ? await Promise.all(ids.map(async (id) => (await find(id)).brush)) : await load();
1703
- return { format: "pixerate-brushes", version: 2, brushes: brushes.map(copy3) };
1704
- }),
1705
- importFile: (raw, o = {}) => run(async () => {
1706
- const problems = [];
1707
- const incoming = parseBrushLibrary(raw, problems);
1708
- const result = { added: [], replaced: [], skipped: [], problems };
1709
- let brushes = [...await load()];
1710
- for (const brush of incoming) {
1711
- if (builtinIds.has(brush.id)) {
1712
- result.skipped.push(brush.id);
1713
- problems.push(`"${brush.id}" is a built-in brush id; skipped (rename it to import it).`);
1714
- continue;
1715
- }
1716
- const at = brushes.findIndex((b) => b.id === brush.id);
1717
- if (at === -1) {
1718
- brushes.push(brush);
1719
- result.added.push(brush.id);
1720
- } else if (o.overwrite) {
1721
- brushes = brushes.map((b, i) => i === at ? brush : b);
1722
- result.replaced.push(brush.id);
1723
- } else {
1724
- result.skipped.push(brush.id);
1725
- problems.push(`"${brush.id}" exists already; skipped (pass overwrite to replace it).`);
1726
- }
1727
- }
1728
- if (result.added.length || result.replaced.length) {
1729
- await store(brushes);
1730
- notify();
1731
- }
1732
- return result;
1733
- }),
1734
- subscribe(listener) {
1735
- listeners.add(listener);
1736
- return () => {
1737
- listeners.delete(listener);
1738
- };
1739
- }
119
+ const slider = (c, path = facetOf(c.path)) => {
120
+ if (!path) return null;
121
+ const v = value(path);
122
+ return withNote({ kind: "slider", path, label: c.label, ariaLabel: aria(path, c.label), min: c.min, max: path === "tip.size" ? Math.min(c.max, _chunkFQPUF2W2cjs.MAX_BRUSH_SIZE) : c.max, step: c.step, value: typeof v === "number" ? v : c.min });
1740
123
  };
1741
- }
1742
- function closest(id, ids, max = 3) {
1743
- const stem = id.toLowerCase();
1744
- const score = (c) => {
1745
- let n = 0;
1746
- while (n < Math.min(c.length, stem.length) && c[n] === stem[n]) n++;
1747
- return n + (c.includes(stem) || stem.includes(c) ? 4 : 0) + words(c).filter((w) => words(stem).includes(w)).length * 3;
124
+ const choice = (path, label, opts, fallback = _nullishCoalesce(_optionalChain([opts, 'access', _5 => _5[0], 'optionalAccess', _6 => _6.value]), () => ( ""))) => withNote({ kind: "choice", path, label, ariaLabel: aria(path, label), options: opts, value: String(_nullishCoalesce(value(path), () => ( fallback))) });
125
+ const toggle = (path, label) => withNote({ kind: "toggle", path, label, ariaLabel: aria(path, label), value: Boolean(value(path)) });
126
+ const present = (cs) => cs.filter((c) => Boolean(c));
127
+ const sections = [];
128
+ const add = (id, name, controls) => {
129
+ const list = present(controls);
130
+ if (list.length) sections.push({ id, title: name, controls: list });
1748
131
  };
1749
- const near = ids.map((c) => [c, score(c)]).filter(([, s]) => s >= 3).sort((a, b) => b[1] - a[1]).slice(0, max).map(([c]) => c);
1750
- return near.length ? `Closest: ${near.join(", ")}. ` : "";
132
+ const isTool = brush.kind === "tool";
133
+ const shape = brush.tip.shape;
134
+ const marks = _nullishCoalesce(_optionalChain([brush, 'access', _7 => _7.marks, 'optionalAccess', _8 => _8.kind]), () => ( "continuous"));
135
+ add("medium", "Medium", [
136
+ profile.kinds.length > 1 && choice("kind", "Kind", options(profile.kinds, { mark: "Lays a material", tool: "Works on paint" })),
137
+ !isTool && choice("medium", "Medium", [{ value: "", label: "Any" }, ...options(_chunk34KLWUH4cjs.BRUSH_MEDIA, MEDIUM_LABELS)], ""),
138
+ !isTool && withNote({ kind: "text", path: "variety", label: "Variety", ariaLabel: "Brush variety", value: _nullishCoalesce(brush.variety, () => ( "")), placeholder: "pencil, oil, highlighter\u2026" }),
139
+ isTool && profile.tools.length > 0 && choice("tool", "Tool", options(profile.tools, TOOL_LABELS))
140
+ ]);
141
+ const engineControls = _chunk34KLWUH4cjs.brushEngineControls.call(void 0, resolved);
142
+ const tipSliders = _chunk34KLWUH4cjs.BRUSH_SLIDERS.filter((c) => c.path.startsWith("tip.") && c.path !== "tip.smoothing");
143
+ add("shape", "Shape", [
144
+ choice("tip.shape", "Tip", options(profile.tipShapes.filter((s) => !isTool || s !== "stamp"), SHAPE_LABELS)),
145
+ ...tipSliders.map((c) => slider(c)),
146
+ engine === "dab" && choice("tip.blend", "Blend", _chunk2VX55NFMcjs.BLEND_MODES.map((m) => ({ value: m.id, label: m.label })), "normal"),
147
+ ...shape === "bristle" && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("bristle.")).map((c) => slider(c)) : [],
148
+ shape === "stamp" && withNote({ kind: "stamps", path: "tip.stamps", label: "Stamps", ariaLabel: "Brush stamps", value: [..._nullishCoalesce(value("tip.stamps"), () => ( []))], max: marks === "particles" ? _chunkHS6QDWDFcjs.MAX_DOT_TEXTURES : _chunkHS6QDWDFcjs.MAX_STICKER_STAMPS, options: STAMP_OPTIONS })
149
+ ]);
150
+ if (!isTool && profile.marks.length > 1) {
151
+ const particles = marks === "particles" && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("particle.") && c.path !== "particle.waveSpeed") : [];
152
+ const objects = marks === "objects" && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("sticker.")) : [];
153
+ const render = value("marks.render");
154
+ add("marks", "Marks", [
155
+ choice("marks.kind", "Laid as", options(profile.marks, MARKS_LABELS), "continuous"),
156
+ marks === "particles" && choice("marks.physics", "Particles", options(_chunkHS6QDWDFcjs.PARTICLE_KINDS, PHYSICS_LABELS)),
157
+ marks === "particles" && choice("marks.render", "Drawn as", options(_chunkHS6QDWDFcjs.PARTICLE_RENDERS, RENDER_LABELS)),
158
+ marks === "particles" && render === "ribbon" && choice("marks.ribbonPairs", "Ribbon joins", options(_chunkHS6QDWDFcjs.RIBBON_PAIRS, { adjacent: "Neighbours", "first-last": "Outermost" })),
159
+ marks === "particles" && shape === "stamp" && choice("marks.stampOrder", "Texture order", options(_chunkHS6QDWDFcjs.DOT_TEXTURE_ORDERS, { sequence: "In turn", random: "Random" })),
160
+ marks === "particles" && shape === "stamp" && choice("marks.stampRotation", "Texture turn", options(_chunkHS6QDWDFcjs.DOT_ROTATIONS, { follow: "Follow motion", random: "Random", fixed: "Tip angle" })),
161
+ ...particles.map((c) => slider(c)),
162
+ ...objects.map((c) => slider(c))
163
+ ]);
164
+ }
165
+ const feel = _chunk34KLWUH4cjs.BRUSH_SLIDERS.filter((c) => ["tip.smoothing", "pressureSmoothing", "dynamics.minSize", "dynamics.tiltSize", "dynamics.velocitySize"].includes(c.path)).filter((c) => !(c.path === "dynamics.tiltSize" && profile.tilt === "none") && !(c.path === "dynamics.velocitySize" && profile.velocity === "none"));
166
+ add("feel", "Feel", [
167
+ ...feel.map((c) => slider(c)),
168
+ toggle("dynamics.pressureSize", "Pressure \u2192 size"),
169
+ toggle("dynamics.pressureOpacity", "Pressure \u2192 opacity"),
170
+ choice("dynamics.rotation", "Rotation", options(_chunkHS6QDWDFcjs.BRUSH_ROTATIONS, ROTATION_LABELS))
171
+ ]);
172
+ const surfaces = profile.surfaces;
173
+ const engineSliders = (_nullishCoalesce(_optionalChain([engineControls, 'optionalAccess', _9 => _9.sliders]), () => ( []))).filter((c) => !/^(bristle|particle|sticker)\./.test(c.path) && c.path !== "light.amount" && c.path !== "impasto.rim" && c.path !== "impasto.clarity");
174
+ const exchangeOk = (path) => !_optionalChain([path, 'optionalAccess', _10 => _10.startsWith, 'call', _11 => _11("behaviour.exchange")]) || (path.startsWith("behaviour.exchange.reservoir") ? profile.reservoir !== "none" : profile.exchange !== "none");
175
+ add("behaviour", _nullishCoalesce(_optionalChain([engineControls, 'optionalAccess', _12 => _12.title]), () => ( "Behaviour")), [
176
+ !isTool && surfaces.length > 1 && choice("behaviour.surface", "Surface", options(surfaces, SURFACE_LABELS), "dry"),
177
+ profile.grain !== "none" && slider({ path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 }, "behaviour.grain"),
178
+ engine === "wet" && !isTool && choice("behaviour.wet.medium", "Medium", options(_chunkHS6QDWDFcjs.WET_MEDIA, WET_MEDIUM_LABELS), "watercolor"),
179
+ engine === "impasto" && choice("behaviour.thick.applicator", "Tool", options(_chunkHS6QDWDFcjs.IMPASTO_TOOLS, APPLICATOR_LABELS), "brush"),
180
+ ...engineSliders.map((c) => exchangeOk(facetOf(c.path)) ? slider(c) : null),
181
+ _optionalChain([engineControls, 'optionalAccess', _13 => _13.mixingPath]) && fieldOf("behaviour.mixing") && choice("behaviour.mixing", "Mixing", options(["pigment", "rgb"], { pigment: "Pigment", rgb: "RGB" })),
182
+ _optionalChain([engineControls, 'optionalAccess', _14 => _14.dirty]) && profile.exchange !== "none" && fieldOf("behaviour.exchange.dirty") && toggle("behaviour.exchange.dirty", "Dirty brush"),
183
+ engine === "mixer" && toggle("behaviour.exchange.reservoir.refill", "Load each stroke")
184
+ ]);
185
+ if (!isTool && profile.light !== "none" && _optionalChain([engineControls, 'optionalAccess', _15 => _15.light])) {
186
+ add("light", "Light", [
187
+ slider({ path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 }),
188
+ toggle("light.pressure", "Light follows pressure"),
189
+ ...engine === "impasto" ? [slider({ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }), slider({ path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 })] : [],
190
+ withNote({ kind: "slider", path: "light.spill.radius", label: "Spill", ariaLabel: "Brush light spill", min: 0, max: 128, step: 1, value: _nullishCoalesce(_optionalChain([brush, 'access', _16 => _16.light, 'optionalAccess', _17 => _17.spill, 'optionalAccess', _18 => _18.radius]), () => ( 24)) }),
191
+ toggle("light.glow", "Suggest a glow")
192
+ ]);
193
+ }
194
+ if (profile.motion !== "none" && marks === "particles") add("motion", "Motion", [slider({ path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 })]);
195
+ if (!isTool) {
196
+ const own = _optionalChain([brush, 'access', _19 => _19.colour, 'optionalAccess', _20 => _20.default]);
197
+ add("colour", "Colour", [
198
+ withNote({ kind: "colour", path: "colour.default", label: "Default colour", ariaLabel: "Brush default colour", value: typeof own === "string" && /^#[0-9a-f]{6}/i.test(own) ? own.slice(0, 7).toLowerCase() : null }),
199
+ withNote({ kind: "palette", path: "colour.palette", label: "Palette", ariaLabel: "Brush palette", value: (_nullishCoalesce(_optionalChain([brush, 'access', _21 => _21.colour, 'optionalAccess', _22 => _22.palette]), () => ( []))).filter((c) => typeof c === "string"), max: _chunk34KLWUH4cjs.MAX_BRUSH_PALETTE }),
200
+ toggle("colour.own", "Always its own colour")
201
+ ]);
202
+ }
203
+ return sections;
1751
204
  }
1752
-
1753
- // src/brushes/tools.ts
1754
- var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
1755
- var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
1756
- var FACETS = BRUSH_JSON_SCHEMA.properties;
1757
- var ID = { type: "string", pattern: String(FACETS.id.pattern), description: "Brush id from list_brush_presets (lowercase letters, digits and dashes)." };
1758
- var FACET_PROPS = (({ id: _id, name: _name, ...rest }) => rest)(FACETS);
1759
- FACET_PROPS.tip = (({ required: _r, ...tip }) => ({ ...tip, description: `${tip.description} In a patch, shape may be left out to keep it.` }))(FACET_PROPS.tip);
1760
- var UNSETTABLE = ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
1761
- var HOSTS = Object.keys(BRUSH_HOST_PROFILES);
1762
- var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1763
- var copy4 = (v) => JSON.parse(JSON.stringify(v));
1764
- function merge(base, patch, path = "") {
1765
- const out = { ...base };
1766
- for (const [k, v] of Object.entries(patch)) {
1767
- if (v === void 0) continue;
1768
- const old = out[k];
1769
- const replaceMarks = path === "" && k === "marks" && isObject3(old) && isObject3(v) && old.kind !== v.kind;
1770
- out[k] = isObject3(v) && isObject3(old) && !replaceMarks ? merge(old, v, `${path}${k}.`) : copy4(v);
205
+ function setBrushEditorValue(brush, path, value) {
206
+ const next = JSON.parse(JSON.stringify(brush));
207
+ if (path === "kind") {
208
+ if (value === "tool") {
209
+ delete next.medium;
210
+ delete next.variety;
211
+ delete next.marks;
212
+ delete next.light;
213
+ delete next.colour;
214
+ next.tool = _nullishCoalesce(next.tool, () => ( "eraser"));
215
+ if (next.tip.shape === "stamp") next.tip = { shape: "round", ...next.tip.size !== void 0 ? { size: next.tip.size } : {} };
216
+ } else delete next.tool;
217
+ next.kind = value;
218
+ } else if (path === "marks.kind") {
219
+ next.marks = value === "continuous" ? void 0 : { kind: value };
220
+ if (value !== "continuous" && next.tip.shape === "bristle") next.tip = { ...next.tip, shape: "round", bristle: void 0 };
221
+ if (value === "objects") next.tip = { ...next.tip, shape: "stamp" };
222
+ } else if (path === "tip.shape" && value !== "stamp") {
223
+ next.tip = { ...next.tip, shape: value, stamps: void 0 };
224
+ if (value !== "bristle") delete next.tip.bristle;
225
+ } else if (value === void 0 || value === "") {
226
+ const keys = path.split(".");
227
+ const parent = _chunk34KLWUH4cjs.getPath.call(void 0, next, keys.slice(0, -1).join("."));
228
+ if (keys.length === 1) delete next[keys[0]];
229
+ else if (parent) delete parent[keys[keys.length - 1]];
230
+ } else _chunk34KLWUH4cjs.setPath.call(void 0, next, path, value);
231
+ const problems = [];
232
+ try {
233
+ return { brush: _chunk34KLWUH4cjs.normalizeBrushSpec.call(void 0, JSON.parse(JSON.stringify(next)), problems), problems };
234
+ } catch (e) {
235
+ return { brush, problems: [e.message] };
1771
236
  }
1772
- return out;
1773
237
  }
1774
- function changedPaths(a, b, path = "") {
1775
- if (isObject3(a) && isObject3(b)) return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedPaths(a[k], b[k], path ? `${path}.${k}` : k));
1776
- return JSON.stringify(a) === JSON.stringify(b) ? [] : [path];
1777
- }
1778
- function summary(entry, profile, withCheck) {
1779
- const b = entry.brush;
1780
- const engine = resolveEngine(b);
1781
- const check = withCheck ? checkBrush(b, profile) : null;
1782
- return {
1783
- id: b.id,
1784
- name: b.name,
1785
- builtin: entry.builtin,
1786
- kind: b.kind,
1787
- ...b.medium ? { medium: b.medium } : {},
1788
- ...b.variety ? { variety: b.variety } : {},
1789
- ...b.tool ? { tool: b.tool } : {},
1790
- category: resolveCategory(b, engine),
1791
- engine,
1792
- tip: b.tip.shape,
1793
- ...b.tip.size !== void 0 ? { size: b.tip.size } : {},
1794
- marks: _nullishCoalesce(_optionalChain([b, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]), () => ( "continuous")),
1795
- surface: _nullishCoalesce(_optionalChain([b, 'access', _48 => _48.behaviour, 'optionalAccess', _49 => _49.surface]), () => ( "dry")),
1796
- ..._optionalChain([b, 'access', _50 => _50.light, 'optionalAccess', _51 => _51.amount]) ? { light: b.light.amount } : {},
1797
- ..._optionalChain([b, 'access', _52 => _52.tags, 'optionalAccess', _53 => _53.length]) ? { tags: b.tags } : {},
1798
- ...check ? { renderable: check.ok } : {}
1799
- };
1800
- }
1801
- var refusal = (check, host) => `${host.name} can't use this brush: ${check.issues.filter((i) => i.severity === "error").map((i) => `${i.facet}: ${i.reason} ${i.fix}`).join(" ")} (check_brush lists every issue; nothing was saved.)`;
1802
- var warnings = (check) => check.issues.filter((i) => i.severity === "warning");
1803
- function createBrushLibraryTools(library, options = {}) {
1804
- const profile = _nullishCoalesce(options.profile, () => ( PAINT_PROFILE));
1805
- const hostOf = (id) => {
1806
- if (id === void 0) return profile;
1807
- const p = BRUSH_HOST_PROFILES[id];
1808
- if (!p) throw new BrushLibraryError("invalid", `No host "${id}"; use one of: ${HOSTS.join(", ")}.`);
1809
- return p;
1810
- };
1811
- const entry = async (id) => {
1812
- const found = await library.get(id);
1813
- if (found) return found;
1814
- const ids = (await library.list()).map((e) => e.brush.id);
1815
- throw new BrushLibraryError("unknown_brush", `No brush "${id}". ${closest(id, ids)}list_brush_presets lists them.`);
1816
- };
1817
- const store = async (raw, overwrite) => {
1818
- const problems = [];
1819
- let brush;
1820
- try {
1821
- brush = normalizeBrushSpec(raw, problems);
1822
- } catch (e) {
1823
- throw new BrushLibraryError("invalid", e.message);
238
+ function brushLibraryGroups(entries, filter = {}) {
239
+ const words = (s) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
240
+ const wanted = words(_nullishCoalesce(filter.text, () => ( "")));
241
+ const groups = /* @__PURE__ */ new Map();
242
+ for (const e of entries) {
243
+ const b = e.brush;
244
+ if (filter.source && filter.source === "builtin" !== e.builtin) continue;
245
+ if (wanted.length) {
246
+ const text = words([b.id, b.name, _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
247
+ if (!wanted.every((w) => text.some((t) => t.startsWith(w)))) continue;
1824
248
  }
1825
- const check2 = checkBrush(brush, profile);
1826
- if (!check2.ok) throw new BrushLibraryError("invalid", refusal(check2, profile));
1827
- const saved = await library.save(brush, { overwrite });
1828
- return { ...saved, problems, check: check2 };
1829
- };
1830
- const list2 = {
1831
- name: "list_brush_presets",
1832
- title: "List brushes",
1833
- description: `Lists the brush library (built-in presets and saved brushes) that this host (${profile.name}) can render, filtered by kind, medium, variety, tool, tag or words. Returns each brush's id (for describe_brush, create_brush basedOn, duplicate_brush and the editor's set_brush), name, builtin, kind, medium, variety or tool, category, engine, tip shape and size, marks, surface and light, plus total. Call it first to find the nearest brush to start from. Changes nothing.`,
1834
- inputSchema: {
1835
- type: "object",
1836
- properties: {
1837
- kind: { type: "string", enum: [...BRUSH_KINDS], description: "mark (lays a material) or tool (works on paint already down)." },
1838
- medium: { type: "string", enum: [...BRUSH_MEDIA], description: "Marks laying this medium." },
1839
- variety: { type: "string", maxLength: 40, description: "This variety (oil, watercolor, pencil, highlighter\u2026)." },
1840
- tool: { type: "string", enum: [...BRUSH_TOOLS], description: "Tools that do this." },
1841
- tag: { type: "string", maxLength: 40, description: "Brushes with this tag." },
1842
- text: { type: "string", maxLength: 200, description: "Words in the id, name, description, variety or tags (each word must match the start of one)." },
1843
- source: { type: "string", enum: ["all", "builtin", "user"], description: "all (default), builtin presets only, or user (saved) brushes only." },
1844
- all: { type: "boolean", description: "Also list brushes this host can't render, each with renderable false (default false)." },
1845
- offset: { type: "integer", minimum: 0, description: "Skip this many (default 0)." },
1846
- limit: { type: "integer", minimum: 1, maximum: 200, description: "At most this many (default 100)." }
1847
- },
1848
- additionalProperties: false
1849
- },
1850
- annotations: { title: "List brushes", ...READ },
1851
- handler: async (a) => {
1852
- const source = a.source === "all" ? void 0 : a.source;
1853
- const found = await library.list({ kind: a.kind, medium: a.medium, variety: a.variety, tool: a.tool, tag: a.tag, text: a.text, source, ...a.all ? {} : { profile } });
1854
- const offset = _nullishCoalesce(a.offset, () => ( 0));
1855
- const page = found.slice(offset, offset + (_nullishCoalesce(a.limit, () => ( 100))));
1856
- return { host: profile.id, total: found.length, brushes: page.map((e) => summary(e, profile, !!a.all)) };
1857
- }
1858
- };
1859
- const describe = {
1860
- name: "describe_brush",
1861
- title: "Describe a brush",
1862
- description: `Reads one brush by id: its full facet spec (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour), whether it is built in, the engine and category it resolves to, and check: what this host (${profile.name}) refuses or paints without, each with a fix. Unknown ids are refused with the closest ones. Changes nothing.`,
1863
- inputSchema: { type: "object", properties: { id: ID }, required: ["id"], additionalProperties: false },
1864
- annotations: { title: "Describe a brush", ...READ },
1865
- handler: async ({ id }) => {
1866
- const e = await entry(id);
1867
- const engine = resolveEngine(e.brush);
1868
- return { brush: e.brush, builtin: e.builtin, engine, category: resolveCategory(e.brush, engine), check: checkBrush(e.brush, profile) };
1869
- }
1870
- };
1871
- const check = {
1872
- name: "check_brush",
1873
- title: "Check a brush for a host",
1874
- description: `Checks a brush (by id, or a facet spec not yet saved) against a host profile: ${HOSTS.join(", ")} (default this host, ${profile.id}). Returns ok (no errors), issues (facet, severity error or warning, reason, fix) and, when it is refused, degraded: the brush with the refused facets stripped and what was dropped. Use it before create_brush, or to see whether a brush works in another editor. Changes nothing.`,
1875
- inputSchema: {
1876
- type: "object",
1877
- properties: {
1878
- id: { ...ID, description: "A brush in the library (give id or brush, not both)." },
1879
- brush: { type: "object", description: "A facet brush spec (the create_brush fields, with id and name)." },
1880
- host: { type: "string", enum: HOSTS, description: `The host profile (default ${profile.id}).` }
1881
- },
1882
- additionalProperties: false
1883
- },
1884
- annotations: { title: "Check a brush for a host", ...READ },
1885
- handler: async ({ id, brush, host }) => {
1886
- if (id === void 0 === (brush === void 0)) throw new BrushLibraryError("invalid", "Give either id (a library brush) or brush (a spec), not both or neither.");
1887
- const p = hostOf(host);
1888
- let b;
1889
- const problems = [];
1890
- if (id !== void 0) b = (await entry(id)).brush;
1891
- else {
1892
- try {
1893
- b = normalizeBrushSpec(brush, problems);
1894
- } catch (e) {
1895
- throw new BrushLibraryError("invalid", e.message);
1896
- }
1897
- }
1898
- const result = checkBrush(b, p);
1899
- return { host: p.id, ...result, ...problems.length ? { problems } : {}, ...!result.ok ? { degraded: degrade(b, p) } : {} };
1900
- }
1901
- };
1902
- const exportTool = {
1903
- name: "export_brushes",
1904
- title: "Export brushes",
1905
- description: "Writes a brush library file (format pixerate-brushes, version 2) of the saved brushes, or of the ids given (built-ins included). Returns the file as JSON, for import_brushes in another library. Changes nothing.",
1906
- inputSchema: { type: "object", properties: { ids: { type: "array", minItems: 1, maxItems: 500, uniqueItems: true, items: ID, description: "Only these brushes (default every saved brush)." } }, additionalProperties: false },
1907
- annotations: { title: "Export brushes", ...READ },
1908
- handler: async ({ ids }) => ({ file: await library.exportFile(ids) })
1909
- };
1910
- const tools = [list2, describe, check, exportTool];
1911
- if (options.renderPreview) {
1912
- const render = options.renderPreview;
1913
- tools.push({
1914
- name: "preview_brush",
1915
- title: "Preview a brush",
1916
- description: "Draws a sample S-stroke with a brush (by id) on a plain background and returns it as an image, to see a brush before painting with it or after create_brush / update_brush. Returns the image plus id, width and height. Changes nothing.",
1917
- inputSchema: {
1918
- type: "object",
1919
- properties: {
1920
- id: ID,
1921
- width: { type: "integer", minimum: 32, maximum: 1024, description: "Image width in px (default 256)." },
1922
- height: { type: "integer", minimum: 16, maximum: 512, description: "Image height in px (default 96)." },
1923
- color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Paint colour #rrggbb (default the brush's own default colour, else #1f2937)." }
1924
- },
1925
- required: ["id"],
1926
- additionalProperties: false
1927
- },
1928
- annotations: { title: "Preview a brush", ...READ },
1929
- handler: async ({ id, width, height, color }) => {
1930
- const { brush } = await entry(id);
1931
- const own = typeof _optionalChain([brush, 'access', _54 => _54.colour, 'optionalAccess', _55 => _55.default]) === "string" && /^#[0-9a-f]{6}/i.test(brush.colour.default) ? brush.colour.default.slice(0, 7) : void 0;
1932
- const w = _nullishCoalesce(width, () => ( 256));
1933
- const h = _nullishCoalesce(height, () => ( 96));
1934
- const image = await render(brush, { width: w, height: h, color: _nullishCoalesce(_nullishCoalesce(color, () => ( own)), () => ( "#1f2937")) });
1935
- return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }], structuredContent: { id, width: w, height: h } };
1936
- }
1937
- });
1938
- }
1939
- if (options.readOnly) return tools;
1940
- tools.push(
1941
- {
1942
- name: "create_brush",
1943
- title: "Create a brush",
1944
- description: `Saves a new brush made of facets, optionally basedOn an existing brush (its facets are copied and the ones given here laid over them: objects merge, lists and values replace). Values are normalised (out-of-range numbers clamped, unused facets dropped, each reported in problems) and checked against this host (${profile.name}): a brush it can't render is refused with the facet and fix, and nothing is saved. Returns the new id, the stored brush, the engine it resolves to, problems and warnings (settings this host paints without). Start from the nearest preset (list_brush_presets) rather than from nothing.`,
1945
- inputSchema: {
1946
- type: "object",
1947
- properties: {
1948
- id: { ...FACETS.id, description: "The new brush id: lowercase letters, digits and dashes, not taken (list_brush_presets)." },
1949
- name: FACETS.name,
1950
- basedOn: { ...ID, description: "Copy this brush's facets first (a built-in preset or a saved brush)." },
1951
- ...FACET_PROPS
1952
- },
1953
- required: ["id", "name"],
1954
- additionalProperties: false
1955
- },
1956
- annotations: { title: "Create a brush", ...WRITE },
1957
- handler: async ({ basedOn, ...spec }) => {
1958
- if (library.isBuiltin(spec.id)) throw new BrushLibraryError("builtin", `"${spec.id}" is a built-in brush id; pick another id (or basedOn "${spec.id}" with a new id).`);
1959
- if (await library.get(spec.id)) throw new BrushLibraryError("exists", `A brush "${spec.id}" exists already; pick another id, or change it with update_brush.`);
1960
- const base = basedOn !== void 0 ? (await entry(basedOn)).brush : void 0;
1961
- const raw = base ? merge(base, spec) : spec;
1962
- const saved = await store(raw, false);
1963
- return { id: saved.brush.id, brush: saved.brush, engine: resolveEngine(saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
1964
- }
1965
- },
1966
- {
1967
- name: "update_brush",
1968
- title: "Update a brush",
1969
- description: `Changes a saved brush: the facets given are laid over its own (objects merge, lists and values replace; marks of another kind replace the marks), and unset removes whole facets back to their defaults. Built-in presets are refused (duplicate_brush first). Checked against this host like create_brush; a refused change saves nothing. One change. Returns the brush, the leaf paths changed, problems and warnings.`,
1970
- inputSchema: {
1971
- type: "object",
1972
- properties: {
1973
- id: { ...ID, description: "The saved brush to change (not a built-in)." },
1974
- name: FACETS.name,
1975
- ...FACET_PROPS,
1976
- unset: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: [...UNSETTABLE], description: "A facet to remove." }, description: "Facets to remove (applied before the facets given)." }
1977
- },
1978
- required: ["id"],
1979
- additionalProperties: false
1980
- },
1981
- annotations: { title: "Update a brush", ...WRITE, idempotentHint: true },
1982
- handler: async ({ id, unset, ...patch }) => {
1983
- const e = await entry(id);
1984
- if (e.builtin) throw new BrushLibraryError("builtin", `"${id}" is a built-in brush and can't be changed; duplicate_brush makes a copy you can change.`);
1985
- const base = copy4(e.brush);
1986
- for (const k of _nullishCoalesce(unset, () => ( []))) delete base[k];
1987
- const saved = await store(merge(base, { ...patch, id }), true);
1988
- return { id, brush: saved.brush, changed: changedPaths(e.brush, saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
1989
- }
1990
- },
1991
- {
1992
- name: "duplicate_brush",
1993
- title: "Duplicate a brush",
1994
- description: "Copies any brush (built-in presets included) to a new saved brush you can change with update_brush. Returns the new id (default <id>-copy, -copy-2\u2026) and the brush.",
1995
- inputSchema: {
1996
- type: "object",
1997
- properties: {
1998
- id: ID,
1999
- newId: { ...FACETS.id, description: "Id for the copy (default <id>-copy, -copy-2\u2026); must not be taken." },
2000
- name: { ...FACETS.name, description: 'Name for the copy (default "<name> copy").' }
2001
- },
2002
- required: ["id"],
2003
- additionalProperties: false
2004
- },
2005
- annotations: { title: "Duplicate a brush", ...WRITE },
2006
- handler: async ({ id, newId, name }) => {
2007
- const brush = await library.duplicate(id, { ...newId !== void 0 ? { id: newId } : {}, ...name !== void 0 ? { name } : {} });
2008
- return { id: brush.id, brush };
2009
- }
2010
- },
2011
- {
2012
- name: "remove_brush",
2013
- title: "Remove a brush",
2014
- description: "Deletes a saved brush from the library for good (export_brushes first to keep a copy). Built-in presets are refused. Returns the id removed.",
2015
- inputSchema: { type: "object", properties: { id: { ...ID, description: "The saved brush to remove (not a built-in)." } }, required: ["id"], additionalProperties: false },
2016
- annotations: { title: "Remove a brush", ...WRITE, destructiveHint: true },
2017
- handler: async ({ id }) => {
2018
- await entry(id);
2019
- await library.remove(id);
2020
- return { removed: id };
2021
- }
2022
- },
2023
- {
2024
- name: "import_brushes",
2025
- title: "Import brushes",
2026
- description: "Adds the brushes of a library file (format pixerate-brushes version 2 from export_brushes, a version 1 file of the paint editor's brushes, which are migrated, or a bare list of brushes). Brushes with a built-in id are skipped, and so are ids already saved unless overwrite. Returns added, replaced and skipped ids and problems (each fix or skip, per brush).",
2027
- inputSchema: {
2028
- type: "object",
2029
- properties: {
2030
- file: { type: ["object", "array"], description: "The library file object, or an array of brushes." },
2031
- overwrite: { type: "boolean", description: "Replace saved brushes with the same id (default false: skip them)." }
2032
- },
2033
- required: ["file"],
2034
- additionalProperties: false
2035
- },
2036
- annotations: { title: "Import brushes", ...WRITE, destructiveHint: true },
2037
- handler: async ({ file, overwrite }) => library.importFile(file, { overwrite: !!overwrite })
2038
- }
2039
- );
2040
- return tools;
2041
- }
2042
- function createBrushLibraryToolProvider(library, options = {}) {
2043
- const profile = _nullishCoalesce(options.profile, () => ( PAINT_PROFILE));
2044
- const tools = createBrushLibraryTools(library, options);
2045
- return {
2046
- id: _nullishCoalesce(options.id, () => ( "brushes")),
2047
- namespace: options.namespace,
2048
- title: "Brush library",
2049
- description: `The shared brush library: find, describe, create, change, check and share brushes made of facets, for ${profile.name}.`,
2050
- skill: "pixerate-brushes",
2051
- tools
2052
- };
249
+ if (filter.profile && !_chunk34KLWUH4cjs.checkBrush.call(void 0, b, filter.profile).ok) continue;
250
+ const id = b.kind === "tool" ? "tools" : _nullishCoalesce(b.medium, () => ( "other"));
251
+ let g = groups.get(id);
252
+ if (!g) groups.set(id, g = { id, title: id === "tools" ? "Tools" : id === "other" ? "Other" : MEDIUM_LABELS[id], varieties: [] });
253
+ const variety = b.kind === "tool" ? "" : _nullishCoalesce(b.variety, () => ( ""));
254
+ let v = g.varieties.find((x) => x.variety === variety);
255
+ if (!v) g.varieties.push(v = { variety, title: variety ? title(variety) : "", entries: [] });
256
+ v.entries.push(e);
257
+ }
258
+ const order = ["dry", "ink", "paint", "spray", "other", "tools"];
259
+ return [...groups.values()].sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
2053
260
  }
2054
261
 
2055
262
 
@@ -2084,4 +291,12 @@ function createBrushLibraryToolProvider(library, options = {}) {
2084
291
 
2085
292
 
2086
293
 
2087
- exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_WORD = BRUSH_WORD; exports.BrushLibraryError = BrushLibraryError; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.PAINT_PROFILE = PAINT_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.createBrushLibrary = createBrushLibrary; exports.createBrushLibraryToolProvider = createBrushLibraryToolProvider; exports.createBrushLibraryTools = createBrushLibraryTools; exports.degrade = degrade; exports.listPresets = listPresets; exports.migrateBrushV1 = migrateBrushV1; exports.normalizeBrushSpec = normalizeBrushSpec; exports.parseBrushLibrary = parseBrushLibrary; exports.resolveBrush = resolveBrush; exports.resolveCategory = resolveCategory; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium;
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+ exports.BRUSH_FACET_SCHEMAS = _chunk34KLWUH4cjs.BRUSH_FACET_SCHEMAS; exports.BRUSH_HOST_PROFILES = _chunk34KLWUH4cjs.BRUSH_HOST_PROFILES; exports.BRUSH_JSON_SCHEMA = _chunk34KLWUH4cjs.BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = _chunk34KLWUH4cjs.BRUSH_KINDS; exports.BRUSH_MARKS_KINDS = _chunk34KLWUH4cjs.BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = _chunk34KLWUH4cjs.BRUSH_MEDIA; exports.BRUSH_PATCH_PROPERTIES = _chunk34KLWUH4cjs.BRUSH_PATCH_PROPERTIES; exports.BRUSH_PRESETS = _chunk34KLWUH4cjs.BRUSH_PRESETS; exports.BRUSH_SURFACES = _chunk34KLWUH4cjs.BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = _chunk34KLWUH4cjs.BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = _chunk34KLWUH4cjs.BRUSH_TOOLS; exports.BRUSH_UNSET_PROPERTY = _chunk34KLWUH4cjs.BRUSH_UNSET_PROPERTY; exports.BRUSH_WORD = _chunk34KLWUH4cjs.BRUSH_WORD; exports.BrushLibraryError = _chunk34KLWUH4cjs.BrushLibraryError; exports.CANVAS_PROFILE = _chunk34KLWUH4cjs.CANVAS_PROFILE; exports.IMAGE_PROFILE = _chunk34KLWUH4cjs.IMAGE_PROFILE; exports.MAX_BRUSH_PALETTE = _chunk34KLWUH4cjs.MAX_BRUSH_PALETTE; exports.PAINT_PROFILE = _chunk34KLWUH4cjs.PAINT_PROFILE; exports.UNSETTABLE_BRUSH_FACETS = _chunk34KLWUH4cjs.UNSETTABLE_BRUSH_FACETS; exports.VECTOR_PROFILE = _chunk34KLWUH4cjs.VECTOR_PROFILE; exports.WET_BRUSH_TOOLS = _chunk34KLWUH4cjs.WET_BRUSH_TOOLS; exports.brushColourKind = _chunk34KLWUH4cjs.brushColourKind; exports.brushEditorSections = brushEditorSections; exports.brushLibraryGroups = brushLibraryGroups; exports.changedBrushPaths = _chunk34KLWUH4cjs.changedBrushPaths; exports.checkBrush = _chunk34KLWUH4cjs.checkBrush; exports.createBrushLibrary = _chunk34KLWUH4cjs.createBrushLibrary; exports.createBrushLibraryToolProvider = _chunk34KLWUH4cjs.createBrushLibraryToolProvider; exports.createBrushLibraryTools = _chunk34KLWUH4cjs.createBrushLibraryTools; exports.degrade = _chunk34KLWUH4cjs.degrade; exports.listPresets = _chunk34KLWUH4cjs.listPresets; exports.mergeBrushPatch = _chunk34KLWUH4cjs.mergeBrushPatch; exports.migrateBrushV1 = _chunk34KLWUH4cjs.migrateBrushV1; exports.normalizeBrushSpec = _chunk34KLWUH4cjs.normalizeBrushSpec; exports.parseBrushLibrary = _chunk34KLWUH4cjs.parseBrushLibrary; exports.resolveBrush = _chunk34KLWUH4cjs.resolveBrush; exports.resolveCategory = _chunk34KLWUH4cjs.resolveCategory; exports.resolveEngine = _chunk34KLWUH4cjs.resolveEngine; exports.resolveWetMedium = _chunk34KLWUH4cjs.resolveWetMedium; exports.setBrushEditorValue = setBrushEditorValue;