fix(grid): overlay children belong to the GridView, not the shared GridData

A grid child (speech bubble, marker, range indicator) is an OVERLAY painted over
the map through one camera -- it occupies no cell, nothing collides with it, and
the turn manager cannot see it. Entities are the world contents. So children are
per-view and entities are shared, not the other way around.

Ownership follows from that. A UIDrawable has one `parent`, but a GridData may
have N views, so a child owned by the data could only ever name one of them --
arbitrarily -- and would dangle if that view died while the others kept drawing
it. That is why children's dirty push dead-ended at the internal UIGrid, which is
in no scene: a Frame(cache_subtree=True) above the grid was never told to
re-composite and re-blitted a stale cache. Owned by the view, a child's parent is
a real scene-graph drawable and the dead-end cannot be expressed.

- GridData::children/children_need_sort -> UIGridView; UIGrid::get_children ->
  UIGridView::get_children (collection owner = the view, so UICollection::append
  already parents and invalidates correctly)
- UIDrawable::removeFromParent drops its UIGRID arm; set_parent rejects _GridData
- UIGrid::render's child-drawing block deleted (the ghost path has no children);
  UIGrid::resize's aligned-child loop moved to UIGridView::resize, which never had
  one -- a resized grid was not re-anchoring aligned children at all
- PyScene hover, mcrfpy.find()/findAll(), and the ImGui explorer read the view's
  children. find() no longer dedups children per-GridData (two views hold
  different children); entities still dedup.

Also fixes a latent staleness this exposed: UIGridView::render's #351 early-out
never consulted its own render_dirty. It survived on content_generation plus a
"grid has children -> always full render" carve-out -- but removing the LAST child
lifts that carve-out, leaving every other input unchanged, so the view re-blitted a
raster with the removed child still baked in. can_skip now requires !render_dirty,
and render() clears it after rasterizing.

mcrfpy.Grid IS the UIGridView, so grid.children.append(...) is unchanged for every
single-view script; only the internal _GridData loses `children` (API snapshot:
delegation 62 -> 61, public surface identical).

Answers #361's Q1 (one child, one view, one screen pixel) and moots its Q2 (no
non-drawable parent kind remains to represent), unblocking it.

Test: tests/regression/issue_364_grid_children_on_view_test.py -- 17 assertions;
the load-bearing ones screenshot a grid inside a Frame(cache_subtree=True) and
assert move/recolor/remove of a child change the pixels. All three were
byte-identical before. Suite 326/326.

Spun out: #368 (markContentDirty's was_dirty latch swallows every invalidation
after the first -- not grid-specific), #369 (.parent allocates a fresh wrapper, so
`child.parent is parent` is always False).

closes #364
This commit is contained in:
John McCardle 2026-07-12 23:06:59 -04:00
commit 3e18c1fbf6
16 changed files with 327 additions and 139 deletions

File diff suppressed because one or more lines are too long