@elixpo/lixsketch 4.5.8

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/fonts/fonts.css +29 -0
  4. package/fonts/lixCode.ttf +0 -0
  5. package/fonts/lixDefault.ttf +0 -0
  6. package/fonts/lixDocs.ttf +0 -0
  7. package/fonts/lixFancy.ttf +0 -0
  8. package/fonts/lixFont.woff2 +0 -0
  9. package/package.json +49 -0
  10. package/src/SketchEngine.js +473 -0
  11. package/src/core/AIRenderer.js +1390 -0
  12. package/src/core/CopyPaste.js +655 -0
  13. package/src/core/EraserTrail.js +234 -0
  14. package/src/core/EventDispatcher.js +371 -0
  15. package/src/core/GraphEngine.js +150 -0
  16. package/src/core/GraphMathParser.js +231 -0
  17. package/src/core/GraphRenderer.js +255 -0
  18. package/src/core/LayerOrder.js +91 -0
  19. package/src/core/LixScriptParser.js +1299 -0
  20. package/src/core/MermaidFlowchartRenderer.js +475 -0
  21. package/src/core/MermaidSequenceParser.js +197 -0
  22. package/src/core/MermaidSequenceRenderer.js +479 -0
  23. package/src/core/ResizeCode.js +175 -0
  24. package/src/core/ResizeShapes.js +318 -0
  25. package/src/core/SceneSerializer.js +778 -0
  26. package/src/core/Selection.js +1861 -0
  27. package/src/core/SnapGuides.js +273 -0
  28. package/src/core/UndoRedo.js +1358 -0
  29. package/src/core/ZoomPan.js +258 -0
  30. package/src/core/ai-system-prompt.js +663 -0
  31. package/src/index.js +69 -0
  32. package/src/shapes/Arrow.js +1979 -0
  33. package/src/shapes/Circle.js +751 -0
  34. package/src/shapes/CodeShape.js +244 -0
  35. package/src/shapes/Frame.js +1460 -0
  36. package/src/shapes/FreehandStroke.js +724 -0
  37. package/src/shapes/IconShape.js +265 -0
  38. package/src/shapes/ImageShape.js +270 -0
  39. package/src/shapes/Line.js +738 -0
  40. package/src/shapes/Rectangle.js +794 -0
  41. package/src/shapes/TextShape.js +225 -0
  42. package/src/tools/arrowTool.js +581 -0
  43. package/src/tools/circleTool.js +619 -0
  44. package/src/tools/codeTool.js +2103 -0
  45. package/src/tools/eraserTool.js +131 -0
  46. package/src/tools/frameTool.js +241 -0
  47. package/src/tools/freehandTool.js +620 -0
  48. package/src/tools/iconTool.js +1344 -0
  49. package/src/tools/imageTool.js +1323 -0
  50. package/src/tools/laserTool.js +317 -0
  51. package/src/tools/lineTool.js +502 -0
  52. package/src/tools/rectangleTool.js +544 -0
  53. package/src/tools/textTool.js +1823 -0
  54. package/src/utils/imageCompressor.js +107 -0
@@ -0,0 +1,778 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * SceneSerializer - Save and load .lixsketch scene files
4
+ *
5
+ * Format: JSON with metadata + serialized shapes array
6
+ * File extension: .lixsketch (actually JSON)
7
+ */
8
+
9
+ import { Rectangle } from '../shapes/Rectangle.js';
10
+ import { Circle } from '../shapes/Circle.js';
11
+ import { Line } from '../shapes/Line.js';
12
+ import { Arrow } from '../shapes/Arrow.js';
13
+ import { FreehandStroke } from '../shapes/FreehandStroke.js';
14
+ import { Frame } from '../shapes/Frame.js';
15
+ import { TextShape } from '../shapes/TextShape.js';
16
+ import { CodeShape } from '../shapes/CodeShape.js';
17
+ import { ImageShape } from '../shapes/ImageShape.js';
18
+ import { IconShape } from '../shapes/IconShape.js';
19
+
20
+ const FORMAT_VERSION = 1;
21
+
22
+ // Generate a unique session ID for each scene
23
+ let _sessionID = null;
24
+ export function getSessionID() {
25
+ if (!_sessionID) {
26
+ _sessionID = `lx-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
27
+ }
28
+ return _sessionID;
29
+ }
30
+ export function resetSessionID() {
31
+ _sessionID = null;
32
+ }
33
+
34
+ function cloneOptions(options) {
35
+ return JSON.parse(JSON.stringify(options));
36
+ }
37
+
38
+ // ============================================================
39
+ // SERIALIZE a single shape to plain data
40
+ // ============================================================
41
+ function serializeShape(shape) {
42
+ const base = {
43
+ shapeID: shape.shapeID,
44
+ parentFrame: shape.parentFrame ? shape.parentFrame.shapeID : null,
45
+ };
46
+
47
+ switch (shape.shapeName) {
48
+ case 'rectangle':
49
+ return {
50
+ ...base,
51
+ type: 'rectangle',
52
+ x: shape.x, y: shape.y,
53
+ width: shape.width, height: shape.height,
54
+ rotation: shape.rotation,
55
+ options: cloneOptions(shape.options),
56
+ };
57
+
58
+ case 'circle':
59
+ return {
60
+ ...base,
61
+ type: 'circle',
62
+ x: shape.x, y: shape.y,
63
+ rx: shape.rx, ry: shape.ry,
64
+ rotation: shape.rotation,
65
+ options: cloneOptions(shape.options),
66
+ };
67
+
68
+ case 'line':
69
+ return {
70
+ ...base,
71
+ type: 'line',
72
+ startPoint: { x: shape.startPoint.x, y: shape.startPoint.y },
73
+ endPoint: { x: shape.endPoint.x, y: shape.endPoint.y },
74
+ controlPoint: shape.controlPoint ? { x: shape.controlPoint.x, y: shape.controlPoint.y } : null,
75
+ isCurved: shape.isCurved || false,
76
+ options: cloneOptions(shape.options),
77
+ };
78
+
79
+ case 'arrow': {
80
+ const data = {
81
+ ...base,
82
+ type: 'arrow',
83
+ startPoint: { x: shape.startPoint.x, y: shape.startPoint.y },
84
+ endPoint: { x: shape.endPoint.x, y: shape.endPoint.y },
85
+ options: cloneOptions(shape.options),
86
+ arrowOutlineStyle: shape.arrowOutlineStyle,
87
+ arrowHeadStyle: shape.arrowHeadStyle,
88
+ arrowCurved: shape.arrowCurved,
89
+ arrowCurveAmount: shape.arrowCurveAmount,
90
+ };
91
+ if (shape.controlPoint1) data.controlPoint1 = { x: shape.controlPoint1.x, y: shape.controlPoint1.y };
92
+ if (shape.controlPoint2) data.controlPoint2 = { x: shape.controlPoint2.x, y: shape.controlPoint2.y };
93
+ // Serialize attachments by shapeID
94
+ if (shape.startAttachment) data.startAttachmentID = shape.startAttachment.shapeID;
95
+ if (shape.endAttachment) data.endAttachmentID = shape.endAttachment.shapeID;
96
+ return data;
97
+ }
98
+
99
+ case 'freehandStroke':
100
+ return {
101
+ ...base,
102
+ type: 'freehandStroke',
103
+ points: JSON.parse(JSON.stringify(shape.points)),
104
+ rotation: shape.rotation,
105
+ options: cloneOptions(shape.options),
106
+ };
107
+
108
+ case 'frame':
109
+ return {
110
+ ...base,
111
+ type: 'frame',
112
+ x: shape.x, y: shape.y,
113
+ width: shape.width, height: shape.height,
114
+ rotation: shape.rotation,
115
+ frameName: shape.frameName,
116
+ fillStyle: shape.fillStyle || 'transparent',
117
+ fillColor: shape.fillColor || '#1e1e28',
118
+ gridSize: shape.gridSize || 20,
119
+ gridColor: shape.gridColor || 'rgba(255,255,255,0.06)',
120
+ options: cloneOptions(shape.options),
121
+ containedShapeIDs: shape.containedShapes
122
+ ? Array.from(shape.containedShapes).map(s => s.shapeID)
123
+ : [],
124
+ };
125
+
126
+ case 'text': {
127
+ const group = shape.group;
128
+ return {
129
+ ...base,
130
+ type: 'text',
131
+ x: shape.x, y: shape.y,
132
+ rotation: shape.rotation,
133
+ groupHTML: group.cloneNode(true).outerHTML,
134
+ };
135
+ }
136
+
137
+ case 'code': {
138
+ const group = shape.group;
139
+ return {
140
+ ...base,
141
+ type: 'code',
142
+ x: shape.x, y: shape.y,
143
+ rotation: shape.rotation,
144
+ groupHTML: group.cloneNode(true).outerHTML,
145
+ };
146
+ }
147
+
148
+ case 'image': {
149
+ const el = shape.element;
150
+ return {
151
+ ...base,
152
+ type: 'image',
153
+ x: shape.x, y: shape.y,
154
+ width: shape.width, height: shape.height,
155
+ rotation: shape.rotation,
156
+ href: el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || '',
157
+ };
158
+ }
159
+
160
+ case 'icon': {
161
+ const el = shape.element;
162
+ return {
163
+ ...base,
164
+ type: 'icon',
165
+ x: shape.x, y: shape.y,
166
+ width: shape.width, height: shape.height,
167
+ rotation: shape.rotation,
168
+ elementHTML: el.cloneNode(true).outerHTML,
169
+ viewboxWidth: parseFloat(el.getAttribute('data-viewbox-width')) || 24,
170
+ viewboxHeight: parseFloat(el.getAttribute('data-viewbox-height')) || 24,
171
+ };
172
+ }
173
+
174
+ default:
175
+ console.warn('[SceneSerializer] Unknown shape type:', shape.shapeName);
176
+ return null;
177
+ }
178
+ }
179
+
180
+ // ============================================================
181
+ // DESERIALIZE: Create a shape from saved data
182
+ // ============================================================
183
+ function deserializeShape(data) {
184
+ const svgEl = window.svg;
185
+ if (!svgEl) return null;
186
+ const ns = 'http://www.w3.org/2000/svg';
187
+
188
+ switch (data.type) {
189
+ case 'rectangle': {
190
+ const shape = new Rectangle(data.x, data.y, data.width, data.height, data.options || {});
191
+ if (data.rotation) shape.rotation = data.rotation;
192
+ if (data.shapeID) shape.shapeID = data.shapeID;
193
+ return shape;
194
+ }
195
+
196
+ case 'circle': {
197
+ const shape = new Circle(data.x, data.y, data.rx, data.ry, data.options || {});
198
+ if (data.rotation) shape.rotation = data.rotation;
199
+ if (data.shapeID) shape.shapeID = data.shapeID;
200
+ return shape;
201
+ }
202
+
203
+ case 'line': {
204
+ const shape = new Line(data.startPoint, data.endPoint, data.options || {});
205
+ if (data.controlPoint) shape.controlPoint = data.controlPoint;
206
+ if (data.isCurved) shape.isCurved = data.isCurved;
207
+ if (data.shapeID) shape.shapeID = data.shapeID;
208
+ return shape;
209
+ }
210
+
211
+ case 'arrow': {
212
+ const shape = new Arrow(data.startPoint, data.endPoint, data.options || {});
213
+ if (data.controlPoint1) shape.controlPoint1 = data.controlPoint1;
214
+ if (data.controlPoint2) shape.controlPoint2 = data.controlPoint2;
215
+ if (data.arrowOutlineStyle) shape.arrowOutlineStyle = data.arrowOutlineStyle;
216
+ if (data.arrowHeadStyle) shape.arrowHeadStyle = data.arrowHeadStyle;
217
+ if (data.arrowCurved) shape.arrowCurved = data.arrowCurved;
218
+ if (data.arrowCurveAmount) shape.arrowCurveAmount = data.arrowCurveAmount;
219
+ if (data.shapeID) shape.shapeID = data.shapeID;
220
+ return shape;
221
+ }
222
+
223
+ case 'freehandStroke': {
224
+ const shape = new FreehandStroke(data.points, data.options || {});
225
+ if (data.rotation) shape.rotation = data.rotation;
226
+ if (data.shapeID) shape.shapeID = data.shapeID;
227
+ return shape;
228
+ }
229
+
230
+ case 'frame': {
231
+ const frameOpts = { ...(data.options || {}), frameName: data.frameName || 'Frame' };
232
+ if (data.fillStyle) frameOpts.fillStyle = data.fillStyle;
233
+ if (data.fillColor) frameOpts.fillColor = data.fillColor;
234
+ if (data.gridSize) frameOpts.gridSize = data.gridSize;
235
+ if (data.gridColor) frameOpts.gridColor = data.gridColor;
236
+ const shape = new Frame(data.x, data.y, data.width, data.height, frameOpts);
237
+ if (data.rotation) shape.rotation = data.rotation;
238
+ if (data.shapeID) shape.shapeID = data.shapeID;
239
+ shape.draw();
240
+ return shape;
241
+ }
242
+
243
+ case 'text': {
244
+ if (!data.groupHTML) return null;
245
+ const parser = new DOMParser();
246
+ const doc = parser.parseFromString(`<svg xmlns="${ns}">${data.groupHTML}</svg>`, 'image/svg+xml');
247
+ const group = doc.querySelector('g');
248
+ if (!group) return null;
249
+ const imported = svgEl.ownerDocument.importNode(group, true);
250
+ svgEl.appendChild(imported);
251
+ const shape = new TextShape(imported);
252
+ if (data.shapeID) shape.shapeID = data.shapeID;
253
+ return shape;
254
+ }
255
+
256
+ case 'code': {
257
+ if (!data.groupHTML) return null;
258
+ const parser = new DOMParser();
259
+ const doc = parser.parseFromString(`<svg xmlns="${ns}">${data.groupHTML}</svg>`, 'image/svg+xml');
260
+ const group = doc.querySelector('g');
261
+ if (!group) return null;
262
+ const imported = svgEl.ownerDocument.importNode(group, true);
263
+ svgEl.appendChild(imported);
264
+ const shape = new CodeShape(imported);
265
+ if (data.shapeID) shape.shapeID = data.shapeID;
266
+ return shape;
267
+ }
268
+
269
+ case 'image': {
270
+ const imgEl = document.createElementNS(ns, 'image');
271
+ imgEl.setAttribute('x', data.x);
272
+ imgEl.setAttribute('y', data.y);
273
+ imgEl.setAttribute('width', data.width);
274
+ imgEl.setAttribute('height', data.height);
275
+ imgEl.setAttribute('href', data.href);
276
+ imgEl.setAttribute('preserveAspectRatio', 'none');
277
+ svgEl.appendChild(imgEl);
278
+ const shape = new ImageShape(imgEl);
279
+ if (data.rotation) shape.rotation = data.rotation;
280
+ if (data.shapeID) shape.shapeID = data.shapeID;
281
+ return shape;
282
+ }
283
+
284
+ case 'icon': {
285
+ if (!data.elementHTML) return null;
286
+ const parser = new DOMParser();
287
+ const doc = parser.parseFromString(data.elementHTML, 'image/svg+xml');
288
+ const svgIcon = doc.documentElement;
289
+ if (!svgIcon) return null;
290
+ const imported = svgEl.ownerDocument.importNode(svgIcon, true);
291
+ svgEl.appendChild(imported);
292
+ const shape = new IconShape(imported);
293
+ if (data.shapeID) shape.shapeID = data.shapeID;
294
+ return shape;
295
+ }
296
+
297
+ default:
298
+ console.warn('[SceneSerializer] Unknown type:', data.type);
299
+ return null;
300
+ }
301
+ }
302
+
303
+ // ============================================================
304
+ // SAVE: Serialize entire scene to .lixsketch JSON
305
+ // ============================================================
306
+ export function saveScene(workspaceName = 'Untitled') {
307
+ const allShapes = window.shapes || [];
308
+ const serialized = [];
309
+
310
+ for (const shape of allShapes) {
311
+ const data = serializeShape(shape);
312
+ if (data) serialized.push(data);
313
+ }
314
+
315
+ const scene = {
316
+ format: 'lixsketch',
317
+ version: FORMAT_VERSION,
318
+ sessionID: getSessionID(),
319
+ name: workspaceName,
320
+ createdAt: new Date().toISOString(),
321
+ viewport: window.currentViewBox ? { ...window.currentViewBox } : null,
322
+ zoom: window.currentZoom || 1,
323
+ shapes: serialized,
324
+ };
325
+
326
+ return scene;
327
+ }
328
+
329
+ // ============================================================
330
+ // LOAD: Deserialize .lixsketch JSON and recreate scene
331
+ // ============================================================
332
+ export function loadScene(sceneData) {
333
+ if (!sceneData || sceneData.format !== 'lixsketch') {
334
+ console.error('[SceneSerializer] Invalid scene format');
335
+ return false;
336
+ }
337
+
338
+ // Clear current scene
339
+ const svgEl = window.svg;
340
+ if (!svgEl) return false;
341
+
342
+ // Remove all existing shape DOM elements
343
+ const existingShapes = window.shapes || [];
344
+ existingShapes.forEach(shape => {
345
+ // For frames, clean up clipGroup and clipPath too
346
+ if (shape.shapeName === 'frame') {
347
+ if (shape.clipGroup && shape.clipGroup.parentNode) {
348
+ shape.clipGroup.parentNode.removeChild(shape.clipGroup);
349
+ }
350
+ if (shape.clipPath && shape.clipPath.parentNode) {
351
+ shape.clipPath.parentNode.removeChild(shape.clipPath);
352
+ }
353
+ }
354
+ if (shape.group && shape.group.parentNode) {
355
+ shape.group.parentNode.removeChild(shape.group);
356
+ } else if (shape.element && shape.element.parentNode) {
357
+ shape.element.parentNode.removeChild(shape.element);
358
+ }
359
+ });
360
+
361
+ window.shapes = [];
362
+ window.currentShape = null;
363
+ window.historyStack = [];
364
+ window.redoStack = [];
365
+
366
+ // Build ID -> shape map for frame containment and arrow attachments
367
+ const idMap = new Map();
368
+
369
+ // First pass: create all shapes (frames first to allow containment)
370
+ const frameData = sceneData.shapes.filter(s => s.type === 'frame');
371
+ const otherData = sceneData.shapes.filter(s => s.type !== 'frame');
372
+
373
+ // Create frames first
374
+ for (const data of frameData) {
375
+ const shape = deserializeShape(data);
376
+ if (shape) {
377
+ window.shapes.push(shape);
378
+ if (data.shapeID) idMap.set(data.shapeID, shape);
379
+ }
380
+ }
381
+
382
+ // Create all other shapes
383
+ for (const data of otherData) {
384
+ const shape = deserializeShape(data);
385
+ if (shape) {
386
+ window.shapes.push(shape);
387
+ if (data.shapeID) idMap.set(data.shapeID, shape);
388
+ }
389
+ }
390
+
391
+ // Second pass: restore frame containment
392
+ for (const data of frameData) {
393
+ const frame = idMap.get(data.shapeID);
394
+ if (frame && data.containedShapeIDs && data.containedShapeIDs.length > 0) {
395
+ for (const childID of data.containedShapeIDs) {
396
+ const child = idMap.get(childID);
397
+ if (!child) {
398
+ console.warn(`[SceneSerializer] Frame "${data.frameName}" references missing shape: ${childID}`);
399
+ continue;
400
+ }
401
+ try {
402
+ if (typeof frame.addShapeToFrame === 'function') {
403
+ frame.addShapeToFrame(child);
404
+ } else {
405
+ // Fallback: manually set containment
406
+ frame.containedShapes.push(child);
407
+ child.parentFrame = frame;
408
+ }
409
+ } catch (err) {
410
+ console.warn(`[SceneSerializer] Failed to restore containment for ${childID} in frame ${data.shapeID}:`, err);
411
+ // Fallback: at least set the reference
412
+ if (!frame.containedShapes.includes(child)) {
413
+ frame.containedShapes.push(child);
414
+ }
415
+ child.parentFrame = frame;
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ // Third pass: restore arrow attachments
422
+ for (const data of sceneData.shapes) {
423
+ if (data.type === 'arrow') {
424
+ const arrow = idMap.get(data.shapeID);
425
+ if (!arrow) continue;
426
+ if (data.startAttachmentID) {
427
+ const target = idMap.get(data.startAttachmentID);
428
+ if (target && arrow.setStartAttachment) arrow.setStartAttachment(target);
429
+ }
430
+ if (data.endAttachmentID) {
431
+ const target = idMap.get(data.endAttachmentID);
432
+ if (target && arrow.setEndAttachment) arrow.setEndAttachment(target);
433
+ }
434
+ }
435
+ }
436
+
437
+ // Restore viewport
438
+ if (sceneData.viewport && svgEl) {
439
+ const vb = sceneData.viewport;
440
+ window.currentViewBox = { ...vb };
441
+ svgEl.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.width} ${vb.height}`);
442
+ }
443
+ if (sceneData.zoom) {
444
+ window.currentZoom = sceneData.zoom;
445
+ }
446
+
447
+ console.log(`[SceneSerializer] Loaded ${window.shapes.length} shapes from "${sceneData.name}"`);
448
+
449
+ // Re-sync tool flags so shapes are interactable after restore
450
+ if (window.__sketchEngine && typeof window.__sketchEngine.setActiveTool === 'function') {
451
+ const store = window.__sketchStoreApi;
452
+ const currentTool = store ? store.getState().activeTool : 'select';
453
+ window.__sketchEngine.setActiveTool(currentTool);
454
+ } else {
455
+ // Fallback: ensure selection tool is active
456
+ window.isSelectionToolActive = true;
457
+ }
458
+
459
+ return true;
460
+ }
461
+
462
+ // ============================================================
463
+ // DOWNLOAD: Trigger browser download of .lixsketch file
464
+ // ============================================================
465
+ export function downloadScene(workspaceName = 'Untitled') {
466
+ const scene = saveScene(workspaceName);
467
+ const json = JSON.stringify(scene, null, 2);
468
+ const blob = new Blob([json], { type: 'application/json' });
469
+ const url = URL.createObjectURL(blob);
470
+
471
+ const a = document.createElement('a');
472
+ a.href = url;
473
+ a.download = `${workspaceName.replace(/[^a-zA-Z0-9_-]/g, '_')}.lixjson`;
474
+ document.body.appendChild(a);
475
+ a.click();
476
+ document.body.removeChild(a);
477
+ URL.revokeObjectURL(url);
478
+ }
479
+
480
+ // ============================================================
481
+ // UPLOAD: Open file picker and load .lixsketch file
482
+ // ============================================================
483
+ // Validate scene JSON structure before loading
484
+ export function validateScene(data) {
485
+ if (!data || typeof data !== 'object') return { valid: false, error: 'Not a valid JSON object' };
486
+ if (data.format !== 'lixsketch') return { valid: false, error: 'Not a LixSketch scene file (missing format field)' };
487
+ if (!data.version || data.version > FORMAT_VERSION) return { valid: false, error: `Unsupported version: ${data.version}` };
488
+ if (!Array.isArray(data.shapes)) return { valid: false, error: 'Invalid scene: missing shapes array' };
489
+ return {
490
+ valid: true,
491
+ name: data.name || 'Untitled',
492
+ shapeCount: data.shapes.length,
493
+ sessionID: data.sessionID || null,
494
+ createdAt: data.createdAt || null,
495
+ };
496
+ }
497
+
498
+ export function uploadScene() {
499
+ return new Promise((resolve, reject) => {
500
+ const input = document.createElement('input');
501
+ input.type = 'file';
502
+ input.accept = '.lixjson,.json';
503
+ input.onchange = (e) => {
504
+ const file = e.target.files[0];
505
+ if (!file) return resolve(false);
506
+
507
+ const reader = new FileReader();
508
+ reader.onload = (ev) => {
509
+ try {
510
+ const data = JSON.parse(ev.target.result);
511
+ const validation = validateScene(data);
512
+ if (!validation.valid) {
513
+ console.error('[SceneSerializer] Invalid file:', validation.error);
514
+ resolve({ success: false, error: validation.error });
515
+ return;
516
+ }
517
+ resetSessionID(); // New session for loaded scene
518
+ const result = loadScene(data);
519
+ resolve({ success: result, validation });
520
+ } catch (err) {
521
+ console.error('[SceneSerializer] Failed to parse file:', err);
522
+ resolve({ success: false, error: 'Failed to parse JSON file' });
523
+ }
524
+ };
525
+ reader.readAsText(file);
526
+ };
527
+ input.click();
528
+ });
529
+ }
530
+
531
+ // ============================================================
532
+ // EXPORT as PNG
533
+ // ============================================================
534
+ export function exportAsPNG() {
535
+ const svgEl = window.svg;
536
+ if (!svgEl) return;
537
+
538
+ const clone = svgEl.cloneNode(true);
539
+ // Remove selection UI elements
540
+ const selectionEls = clone.querySelectorAll('[data-selection], .selection-handle, .resize-handle, .rotation-handle');
541
+ selectionEls.forEach(el => el.remove());
542
+
543
+ const svgData = new XMLSerializer().serializeToString(clone);
544
+ const canvas = document.createElement('canvas');
545
+ const vb = svgEl.viewBox.baseVal;
546
+ canvas.width = vb.width * 2; // 2x for retina
547
+ canvas.height = vb.height * 2;
548
+ const ctx = canvas.getContext('2d');
549
+ ctx.scale(2, 2);
550
+
551
+ const img = new Image();
552
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
553
+ const url = URL.createObjectURL(svgBlob);
554
+
555
+ img.onload = () => {
556
+ // Draw dark background
557
+ ctx.fillStyle = '#121212';
558
+ ctx.fillRect(0, 0, vb.width, vb.height);
559
+ ctx.drawImage(img, 0, 0, vb.width, vb.height);
560
+ URL.revokeObjectURL(url);
561
+
562
+ canvas.toBlob(blob => {
563
+ const a = document.createElement('a');
564
+ a.href = URL.createObjectURL(blob);
565
+ a.download = 'lixsketch-export.png';
566
+ document.body.appendChild(a);
567
+ a.click();
568
+ document.body.removeChild(a);
569
+ }, 'image/png');
570
+ };
571
+ img.src = url;
572
+ }
573
+
574
+ // ============================================================
575
+ // COPY to clipboard as PNG
576
+ // ============================================================
577
+ export function copyAsPNG() {
578
+ const svgEl = window.svg;
579
+ if (!svgEl) return;
580
+
581
+ const clone = svgEl.cloneNode(true);
582
+ const selectionEls = clone.querySelectorAll('[data-selection], .selection-handle, .resize-handle, .rotation-handle');
583
+ selectionEls.forEach(el => el.remove());
584
+
585
+ const svgData = new XMLSerializer().serializeToString(clone);
586
+ const canvas = document.createElement('canvas');
587
+ const vb = svgEl.viewBox.baseVal;
588
+ canvas.width = vb.width * 2;
589
+ canvas.height = vb.height * 2;
590
+ const ctx = canvas.getContext('2d');
591
+ ctx.scale(2, 2);
592
+
593
+ const img = new Image();
594
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
595
+ const url = URL.createObjectURL(svgBlob);
596
+
597
+ img.onload = () => {
598
+ ctx.fillStyle = '#121212';
599
+ ctx.fillRect(0, 0, vb.width, vb.height);
600
+ ctx.drawImage(img, 0, 0, vb.width, vb.height);
601
+ URL.revokeObjectURL(url);
602
+
603
+ canvas.toBlob(blob => {
604
+ if (!blob) return;
605
+ navigator.clipboard.write([
606
+ new ClipboardItem({ 'image/png': blob })
607
+ ]).catch(err => console.warn('[SceneSerializer] Clipboard write failed:', err));
608
+ }, 'image/png');
609
+ };
610
+ img.src = url;
611
+ }
612
+
613
+ // ============================================================
614
+ // COPY to clipboard as SVG
615
+ // ============================================================
616
+ export function copyAsSVG() {
617
+ const svgEl = window.svg;
618
+ if (!svgEl) return;
619
+
620
+ const clone = svgEl.cloneNode(true);
621
+ const selectionEls = clone.querySelectorAll('[data-selection], .selection-handle, .resize-handle, .rotation-handle');
622
+ selectionEls.forEach(el => el.remove());
623
+
624
+ const svgData = new XMLSerializer().serializeToString(clone);
625
+ navigator.clipboard.writeText(svgData)
626
+ .catch(err => console.warn('[SceneSerializer] Clipboard write failed:', err));
627
+ }
628
+
629
+ // ============================================================
630
+ // EXPORT as PDF (uses browser print)
631
+ // ============================================================
632
+ export function exportAsPDF() {
633
+ const svgEl = window.svg;
634
+ if (!svgEl) return;
635
+
636
+ const clone = svgEl.cloneNode(true);
637
+ const selectionEls = clone.querySelectorAll('[data-selection], .selection-handle, .resize-handle, .rotation-handle');
638
+ selectionEls.forEach(el => el.remove());
639
+
640
+ const svgData = new XMLSerializer().serializeToString(clone);
641
+ const printWindow = window.open('', '_blank');
642
+ printWindow.document.write(`
643
+ <!DOCTYPE html>
644
+ <html>
645
+ <head><title>LixSketch Export</title>
646
+ <style>
647
+ body { margin: 0; background: #121212; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
648
+ svg { max-width: 100vw; max-height: 100vh; }
649
+ @media print { body { background: white; } }
650
+ </style>
651
+ </head>
652
+ <body>${svgData}</body>
653
+ </html>
654
+ `);
655
+ printWindow.document.close();
656
+ printWindow.focus();
657
+ setTimeout(() => printWindow.print(), 500);
658
+ }
659
+
660
+ // ============================================================
661
+ // RESET: Clear the entire canvas
662
+ // ============================================================
663
+ export function resetCanvas() {
664
+ const svgEl = window.svg;
665
+ if (!svgEl) return;
666
+
667
+ // Remove all shape DOM elements + frame clipGroups/clipPaths
668
+ const existingShapes = window.shapes || [];
669
+ existingShapes.forEach(shape => {
670
+ if (shape.shapeName === 'frame') {
671
+ if (shape.clipGroup && shape.clipGroup.parentNode) {
672
+ shape.clipGroup.parentNode.removeChild(shape.clipGroup);
673
+ }
674
+ if (shape.clipPath && shape.clipPath.parentNode) {
675
+ shape.clipPath.parentNode.removeChild(shape.clipPath);
676
+ }
677
+ }
678
+ if (shape.group && shape.group.parentNode) {
679
+ shape.group.parentNode.removeChild(shape.group);
680
+ } else if (shape.element && shape.element.parentNode) {
681
+ shape.element.parentNode.removeChild(shape.element);
682
+ }
683
+ });
684
+
685
+ // Remove selection UI
686
+ svgEl.querySelectorAll(
687
+ '.selection-outline, .resize-anchor, .rotation-anchor, [data-selection]'
688
+ ).forEach(el => el.remove());
689
+
690
+ window.shapes = [];
691
+ window.currentShape = null;
692
+ window.historyStack = [];
693
+ window.redoStack = [];
694
+
695
+ if (typeof window.clearAllSelections === 'function') {
696
+ window.clearAllSelections();
697
+ }
698
+ if (typeof window.disableAllSideBars === 'function') {
699
+ window.disableAllSideBars();
700
+ }
701
+
702
+ // Clear auto-save (both legacy and session-scoped keys)
703
+ try {
704
+ localStorage.removeItem('lixsketch-autosave');
705
+ localStorage.removeItem('lixsketch-autosave-meta');
706
+ const sid = window.__sessionID;
707
+ if (sid) {
708
+ localStorage.removeItem(`lixsketch-autosave-${sid}`);
709
+ localStorage.removeItem(`lixsketch-autosave-meta-${sid}`);
710
+ }
711
+ } catch (_) {}
712
+
713
+ console.log('[SceneSerializer] Canvas reset');
714
+ }
715
+
716
+ // ============================================================
717
+ // FIND: Search for text content on the canvas
718
+ // ============================================================
719
+ export function findTextOnCanvas(query) {
720
+ const allShapes = window.shapes || [];
721
+ if (!query || query.trim() === '') return [];
722
+
723
+ const lowerQuery = query.toLowerCase();
724
+ const results = [];
725
+
726
+ for (const shape of allShapes) {
727
+ let textContent = '';
728
+
729
+ if (shape.shapeName === 'text' || shape.shapeName === 'code') {
730
+ const group = shape.group;
731
+ if (group) {
732
+ // Get all text content from SVG text/tspan elements
733
+ const textEls = group.querySelectorAll('text, tspan');
734
+ textEls.forEach(el => {
735
+ if (el.textContent) textContent += el.textContent + ' ';
736
+ });
737
+ // Also check foreignObject content
738
+ const foreignEls = group.querySelectorAll('foreignObject *');
739
+ foreignEls.forEach(el => {
740
+ if (el.textContent) textContent += el.textContent + ' ';
741
+ });
742
+ }
743
+ } else if (shape.shapeName === 'frame') {
744
+ textContent = shape.frameName || '';
745
+ }
746
+
747
+ textContent = textContent.trim();
748
+ if (textContent && textContent.toLowerCase().includes(lowerQuery)) {
749
+ results.push({
750
+ shape,
751
+ text: textContent,
752
+ type: shape.shapeName,
753
+ x: shape.x || 0,
754
+ y: shape.y || 0,
755
+ });
756
+ }
757
+ }
758
+
759
+ return results;
760
+ }
761
+
762
+ // ============================================================
763
+ // Initialize bridge for React components
764
+ // ============================================================
765
+ export function initSceneSerializer() {
766
+ window.__sceneSerializer = {
767
+ save: saveScene,
768
+ load: loadScene,
769
+ download: downloadScene,
770
+ upload: uploadScene,
771
+ exportPNG: exportAsPNG,
772
+ exportPDF: exportAsPDF,
773
+ copyAsPNG,
774
+ copyAsSVG,
775
+ resetCanvas,
776
+ findText: findTextOnCanvas,
777
+ };
778
+ }