diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 377dab23..1fd86ff8 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -295,10 +295,6 @@ export const DynamicComponent: React.FC = ({ 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 @@ -810,7 +806,11 @@ export const DynamicComponent: React.FC = ({ // 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, + // Local order inside .component-interactive-group only. The + // drag-to-front rank is applied on that GROUP (SimulatorCanvas) — + // a z set here is clamped by the group's stacking context and could + // never lift the part above a dragged board. + zIndex: isSelected ? 5 : 1, pointerEvents: 'auto', transform: properties.rotation ? `rotate(${properties.rotation}deg)` : undefined, transformOrigin: 'center center', diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 9cc45fcf..1efcbcd9 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -156,6 +156,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { setBoardPosition, addBoard, components, + zOrders, running, sensorResetNonce, pinManager, @@ -2314,7 +2315,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { position: 'absolute', left: 0, top: 0, - zIndex: isSelected ? 2 : 1, + // Drag-to-front rank applies HERE (the group is the stacking + // context that competes with boards) — see the parts branch. + zIndex: (zOrders[component.id] ?? 0) > 0 + ? 10 + (zOrders[component.id] ?? 0) + : isSelected + ? 2 + : 1, pointerEvents: 'auto', }} > @@ -2376,15 +2383,25 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { // thin parts (resistors, LEDs) and free-floating displays are untouched. const isSeated = (seatedPinsByComponent.get(component.id)?.length ?? 0) > 0; const occludesWhenSeated = DISPLAY_BODY_METADATA_RE.test(String(component.metadataId)); + // Drag-to-front (see raiseItem): the rank MUST be applied on this group, + // not on the inner .dynamic-component-wrapper. The group is a stacking + // context, so a z set inside it is clamped to the group's own level and a + // part could never climb above a board that was dragged (boards live one + // level up, as direct children of .canvas-world) — dragging a board once + // pinned every component under it forever. Same rank space as + // BoardOnCanvas (10 + rank) so the last thing dragged genuinely wins. + const dragRank = zOrders[component.id] ?? 0; const groupZIndex = isBreadboard ? -1 - : isSeated && occludesWhenSeated - ? isSelected - ? 37 - : 36 - : isSelected - ? 2 - : 1; + : dragRank > 0 + ? 10 + dragRank + : isSeated && occludesWhenSeated + ? isSelected + ? 37 + : 36 + : isSelected + ? 2 + : 1; return (