editor: add grid + snap toggles
This commit is contained in:
parent
a9e6d4b0d0
commit
2edcb214ad
@ -14,6 +14,7 @@ export interface CanvasProps {
|
|||||||
scale: number;
|
scale: number;
|
||||||
panX: number;
|
panX: number;
|
||||||
panY: number;
|
panY: number;
|
||||||
|
gridEnabled: boolean;
|
||||||
guides: { xs: number[]; ys: number[] };
|
guides: { xs: number[]; ys: number[] };
|
||||||
onSelectSingle(id?: string): void;
|
onSelectSingle(id?: string): void;
|
||||||
onToggleSelect(id: string): void;
|
onToggleSelect(id: string): void;
|
||||||
@ -276,6 +277,19 @@ export function Canvas(props: CanvasProps) {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{props.gridEnabled ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to right, rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.06) 1px, transparent 1px)',
|
||||||
|
backgroundSize: '20px 20px',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* guides (snap lines) */}
|
{/* guides (snap lines) */}
|
||||||
{props.guides.xs.map((x) => (
|
{props.guides.xs.map((x) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -213,6 +213,31 @@ export function EditorApp() {
|
|||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type={state.canvas.gridEnabled ? 'default' : 'text'}
|
||||||
|
onClick={() => dispatch({ type: 'toggleGrid' })}
|
||||||
|
style={
|
||||||
|
state.canvas.gridEnabled
|
||||||
|
? { background: 'rgba(59,130,246,0.16)', color: '#e2e8f0', border: '1px solid rgba(59,130,246,0.35)' }
|
||||||
|
: { color: '#94a3b8' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Grid
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type={state.canvas.snapEnabled ? 'default' : 'text'}
|
||||||
|
onClick={() => dispatch({ type: 'toggleSnap' })}
|
||||||
|
style={
|
||||||
|
state.canvas.snapEnabled
|
||||||
|
? { background: 'rgba(34,197,94,0.12)', color: '#e2e8f0', border: '1px solid rgba(34,197,94,0.30)' }
|
||||||
|
: { color: '#94a3b8' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Snap
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" style={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
||||||
<Button size="small" style={{ background: 'rgba(255,255,255,0.06)', color: '#e2e8f0', border: 'none' }}>
|
<Button size="small" style={{ background: 'rgba(255,255,255,0.06)', color: '#e2e8f0', border: 'none' }}>
|
||||||
Preview
|
Preview
|
||||||
</Button>
|
</Button>
|
||||||
@ -446,6 +471,7 @@ export function EditorApp() {
|
|||||||
scale={state.canvas.scale}
|
scale={state.canvas.scale}
|
||||||
panX={state.canvas.panX}
|
panX={state.canvas.panX}
|
||||||
panY={state.canvas.panY}
|
panY={state.canvas.panY}
|
||||||
|
gridEnabled={state.canvas.gridEnabled}
|
||||||
guides={state.canvas.guides}
|
guides={state.canvas.guides}
|
||||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||||
onToggleSelect={(id) => dispatch({ type: 'toggleSelect', id })}
|
onToggleSelect={(id) => dispatch({ type: 'toggleSelect', id })}
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export type EditorAction =
|
|||||||
| { type: 'setScale'; scale: number }
|
| { type: 'setScale'; scale: number }
|
||||||
| { type: 'panBy'; dx: number; dy: number }
|
| { type: 'panBy'; dx: number; dy: number }
|
||||||
| { type: 'zoomAt'; scale: number; anchor: { x: number; y: number } }
|
| { type: 'zoomAt'; scale: number; anchor: { x: number; y: number } }
|
||||||
|
| { type: 'toggleGrid' }
|
||||||
|
| { type: 'toggleSnap' }
|
||||||
| { type: 'beginPan'; start: { screenX: number; screenY: number } }
|
| { type: 'beginPan'; start: { screenX: number; screenY: number } }
|
||||||
| { type: 'updatePan'; current: { screenX: number; screenY: number } }
|
| { type: 'updatePan'; current: { screenX: number; screenY: number } }
|
||||||
| { type: 'endPan' }
|
| { type: 'endPan' }
|
||||||
@ -271,6 +273,8 @@ export function createInitialState(): EditorRuntimeState {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
panX: 0,
|
panX: 0,
|
||||||
panY: 0,
|
panY: 0,
|
||||||
|
gridEnabled: false,
|
||||||
|
snapEnabled: true,
|
||||||
guides: { xs: [], ys: [] },
|
guides: { xs: [], ys: [] },
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isPanning: false,
|
isPanning: false,
|
||||||
@ -366,6 +370,28 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'toggleGrid': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
canvas: {
|
||||||
|
...state.canvas,
|
||||||
|
gridEnabled: !state.canvas.gridEnabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'toggleSnap': {
|
||||||
|
// Clearing guides prevents stale guide lines when turning snap off mid-drag.
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
canvas: {
|
||||||
|
...state.canvas,
|
||||||
|
snapEnabled: !state.canvas.snapEnabled,
|
||||||
|
guides: { xs: [], ys: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'updateWidgetRect': {
|
case 'updateWidgetRect': {
|
||||||
const target = state.doc.screen.nodes.find((n) => n.id === action.id);
|
const target = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||||
if (!target) return state;
|
if (!target) return state;
|
||||||
@ -874,8 +900,8 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
|
|
||||||
const bounds = action.bounds;
|
const bounds = action.bounds;
|
||||||
|
|
||||||
// goView-like: only snap when a single node is selected.
|
// goView-like: only snap when enabled and a single node is selected.
|
||||||
const canSnap = state.selection.ids.length === 1;
|
const canSnap = state.canvas.snapEnabled && state.selection.ids.length === 1;
|
||||||
const movingId = state.selection.ids[0];
|
const movingId = state.selection.ids[0];
|
||||||
|
|
||||||
const others = canSnap
|
const others = canSnap
|
||||||
@ -1058,8 +1084,8 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
h: Math.max(0, newH),
|
h: Math.max(0, newH),
|
||||||
};
|
};
|
||||||
|
|
||||||
// goView-like: only snap when resizing a single selected node.
|
// goView-like: only snap when enabled and resizing a single selected node.
|
||||||
const canSnap = state.selection.ids.length === 1;
|
const canSnap = state.canvas.snapEnabled && state.selection.ids.length === 1;
|
||||||
const movingId = drag.targetId;
|
const movingId = drag.targetId;
|
||||||
|
|
||||||
const others = canSnap
|
const others = canSnap
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export interface EditorCanvasState {
|
|||||||
scale: number;
|
scale: number;
|
||||||
panX: number;
|
panX: number;
|
||||||
panY: number;
|
panY: number;
|
||||||
|
gridEnabled: boolean;
|
||||||
|
snapEnabled: boolean;
|
||||||
guides: {
|
guides: {
|
||||||
xs: number[];
|
xs: number[];
|
||||||
ys: number[];
|
ys: number[];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user