Compare commits
No commits in common. "e4cca026a0761b759677ba26c9f3951e19c408ca" and "6b18665ef93f26641099f181c52393b1ce18dba3" have entirely different histories.
e4cca026a0
...
6b18665ef9
@ -29,19 +29,14 @@ export function ContextMenu(props: {
|
||||
state: ContextMenuState | null;
|
||||
nodes: WidgetNode[];
|
||||
selectionIds: string[];
|
||||
clipboardHasNodes: boolean;
|
||||
onClose: () => void;
|
||||
onAddTextAt: (x: number, y: number) => void;
|
||||
onSelectSingle?: (id?: string) => void;
|
||||
onSelectAll?: () => void;
|
||||
onCopySelected?: () => void;
|
||||
onPasteAt?: (x: number, y: number) => void;
|
||||
onDuplicateSelected?: () => void;
|
||||
onToggleLockSelected?: () => void;
|
||||
onToggleHideSelected?: () => void;
|
||||
onBringToFrontSelected?: () => void;
|
||||
onBringForwardSelected?: () => void;
|
||||
onSendBackwardSelected?: () => void;
|
||||
onSendToBackSelected?: () => void;
|
||||
onDeleteSelected?: () => void;
|
||||
}) {
|
||||
@ -199,24 +194,6 @@ export function ContextMenu(props: {
|
||||
|
||||
<div style={{ height: 1, margin: '6px 0', background: 'rgba(255,255,255,0.08)' }} />
|
||||
|
||||
<MenuItem
|
||||
label="Copy"
|
||||
disabled={!hasSelection || !props.onCopySelected}
|
||||
onClick={() => {
|
||||
props.onCopySelected?.();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
label="Paste"
|
||||
disabled={!props.clipboardHasNodes || !props.onPasteAt}
|
||||
onClick={() => {
|
||||
props.onPasteAt?.(ctx.worldX, ctx.worldY);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
label="Duplicate"
|
||||
disabled={!canModifySelection || !props.onDuplicateSelected}
|
||||
@ -265,22 +242,6 @@ export function ContextMenu(props: {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Bring Forward"
|
||||
disabled={!canModifySelection || !props.onBringForwardSelected}
|
||||
onClick={() => {
|
||||
props.onBringForwardSelected?.();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Send Backward"
|
||||
disabled={!canModifySelection || !props.onSendBackwardSelected}
|
||||
onClick={() => {
|
||||
props.onSendBackwardSelected?.();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Send To Back"
|
||||
disabled={!canModifySelection || !props.onSendToBackSelected}
|
||||
|
||||
@ -502,60 +502,14 @@ export function EditorApp() {
|
||||
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>尺寸</Typography.Text>
|
||||
<Space size={8}>
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={selected?.rect.w ?? 0}
|
||||
controls={false}
|
||||
min={1}
|
||||
disabled={!selected || selected.locked}
|
||||
onChange={(v) => {
|
||||
if (!selected) return;
|
||||
if (typeof v !== 'number') return;
|
||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { w: v } });
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={selected?.rect.h ?? 0}
|
||||
controls={false}
|
||||
min={1}
|
||||
disabled={!selected || selected.locked}
|
||||
onChange={(v) => {
|
||||
if (!selected) return;
|
||||
if (typeof v !== 'number') return;
|
||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { h: v } });
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<InputNumber size="small" value={selected?.rect.w ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||
<InputNumber size="small" value={selected?.rect.h ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||
</Space>
|
||||
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>位置</Typography.Text>
|
||||
<Space size={8}>
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={selected?.rect.x ?? 0}
|
||||
controls={false}
|
||||
disabled={!selected || selected.locked}
|
||||
onChange={(v) => {
|
||||
if (!selected) return;
|
||||
if (typeof v !== 'number') return;
|
||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { x: v } });
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={selected?.rect.y ?? 0}
|
||||
controls={false}
|
||||
disabled={!selected || selected.locked}
|
||||
onChange={(v) => {
|
||||
if (!selected) return;
|
||||
if (typeof v !== 'number') return;
|
||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { y: v } });
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<InputNumber size="small" value={selected?.rect.x ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||
<InputNumber size="small" value={selected?.rect.y ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@ -682,19 +636,14 @@ export function EditorApp() {
|
||||
state={ctxMenu}
|
||||
nodes={state.doc.screen.nodes}
|
||||
selectionIds={state.selection.ids}
|
||||
clipboardHasNodes={state.clipboard.nodes.length > 0}
|
||||
onClose={closeContextMenu}
|
||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||
onSelectAll={() => dispatch({ type: 'selectAll' })}
|
||||
onCopySelected={() => dispatchWithMenuSelection({ type: 'copySelected' })}
|
||||
onPasteAt={(x, y) => dispatch({ type: 'pasteClipboard', at: { x, y } })}
|
||||
onDuplicateSelected={() => dispatchWithMenuSelection({ type: 'duplicateSelected' })}
|
||||
onToggleLockSelected={() => dispatchWithMenuSelection({ type: 'toggleLockSelected' })}
|
||||
onToggleHideSelected={() => dispatchWithMenuSelection({ type: 'toggleHideSelected' })}
|
||||
onBringToFrontSelected={() => dispatchWithMenuSelection({ type: 'bringToFrontSelected' })}
|
||||
onBringForwardSelected={() => dispatchWithMenuSelection({ type: 'bringForwardSelected' })}
|
||||
onSendBackwardSelected={() => dispatchWithMenuSelection({ type: 'sendBackwardSelected' })}
|
||||
onSendToBackSelected={() => dispatchWithMenuSelection({ type: 'sendToBackSelected' })}
|
||||
onDeleteSelected={() => dispatchWithMenuSelection({ type: 'deleteSelected' })}
|
||||
/>
|
||||
|
||||
@ -59,15 +59,10 @@ export type EditorAction =
|
||||
| { type: 'deleteSelected' }
|
||||
| { type: 'nudgeSelected'; dx: number; dy: number }
|
||||
| { type: 'duplicateSelected' }
|
||||
| { type: 'copySelected' }
|
||||
| { type: 'pasteClipboard'; at?: { x: number; y: number } }
|
||||
| { type: 'toggleLockSelected' }
|
||||
| { type: 'toggleHideSelected' }
|
||||
| { type: 'bringToFrontSelected' }
|
||||
| { type: 'bringForwardSelected' }
|
||||
| { type: 'sendBackwardSelected' }
|
||||
| { type: 'sendToBackSelected' }
|
||||
| { type: 'updateWidgetRect'; id: string; rect: Partial<Rect> }
|
||||
| UpdateWidgetPropsAction;
|
||||
|
||||
interface DragSession {
|
||||
@ -160,66 +155,6 @@ function ensureSelected(state: EditorRuntimeState, id: string): EditorRuntimeSta
|
||||
return { ...state, selection: { ids: [id] } };
|
||||
}
|
||||
|
||||
function sortNodesBackToFront(nodes: WidgetNode[]): WidgetNode[] {
|
||||
return nodes
|
||||
.map((n, index) => ({ n, index, z: n.zIndex ?? index }))
|
||||
.sort((a, b) => {
|
||||
if (a.z !== b.z) return a.z - b.z;
|
||||
return a.index - b.index;
|
||||
})
|
||||
.map((e) => e.n);
|
||||
}
|
||||
|
||||
function normalizeZIndexBackToFront(nodesBackToFront: WidgetNode[]): WidgetNode[] {
|
||||
return nodesBackToFront.map((n, idx) => ({ ...n, zIndex: idx }));
|
||||
}
|
||||
|
||||
function reorderSelected(
|
||||
nodes: WidgetNode[],
|
||||
selectedIds: Set<string>,
|
||||
mode: 'toFront' | 'toBack' | 'forward' | 'backward',
|
||||
): WidgetNode[] {
|
||||
const ordered = sortNodesBackToFront(nodes);
|
||||
|
||||
const isSelected = (n: WidgetNode) => selectedIds.has(n.id);
|
||||
|
||||
if (mode === 'toFront') {
|
||||
const rest = ordered.filter((n) => !isSelected(n));
|
||||
const picked = ordered.filter((n) => isSelected(n));
|
||||
return normalizeZIndexBackToFront([...rest, ...picked]);
|
||||
}
|
||||
|
||||
if (mode === 'toBack') {
|
||||
const picked = ordered.filter((n) => isSelected(n));
|
||||
const rest = ordered.filter((n) => !isSelected(n));
|
||||
return normalizeZIndexBackToFront([...picked, ...rest]);
|
||||
}
|
||||
|
||||
const arr = [...ordered];
|
||||
|
||||
if (mode === 'forward') {
|
||||
// swap with next non-selected, from front backwards
|
||||
for (let i = arr.length - 2; i >= 0; i--) {
|
||||
if (!isSelected(arr[i]!)) continue;
|
||||
if (isSelected(arr[i + 1]!)) continue;
|
||||
const tmp = arr[i]!;
|
||||
arr[i] = arr[i + 1]!;
|
||||
arr[i + 1] = tmp;
|
||||
}
|
||||
return normalizeZIndexBackToFront(arr);
|
||||
}
|
||||
|
||||
// mode === 'backward'
|
||||
for (let i = 1; i < arr.length; i++) {
|
||||
if (!isSelected(arr[i]!)) continue;
|
||||
if (isSelected(arr[i - 1]!)) continue;
|
||||
const tmp = arr[i]!;
|
||||
arr[i] = arr[i - 1]!;
|
||||
arr[i - 1] = tmp;
|
||||
}
|
||||
return normalizeZIndexBackToFront(arr);
|
||||
}
|
||||
|
||||
export function createInitialState(): EditorRuntimeState {
|
||||
const screen = createEmptyScreen({
|
||||
width: 1920,
|
||||
@ -258,7 +193,6 @@ export function createInitialState(): EditorRuntimeState {
|
||||
},
|
||||
},
|
||||
keyboard: { ctrl: false, space: false },
|
||||
clipboard: { nodes: [] },
|
||||
history: { past: [], future: [] },
|
||||
};
|
||||
}
|
||||
@ -337,41 +271,6 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateWidgetRect': {
|
||||
const target = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!target) return state;
|
||||
if (target.locked) return state;
|
||||
|
||||
const next: Rect = {
|
||||
x: typeof action.rect.x === 'number' ? action.rect.x : target.rect.x,
|
||||
y: typeof action.rect.y === 'number' ? action.rect.y : target.rect.y,
|
||||
w: typeof action.rect.w === 'number' ? action.rect.w : target.rect.w,
|
||||
h: typeof action.rect.h === 'number' ? action.rect.h : target.rect.h,
|
||||
};
|
||||
|
||||
const bounds = { w: state.doc.screen.width, h: state.doc.screen.height };
|
||||
const rect = clampRectToBounds(
|
||||
{
|
||||
x: Math.round(next.x),
|
||||
y: Math.round(next.y),
|
||||
w: Math.round(next.w),
|
||||
h: Math.round(next.h),
|
||||
},
|
||||
bounds,
|
||||
50,
|
||||
);
|
||||
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => (n.id === action.id ? { ...n, rect } : n)),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateWidgetProps': {
|
||||
return updateWidgetProps(state, action);
|
||||
}
|
||||
@ -441,56 +340,6 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
};
|
||||
}
|
||||
|
||||
case 'copySelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
const nodes = state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
||||
if (!nodes.length) return state;
|
||||
return {
|
||||
...state,
|
||||
clipboard: {
|
||||
nodes: nodes.map((n) => ({ ...n, rect: { ...n.rect } })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'pasteClipboard': {
|
||||
const clip = state.clipboard.nodes;
|
||||
if (!clip.length) return state;
|
||||
|
||||
// Compute bounds of clipboard content.
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
|
||||
for (const n of clip) {
|
||||
minX = Math.min(minX, n.rect.x);
|
||||
minY = Math.min(minY, n.rect.y);
|
||||
}
|
||||
|
||||
const bounds = { w: state.doc.screen.width, h: state.doc.screen.height };
|
||||
|
||||
const dx = action.at ? Math.round(action.at.x - minX) : 20;
|
||||
const dy = action.at ? Math.round(action.at.y - minY) : 20;
|
||||
|
||||
const clones: WidgetNode[] = clip.map((n) => {
|
||||
const id = `${n.type}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
const rect = clampRectToBounds({ ...n.rect, x: n.rect.x + dx, y: n.rect.y + dy }, bounds, 50);
|
||||
return {
|
||||
...n,
|
||||
id,
|
||||
rect,
|
||||
locked: false,
|
||||
hidden: false,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: { screen: { ...state.doc.screen, nodes: [...state.doc.screen.nodes, ...clones] } },
|
||||
selection: { ids: clones.map((c) => c.id) },
|
||||
};
|
||||
}
|
||||
|
||||
case 'toggleLockSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
@ -533,59 +382,27 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
|
||||
case 'bringToFrontSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(
|
||||
state.doc.screen.nodes
|
||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
||||
.map((n) => n.id),
|
||||
);
|
||||
if (!ids.size) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
const nodes = state.doc.screen.nodes;
|
||||
|
||||
let maxZ = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const n = nodes[i]!;
|
||||
const z = n.zIndex ?? i;
|
||||
if (z > maxZ) maxZ = z;
|
||||
}
|
||||
|
||||
let bump = 0;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'toFront'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'bringForwardSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(
|
||||
state.doc.screen.nodes
|
||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
||||
.map((n) => n.id),
|
||||
);
|
||||
if (!ids.size) return state;
|
||||
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'forward'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'sendBackwardSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(
|
||||
state.doc.screen.nodes
|
||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
||||
.map((n) => n.id),
|
||||
);
|
||||
if (!ids.size) return state;
|
||||
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'backward'),
|
||||
nodes: nodes.map((n) => {
|
||||
if (!ids.has(n.id)) return n;
|
||||
bump += 1;
|
||||
return { ...n, zIndex: maxZ + bump };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -593,19 +410,27 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
|
||||
case 'sendToBackSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(
|
||||
state.doc.screen.nodes
|
||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
||||
.map((n) => n.id),
|
||||
);
|
||||
if (!ids.size) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
const nodes = state.doc.screen.nodes;
|
||||
|
||||
let minZ = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const n = nodes[i]!;
|
||||
const z = n.zIndex ?? i;
|
||||
if (z < minZ) minZ = z;
|
||||
}
|
||||
|
||||
let bump = 0;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'toBack'),
|
||||
nodes: nodes.map((n) => {
|
||||
if (!ids.has(n.id)) return n;
|
||||
bump += 1;
|
||||
return { ...n, zIndex: minZ - bump };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -55,9 +55,6 @@ export interface EditorState {
|
||||
ctrl: boolean;
|
||||
space: boolean;
|
||||
};
|
||||
clipboard: {
|
||||
nodes: WidgetNode[];
|
||||
};
|
||||
history: {
|
||||
past: HistoryEntry[];
|
||||
future: HistoryEntry[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user