Source code for simvx.core._drawable2d

"""The shared 2D-drawable concept.

The blanket ``Property.__set__ -> queue_redraw`` hook (``descriptors.py``) only
does anything on a node that actually defines ``queue_redraw``. The build-once
retention path rides that same hook to set a per-item dirty bit, so every 2D
drawable needs it: without it a ``Sprite2D`` colour / texture / visible change
would mark nothing dirty and item-level invalidation would never fire.

This module supplies it as a **small mixin** (:class:`Drawable2D`) carrying the
render-retention state + ``queue_redraw``. A mixin rather than a hoist onto
``Node2D``, because the drawable concept must also cover ``CanvasLayer``, which
is a ``Node`` and *not* a ``Node2D``. The mixin lets ``Node2D`` and
``CanvasLayer`` share one implementation without forcing ``CanvasLayer`` through
``Node2D``'s 2D transform cache. ``Control`` inherits it via ``Node2D`` and
*extends* ``queue_redraw`` additively, so its ``_DrawRecorder`` path keeps
working.

The two dirty bits are deliberately SEPARATE
--------------------------------------------
- :attr:`_render_dirty` -- "this drawable's geometry/appearance changed; its
  item(s) must be re-collected/re-uploaded." Its lifetime is owned by the render
  layer: it persists until the upload step clears it (:meth:`_clear_render_dirty`),
  and is NEVER cleared by a ``world_position`` read.
- :attr:`_transform_render_dirty` -- "this drawable moved; its transform row must
  be rewritten (geometry untouched -- the scroll-by-translation fast path)." Also
  render-owned, also persists until the upload step clears it.

Neither is ``Node2D._transform_dirty``: that flag is cleared *lazily* by any
``world_position``/``world_transform`` read (scene-adapter / collision / audio /
culling all read it mid-frame) and ``_invalidate_transform`` short-circuits its
descendant recursion ``if not self._transform_dirty``. A render-dirty bit layered
on it would inherit that early-return and leave descendants stale after a
read-then-move sequence. So the transform-render bit gets its OWN propagation
with no such short-circuit.

Retention bits
--------------
These render-retention bits are read by the 2D item pipeline's
:class:`RenderItemCache` to decide what to re-collect, re-capture, or
patch in place each frame.
"""

from __future__ import annotations

from .descriptors import Property


[docs] class Drawable2D: """Mixin: render-retention dirty state + ``queue_redraw`` for 2D drawables. Mixed into :class:`~simvx.core.nodes_2d.node2d.Node2D` (so every sprite / shape / Text2D / Control inherits it) and :class:`~simvx.core.nodes_2d.canvas.CanvasLayer` (a ``Node``, covered by the mixin rather than by a ``Node2D`` hoist). The mixin owns no transform: it never reads ``position``/``world_transform``. It only flips render-retention flags, which the item pipeline reads and drains after upload. """ # Class-level defaults so the flags read truthy (first-frame dirty) even on a # subclass instance whose ``__init__`` has not yet run the mixin initialiser, # and so a plain ``hasattr`` probe never raises. Instances shadow these with # their own attribute the moment any mark fires. # # ``_render_dirty`` defaults ``True`` here (overriding the base ``Node``'s # ``False``): a freshly built 2D drawable IS dirty so the item pipeline collects # + uploads it once. The render-dirty methods themselves (``queue_redraw`` / # ``_clear_render_dirty`` / ``render_dirty``) and the public ``dynamic`` attribute # live on the base ``Node`` (any node with an ``on_draw`` shares them); this mixin # only adds the 2D-specific TRANSFORM-render bit + the auto-dirty opt-in below. _render_dirty: bool = True _transform_render_dirty: bool = True # Opt into the blanket ``Property.__set__ -> queue_redraw`` auto-dirty hook # (``descriptors.py``): a colour / text / size / anchor change on a 2D drawable # marks it render-dirty with no per-property list. Base ``Node`` leaves this # ``False`` so a non-2D node's Property write never dirties the 2D scan. # (``visible`` is a plain ``@property`` on ``Node``, NOT a ``Property``, so the # blanket hook can't see it; ``Node._propagate_visibility`` marks render-dirty # directly on every effective-visibility flip instead.) _render_auto_dirty: bool = True # HDR-lane override (N1, 2D-in-HDR). ``None`` (default) = by role: a world-space # drawable renders into the HDR target before tonemap when post-processing is on # (so it gets exposure/tonemap/bloom, consistent with the 3D scene), while a # screen-space drawable (HUD/UI) stays post-tonemap at authored LDR. ``True`` # forces this drawable into the HDR lane; ``False`` forces it out (the escape # hatch for stylised flat 2D that must keep its exact authored colour). Changing # it marks the drawable render-dirty via the blanket Property hook so the new # lane takes effect next frame. hdr = Property( None, hint="HDR participation: None=auto by role, True=tonemap/bloom this 2D, False=keep flat LDR", ) # ``queue_redraw`` / ``_clear_render_dirty`` / ``render_dirty`` are inherited # from the base ``Node`` (the render-dirty bit is a per-``on_draw`` concept, not # a 2D-only one). ``Control`` still OVERRIDES ``queue_redraw`` additively to also # poke its legacy ``_draw_dirty`` recorder cache. # -- transform-render-dirty (moved; geometry untouched) ------------------ def _mark_transform_render_dirty(self) -> None: """Mark this drawable's transform row stale (move / scroll fast path). Separate from :meth:`queue_redraw`: a move rewrites one transform row and leaves geometry alone. Driven by the spatial-Property / ``_ObservedVec`` path and ``rotation``'s on_change. Has its OWN propagation to children (each subclass overrides :meth:`_propagate_transform_render_dirty` to recurse), with **no** ``_transform_dirty`` short-circuit, so a read-then-move sequence still re-dirties descendants. """ self._transform_render_dirty = True self._propagate_transform_render_dirty() def _propagate_transform_render_dirty(self) -> None: """Recurse the transform-render-dirty bit into children. Subclass hook. The base mixin does not know the child taxonomy; ``Node2D`` overrides this to mark its 2D descendants (geometry follows ancestor transforms), and ``CanvasLayer`` marks its whole subtree. Default: no descendants. """ def _clear_transform_render_dirty(self) -> None: """Drain the transform-render bit. Called ONLY by the upload step.""" self._transform_render_dirty = False
[docs] @property def transform_render_dirty(self) -> bool: """Whether the transform row needs a rewrite since the last upload.""" return self._transform_render_dirty