@elixpo/lixsketch 5.6.4 → 5.6.6
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-P6G35CXO.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-P6G35CXO.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-M5B4EWHJ.js} +216 -63
- package/dist/react/EventDispatcher-M5B4EWHJ.js.map +7 -0
- package/dist/react/Frame-QEUEYITT.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-NHJE4YBY.js +10 -0
- package/dist/react/ImageShape-33VCMSDP.js +10 -0
- package/dist/react/Line-A6FE232Q.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-JEYTYXZQ.js} +120 -46
- package/dist/react/SceneSerializer-JEYTYXZQ.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-23MOLHEI.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-OPJON7TD.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-QVNRI5OB.js} +8 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-VH3PJPDM.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-2DZHO67H.js} +2 -2
- package/dist/react/{chunk-XOWNCYE4.js → chunk-5NHJJJBZ.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-6GPWSPJF.js} +9 -5
- package/dist/react/{chunk-K5RY4EV7.js.map → chunk-6GPWSPJF.js.map} +2 -2
- package/dist/react/{chunk-ASBC5G35.js → chunk-B6CYUZC4.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-DNFPTVIP.js} +2 -2
- package/dist/react/{chunk-XYUFEIZR.js → chunk-DWEFPKDO.js} +2 -2
- package/dist/react/{chunk-GQUZPON4.js → chunk-F56XUE5S.js} +4 -4
- package/dist/react/{chunk-7CM5XQDN.js → chunk-G3DJQY65.js} +20 -36
- package/dist/react/chunk-G3DJQY65.js.map +7 -0
- package/dist/react/{chunk-LJR7EVW2.js → chunk-JEGLUNBC.js} +3 -3
- package/dist/react/{chunk-6GPEWDRB.js → chunk-M7GAB24F.js} +52 -6
- package/dist/react/chunk-M7GAB24F.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-XFL4R5ZF.js → chunk-OK4ROGMM.js} +16 -35
- package/dist/react/chunk-OK4ROGMM.js.map +7 -0
- package/dist/react/{chunk-APTOPAOY.js → chunk-SSFEN5CY.js} +3 -3
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-UEL2G6D2.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-ULPIHUVC.js} +3 -3
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.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-EOURRJ55.js → chunk-XSE4HQ2D.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-2UODOBCV.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-F4RV7SAI.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-NTGA5BI6.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-L55EUB5C.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-KQQBLGR3.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-LDJ45AD7.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-CSAFD7PW.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-YPD2SIIZ.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-VQHMWEFJ.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 +12 -2
- package/src/core/UndoRedo.js +59 -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-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-QEUEYITT.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-NHJE4YBY.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-33VCMSDP.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-A6FE232Q.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-23MOLHEI.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-OPJON7TD.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-QVNRI5OB.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-VH3PJPDM.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-2DZHO67H.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-5NHJJJBZ.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-B6CYUZC4.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-DNFPTVIP.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-DWEFPKDO.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-F56XUE5S.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-JEGLUNBC.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-SSFEN5CY.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-UEL2G6D2.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-ULPIHUVC.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-XSE4HQ2D.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-2UODOBCV.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-F4RV7SAI.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-NTGA5BI6.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-L55EUB5C.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-KQQBLGR3.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-LDJ45AD7.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-CSAFD7PW.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-YPD2SIIZ.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-VQHMWEFJ.js.map} +0 -0
|
@@ -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;
|
package/src/shapes/Frame.js
CHANGED
|
@@ -101,6 +101,36 @@ class Frame {
|
|
|
101
101
|
let fillValue = "transparent";
|
|
102
102
|
if (this.fillStyle === 'solid') {
|
|
103
103
|
fillValue = this.fillColor;
|
|
104
|
+
} else if (['hachure', 'cross-hatch', 'dots'].includes(this.fillStyle)) {
|
|
105
|
+
const patternId = `frame-fill-${this.shapeID}`;
|
|
106
|
+
let pattern = defs.querySelector(`#${patternId}`);
|
|
107
|
+
if (!pattern) {
|
|
108
|
+
pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
|
|
109
|
+
pattern.setAttribute('id', patternId);
|
|
110
|
+
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
|
|
111
|
+
defs.appendChild(pattern);
|
|
112
|
+
}
|
|
113
|
+
pattern.setAttribute('width', 10);
|
|
114
|
+
pattern.setAttribute('height', 10);
|
|
115
|
+
while (pattern.firstChild) pattern.removeChild(pattern.firstChild);
|
|
116
|
+
if (this.fillStyle === 'dots') {
|
|
117
|
+
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
118
|
+
dot.setAttribute('cx', 5);
|
|
119
|
+
dot.setAttribute('cy', 5);
|
|
120
|
+
dot.setAttribute('r', 1.5);
|
|
121
|
+
dot.setAttribute('fill', this.fillColor);
|
|
122
|
+
pattern.appendChild(dot);
|
|
123
|
+
} else {
|
|
124
|
+
const hatch = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
125
|
+
hatch.setAttribute('d', this.fillStyle === 'cross-hatch'
|
|
126
|
+
? 'M-2 2L2-2M0 10L10 0M8 12L12 8M-2 8L2 12M0 0L10 10M8-2L12 2'
|
|
127
|
+
: 'M-2 2L2-2M0 10L10 0M8 12L12 8');
|
|
128
|
+
hatch.setAttribute('fill', 'none');
|
|
129
|
+
hatch.setAttribute('stroke', this.fillColor);
|
|
130
|
+
hatch.setAttribute('stroke-width', 1.5);
|
|
131
|
+
pattern.appendChild(hatch);
|
|
132
|
+
}
|
|
133
|
+
fillValue = `url(#${patternId})`;
|
|
104
134
|
} else if (this.fillStyle === 'grid') {
|
|
105
135
|
// Create or update a grid pattern unique to this frame
|
|
106
136
|
const patternId = `frame-grid-${this.shapeID}`;
|
|
@@ -426,6 +456,14 @@ move(dx, dy) {
|
|
|
426
456
|
this.updateClipPath();
|
|
427
457
|
}
|
|
428
458
|
|
|
459
|
+
finalizeMove() {
|
|
460
|
+
this.containedShapes.forEach(shape => {
|
|
461
|
+
if (shape && typeof shape.finalizeMove === 'function') {
|
|
462
|
+
shape.finalizeMove();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
429
467
|
handleResizeLegacy(anchorIndex, currentPos, startPos, initialFrame) {
|
|
430
468
|
// Legacy version - superseded by the handleResize below with contained shapes scaling
|
|
431
469
|
const dx = currentPos.x - startPos.x;
|
|
@@ -1409,75 +1447,6 @@ restoreToFrame(shape) {
|
|
|
1409
1447
|
this.draw();
|
|
1410
1448
|
}
|
|
1411
1449
|
|
|
1412
|
-
destroy() {
|
|
1413
|
-
const isDiagramFrame = !!this._diagramType;
|
|
1414
|
-
|
|
1415
|
-
[...this.containedShapes].forEach(shape => {
|
|
1416
|
-
if (isDiagramFrame) {
|
|
1417
|
-
// Diagram/graph frame: destroy contained shapes with the frame
|
|
1418
|
-
if (typeof window.cleanupAttachments === 'function') {
|
|
1419
|
-
window.cleanupAttachments(shape);
|
|
1420
|
-
}
|
|
1421
|
-
const el = shape.group || shape.element;
|
|
1422
|
-
if (el && el.parentNode) {
|
|
1423
|
-
el.parentNode.removeChild(el);
|
|
1424
|
-
}
|
|
1425
|
-
if (shape.shapeName === 'frame') {
|
|
1426
|
-
if (shape.clipGroup && shape.clipGroup.parentNode) {
|
|
1427
|
-
shape.clipGroup.parentNode.removeChild(shape.clipGroup);
|
|
1428
|
-
}
|
|
1429
|
-
if (shape.clipPath && shape.clipPath.parentNode) {
|
|
1430
|
-
shape.clipPath.parentNode.removeChild(shape.clipPath);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
const idx = shapes.indexOf(shape);
|
|
1434
|
-
if (idx > -1) {
|
|
1435
|
-
shapes.splice(idx, 1);
|
|
1436
|
-
}
|
|
1437
|
-
} else {
|
|
1438
|
-
// Regular frame: release contained shapes back to canvas
|
|
1439
|
-
const el = shape.group || shape.element;
|
|
1440
|
-
if (el) {
|
|
1441
|
-
if (el.parentNode === this.clipGroup) {
|
|
1442
|
-
this.clipGroup.removeChild(el);
|
|
1443
|
-
}
|
|
1444
|
-
svg.appendChild(el);
|
|
1445
|
-
}
|
|
1446
|
-
if (shape.shapeName === 'frame' && shape.clipGroup) {
|
|
1447
|
-
if (shape.clipGroup.parentNode === this.clipGroup) {
|
|
1448
|
-
this.clipGroup.removeChild(shape.clipGroup);
|
|
1449
|
-
}
|
|
1450
|
-
svg.appendChild(shape.clipGroup);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
shape.parentFrame = null;
|
|
1454
|
-
delete shape.isBeingMovedByFrame;
|
|
1455
|
-
});
|
|
1456
|
-
this.containedShapes = [];
|
|
1457
|
-
|
|
1458
|
-
// Remove clip path
|
|
1459
|
-
if (this.clipPath && this.clipPath.parentNode) {
|
|
1460
|
-
this.clipPath.parentNode.removeChild(this.clipPath);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Remove groups (clipGroup should be empty now)
|
|
1464
|
-
if (this.clipGroup && this.clipGroup.parentNode) {
|
|
1465
|
-
this.clipGroup.parentNode.removeChild(this.clipGroup);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
if (this.group && this.group.parentNode) {
|
|
1469
|
-
this.group.parentNode.removeChild(this.group);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const index = shapes.indexOf(this);
|
|
1473
|
-
if (index > -1) {
|
|
1474
|
-
shapes.splice(index, 1);
|
|
1475
|
-
}
|
|
1476
|
-
if (currentShape === this) {
|
|
1477
|
-
currentShape = null;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
1450
|
/**
|
|
1482
1451
|
* Set a background image for this frame from a URL.
|
|
1483
1452
|
* The image will fit/cover the frame dimensions.
|
|
@@ -69,6 +69,8 @@ class FreehandStroke {
|
|
|
69
69
|
this.selectionOutline = null;
|
|
70
70
|
this.boundingBox = { x: 0, y: 0, width: 0, height: 0 };
|
|
71
71
|
this.shapeName = "freehandStroke";
|
|
72
|
+
this.shapeID = `freehand-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
|
|
73
|
+
this.group.setAttribute('id', this.shapeID);
|
|
72
74
|
this._moveOffsetX = 0;
|
|
73
75
|
this._moveOffsetY = 0;
|
|
74
76
|
|
|
@@ -304,7 +306,16 @@ class FreehandStroke {
|
|
|
304
306
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
305
307
|
const pathData = this.getPathData();
|
|
306
308
|
path.setAttribute('d', pathData);
|
|
307
|
-
|
|
309
|
+
const closedFillStyle = this.options.fillStyle || 'solid';
|
|
310
|
+
const closedFill = closedFillStyle === 'none'
|
|
311
|
+
? 'transparent'
|
|
312
|
+
: (this.options.fill || this.options.stroke);
|
|
313
|
+
const patternedFill = this.options.closedFill
|
|
314
|
+
&& !['none', 'solid'].includes(closedFillStyle)
|
|
315
|
+
&& typeof rough !== 'undefined';
|
|
316
|
+
path.setAttribute('fill', this.options.closedFill
|
|
317
|
+
? (patternedFill ? 'transparent' : closedFill)
|
|
318
|
+
: this.options.stroke);
|
|
308
319
|
path.setAttribute('fill-opacity', this.options.strokeOpacity);
|
|
309
320
|
path.setAttribute('stroke', this.options.closedFill ? (this.options.outlineStroke || 'none') : 'none');
|
|
310
321
|
if (this.options.closedFill) {
|
|
@@ -330,6 +341,18 @@ class FreehandStroke {
|
|
|
330
341
|
this.group.appendChild(overlay);
|
|
331
342
|
}
|
|
332
343
|
|
|
344
|
+
if (patternedFill) {
|
|
345
|
+
const fillGroup = rough.svg(svg).path(pathData, {
|
|
346
|
+
stroke: 'none',
|
|
347
|
+
fill: closedFill,
|
|
348
|
+
fillStyle: closedFillStyle,
|
|
349
|
+
fillWeight: Math.max(1, (this.options.strokeWidth || 2) / 2),
|
|
350
|
+
roughness: 0.8,
|
|
351
|
+
});
|
|
352
|
+
fillGroup.setAttribute('data-freehand-fill-pattern', closedFillStyle);
|
|
353
|
+
this.group.appendChild(fillGroup);
|
|
354
|
+
}
|
|
355
|
+
|
|
333
356
|
this.element = path;
|
|
334
357
|
this.group.appendChild(path);
|
|
335
358
|
|
package/src/shapes/Rectangle.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
|
// Rectangle shape class - extracted from drawSquare.js
|
|
4
5
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
|
|
5
6
|
|
|
@@ -640,18 +641,17 @@ class Rectangle {
|
|
|
640
641
|
// Helper to check if a point is near an anchor
|
|
641
642
|
isNearAnchor(x, y) {
|
|
642
643
|
if (!this.isSelected) return null;
|
|
643
|
-
const
|
|
644
|
-
const
|
|
644
|
+
const zoom = window.currentZoom || 1;
|
|
645
|
+
const buffer = 10 / zoom;
|
|
646
|
+
const anchorSize = 10 / zoom;
|
|
647
|
+
const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: this.width / 2, centerY: this.height / 2 };
|
|
645
648
|
|
|
646
649
|
// Iterate through anchors
|
|
647
650
|
for (let i = 0; i < this.anchors.length; i++) {
|
|
648
651
|
const anchor = this.anchors[i];
|
|
649
652
|
const anchorLocalX = parseFloat(anchor.getAttribute('x')) + anchorSize / 2;
|
|
650
653
|
const anchorLocalY = parseFloat(anchor.getAttribute('y')) + anchorSize / 2;
|
|
651
|
-
const
|
|
652
|
-
svgPoint.x = anchorLocalX;
|
|
653
|
-
svgPoint.y = anchorLocalY;
|
|
654
|
-
const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
|
|
654
|
+
const transformedPoint = localToCanvas({ x: anchorLocalX, y: anchorLocalY }, transform);
|
|
655
655
|
const anchorLeft = transformedPoint.x - anchorSize/2 - buffer;
|
|
656
656
|
const anchorRight = transformedPoint.x + anchorSize/2 + buffer;
|
|
657
657
|
const anchorTop = transformedPoint.y - anchorSize/2 - buffer;
|
|
@@ -665,10 +665,7 @@ class Rectangle {
|
|
|
665
665
|
if (this.rotationAnchor) {
|
|
666
666
|
const rotateAnchorLocalX = parseFloat(this.rotationAnchor.getAttribute('cx'));
|
|
667
667
|
const rotateAnchorLocalY = parseFloat(this.rotationAnchor.getAttribute('cy'));
|
|
668
|
-
const
|
|
669
|
-
svgPoint.x = rotateAnchorLocalX;
|
|
670
|
-
svgPoint.y = rotateAnchorLocalY;
|
|
671
|
-
const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
|
|
668
|
+
const transformedPoint = localToCanvas({ x: rotateAnchorLocalX, y: rotateAnchorLocalY }, transform);
|
|
672
669
|
const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r'));
|
|
673
670
|
const distSq = (x - transformedPoint.x)**2 + (y - transformedPoint.y)**2;
|
|
674
671
|
if (distSq <= (rotateAnchorRadius + buffer)**2) {
|
|
@@ -729,16 +726,10 @@ updateFrameContainment() {
|
|
|
729
726
|
}
|
|
730
727
|
|
|
731
728
|
updatePosition(anchorIndex, newMouseX, newMouseY) {
|
|
732
|
-
const CTM = this.group.getCTM();
|
|
733
|
-
if (!CTM) return;
|
|
734
|
-
const inverseCTM = CTM.inverse();
|
|
735
|
-
const svgPoint = svg.createSVGPoint();
|
|
736
|
-
svgPoint.x = newMouseX;
|
|
737
|
-
svgPoint.y = newMouseY;
|
|
738
|
-
const localMouse = svgPoint.matrixTransform(inverseCTM);
|
|
739
|
-
|
|
740
729
|
const oldW = this.width;
|
|
741
730
|
const oldH = this.height;
|
|
731
|
+
const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: oldW / 2, centerY: oldH / 2 };
|
|
732
|
+
const localMouse = canvasToLocal({ x: newMouseX, y: newMouseY }, transform);
|
|
742
733
|
|
|
743
734
|
// Determine fixed point in old local coords and compute raw new dimensions
|
|
744
735
|
let fixedOldX, fixedOldY, rawW, rawH;
|
|
@@ -754,10 +745,7 @@ updateFrameContainment() {
|
|
|
754
745
|
}
|
|
755
746
|
|
|
756
747
|
// Get world position of the fixed point using current CTM
|
|
757
|
-
const
|
|
758
|
-
fp.x = fixedOldX;
|
|
759
|
-
fp.y = fixedOldY;
|
|
760
|
-
const fixedWorld = fp.matrixTransform(CTM);
|
|
748
|
+
const fixedWorld = localToCanvas({ x: fixedOldX, y: fixedOldY }, transform);
|
|
761
749
|
|
|
762
750
|
// Handle negative dimensions (dragged past opposite edge)
|
|
763
751
|
const newW = Math.abs(rawW);
|