@zseven-w/pen-core 0.6.0 → 0.7.1
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/LICENSE +21 -0
- package/README.md +177 -25
- package/package.json +19 -5
- package/src/__tests__/arc-path.test.ts +23 -23
- package/src/__tests__/codegen-utils.test.ts +50 -0
- package/src/__tests__/design-md-parser.test.ts +49 -0
- package/src/__tests__/font-utils.test.ts +15 -15
- package/src/__tests__/layout-engine.test.ts +169 -83
- package/src/__tests__/merge-helpers.test.ts +143 -0
- package/src/__tests__/node-diff.test.ts +139 -0
- package/src/__tests__/node-helpers.test.ts +19 -19
- package/src/__tests__/node-merge.test.ts +425 -0
- package/src/__tests__/normalize-stroke-fill-schema.test.ts +416 -0
- package/src/__tests__/normalize-tree-layout.test.ts +294 -0
- package/src/__tests__/normalize.test.ts +119 -80
- package/src/__tests__/path-anchors.test.ts +98 -0
- package/src/__tests__/resolve-variables-recursive.test.ts +109 -54
- package/src/__tests__/strip-redundant-section-fills.test.ts +278 -0
- package/src/__tests__/text-measure.test.ts +84 -79
- package/src/__tests__/tree-utils.test.ts +133 -102
- package/src/__tests__/unwrap-fake-phone-mockup.test.ts +322 -0
- package/src/__tests__/variables.test.ts +68 -65
- package/src/arc-path.ts +35 -35
- package/src/boolean-ops.ts +131 -142
- package/src/constants.ts +36 -36
- package/src/design-md-parser.ts +363 -0
- package/src/font-utils.ts +30 -15
- package/src/id.ts +1 -1
- package/src/index.ts +47 -13
- package/src/layout/engine.ts +255 -224
- package/src/layout/normalize-tree.ts +140 -0
- package/src/layout/strip-redundant-section-fills.ts +155 -0
- package/src/layout/text-measure.ts +130 -106
- package/src/layout/unwrap-fake-phone-mockup.ts +147 -0
- package/src/merge/index.ts +16 -0
- package/src/merge/merge-helpers.ts +113 -0
- package/src/merge/node-diff.ts +123 -0
- package/src/merge/node-merge.ts +651 -0
- package/src/node-helpers.ts +18 -5
- package/src/normalize/normalize-stroke-fill-schema.ts +297 -0
- package/src/normalize.ts +75 -81
- package/src/path-anchors.ts +331 -0
- package/src/sync-lock.ts +3 -3
- package/src/tree-utils.ts +180 -158
- package/src/variables/replace-refs.ts +63 -60
- package/src/variables/resolve.ts +98 -106
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type { PenPathAnchor, PenPathHandle, PenPathPointType } from '@zseven-w/pen-types';
|
|
2
|
+
|
|
3
|
+
export interface PathAnchorParseResult {
|
|
4
|
+
anchors: PenPathAnchor[];
|
|
5
|
+
closed: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PathBounds {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const COMMAND_RE = /^[MmLlHhVvCcSsZz]$/;
|
|
16
|
+
const UNSUPPORTED_COMMAND_RE = /[QqTtAa]/;
|
|
17
|
+
const TOKEN_RE = /[MmLlHhVvCcSsZz]|[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
18
|
+
const EPSILON = 1e-6;
|
|
19
|
+
|
|
20
|
+
export function pathDataToAnchors(d: string): PathAnchorParseResult | null {
|
|
21
|
+
if (!d.trim()) return null;
|
|
22
|
+
if (UNSUPPORTED_COMMAND_RE.test(d)) return null;
|
|
23
|
+
|
|
24
|
+
const tokens = d.match(TOKEN_RE);
|
|
25
|
+
if (!tokens || tokens.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const anchors: PenPathAnchor[] = [];
|
|
28
|
+
let closed = false;
|
|
29
|
+
let index = 0;
|
|
30
|
+
let currentX = 0;
|
|
31
|
+
let currentY = 0;
|
|
32
|
+
let lastCommand = '';
|
|
33
|
+
let lastCubicControlX: number | null = null;
|
|
34
|
+
let lastCubicControlY: number | null = null;
|
|
35
|
+
|
|
36
|
+
const readNumbers = (count: number): number[] | null => {
|
|
37
|
+
const values: number[] = [];
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
const token = tokens[index];
|
|
40
|
+
if (!token || COMMAND_RE.test(token)) return null;
|
|
41
|
+
const value = Number.parseFloat(token);
|
|
42
|
+
if (!Number.isFinite(value)) return null;
|
|
43
|
+
values.push(value);
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const pushAnchor = (x: number, y: number, handleIn: PenPathAnchor['handleIn']) => {
|
|
50
|
+
anchors.push({
|
|
51
|
+
x,
|
|
52
|
+
y,
|
|
53
|
+
handleIn,
|
|
54
|
+
handleOut: null,
|
|
55
|
+
});
|
|
56
|
+
currentX = x;
|
|
57
|
+
currentY = y;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
while (index < tokens.length) {
|
|
61
|
+
let command = tokens[index];
|
|
62
|
+
if (COMMAND_RE.test(command)) {
|
|
63
|
+
lastCommand = command;
|
|
64
|
+
index += 1;
|
|
65
|
+
} else if (lastCommand) {
|
|
66
|
+
command = lastCommand;
|
|
67
|
+
} else {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (command) {
|
|
72
|
+
case 'M':
|
|
73
|
+
case 'm': {
|
|
74
|
+
const pair = readNumbers(2);
|
|
75
|
+
if (!pair) return null;
|
|
76
|
+
const x = command === 'm' ? currentX + pair[0] : pair[0];
|
|
77
|
+
const y = command === 'm' ? currentY + pair[1] : pair[1];
|
|
78
|
+
if (anchors.length > 0) return null;
|
|
79
|
+
pushAnchor(x, y, null);
|
|
80
|
+
lastCommand = command === 'm' ? 'l' : 'L';
|
|
81
|
+
lastCubicControlX = null;
|
|
82
|
+
lastCubicControlY = null;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'L':
|
|
87
|
+
case 'l': {
|
|
88
|
+
const pair = readNumbers(2);
|
|
89
|
+
if (!pair || anchors.length === 0) return null;
|
|
90
|
+
const x = command === 'l' ? currentX + pair[0] : pair[0];
|
|
91
|
+
const y = command === 'l' ? currentY + pair[1] : pair[1];
|
|
92
|
+
pushAnchor(x, y, null);
|
|
93
|
+
lastCubicControlX = null;
|
|
94
|
+
lastCubicControlY = null;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'H':
|
|
99
|
+
case 'h': {
|
|
100
|
+
const pair = readNumbers(1);
|
|
101
|
+
if (!pair || anchors.length === 0) return null;
|
|
102
|
+
const x = command === 'h' ? currentX + pair[0] : pair[0];
|
|
103
|
+
pushAnchor(x, currentY, null);
|
|
104
|
+
lastCubicControlX = null;
|
|
105
|
+
lastCubicControlY = null;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'V':
|
|
110
|
+
case 'v': {
|
|
111
|
+
const pair = readNumbers(1);
|
|
112
|
+
if (!pair || anchors.length === 0) return null;
|
|
113
|
+
const y = command === 'v' ? currentY + pair[0] : pair[0];
|
|
114
|
+
pushAnchor(currentX, y, null);
|
|
115
|
+
lastCubicControlX = null;
|
|
116
|
+
lastCubicControlY = null;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'C':
|
|
121
|
+
case 'c': {
|
|
122
|
+
const values = readNumbers(6);
|
|
123
|
+
if (!values || anchors.length === 0) return null;
|
|
124
|
+
const prev = anchors[anchors.length - 1];
|
|
125
|
+
const cx1 = command === 'c' ? currentX + values[0] : values[0];
|
|
126
|
+
const cy1 = command === 'c' ? currentY + values[1] : values[1];
|
|
127
|
+
const cx2 = command === 'c' ? currentX + values[2] : values[2];
|
|
128
|
+
const cy2 = command === 'c' ? currentY + values[3] : values[3];
|
|
129
|
+
const x = command === 'c' ? currentX + values[4] : values[4];
|
|
130
|
+
const y = command === 'c' ? currentY + values[5] : values[5];
|
|
131
|
+
prev.handleOut = { x: cx1 - prev.x, y: cy1 - prev.y };
|
|
132
|
+
pushAnchor(x, y, { x: cx2 - x, y: cy2 - y });
|
|
133
|
+
lastCubicControlX = cx2;
|
|
134
|
+
lastCubicControlY = cy2;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'S':
|
|
139
|
+
case 's': {
|
|
140
|
+
const values = readNumbers(4);
|
|
141
|
+
if (!values || anchors.length === 0) return null;
|
|
142
|
+
const prev = anchors[anchors.length - 1];
|
|
143
|
+
const cx1 =
|
|
144
|
+
lastCommand.toLowerCase() === 'c' || lastCommand.toLowerCase() === 's'
|
|
145
|
+
? 2 * currentX - (lastCubicControlX ?? currentX)
|
|
146
|
+
: currentX;
|
|
147
|
+
const cy1 =
|
|
148
|
+
lastCommand.toLowerCase() === 'c' || lastCommand.toLowerCase() === 's'
|
|
149
|
+
? 2 * currentY - (lastCubicControlY ?? currentY)
|
|
150
|
+
: currentY;
|
|
151
|
+
const cx2 = command === 's' ? currentX + values[0] : values[0];
|
|
152
|
+
const cy2 = command === 's' ? currentY + values[1] : values[1];
|
|
153
|
+
const x = command === 's' ? currentX + values[2] : values[2];
|
|
154
|
+
const y = command === 's' ? currentY + values[3] : values[3];
|
|
155
|
+
prev.handleOut = { x: cx1 - prev.x, y: cy1 - prev.y };
|
|
156
|
+
pushAnchor(x, y, { x: cx2 - x, y: cy2 - y });
|
|
157
|
+
lastCubicControlX = cx2;
|
|
158
|
+
lastCubicControlY = cy2;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'Z':
|
|
163
|
+
case 'z':
|
|
164
|
+
if (anchors.length < 2) return null;
|
|
165
|
+
closed = true;
|
|
166
|
+
lastCubicControlX = null;
|
|
167
|
+
lastCubicControlY = null;
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (anchors.length < 2) return null;
|
|
176
|
+
return { anchors, closed };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function anchorsToPathData(anchors: PenPathAnchor[], closed: boolean): string {
|
|
180
|
+
if (anchors.length === 0) return '';
|
|
181
|
+
|
|
182
|
+
const parts: string[] = [`M ${anchors[0].x} ${anchors[0].y}`];
|
|
183
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
184
|
+
appendSegment(parts, anchors[i - 1], anchors[i]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (closed && anchors.length > 1) {
|
|
188
|
+
appendSegment(parts, anchors[anchors.length - 1], anchors[0]);
|
|
189
|
+
parts.push('Z');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parts.join(' ');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function appendSegment(parts: string[], from: PenPathAnchor, to: PenPathAnchor) {
|
|
196
|
+
if (!from.handleOut && !to.handleIn) {
|
|
197
|
+
parts.push(`L ${to.x} ${to.y}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const cx1 = from.x + (from.handleOut?.x ?? 0);
|
|
202
|
+
const cy1 = from.y + (from.handleOut?.y ?? 0);
|
|
203
|
+
const cx2 = to.x + (to.handleIn?.x ?? 0);
|
|
204
|
+
const cy2 = to.y + (to.handleIn?.y ?? 0);
|
|
205
|
+
parts.push(`C ${cx1} ${cy1} ${cx2} ${cy2} ${to.x} ${to.y}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function inferPathAnchorPointType(anchor: PenPathAnchor): PenPathPointType {
|
|
209
|
+
const hasIn = hasMeaningfulHandle(anchor.handleIn);
|
|
210
|
+
const hasOut = hasMeaningfulHandle(anchor.handleOut);
|
|
211
|
+
|
|
212
|
+
if (!hasIn && !hasOut) return 'corner';
|
|
213
|
+
if (hasIn && hasOut && areMirroredHandles(anchor.handleIn!, anchor.handleOut!)) {
|
|
214
|
+
return 'mirrored';
|
|
215
|
+
}
|
|
216
|
+
return 'independent';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getPathBoundsFromAnchors(anchors: PenPathAnchor[], closed: boolean): PathBounds {
|
|
220
|
+
if (anchors.length === 0) {
|
|
221
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let minX = Infinity;
|
|
225
|
+
let minY = Infinity;
|
|
226
|
+
let maxX = -Infinity;
|
|
227
|
+
let maxY = -Infinity;
|
|
228
|
+
|
|
229
|
+
const includePoint = (x: number, y: number) => {
|
|
230
|
+
minX = Math.min(minX, x);
|
|
231
|
+
minY = Math.min(minY, y);
|
|
232
|
+
maxX = Math.max(maxX, x);
|
|
233
|
+
maxY = Math.max(maxY, y);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const includeSegment = (from: PenPathAnchor, to: PenPathAnchor) => {
|
|
237
|
+
includePoint(from.x, from.y);
|
|
238
|
+
includePoint(to.x, to.y);
|
|
239
|
+
|
|
240
|
+
const p0 = { x: from.x, y: from.y };
|
|
241
|
+
const p1 = {
|
|
242
|
+
x: from.x + (from.handleOut?.x ?? 0),
|
|
243
|
+
y: from.y + (from.handleOut?.y ?? 0),
|
|
244
|
+
};
|
|
245
|
+
const p2 = {
|
|
246
|
+
x: to.x + (to.handleIn?.x ?? 0),
|
|
247
|
+
y: to.y + (to.handleIn?.y ?? 0),
|
|
248
|
+
};
|
|
249
|
+
const p3 = { x: to.x, y: to.y };
|
|
250
|
+
|
|
251
|
+
const roots = new Set<number>([
|
|
252
|
+
...solveCubicDerivativeRoots(p0.x, p1.x, p2.x, p3.x),
|
|
253
|
+
...solveCubicDerivativeRoots(p0.y, p1.y, p2.y, p3.y),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
for (const t of roots) {
|
|
257
|
+
includePoint(
|
|
258
|
+
evaluateCubic(p0.x, p1.x, p2.x, p3.x, t),
|
|
259
|
+
evaluateCubic(p0.y, p1.y, p2.y, p3.y, t),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
265
|
+
includeSegment(anchors[i - 1], anchors[i]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (closed && anchors.length > 1) {
|
|
269
|
+
includeSegment(anchors[anchors.length - 1], anchors[0]);
|
|
270
|
+
} else if (anchors.length === 1) {
|
|
271
|
+
includePoint(anchors[0].x, anchors[0].y);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
|
|
275
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
x: minX,
|
|
280
|
+
y: minY,
|
|
281
|
+
width: maxX - minX,
|
|
282
|
+
height: maxY - minY,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function hasMeaningfulHandle(handle: PenPathHandle | null): boolean {
|
|
287
|
+
if (!handle) return false;
|
|
288
|
+
return Math.abs(handle.x) > EPSILON || Math.abs(handle.y) > EPSILON;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function areMirroredHandles(a: PenPathHandle, b: PenPathHandle): boolean {
|
|
292
|
+
const lenA = Math.hypot(a.x, a.y);
|
|
293
|
+
const lenB = Math.hypot(b.x, b.y);
|
|
294
|
+
if (lenA <= EPSILON || lenB <= EPSILON) return false;
|
|
295
|
+
|
|
296
|
+
const tol = Math.max(EPSILON, Math.max(lenA, lenB) * 1e-3);
|
|
297
|
+
return Math.abs(a.x + b.x) <= tol && Math.abs(a.y + b.y) <= tol;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function solveCubicDerivativeRoots(p0: number, p1: number, p2: number, p3: number): number[] {
|
|
301
|
+
const a = -p0 + 3 * p1 - 3 * p2 + p3;
|
|
302
|
+
const b = 2 * (p0 - 2 * p1 + p2);
|
|
303
|
+
const c = -p0 + p1;
|
|
304
|
+
|
|
305
|
+
if (Math.abs(a) <= EPSILON) {
|
|
306
|
+
if (Math.abs(b) <= EPSILON) return [];
|
|
307
|
+
const t = -c / b;
|
|
308
|
+
return isUnitIntervalInterior(t) ? [t] : [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const disc = b * b - 4 * a * c;
|
|
312
|
+
if (disc < -EPSILON) return [];
|
|
313
|
+
if (Math.abs(disc) <= EPSILON) {
|
|
314
|
+
const t = -b / (2 * a);
|
|
315
|
+
return isUnitIntervalInterior(t) ? [t] : [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sqrtDisc = Math.sqrt(Math.max(disc, 0));
|
|
319
|
+
const t1 = (-b + sqrtDisc) / (2 * a);
|
|
320
|
+
const t2 = (-b - sqrtDisc) / (2 * a);
|
|
321
|
+
return [t1, t2].filter(isUnitIntervalInterior);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isUnitIntervalInterior(t: number): boolean {
|
|
325
|
+
return t > EPSILON && t < 1 - EPSILON;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function evaluateCubic(p0: number, p1: number, p2: number, p3: number, t: number): number {
|
|
329
|
+
const mt = 1 - t;
|
|
330
|
+
return mt * mt * mt * p0 + 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t * p3;
|
|
331
|
+
}
|
package/src/sync-lock.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* reads always resolve the current value — even if the bundler does not
|
|
6
6
|
* preserve ES-module live bindings for `let` variables.
|
|
7
7
|
*/
|
|
8
|
-
let _locked = false
|
|
8
|
+
let _locked = false;
|
|
9
9
|
|
|
10
10
|
export function isFabricSyncLocked(): boolean {
|
|
11
|
-
return _locked
|
|
11
|
+
return _locked;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function setFabricSyncLock(v: boolean) {
|
|
15
|
-
_locked = v
|
|
15
|
+
_locked = v;
|
|
16
16
|
}
|