File size: 6,268 Bytes
6f91ce5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | // Layout v0 (T02): node --test server/layout.test.ts
import type { TLRecord } from '@tldraw/tlschema'
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { CARD_SIZE } from '../shared/shapes.ts'
import { centre, isFree, MARGIN, placeInside, placeNear, shapeBoxes, type Box } from './layout.ts'
const shape = (id: string, x: number, y: number, type: string, props: Record<string, unknown>, parentId = 'page:page') =>
({ typeName: 'shape', id: `shape:${id}`, type, x, y, parentId, props }) as unknown as TLRecord
const card = (id: string, x: number, y: number, size = 48) => shape(id, x, y, 'paper-card', { w: size, h: size })
// what the browser draws (cardOffset): a CARD_SIZE card centred on each node; other shapes as they are
function drawn(records: TLRecord[], except = ''): Box[] {
const types = new Map<string, string | undefined>(records.map((r) => [r.id, (r as { type?: string }).type]))
return [...shapeBoxes(records)]
.filter(([id]) => id !== except)
.map(([id, b]) => {
if (types.get(id) !== 'paper-card') return b
const c = centre(b)
return { x: c.x - CARD_SIZE.w / 2, y: c.y - CARD_SIZE.h / 2, w: CARD_SIZE.w, h: CARD_SIZE.h }
})
}
const assertApart = (boxes: Box[]) =>
boxes.forEach((box, i) => assert.ok(isFree(boxes.slice(0, i), box), `box ${i} overlaps an earlier one`))
// deterministic pseudo-random numbers, so a failure can be replayed
function rng(seed: number) {
return () => ((seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31)
}
function placeMany(n: number, seed: number) {
const random = rng(seed)
const records: TLRecord[] = [card('first', 0, 0), shape('n', 300, -40, 'note', { scale: 1, growY: 0 })]
for (let i = 0; i < n; i++) {
// old boards' nodes grew with their links, so sizes still vary
const size = 40 + Math.floor(random() * 5) * 8
const near = records[Math.floor(random() * records.length)] as unknown as Box & { props: Box }
const anchor = random() < 0.2 ? null : centre({ x: near.x, y: near.y, w: near.props.w ?? 200, h: near.props.h ?? 200 })
const { x, y } = placeNear(records, { w: size, h: size }, anchor)
records.push(card(`p${i}`, x, y, size))
}
return records
}
test('200 cards placed one after another never overlap as drawn, margin included', () => {
const boxes = drawn(placeMany(200, 7))
assert.equal(boxes.length, 202)
assertApart(boxes)
})
test('placement is deterministic', () => {
const a = placeMany(60, 42).map((r) => [(r as { x: number }).x, (r as { y: number }).y])
const b = placeMany(60, 42).map((r) => [(r as { x: number }).x, (r as { y: number }).y])
assert.deepEqual(a, b)
})
test('near is respected: the first free ring around the anchor, 212 px out, right side first', () => {
const records = [card('a', 0, 0), card('far', 2000, 2000)]
const anchor = centre({ x: 0, y: 0, w: 48, h: 48 })
const distance = (p: { x: number; y: number }) => Math.round(Math.hypot(p.x + 24 - anchor.x, p.y + 24 - anchor.y))
assert.deepEqual(placeNear(records, { w: 48, h: 48 }, anchor), { x: 212, y: 0 })
// the first ring takes six cards (the spots straight above and below would touch their neighbours), then the next ring
const rings = []
for (let i = 0; i < 7; i++) {
const p = placeNear(records, { w: 48, h: 48 }, anchor)
rings.push(distance(p))
records.push(card(`p${i}`, p.x, p.y))
}
assert.deepEqual(rings, [212, 212, 212, 212, 212, 212, 424])
assertApart(drawn(records))
})
test('without an anchor it goes just right of the content', () => {
const p = placeNear([card('a', 0, 0), card('b', 500, 300, 80)], { w: 48, h: 48 }, null)
// b's card is drawn up to x 626; the new one starts clear of it and top-aligned with a's
assert.ok(p.x + 24 - CARD_SIZE.w / 2 >= 626 + MARGIN, `x ${p.x}`)
assert.equal(p.y, 0)
assert.deepEqual(placeNear([], { w: 48, h: 48 }, null), { x: 176, y: 176 })
})
test('seeds added one after another with no anchor, as the seed route adds them, are drawn apart', () => {
const records: TLRecord[] = []
for (let i = 0; i < 5; i++) {
const at = placeNear(records, { w: 48, h: 48 }, null)
records.push(card(`seed${i}`, at.x, at.y))
}
assertApart(drawn(records))
})
test('boxes: a card keeps its footprint, notes are 200 × scale, arrows take no space, frame children are in page space', () => {
const boxes = shapeBoxes([
card('card', 0, 0),
card('big', 500, 0, 80),
shape('note', 10, 10, 'note', { scale: 2, growY: 0 }),
shape('arrow', 0, 0, 'arrow', { start: { x: 0, y: 0 }, end: { x: 500, y: 500 } }),
shape('frame', 1000, 1000, 'frame', { w: 400, h: 400 }),
shape('child', 20, 30, 'geo', { w: 10, h: 10 }, 'shape:frame'),
])
assert.deepEqual(boxes.get('shape:card'), { x: -62, y: -11, w: 172, h: 70 })
assert.deepEqual(boxes.get('shape:big'), { x: 454, y: 0, w: 172, h: 80 })
assert.deepEqual(boxes.get('shape:note'), { x: 10, y: 10, w: 400, h: 400 })
assert.equal(boxes.has('shape:arrow'), false)
assert.deepEqual(boxes.get('shape:child'), { x: 1020, y: 1030, w: 10, h: 10 })
})
test('placeInside: free spots inside a frame, clear of its cards and of shapes over it, until it is full', () => {
const records: TLRecord[] = [
shape('frame', 1000, 1000, 'frame', { w: 700, h: 460 }),
// a card in the frame (frame coordinates) and one on the page lying over it
shape('child', 100, 60, 'paper-card', { w: 48, h: 48 }, 'shape:frame'),
card('over', 1400, 1250),
]
const frame = { x: 1000, y: 1000, w: 700, h: 460 }
const inner = { x: frame.x + MARGIN, y: frame.y + MARGIN, w: frame.w - 2 * MARGIN, h: frame.h - 2 * MARGIN }
const placed: TLRecord[] = []
for (;;) {
const spot = placeInside([...records, ...placed], { w: 48, h: 48 }, centre(frame), 'shape:frame')
if (!spot) break
const p = [...records, ...placed, card('new', spot.x, spot.y)]
const box = drawn(p).at(-1)!
assert.ok(box.x >= inner.x && box.y >= inner.y && box.x + box.w <= inner.x + inner.w && box.y + box.h <= inner.y + inner.h)
assert.ok(isFree(drawn(p, 'shape:frame').slice(0, -1), box), `spot ${JSON.stringify(spot)} overlaps`)
placed.push(card(`p${placed.length}`, spot.x, spot.y))
assert.ok(placed.length < 20, 'never fills forever')
}
assert.ok(placed.length >= 2)
assert.equal(placeInside(records, { w: 48, h: 48 }, null, 'shape:missing'), null)
})
|