Source code for dazpy._geometry

from __future__ import annotations

from ._element import DazElement
from ._node import NodeIdentifier
from ._script_builder import ScriptBuilder


[docs] class DazGeometry(DazElement): """Proxy for the ``DzGeometry`` of a node's current shape. Provides chunked access to vertices, faces, normals, UV sets, and face / material groups. Use :class:`~dazpy.DazNode` or construct directly:: from dazpy import DazClient, DazGeometry, NodeIdentifier geo = DazGeometry(DazClient(), NodeIdentifier("Genesis9")) verts = geo.vertex_positions_all() Args: client: The :class:`~dazpy.DazClient` for remote calls. identifier: Identifies the scene node whose geometry to access. """ def __init__(self, client: "DazClient", identifier: NodeIdentifier): # noqa: F821 locator = ( f"(function(){{" f"var n = {ScriptBuilder.find_node_expr(identifier)};" f"if (!n) return null;" f"var obj = n.getObject();" f"if (!obj) return null;" f"var sh = obj.getCurrentShape();" f"return sh ? sh.getGeometry() : null;" f"}})()" ) super().__init__(client, locator) object.__setattr__(self, "_identifier", identifier) @property def vertex_count(self) -> int | None: """Total number of vertices in the mesh (read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator}; return g ? g.getNumVertices() : null;" ) return self._client.execute(script).value @property def facet_count(self) -> int | None: """Total number of faces (triangles + quads, read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator}; return g ? g.getNumFacets() : null;" ) return self._client.execute(script).value
[docs] def vertex_positions(self, start: int = 0, count: int = 5000) -> dict: """Fetch a chunk of vertex positions. Args: start: Zero-based index of the first vertex to return. count: Maximum number of vertices to return in this chunk. Returns: ``{"total": int, "start": int, "count": int, "vertices": [[x, y, z], ...]}`` """ script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g) return null; var total = g.getNumVertices(); var end = Math.min({start} + {count}, total); var verts = []; for (var i = {start}; i < end; i++) {{ var v = g.getVertex(i); verts.push([v.x, v.y, v.z]); }} return {{total: total, start: {start}, count: verts.length, vertices: verts}}; """) return self._client.execute(script).value or {}
[docs] def vertex_positions_all(self, chunk_size: int = 5000) -> list[list[float]]: """Return all vertex positions, automatically paginating by *chunk_size*. Args: chunk_size: Number of vertices fetched per network round-trip. Returns: List of ``[x, y, z]`` coordinates for every vertex. """ first = self.vertex_positions(0, chunk_size) total = first.get("total", 0) all_verts = list(first.get("vertices", [])) offset = len(all_verts) while offset < total: chunk = self.vertex_positions(offset, chunk_size) all_verts.extend(chunk.get("vertices", [])) offset += len(chunk.get("vertices", []) or []) if not chunk.get("vertices"): break return all_verts
# ── Section 7a ────────────────────────────────────────────────────────────
[docs] def face_vertex_indices(self, start: int = 0, count: int = 1000) -> dict: """Chunked access to facet vertex indices. Returns {total, start, facets: [[v0,v1,v2(,v3)], ...]}.""" script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g) return null; var total = g.getNumFacets(); var end = Math.min({start} + {count}, total); var facets = []; for (var i = {start}; i < end; i++) {{ var f = g.getFacet(i); if (f.isQuad()) {{ facets.push([f.vertIdx1, f.vertIdx2, f.vertIdx3, f.vertIdx4]); }} else {{ facets.push([f.vertIdx1, f.vertIdx2, f.vertIdx3]); }} }} return {{total: total, start: {start}, facets: facets}}; """) return self._client.execute(script).value or {}
[docs] def normals(self, start: int = 0, count: int = 5000) -> dict: """Chunked access to face normals. Returns {total, start, normals: [[x,y,z], ...]}.""" script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g) return null; var total = g.getNumNormals ? g.getNumNormals() : 0; var end = Math.min({start} + {count}, total); var norms = []; for (var i = {start}; i < end; i++) {{ var n = g.getNormal(i); norms.push([n.x, n.y, n.z]); }} return {{total: total, start: {start}, normals: norms}}; """) return self._client.execute(script).value or {}
@property def uv_set_count(self) -> int | None: """Number of UV sets on this mesh (read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator}; return g ? g.getNumUVSets() : null;" ) return self._client.execute(script).value
[docs] def uv_positions(self, uv_set: int = 0, start: int = 0, count: int = 5000) -> dict: """Chunked access to UV coordinates. Returns {total, start, uvs: [[u,v], ...]}.""" script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g) return null; var uvMap = ({uv_set} === 0) ? g.getUVs() : g.getUVSet({uv_set}); if (!uvMap) return null; var total = uvMap.getNumValues(); var end = Math.min({start} + {count}, total); var uvs = []; for (var i = {start}; i < end; i++) {{ var p = uvMap.getPnt2Vec(i); uvs.push([p.x, p.y]); }} return {{total: total, start: {start}, uvs: uvs}}; """) return self._client.execute(script).value or {}
# ── Section 7b ────────────────────────────────────────────────────────────
[docs] def face_group_names(self) -> list[str]: script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g || !g.getNumFaceGroups) return []; var n = g.getNumFaceGroups(); var names = []; for (var i = 0; i < n; i++) {{ var grp = g.getFaceGroup(i); names.push(grp ? grp.getName() : null); }} return names; """) return self._client.execute(script).value or []
[docs] def material_group_names(self) -> list[str]: script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g || !g.getNumMaterialGroups) return []; var n = g.getNumMaterialGroups(); var names = []; for (var i = 0; i < n; i++) {{ var grp = g.getMaterialGroup(i); names.push(grp ? grp.getName() : null); }} return names; """) return self._client.execute(script).value or []
@property def subdivision_level(self) -> int | None: """Current subdivision level (0 = base mesh, read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator};" f"return (g && g.getCurrentSubDivisionLevel) ? g.getCurrentSubDivisionLevel() : null;" ) return self._client.execute(script).value # ── Section 7c: posed (world-space) positions ─────────────────────────────
[docs] def vertex_positions_posed(self, start: int = 0, count: int = 5000) -> dict: """Fetch a chunk of fully-deformed world-space vertex positions. Uses ``DzObject.getCachedGeom()`` which returns the final mesh after morph deformations *and* skeleton skinning have been applied, in world-space coordinates. Use :meth:`vertex_positions_posed_all` to retrieve all vertices automatically. Returns: ``{"total": int, "start": int, "count": int, "vertices": [[x, y, z], ...]}`` """ script = ScriptBuilder.iife(f""" var n = {ScriptBuilder.find_node_expr(self._identifier)}; if (!n) return null; var obj = n.getObject(); if (!obj) return null; obj.forceCacheUpdate(n, false); var cached = obj.getCachedGeom(); if (!cached) return null; var total = cached.getNumVertices(); var end = Math.min({start} + {count}, total); var verts = []; for (var i = {start}; i < end; i++) {{ var v = cached.getVertex(i); verts.push([v.x, v.y, v.z]); }} return {{total: total, start: {start}, count: verts.length, vertices: verts}}; """) return self._client.execute(script).value or {}
[docs] def vertex_positions_posed_all(self, chunk_size: int = 5000) -> list[list[float]]: """Return all world-space posed+morphed vertex positions. The first call triggers :meth:`vertex_positions_posed` which forces a cache update; subsequent chunks reuse the same cached geometry. Returns: List of ``[x, y, z]`` world-space coordinates for every vertex, after skeleton skinning and morph deformations. """ first = self.vertex_positions_posed(0, chunk_size) total = first.get("total", 0) all_verts = list(first.get("vertices", [])) offset = len(all_verts) while offset < total: chunk = self.vertex_positions_posed(offset, chunk_size) all_verts.extend(chunk.get("vertices", [])) offset += len(chunk.get("vertices", []) or []) if not chunk.get("vertices"): break return all_verts
@property def tris_count(self) -> int | None: """Number of triangular faces (read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator}; return (g && g.getNumTris) ? g.getNumTris() : null;" ) return self._client.execute(script).value @property def quads_count(self) -> int | None: """Number of quad faces (read-only).""" script = ScriptBuilder.iife( f"var g = {self._locator}; return (g && g.getNumQuads) ? g.getNumQuads() : null;" ) return self._client.execute(script).value # ── bulk metadata ─────────────────────────────────────────────────────────
[docs] def mesh_info(self) -> dict | None: """Fetch all mesh metadata in a single HTTP call. Consolidates what would otherwise be 7+ separate property reads. Returns: ``{vertex_count, facet_count, tris_count, quads_count, subdivision_level, uv_set_count, face_group_names, material_group_names}`` or ``None`` if the geometry is unavailable. """ script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g) return null; var fgNames = []; if (g.getNumFaceGroups) {{ for (var i = 0; i < g.getNumFaceGroups(); i++) {{ var fg = g.getFaceGroup(i); fgNames.push(fg ? fg.getName() : null); }} }} var mgNames = []; if (g.getNumMaterialGroups) {{ for (var i = 0; i < g.getNumMaterialGroups(); i++) {{ var mg = g.getMaterialGroup(i); mgNames.push(mg ? mg.getName() : null); }} }} return {{ vertex_count: g.getNumVertices(), facet_count: g.getNumFacets(), tris_count: g.getNumTris ? g.getNumTris() : null, quads_count: g.getNumQuads ? g.getNumQuads() : null, subdivision_level: g.getCurrentSubDivisionLevel ? g.getCurrentSubDivisionLevel() : null, uv_set_count: g.getNumUVSets ? g.getNumUVSets() : null, face_group_names: fgNames, material_group_names: mgNames, }}; """) return self._client.execute(script).value
# ── bounding box ──────────────────────────────────────────────────────────
[docs] def bounding_box(self) -> "BoundingBox | None": # noqa: F821 """Axis-aligned bounding box of the base mesh in one HTTP call. Iterates all vertices server-side, avoiding the need to transfer every position to Python just to compute an AABB. Returns: A :class:`~dazpy.math3.BoundingBox`, or ``None`` if the geometry is unavailable or empty. """ script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g || g.getNumVertices() === 0) return null; var v = g.getVertex(0); var mnX = v.x, mnY = v.y, mnZ = v.z; var mxX = v.x, mxY = v.y, mxZ = v.z; var n = g.getNumVertices(); for (var i = 1; i < n; i++) {{ v = g.getVertex(i); if (v.x < mnX) mnX = v.x; else if (v.x > mxX) mxX = v.x; if (v.y < mnY) mnY = v.y; else if (v.y > mxY) mxY = v.y; if (v.z < mnZ) mnZ = v.z; else if (v.z > mxZ) mxZ = v.z; }} return {{min: {{x:mnX, y:mnY, z:mnZ}}, max: {{x:mxX, y:mxY, z:mxZ}}}}; """) result = self._client.execute(script).value if result is None: return None from .math3 import BoundingBox return BoundingBox.from_dict(result)
[docs] def bounding_box_posed(self) -> "BoundingBox | None": # noqa: F821 """AABB of the world-space posed-and-morphed mesh in one HTTP call. Uses ``DzObject.getCachedGeom()`` after forcing a cache update, so the result reflects the current bone pose and all active morphs. Returns: A :class:`~dazpy.math3.BoundingBox` in world space, or ``None``. """ script = ScriptBuilder.iife(f""" var _nd = {ScriptBuilder.find_node_expr(self._identifier)}; if (!_nd) return null; var obj = _nd.getObject(); if (!obj) return null; obj.forceCacheUpdate(_nd, false); var g = obj.getCachedGeom(); if (!g || g.getNumVertices() === 0) return null; var v = g.getVertex(0); var mnX = v.x, mnY = v.y, mnZ = v.z; var mxX = v.x, mxY = v.y, mxZ = v.z; var n = g.getNumVertices(); for (var i = 1; i < n; i++) {{ v = g.getVertex(i); if (v.x < mnX) mnX = v.x; else if (v.x > mxX) mxX = v.x; if (v.y < mnY) mnY = v.y; else if (v.y > mxY) mxY = v.y; if (v.z < mnZ) mnZ = v.z; else if (v.z > mxZ) mxZ = v.z; }} return {{min: {{x:mnX, y:mnY, z:mnZ}}, max: {{x:mxX, y:mxY, z:mxZ}}}}; """) result = self._client.execute(script).value if result is None: return None from .math3 import BoundingBox return BoundingBox.from_dict(result)
# ── paginated _all() wrappers ─────────────────────────────────────────────
[docs] def face_vertex_indices_all(self, chunk_size: int = 1000) -> list[list[int]]: """Return all face vertex indices, paginating automatically. Args: chunk_size: Faces fetched per network round-trip. Returns: List of faces; each face is ``[v0, v1, v2]`` (tri) or ``[v0, v1, v2, v3]`` (quad). """ first = self.face_vertex_indices(0, chunk_size) total = first.get("total", 0) all_faces = list(first.get("facets", [])) offset = len(all_faces) while offset < total: chunk = self.face_vertex_indices(offset, chunk_size) batch = chunk.get("facets") or [] if not batch: break all_faces.extend(batch) offset += len(batch) return all_faces
[docs] def normals_all(self, chunk_size: int = 5000) -> list[list[float]]: """Return all face normals, paginating automatically. Args: chunk_size: Normals fetched per network round-trip. Returns: List of ``[x, y, z]`` unit normals. """ first = self.normals(0, chunk_size) total = first.get("total", 0) all_norms = list(first.get("normals", [])) offset = len(all_norms) while offset < total: chunk = self.normals(offset, chunk_size) batch = chunk.get("normals") or [] if not batch: break all_norms.extend(batch) offset += len(batch) return all_norms
[docs] def uv_positions_all(self, uv_set: int = 0, chunk_size: int = 5000) -> list[list[float]]: """Return all UV coordinates for *uv_set*, paginating automatically. Args: uv_set: UV set index (0 = primary). chunk_size: UV pairs fetched per network round-trip. Returns: List of ``[u, v]`` pairs. """ first = self.uv_positions(uv_set, 0, chunk_size) total = first.get("total", 0) all_uvs = list(first.get("uvs", [])) offset = len(all_uvs) while offset < total: chunk = self.uv_positions(uv_set, offset, chunk_size) batch = chunk.get("uvs") or [] if not batch: break all_uvs.extend(batch) offset += len(batch) return all_uvs
# ── group membership ──────────────────────────────────────────────────────
[docs] def face_group_faces(self, name: str) -> list[int]: """Return the face indices belonging to the named face group. Args: name: Face group name as returned by :meth:`face_group_names`. Returns: List of 0-based face indices, or ``[]`` if the group doesn't exist. """ name_js = ScriptBuilder.escape_string(name) script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g || !g.getNumFaceGroups) return []; for (var i = 0; i < g.getNumFaceGroups(); i++) {{ var grp = g.getFaceGroup(i); if (grp && grp.getName() === {name_js}) {{ var idx = []; for (var j = 0; j < grp.count(); j++) idx.push(grp.getIndexAt(j)); return idx; }} }} return []; """) return self._client.execute(script).value or []
[docs] def material_group_faces(self, name: str) -> list[int]: """Return the face indices belonging to the named material group. Args: name: Material group name as returned by :meth:`material_group_names`. Returns: List of 0-based face indices, or ``[]`` if the group doesn't exist. """ name_js = ScriptBuilder.escape_string(name) script = ScriptBuilder.iife(f""" var g = {self._locator}; if (!g || !g.getNumMaterialGroups) return []; for (var i = 0; i < g.getNumMaterialGroups(); i++) {{ var grp = g.getMaterialGroup(i); if (grp && grp.getName() === {name_js}) {{ var idx = []; for (var j = 0; j < grp.count(); j++) idx.push(grp.getIndexAt(j)); return idx; }} }} return []; """) return self._client.execute(script).value or []
# ── pure-Python mesh utilities ────────────────────────────────────────────
[docs] @staticmethod def triangulate(faces: list) -> list[list[int]]: """Convert a list of face index arrays (tris or quads) to all-triangles. Quads are split along the 0→2 diagonal: ``[v0, v1, v2, v3]`` → ``[v0, v1, v2]`` + ``[v0, v2, v3]``. Triangles are passed through unchanged. Faces with any other vertex count are silently skipped. This is a pure-Python operation — no HTTP round-trip. Args: faces: List of faces, each a list/tuple of vertex indices, as returned by :meth:`face_vertex_indices_all`. Returns: All-triangle face list. """ result = [] for f in faces: if len(f) == 3: result.append(list(f)) elif len(f) == 4: result.append([f[0], f[1], f[2]]) result.append([f[0], f[2], f[3]]) return result
[docs] @staticmethod def as_vec3(vertices: list) -> list: """Wrap ``[[x, y, z], ...]`` vertex data in :class:`~dazpy.math3.Vec3` objects. This is a pure-Python operation — no HTTP round-trip. Args: vertices: List of ``[x, y, z]`` arrays, as returned by :meth:`vertex_positions_all` or :meth:`vertex_positions_posed_all`. Returns: List of :class:`~dazpy.math3.Vec3`. """ from .math3 import Vec3 return [Vec3.from_list(v) for v in vertices]