feat(canvas): drag-to-front — whatever you dragged last paints on top
An unseated board dropped over a component was painted underneath it (boards z 0, components z 1) and became impossible to grab back; the earlier blanket z bump for boards broke the opposite case, hiding LEDs and transistors behind every board. Neither static order can win both. The rule is now the user's own gesture: the FIRST movement of a drag raises that item (board or part, symmetrically) to the top of a monotonic dragged-stack, so a board dropped over an LED sits visibly on top — and dragging the LED afterwards wins the stack right back. Click-select alone never raises: selection must not reshuffle a scene you arranged. Untouched items keep the static layering (components above unseated boards, seated boards above their socket), and the rank map is ephemeral — never saved with the project.
This commit is contained in:
parent
94bfe627d3
commit
cb26e36b28
|
|
@ -295,6 +295,10 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent);
|
||||
const running = useSimulatorStore((s) => s.running);
|
||||
const simulator = useSimulatorStore((s) => s.simulator);
|
||||
// Drag-to-front rank: 0 = never dragged, static layering applies. A part
|
||||
// the user dragged paints above everything it overlaps — including a board
|
||||
// that was raised earlier — so the stack always reads "last dragged wins".
|
||||
const zRaise = useSimulatorStore((s) => s.zOrders[id] ?? 0);
|
||||
// Board-less SPICE circuits (digital / analog gallery) have no MCU to
|
||||
// run, so `running` is always false — but interactive parts like
|
||||
// slide-switches and pushbuttons should still show a pointer cursor
|
||||
|
|
@ -803,7 +807,10 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
userSelect: 'none',
|
||||
zIndex: isSelected ? 5 : 1,
|
||||
// Drag-to-front (zRaise) beats the static layers: a part dragged onto
|
||||
// a board — or a board dragged onto a part — the last one dragged
|
||||
// paints on top. Untouched parts keep the classic selected/idle z.
|
||||
zIndex: zRaise > 0 ? 10 + zRaise : isSelected ? 5 : 1,
|
||||
pointerEvents: 'auto',
|
||||
transform: properties.rotation ? `rotate(${properties.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ export const BoardOnCanvas = ({
|
|||
() => isBoardSeated(id, boardKind, x, y, components),
|
||||
[id, boardKind, x, y, components, seatEpoch],
|
||||
);
|
||||
// Drag-to-front rank: 0 = never dragged (static layering applies).
|
||||
const zRaise = useSimulatorStore((st) => st.zOrders[id] ?? 0);
|
||||
|
||||
// Status dot color: green=running, amber=compiled, gray=idle
|
||||
const statusColor = board.running ? '#22c55e' : board.compiledProgram ? '#f59e0b' : '#6b7280';
|
||||
|
|
@ -211,12 +213,20 @@ export const BoardOnCanvas = ({
|
|||
// Stacking: boards normally sit BELOW components (z 0 vs their 1/2) —
|
||||
// a resistor next to an Arduino must be visible on top, and a blanket
|
||||
// z bump here once hid it behind the board in every ordinary example.
|
||||
// The ONE exception is a board SEATED on a socket component (the Round
|
||||
// Display back header): then the board is the thing you see, the way
|
||||
// the physical XIAO stacks on the shield. The magnet snap places the
|
||||
// board exactly on the seat, so the moment it clicks it pops on top,
|
||||
// and dragging it off drops it back under.
|
||||
style={{ position: 'absolute', left: 0, top: 0, zIndex: seated ? 3 : 0 }}
|
||||
// Two exceptions:
|
||||
// - a board SEATED on a socket component (the Round Display / reSpeaker
|
||||
// back header): then the board is the thing you see, the way the
|
||||
// physical XIAO stacks on the shield;
|
||||
// - a board the user has DRAGGED (zRaise > 0): drag-to-front puts
|
||||
// whatever you dragged last above everything it overlaps, so a board
|
||||
// dropped over a part is never lost underneath it — and dragging the
|
||||
// part afterwards wins the stack right back.
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: zRaise > 0 ? 10 + zRaise : seated ? 3 : 0,
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -339,6 +339,9 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
|
||||
// Component dragging state
|
||||
const [draggedComponentId, setDraggedComponentId] = useState<string | null>(null);
|
||||
// Which drag session already raised its item (drag-to-front fires once per
|
||||
// drag, on the first mousemove).
|
||||
const raisedThisDragRef = useRef<string | null>(null);
|
||||
// Captures (x, y) of the dragged component at mousedown so a drag-end
|
||||
// can record the diff as a single undoable Move. Boards are intentionally
|
||||
// skipped — board moves don't go through component history.
|
||||
|
|
@ -1529,6 +1532,17 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
|
||||
// Handle component/board dragging
|
||||
if (draggedComponentId) {
|
||||
// Drag-to-front: the FIRST movement of a drag raises the item above
|
||||
// everything else on the canvas (boards over parts and parts over
|
||||
// boards, symmetrically — whatever you dragged last wins). Movement,
|
||||
// not mousedown: a plain click-select must not reshuffle the stack.
|
||||
if (raisedThisDragRef.current !== draggedComponentId) {
|
||||
raisedThisDragRef.current = draggedComponentId;
|
||||
const raiseId = draggedComponentId.startsWith('__board__:')
|
||||
? draggedComponentId.slice('__board__:'.length)
|
||||
: draggedComponentId;
|
||||
if (raiseId !== '__board__') useSimulatorStore.getState().raiseItem(raiseId);
|
||||
}
|
||||
const world = toWorld(e.clientX, e.clientY);
|
||||
if (draggedComponentId.startsWith('__board__:')) {
|
||||
const boardId = draggedComponentId.slice('__board__:'.length);
|
||||
|
|
@ -1855,6 +1869,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
|
||||
recalculateAllWirePositions();
|
||||
setDraggedComponentId(null);
|
||||
raisedThisDragRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2809,6 +2824,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
isPanningRef.current = false;
|
||||
setPan({ ...panRef.current });
|
||||
setDraggedComponentId(null);
|
||||
raisedThisDragRef.current = null;
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1016,6 +1016,23 @@ interface SimulatorState {
|
|||
updateWirePositions: (componentId: string) => void;
|
||||
recalculateAllWirePositions: () => void;
|
||||
|
||||
// ── Drag-to-front stacking ───────────────────────────────────────────────
|
||||
/**
|
||||
* Items the user has DRAGGED, in drag order: id -> monotonic rank. The
|
||||
* canvas raises an item on the first movement of its drag, so whatever you
|
||||
* dragged last paints above everything it overlaps — boards over parts,
|
||||
* parts over boards, symmetrically. Click-select alone never raises
|
||||
* (deliberate: selection should not reshuffle a scene you arranged), and
|
||||
* untouched items keep the static layering (components above unseated
|
||||
* boards, seated boards above their socket). Ephemeral: not saved with the
|
||||
* project.
|
||||
*/
|
||||
zOrders: Record<string, number>;
|
||||
/** Highest rank handed out so far. */
|
||||
zTop: number;
|
||||
/** Move an item (board or component id) to the top of the dragged stack. */
|
||||
raiseItem: (id: string) => void;
|
||||
|
||||
// ── Undo/redo ────────────────────────────────────────────────────────────
|
||||
/** Bounded ring buffer of canvas mutations (HISTORY_MAX = 50). */
|
||||
history: CanvasCommand[];
|
||||
|
|
@ -3054,6 +3071,15 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
});
|
||||
},
|
||||
|
||||
zOrders: {},
|
||||
zTop: 0,
|
||||
raiseItem: (id: string) => {
|
||||
const s = get();
|
||||
// Already on top: re-raising would only churn renders.
|
||||
if (s.zOrders[id] === s.zTop && s.zTop > 0) return;
|
||||
set({ zTop: s.zTop + 1, zOrders: { ...s.zOrders, [id]: s.zTop + 1 } });
|
||||
},
|
||||
|
||||
recalculateAllWirePositions: () => {
|
||||
const state = get();
|
||||
const updatedWires = state.wires.map((wire) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue