fix(canvas): boards added from the picker land in the visible corner like components
Boards were placed 420 world-px right of the ACTIVE board plus 60/30 px per board already on the canvas: a fixed world offset that ignored pan/zoom (the new board could land off-screen) and kept marching further out with every board added, even after the earlier ones had been moved away or removed. Components had already been fixed to start at the visible top-left and take the first free slot (utils/dropSlot); boards now go through the same rule, and both count each other as parked items so they cascade over the same slots.
This commit is contained in:
parent
990cced209
commit
e71c78f734
|
|
@ -66,6 +66,27 @@ describe('pickDropSlot', () => {
|
|||
expect(slot).toEqual({ x: ORIGIN.x + 12 * STEP, y: ORIGIN.y + 12 * STEP });
|
||||
});
|
||||
|
||||
it('treats boards and components as one set of parked items', () => {
|
||||
// Boards used to be placed 420 world-px right of the active board plus a
|
||||
// per-board offset — never in the visible corner, and marching further
|
||||
// out with every board added. The canvas now feeds boards and components
|
||||
// through the same rule, so a board parked in the corner pushes the next
|
||||
// component to the next slot and a component pushes the next board.
|
||||
const boardInCorner = [{ x: ORIGIN.x, y: ORIGIN.y, boardKind: 'arduino-uno' }];
|
||||
expect(pickDropSlot(ORIGIN, boardInCorner, { step: STEP })).toEqual({
|
||||
x: ORIGIN.x + STEP,
|
||||
y: ORIGIN.y + STEP,
|
||||
});
|
||||
const mixed = [
|
||||
{ x: ORIGIN.x, y: ORIGIN.y, metadataId: 'led' },
|
||||
{ x: ORIGIN.x + STEP, y: ORIGIN.y + STEP, boardKind: 'esp32' },
|
||||
];
|
||||
expect(pickDropSlot(ORIGIN, mixed, { step: STEP })).toEqual({
|
||||
x: ORIGIN.x + 2 * STEP,
|
||||
y: ORIGIN.y + 2 * STEP,
|
||||
});
|
||||
});
|
||||
|
||||
it('scales with zoom, since the step arrives in world units', () => {
|
||||
// The canvas passes 36 screen-px / zoom, so the gap looks the same
|
||||
// whatever the zoom level.
|
||||
|
|
|
|||
|
|
@ -1503,22 +1503,30 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedComponentId, recordRemoveComponent]);
|
||||
|
||||
// Handle component selection from modal
|
||||
const handleSelectComponent = (metadata: ComponentMetadata) => {
|
||||
// Anchor new components to the visible top-left of the canvas, so they
|
||||
// appear in the user's current viewport regardless of pan/zoom (instead
|
||||
// of growing off-screen at fixed world coords like (400, 100 + row*250)).
|
||||
// Where the next thing added from the picker lands — components AND boards
|
||||
// share this, so both start at the same corner and cascade over the same
|
||||
// occupied slots (a board dropped in the corner counts as parking there for
|
||||
// the next part, and vice versa).
|
||||
//
|
||||
// Anchored to the visible top-left of the canvas, so it appears in the
|
||||
// user's current viewport regardless of pan/zoom (instead of growing
|
||||
// off-screen at fixed world coords like (400, 100 + row*250)). From that
|
||||
// corner it cascades down-right, taking the first FREE slot — see
|
||||
// utils/dropSlot for why it is keyed on what is parked there rather than on
|
||||
// how many items the project has.
|
||||
const nextDropSlot = (): { x: number; y: number } => {
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
const z = zoomRef.current || 1;
|
||||
const screenMargin = 60; // px on screen — keeps the part off the toolbar/edge
|
||||
const worldOrigin = rect
|
||||
? toWorld(rect.left + screenMargin, rect.top + screenMargin)
|
||||
: { x: 100, y: 100 };
|
||||
return pickDropSlot(worldOrigin, [...components, ...boards], { step: 36 / z });
|
||||
};
|
||||
|
||||
// Cascade down-right from that corner, taking the first FREE slot — see
|
||||
// utils/dropSlot for why it is keyed on what is parked there rather than
|
||||
// on how many components the project has.
|
||||
const { x, y } = pickDropSlot(worldOrigin, components, { step: 36 / z });
|
||||
// Handle component selection from modal
|
||||
const handleSelectComponent = (metadata: ComponentMetadata) => {
|
||||
const { x, y } = nextDropSlot();
|
||||
|
||||
const component = createComponentFromMetadata(metadata, x, y);
|
||||
trackAddComponent(metadata.id);
|
||||
|
|
@ -3581,13 +3589,15 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
return;
|
||||
}
|
||||
trackSelectBoard(kind);
|
||||
const sameKind = boards.filter((b) => b.boardKind === kind);
|
||||
const newBoardId = sameKind.length === 0 ? kind : `${kind}-${sameKind.length + 1}`;
|
||||
const x = boardPosition.x + boards.length * 60 + 420;
|
||||
const y = boardPosition.y + boards.length * 30;
|
||||
// Same landing rule as components: the visible corner, first free
|
||||
// slot. Boards used to be placed 420 world-px right of the ACTIVE
|
||||
// board plus 60/30 px per board already on the canvas — a fixed
|
||||
// world offset that ignored pan/zoom (so the new board could land
|
||||
// off-screen) and that kept marching further out as boards were
|
||||
// added, even after they had been moved away or removed.
|
||||
const { x, y } = nextDropSlot();
|
||||
addBoard(kind, x, y);
|
||||
// file group is created inside addBoard
|
||||
void newBoardId;
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue