@zseven-w/pen-renderer 0.5.2 → 0.7.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 +6 -6
- package/package.json +9 -9
- package/src/__tests__/document-flattener.test.ts +277 -0
- package/src/__tests__/font-manager.test.ts +65 -0
- package/src/__tests__/image-loader.test.ts +136 -0
- package/src/__tests__/paint-utils.test.ts +61 -0
- package/src/__tests__/render-node-thumbnail.test.ts +312 -0
- package/src/document-flattener.ts +228 -159
- package/src/font-manager.ts +221 -190
- package/src/image-loader.ts +138 -51
- package/src/index.ts +18 -17
- package/src/init.ts +50 -21
- package/src/node-renderer.ts +957 -386
- package/src/paint-utils.ts +99 -71
- package/src/path-utils.ts +235 -115
- package/src/render-node-thumbnail.ts +155 -0
- package/src/renderer.ts +196 -175
- package/src/spatial-index.ts +139 -27
- package/src/text-renderer.ts +367 -304
- package/src/types.ts +18 -22
- package/src/viewport.ts +28 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PenNode, ContainerProps, RefNode } from '@zseven-w/pen-types'
|
|
1
|
+
import type { PenNode, ContainerProps, RefNode } from '@zseven-w/pen-types';
|
|
2
2
|
import {
|
|
3
3
|
resolvePadding,
|
|
4
4
|
isNodeVisible,
|
|
@@ -10,21 +10,21 @@ import {
|
|
|
10
10
|
defaultLineHeight,
|
|
11
11
|
findNodeInTree,
|
|
12
12
|
cssFontFamily,
|
|
13
|
-
} from '@zseven-w/pen-core'
|
|
14
|
-
import { wrapLine } from './paint-utils.js'
|
|
15
|
-
import type { RenderNode } from './types.js'
|
|
13
|
+
} from '@zseven-w/pen-core';
|
|
14
|
+
import { wrapLine } from './paint-utils.js';
|
|
15
|
+
import type { RenderNode } from './types.js';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Pre-measure text widths using Canvas 2D (browser fonts)
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
|
-
let _measureCtx: CanvasRenderingContext2D | null = null
|
|
21
|
+
let _measureCtx: CanvasRenderingContext2D | null = null;
|
|
22
22
|
function getMeasureCtx(): CanvasRenderingContext2D {
|
|
23
23
|
if (!_measureCtx) {
|
|
24
|
-
const c = document.createElement('canvas')
|
|
25
|
-
_measureCtx = c.getContext('2d')
|
|
24
|
+
const c = document.createElement('canvas');
|
|
25
|
+
_measureCtx = c.getContext('2d')!;
|
|
26
26
|
}
|
|
27
|
-
return _measureCtx
|
|
27
|
+
return _measureCtx;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -40,65 +40,84 @@ function getMeasureCtx(): CanvasRenderingContext2D {
|
|
|
40
40
|
*/
|
|
41
41
|
export function premeasureTextHeights(nodes: PenNode[]): PenNode[] {
|
|
42
42
|
return nodes.map((node) => {
|
|
43
|
-
let result = node
|
|
43
|
+
let result = node;
|
|
44
44
|
|
|
45
45
|
if (node.type === 'text') {
|
|
46
|
-
const tNode = node as PenNode & {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
46
|
+
const tNode = node as PenNode & {
|
|
47
|
+
width?: number | string;
|
|
48
|
+
height?: number | string;
|
|
49
|
+
fontSize?: number;
|
|
50
|
+
fontWeight?: string;
|
|
51
|
+
fontFamily?: string;
|
|
52
|
+
lineHeight?: number;
|
|
53
|
+
textAlign?: string;
|
|
54
|
+
textGrowth?: string;
|
|
55
|
+
content?: string | { text?: string }[];
|
|
56
|
+
};
|
|
57
|
+
const hasFixedWidth = typeof tNode.width === 'number' && tNode.width > 0;
|
|
58
|
+
const isContainerHeight =
|
|
59
|
+
typeof tNode.height === 'string' &&
|
|
60
|
+
(tNode.height === 'fill_container' || tNode.height === 'fit_content');
|
|
61
|
+
const textGrowth = tNode.textGrowth;
|
|
62
|
+
const content =
|
|
63
|
+
typeof tNode.content === 'string'
|
|
64
|
+
? tNode.content
|
|
65
|
+
: Array.isArray(tNode.content)
|
|
66
|
+
? tNode.content.map((s) => s.text ?? '').join('')
|
|
67
|
+
: (((tNode as unknown as Record<string, unknown>).text as string) ?? '');
|
|
68
|
+
|
|
69
|
+
const textAlign = tNode.textAlign;
|
|
70
|
+
const isFixedWidthText =
|
|
71
|
+
textGrowth === 'fixed-width' ||
|
|
72
|
+
textGrowth === 'fixed-width-height' ||
|
|
73
|
+
(textGrowth !== 'auto' && textAlign != null && textAlign !== 'left');
|
|
60
74
|
if (content && hasFixedWidth && isFixedWidthText && !isContainerHeight) {
|
|
61
|
-
const fontSize = tNode.fontSize ?? 16
|
|
62
|
-
const fontWeight = tNode.fontWeight ?? '400'
|
|
63
|
-
const fontFamily =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
75
|
+
const fontSize = tNode.fontSize ?? 16;
|
|
76
|
+
const fontWeight = tNode.fontWeight ?? '400';
|
|
77
|
+
const fontFamily =
|
|
78
|
+
tNode.fontFamily ??
|
|
79
|
+
'Inter, -apple-system, "Noto Sans SC", "PingFang SC", system-ui, sans-serif';
|
|
80
|
+
const ctx = getMeasureCtx();
|
|
81
|
+
ctx.font = `${fontWeight} ${fontSize}px ${cssFontFamily(fontFamily)}`;
|
|
82
|
+
|
|
83
|
+
const wrapWidth = (tNode.width as number) + fontSize * 0.2;
|
|
84
|
+
const rawLines = content.split('\n');
|
|
85
|
+
const wrappedLines: string[] = [];
|
|
70
86
|
for (const raw of rawLines) {
|
|
71
|
-
if (!raw) {
|
|
72
|
-
|
|
87
|
+
if (!raw) {
|
|
88
|
+
wrappedLines.push('');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
wrapLine(ctx, raw, wrapWidth, wrappedLines);
|
|
73
92
|
}
|
|
74
|
-
const lineHeightMul = tNode.lineHeight ?? defaultLineHeight(fontSize)
|
|
75
|
-
const lineHeight = lineHeightMul * fontSize
|
|
76
|
-
const glyphH = fontSize * 1.13
|
|
93
|
+
const lineHeightMul = tNode.lineHeight ?? defaultLineHeight(fontSize);
|
|
94
|
+
const lineHeight = lineHeightMul * fontSize;
|
|
95
|
+
const glyphH = fontSize * 1.13;
|
|
77
96
|
const measuredHeight = Math.ceil(
|
|
78
97
|
wrappedLines.length <= 1
|
|
79
98
|
? glyphH + 2
|
|
80
99
|
: (wrappedLines.length - 1) * lineHeight + glyphH + 2,
|
|
81
|
-
)
|
|
82
|
-
const currentHeight = typeof tNode.height === 'number' ? tNode.height : 0
|
|
83
|
-
const explicitLineCount = rawLines.length
|
|
84
|
-
const needsHeight = currentHeight <= 0 || wrappedLines.length > explicitLineCount
|
|
100
|
+
);
|
|
101
|
+
const currentHeight = typeof tNode.height === 'number' ? tNode.height : 0;
|
|
102
|
+
const explicitLineCount = rawLines.length;
|
|
103
|
+
const needsHeight = currentHeight <= 0 || wrappedLines.length > explicitLineCount;
|
|
85
104
|
if (needsHeight && measuredHeight > currentHeight) {
|
|
86
|
-
result = { ...node, height: measuredHeight } as unknown as PenNode
|
|
105
|
+
result = { ...node, height: measuredHeight } as unknown as PenNode;
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
// Recurse into children
|
|
92
111
|
if ('children' in result && result.children) {
|
|
93
|
-
const children = result.children
|
|
94
|
-
const measured = premeasureTextHeights(children)
|
|
112
|
+
const children = result.children;
|
|
113
|
+
const measured = premeasureTextHeights(children);
|
|
95
114
|
if (measured !== children) {
|
|
96
|
-
result = { ...result, children: measured } as unknown as PenNode
|
|
115
|
+
result = { ...result, children: measured } as unknown as PenNode;
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
return result
|
|
101
|
-
})
|
|
119
|
+
return result;
|
|
120
|
+
});
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
// ---------------------------------------------------------------------------
|
|
@@ -106,24 +125,28 @@ export function premeasureTextHeights(nodes: PenNode[]): PenNode[] {
|
|
|
106
125
|
// ---------------------------------------------------------------------------
|
|
107
126
|
|
|
108
127
|
interface ClipInfo {
|
|
109
|
-
x: number;
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
w: number;
|
|
131
|
+
h: number;
|
|
132
|
+
rx: number;
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
function sizeToNumber(val: number | string | undefined, fallback: number): number {
|
|
113
|
-
if (typeof val === 'number') return val
|
|
136
|
+
if (typeof val === 'number') return val;
|
|
114
137
|
if (typeof val === 'string') {
|
|
115
|
-
const m = val.match(/\((\d+(?:\.\d+)?)\)/)
|
|
116
|
-
if (m) return parseFloat(m[1])
|
|
117
|
-
const n = parseFloat(val)
|
|
118
|
-
if (!isNaN(n)) return n
|
|
138
|
+
const m = val.match(/\((\d+(?:\.\d+)?)\)/);
|
|
139
|
+
if (m) return parseFloat(m[1]);
|
|
140
|
+
const n = parseFloat(val);
|
|
141
|
+
if (!isNaN(n)) return n;
|
|
119
142
|
}
|
|
120
|
-
return fallback
|
|
143
|
+
return fallback;
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
function cornerRadiusVal(cr: number | [number, number, number, number] | undefined): number {
|
|
124
|
-
if (cr === undefined) return 0
|
|
125
|
-
if (typeof cr === 'number') return cr
|
|
126
|
-
return cr[0]
|
|
147
|
+
if (cr === undefined) return 0;
|
|
148
|
+
if (typeof cr === 'number') return cr;
|
|
149
|
+
return cr[0];
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
export function flattenToRenderNodes(
|
|
@@ -135,129 +158,165 @@ export function flattenToRenderNodes(
|
|
|
135
158
|
clipCtx?: ClipInfo,
|
|
136
159
|
depth = 0,
|
|
137
160
|
): RenderNode[] {
|
|
138
|
-
const result: RenderNode[] = []
|
|
161
|
+
const result: RenderNode[] = [];
|
|
139
162
|
|
|
140
163
|
// Reverse order: children[0] = top layer = rendered last (frontmost)
|
|
141
164
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
142
|
-
const node = nodes[i]
|
|
143
|
-
if (!isNodeVisible(node)) continue
|
|
165
|
+
const node = nodes[i];
|
|
166
|
+
if (!isNodeVisible(node)) continue;
|
|
144
167
|
|
|
145
168
|
// Resolve fill_container / fit_content
|
|
146
|
-
let resolved = node
|
|
169
|
+
let resolved = node;
|
|
147
170
|
if (parentAvailW !== undefined || parentAvailH !== undefined) {
|
|
148
|
-
let changed = false
|
|
149
|
-
const r: Record<string, unknown> = { ...node }
|
|
171
|
+
let changed = false;
|
|
172
|
+
const r: Record<string, unknown> = { ...node };
|
|
150
173
|
if ('width' in node && typeof node.width !== 'number') {
|
|
151
|
-
const s = parseSizing(node.width)
|
|
152
|
-
if (s === 'fill' && parentAvailW) {
|
|
153
|
-
|
|
174
|
+
const s = parseSizing(node.width);
|
|
175
|
+
if (s === 'fill' && parentAvailW) {
|
|
176
|
+
r.width = parentAvailW;
|
|
177
|
+
changed = true;
|
|
178
|
+
} else if (s === 'fit') {
|
|
179
|
+
r.width = getNodeWidth(node, parentAvailW);
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
154
182
|
}
|
|
155
183
|
if ('height' in node && typeof node.height !== 'number') {
|
|
156
|
-
const s = parseSizing(node.height)
|
|
157
|
-
if (s === 'fill' && parentAvailH) {
|
|
158
|
-
|
|
184
|
+
const s = parseSizing(node.height);
|
|
185
|
+
if (s === 'fill' && parentAvailH) {
|
|
186
|
+
r.height = parentAvailH;
|
|
187
|
+
changed = true;
|
|
188
|
+
} else if (s === 'fit') {
|
|
189
|
+
r.height = getNodeHeight(node, parentAvailH, parentAvailW);
|
|
190
|
+
changed = true;
|
|
191
|
+
}
|
|
159
192
|
}
|
|
160
|
-
if (changed) resolved = r as unknown as PenNode
|
|
193
|
+
if (changed) resolved = r as unknown as PenNode;
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
// Compute height for frames without explicit numeric height
|
|
164
197
|
if (
|
|
165
|
-
node.type === 'frame'
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
node.type === 'frame' &&
|
|
199
|
+
'children' in node &&
|
|
200
|
+
node.children?.length &&
|
|
201
|
+
(!('height' in resolved) || typeof resolved.height !== 'number')
|
|
168
202
|
) {
|
|
169
|
-
const computedH = getNodeHeight(resolved, parentAvailH, parentAvailW)
|
|
170
|
-
if (computedH > 0) resolved = { ...resolved, height: computedH } as unknown as PenNode
|
|
203
|
+
const computedH = getNodeHeight(resolved, parentAvailH, parentAvailW);
|
|
204
|
+
if (computedH > 0) resolved = { ...resolved, height: computedH } as unknown as PenNode;
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
const absX = (resolved.x ?? 0) + offsetX
|
|
174
|
-
const absY = (resolved.y ?? 0) + offsetY
|
|
175
|
-
|
|
176
|
-
|
|
207
|
+
const absX = (resolved.x ?? 0) + offsetX;
|
|
208
|
+
const absY = (resolved.y ?? 0) + offsetY;
|
|
209
|
+
|
|
210
|
+
// Compute authoritative dimensions once via getNodeWidth/getNodeHeight.
|
|
211
|
+
// Used for: RenderNode absW/absH, child available space, and clip rect.
|
|
212
|
+
// This replaces the prior split where absW/absH used sizeToNumber (raw
|
|
213
|
+
// parse + 100 fallback) while child layout used getNodeWidth/getNodeHeight,
|
|
214
|
+
// causing divergence when nodes lacked numeric dimensions.
|
|
215
|
+
const nodeW = getNodeWidth(resolved, parentAvailW);
|
|
216
|
+
const nodeH = getNodeHeight(resolved, parentAvailH, parentAvailW);
|
|
217
|
+
const absW = nodeW > 0 ? nodeW : 'width' in resolved ? sizeToNumber(resolved.width, 100) : 100;
|
|
218
|
+
const absH =
|
|
219
|
+
nodeH > 0 ? nodeH : 'height' in resolved ? sizeToNumber(resolved.height, 100) : 100;
|
|
177
220
|
|
|
178
221
|
result.push({
|
|
179
222
|
node: { ...resolved, x: absX, y: absY } as PenNode,
|
|
180
|
-
absX,
|
|
223
|
+
absX,
|
|
224
|
+
absY,
|
|
225
|
+
absW,
|
|
226
|
+
absH,
|
|
181
227
|
clipRect: clipCtx,
|
|
182
|
-
})
|
|
228
|
+
});
|
|
183
229
|
|
|
184
230
|
// Recurse into children
|
|
185
|
-
const children = 'children' in node ? node.children : undefined
|
|
231
|
+
const children = 'children' in node ? node.children : undefined;
|
|
186
232
|
if (children && children.length > 0) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const childAvailW = Math.max(0, nodeW - pad.left - pad.right)
|
|
191
|
-
const childAvailH = Math.max(0, nodeH - pad.top - pad.bottom)
|
|
233
|
+
const pad = resolvePadding(
|
|
234
|
+
'padding' in resolved ? (resolved as PenNode & ContainerProps).padding : undefined,
|
|
235
|
+
);
|
|
236
|
+
const childAvailW = Math.max(0, nodeW - pad.left - pad.right);
|
|
237
|
+
const childAvailH = Math.max(0, nodeH - pad.top - pad.bottom);
|
|
192
238
|
|
|
193
|
-
const layout =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
: children
|
|
239
|
+
const layout =
|
|
240
|
+
('layout' in node ? (node as ContainerProps).layout : undefined) || inferLayout(node);
|
|
241
|
+
const positioned =
|
|
242
|
+
layout && layout !== 'none' ? computeLayoutPositions(resolved, children) : children;
|
|
197
243
|
|
|
198
244
|
// Clipping — only clip for root frames (artboard behavior).
|
|
199
|
-
let childClip = clipCtx
|
|
200
|
-
const isRootFrame = node.type === 'frame' && depth === 0
|
|
245
|
+
let childClip = clipCtx;
|
|
246
|
+
const isRootFrame = node.type === 'frame' && depth === 0;
|
|
201
247
|
if (isRootFrame) {
|
|
202
|
-
const crRaw = 'cornerRadius' in node ? cornerRadiusVal(node.cornerRadius) : 0
|
|
203
|
-
const cr = Math.min(crRaw, nodeH / 2)
|
|
204
|
-
childClip = { x: absX, y: absY, w: nodeW, h: nodeH, rx: cr }
|
|
248
|
+
const crRaw = 'cornerRadius' in node ? cornerRadiusVal(node.cornerRadius) : 0;
|
|
249
|
+
const cr = Math.min(crRaw, nodeH / 2);
|
|
250
|
+
childClip = { x: absX, y: absY, w: nodeW, h: nodeH, rx: cr };
|
|
205
251
|
}
|
|
206
252
|
|
|
207
|
-
const childRNs = flattenToRenderNodes(
|
|
253
|
+
const childRNs = flattenToRenderNodes(
|
|
254
|
+
positioned,
|
|
255
|
+
absX,
|
|
256
|
+
absY,
|
|
257
|
+
childAvailW,
|
|
258
|
+
childAvailH,
|
|
259
|
+
childClip,
|
|
260
|
+
depth + 1,
|
|
261
|
+
);
|
|
208
262
|
|
|
209
263
|
// Propagate parent flip to children
|
|
210
|
-
const parentFlipX = node.flipX === true
|
|
211
|
-
const parentFlipY = node.flipY === true
|
|
264
|
+
const parentFlipX = node.flipX === true;
|
|
265
|
+
const parentFlipY = node.flipY === true;
|
|
212
266
|
if (parentFlipX || parentFlipY) {
|
|
213
|
-
const pcx = absX + nodeW / 2
|
|
214
|
-
const pcy = absY + nodeH / 2
|
|
267
|
+
const pcx = absX + nodeW / 2;
|
|
268
|
+
const pcy = absY + nodeH / 2;
|
|
215
269
|
for (const crn of childRNs) {
|
|
216
|
-
const updates: Record<string, unknown> = {}
|
|
270
|
+
const updates: Record<string, unknown> = {};
|
|
217
271
|
if (parentFlipX) {
|
|
218
|
-
const ccx = crn.absX + crn.absW / 2
|
|
219
|
-
crn.absX = 2 * pcx - ccx - crn.absW / 2
|
|
220
|
-
const childFlip = crn.node.flipX === true
|
|
221
|
-
updates.flipX = !childFlip || undefined
|
|
272
|
+
const ccx = crn.absX + crn.absW / 2;
|
|
273
|
+
crn.absX = 2 * pcx - ccx - crn.absW / 2;
|
|
274
|
+
const childFlip = crn.node.flipX === true;
|
|
275
|
+
updates.flipX = !childFlip || undefined;
|
|
222
276
|
}
|
|
223
277
|
if (parentFlipY) {
|
|
224
|
-
const ccy = crn.absY + crn.absH / 2
|
|
225
|
-
crn.absY = 2 * pcy - ccy - crn.absH / 2
|
|
226
|
-
const childFlip = crn.node.flipY === true
|
|
227
|
-
updates.flipY = !childFlip || undefined
|
|
278
|
+
const ccy = crn.absY + crn.absH / 2;
|
|
279
|
+
crn.absY = 2 * pcy - ccy - crn.absH / 2;
|
|
280
|
+
const childFlip = crn.node.flipY === true;
|
|
281
|
+
updates.flipY = !childFlip || undefined;
|
|
228
282
|
}
|
|
229
|
-
crn.node = { ...crn.node, x: crn.absX, y: crn.absY, ...updates } as PenNode
|
|
283
|
+
crn.node = { ...crn.node, x: crn.absX, y: crn.absY, ...updates } as PenNode;
|
|
230
284
|
}
|
|
231
285
|
}
|
|
232
286
|
|
|
233
287
|
// Propagate parent rotation to children
|
|
234
|
-
const parentRot = node.rotation ?? 0
|
|
288
|
+
const parentRot = node.rotation ?? 0;
|
|
235
289
|
if (parentRot !== 0) {
|
|
236
|
-
const cx = absX + nodeW / 2
|
|
237
|
-
const cy = absY + nodeH / 2
|
|
238
|
-
const rad = parentRot * Math.PI / 180
|
|
239
|
-
const cosA = Math.cos(rad)
|
|
240
|
-
const sinA = Math.sin(rad)
|
|
290
|
+
const cx = absX + nodeW / 2;
|
|
291
|
+
const cy = absY + nodeH / 2;
|
|
292
|
+
const rad = (parentRot * Math.PI) / 180;
|
|
293
|
+
const cosA = Math.cos(rad);
|
|
294
|
+
const sinA = Math.sin(rad);
|
|
241
295
|
|
|
242
296
|
for (const crn of childRNs) {
|
|
243
|
-
const ccx = crn.absX + crn.absW / 2
|
|
244
|
-
const ccy = crn.absY + crn.absH / 2
|
|
245
|
-
const dx = ccx - cx
|
|
246
|
-
const dy = ccy - cy
|
|
247
|
-
const newCx = cx + dx * cosA - dy * sinA
|
|
248
|
-
const newCy = cy + dx * sinA + dy * cosA
|
|
249
|
-
crn.absX = newCx - crn.absW / 2
|
|
250
|
-
crn.absY = newCy - crn.absH / 2
|
|
251
|
-
const childRot = crn.node.rotation ?? 0
|
|
252
|
-
crn.node = {
|
|
297
|
+
const ccx = crn.absX + crn.absW / 2;
|
|
298
|
+
const ccy = crn.absY + crn.absH / 2;
|
|
299
|
+
const dx = ccx - cx;
|
|
300
|
+
const dy = ccy - cy;
|
|
301
|
+
const newCx = cx + dx * cosA - dy * sinA;
|
|
302
|
+
const newCy = cy + dx * sinA + dy * cosA;
|
|
303
|
+
crn.absX = newCx - crn.absW / 2;
|
|
304
|
+
crn.absY = newCy - crn.absH / 2;
|
|
305
|
+
const childRot = crn.node.rotation ?? 0;
|
|
306
|
+
crn.node = {
|
|
307
|
+
...crn.node,
|
|
308
|
+
x: crn.absX,
|
|
309
|
+
y: crn.absY,
|
|
310
|
+
rotation: childRot + parentRot,
|
|
311
|
+
} as PenNode;
|
|
253
312
|
}
|
|
254
313
|
}
|
|
255
314
|
|
|
256
|
-
result.push(...childRNs)
|
|
315
|
+
result.push(...childRNs);
|
|
257
316
|
}
|
|
258
317
|
}
|
|
259
318
|
|
|
260
|
-
return result
|
|
319
|
+
return result;
|
|
261
320
|
}
|
|
262
321
|
|
|
263
322
|
// ---------------------------------------------------------------------------
|
|
@@ -271,46 +330,56 @@ export function resolveRefs(
|
|
|
271
330
|
findInTree?: (nodes: PenNode[], id: string) => PenNode | null,
|
|
272
331
|
visited = new Set<string>(),
|
|
273
332
|
): PenNode[] {
|
|
274
|
-
const finder = findInTree ?? ((ns: PenNode[], id: string) => findNodeInTree(ns, id) ?? null)
|
|
333
|
+
const finder = findInTree ?? ((ns: PenNode[], id: string) => findNodeInTree(ns, id) ?? null);
|
|
275
334
|
return nodes.flatMap((node) => {
|
|
276
335
|
if (node.type !== 'ref') {
|
|
277
336
|
if ('children' in node && node.children) {
|
|
278
|
-
return [
|
|
337
|
+
return [
|
|
338
|
+
{ ...node, children: resolveRefs(node.children, rootNodes, finder, visited) } as PenNode,
|
|
339
|
+
];
|
|
279
340
|
}
|
|
280
|
-
return [node]
|
|
341
|
+
return [node];
|
|
281
342
|
}
|
|
282
|
-
if (visited.has(node.ref)) return []
|
|
283
|
-
const component = finder(rootNodes, node.ref)
|
|
284
|
-
if (!component) return []
|
|
285
|
-
visited.add(node.ref)
|
|
286
|
-
const resolved: Record<string, unknown> = { ...component }
|
|
343
|
+
if (visited.has(node.ref)) return [];
|
|
344
|
+
const component = finder(rootNodes, node.ref);
|
|
345
|
+
if (!component) return [];
|
|
346
|
+
visited.add(node.ref);
|
|
347
|
+
const resolved: Record<string, unknown> = { ...component };
|
|
287
348
|
for (const [key, val] of Object.entries(node)) {
|
|
288
|
-
if (key === 'type' || key === 'ref' || key === 'descendants' || key === 'children') continue
|
|
289
|
-
if (val !== undefined) resolved[key] = val
|
|
349
|
+
if (key === 'type' || key === 'ref' || key === 'descendants' || key === 'children') continue;
|
|
350
|
+
if (val !== undefined) resolved[key] = val;
|
|
290
351
|
}
|
|
291
|
-
resolved.type = component.type
|
|
292
|
-
if (!resolved.name) resolved.name = component.name
|
|
293
|
-
delete resolved.reusable
|
|
294
|
-
const resolvedNode = resolved as unknown as PenNode
|
|
352
|
+
resolved.type = component.type;
|
|
353
|
+
if (!resolved.name) resolved.name = component.name;
|
|
354
|
+
delete resolved.reusable;
|
|
355
|
+
const resolvedNode = resolved as unknown as PenNode;
|
|
295
356
|
if ('children' in component && component.children) {
|
|
296
|
-
const refNode = node as RefNode
|
|
297
|
-
|
|
357
|
+
const refNode = node as RefNode;
|
|
358
|
+
(resolvedNode as PenNode & ContainerProps).children = remapIds(
|
|
359
|
+
component.children,
|
|
360
|
+
node.id,
|
|
361
|
+
refNode.descendants,
|
|
362
|
+
);
|
|
298
363
|
}
|
|
299
|
-
visited.delete(node.ref)
|
|
300
|
-
return [resolvedNode]
|
|
301
|
-
})
|
|
364
|
+
visited.delete(node.ref);
|
|
365
|
+
return [resolvedNode];
|
|
366
|
+
});
|
|
302
367
|
}
|
|
303
368
|
|
|
304
|
-
export function remapIds(
|
|
369
|
+
export function remapIds(
|
|
370
|
+
children: PenNode[],
|
|
371
|
+
refId: string,
|
|
372
|
+
overrides?: Record<string, Partial<PenNode>>,
|
|
373
|
+
): PenNode[] {
|
|
305
374
|
return children.map((child) => {
|
|
306
|
-
const virtualId = `${refId}__${child.id}
|
|
307
|
-
const ov = overrides?.[child.id] ?? {}
|
|
308
|
-
const mapped = { ...child, ...ov, id: virtualId } as PenNode
|
|
375
|
+
const virtualId = `${refId}__${child.id}`;
|
|
376
|
+
const ov = overrides?.[child.id] ?? {};
|
|
377
|
+
const mapped = { ...child, ...ov, id: virtualId } as PenNode;
|
|
309
378
|
if ('children' in mapped && mapped.children) {
|
|
310
|
-
(mapped as PenNode & ContainerProps).children = remapIds(mapped.children, refId, overrides)
|
|
379
|
+
(mapped as PenNode & ContainerProps).children = remapIds(mapped.children, refId, overrides);
|
|
311
380
|
}
|
|
312
|
-
return mapped
|
|
313
|
-
})
|
|
381
|
+
return mapped;
|
|
382
|
+
});
|
|
314
383
|
}
|
|
315
384
|
|
|
316
385
|
// ---------------------------------------------------------------------------
|
|
@@ -320,10 +389,10 @@ export function remapIds(children: PenNode[], refId: string, overrides?: Record<
|
|
|
320
389
|
export function collectReusableIds(nodes: PenNode[], result: Set<string>) {
|
|
321
390
|
for (const node of nodes) {
|
|
322
391
|
if (node.type === 'frame' && node.reusable === true) {
|
|
323
|
-
result.add(node.id)
|
|
392
|
+
result.add(node.id);
|
|
324
393
|
}
|
|
325
394
|
if ('children' in node && node.children) {
|
|
326
|
-
collectReusableIds(node.children, result)
|
|
395
|
+
collectReusableIds(node.children, result);
|
|
327
396
|
}
|
|
328
397
|
}
|
|
329
398
|
}
|
|
@@ -331,10 +400,10 @@ export function collectReusableIds(nodes: PenNode[], result: Set<string>) {
|
|
|
331
400
|
export function collectInstanceIds(nodes: PenNode[], result: Set<string>) {
|
|
332
401
|
for (const node of nodes) {
|
|
333
402
|
if (node.type === 'ref') {
|
|
334
|
-
result.add(node.id)
|
|
403
|
+
result.add(node.id);
|
|
335
404
|
}
|
|
336
405
|
if ('children' in node && node.children) {
|
|
337
|
-
collectInstanceIds(node.children, result)
|
|
406
|
+
collectInstanceIds(node.children, result);
|
|
338
407
|
}
|
|
339
408
|
}
|
|
340
409
|
}
|