@remotion/studio-server 4.0.494 → 4.0.496
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/dist/codemods/recast-mods.js +4 -15
- package/dist/codemods/strip-parenthesized-extra.d.ts +1 -0
- package/dist/codemods/strip-parenthesized-extra.js +16 -0
- package/dist/codemods/update-keyframes/update-keyframes.js +61 -6
- package/dist/figma/figma-clipboard.d.ts +6 -0
- package/dist/figma/figma-clipboard.js +312 -0
- package/dist/figma/figma-to-svg.d.ts +116 -0
- package/dist/figma/figma-to-svg.js +1300 -0
- package/dist/helpers/resolve-composition-component.js +73 -5
- package/dist/helpers/svg-to-jsx.d.ts +2 -0
- package/dist/helpers/svg-to-jsx.js +70 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -0
- package/dist/max-timeline-tracks.d.ts +1 -0
- package/dist/max-timeline-tracks.js +5 -1
- package/dist/preview-server/api-routes.js +2 -0
- package/dist/preview-server/dev-middleware/middleware.js +6 -4
- package/dist/preview-server/routes/convert-figma-clipboard-to-svg.d.ts +3 -0
- package/dist/preview-server/routes/convert-figma-clipboard-to-svg.js +18 -0
- package/dist/preview-server/routes/delete-keyframes.js +2 -0
- package/dist/preview-server/routes/insert-jsx-element.js +10 -1
- package/dist/preview-server/start-server.d.ts +3 -3
- package/dist/preview-server/start-server.js +3 -3
- package/dist/routes.d.ts +4 -4
- package/dist/routes.js +9 -9
- package/dist/start-studio.d.ts +4 -4
- package/dist/start-studio.js +4 -4
- package/package.json +9 -6
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderFigmaMessageToSvg = exports.decodeFigmaVectorNetwork = void 0;
|
|
4
|
+
const maxVectorItems = 100000;
|
|
5
|
+
const maxRenderingDepth = 256;
|
|
6
|
+
const supportedContainers = new Set(['FRAME', 'GROUP']);
|
|
7
|
+
const fullCircle = Math.PI * 2;
|
|
8
|
+
const fullCircleTolerance = 0.00001;
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new Error(`Cannot import Figma selection: ${message}`);
|
|
11
|
+
};
|
|
12
|
+
const guidKey = (guid) => {
|
|
13
|
+
if (guid === undefined ||
|
|
14
|
+
!Number.isInteger(guid.sessionID) ||
|
|
15
|
+
!Number.isInteger(guid.localID)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return `${guid.sessionID}:${guid.localID}`;
|
|
19
|
+
};
|
|
20
|
+
const nodeLabel = (node) => {
|
|
21
|
+
var _a;
|
|
22
|
+
return node.name ? `“${node.name}”` : ((_a = node.type) !== null && _a !== void 0 ? _a : 'unknown node');
|
|
23
|
+
};
|
|
24
|
+
const finiteNumber = ({ fallback, label, value, }) => {
|
|
25
|
+
const resolved = value !== null && value !== void 0 ? value : fallback;
|
|
26
|
+
if (!Number.isFinite(resolved)) {
|
|
27
|
+
fail(`${label} is not finite`);
|
|
28
|
+
}
|
|
29
|
+
return resolved;
|
|
30
|
+
};
|
|
31
|
+
const unitInterval = ({ fallback, label, value, }) => {
|
|
32
|
+
const resolved = finiteNumber({ fallback, label, value });
|
|
33
|
+
if (resolved < 0 || resolved > 1) {
|
|
34
|
+
fail(`${label} must be between 0 and 1`);
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
};
|
|
38
|
+
const formatNumber = (value) => {
|
|
39
|
+
const normalized = Math.abs(value) < 5e-7 ? 0 : value;
|
|
40
|
+
return normalized
|
|
41
|
+
.toFixed(6)
|
|
42
|
+
.replace(/\.0+$/, '')
|
|
43
|
+
.replace(/(\.\d*?)0+$/, '$1');
|
|
44
|
+
};
|
|
45
|
+
const escapeAttribute = (value) => {
|
|
46
|
+
return value
|
|
47
|
+
.replaceAll('&', '&')
|
|
48
|
+
.replaceAll('"', '"')
|
|
49
|
+
.replaceAll('<', '<')
|
|
50
|
+
.replaceAll('>', '>');
|
|
51
|
+
};
|
|
52
|
+
const readCount = ({ data, label, offset, view, }) => {
|
|
53
|
+
if (offset + 4 > data.length) {
|
|
54
|
+
fail(`vector geometry ended while reading ${label}`);
|
|
55
|
+
}
|
|
56
|
+
const count = view.getUint32(offset, true);
|
|
57
|
+
if (count > maxVectorItems) {
|
|
58
|
+
fail(`vector geometry has too many ${label}`);
|
|
59
|
+
}
|
|
60
|
+
return count;
|
|
61
|
+
};
|
|
62
|
+
const readFloat = ({ data, label, offset, view, }) => {
|
|
63
|
+
if (offset + 4 > data.length) {
|
|
64
|
+
fail(`vector geometry ended while reading ${label}`);
|
|
65
|
+
}
|
|
66
|
+
const value = view.getFloat32(offset, true);
|
|
67
|
+
if (!Number.isFinite(value)) {
|
|
68
|
+
fail(`vector geometry contains an invalid ${label}`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
};
|
|
72
|
+
const decodeFigmaVectorNetwork = (data) => {
|
|
73
|
+
if (data.length < 12) {
|
|
74
|
+
fail('vector geometry is truncated');
|
|
75
|
+
}
|
|
76
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
77
|
+
const vertexCount = readCount({
|
|
78
|
+
data,
|
|
79
|
+
label: 'vertices',
|
|
80
|
+
offset: 0,
|
|
81
|
+
view,
|
|
82
|
+
});
|
|
83
|
+
const segmentCount = readCount({
|
|
84
|
+
data,
|
|
85
|
+
label: 'segments',
|
|
86
|
+
offset: 4,
|
|
87
|
+
view,
|
|
88
|
+
});
|
|
89
|
+
const regionCount = readCount({
|
|
90
|
+
data,
|
|
91
|
+
label: 'regions',
|
|
92
|
+
offset: 8,
|
|
93
|
+
view,
|
|
94
|
+
});
|
|
95
|
+
let itemCount = vertexCount + segmentCount + regionCount;
|
|
96
|
+
const countItems = (count) => {
|
|
97
|
+
if (count > maxVectorItems - itemCount) {
|
|
98
|
+
fail('vector geometry has too many items');
|
|
99
|
+
}
|
|
100
|
+
itemCount += count;
|
|
101
|
+
};
|
|
102
|
+
if (itemCount > maxVectorItems) {
|
|
103
|
+
fail('vector geometry has too many items');
|
|
104
|
+
}
|
|
105
|
+
const fixedLength = 12 + vertexCount * 12 + segmentCount * 28;
|
|
106
|
+
if (fixedLength > data.length) {
|
|
107
|
+
fail('vector geometry is truncated');
|
|
108
|
+
}
|
|
109
|
+
let offset = 12;
|
|
110
|
+
const vertices = [];
|
|
111
|
+
for (let index = 0; index < vertexCount; index++) {
|
|
112
|
+
offset += 4;
|
|
113
|
+
const x = readFloat({ data, label: 'vertex x', offset, view });
|
|
114
|
+
offset += 4;
|
|
115
|
+
const y = readFloat({ data, label: 'vertex y', offset, view });
|
|
116
|
+
offset += 4;
|
|
117
|
+
vertices.push({ x, y });
|
|
118
|
+
}
|
|
119
|
+
const segments = [];
|
|
120
|
+
for (let index = 0; index < segmentCount; index++) {
|
|
121
|
+
offset += 4;
|
|
122
|
+
const start = view.getUint32(offset, true);
|
|
123
|
+
offset += 4;
|
|
124
|
+
const tangentStartX = readFloat({
|
|
125
|
+
data,
|
|
126
|
+
label: 'start tangent x',
|
|
127
|
+
offset,
|
|
128
|
+
view,
|
|
129
|
+
});
|
|
130
|
+
offset += 4;
|
|
131
|
+
const tangentStartY = readFloat({
|
|
132
|
+
data,
|
|
133
|
+
label: 'start tangent y',
|
|
134
|
+
offset,
|
|
135
|
+
view,
|
|
136
|
+
});
|
|
137
|
+
offset += 4;
|
|
138
|
+
const end = view.getUint32(offset, true);
|
|
139
|
+
offset += 4;
|
|
140
|
+
const tangentEndX = readFloat({
|
|
141
|
+
data,
|
|
142
|
+
label: 'end tangent x',
|
|
143
|
+
offset,
|
|
144
|
+
view,
|
|
145
|
+
});
|
|
146
|
+
offset += 4;
|
|
147
|
+
const tangentEndY = readFloat({
|
|
148
|
+
data,
|
|
149
|
+
label: 'end tangent y',
|
|
150
|
+
offset,
|
|
151
|
+
view,
|
|
152
|
+
});
|
|
153
|
+
offset += 4;
|
|
154
|
+
if (start >= vertices.length || end >= vertices.length) {
|
|
155
|
+
fail('vector geometry references a missing vertex');
|
|
156
|
+
}
|
|
157
|
+
segments.push({
|
|
158
|
+
start,
|
|
159
|
+
end,
|
|
160
|
+
tangentStart: { x: tangentStartX, y: tangentStartY },
|
|
161
|
+
tangentEnd: { x: tangentEndX, y: tangentEndY },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const regions = [];
|
|
165
|
+
for (let regionIndex = 0; regionIndex < regionCount; regionIndex++) {
|
|
166
|
+
if (offset + 8 > data.length) {
|
|
167
|
+
fail('vector region is truncated');
|
|
168
|
+
}
|
|
169
|
+
const windingRuleValue = view.getUint32(offset, true);
|
|
170
|
+
offset += 4;
|
|
171
|
+
if (windingRuleValue !== 0 && windingRuleValue !== 1) {
|
|
172
|
+
fail('vector geometry has an unknown winding rule');
|
|
173
|
+
}
|
|
174
|
+
const loopCount = readCount({
|
|
175
|
+
data,
|
|
176
|
+
label: 'region loops',
|
|
177
|
+
offset,
|
|
178
|
+
view,
|
|
179
|
+
});
|
|
180
|
+
countItems(loopCount);
|
|
181
|
+
offset += 4;
|
|
182
|
+
const loops = [];
|
|
183
|
+
for (let loopIndex = 0; loopIndex < loopCount; loopIndex++) {
|
|
184
|
+
const segmentIndexCount = readCount({
|
|
185
|
+
data,
|
|
186
|
+
label: 'loop segments',
|
|
187
|
+
offset,
|
|
188
|
+
view,
|
|
189
|
+
});
|
|
190
|
+
countItems(segmentIndexCount);
|
|
191
|
+
offset += 4;
|
|
192
|
+
if (offset + segmentIndexCount * 4 > data.length) {
|
|
193
|
+
fail('vector loop is truncated');
|
|
194
|
+
}
|
|
195
|
+
const loop = [];
|
|
196
|
+
for (let index = 0; index < segmentIndexCount; index++) {
|
|
197
|
+
const segmentIndex = view.getUint32(offset, true);
|
|
198
|
+
offset += 4;
|
|
199
|
+
if (segmentIndex >= segments.length) {
|
|
200
|
+
fail('vector loop references a missing segment');
|
|
201
|
+
}
|
|
202
|
+
loop.push(segmentIndex);
|
|
203
|
+
}
|
|
204
|
+
loops.push(loop);
|
|
205
|
+
}
|
|
206
|
+
regions.push({
|
|
207
|
+
windingRule: windingRuleValue === 1 ? 'evenodd' : 'nonzero',
|
|
208
|
+
loops,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (offset !== data.length) {
|
|
212
|
+
fail('vector geometry uses a newer unsupported format');
|
|
213
|
+
}
|
|
214
|
+
return { vertices, segments, regions };
|
|
215
|
+
};
|
|
216
|
+
exports.decodeFigmaVectorNetwork = decodeFigmaVectorNetwork;
|
|
217
|
+
const segmentCommand = ({ network, reversed, segment, }) => {
|
|
218
|
+
const originalStart = network.vertices[segment.start];
|
|
219
|
+
const originalEnd = network.vertices[segment.end];
|
|
220
|
+
const end = reversed ? originalStart : originalEnd;
|
|
221
|
+
const control1 = reversed
|
|
222
|
+
? {
|
|
223
|
+
x: originalEnd.x + segment.tangentEnd.x,
|
|
224
|
+
y: originalEnd.y + segment.tangentEnd.y,
|
|
225
|
+
}
|
|
226
|
+
: {
|
|
227
|
+
x: originalStart.x + segment.tangentStart.x,
|
|
228
|
+
y: originalStart.y + segment.tangentStart.y,
|
|
229
|
+
};
|
|
230
|
+
const control2 = reversed
|
|
231
|
+
? {
|
|
232
|
+
x: originalStart.x + segment.tangentStart.x,
|
|
233
|
+
y: originalStart.y + segment.tangentStart.y,
|
|
234
|
+
}
|
|
235
|
+
: {
|
|
236
|
+
x: originalEnd.x + segment.tangentEnd.x,
|
|
237
|
+
y: originalEnd.y + segment.tangentEnd.y,
|
|
238
|
+
};
|
|
239
|
+
const hasCurve = segment.tangentStart.x !== 0 ||
|
|
240
|
+
segment.tangentStart.y !== 0 ||
|
|
241
|
+
segment.tangentEnd.x !== 0 ||
|
|
242
|
+
segment.tangentEnd.y !== 0;
|
|
243
|
+
if (!hasCurve) {
|
|
244
|
+
return `L ${formatNumber(end.x)} ${formatNumber(end.y)}`;
|
|
245
|
+
}
|
|
246
|
+
return `C ${formatNumber(control1.x)} ${formatNumber(control1.y)} ${formatNumber(control2.x)} ${formatNumber(control2.y)} ${formatNumber(end.x)} ${formatNumber(end.y)}`;
|
|
247
|
+
};
|
|
248
|
+
const buildOrderedPath = ({ close, network, segmentIndices, }) => {
|
|
249
|
+
if (segmentIndices.length === 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
for (const reverseFirst of [false, true]) {
|
|
253
|
+
const firstSegment = network.segments[segmentIndices[0]];
|
|
254
|
+
const startIndex = reverseFirst ? firstSegment.end : firstSegment.start;
|
|
255
|
+
let endIndex = reverseFirst ? firstSegment.start : firstSegment.end;
|
|
256
|
+
const start = network.vertices[startIndex];
|
|
257
|
+
const commands = [
|
|
258
|
+
`M ${formatNumber(start.x)} ${formatNumber(start.y)}`,
|
|
259
|
+
segmentCommand({
|
|
260
|
+
network,
|
|
261
|
+
reversed: reverseFirst,
|
|
262
|
+
segment: firstSegment,
|
|
263
|
+
}),
|
|
264
|
+
];
|
|
265
|
+
let valid = true;
|
|
266
|
+
for (let index = 1; index < segmentIndices.length; index++) {
|
|
267
|
+
const segment = network.segments[segmentIndices[index]];
|
|
268
|
+
const reversed = segment.end === endIndex;
|
|
269
|
+
if (segment.start !== endIndex && !reversed) {
|
|
270
|
+
valid = false;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
commands.push(segmentCommand({ network, reversed, segment }));
|
|
274
|
+
endIndex = reversed ? segment.start : segment.end;
|
|
275
|
+
}
|
|
276
|
+
if (valid && (!close || endIndex === startIndex)) {
|
|
277
|
+
if (close) {
|
|
278
|
+
commands.push('Z');
|
|
279
|
+
}
|
|
280
|
+
return commands.join(' ');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
};
|
|
285
|
+
const traceNetworkPaths = (network) => {
|
|
286
|
+
const degree = Array.from({ length: network.vertices.length }, () => 0);
|
|
287
|
+
for (const segment of network.segments) {
|
|
288
|
+
degree[segment.start]++;
|
|
289
|
+
degree[segment.end]++;
|
|
290
|
+
}
|
|
291
|
+
if (degree.some((value) => value > 2)) {
|
|
292
|
+
fail('branched vector networks are not supported yet');
|
|
293
|
+
}
|
|
294
|
+
const unvisited = new Set(network.segments.map((_, index) => index));
|
|
295
|
+
const paths = [];
|
|
296
|
+
while (unvisited.size > 0) {
|
|
297
|
+
let firstIndex = unvisited.values().next().value;
|
|
298
|
+
for (const candidate of unvisited) {
|
|
299
|
+
const segment = network.segments[candidate];
|
|
300
|
+
if (degree[segment.start] === 1 || degree[segment.end] === 1) {
|
|
301
|
+
firstIndex = candidate;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const first = network.segments[firstIndex];
|
|
306
|
+
const reverseFirst = degree[first.end] === 1 && degree[first.start] !== 1;
|
|
307
|
+
const startIndex = reverseFirst ? first.end : first.start;
|
|
308
|
+
let endIndex = reverseFirst ? first.start : first.end;
|
|
309
|
+
const start = network.vertices[startIndex];
|
|
310
|
+
const commands = [
|
|
311
|
+
`M ${formatNumber(start.x)} ${formatNumber(start.y)}`,
|
|
312
|
+
segmentCommand({ network, reversed: reverseFirst, segment: first }),
|
|
313
|
+
];
|
|
314
|
+
unvisited.delete(firstIndex);
|
|
315
|
+
while (true) {
|
|
316
|
+
let nextIndex = null;
|
|
317
|
+
for (const candidate of unvisited) {
|
|
318
|
+
const candidateSegment = network.segments[candidate];
|
|
319
|
+
if (candidateSegment.start === endIndex ||
|
|
320
|
+
candidateSegment.end === endIndex) {
|
|
321
|
+
nextIndex = candidate;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (nextIndex === null) {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const segment = network.segments[nextIndex];
|
|
329
|
+
const reversed = segment.end === endIndex;
|
|
330
|
+
commands.push(segmentCommand({ network, reversed, segment }));
|
|
331
|
+
endIndex = reversed ? segment.start : segment.end;
|
|
332
|
+
unvisited.delete(nextIndex);
|
|
333
|
+
}
|
|
334
|
+
const closed = endIndex === startIndex;
|
|
335
|
+
if (closed) {
|
|
336
|
+
commands.push('Z');
|
|
337
|
+
}
|
|
338
|
+
paths.push({ closed, d: commands.join(' ') });
|
|
339
|
+
}
|
|
340
|
+
return paths;
|
|
341
|
+
};
|
|
342
|
+
const getVisiblePaint = ({ kind, node, paints, }) => {
|
|
343
|
+
const visible = (paints !== null && paints !== void 0 ? paints : []).filter((candidatePaint) => candidatePaint.visible !== false);
|
|
344
|
+
if (visible.length > 1) {
|
|
345
|
+
fail(`${nodeLabel(node)} has multiple visible ${kind}s`);
|
|
346
|
+
}
|
|
347
|
+
const paint = visible[0];
|
|
348
|
+
if (paint === undefined) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (paint.type !== 'SOLID' || paint.color === undefined) {
|
|
352
|
+
fail(`${nodeLabel(node)} uses an unsupported ${kind} type`);
|
|
353
|
+
}
|
|
354
|
+
if (paint.blendMode !== undefined && paint.blendMode !== 'NORMAL') {
|
|
355
|
+
fail(`${nodeLabel(node)} uses an unsupported ${kind} blend mode`);
|
|
356
|
+
}
|
|
357
|
+
return paint;
|
|
358
|
+
};
|
|
359
|
+
const getPaintAttributes = ({ kind, paint, }) => {
|
|
360
|
+
const color = paint.color;
|
|
361
|
+
const r = Math.round(unitInterval({ fallback: 0, label: `${kind} red`, value: color.r }) * 255);
|
|
362
|
+
const g = Math.round(unitInterval({ fallback: 0, label: `${kind} green`, value: color.g }) * 255);
|
|
363
|
+
const b = Math.round(unitInterval({ fallback: 0, label: `${kind} blue`, value: color.b }) * 255);
|
|
364
|
+
const alpha = unitInterval({ fallback: 1, label: `${kind} alpha`, value: color.a }) *
|
|
365
|
+
unitInterval({
|
|
366
|
+
fallback: 1,
|
|
367
|
+
label: `${kind} opacity`,
|
|
368
|
+
value: paint.opacity,
|
|
369
|
+
});
|
|
370
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g
|
|
371
|
+
.toString(16)
|
|
372
|
+
.padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
373
|
+
const attributes = [`${kind}="${hex}"`];
|
|
374
|
+
if (alpha < 1) {
|
|
375
|
+
attributes.push(`${kind}-opacity="${formatNumber(alpha)}"`);
|
|
376
|
+
}
|
|
377
|
+
return attributes;
|
|
378
|
+
};
|
|
379
|
+
const getTransform = (node) => {
|
|
380
|
+
const { transform } = node;
|
|
381
|
+
const values = {
|
|
382
|
+
m00: finiteNumber({
|
|
383
|
+
fallback: 1,
|
|
384
|
+
label: `${nodeLabel(node)} transform`,
|
|
385
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m00,
|
|
386
|
+
}),
|
|
387
|
+
m01: finiteNumber({
|
|
388
|
+
fallback: 0,
|
|
389
|
+
label: `${nodeLabel(node)} transform`,
|
|
390
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m01,
|
|
391
|
+
}),
|
|
392
|
+
m02: finiteNumber({
|
|
393
|
+
fallback: 0,
|
|
394
|
+
label: `${nodeLabel(node)} transform`,
|
|
395
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m02,
|
|
396
|
+
}),
|
|
397
|
+
m10: finiteNumber({
|
|
398
|
+
fallback: 0,
|
|
399
|
+
label: `${nodeLabel(node)} transform`,
|
|
400
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m10,
|
|
401
|
+
}),
|
|
402
|
+
m11: finiteNumber({
|
|
403
|
+
fallback: 1,
|
|
404
|
+
label: `${nodeLabel(node)} transform`,
|
|
405
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m11,
|
|
406
|
+
}),
|
|
407
|
+
m12: finiteNumber({
|
|
408
|
+
fallback: 0,
|
|
409
|
+
label: `${nodeLabel(node)} transform`,
|
|
410
|
+
value: transform === null || transform === void 0 ? void 0 : transform.m12,
|
|
411
|
+
}),
|
|
412
|
+
};
|
|
413
|
+
const identity = values.m00 === 1 &&
|
|
414
|
+
values.m01 === 0 &&
|
|
415
|
+
values.m02 === 0 &&
|
|
416
|
+
values.m10 === 0 &&
|
|
417
|
+
values.m11 === 1 &&
|
|
418
|
+
values.m12 === 0;
|
|
419
|
+
return {
|
|
420
|
+
identity,
|
|
421
|
+
value: `matrix(${formatNumber(values.m00)} ${formatNumber(values.m10)} ${formatNumber(values.m01)} ${formatNumber(values.m11)} ${formatNumber(values.m02)} ${formatNumber(values.m12)})`,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const getNodeSize = (node) => {
|
|
425
|
+
var _a, _b;
|
|
426
|
+
const width = finiteNumber({
|
|
427
|
+
fallback: 0,
|
|
428
|
+
label: `${nodeLabel(node)} width`,
|
|
429
|
+
value: (_a = node.size) === null || _a === void 0 ? void 0 : _a.x,
|
|
430
|
+
});
|
|
431
|
+
const height = finiteNumber({
|
|
432
|
+
fallback: 0,
|
|
433
|
+
label: `${nodeLabel(node)} height`,
|
|
434
|
+
value: (_b = node.size) === null || _b === void 0 ? void 0 : _b.y,
|
|
435
|
+
});
|
|
436
|
+
if (width < 0 || height < 0) {
|
|
437
|
+
fail(`${nodeLabel(node)} has negative dimensions`);
|
|
438
|
+
}
|
|
439
|
+
return { height, width };
|
|
440
|
+
};
|
|
441
|
+
const getCornerRadius = (node) => {
|
|
442
|
+
const cornerSmoothing = finiteNumber({
|
|
443
|
+
fallback: 0,
|
|
444
|
+
label: `${nodeLabel(node)} corner smoothing`,
|
|
445
|
+
value: node.cornerSmoothing,
|
|
446
|
+
});
|
|
447
|
+
if (cornerSmoothing !== 0) {
|
|
448
|
+
fail(`${nodeLabel(node)} uses unsupported corner smoothing`);
|
|
449
|
+
}
|
|
450
|
+
const independentRadii = [
|
|
451
|
+
node.rectangleTopLeftCornerRadius,
|
|
452
|
+
node.rectangleTopRightCornerRadius,
|
|
453
|
+
node.rectangleBottomRightCornerRadius,
|
|
454
|
+
node.rectangleBottomLeftCornerRadius,
|
|
455
|
+
].map((value) => finiteNumber({
|
|
456
|
+
fallback: 0,
|
|
457
|
+
label: `${nodeLabel(node)} corner radius`,
|
|
458
|
+
value,
|
|
459
|
+
}));
|
|
460
|
+
if (node.rectangleCornerRadiiIndependent === true &&
|
|
461
|
+
independentRadii.some((corner) => corner !== 0)) {
|
|
462
|
+
fail(`${nodeLabel(node)} uses independent corner radii`);
|
|
463
|
+
}
|
|
464
|
+
const radius = finiteNumber({
|
|
465
|
+
fallback: 0,
|
|
466
|
+
label: `${nodeLabel(node)} corner radius`,
|
|
467
|
+
value: node.cornerRadius,
|
|
468
|
+
});
|
|
469
|
+
if (radius < 0) {
|
|
470
|
+
fail(`${nodeLabel(node)} has a negative corner radius`);
|
|
471
|
+
}
|
|
472
|
+
if (independentRadii.some((corner) => corner < 0)) {
|
|
473
|
+
fail(`${nodeLabel(node)} has a negative corner radius`);
|
|
474
|
+
}
|
|
475
|
+
return radius;
|
|
476
|
+
};
|
|
477
|
+
const renderFrameShape = ({ node }) => {
|
|
478
|
+
const fill = getVisiblePaint({ kind: 'fill', node, paints: node.fillPaints });
|
|
479
|
+
const stroke = getVisiblePaint({
|
|
480
|
+
kind: 'stroke',
|
|
481
|
+
node,
|
|
482
|
+
paints: node.strokePaints,
|
|
483
|
+
});
|
|
484
|
+
if (fill === null && stroke === null) {
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
const { height, width } = getNodeSize(node);
|
|
488
|
+
const radius = getCornerRadius(node);
|
|
489
|
+
const attributes = [
|
|
490
|
+
'x="0"',
|
|
491
|
+
'y="0"',
|
|
492
|
+
`width="${formatNumber(width)}"`,
|
|
493
|
+
`height="${formatNumber(height)}"`,
|
|
494
|
+
];
|
|
495
|
+
if (radius > 0) {
|
|
496
|
+
attributes.push(`rx="${formatNumber(radius)}"`);
|
|
497
|
+
}
|
|
498
|
+
if (fill) {
|
|
499
|
+
attributes.push(...getPaintAttributes({
|
|
500
|
+
kind: 'fill',
|
|
501
|
+
paint: fill,
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
attributes.push('fill="none"');
|
|
506
|
+
}
|
|
507
|
+
if (stroke) {
|
|
508
|
+
if (node.strokeAlign !== undefined && node.strokeAlign !== 'CENTER') {
|
|
509
|
+
fail(`${nodeLabel(node)} uses non-centered frame strokes`);
|
|
510
|
+
}
|
|
511
|
+
const strokeWidth = finiteNumber({
|
|
512
|
+
fallback: 1,
|
|
513
|
+
label: `${nodeLabel(node)} stroke width`,
|
|
514
|
+
value: node.strokeWeight,
|
|
515
|
+
});
|
|
516
|
+
if (strokeWidth < 0) {
|
|
517
|
+
fail(`${nodeLabel(node)} has a negative stroke width`);
|
|
518
|
+
}
|
|
519
|
+
attributes.push(...getPaintAttributes({
|
|
520
|
+
kind: 'stroke',
|
|
521
|
+
paint: stroke,
|
|
522
|
+
}), `stroke-width="${formatNumber(strokeWidth)}"`);
|
|
523
|
+
if (node.dashPattern && node.dashPattern.length > 0) {
|
|
524
|
+
const dashes = node.dashPattern.map((dash) => finiteNumber({
|
|
525
|
+
fallback: 0,
|
|
526
|
+
label: `${nodeLabel(node)} stroke dash`,
|
|
527
|
+
value: dash,
|
|
528
|
+
}));
|
|
529
|
+
if (dashes.some((dash) => dash < 0)) {
|
|
530
|
+
fail(`${nodeLabel(node)} has a negative stroke dash`);
|
|
531
|
+
}
|
|
532
|
+
attributes.push(`stroke-dasharray="${dashes.map(formatNumber).join(' ')}"`);
|
|
533
|
+
}
|
|
534
|
+
const miterLimit = finiteNumber({
|
|
535
|
+
fallback: 4,
|
|
536
|
+
label: `${nodeLabel(node)} miter limit`,
|
|
537
|
+
value: node.miterLimit,
|
|
538
|
+
});
|
|
539
|
+
if (miterLimit <= 0) {
|
|
540
|
+
fail(`${nodeLabel(node)} has an invalid miter limit`);
|
|
541
|
+
}
|
|
542
|
+
if (miterLimit !== 4) {
|
|
543
|
+
attributes.push(`stroke-miterlimit="${formatNumber(miterLimit)}"`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return `<rect ${attributes.join(' ')} />`;
|
|
547
|
+
};
|
|
548
|
+
const pointOnEllipse = ({ angle, centerX, centerY, radiusX, radiusY, scale, }) => {
|
|
549
|
+
return {
|
|
550
|
+
x: centerX + radiusX * scale * Math.cos(angle),
|
|
551
|
+
y: centerY + radiusY * scale * Math.sin(angle),
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
const formatPoint = (point) => {
|
|
555
|
+
return `${formatNumber(point.x)} ${formatNumber(point.y)}`;
|
|
556
|
+
};
|
|
557
|
+
const getEllipseGeometry = (node) => {
|
|
558
|
+
var _a, _b, _c;
|
|
559
|
+
const { height, width } = getNodeSize(node);
|
|
560
|
+
if (width === 0 || height === 0) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const centerX = width / 2;
|
|
564
|
+
const centerY = height / 2;
|
|
565
|
+
const radiusX = width / 2;
|
|
566
|
+
const radiusY = height / 2;
|
|
567
|
+
const startingAngle = finiteNumber({
|
|
568
|
+
fallback: 0,
|
|
569
|
+
label: `${nodeLabel(node)} starting angle`,
|
|
570
|
+
value: (_a = node.arcData) === null || _a === void 0 ? void 0 : _a.startingAngle,
|
|
571
|
+
});
|
|
572
|
+
const endingAngle = finiteNumber({
|
|
573
|
+
fallback: fullCircle,
|
|
574
|
+
label: `${nodeLabel(node)} ending angle`,
|
|
575
|
+
value: (_b = node.arcData) === null || _b === void 0 ? void 0 : _b.endingAngle,
|
|
576
|
+
});
|
|
577
|
+
const innerRadius = unitInterval({
|
|
578
|
+
fallback: 0,
|
|
579
|
+
label: `${nodeLabel(node)} inner radius`,
|
|
580
|
+
value: (_c = node.arcData) === null || _c === void 0 ? void 0 : _c.innerRadius,
|
|
581
|
+
});
|
|
582
|
+
const rawSweep = finiteNumber({
|
|
583
|
+
fallback: 0,
|
|
584
|
+
label: `${nodeLabel(node)} arc sweep`,
|
|
585
|
+
value: endingAngle - startingAngle,
|
|
586
|
+
});
|
|
587
|
+
const isFullCircle = Math.abs(rawSweep) >= fullCircle - fullCircleTolerance;
|
|
588
|
+
if (isFullCircle && innerRadius === 0) {
|
|
589
|
+
return {
|
|
590
|
+
attributes: [
|
|
591
|
+
`cx="${formatNumber(centerX)}"`,
|
|
592
|
+
`cy="${formatNumber(centerY)}"`,
|
|
593
|
+
`rx="${formatNumber(radiusX)}"`,
|
|
594
|
+
`ry="${formatNumber(radiusY)}"`,
|
|
595
|
+
],
|
|
596
|
+
fillRule: null,
|
|
597
|
+
tagName: 'ellipse',
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
let sweep = rawSweep % fullCircle;
|
|
601
|
+
if (sweep < 0) {
|
|
602
|
+
sweep += fullCircle;
|
|
603
|
+
}
|
|
604
|
+
if (!isFullCircle && sweep < fullCircleTolerance) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const outerStart = pointOnEllipse({
|
|
608
|
+
angle: startingAngle,
|
|
609
|
+
centerX,
|
|
610
|
+
centerY,
|
|
611
|
+
radiusX,
|
|
612
|
+
radiusY,
|
|
613
|
+
scale: 1,
|
|
614
|
+
});
|
|
615
|
+
if (isFullCircle) {
|
|
616
|
+
const outerOpposite = pointOnEllipse({
|
|
617
|
+
angle: startingAngle + Math.PI,
|
|
618
|
+
centerX,
|
|
619
|
+
centerY,
|
|
620
|
+
radiusX,
|
|
621
|
+
radiusY,
|
|
622
|
+
scale: 1,
|
|
623
|
+
});
|
|
624
|
+
const fullInnerStart = pointOnEllipse({
|
|
625
|
+
angle: startingAngle,
|
|
626
|
+
centerX,
|
|
627
|
+
centerY,
|
|
628
|
+
radiusX,
|
|
629
|
+
radiusY,
|
|
630
|
+
scale: innerRadius,
|
|
631
|
+
});
|
|
632
|
+
const innerOpposite = pointOnEllipse({
|
|
633
|
+
angle: startingAngle + Math.PI,
|
|
634
|
+
centerX,
|
|
635
|
+
centerY,
|
|
636
|
+
radiusX,
|
|
637
|
+
radiusY,
|
|
638
|
+
scale: innerRadius,
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
attributes: [
|
|
642
|
+
`d="M ${formatPoint(outerStart)} A ${formatNumber(radiusX)} ${formatNumber(radiusY)} 0 0 1 ${formatPoint(outerOpposite)} A ${formatNumber(radiusX)} ${formatNumber(radiusY)} 0 0 1 ${formatPoint(outerStart)} Z M ${formatPoint(fullInnerStart)} A ${formatNumber(radiusX * innerRadius)} ${formatNumber(radiusY * innerRadius)} 0 0 0 ${formatPoint(innerOpposite)} A ${formatNumber(radiusX * innerRadius)} ${formatNumber(radiusY * innerRadius)} 0 0 0 ${formatPoint(fullInnerStart)} Z"`,
|
|
643
|
+
],
|
|
644
|
+
fillRule: 'evenodd',
|
|
645
|
+
tagName: 'path',
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const endingAngleAfterWrap = startingAngle + sweep;
|
|
649
|
+
const outerEnd = pointOnEllipse({
|
|
650
|
+
angle: endingAngleAfterWrap,
|
|
651
|
+
centerX,
|
|
652
|
+
centerY,
|
|
653
|
+
radiusX,
|
|
654
|
+
radiusY,
|
|
655
|
+
scale: 1,
|
|
656
|
+
});
|
|
657
|
+
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
658
|
+
if (innerRadius === 0) {
|
|
659
|
+
return {
|
|
660
|
+
attributes: [
|
|
661
|
+
`d="M ${formatNumber(centerX)} ${formatNumber(centerY)} L ${formatPoint(outerStart)} A ${formatNumber(radiusX)} ${formatNumber(radiusY)} 0 ${largeArc} 1 ${formatPoint(outerEnd)} Z"`,
|
|
662
|
+
],
|
|
663
|
+
fillRule: null,
|
|
664
|
+
tagName: 'path',
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const innerEnd = pointOnEllipse({
|
|
668
|
+
angle: endingAngleAfterWrap,
|
|
669
|
+
centerX,
|
|
670
|
+
centerY,
|
|
671
|
+
radiusX,
|
|
672
|
+
radiusY,
|
|
673
|
+
scale: innerRadius,
|
|
674
|
+
});
|
|
675
|
+
const innerStart = pointOnEllipse({
|
|
676
|
+
angle: startingAngle,
|
|
677
|
+
centerX,
|
|
678
|
+
centerY,
|
|
679
|
+
radiusX,
|
|
680
|
+
radiusY,
|
|
681
|
+
scale: innerRadius,
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
attributes: [
|
|
685
|
+
`d="M ${formatPoint(outerStart)} A ${formatNumber(radiusX)} ${formatNumber(radiusY)} 0 ${largeArc} 1 ${formatPoint(outerEnd)} L ${formatPoint(innerEnd)} A ${formatNumber(radiusX * innerRadius)} ${formatNumber(radiusY * innerRadius)} 0 ${largeArc} 0 ${formatPoint(innerStart)} Z"`,
|
|
686
|
+
],
|
|
687
|
+
fillRule: 'evenodd',
|
|
688
|
+
tagName: 'path',
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
const getRoundedRectangleGeometry = (node) => {
|
|
692
|
+
const { height, width } = getNodeSize(node);
|
|
693
|
+
if (width === 0 || height === 0) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const cornerSmoothing = finiteNumber({
|
|
697
|
+
fallback: 0,
|
|
698
|
+
label: `${nodeLabel(node)} corner smoothing`,
|
|
699
|
+
value: node.cornerSmoothing,
|
|
700
|
+
});
|
|
701
|
+
if (cornerSmoothing !== 0) {
|
|
702
|
+
fail(`${nodeLabel(node)} uses unsupported corner smoothing`);
|
|
703
|
+
}
|
|
704
|
+
const uniformRadius = finiteNumber({
|
|
705
|
+
fallback: 0,
|
|
706
|
+
label: `${nodeLabel(node)} corner radius`,
|
|
707
|
+
value: node.cornerRadius,
|
|
708
|
+
});
|
|
709
|
+
const rawRadii = node.rectangleCornerRadiiIndependent === true
|
|
710
|
+
? [
|
|
711
|
+
node.rectangleTopLeftCornerRadius,
|
|
712
|
+
node.rectangleTopRightCornerRadius,
|
|
713
|
+
node.rectangleBottomRightCornerRadius,
|
|
714
|
+
node.rectangleBottomLeftCornerRadius,
|
|
715
|
+
].map((value) => finiteNumber({
|
|
716
|
+
fallback: uniformRadius,
|
|
717
|
+
label: `${nodeLabel(node)} corner radius`,
|
|
718
|
+
value,
|
|
719
|
+
}))
|
|
720
|
+
: [uniformRadius, uniformRadius, uniformRadius, uniformRadius];
|
|
721
|
+
if (rawRadii.some((radius) => radius < 0)) {
|
|
722
|
+
fail(`${nodeLabel(node)} has a negative corner radius`);
|
|
723
|
+
}
|
|
724
|
+
const maxRadius = Math.min(width / 2, height / 2);
|
|
725
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = rawRadii.map((radius) => Math.min(radius, maxRadius));
|
|
726
|
+
const baseAttributes = [
|
|
727
|
+
'x="0"',
|
|
728
|
+
'y="0"',
|
|
729
|
+
`width="${formatNumber(width)}"`,
|
|
730
|
+
`height="${formatNumber(height)}"`,
|
|
731
|
+
];
|
|
732
|
+
if (topLeft === topRight &&
|
|
733
|
+
topLeft === bottomRight &&
|
|
734
|
+
topLeft === bottomLeft) {
|
|
735
|
+
if (topLeft > 0) {
|
|
736
|
+
baseAttributes.push(`rx="${formatNumber(topLeft)}"`);
|
|
737
|
+
}
|
|
738
|
+
return { attributes: baseAttributes, fillRule: null, tagName: 'rect' };
|
|
739
|
+
}
|
|
740
|
+
const corner = ({ radius, x, y }) => {
|
|
741
|
+
return radius === 0
|
|
742
|
+
? `L ${formatNumber(x)} ${formatNumber(y)}`
|
|
743
|
+
: `A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 1 ${formatNumber(x)} ${formatNumber(y)}`;
|
|
744
|
+
};
|
|
745
|
+
return {
|
|
746
|
+
attributes: [
|
|
747
|
+
`d="M ${formatNumber(topLeft)} 0 H ${formatNumber(width - topRight)} ${corner({ radius: topRight, x: width, y: topRight })} V ${formatNumber(height - bottomRight)} ${corner({ radius: bottomRight, x: width - bottomRight, y: height })} H ${formatNumber(bottomLeft)} ${corner({ radius: bottomLeft, x: 0, y: height - bottomLeft })} V ${formatNumber(topLeft)} ${corner({ radius: topLeft, x: topLeft, y: 0 })} Z"`,
|
|
748
|
+
],
|
|
749
|
+
fillRule: null,
|
|
750
|
+
tagName: 'path',
|
|
751
|
+
};
|
|
752
|
+
};
|
|
753
|
+
const renderShapeGeometry = ({ attributes, geometry, useClipRule, }) => {
|
|
754
|
+
const fillRule = geometry.fillRule
|
|
755
|
+
? [`${useClipRule ? 'clip-rule' : 'fill-rule'}="${geometry.fillRule}"`]
|
|
756
|
+
: [];
|
|
757
|
+
return `<${geometry.tagName} ${[...geometry.attributes, ...attributes, ...fillRule].join(' ')} />`;
|
|
758
|
+
};
|
|
759
|
+
const getShapeStrokeAttributes = ({ node, paint, strokeWidth, }) => {
|
|
760
|
+
const cap = node.strokeCap === undefined || node.strokeCap === 'NONE'
|
|
761
|
+
? 'butt'
|
|
762
|
+
: node.strokeCap === 'ROUND'
|
|
763
|
+
? 'round'
|
|
764
|
+
: node.strokeCap === 'SQUARE'
|
|
765
|
+
? 'square'
|
|
766
|
+
: null;
|
|
767
|
+
if (cap === null) {
|
|
768
|
+
fail(`${nodeLabel(node)} uses an unsupported stroke cap`);
|
|
769
|
+
}
|
|
770
|
+
const join = node.strokeJoin === undefined || node.strokeJoin === 'MITER'
|
|
771
|
+
? 'miter'
|
|
772
|
+
: node.strokeJoin === 'ROUND'
|
|
773
|
+
? 'round'
|
|
774
|
+
: node.strokeJoin === 'BEVEL'
|
|
775
|
+
? 'bevel'
|
|
776
|
+
: null;
|
|
777
|
+
if (join === null) {
|
|
778
|
+
fail(`${nodeLabel(node)} uses an unsupported stroke join`);
|
|
779
|
+
}
|
|
780
|
+
const miterLimit = finiteNumber({
|
|
781
|
+
fallback: 4,
|
|
782
|
+
label: `${nodeLabel(node)} miter limit`,
|
|
783
|
+
value: node.miterLimit,
|
|
784
|
+
});
|
|
785
|
+
if (miterLimit <= 0) {
|
|
786
|
+
fail(`${nodeLabel(node)} has an invalid miter limit`);
|
|
787
|
+
}
|
|
788
|
+
const attributes = [
|
|
789
|
+
'fill="none"',
|
|
790
|
+
...getPaintAttributes({
|
|
791
|
+
kind: 'stroke',
|
|
792
|
+
paint,
|
|
793
|
+
}),
|
|
794
|
+
`stroke-width="${formatNumber(strokeWidth)}"`,
|
|
795
|
+
`stroke-linecap="${cap}"`,
|
|
796
|
+
`stroke-linejoin="${join}"`,
|
|
797
|
+
];
|
|
798
|
+
if (miterLimit !== 4) {
|
|
799
|
+
attributes.push(`stroke-miterlimit="${formatNumber(miterLimit)}"`);
|
|
800
|
+
}
|
|
801
|
+
if (node.dashPattern && node.dashPattern.length > 0) {
|
|
802
|
+
const dashes = node.dashPattern.map((dash) => finiteNumber({
|
|
803
|
+
fallback: 0,
|
|
804
|
+
label: `${nodeLabel(node)} stroke dash`,
|
|
805
|
+
value: dash,
|
|
806
|
+
}));
|
|
807
|
+
if (dashes.some((dash) => dash < 0)) {
|
|
808
|
+
fail(`${nodeLabel(node)} has a negative stroke dash`);
|
|
809
|
+
}
|
|
810
|
+
attributes.push(`stroke-dasharray="${dashes.map(formatNumber).join(' ')}"`);
|
|
811
|
+
}
|
|
812
|
+
return attributes;
|
|
813
|
+
};
|
|
814
|
+
const getShapeStrokeWidth = (node) => {
|
|
815
|
+
const strokeWidth = finiteNumber({
|
|
816
|
+
fallback: 1,
|
|
817
|
+
label: `${nodeLabel(node)} stroke width`,
|
|
818
|
+
value: node.strokeWeight,
|
|
819
|
+
});
|
|
820
|
+
if (strokeWidth < 0) {
|
|
821
|
+
fail(`${nodeLabel(node)} has a negative stroke width`);
|
|
822
|
+
}
|
|
823
|
+
if (node.borderStrokeWeightsIndependent !== true) {
|
|
824
|
+
return strokeWidth;
|
|
825
|
+
}
|
|
826
|
+
const borderWidths = [
|
|
827
|
+
node.borderTopWeight,
|
|
828
|
+
node.borderRightWeight,
|
|
829
|
+
node.borderBottomWeight,
|
|
830
|
+
node.borderLeftWeight,
|
|
831
|
+
].map((value) => finiteNumber({
|
|
832
|
+
fallback: strokeWidth,
|
|
833
|
+
label: `${nodeLabel(node)} border width`,
|
|
834
|
+
value,
|
|
835
|
+
}));
|
|
836
|
+
if (borderWidths.some((width) => width < 0)) {
|
|
837
|
+
fail(`${nodeLabel(node)} has a negative border width`);
|
|
838
|
+
}
|
|
839
|
+
if (borderWidths.some((width) => width !== borderWidths[0])) {
|
|
840
|
+
fail(`${nodeLabel(node)} uses unsupported independent stroke weights`);
|
|
841
|
+
}
|
|
842
|
+
return borderWidths[0];
|
|
843
|
+
};
|
|
844
|
+
const renderPaintedShape = ({ context, getGeometry, idPrefix, node, shapeLabel, }) => {
|
|
845
|
+
var _a;
|
|
846
|
+
const fill = getVisiblePaint({ kind: 'fill', node, paints: node.fillPaints });
|
|
847
|
+
const stroke = getVisiblePaint({
|
|
848
|
+
kind: 'stroke',
|
|
849
|
+
node,
|
|
850
|
+
paints: node.strokePaints,
|
|
851
|
+
});
|
|
852
|
+
if (fill === null && stroke === null) {
|
|
853
|
+
return '';
|
|
854
|
+
}
|
|
855
|
+
const geometry = getGeometry();
|
|
856
|
+
if (geometry === null) {
|
|
857
|
+
return '';
|
|
858
|
+
}
|
|
859
|
+
const fillAttributes = fill
|
|
860
|
+
? getPaintAttributes({
|
|
861
|
+
kind: 'fill',
|
|
862
|
+
paint: fill,
|
|
863
|
+
})
|
|
864
|
+
: ['fill="none"'];
|
|
865
|
+
if (stroke === null) {
|
|
866
|
+
return renderShapeGeometry({
|
|
867
|
+
attributes: fillAttributes,
|
|
868
|
+
geometry,
|
|
869
|
+
useClipRule: false,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const strokeWidth = getShapeStrokeWidth(node);
|
|
873
|
+
const strokeAlign = (_a = node.strokeAlign) !== null && _a !== void 0 ? _a : 'CENTER';
|
|
874
|
+
if (strokeAlign !== 'CENTER' && strokeAlign !== 'INSIDE') {
|
|
875
|
+
fail(`${nodeLabel(node)} uses an unsupported ${shapeLabel} stroke alignment`);
|
|
876
|
+
}
|
|
877
|
+
if (strokeAlign === 'CENTER') {
|
|
878
|
+
return renderShapeGeometry({
|
|
879
|
+
attributes: [
|
|
880
|
+
...fillAttributes,
|
|
881
|
+
...getShapeStrokeAttributes({
|
|
882
|
+
node,
|
|
883
|
+
paint: stroke,
|
|
884
|
+
strokeWidth,
|
|
885
|
+
}).filter((attribute) => attribute !== 'fill="none"'),
|
|
886
|
+
],
|
|
887
|
+
geometry,
|
|
888
|
+
useClipRule: false,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
const id = `figma-${idPrefix}-stroke-${context.nextId++}`;
|
|
892
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse">${renderShapeGeometry({ attributes: [], geometry, useClipRule: true })}</clipPath>`);
|
|
893
|
+
const fillMarkup = fill
|
|
894
|
+
? renderShapeGeometry({
|
|
895
|
+
attributes: fillAttributes,
|
|
896
|
+
geometry,
|
|
897
|
+
useClipRule: false,
|
|
898
|
+
})
|
|
899
|
+
: '';
|
|
900
|
+
const strokeMarkup = renderShapeGeometry({
|
|
901
|
+
attributes: getShapeStrokeAttributes({
|
|
902
|
+
node,
|
|
903
|
+
paint: stroke,
|
|
904
|
+
strokeWidth: strokeWidth * 2,
|
|
905
|
+
}),
|
|
906
|
+
geometry,
|
|
907
|
+
useClipRule: false,
|
|
908
|
+
});
|
|
909
|
+
return [fillMarkup, `<g clip-path="url(#${id})">${strokeMarkup}</g>`]
|
|
910
|
+
.filter(Boolean)
|
|
911
|
+
.join('\n');
|
|
912
|
+
};
|
|
913
|
+
const renderEllipse = ({ context, node, }) => {
|
|
914
|
+
return renderPaintedShape({
|
|
915
|
+
context,
|
|
916
|
+
getGeometry: () => getEllipseGeometry(node),
|
|
917
|
+
idPrefix: 'ellipse',
|
|
918
|
+
node,
|
|
919
|
+
shapeLabel: 'ellipse',
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
const renderRoundedRectangle = ({ context, node, }) => {
|
|
923
|
+
return renderPaintedShape({
|
|
924
|
+
context,
|
|
925
|
+
getGeometry: () => getRoundedRectangleGeometry(node),
|
|
926
|
+
idPrefix: 'rounded-rectangle',
|
|
927
|
+
node,
|
|
928
|
+
shapeLabel: 'rounded rectangle',
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
const getVectorNetwork = ({ context, node, }) => {
|
|
932
|
+
var _a, _b;
|
|
933
|
+
const blobIndex = (_a = node.vectorData) === null || _a === void 0 ? void 0 : _a.vectorNetworkBlob;
|
|
934
|
+
if (typeof blobIndex !== 'number' || !Number.isInteger(blobIndex)) {
|
|
935
|
+
fail(`${nodeLabel(node)} has no supported vector geometry`);
|
|
936
|
+
}
|
|
937
|
+
const blob = context.scene.blobs[blobIndex];
|
|
938
|
+
if (blob === undefined) {
|
|
939
|
+
fail(`${nodeLabel(node)} references missing vector geometry`);
|
|
940
|
+
}
|
|
941
|
+
const network = (0, exports.decodeFigmaVectorNetwork)(blob);
|
|
942
|
+
const normalizedSize = (_b = node.vectorData) === null || _b === void 0 ? void 0 : _b.normalizedSize;
|
|
943
|
+
const { height, width } = getNodeSize(node);
|
|
944
|
+
if (normalizedSize !== undefined) {
|
|
945
|
+
const normalizedWidth = finiteNumber({
|
|
946
|
+
fallback: width,
|
|
947
|
+
label: `${nodeLabel(node)} normalized width`,
|
|
948
|
+
value: normalizedSize.x,
|
|
949
|
+
});
|
|
950
|
+
const normalizedHeight = finiteNumber({
|
|
951
|
+
fallback: height,
|
|
952
|
+
label: `${nodeLabel(node)} normalized height`,
|
|
953
|
+
value: normalizedSize.y,
|
|
954
|
+
});
|
|
955
|
+
const scaleX = normalizedWidth === 0 ? 1 : width / normalizedWidth;
|
|
956
|
+
const scaleY = normalizedHeight === 0 ? 1 : height / normalizedHeight;
|
|
957
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
958
|
+
for (const vertex of network.vertices) {
|
|
959
|
+
vertex.x *= scaleX;
|
|
960
|
+
vertex.y *= scaleY;
|
|
961
|
+
}
|
|
962
|
+
for (const segment of network.segments) {
|
|
963
|
+
segment.tangentStart.x *= scaleX;
|
|
964
|
+
segment.tangentStart.y *= scaleY;
|
|
965
|
+
segment.tangentEnd.x *= scaleX;
|
|
966
|
+
segment.tangentEnd.y *= scaleY;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return network;
|
|
971
|
+
};
|
|
972
|
+
const getFillPaths = (network) => {
|
|
973
|
+
if (network.regions.length === 0) {
|
|
974
|
+
return [
|
|
975
|
+
{
|
|
976
|
+
d: traceNetworkPaths(network)
|
|
977
|
+
.map(({ closed, d }) => (closed ? d : `${d} Z`))
|
|
978
|
+
.join(' '),
|
|
979
|
+
windingRule: 'nonzero',
|
|
980
|
+
},
|
|
981
|
+
];
|
|
982
|
+
}
|
|
983
|
+
return network.regions.map((region) => {
|
|
984
|
+
const paths = region.loops.map((loop) => {
|
|
985
|
+
const path = buildOrderedPath({
|
|
986
|
+
close: true,
|
|
987
|
+
network,
|
|
988
|
+
segmentIndices: loop,
|
|
989
|
+
});
|
|
990
|
+
if (path === null) {
|
|
991
|
+
fail('a vector fill loop is disconnected');
|
|
992
|
+
}
|
|
993
|
+
return path;
|
|
994
|
+
});
|
|
995
|
+
return { d: paths.join(' '), windingRule: region.windingRule };
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
const renderVector = ({ context, node, }) => {
|
|
999
|
+
const fill = getVisiblePaint({ kind: 'fill', node, paints: node.fillPaints });
|
|
1000
|
+
const stroke = getVisiblePaint({
|
|
1001
|
+
kind: 'stroke',
|
|
1002
|
+
node,
|
|
1003
|
+
paints: node.strokePaints,
|
|
1004
|
+
});
|
|
1005
|
+
if (fill === null && stroke === null) {
|
|
1006
|
+
return '';
|
|
1007
|
+
}
|
|
1008
|
+
const network = getVectorNetwork({ context, node });
|
|
1009
|
+
const paths = [];
|
|
1010
|
+
if (fill) {
|
|
1011
|
+
for (const fillPath of getFillPaths(network)) {
|
|
1012
|
+
paths.push(`<path d="${escapeAttribute(fillPath.d)}" ${getPaintAttributes({ kind: 'fill', paint: fill }).join(' ')} fill-rule="${fillPath.windingRule}" />`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (stroke) {
|
|
1016
|
+
if (node.strokeAlign !== undefined && node.strokeAlign !== 'CENTER') {
|
|
1017
|
+
fail(`${nodeLabel(node)} uses non-centered vector strokes`);
|
|
1018
|
+
}
|
|
1019
|
+
const strokeWidth = finiteNumber({
|
|
1020
|
+
fallback: 1,
|
|
1021
|
+
label: `${nodeLabel(node)} stroke width`,
|
|
1022
|
+
value: node.strokeWeight,
|
|
1023
|
+
});
|
|
1024
|
+
if (strokeWidth < 0) {
|
|
1025
|
+
fail(`${nodeLabel(node)} has a negative stroke width`);
|
|
1026
|
+
}
|
|
1027
|
+
const cap = node.strokeCap === undefined || node.strokeCap === 'NONE'
|
|
1028
|
+
? 'butt'
|
|
1029
|
+
: node.strokeCap === 'ROUND'
|
|
1030
|
+
? 'round'
|
|
1031
|
+
: node.strokeCap === 'SQUARE'
|
|
1032
|
+
? 'square'
|
|
1033
|
+
: null;
|
|
1034
|
+
if (cap === null) {
|
|
1035
|
+
fail(`${nodeLabel(node)} uses an unsupported stroke cap`);
|
|
1036
|
+
}
|
|
1037
|
+
const join = node.strokeJoin === undefined || node.strokeJoin === 'MITER'
|
|
1038
|
+
? 'miter'
|
|
1039
|
+
: node.strokeJoin === 'ROUND'
|
|
1040
|
+
? 'round'
|
|
1041
|
+
: node.strokeJoin === 'BEVEL'
|
|
1042
|
+
? 'bevel'
|
|
1043
|
+
: null;
|
|
1044
|
+
if (join === null) {
|
|
1045
|
+
fail(`${nodeLabel(node)} uses an unsupported stroke join`);
|
|
1046
|
+
}
|
|
1047
|
+
const miterLimit = finiteNumber({
|
|
1048
|
+
fallback: 4,
|
|
1049
|
+
label: `${nodeLabel(node)} miter limit`,
|
|
1050
|
+
value: node.miterLimit,
|
|
1051
|
+
});
|
|
1052
|
+
if (miterLimit <= 0) {
|
|
1053
|
+
fail(`${nodeLabel(node)} has an invalid miter limit`);
|
|
1054
|
+
}
|
|
1055
|
+
const strokePaths = traceNetworkPaths(network);
|
|
1056
|
+
for (const { d } of strokePaths) {
|
|
1057
|
+
const attributes = [
|
|
1058
|
+
`d="${escapeAttribute(d)}"`,
|
|
1059
|
+
'fill="none"',
|
|
1060
|
+
...getPaintAttributes({
|
|
1061
|
+
kind: 'stroke',
|
|
1062
|
+
paint: stroke,
|
|
1063
|
+
}),
|
|
1064
|
+
`stroke-width="${formatNumber(strokeWidth)}"`,
|
|
1065
|
+
`stroke-linecap="${cap}"`,
|
|
1066
|
+
`stroke-linejoin="${join}"`,
|
|
1067
|
+
];
|
|
1068
|
+
if (miterLimit !== 4) {
|
|
1069
|
+
attributes.push(`stroke-miterlimit="${formatNumber(miterLimit)}"`);
|
|
1070
|
+
}
|
|
1071
|
+
if (node.dashPattern && node.dashPattern.length > 0) {
|
|
1072
|
+
const dashes = node.dashPattern.map((dash) => finiteNumber({
|
|
1073
|
+
fallback: 0,
|
|
1074
|
+
label: `${nodeLabel(node)} stroke dash`,
|
|
1075
|
+
value: dash,
|
|
1076
|
+
}));
|
|
1077
|
+
if (dashes.some((dash) => dash < 0)) {
|
|
1078
|
+
fail(`${nodeLabel(node)} has a negative stroke dash`);
|
|
1079
|
+
}
|
|
1080
|
+
attributes.push(`stroke-dasharray="${dashes.map(formatNumber).join(' ')}"`);
|
|
1081
|
+
}
|
|
1082
|
+
paths.push(`<path ${attributes.join(' ')} />`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return paths.join('\n');
|
|
1086
|
+
};
|
|
1087
|
+
const validateNodeFeatures = (node) => {
|
|
1088
|
+
var _a, _b;
|
|
1089
|
+
if (((_a = node.backgroundPaints) === null || _a === void 0 ? void 0 : _a.some((paint) => paint.visible !== false)) ||
|
|
1090
|
+
((_b = node.effects) === null || _b === void 0 ? void 0 : _b.some((effect) => effect.visible !== false)) ||
|
|
1091
|
+
(node.blendMode !== undefined &&
|
|
1092
|
+
node.blendMode !== 'NORMAL' &&
|
|
1093
|
+
node.blendMode !== 'PASS_THROUGH')) {
|
|
1094
|
+
fail(`${nodeLabel(node)} uses unsupported effects or blending`);
|
|
1095
|
+
}
|
|
1096
|
+
unitInterval({
|
|
1097
|
+
fallback: 1,
|
|
1098
|
+
label: `${nodeLabel(node)} opacity`,
|
|
1099
|
+
value: node.opacity,
|
|
1100
|
+
});
|
|
1101
|
+
};
|
|
1102
|
+
const renderChildren = ({ context, node, }) => {
|
|
1103
|
+
var _a;
|
|
1104
|
+
const children = (_a = context.scene.children.get(node)) !== null && _a !== void 0 ? _a : [];
|
|
1105
|
+
return children
|
|
1106
|
+
.map((child) => renderNode({
|
|
1107
|
+
context,
|
|
1108
|
+
includeTransform: true,
|
|
1109
|
+
node: child,
|
|
1110
|
+
}))
|
|
1111
|
+
.filter(Boolean)
|
|
1112
|
+
.join('\n');
|
|
1113
|
+
};
|
|
1114
|
+
const renderContainerChildren = ({ childrenMarkup, context, node, }) => {
|
|
1115
|
+
if (childrenMarkup === '' ||
|
|
1116
|
+
node.frameMaskDisabled !== false ||
|
|
1117
|
+
// Figma serializes auto-bounds groups as frames with resizeToFit. Their
|
|
1118
|
+
// bounds describe their children; they are not clipping rectangles.
|
|
1119
|
+
node.resizeToFit === true) {
|
|
1120
|
+
return childrenMarkup;
|
|
1121
|
+
}
|
|
1122
|
+
const { height, width } = getNodeSize(node);
|
|
1123
|
+
if (width === 0 || height === 0) {
|
|
1124
|
+
return childrenMarkup;
|
|
1125
|
+
}
|
|
1126
|
+
const id = `figma-clip-${context.nextId++}`;
|
|
1127
|
+
const radius = getCornerRadius(node);
|
|
1128
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><rect x="0" y="0" width="${formatNumber(width)}" height="${formatNumber(height)}"${radius > 0 ? ` rx="${formatNumber(radius)}"` : ''} /></clipPath>`);
|
|
1129
|
+
return `<g clip-path="url(#${id})">${childrenMarkup}</g>`;
|
|
1130
|
+
};
|
|
1131
|
+
const renderNode = ({ context, includeTransform, node, }) => {
|
|
1132
|
+
var _a, _b, _c;
|
|
1133
|
+
if (node.visible === false) {
|
|
1134
|
+
return '';
|
|
1135
|
+
}
|
|
1136
|
+
if (context.renderingDepth >= maxRenderingDepth) {
|
|
1137
|
+
fail('clipboard scene is nested too deeply');
|
|
1138
|
+
}
|
|
1139
|
+
context.renderingDepth++;
|
|
1140
|
+
try {
|
|
1141
|
+
validateNodeFeatures(node);
|
|
1142
|
+
let content = '';
|
|
1143
|
+
if (supportedContainers.has((_a = node.type) !== null && _a !== void 0 ? _a : '')) {
|
|
1144
|
+
const childrenMarkup = renderContainerChildren({
|
|
1145
|
+
childrenMarkup: renderChildren({ context, node }),
|
|
1146
|
+
context,
|
|
1147
|
+
node,
|
|
1148
|
+
});
|
|
1149
|
+
content = [renderFrameShape({ node }), childrenMarkup]
|
|
1150
|
+
.filter(Boolean)
|
|
1151
|
+
.join('\n');
|
|
1152
|
+
}
|
|
1153
|
+
else if (node.type === 'VECTOR' ||
|
|
1154
|
+
node.type === 'ELLIPSE' ||
|
|
1155
|
+
node.type === 'ROUNDED_RECTANGLE') {
|
|
1156
|
+
if (((_b = context.scene.children.get(node)) !== null && _b !== void 0 ? _b : []).length > 0) {
|
|
1157
|
+
fail(`${nodeLabel(node)} has unsupported nested content`);
|
|
1158
|
+
}
|
|
1159
|
+
content =
|
|
1160
|
+
node.type === 'VECTOR'
|
|
1161
|
+
? renderVector({ context, node })
|
|
1162
|
+
: node.type === 'ELLIPSE'
|
|
1163
|
+
? renderEllipse({ context, node })
|
|
1164
|
+
: renderRoundedRectangle({ context, node });
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
fail(`${nodeLabel(node)} has unsupported type ${(_c = node.type) !== null && _c !== void 0 ? _c : 'UNKNOWN'}`);
|
|
1168
|
+
}
|
|
1169
|
+
const opacity = unitInterval({
|
|
1170
|
+
fallback: 1,
|
|
1171
|
+
label: `${nodeLabel(node)} opacity`,
|
|
1172
|
+
value: node.opacity,
|
|
1173
|
+
});
|
|
1174
|
+
if (opacity < 1 && content !== '') {
|
|
1175
|
+
content = `<g opacity="${formatNumber(opacity)}">${content}</g>`;
|
|
1176
|
+
}
|
|
1177
|
+
if (includeTransform && content !== '') {
|
|
1178
|
+
const transform = getTransform(node);
|
|
1179
|
+
if (!transform.identity) {
|
|
1180
|
+
content = `<g transform="${transform.value}">${content}</g>`;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return content;
|
|
1184
|
+
}
|
|
1185
|
+
finally {
|
|
1186
|
+
context.renderingDepth--;
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
const buildScene = ({ message, selectedNodeId, }) => {
|
|
1190
|
+
var _a;
|
|
1191
|
+
var _b, _c;
|
|
1192
|
+
if (message.type !== 'NODE_CHANGES') {
|
|
1193
|
+
fail('clipboard data is not a Figma scene');
|
|
1194
|
+
}
|
|
1195
|
+
const { blobs: messageBlobs, nodeChanges } = message;
|
|
1196
|
+
if (!Array.isArray(nodeChanges) || !Array.isArray(messageBlobs)) {
|
|
1197
|
+
fail('Pasting images from Figma is not supported');
|
|
1198
|
+
}
|
|
1199
|
+
const validNodeChanges = nodeChanges;
|
|
1200
|
+
const validMessageBlobs = messageBlobs;
|
|
1201
|
+
const nodesById = new Map();
|
|
1202
|
+
const orderByNode = new Map();
|
|
1203
|
+
for (let index = 0; index < validNodeChanges.length; index++) {
|
|
1204
|
+
const change = validNodeChanges[index];
|
|
1205
|
+
const key = guidKey(change.guid);
|
|
1206
|
+
if (key === null) {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (change.phase === 'DELETED') {
|
|
1210
|
+
nodesById.delete(key);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const previous = nodesById.get(key);
|
|
1214
|
+
const node = previous ? { ...previous, ...change } : change;
|
|
1215
|
+
nodesById.set(key, node);
|
|
1216
|
+
orderByNode.set(node, index);
|
|
1217
|
+
}
|
|
1218
|
+
const root = (_b = nodesById.get(selectedNodeId)) !== null && _b !== void 0 ? _b : fail('the selected Figma node is missing from the clipboard');
|
|
1219
|
+
const children = new Map();
|
|
1220
|
+
for (const node of nodesById.values()) {
|
|
1221
|
+
const parentKey = guidKey((_a = node.parentIndex) === null || _a === void 0 ? void 0 : _a.guid);
|
|
1222
|
+
const parent = parentKey ? nodesById.get(parentKey) : undefined;
|
|
1223
|
+
if (parent === undefined) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const siblings = (_c = children.get(parent)) !== null && _c !== void 0 ? _c : [];
|
|
1227
|
+
siblings.push(node);
|
|
1228
|
+
children.set(parent, siblings);
|
|
1229
|
+
}
|
|
1230
|
+
for (const siblings of children.values()) {
|
|
1231
|
+
siblings.sort((left, right) => {
|
|
1232
|
+
var _a, _b;
|
|
1233
|
+
var _c, _d, _e, _f;
|
|
1234
|
+
const leftPosition = (_c = (_a = left.parentIndex) === null || _a === void 0 ? void 0 : _a.position) !== null && _c !== void 0 ? _c : '';
|
|
1235
|
+
const rightPosition = (_d = (_b = right.parentIndex) === null || _b === void 0 ? void 0 : _b.position) !== null && _d !== void 0 ? _d : '';
|
|
1236
|
+
const positionDifference = leftPosition < rightPosition
|
|
1237
|
+
? -1
|
|
1238
|
+
: leftPosition > rightPosition
|
|
1239
|
+
? 1
|
|
1240
|
+
: 0;
|
|
1241
|
+
return (positionDifference ||
|
|
1242
|
+
((_e = orderByNode.get(left)) !== null && _e !== void 0 ? _e : 0) - ((_f = orderByNode.get(right)) !== null && _f !== void 0 ? _f : 0));
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const blobs = validMessageBlobs.map((blob) => {
|
|
1246
|
+
const { bytes } = blob;
|
|
1247
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1248
|
+
fail('clipboard scene contains an invalid geometry blob');
|
|
1249
|
+
}
|
|
1250
|
+
return bytes;
|
|
1251
|
+
});
|
|
1252
|
+
return { blobs, children, root };
|
|
1253
|
+
};
|
|
1254
|
+
const assertNoMasks = (scene) => {
|
|
1255
|
+
var _a;
|
|
1256
|
+
const pending = [scene.root];
|
|
1257
|
+
const visited = new Set();
|
|
1258
|
+
while (pending.length > 0) {
|
|
1259
|
+
const node = pending.pop();
|
|
1260
|
+
if (visited.has(node)) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
visited.add(node);
|
|
1264
|
+
if (node.mask === true || node.maskIsOutline === true) {
|
|
1265
|
+
fail('Pasting Figma selections with masks is not supported');
|
|
1266
|
+
}
|
|
1267
|
+
pending.push(...((_a = scene.children.get(node)) !== null && _a !== void 0 ? _a : []));
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
const renderFigmaMessageToSvg = ({ message, selectedNodeId, }) => {
|
|
1271
|
+
const scene = buildScene({ message, selectedNodeId });
|
|
1272
|
+
assertNoMasks(scene);
|
|
1273
|
+
const { height, width } = getNodeSize(scene.root);
|
|
1274
|
+
if (width <= 0 || height <= 0) {
|
|
1275
|
+
fail('the selected node has empty bounds');
|
|
1276
|
+
}
|
|
1277
|
+
const context = {
|
|
1278
|
+
defs: [],
|
|
1279
|
+
nextId: 0,
|
|
1280
|
+
renderingDepth: 0,
|
|
1281
|
+
scene,
|
|
1282
|
+
};
|
|
1283
|
+
const content = renderNode({
|
|
1284
|
+
context,
|
|
1285
|
+
includeTransform: false,
|
|
1286
|
+
node: scene.root,
|
|
1287
|
+
});
|
|
1288
|
+
if (content === '') {
|
|
1289
|
+
fail('the selected node has no visible supported content');
|
|
1290
|
+
}
|
|
1291
|
+
const defs = context.defs.length === 0
|
|
1292
|
+
? ''
|
|
1293
|
+
: `<defs>\n${context.defs.join('\n')}\n</defs>\n`;
|
|
1294
|
+
return {
|
|
1295
|
+
height,
|
|
1296
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${formatNumber(width)}" height="${formatNumber(height)}" viewBox="0 0 ${formatNumber(width)} ${formatNumber(height)}">\n${defs}${content}\n</svg>`,
|
|
1297
|
+
width,
|
|
1298
|
+
};
|
|
1299
|
+
};
|
|
1300
|
+
exports.renderFigmaMessageToSvg = renderFigmaMessageToSvg;
|