Source code for simvx.core.graphics.texture_slot

"""The one seam a backend publishes a resolved texture handle through."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..node import Node

    _Base = Node
else:
    _Base = object

__all__ = ["TextureSlot"]


[docs] class TextureSlot(_Base): """Mixin for a node that shows a texture and caches the backend's handle. A backend resolves ``node.texture`` to a bindless slot and has to hand that number back to the node. Doing it with a plain attribute write is the bug this mixin removes: the write fires none of the invalidation a :class:`~simvx.core.descriptors.Property` fires, so a node that drew nothing while its slot was still ``-1`` keeps serving that empty draw out of the retained 2D cache forever. It is silent -- an invisible node and no error -- and it was fixed once per backend, per node type, before it was fixed here. So the slot is not assignable. :meth:`publish_texture_slot` is the only route, it invalidates, and it is a no-op when the slot has not moved, which is what lets an offscreen target republish its handle every frame without dirtying anything. The other half is re-resolution. ``_invalidate_texture_slot`` is the ``on_change`` hook on every ``texture`` property: reassigning the texture drops the cached slot, which is what re-opens the backend's resolve gate. Without it the first successful resolve latches and the node draws the old image for the life of the scene. """ # The two fields below are this seam's own storage. Read them through # ``texture_slot`` / ``_texture_id``; write them only through # ``publish_texture_slot`` / ``publish_overlay_slot`` / # ``_invalidate_texture_slot``. A direct write is the bug described above -- # it skips the redraw and the node keeps serving its old draw. #: Backend handle for the image being drawn; ``-1`` until one is published. _texture_slot: int = -1 #: Web only: the same image's id in the 2D overlay table, which is a #: different id space from the 3D material table. _web_overlay_slot: int = -1 @property def _texture_id(self) -> int: """The published backend handle, or ``-1``. Read-only: see :meth:`publish_texture_slot`.""" return self._texture_slot @_texture_id.setter def _texture_id(self, slot: int) -> None: raise AttributeError( f"{type(self).__name__}._texture_id is published by the backend, not assigned. " "Call publish_texture_slot(slot), which also invalidates the node's cached draw." )
[docs] @property def texture_slot(self) -> int: """The published backend handle for the current texture, or ``-1``.""" return self._texture_slot
[docs] def publish_texture_slot(self, slot: int) -> None: """Publish a resolved backend handle and invalidate the cached draw. Idempotent: republishing the handle a node already has does nothing, so a per-frame publish costs one comparison and never dirties the node. """ slot = int(slot) if slot == self._texture_slot: return self._texture_slot = slot self.queue_redraw()
[docs] def publish_overlay_slot(self, slot: int) -> None: """Web: publish the 2D overlay-table handle and draw with it. A combined 2D+3D export may have already published a 3D material-table handle on this node; for a 2D overlay the overlay table is the right one, so it wins. """ self._web_overlay_slot = int(slot) self.publish_texture_slot(slot)
def _invalidate_texture_slot(self) -> None: """Drop the cached handles so the backend resolves the new texture.""" self._texture_slot = -1 self._web_overlay_slot = -1