editor: add grid + snap toggles

This commit is contained in:
clawdbot 2026-01-29 08:40:40 +08:00
parent a9e6d4b0d0
commit 2edcb214ad
4 changed files with 72 additions and 4 deletions

View File

@ -14,6 +14,7 @@ export interface CanvasProps {
scale: number;
panX: number;
panY: number;
gridEnabled: boolean;
guides: { xs: number[]; ys: number[] };
onSelectSingle(id?: string): void;
onToggleSelect(id: string): void;
@ -276,6 +277,19 @@ export function Canvas(props: CanvasProps) {
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) */}
{props.guides.xs.map((x) => (
<div

View File

@ -213,6 +213,31 @@ export function EditorApp() {
</Space>
<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' }}>
Preview
</Button>
@ -446,6 +471,7 @@ export function EditorApp() {
scale={state.canvas.scale}
panX={state.canvas.panX}
panY={state.canvas.panY}
gridEnabled={state.canvas.gridEnabled}
guides={state.canvas.guides}
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
onToggleSelect={(id) => dispatch({ type: 'toggleSelect', id })}

View File

@ -34,6 +34,8 @@ export type EditorAction =
| { type: 'setScale'; scale: number }
| { type: 'panBy'; dx: number; dy: number }
| { type: 'zoomAt'; scale: number; anchor: { x: number; y: number } }
| { type: 'toggleGrid' }
| { type: 'toggleSnap' }
| { type: 'beginPan'; start: { screenX: number; screenY: number } }
| { type: 'updatePan'; current: { screenX: number; screenY: number } }
| { type: 'endPan' }
@ -271,6 +273,8 @@ export function createInitialState(): EditorRuntimeState {
scale: 1,
panX: 0,
panY: 0,
gridEnabled: false,
snapEnabled: true,
guides: { xs: [], ys: [] },
isDragging: 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': {
const target = state.doc.screen.nodes.find((n) => n.id === action.id);
if (!target) return state;
@ -874,8 +900,8 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
const bounds = action.bounds;
// goView-like: only snap when a single node is selected.
const canSnap = state.selection.ids.length === 1;
// goView-like: only snap when enabled and a single node is selected.
const canSnap = state.canvas.snapEnabled && state.selection.ids.length === 1;
const movingId = state.selection.ids[0];
const others = canSnap
@ -1058,8 +1084,8 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
h: Math.max(0, newH),
};
// goView-like: only snap when resizing a single selected node.
const canSnap = state.selection.ids.length === 1;
// goView-like: only snap when enabled and resizing a single selected node.
const canSnap = state.canvas.snapEnabled && state.selection.ids.length === 1;
const movingId = drag.targetId;
const others = canSnap

View File

@ -16,6 +16,8 @@ export interface EditorCanvasState {
scale: number;
panX: number;
panY: number;
gridEnabled: boolean;
snapEnabled: boolean;
guides: {
xs: number[];
ys: number[];