fix(canvas): apply the drag rank on the group, not inside it

Drag-to-front lifted boards but never parts: a component's z lives
inside .component-interactive-group, which is itself a stacking
context, so the 10+rank set on the inner wrapper was clamped to the
group's own level (1/2) while boards compete one level up as direct
children of .canvas-world. Once any board had been dragged it sat at
10+rank and no component could ever climb back over it — drag an LED
onto the Arduino and the board swallowed it, exactly as reported.

The rank now lands on the group itself (both branches: instruments and
ordinary parts), so parts and boards share ONE rank space and the last
thing dragged really is the thing on top. The inner wrapper keeps its
local 1/5 for selection order within the group.
This commit is contained in:
David Montero Crespo 2026-07-31 16:40:01 +02:00
parent cb26e36b28
commit e0a69fecae
2 changed files with 30 additions and 13 deletions

View File

@ -295,10 +295,6 @@ 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
@ -810,7 +806,11 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
// 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',

View File

@ -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 (
<React.Fragment key={component.id}>