Source code for simvx.core.nodes_2d.remote_transform
"""RemoteTransform2D -- Pushes transform to a remote node."""
from __future__ import annotations
import logging
from ..descriptors import Property
from ..math.types import Vec2
from .node2d import Node2D
log = logging.getLogger(__name__)
[docs]
class RemoteTransform2D(Node2D):
"""Pushes this node's transform to a remote Node2D each frame.
Useful for making a node follow another without parenting, or for
synchronising transforms across different branches of the scene tree.
Set ``remote_path`` to a node path (e.g. ``'../Camera2D'``). Each frame
the resolved node's position/rotation/scale are overwritten with this
node's values, according to the ``update_*`` flags.
"""
remote_path = Property("", hint="Path to the target node (e.g. '../Camera2D')")
update_position = Property(True, hint="Push position to remote node")
update_rotation = Property(True, hint="Push rotation to remote node")
update_scale = Property(False, hint="Push scale to remote node")
use_global_coordinates = Property(True, hint="Use global transforms instead of local")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cached_remote: Node2D | None = None
self._cached_path: str = ""
def _resolve_remote(self) -> Node2D | None:
"""Resolve remote_path to a Node2D, caching the result."""
path = self.remote_path
if not path:
self._cached_remote = None
self._cached_path = ""
return None
if path == self._cached_path and self._cached_remote is not None:
# Verify the cached node is still in the tree
if self._cached_remote._tree is not None:
return self._cached_remote
try:
node = self.get_node(path)
except (ValueError, KeyError, AttributeError):
if path != self._cached_path:
log.warning("RemoteTransform2D '%s': invalid remote_path '%s'", self.name, path)
self._cached_remote = None
self._cached_path = path
return None
if not isinstance(node, Node2D):
if path != self._cached_path:
log.warning("RemoteTransform2D '%s': target '%s' is not a Node2D", self.name, path)
self._cached_remote = None
self._cached_path = path
return None
self._cached_remote = node
self._cached_path = path
return node
[docs]
def process(self, dt: float):
remote = self._resolve_remote()
if remote is None:
return
if self.use_global_coordinates:
if self.update_position:
remote.world_position = self.world_position
if self.update_rotation:
# Node2D has no world_rotation setter -- compute local rotation
parent_rot = (
remote.parent.world_rotation
if remote.parent and isinstance(remote.parent, Node2D) else 0.0
)
remote.rotation = self.world_rotation - parent_rot
if self.update_scale:
# Node2D has no world_scale setter -- compute local scale
if remote.parent and isinstance(remote.parent, Node2D):
ps = remote.parent.world_scale
remote.scale = Vec2(
self.world_scale.x / ps.x if ps.x != 0 else 1.0,
self.world_scale.y / ps.y if ps.y != 0 else 1.0,
)
else:
remote.scale = Vec2(self.world_scale)
else:
if self.update_position:
remote.position = Vec2(self.position)
if self.update_rotation:
remote.rotation = self.rotation
if self.update_scale:
remote.scale = Vec2(self.scale)