diff --git a/resources/js/environment-canvas.js b/resources/js/environment-canvas.js index 3b8cbe49fc..3d3472d222 100644 --- a/resources/js/environment-canvas.js +++ b/resources/js/environment-canvas.js @@ -3,7 +3,7 @@ // // DOM-free: `node --test` imports this file. -import { apart, BOX_GAP, freeSpot, MAX_ZOOM, MIN_ZOOM } from './canvas-core.js'; +import { BOX_GAP, freeSpot, MAX_ZOOM, MIN_ZOOM } from './canvas-core.js'; export const CARD_WIDTH = 260; export const COLUMN_GAP = 48; @@ -464,12 +464,18 @@ const frameAround = (boxes, side, top) => { return grow({ x, y, w: Math.max(...boxes.map(b => b.x + b.w)) - x, h: Math.max(...boxes.map(b => b.y + b.h)) - y }, side, top); }; +// Area of `b` within `gap` of `a`: 0 when they are apart(). +const crowding = (a, b, gap) => Math.max(0, Math.min(a.x + a.w + gap, b.x + b.w) - Math.max(a.x - gap, b.x)) + * Math.max(0, Math.min(a.y + a.h + gap, b.y + b.h) - Math.max(a.y - gap, b.y)); + /** * What a dragged card must keep clear of, for freeSpot(): `others` are the * other cards and the other zone frames (and, server view, the other project / * environment frames in its zone), grown by the room the card's own frames * take around it. `fits` checks the card's own frames, which also span its - * other cards, against those frames. + * other cards, against those frames: they must not come closer to them than + * at the start of the drag. Frames that are already too close (the layout + * uses estimated card heights) do not lock the cards in place. */ export function dragRules(graph, positions, sizes, id) { const node = graph.nodes.find(n => n.id === id); @@ -487,11 +493,20 @@ export function dragRules(graph, positions, sizes, id) { const groupMates = grouped ? rects(own.nodes.filter(n => context(n) === context(node))) : []; // The other box must keep the frame's room on the side that faces the card. const room = (b, s, t) => ({ x: b.x - s, y: b.y - s, w: b.w + s * 2, h: b.h + s + t }); + const at = node && cardRect(node, positions, sizes); + const notCloser = (frame, boxes, gap) => { + const start = at ? frame(toBox(at)) : null; + return box => { + const moved = frame(box); + return boxes.every(b => crowding(moved, b, gap) <= (start ? crowding(start, b, gap) : 0)); + }; + }; + const zoneFits = notCloser(box => frameAround([...zoneMates, box], side, top), zones, BOX_GAP); + // Same gap between groups as layoutServerView, so a reload does not move the group. + const groupFits = grouped ? notCloser(box => frameAround([...groupMates, box], groupSide, groupTop), groups, GROUP_GAP) : () => true; return { others: [...rects(graph.nodes), ...zones.map(z => room(z, side, top)), ...groups.map(g => room(g, groupSide, groupTop))], - fits: box => zones.every(z => apart(frameAround([...zoneMates, box], side, top), z, BOX_GAP)) - // Same gap between groups as layoutServerView, so a reload does not move the group. - && (!grouped || groups.every(g => apart(frameAround([...groupMates, box], groupSide, groupTop), g, GROUP_GAP))), + fits: box => zoneFits(box) && groupFits(box), }; } diff --git a/resources/js/environment-canvas.test.js b/resources/js/environment-canvas.test.js index 015a997028..0d2bc1753b 100644 --- a/resources/js/environment-canvas.test.js +++ b/resources/js/environment-canvas.test.js @@ -629,6 +629,24 @@ test('server view: a dragged card keeps its group as far from another group as t assert.equal(freeSpot({ ...start, x: start.x - 10 }, others, undefined, fits), null); }); +test('server view: cards still move when the measured cards put their group too close to another group', () => { + const g = { + layout: 'server', + zones: [{ id: 'z1', name: 'web' }], + nodes: [srv('a', 'application', 'Shop / prod'), srv('c', 'application', 'Blog / prod')], + links: [], + positions: {}, + }; + const before = layoutServerView(g); + // The layout uses estimated sizes; the real Blog card is wider, so the groups are less than GROUP_GAP apart. + const sizes = { c: { width: CARD_WIDTH + 20, height: estimateNodeHeight(g.nodes[1]) } }; + const { fits } = dragRules(g, before, sizes, 'a'); + const start = { ...before.a, w: CARD_WIDTH, h: estimateNodeHeight(g.nodes[0]) }; + assert.ok(fits(start), 'the card can stay where it is'); + assert.ok(fits({ ...start, x: start.x + 50 }), 'the card can move away from the other group'); + assert.ok(!fits({ ...start, x: start.x - 10 }), 'the card cannot move its group closer to the other group'); +}); + test('groupKey: a group with only an empty-environment placeholder has no key, so it cannot be moved', () => { const environment = { uuid: 'env', name: 'production' }; assert.equal(groupKey([{ id: 'empty:env', type: 'empty', environment }], 'empty'), null);