@1agh/maude 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/design.mjs +5 -0
- package/cli/lib/design-link.mjs +13 -6
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/gitignore-block.test.mjs +1 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-optimize.mjs +35 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/draw-build.sh +48 -0
- package/plugins/design/dev-server/bin/draw-proof.sh +129 -0
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/bin/svg-optimize.sh +24 -0
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-lib.tsx +110 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/config.schema.json +10 -0
- package/plugins/design/dev-server/context.ts +9 -0
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/draw/brush.ts +639 -0
- package/plugins/design/dev-server/draw/composition.ts +229 -0
- package/plugins/design/dev-server/draw/geometry.ts +578 -0
- package/plugins/design/dev-server/draw/index.ts +28 -0
- package/plugins/design/dev-server/draw/layout.ts +260 -0
- package/plugins/design/dev-server/draw/optimize.ts +65 -0
- package/plugins/design/dev-server/draw/palette.ts +417 -0
- package/plugins/design/dev-server/draw/primitives.ts +643 -0
- package/plugins/design/dev-server/draw/serialize.ts +458 -0
- package/plugins/design/dev-server/draw/test/brush.test.ts +213 -0
- package/plugins/design/dev-server/draw/test/composition.test.ts +141 -0
- package/plugins/design/dev-server/draw/test/geometry.test.ts +199 -0
- package/plugins/design/dev-server/draw/test/gradient.test.ts +167 -0
- package/plugins/design/dev-server/draw/test/layout.test.ts +120 -0
- package/plugins/design/dev-server/draw/test/optimize.test.ts +40 -0
- package/plugins/design/dev-server/draw/test/palette.test.ts +123 -0
- package/plugins/design/dev-server/draw/test/primitives.test.ts +129 -0
- package/plugins/design/dev-server/draw/test/serialize.test.ts +105 -0
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/sync/index.ts +73 -17
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +52 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/tsconfig.json +8 -1
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +15 -0
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file draw/primitives.ts — Phase 25 geometry-engine primitive layer
|
|
3
|
+
* @scope plugins/design/dev-server/draw/primitives.ts
|
|
4
|
+
* @purpose The `DrawPrimitive` union + grid-snapped constructors + transform
|
|
5
|
+
* composition + viewBox presets. This is the "intent" layer the
|
|
6
|
+
* draw-agent emits: the LLM names shapes and where they go; the
|
|
7
|
+
* constructors here produce exact, structured primitives that the
|
|
8
|
+
* serializer (`serialize.ts`) turns into BOTH an SVG string and JSX
|
|
9
|
+
* from the SAME source — so on-disk and on-canvas forms can never
|
|
10
|
+
* drift. This generalizes `canvas-arrowheads.ts`'s `SvgPrimitive`
|
|
11
|
+
* reducer pattern to the full drawing vocabulary.
|
|
12
|
+
*
|
|
13
|
+
* Back-compat note: this is a superset of the arrow `SvgPrimitive`
|
|
14
|
+
* (line / path / polyline / polygon / circle) extended with rect,
|
|
15
|
+
* ellipse, text, group, use, symbol + a shared `DrawStyle`.
|
|
16
|
+
*
|
|
17
|
+
* DEPENDENCY RULE (DDR-067): this module imports NOTHING from react
|
|
18
|
+
* or any `.tsx`. JSX rendering lives ONLY in `serialize.ts#toJsx`,
|
|
19
|
+
* which produces a JSX *string*, never a React element. A `.ts`
|
|
20
|
+
* root in a re-export cycle with a react `.tsx` breaks
|
|
21
|
+
* @types/react's global `JSX` namespace under this tsconfig's
|
|
22
|
+
* `types: ["bun-types"]` — keep the dependency one-way.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A 2-D point in viewBox user units. */
|
|
26
|
+
export interface Point {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Presentation attributes shared by every drawable primitive. Geometry lives on
|
|
33
|
+
* the primitive itself; everything paint-related lives here so a single style
|
|
34
|
+
* shape feeds every element kind. Names are canonical (camelCase); the
|
|
35
|
+
* serializer maps them to `stroke-width` (SVG) vs `strokeWidth` (JSX).
|
|
36
|
+
*
|
|
37
|
+
* `dash` accepts `true` (a sensible default dash pattern), a number array
|
|
38
|
+
* (explicit `stroke-dasharray`), or is omitted for a solid stroke — mirroring
|
|
39
|
+
* the arrow `dash: boolean` field while allowing richer patterns.
|
|
40
|
+
*/
|
|
41
|
+
export interface DrawStyle {
|
|
42
|
+
/** Fill paint. Omit to let the serializer default to `currentColor`. */
|
|
43
|
+
fill?: string;
|
|
44
|
+
/** `'none'` for an unfilled outline shape. */
|
|
45
|
+
stroke?: string;
|
|
46
|
+
strokeWidth?: number;
|
|
47
|
+
strokeLinecap?: 'butt' | 'round' | 'square';
|
|
48
|
+
strokeLinejoin?: 'miter' | 'round' | 'bevel';
|
|
49
|
+
/** Element-wide opacity (0–1). */
|
|
50
|
+
opacity?: number;
|
|
51
|
+
fillOpacity?: number;
|
|
52
|
+
strokeOpacity?: number;
|
|
53
|
+
/** Solid (omitted/false), default dashes (true), or explicit pattern. */
|
|
54
|
+
dash?: boolean | number[];
|
|
55
|
+
/** Optional id (for `<use href>` targets, testing hooks, a11y). */
|
|
56
|
+
id?: string;
|
|
57
|
+
/** `url(#id)` of a {@link filter} (blur / shadow / grain / glow). */
|
|
58
|
+
filter?: string;
|
|
59
|
+
/** `url(#id)` of a {@link mask} (vignette / scrim / fade overlay). */
|
|
60
|
+
mask?: string;
|
|
61
|
+
/** `url(#id)` of a {@link clipPath}. */
|
|
62
|
+
clipPath?: string;
|
|
63
|
+
/** CSS compositing — `'multiply' | 'screen' | 'overlay' | 'soft-light' | …`. */
|
|
64
|
+
mixBlendMode?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `gradientUnits` mode. `objectBoundingBox` (default) uses 0–1 coords. */
|
|
68
|
+
export type GradientUnits = 'objectBoundingBox' | 'userSpaceOnUse';
|
|
69
|
+
|
|
70
|
+
/** One color stop of a gradient. `offset` 0–1; optional per-stop opacity. */
|
|
71
|
+
export interface GradientStop {
|
|
72
|
+
offset: number;
|
|
73
|
+
color: string;
|
|
74
|
+
opacity?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A single SVG filter-primitive node (`feGaussianBlur`, `feTurbulence`,
|
|
79
|
+
* `feDropShadow`, `feColorMatrix`, `feMerge` + `feMergeNode`, …). Open-ended by
|
|
80
|
+
* design — the filter sub-language has dozens of elements, so this is a generic
|
|
81
|
+
* `{ tag, attrs, children }` rather than an enumerated union. Attr keys are
|
|
82
|
+
* camelCase (`stdDeviation`, `baseFrequency`, `floodColor`); the serializer
|
|
83
|
+
* maps the few kebab ones (`flood-color`) per dialect.
|
|
84
|
+
*/
|
|
85
|
+
export interface FePrimitive {
|
|
86
|
+
fe: string;
|
|
87
|
+
attrs?: Record<string, string | number>;
|
|
88
|
+
children?: FePrimitive[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Text-specific attributes layered on top of {@link DrawStyle}. */
|
|
92
|
+
export interface TextAttrs {
|
|
93
|
+
fontSize?: number;
|
|
94
|
+
fontFamily?: string;
|
|
95
|
+
fontWeight?: number | string;
|
|
96
|
+
textAnchor?: 'start' | 'middle' | 'end';
|
|
97
|
+
dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'text-after-edge';
|
|
98
|
+
letterSpacing?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The full drawing vocabulary. Every variant carries `el` as its discriminant
|
|
103
|
+
* (matching the `SvgPrimitive` convention) plus geometry, plus an inlined
|
|
104
|
+
* {@link DrawStyle}. Containers (`group`, `defs`, `symbol`) nest children.
|
|
105
|
+
*/
|
|
106
|
+
export type DrawPrimitive =
|
|
107
|
+
| ({
|
|
108
|
+
el: 'rect';
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
rx?: number;
|
|
114
|
+
ry?: number;
|
|
115
|
+
} & DrawStyle)
|
|
116
|
+
| ({ el: 'circle'; cx: number; cy: number; r: number } & DrawStyle)
|
|
117
|
+
| ({ el: 'ellipse'; cx: number; cy: number; rx: number; ry: number } & DrawStyle)
|
|
118
|
+
| ({ el: 'line'; x1: number; y1: number; x2: number; y2: number } & DrawStyle)
|
|
119
|
+
| ({ el: 'polyline'; points: Point[] } & DrawStyle)
|
|
120
|
+
| ({ el: 'polygon'; points: Point[] } & DrawStyle)
|
|
121
|
+
| ({ el: 'path'; d: string } & DrawStyle)
|
|
122
|
+
| ({ el: 'text'; x: number; y: number; content: string } & TextAttrs & DrawStyle)
|
|
123
|
+
| ({ el: 'group'; children: DrawPrimitive[]; transform?: string } & Pick<
|
|
124
|
+
DrawStyle,
|
|
125
|
+
'opacity' | 'id' | 'filter' | 'mask' | 'clipPath' | 'mixBlendMode'
|
|
126
|
+
>)
|
|
127
|
+
| { el: 'defs'; children: DrawPrimitive[] }
|
|
128
|
+
| { el: 'symbol'; id: string; children: DrawPrimitive[]; viewBox?: string }
|
|
129
|
+
| ({
|
|
130
|
+
el: 'use';
|
|
131
|
+
href: string;
|
|
132
|
+
x?: number;
|
|
133
|
+
y?: number;
|
|
134
|
+
width?: number;
|
|
135
|
+
height?: number;
|
|
136
|
+
transform?: string;
|
|
137
|
+
} & DrawStyle)
|
|
138
|
+
| {
|
|
139
|
+
el: 'linearGradient';
|
|
140
|
+
id: string;
|
|
141
|
+
stops: GradientStop[];
|
|
142
|
+
x1?: number;
|
|
143
|
+
y1?: number;
|
|
144
|
+
x2?: number;
|
|
145
|
+
y2?: number;
|
|
146
|
+
gradientUnits?: GradientUnits;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
el: 'radialGradient';
|
|
150
|
+
id: string;
|
|
151
|
+
stops: GradientStop[];
|
|
152
|
+
cx?: number;
|
|
153
|
+
cy?: number;
|
|
154
|
+
r?: number;
|
|
155
|
+
fx?: number;
|
|
156
|
+
fy?: number;
|
|
157
|
+
gradientUnits?: GradientUnits;
|
|
158
|
+
}
|
|
159
|
+
| {
|
|
160
|
+
el: 'filter';
|
|
161
|
+
id: string;
|
|
162
|
+
prims: FePrimitive[];
|
|
163
|
+
x?: string | number;
|
|
164
|
+
y?: string | number;
|
|
165
|
+
width?: string | number;
|
|
166
|
+
height?: string | number;
|
|
167
|
+
colorInterpolationFilters?: 'sRGB' | 'linearRGB';
|
|
168
|
+
}
|
|
169
|
+
| {
|
|
170
|
+
el: 'pattern';
|
|
171
|
+
id: string;
|
|
172
|
+
width: number;
|
|
173
|
+
height: number;
|
|
174
|
+
children: DrawPrimitive[];
|
|
175
|
+
patternUnits?: GradientUnits;
|
|
176
|
+
patternTransform?: string;
|
|
177
|
+
}
|
|
178
|
+
| { el: 'mask'; id: string; children: DrawPrimitive[] }
|
|
179
|
+
| { el: 'clipPath'; id: string; children: DrawPrimitive[] };
|
|
180
|
+
|
|
181
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
// Grid snapping
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Snap a coordinate to a grid. `grid <= 0` (the default) is a no-op so optical
|
|
187
|
+
* adjustments survive; pass `grid: 1` for pixel-snapping, `4`/`8` for the
|
|
188
|
+
* spacing scale. Rounds half-up, deterministic (no `Math.random`).
|
|
189
|
+
*/
|
|
190
|
+
export function snap(value: number, grid = 0): number {
|
|
191
|
+
if (!grid || grid <= 0) return value;
|
|
192
|
+
return Math.round(value / grid) * grid;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function snapPoint(p: Point, grid: number): Point {
|
|
196
|
+
return { x: snap(p.x, grid), y: snap(p.y, grid) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Constructors — each accepts geometry + style + an optional `grid` to snap.
|
|
201
|
+
// The `grid` field is stripped from the emitted primitive (it's a build-time
|
|
202
|
+
// instruction, not an SVG attribute).
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
type WithGrid<T> = T & { grid?: number };
|
|
206
|
+
|
|
207
|
+
function styleOf<T extends DrawStyle>(o: T): DrawStyle {
|
|
208
|
+
const s: DrawStyle = {};
|
|
209
|
+
if (o.fill !== undefined) s.fill = o.fill;
|
|
210
|
+
if (o.stroke !== undefined) s.stroke = o.stroke;
|
|
211
|
+
if (o.strokeWidth !== undefined) s.strokeWidth = o.strokeWidth;
|
|
212
|
+
if (o.strokeLinecap !== undefined) s.strokeLinecap = o.strokeLinecap;
|
|
213
|
+
if (o.strokeLinejoin !== undefined) s.strokeLinejoin = o.strokeLinejoin;
|
|
214
|
+
if (o.opacity !== undefined) s.opacity = o.opacity;
|
|
215
|
+
if (o.fillOpacity !== undefined) s.fillOpacity = o.fillOpacity;
|
|
216
|
+
if (o.strokeOpacity !== undefined) s.strokeOpacity = o.strokeOpacity;
|
|
217
|
+
if (o.dash !== undefined) s.dash = o.dash;
|
|
218
|
+
if (o.id !== undefined) s.id = o.id;
|
|
219
|
+
if (o.filter !== undefined) s.filter = o.filter;
|
|
220
|
+
if (o.mask !== undefined) s.mask = o.mask;
|
|
221
|
+
if (o.clipPath !== undefined) s.clipPath = o.clipPath;
|
|
222
|
+
if (o.mixBlendMode !== undefined) s.mixBlendMode = o.mixBlendMode;
|
|
223
|
+
return s;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function rect(
|
|
227
|
+
o: WithGrid<
|
|
228
|
+
{ x: number; y: number; width: number; height: number; rx?: number; ry?: number } & DrawStyle
|
|
229
|
+
>
|
|
230
|
+
): DrawPrimitive {
|
|
231
|
+
const g = o.grid ?? 0;
|
|
232
|
+
const out: DrawPrimitive = {
|
|
233
|
+
el: 'rect',
|
|
234
|
+
x: snap(o.x, g),
|
|
235
|
+
y: snap(o.y, g),
|
|
236
|
+
width: snap(o.width, g),
|
|
237
|
+
height: snap(o.height, g),
|
|
238
|
+
...styleOf(o),
|
|
239
|
+
};
|
|
240
|
+
if (o.rx !== undefined) (out as { rx?: number }).rx = snap(o.rx, g);
|
|
241
|
+
if (o.ry !== undefined) (out as { ry?: number }).ry = snap(o.ry, g);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function circle(
|
|
246
|
+
o: WithGrid<{ cx: number; cy: number; r: number } & DrawStyle>
|
|
247
|
+
): DrawPrimitive {
|
|
248
|
+
const g = o.grid ?? 0;
|
|
249
|
+
return { el: 'circle', cx: snap(o.cx, g), cy: snap(o.cy, g), r: snap(o.r, g), ...styleOf(o) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function ellipse(
|
|
253
|
+
o: WithGrid<{ cx: number; cy: number; rx: number; ry: number } & DrawStyle>
|
|
254
|
+
): DrawPrimitive {
|
|
255
|
+
const g = o.grid ?? 0;
|
|
256
|
+
return {
|
|
257
|
+
el: 'ellipse',
|
|
258
|
+
cx: snap(o.cx, g),
|
|
259
|
+
cy: snap(o.cy, g),
|
|
260
|
+
rx: snap(o.rx, g),
|
|
261
|
+
ry: snap(o.ry, g),
|
|
262
|
+
...styleOf(o),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function line(
|
|
267
|
+
o: WithGrid<{ x1: number; y1: number; x2: number; y2: number } & DrawStyle>
|
|
268
|
+
): DrawPrimitive {
|
|
269
|
+
const g = o.grid ?? 0;
|
|
270
|
+
return {
|
|
271
|
+
el: 'line',
|
|
272
|
+
x1: snap(o.x1, g),
|
|
273
|
+
y1: snap(o.y1, g),
|
|
274
|
+
x2: snap(o.x2, g),
|
|
275
|
+
y2: snap(o.y2, g),
|
|
276
|
+
...styleOf(o),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function polyline(o: WithGrid<{ points: Point[] } & DrawStyle>): DrawPrimitive {
|
|
281
|
+
const g = o.grid ?? 0;
|
|
282
|
+
return { el: 'polyline', points: o.points.map((p) => snapPoint(p, g)), ...styleOf(o) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function polygon(o: WithGrid<{ points: Point[] } & DrawStyle>): DrawPrimitive {
|
|
286
|
+
const g = o.grid ?? 0;
|
|
287
|
+
return { el: 'polygon', points: o.points.map((p) => snapPoint(p, g)), ...styleOf(o) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function path(o: { d: string } & DrawStyle): DrawPrimitive {
|
|
291
|
+
return { el: 'path', d: o.d, ...styleOf(o) };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function text(
|
|
295
|
+
o: WithGrid<{ x: number; y: number; content: string } & TextAttrs & DrawStyle>
|
|
296
|
+
): DrawPrimitive {
|
|
297
|
+
const g = o.grid ?? 0;
|
|
298
|
+
const t: DrawPrimitive = {
|
|
299
|
+
el: 'text',
|
|
300
|
+
x: snap(o.x, g),
|
|
301
|
+
y: snap(o.y, g),
|
|
302
|
+
content: o.content,
|
|
303
|
+
...styleOf(o),
|
|
304
|
+
};
|
|
305
|
+
const attrs = t as TextAttrs;
|
|
306
|
+
if (o.fontSize !== undefined) attrs.fontSize = o.fontSize;
|
|
307
|
+
if (o.fontFamily !== undefined) attrs.fontFamily = o.fontFamily;
|
|
308
|
+
if (o.fontWeight !== undefined) attrs.fontWeight = o.fontWeight;
|
|
309
|
+
if (o.textAnchor !== undefined) attrs.textAnchor = o.textAnchor;
|
|
310
|
+
if (o.dominantBaseline !== undefined) attrs.dominantBaseline = o.dominantBaseline;
|
|
311
|
+
if (o.letterSpacing !== undefined) attrs.letterSpacing = o.letterSpacing;
|
|
312
|
+
return t;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function group(
|
|
316
|
+
children: DrawPrimitive[],
|
|
317
|
+
o: {
|
|
318
|
+
transform?: string;
|
|
319
|
+
opacity?: number;
|
|
320
|
+
id?: string;
|
|
321
|
+
filter?: string;
|
|
322
|
+
mask?: string;
|
|
323
|
+
clipPath?: string;
|
|
324
|
+
mixBlendMode?: string;
|
|
325
|
+
} = {}
|
|
326
|
+
): DrawPrimitive {
|
|
327
|
+
const out = { el: 'group' as const, children } as DrawPrimitive & {
|
|
328
|
+
transform?: string;
|
|
329
|
+
opacity?: number;
|
|
330
|
+
id?: string;
|
|
331
|
+
filter?: string;
|
|
332
|
+
mask?: string;
|
|
333
|
+
clipPath?: string;
|
|
334
|
+
mixBlendMode?: string;
|
|
335
|
+
};
|
|
336
|
+
if (o.transform !== undefined) out.transform = o.transform;
|
|
337
|
+
if (o.opacity !== undefined) out.opacity = o.opacity;
|
|
338
|
+
if (o.id !== undefined) out.id = o.id;
|
|
339
|
+
if (o.filter !== undefined) out.filter = o.filter;
|
|
340
|
+
if (o.mask !== undefined) out.mask = o.mask;
|
|
341
|
+
if (o.clipPath !== undefined) out.clipPath = o.clipPath;
|
|
342
|
+
if (o.mixBlendMode !== undefined) out.mixBlendMode = o.mixBlendMode;
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function defs(children: DrawPrimitive[]): DrawPrimitive {
|
|
347
|
+
return { el: 'defs', children };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function symbol(id: string, children: DrawPrimitive[], viewBox?: string): DrawPrimitive {
|
|
351
|
+
const out: DrawPrimitive = { el: 'symbol', id, children };
|
|
352
|
+
if (viewBox !== undefined) (out as { viewBox?: string }).viewBox = viewBox;
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function use(
|
|
357
|
+
o: {
|
|
358
|
+
href: string;
|
|
359
|
+
x?: number;
|
|
360
|
+
y?: number;
|
|
361
|
+
width?: number;
|
|
362
|
+
height?: number;
|
|
363
|
+
transform?: string;
|
|
364
|
+
} & DrawStyle
|
|
365
|
+
): DrawPrimitive {
|
|
366
|
+
const out = { el: 'use' as const, href: o.href, ...styleOf(o) } as DrawPrimitive & {
|
|
367
|
+
x?: number;
|
|
368
|
+
y?: number;
|
|
369
|
+
width?: number;
|
|
370
|
+
height?: number;
|
|
371
|
+
transform?: string;
|
|
372
|
+
};
|
|
373
|
+
if (o.x !== undefined) out.x = o.x;
|
|
374
|
+
if (o.y !== undefined) out.y = o.y;
|
|
375
|
+
if (o.width !== undefined) out.width = o.width;
|
|
376
|
+
if (o.height !== undefined) out.height = o.height;
|
|
377
|
+
if (o.transform !== undefined) out.transform = o.transform;
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A linear gradient definition (place inside `defs(...)` and reference with
|
|
383
|
+
* `fill: 'url(#id)'`). Coords default to a top→bottom sweep in objectBoundingBox
|
|
384
|
+
* space — exactly what a sky/backdrop wants.
|
|
385
|
+
*/
|
|
386
|
+
export function linearGradient(o: {
|
|
387
|
+
id: string;
|
|
388
|
+
stops: GradientStop[];
|
|
389
|
+
x1?: number;
|
|
390
|
+
y1?: number;
|
|
391
|
+
x2?: number;
|
|
392
|
+
y2?: number;
|
|
393
|
+
gradientUnits?: GradientUnits;
|
|
394
|
+
}): DrawPrimitive {
|
|
395
|
+
const g = { el: 'linearGradient' as const, id: o.id, stops: o.stops };
|
|
396
|
+
const out = g as DrawPrimitive & {
|
|
397
|
+
x1?: number;
|
|
398
|
+
y1?: number;
|
|
399
|
+
x2?: number;
|
|
400
|
+
y2?: number;
|
|
401
|
+
gradientUnits?: GradientUnits;
|
|
402
|
+
};
|
|
403
|
+
if (o.x1 !== undefined) out.x1 = o.x1;
|
|
404
|
+
if (o.y1 !== undefined) out.y1 = o.y1;
|
|
405
|
+
if (o.x2 !== undefined) out.x2 = o.x2;
|
|
406
|
+
if (o.y2 !== undefined) out.y2 = o.y2;
|
|
407
|
+
if (o.gradientUnits !== undefined) out.gradientUnits = o.gradientUnits;
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** A radial gradient definition (e.g. a sun/glow). Reference with `fill: 'url(#id)'`. */
|
|
412
|
+
export function radialGradient(o: {
|
|
413
|
+
id: string;
|
|
414
|
+
stops: GradientStop[];
|
|
415
|
+
cx?: number;
|
|
416
|
+
cy?: number;
|
|
417
|
+
r?: number;
|
|
418
|
+
fx?: number;
|
|
419
|
+
fy?: number;
|
|
420
|
+
gradientUnits?: GradientUnits;
|
|
421
|
+
}): DrawPrimitive {
|
|
422
|
+
const g = { el: 'radialGradient' as const, id: o.id, stops: o.stops };
|
|
423
|
+
const out = g as DrawPrimitive & {
|
|
424
|
+
cx?: number;
|
|
425
|
+
cy?: number;
|
|
426
|
+
r?: number;
|
|
427
|
+
fx?: number;
|
|
428
|
+
fy?: number;
|
|
429
|
+
gradientUnits?: GradientUnits;
|
|
430
|
+
};
|
|
431
|
+
if (o.cx !== undefined) out.cx = o.cx;
|
|
432
|
+
if (o.cy !== undefined) out.cy = o.cy;
|
|
433
|
+
if (o.r !== undefined) out.r = o.r;
|
|
434
|
+
if (o.fx !== undefined) out.fx = o.fx;
|
|
435
|
+
if (o.fy !== undefined) out.fy = o.fy;
|
|
436
|
+
if (o.gradientUnits !== undefined) out.gradientUnits = o.gradientUnits;
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
// Filters / patterns / masks / clips — the rest of the legit design toolkit.
|
|
442
|
+
// Place each inside `defs(...)` and reference it from a style field:
|
|
443
|
+
// filter → `filter: 'url(#id)'` · pattern → `fill: 'url(#id)'`
|
|
444
|
+
// mask → `mask: 'url(#id)'` · clipPath → `clipPath: 'url(#id)'`
|
|
445
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
/** A generic filter primitive — `fe('feGaussianBlur', { stdDeviation: 4 })`. */
|
|
448
|
+
export function fe(
|
|
449
|
+
tag: string,
|
|
450
|
+
attrs?: Record<string, string | number>,
|
|
451
|
+
children?: FePrimitive[]
|
|
452
|
+
): FePrimitive {
|
|
453
|
+
const out: FePrimitive = { fe: tag };
|
|
454
|
+
if (attrs) out.attrs = attrs;
|
|
455
|
+
if (children) out.children = children;
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A `<filter>` def assembled from filter primitives (build them with {@link fe}). */
|
|
460
|
+
export function filter(
|
|
461
|
+
id: string,
|
|
462
|
+
prims: FePrimitive[],
|
|
463
|
+
region?: {
|
|
464
|
+
x?: string | number;
|
|
465
|
+
y?: string | number;
|
|
466
|
+
width?: string | number;
|
|
467
|
+
height?: string | number;
|
|
468
|
+
colorInterpolationFilters?: 'sRGB' | 'linearRGB';
|
|
469
|
+
}
|
|
470
|
+
): DrawPrimitive {
|
|
471
|
+
const out = { el: 'filter' as const, id, prims } as DrawPrimitive & {
|
|
472
|
+
x?: string | number;
|
|
473
|
+
y?: string | number;
|
|
474
|
+
width?: string | number;
|
|
475
|
+
height?: string | number;
|
|
476
|
+
colorInterpolationFilters?: 'sRGB' | 'linearRGB';
|
|
477
|
+
};
|
|
478
|
+
if (region?.x !== undefined) out.x = region.x;
|
|
479
|
+
if (region?.y !== undefined) out.y = region.y;
|
|
480
|
+
if (region?.width !== undefined) out.width = region.width;
|
|
481
|
+
if (region?.height !== undefined) out.height = region.height;
|
|
482
|
+
if (region?.colorInterpolationFilters !== undefined)
|
|
483
|
+
out.colorInterpolationFilters = region.colorInterpolationFilters;
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Convenience: a Gaussian-blur filter. */
|
|
488
|
+
export function blurFilter(id: string, stdDeviation: number): DrawPrimitive {
|
|
489
|
+
return filter(id, [fe('feGaussianBlur', { in: 'SourceGraphic', stdDeviation })]);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Convenience: a soft drop-shadow filter (a generous filter region by default). */
|
|
493
|
+
export function dropShadowFilter(
|
|
494
|
+
id: string,
|
|
495
|
+
o: { dx?: number; dy?: number; blur?: number; color?: string; opacity?: number } = {}
|
|
496
|
+
): DrawPrimitive {
|
|
497
|
+
const attrs: Record<string, string | number> = {
|
|
498
|
+
dx: o.dx ?? 0,
|
|
499
|
+
dy: o.dy ?? 2,
|
|
500
|
+
stdDeviation: o.blur ?? 3,
|
|
501
|
+
floodColor: o.color ?? '#000000',
|
|
502
|
+
floodOpacity: o.opacity ?? 0.3,
|
|
503
|
+
};
|
|
504
|
+
return filter(id, [fe('feDropShadow', attrs)], {
|
|
505
|
+
x: '-30%',
|
|
506
|
+
y: '-30%',
|
|
507
|
+
width: '160%',
|
|
508
|
+
height: '160%',
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Convenience: a film-grain / noise overlay filter (feTurbulence → desaturate →
|
|
514
|
+
* fade). Apply to a full-bleed rect with a low opacity + `mix-blend-mode` for a
|
|
515
|
+
* tasteful textured overlay.
|
|
516
|
+
*/
|
|
517
|
+
export function grainFilter(
|
|
518
|
+
id: string,
|
|
519
|
+
o: { frequency?: number; octaves?: number; opacity?: number } = {}
|
|
520
|
+
): DrawPrimitive {
|
|
521
|
+
const freq = o.frequency ?? 0.9;
|
|
522
|
+
const oct = o.octaves ?? 2;
|
|
523
|
+
const op = o.opacity ?? 0.5;
|
|
524
|
+
return filter(id, [
|
|
525
|
+
fe('feTurbulence', {
|
|
526
|
+
type: 'fractalNoise',
|
|
527
|
+
baseFrequency: freq,
|
|
528
|
+
numOctaves: oct,
|
|
529
|
+
stitchTiles: 'stitch',
|
|
530
|
+
result: 'noise',
|
|
531
|
+
}),
|
|
532
|
+
fe('feColorMatrix', { in: 'noise', type: 'saturate', values: 0 }),
|
|
533
|
+
fe('feComponentTransfer', undefined, [fe('feFuncA', { type: 'linear', slope: op })]),
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** A tiled `<pattern>` def. Build the tile from any primitives (dots, stripes, grid). */
|
|
538
|
+
export function pattern(o: {
|
|
539
|
+
id: string;
|
|
540
|
+
width: number;
|
|
541
|
+
height: number;
|
|
542
|
+
children: DrawPrimitive[];
|
|
543
|
+
patternUnits?: GradientUnits;
|
|
544
|
+
patternTransform?: string;
|
|
545
|
+
}): DrawPrimitive {
|
|
546
|
+
const out = {
|
|
547
|
+
el: 'pattern' as const,
|
|
548
|
+
id: o.id,
|
|
549
|
+
width: o.width,
|
|
550
|
+
height: o.height,
|
|
551
|
+
children: o.children,
|
|
552
|
+
} as DrawPrimitive & { patternUnits?: GradientUnits; patternTransform?: string };
|
|
553
|
+
if (o.patternUnits !== undefined) out.patternUnits = o.patternUnits;
|
|
554
|
+
if (o.patternTransform !== undefined) out.patternTransform = o.patternTransform;
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** A `<mask>` def (white shows, black hides) — vignettes, edge fades, scrims. */
|
|
559
|
+
export function mask(id: string, children: DrawPrimitive[]): DrawPrimitive {
|
|
560
|
+
return { el: 'mask', id, children };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** A `<clipPath>` def. */
|
|
564
|
+
export function clipPath(id: string, children: DrawPrimitive[]): DrawPrimitive {
|
|
565
|
+
return { el: 'clipPath', id, children };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
569
|
+
// Transform composition
|
|
570
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
export interface PlaceOpts {
|
|
573
|
+
x?: number;
|
|
574
|
+
y?: number;
|
|
575
|
+
scale?: number;
|
|
576
|
+
/** Degrees, clockwise (SVG convention). */
|
|
577
|
+
rotate?: number;
|
|
578
|
+
/** Rotation pivot (defaults to the local origin). */
|
|
579
|
+
originX?: number;
|
|
580
|
+
originY?: number;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Round transform numbers to avoid `0.30000000000000004`-style noise. */
|
|
584
|
+
function fmt(n: number): string {
|
|
585
|
+
return Number.isInteger(n) ? String(n) : String(Math.round(n * 1e4) / 1e4);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Build an SVG `transform` string in the canonical order
|
|
590
|
+
* `translate → rotate → scale`. Each part is emitted only when it's non-identity
|
|
591
|
+
* so trivial placements stay clean. Deterministic; no floating noise.
|
|
592
|
+
*/
|
|
593
|
+
export function transformString(o: PlaceOpts): string {
|
|
594
|
+
const parts: string[] = [];
|
|
595
|
+
const x = o.x ?? 0;
|
|
596
|
+
const y = o.y ?? 0;
|
|
597
|
+
if (x !== 0 || y !== 0) parts.push(`translate(${fmt(x)} ${fmt(y)})`);
|
|
598
|
+
if (o.rotate) {
|
|
599
|
+
if (o.originX !== undefined || o.originY !== undefined) {
|
|
600
|
+
parts.push(`rotate(${fmt(o.rotate)} ${fmt(o.originX ?? 0)} ${fmt(o.originY ?? 0)})`);
|
|
601
|
+
} else {
|
|
602
|
+
parts.push(`rotate(${fmt(o.rotate)})`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const s = o.scale ?? 1;
|
|
606
|
+
if (s !== 1) parts.push(`scale(${fmt(s)})`);
|
|
607
|
+
return parts.join(' ');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Compose a sub-drawing by wrapping it in a `<g transform=…>`. The single
|
|
612
|
+
* highest-leverage composition primitive: parts are authored once at the origin
|
|
613
|
+
* and *placed* — no manual coordinate arithmetic, which is exactly the LLM
|
|
614
|
+
* failure mode (coordinate drift) the engine removes.
|
|
615
|
+
*/
|
|
616
|
+
export function place(part: DrawPrimitive[], opts: PlaceOpts): DrawPrimitive {
|
|
617
|
+
return group(part, { transform: transformString(opts) });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
621
|
+
// viewBox presets
|
|
622
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
/** A canonical square viewBox `0 0 n n`. */
|
|
625
|
+
export function squareViewBox(n: number): string {
|
|
626
|
+
return `0 0 ${n} ${n}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** An arbitrary `0 0 w h` viewBox. */
|
|
630
|
+
export function boxViewBox(w: number, h: number): string {
|
|
631
|
+
return `0 0 ${w} ${h}`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Common viewBox presets. `icon` = the 24-grid Material/Lucide standard;
|
|
636
|
+
* `iconLg` = 48-grid; `logo` = 64-grid lockup; `social` = wide marketing card.
|
|
637
|
+
*/
|
|
638
|
+
export const VIEWBOX = {
|
|
639
|
+
icon: squareViewBox(24),
|
|
640
|
+
iconLg: squareViewBox(48),
|
|
641
|
+
logo: squareViewBox(64),
|
|
642
|
+
social: boxViewBox(1200, 630),
|
|
643
|
+
} as const;
|