Source code for frontend._scene_object_

# File: _scene_object_.py
# Code: Claude Code and Codex
# Review: Ryoichi Ando (ryoichi.ando@zozo.com)
# License: Apache v2.0

"""The :class:`Object` class.

An ``Object`` is a placed instance of a registered asset. It carries a
4x4 transform, per-vertex colors, pins, stitches, and material
parameters. Instances are created via ``scene.add(...)`` in
``_scene_.py`` and are typically configured through chainable methods
defined on this class.

The class is re-exported from :mod:`frontend._scene_` for backward
compatibility.
"""

from typing import Optional, Union

import numpy as np

from . import _rust  # type: ignore[attr-defined]

from ._asset_ import AssetManager
from ._param_ import ParamHolder, object_param
from ._scene_fixed_ import EnumColor
from ._scene_pin_ import PinHolder
from ._scene_transform_ import (
    TransformAnimation,
    _apply_transform_to_verts,
    _axis_angle_to_quat,
    _quat_multiply,
    _quat_to_mat3,
)

EPS = 1e-3


[docs] class Object: """The object class. An ``Object`` is a placed instance of a registered asset. It carries a 4x4 transform, per-vertex colors, pins, stitches, and material parameters. Instances are created via ``scene.add("<mesh_name>")`` and are typically configured through chainable methods. Example: Chain placement, material, and pinning onto a cloth sheet:: sheet = scene.add("sheet").at(0, 0.6, 0).jitter() sheet.param.set("strain-limit", 0.05) sheet.pin(sheet.grab([-1, 0, -1]) + sheet.grab([1, 0, -1])) """ def __init__(self, asset: AssetManager, name: str): self._asset = asset self._name = name self._static = False self._param = ParamHolder(object_param(self.obj_type)) self.clear() @property def name(self) -> str: """Get name of the object. Example: Read the asset reference name back from an object:: obj = scene.add("sheet") assert obj.name == "sheet" """ return self._name @property def static(self) -> bool: """Get whether the object is static. Example: Pinning every vertex turns an object into a static collider:: obj = scene.add("sphere").pin().object assert obj.static """ return self._static @property def param(self) -> ParamHolder: """Get the material parameters of the object. Returns: ParamHolder: The material parameters of the object. Example: Configure the Young's modulus through the returned holder:: scene.add("sheet").param.set("young-mod", 1e5) """ return self._param @property def obj_type(self) -> str: """Get the type of the object. Returns: str: The type of the object, either "rod", "tri", or "tet". Example: Branch on object topology when iterating the scene:: for obj in scene.object_dict.values(): if obj.obj_type == "tet": obj.param.set("young-mod", 1e6) """ return self._asset.fetch.get_type(self._name) @property def object_color(self) -> Optional[list[float]]: """Get the object color. Example: Read the RGB color previously assigned via :meth:`color`:: obj = scene.add("sheet").color(0.9, 0.3, 0.3) print(obj.object_color) """ color = self._color if color is None: return None elif isinstance(color, list): return color else: return color.tolist() @property def position(self) -> list[float]: """Get the object translation from the transform matrix. Example: Read back the translation set by :meth:`at`:: obj = scene.add("sheet").at(0, 0.6, 0) assert obj.position == [0.0, 0.6, 0.0] """ return self._transform[:3, 3].tolist() @property def object_velocity(self) -> list[float] | np.ndarray: """Get the object velocity. Example: Inspect the initial velocity set via :meth:`velocity`:: obj = scene.add("sheet").velocity(0, 0, -1) print(obj.object_velocity) """ return self._velocity @property def uv_coords(self) -> Optional[list[np.ndarray]]: """Get the UV coordinates. Example: Check whether the asset carries UV data for texturing:: obj = scene.add("sheet") if obj.uv_coords is not None: print(obj.uv_coords[0].shape) """ return self._uv @property def dynamic_color(self) -> EnumColor: """Get the dynamic color type. Example: Inspect the dynamic color mode previously selected:: obj = scene.add("sheet") print(obj.dynamic_color) """ return self._dyn_color @property def dynamic_intensity(self) -> float: """Get the dynamic color intensity. Example: Read the scalar intensity used by dynamic coloring:: obj = scene.add("sheet") print(obj.dynamic_intensity) """ return self._dyn_intensity @property def pin_list(self) -> list[PinHolder]: """Get the list of pin holders. Example: Iterate pin holders attached to an object:: obj = scene.add("sheet") obj.pin([0, 1, 2]) for holder in obj.pin_list: print(len(holder.index)) """ return self._pin
[docs] def clear(self): """Clear the object data. Example: Reset an object's transform and pin state:: obj = scene.select("sheet") obj.clear() obj.at(0, 0.6, 0) """ self._transform = np.eye(4) # Single 4x4 matrix for all transforms self._color: Union[np.ndarray, list[float], None] = None self._dyn_color = EnumColor.NONE self._dyn_intensity = 1.0 self._static_color = [0.75, 0.75, 0.75] self._default_color = [1.0, 0.85, 0.0] self._velocity = [0.0, 0.0, 0.0] self._velocity_schedule = [] self._collision_windows = [] self._pin: list[PinHolder] = [] self._normalize = False self._stitch = None self._uv = None self._transform_animation: Optional[TransformAnimation] = None # Flagged by the decoder when the object is a static-moving mesh # driven by a pin-shell (either fcurve or UI-op). Propagates into # PinData.hide_in_preview so the JupyterLab viewer doesn't draw # the all-vertex pin as user-facing pin markers. self._is_static_moving = False
[docs] def report(self): """Report the object data. Example: Print a summary of an object after configuring it:: obj = scene.add("sheet").at(0, 0.6, 0) obj.pin(obj.grab([-1, 0, -1])) obj.report() """ print("transform:") print(self._transform) print("color:", self._color) print("velocity:", self._velocity) print("normalize:", self._normalize) self.update_static() if self.static: print("pin: static") else: print("pin:", sum([len(p.index) for p in self._pin]))
[docs] def bbox(self) -> tuple[np.ndarray, np.ndarray]: """Compute the bounding box of the object. Returns: tuple[np.ndarray, np.ndarray]: The dimensions and center of the bounding box. Example: Read a sheet's size and center in world space:: sheet = scene.add("sheet") size, center = sheet.bbox() print(size, center) """ vert = self.get("V") if vert is None: raise Exception("vertex does not exist") # apply_transform handles both the normalize and non-normalize # branches; passing identity to the bbox kernel just iterates # the already-transformed buffer. transformed = self.apply_transform(vert, False) v_in = np.ascontiguousarray(transformed, dtype=np.float64) M = np.eye(4, dtype=np.float64) size, center = _rust.scene_object_bbox(v_in, M) return (size, center)
[docs] def normalize(self) -> "Object": """Normalize the object so that it fits within a unit cube. Returns: Object: The normalized object. Example: Normalize a tetrahedral asset before scaling it into place:: arm = scene.add("armadillo").normalize().scale(0.75) arm.at(0, 1, 0) """ _rust.scene_validate_object_normalize(bool(self._normalize)) self._bbox, self._center = self.bbox() self._normalize = True return self
[docs] def get(self, key: str) -> Optional[np.ndarray]: """Get an associated value of the object with respect to the key. Args: key (str): The key of the value. Returns: Optional[np.ndarray]: The value associated with the key. Example: Fetch the rest-pose vertices and face list of an asset:: sheet = scene.add("sheet") V = sheet.get("V") F = sheet.get("F") """ if key == "color": if self._color is not None: return np.array(self._color) else: if self.static: return np.array(self._static_color) else: return np.array(self._default_color) elif key == "Ind": if self._stitch is not None: return self._stitch[0] else: return None elif key == "W": if self._stitch is not None: return self._stitch[1] else: return None else: result = self._asset.fetch.get(self._name) if key in result: return result[key] else: return None
[docs] def vertex(self, translate: bool) -> np.ndarray: """Get the transformed vertices of the object. Args: translate (bool): Whether to translate the vertices. Returns: np.ndarray: The transformed vertices. Example: Read world-space vertex positions after ``.at`` has been set:: sheet = scene.add("sheet").at(0, 0.6, 0) world_vert = sheet.vertex(True) """ vert = self.get("V") if vert is None: raise Exception("vertex does not exist") else: return self.apply_transform(vert, translate)
[docs] def grab(self, direction: list[float], eps: float = 1e-3) -> list[int]: """Select vertices that are furthest along a specified direction. Args: direction (list[float]): The direction vector. eps (float, optional): Tolerance (in dot-product units) from the maximum. Defaults to 1e-3. Returns: list[int]: The indices of the selected vertices. Example: Pin the two top corners of a sheet to hang it:: sheet = scene.add("sheet") sheet.pin(sheet.grab([-1, 1, 0]) + sheet.grab([1, 1, 0])) """ vert = self.vertex(False) v = np.ascontiguousarray(vert, dtype=np.float64) d = [float(direction[0]), float(direction[1]), float(direction[2])] return [int(i) for i in _rust.scene_grab_indices(v, d, float(eps))]
[docs] def mat4x4(self, matrix: np.ndarray) -> "Object": """Set the full 4x4 transformation matrix directly. Replaces the current transform entirely. The matrix is applied as:: world_pos = matrix[:3,:3] @ local_pos + matrix[:3,3] Args: matrix (np.ndarray): A 4x4 transformation matrix. Returns: Object: The object with the updated transform. Example: Apply a pre-computed transform imported from Blender:: import numpy as np M = np.eye(4) M[:3, 3] = [0, 0.6, 0] scene.add("sheet").mat4x4(M) """ self._transform = np.asarray(matrix, dtype=np.float64).reshape(4, 4) return self
@property def transform_matrix(self) -> np.ndarray: """Get the current 4x4 transformation matrix. Example: Inspect the composed translation, rotation, and scale:: obj = scene.add("sheet").at(0, 0.6, 0) print(obj.transform_matrix) """ return self._transform
[docs] def at(self, x: float, y: float, z: float) -> "Object": """Set the translation component of the transform. Args: x (float): The x-coordinate. y (float): The y-coordinate. z (float): The z-coordinate. Returns: Object: The object with the updated position. Example: Drop a sheet 0.6 units above the origin:: scene.add("sheet").at(0, 0.6, 0) """ self._transform[:3, 3] = [x, y, z] return self
[docs] def jitter(self, r: float = 1e-2) -> "Object": """Add random jitter to the translation. Args: r (float, optional): The jitter magnitude. Returns: Object: The object with the jittered position. Example: Break symmetry for a falling armadillo on a trampoline:: scene.add("armadillo").at(0, 1, 0).jitter().velocity(0, -5, 0) """ self._transform[0, 3] += r * np.random.random() self._transform[1, 3] += r * np.random.random() self._transform[2, 3] += r * np.random.random() return self
[docs] def scale(self, _scale: float) -> "Object": """Apply uniform scale to the transform. Args: _scale (float): The scale factor. Returns: Object: The object with the updated scale. Example: Shrink an armadillo to 0.75 of its original size:: scene.add("armadillo").scale(0.75).at(0, 1, 0) """ M = np.ascontiguousarray(self._transform, dtype=np.float64) self._transform = _rust.scene_mat4_apply_scale(M, float(_scale)) return self
[docs] def rotate(self, angle: float, axis: str) -> "Object": """Apply rotation around a specified axis to the transform. Args: angle (float): The rotation angle in degrees. axis (str): The rotation axis ('x', 'y', or 'z'). Returns: Object: The object with the updated rotation. Example: Stand a sheet upright by rotating 90 degrees around x:: scene.add("sheet").rotate(90, "x").at(0, 0.5, 0) """ a = _rust.scene_validate_object_rotate_axis(axis) M = np.ascontiguousarray(self._transform, dtype=np.float64) self._transform = _rust.scene_mat4_apply_rotate(M, float(angle), a) return self
[docs] def move( self, delta, t_start: float = 0.0, t_end: float = 1.0 ) -> "Object": """Animate the static object by a translational delta over time. Args: delta: [dx, dy, dz] translation delta in world space. t_start (float): Start time in seconds. t_end (float): End time in seconds. Returns: Object: The object with the animation added. Example: Slide a static (fully pinned) collider along +x between t=0 and t=2:: scene.add("sphere").at(-1, 0, 0).pin() scene.select("sphere").move([2, 0, 0], t_start=0.0, t_end=2.0) """ delta = np.asarray(delta, dtype=np.float64) _rust.scene_validate_time_window(float(t_start), float(t_end)) self._ensure_transform_animation() anim = self._transform_animation start_trans = anim.translations[-1].copy() start_quat = anim.quaternions[-1].copy() start_scale = anim.scales[-1].copy() if anim.times[-1] < t_start: anim.times.append(t_start) anim.translations.append(start_trans.copy()) anim.quaternions.append(start_quat.copy()) anim.scales.append(start_scale.copy()) anim.times.append(t_end) anim.translations.append(start_trans + delta) anim.quaternions.append(start_quat.copy()) anim.scales.append(start_scale.copy()) return self
[docs] def animate_rotate( self, axis, angle: float, center=None, t_start: float = 0.0, t_end: float = 1.0, ) -> "Object": """Animate the static object by rotating around an axis over time. Args: axis: [ax, ay, az] rotation axis (will be normalized). angle (float): Rotation angle in degrees. center: Center of rotation [cx, cy, cz]. Defaults to object centroid. t_start (float): Start time in seconds. t_end (float): End time in seconds. Returns: Object: The object with the animation added. Example: Rotate a pinned static collider 180 degrees around y over 2 seconds:: roller = scene.add("cylinder").at(0, 0.5, 0).pin() scene.select("cylinder").animate_rotate( [0, 1, 0], 180.0, t_start=0.0, t_end=2.0, ) """ axis = np.asarray(axis, dtype=np.float64) _rust.scene_validate_time_window(float(t_start), float(t_end)) self._ensure_transform_animation() anim = self._transform_animation start_trans = anim.translations[-1].copy() start_quat = anim.quaternions[-1].copy() start_scale = anim.scales[-1].copy() if center is None: center = np.mean( _apply_transform_to_verts(anim.local_vert, start_trans, start_quat, start_scale), axis=0, ) else: center = np.asarray(center, dtype=np.float64) rot_quat = _axis_angle_to_quat(axis, angle) R = _quat_to_mat3(rot_quat) new_trans = center + R @ (start_trans - center) new_quat = _quat_multiply(rot_quat, start_quat) if anim.times[-1] < t_start: anim.times.append(t_start) anim.translations.append(start_trans.copy()) anim.quaternions.append(start_quat.copy()) anim.scales.append(start_scale.copy()) anim.times.append(t_end) anim.translations.append(new_trans) anim.quaternions.append(new_quat) anim.scales.append(start_scale.copy()) return self
def _ensure_transform_animation(self): """Initialize TransformAnimation from current transform if not already set.""" if self._transform_animation is not None: return vert = self.get("V") if vert is None: raise ValueError("Object has no vertices; cannot create transform animation") translation, quat, scale = _rust.scene_decompose_trs( np.ascontiguousarray(self._transform, dtype=np.float64) ) self._transform_animation = TransformAnimation( local_vert=vert.copy(), times=[0.0], translations=[translation], quaternions=[quat], scales=[scale], )
[docs] def max(self, dim: str) -> float: """Get the maximum coordinate value along a specified dimension. Args: dim (str): The dimension to get the maximum value along, either "x", "y", or "z". Returns: float: The maximum coordinate value. Example: Check that a sheet sits above y=0:: sheet = scene.add("sheet").at(0, 0.6, 0) assert sheet.min("y") >= 0 print(sheet.max("y")) """ ax = {"x": 0, "y": 1, "z": 2}[dim] v = np.ascontiguousarray(self.vertex(True), dtype=np.float64) _, hi = _rust.scene_axis_min_max(v, ax) return float(hi)
[docs] def min(self, dim: str) -> float: """Get the minimum coordinate value along a specified dimension. Args: dim (str): The dimension to get the minimum value along, either "x", "y", or "z". Returns: float: The minimum coordinate value. Example: Lift an object so its lowest point sits at y=0:: obj = scene.add("armadillo") obj.at(0, -obj.min("y"), 0) """ ax = {"x": 0, "y": 1, "z": 2}[dim] v = np.ascontiguousarray(self.vertex(True), dtype=np.float64) lo, _ = _rust.scene_axis_min_max(v, ax) return float(lo)
[docs] def apply_transform(self, x: np.ndarray, translate: bool) -> np.ndarray: """Apply the object's transformation to a set of vertices. Args: x (np.ndarray): The vertices to transform (N, 3). translate (bool): Whether to include the translation component. Returns: np.ndarray: The transformed vertices (N, 3). Example: Manually map a batch of points through the object's current transform:: obj = scene.add("sheet").at(0, 0.6, 0) world_pts = obj.apply_transform(obj.get("V"), True) """ if len(x.shape) == 1: raise Exception("vertex should be 2D array") v_in = np.ascontiguousarray(x, dtype=np.float64) M = np.ascontiguousarray(self._transform, dtype=np.float64) if self._normalize: bbox = np.ascontiguousarray(self._bbox, dtype=np.float64) center = np.ascontiguousarray(self._center, dtype=np.float64) return _rust.scene_apply_transform_batch( v_in, M, bool(translate), bbox, center, ) return _rust.scene_apply_transform_batch(v_in, M, bool(translate))
[docs] def static_color(self, red: float, green: float, blue: float) -> "Object": """Set the static color of the object. Args: red (float): The red component. green (float): The green component. blue (float): The blue component. Returns: Object: The object with the updated static color. Example: Tint a fully pinned collider a light gray:: scene.add("sphere").pin() scene.select("sphere").static_color(0.75, 0.75, 0.75) """ self._static_color = [red, green, blue] return self
[docs] def default_color(self, red: float, green: float, blue: float) -> "Object": """Set the default color of the object. Args: red (float): The red component. green (float): The green component. blue (float): The blue component. Returns: Object: The object with the updated default color. Example: Override the auto-assigned hue for a dynamic object:: scene.add("sheet").default_color(1.0, 0.85, 0.0) """ self._default_color = [red, green, blue] return self
[docs] def color(self, red: float, green: float, blue: float) -> "Object": """Set the color of the object. Args: red (float): The red component. green (float): The green component. blue (float): The blue component. Returns: Object: The object with the updated color. Example: Color a falling armadillo light gray:: scene.add("armadillo").color(0.75, 0.75, 0.75) """ self._color = [red, green, blue] return self
[docs] def vert_color(self, color: np.ndarray) -> "Object": """Set the vertex colors of the object. Args: color (np.ndarray): The vertex colors. Returns: Object: The object with the updated vertex colors. Example: Paint each vertex of a sheet with its own RGB:: import numpy as np sheet = scene.add("sheet") n = len(sheet.get("V")) sheet.vert_color(np.random.rand(n, 3)) """ self._color = color return self
[docs] def direction_color(self, x: float, y: float, z: float) -> "Object": """Set the color along the direction of the object. Args: x (float): The x-component of the direction. y (float): The y-component of the direction. z (float): The z-component of the direction. Returns: Object: The object with the updated color. Example: Shade a cylinder along its long axis:: scene.add("cylinder").direction_color(1, 0, 0) """ vertex = self.vertex(False) color = _rust.scene_direction_color( np.ascontiguousarray(vertex, dtype=np.float64), [float(x), float(y), float(z)], ) return self.vert_color(color)
[docs] def cylinder_color( self, center: list[float], direction: list[float], up: list[float] ) -> "Object": """Set the color along the cylinder direction. Args: center (list[float]): The center of the cylinder. direction (list[float]): The direction of the cylinder. up (list[float]): The up vector of the cylinder. Returns: Object: The object with the updated color. Example: Apply a cylinder gradient used in the twist demo:: obj = scene.add("cylinder").cylinder_color( [0, 0, 0], [1, 0, 0], [0, 1, 0], ) """ vertex = self.vertex(False) color = _rust.scene_cylinder_color( np.ascontiguousarray(vertex, dtype=np.float64), [float(center[0]), float(center[1]), float(center[2])], [float(direction[0]), float(direction[1]), float(direction[2])], [float(up[0]), float(up[1]), float(up[2])], ) return self.vert_color(color)
[docs] def dyn_color(self, color: str, intensity: float = 0.75) -> "Object": """Set the dynamic color of the object. Args: color (str): The dynamic color type. Currently only ``"area"`` is supported. intensity (float, optional): Blend intensity of the dynamic color. Defaults to 0.75. Returns: Object: The object with the updated dynamic color. Example: Highlight stretched triangles on a trampoline sheet:: scene.add("sheet").dyn_color("area", 1.0) """ if not _rust.scene_is_supported_dyn_color(color): raise Exception("invalid color type") self._dyn_color = EnumColor.AREA self._dyn_intensity = intensity return self
[docs] def velocity(self, u: float, v: float, w: float, t: float = 0.0) -> "Object": """Set the velocity of the object. Args: u (float): The velocity in the x-direction. v (float): The velocity in the y-direction. w (float): The velocity in the z-direction. t (float): Time in seconds. 0.0 sets initial velocity; >0 adds a timed override. Returns: Object: The object with the updated velocity. Example: Give an armadillo a downward initial velocity of 5 m/s:: scene.add("armadillo").at(0, 1, 0).velocity(0, -5, 0) """ _rust.scene_validate_object_not_static(bool(self.static)) replace, vel = _rust.scene_classify_velocity_entry( float(u), float(v), float(w), float(t) ) if replace: self._velocity = np.array(list(vel)) else: self._velocity_schedule.append((t, list(vel))) return self
[docs] def velocity_schedule(self, schedule: list) -> "Object": """Set a list of (time, [vx, vy, vz]) velocity overrides. Example: Kick an object, then stop it a second later:: obj = scene.add("armadillo").at(0, 1, 0) obj.velocity_schedule([ (0.0, [0, -5, 0]), (1.0, [0, 0, 0]), ]) """ self._velocity_schedule = schedule return self
[docs] def collision_windows(self, windows: list) -> "Object": """Set collision active time windows: list of (t_start, t_end) pairs. Example: Enable contact only during t in [0.2, 1.0] and [2.0, 3.0]:: scene.add("sheet").collision_windows([(0.2, 1.0), (2.0, 3.0)]) """ self._collision_windows = windows return self
[docs] def update_static(self): """Recompute whether the object is static. When every vertex is pinned and no pin carries operations, a pull strength, or an unpin time, the object is treated as static. The result is cached on ``self._static``. Example: Typically invoked internally by :meth:`Scene.build` after pins are finalized, but can be called directly to refresh the cached flag after editing pin data in place:: obj = scene.select("sheet") obj.pin() obj.update_static() assert obj.static """ if not self._pin: self._static = False return for p in self._pin: if len(p.operations) > 0 or p.pull_strength or p.unpin_time is not None: return vert = self.get("V") if vert is None: self._static = False return self._static = _rust.scene_all_vertices_pinned( int(len(vert)), [list(p.index) for p in self._pin], )
[docs] def pin(self, ind: Optional[list[int]] = None) -> PinHolder: """Set specified vertices as pinned. An object with every vertex pinned is a *static collider*: its motion is prescribed, not simulated. Use the returned :class:`PinHolder` to animate it via :meth:`PinHolder.move_by`, :meth:`PinHolder.move_to`, or :meth:`PinHolder.transform_keyframes`. Args: ind (Optional[list[int]], optional): The indices of the vertices to pin. If None, all vertices are pinned. Defaults to None. Returns: PinHolder: The pin holder. Example: Static sphere that slides across the scene:: (scene.add("sphere") .at(-1, 0, 0) .pin() .move_by([8, 0, 0], t_start=0.0, t_end=5.0)) """ if ind is None: vert: np.ndarray = self.vertex(False) ind = list(range(len(vert))) holder = PinHolder(self, ind) self._pin.append(holder) return holder
[docs] def stitch(self, name: str) -> "Object": """Apply stitch to the object. Args: name (str): The name of stitch registered in the asset manager. Returns: Object: The stitched object. Example: Attach a glue stitch registered earlier in the asset manager:: app.asset.add.stitch("glue", stitch_data) scene.add("dress").stitch("glue").rotate(-90, "x") """ # Static check first (matches Python source: avoids asset fetch # when the object is already pinned every-vertex). _rust.scene_validate_object_not_static(bool(self.static)) stitch = self._asset.fetch.get(name) _rust.scene_validate_stitch_attach( False, "Ind" in stitch, "W" in stitch ) self._stitch = (stitch["Ind"], stitch["W"]) return self
[docs] def set_uv(self, uv: list[np.ndarray]) -> "Object": """Set the UV coordinates of the object. Args: uv (list[np.ndarray]): The UV coordinates for each face. Returns: Object: The object with the updated UV coordinates. Example: Supply per-face UVs for a triangulated sheet:: import numpy as np sheet = scene.add("sheet") n_faces = len(sheet.get("F")) uv = [np.zeros((3, 2), dtype=np.float32) for _ in range(n_faces)] sheet.set_uv(uv) """ _rust.scene_validate_set_uv_obj_type(self.obj_type) self._uv = uv return self
[docs] def direction(self, _ex: list[float], _ey: list[float]) -> "Object": """Set two orthogonal directions of a shell required for Baraff-Witkin model. Args: _ex (list[float]): The 3D x-direction vector. _ey (list[float]): The 3D y-direction vector. Returns: Object: The object with the updated direction. Example: Pin the warp and weft directions of a flat sheet in the xz plane:: sheet = scene.add("sheet") sheet.param.set("model", "baraff-witkin") sheet.direction([1, 0, 0], [0, 0, 1]) """ vert, tri = self.vertex(False), self.get("F") if vert is None: raise Exception("vertex does not exist") if tri is None: raise Exception("face does not exist") try: uv_arr = _rust.scene_uv_from_directions( np.ascontiguousarray(vert, dtype=np.float64), np.ascontiguousarray(tri, dtype=np.int64), [float(_ex[0]), float(_ex[1]), float(_ex[2])], [float(_ey[0]), float(_ey[1]), float(_ey[2])], EPS, ) except ValueError as e: raise Exception(str(e)) self._uv = [uv_arr[i] for i in range(uv_arr.shape[0])] return self