@zseven-w/pen-core 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +177 -25
- package/package.json +19 -5
- package/src/__tests__/arc-path.test.ts +23 -23
- package/src/__tests__/codegen-utils.test.ts +50 -0
- package/src/__tests__/design-md-parser.test.ts +49 -0
- package/src/__tests__/font-utils.test.ts +15 -15
- package/src/__tests__/layout-engine.test.ts +169 -83
- package/src/__tests__/merge-helpers.test.ts +143 -0
- package/src/__tests__/node-diff.test.ts +139 -0
- package/src/__tests__/node-helpers.test.ts +19 -19
- package/src/__tests__/node-merge.test.ts +425 -0
- package/src/__tests__/normalize-stroke-fill-schema.test.ts +416 -0
- package/src/__tests__/normalize-tree-layout.test.ts +294 -0
- package/src/__tests__/normalize.test.ts +119 -80
- package/src/__tests__/path-anchors.test.ts +98 -0
- package/src/__tests__/resolve-variables-recursive.test.ts +109 -54
- package/src/__tests__/strip-redundant-section-fills.test.ts +278 -0
- package/src/__tests__/text-measure.test.ts +84 -79
- package/src/__tests__/tree-utils.test.ts +133 -102
- package/src/__tests__/unwrap-fake-phone-mockup.test.ts +322 -0
- package/src/__tests__/variables.test.ts +68 -65
- package/src/arc-path.ts +35 -35
- package/src/boolean-ops.ts +131 -142
- package/src/constants.ts +36 -36
- package/src/design-md-parser.ts +363 -0
- package/src/font-utils.ts +30 -15
- package/src/id.ts +1 -1
- package/src/index.ts +47 -13
- package/src/layout/engine.ts +255 -224
- package/src/layout/normalize-tree.ts +140 -0
- package/src/layout/strip-redundant-section-fills.ts +155 -0
- package/src/layout/text-measure.ts +130 -106
- package/src/layout/unwrap-fake-phone-mockup.ts +147 -0
- package/src/merge/index.ts +16 -0
- package/src/merge/merge-helpers.ts +113 -0
- package/src/merge/node-diff.ts +123 -0
- package/src/merge/node-merge.ts +651 -0
- package/src/node-helpers.ts +18 -5
- package/src/normalize/normalize-stroke-fill-schema.ts +297 -0
- package/src/normalize.ts +75 -81
- package/src/path-anchors.ts +331 -0
- package/src/sync-lock.ts +3 -3
- package/src/tree-utils.ts +180 -158
- package/src/variables/replace-refs.ts +63 -60
- package/src/variables/resolve.ts +98 -106
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Used when renaming or deleting a variable to keep the tree consistent.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { PenNode } from '@zseven-w/pen-types'
|
|
8
|
-
import type { PenFill } from '@zseven-w/pen-types'
|
|
9
|
-
import type { VariableDefinition } from '@zseven-w/pen-types'
|
|
10
|
-
import { resolveVariableRef } from './resolve.js'
|
|
7
|
+
import type { PenNode } from '@zseven-w/pen-types';
|
|
8
|
+
import type { PenFill } from '@zseven-w/pen-types';
|
|
9
|
+
import type { VariableDefinition } from '@zseven-w/pen-types';
|
|
10
|
+
import { resolveVariableRef } from './resolve.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Replace all occurrences of `$oldRef` with `$newRef` in the node tree.
|
|
@@ -20,130 +20,133 @@ export function replaceVariableRefsInTree(
|
|
|
20
20
|
variables: Record<string, VariableDefinition>,
|
|
21
21
|
activeTheme: Record<string, string>,
|
|
22
22
|
): PenNode[] {
|
|
23
|
-
const oldToken = `$${oldRef}
|
|
24
|
-
const replacement = newRef ? `$${newRef}` : undefined
|
|
23
|
+
const oldToken = `$${oldRef}`;
|
|
24
|
+
const replacement = newRef ? `$${newRef}` : undefined;
|
|
25
25
|
|
|
26
26
|
function resolveOrReplace(val: string): string {
|
|
27
|
-
if (val !== oldToken) return val
|
|
28
|
-
if (replacement) return replacement
|
|
29
|
-
const resolved = resolveVariableRef(oldToken, variables, activeTheme)
|
|
30
|
-
return typeof resolved === 'string' ? resolved : val
|
|
27
|
+
if (val !== oldToken) return val;
|
|
28
|
+
if (replacement) return replacement;
|
|
29
|
+
const resolved = resolveVariableRef(oldToken, variables, activeTheme);
|
|
30
|
+
return typeof resolved === 'string' ? resolved : val;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function resolveOrReplaceNumeric(val: string | number): string | number {
|
|
34
|
-
if (typeof val !== 'string' || val !== oldToken) return val
|
|
35
|
-
if (replacement) return replacement
|
|
36
|
-
const resolved = resolveVariableRef(oldToken, variables, activeTheme)
|
|
37
|
-
return typeof resolved === 'number' ? resolved : val
|
|
34
|
+
if (typeof val !== 'string' || val !== oldToken) return val;
|
|
35
|
+
if (replacement) return replacement;
|
|
36
|
+
const resolved = resolveVariableRef(oldToken, variables, activeTheme);
|
|
37
|
+
return typeof resolved === 'number' ? resolved : val;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function replaceFills(fills: PenFill[] | string | undefined): PenFill[] | string | undefined {
|
|
41
|
-
if (!fills || typeof fills === 'string') return fills
|
|
41
|
+
if (!fills || typeof fills === 'string') return fills;
|
|
42
42
|
return fills.map((f) => {
|
|
43
43
|
if (f.type === 'solid' && f.color === oldToken) {
|
|
44
|
-
return { ...f, color: resolveOrReplace(f.color) }
|
|
44
|
+
return { ...f, color: resolveOrReplace(f.color) };
|
|
45
45
|
}
|
|
46
46
|
if (f.type === 'linear_gradient' || f.type === 'radial_gradient') {
|
|
47
47
|
const newStops = f.stops.map((s) =>
|
|
48
48
|
s.color === oldToken ? { ...s, color: resolveOrReplace(s.color) } : s,
|
|
49
|
-
)
|
|
50
|
-
return { ...f, stops: newStops }
|
|
49
|
+
);
|
|
50
|
+
return { ...f, stops: newStops };
|
|
51
51
|
}
|
|
52
|
-
return f
|
|
53
|
-
})
|
|
52
|
+
return f;
|
|
53
|
+
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function replaceInNode(node: PenNode): PenNode {
|
|
57
|
-
const out: Record<string, unknown> = { ...node }
|
|
58
|
-
let changed = false
|
|
57
|
+
const out: Record<string, unknown> = { ...node };
|
|
58
|
+
let changed = false;
|
|
59
59
|
|
|
60
60
|
// Opacity
|
|
61
61
|
if (typeof node.opacity === 'string' && node.opacity === oldToken) {
|
|
62
|
-
out.opacity = resolveOrReplaceNumeric(node.opacity)
|
|
63
|
-
changed = true
|
|
62
|
+
out.opacity = resolveOrReplaceNumeric(node.opacity);
|
|
63
|
+
changed = true;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Gap
|
|
67
67
|
if ('gap' in node && (node as unknown as Record<string, unknown>).gap === oldToken) {
|
|
68
|
-
out.gap = resolveOrReplaceNumeric(oldToken)
|
|
69
|
-
changed = true
|
|
68
|
+
out.gap = resolveOrReplaceNumeric(oldToken);
|
|
69
|
+
changed = true;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Padding
|
|
73
73
|
if ('padding' in node && (node as unknown as Record<string, unknown>).padding === oldToken) {
|
|
74
|
-
out.padding = resolveOrReplaceNumeric(oldToken)
|
|
75
|
-
changed = true
|
|
74
|
+
out.padding = resolveOrReplaceNumeric(oldToken);
|
|
75
|
+
changed = true;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Fill
|
|
79
79
|
if ('fill' in node && (node as unknown as Record<string, unknown>).fill) {
|
|
80
|
-
const fills = (node as unknown as Record<string, unknown>).fill as PenFill[]
|
|
81
|
-
const newFills = replaceFills(fills)
|
|
80
|
+
const fills = (node as unknown as Record<string, unknown>).fill as PenFill[];
|
|
81
|
+
const newFills = replaceFills(fills);
|
|
82
82
|
if (newFills !== fills) {
|
|
83
|
-
out.fill = newFills
|
|
84
|
-
changed = true
|
|
83
|
+
out.fill = newFills;
|
|
84
|
+
changed = true;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Stroke fill & thickness
|
|
89
89
|
if ('stroke' in node && (node as unknown as Record<string, unknown>).stroke) {
|
|
90
|
-
const stroke = (node as unknown as Record<string, unknown>).stroke as Record<string, unknown
|
|
91
|
-
const newStroke = { ...stroke }
|
|
92
|
-
let strokeChanged = false
|
|
90
|
+
const stroke = (node as unknown as Record<string, unknown>).stroke as Record<string, unknown>;
|
|
91
|
+
const newStroke = { ...stroke };
|
|
92
|
+
let strokeChanged = false;
|
|
93
93
|
if (typeof stroke.thickness === 'string' && stroke.thickness === oldToken) {
|
|
94
|
-
newStroke.thickness = resolveOrReplaceNumeric(oldToken)
|
|
95
|
-
strokeChanged = true
|
|
94
|
+
newStroke.thickness = resolveOrReplaceNumeric(oldToken);
|
|
95
|
+
strokeChanged = true;
|
|
96
96
|
}
|
|
97
97
|
if (stroke.fill) {
|
|
98
|
-
const newFill = replaceFills(stroke.fill as PenFill[])
|
|
98
|
+
const newFill = replaceFills(stroke.fill as PenFill[]);
|
|
99
99
|
if (newFill !== stroke.fill) {
|
|
100
|
-
newStroke.fill = newFill
|
|
101
|
-
strokeChanged = true
|
|
100
|
+
newStroke.fill = newFill;
|
|
101
|
+
strokeChanged = true;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
if (strokeChanged) {
|
|
105
|
-
out.stroke = newStroke
|
|
106
|
-
changed = true
|
|
105
|
+
out.stroke = newStroke;
|
|
106
|
+
changed = true;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Effects
|
|
111
111
|
if ('effects' in node && Array.isArray((node as unknown as Record<string, unknown>).effects)) {
|
|
112
|
-
const effects = (node as unknown as Record<string, unknown>).effects as Record<
|
|
112
|
+
const effects = (node as unknown as Record<string, unknown>).effects as Record<
|
|
113
|
+
string,
|
|
114
|
+
unknown
|
|
115
|
+
>[];
|
|
113
116
|
const newEffects = effects.map((e) => {
|
|
114
|
-
const ne = { ...e }
|
|
115
|
-
let ec = false
|
|
117
|
+
const ne = { ...e };
|
|
118
|
+
let ec = false;
|
|
116
119
|
if (typeof e.color === 'string' && e.color === oldToken) {
|
|
117
|
-
ne.color = resolveOrReplace(e.color as string)
|
|
118
|
-
ec = true
|
|
120
|
+
ne.color = resolveOrReplace(e.color as string);
|
|
121
|
+
ec = true;
|
|
119
122
|
}
|
|
120
123
|
for (const key of ['blur', 'offsetX', 'offsetY', 'spread']) {
|
|
121
124
|
if (typeof e[key] === 'string' && e[key] === oldToken) {
|
|
122
|
-
ne[key] = resolveOrReplaceNumeric(oldToken)
|
|
123
|
-
ec = true
|
|
125
|
+
ne[key] = resolveOrReplaceNumeric(oldToken);
|
|
126
|
+
ec = true;
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
return ec ? ne : e
|
|
127
|
-
})
|
|
128
|
-
out.effects = newEffects
|
|
129
|
-
changed = true
|
|
129
|
+
return ec ? ne : e;
|
|
130
|
+
});
|
|
131
|
+
out.effects = newEffects;
|
|
132
|
+
changed = true;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
// Text content
|
|
133
136
|
if (node.type === 'text' && typeof node.content === 'string' && node.content === oldToken) {
|
|
134
|
-
out.content = resolveOrReplace(node.content)
|
|
135
|
-
changed = true
|
|
137
|
+
out.content = resolveOrReplace(node.content);
|
|
138
|
+
changed = true;
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
// Recurse children
|
|
139
142
|
if ('children' in node && (node as unknown as Record<string, unknown>).children) {
|
|
140
|
-
const children = (node as unknown as Record<string, unknown>).children as PenNode[]
|
|
141
|
-
out.children = replaceVariableRefsInTree(children, oldRef, newRef, variables, activeTheme)
|
|
142
|
-
changed = true
|
|
143
|
+
const children = (node as unknown as Record<string, unknown>).children as PenNode[];
|
|
144
|
+
out.children = replaceVariableRefsInTree(children, oldRef, newRef, variables, activeTheme);
|
|
145
|
+
changed = true;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
return changed ? (out as unknown as PenNode) : node
|
|
148
|
+
return changed ? (out as unknown as PenNode) : node;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
return nodes.map(replaceInNode)
|
|
151
|
+
return nodes.map(replaceInNode);
|
|
149
152
|
}
|
package/src/variables/resolve.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* optionally matching themed values to an active theme context.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { PenNode } from '@zseven-w/pen-types'
|
|
9
|
-
import type { PenFill, PenStroke, PenEffect } from '@zseven-w/pen-types'
|
|
10
|
-
import type { VariableDefinition, ThemedValue } from '@zseven-w/pen-types'
|
|
8
|
+
import type { PenNode } from '@zseven-w/pen-types';
|
|
9
|
+
import type { PenFill, PenStroke, PenEffect } from '@zseven-w/pen-types';
|
|
10
|
+
import type { VariableDefinition, ThemedValue } from '@zseven-w/pen-types';
|
|
11
11
|
|
|
12
|
-
type Vars = Record<string, VariableDefinition
|
|
13
|
-
type Theme = Record<string, string
|
|
12
|
+
type Vars = Record<string, VariableDefinition>;
|
|
13
|
+
type Theme = Record<string, string>;
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Helpers
|
|
@@ -18,19 +18,17 @@ type Theme = Record<string, string>
|
|
|
18
18
|
|
|
19
19
|
/** Check whether a value is a `$variable` reference string. */
|
|
20
20
|
export function isVariableRef(value: unknown): value is string {
|
|
21
|
-
return typeof value === 'string' && value.startsWith('$')
|
|
21
|
+
return typeof value === 'string' && value.startsWith('$');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Build the default theme map (first value per axis) from PenDocument.themes. */
|
|
25
|
-
export function getDefaultTheme(
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
const result: Theme = {}
|
|
29
|
-
if (!themes) return result
|
|
25
|
+
export function getDefaultTheme(themes: Record<string, string[]> | undefined): Theme {
|
|
26
|
+
const result: Theme = {};
|
|
27
|
+
if (!themes) return result;
|
|
30
28
|
for (const [key, values] of Object.entries(themes)) {
|
|
31
|
-
if (values.length > 0) result[key] = values[0]
|
|
29
|
+
if (values.length > 0) result[key] = values[0];
|
|
32
30
|
}
|
|
33
|
-
return result
|
|
31
|
+
return result;
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
// ---------------------------------------------------------------------------
|
|
@@ -44,14 +42,12 @@ function resolveThemedValue(
|
|
|
44
42
|
): string | number | boolean | undefined {
|
|
45
43
|
if (activeTheme && Object.keys(activeTheme).length > 0) {
|
|
46
44
|
const match = values.find((v) => {
|
|
47
|
-
if (!v.theme) return false
|
|
48
|
-
return Object.entries(activeTheme).every(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
if (match) return match.value
|
|
45
|
+
if (!v.theme) return false;
|
|
46
|
+
return Object.entries(activeTheme).every(([key, expected]) => v.theme?.[key] === expected);
|
|
47
|
+
});
|
|
48
|
+
if (match) return match.value;
|
|
53
49
|
}
|
|
54
|
-
return values[0]?.value
|
|
50
|
+
return values[0]?.value;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|
|
@@ -63,21 +59,21 @@ export function resolveVariableRef(
|
|
|
63
59
|
variables: Vars,
|
|
64
60
|
activeTheme?: Theme,
|
|
65
61
|
): string | number | boolean | undefined {
|
|
66
|
-
if (!ref.startsWith('$')) return undefined
|
|
67
|
-
const name = ref.slice(1)
|
|
68
|
-
const def = variables[name]
|
|
69
|
-
if (!def) return undefined
|
|
62
|
+
if (!ref.startsWith('$')) return undefined;
|
|
63
|
+
const name = ref.slice(1);
|
|
64
|
+
const def = variables[name];
|
|
65
|
+
if (!def) return undefined;
|
|
70
66
|
|
|
71
|
-
const val = def.value
|
|
67
|
+
const val = def.value;
|
|
72
68
|
if (Array.isArray(val)) {
|
|
73
|
-
const resolved = resolveThemedValue(val, activeTheme)
|
|
69
|
+
const resolved = resolveThemedValue(val, activeTheme);
|
|
74
70
|
// Circular guard: if resolved value is also a $ref, stop
|
|
75
|
-
if (typeof resolved === 'string' && resolved.startsWith('$')) return undefined
|
|
76
|
-
return resolved
|
|
71
|
+
if (typeof resolved === 'string' && resolved.startsWith('$')) return undefined;
|
|
72
|
+
return resolved;
|
|
77
73
|
}
|
|
78
74
|
// Circular guard
|
|
79
|
-
if (typeof val === 'string' && val.startsWith('$')) return undefined
|
|
80
|
-
return val
|
|
75
|
+
if (typeof val === 'string' && val.startsWith('$')) return undefined;
|
|
76
|
+
return val;
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
/**
|
|
@@ -89,10 +85,10 @@ export function resolveColorRef(
|
|
|
89
85
|
variables: Vars,
|
|
90
86
|
activeTheme?: Theme,
|
|
91
87
|
): string | undefined {
|
|
92
|
-
if (color === undefined) return undefined
|
|
93
|
-
if (!isVariableRef(color)) return color
|
|
94
|
-
const resolved = resolveVariableRef(color, variables, activeTheme)
|
|
95
|
-
return typeof resolved === 'string' ? resolved : undefined
|
|
88
|
+
if (color === undefined) return undefined;
|
|
89
|
+
if (!isVariableRef(color)) return color;
|
|
90
|
+
const resolved = resolveVariableRef(color, variables, activeTheme);
|
|
91
|
+
return typeof resolved === 'string' ? resolved : undefined;
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
/**
|
|
@@ -104,16 +100,16 @@ export function resolveNumericRef(
|
|
|
104
100
|
variables: Vars,
|
|
105
101
|
activeTheme?: Theme,
|
|
106
102
|
): number | undefined {
|
|
107
|
-
if (typeof value === 'number') return value
|
|
103
|
+
if (typeof value === 'number') return value;
|
|
108
104
|
if (typeof value === 'string') {
|
|
109
105
|
if (isVariableRef(value)) {
|
|
110
|
-
const resolved = resolveVariableRef(value, variables, activeTheme)
|
|
111
|
-
return typeof resolved === 'number' ? resolved : undefined
|
|
106
|
+
const resolved = resolveVariableRef(value, variables, activeTheme);
|
|
107
|
+
return typeof resolved === 'number' ? resolved : undefined;
|
|
112
108
|
}
|
|
113
|
-
const num = parseFloat(value)
|
|
114
|
-
return isNaN(num) ? undefined : num
|
|
109
|
+
const num = parseFloat(value);
|
|
110
|
+
return isNaN(num) ? undefined : num;
|
|
115
111
|
}
|
|
116
|
-
return undefined
|
|
112
|
+
return undefined;
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
// ---------------------------------------------------------------------------
|
|
@@ -125,22 +121,22 @@ function resolveFillsForCanvas(
|
|
|
125
121
|
vars: Vars,
|
|
126
122
|
theme?: Theme,
|
|
127
123
|
): PenFill[] | string | undefined {
|
|
128
|
-
if (!fills) return fills
|
|
129
|
-
if (typeof fills === 'string') return fills
|
|
124
|
+
if (!fills) return fills;
|
|
125
|
+
if (typeof fills === 'string') return fills;
|
|
130
126
|
return fills.map((fill) => {
|
|
131
127
|
if (fill.type === 'solid') {
|
|
132
|
-
const color = resolveColorRef(fill.color, vars, theme)
|
|
133
|
-
return color !== fill.color ? { ...fill, color: color ?? '#000000' } : fill
|
|
128
|
+
const color = resolveColorRef(fill.color, vars, theme);
|
|
129
|
+
return color !== fill.color ? { ...fill, color: color ?? '#000000' } : fill;
|
|
134
130
|
}
|
|
135
131
|
if (fill.type === 'linear_gradient' || fill.type === 'radial_gradient') {
|
|
136
132
|
const newStops = fill.stops.map((stop) => {
|
|
137
|
-
const color = resolveColorRef(stop.color, vars, theme)
|
|
138
|
-
return color !== stop.color ? { ...stop, color: color ?? '#000000' } : stop
|
|
139
|
-
})
|
|
140
|
-
return newStops !== fill.stops ? { ...fill, stops: newStops } : fill
|
|
133
|
+
const color = resolveColorRef(stop.color, vars, theme);
|
|
134
|
+
return color !== stop.color ? { ...stop, color: color ?? '#000000' } : stop;
|
|
135
|
+
});
|
|
136
|
+
return newStops !== fill.stops ? { ...fill, stops: newStops } : fill;
|
|
141
137
|
}
|
|
142
|
-
return fill
|
|
143
|
-
})
|
|
138
|
+
return fill;
|
|
139
|
+
});
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
function resolveStrokeForCanvas(
|
|
@@ -148,26 +144,26 @@ function resolveStrokeForCanvas(
|
|
|
148
144
|
vars: Vars,
|
|
149
145
|
theme?: Theme,
|
|
150
146
|
): PenStroke | undefined {
|
|
151
|
-
if (!stroke) return stroke
|
|
152
|
-
let changed = false
|
|
153
|
-
const out: Record<string, unknown> = { ...stroke }
|
|
147
|
+
if (!stroke) return stroke;
|
|
148
|
+
let changed = false;
|
|
149
|
+
const out: Record<string, unknown> = { ...stroke };
|
|
154
150
|
|
|
155
151
|
// Resolve thickness
|
|
156
152
|
if (typeof stroke.thickness === 'string' && isVariableRef(stroke.thickness)) {
|
|
157
|
-
out.thickness = resolveNumericRef(stroke.thickness, vars, theme) ?? 1
|
|
158
|
-
changed = true
|
|
153
|
+
out.thickness = resolveNumericRef(stroke.thickness, vars, theme) ?? 1;
|
|
154
|
+
changed = true;
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
// Resolve stroke fill colors
|
|
162
158
|
if (stroke.fill) {
|
|
163
|
-
const resolved = resolveFillsForCanvas(stroke.fill, vars, theme)
|
|
159
|
+
const resolved = resolveFillsForCanvas(stroke.fill, vars, theme);
|
|
164
160
|
if (resolved !== stroke.fill) {
|
|
165
|
-
out.fill = resolved
|
|
166
|
-
changed = true
|
|
161
|
+
out.fill = resolved;
|
|
162
|
+
changed = true;
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
165
|
|
|
170
|
-
return changed ? (out as unknown as PenStroke) : stroke
|
|
166
|
+
return changed ? (out as unknown as PenStroke) : stroke;
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
function resolveEffectsForCanvas(
|
|
@@ -175,26 +171,26 @@ function resolveEffectsForCanvas(
|
|
|
175
171
|
vars: Vars,
|
|
176
172
|
theme?: Theme,
|
|
177
173
|
): PenEffect[] | undefined {
|
|
178
|
-
if (!effects) return effects
|
|
174
|
+
if (!effects) return effects;
|
|
179
175
|
return effects.map((effect) => {
|
|
180
|
-
if (effect.type !== 'shadow') return effect
|
|
181
|
-
let changed = false
|
|
182
|
-
const out: Record<string, unknown> = { ...effect }
|
|
176
|
+
if (effect.type !== 'shadow') return effect;
|
|
177
|
+
let changed = false;
|
|
178
|
+
const out: Record<string, unknown> = { ...effect };
|
|
183
179
|
|
|
184
180
|
if (typeof effect.color === 'string' && isVariableRef(effect.color)) {
|
|
185
|
-
out.color = resolveColorRef(effect.color, vars, theme) ?? '#000000'
|
|
186
|
-
changed = true
|
|
181
|
+
out.color = resolveColorRef(effect.color, vars, theme) ?? '#000000';
|
|
182
|
+
changed = true;
|
|
187
183
|
}
|
|
188
184
|
for (const key of ['blur', 'offsetX', 'offsetY', 'spread'] as const) {
|
|
189
|
-
const val = effect[key]
|
|
185
|
+
const val = effect[key];
|
|
190
186
|
if (typeof val === 'string' && isVariableRef(val)) {
|
|
191
|
-
out[key] = resolveNumericRef(val, vars, theme) ?? 0
|
|
192
|
-
changed = true
|
|
187
|
+
out[key] = resolveNumericRef(val, vars, theme) ?? 0;
|
|
188
|
+
changed = true;
|
|
193
189
|
}
|
|
194
190
|
}
|
|
195
191
|
|
|
196
|
-
return changed ? (out as unknown as PenEffect) : effect
|
|
197
|
-
})
|
|
192
|
+
return changed ? (out as unknown as PenEffect) : effect;
|
|
193
|
+
});
|
|
198
194
|
}
|
|
199
195
|
|
|
200
196
|
// ---------------------------------------------------------------------------
|
|
@@ -207,91 +203,87 @@ function resolveEffectsForCanvas(
|
|
|
207
203
|
*
|
|
208
204
|
* Returns the same object reference when no variables are present.
|
|
209
205
|
*/
|
|
210
|
-
export function resolveNodeForCanvas(
|
|
211
|
-
node
|
|
212
|
-
variables: Vars,
|
|
213
|
-
activeTheme?: Theme,
|
|
214
|
-
): PenNode {
|
|
215
|
-
if (!variables || Object.keys(variables).length === 0) return node
|
|
206
|
+
export function resolveNodeForCanvas(node: PenNode, variables: Vars, activeTheme?: Theme): PenNode {
|
|
207
|
+
if (!variables || Object.keys(variables).length === 0) return node;
|
|
216
208
|
|
|
217
|
-
let changed = false
|
|
218
|
-
const out: Record<string, unknown> = { ...node }
|
|
209
|
+
let changed = false;
|
|
210
|
+
const out: Record<string, unknown> = { ...node };
|
|
219
211
|
|
|
220
212
|
// Opacity
|
|
221
213
|
if (typeof node.opacity === 'string' && isVariableRef(node.opacity)) {
|
|
222
|
-
out.opacity = resolveNumericRef(node.opacity, variables, activeTheme) ?? 1
|
|
223
|
-
changed = true
|
|
214
|
+
out.opacity = resolveNumericRef(node.opacity, variables, activeTheme) ?? 1;
|
|
215
|
+
changed = true;
|
|
224
216
|
}
|
|
225
217
|
|
|
226
218
|
// Gap
|
|
227
219
|
if ('gap' in node && typeof (node as unknown as Record<string, unknown>).gap === 'string') {
|
|
228
|
-
const gap = (node as unknown as Record<string, unknown>).gap as string
|
|
220
|
+
const gap = (node as unknown as Record<string, unknown>).gap as string;
|
|
229
221
|
if (isVariableRef(gap)) {
|
|
230
|
-
out.gap = resolveNumericRef(gap, variables, activeTheme) ?? 0
|
|
231
|
-
changed = true
|
|
222
|
+
out.gap = resolveNumericRef(gap, variables, activeTheme) ?? 0;
|
|
223
|
+
changed = true;
|
|
232
224
|
}
|
|
233
225
|
}
|
|
234
226
|
|
|
235
227
|
// Padding
|
|
236
228
|
if ('padding' in node) {
|
|
237
|
-
const padding = (node as unknown as Record<string, unknown>).padding
|
|
229
|
+
const padding = (node as unknown as Record<string, unknown>).padding;
|
|
238
230
|
if (typeof padding === 'string' && isVariableRef(padding)) {
|
|
239
|
-
out.padding = resolveNumericRef(padding, variables, activeTheme) ?? 0
|
|
240
|
-
changed = true
|
|
231
|
+
out.padding = resolveNumericRef(padding, variables, activeTheme) ?? 0;
|
|
232
|
+
changed = true;
|
|
241
233
|
}
|
|
242
234
|
}
|
|
243
235
|
|
|
244
236
|
// Fill
|
|
245
237
|
if ('fill' in node && (node as unknown as Record<string, unknown>).fill) {
|
|
246
|
-
const fills = (node as unknown as Record<string, unknown>).fill as PenFill[] | string
|
|
247
|
-
const resolved = resolveFillsForCanvas(fills, variables, activeTheme)
|
|
238
|
+
const fills = (node as unknown as Record<string, unknown>).fill as PenFill[] | string;
|
|
239
|
+
const resolved = resolveFillsForCanvas(fills, variables, activeTheme);
|
|
248
240
|
if (resolved !== fills) {
|
|
249
|
-
out.fill = resolved
|
|
250
|
-
changed = true
|
|
241
|
+
out.fill = resolved;
|
|
242
|
+
changed = true;
|
|
251
243
|
}
|
|
252
244
|
}
|
|
253
245
|
|
|
254
246
|
// Stroke
|
|
255
247
|
if ('stroke' in node && (node as unknown as Record<string, unknown>).stroke) {
|
|
256
|
-
const stroke = (node as unknown as Record<string, unknown>).stroke as PenStroke
|
|
257
|
-
const resolved = resolveStrokeForCanvas(stroke, variables, activeTheme)
|
|
248
|
+
const stroke = (node as unknown as Record<string, unknown>).stroke as PenStroke;
|
|
249
|
+
const resolved = resolveStrokeForCanvas(stroke, variables, activeTheme);
|
|
258
250
|
if (resolved !== stroke) {
|
|
259
|
-
out.stroke = resolved
|
|
260
|
-
changed = true
|
|
251
|
+
out.stroke = resolved;
|
|
252
|
+
changed = true;
|
|
261
253
|
}
|
|
262
254
|
}
|
|
263
255
|
|
|
264
256
|
// Effects
|
|
265
257
|
if ('effects' in node && (node as unknown as Record<string, unknown>).effects) {
|
|
266
|
-
const effects = (node as unknown as Record<string, unknown>).effects as PenEffect[]
|
|
267
|
-
const resolved = resolveEffectsForCanvas(effects, variables, activeTheme)
|
|
258
|
+
const effects = (node as unknown as Record<string, unknown>).effects as PenEffect[];
|
|
259
|
+
const resolved = resolveEffectsForCanvas(effects, variables, activeTheme);
|
|
268
260
|
if (resolved !== effects) {
|
|
269
|
-
out.effects = resolved
|
|
270
|
-
changed = true
|
|
261
|
+
out.effects = resolved;
|
|
262
|
+
changed = true;
|
|
271
263
|
}
|
|
272
264
|
}
|
|
273
265
|
|
|
274
266
|
// Text content
|
|
275
267
|
if (node.type === 'text' && typeof node.content === 'string' && isVariableRef(node.content)) {
|
|
276
|
-
const resolved = resolveVariableRef(node.content, variables, activeTheme)
|
|
268
|
+
const resolved = resolveVariableRef(node.content, variables, activeTheme);
|
|
277
269
|
if (typeof resolved === 'string') {
|
|
278
|
-
out.content = resolved
|
|
279
|
-
changed = true
|
|
270
|
+
out.content = resolved;
|
|
271
|
+
changed = true;
|
|
280
272
|
}
|
|
281
273
|
}
|
|
282
274
|
|
|
283
275
|
// Recurse into children
|
|
284
276
|
if ('children' in node && node.children) {
|
|
285
|
-
const children = node.children
|
|
277
|
+
const children = node.children;
|
|
286
278
|
const resolvedChildren = children.map((child) =>
|
|
287
279
|
resolveNodeForCanvas(child, variables, activeTheme),
|
|
288
|
-
)
|
|
280
|
+
);
|
|
289
281
|
// Only allocate new array if any child actually changed
|
|
290
282
|
if (resolvedChildren.some((rc, i) => rc !== children[i])) {
|
|
291
|
-
out.children = resolvedChildren
|
|
292
|
-
changed = true
|
|
283
|
+
out.children = resolvedChildren;
|
|
284
|
+
changed = true;
|
|
293
285
|
}
|
|
294
286
|
}
|
|
295
287
|
|
|
296
|
-
return changed ? (out as unknown as PenNode) : node
|
|
288
|
+
return changed ? (out as unknown as PenNode) : node;
|
|
297
289
|
}
|