Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions web/src/lantern/areaNames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,66 @@ describe('keepMarksApart', () => {
expect(b.x >= 110 && b.x <= 310 && b.y >= 100 && b.y <= 180).toBe(false)
})

it('never slides a stepped mark so its dot lies half under the words beside it', () => {
// Books, kept apart from Shelves, slides right along its row: its middle
// would land a hair left of the spoken line, its right half under it.
const words = { x: 180, y: 100, w: 150, h: 40 }
const out = keepMarksApart(
[
mark('shelves', 150, 110, { x: 150, y: 60 }),
mark('books', 155, 110, { x: 156, y: 60 }),
],
[],
visible,
[words],
)
const books = out[1]
const nearestX = Math.max(words.x, Math.min(books.x, words.x + words.w))
const nearestY = Math.max(words.y, Math.min(books.y, words.y + words.h))
// The whole dot, its glow included, keeps off the words.
expect(
Math.hypot(books.x - nearestX, books.y - nearestY),
).toBeGreaterThanOrEqual(8)
expect(Math.hypot(books.x - 150, books.y - 110)).toBeGreaterThanOrEqual(24)
})

it('gives way a row further up, beside its neighbour, when the words end its slide', () => {
// As in the cottage: both stepped up off their things below, the spoken
// line in their band to the right.
const words = { x: 180, y: 100, w: 150, h: 40 }
const [shelves, books] = keepMarksApart(
[
mark('shelves', 150, 110, { x: 150, y: 160 }),
mark('books', 155, 110, { x: 156, y: 160 }),
],
[],
visible,
[words],
)
// Not across the line, and not to the other side of Shelves.
expect(books).toMatchObject({ x: 155, y: 86 })
expect(books.x).toBeGreaterThanOrEqual(shelves.x)
})

it('gives way to the other side of its neighbour when the words end its slide and no row lies further out', () => {
const words = { x: 180, y: 560, w: 150, h: 140 }
const [shelves, books] = keepMarksApart(
[
mark('shelves', 150, 690, { x: 150, y: 600 }),
mark('books', 155, 690, { x: 156, y: 600 }),
],
[],
visible,
[words],
)
expect(
Math.hypot(books.x - shelves.x, books.y - shelves.y),
).toBeGreaterThanOrEqual(24)
// Never across the words.
expect(books.x).toBeLessThan(words.x)
expect(books.y).toBe(690)
})

it('keeps marks moved in off the foot apart too', () => {
const [a, b] = keepMarksApart(
[
Expand Down
68 changes: 50 additions & 18 deletions web/src/lantern/areaNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,15 @@ const MARK_APART = 24
* whole line is full onto a row one `MARK_APART` further from where it came
* from (below the line, for a mark that stands on its thing). A spot must be on
* the `visible` part, under none of the `panels` and not under the `words`
* (which would cover it); a mark with no such spot keeps where it is. A mark
* stepped out vertically gives way level with the nearest mark it gives way
* to when it can, first to the side its thing lies on (left when its thing
* lies left of that mark's thing, else right). A mark standing on the edge of
* (which would cover it); a spot the mark slides to keeps its whole dot
* (`MARK_RADIUS` from its middle) off the words, so a press anywhere on it
* reaches it. The words end a slide, which never goes across them: a mark
* whose slide reaches them goes on to the next row further out rather than
* to the other side, and tries the other side only when no row has a spot.
* A mark with no such spot keeps where it is. A mark stepped out vertically
* gives way level with the nearest mark it gives way to when it can, first
* to the side its thing lies on (left when its thing lies left of that
* mark's thing, else right). A mark standing on the edge of
* the words (`onWords`) slides only along that edge, in the words' own band.
*/
export function keepMarksApart(
Expand All @@ -390,6 +395,15 @@ export function keepMarksApart(
const result = [...marks]
const underWords = (x: number, y: number): boolean =>
words.some((r) => inWords(x, y, r))
/** Whether a dot with its middle at (`x`, `y`) would lie even partly under the words. */
const touchesWords = (x: number, y: number): boolean =>
words.some(
(r) =>
x > r.x - MARK_RADIUS &&
x < r.x + r.w + MARK_RADIUS &&
y > r.y - MARK_RADIUS &&
y < r.y + r.h + MARK_RADIUS,
)
// A mark standing on its thing, clear of every other such mark, holds its
// own spot: no mark that slides lands on it, so it never has to move.
const holding = marks.filter(
Expand All @@ -415,7 +429,9 @@ export function keepMarksApart(
(self.onWords
? Math.abs(y - self.y) < 0.5 &&
words.some((r) => x >= r.x && x <= r.x + r.w)
: !underWords(x, y)) &&
: Math.hypot(x - self.x, y - self.y) < 0.5
? !underWords(x, y)
: !touchesWords(x, y)) &&
placedAt.every(([px, py]) => Math.hypot(px - x, py - y) >= MARK_APART - 0.5)
for (const { entry, index } of order) {
// A mark standing on its thing under the words (none, once `clearOfWords` has run) is covered with them: it
Expand Down Expand Up @@ -447,19 +463,35 @@ export function keepMarksApart(
const first = stepped && blocker && from.x < blocker[2] ? -1 : 1
const level = stepped && blocker ? blocker[1] - entry.y : 0
let spot: [number, number] | null = null
search: for (let row = 0; row <= reach; row++) {
const bx = entry.x + away[0] * row * MARK_APART
const by = entry.y + away[1] * row * MARK_APART
for (let n = 0; n <= reach * 2; n++) {
// 0, then onward along the line, then the other side of where it landed.
const k = (n <= reach ? n : -(n - reach)) * first
const cx = bx + along[0] * k * MARK_APART
// At each step the level spot first, then the mark's own line.
for (const lift of row === 0 && level !== 0 ? [level, 0] : [0]) {
const cy = by + lift + along[1] * k * MARK_APART
if (free(cx, cy, entry)) {
spot = [cx, cy]
break search
// The words end a slide: first the mark gives way a row further out
// rather than to the other side of its neighbour; only when no row has a
// spot does it try the other side as well. It never slides across them.
find: for (const otherSide of [false, true]) {
search: for (let row = 0; row <= reach; row++) {
const bx = entry.x + away[0] * row * MARK_APART
const by = entry.y + away[1] * row * MARK_APART
slide: for (let n = 0; n <= reach * 2; n++) {
// 0, then onward along the line, then the other side of where it landed.
const back = n > reach
const k = (back ? -(n - reach) : n) * first
const cx = bx + along[0] * k * MARK_APART
// At each step the level spot first, then the mark's own line.
for (const lift of row === 0 && level !== 0 ? [level, 0] : [0]) {
const cy = by + lift + along[1] * k * MARK_APART
if (
k !== 0 &&
lift === 0 &&
!entry.onWords &&
touchesWords(cx, cy)
) {
if (back || !otherSide) continue search
n = reach
continue slide
}
if (free(cx, cy, entry)) {
spot = [cx, cy]
break find
}
}
}
}
Expand Down
Loading