@zseven-w/pen-renderer 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 +153 -27
- package/package.json +23 -9
- package/src/__tests__/document-flattener.test.ts +166 -90
- 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 +222 -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 -74
- 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 +360 -302
- 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,135 +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
|
|
207
|
+
const absX = (resolved.x ?? 0) + offsetX;
|
|
208
|
+
const absY = (resolved.y ?? 0) + offsetY;
|
|
175
209
|
|
|
176
210
|
// Compute authoritative dimensions once via getNodeWidth/getNodeHeight.
|
|
177
211
|
// Used for: RenderNode absW/absH, child available space, and clip rect.
|
|
178
212
|
// This replaces the prior split where absW/absH used sizeToNumber (raw
|
|
179
213
|
// parse + 100 fallback) while child layout used getNodeWidth/getNodeHeight,
|
|
180
214
|
// causing divergence when nodes lacked numeric dimensions.
|
|
181
|
-
const nodeW = getNodeWidth(resolved, parentAvailW)
|
|
182
|
-
const nodeH = getNodeHeight(resolved, parentAvailH, parentAvailW)
|
|
183
|
-
const absW = nodeW > 0 ? nodeW :
|
|
184
|
-
const absH =
|
|
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;
|
|
185
220
|
|
|
186
221
|
result.push({
|
|
187
222
|
node: { ...resolved, x: absX, y: absY } as PenNode,
|
|
188
|
-
absX,
|
|
223
|
+
absX,
|
|
224
|
+
absY,
|
|
225
|
+
absW,
|
|
226
|
+
absH,
|
|
189
227
|
clipRect: clipCtx,
|
|
190
|
-
})
|
|
228
|
+
});
|
|
191
229
|
|
|
192
230
|
// Recurse into children
|
|
193
|
-
const children = 'children' in node ? node.children : undefined
|
|
231
|
+
const children = 'children' in node ? node.children : undefined;
|
|
194
232
|
if (children && children.length > 0) {
|
|
195
|
-
const pad = resolvePadding(
|
|
196
|
-
|
|
197
|
-
|
|
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);
|
|
198
238
|
|
|
199
|
-
const layout =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
: 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;
|
|
203
243
|
|
|
204
244
|
// Clipping — only clip for root frames (artboard behavior).
|
|
205
|
-
let childClip = clipCtx
|
|
206
|
-
const isRootFrame = node.type === 'frame' && depth === 0
|
|
245
|
+
let childClip = clipCtx;
|
|
246
|
+
const isRootFrame = node.type === 'frame' && depth === 0;
|
|
207
247
|
if (isRootFrame) {
|
|
208
|
-
const crRaw = 'cornerRadius' in node ? cornerRadiusVal(node.cornerRadius) : 0
|
|
209
|
-
const cr = Math.min(crRaw, nodeH / 2)
|
|
210
|
-
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 };
|
|
211
251
|
}
|
|
212
252
|
|
|
213
|
-
const childRNs = flattenToRenderNodes(
|
|
253
|
+
const childRNs = flattenToRenderNodes(
|
|
254
|
+
positioned,
|
|
255
|
+
absX,
|
|
256
|
+
absY,
|
|
257
|
+
childAvailW,
|
|
258
|
+
childAvailH,
|
|
259
|
+
childClip,
|
|
260
|
+
depth + 1,
|
|
261
|
+
);
|
|
214
262
|
|
|
215
263
|
// Propagate parent flip to children
|
|
216
|
-
const parentFlipX = node.flipX === true
|
|
217
|
-
const parentFlipY = node.flipY === true
|
|
264
|
+
const parentFlipX = node.flipX === true;
|
|
265
|
+
const parentFlipY = node.flipY === true;
|
|
218
266
|
if (parentFlipX || parentFlipY) {
|
|
219
|
-
const pcx = absX + nodeW / 2
|
|
220
|
-
const pcy = absY + nodeH / 2
|
|
267
|
+
const pcx = absX + nodeW / 2;
|
|
268
|
+
const pcy = absY + nodeH / 2;
|
|
221
269
|
for (const crn of childRNs) {
|
|
222
|
-
const updates: Record<string, unknown> = {}
|
|
270
|
+
const updates: Record<string, unknown> = {};
|
|
223
271
|
if (parentFlipX) {
|
|
224
|
-
const ccx = crn.absX + crn.absW / 2
|
|
225
|
-
crn.absX = 2 * pcx - ccx - crn.absW / 2
|
|
226
|
-
const childFlip = crn.node.flipX === true
|
|
227
|
-
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;
|
|
228
276
|
}
|
|
229
277
|
if (parentFlipY) {
|
|
230
|
-
const ccy = crn.absY + crn.absH / 2
|
|
231
|
-
crn.absY = 2 * pcy - ccy - crn.absH / 2
|
|
232
|
-
const childFlip = crn.node.flipY === true
|
|
233
|
-
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;
|
|
234
282
|
}
|
|
235
|
-
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;
|
|
236
284
|
}
|
|
237
285
|
}
|
|
238
286
|
|
|
239
287
|
// Propagate parent rotation to children
|
|
240
|
-
const parentRot = node.rotation ?? 0
|
|
288
|
+
const parentRot = node.rotation ?? 0;
|
|
241
289
|
if (parentRot !== 0) {
|
|
242
|
-
const cx = absX + nodeW / 2
|
|
243
|
-
const cy = absY + nodeH / 2
|
|
244
|
-
const rad = parentRot * Math.PI / 180
|
|
245
|
-
const cosA = Math.cos(rad)
|
|
246
|
-
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);
|
|
247
295
|
|
|
248
296
|
for (const crn of childRNs) {
|
|
249
|
-
const ccx = crn.absX + crn.absW / 2
|
|
250
|
-
const ccy = crn.absY + crn.absH / 2
|
|
251
|
-
const dx = ccx - cx
|
|
252
|
-
const dy = ccy - cy
|
|
253
|
-
const newCx = cx + dx * cosA - dy * sinA
|
|
254
|
-
const newCy = cy + dx * sinA + dy * cosA
|
|
255
|
-
crn.absX = newCx - crn.absW / 2
|
|
256
|
-
crn.absY = newCy - crn.absH / 2
|
|
257
|
-
const childRot = crn.node.rotation ?? 0
|
|
258
|
-
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;
|
|
259
312
|
}
|
|
260
313
|
}
|
|
261
314
|
|
|
262
|
-
result.push(...childRNs)
|
|
315
|
+
result.push(...childRNs);
|
|
263
316
|
}
|
|
264
317
|
}
|
|
265
318
|
|
|
266
|
-
return result
|
|
319
|
+
return result;
|
|
267
320
|
}
|
|
268
321
|
|
|
269
322
|
// ---------------------------------------------------------------------------
|
|
@@ -277,46 +330,56 @@ export function resolveRefs(
|
|
|
277
330
|
findInTree?: (nodes: PenNode[], id: string) => PenNode | null,
|
|
278
331
|
visited = new Set<string>(),
|
|
279
332
|
): PenNode[] {
|
|
280
|
-
const finder = findInTree ?? ((ns: PenNode[], id: string) => findNodeInTree(ns, id) ?? null)
|
|
333
|
+
const finder = findInTree ?? ((ns: PenNode[], id: string) => findNodeInTree(ns, id) ?? null);
|
|
281
334
|
return nodes.flatMap((node) => {
|
|
282
335
|
if (node.type !== 'ref') {
|
|
283
336
|
if ('children' in node && node.children) {
|
|
284
|
-
return [
|
|
337
|
+
return [
|
|
338
|
+
{ ...node, children: resolveRefs(node.children, rootNodes, finder, visited) } as PenNode,
|
|
339
|
+
];
|
|
285
340
|
}
|
|
286
|
-
return [node]
|
|
341
|
+
return [node];
|
|
287
342
|
}
|
|
288
|
-
if (visited.has(node.ref)) return []
|
|
289
|
-
const component = finder(rootNodes, node.ref)
|
|
290
|
-
if (!component) return []
|
|
291
|
-
visited.add(node.ref)
|
|
292
|
-
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 };
|
|
293
348
|
for (const [key, val] of Object.entries(node)) {
|
|
294
|
-
if (key === 'type' || key === 'ref' || key === 'descendants' || key === 'children') continue
|
|
295
|
-
if (val !== undefined) resolved[key] = val
|
|
349
|
+
if (key === 'type' || key === 'ref' || key === 'descendants' || key === 'children') continue;
|
|
350
|
+
if (val !== undefined) resolved[key] = val;
|
|
296
351
|
}
|
|
297
|
-
resolved.type = component.type
|
|
298
|
-
if (!resolved.name) resolved.name = component.name
|
|
299
|
-
delete resolved.reusable
|
|
300
|
-
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;
|
|
301
356
|
if ('children' in component && component.children) {
|
|
302
|
-
const refNode = node as RefNode
|
|
303
|
-
|
|
357
|
+
const refNode = node as RefNode;
|
|
358
|
+
(resolvedNode as PenNode & ContainerProps).children = remapIds(
|
|
359
|
+
component.children,
|
|
360
|
+
node.id,
|
|
361
|
+
refNode.descendants,
|
|
362
|
+
);
|
|
304
363
|
}
|
|
305
|
-
visited.delete(node.ref)
|
|
306
|
-
return [resolvedNode]
|
|
307
|
-
})
|
|
364
|
+
visited.delete(node.ref);
|
|
365
|
+
return [resolvedNode];
|
|
366
|
+
});
|
|
308
367
|
}
|
|
309
368
|
|
|
310
|
-
export function remapIds(
|
|
369
|
+
export function remapIds(
|
|
370
|
+
children: PenNode[],
|
|
371
|
+
refId: string,
|
|
372
|
+
overrides?: Record<string, Partial<PenNode>>,
|
|
373
|
+
): PenNode[] {
|
|
311
374
|
return children.map((child) => {
|
|
312
|
-
const virtualId = `${refId}__${child.id}
|
|
313
|
-
const ov = overrides?.[child.id] ?? {}
|
|
314
|
-
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;
|
|
315
378
|
if ('children' in mapped && mapped.children) {
|
|
316
|
-
(mapped as PenNode & ContainerProps).children = remapIds(mapped.children, refId, overrides)
|
|
379
|
+
(mapped as PenNode & ContainerProps).children = remapIds(mapped.children, refId, overrides);
|
|
317
380
|
}
|
|
318
|
-
return mapped
|
|
319
|
-
})
|
|
381
|
+
return mapped;
|
|
382
|
+
});
|
|
320
383
|
}
|
|
321
384
|
|
|
322
385
|
// ---------------------------------------------------------------------------
|
|
@@ -326,10 +389,10 @@ export function remapIds(children: PenNode[], refId: string, overrides?: Record<
|
|
|
326
389
|
export function collectReusableIds(nodes: PenNode[], result: Set<string>) {
|
|
327
390
|
for (const node of nodes) {
|
|
328
391
|
if (node.type === 'frame' && node.reusable === true) {
|
|
329
|
-
result.add(node.id)
|
|
392
|
+
result.add(node.id);
|
|
330
393
|
}
|
|
331
394
|
if ('children' in node && node.children) {
|
|
332
|
-
collectReusableIds(node.children, result)
|
|
395
|
+
collectReusableIds(node.children, result);
|
|
333
396
|
}
|
|
334
397
|
}
|
|
335
398
|
}
|
|
@@ -337,10 +400,10 @@ export function collectReusableIds(nodes: PenNode[], result: Set<string>) {
|
|
|
337
400
|
export function collectInstanceIds(nodes: PenNode[], result: Set<string>) {
|
|
338
401
|
for (const node of nodes) {
|
|
339
402
|
if (node.type === 'ref') {
|
|
340
|
-
result.add(node.id)
|
|
403
|
+
result.add(node.id);
|
|
341
404
|
}
|
|
342
405
|
if ('children' in node && node.children) {
|
|
343
|
-
collectInstanceIds(node.children, result)
|
|
406
|
+
collectInstanceIds(node.children, result);
|
|
344
407
|
}
|
|
345
408
|
}
|
|
346
409
|
}
|