76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
/**
|
|
* Where a new component lands.
|
|
*
|
|
* The complaint this fixes: "I add an element and sometimes I cannot even
|
|
* see where it went". Two causes — placement at fixed world coordinates
|
|
* (off-screen once the canvas is panned, fixed elsewhere by anchoring to the
|
|
* visible corner) and a cascade keyed on components.length, which kept
|
|
* marching down-right forever even after parts were moved away or deleted.
|
|
*
|
|
* These lock the rule that replaced it: first FREE slot from the corner.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { pickDropSlot } from '../utils/dropSlot';
|
|
|
|
const ORIGIN = { x: 100, y: 50 };
|
|
const STEP = 36;
|
|
|
|
describe('pickDropSlot', () => {
|
|
it('drops at the corner when nothing is there', () => {
|
|
expect(pickDropSlot(ORIGIN, [], { step: STEP })).toEqual(ORIGIN);
|
|
});
|
|
|
|
it('steps down-right when the corner is taken', () => {
|
|
const slot = pickDropSlot(ORIGIN, [ORIGIN], { step: STEP });
|
|
expect(slot).toEqual({ x: 100 + STEP, y: 50 + STEP });
|
|
});
|
|
|
|
it('keeps cascading while each slot in turn is taken', () => {
|
|
const placed = [ORIGIN, { x: 100 + STEP, y: 50 + STEP }];
|
|
expect(pickDropSlot(ORIGIN, placed, { step: STEP })).toEqual({
|
|
x: 100 + 2 * STEP,
|
|
y: 50 + 2 * STEP,
|
|
});
|
|
});
|
|
|
|
it('reclaims a slot whose component was moved away', () => {
|
|
// The whole point of not counting components: the user parked the last
|
|
// drop somewhere else, so the corner is free again and the next one
|
|
// belongs there — not three steps further down the diagonal.
|
|
const movedAside = [{ x: 900, y: 700 }];
|
|
expect(pickDropSlot(ORIGIN, movedAside, { step: STEP })).toEqual(ORIGIN);
|
|
});
|
|
|
|
it('ignores components that are merely near, not on, the slot', () => {
|
|
const nearby = [{ x: ORIGIN.x + STEP * 0.9, y: ORIGIN.y + STEP * 0.9 }];
|
|
expect(pickDropSlot(ORIGIN, nearby, { step: STEP })).toEqual(ORIGIN);
|
|
});
|
|
|
|
it('treats a slightly nudged component as still parked', () => {
|
|
// A few px of drag should not make the next drop land on top of it.
|
|
const nudged = [{ x: ORIGIN.x + 4, y: ORIGIN.y - 3 }];
|
|
expect(pickDropSlot(ORIGIN, nudged, { step: STEP })).toEqual({
|
|
x: ORIGIN.x + STEP,
|
|
y: ORIGIN.y + STEP,
|
|
});
|
|
});
|
|
|
|
it('stops cascading instead of walking off the viewport', () => {
|
|
// Every slot occupied: it stacks at the last one rather than marching
|
|
// out of sight, which is the failure mode being fixed.
|
|
const full = Array.from({ length: 40 }, (_, i) => ({
|
|
x: ORIGIN.x + i * STEP,
|
|
y: ORIGIN.y + i * STEP,
|
|
}));
|
|
const slot = pickDropSlot(ORIGIN, full, { step: STEP, maxSlots: 12 });
|
|
expect(slot).toEqual({ x: ORIGIN.x + 12 * STEP, y: ORIGIN.y + 12 * 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.
|
|
const zoomedOut = pickDropSlot(ORIGIN, [ORIGIN], { step: 36 / 0.5 });
|
|
expect(zoomedOut).toEqual({ x: 100 + 72, y: 50 + 72 });
|
|
});
|
|
});
|