Source code for dazpy._node

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import TYPE_CHECKING

from ._element import DazElement
from ._script_builder import ScriptBuilder

if TYPE_CHECKING:
    from ._material import DazMaterial
    from ._modifier import DazModifier
    from ._morph import DazMorph
    from ._dforce import DazDForce


[docs] @dataclass class NodeIdentifier: """Identifies a scene node by name or label. Attributes: value: The name or label string used to look up the node. kind: Either ``"name"`` (uses ``Scene.findNode()``) or ``"label"`` (uses ``Scene.findNodeByLabel()``). """ value: str kind: str = "name" # "name" or "label"
[docs] class DazNode(DazElement): """Proxy for a ``DzNode`` in the active DAZ Studio scene. Provides access to transforms, hierarchy, visibility, materials, modifiers, and geometry. Instances are typically obtained from :class:`~dazpy.DazScene` rather than constructed directly. All properties that read from the server may return ``None`` if the node no longer exists in the scene. Args: client: The :class:`~dazpy.DazClient` for remote calls. identifier: Identifies the underlying ``DzNode``. """ def __init__(self, client: "DazClient", identifier: NodeIdentifier): # noqa: F821 locator = ScriptBuilder.find_node_expr(identifier) super().__init__(client, locator) object.__setattr__(self, "_identifier", identifier) @property def label(self) -> str | None: """User-visible display label shown in the Scene panel (read/write).""" script = ScriptBuilder.node_body(self._identifier, "return _node.getLabel();") return self._client.execute(script).value @label.setter def label(self, value: str) -> None: script = ScriptBuilder.node_body( self._identifier, f"_node.setLabel({json.dumps(value)});" ) self._client.execute(script) @property def name(self) -> str | None: """Internal node name used to look up the node (read-only).""" script = ScriptBuilder.node_body(self._identifier, "return _node.getName();") return self._client.execute(script).value @property def position(self) -> dict | None: """World-space position as ``{"x": float, "y": float, "z": float}`` (read-only). Use :meth:`set_position` to change. """ script = ScriptBuilder.node_body( self._identifier, "var p = _node.getWSPos(); return {x: p.x, y: p.y, z: p.z};" ) return self._client.execute(script).value
[docs] def set_position(self, x: float, y: float, z: float) -> None: """Set the world-space position of this node. Args: x: X coordinate in DAZ Studio units (centimetres by default). y: Y coordinate. z: Z coordinate. """ script = ScriptBuilder.node_body( self._identifier, f"_node.setWSPos(new DzVec3({x}, {y}, {z}));" ) self._client.execute(script)
@property def rotation(self) -> dict | None: """World-space rotation as ``{"x", "y", "z", "w"}`` quaternion (read-only). Use :meth:`set_rotation` to change (accepts Euler angles in degrees). """ script = ScriptBuilder.node_body( self._identifier, "var r = _node.getWSRot(); return {x: r.x, y: r.y, z: r.z, w: r.w};" ) return self._client.execute(script).value @property def general_scale(self) -> float | None: """Uniform scale factor (read-only).""" script = ScriptBuilder.node_body( self._identifier, "return _node.getScaleControl().getValue();" ) return self._client.execute(script).value @property def scale(self) -> dict | None: """Per-axis and uniform scale as ``{"x", "y", "z", "general"}`` (read-only). Use :meth:`set_scale` to change the per-axis values. """ script = ScriptBuilder.node_body( self._identifier, "return {x: _node.getXScaleControl().getValue(), y: _node.getYScaleControl().getValue(), z: _node.getZScaleControl().getValue(), general: _node.getScaleControl().getValue()};" ) return self._client.execute(script).value
[docs] def set_scale(self, x: float, y: float, z: float) -> None: """Set per-axis local scale. Does not affect the general/uniform scale dial (:attr:`general_scale`) — DAZ Studio tracks per-axis and uniform scale as separate controls. Args: x: X-axis scale factor (1.0 = unscaled). y: Y-axis scale factor. z: Z-axis scale factor. """ script = ScriptBuilder.node_body( self._identifier, f"_node.getXScaleControl().setValue({x}); _node.getYScaleControl().setValue({y}); _node.getZScaleControl().setValue({z});" ) self._client.execute(script)
[docs] def set_transform( self, position: tuple[float, float, float] | None = None, rotation: tuple[float, float, float] | None = None, scale: tuple[float, float, float] | None = None, ) -> None: """Set any combination of local position, rotation, and scale in one call. Every argument is optional. Omitted components are left untouched. Equivalent to calling :meth:`set_local_position`, :meth:`set_local_rotation`, and/or :meth:`set_scale` individually, but round-trips only once. Args: position: ``(x, y, z)`` local-space position, or ``None`` to leave unchanged. rotation: ``(x, y, z)`` Euler rotation in degrees, or ``None`` to leave unchanged. scale: ``(x, y, z)`` per-axis scale, or ``None`` to leave unchanged. """ lines = [] if position is not None: x, y, z = position lines.append(f"_node.setLocalPos(new DzVec3({x}, {y}, {z}));") if rotation is not None: x, y, z = rotation lines.append( f"_node.getXRotControl().setValue({x}); " f"_node.getYRotControl().setValue({y}); " f"_node.getZRotControl().setValue({z});" ) if scale is not None: x, y, z = scale lines.append( f"_node.getXScaleControl().setValue({x}); " f"_node.getYScaleControl().setValue({y}); " f"_node.getZScaleControl().setValue({z});" ) if not lines: return script = ScriptBuilder.node_body(self._identifier, "\n".join(lines)) self._client.execute(script)
@property def visible(self) -> bool | None: """General visibility flag (read/write). Affects both viewport and render visibility unless overridden by the per-channel visibility setters. """ script = ScriptBuilder.node_body(self._identifier, "return _node.isVisible();") return self._client.execute(script).value @visible.setter def visible(self, value: bool) -> None: flag = "true" if value else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisible({flag});") self._client.execute(script) @property def parent(self) -> DazNode | None: """Parent node in the scene hierarchy, or ``None`` for root nodes.""" script = ScriptBuilder.node_body( self._identifier, "var p = _node.getNodeParent(); return p ? p.getName() : null;" ) name = self._client.execute(script).value if name is None: return None return DazNode(self._client, NodeIdentifier(name)) @property def children(self) -> list[DazNode]: """Direct child nodes.""" script = ScriptBuilder.node_body( self._identifier, """ var names = []; for (var i = 0; i < _node.getNumNodeChildren(); i++) { names.push(_node.getNodeChild(i).getName()); } return names; """ ) names = self._client.execute(script).value or [] return [DazNode(self._client, NodeIdentifier(n)) for n in names]
[docs] def delete(self) -> bool: """Remove this node from the scene entirely. Returns: ``True`` if the node was found and removed, ``False`` otherwise. """ script = ScriptBuilder.node_body(self._identifier, "return Scene.removeNode(_node);") return bool(self._client.execute(script).value)
[docs] def reparent(self, new_parent: "DazNode", *, preserve_world_transform: bool = True) -> None: """Move this node to a new position in the scene hierarchy. Detaches the node from its current parent (if any) and attaches it as a child of *new_parent*, following the ``removeNodeChild`` / ``addNodeChild`` DazScript pattern. Args: new_parent: The node to reparent under. preserve_world_transform: When ``True`` (default), the node keeps its current world-space position/rotation/scale, adjusting its local transform to compensate. When ``False``, the node's local transform values are left unchanged, which may move it in world space. Raises: :class:`~dazpy.exceptions.ScriptRuntimeError`: If the reparent operation fails (e.g. *new_parent* cannot be found, or would create a cycle). """ from .exceptions import ScriptRuntimeError parent_expr = ScriptBuilder.find_node_expr(new_parent._identifier) in_place = "true" if preserve_world_transform else "false" script = ScriptBuilder.node_body( self._identifier, f""" var _newParent = {parent_expr}; if (!_newParent) return "new parent not found"; var _oldParent = _node.getNodeParent(); if (_oldParent) _oldParent.removeNodeChild(_node, {in_place}); var _err = _newParent.addNodeChild(_node, {in_place}); var _errNum = _err ? _err.valueOf() : 0; return _errNum !== 0 ? ("DzError code " + _errNum) : null; """ ) result = self._client.execute(script).value if result: raise ScriptRuntimeError(f"reparent failed: {result}")
@staticmethod def _modifier_class_for(class_name: str): from ._modifier import DazModifier from ._morph import DazMorph from ._dforce import DazDForce if class_name == "DzMorph": return DazMorph if class_name == "DzDForceModifier": return DazDForce return DazModifier def _modifier_locator(self, modifier_name: str) -> str: return ( f"(function(){{" f" var _o = {self._locator};" f" _o = _o ? _o.getObject() : null;" f" return _o ? _o.findModifier({json.dumps(modifier_name)}) : null;" f"}})()" )
[docs] def modifiers(self) -> list["DazModifier"]: """Return all modifiers (morphs, constraints, etc.) on this node. Returns: A list of :class:`~dazpy.DazMorph` and :class:`~dazpy.DazModifier` instances. """ script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return []; var mods = []; for (var i = 0; i < obj.getNumModifiers(); i++) { var m = obj.getModifier(i); mods.push({name: m.getName(), className: m.className()}); } return mods; """ ) items = self._client.execute(script).value or [] result = [] for item in items: loc = self._modifier_locator(item["name"]) cls = self._modifier_class_for(item["className"]) result.append(cls(self._client, loc)) return result
[docs] def find_modifier(self, name: str) -> "DazModifier | None": """Find a modifier by internal name. Args: name: The ``getName()`` string of the modifier. Returns: A :class:`~dazpy.DazMorph` or :class:`~dazpy.DazModifier`, or ``None`` if not found. """ script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; var m = obj.findModifier({json.dumps(name)}); return m ? {{name: m.getName(), className: m.className()}} : null; """ ) result = self._client.execute(script).value if result is None: return None loc = self._modifier_locator(result["name"]) return self._modifier_class_for(result["className"])(self._client, loc)
def _material_locator(self, material_name: str) -> str: return ( f"(function(){{" f" var _n = {self._locator};" f" if (!_n) return null;" f" var _o = _n.getObject();" f" if (!_o) return null;" f" var _s = _o.getCurrentShape();" f" return _s ? _s.findMaterial({json.dumps(material_name)}) : null;" f"}})()" )
[docs] def materials(self) -> list["DazMaterial"]: """Return all surface materials on this node's current shape.""" from ._material import DazMaterial script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return []; var shape = obj.getCurrentShape(); if (!shape) return []; var names = []; for (var i = 0; i < shape.getNumMaterials(); i++) { names.push(shape.getMaterial(i).getName()); } return names; """ ) names = self._client.execute(script).value or [] return [DazMaterial(self._client, self._material_locator(n)) for n in names]
[docs] def find_material(self, name: str) -> "DazMaterial | None": """Find a surface material by name. Args: name: The material's ``getName()`` string. Returns: A :class:`~dazpy.DazMaterial` proxy, or ``None`` if not found. """ from ._material import DazMaterial script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; var shape = obj.getCurrentShape(); if (!shape) return null; var m = shape.findMaterial({json.dumps(name)}); return m ? m.getName() : null; """ ) result = self._client.execute(script).value if result is None: return None return DazMaterial(self._client, self._material_locator(result))
[docs] def find_modifier_by_label(self, label: str) -> "DazModifier | None": """Find a modifier by its user-visible label (the name shown in the DAZ UI). DAZ Studio morphs have both an internal name (e.g. ``"PHMSmileFull"``) and a display label (e.g. ``"Smile Full Face"``). :meth:`find_modifier` matches the internal name; this method matches the label instead. Args: label: The ``getLabel()`` string shown in the Parameters pane. Returns: A :class:`~dazpy.DazMorph` or :class:`~dazpy.DazModifier`, or ``None`` if no modifier with that label exists. """ script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; for (var i = 0; i < obj.getNumModifiers(); i++) {{ var m = obj.getModifier(i); if (m.getLabel() === {json.dumps(label)}) {{ return {{name: m.getName(), className: m.className()}}; }} }} return null; """ ) result = self._client.execute(script).value if result is None: return None loc = self._modifier_locator(result["name"]) return self._modifier_class_for(result["className"])(self._client, loc)
[docs] def find_property(self, name: str) -> "DazProperty | None": # noqa: F821 """Find a node-level property by its internal name. This searches properties directly on the node (via ``DzNode::findProperty``), which covers pose controls, FACS dials, and other parameter channels that are **not** geometry modifiers and therefore invisible to :meth:`find_modifier`. Args: name: The ``getName()`` / ID string of the property (e.g. ``"facs_ctrl_SmileFullFace"``). Returns: A :class:`~dazpy.DazProperty` proxy, or ``None`` if not found. """ from ._property import DazProperty locator = ( f"(function(){{var _n={self._locator};" f"return _n ? _n.findProperty({json.dumps(name)}) : null;}})()" ) exists = self._client.execute( ScriptBuilder.iife(f"return !!({locator});") ).value if not exists: return None return DazProperty._from_locator(self._client, locator)
[docs] def find_property_by_label(self, label: str) -> "DazProperty | None": # noqa: F821 """Find a node-level property by its user-visible label. Equivalent to :meth:`find_property` but matches on ``getLabel()`` instead of ``getName()``. Use this when you know the label shown in the DAZ Studio Parameters pane (e.g. ``"Smile Full Face"``) but not the internal ID. Args: label: The ``getLabel()`` string shown in the Parameters pane. Returns: A :class:`~dazpy.DazProperty` proxy, or ``None`` if not found. """ from ._property import DazProperty locator = ( f"(function(){{var _n={self._locator};" f"return _n ? _n.findPropertyByLabel({json.dumps(label)}) : null;}})()" ) exists = self._client.execute( ScriptBuilder.iife(f"return !!({locator});") ).value if not exists: return None return DazProperty._from_locator(self._client, locator)
[docs] def morphs(self) -> list["DazMorph"]: """Return only the morph modifiers on this node (convenience filter).""" from ._morph import DazMorph return [m for m in self.modifiers() if isinstance(m, DazMorph)]
[docs] def dforce_modifiers(self) -> list["DazDForce"]: """Return only the dForce simulation modifiers on this node (convenience filter).""" from ._dforce import DazDForce return [m for m in self.modifiers() if isinstance(m, DazDForce)]
[docs] def set_rotation(self, x: float, y: float, z: float) -> None: """Set the world-space rotation using Euler angles in degrees. Args: x: Rotation around the X axis in degrees. y: Rotation around the Y axis in degrees. z: Rotation around the Z axis in degrees. """ script = ScriptBuilder.node_body( self._identifier, f"_node.getXRotControl().setValue({x}); _node.getYRotControl().setValue({y}); _node.getZRotControl().setValue({z});" ) self._client.execute(script)
[docs] def set_position_at_frame(self, frame: int, x: float, y: float, z: float) -> None: """Write a real keyframe for world-space position at timeline *frame*. Unlike :meth:`set_position`, this writes an actual animation curve key (via ``DzNode.setWSPos(tm, pos)``) rather than the "current" value — the position at frames between keys is DAZ Studio's own interpolation, not something the caller has to bake per-frame. *frame* is converted to DAZ Studio time ticks via the scene's current ``Scene.getTimeStep()`` (ticks per frame), not a hardcoded constant — that value depends on the scene's frame rate. Args: frame: Timeline frame number for this keyframe. x: X coordinate in DAZ Studio units (centimetres by default). y: Y coordinate. z: Z coordinate. """ script = ScriptBuilder.node_body( self._identifier, f"var _tm = {int(frame)} * Scene.getTimeStep(); " f"_node.setWSPos(_tm, new DzVec3({x}, {y}, {z}));" ) self._client.execute(script)
[docs] def set_rotation_at_frame(self, frame: int, x: float, y: float, z: float) -> None: """Write a real keyframe for world-space Euler rotation at timeline *frame*. Companion to :meth:`set_position_at_frame` — see its docstring for the tick-conversion note. Writes to the same per-axis rotation controls as :meth:`set_rotation`, but as a keyed value at *frame* rather than the "current" value. Args: frame: Timeline frame number for this keyframe. x: Rotation around the X axis in degrees. y: Rotation around the Y axis in degrees. z: Rotation around the Z axis in degrees. """ script = ScriptBuilder.node_body( self._identifier, f"var _tm = {int(frame)} * Scene.getTimeStep(); " f"_node.getXRotControl().setDoubleValue(_tm, {x}); " f"_node.getYRotControl().setDoubleValue(_tm, {y}); " f"_node.getZRotControl().setDoubleValue(_tm, {z});" ) self._client.execute(script)
[docs] def clear_position_keys(self) -> None: """Remove all keyframes from this node's X/Y/Z position controls. Newly-created nodes can already carry a default key (e.g. a preset camera position at its creation time); call this before writing a fresh animated curve with :meth:`set_position_at_frame`, otherwise the leftover default key distorts interpolation/extrapolation around the new keys. """ script = ScriptBuilder.node_body( self._identifier, "_node.getXPosControl().deleteAllKeys(); " "_node.getYPosControl().deleteAllKeys(); " "_node.getZPosControl().deleteAllKeys();" ) self._client.execute(script)
[docs] def clear_rotation_keys(self) -> None: """Remove all keyframes from this node's X/Y/Z rotation controls. See :meth:`clear_position_keys` — same rationale, for orientation. """ script = ScriptBuilder.node_body( self._identifier, "_node.getXRotControl().deleteAllKeys(); " "_node.getYRotControl().deleteAllKeys(); " "_node.getZRotControl().deleteAllKeys();" ) self._client.execute(script)
@property def local_position(self) -> dict | None: """Local-space position as ``{"x", "y", "z"}`` (read-only).""" script = ScriptBuilder.node_body( self._identifier, "var p = _node.getLocalPos(); return {x: p.x, y: p.y, z: p.z};" ) return self._client.execute(script).value
[docs] def set_local_position(self, x: float, y: float, z: float) -> None: """Set the local-space position of this node. Args: x: X coordinate relative to the parent. y: Y coordinate. z: Z coordinate. """ script = ScriptBuilder.node_body( self._identifier, f"_node.setLocalPos(new DzVec3({x}, {y}, {z}));" ) self._client.execute(script)
@property def local_euler(self) -> tuple[float, float, float] | None: """Local-space rotation as an ``(x, y, z)`` tuple of Euler angles in degrees. Reads the rotation controls written by :meth:`set_local_rotation`, so the two are exact inverses. Returns: ``(x, y, z)`` in degrees, or ``None`` if the node cannot be found. """ script = ScriptBuilder.node_body( self._identifier, "return [_node.getXRotControl().getValue(), " "_node.getYRotControl().getValue(), " "_node.getZRotControl().getValue()];" ) result = self._client.execute(script).value if result is None: return None return (result[0], result[1], result[2]) @property def local_rotation(self) -> dict | None: """Local-space rotation as ``{"x", "y", "z", "w"}`` quaternion (read-only).""" script = ScriptBuilder.node_body( self._identifier, "var r = _node.getLocalRot(); return {x: r.x, y: r.y, z: r.z, w: r.w};" ) return self._client.execute(script).value
[docs] def set_local_rotation(self, x: float, y: float, z: float) -> None: """Set the local-space rotation using Euler angles in degrees. Args: x: Rotation around the local X axis in degrees. y: Rotation around the local Y axis in degrees. z: Rotation around the local Z axis in degrees. """ script = ScriptBuilder.node_body( self._identifier, f"_node.getXRotControl().setValue({x}); _node.getYRotControl().setValue({y}); _node.getZRotControl().setValue({z});" ) self._client.execute(script)
[docs] def is_selected(self) -> bool: """Return ``True`` if this node is currently selected.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isSelected();") return bool(self._client.execute(script).value)
[docs] def select(self, on: bool = True) -> None: """Select or deselect this node. Args: on: ``True`` to select, ``False`` to deselect. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.select({flag});") self._client.execute(script)
[docs] def is_in_scene(self) -> bool: """Return ``True`` if this node is still part of the active scene.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isInScene();") return bool(self._client.execute(script).value)
[docs] def is_root(self) -> bool: """Return ``True`` if this node has no parent (top-level node).""" script = ScriptBuilder.node_body(self._identifier, "return _node.isRootNode();") return bool(self._client.execute(script).value)
[docs] def is_visible_in_render(self) -> bool: """Return ``True`` if this node is visible in render output.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isVisibleInRender();") return bool(self._client.execute(script).value)
[docs] def set_visible_in_render(self, on: bool) -> None: """Set render visibility. Args: on: ``True`` to show in render, ``False`` to hide. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisibleInRender({flag});") self._client.execute(script)
[docs] def is_visible_in_viewport(self) -> bool: """Return ``True`` if this node is visible in the 3D viewport.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isVisibleInViewport();") return bool(self._client.execute(script).value)
[docs] def set_visible_in_viewport(self, on: bool) -> None: """Set viewport visibility. Args: on: ``True`` to show, ``False`` to hide. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisibleInViewport({flag});") self._client.execute(script)
[docs] def bounding_box(self) -> dict | None: """Return the world-space axis-aligned bounding box. Returns: A dict ``{"min": {"x", "y", "z"}, "max": {"x", "y", "z"}}`` or ``None`` if the node has no geometry. """ script = ScriptBuilder.node_body( self._identifier, "var bb = _node.getWSBoundingBox(); return {min: {x: bb.min.x, y: bb.min.y, z: bb.min.z}, max: {x: bb.max.x, y: bb.max.y, z: bb.max.z}};" ) return self._client.execute(script).value
[docs] def fit_to(self, figure: "DazNode") -> str: """Fit this clothing/hair/prop node to a base figure. For figure-based conforming clothing and hair (nodes exposing ``setFollowTarget``/``followSkeleton``), this sets the follow relationship so the item deforms with the figure's pose. For plain props/accessories, it falls back to parenting the item under the figure, preserving its current world-space transform. Args: figure: The target base figure node. Must already be loaded into the scene. Returns: The DazScript API used: ``"setFollowTarget"``, ``"followSkeleton"``, or ``"addNodeChild"`` (parenting fallback). Raises: NodeNotFoundError: If this node or *figure* cannot be found. """ from .exceptions import NodeNotFoundError figure_expr = ScriptBuilder.find_node_expr(figure._identifier) script = ScriptBuilder.node_body( self._identifier, f""" var _figure = {figure_expr}; if (!_figure) return null; var _method; if (typeof _node.setFollowTarget === 'function') {{ _node.setFollowTarget(_figure); _method = "setFollowTarget"; }} else if (typeof _node.followSkeleton === 'function') {{ _node.followSkeleton(_figure); _method = "followSkeleton"; }} else {{ _figure.addNodeChild(_node, true); _method = "addNodeChild"; }} return _method; """ ) result = self._client.execute(script).value if result is None: raise NodeNotFoundError( f"Could not fit node to figure: one of the nodes was not found." ) return result
[docs] def unfit(self) -> dict: """Remove this node's fitting relationship with its figure. Clears any follow-target relationship (conforming clothing/hair) and detaches this node from its parent skeleton (props), leaving it as a free-standing scene node at its current world position. Returns: A dict with keys: - ``"previous_figure"`` — internal name of the figure this node was fitted to, or ``None`` if it had no fitting relationship. - ``"actions"`` — list of operations performed (e.g. ``["cleared follow target", "detached from parent"]``), or ``[]`` if nothing was fitted. """ script = ScriptBuilder.node_body( self._identifier, """ var _prevFigure = null; var _actions = []; if (typeof _node.getFollowTarget === 'function') { var _ft = _node.getFollowTarget(); if (_ft) { _prevFigure = _ft.getName(); if (typeof _node.setFollowTarget === 'function') { _node.setFollowTarget(null); _actions.push("cleared follow target"); } } } var _parent = _node.getNodeParent(); if (_parent && _parent.inherits && _parent.inherits("DzSkeleton")) { _prevFigure = _prevFigure || _parent.getName(); _parent.removeNodeChild(_node, true); _actions.push("detached from parent"); } return {previous_figure: _prevFigure, actions: _actions}; """ ) return self._client.execute(script).value or {"previous_figure": None, "actions": []}
[docs] def fitted_items(self) -> list["DazNode"]: """Return all clothing, hair, and prop nodes fitted to this figure. A node counts as fitted if it either follows this node (figure-based conforming clothing/hair) or is directly parented to it (props and accessories). Returns: A list of :class:`~dazpy.DazNode` proxies for each fitted item. """ script = ScriptBuilder.node_body( self._identifier, """ var _fitted = []; var _numNodes = Scene.getNumNodes(); for (var i = 0; i < _numNodes; i++) { var _n = Scene.getNode(i); if (!_n || _n === _node) continue; var _isFitted = false; if (typeof _n.getFollowTarget === 'function') { var _ft = _n.getFollowTarget(); if (_ft && _ft.elementID === _node.elementID) _isFitted = true; } if (!_isFitted && typeof _n.getNodeParent === 'function') { var _p = _n.getNodeParent(); if (_p && _p.elementID === _node.elementID) _isFitted = true; } if (_isFitted) _fitted.push(_n.getName()); } return _fitted; """ ) names = self._client.execute(script).value or [] return [DazNode(self._client, NodeIdentifier(n)) for n in names]
@property def geometry_vertex_count(self) -> int | None: """Total vertex count of this node's current geometry, or ``None``.""" script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return null; var shape = obj.getCurrentShape(); if (!shape) return null; var geo = shape.getGeometry(); if (!geo) return null; return geo.getNumVertices(); """ ) return self._client.execute(script).value