Commit graph

6 commits

Author SHA1 Message Date
7952d25cad fix(grid): UICollection.repr names Grids again instead of "UIDrawable"
UICollection::repr tallied contents by derived_type(), feeding its "Grid"
bucket from PyObjectsEnum::UIGRID and routing UIGRIDVIEW into other_count.
Since #252 every mcrfpy.Grid in a scene graph is a UIGRIDVIEW, so grids were
never named: repr(scene.children) reported "1 Frame, 2 UIDrawables".

Dispatch on the asGridData() virtual (added in #355) rather than the enum,
which covers both the view and the internal _GridData type.

This was the last dead derived_type()==UIGRID gate in a user-visible path --
the same bug class #355 was opened to eliminate. One live UIGRID arm remains
in UIDrawable::removeFromParent, but it is not dead (grid children genuinely
do get parent = the internal UIGrid today) and it is redundant rather than
broken; it disappears with #361.

closes #366
2026-07-12 20:26:32 -04:00
540c793e4e docs(grid): document grid-world child coordinates and the overlay pattern
Grid children are positioned in the grid's pixel-world coordinates — the same
origin as Entity positions — while Frame children are frame-local. Neither was
documented, so the difference read as an inconsistency (it surfaced during #355,
which had to decide whether to preserve it).

It is not an inconsistency. A Frame has no camera and cannot pan its content, so
frame-local is effectively screen space; a Grid does have a camera, and its
children are deliberately anchored to world content, not to the viewport. Testing
grid children at their visible coordinates would break all positioning the moment
the camera moved. The use case is diegetic UI — speech bubbles, damage numbers,
range indicators — that belongs to a place or an entity in the world.

Documents the convention on both Grid.children and Frame.children (each pointing
at the other), and adds docs/grid-coordinate-spaces.md with the overlay pattern
for screen-space UI over a grid: a sibling Frame matching the Grid's pos/size,
rather than Grid.children.

Note: docs/ is gitignored wholesale with tracked files force-added, so the guide
needed `git add -f` — it would otherwise have been silently dropped.

closes #360

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:02:29 -04:00
521d99916a fix(imgui): scene explorer can expand grid children and entities
ImGuiSceneExplorer::renderDrawableNode switched on derived_type() with a UIGRID
arm that nothing in a scene graph has matched since #252; UIGRIDVIEW fell through
to `default: break`, so a Grid node in the explorer tree could not be expanded to
reveal its children or entities.

Uses UIDrawable::asGridData() (#355) rather than adding a UIGRIDVIEW enum arm.
Debug tooling only — but the explorer's blindness here is part of why the whole
#355 family stayed invisible for so long.

closes #358

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:02:11 -04:00
086c886d70 fix(grid): find()/findAll() descend into grids again
McRFPy_API::find_in_collection gated its descent into a grid's entities and
overlay children on derived_type() == PyObjectsEnum::UIGRID — dead since #252,
because every mcrfpy.Grid in a scene graph is a UIGRIDVIEW. Named entities and
named child drawables inside a Grid were simply unfindable.

Uses the UIDrawable::asGridData() virtual added by #355 rather than adding a
UIGRIDVIEW arm next to the dead UIGRID one, which would only relocate the bug.

Two defects found in review of the now-reachable code:
  - find_in_grid_entities built Entity wrappers with a raw tp_alloc, bypassing
    the PythonObjectCache lookup every other entity accessor performs. When the
    duplicate wrapper was deallocated, PyUIEntityType's tp_dealloc unconditionally
    ran `self->data->pyobject = nullptr`, dropping the entity's #266 strong
    identity reference without a DECREF. It now consults the cache first, exactly
    as UIEntityCollection::getitem does, so find('hero') returns the user's Hero
    instance rather than a base Entity.
  - With N views sharing one GridData (the split-screen/minimap case), the
    asGridData() recursion visited the shared data once per view, so findAll()
    returned every grid entity and child N times. find_in_collection now threads
    a visited-GridData set.

closes #357

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:02:04 -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
54624b353d Untrack generated docs; add API manifest infra and pre-commit hook
Rendered/generated documentation no longer lives in git -- it's regenerated
by a pre-commit hook (tools/hooks/pre-commit, installed via `make
install-hooks`) and shipped inside release artifacts (tools/package.sh) instead.
A compact api/manifest.json (signatures + docstring hashes + carried-forward
lifecycle metadata: since/modified per object and member) is the one exception
and stays committed, refreshed by the same hook, so API changes remain
trackable by commit/tag via `git show <ref>:api/manifest.json` and
`tools/api_delta.py REF1 REF2` without needing a full rebuild at old refs.

- tools/generate_api_manifest.py: engine-side introspection, run headless;
  emits api/manifest.json (committed) and docs/generated/api_full.json
  (gitignored, full docstrings -- the docs-site generator's data source).
- tools/api_delta.py: stdlib-only diff CLI (md/json/gitea-checklist formats),
  with --site-dir to cross-reference affected docs-site pages via their mcrf
  frontmatter.
- tools/hooks/pre-commit: incremental build + frozen-docstring gate + doc
  regen + manifest refresh, gated on staged src/ changes.
- .gitignore covers the rendered docs/stubs plus personal working-notes docs
  (plans, audits, research write-ups) that were never McRogueFace-user-facing.
- docs/api-stability.md and docs/threading-model.md removed: their content is
  now the wiki's "API Stability Contract" and "Threading Model" system pages
  (ratified 2026-07-02, migration verified 2026-07-11); the repo continues to
  enforce them via tests/unit/api_surface_snapshot_test.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 20:57:31 -04:00