@elixpo/lixsketch 5.6.5 → 5.6.7
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/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +2 -2
- package/dist/mcp/stdio.js +5 -1
- package/dist/mcp/stdio.js.map +2 -2
- package/dist/react/{AIRenderer-SRF7LU5K.js → AIRenderer-R5M2CIDO.js} +13 -6
- package/dist/react/AIRenderer-R5M2CIDO.js.map +7 -0
- package/dist/react/Circle-HMM5PKJ2.js +10 -0
- package/dist/react/{CopyPaste-YUDZ6DHY.js → CopyPaste-PPDS3AD6.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-PPDS3AD6.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-UV7LFSAS.js} +216 -63
- package/dist/react/EventDispatcher-UV7LFSAS.js.map +7 -0
- package/dist/react/Frame-DELCMYV6.js +11 -0
- package/dist/react/{FreehandStroke-U6GYZOM4.js → FreehandStroke-WXIJKD5I.js} +2 -2
- package/dist/react/{GraphEngine-MM6ALA2N.js → GraphEngine-XH3RPYO4.js} +9 -2
- package/dist/react/GraphEngine-XH3RPYO4.js.map +7 -0
- package/dist/react/IconShape-PZHMZSLR.js +10 -0
- package/dist/react/ImageShape-ZMBZARRI.js +10 -0
- package/dist/react/Line-VPVPL6ET.js +10 -0
- package/dist/react/{LixScriptParser-JYWJX375.js → LixScriptParser-YS3ZOTGS.js} +6 -2
- package/dist/react/LixScriptParser-YS3ZOTGS.js.map +7 -0
- package/dist/react/{MermaidFlowchartRenderer-PQEC4TMI.js → MermaidFlowchartRenderer-EL3Q2MNW.js} +6 -2
- package/dist/react/MermaidFlowchartRenderer-EL3Q2MNW.js.map +7 -0
- package/dist/react/MermaidSequenceParser-DBYXW23P.js +8 -0
- package/dist/react/{MermaidSequenceRenderer-KCUHO7UI.js → MermaidSequenceRenderer-K4KHHEVC.js} +10 -3
- package/dist/react/MermaidSequenceRenderer-K4KHHEVC.js.map +7 -0
- package/dist/react/{MermaidStructuredRenderer-W7UKN7BM.js → MermaidStructuredRenderer-6LN3WISP.js} +7 -2
- package/dist/react/MermaidStructuredRenderer-6LN3WISP.js.map +7 -0
- package/dist/react/Rectangle-4U4CD2ER.js +10 -0
- package/dist/react/{SceneSerializer-SG6SJN5X.js → SceneSerializer-H6KMQCO2.js} +120 -46
- package/dist/react/SceneSerializer-H6KMQCO2.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-CFWRGVKH.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-UFLZJQXS.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-RXO7YHA2.js} +12 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-DO327FKU.js} +3 -3
- package/dist/react/{chunk-EOURRJ55.js → chunk-2ALS5JGS.js} +2 -2
- package/dist/react/{chunk-LJR7EVW2.js → chunk-37PF7FG2.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-XOWNCYE4.js → chunk-6LWUR7XB.js} +3 -3
- package/dist/react/{chunk-ASBC5G35.js → chunk-7DD2JRBC.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-BOZEDUAF.js} +2 -2
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-CGMUXYPJ.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-DODPOWNX.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-GQLXUZHQ.js} +2 -2
- package/dist/react/{chunk-7CM5XQDN.js → chunk-I7TVGJFF.js} +20 -36
- package/dist/react/chunk-I7TVGJFF.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-KC26KJW4.js} +26 -17
- package/dist/react/chunk-KC26KJW4.js.map +7 -0
- package/dist/react/{chunk-XFL4R5ZF.js → chunk-KZBKYHZO.js} +16 -35
- package/dist/react/chunk-KZBKYHZO.js.map +7 -0
- package/dist/react/{chunk-VQVEPJSA.js → chunk-MHV5P6TS.js} +2 -1
- package/dist/react/chunk-MHV5P6TS.js.map +7 -0
- package/dist/react/{chunk-PXFPPO3R.js → chunk-NH3NVSWT.js} +18 -2
- package/dist/react/chunk-NH3NVSWT.js.map +7 -0
- package/dist/react/{chunk-APTOPAOY.js → chunk-O7KXLFZN.js} +3 -3
- package/dist/react/{chunk-GQUZPON4.js → chunk-RK3NVAER.js} +4 -4
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.js.map +7 -0
- package/dist/react/{chunk-6GPEWDRB.js → chunk-VZX55VQL.js} +115 -6
- package/dist/react/chunk-VZX55VQL.js.map +7 -0
- package/dist/react/{chunk-TKXKJNTX.js → chunk-W7XUY4GR.js} +16 -19
- package/dist/react/chunk-W7XUY4GR.js.map +7 -0
- package/dist/react/{chunk-XYUFEIZR.js → chunk-X5ENBMGL.js} +2 -2
- package/dist/react/{chunk-K5BG5MHY.js → chunk-ZNZAICWY.js} +36 -60
- package/dist/react/chunk-ZNZAICWY.js.map +7 -0
- package/dist/react/{circleTool-LDKGZALF.js → circleTool-NK4RVDAR.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-3WF4HEOW.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-VCNV2NG2.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-TBAOD46K.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-65JCAKYF.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-UQZUBOK2.js} +4 -4
- package/dist/react/index.js +148 -37
- package/dist/react/index.js.map +4 -4
- package/dist/react/{lineTool-ZMR27U5L.js → lineTool-762KBYOM.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-IQK3HZK6.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-ITG2SBJK.js} +5 -5
- package/package.json +5 -2
- package/src/core/AIRenderer.js +8 -2
- package/src/core/CanvasSpace.js +28 -0
- package/src/core/GraphRenderer.js +8 -3
- package/src/core/LixScriptParser.js +7 -1
- package/src/core/MermaidFlowchartRenderer.js +5 -1
- package/src/core/MermaidSequenceParser.js +1 -0
- package/src/core/MermaidSequenceRenderer.js +8 -3
- package/src/core/MermaidStructuredRenderer.js +6 -1
- package/src/core/SceneSerializer.js +121 -38
- package/src/core/Selection.js +29 -14
- package/src/core/UndoRedo.js +126 -5
- package/src/react/components/AppMenu.jsx +20 -19
- package/src/react/components/canvas/CanvasRulers.jsx +8 -13
- package/src/react/components/canvas/SVGCanvas.jsx +2 -1
- package/src/react/components/canvas/rulerUnits.js +28 -0
- package/src/react/components/sidebars/PaintBucketSidebar.jsx +1 -1
- package/src/react/store/useSketchStore.js +24 -3
- package/src/react/store/useUIStore.js +24 -1
- package/src/react/utils/canvasBackgrounds.js +29 -0
- package/src/shapes/Circle.js +12 -19
- package/src/shapes/Frame.js +38 -69
- package/src/shapes/FreehandStroke.js +24 -1
- package/src/shapes/Rectangle.js +10 -22
- package/src/tools/circleTool.js +3 -27
- package/src/tools/paintBucketTool.js +72 -15
- package/src/tools/rectangleTool.js +7 -23
- package/src/tools/shapeRecognition.js +270 -0
- package/src/tools/shapeRecognitionTool.js +36 -183
- package/dist/react/AIRenderer-SRF7LU5K.js.map +0 -7
- package/dist/react/Circle-6POZCUZD.js +0 -9
- package/dist/react/EventDispatcher-7LAK7GQ7.js.map +0 -7
- package/dist/react/Frame-7D2MXCFF.js +0 -11
- package/dist/react/GraphEngine-MM6ALA2N.js.map +0 -7
- package/dist/react/IconShape-ZADX7Y33.js +0 -10
- package/dist/react/ImageShape-UJQN2HI7.js +0 -10
- package/dist/react/Line-APEULMHE.js +0 -10
- package/dist/react/LixScriptParser-JYWJX375.js.map +0 -7
- package/dist/react/MermaidFlowchartRenderer-PQEC4TMI.js.map +0 -7
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +0 -8
- package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +0 -7
- package/dist/react/MermaidStructuredRenderer-W7UKN7BM.js.map +0 -7
- package/dist/react/Rectangle-S7XUKWYD.js +0 -9
- package/dist/react/SceneSerializer-SG6SJN5X.js.map +0 -7
- package/dist/react/chunk-6GPEWDRB.js.map +0 -7
- package/dist/react/chunk-7CM5XQDN.js.map +0 -7
- package/dist/react/chunk-FETTJLCV.js.map +0 -7
- package/dist/react/chunk-K5BG5MHY.js.map +0 -7
- package/dist/react/chunk-K5RY4EV7.js.map +0 -7
- package/dist/react/chunk-PXFPPO3R.js.map +0 -7
- package/dist/react/chunk-TKXKJNTX.js.map +0 -7
- package/dist/react/chunk-VQVEPJSA.js.map +0 -7
- package/dist/react/chunk-XFL4R5ZF.js.map +0 -7
- /package/dist/react/{Circle-6POZCUZD.js.map → Circle-HMM5PKJ2.js.map} +0 -0
- /package/dist/react/{Frame-7D2MXCFF.js.map → Frame-DELCMYV6.js.map} +0 -0
- /package/dist/react/{FreehandStroke-U6GYZOM4.js.map → FreehandStroke-WXIJKD5I.js.map} +0 -0
- /package/dist/react/{IconShape-ZADX7Y33.js.map → IconShape-PZHMZSLR.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-ZMBZARRI.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-VPVPL6ET.js.map} +0 -0
- /package/dist/react/{MermaidSequenceParser-UKG2NSZY.js.map → MermaidSequenceParser-DBYXW23P.js.map} +0 -0
- /package/dist/react/{Rectangle-S7XUKWYD.js.map → Rectangle-4U4CD2ER.js.map} +0 -0
- /package/dist/react/{Selection-2TSH3J6P.js.map → Selection-CFWRGVKH.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-UFLZJQXS.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-RXO7YHA2.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-DO327FKU.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-2ALS5JGS.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-37PF7FG2.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-6LWUR7XB.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-7DD2JRBC.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-BOZEDUAF.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-CGMUXYPJ.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-DODPOWNX.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-GQLXUZHQ.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-O7KXLFZN.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-RK3NVAER.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-X5ENBMGL.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-NK4RVDAR.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-3WF4HEOW.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-VCNV2NG2.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-TBAOD46K.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-65JCAKYF.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-UQZUBOK2.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-762KBYOM.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-IQK3HZK6.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-ITG2SBJK.js.map} +0 -0
package/src/core/UndoRedo.js
CHANGED
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
const undoStack = [];
|
|
6
6
|
const redoStack = [];
|
|
7
7
|
|
|
8
|
+
export function beginUndoBatch() {
|
|
9
|
+
return { startIndex: undoStack.length };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function endUndoBatch(batch, label = 'batch') {
|
|
13
|
+
if (!batch || !Number.isInteger(batch.startIndex)) return;
|
|
14
|
+
const startIndex = Math.min(Math.max(batch.startIndex, 0), undoStack.length);
|
|
15
|
+
let actions = undoStack.splice(startIndex);
|
|
16
|
+
if (actions.length === 0) return;
|
|
17
|
+
const groupedAction = actions.length === 1 ? actions[0] : { type: 'batch', label, actions };
|
|
18
|
+
undoStack.push(groupedAction);
|
|
19
|
+
redoStack.length = 0;
|
|
20
|
+
notifyCollaboration();
|
|
21
|
+
return groupedAction;
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
// Import references to text-related variables and functions
|
|
9
25
|
let selectedElement = null;
|
|
10
26
|
let updateSelectionFeedback = null;
|
|
@@ -58,6 +74,9 @@ export function pushDeleteAction(shape, meta = null) {
|
|
|
58
74
|
// when the frame is restored, instead of leaving an empty box.
|
|
59
75
|
const action = { type: 'delete', shape };
|
|
60
76
|
if (meta && meta.childSnapshot) action.childSnapshot = meta.childSnapshot;
|
|
77
|
+
if (Number.isInteger(meta?.shapeIndex)) action.shapeIndex = meta.shapeIndex;
|
|
78
|
+
if (meta?.parentFrame) action.parentFrame = meta.parentFrame;
|
|
79
|
+
if (Number.isInteger(meta?.frameIndex)) action.frameIndex = meta.frameIndex;
|
|
61
80
|
undoStack.push(action);
|
|
62
81
|
|
|
63
82
|
// Clear redo stack when new action is performed
|
|
@@ -65,6 +84,30 @@ export function pushDeleteAction(shape, meta = null) {
|
|
|
65
84
|
notifyCollaboration();
|
|
66
85
|
}
|
|
67
86
|
|
|
87
|
+
export function pushCanvasResetAction(snapshot, restore, reset) {
|
|
88
|
+
if (!snapshot || typeof restore !== 'function' || typeof reset !== 'function') return;
|
|
89
|
+
// A reset is a new history boundary. Older per-shape actions reference
|
|
90
|
+
// objects that the scene loader will replace, so only the atomic reset
|
|
91
|
+
// snapshot remains undoable.
|
|
92
|
+
undoStack.length = 0;
|
|
93
|
+
redoStack.length = 0;
|
|
94
|
+
undoStack.push({ type: 'canvasReset', snapshot, restore, reset });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function invalidateCanvasResetUndo() {
|
|
98
|
+
for (let index = undoStack.length - 1; index >= 0; index -= 1) {
|
|
99
|
+
if (undoStack[index]?.type === 'canvasReset') undoStack.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
for (let index = redoStack.length - 1; index >= 0; index -= 1) {
|
|
102
|
+
if (redoStack[index]?.type === 'canvasReset') redoStack.splice(index, 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function clearUndoHistory() {
|
|
107
|
+
undoStack.length = 0;
|
|
108
|
+
redoStack.length = 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
68
111
|
// Enhanced delete action to clean up attached arrows
|
|
69
112
|
export function pushDeleteActionWithAttachments(shape) {
|
|
70
113
|
let affectedArrows = [];
|
|
@@ -442,6 +485,15 @@ export function pushOptionsChangeAction(shape, oldOptions, newOptions = null) {
|
|
|
442
485
|
notifyCollaboration();
|
|
443
486
|
}
|
|
444
487
|
|
|
488
|
+
function applyOptionsSnapshot(shape, options) {
|
|
489
|
+
shape.options = { ...options };
|
|
490
|
+
if (shape.shapeName === 'frame') {
|
|
491
|
+
shape.fillColor = options.fillColor;
|
|
492
|
+
shape.fillStyle = options.fillStyle;
|
|
493
|
+
}
|
|
494
|
+
shape.draw();
|
|
495
|
+
}
|
|
496
|
+
|
|
445
497
|
// Issue #34 bug #4: refresh the multi-selection outline + handles so it
|
|
446
498
|
// follows the reverted geometry. Without this the selection rect stays
|
|
447
499
|
// pinned to the pre-undo coordinates and the user sees a ghost rect
|
|
@@ -454,10 +506,54 @@ function refreshSelectionAfterAction() {
|
|
|
454
506
|
try { ms.updateControls(); } catch (err) { console.warn('[UndoRedo] selection refresh failed:', err); }
|
|
455
507
|
}
|
|
456
508
|
|
|
509
|
+
function restoreDeletedShapeFrame(action) {
|
|
510
|
+
const frame = action.parentFrame;
|
|
511
|
+
const shape = action.shape;
|
|
512
|
+
if (!frame || !shape || typeof frame.addShapeToFrame !== 'function') return;
|
|
513
|
+
frame.addShapeToFrame(shape);
|
|
514
|
+
if (Number.isInteger(action.frameIndex) && Array.isArray(frame.containedShapes)) {
|
|
515
|
+
const currentIndex = frame.containedShapes.indexOf(shape);
|
|
516
|
+
if (currentIndex >= 0) frame.containedShapes.splice(currentIndex, 1);
|
|
517
|
+
const targetIndex = Math.min(Math.max(action.frameIndex, 0), frame.containedShapes.length);
|
|
518
|
+
frame.containedShapes.splice(targetIndex, 0, shape);
|
|
519
|
+
const element = shape.group || shape.element;
|
|
520
|
+
const nextShape = frame.containedShapes[targetIndex + 1];
|
|
521
|
+
const nextElement = nextShape?.group || nextShape?.element;
|
|
522
|
+
if (element && frame.clipGroup && nextElement?.parentNode === frame.clipGroup) {
|
|
523
|
+
frame.clipGroup.insertBefore(element, nextElement);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function detachDeletedShapeFrame(action) {
|
|
529
|
+
const frame = action.parentFrame;
|
|
530
|
+
const shape = action.shape;
|
|
531
|
+
if (frame && shape && typeof frame.removeShapeFromFrame === 'function' && shape.parentFrame === frame) {
|
|
532
|
+
frame.removeShapeFromFrame(shape);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
457
536
|
export function undo() {
|
|
458
537
|
if (undoStack.length === 0) return;
|
|
459
538
|
const action = undoStack.pop();
|
|
460
539
|
try {
|
|
540
|
+
if (action.type === 'batch') {
|
|
541
|
+
for (let index = action.actions.length - 1; index >= 0; index -= 1) {
|
|
542
|
+
undoStack.push(action.actions[index]);
|
|
543
|
+
undo();
|
|
544
|
+
redoStack.pop();
|
|
545
|
+
}
|
|
546
|
+
redoStack.push(action);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (action.type === 'canvasReset') {
|
|
551
|
+
action.restore(action.snapshot);
|
|
552
|
+
redoStack.push(action);
|
|
553
|
+
notifyCollaboration();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
461
557
|
if (action.type === 'frameTransform') {
|
|
462
558
|
// Handle frame transformation undo
|
|
463
559
|
action.shape.x = action.oldPos.x;
|
|
@@ -652,7 +748,12 @@ export function undo() {
|
|
|
652
748
|
action.shape.restore();
|
|
653
749
|
} else {
|
|
654
750
|
// Handle other shape deletion undo
|
|
655
|
-
shapes.
|
|
751
|
+
if (shapes.indexOf(action.shape) === -1) {
|
|
752
|
+
const index = Number.isInteger(action.shapeIndex)
|
|
753
|
+
? Math.min(Math.max(action.shapeIndex, 0), shapes.length)
|
|
754
|
+
: shapes.length;
|
|
755
|
+
shapes.splice(index, 0, action.shape);
|
|
756
|
+
}
|
|
656
757
|
if (svg) {
|
|
657
758
|
svg.appendChild(action.shape.group);
|
|
658
759
|
}
|
|
@@ -684,6 +785,7 @@ export function undo() {
|
|
|
684
785
|
}
|
|
685
786
|
}
|
|
686
787
|
}
|
|
788
|
+
restoreDeletedShapeFrame(action);
|
|
687
789
|
redoStack.push(action);
|
|
688
790
|
return;
|
|
689
791
|
}
|
|
@@ -959,8 +1061,7 @@ export function undo() {
|
|
|
959
1061
|
action.shape.draw();
|
|
960
1062
|
} else {
|
|
961
1063
|
// Handle other shape options change undo
|
|
962
|
-
action.shape
|
|
963
|
-
action.shape.draw();
|
|
1064
|
+
applyOptionsSnapshot(action.shape, action.oldOptions);
|
|
964
1065
|
}
|
|
965
1066
|
redoStack.push(action);
|
|
966
1067
|
return;
|
|
@@ -975,6 +1076,23 @@ export function redo() {
|
|
|
975
1076
|
if (redoStack.length === 0) return;
|
|
976
1077
|
const action = redoStack.pop();
|
|
977
1078
|
try {
|
|
1079
|
+
if (action.type === 'batch') {
|
|
1080
|
+
for (const childAction of action.actions) {
|
|
1081
|
+
redoStack.push(childAction);
|
|
1082
|
+
redo();
|
|
1083
|
+
undoStack.pop();
|
|
1084
|
+
}
|
|
1085
|
+
undoStack.push(action);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (action.type === 'canvasReset') {
|
|
1090
|
+
action.reset();
|
|
1091
|
+
undoStack.push(action);
|
|
1092
|
+
notifyCollaboration();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
978
1096
|
if (action.type === 'frameTransform') {
|
|
979
1097
|
// Handle frame transformation redo
|
|
980
1098
|
action.shape.x = action.newPos.x;
|
|
@@ -1137,6 +1255,7 @@ export function redo() {
|
|
|
1137
1255
|
}
|
|
1138
1256
|
|
|
1139
1257
|
if (action.type === 'delete') {
|
|
1258
|
+
detachDeletedShapeFrame(action);
|
|
1140
1259
|
if (action.shape.type === 'text') {
|
|
1141
1260
|
// Handle text deletion redo
|
|
1142
1261
|
if ((action.shape.element || action.shape).parentNode) {
|
|
@@ -1453,8 +1572,7 @@ export function redo() {
|
|
|
1453
1572
|
action.shape.arrowCurveAmount = action.newOptions.arrowCurveAmount;
|
|
1454
1573
|
action.shape.draw();
|
|
1455
1574
|
} else {
|
|
1456
|
-
action.shape
|
|
1457
|
-
action.shape.draw();
|
|
1575
|
+
applyOptionsSnapshot(action.shape, action.newOptions);
|
|
1458
1576
|
}
|
|
1459
1577
|
undoStack.push(action);
|
|
1460
1578
|
return;
|
|
@@ -1484,6 +1602,9 @@ document.getElementById('redo')?.addEventListener('click', redo);
|
|
|
1484
1602
|
// Expose globally for plain scripts
|
|
1485
1603
|
window.pushCreateAction = pushCreateAction;
|
|
1486
1604
|
window.pushDeleteAction = pushDeleteAction;
|
|
1605
|
+
window.pushCanvasResetAction = pushCanvasResetAction;
|
|
1606
|
+
window.invalidateCanvasResetUndo = invalidateCanvasResetUndo;
|
|
1607
|
+
window.clearUndoHistory = clearUndoHistory;
|
|
1487
1608
|
window.pushDeleteActionWithAttachments = pushDeleteActionWithAttachments;
|
|
1488
1609
|
window.pushTransformAction = pushTransformAction;
|
|
1489
1610
|
window.pushOptionsChangeAction = pushOptionsChangeAction;
|
|
@@ -7,23 +7,7 @@ import useSketchStore from '../store/useSketchStore'
|
|
|
7
7
|
import { useAuthStore } from '../hooks/inertStores'
|
|
8
8
|
import { triggerCloudSync } from '../hooks/inertStores'
|
|
9
9
|
import { useTranslation } from '../hooks/useTranslation'
|
|
10
|
-
|
|
11
|
-
// Issue #38 follow-up: theme-paired swatches. The menu picks the
|
|
12
|
-
// matching list at render time based on the active theme.
|
|
13
|
-
const CANVAS_BACKGROUNDS_LIGHT = [
|
|
14
|
-
{ color: '#ffffff', label: 'menu.canvasBg.white' },
|
|
15
|
-
{ color: '#fbf9fd', label: 'menu.canvasBg.cream' },
|
|
16
|
-
{ color: '#f5f3ed', label: 'menu.canvasBg.paper' },
|
|
17
|
-
{ color: '#f0f5fb', label: 'menu.canvasBg.skyTint' },
|
|
18
|
-
{ color: '#f0f5ef', label: 'menu.canvasBg.sageTint' },
|
|
19
|
-
]
|
|
20
|
-
const CANVAS_BACKGROUNDS_DARK = [
|
|
21
|
-
{ color: '#000000', label: 'menu.canvasBg.black' },
|
|
22
|
-
{ color: '#161718', label: 'menu.canvasBg.darkGray' },
|
|
23
|
-
{ color: '#15111f', label: 'menu.canvasBg.blueBlack' },
|
|
24
|
-
{ color: '#181605', label: 'menu.canvasBg.darkYellow' },
|
|
25
|
-
{ color: '#1B1615', label: 'menu.canvasBg.darkBrown' },
|
|
26
|
-
]
|
|
10
|
+
import { CANVAS_BACKGROUNDS } from '../utils/canvasBackgrounds'
|
|
27
11
|
|
|
28
12
|
function DangerWarningDialog({ action, workspaceName, onCancel, onConfirm }) {
|
|
29
13
|
useEffect(() => {
|
|
@@ -130,6 +114,8 @@ export default function AppMenu() {
|
|
|
130
114
|
const toggleGrid = useSketchStore((s) => s.toggleGrid)
|
|
131
115
|
const rulersEnabled = useSketchStore((s) => s.rulersEnabled)
|
|
132
116
|
const toggleRulers = useSketchStore((s) => s.toggleRulers)
|
|
117
|
+
const rulerUnit = useSketchStore((s) => s.rulerUnit)
|
|
118
|
+
const setRulerUnit = useSketchStore((s) => s.setRulerUnit)
|
|
133
119
|
|
|
134
120
|
const viewMode = useSketchStore((s) => s.viewMode)
|
|
135
121
|
const zenMode = useSketchStore((s) => s.zenMode)
|
|
@@ -300,6 +286,21 @@ export default function AppMenu() {
|
|
|
300
286
|
<option value="en">English</option>
|
|
301
287
|
<option value="bg">Български</option>
|
|
302
288
|
<option value="de">Deutsch</option>
|
|
289
|
+
<option value="hi">हिन्दी</option>
|
|
290
|
+
</select>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="w-full flex items-center justify-between px-3 py-2 border-b border-border-light text-text-secondary text-[11px]">
|
|
294
|
+
<span>Ruler unit</span>
|
|
295
|
+
<select
|
|
296
|
+
aria-label="Ruler unit"
|
|
297
|
+
className="cursor-pointer rounded border border-border-light bg-surface-hover px-1 text-[10px] uppercase text-text-primary outline-none"
|
|
298
|
+
value={rulerUnit}
|
|
299
|
+
onChange={(event) => setRulerUnit(event.target.value)}
|
|
300
|
+
>
|
|
301
|
+
<option value="px">px</option>
|
|
302
|
+
<option value="cm">cm</option>
|
|
303
|
+
<option value="in">in</option>
|
|
303
304
|
</select>
|
|
304
305
|
</div>
|
|
305
306
|
|
|
@@ -453,10 +454,10 @@ export default function AppMenu() {
|
|
|
453
454
|
{t('menu.canvasBackground')}
|
|
454
455
|
</p>
|
|
455
456
|
<div className="flex items-center gap-1.5">
|
|
456
|
-
{
|
|
457
|
+
{CANVAS_BACKGROUNDS[theme === 'light' ? 'light' : 'dark'].map((bg) => (
|
|
457
458
|
<button
|
|
458
459
|
key={bg.color}
|
|
459
|
-
onClick={() => setCanvasBackground(bg.color)}
|
|
460
|
+
onClick={() => setCanvasBackground(bg.color, theme === 'light' ? 'light' : 'dark')}
|
|
460
461
|
title={t(bg.label)}
|
|
461
462
|
className={`w-7 h-7 rounded-full border-2 cursor-pointer transition-all duration-200 ${
|
|
462
463
|
canvasBackground === bg.color
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { formatRulerValue, normalizeRulerUnit } from './rulerUnits.js'
|
|
4
5
|
|
|
5
6
|
const GRID_SIZE = 20
|
|
6
7
|
const MIN_LABEL_GAP = 52
|
|
@@ -16,14 +17,6 @@ function getRulerStep(zoom) {
|
|
|
16
17
|
return GRID_SIZE
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function formatDimension(value) {
|
|
20
|
-
const rounded = Math.round(value)
|
|
21
|
-
const absolute = Math.abs(rounded)
|
|
22
|
-
if (absolute >= 1000000) return `${Number((rounded / 1000000).toFixed(1))}m`
|
|
23
|
-
if (absolute >= 10000) return `${Number((rounded / 1000).toFixed(1))}k`
|
|
24
|
-
return String(rounded)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
20
|
function buildTicks(start, extent, zoom, step) {
|
|
28
21
|
const first = Math.floor(start / step) * step
|
|
29
22
|
const last = start + extent / zoom
|
|
@@ -34,7 +27,8 @@ function buildTicks(start, extent, zoom, step) {
|
|
|
34
27
|
return ticks
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
export default function CanvasRulers({ enabled, svgRef }) {
|
|
30
|
+
export default function CanvasRulers({ enabled, svgRef, unit = 'px' }) {
|
|
31
|
+
const rulerUnit = normalizeRulerUnit(unit)
|
|
38
32
|
const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1, width: 0, height: 0, rulerLeft: 64, rulerTop: 48 })
|
|
39
33
|
const [pointer, setPointer] = useState({ x: 0, y: 0, visible: false })
|
|
40
34
|
|
|
@@ -113,28 +107,29 @@ export default function CanvasRulers({ enabled, svgRef }) {
|
|
|
113
107
|
<div className="absolute right-0 overflow-hidden border-b border-border-light bg-surface/95 shadow-sm backdrop-blur-sm" style={{ height: RULER_SIZE, left: horizontalStart, top: viewport.rulerTop }}>
|
|
114
108
|
{horizontalTicks.map(({ value, position }) => (
|
|
115
109
|
<div key={value} className="absolute bottom-0 h-2 border-l border-text-dim" style={{ left: position - horizontalStart }}>
|
|
116
|
-
<span className="absolute bottom-2 left-1 font-mono text-[9px] leading-none tabular-nums">{
|
|
110
|
+
<span className="absolute bottom-2 left-1 font-mono text-[9px] leading-none tabular-nums">{formatRulerValue(value, rulerUnit)}</span>
|
|
117
111
|
</div>
|
|
118
112
|
))}
|
|
119
113
|
{pointerInDrawingArea && (
|
|
120
114
|
<div className="absolute inset-y-0 border-l border-accent" style={{ left: pointer.x - horizontalStart }}>
|
|
121
|
-
<span className="absolute left-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm">{
|
|
115
|
+
<span className="absolute left-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm">{formatRulerValue(pointerWorldX, rulerUnit, true)}</span>
|
|
122
116
|
</div>
|
|
123
117
|
)}
|
|
124
118
|
</div>
|
|
125
119
|
<div className="absolute bottom-0 overflow-hidden border-r border-border-light bg-surface/95 shadow-sm backdrop-blur-sm" style={{ width: RULER_SIZE, left: viewport.rulerLeft, top: verticalStart }}>
|
|
126
120
|
{verticalTicks.map(({ value, position }) => (
|
|
127
121
|
<div key={value} className="absolute right-0 w-2 border-t border-text-dim" style={{ top: position - verticalStart }}>
|
|
128
|
-
<span className="absolute right-2 top-1 font-mono text-[9px] leading-none tabular-nums" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{
|
|
122
|
+
<span className="absolute right-2 top-1 font-mono text-[9px] leading-none tabular-nums" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatRulerValue(value, rulerUnit)}</span>
|
|
129
123
|
</div>
|
|
130
124
|
))}
|
|
131
125
|
{pointerInDrawingArea && (
|
|
132
126
|
<div className="absolute inset-x-0 border-t border-accent" style={{ top: pointer.y - verticalStart }}>
|
|
133
|
-
<span className="absolute right-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{
|
|
127
|
+
<span className="absolute right-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatRulerValue(pointerWorldY, rulerUnit, true)}</span>
|
|
134
128
|
</div>
|
|
135
129
|
)}
|
|
136
130
|
</div>
|
|
137
131
|
<div className="absolute border-b border-r border-border-light bg-accent/20" style={{ width: RULER_SIZE, height: RULER_SIZE, left: viewport.rulerLeft, top: viewport.rulerTop }}>
|
|
132
|
+
<span className="absolute bottom-0.5 left-1 font-mono text-[8px] uppercase text-accent">{rulerUnit}</span>
|
|
138
133
|
<span className="absolute left-1/2 top-1/2 h-2.5 -translate-x-1/2 -translate-y-1/2 border-l border-accent" />
|
|
139
134
|
<span className="absolute left-1/2 top-1/2 w-2.5 -translate-x-1/2 -translate-y-1/2 border-t border-accent" />
|
|
140
135
|
</div>
|
|
@@ -14,6 +14,7 @@ export default function SVGCanvas() {
|
|
|
14
14
|
const gridEnabled = useSketchStore((s) => s.gridEnabled)
|
|
15
15
|
const hydrateGrid = useSketchStore((s) => s.hydrateGrid)
|
|
16
16
|
const rulersEnabled = useSketchStore((s) => s.rulersEnabled)
|
|
17
|
+
const rulerUnit = useSketchStore((s) => s.rulerUnit)
|
|
17
18
|
const hydrateRulers = useSketchStore((s) => s.hydrateRulers)
|
|
18
19
|
const getCursor = useSketchStore((s) => s.getCursor)
|
|
19
20
|
const cursor = getCursor()
|
|
@@ -152,7 +153,7 @@ export default function SVGCanvas() {
|
|
|
152
153
|
/>
|
|
153
154
|
)}
|
|
154
155
|
</svg>
|
|
155
|
-
<CanvasRulers enabled={rulersEnabled} svgRef={svgRef} />
|
|
156
|
+
<CanvasRulers enabled={rulersEnabled} svgRef={svgRef} unit={rulerUnit} />
|
|
156
157
|
</>
|
|
157
158
|
)
|
|
158
159
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const RULER_UNITS = ['px', 'cm', 'in']
|
|
2
|
+
|
|
3
|
+
const PIXELS_PER_UNIT = { px: 1, cm: 96 / 2.54, in: 96 }
|
|
4
|
+
|
|
5
|
+
export function normalizeRulerUnit(unit) {
|
|
6
|
+
return RULER_UNITS.includes(unit) ? unit : 'px'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function convertRulerValue(pixelValue, unit = 'px') {
|
|
10
|
+
return pixelValue / PIXELS_PER_UNIT[normalizeRulerUnit(unit)]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatRulerValue(pixelValue, unit = 'px', suffix = false) {
|
|
14
|
+
const normalized = normalizeRulerUnit(unit)
|
|
15
|
+
const converted = convertRulerValue(pixelValue, normalized)
|
|
16
|
+
let label
|
|
17
|
+
if (normalized === 'px') {
|
|
18
|
+
const rounded = Math.round(converted)
|
|
19
|
+
const absolute = Math.abs(rounded)
|
|
20
|
+
if (absolute >= 1000000) label = `${Number((rounded / 1000000).toFixed(1))}m`
|
|
21
|
+
else if (absolute >= 10000) label = `${Number((rounded / 1000).toFixed(1))}k`
|
|
22
|
+
else label = String(rounded)
|
|
23
|
+
} else {
|
|
24
|
+
const precision = Math.abs(converted) < 10 ? 2 : Math.abs(converted) < 100 ? 1 : 0
|
|
25
|
+
label = String(Number(converted.toFixed(precision)))
|
|
26
|
+
}
|
|
27
|
+
return suffix ? `${label} ${normalized}` : label
|
|
28
|
+
}
|
|
@@ -43,7 +43,7 @@ export default function PaintBucketSidebar() {
|
|
|
43
43
|
{FILLS.map((fill) => <button key={fill.value} type="button" onClick={() => setFillStyle(fill.value)} className={`flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs transition ${fillStyle === fill.value ? 'bg-accent text-white' : 'text-text-secondary hover:bg-surface-hover'}`}><span className="h-1.5 w-1.5 rounded-full bg-current" />{fill.label}</button>)}
|
|
44
44
|
</div>
|
|
45
45
|
</ToolbarButton>
|
|
46
|
-
<div className="max-w-[185px] px-2 text-[10px] leading-4 text-text-dim">
|
|
46
|
+
<div className="max-w-[185px] px-2 text-[10px] leading-4 text-text-dim">Fill rectangles, circles, frames, and closed freehand shapes. Lines and arrows are left unchanged.</div>
|
|
47
47
|
</ShapeSidebar>
|
|
48
48
|
)
|
|
49
49
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
+
import { DEFAULT_CANVAS_BACKGROUNDS, canvasBackgroundStorageKey, readCanvasBackground } from '../utils/canvasBackgrounds'
|
|
2
3
|
|
|
3
4
|
const GRID_STORAGE_KEY = 'lixsketch-grid-enabled'
|
|
4
5
|
const RULER_STORAGE_KEY = 'lixsketch-rulers-enabled'
|
|
6
|
+
const RULER_UNIT_STORAGE_KEY = 'lixsketch-ruler-unit'
|
|
5
7
|
|
|
6
8
|
// Tool enum replaces 15 boolean flags
|
|
7
9
|
export const TOOLS = {
|
|
@@ -193,13 +195,22 @@ const useSketchStore = create((set, get) => ({
|
|
|
193
195
|
setPanStart: (p) => set({ panStart: p }),
|
|
194
196
|
|
|
195
197
|
// --- Canvas background ---
|
|
196
|
-
canvasBackground:
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
canvasBackground: DEFAULT_CANVAS_BACKGROUNDS.dark,
|
|
199
|
+
canvasBackgrounds: { ...DEFAULT_CANVAS_BACKGROUNDS },
|
|
200
|
+
setCanvasBackground: (color, theme = 'dark') => {
|
|
201
|
+
const resolved = theme === 'light' ? 'light' : 'dark'
|
|
202
|
+
set((state) => ({ canvasBackground: color, canvasBackgrounds: { ...state.canvasBackgrounds, [resolved]: color } }))
|
|
199
203
|
if (typeof window !== 'undefined') {
|
|
204
|
+
try { localStorage.setItem(canvasBackgroundStorageKey(resolved), color) } catch {}
|
|
200
205
|
requestAnimationFrame(() => window.__adaptCanvasContrast?.(color))
|
|
201
206
|
}
|
|
202
207
|
},
|
|
208
|
+
restoreCanvasBackground: (theme) => {
|
|
209
|
+
const resolved = theme === 'light' ? 'light' : 'dark'
|
|
210
|
+
const color = readCanvasBackground(resolved, typeof window === 'undefined' ? null : localStorage)
|
|
211
|
+
set((state) => ({ canvasBackground: color, canvasBackgrounds: { ...state.canvasBackgrounds, [resolved]: color } }))
|
|
212
|
+
if (typeof window !== 'undefined') requestAnimationFrame(() => window.__adaptCanvasContrast?.(color))
|
|
213
|
+
},
|
|
203
214
|
|
|
204
215
|
// --- Grid ---
|
|
205
216
|
gridEnabled: false,
|
|
@@ -222,6 +233,7 @@ const useSketchStore = create((set, get) => ({
|
|
|
222
233
|
|
|
223
234
|
// --- Rulers ---
|
|
224
235
|
rulersEnabled: false,
|
|
236
|
+
rulerUnit: 'px',
|
|
225
237
|
toggleRulers: () => set((s) => {
|
|
226
238
|
const rulersEnabled = !s.rulersEnabled
|
|
227
239
|
if (typeof window !== 'undefined') {
|
|
@@ -229,6 +241,13 @@ const useSketchStore = create((set, get) => ({
|
|
|
229
241
|
}
|
|
230
242
|
return { rulersEnabled }
|
|
231
243
|
}),
|
|
244
|
+
setRulerUnit: (unit) => {
|
|
245
|
+
const rulerUnit = ['px', 'cm', 'in'].includes(unit) ? unit : 'px'
|
|
246
|
+
if (typeof window !== 'undefined') {
|
|
247
|
+
try { localStorage.setItem(RULER_UNIT_STORAGE_KEY, rulerUnit) } catch {}
|
|
248
|
+
}
|
|
249
|
+
set({ rulerUnit })
|
|
250
|
+
},
|
|
232
251
|
hydrateRulers: () => {
|
|
233
252
|
if (typeof window === 'undefined') return
|
|
234
253
|
try {
|
|
@@ -236,6 +255,8 @@ const useSketchStore = create((set, get) => ({
|
|
|
236
255
|
if (saved === 'true' || saved === 'false') {
|
|
237
256
|
set({ rulersEnabled: saved === 'true' })
|
|
238
257
|
}
|
|
258
|
+
const savedUnit = localStorage.getItem(RULER_UNIT_STORAGE_KEY)
|
|
259
|
+
if (['px', 'cm', 'in'].includes(savedUnit)) set({ rulerUnit: savedUnit })
|
|
239
260
|
} catch {}
|
|
240
261
|
},
|
|
241
262
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
+
import useSketchStore from './useSketchStore'
|
|
2
3
|
|
|
3
4
|
export const MAX_WORKSPACE_NAME_LENGTH = 20
|
|
4
5
|
|
|
@@ -111,6 +112,20 @@ function invertShapeColors(prevResolved, nextResolved) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
function readStoredTheme() {
|
|
116
|
+
if (typeof window === 'undefined') return 'dark'
|
|
117
|
+
try {
|
|
118
|
+
const theme = localStorage.getItem('theme')
|
|
119
|
+
if (theme && ['dark', 'light', 'system'].includes(theme)) return theme
|
|
120
|
+
} catch {}
|
|
121
|
+
try {
|
|
122
|
+
const prefs = JSON.parse(localStorage.getItem('lix_ui_prefs') || '{}')
|
|
123
|
+
return ['dark', 'light', 'system'].includes(prefs.theme) ? prefs.theme : 'dark'
|
|
124
|
+
} catch {
|
|
125
|
+
return 'dark'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
function applyTheme(theme) {
|
|
115
130
|
// Issue #38 bug #1: scope theme to body classes so light mode only
|
|
116
131
|
// affects the canvas, not the landing/blog pages. Token values live
|
|
@@ -215,7 +230,7 @@ const useUIStore = create((set, get) => ({
|
|
|
215
230
|
setCanvasLoading: (loading, message) => set({ canvasLoading: loading, canvasLoadingMessage: message || 'Loading canvas...' }),
|
|
216
231
|
|
|
217
232
|
// --- Theme ---
|
|
218
|
-
theme:
|
|
233
|
+
theme: readStoredTheme(),
|
|
219
234
|
setTheme: (newTheme) => {
|
|
220
235
|
const prev = get().theme
|
|
221
236
|
const resolve = (t) => t === 'system'
|
|
@@ -224,6 +239,14 @@ const useUIStore = create((set, get) => ({
|
|
|
224
239
|
invertShapeColors(resolve(prev), resolve(newTheme))
|
|
225
240
|
applyTheme(newTheme)
|
|
226
241
|
set({ theme: newTheme })
|
|
242
|
+
useSketchStore.getState().restoreCanvasBackground(resolve(newTheme))
|
|
243
|
+
if (typeof window !== 'undefined') {
|
|
244
|
+
try {
|
|
245
|
+
localStorage.setItem('theme', newTheme)
|
|
246
|
+
const prefs = JSON.parse(localStorage.getItem('lix_ui_prefs') || '{}')
|
|
247
|
+
localStorage.setItem('lix_ui_prefs', JSON.stringify({ ...prefs, theme: newTheme }))
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
227
250
|
},
|
|
228
251
|
|
|
229
252
|
// --- Language / i18n ---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const CANVAS_BACKGROUNDS = {
|
|
2
|
+
light: [
|
|
3
|
+
{ color: '#FBE4DB', label: 'menu.canvasBg.white' },
|
|
4
|
+
{ color: '#F6DBC0', label: 'menu.canvasBg.cream' },
|
|
5
|
+
{ color: '#F5EED2', label: 'menu.canvasBg.paper' },
|
|
6
|
+
{ color: '#F5F2ED', label: 'menu.canvasBg.skyTint' },
|
|
7
|
+
{ color: '#F5DABF', label: 'menu.canvasBg.sageTint' },
|
|
8
|
+
],
|
|
9
|
+
dark: [
|
|
10
|
+
{ color: '#000000', label: 'menu.canvasBg.black' },
|
|
11
|
+
{ color: '#161718', label: 'menu.canvasBg.darkGray' },
|
|
12
|
+
{ color: '#15111f', label: 'menu.canvasBg.blueBlack' },
|
|
13
|
+
{ color: '#181605', label: 'menu.canvasBg.darkYellow' },
|
|
14
|
+
{ color: '#1B1615', label: 'menu.canvasBg.darkBrown' },
|
|
15
|
+
],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CANVAS_BACKGROUNDS = { dark: '#15111f', light: '#F5F2ED' }
|
|
19
|
+
export const canvasBackgroundStorageKey = (theme) => `lixsketch-canvas-background-${theme}`
|
|
20
|
+
|
|
21
|
+
export function readCanvasBackground(theme, storage) {
|
|
22
|
+
const resolved = theme === 'light' ? 'light' : 'dark'
|
|
23
|
+
try {
|
|
24
|
+
const stored = storage?.getItem(canvasBackgroundStorageKey(resolved))
|
|
25
|
+
const match = CANVAS_BACKGROUNDS[resolved].find(({ color }) => color.toLowerCase() === stored?.toLowerCase())
|
|
26
|
+
if (match) return match.color
|
|
27
|
+
} catch {}
|
|
28
|
+
return DEFAULT_CANVAS_BACKGROUNDS[resolved]
|
|
29
|
+
}
|
package/src/shapes/Circle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
|
|
3
|
+
import { canvasToLocal, localToCanvas } from '../core/CanvasSpace.js';
|
|
3
4
|
// Circle shape class - extracted from drawCircle.js
|
|
4
5
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
|
|
5
6
|
|
|
@@ -432,18 +433,17 @@ class Circle {
|
|
|
432
433
|
}
|
|
433
434
|
isNearAnchor(x, y) {
|
|
434
435
|
if (!this.isSelected) return null;
|
|
435
|
-
const
|
|
436
|
-
const
|
|
436
|
+
const zoom = window.currentZoom || 1;
|
|
437
|
+
const buffer = 10 / zoom; // Scale buffer by zoom level
|
|
438
|
+
const anchorSize = 10 / zoom; // Scale anchor size by zoom level
|
|
439
|
+
const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: 0, centerY: 0 };
|
|
437
440
|
|
|
438
441
|
// Iterate through anchors
|
|
439
442
|
for (let i = 0; i < this.anchors.length; i++) {
|
|
440
443
|
const anchor = this.anchors[i];
|
|
441
444
|
const anchorLocalX = parseFloat(anchor.getAttribute('x')) + anchorSize / 2;
|
|
442
445
|
const anchorLocalY = parseFloat(anchor.getAttribute('y')) + anchorSize / 2;
|
|
443
|
-
const
|
|
444
|
-
svgPoint.x = anchorLocalX;
|
|
445
|
-
svgPoint.y = anchorLocalY;
|
|
446
|
-
const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
|
|
446
|
+
const transformedPoint = localToCanvas({ x: anchorLocalX, y: anchorLocalY }, transform);
|
|
447
447
|
|
|
448
448
|
const anchorLeft = transformedPoint.x - anchorSize/2 - buffer;
|
|
449
449
|
const anchorRight = transformedPoint.x + anchorSize/2 + buffer;
|
|
@@ -459,12 +459,9 @@ class Circle {
|
|
|
459
459
|
if (this.rotationAnchor) {
|
|
460
460
|
const rotateAnchorLocalX = parseFloat(this.rotationAnchor.getAttribute('cx'));
|
|
461
461
|
const rotateAnchorLocalY = parseFloat(this.rotationAnchor.getAttribute('cy'));
|
|
462
|
-
const
|
|
463
|
-
svgPoint.x = rotateAnchorLocalX;
|
|
464
|
-
svgPoint.y = rotateAnchorLocalY;
|
|
465
|
-
const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
|
|
462
|
+
const transformedPoint = localToCanvas({ x: rotateAnchorLocalX, y: rotateAnchorLocalY }, transform);
|
|
466
463
|
|
|
467
|
-
const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r'))
|
|
464
|
+
const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r'));
|
|
468
465
|
const distSq = (x - transformedPoint.x)**2 + (y - transformedPoint.y)**2;
|
|
469
466
|
if (distSq <= (rotateAnchorRadius + buffer)**2) {
|
|
470
467
|
return { type: 'rotate' };
|
|
@@ -475,14 +472,10 @@ class Circle {
|
|
|
475
472
|
}
|
|
476
473
|
|
|
477
474
|
updatePosition(anchorIndex, newMouseX, newMouseY) {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const svgPoint = svg.createSVGPoint();
|
|
483
|
-
svgPoint.x = newMouseX;
|
|
484
|
-
svgPoint.y = newMouseY;
|
|
485
|
-
const transformedPoint = svgPoint.matrixTransform(inverseCTM);
|
|
475
|
+
const transformedPoint = canvasToLocal(
|
|
476
|
+
{ x: newMouseX, y: newMouseY },
|
|
477
|
+
{ x: this.x, y: this.y, rotation: this.rotation, centerX: 0, centerY: 0 },
|
|
478
|
+
);
|
|
486
479
|
|
|
487
480
|
const dx = transformedPoint.x;
|
|
488
481
|
const dy = transformedPoint.y;
|