canvas-can-do 0.1.9 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +405 -27
- package/dist/{SharedSystems-CZrQpXWg.js → SharedSystems-Cg8-nEUA.js} +2 -2
- package/dist/{WebGLRenderer-guxg9y4k.js → WebGLRenderer-CWOX7V4e.js} +3 -3
- package/dist/{WebGPURenderer-v9P1o84e.js → WebGPURenderer-pdpNSHly.js} +3 -3
- package/dist/{browserAll-CKZpLV_L.js → browserAll-D358sNiQ.js} +2 -2
- package/dist/canvas-can-do.js +1 -1
- package/dist/canvas-can-do.umd.cjs +110 -110
- package/dist/{colorToUniform-DOf21nfM.js → colorToUniform-Dy_GZxOz.js} +1 -1
- package/dist/{index-D_NFBR4n.js → index-CbUjW4XO.js} +5361 -3842
- package/dist/types/PointerController.d.ts +30 -0
- package/dist/types/PointerController.d.ts.map +1 -1
- package/dist/types/core/export/exportSettings.d.ts +25 -0
- package/dist/types/core/export/exportSettings.d.ts.map +1 -0
- package/dist/types/core/fonts/fontOptions.d.ts +8 -0
- package/dist/types/core/fonts/fontOptions.d.ts.map +1 -0
- package/dist/types/core/history/HistoryManager.d.ts +21 -1
- package/dist/types/core/history/HistoryManager.d.ts.map +1 -1
- package/dist/types/core/layers/LayerHierarchy.d.ts.map +1 -1
- package/dist/types/core/nodes/BaseNode.d.ts +3 -2
- package/dist/types/core/nodes/BaseNode.d.ts.map +1 -1
- package/dist/types/core/nodes/FrameNode.d.ts +42 -0
- package/dist/types/core/nodes/FrameNode.d.ts.map +1 -0
- package/dist/types/core/nodes/ImageNode.d.ts.map +1 -1
- package/dist/types/core/nodes/LineNode.d.ts.map +1 -1
- package/dist/types/core/nodes/TextNode.d.ts.map +1 -1
- package/dist/types/core/nodes/index.d.ts +1 -0
- package/dist/types/core/nodes/index.d.ts.map +1 -1
- package/dist/types/core/selection/LineTransformController.d.ts +1 -0
- package/dist/types/core/selection/LineTransformController.d.ts.map +1 -1
- package/dist/types/core/selection/SelectionManager.d.ts +27 -2
- package/dist/types/core/selection/SelectionManager.d.ts.map +1 -1
- package/dist/types/core/selection/TransformController.d.ts +1 -0
- package/dist/types/core/selection/TransformController.d.ts.map +1 -1
- package/dist/types/events.d.ts +1 -0
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +163 -17
- package/dist/types/index.d.ts.map +1 -1
- package/dist/{webworkerAll-B_02fub1.js → webworkerAll-EieaH4C5.js} +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -21,12 +21,13 @@ app.useTool('select');
|
|
|
21
21
|
|
|
22
22
|
## Key Features
|
|
23
23
|
|
|
24
|
+
- Frame creation: via API (`addFrame`) and drag-to-draw frame tool (`useTool('frame')` / `F`)
|
|
24
25
|
- Shapes: rectangle, ellipse, line, star, text, image
|
|
25
26
|
- Transform tools: move, resize, rotate, multi-select
|
|
26
27
|
- Shift to constrain resize ratio
|
|
27
28
|
- Undo/redo (Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl/Cmd+Y)
|
|
28
|
-
- Export: PNG/JPG/SVG
|
|
29
|
-
- Save/Load: JSON with embedded image data URLs
|
|
29
|
+
- Export: PNG/JPG/SVG via node-linked presets
|
|
30
|
+
- Save/Load: JSON with embedded image data URLs + document export preset store
|
|
30
31
|
- Rulers with pan/zoom indicators
|
|
31
32
|
|
|
32
33
|
## API Highlights
|
|
@@ -34,47 +35,424 @@ app.useTool('select');
|
|
|
34
35
|
```ts
|
|
35
36
|
// Tools
|
|
36
37
|
app.useTool('rectangle');
|
|
37
|
-
|
|
38
|
-
// Export raster (PNG/JPG)
|
|
39
|
-
const png = await app.exportRaster({ type: 'png', scope: 'all' });
|
|
40
|
-
|
|
41
|
-
// Export SVG (embed images)
|
|
42
|
-
const svg = await app.exportSVG({
|
|
43
|
-
scope: 'selection',
|
|
44
|
-
imageEmbed: 'display', // 'original' | 'display' | 'max'
|
|
45
|
-
imageMaxEdge: 2048,
|
|
46
|
-
});
|
|
38
|
+
app.useTool('frame'); // drag to draw a frame
|
|
47
39
|
|
|
48
40
|
// Save/Load JSON (embedded images)
|
|
49
41
|
const doc = await app.exportJSON();
|
|
50
42
|
if (doc) await app.importJSON(doc);
|
|
51
43
|
|
|
44
|
+
// Frames
|
|
45
|
+
const frame = await app.addFrame({
|
|
46
|
+
name: 'Frame 1',
|
|
47
|
+
width: 1280,
|
|
48
|
+
height: 720,
|
|
49
|
+
backgroundColor: '#ffffff',
|
|
50
|
+
clipContent: true,
|
|
51
|
+
});
|
|
52
|
+
const frames = app.getFrames();
|
|
53
|
+
|
|
54
|
+
// Document export preset store linked to node ids (persisted in exportJSON/importJSON)
|
|
55
|
+
const preset = await app.addExportSetting(frame!.id, {
|
|
56
|
+
format: 'png',
|
|
57
|
+
scale: 2,
|
|
58
|
+
suffix: '@2x',
|
|
59
|
+
});
|
|
60
|
+
const asset = preset ? await app.exportNodeByPreset(frame!.id, preset.id) : null;
|
|
61
|
+
if (asset?.contentType === 'dataUrl') {
|
|
62
|
+
// asset.content => data URL
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
// Access Pixi Application
|
|
53
66
|
const pixiApp = app.getPixiApp();
|
|
67
|
+
|
|
68
|
+
// Layers (for external layer panel UIs)
|
|
69
|
+
const flatLayers = app.getFlatLayers({ recursive: true, topFirst: true });
|
|
70
|
+
const canMove = app.canMoveLayers(['node-a'], 'node-b', 'before');
|
|
71
|
+
if (canMove.ok) {
|
|
72
|
+
await app.moveLayers(['node-a'], 'node-b', 'before');
|
|
73
|
+
}
|
|
54
74
|
```
|
|
55
75
|
|
|
56
|
-
##
|
|
76
|
+
## Layer Reordering API
|
|
77
|
+
|
|
78
|
+
Use this API when your layer panel UI lives outside this library and you need a stable, id-based way to reorder layers and move nodes in/out of groups.
|
|
79
|
+
|
|
80
|
+
### Goals
|
|
81
|
+
|
|
82
|
+
- Keep UI code decoupled from Pixi internals
|
|
83
|
+
- Support reorder and reparent in one atomic operation
|
|
84
|
+
- Preserve visual placement when moving across parents/groups
|
|
85
|
+
- Keep undo/redo clean (`1 drop = 1 history entry`)
|
|
86
|
+
|
|
87
|
+
### Read Layers
|
|
57
88
|
|
|
58
89
|
```ts
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
quality?: number, // for JPG
|
|
64
|
-
padding?: number, // extra pixels around bounds
|
|
65
|
-
background?: string, // e.g. '#ffffff'
|
|
90
|
+
const layers = app.getFlatLayers({
|
|
91
|
+
parentId: null, // null = root object layer
|
|
92
|
+
recursive: true, // include descendants
|
|
93
|
+
topFirst: true, // top-most first (panel-friendly)
|
|
66
94
|
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each item includes:
|
|
98
|
+
|
|
99
|
+
- `id`, `type`, `name`
|
|
100
|
+
- `parentId`, `depth`
|
|
101
|
+
- `zIndex`
|
|
102
|
+
- `isGroup`, `childCount`
|
|
103
|
+
- `visible`, `locked`
|
|
104
|
+
|
|
105
|
+
### Move Semantics
|
|
106
|
+
|
|
107
|
+
Position values:
|
|
108
|
+
|
|
109
|
+
- `'before'`: insert source before target in target's parent
|
|
110
|
+
- `'after'`: insert source after target in target's parent
|
|
111
|
+
- `'inside'`: insert source into target (target must be a group or frame)
|
|
112
|
+
|
|
113
|
+
Helpers:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const checkOne = app.canMoveLayer(sourceId, targetId, 'before');
|
|
117
|
+
const checkMany = app.canMoveLayers(sourceIds, targetId, 'inside');
|
|
118
|
+
|
|
119
|
+
if (checkMany.ok) {
|
|
120
|
+
await app.moveLayers(sourceIds, targetId, 'inside');
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Write methods:
|
|
125
|
+
|
|
126
|
+
- `moveLayer(sourceId, targetId, position, options?)`
|
|
127
|
+
- `moveLayers(sourceIds, targetId, position, options?)`
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
|
|
131
|
+
- `recordHistory?: boolean` (default `true`)
|
|
132
|
+
|
|
133
|
+
### Validation Rules
|
|
134
|
+
|
|
135
|
+
Move is rejected (`ok: false`) when:
|
|
136
|
+
|
|
137
|
+
- source/target ids are invalid
|
|
138
|
+
- source and target are the same node
|
|
139
|
+
- `position === 'inside'` but target is not a group/frame
|
|
140
|
+
- source node is locked
|
|
141
|
+
- destination parent is locked
|
|
142
|
+
- move would create a parent/child cycle (ancestor into descendant)
|
|
143
|
+
- moving a frame into non-root parent (frames are root-only)
|
|
144
|
+
|
|
145
|
+
### Atomic Move Behavior
|
|
67
146
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
`moveLayer(s)` performs one transaction-like operation:
|
|
148
|
+
|
|
149
|
+
1. Validate request
|
|
150
|
+
2. Normalize source list (avoid parent+child duplicate moves)
|
|
151
|
+
3. Resolve insertion index
|
|
152
|
+
4. Capture each source node world transform
|
|
153
|
+
5. Reparent/reorder
|
|
154
|
+
6. Re-apply transform in destination parent space (so node does not jump)
|
|
155
|
+
7. Emit `layer:changed`
|
|
156
|
+
8. Capture history (unless `recordHistory: false`)
|
|
157
|
+
|
|
158
|
+
This is what enables: selecting an item inside a group, dragging it above an external layer, and having it both leave the group and land at the correct z-order in one drop.
|
|
159
|
+
|
|
160
|
+
### Typical External UI Flow
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// 1) render panel from library state
|
|
164
|
+
const layers = app.getFlatLayers({ recursive: true, topFirst: true });
|
|
165
|
+
|
|
166
|
+
// 2) while dragging, probe validity
|
|
167
|
+
const probe = app.canMoveLayers(dragSourceIds, hoverTargetId, hoverPosition);
|
|
168
|
+
showDropIndicator(probe.ok);
|
|
169
|
+
|
|
170
|
+
// 3) on drop, commit once
|
|
171
|
+
if (probe.ok) {
|
|
172
|
+
await app.moveLayers(dragSourceIds, hoverTargetId, hoverPosition);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4) listen and re-render
|
|
176
|
+
app.addEventListener('layer:changed', () => {
|
|
177
|
+
rerenderPanel(app.getFlatLayers({ recursive: true, topFirst: true }));
|
|
75
178
|
});
|
|
76
179
|
```
|
|
77
180
|
|
|
181
|
+
## Frame Concept (Figma-like)
|
|
182
|
+
|
|
183
|
+
`Frame` is different from `Group`.
|
|
184
|
+
|
|
185
|
+
- `Group`: logical grouping for transforming multiple nodes together
|
|
186
|
+
- `Frame`: a bounded working area with optional background and clipping
|
|
187
|
+
|
|
188
|
+
Think of `Frame` as an artboard/container that can also be used as an export boundary.
|
|
189
|
+
|
|
190
|
+
### Frame Behavior
|
|
191
|
+
|
|
192
|
+
- Frame is **root-only**:
|
|
193
|
+
- cannot be grouped
|
|
194
|
+
- cannot be child of group/frame
|
|
195
|
+
- Can contain child nodes (like a container)
|
|
196
|
+
- Has explicit `width` and `height`
|
|
197
|
+
- Supports background color or transparent background
|
|
198
|
+
- Supports clipping/masking at frame bounds (`clipContent`)
|
|
199
|
+
- Can be target of drag/drop reparent operations (`inside`)
|
|
200
|
+
- Can be used as export target node for preset-based export
|
|
201
|
+
|
|
202
|
+
### Drawing and Moving In/Out of Frame
|
|
203
|
+
|
|
204
|
+
- Frame can be created by API (`addFrame`) or drag tool (`frame` / `F`)
|
|
205
|
+
- Drawing non-frame shapes starts in the frame under pointer (if any), otherwise root canvas
|
|
206
|
+
- Drag/drop can move nodes into a frame (`inside`) or out of a frame (`before`/`after` against external target)
|
|
207
|
+
- Reparent + z-order update happen in one atomic operation
|
|
208
|
+
- World transform is preserved when reparenting so nodes do not visually jump
|
|
209
|
+
- Auto drag-reparent is blocked for group-managed nodes (group or descendants of group)
|
|
210
|
+
|
|
211
|
+
### Canvas Presentation (Editor Visual)
|
|
212
|
+
|
|
213
|
+
Frame should look visibly different from regular objects so users can immediately identify it as a working boundary.
|
|
214
|
+
|
|
215
|
+
- Visible frame border at all times (scale-aware stroke)
|
|
216
|
+
- Frame name label near top-left corner
|
|
217
|
+
- Optional background fill (or transparent mode)
|
|
218
|
+
- Clear visual cue when clipping is enabled
|
|
219
|
+
- Distinct `idle`, `hover`, `selected`, and `drop-target` states
|
|
220
|
+
|
|
221
|
+
Recommended editor-only rendering split:
|
|
222
|
+
|
|
223
|
+
- `FrameNode` for document content (background + children + clip)
|
|
224
|
+
- `FrameOverlay` for editor cues (border, label, highlight, drop indicator)
|
|
225
|
+
|
|
226
|
+
This keeps export output clean while still giving strong authoring feedback on canvas.
|
|
227
|
+
|
|
228
|
+
### Suggested Frame Visual States
|
|
229
|
+
|
|
230
|
+
- `idle`: neutral border, subtle label
|
|
231
|
+
- `hover`: stronger border to indicate targetability
|
|
232
|
+
- `selected`: accent border + resize handles + prominent label
|
|
233
|
+
- `drop-target`: temporary insertion highlight while dragging
|
|
234
|
+
|
|
235
|
+
### Export Semantics
|
|
236
|
+
|
|
237
|
+
- Support exporting by frame id (raster/SVG)
|
|
238
|
+
- If `clipContent=true`, export respects frame bounds
|
|
239
|
+
- If transparent background is selected, exported background remains transparent
|
|
240
|
+
|
|
241
|
+
### Suggested API Surface
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// creation / query
|
|
245
|
+
const frame = await app.addFrame({
|
|
246
|
+
name: 'Frame 1',
|
|
247
|
+
x: 100,
|
|
248
|
+
y: 80,
|
|
249
|
+
width: 1280,
|
|
250
|
+
height: 720,
|
|
251
|
+
backgroundColor: '#ffffff', // null for transparent
|
|
252
|
+
clipContent: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const frames = app.getFrames();
|
|
256
|
+
|
|
257
|
+
// export by preset
|
|
258
|
+
const pngPreset = await app.addExportSetting(frame.id, { format: 'png', scale: 1, suffix: '' });
|
|
259
|
+
const pngAsset = pngPreset ? await app.exportNodeByPreset(frame.id, pngPreset.id) : null;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Export Presets (Figma-like)
|
|
263
|
+
|
|
264
|
+
Presets are stored in a centralized document-level registry (`exportStore`) and linked to nodes by id.
|
|
265
|
+
This keeps lookup/edit fast and avoids recursive node scans.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
type NodeExportPreset = {
|
|
269
|
+
id: string;
|
|
270
|
+
format: 'png' | 'jpg' | 'svg';
|
|
271
|
+
scale: number;
|
|
272
|
+
suffix: string; // e.g. '', '@2x'
|
|
273
|
+
quality?: number; // jpg
|
|
274
|
+
padding?: number;
|
|
275
|
+
backgroundMode?: 'auto' | 'transparent' | 'solid';
|
|
276
|
+
backgroundColor?: string; // used when backgroundMode = 'solid'
|
|
277
|
+
imageEmbed?: 'original' | 'display' | 'max'; // svg
|
|
278
|
+
imageMaxEdge?: number; // svg
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Preset APIs
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// add preset and link to node
|
|
286
|
+
const added = await app.addExportSetting(nodeId, {
|
|
287
|
+
format: 'png',
|
|
288
|
+
scale: 1,
|
|
289
|
+
suffix: '',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// get preset entity by id
|
|
293
|
+
const preset = app.getExportSettingById(added!.id);
|
|
294
|
+
|
|
295
|
+
// edit preset entity
|
|
296
|
+
await app.editExportSetting(added!.id, {
|
|
297
|
+
scale: 2,
|
|
298
|
+
suffix: '@2x',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// list preset ids linked to a node
|
|
302
|
+
const presetIds = app.getExportSettingIds(nodeId);
|
|
303
|
+
|
|
304
|
+
// list all presets in document (without scanning nodes manually)
|
|
305
|
+
const allPresets = app.getAllExportSettings();
|
|
306
|
+
// [{ id, preset, linkedNodeIds }]
|
|
307
|
+
|
|
308
|
+
// inspect linked nodes before editing shared presets
|
|
309
|
+
const usedBy = app.getExportPresetUsage(preset!.id);
|
|
310
|
+
|
|
311
|
+
// export one node using its preset
|
|
312
|
+
const asset = await app.exportNodeByPreset(nodeId, preset!.id);
|
|
313
|
+
if (asset) {
|
|
314
|
+
// asset.filename, asset.mimeType, asset.contentType, asset.content
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// export many nodes in one call
|
|
318
|
+
const frameAPreset = await app.addExportSetting(frameA, { format: 'png', scale: 1, suffix: '' });
|
|
319
|
+
const frameBPreset = await app.addExportSetting(frameB, { format: 'svg', scale: 1, suffix: '' });
|
|
320
|
+
const assets = await app.exportNodesByPreset([
|
|
321
|
+
{ nodeId: frameA, presetId: frameAPreset!.id },
|
|
322
|
+
{ nodeId: frameB, presetId: frameBPreset!.id },
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
// delete preset entity (and unlink from all nodes)
|
|
326
|
+
await app.deleteExportSetting(added!.id);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Export Contract
|
|
330
|
+
|
|
331
|
+
Preset-first only:
|
|
332
|
+
|
|
333
|
+
- Create preset via `addExportSetting(nodeId, preset)`
|
|
334
|
+
- Edit/delete via `editExportSetting` / `deleteExportSetting`
|
|
335
|
+
- Export only via `exportNodeByPreset(nodeId, presetId)` or `exportNodesByPreset(...)`
|
|
336
|
+
- `presetId` must be explicitly provided and linked to that node
|
|
337
|
+
|
|
338
|
+
Format behavior:
|
|
339
|
+
|
|
340
|
+
- `jpg` should use opaque background (`backgroundMode: 'solid'`, `backgroundColor`)
|
|
341
|
+
- `png` / `svg` can keep transparency with `backgroundMode: 'auto'` or `'transparent'`
|
|
342
|
+
|
|
343
|
+
## Save/Load Contract (Revision)
|
|
344
|
+
|
|
345
|
+
This section defines the document model expectations after adding frames and expanded layer behavior.
|
|
346
|
+
|
|
347
|
+
### Document Goals
|
|
348
|
+
|
|
349
|
+
- Preserve exact hierarchy (parent/child + order).
|
|
350
|
+
- Preserve per-node transform and visibility/locking.
|
|
351
|
+
- Preserve frame-specific properties and behavior.
|
|
352
|
+
- Enforce frame root-only invariant on import/restore.
|
|
353
|
+
|
|
354
|
+
### Required Frame Fields (logical model)
|
|
355
|
+
|
|
356
|
+
- geometry: `x`, `y`, `width`, `height`
|
|
357
|
+
- transform: `rotation` (currently fixed to 0 by interaction), `scale`
|
|
358
|
+
- visibility/state: `visible`, `locked`
|
|
359
|
+
- frame style: `backgroundColor`
|
|
360
|
+
- frame behavior: `clipContent`
|
|
361
|
+
- hierarchy: `children[]` in stable z-order
|
|
362
|
+
|
|
363
|
+
### Round-trip Invariants
|
|
364
|
+
|
|
365
|
+
After `exportJSON -> importJSON`:
|
|
366
|
+
|
|
367
|
+
- node count and hierarchy must match
|
|
368
|
+
- z-order must match
|
|
369
|
+
- frame bounds/style/clip settings must match
|
|
370
|
+
- locked/visible state must match
|
|
371
|
+
- export output for same node + preset should remain equivalent
|
|
372
|
+
- frame never appears as a child of group/frame after import normalization
|
|
373
|
+
|
|
374
|
+
### Versioning and Migration
|
|
375
|
+
|
|
376
|
+
- Current baseline is `document.version = 1` only.
|
|
377
|
+
- Keep schema simple while not in production.
|
|
378
|
+
- If schema changes later, introduce migrations in the next version.
|
|
379
|
+
|
|
380
|
+
Import normalization defaults for frame fields:
|
|
381
|
+
|
|
382
|
+
- `backgroundColor = '#ffffff'`
|
|
383
|
+
- `clipContent = true`
|
|
384
|
+
|
|
385
|
+
### Server Persistence Flow
|
|
386
|
+
|
|
387
|
+
Use `exportJSON()` as the payload to your backend.
|
|
388
|
+
This payload now includes `exportStore` (preset registry + node links), so loading back with `importJSON()` restores export presets too.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
// save
|
|
392
|
+
const doc = await app.exportJSON();
|
|
393
|
+
await fetch('/api/documents/123', {
|
|
394
|
+
method: 'PUT',
|
|
395
|
+
headers: { 'content-type': 'application/json' },
|
|
396
|
+
body: JSON.stringify(doc),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// load
|
|
400
|
+
const loaded = await fetch('/api/documents/123').then((r) => r.json());
|
|
401
|
+
await app.importJSON(loaded);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Round-trip (including export presets)
|
|
405
|
+
|
|
406
|
+
After `exportJSON -> save to server -> load from server -> importJSON`:
|
|
407
|
+
|
|
408
|
+
- node hierarchy/order must match
|
|
409
|
+
- frame properties must match
|
|
410
|
+
- export registry + node links must match (`id/format/scale/suffix/...`)
|
|
411
|
+
- exporting the same node + preset should produce equivalent output
|
|
412
|
+
|
|
413
|
+
### Post-change Checklist
|
|
414
|
+
|
|
415
|
+
- Verify preset-based export for PNG/JPG/SVG on target nodes.
|
|
416
|
+
- Verify frame with `clipContent` on/off.
|
|
417
|
+
- Verify frame remains root-only after save/load.
|
|
418
|
+
- Verify lock/visible states survive save/load.
|
|
419
|
+
- Verify undo/redo still works immediately after load.
|
|
420
|
+
|
|
421
|
+
## Interaction Policy
|
|
422
|
+
|
|
423
|
+
- Group-first hit test: pointer hover/click on a group or any descendant selects the group.
|
|
424
|
+
- Frame-child-first hit test: inside frame, children are preferred over selecting frame body.
|
|
425
|
+
- Frame body selection is intentionally disabled on canvas hit-test; select frame via label or layer/API.
|
|
426
|
+
- Layer panel/API can still select specific child ids directly (`selectNodeById` / `selectNodesById`).
|
|
427
|
+
|
|
428
|
+
## UI Recommendation (Figma-like Export)
|
|
429
|
+
|
|
430
|
+
Suggested UX in the right sidebar when 1 node is selected:
|
|
431
|
+
|
|
432
|
+
1. Section title: `Export`
|
|
433
|
+
2. List rows: one row per preset (`PNG 1x`, `PNG 2x`, `SVG`)
|
|
434
|
+
3. Each row editable: `format`, `scale`, `suffix`, advanced options
|
|
435
|
+
4. Row actions: duplicate / delete preset
|
|
436
|
+
5. Primary button: `Export <NodeName>` (export selected preset)
|
|
437
|
+
6. Secondary button: `Export All` (all presets for current node)
|
|
438
|
+
|
|
439
|
+
Suggested UI behavior:
|
|
440
|
+
|
|
441
|
+
- Auto-save preset edits immediately via `addExportSetting` / `editExportSetting` / `deleteExportSetting`
|
|
442
|
+
- Keep preset `id` stable; use `id` for persistence and updates (shared ids = shared edits)
|
|
443
|
+
- Export requires explicit `presetId` linked to that node (no implicit fallback preset)
|
|
444
|
+
- Show final filename preview from `name + suffix + extension`
|
|
445
|
+
- When exporting multiple selected nodes, call `exportNodesByPreset`
|
|
446
|
+
|
|
447
|
+
## Implementation Guide
|
|
448
|
+
|
|
449
|
+
1. Add an `ExportPanel` in your inspector that reads `getAllExportSettings()` for document-level preset list, and `getExportSettingIds(nodeId)` for per-node links.
|
|
450
|
+
2. Bind form fields to preset objects and debounce writes to `editExportSetting`.
|
|
451
|
+
3. On export click, call `exportNodeByPreset(nodeId, presetId)`.
|
|
452
|
+
4. If `asset.contentType === 'dataUrl'`, download with anchor `href = dataUrl`.
|
|
453
|
+
5. If `asset.contentType === 'text'` (SVG), create `Blob([asset.content], { type: asset.mimeType })`.
|
|
454
|
+
6. For batch export, call `exportNodesByPreset` then zip/download in your app shell.
|
|
455
|
+
|
|
78
456
|
## Events
|
|
79
457
|
|
|
80
458
|
```ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { x as Oe, n as ie, M as g, G as Fe, a as Le, y as oe, E as u, e as F, z as w, F as He, H as L, I as y, R as H, J as le, K as We, v as m, d as f, k as G, w as W, L as X, N as ze, h as Q, B as k, l as A, O as Ve, u as M, m as S, Q as P, V as Ne, b as je, W as ue, X as de, Y as ce, Z as he, C as U, _ as qe, $ as I, a0 as Z, D as z, a1 as $e, a2 as Ke, P as Ye, i as Je, T as ee, a3 as te, a4 as v, a5 as Xe, a6 as Qe } from "./index-
|
|
2
|
-
import { F as Ze, S as et, B as fe, c as tt } from "./colorToUniform-
|
|
1
|
+
import { x as Oe, n as ie, M as g, G as Fe, a as Le, y as oe, E as u, e as F, z as w, F as He, H as L, I as y, R as H, J as le, K as We, v as m, d as f, k as G, w as W, L as X, N as ze, h as Q, B as k, l as A, O as Ve, u as M, m as S, Q as P, V as Ne, b as je, W as ue, X as de, Y as ce, Z as he, C as U, _ as qe, $ as I, a0 as Z, D as z, a1 as $e, a2 as Ke, P as Ye, i as Je, T as ee, a3 as te, a4 as v, a5 as Xe, a6 as Qe } from "./index-CbUjW4XO.js";
|
|
2
|
+
import { F as Ze, S as et, B as fe, c as tt } from "./colorToUniform-Dy_GZxOz.js";
|
|
3
3
|
var rt = `in vec2 vMaskCoord;
|
|
4
4
|
in vec2 vTextureCoord;
|
|
5
5
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as d, c as G, B as T, w as m, D as S, L as K, a as Be, S as U, v as b, a7 as Ae, m as $, a8 as Ne, d as p, Q as w, l as B, k as A, n as F, M as z, a9 as Y, aa as ye, ab as q, ac as Ce, ad as De, A as Ie, R as Ge, e as v } from "./index-
|
|
2
|
-
import { S as O, b as Z } from "./colorToUniform-
|
|
3
|
-
import { e as Ue, G as Fe, c as Oe, b as Pe, U as Me, R as Le, B as Q, d as N, f as we, S as He, a as ke } from "./SharedSystems-
|
|
1
|
+
import { E as d, c as G, B as T, w as m, D as S, L as K, a as Be, S as U, v as b, a7 as Ae, m as $, a8 as Ne, d as p, Q as w, l as B, k as A, n as F, M as z, a9 as Y, aa as ye, ab as q, ac as Ce, ad as De, A as Ie, R as Ge, e as v } from "./index-CbUjW4XO.js";
|
|
2
|
+
import { S as O, b as Z } from "./colorToUniform-Dy_GZxOz.js";
|
|
3
|
+
import { e as Ue, G as Fe, c as Oe, b as Pe, U as Me, R as Le, B as Q, d as N, f as we, S as He, a as ke } from "./SharedSystems-Cg8-nEUA.js";
|
|
4
4
|
class J {
|
|
5
5
|
constructor() {
|
|
6
6
|
this._tempState = O.for2d(), this._didUploadHash = {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { g as E, E as f, c as L, u as ue, f as ce, D as P, d as v, B as T, h as de, i as A, j as M, w as C, k as x, l as he, m as pe, n as D, o as w, M as k, p as z, q as le, s as F, t as fe, S as I, v as R, A as ge, R as me, e as B } from "./index-
|
|
2
|
-
import { S as O, l as _e, a as be } from "./colorToUniform-
|
|
3
|
-
import { c as xe, u as ye, U as Ge, B as Pe, G as Be, e as Se, R as Te, t as ve, S as Ce, a as Ue } from "./SharedSystems-
|
|
1
|
+
import { g as E, E as f, c as L, u as ue, f as ce, D as P, d as v, B as T, h as de, i as A, j as M, w as C, k as x, l as he, m as pe, n as D, o as w, M as k, p as z, q as le, s as F, t as fe, S as I, v as R, A as ge, R as me, e as B } from "./index-CbUjW4XO.js";
|
|
2
|
+
import { S as O, l as _e, a as be } from "./colorToUniform-Dy_GZxOz.js";
|
|
3
|
+
import { c as xe, u as ye, U as Ge, B as Pe, G as Be, e as Se, R as Te, t as ve, S as Ce, a as Ue } from "./SharedSystems-Cg8-nEUA.js";
|
|
4
4
|
const y = O.for2d();
|
|
5
5
|
class W {
|
|
6
6
|
start(e, t, r) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as M, U as Z, P as m, r as te, E as y, b as ie, w as g, e as P, C as V } from "./index-
|
|
2
|
-
import "./webworkerAll-
|
|
1
|
+
import { T as M, U as Z, P as m, r as te, E as y, b as ie, w as g, e as P, C as V } from "./index-CbUjW4XO.js";
|
|
2
|
+
import "./webworkerAll-EieaH4C5.js";
|
|
3
3
|
class q {
|
|
4
4
|
constructor(e) {
|
|
5
5
|
this._lastTransform = "", this._observer = null, this._tickerAttached = !1, this.updateTranslation = () => {
|
package/dist/canvas-can-do.js
CHANGED