Commit graph

2 commits

Author SHA1 Message Date
c696b4ef41 refactor(grid): GridData is a map, not a widget -- delete UIGrid's drawable half
closes #361
closes #370
closes #371

UIGrid inherited both UIDrawable and GridData, so every map carried a second
camera and RenderTexture that nothing on screen corresponded to. Appending a
_GridData to a scene drew the whole map again through that ghost, frozen at
construction values. GridData is now pure data -- no position, no size, no
render() -- and UIGridView is the only widget.

UIGrid is deleted rather than demoted: shedding UIDrawable left an empty
subclass whose only job was to be what an aliasing shared_ptr pointed into.
GridData absorbs the texture (it defines the cell size, which every tile<->pixel
conversion depends on) and its own PythonObjectCache serial; the binding layer
becomes struct PyGridData. PyObjectsEnum::UIGRID and its 14 case arms go with it.

  * mcrfpy.GridData is public and standalone-constructible. A map with no view
    at all -- an offscreen level, still steppable and pathable -- was previously
    undefined behavior: GridData::markDirty did static_cast<UIGrid*>(this).
  * UIGridView::init constructs the GridData directly instead of building a
    throwaway Python _GridData, stealing its base subobject, and copying ~15
    fields of rendering state out of the discarded wrapper. One init() serves
    both modes, so a second view is no longer a crippled kwarg subset
    (Grid(grid=other, z_index=5) used to raise TypeError).
  * entity.grid returns the GridData. Accepted break: an entity may be on a map
    with no view or with several, so "the grid an entity is in" is not a camera.
    Migrate player.grid.center_camera(p) -> view.center_camera(p).
  * tp_name is mcrfpy.GridView; mcrfpy.Grid is an alias to the same type object.

Two latent bugs surfaced and are fixed here:

  #370  Grid.center_camera() and Grid.size = ... were SILENT NO-OPS -- neither
        was defined on the view, so both delegated to the ghost camera. On
        master, center_camera((5,5)) leaves center_x at 50.0.
  #371  The shader uniform binder strcmp'd tp_name == "mcrfpy.Grid" (the VIEW's
        name) then cast to PyUIGridObject. UB that only worked because both
        wrappers had identical layout and both classes put UIDrawable at offset 0.

Suite 327/327.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 07:14:43 -04:00
85e4bbec07 fix(grid): revive Grid input — move cell callbacks and hit-testing to GridView; N-view registry
Since the #252 Grid/GridView split, mcrfpy.Grid(...) constructs a UIGridView
(PyObjectsEnum::UIGRIDVIEW), but every input dispatch site gated on
derived_type() == UIGRID — the internal mcrfpy._GridData type that no scene-graph
node is ever an instance of. All grid input was dead: cell click/enter/exit
callbacks never fired, clicks on a grid's child drawables and entities never
dispatched, and hover inside grids did nothing.

The cell-callback family was split three ways, which is the actual bug: storage
on GridData, firing on UIGrid (it needed UIDrawable::serial_number), and the
camera required to resolve a screen pixel into a cell on UIGridView. So merely
re-pointing the enum gate at UIGRIDVIEW would still report wrong cells for any
panned or zoomed grid, and UIGrid::fireCellClick's subclass lookup resolved the
serial of the throwaway _GridData wrapper that init_with_data DECREFs — meaning
a Python subclass overriding on_cell_click as a method could never dispatch.

Input ownership now lives entirely on UIGridView:
  - on_cell_click/enter/exit, hovered_cell, last_clicked_cell and the subclass
    dispatch cache move off GridData/UIGrid (hovered_cell must be per-view: two
    views over one GridData would otherwise ping-pong exit/enter).
  - UIGridView::click_at descends children (reverse z) -> entities -> cell.
    Children keep grid-world pixel coordinates: that is by design (#360), not an
    inconsistency with Frame's frame-local children.
  - The six derived_type()==UIGRID gates are replaced by virtuals on UIDrawable
    (dispatchCellClick, updateHover, asGridData) so no caller switches on
    grid-ness again.
  - Fixes a prerequisite bug: UIGridView never overrode onPositionChanged(), so
    box and position diverged and a repositioned Grid rendered at the stale spot
    while hit-testing against the new one. Any cell hit-test was meaningless
    until these agreed.
  - grid.perspective was silently dead (the FOV overlay gated on a
    UIGridView::perspective_enabled that nothing ever set); perspective state
    moves to the view with the rest.

#359 is fused into this commit rather than sequenced after it: the shared-data
back-reference is rewritten by the same hunks that move the callbacks off
GridData, and separating them would mean authoring an intermediate GridData.h /
UIGridView.cpp that never existed. GridData::owning_view (a single weak_ptr, for
a relationship that is N views to one GridData) becomes a vector<weak_ptr>
registry with register/unregister. The registry is kept rather than deleted: the
markCompositeDirty push is NOT redundant with #351's content_generation poll,
because it propagates bottom-up through each view's ancestor chain (e.g. a
Frame(use_render_texture=True) wrapping a view), which a top-down render-time
poll cannot do.

Grid subclass method dispatch, panned/zoomed cell resolution, grid-child clicks,
and cell hover are all covered by new regression tests. tests/unit/
test_grid_cell_events.py previously printed "PARTIAL (events may require
interactive mode)" and exited 0 having fired zero events — that dishonest pass is
why this shipped broken; it now asserts.

Suite: 322/322.

closes #355
closes #359

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:40 -04:00