@elixpo/lixsketch 5.4.7 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/AIRenderer-M4LMJ66J.js +1105 -0
- package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
- package/dist/react/Arrow-BJDAXPMH.js +8 -0
- package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
- package/dist/react/Circle-KPG7TKIX.js +8 -0
- package/dist/react/Circle-KPG7TKIX.js.map +7 -0
- package/dist/react/CodeShape-K4DIZAIV.js +8 -0
- package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
- package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
- package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
- package/dist/react/Frame-6UR5XPAX.js +10 -0
- package/dist/react/Frame-6UR5XPAX.js.map +7 -0
- package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
- package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
- package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
- package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
- package/dist/react/IconShape-KMIA6T2H.js +10 -0
- package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
- package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
- package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
- package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
- package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
- package/dist/react/Line-2KOQEEGA.js +10 -0
- package/dist/react/Line-2KOQEEGA.js.map +7 -0
- package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
- package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
- package/dist/react/Rectangle-6FE7RN6W.js +8 -0
- package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
- package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
- package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
- package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
- package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
- package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
- package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
- package/dist/react/Selection-VVUHY5YJ.js +34 -0
- package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
- package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
- package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
- package/dist/react/TextShape-54ZJ66E3.js +8 -0
- package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
- package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
- package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
- package/dist/react/arrowTool-CWEMIRXK.js +17 -0
- package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
- package/dist/react/boxicons-5JNJQPUP.woff +0 -0
- package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
- package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
- package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
- package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
- package/dist/react/chunk-2OQXI7QB.js +564 -0
- package/dist/react/chunk-2OQXI7QB.js.map +7 -0
- package/dist/react/chunk-5OUBTHHO.js +187 -0
- package/dist/react/chunk-5OUBTHHO.js.map +7 -0
- package/dist/react/chunk-6PFXZKL2.js +628 -0
- package/dist/react/chunk-6PFXZKL2.js.map +7 -0
- package/dist/react/chunk-6SFNWGGZ.js +453 -0
- package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
- package/dist/react/chunk-7TZSIR6V.js +210 -0
- package/dist/react/chunk-7TZSIR6V.js.map +7 -0
- package/dist/react/chunk-AJAZC37E.js +662 -0
- package/dist/react/chunk-AJAZC37E.js.map +7 -0
- package/dist/react/chunk-BU4D2WYQ.js +1213 -0
- package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
- package/dist/react/chunk-BZE4CEBG.js +747 -0
- package/dist/react/chunk-BZE4CEBG.js.map +7 -0
- package/dist/react/chunk-CB2RANCB.js +212 -0
- package/dist/react/chunk-CB2RANCB.js.map +7 -0
- package/dist/react/chunk-DNAG7ML6.js +128 -0
- package/dist/react/chunk-DNAG7ML6.js.map +7 -0
- package/dist/react/chunk-HZWLPXFN.js +198 -0
- package/dist/react/chunk-HZWLPXFN.js.map +7 -0
- package/dist/react/chunk-I6HCCIXB.js +572 -0
- package/dist/react/chunk-I6HCCIXB.js.map +7 -0
- package/dist/react/chunk-IZ5OGWGW.js +235 -0
- package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
- package/dist/react/chunk-JJCJNLNN.js +1490 -0
- package/dist/react/chunk-JJCJNLNN.js.map +7 -0
- package/dist/react/chunk-JQ3IRFDP.js +222 -0
- package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
- package/dist/react/chunk-L5OARZJ7.js +1134 -0
- package/dist/react/chunk-L5OARZJ7.js.map +7 -0
- package/dist/react/chunk-LHRTXNVE.js +1600 -0
- package/dist/react/chunk-LHRTXNVE.js.map +7 -0
- package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
- package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
- package/dist/react/chunk-QNSE5BAY.js +519 -0
- package/dist/react/chunk-QNSE5BAY.js.map +7 -0
- package/dist/react/chunk-QXFBG7OM.js +191 -0
- package/dist/react/chunk-QXFBG7OM.js.map +7 -0
- package/dist/react/chunk-T6ZVV5ZO.js +714 -0
- package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
- package/dist/react/chunk-URB6MY46.js +974 -0
- package/dist/react/chunk-URB6MY46.js.map +7 -0
- package/dist/react/chunk-VN62KLOC.js +931 -0
- package/dist/react/chunk-VN62KLOC.js.map +7 -0
- package/dist/react/chunk-VQVEPJSA.js +158 -0
- package/dist/react/chunk-VQVEPJSA.js.map +7 -0
- package/dist/react/chunk-XISEAGBV.js +1632 -0
- package/dist/react/chunk-XISEAGBV.js.map +7 -0
- package/dist/react/chunk-YKJUBFHE.js +575 -0
- package/dist/react/chunk-YKJUBFHE.js.map +7 -0
- package/dist/react/circleTool-NUATSEI2.js +14 -0
- package/dist/react/circleTool-NUATSEI2.js.map +7 -0
- package/dist/react/codeTool-MHCB2FNB.js +38 -0
- package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
- package/dist/react/eraserTool-2GS2LQCS.js +103 -0
- package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
- package/dist/react/frameTool-PWVKKXSP.js +14 -0
- package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
- package/dist/react/freehandTool-SLYGASVP.js +14 -0
- package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
- package/dist/react/iconTool-GPZ7O7NH.js +18 -0
- package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
- package/dist/react/imageTool-D5R4XGEU.js +15 -0
- package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
- package/dist/react/index.js +5342 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/laserTool-XVMV63AV.js +239 -0
- package/dist/react/laserTool-XVMV63AV.js.map +7 -0
- package/dist/react/lineTool-WNI5EMZ5.js +14 -0
- package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
- package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
- package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
- package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
- package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
- package/dist/react/styles.css +6627 -0
- package/dist/react/textTool-M7S6F3VC.js +21 -0
- package/dist/react/textTool-M7S6F3VC.js.map +7 -0
- package/package.json +39 -5
- package/src/EngineShortcuts.js +83 -27
- package/src/core/AIRenderer.js +30 -18
- package/src/core/GraphEngine.js +64 -41
- package/src/core/MermaidFlowchartRenderer.js +153 -88
- package/src/core/MermaidSequenceRenderer.js +168 -93
- package/src/core/SceneSerializer.js +86 -2
- package/src/core/Selection.js +119 -19
- package/src/core/UndoRedo.js +82 -15
- package/src/core/ZoomPan.js +15 -5
- package/src/index.js +14 -1
- package/src/react/LixSketchCanvas.jsx +347 -0
- package/src/react/components/AppMenu.jsx +475 -0
- package/src/react/components/Footer.jsx +85 -0
- package/src/react/components/Toolbar.jsx +140 -0
- package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
- package/src/react/components/canvas/ContextMenu.jsx +466 -0
- package/src/react/components/canvas/FindBar.jsx +177 -0
- package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
- package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
- package/src/react/components/canvas/SVGCanvas.jsx +145 -0
- package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
- package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
- package/src/react/components/modals/CommandPalette.jsx +307 -0
- package/src/react/components/modals/ExportImageModal.jsx +287 -0
- package/src/react/components/modals/GraphModal.jsx +174 -0
- package/src/react/components/modals/HelpModal.jsx +208 -0
- package/src/react/components/modals/LixScriptModal.jsx +130 -0
- package/src/react/components/modals/ShortcutsModal.jsx +125 -0
- package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
- package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
- package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
- package/src/react/components/sidebars/IconSidebar.jsx +279 -0
- package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
- package/src/react/components/sidebars/LineSidebar.jsx +109 -0
- package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
- package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
- package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
- package/src/react/components/sidebars/TextSidebar.jsx +174 -0
- package/src/react/hooks/inertStores.js +58 -0
- package/src/react/hooks/useSketchEngine.js +76 -0
- package/src/react/hooks/useTranslation.js +92 -0
- package/src/react/index.js +41 -0
- package/src/react/store/useSketchStore.js +234 -0
- package/src/react/store/useUIStore.js +196 -0
- package/src/react/styles/arrowSidebar.css +263 -0
- package/src/react/styles/canvasStroke.css +47 -0
- package/src/react/styles/canvasTools.css +769 -0
- package/src/react/styles/circleSidebar.css +206 -0
- package/src/react/styles/fonts/fonts.css +29 -0
- package/src/react/styles/fonts/lixCode.ttf +0 -0
- package/src/react/styles/fonts/lixDefault.ttf +0 -0
- package/src/react/styles/fonts/lixDocs.ttf +0 -0
- package/src/react/styles/fonts/lixFancy.ttf +0 -0
- package/src/react/styles/fonts/lixFont.woff2 +0 -0
- package/src/react/styles/lineSidebar.css +207 -0
- package/src/react/styles/paintbrushSidebar.css +247 -0
- package/src/react/styles/squareSidebar.css +206 -0
- package/src/react/styles/writeText.css +149 -0
- package/src/react/styles.css +175 -0
- package/src/react/toolsEnum.js +20 -0
- package/src/react/utils/toast.js +18 -0
- package/src/shapes/Frame.js +7 -1
- package/src/shapes/Line.js +11 -5
- package/src/tools/arrowTool.js +30 -4
- package/src/tools/circleTool.js +33 -9
- package/src/tools/codeTool.js +5 -3
- package/src/tools/frameTool.js +47 -7
- package/src/tools/freehandTool.js +12 -4
- package/src/tools/iconTool.js +7 -1
- package/src/tools/imageTool.js +7 -1
- package/src/tools/lineTool.js +27 -3
- package/src/tools/rectangleTool.js +45 -8
- package/src/tools/textTool.js +23 -6
|
@@ -122,8 +122,14 @@ function serializeShape(shape) {
|
|
|
122
122
|
gridSize: shape.gridSize || 20,
|
|
123
123
|
gridColor: shape.gridColor || 'rgba(255,255,255,0.06)',
|
|
124
124
|
options: cloneOptions(shape.options),
|
|
125
|
+
// Issue #24 bug #10: filter out null / undefined / shapeID-less
|
|
126
|
+
// children. A stale reference would serialize as `null` and the
|
|
127
|
+
// load path would skip it (logging a misleading "missing shape"
|
|
128
|
+
// warning) — but also previously skewed the order tracking.
|
|
125
129
|
containedShapeIDs: shape.containedShapes
|
|
126
|
-
? Array.from(shape.containedShapes)
|
|
130
|
+
? Array.from(shape.containedShapes)
|
|
131
|
+
.filter(s => s && s.shapeID)
|
|
132
|
+
.map(s => s.shapeID)
|
|
127
133
|
: [],
|
|
128
134
|
};
|
|
129
135
|
|
|
@@ -312,9 +318,31 @@ export function saveScene(workspaceName = 'Untitled') {
|
|
|
312
318
|
const allShapes = window.shapes || [];
|
|
313
319
|
const serialized = [];
|
|
314
320
|
|
|
321
|
+
// Issue #24 bug #10: rebuild containment from each shape's own
|
|
322
|
+
// `parentFrame` ref before serialising. The `containedShapes` array
|
|
323
|
+
// on the frame side drifts over time (eraser, partial deletes, drag
|
|
324
|
+
// out + drag back, etc.), and a missing entry there means the child
|
|
325
|
+
// gets serialised as a top-level shape and the frame restores empty.
|
|
326
|
+
// Each shape knows its own parent, so we trust THAT and override the
|
|
327
|
+
// frame's array at save time.
|
|
328
|
+
const childrenByFrame = new Map();
|
|
329
|
+
for (const s of allShapes) {
|
|
330
|
+
if (s && s.parentFrame && s.shapeID && s !== s.parentFrame) {
|
|
331
|
+
const arr = childrenByFrame.get(s.parentFrame.shapeID) || [];
|
|
332
|
+
arr.push(s.shapeID);
|
|
333
|
+
childrenByFrame.set(s.parentFrame.shapeID, arr);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
for (const shape of allShapes) {
|
|
316
338
|
const data = serializeShape(shape);
|
|
317
|
-
if (data)
|
|
339
|
+
if (!data) continue;
|
|
340
|
+
if (data.type === 'frame' && shape.shapeID && childrenByFrame.has(shape.shapeID)) {
|
|
341
|
+
const merged = new Set(data.containedShapeIDs || []);
|
|
342
|
+
for (const id of childrenByFrame.get(shape.shapeID)) merged.add(id);
|
|
343
|
+
data.containedShapeIDs = Array.from(merged);
|
|
344
|
+
}
|
|
345
|
+
serialized.push(data);
|
|
318
346
|
}
|
|
319
347
|
|
|
320
348
|
const scene = {
|
|
@@ -444,6 +472,62 @@ export function loadScene(sceneData) {
|
|
|
444
472
|
}
|
|
445
473
|
}
|
|
446
474
|
|
|
475
|
+
// ── Fourth pass: frame containment reconciliation (issue #24 bug #10) ──
|
|
476
|
+
//
|
|
477
|
+
// The pass above attaches each child via `frame.addShapeToFrame(child)`,
|
|
478
|
+
// which is supposed to move the child's <g> into the frame's clipGroup.
|
|
479
|
+
// In practice LixScript-rendered scenes were reloading with empty
|
|
480
|
+
// frames — children present in the shapes array, frame box drawn, but
|
|
481
|
+
// the children's <g> elements stranded at the svg root (so visually
|
|
482
|
+
// they look orphaned). The cause varies (transient `isBeingMovedByFrame`
|
|
483
|
+
// flag carried over from save time, ordering glitches when nested
|
|
484
|
+
// frames are restored, etc.). Rather than chase the source, repair the
|
|
485
|
+
// DOM state here from the authoritative `containedShapes` array.
|
|
486
|
+
for (const data of frameData) {
|
|
487
|
+
const frame = idMap.get(data.shapeID);
|
|
488
|
+
if (!frame) continue;
|
|
489
|
+
|
|
490
|
+
// Clear stale flags that suppress DOM mutation inside addShapeToFrame.
|
|
491
|
+
delete frame.isBeingMovedByFrame;
|
|
492
|
+
delete frame.isDraggedOutTemporarily;
|
|
493
|
+
|
|
494
|
+
// Ensure the frame's own clipGroup is direct child of svg — sub-frames
|
|
495
|
+
// that didn't get re-parented yet are handled in the next loop.
|
|
496
|
+
if (frame.clipGroup && frame.clipGroup.parentNode !== svgEl) {
|
|
497
|
+
svgEl.appendChild(frame.clipGroup);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const child of frame.containedShapes || []) {
|
|
501
|
+
if (!child) continue;
|
|
502
|
+
delete child.isBeingMovedByFrame;
|
|
503
|
+
delete child.isDraggedOutTemporarily;
|
|
504
|
+
|
|
505
|
+
const el = child.group || child.element;
|
|
506
|
+
if (el && el.parentNode !== frame.clipGroup) {
|
|
507
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
508
|
+
frame.clipGroup.appendChild(el);
|
|
509
|
+
}
|
|
510
|
+
// Sub-frames also carry their own clipGroup — nest it.
|
|
511
|
+
if (child.shapeName === 'frame' && child.clipGroup
|
|
512
|
+
&& child.clipGroup.parentNode !== frame.clipGroup) {
|
|
513
|
+
if (child.clipGroup.parentNode) child.clipGroup.parentNode.removeChild(child.clipGroup);
|
|
514
|
+
frame.clipGroup.appendChild(child.clipGroup);
|
|
515
|
+
}
|
|
516
|
+
// Authoritative parent ref — `addShapeToFrame` should already
|
|
517
|
+
// have set this but the no-op early return when the child was
|
|
518
|
+
// already in containedShapes skips it.
|
|
519
|
+
child.parentFrame = frame;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Force the clip rect + visual to match. Some saved frames came
|
|
523
|
+
// through with a stale clipRect from before the resize that lost
|
|
524
|
+
// the children visually.
|
|
525
|
+
if (typeof frame.updateClipPath === 'function') frame.updateClipPath();
|
|
526
|
+
if (typeof frame.draw === 'function') {
|
|
527
|
+
try { frame.draw(); } catch (err) { console.warn('[SceneSerializer] frame redraw failed:', err); }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
447
531
|
// Restore viewport
|
|
448
532
|
if (sceneData.viewport && svgEl) {
|
|
449
533
|
const vb = sceneData.viewport;
|
package/src/core/Selection.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Multi-selection system - copied from selection.js
|
|
3
3
|
|
|
4
4
|
import { cleanupAttachments } from '../tools/arrowTool.js';
|
|
5
|
-
import { pushTransformAction } from './UndoRedo.js';
|
|
5
|
+
import { pushTransformAction, pushFrameAttachmentAction, pushDeleteAction } from './UndoRedo.js';
|
|
6
6
|
import { calculateSnap, clearSnapGuides } from './SnapGuides.js';
|
|
7
7
|
|
|
8
8
|
let isMultiSelecting = false;
|
|
@@ -1370,6 +1370,41 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1370
1370
|
shape.isSelected = true;
|
|
1371
1371
|
});
|
|
1372
1372
|
|
|
1373
|
+
// Re-parent any selected shape that now sits inside a different frame
|
|
1374
|
+
// (Issue #22, bug #9). The per-tool drag handlers (rectangleTool, etc.)
|
|
1375
|
+
// do this for single-shape drags, but a multi-selection drag bypassed
|
|
1376
|
+
// them and left the shape geometrically inside the frame yet logically
|
|
1377
|
+
// outside its child list.
|
|
1378
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1379
|
+
this.selectedShapes.forEach(shape => {
|
|
1380
|
+
if (!shape || shape.shapeName === 'frame') return;
|
|
1381
|
+
// Find the deepest frame whose bounds contain the shape after the drag.
|
|
1382
|
+
let target = null;
|
|
1383
|
+
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
1384
|
+
const f = shapes[i];
|
|
1385
|
+
if (f.shapeName !== 'frame') continue;
|
|
1386
|
+
if (this.selectedShapes.has(f)) continue; // moving with us — skip
|
|
1387
|
+
if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(shape)) {
|
|
1388
|
+
target = f;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const current = shape.parentFrame || null;
|
|
1393
|
+
if (target === current) return;
|
|
1394
|
+
if (current && typeof current.removeShapeFromFrame === 'function') {
|
|
1395
|
+
current.removeShapeFromFrame(shape);
|
|
1396
|
+
}
|
|
1397
|
+
if (target && typeof target.addShapeToFrame === 'function') {
|
|
1398
|
+
target.addShapeToFrame(shape);
|
|
1399
|
+
if (typeof pushFrameAttachmentAction === 'function') {
|
|
1400
|
+
pushFrameAttachmentAction(target, shape, 'attach', current);
|
|
1401
|
+
}
|
|
1402
|
+
} else if (current && typeof pushFrameAttachmentAction === 'function') {
|
|
1403
|
+
pushFrameAttachmentAction(current, shape, 'detach', current);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1373
1408
|
this.initialPositions.clear();
|
|
1374
1409
|
|
|
1375
1410
|
// Push undo for all moved shapes
|
|
@@ -1788,33 +1823,98 @@ function deleteSelectedShapes() {
|
|
|
1788
1823
|
const shapesToDelete = Array.from(multiSelection.selectedShapes);
|
|
1789
1824
|
multiSelection.clearSelection();
|
|
1790
1825
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
shape.element.parentNode.removeChild(shape.element);
|
|
1797
|
-
}
|
|
1826
|
+
// Issue #24 bug #3: when deleting a (regular, non-diagram) frame, its
|
|
1827
|
+
// children are released back to the canvas — capture them here so we
|
|
1828
|
+
// can re-select them and drop the user into the pointer tool. Diagram
|
|
1829
|
+
// frames destroy their children together, so we skip those.
|
|
1830
|
+
const releasedChildren = [];
|
|
1798
1831
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1832
|
+
shapesToDelete.forEach(shape => {
|
|
1833
|
+
const isFrame = shape.shapeName === 'frame';
|
|
1834
|
+
const isDiagramFrame = isFrame && !!shape._diagramType;
|
|
1835
|
+
|
|
1836
|
+
// Issue #24 bug #7: for a regular (non-diagram) frame, we don't
|
|
1837
|
+
// want destroy() — it empties containedShapes and tears down the
|
|
1838
|
+
// clipGroup, leaving nothing for undo to rehydrate. Instead:
|
|
1839
|
+
// 1. snapshot the children references (for the undo action),
|
|
1840
|
+
// 2. release each child visually back to the svg root,
|
|
1841
|
+
// 3. detach the frame's <g>, clipGroup, clipPath from DOM.
|
|
1842
|
+
// The frame JS object stays alive in the undo action's `shape`
|
|
1843
|
+
// reference; on undo, the UndoRedo branch re-appends clipGroup +
|
|
1844
|
+
// clipPath and re-attaches each child via addShapeToFrame.
|
|
1845
|
+
let childSnapshot = null;
|
|
1846
|
+
if (isFrame && !isDiagramFrame) {
|
|
1847
|
+
childSnapshot = [...(shape.containedShapes || [])];
|
|
1848
|
+
for (const child of childSnapshot) {
|
|
1849
|
+
if (!child || child === shape) continue;
|
|
1850
|
+
const el = child.group || child.element;
|
|
1851
|
+
if (el) {
|
|
1852
|
+
if (shape.clipGroup && el.parentNode === shape.clipGroup) {
|
|
1853
|
+
shape.clipGroup.removeChild(el);
|
|
1854
|
+
}
|
|
1855
|
+
if (window.svg && el.parentNode !== window.svg) window.svg.appendChild(el);
|
|
1856
|
+
}
|
|
1857
|
+
child.parentFrame = null;
|
|
1858
|
+
delete child.isBeingMovedByFrame;
|
|
1859
|
+
releasedChildren.push(child);
|
|
1860
|
+
}
|
|
1803
1861
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1862
|
+
// Clear containedShapes — undo will repopulate via
|
|
1863
|
+
// addShapeToFrame, which short-circuits on an existing entry.
|
|
1864
|
+
shape.containedShapes = [];
|
|
1806
1865
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
shape.
|
|
1866
|
+
// Detach the frame's own DOM bits — but keep the JS object
|
|
1867
|
+
// and its clipGroup/clipPath references intact for undo.
|
|
1868
|
+
if (shape.group && shape.group.parentNode) shape.group.parentNode.removeChild(shape.group);
|
|
1869
|
+
if (shape.clipGroup && shape.clipGroup.parentNode) shape.clipGroup.parentNode.removeChild(shape.clipGroup);
|
|
1870
|
+
if (shape.clipPath && shape.clipPath.parentNode) shape.clipPath.parentNode.removeChild(shape.clipPath);
|
|
1871
|
+
} else {
|
|
1872
|
+
// Non-frame OR diagram frame: standard delete path.
|
|
1873
|
+
if (shape.group && shape.group.parentNode) {
|
|
1874
|
+
shape.group.parentNode.removeChild(shape.group);
|
|
1875
|
+
} else if (shape.element && shape.element.parentNode) {
|
|
1876
|
+
shape.element.parentNode.removeChild(shape.element);
|
|
1877
|
+
}
|
|
1878
|
+
if (shape.parentFrame && typeof shape.parentFrame.removeShapeFromFrame === 'function') {
|
|
1879
|
+
shape.parentFrame.removeShapeFromFrame(shape);
|
|
1880
|
+
}
|
|
1881
|
+
cleanupAttachments(shape);
|
|
1882
|
+
if (isDiagramFrame && typeof shape.destroy === 'function') {
|
|
1883
|
+
shape.destroy();
|
|
1884
|
+
}
|
|
1810
1885
|
}
|
|
1811
1886
|
|
|
1812
1887
|
// Remove from shapes array
|
|
1813
1888
|
const idx = shapes.indexOf(shape);
|
|
1814
|
-
if (idx !== -1)
|
|
1815
|
-
|
|
1889
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
1890
|
+
|
|
1891
|
+
// Issue #24 bug #7: record the delete on the undo stack. Without
|
|
1892
|
+
// this the user can't undo a multi-selection delete at all —
|
|
1893
|
+
// the action just disappears. For frames we attach the child
|
|
1894
|
+
// snapshot so undo can re-attach them.
|
|
1895
|
+
try {
|
|
1896
|
+
if (childSnapshot) {
|
|
1897
|
+
pushDeleteAction(shape, { childSnapshot });
|
|
1898
|
+
} else {
|
|
1899
|
+
pushDeleteAction(shape);
|
|
1900
|
+
}
|
|
1901
|
+
} catch (err) {
|
|
1902
|
+
console.warn('[deleteSelectedShapes] pushDeleteAction failed:', err);
|
|
1816
1903
|
}
|
|
1817
1904
|
});
|
|
1905
|
+
|
|
1906
|
+
// Bug #3 follow-through: auto-select the released children and switch
|
|
1907
|
+
// to the pointer tool so the user can immediately keep manipulating
|
|
1908
|
+
// them. Skip if nothing was released (no frames deleted).
|
|
1909
|
+
if (releasedChildren.length > 0) {
|
|
1910
|
+
for (const child of releasedChildren) {
|
|
1911
|
+
if (shapes.indexOf(child) === -1) continue;
|
|
1912
|
+
multiSelection.addShape(child);
|
|
1913
|
+
}
|
|
1914
|
+
if (window.__sketchStoreApi && typeof window.__sketchStoreApi.setActiveTool === 'function') {
|
|
1915
|
+
window.__sketchStoreApi.setActiveTool('select');
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1818
1918
|
}
|
|
1819
1919
|
|
|
1820
1920
|
/**
|
package/src/core/UndoRedo.js
CHANGED
|
@@ -38,12 +38,15 @@ export function pushCreateAction(shape) {
|
|
|
38
38
|
redoStack.length = 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function pushDeleteAction(shape) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
export function pushDeleteAction(shape, meta = null) {
|
|
42
|
+
// Issue #24 bug #7: when the deleted shape is a frame, the caller may
|
|
43
|
+
// pass `meta.childSnapshot` — the contained shape REFERENCES at the
|
|
44
|
+
// time of delete. The undo branch uses this to re-attach the children
|
|
45
|
+
// when the frame is restored, instead of leaving an empty box.
|
|
46
|
+
const action = { type: 'delete', shape };
|
|
47
|
+
if (meta && meta.childSnapshot) action.childSnapshot = meta.childSnapshot;
|
|
48
|
+
undoStack.push(action);
|
|
49
|
+
|
|
47
50
|
// Clear redo stack when new action is performed
|
|
48
51
|
redoStack.length = 0;
|
|
49
52
|
}
|
|
@@ -419,10 +422,22 @@ export function pushOptionsChangeAction(shape, oldOptions) {
|
|
|
419
422
|
redoStack.length = 0;
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
// Issue #34 bug #4: refresh the multi-selection outline + handles so it
|
|
426
|
+
// follows the reverted geometry. Without this the selection rect stays
|
|
427
|
+
// pinned to the pre-undo coordinates and the user sees a ghost rect
|
|
428
|
+
// floating away from the shapes it claims to wrap.
|
|
429
|
+
function refreshSelectionAfterAction() {
|
|
430
|
+
const ms = (typeof window !== 'undefined') ? window.multiSelection : null;
|
|
431
|
+
if (!ms) return;
|
|
432
|
+
if (typeof ms.updateControls !== 'function') return;
|
|
433
|
+
if (!ms.selectedShapes || ms.selectedShapes.size === 0) return;
|
|
434
|
+
try { ms.updateControls(); } catch (err) { console.warn('[UndoRedo] selection refresh failed:', err); }
|
|
435
|
+
}
|
|
436
|
+
|
|
422
437
|
export function undo() {
|
|
423
438
|
if (undoStack.length === 0) return;
|
|
424
439
|
const action = undoStack.pop();
|
|
425
|
-
|
|
440
|
+
try {
|
|
426
441
|
if (action.type === 'frameTransform') {
|
|
427
442
|
// Handle frame transformation undo
|
|
428
443
|
action.shape.x = action.oldPos.x;
|
|
@@ -604,6 +619,33 @@ export function undo() {
|
|
|
604
619
|
if (svg) {
|
|
605
620
|
svg.appendChild(action.shape.group);
|
|
606
621
|
}
|
|
622
|
+
|
|
623
|
+
// Issue #24 bug #7: rehydrate frame state. The delete path
|
|
624
|
+
// orphaned the frame (removed <g>, clipGroup, clipPath from
|
|
625
|
+
// DOM) but kept the JS object intact. Re-attach clipGroup +
|
|
626
|
+
// clipPath, then re-parent every child from the snapshot via
|
|
627
|
+
// addShapeToFrame. Skip children that were subsequently
|
|
628
|
+
// deleted (they'll get re-added when their own delete action
|
|
629
|
+
// is undone in turn).
|
|
630
|
+
if (action.shape.shapeName === 'frame' && svg) {
|
|
631
|
+
if (action.shape.clipGroup && !action.shape.clipGroup.parentNode) {
|
|
632
|
+
svg.appendChild(action.shape.clipGroup);
|
|
633
|
+
}
|
|
634
|
+
if (action.shape.clipPath && !action.shape.clipPath.parentNode) {
|
|
635
|
+
const defs = svg.querySelector('defs');
|
|
636
|
+
if (defs) defs.appendChild(action.shape.clipPath);
|
|
637
|
+
else svg.insertBefore(action.shape.clipPath, svg.firstChild);
|
|
638
|
+
}
|
|
639
|
+
if (action.childSnapshot && Array.isArray(action.childSnapshot)) {
|
|
640
|
+
for (const child of action.childSnapshot) {
|
|
641
|
+
if (!child) continue;
|
|
642
|
+
if (shapes.indexOf(child) === -1) continue;
|
|
643
|
+
if (typeof action.shape.addShapeToFrame === 'function') {
|
|
644
|
+
action.shape.addShapeToFrame(child);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
607
649
|
}
|
|
608
650
|
redoStack.push(action);
|
|
609
651
|
return;
|
|
@@ -875,12 +917,13 @@ export function undo() {
|
|
|
875
917
|
redoStack.push(action);
|
|
876
918
|
return;
|
|
877
919
|
}
|
|
920
|
+
} finally { refreshSelectionAfterAction(); }
|
|
878
921
|
}
|
|
879
922
|
|
|
880
923
|
export function redo() {
|
|
881
924
|
if (redoStack.length === 0) return;
|
|
882
925
|
const action = redoStack.pop();
|
|
883
|
-
|
|
926
|
+
try {
|
|
884
927
|
if (action.type === 'frameTransform') {
|
|
885
928
|
// Handle frame transformation redo
|
|
886
929
|
action.shape.x = action.newPos.x;
|
|
@@ -1053,13 +1096,36 @@ export function redo() {
|
|
|
1053
1096
|
// Handle other shape deletion redo
|
|
1054
1097
|
const idx = shapes.indexOf(action.shape);
|
|
1055
1098
|
if (idx !== -1) shapes.splice(idx, 1);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
//
|
|
1061
|
-
if (action.shape.shapeName === 'frame') {
|
|
1062
|
-
action.
|
|
1099
|
+
|
|
1100
|
+
// Issue #24 bug #7: for a regular frame, re-orphan instead of
|
|
1101
|
+
// destroy() — releases children visually, detaches the frame's
|
|
1102
|
+
// own DOM bits, but keeps the JS object and child references
|
|
1103
|
+
// intact so a subsequent undo can rehydrate again.
|
|
1104
|
+
if (action.shape.shapeName === 'frame' && !action.shape._diagramType && action.childSnapshot) {
|
|
1105
|
+
for (const child of action.childSnapshot) {
|
|
1106
|
+
if (!child) continue;
|
|
1107
|
+
const el = child.group || child.element;
|
|
1108
|
+
if (el && svg) {
|
|
1109
|
+
if (action.shape.clipGroup && el.parentNode === action.shape.clipGroup) {
|
|
1110
|
+
action.shape.clipGroup.removeChild(el);
|
|
1111
|
+
}
|
|
1112
|
+
if (el.parentNode !== svg) svg.appendChild(el);
|
|
1113
|
+
}
|
|
1114
|
+
child.parentFrame = null;
|
|
1115
|
+
delete child.isBeingMovedByFrame;
|
|
1116
|
+
}
|
|
1117
|
+
action.shape.containedShapes = [];
|
|
1118
|
+
if (action.shape.group && action.shape.group.parentNode) action.shape.group.parentNode.removeChild(action.shape.group);
|
|
1119
|
+
if (action.shape.clipGroup && action.shape.clipGroup.parentNode) action.shape.clipGroup.parentNode.removeChild(action.shape.clipGroup);
|
|
1120
|
+
if (action.shape.clipPath && action.shape.clipPath.parentNode) action.shape.clipPath.parentNode.removeChild(action.shape.clipPath);
|
|
1121
|
+
} else {
|
|
1122
|
+
if (action.shape.group && action.shape.group.parentNode) {
|
|
1123
|
+
action.shape.group.parentNode.removeChild(action.shape.group);
|
|
1124
|
+
}
|
|
1125
|
+
// Diagram frame OR no snapshot → fall back to destroy().
|
|
1126
|
+
if (action.shape.shapeName === 'frame' && typeof action.shape.destroy === 'function') {
|
|
1127
|
+
action.shape.destroy();
|
|
1128
|
+
}
|
|
1063
1129
|
}
|
|
1064
1130
|
}
|
|
1065
1131
|
undoStack.push(action);
|
|
@@ -1329,6 +1395,7 @@ export function redo() {
|
|
|
1329
1395
|
undoStack.push(action);
|
|
1330
1396
|
return;
|
|
1331
1397
|
}
|
|
1398
|
+
} finally { refreshSelectionAfterAction(); }
|
|
1332
1399
|
}
|
|
1333
1400
|
|
|
1334
1401
|
// Optional: Keyboard shortcuts
|
package/src/core/ZoomPan.js
CHANGED
|
@@ -74,14 +74,24 @@ window.zoomFromCenter = function(direction) {
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
window.zoomReset = function() {
|
|
77
|
+
// Issue #24 bug #5: reset zoom in place around the current viewport
|
|
78
|
+
// centre — do NOT recenter to (0, 0). Recentering used to make whatever
|
|
79
|
+
// the user had on screen vanish until they panned back to it.
|
|
80
|
+
const cx = currentViewBox.x + currentViewBox.width / 2;
|
|
81
|
+
const cy = currentViewBox.y + currentViewBox.height / 2;
|
|
82
|
+
|
|
77
83
|
currentZoom = 1;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const rect = freehandCanvas.getBoundingClientRect();
|
|
85
|
+
const vbW = rect.width || window.innerWidth;
|
|
86
|
+
const vbH = rect.height || window.innerHeight;
|
|
87
|
+
|
|
88
|
+
currentViewBox.x = cx - vbW / 2;
|
|
89
|
+
currentViewBox.y = cy - vbH / 2;
|
|
90
|
+
currentViewBox.width = vbW;
|
|
91
|
+
currentViewBox.height = vbH;
|
|
82
92
|
freehandCanvas.setAttribute(
|
|
83
93
|
"viewBox",
|
|
84
|
-
|
|
94
|
+
`${currentViewBox.x} ${currentViewBox.y} ${vbW} ${vbH}`
|
|
85
95
|
);
|
|
86
96
|
updateZoomDisplay();
|
|
87
97
|
if (window.__sketchStoreApi && window.__sketchStoreApi.setZoom) {
|
package/src/index.js
CHANGED
|
@@ -47,7 +47,20 @@ export function createSketchEngine(svgElement, options = {}) {
|
|
|
47
47
|
// need to round-trip a scene through their own storage. saveScene reads from
|
|
48
48
|
// window.shapes (populated by an active engine); loadScene rebuilds shapes onto
|
|
49
49
|
// the active engine's SVG. Both must be called after engine.init() completes.
|
|
50
|
-
|
|
50
|
+
// Scene serializer is intentionally NOT re-exported here.
|
|
51
|
+
//
|
|
52
|
+
// SceneSerializer.js statically imports every shape module, and the shape
|
|
53
|
+
// modules reference bare `rough` / `svg` globals that only exist after
|
|
54
|
+
// engine.init() runs. Re-exporting `saveScene`/`loadScene` at this module
|
|
55
|
+
// scope makes any `import { ... } from '@elixpo/lixsketch'` evaluate the
|
|
56
|
+
// shape graph eagerly and crash with "rough is not defined".
|
|
57
|
+
//
|
|
58
|
+
// Hosts that need scene serialization should import the subpath directly,
|
|
59
|
+
// AFTER mounting the engine:
|
|
60
|
+
// const { saveScene, loadScene } = await import(
|
|
61
|
+
// '@elixpo/lixsketch/src/core/SceneSerializer.js'
|
|
62
|
+
// );
|
|
63
|
+
// or use window.__sceneSerializer which engine.init() exposes.
|
|
51
64
|
|
|
52
65
|
// Adaptive image compressor — same one the engine uses internally for
|
|
53
66
|
// uploads. Exported so embedded hosts can pre-compress images before
|