Skip to content

Named chunk areas, drawn on every map surface (#144) - #145

Merged
CaYatur merged 3 commits into
mainfrom
feat/chunk-areas
Jul 29, 2026
Merged

Named chunk areas, drawn on every map surface (#144)#145
CaYatur merged 3 commits into
mainfrom
feat/chunk-areas

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #144.

An operator marks chunks as a named, coloured region with a note, and everyone looking at the map reads it — desktop app, admin panel, public site, and over the API.

Decided once, in @shared/chunkAreas, not four times

#129 unified three maps that had drifted apart. Adding a feature to one and backfilling the rest is how they drifted in the first place, so the rules went in the pure layer first and every surface reads them.

  • Dimension. An area belongs to exactly one, or an overworld claim paints the same rectangle over the nether.
  • Overlap: smallest wins. A plot inside a town inside a claimed continent reads as the plot — the specific label is the informative one, and the big region is still visible everywhere the small one is not. Ties break on the later edit, then on id, so the order is total. An explicit z-order would be one more thing for four UIs to get right.
  • Shape is a list of rectangles, not of chunks. 100 chunks square is one rectangle or ten thousand pairs, and this is served to a public page on every map load. Clicking and typing produce the same structure; neighbours sharing a full edge merge, so the stored shape covers exactly the chunks that were sent, written down smaller.
  • What a visitor may read is its own type, like PublicMapPlayer: name, colour, note, rectangles. Not the timestamps, and not the hidden flag — which would tell a stranger that hidden areas exist.

Areas default to on, unlike structure markers. A structure marker is information the operator may not want published; an area is a label they wrote on purpose for people to read.

Two ways to build one

Clicking chunks is how you draw a town you can see; typing coordinates (10,20 or 30,40 - 32,42) is how you enter the four hundred somebody sent you in a message. They edit one selection, so switching mid-edit loses nothing. Picking is a mode, not a held modifier — the panel is used on a phone, and the map already pans on drag, so a distance check tells a click from the end of a pan.

Over the API: GET/POST/DELETE on /servers/{id}/areas, documented in the route table, audited, view to read and settings to write.

What the self-review found

Removing one chunk from a big selection deleted 143 others. normalizeRects sliced its input at 256 rectangles, and removal expanded the selection to one rectangle per chunk before filtering — so a 20×20 region became 400 rectangles, then 399, then the first 256. Silent. The first test used a row of four chunks, which is exactly why it stayed green.

Removal now splits the rectangle around the chunk into the at most four pieces beside it. The same slice had two more consequences, both fixed by refusing rather than trimming: an API caller who sent a hundred scattered chunks got a 200 with sixty-four stored, and the panel's own tidy had no cap so it disagreed with the app above 256.

Also: the desktop editor was permanently open against its own comment saying it should not be, and forgetServerAreas was written, documented as "called when a server is forgotten", and called by nothing.

Verification

12/12 gates. Three checks proved failable by reintroducing the bug:

Reintroduced Gate says
the input slice + expand-and-filter removal removing one chunk from 400 left 256
the page's smallest-wins rule deleted the page disagrees about -3,-3 in overworld: app says plot, page says town

The second one is worth noting: it passed at first. The battery stepped 3 and 7 and walked straight over the 3×3 plot and the 2×2 tie pair, so it only ever compared chunks where nothing overlaps. It now tests every chunk in the range and counts how many are genuinely contested.

CaYatur added 3 commits July 29, 2026 16:14
An operator can mark chunks as a named, coloured region with a note, and
everyone looking at the map reads it.

Built pure-first, in `@shared/chunkAreas`, because four surfaces draw
this — desktop app, admin panel, public site, and the map page still to
come. #129 unified three maps that had drifted apart; adding a feature
to one of them and backfilling the rest is how they drifted in the first
place.

The decisions that had to be made once rather than four times:

  Dimension. An area belongs to exactly one, or an overworld claim
  paints the same rectangle over the nether, where it means nothing.

  Overlap: SMALLEST WINS. A plot inside a town inside a claimed
  continent should read as the plot — the specific label is the
  informative one, and the big region is still visible everywhere the
  small one is not. Ties break on the later edit, then on id, so the
  order is total and every surface resolves a chunk identically. An
  explicit z-order would be one more thing for four UIs to get right.

  Shape is a list of RECTANGLES, not of chunks. A region 100 chunks
  square is one rect or ten thousand pairs, and this is served to a
  public page on every map load. Clicking chunks and typing coordinates
  both produce the same structure, and the tidy-up merges neighbours
  that share a full edge — so the stored shape covers exactly the chunks
  that were sent, written down smaller.

  What a visitor may read is its own type, like `PublicMapPlayer`: name,
  colour, note, rectangles. Not the timestamps, and not the hidden flag,
  which would tell a stranger that hidden areas exist.

Areas default to ON, unlike structure markers. A structure marker is
information the operator may not want published; an area is a label they
wrote on purpose for people to read.

The two web pages carry their own copy of the lookup, the convention
this codebase already uses for the view transform — a page pasted
together as a string cannot import from @shared. The smoke runs both
over every chunk in a range against a battery with three nested areas
and a tie pair, and fails on the first disagreement.

Verified: 12/12 gates.

Both new checks proved failable. Deleting the page's smallest-wins rule
first left the gate GREEN — the battery stepped 3 and 7 and walked
straight over the 3x3 plot and the 2x2 tie pair, so it only ever
compared chunks where nothing overlaps. Testing every chunk instead, and
counting how many are actually contested, turns the same break into "the
page disagrees about -3,-3 in overworld: app says plot, page says town".
The first commit gave the panel a map that draws areas and no way to make
one. "Yönetim panellerinden" is plural, and an operator who can only
create areas from the desktop app has half a feature.

Same two ways in as the desktop editor, for the same reason: clicking
chunks is how you draw a town you can see, typing coordinates is how you
enter the four hundred somebody sent you in a message. They edit one
selection, so switching mid-edit loses nothing.

Picking is a MODE, not a held modifier. This panel is used on a phone,
where there is no shift key, and the map already pans on drag - a
distance check tells a click from the end of a pan.

Taking a chunk back out expands the selection and re-derives it without
that chunk, rather than dropping the rectangle containing it. Rects are
merged on the way in, so the chunk under the pointer is usually inside
one covering forty others; dropping it would take all forty. The smoke
removes the middle chunk of a merged row of four and checks that three
survive.

The shared map engine gained three optional host hooks - the picker
state, the selection, and a redraw signal. The public site defines none
of them and gets a read-only map, which is the point: one engine, and
what a surface may do is what it declares, not what it is trusted to
avoid calling.

Verified: 12/12 gates. Two test defects fixed on the way: the stub canvas
had no setLineDash, so a dashed selection failed the run with a TypeError
that read like a page bug; and the tidy comparison went through
JSON.stringify, which called a difference in key order a difference in
the answer.
…rs (#144)

`normalizeRects` sliced its INPUT at 256 rectangles, and the removal path
expanded the selection to one rectangle per chunk before filtering. So a
20x20 region — one rectangle, 400 chunks — became 400 rectangles, then
399, then the first 256. A hundred and forty-three chunks disappeared
with no error, no warning, and nothing to say which ones.

The first test used a row of four chunks, which is exactly why it stayed
green. Restoring the slice now fails with "removing one chunk from 400
left 256".

Removal no longer expands anything. It splits the rectangle AROUND the
chunk into the at most four pieces beside it, which touches only the
rectangle involved and cannot grow the list by more than three, whatever
the selection's size. The test now removes an interior chunk, a corner,
an edge, the only chunk, and one that was never selected — and checks
every one of the other 399 is covered exactly once, so a split that
overlapped would fail too.

The same slice had two more consequences, both fixed by refusing instead
of trimming:

  An API caller who POSTed a hundred scattered chunks got a 200 with
  sixty-four of them stored. `checkArea` counted the chunks and the
  emptiness but never noticed rectangles had been dropped. There is now
  a named `too-many-rects`, documented beside `too-many-chunks`, and the
  cap is checked before normalising as well as after — a shape too
  complex to store is refused, never silently trimmed. Three hundred
  chunks in a ROW still pass, because they merge to one rectangle.

  The panel's own tidy had no cap at all, so above 256 it and the app
  disagreed about what had been selected. The panel now splits the same
  way, and the smoke checks its answer against `subtractChunk`'s on the
  400-chunk case rather than only on four.

Also: the desktop editor was permanently open, against its own comment
saying it should not be — drawing areas and editing them are two
decisions, which is how the web panel already had it. And
`forgetServerAreas` was written, documented as "called when a server is
forgotten", and called by nothing; `removeServer` calls it now, so a
later server issued the same id cannot inherit someone else's
annotations.

Verified: 12/12 gates.
Copilot AI review requested due to automatic review settings July 29, 2026 13:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CaYatur
CaYatur merged commit b9983d1 into main Jul 29, 2026
1 check passed
@CaYatur
CaYatur deleted the feat/chunk-areas branch July 29, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Named chunk areas: colour a region, name it, and let everyone read the note

2 participants