js.documents 2.0.0 → 2.2.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/README.md +49 -29
- package/dist/codecs/registry.cjs +14 -0
- package/dist/codecs/registry.js +14 -0
- package/dist/convert/capability.cjs +10 -0
- package/dist/convert/capability.js +10 -0
- package/dist/convert/codec.cjs +30 -0
- package/dist/convert/codec.d.cts +7 -1
- package/dist/convert/codec.d.ts +7 -1
- package/dist/convert/codec.js +27 -3
- package/dist/convert/composition.cjs +49 -7
- package/dist/convert/composition.d.cts +12 -5
- package/dist/convert/composition.d.ts +12 -5
- package/dist/convert/composition.js +49 -7
- package/dist/convert/convert.cjs +48 -0
- package/dist/convert/convert.d.cts +30 -1
- package/dist/convert/convert.d.ts +30 -1
- package/dist/convert/convert.js +37 -1
- package/dist/convert/local.cjs +4 -1
- package/dist/convert/local.js +4 -1
- package/dist/convert/port.cjs +2 -0
- package/dist/convert/port.d.cts +5 -0
- package/dist/convert/port.d.ts +5 -0
- package/dist/convert/port.js +2 -0
- package/dist/csv/read.cjs +85 -0
- package/dist/csv/read.d.cts +10 -0
- package/dist/csv/read.d.ts +10 -0
- package/dist/csv/read.js +84 -0
- package/dist/csv/records.cjs +85 -0
- package/dist/csv/records.d.cts +10 -0
- package/dist/csv/records.d.ts +10 -0
- package/dist/csv/records.js +80 -0
- package/dist/csv/text.cjs +22 -0
- package/dist/csv/text.d.cts +8 -0
- package/dist/csv/text.d.ts +8 -0
- package/dist/csv/text.js +19 -0
- package/dist/csv/write.cjs +75 -0
- package/dist/csv/write.d.cts +22 -0
- package/dist/csv/write.d.ts +22 -0
- package/dist/csv/write.js +71 -0
- package/dist/edit/odg/scaffold.js +8 -8
- package/dist/edit/odp/scaffold.js +8 -8
- package/dist/edit/ods/scaffold.js +8 -8
- package/dist/edit/odt/scaffold.js +8 -8
- package/dist/index.cjs +48 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +13 -5
- package/dist/layout/reconstruct.cjs +1 -1
- package/dist/layout/reconstruct.js +1 -1
- package/dist/metadata/write.cjs +2 -0
- package/dist/metadata/write.js +2 -0
- package/dist/model/bytes.cjs +7 -0
- package/dist/model/bytes.d.cts +3 -1
- package/dist/model/bytes.d.ts +3 -1
- package/dist/model/bytes.js +6 -1
- package/dist/odb/csv.cjs +3 -6
- package/dist/odb/csv.js +3 -6
- package/dist/svg/diagnostics.cjs +19 -0
- package/dist/svg/diagnostics.d.cts +10 -0
- package/dist/svg/diagnostics.d.ts +10 -0
- package/dist/svg/diagnostics.js +18 -0
- package/dist/svg/paint.cjs +817 -0
- package/dist/svg/paint.d.cts +19 -0
- package/dist/svg/paint.d.ts +19 -0
- package/dist/svg/paint.js +814 -0
- package/dist/svg/path.cjs +337 -0
- package/dist/svg/path.d.cts +22 -0
- package/dist/svg/path.d.ts +22 -0
- package/dist/svg/path.js +336 -0
- package/dist/svg/read.cjs +677 -0
- package/dist/svg/read.d.cts +12 -0
- package/dist/svg/read.d.ts +12 -0
- package/dist/svg/read.js +675 -0
- package/dist/svg/text.cjs +22 -0
- package/dist/svg/text.d.cts +8 -0
- package/dist/svg/text.d.ts +8 -0
- package/dist/svg/text.js +19 -0
- package/dist/svg/transform.cjs +206 -0
- package/dist/svg/transform.d.cts +23 -0
- package/dist/svg/transform.d.ts +23 -0
- package/dist/svg/transform.js +197 -0
- package/dist/svg/units.cjs +47 -0
- package/dist/svg/units.d.cts +12 -0
- package/dist/svg/units.d.ts +12 -0
- package/dist/svg/units.js +44 -0
- package/dist/svg/write.cjs +127 -0
- package/dist/svg/write.d.cts +23 -0
- package/dist/svg/write.d.ts +23 -0
- package/dist/svg/write.js +123 -0
- package/dist/xml/odf-text.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/svg/path.ts
|
|
3
|
+
const PATH_NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/y;
|
|
4
|
+
var PathScanner = class {
|
|
5
|
+
source;
|
|
6
|
+
pos = 0;
|
|
7
|
+
constructor(source) {
|
|
8
|
+
this.source = source;
|
|
9
|
+
}
|
|
10
|
+
skipSeparators() {
|
|
11
|
+
while (this.pos < this.source.length && /[\s,]/.test(this.source[this.pos])) this.pos++;
|
|
12
|
+
}
|
|
13
|
+
atEnd() {
|
|
14
|
+
this.skipSeparators();
|
|
15
|
+
return this.pos >= this.source.length;
|
|
16
|
+
}
|
|
17
|
+
nextNumber() {
|
|
18
|
+
this.skipSeparators();
|
|
19
|
+
PATH_NUMBER.lastIndex = this.pos;
|
|
20
|
+
const match = PATH_NUMBER.exec(this.source);
|
|
21
|
+
if (match === null) return;
|
|
22
|
+
this.pos = PATH_NUMBER.lastIndex;
|
|
23
|
+
const value = Number(match[0]);
|
|
24
|
+
return Number.isFinite(value) ? value : void 0;
|
|
25
|
+
}
|
|
26
|
+
nextArcFlag() {
|
|
27
|
+
this.skipSeparators();
|
|
28
|
+
const char = this.source[this.pos];
|
|
29
|
+
if (char === "0" || char === "1") {
|
|
30
|
+
this.pos++;
|
|
31
|
+
return char === "0" ? 0 : 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
nextCommandLetter() {
|
|
35
|
+
this.skipSeparators();
|
|
36
|
+
const char = this.source[this.pos];
|
|
37
|
+
if (char !== void 0 && /[a-zA-Z]/.test(char)) {
|
|
38
|
+
this.pos++;
|
|
39
|
+
return char;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function addLine(cursor, acc, to) {
|
|
44
|
+
acc.current.segments.push({
|
|
45
|
+
kind: "line",
|
|
46
|
+
to
|
|
47
|
+
});
|
|
48
|
+
cursor.x = to.x;
|
|
49
|
+
cursor.y = to.y;
|
|
50
|
+
cursor.lastCubicControl = void 0;
|
|
51
|
+
cursor.lastQuadControl = void 0;
|
|
52
|
+
}
|
|
53
|
+
function addCubic(cursor, acc, control1, control2, to) {
|
|
54
|
+
acc.current.segments.push({
|
|
55
|
+
kind: "cubic",
|
|
56
|
+
control1,
|
|
57
|
+
control2,
|
|
58
|
+
to
|
|
59
|
+
});
|
|
60
|
+
cursor.x = to.x;
|
|
61
|
+
cursor.y = to.y;
|
|
62
|
+
cursor.lastCubicControl = control2;
|
|
63
|
+
cursor.lastQuadControl = void 0;
|
|
64
|
+
}
|
|
65
|
+
function addQuad(cursor, acc, control, to) {
|
|
66
|
+
const from = {
|
|
67
|
+
x: cursor.x,
|
|
68
|
+
y: cursor.y
|
|
69
|
+
};
|
|
70
|
+
addCubic(cursor, acc, {
|
|
71
|
+
x: from.x + 2 / 3 * (control.x - from.x),
|
|
72
|
+
y: from.y + 2 / 3 * (control.y - from.y)
|
|
73
|
+
}, {
|
|
74
|
+
x: to.x + 2 / 3 * (control.x - to.x),
|
|
75
|
+
y: to.y + 2 / 3 * (control.y - to.y)
|
|
76
|
+
}, to);
|
|
77
|
+
cursor.lastQuadControl = control;
|
|
78
|
+
}
|
|
79
|
+
function addArc(cursor, acc, rx, ry, xRotDeg, largeArc, sweep, to) {
|
|
80
|
+
const from = {
|
|
81
|
+
x: cursor.x,
|
|
82
|
+
y: cursor.y
|
|
83
|
+
};
|
|
84
|
+
if (rx === 0 || ry === 0 || from.x === to.x && from.y === to.y) {
|
|
85
|
+
if (from.x !== to.x || from.y !== to.y) addLine(cursor, acc, to);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const xRot = xRotDeg * Math.PI / 180;
|
|
89
|
+
const cosRot = Math.cos(xRot);
|
|
90
|
+
const sinRot = Math.sin(xRot);
|
|
91
|
+
const dx = (from.x - to.x) / 2;
|
|
92
|
+
const dy = (from.y - to.y) / 2;
|
|
93
|
+
const x1p = cosRot * dx + sinRot * dy;
|
|
94
|
+
const y1p = -sinRot * dx + cosRot * dy;
|
|
95
|
+
const radiiSpan = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
96
|
+
if (radiiSpan > 1) {
|
|
97
|
+
const factor = Math.sqrt(radiiSpan);
|
|
98
|
+
rx *= factor;
|
|
99
|
+
ry *= factor;
|
|
100
|
+
}
|
|
101
|
+
const sign = largeArc !== sweep ? 1 : -1;
|
|
102
|
+
const numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
103
|
+
const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
104
|
+
const coefficient = denominator === 0 ? 0 : sign * Math.sqrt(Math.max(numerator, 0) / denominator);
|
|
105
|
+
const cxp = coefficient * rx * y1p / ry;
|
|
106
|
+
const cyp = -(coefficient * ry * x1p) / rx;
|
|
107
|
+
const cx = cosRot * cxp - sinRot * cyp + (from.x + to.x) / 2;
|
|
108
|
+
const cy = sinRot * cxp + cosRot * cyp + (from.y + to.y) / 2;
|
|
109
|
+
const angleBetween = (ux, uy, vx, vy) => {
|
|
110
|
+
const dot = ux * vx + uy * vy;
|
|
111
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
112
|
+
if (len === 0) return 0;
|
|
113
|
+
const inner = Math.min(1, Math.max(-1, dot / len));
|
|
114
|
+
const result = Math.acos(inner);
|
|
115
|
+
return ux * vy - uy * vx < 0 ? -result : result;
|
|
116
|
+
};
|
|
117
|
+
const theta1 = angleBetween(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
118
|
+
let deltaTheta = angleBetween((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
119
|
+
if (sweep === 0 && deltaTheta > 0) deltaTheta -= 2 * Math.PI;
|
|
120
|
+
else if (sweep === 1 && deltaTheta < 0) deltaTheta += 2 * Math.PI;
|
|
121
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(deltaTheta) / (Math.PI / 2)));
|
|
122
|
+
const deltaPerSegment = deltaTheta / segmentCount;
|
|
123
|
+
const kappa = 4 / 3 * Math.tan(deltaPerSegment / 4);
|
|
124
|
+
const pointAt = (t) => ({
|
|
125
|
+
x: cx + rx * cosRot * Math.cos(t) - ry * sinRot * Math.sin(t),
|
|
126
|
+
y: cy + rx * sinRot * Math.cos(t) + ry * cosRot * Math.sin(t)
|
|
127
|
+
});
|
|
128
|
+
const tangentAt = (t) => ({
|
|
129
|
+
x: -rx * cosRot * Math.sin(t) - ry * sinRot * Math.cos(t),
|
|
130
|
+
y: -rx * sinRot * Math.sin(t) + ry * cosRot * Math.cos(t)
|
|
131
|
+
});
|
|
132
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
133
|
+
const start = pointAt(theta1 + deltaPerSegment * i);
|
|
134
|
+
const end = pointAt(theta1 + deltaPerSegment * (i + 1));
|
|
135
|
+
const t0 = tangentAt(theta1 + deltaPerSegment * i);
|
|
136
|
+
const t1 = tangentAt(theta1 + deltaPerSegment * (i + 1));
|
|
137
|
+
addCubic(cursor, acc, {
|
|
138
|
+
x: start.x + kappa * t0.x,
|
|
139
|
+
y: start.y + kappa * t0.y
|
|
140
|
+
}, {
|
|
141
|
+
x: end.x - kappa * t1.x,
|
|
142
|
+
y: end.y - kappa * t1.y
|
|
143
|
+
}, end);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const COMMAND_ARITY = {
|
|
147
|
+
M: 2,
|
|
148
|
+
m: 2,
|
|
149
|
+
L: 2,
|
|
150
|
+
l: 2,
|
|
151
|
+
H: 1,
|
|
152
|
+
h: 1,
|
|
153
|
+
V: 1,
|
|
154
|
+
v: 1,
|
|
155
|
+
C: 6,
|
|
156
|
+
c: 6,
|
|
157
|
+
S: 4,
|
|
158
|
+
s: 4,
|
|
159
|
+
Q: 4,
|
|
160
|
+
q: 4,
|
|
161
|
+
T: 2,
|
|
162
|
+
t: 2,
|
|
163
|
+
A: 7,
|
|
164
|
+
a: 7
|
|
165
|
+
};
|
|
166
|
+
function parseSvgPathData(d) {
|
|
167
|
+
const scanner = new PathScanner(d.trim());
|
|
168
|
+
const cursor = {
|
|
169
|
+
x: 0,
|
|
170
|
+
y: 0,
|
|
171
|
+
subpathStartX: 0,
|
|
172
|
+
subpathStartY: 0
|
|
173
|
+
};
|
|
174
|
+
const acc = { subpaths: [] };
|
|
175
|
+
let command;
|
|
176
|
+
const closeSubpath = () => {
|
|
177
|
+
if (acc.current !== void 0) {
|
|
178
|
+
acc.subpaths.push({
|
|
179
|
+
start: acc.current.start,
|
|
180
|
+
closed: true,
|
|
181
|
+
segments: acc.current.segments
|
|
182
|
+
});
|
|
183
|
+
acc.current = void 0;
|
|
184
|
+
}
|
|
185
|
+
cursor.x = cursor.subpathStartX;
|
|
186
|
+
cursor.y = cursor.subpathStartY;
|
|
187
|
+
cursor.lastCubicControl = void 0;
|
|
188
|
+
cursor.lastQuadControl = void 0;
|
|
189
|
+
};
|
|
190
|
+
const startSubpath = (at) => {
|
|
191
|
+
if (acc.current !== void 0) {
|
|
192
|
+
acc.subpaths.push({
|
|
193
|
+
start: acc.current.start,
|
|
194
|
+
closed: false,
|
|
195
|
+
segments: acc.current.segments
|
|
196
|
+
});
|
|
197
|
+
acc.current = void 0;
|
|
198
|
+
}
|
|
199
|
+
acc.current = {
|
|
200
|
+
start: at,
|
|
201
|
+
segments: []
|
|
202
|
+
};
|
|
203
|
+
cursor.x = at.x;
|
|
204
|
+
cursor.y = at.y;
|
|
205
|
+
cursor.subpathStartX = at.x;
|
|
206
|
+
cursor.subpathStartY = at.y;
|
|
207
|
+
cursor.lastCubicControl = void 0;
|
|
208
|
+
cursor.lastQuadControl = void 0;
|
|
209
|
+
};
|
|
210
|
+
while (!scanner.atEnd()) {
|
|
211
|
+
const letter = scanner.nextCommandLetter();
|
|
212
|
+
if (letter !== void 0) command = letter;
|
|
213
|
+
else if (command === void 0) return;
|
|
214
|
+
const active = command;
|
|
215
|
+
if (active === "Z" || active === "z") {
|
|
216
|
+
closeSubpath();
|
|
217
|
+
command = void 0;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const arity = COMMAND_ARITY[active];
|
|
221
|
+
if (arity === void 0) return;
|
|
222
|
+
let groupIndex = 0;
|
|
223
|
+
for (;;) {
|
|
224
|
+
if (active === "A" || active === "a") {
|
|
225
|
+
const rx = scanner.nextNumber();
|
|
226
|
+
const ry = scanner.nextNumber();
|
|
227
|
+
const rotation = scanner.nextNumber();
|
|
228
|
+
const largeArc = scanner.nextArcFlag();
|
|
229
|
+
const sweep = scanner.nextArcFlag();
|
|
230
|
+
const x = scanner.nextNumber();
|
|
231
|
+
const y = scanner.nextNumber();
|
|
232
|
+
if (rx === void 0 || ry === void 0 || rotation === void 0 || largeArc === void 0 || sweep === void 0 || x === void 0 || y === void 0) return;
|
|
233
|
+
if (acc.current === void 0) return;
|
|
234
|
+
const to = active === "A" ? {
|
|
235
|
+
x,
|
|
236
|
+
y
|
|
237
|
+
} : {
|
|
238
|
+
x: cursor.x + x,
|
|
239
|
+
y: cursor.y + y
|
|
240
|
+
};
|
|
241
|
+
addArc(cursor, acc, Math.abs(rx), Math.abs(ry), rotation, largeArc, sweep, to);
|
|
242
|
+
} else {
|
|
243
|
+
const args = [];
|
|
244
|
+
for (let i = 0; i < arity; i++) {
|
|
245
|
+
const value = scanner.nextNumber();
|
|
246
|
+
if (value === void 0) return;
|
|
247
|
+
args.push(value);
|
|
248
|
+
}
|
|
249
|
+
const relative = active === active.toLowerCase() && active !== active.toUpperCase();
|
|
250
|
+
const point = (x, y) => relative ? {
|
|
251
|
+
x: cursor.x + x,
|
|
252
|
+
y: cursor.y + y
|
|
253
|
+
} : {
|
|
254
|
+
x,
|
|
255
|
+
y
|
|
256
|
+
};
|
|
257
|
+
const upper = active.toUpperCase();
|
|
258
|
+
if (upper === "M") if (groupIndex === 0) startSubpath(point(args[0], args[1]));
|
|
259
|
+
else {
|
|
260
|
+
if (acc.current === void 0) return;
|
|
261
|
+
addLine(cursor, acc, point(args[0], args[1]));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
if (acc.current === void 0) return;
|
|
265
|
+
switch (upper) {
|
|
266
|
+
case "L":
|
|
267
|
+
addLine(cursor, acc, point(args[0], args[1]));
|
|
268
|
+
break;
|
|
269
|
+
case "H":
|
|
270
|
+
addLine(cursor, acc, relative ? {
|
|
271
|
+
x: cursor.x + args[0],
|
|
272
|
+
y: cursor.y
|
|
273
|
+
} : {
|
|
274
|
+
x: args[0],
|
|
275
|
+
y: cursor.y
|
|
276
|
+
});
|
|
277
|
+
break;
|
|
278
|
+
case "V":
|
|
279
|
+
addLine(cursor, acc, relative ? {
|
|
280
|
+
x: cursor.x,
|
|
281
|
+
y: cursor.y + args[0]
|
|
282
|
+
} : {
|
|
283
|
+
x: cursor.x,
|
|
284
|
+
y: args[0]
|
|
285
|
+
});
|
|
286
|
+
break;
|
|
287
|
+
case "C":
|
|
288
|
+
addCubic(cursor, acc, point(args[0], args[1]), point(args[2], args[3]), point(args[4], args[5]));
|
|
289
|
+
break;
|
|
290
|
+
case "S": {
|
|
291
|
+
const from = {
|
|
292
|
+
x: cursor.x,
|
|
293
|
+
y: cursor.y
|
|
294
|
+
};
|
|
295
|
+
addCubic(cursor, acc, cursor.lastCubicControl === void 0 ? from : {
|
|
296
|
+
x: 2 * from.x - cursor.lastCubicControl.x,
|
|
297
|
+
y: 2 * from.y - cursor.lastCubicControl.y
|
|
298
|
+
}, point(args[0], args[1]), point(args[2], args[3]));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "Q":
|
|
302
|
+
addQuad(cursor, acc, point(args[0], args[1]), point(args[2], args[3]));
|
|
303
|
+
break;
|
|
304
|
+
case "T": {
|
|
305
|
+
const from = {
|
|
306
|
+
x: cursor.x,
|
|
307
|
+
y: cursor.y
|
|
308
|
+
};
|
|
309
|
+
const to = point(args[0], args[1]);
|
|
310
|
+
addQuad(cursor, acc, cursor.lastQuadControl === void 0 ? from : {
|
|
311
|
+
x: 2 * from.x - cursor.lastQuadControl.x,
|
|
312
|
+
y: 2 * from.y - cursor.lastQuadControl.y
|
|
313
|
+
}, to);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
groupIndex++;
|
|
320
|
+
if (!hasNextNumberToken(scanner)) break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (acc.current !== void 0) acc.subpaths.push({
|
|
324
|
+
start: acc.current.start,
|
|
325
|
+
closed: false,
|
|
326
|
+
segments: acc.current.segments
|
|
327
|
+
});
|
|
328
|
+
return acc.subpaths.filter((subpath) => subpath.segments.length > 0);
|
|
329
|
+
}
|
|
330
|
+
function hasNextNumberToken(scanner) {
|
|
331
|
+
let probe = scanner.pos;
|
|
332
|
+
while (probe < scanner.source.length && /[\s,]/.test(scanner.source[probe])) probe++;
|
|
333
|
+
const char = scanner.source[probe];
|
|
334
|
+
return char !== void 0 && /[0-9.\-+]/.test(char);
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
exports.parseSvgPathData = parseSvgPathData;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/svg/path.d.ts
|
|
2
|
+
interface ParsedPathPoint {
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
}
|
|
6
|
+
type ParsedPathSegment = {
|
|
7
|
+
readonly kind: 'line';
|
|
8
|
+
readonly to: ParsedPathPoint;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'cubic';
|
|
11
|
+
readonly control1: ParsedPathPoint;
|
|
12
|
+
readonly control2: ParsedPathPoint;
|
|
13
|
+
readonly to: ParsedPathPoint;
|
|
14
|
+
};
|
|
15
|
+
interface ParsedPathSubpath {
|
|
16
|
+
readonly start: ParsedPathPoint;
|
|
17
|
+
readonly closed: boolean;
|
|
18
|
+
readonly segments: readonly ParsedPathSegment[];
|
|
19
|
+
}
|
|
20
|
+
declare function parseSvgPathData(d: string): readonly ParsedPathSubpath[] | undefined;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { ParsedPathPoint, ParsedPathSegment, ParsedPathSubpath, parseSvgPathData };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/svg/path.d.ts
|
|
2
|
+
interface ParsedPathPoint {
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
}
|
|
6
|
+
type ParsedPathSegment = {
|
|
7
|
+
readonly kind: 'line';
|
|
8
|
+
readonly to: ParsedPathPoint;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'cubic';
|
|
11
|
+
readonly control1: ParsedPathPoint;
|
|
12
|
+
readonly control2: ParsedPathPoint;
|
|
13
|
+
readonly to: ParsedPathPoint;
|
|
14
|
+
};
|
|
15
|
+
interface ParsedPathSubpath {
|
|
16
|
+
readonly start: ParsedPathPoint;
|
|
17
|
+
readonly closed: boolean;
|
|
18
|
+
readonly segments: readonly ParsedPathSegment[];
|
|
19
|
+
}
|
|
20
|
+
declare function parseSvgPathData(d: string): readonly ParsedPathSubpath[] | undefined;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { ParsedPathPoint, ParsedPathSegment, ParsedPathSubpath, parseSvgPathData };
|
package/dist/svg/path.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
//#region src/svg/path.ts
|
|
2
|
+
const PATH_NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/y;
|
|
3
|
+
var PathScanner = class {
|
|
4
|
+
source;
|
|
5
|
+
pos = 0;
|
|
6
|
+
constructor(source) {
|
|
7
|
+
this.source = source;
|
|
8
|
+
}
|
|
9
|
+
skipSeparators() {
|
|
10
|
+
while (this.pos < this.source.length && /[\s,]/.test(this.source[this.pos])) this.pos++;
|
|
11
|
+
}
|
|
12
|
+
atEnd() {
|
|
13
|
+
this.skipSeparators();
|
|
14
|
+
return this.pos >= this.source.length;
|
|
15
|
+
}
|
|
16
|
+
nextNumber() {
|
|
17
|
+
this.skipSeparators();
|
|
18
|
+
PATH_NUMBER.lastIndex = this.pos;
|
|
19
|
+
const match = PATH_NUMBER.exec(this.source);
|
|
20
|
+
if (match === null) return;
|
|
21
|
+
this.pos = PATH_NUMBER.lastIndex;
|
|
22
|
+
const value = Number(match[0]);
|
|
23
|
+
return Number.isFinite(value) ? value : void 0;
|
|
24
|
+
}
|
|
25
|
+
nextArcFlag() {
|
|
26
|
+
this.skipSeparators();
|
|
27
|
+
const char = this.source[this.pos];
|
|
28
|
+
if (char === "0" || char === "1") {
|
|
29
|
+
this.pos++;
|
|
30
|
+
return char === "0" ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
nextCommandLetter() {
|
|
34
|
+
this.skipSeparators();
|
|
35
|
+
const char = this.source[this.pos];
|
|
36
|
+
if (char !== void 0 && /[a-zA-Z]/.test(char)) {
|
|
37
|
+
this.pos++;
|
|
38
|
+
return char;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function addLine(cursor, acc, to) {
|
|
43
|
+
acc.current.segments.push({
|
|
44
|
+
kind: "line",
|
|
45
|
+
to
|
|
46
|
+
});
|
|
47
|
+
cursor.x = to.x;
|
|
48
|
+
cursor.y = to.y;
|
|
49
|
+
cursor.lastCubicControl = void 0;
|
|
50
|
+
cursor.lastQuadControl = void 0;
|
|
51
|
+
}
|
|
52
|
+
function addCubic(cursor, acc, control1, control2, to) {
|
|
53
|
+
acc.current.segments.push({
|
|
54
|
+
kind: "cubic",
|
|
55
|
+
control1,
|
|
56
|
+
control2,
|
|
57
|
+
to
|
|
58
|
+
});
|
|
59
|
+
cursor.x = to.x;
|
|
60
|
+
cursor.y = to.y;
|
|
61
|
+
cursor.lastCubicControl = control2;
|
|
62
|
+
cursor.lastQuadControl = void 0;
|
|
63
|
+
}
|
|
64
|
+
function addQuad(cursor, acc, control, to) {
|
|
65
|
+
const from = {
|
|
66
|
+
x: cursor.x,
|
|
67
|
+
y: cursor.y
|
|
68
|
+
};
|
|
69
|
+
addCubic(cursor, acc, {
|
|
70
|
+
x: from.x + 2 / 3 * (control.x - from.x),
|
|
71
|
+
y: from.y + 2 / 3 * (control.y - from.y)
|
|
72
|
+
}, {
|
|
73
|
+
x: to.x + 2 / 3 * (control.x - to.x),
|
|
74
|
+
y: to.y + 2 / 3 * (control.y - to.y)
|
|
75
|
+
}, to);
|
|
76
|
+
cursor.lastQuadControl = control;
|
|
77
|
+
}
|
|
78
|
+
function addArc(cursor, acc, rx, ry, xRotDeg, largeArc, sweep, to) {
|
|
79
|
+
const from = {
|
|
80
|
+
x: cursor.x,
|
|
81
|
+
y: cursor.y
|
|
82
|
+
};
|
|
83
|
+
if (rx === 0 || ry === 0 || from.x === to.x && from.y === to.y) {
|
|
84
|
+
if (from.x !== to.x || from.y !== to.y) addLine(cursor, acc, to);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const xRot = xRotDeg * Math.PI / 180;
|
|
88
|
+
const cosRot = Math.cos(xRot);
|
|
89
|
+
const sinRot = Math.sin(xRot);
|
|
90
|
+
const dx = (from.x - to.x) / 2;
|
|
91
|
+
const dy = (from.y - to.y) / 2;
|
|
92
|
+
const x1p = cosRot * dx + sinRot * dy;
|
|
93
|
+
const y1p = -sinRot * dx + cosRot * dy;
|
|
94
|
+
const radiiSpan = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
95
|
+
if (radiiSpan > 1) {
|
|
96
|
+
const factor = Math.sqrt(radiiSpan);
|
|
97
|
+
rx *= factor;
|
|
98
|
+
ry *= factor;
|
|
99
|
+
}
|
|
100
|
+
const sign = largeArc !== sweep ? 1 : -1;
|
|
101
|
+
const numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
102
|
+
const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
103
|
+
const coefficient = denominator === 0 ? 0 : sign * Math.sqrt(Math.max(numerator, 0) / denominator);
|
|
104
|
+
const cxp = coefficient * rx * y1p / ry;
|
|
105
|
+
const cyp = -(coefficient * ry * x1p) / rx;
|
|
106
|
+
const cx = cosRot * cxp - sinRot * cyp + (from.x + to.x) / 2;
|
|
107
|
+
const cy = sinRot * cxp + cosRot * cyp + (from.y + to.y) / 2;
|
|
108
|
+
const angleBetween = (ux, uy, vx, vy) => {
|
|
109
|
+
const dot = ux * vx + uy * vy;
|
|
110
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
111
|
+
if (len === 0) return 0;
|
|
112
|
+
const inner = Math.min(1, Math.max(-1, dot / len));
|
|
113
|
+
const result = Math.acos(inner);
|
|
114
|
+
return ux * vy - uy * vx < 0 ? -result : result;
|
|
115
|
+
};
|
|
116
|
+
const theta1 = angleBetween(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
117
|
+
let deltaTheta = angleBetween((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
118
|
+
if (sweep === 0 && deltaTheta > 0) deltaTheta -= 2 * Math.PI;
|
|
119
|
+
else if (sweep === 1 && deltaTheta < 0) deltaTheta += 2 * Math.PI;
|
|
120
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(deltaTheta) / (Math.PI / 2)));
|
|
121
|
+
const deltaPerSegment = deltaTheta / segmentCount;
|
|
122
|
+
const kappa = 4 / 3 * Math.tan(deltaPerSegment / 4);
|
|
123
|
+
const pointAt = (t) => ({
|
|
124
|
+
x: cx + rx * cosRot * Math.cos(t) - ry * sinRot * Math.sin(t),
|
|
125
|
+
y: cy + rx * sinRot * Math.cos(t) + ry * cosRot * Math.sin(t)
|
|
126
|
+
});
|
|
127
|
+
const tangentAt = (t) => ({
|
|
128
|
+
x: -rx * cosRot * Math.sin(t) - ry * sinRot * Math.cos(t),
|
|
129
|
+
y: -rx * sinRot * Math.sin(t) + ry * cosRot * Math.cos(t)
|
|
130
|
+
});
|
|
131
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
132
|
+
const start = pointAt(theta1 + deltaPerSegment * i);
|
|
133
|
+
const end = pointAt(theta1 + deltaPerSegment * (i + 1));
|
|
134
|
+
const t0 = tangentAt(theta1 + deltaPerSegment * i);
|
|
135
|
+
const t1 = tangentAt(theta1 + deltaPerSegment * (i + 1));
|
|
136
|
+
addCubic(cursor, acc, {
|
|
137
|
+
x: start.x + kappa * t0.x,
|
|
138
|
+
y: start.y + kappa * t0.y
|
|
139
|
+
}, {
|
|
140
|
+
x: end.x - kappa * t1.x,
|
|
141
|
+
y: end.y - kappa * t1.y
|
|
142
|
+
}, end);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const COMMAND_ARITY = {
|
|
146
|
+
M: 2,
|
|
147
|
+
m: 2,
|
|
148
|
+
L: 2,
|
|
149
|
+
l: 2,
|
|
150
|
+
H: 1,
|
|
151
|
+
h: 1,
|
|
152
|
+
V: 1,
|
|
153
|
+
v: 1,
|
|
154
|
+
C: 6,
|
|
155
|
+
c: 6,
|
|
156
|
+
S: 4,
|
|
157
|
+
s: 4,
|
|
158
|
+
Q: 4,
|
|
159
|
+
q: 4,
|
|
160
|
+
T: 2,
|
|
161
|
+
t: 2,
|
|
162
|
+
A: 7,
|
|
163
|
+
a: 7
|
|
164
|
+
};
|
|
165
|
+
function parseSvgPathData(d) {
|
|
166
|
+
const scanner = new PathScanner(d.trim());
|
|
167
|
+
const cursor = {
|
|
168
|
+
x: 0,
|
|
169
|
+
y: 0,
|
|
170
|
+
subpathStartX: 0,
|
|
171
|
+
subpathStartY: 0
|
|
172
|
+
};
|
|
173
|
+
const acc = { subpaths: [] };
|
|
174
|
+
let command;
|
|
175
|
+
const closeSubpath = () => {
|
|
176
|
+
if (acc.current !== void 0) {
|
|
177
|
+
acc.subpaths.push({
|
|
178
|
+
start: acc.current.start,
|
|
179
|
+
closed: true,
|
|
180
|
+
segments: acc.current.segments
|
|
181
|
+
});
|
|
182
|
+
acc.current = void 0;
|
|
183
|
+
}
|
|
184
|
+
cursor.x = cursor.subpathStartX;
|
|
185
|
+
cursor.y = cursor.subpathStartY;
|
|
186
|
+
cursor.lastCubicControl = void 0;
|
|
187
|
+
cursor.lastQuadControl = void 0;
|
|
188
|
+
};
|
|
189
|
+
const startSubpath = (at) => {
|
|
190
|
+
if (acc.current !== void 0) {
|
|
191
|
+
acc.subpaths.push({
|
|
192
|
+
start: acc.current.start,
|
|
193
|
+
closed: false,
|
|
194
|
+
segments: acc.current.segments
|
|
195
|
+
});
|
|
196
|
+
acc.current = void 0;
|
|
197
|
+
}
|
|
198
|
+
acc.current = {
|
|
199
|
+
start: at,
|
|
200
|
+
segments: []
|
|
201
|
+
};
|
|
202
|
+
cursor.x = at.x;
|
|
203
|
+
cursor.y = at.y;
|
|
204
|
+
cursor.subpathStartX = at.x;
|
|
205
|
+
cursor.subpathStartY = at.y;
|
|
206
|
+
cursor.lastCubicControl = void 0;
|
|
207
|
+
cursor.lastQuadControl = void 0;
|
|
208
|
+
};
|
|
209
|
+
while (!scanner.atEnd()) {
|
|
210
|
+
const letter = scanner.nextCommandLetter();
|
|
211
|
+
if (letter !== void 0) command = letter;
|
|
212
|
+
else if (command === void 0) return;
|
|
213
|
+
const active = command;
|
|
214
|
+
if (active === "Z" || active === "z") {
|
|
215
|
+
closeSubpath();
|
|
216
|
+
command = void 0;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const arity = COMMAND_ARITY[active];
|
|
220
|
+
if (arity === void 0) return;
|
|
221
|
+
let groupIndex = 0;
|
|
222
|
+
for (;;) {
|
|
223
|
+
if (active === "A" || active === "a") {
|
|
224
|
+
const rx = scanner.nextNumber();
|
|
225
|
+
const ry = scanner.nextNumber();
|
|
226
|
+
const rotation = scanner.nextNumber();
|
|
227
|
+
const largeArc = scanner.nextArcFlag();
|
|
228
|
+
const sweep = scanner.nextArcFlag();
|
|
229
|
+
const x = scanner.nextNumber();
|
|
230
|
+
const y = scanner.nextNumber();
|
|
231
|
+
if (rx === void 0 || ry === void 0 || rotation === void 0 || largeArc === void 0 || sweep === void 0 || x === void 0 || y === void 0) return;
|
|
232
|
+
if (acc.current === void 0) return;
|
|
233
|
+
const to = active === "A" ? {
|
|
234
|
+
x,
|
|
235
|
+
y
|
|
236
|
+
} : {
|
|
237
|
+
x: cursor.x + x,
|
|
238
|
+
y: cursor.y + y
|
|
239
|
+
};
|
|
240
|
+
addArc(cursor, acc, Math.abs(rx), Math.abs(ry), rotation, largeArc, sweep, to);
|
|
241
|
+
} else {
|
|
242
|
+
const args = [];
|
|
243
|
+
for (let i = 0; i < arity; i++) {
|
|
244
|
+
const value = scanner.nextNumber();
|
|
245
|
+
if (value === void 0) return;
|
|
246
|
+
args.push(value);
|
|
247
|
+
}
|
|
248
|
+
const relative = active === active.toLowerCase() && active !== active.toUpperCase();
|
|
249
|
+
const point = (x, y) => relative ? {
|
|
250
|
+
x: cursor.x + x,
|
|
251
|
+
y: cursor.y + y
|
|
252
|
+
} : {
|
|
253
|
+
x,
|
|
254
|
+
y
|
|
255
|
+
};
|
|
256
|
+
const upper = active.toUpperCase();
|
|
257
|
+
if (upper === "M") if (groupIndex === 0) startSubpath(point(args[0], args[1]));
|
|
258
|
+
else {
|
|
259
|
+
if (acc.current === void 0) return;
|
|
260
|
+
addLine(cursor, acc, point(args[0], args[1]));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
if (acc.current === void 0) return;
|
|
264
|
+
switch (upper) {
|
|
265
|
+
case "L":
|
|
266
|
+
addLine(cursor, acc, point(args[0], args[1]));
|
|
267
|
+
break;
|
|
268
|
+
case "H":
|
|
269
|
+
addLine(cursor, acc, relative ? {
|
|
270
|
+
x: cursor.x + args[0],
|
|
271
|
+
y: cursor.y
|
|
272
|
+
} : {
|
|
273
|
+
x: args[0],
|
|
274
|
+
y: cursor.y
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
case "V":
|
|
278
|
+
addLine(cursor, acc, relative ? {
|
|
279
|
+
x: cursor.x,
|
|
280
|
+
y: cursor.y + args[0]
|
|
281
|
+
} : {
|
|
282
|
+
x: cursor.x,
|
|
283
|
+
y: args[0]
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
case "C":
|
|
287
|
+
addCubic(cursor, acc, point(args[0], args[1]), point(args[2], args[3]), point(args[4], args[5]));
|
|
288
|
+
break;
|
|
289
|
+
case "S": {
|
|
290
|
+
const from = {
|
|
291
|
+
x: cursor.x,
|
|
292
|
+
y: cursor.y
|
|
293
|
+
};
|
|
294
|
+
addCubic(cursor, acc, cursor.lastCubicControl === void 0 ? from : {
|
|
295
|
+
x: 2 * from.x - cursor.lastCubicControl.x,
|
|
296
|
+
y: 2 * from.y - cursor.lastCubicControl.y
|
|
297
|
+
}, point(args[0], args[1]), point(args[2], args[3]));
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "Q":
|
|
301
|
+
addQuad(cursor, acc, point(args[0], args[1]), point(args[2], args[3]));
|
|
302
|
+
break;
|
|
303
|
+
case "T": {
|
|
304
|
+
const from = {
|
|
305
|
+
x: cursor.x,
|
|
306
|
+
y: cursor.y
|
|
307
|
+
};
|
|
308
|
+
const to = point(args[0], args[1]);
|
|
309
|
+
addQuad(cursor, acc, cursor.lastQuadControl === void 0 ? from : {
|
|
310
|
+
x: 2 * from.x - cursor.lastQuadControl.x,
|
|
311
|
+
y: 2 * from.y - cursor.lastQuadControl.y
|
|
312
|
+
}, to);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
groupIndex++;
|
|
319
|
+
if (!hasNextNumberToken(scanner)) break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (acc.current !== void 0) acc.subpaths.push({
|
|
323
|
+
start: acc.current.start,
|
|
324
|
+
closed: false,
|
|
325
|
+
segments: acc.current.segments
|
|
326
|
+
});
|
|
327
|
+
return acc.subpaths.filter((subpath) => subpath.segments.length > 0);
|
|
328
|
+
}
|
|
329
|
+
function hasNextNumberToken(scanner) {
|
|
330
|
+
let probe = scanner.pos;
|
|
331
|
+
while (probe < scanner.source.length && /[\s,]/.test(scanner.source[probe])) probe++;
|
|
332
|
+
const char = scanner.source[probe];
|
|
333
|
+
return char !== void 0 && /[0-9.\-+]/.test(char);
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { parseSvgPathData };
|