@nnao45/figma-use 0.1.0 → 0.1.3
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/CHANGELOG.md +10 -0
- package/LICENSE +0 -1
- package/SKILL.md +14 -0
- package/dist/cli/index.js +252 -250
- package/package.json +6 -1
- package/packages/cli/src/render/icon.ts +66 -7
- package/packages/cli/src/render/tree.ts +20 -0
- package/packages/plugin/src/main.ts +79 -9
- package/packages/plugin/src/rpc.ts +213 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nnao45/figma-use",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Control Figma from the command line. Full read/write access for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,6 +55,11 @@
|
|
|
55
55
|
"build:fast": "bun build packages/cli/src/fast.ts --outfile dist/fast.mjs --target node --minify"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@iconify-json/heroicons": "^1.2.3",
|
|
59
|
+
"@iconify-json/lucide": "^1.2.88",
|
|
60
|
+
"@iconify-json/mdi": "^1.2.3",
|
|
61
|
+
"@iconify-json/ph": "^1.2.2",
|
|
62
|
+
"@iconify-json/tabler": "^1.2.26",
|
|
58
63
|
"agentfmt": "^0.1.3",
|
|
59
64
|
"citty": "^0.1.6",
|
|
60
65
|
"consola": "^3.4.2",
|
|
@@ -5,9 +5,25 @@ import { iconToSVG } from '@iconify/utils'
|
|
|
5
5
|
|
|
6
6
|
import type { ReactNode } from './mini-react.ts'
|
|
7
7
|
import type { Props, ReactElement } from './tree.ts'
|
|
8
|
-
import type { IconifyIcon } from '@iconify/types'
|
|
8
|
+
import type { IconifyIcon, IconifyJSON } from '@iconify/types'
|
|
9
|
+
|
|
10
|
+
// Bundled icon sets (loaded on first use)
|
|
11
|
+
import { icons as lucideIcons } from '@iconify-json/lucide'
|
|
12
|
+
import { icons as mdiIcons } from '@iconify-json/mdi'
|
|
13
|
+
import { icons as tablerIcons } from '@iconify-json/tabler'
|
|
14
|
+
import { icons as heroiconsIcons } from '@iconify-json/heroicons'
|
|
15
|
+
import { icons as phIcons } from '@iconify-json/ph'
|
|
16
|
+
|
|
17
|
+
// Map of bundled icon sets
|
|
18
|
+
const bundledIconSets: Record<string, IconifyJSON> = {
|
|
19
|
+
lucide: lucideIcons,
|
|
20
|
+
mdi: mdiIcons,
|
|
21
|
+
tabler: tablerIcons,
|
|
22
|
+
heroicons: heroiconsIcons,
|
|
23
|
+
ph: phIcons,
|
|
24
|
+
}
|
|
9
25
|
|
|
10
|
-
// Initialize API module
|
|
26
|
+
// Initialize API module (for fallback to remote API)
|
|
11
27
|
setAPIModule('', fetchAPIModule)
|
|
12
28
|
|
|
13
29
|
export interface IconData {
|
|
@@ -23,14 +39,44 @@ const iconCache = new Map<string, IconData>()
|
|
|
23
39
|
// Raw icon data cache (before size transformation)
|
|
24
40
|
const rawIconCache = new Map<string, IconifyIcon>()
|
|
25
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Try to get icon from bundled icon sets
|
|
44
|
+
*/
|
|
45
|
+
function getBundledIcon(name: string): IconifyIcon | null {
|
|
46
|
+
const [prefix, iconName] = name.split(':')
|
|
47
|
+
if (!prefix || !iconName) return null
|
|
48
|
+
|
|
49
|
+
const iconSet = bundledIconSets[prefix]
|
|
50
|
+
if (!iconSet) return null
|
|
51
|
+
|
|
52
|
+
const iconData = iconSet.icons[iconName]
|
|
53
|
+
if (!iconData) return null
|
|
54
|
+
|
|
55
|
+
// Merge with default values from the icon set
|
|
56
|
+
return {
|
|
57
|
+
...iconData,
|
|
58
|
+
width: iconData.width ?? iconSet.width ?? 24,
|
|
59
|
+
height: iconData.height ?? iconSet.height ?? 24,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
/**
|
|
27
64
|
* Load raw icon data (without size transformation)
|
|
65
|
+
* Tries bundled icon sets first, falls back to API
|
|
28
66
|
*/
|
|
29
67
|
async function loadRawIcon(name: string): Promise<IconifyIcon | null> {
|
|
30
68
|
if (rawIconCache.has(name)) {
|
|
31
69
|
return rawIconCache.get(name)!
|
|
32
70
|
}
|
|
33
71
|
|
|
72
|
+
// Try bundled icon sets first (no network needed)
|
|
73
|
+
const bundledIcon = getBundledIcon(name)
|
|
74
|
+
if (bundledIcon) {
|
|
75
|
+
rawIconCache.set(name, bundledIcon)
|
|
76
|
+
return bundledIcon
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback to API for non-bundled icon sets
|
|
34
80
|
const icon = await loadIcon(name)
|
|
35
81
|
if (!icon) {
|
|
36
82
|
return null
|
|
@@ -95,21 +141,34 @@ export async function preloadIcons(icons: Array<{ name: string; size?: number }>
|
|
|
95
141
|
await Promise.all(icons.map(({ name, size }) => loadIconSvg(name, size || 24)))
|
|
96
142
|
}
|
|
97
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Get list of bundled icon set prefixes
|
|
146
|
+
*/
|
|
147
|
+
export const bundledIconSetPrefixes = Object.keys(bundledIconSets)
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if an icon set is bundled (available offline)
|
|
151
|
+
*/
|
|
152
|
+
export function isIconSetBundled(prefix: string): boolean {
|
|
153
|
+
return prefix in bundledIconSets
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
/**
|
|
99
157
|
* Get list of popular icon sets
|
|
158
|
+
* Sets marked with (bundled) are available offline
|
|
100
159
|
*/
|
|
101
160
|
export const iconSets = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
161
|
+
lucide: 'Lucide (bundled)',
|
|
162
|
+
mdi: 'Material Design Icons (bundled)',
|
|
163
|
+
tabler: 'Tabler Icons (bundled)',
|
|
164
|
+
heroicons: 'Heroicons (bundled)',
|
|
165
|
+
ph: 'Phosphor (bundled)',
|
|
105
166
|
'heroicons-outline': 'Heroicons Outline',
|
|
106
167
|
'heroicons-solid': 'Heroicons Solid',
|
|
107
|
-
tabler: 'Tabler Icons',
|
|
108
168
|
'fa-solid': 'Font Awesome Solid',
|
|
109
169
|
'fa-regular': 'Font Awesome Regular',
|
|
110
170
|
'fa-brands': 'Font Awesome Brands',
|
|
111
171
|
ri: 'Remix Icon',
|
|
112
|
-
ph: 'Phosphor',
|
|
113
172
|
'ph-bold': 'Phosphor Bold',
|
|
114
173
|
'ph-fill': 'Phosphor Fill',
|
|
115
174
|
carbon: 'Carbon',
|
|
@@ -125,6 +125,26 @@ export interface StyleProps {
|
|
|
125
125
|
// Other
|
|
126
126
|
src?: string
|
|
127
127
|
href?: string
|
|
128
|
+
|
|
129
|
+
// Line stroke caps (for start/end independently)
|
|
130
|
+
startCap?:
|
|
131
|
+
| 'none'
|
|
132
|
+
| 'round'
|
|
133
|
+
| 'square'
|
|
134
|
+
| 'arrow'
|
|
135
|
+
| 'arrow-equilateral'
|
|
136
|
+
| 'triangle'
|
|
137
|
+
| 'diamond'
|
|
138
|
+
| 'circle'
|
|
139
|
+
endCap?:
|
|
140
|
+
| 'none'
|
|
141
|
+
| 'round'
|
|
142
|
+
| 'square'
|
|
143
|
+
| 'arrow'
|
|
144
|
+
| 'arrow-equilateral'
|
|
145
|
+
| 'triangle'
|
|
146
|
+
| 'diamond'
|
|
147
|
+
| 'circle'
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
// Custom child type that allows TreeNode in JSX
|
|
@@ -57,6 +57,42 @@ function loadFont(family: string, style: string): Promise<void> | void {
|
|
|
57
57
|
return promise
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const LINE_CAP_VALUES = [
|
|
61
|
+
'none',
|
|
62
|
+
'round',
|
|
63
|
+
'square',
|
|
64
|
+
'arrow',
|
|
65
|
+
'arrow-lines',
|
|
66
|
+
'arrow-equilateral',
|
|
67
|
+
'triangle',
|
|
68
|
+
'diamond',
|
|
69
|
+
'circle'
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const LINE_CAP_MAP: Record<string, StrokeCap> = {
|
|
73
|
+
none: 'NONE',
|
|
74
|
+
round: 'ROUND',
|
|
75
|
+
square: 'SQUARE',
|
|
76
|
+
arrow: 'ARROW_LINES',
|
|
77
|
+
'arrow-lines': 'ARROW_LINES',
|
|
78
|
+
'arrow-equilateral': 'ARROW_EQUILATERAL',
|
|
79
|
+
triangle: 'TRIANGLE_FILLED',
|
|
80
|
+
diamond: 'DIAMOND_FILLED',
|
|
81
|
+
circle: 'CIRCLE_FILLED'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeLineCap(cap?: string): StrokeCap | undefined {
|
|
85
|
+
if (!cap) return undefined
|
|
86
|
+
const key = cap.toLowerCase()
|
|
87
|
+
const mapped = LINE_CAP_MAP[key]
|
|
88
|
+
if (!mapped) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Invalid stroke cap "${cap}". Allowed: ${LINE_CAP_VALUES.map((v) => `"${v}"`).join(', ')}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return mapped
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
// Fast node creation for batch operations - skips full serialization
|
|
61
97
|
async function createNodeFast(
|
|
62
98
|
command: string,
|
|
@@ -589,16 +625,50 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
589
625
|
}
|
|
590
626
|
|
|
591
627
|
case 'create-line': {
|
|
592
|
-
const { x, y, length, rotation, name, parentId, stroke, strokeWeight } =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
628
|
+
const { x, y, length, rotation, name, parentId, stroke, strokeWeight, startCap, endCap } =
|
|
629
|
+
args as {
|
|
630
|
+
x: number
|
|
631
|
+
y: number
|
|
632
|
+
length: number
|
|
633
|
+
rotation?: number
|
|
634
|
+
name?: string
|
|
635
|
+
parentId?: string
|
|
636
|
+
stroke?: string
|
|
637
|
+
strokeWeight?: number
|
|
638
|
+
startCap?: string
|
|
639
|
+
endCap?: string
|
|
640
|
+
}
|
|
641
|
+
const startCapValue = normalizeLineCap(startCap)
|
|
642
|
+
const endCapValue = normalizeLineCap(endCap)
|
|
643
|
+
const useVector =
|
|
644
|
+
(startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
|
|
645
|
+
|
|
646
|
+
// If caps are specified, use VectorNetwork for independent start/end caps
|
|
647
|
+
if (useVector) {
|
|
648
|
+
const vector = figma.createVector()
|
|
649
|
+
vector.x = x
|
|
650
|
+
vector.y = y
|
|
651
|
+
|
|
652
|
+
vector.vectorNetwork = {
|
|
653
|
+
vertices: [
|
|
654
|
+
{ x: 0, y: 0, strokeCap: startCapValue ?? 'NONE' },
|
|
655
|
+
{ x: length, y: 0, strokeCap: endCapValue ?? 'NONE' }
|
|
656
|
+
],
|
|
657
|
+
segments: [
|
|
658
|
+
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
659
|
+
],
|
|
660
|
+
regions: []
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (stroke) vector.strokes = [await createSolidPaint(stroke)]
|
|
664
|
+
if (strokeWeight !== undefined) vector.strokeWeight = strokeWeight
|
|
665
|
+
if (rotation) vector.rotation = rotation
|
|
666
|
+
if (name) vector.name = name
|
|
667
|
+
await appendToParent(vector, parentId)
|
|
668
|
+
return serializeNode(vector)
|
|
601
669
|
}
|
|
670
|
+
|
|
671
|
+
// No caps specified - use simple Line
|
|
602
672
|
const line = figma.createLine()
|
|
603
673
|
line.x = x
|
|
604
674
|
line.y = y
|
|
@@ -55,6 +55,42 @@ function loadFont(family: string, style: string): Promise<void> | void {
|
|
|
55
55
|
return promise
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const LINE_CAP_VALUES = [
|
|
59
|
+
'none',
|
|
60
|
+
'round',
|
|
61
|
+
'square',
|
|
62
|
+
'arrow',
|
|
63
|
+
'arrow-lines',
|
|
64
|
+
'arrow-equilateral',
|
|
65
|
+
'triangle',
|
|
66
|
+
'diamond',
|
|
67
|
+
'circle'
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const LINE_CAP_MAP: Record<string, StrokeCap> = {
|
|
71
|
+
none: 'NONE',
|
|
72
|
+
round: 'ROUND',
|
|
73
|
+
square: 'SQUARE',
|
|
74
|
+
arrow: 'ARROW_LINES',
|
|
75
|
+
'arrow-lines': 'ARROW_LINES',
|
|
76
|
+
'arrow-equilateral': 'ARROW_EQUILATERAL',
|
|
77
|
+
triangle: 'TRIANGLE_FILLED',
|
|
78
|
+
diamond: 'DIAMOND_FILLED',
|
|
79
|
+
circle: 'CIRCLE_FILLED'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeLineCap(cap?: string): StrokeCap | undefined {
|
|
83
|
+
if (!cap) return undefined
|
|
84
|
+
const key = cap.toLowerCase()
|
|
85
|
+
const mapped = LINE_CAP_MAP[key]
|
|
86
|
+
if (!mapped) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid stroke cap "${cap}". Allowed: ${LINE_CAP_VALUES.map((v) => `"${v}"`).join(', ')}`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return mapped
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
// Fast node creation for batch operations - skips full serialization
|
|
59
95
|
async function createNodeFast(
|
|
60
96
|
command: string,
|
|
@@ -863,16 +899,50 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
863
899
|
}
|
|
864
900
|
|
|
865
901
|
case 'create-line': {
|
|
866
|
-
const { x, y, length, rotation, name, parentId, stroke, strokeWeight } =
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
902
|
+
const { x, y, length, rotation, name, parentId, stroke, strokeWeight, startCap, endCap } =
|
|
903
|
+
args as {
|
|
904
|
+
x: number
|
|
905
|
+
y: number
|
|
906
|
+
length: number
|
|
907
|
+
rotation?: number
|
|
908
|
+
name?: string
|
|
909
|
+
parentId?: string
|
|
910
|
+
stroke?: string
|
|
911
|
+
strokeWeight?: number
|
|
912
|
+
startCap?: string
|
|
913
|
+
endCap?: string
|
|
914
|
+
}
|
|
915
|
+
const startCapValue = normalizeLineCap(startCap)
|
|
916
|
+
const endCapValue = normalizeLineCap(endCap)
|
|
917
|
+
const useVector =
|
|
918
|
+
(startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
|
|
919
|
+
|
|
920
|
+
// If caps are specified, use VectorNetwork for independent start/end caps
|
|
921
|
+
if (useVector) {
|
|
922
|
+
const vector = figma.createVector()
|
|
923
|
+
vector.x = x
|
|
924
|
+
vector.y = y
|
|
925
|
+
|
|
926
|
+
vector.vectorNetwork = {
|
|
927
|
+
vertices: [
|
|
928
|
+
{ x: 0, y: 0, strokeCap: startCapValue ?? 'NONE' },
|
|
929
|
+
{ x: length, y: 0, strokeCap: endCapValue ?? 'NONE' }
|
|
930
|
+
],
|
|
931
|
+
segments: [
|
|
932
|
+
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
933
|
+
],
|
|
934
|
+
regions: []
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (stroke) vector.strokes = [await createSolidPaint(stroke)]
|
|
938
|
+
if (strokeWeight !== undefined) vector.strokeWeight = strokeWeight
|
|
939
|
+
if (rotation) vector.rotation = rotation
|
|
940
|
+
if (name) vector.name = name
|
|
941
|
+
await appendToParent(vector, parentId)
|
|
942
|
+
return serializeNode(vector)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// No caps specified - use simple Line
|
|
876
946
|
const line = figma.createLine()
|
|
877
947
|
line.x = x
|
|
878
948
|
line.y = y
|
|
@@ -2390,7 +2460,9 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2390
2460
|
overflow: '__overflow',
|
|
2391
2461
|
shadow: '__shadow',
|
|
2392
2462
|
blur: '__blur',
|
|
2393
|
-
blendMode: '__blendMode'
|
|
2463
|
+
blendMode: '__blendMode',
|
|
2464
|
+
startCap: '__startCap',
|
|
2465
|
+
endCap: '__endCap'
|
|
2394
2466
|
}
|
|
2395
2467
|
|
|
2396
2468
|
const DIRECTION_MAP: Record<string, string> = {
|
|
@@ -2433,6 +2505,17 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2433
2505
|
shadow?: string
|
|
2434
2506
|
blur?: number
|
|
2435
2507
|
blendMode?: string
|
|
2508
|
+
startCap?: string
|
|
2509
|
+
endCap?: string
|
|
2510
|
+
}> = []
|
|
2511
|
+
|
|
2512
|
+
// Nodes that need vector replacement for stroke caps
|
|
2513
|
+
const lineCapNodes: Array<{
|
|
2514
|
+
path: number[]
|
|
2515
|
+
startCap?: StrokeCap
|
|
2516
|
+
endCap?: StrokeCap
|
|
2517
|
+
stroke?: string
|
|
2518
|
+
strokeWidth?: number
|
|
2436
2519
|
}> = []
|
|
2437
2520
|
|
|
2438
2521
|
function processProps(
|
|
@@ -2548,6 +2631,33 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2548
2631
|
const isText = type === 'text'
|
|
2549
2632
|
const processed = processProps(props || {}, isText)
|
|
2550
2633
|
|
|
2634
|
+
// Handle <line> with startCap/endCap - mark for VectorNetwork replacement
|
|
2635
|
+
if (type === 'line' && (props.startCap || props.endCap)) {
|
|
2636
|
+
const startCapValue = normalizeLineCap(props.startCap as string | undefined)
|
|
2637
|
+
const endCapValue = normalizeLineCap(props.endCap as string | undefined)
|
|
2638
|
+
const useVector =
|
|
2639
|
+
(startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
|
|
2640
|
+
|
|
2641
|
+
if (useVector) {
|
|
2642
|
+
lineCapNodes.push({
|
|
2643
|
+
path: [...path],
|
|
2644
|
+
startCap: startCapValue,
|
|
2645
|
+
endCap: endCapValue,
|
|
2646
|
+
stroke: processed.stroke as string | undefined,
|
|
2647
|
+
strokeWidth: processed.strokeWidth as number | undefined
|
|
2648
|
+
})
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// Don't pass startCap/endCap to Widget Line
|
|
2652
|
+
const cleanProps = { ...processed }
|
|
2653
|
+
delete cleanProps.startCap
|
|
2654
|
+
delete cleanProps.endCap
|
|
2655
|
+
delete cleanProps.__startCap
|
|
2656
|
+
delete cleanProps.__endCap
|
|
2657
|
+
// Create normal Line as placeholder
|
|
2658
|
+
return h(Line, cleanProps)
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2551
2661
|
// Track wrap nodes
|
|
2552
2662
|
if (processed.__wrap) {
|
|
2553
2663
|
wrapNodes.push({ path: [...path], rowGap: processed.__rowGap as number | undefined })
|
|
@@ -2579,7 +2689,9 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2579
2689
|
'__overflow',
|
|
2580
2690
|
'__shadow',
|
|
2581
2691
|
'__blur',
|
|
2582
|
-
'__blendMode'
|
|
2692
|
+
'__blendMode',
|
|
2693
|
+
'__startCap',
|
|
2694
|
+
'__endCap'
|
|
2583
2695
|
]
|
|
2584
2696
|
for (const key of postKeys) {
|
|
2585
2697
|
if (processed[key] !== undefined) {
|
|
@@ -2602,7 +2714,7 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2602
2714
|
}
|
|
2603
2715
|
|
|
2604
2716
|
const widgetTree = buildTree(tree)
|
|
2605
|
-
|
|
2717
|
+
let node = await figma.createNodeFromJSXAsync(
|
|
2606
2718
|
widgetTree as Parameters<typeof figma.createNodeFromJSXAsync>[0]
|
|
2607
2719
|
)
|
|
2608
2720
|
|
|
@@ -2797,6 +2909,94 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
|
|
|
2797
2909
|
}
|
|
2798
2910
|
}
|
|
2799
2911
|
|
|
2912
|
+
// Replace Line placeholders with Vector nodes for stroke cap support
|
|
2913
|
+
const copyLineStyle = (from: LineNode, to: VectorNode) => {
|
|
2914
|
+
const keys = [
|
|
2915
|
+
'visible',
|
|
2916
|
+
'locked',
|
|
2917
|
+
'opacity',
|
|
2918
|
+
'blendMode',
|
|
2919
|
+
'effects',
|
|
2920
|
+
'effectStyleId',
|
|
2921
|
+
'fills',
|
|
2922
|
+
'fillStyleId',
|
|
2923
|
+
'strokes',
|
|
2924
|
+
'strokeStyleId',
|
|
2925
|
+
'strokeAlign',
|
|
2926
|
+
'strokeJoin',
|
|
2927
|
+
'strokeMiterLimit',
|
|
2928
|
+
'dashPattern',
|
|
2929
|
+
'layoutAlign',
|
|
2930
|
+
'layoutGrow',
|
|
2931
|
+
'layoutPositioning',
|
|
2932
|
+
'constraints'
|
|
2933
|
+
]
|
|
2934
|
+
for (const key of keys) {
|
|
2935
|
+
const value = (from as Record<string, unknown>)[key]
|
|
2936
|
+
if (value !== undefined && value !== figma.mixed) {
|
|
2937
|
+
;(to as Record<string, unknown>)[key] = value
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
for (const { path, startCap, endCap, stroke, strokeWidth } of lineCapNodes) {
|
|
2942
|
+
let target: SceneNode = node
|
|
2943
|
+
let parent: FrameNode | null = null
|
|
2944
|
+
let childIndex = 0
|
|
2945
|
+
for (let i = 0; i < path.length; i++) {
|
|
2946
|
+
if ('children' in target) {
|
|
2947
|
+
parent = target as FrameNode
|
|
2948
|
+
childIndex = path[i]
|
|
2949
|
+
target = parent.children[childIndex]
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (target && target.type === 'LINE') {
|
|
2953
|
+
const lineNode = target as LineNode
|
|
2954
|
+
const vector = figma.createVector()
|
|
2955
|
+
vector.x = lineNode.x
|
|
2956
|
+
vector.y = lineNode.y
|
|
2957
|
+
vector.name = lineNode.name
|
|
2958
|
+
vector.rotation = lineNode.rotation
|
|
2959
|
+
const length = lineNode.width
|
|
2960
|
+
|
|
2961
|
+
vector.vectorNetwork = {
|
|
2962
|
+
vertices: [
|
|
2963
|
+
{ x: 0, y: 0, strokeCap: startCap ?? 'NONE' },
|
|
2964
|
+
{ x: length, y: 0, strokeCap: endCap ?? 'NONE' }
|
|
2965
|
+
],
|
|
2966
|
+
segments: [
|
|
2967
|
+
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
2968
|
+
],
|
|
2969
|
+
regions: []
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// Copy stroke properties
|
|
2973
|
+
if (stroke) {
|
|
2974
|
+
vector.strokes = [await createSolidPaint(stroke)]
|
|
2975
|
+
} else if (lineNode.strokes.length > 0) {
|
|
2976
|
+
vector.strokes = [...lineNode.strokes]
|
|
2977
|
+
}
|
|
2978
|
+
if (strokeWidth !== undefined) {
|
|
2979
|
+
vector.strokeWeight = strokeWidth
|
|
2980
|
+
} else {
|
|
2981
|
+
vector.strokeWeight = lineNode.strokeWeight
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
copyLineStyle(lineNode, vector)
|
|
2985
|
+
|
|
2986
|
+
const lineParent = lineNode.parent
|
|
2987
|
+
if (lineParent && 'insertChild' in lineParent) {
|
|
2988
|
+
const index = lineParent.children.indexOf(lineNode as SceneNode)
|
|
2989
|
+
lineParent.insertChild(index === -1 ? childIndex : index, vector)
|
|
2990
|
+
lineNode.remove()
|
|
2991
|
+
if (path.length === 0) node = vector
|
|
2992
|
+
} else {
|
|
2993
|
+
figma.currentPage.appendChild(vector)
|
|
2994
|
+
lineNode.remove()
|
|
2995
|
+
if (path.length === 0) node = vector
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
|
|
2800
3000
|
// Attach to parent
|
|
2801
3001
|
if (parentId) {
|
|
2802
3002
|
const parent = await figma.getNodeByIdAsync(parentId)
|