@grida/svg-editor 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -1,461 +0,0 @@
1
- import { SVGPathData, SVGPathDataTransformer } from "svg-pathdata";
2
- //#region src/core/intents.ts
3
- function num(doc, id, name, fallback = 0) {
4
- const v = doc.get_attr(id, name);
5
- if (v === null || v === "") return fallback;
6
- const n = parseFloat(v);
7
- return Number.isFinite(n) ? n : fallback;
8
- }
9
- function capture_translate_baseline(doc, id) {
10
- const tag = doc.tag_of(id);
11
- const own_transform = doc.get_attr(id, "transform");
12
- if (own_transform !== null || tag === "g") return {
13
- type: "viaTransform",
14
- transform: own_transform
15
- };
16
- switch (tag) {
17
- case "rect": return {
18
- type: "rect",
19
- x: num(doc, id, "x"),
20
- y: num(doc, id, "y")
21
- };
22
- case "circle": return {
23
- type: "circle",
24
- cx: num(doc, id, "cx"),
25
- cy: num(doc, id, "cy")
26
- };
27
- case "ellipse": return {
28
- type: "ellipse",
29
- cx: num(doc, id, "cx"),
30
- cy: num(doc, id, "cy")
31
- };
32
- case "line": return {
33
- type: "line",
34
- x1: num(doc, id, "x1"),
35
- y1: num(doc, id, "y1"),
36
- x2: num(doc, id, "x2"),
37
- y2: num(doc, id, "y2")
38
- };
39
- case "polyline": return {
40
- type: "polyline",
41
- points: doc.get_attr(id, "points") ?? ""
42
- };
43
- case "polygon": return {
44
- type: "polygon",
45
- points: doc.get_attr(id, "points") ?? ""
46
- };
47
- case "path": return {
48
- type: "path",
49
- d: doc.get_attr(id, "d") ?? ""
50
- };
51
- case "text": return {
52
- type: "text",
53
- x: num(doc, id, "x"),
54
- y: num(doc, id, "y")
55
- };
56
- case "tspan": return {
57
- type: "tspan",
58
- x: num(doc, id, "x"),
59
- y: num(doc, id, "y")
60
- };
61
- case "image": return {
62
- type: "image",
63
- x: num(doc, id, "x"),
64
- y: num(doc, id, "y")
65
- };
66
- case "use": return {
67
- type: "use",
68
- x: num(doc, id, "x"),
69
- y: num(doc, id, "y")
70
- };
71
- default: return { type: "unsupported" };
72
- }
73
- }
74
- function shift_points_string(points, dx, dy) {
75
- const nums = points.split(/[\s,]+/).filter(Boolean).map(Number);
76
- const out = [];
77
- for (let i = 0; i + 1 < nums.length; i += 2) out.push(`${nums[i] + dx},${nums[i + 1] + dy}`);
78
- return out.join(" ");
79
- }
80
- function compose_leading_translate(existing, dx, dy) {
81
- if (dx === 0 && dy === 0) return existing ? existing : null;
82
- if (!existing) return `translate(${dx} ${dy})`;
83
- const N = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?";
84
- const re = new RegExp(`^\\s*translate\\(\\s*(${N})(?:\\s*,\\s*|\\s+)(${N})\\s*\\)\\s*(.*)$`);
85
- const m = existing.match(re);
86
- if (m) {
87
- const tx = parseFloat(m[1]) + dx;
88
- const ty = parseFloat(m[2]) + dy;
89
- const rest = m[3].trim();
90
- return rest ? `translate(${tx} ${ty}) ${rest}` : `translate(${tx} ${ty})`;
91
- }
92
- return `translate(${dx} ${dy}) ${existing}`;
93
- }
94
- function shift_path_d(d, dx, dy) {
95
- if (dx === 0 && dy === 0) return d;
96
- try {
97
- return new SVGPathData(d).transform(SVGPathDataTransformer.TRANSLATE(dx, dy)).encode();
98
- } catch {
99
- return d;
100
- }
101
- }
102
- function apply_translate(doc, id, baseline, dx, dy) {
103
- switch (baseline.type) {
104
- case "viaTransform":
105
- doc.set_attr(id, "transform", compose_leading_translate(baseline.transform ?? "", dx, dy));
106
- return;
107
- case "rect":
108
- case "image":
109
- case "use":
110
- case "text":
111
- case "tspan":
112
- doc.set_attr(id, "x", String(baseline.x + dx));
113
- doc.set_attr(id, "y", String(baseline.y + dy));
114
- return;
115
- case "circle":
116
- case "ellipse":
117
- doc.set_attr(id, "cx", String(baseline.cx + dx));
118
- doc.set_attr(id, "cy", String(baseline.cy + dy));
119
- return;
120
- case "line":
121
- doc.set_attr(id, "x1", String(baseline.x1 + dx));
122
- doc.set_attr(id, "y1", String(baseline.y1 + dy));
123
- doc.set_attr(id, "x2", String(baseline.x2 + dx));
124
- doc.set_attr(id, "y2", String(baseline.y2 + dy));
125
- return;
126
- case "polyline":
127
- case "polygon":
128
- doc.set_attr(id, "points", shift_points_string(baseline.points, dx, dy));
129
- return;
130
- case "path":
131
- doc.set_attr(id, "d", shift_path_d(baseline.d, dx, dy));
132
- return;
133
- case "unsupported": return;
134
- }
135
- }
136
- function is_resizable(tag) {
137
- switch (tag) {
138
- case "rect":
139
- case "image":
140
- case "use":
141
- case "circle":
142
- case "ellipse":
143
- case "line":
144
- case "polyline":
145
- case "polygon":
146
- case "path":
147
- case "text": return true;
148
- default: return false;
149
- }
150
- }
151
- function capture_resize_baseline(doc, id, bbox) {
152
- const tag = doc.tag_of(id);
153
- let attrs;
154
- switch (tag) {
155
- case "rect":
156
- attrs = {
157
- kind: "rect",
158
- x: num(doc, id, "x"),
159
- y: num(doc, id, "y"),
160
- w: num(doc, id, "width", bbox.width),
161
- h: num(doc, id, "height", bbox.height)
162
- };
163
- break;
164
- case "image":
165
- attrs = {
166
- kind: "image",
167
- x: num(doc, id, "x"),
168
- y: num(doc, id, "y"),
169
- w: num(doc, id, "width", bbox.width),
170
- h: num(doc, id, "height", bbox.height)
171
- };
172
- break;
173
- case "use":
174
- attrs = {
175
- kind: "use",
176
- x: num(doc, id, "x"),
177
- y: num(doc, id, "y"),
178
- w: num(doc, id, "width", bbox.width),
179
- h: num(doc, id, "height", bbox.height)
180
- };
181
- break;
182
- case "circle":
183
- attrs = {
184
- kind: "circle",
185
- cx: num(doc, id, "cx"),
186
- cy: num(doc, id, "cy"),
187
- r: num(doc, id, "r")
188
- };
189
- break;
190
- case "ellipse":
191
- attrs = {
192
- kind: "ellipse",
193
- cx: num(doc, id, "cx"),
194
- cy: num(doc, id, "cy"),
195
- rx: num(doc, id, "rx"),
196
- ry: num(doc, id, "ry")
197
- };
198
- break;
199
- case "line":
200
- attrs = {
201
- kind: "line",
202
- x1: num(doc, id, "x1"),
203
- y1: num(doc, id, "y1"),
204
- x2: num(doc, id, "x2"),
205
- y2: num(doc, id, "y2")
206
- };
207
- break;
208
- case "polyline":
209
- attrs = {
210
- kind: "polyline",
211
- points: doc.get_attr(id, "points") ?? ""
212
- };
213
- break;
214
- case "polygon":
215
- attrs = {
216
- kind: "polygon",
217
- points: doc.get_attr(id, "points") ?? ""
218
- };
219
- break;
220
- case "path":
221
- attrs = {
222
- kind: "path",
223
- d: doc.get_attr(id, "d") ?? ""
224
- };
225
- break;
226
- case "text":
227
- attrs = {
228
- kind: "text",
229
- x: num(doc, id, "x"),
230
- y: num(doc, id, "y"),
231
- fontSize: parseFloat(doc.get_attr(id, "font-size") ?? "16") || 16
232
- };
233
- break;
234
- default: attrs = { kind: "unsupported" };
235
- }
236
- return {
237
- bbox,
238
- attrs
239
- };
240
- }
241
- function compute_resize_factors(baseline, dir, dx, dy, shift) {
242
- const b = baseline.bbox;
243
- let anchorX = 0;
244
- let anchorY = 0;
245
- let baseHX = 0;
246
- let baseHY = 0;
247
- let affectsX = true;
248
- let affectsY = true;
249
- switch (dir) {
250
- case "nw":
251
- anchorX = b.x + b.width;
252
- anchorY = b.y + b.height;
253
- baseHX = b.x;
254
- baseHY = b.y;
255
- break;
256
- case "n":
257
- anchorX = b.x + b.width / 2;
258
- anchorY = b.y + b.height;
259
- baseHX = b.x + b.width / 2;
260
- baseHY = b.y;
261
- affectsX = false;
262
- break;
263
- case "ne":
264
- anchorX = b.x;
265
- anchorY = b.y + b.height;
266
- baseHX = b.x + b.width;
267
- baseHY = b.y;
268
- break;
269
- case "e":
270
- anchorX = b.x;
271
- anchorY = b.y + b.height / 2;
272
- baseHX = b.x + b.width;
273
- baseHY = b.y + b.height / 2;
274
- affectsY = false;
275
- break;
276
- case "se":
277
- anchorX = b.x;
278
- anchorY = b.y;
279
- baseHX = b.x + b.width;
280
- baseHY = b.y + b.height;
281
- break;
282
- case "s":
283
- anchorX = b.x + b.width / 2;
284
- anchorY = b.y;
285
- baseHX = b.x + b.width / 2;
286
- baseHY = b.y + b.height;
287
- affectsX = false;
288
- break;
289
- case "sw":
290
- anchorX = b.x + b.width;
291
- anchorY = b.y;
292
- baseHX = b.x;
293
- baseHY = b.y + b.height;
294
- break;
295
- case "w":
296
- anchorX = b.x + b.width;
297
- anchorY = b.y + b.height / 2;
298
- baseHX = b.x;
299
- baseHY = b.y + b.height / 2;
300
- affectsY = false;
301
- break;
302
- }
303
- const newHX = baseHX + (affectsX ? dx : 0);
304
- const newHY = baseHY + (affectsY ? dy : 0);
305
- const denomX = baseHX - anchorX;
306
- const denomY = baseHY - anchorY;
307
- let sx = affectsX && denomX !== 0 ? (newHX - anchorX) / denomX : 1;
308
- let sy = affectsY && denomY !== 0 ? (newHY - anchorY) / denomY : 1;
309
- if (shift && affectsX && affectsY) {
310
- const mag = Math.max(Math.abs(sx), Math.abs(sy));
311
- sx = sx >= 0 ? mag : -mag;
312
- sy = sy >= 0 ? mag : -mag;
313
- }
314
- sx = Math.max(.001, sx);
315
- sy = Math.max(.001, sy);
316
- return {
317
- sx,
318
- sy,
319
- origin: {
320
- x: anchorX,
321
- y: anchorY
322
- }
323
- };
324
- }
325
- function scale_points_string(points, origin, sx, sy) {
326
- const nums = points.split(/[\s,]+/).filter(Boolean).map(Number);
327
- const out = [];
328
- for (let i = 0; i + 1 < nums.length; i += 2) {
329
- const x = origin.x + (nums[i] - origin.x) * sx;
330
- const y = origin.y + (nums[i + 1] - origin.y) * sy;
331
- out.push(`${x},${y}`);
332
- }
333
- return out.join(" ");
334
- }
335
- function scale_path_d(d, origin, sx, sy) {
336
- try {
337
- const e = origin.x * (1 - sx);
338
- const f = origin.y * (1 - sy);
339
- return new SVGPathData(d).transform(SVGPathDataTransformer.MATRIX(sx, 0, 0, sy, e, f)).encode();
340
- } catch {
341
- return d;
342
- }
343
- }
344
- function apply_resize(doc, id, baseline, sx, sy, origin) {
345
- const a = baseline.attrs;
346
- switch (a.kind) {
347
- case "rect":
348
- case "image":
349
- case "use":
350
- doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * sx));
351
- doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * sy));
352
- doc.set_attr(id, "width", String(Math.max(.001, a.w * sx)));
353
- doc.set_attr(id, "height", String(Math.max(.001, a.h * sy)));
354
- return;
355
- case "circle": {
356
- const s = Math.min(sx, sy);
357
- doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * s));
358
- doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * s));
359
- doc.set_attr(id, "r", String(Math.max(.001, a.r * s)));
360
- return;
361
- }
362
- case "ellipse":
363
- doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * sx));
364
- doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * sy));
365
- doc.set_attr(id, "rx", String(Math.max(.001, a.rx * sx)));
366
- doc.set_attr(id, "ry", String(Math.max(.001, a.ry * sy)));
367
- return;
368
- case "line":
369
- doc.set_attr(id, "x1", String(origin.x + (a.x1 - origin.x) * sx));
370
- doc.set_attr(id, "y1", String(origin.y + (a.y1 - origin.y) * sy));
371
- doc.set_attr(id, "x2", String(origin.x + (a.x2 - origin.x) * sx));
372
- doc.set_attr(id, "y2", String(origin.y + (a.y2 - origin.y) * sy));
373
- return;
374
- case "polyline":
375
- case "polygon":
376
- doc.set_attr(id, "points", scale_points_string(a.points, origin, sx, sy));
377
- return;
378
- case "path":
379
- doc.set_attr(id, "d", scale_path_d(a.d, origin, sx, sy));
380
- return;
381
- case "text": {
382
- if (!(sx !== 1 && sy !== 1)) return;
383
- const s = Math.min(sx, sy);
384
- doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * s));
385
- doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * s));
386
- doc.set_attr(id, "font-size", String(Math.max(1, a.fontSize * s)));
387
- return;
388
- }
389
- case "unsupported": return;
390
- }
391
- }
392
- //#endregion
393
- //#region src/core/paint.ts
394
- /**
395
- * Parse a *computed* paint string into the discriminated union. Returns null
396
- * for `inherit` / `var()` / empty. Returns an invalid-computed-value record
397
- * for syntactic errors (rare; we're permissive).
398
- */
399
- function parse_paint(declared) {
400
- if (declared === null || declared === "") return null;
401
- const trimmed = declared.trim();
402
- if (trimmed === "") return null;
403
- if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
404
- if (/^var\s*\(/i.test(trimmed)) return {
405
- error: "invalid_at_computed_value_time",
406
- reason: "var() substitution requires a cascade engine (not implemented)"
407
- };
408
- if (trimmed === "none") return { kind: "none" };
409
- if (trimmed === "context-fill" || trimmed === "contextFill") return { kind: "context_fill" };
410
- if (trimmed === "context-stroke" || trimmed === "contextStroke") return { kind: "context_stroke" };
411
- const url_match = trimmed.match(/^url\(\s*(["']?)#([^)"']+)\1\s*\)\s*(.*)$/i);
412
- if (url_match) {
413
- const id = url_match[2];
414
- const rest = url_match[3].trim();
415
- let fallback;
416
- if (rest !== "") {
417
- const f = parse_paint(rest);
418
- if (f && f.kind === "none") fallback = { kind: "none" };
419
- else if (f && f.kind === "color") fallback = {
420
- kind: "color",
421
- value: f.value
422
- };
423
- }
424
- return fallback ? {
425
- kind: "ref",
426
- id,
427
- fallback
428
- } : {
429
- kind: "ref",
430
- id
431
- };
432
- }
433
- if (/^currentcolor$/i.test(trimmed)) return {
434
- kind: "color",
435
- value: { kind: "current_color" }
436
- };
437
- return {
438
- kind: "color",
439
- value: {
440
- kind: "rgb",
441
- value: trimmed
442
- }
443
- };
444
- }
445
- /** Serialize a Paint back to an SVG attribute / inline-style value. */
446
- function serialize_paint(paint) {
447
- switch (paint.kind) {
448
- case "none": return "none";
449
- case "context_fill": return "context-fill";
450
- case "context_stroke": return "context-stroke";
451
- case "color": return paint.value.kind === "current_color" ? "currentColor" : paint.value.value;
452
- case "ref":
453
- if (paint.fallback) {
454
- const f = paint.fallback.kind === "none" ? "none" : paint.fallback.value.kind === "current_color" ? "currentColor" : paint.fallback.value.value;
455
- return `url(#${paint.id}) ${f}`;
456
- }
457
- return `url(#${paint.id})`;
458
- }
459
- }
460
- //#endregion
461
- export { capture_resize_baseline as a, is_resizable as c, apply_translate as i, serialize_paint as n, capture_translate_baseline as o, apply_resize as r, compute_resize_factors as s, parse_paint as t };