Source code for mimic_scene_edition.editors.actor_editor

"""
Actor editing for scene manipulation.

The :class:`ActorEditor` provides methods to edit individual actors
(vehicles, pedestrians, etc.) in a scene. Access via ``scene.actor(id)``.

Example
-------
>>> # Access an actor and apply transformations
>>> scene.actor(4).shift(left=2)
>>> scene.actor(4).displace(forward=10).rotate(yaw=15)

>>> # Method chaining for complex edits
>>> scene.actor(4) \\
...     .displace(forward=10) \\
...     .shift(left=2) \\
...     .rotate(yaw=15) \\
...     .stop(at_idx=50)
"""

from __future__ import annotations
from typing import Optional, Literal

import numpy as np

from mimic_data_api.scene import Scene
from mimic_data_api.actor import Actor
from mimic_data_api.trajectory_numpy import WorldTrajectory

from mimic_scene_edition.editors.base import (
    log_method,
    check_value_limits,
    EditionDict,
    validate_referential,
    Referential,
    Axis,
)
from mimic_scene_edition.editors.trajectory_editor import TrajectoryEditor
from mimic_scene_edition.strategies.stop import build_graceful_stop_T_seq

__all__ = ["ActorEditor"]

#: How close an actor timestamp has to be to a frame's to count as that frame.
_GRID_ATOL = 1e-6


[docs] class ActorEditor: """Editor for manipulating individual actors in a scene. Provides comprehensive methods for transforming actor trajectories, controlling motion, and modifying actor properties. All transformation methods return ``self`` to enable fluent method chaining. Parameters ---------- root : SceneEditor The parent scene editor that owns this actor. actor_id : int The unique identifier of the actor to edit. limited_mode : bool, optional If True, enable parameter value limit checking. Default is False. value_limits : dict[str, tuple[float, float]] | None, optional Dictionary mapping parameter names to (min, max) bounds. editions : list[EditionDict] | None, optional List to append edition records to. Attributes ---------- actor_id : int The actor's unique identifier. actor : Actor The underlying Actor object being edited. traj : WorldTrajectory The actor's trajectory. Notes ----- ActorEditor instances should be obtained through ``SceneEditor.actor(id)`` rather than instantiated directly. Examples -------- Basic translation: >>> scene.actor(4).shift(left=2) # Move 2m left >>> scene.actor(4).shift(forward=5, up=0.5) # Forward and up Path displacement (moves along trajectory path): >>> scene.actor(4).displace(forward=10) # 10m ahead on path >>> scene.actor(4).displace(forward=10).shift(left=2) # Combine with lateral offset Stop the actor: >>> scene.actor(4).stop(at_idx=30) # Stop at frame 30 >>> scene.actor(4).stop(at_idx=30, decel=0) # Instant stop Method chaining: >>> scene.actor(4) \\ ... .displace(forward=10) \\ ... .shift(left=2) \\ ... .rotate(yaw=15) \\ ... .stop(at_idx=50) """
[docs] def __init__( self, root: "SceneEditor", actor_id: int, *, limited_mode: bool = False, value_limits: dict[str, tuple[float, float]] | None = None, editions: list[EditionDict] | None = None, ): self._root = root self._id = int(actor_id) self.limited_mode = limited_mode self.value_limits = value_limits or {} self._editions = editions if editions is not None else []
@property def actor_id(self) -> int: """The actor's unique identifier. Returns ------- int The actor ID. """ return self._id @property def scene(self) -> Scene: """The scene containing this actor. Returns ------- Scene The parent scene object. """ return self._root.scene @property def actor(self) -> Actor: """The underlying Actor object. Returns ------- Actor The actor being edited. """ idx = self._root._idx_from_ref(self._id) # id -> current index return self.scene.actors[idx] @property def traj(self) -> WorldTrajectory: """The actor's trajectory. Returns ------- WorldTrajectory The actor's world trajectory. """ return self.actor.trajectory # ---------- helpers ---------- def _te(self) -> TrajectoryEditor: """Create a fresh TrajectoryEditor bound to the live trajectory. Returns ------- TrajectoryEditor A new editor for the actor's trajectory. """ return TrajectoryEditor( traj=self.traj, limited_mode=self.limited_mode, value_limits=self.value_limits, editions=self._editions, ) def _commit_traj(self, te: TrajectoryEditor) -> None: """Commit trajectory changes from the editor to the actor. Parameters ---------- te : TrajectoryEditor The trajectory editor with modifications. """ self.actor.trajectory = te.traj @staticmethod def _window( N: int, start_idx: Optional[int], end_idx: Optional[int] ) -> tuple[int, int]: """Validate and normalize frame index window bounds. Parameters ---------- N : int Total number of frames. start_idx : int | None Start index (None means 0). end_idx : int | None End index exclusive (None means N). Returns ------- tuple[int, int] Validated (start, end) indices. Raises ------ ValueError If the window bounds are invalid. """ i0 = 0 if start_idx is None else int(start_idx) i1 = N if end_idx is None else int(end_idx) if not (0 <= i0 <= i1 <= N): raise ValueError(f"Invalid window [{i0}, {i1}) for length {N}.") return i0, i1 # ---------- frame indices ---------- # Every index an op takes counts the scene's frames — the ego's samples — # not this actor's own. Annotations are often coarser than the rig: an # actor boxed at 2 Hz under a 10 Hz ego has 21 samples beside 100 frames, # and frame 20 is its sample 4. An actor sampled on the frames keeps its # indices untouched. def _off_grid(self) -> Optional[np.ndarray]: """The scene's frame times, or ``None`` when this actor is sampled on them.""" tf = np.asarray(self.scene.ego_vehicle.trajectory.timestamps, float) ta = np.asarray(self.traj.timestamps, float) if len(tf) == 0 or ( len(tf) == len(ta) and np.allclose(tf, ta, rtol=0.0, atol=_GRID_ATOL) ): return None return tf def _sample(self, frame: int) -> int: """The actor sample a frame stops on: the last one at or before its time. Raises ------ ValueError If the scene has no such frame. """ tf = self._off_grid() if tf is None: return int(frame) i = int(frame) if not 0 <= i < len(tf): raise ValueError(f"frame {i} is outside the scene's {len(tf)} frames") ta = np.asarray(self.traj.timestamps, float) return max(0, int(np.searchsorted(ta, tf[i] + _GRID_ATOL, side="right")) - 1) def _span( self, start_idx: Optional[int], end_idx: Optional[int] ) -> tuple[Optional[int], Optional[int]]: """The actor samples a frame window ``[start_idx, end_idx)`` covers. Those whose time falls inside it; a window that starts at the first frame or ends at the last also keeps the samples beyond it. ``None`` stays ``None``. """ tf = self._off_grid() if tf is None: return start_idx, end_idx n = len(tf) i0 = 0 if start_idx is None else int(start_idx) i1 = n if end_idx is None else int(end_idx) if not (0 <= i0 <= i1 <= n): raise ValueError(f"Invalid window [{i0}, {i1}) for the scene's {n} frames.") ta = np.asarray(self.traj.timestamps, float) def edge(i: int) -> int: if i == 0: return 0 if i == n: return len(ta) return int(np.searchsorted(ta, tf[i] - _GRID_ATOL, side="left")) return ( None if start_idx is None else edge(i0), None if end_idx is None else edge(i1), ) def _frames_held(self) -> str: """The scene frames this actor has samples in, for an error message.""" ta = np.asarray(self.traj.timestamps, float) if len(ta) == 0: return "it has none" tf = self._off_grid() if tf is None: return f"it is in frames 0-{len(ta) - 1}" first = int(np.searchsorted(tf, ta[0] - _GRID_ATOL, side="left")) last = int(np.searchsorted(tf, ta[-1] + _GRID_ATOL, side="right")) - 1 return f"it is in frames {first}-{last}" def _per_sample( self, T: np.ndarray, start_idx: Optional[int], end_idx: Optional[int] ) -> tuple[np.ndarray, Optional[int], Optional[int]]: """Re-index a per-frame ``T`` onto the samples its frame window covers. Each sample reads the transform of the frame it falls on. A single ``(4, 4)`` transform needs no re-indexing, only the window does. """ T = np.asarray(T, float) s0, s1 = self._span(start_idx, end_idx) tf = self._off_grid() if tf is None or T.ndim != 3: return T, s0, s1 i0 = 0 if start_idx is None else int(start_idx) i1 = len(tf) if end_idx is None else int(end_idx) if T.shape[0] != i1 - i0: raise ValueError( f"T must be (4,4) or (K,4,4) with K={i1 - i0}, the frames in " f"[{i0}, {i1}); got {T.shape}." ) ta = np.asarray(self.traj.timestamps, float)[s0 or 0 : s1] frames = np.searchsorted(tf, ta + _GRID_ATOL, side="right") - 1 return T[np.clip(frames, i0, i1 - 1) - i0], s0, s1
[docs] @check_value_limits @log_method def translate(self, **xyz: float) -> "ActorEditor": """Translate the actor using semantic direction components. Alias for :meth:`shift`. Applies a local-frame translation using intuitive direction names. Parameters ---------- **xyz : float Named translation components: - ``forward``: Distance forward (positive X) in meters - ``backward``: Distance backward (negative X) in meters - ``left``: Distance left (positive Y) in meters - ``right``: Distance right (negative Y) in meters - ``up``: Distance up (positive Z) in meters - ``down``: Distance down (negative Z) in meters Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).translate(forward=5, left=2) See Also -------- shift : Primary translation method. """ te = self._te() te.translate(**xyz) self._commit_traj(te) return self
[docs] @check_value_limits @log_method @validate_referential("referential") def rotate( self, *, roll: float = 0.0, pitch: float = 0.0, yaw: float = 0.0, referential: Referential = "local", start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Rotate the actor using Euler angles. Applies a rotation to the actor's trajectory using roll-pitch-yaw Euler angles in degrees. Parameters ---------- roll : float, optional Rotation around the X (forward) axis in degrees. Default is 0. pitch : float, optional Rotation around the Y (left) axis in degrees. Default is 0. yaw : float, optional Rotation around the Z (up) axis in degrees. Default is 0. referential : {"local", "world"}, optional Coordinate frame for the rotation: - ``"local"``: Rotation in actor's local frame (default). - ``"world"``: Rotation in world frame. start_idx : int | None, optional First frame to rotate (inclusive). None means all frames. end_idx : int | None, optional Last frame to rotate (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).rotate(yaw=90) # Turn 90° left >>> scene.actor(4).rotate(yaw=180) # Face opposite direction See Also -------- transform : Combined translation and rotation. """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.rotate( roll=roll, pitch=pitch, yaw=yaw, referential=referential, start_idx=start_idx, end_idx=end_idx, ) self._commit_traj(te) return self
[docs] @check_value_limits @log_method @validate_referential("referential") def transform( self, *, left: float = 0.0, right: float = 0.0, up: float = 0.0, down: float = 0.0, forward: float = 0.0, backward: float = 0.0, roll: float = 0.0, pitch: float = 0.0, yaw: float = 0.0, referential: Referential = "local", start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Apply both translation and rotation in a single operation. Combines translation and rotation into a single SE(3) transformation applied to the specified frame range. Parameters ---------- left : float, optional Distance left (positive Y) in meters. Default is 0. right : float, optional Distance right (negative Y) in meters. Default is 0. up : float, optional Distance up (positive Z) in meters. Default is 0. down : float, optional Distance down (negative Z) in meters. Default is 0. forward : float, optional Distance forward (positive X) in meters. Default is 0. backward : float, optional Distance backward (negative X) in meters. Default is 0. roll : float, optional Rotation around X axis in degrees. Default is 0. pitch : float, optional Rotation around Y axis in degrees. Default is 0. yaw : float, optional Rotation around Z axis in degrees. Default is 0. referential : {"local", "world"}, optional Coordinate frame: - ``"local"``: Transform in actor's local axes (default). - ``"world"``: Transform in world axes. start_idx : int | None, optional First frame to transform (inclusive). None means all frames. end_idx : int | None, optional Last frame to transform (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).transform(forward=5, left=2, yaw=15) See Also -------- shift : Translation only. rotate : Rotation only. """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.transform( left=left, right=right, up=up, down=down, forward=forward, backward=backward, roll=roll, pitch=pitch, yaw=yaw, referential=referential, start_idx=start_idx, end_idx=end_idx, ) self._commit_traj(te) return self
[docs] @check_value_limits @log_method @validate_referential("referential") def shift( self, *, forward: float = 0.0, backward: float = 0.0, left: float = 0.0, right: float = 0.0, up: float = 0.0, down: float = 0.0, referential: Referential = "local", start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Translate the actor using semantic direction components. Pure translation using intuitive direction names. The coordinate system follows vehicle convention (x-forward, y-left, z-up). Parameters ---------- forward : float, optional Distance forward (positive X) in meters. Default is 0. backward : float, optional Distance backward (negative X) in meters. Default is 0. left : float, optional Distance left (positive Y) in meters. Default is 0. right : float, optional Distance right (negative Y) in meters. Default is 0. up : float, optional Distance up (positive Z) in meters. Default is 0. down : float, optional Distance down (negative Z) in meters. Default is 0. referential : {"local", "world"}, optional Coordinate frame: - ``"local"``: Components in actor's local axes (default). Applied as post-multiplication. - ``"world"``: Components in world axes. Applied as pre-multiplication. start_idx : int | None, optional First frame to shift (inclusive). None means all frames. end_idx : int | None, optional Last frame to shift (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).shift(left=2) # Move 2m left >>> scene.actor(4).shift(forward=5, up=0.5) # Forward and up >>> scene.actor(4).shift(right=3, referential="world") # In world coords See Also -------- translate : Alias for shift. displace : Move along trajectory path. """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.transform( forward=forward, backward=backward, left=left, right=right, up=up, down=down, referential=referential, start_idx=start_idx, end_idx=end_idx, ) self._commit_traj(te) return self
[docs] def object_appearance(self, enabled: bool) -> "ActorEditor": """Enable or disable the AutoSplat per-frame appearance residual for this actor. The appearance residual carries the actor's dynamic, per-frame look (brake lights, blinkers, and other view-dependent overlays). Muting it renders the actor with its static baseline appearance only. This toggle only takes visible effect inside the rendering pipeline, which installs a callback via ``scene._object_appearance_sink``. In a pure offline editor the call is still recorded in :attr:`editions` (so it replays with the rest of the edit history) but is otherwise a no-op. Parameters ---------- enabled : bool ``True`` to render the per-frame appearance residual for this actor, ``False`` to mute it. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(28).object_appearance(False) # mute this actor >>> scene.actor(28).object_appearance(True) # restore it See Also -------- SceneEditor.object_appearance : Toggle every actor at once. """ self._root._apply_object_appearance(self._id, bool(enabled)) return self
[docs] @log_method @validate_referential("referential") def apply_T_sequence( self, T: np.ndarray, *, start_idx: Optional[int] = None, end_idx: Optional[int] = None, referential: Referential = "local", ) -> "ActorEditor": """Apply a sequence of 4x4 transformation matrices. Applies SE(3) transformation matrices to the actor's trajectory poses in the specified frame range. Parameters ---------- T : np.ndarray Transformation matrices with shape ``(4, 4)`` for a single transform applied to all frames, or ``(N, 4, 4)`` for per-frame transforms, one per scene frame in the window. An actor sampled off the frames reads each sample's transform at the frame it falls on. start_idx : int | None, optional First frame to transform (inclusive). None means 0. end_idx : int | None, optional Last frame to transform (exclusive). None means end. referential : {"local", "world"}, optional How to apply the transformation: - ``"local"``: Right-multiply (pose @ T). Default. - ``"world"``: Left-multiply (T @ pose). Returns ------- ActorEditor Self for method chaining. Examples -------- >>> T = np.eye(4) >>> T[:3, 3] = [1, 0, 0] # Translation >>> scene.actor(4).apply_T_sequence(T) """ T, start_idx, end_idx = self._per_sample(T, start_idx, end_idx) te = self._te() te.apply_T_sequence( T, start_idx=start_idx, end_idx=end_idx, referential=referential ) self._commit_traj(te) return self
# ---------- Strategies / maneuvers ----------
[docs] @log_method def displace_along_forward( self, meters: float, *, start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Displace the actor along its trajectory path by arc-length. Shortcut for ``displace(forward=meters)``. Parameters ---------- meters : float Distance to displace in meters. Positive moves forward. start_idx : int | None, optional First frame to displace (inclusive). None means all frames. end_idx : int | None, optional Last frame to displace (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).displace_along_forward(10) # 10m ahead on path See Also -------- displace : Displacement with named direction parameters. """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.displace_along_forward(meters, start_idx=start_idx, end_idx=end_idx) self._commit_traj(te) return self
[docs] @check_value_limits @log_method def displace( self, *, forward: float = 0.0, backward: float = 0.0, start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Move the actor along its trajectory path by arc-length. Unlike :meth:`shift`, this moves the actor along the *path it's already following*, not in a straight line. This is useful for positioning actors relative to their motion path. Parameters ---------- forward : float, optional Distance to move forward along path in meters. Default is 0. backward : float, optional Distance to move backward along path in meters. Default is 0. start_idx : int | None, optional First frame to displace (inclusive). None means all frames. end_idx : int | None, optional Last frame to displace (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).displace(forward=10) # 10m ahead on path >>> scene.actor(4).displace(backward=5) # 5m behind on path >>> # Combine with shift for lateral offset >>> scene.actor(4).displace(forward=10).shift(left=2) See Also -------- shift : Straight-line translation. displace_along_forward : Shortcut with single distance parameter. """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.displace( forward=forward, backward=backward, start_idx=start_idx, end_idx=end_idx, ) self._commit_traj(te) return self
[docs] @log_method def move_forward( self, *, start_idx: int, speed: float, ) -> "ActorEditor": """Make the actor move forward at a constant speed. Modifies the trajectory so the actor moves forward at the specified speed from the given frame index. Parameters ---------- start_idx : int Frame index to start the constant motion. speed : float Speed in meters per second. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).move_forward(start_idx=10, speed=5.0) # 5 m/s from frame 10 """ # The sequence is built on the actor's own samples, so it is applied # there rather than through :meth:`apply_T_sequence`, which reads frames. start, _ = self._span(start_idx, None) te = self._te() T_seq = te.move_local_with_speed(start_idx=start, speed=speed, axis="x") te.apply_T_sequence(T_seq, start_idx=start, referential="local") self._commit_traj(te) return self
[docs] @log_method def stop( self, at_idx: int, *, duration: int | None = None, decel: int = 10, profile: Literal["linear", "min_jerk"] = "min_jerk", ) -> "ActorEditor": """Stop the actor at a specific frame. Brings the actor to a stop at the specified frame index, optionally with a smooth deceleration profile. Parameters ---------- at_idx : int Frame index to stop at. If ``decel > 0``, this is where the deceleration ends (actor becomes stationary). duration : int | None, optional Number of frames to stay stopped. If None, the actor remains stopped until the end of the trajectory. decel : int, optional Number of frames for gradual deceleration. Use 0 for instant stop. Default is 10. profile : {"linear", "min_jerk"}, optional Deceleration profile: - ``"linear"``: Linear velocity reduction. - ``"min_jerk"``: Smooth minimum-jerk profile (default). Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).stop(at_idx=30) # Stop at frame 30 >>> scene.actor(4).stop(at_idx=30, decel=0) # Instant stop >>> scene.actor(4).stop(at_idx=30, duration=50) # Stop for 50 frames See Also -------- make_static_at : Make completely static at a position. """ te = self._te() k = self._sample(at_idx) if decel > 0: # ``decel`` counts frames too: the ramp starts on the sample the # frame ``decel`` before the stop lands on. d = k - self._sample(max(0, at_idx - decel)) T_seq, i_start, i_end = build_graceful_stop_T_seq( te.traj, at_idx=k, decel=d, profile=profile ) if T_seq.shape[0] > 0: te.apply_T_sequence( T_seq, start_idx=i_start, end_idx=i_end, referential="local" ) if duration is None: te.freeze_from(k) else: N = len(te.traj.object2world) tf = self._off_grid() frames = N if tf is None else len(tf) _, end = self._span(at_idx, min(frames, at_idx + duration)) te.freeze_window(k, min(N, end), persist_tail=True) self._commit_traj(te) return self
[docs] @log_method def spin( self, *, period_s: Optional[float] = None, rpm: Optional[float] = None, axis: Axis = "z", start_idx: Optional[int] = None, end_idx: Optional[int] = None, ) -> "ActorEditor": """Apply continuous rotation (spinning) to the actor. Makes the actor rotate continuously around the specified axis. Parameters ---------- period_s : float | None, optional Seconds per full revolution. Mutually exclusive with ``rpm``. rpm : float | None, optional Revolutions per minute. Mutually exclusive with ``period_s``. axis : {"x", "y", "z"}, optional Rotation axis. Default is ``"z"`` (vertical). start_idx : int | None, optional First frame to apply spin (inclusive). None means all frames. end_idx : int | None, optional Last frame to apply spin (exclusive). None means all frames. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).spin(rpm=60, axis="z") # Spin at 60 RPM """ start_idx, end_idx = self._span(start_idx, end_idx) te = self._te() te.spin( period_s=period_s, rpm=rpm, axis=axis, start_idx=start_idx, end_idx=end_idx ) self._commit_traj(te) return self
[docs] @log_method def make_static_at(self, *, at_idx: int = 0) -> "ActorEditor": """Make the actor completely static at a specific frame's position. Anchors the actor's trajectory at the specified frame, making the actor remain completely stationary at that position for the entire duration. Parameters ---------- at_idx : int, optional Frame index to anchor the trajectory at. Default is 0. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).make_static_at(at_idx=0) # Freeze at initial position See Also -------- stop : Stop with optional deceleration. """ tr = self.traj t = np.asarray(tr.timestamps, float) N = len(t) if len(t) == 0: return self frames = self._off_grid() if frames is not None: at_idx = self._sample(int(np.clip(at_idx, 0, len(frames) - 1))) i = int(np.clip(at_idx, 0, len(t) - 1)) anchor = np.asarray(tr.object2world[i], float) new_traj = tr.model_copy(deep=True) new_traj.object2world = np.broadcast_to(anchor, (N, 4, 4)).copy() new_traj.linear_velocity = np.zeros((N, 3), float) new_traj.angular_velocity = np.zeros((N, 3), float) self.replace_trajectory(new_traj) return self
# ---------- Temporal ----------
[docs] @log_method def offset_frames(self, offset_frames: int) -> "ActorEditor": """Shift the actor's timeline by a number of frames. Performs a circular shift of the trajectory, wrapping around at the boundaries. Parameters ---------- offset_frames : int Number of frames to shift. Positive values shift forward. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).offset_frames(5) # Shift 5 frames forward """ te = self._te() te.offset_frames(int(offset_frames)) self._commit_traj(te) return self
[docs] @log_method def resample(self, hz: float) -> "ActorEditor": """Resample the trajectory to a target frame rate. Interpolates the trajectory to produce poses at a uniform temporal spacing determined by the specified frequency. Parameters ---------- hz : float Target frame rate in Hz (frames per second). Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).resample(hz=20) # Resample to 20 Hz See Also -------- resample_to_times : Resample to specific timestamps. """ te = self._te() te.resample_to_rate(float(hz)) self._commit_traj(te) return self
[docs] @log_method def resample_to_times(self, new_t: np.ndarray) -> "ActorEditor": """Resample the trajectory to specific timestamps. Interpolates poses to the specified query timestamps. Parameters ---------- new_t : np.ndarray Array of target timestamps to sample at. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> new_times = np.linspace(0, 10, 100) >>> scene.actor(4).resample_to_times(new_times) See Also -------- resample : Resample to a uniform frame rate. """ te = self._te() te.resample_to_times(np.asarray(new_t, float)) self._commit_traj(te) return self
[docs] @log_method def slice(self, start_idx: int, end_idx: int) -> "ActorEditor": """Keep only frames in the specified range. Truncates the trajectory to contain only poses within the specified index range. Parameters ---------- start_idx : int First frame to keep (inclusive). end_idx : int Last frame to keep (exclusive). Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).slice(10, 50) # Keep frames 10-49 Raises ------ ValueError If the window holds none of the actor's samples — which would leave it with no trajectory at all. """ s0, s1 = self._span(int(start_idx), int(end_idx)) if s0 == s1: raise ValueError( f"Frames [{start_idx}, {end_idx}) hold none of actor {self.actor.id}'s " f"samples ({self._frames_held()}); slice would leave it with no trajectory." ) te = self._te() te.slice(int(s0), int(s1)) self._commit_traj(te) return self
[docs] @check_value_limits @log_method def speed(self, multiplier: float) -> "ActorEditor": """Change the actor's apparent speed by retiming timestamps. Adjusts trajectory timestamps to change perceived speed without modifying the spatial path. Parameters ---------- multiplier : float Speed scaling factor: - ``> 1``: Faster (double speed with 2.0) - ``< 1``: Slower (half speed with 0.5) Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).speed(multiplier=2.0) # Double speed >>> scene.actor(4).speed(multiplier=0.5) # Half speed """ te = self._te() te.scale_speed(multiplier=multiplier) self._commit_traj(te) return self
# ---------- Utilities ----------
[docs] @log_method def replace_trajectory(self, traj: WorldTrajectory) -> "ActorEditor": """Replace the actor's entire trajectory. Substitutes the current trajectory with a new one. Velocities are recomputed automatically. Parameters ---------- traj : WorldTrajectory The new trajectory to use. Returns ------- ActorEditor Self for method chaining. Examples -------- >>> new_traj = create_custom_trajectory() >>> scene.actor(4).replace_trajectory(new_traj) """ te = self._te() te.replace(traj) self._commit_traj(te) return self
[docs] @log_method def set_dimensions( self, *, length: float | None = None, width: float | None = None, height: float | None = None, ) -> "ActorEditor": """Modify the actor's bounding box dimensions. Updates the actor's physical dimensions. Only provided parameters are updated. Parameters ---------- length : float | None, optional New length in meters (X dimension). width : float | None, optional New width in meters (Y dimension). height : float | None, optional New height in meters (Z dimension). Returns ------- ActorEditor Self for method chaining. Examples -------- >>> scene.actor(4).set_dimensions(length=4.5, width=1.8) """ a = self.actor if length is not None: a.dimensions.length = float(length) if width is not None: a.dimensions.width = float(width) if height is not None: a.dimensions.height = float(height) return self
[docs] @log_method def duplicate(self, *, forward: float = 0.0, backward: float = 0.0) -> Actor: """Create a copy of the actor with optional displacement. Creates a deep copy of the actor, optionally displaced along its trajectory path. The copy is **not** inserted into the scene; use ``scene.insert_actor()`` to add it. Parameters ---------- forward : float, optional Distance to displace forward on path in meters. Default is 0. backward : float, optional Distance to displace backward on path in meters. Default is 0. Returns ------- Actor A new Actor object (not yet in scene). Examples -------- >>> new_actor = scene.actor(4).duplicate(forward=20) >>> new_id = scene.insert_actor(new_actor) See Also -------- SceneEditor.insert_actor : Insert the duplicated actor. SceneEditor.duplicate_actor : Duplicate and insert in one step. """ base = self.actor dup = base.model_copy(deep=True) dist = float(forward or -backward) if abs(dist) > 1e-12: te = TrajectoryEditor( traj=dup.trajectory, limited_mode=self.limited_mode, value_limits=self.value_limits, editions=self._editions, ) te.displace_along_forward(dist) dup.trajectory = te.traj return dup
class RemovedActorEditor(ActorEditor): """What :meth:`SceneEditor.actor` hands back for an actor that is gone. ``Removed`` is sticky. A script that keeps the Python name a removal invalidated and edits through it does not bring the actor back, so every op on this receiver is a no-op that returns the receiver — a fluent chain still chains, and the scene is left exactly as the removal left it. The alternative, raising, would make the rule unrepresentable: the recorder accepts the stale edit by design (see :mod:`mimic_scene_edition.oplist.ir`), and a record no receiver can execute is a record the op-list cannot replay. """ def __repr__(self) -> str: # pragma: no cover - debugging aid return f"RemovedActorEditor(actor_id={self._id})" def _ignored(name: str): """A no-op stand-in for ``ActorEditor.<name>`` that keeps the chain going.""" def op(self, *args, **kwargs): return self op.__name__ = name op.__qualname__ = f"RemovedActorEditor.{name}" op.__doc__ = "No-op: the actor this editor names was removed from the scene." return op # Every public op, tombstoned in one place: a method added to ``ActorEditor`` # later is silently covered rather than silently resurrecting a removed actor. for _name, _member in list(vars(ActorEditor).items()): if not _name.startswith("_") and callable(_member): setattr(RemovedActorEditor, _name, _ignored(_name)) del _name, _member