Source code for simvx.core.physics._shape_owner
"""Per-resource ownership of backend collision-shape handles.
A ``Shape`` / ``Shape2D`` resource owns the backend shape it was built into, the
same model :class:`~simvx.core.resources.Mesh` and materials already follow: the
handle lives exactly as long as the resource that asked for it, and is released
when that resource is collected. One resource can be live in several worlds at
once (a scene running a 3D world and a preview world), so the ownership record is
a small per-world table rather than a single handle.
Worlds are held WEAKLY. A world that has already been collected has taken its
whole shape table with it, so there is nothing to release and nothing to keep
alive; holding it strongly would instead make every shape resource pin the world
it was ever used in.
"""
from __future__ import annotations
import weakref
from typing import Any, Protocol
class _ShapeSink(Protocol):
"""The one method this module needs from a physics world."""
def destroy_shape(self, shape: Any) -> None: ...
[docs]
class OwnedShapeHandles:
"""The backend handles one shape resource owns, one per world it was built in.
Keyed by ``id(world)`` for a dict-speed lookup on the build path, with the
world's own weak reference stored alongside so a recycled ``id`` can never be
mistaken for a live entry: the stored reference is compared against the world
being asked about, and a dead world drops its row through the weakref
callback the moment it is collected.
"""
__slots__ = ("_entries", "__weakref__")
def __init__(self) -> None:
self._entries: dict[int, tuple[weakref.ref[Any], Any]] = {}
[docs]
def get(self, world: object) -> Any | None:
"""Return the handle this resource owns in ``world``, or ``None``."""
entry = self._entries.get(id(world))
if entry is None:
return None
ref, handle = entry
return handle if ref() is world else None
[docs]
def put(self, world: object, handle: Any) -> None:
"""Record ``handle`` as this resource's handle in ``world``."""
key = id(world)
entries = self._entries
def _forget(_ref: object, k: int = key) -> None:
entries.pop(k, None)
entries[key] = (weakref.ref(world, _forget), handle)
[docs]
def release(self) -> None:
"""Destroy every handle this resource owns, in every world still alive.
Runs from the resource's finaliser, so it must never touch the resource
itself. Destroying a handle a body is still built on is safe and
deliberate: a backend reaches a body's geometry through the body's own
record, so the body keeps simulating (see ``PhysicsWorld.destroy_shape``).
"""
for ref, handle in list(self._entries.values()):
world: _ShapeSink | None = ref()
if world is not None:
world.destroy_shape(handle)
self._entries.clear()
[docs]
def build_owned(resource: object, world: object, create: Any) -> Any:
"""Return ``resource``'s handle in ``world``, building it once on first ask.
The memo behind the shape contract: a resource asked twice for the same world
hands back the same handle, so every body built from one resource shares one
backend record however many times the resource is consulted. The first build
also arms the finaliser that releases the handles when ``resource`` dies.
Args:
resource: The shape resource that will own the handle.
world: The physics world to build into.
create: Zero-argument factory called only on a miss.
Returns:
The opaque backend shape handle ``resource`` owns in ``world``.
"""
owned: OwnedShapeHandles | None = resource._owned_handles # type: ignore[attr-defined]
if owned is None:
owned = OwnedShapeHandles()
resource._owned_handles = owned # type: ignore[attr-defined]
weakref.finalize(resource, owned.release)
handle = owned.get(world)
if handle is None:
handle = create()
owned.put(world, handle)
return handle