"""Functions related to Houdini geometry."""
# Future
from __future__ import annotations
# Standard Library
from typing import TYPE_CHECKING
# Houdini Core Tools
from houdini_core_tools import exceptions, math
# Houdini
import hou
if TYPE_CHECKING:
from collections.abc import Sequence
# Non-Public Functions
def _set_all_shared_values(attribute: hou.Attrib, value: str) -> None:
"""Set a string attribute value for all elements.
Args:
attribute: The attribute to set.
value: The value to set.
Raises:
InvalidAttributeTypeError: If an invalid attribute type ie passed.
"""
geometry = attribute.geometry()
match attribute.type():
case hou.attribType.Point:
geometry.setPointStringAttribValues(attribute.name(), [value] * num_points(geometry))
case hou.attribType.Prim:
geometry.setPrimStringAttribValues(attribute.name(), [value] * num_prims(geometry))
case hou.attribType.Vertex:
geometry.setVertexStringAttribValues(attribute.name(), [value] * num_vertices(geometry))
case _:
raise exceptions.InvalidAttributeTypeError(
attribute.type(), (hou.attribType.Point, hou.attribType.Prim, hou.attribType.Vertex)
)
def _set_group_shared_values(
attribute: hou.Attrib, group: hou.PointGroup | hou.PrimGroup | hou.VertexGroup, value: str
) -> None:
"""Set a string attribute value for group elements.
Args:
attribute: The attribute to set.
group: The group to set
value: The value to set.
Raises:
InvalidAttributeTypeError: If an invalid attribute type ie passed.
"""
geometry = attribute.geometry()
match attribute.type():
case hou.attribType.Point:
if not isinstance(group, hou.PointGroup):
raise exceptions.InvalidGroupTypeError(type(group), hou.PointGroup)
values = list(geometry.pointStringAttribValues(attribute.name()))
for point in group.iterPoints():
values[point.number()] = value
geometry.setPointStringAttribValues(attribute.name(), values)
case hou.attribType.Prim:
if not isinstance(group, hou.PrimGroup):
raise exceptions.InvalidGroupTypeError(type(group), hou.PrimGroup)
values = list(geometry.primStringAttribValues(attribute.name()))
for prim in group.iterPrims():
values[prim.number()] = value
geometry.setPrimStringAttribValues(attribute.name(), values)
case hou.attribType.Vertex:
if not isinstance(group, hou.VertexGroup):
raise exceptions.InvalidGroupTypeError(type(group), hou.VertexGroup)
values = list(geometry.vertexStringAttribValues(attribute.name()))
for vertex in group.iterVertices():
values[vertex.linearNumber()] = value
geometry.setVertexStringAttribValues(attribute.name(), values)
case _:
raise exceptions.InvalidAttributeTypeError(
attribute.type(), (hou.attribType.Point, hou.attribType.Prim, hou.attribType.Vertex)
)
# Functions
[docs]
def check_minimum_polygon_vertex_count(
geometry: hou.Geometry, minimum_vertices: int, *, ignore_open: bool = True
) -> bool:
"""Check that all polygons have a minimum number of vertices.
This will ignore non-polygon types such as packed and volume primitives.
Args:
geometry: The geometry to check.
minimum_vertices: The minimum number of vertices a polygon must have.
ignore_open: Ignore polygons which are open.
Returns:
Whether all the polygons have the minimum number of vertices.
"""
for prim in geometry.iterPrimsOfType(hou.primType.Polygon):
if prim.numVertices() < minimum_vertices:
if ignore_open and not prim.isClosed():
continue
return False
return True
[docs]
def connected_points(point: hou.Point) -> tuple[hou.Point, ...]:
"""Get all points that share an edge with the point.
Args:
point: The source point.
Returns:
Connected points
"""
prims = point.prims()
connected = set()
for prim in prims:
prim_points = prim.points()
for prim_point in prim_points:
if face_has_edge(prim, prim_point, point):
connected.add(prim_point)
return tuple(sorted(connected, key=lambda pt: pt.number()))
[docs]
def face_has_edge(face: hou.Face, point1: hou.Point, point2: hou.Point) -> bool:
"""Test if the face has an edge between two points.
Args:
face: The face to check for an edge.
point1: A point to test for an edge with.
point2: A point to test for an edge with.
Returns:
Whether the points share an edge.
"""
# Test for the edge.
edges: tuple[hou.Edge] = face.edges()
pt_nums = tuple(sorted([point1.number(), point2.number()]))
for edge in edges:
edge_pt_nums = tuple(sorted(point.number() for point in edge.points()))
if edge_pt_nums == pt_nums:
return True
return False
[docs]
def find_attrib(geometry: hou.Geometry, attrib_type: hou.attribType, name: str) -> hou.Attrib | None:
"""Find an attribute with a given name and type on the geometry.
Args:
geometry: The geometry to find an attribute on.
attrib_type: The attribute type.
name: The attribute name.
Returns:
A found attribute, otherwise None.
Raises:
UnexpectedAttributeTypeError: When an invalid attrib_type is passed.
"""
match attrib_type:
case hou.attribType.Vertex:
return geometry.findVertexAttrib(name)
case hou.attribType.Point:
return geometry.findPointAttrib(name)
case hou.attribType.Prim:
return geometry.findPrimAttrib(name)
case hou.attribType.Global:
return geometry.findGlobalAttrib(name)
case _:
raise exceptions.UnexpectedAttributeTypeError(attrib_type)
[docs]
def find_group(
geometry: hou.Geometry,
group_type: type[hou.EdgeGroup | hou.PointGroup | hou.PrimGroup | hou.VertexGroup],
name: str,
) -> hou.EdgeGroup | hou.PointGroup | hou.PrimGroup | hou.VertexGroup | None:
"""Find a group with a given name and type on the geometry.
Args:
geometry: The geometry to find a group on.
group_type: The group type.
name: The group name.
Returns:
A found group, otherwise None.
Raises:
UnexpectedGroupTypeError: When an invalid group type is passed.
"""
match group_type:
case hou.PointGroup:
return geometry.findPointGroup(name)
case hou.PrimGroup:
return geometry.findPrimGroup(name)
case hou.VertexGroup:
return geometry.findVertexGroup(name)
case hou.EdgeGroup:
return geometry.findEdgeGroup(name)
case _:
raise exceptions.UnexpectedGroupTypeError(group_type)
[docs]
def geo_details_match(geometry1: hou.Geometry, geometry2: hou.Geometry) -> bool:
"""Test if two hou.Geometry objects point to the same detail.
Args:
geometry1: A geometry detail.
geometry2: A geometry detail.
Returns:
Whether the objects represent the same detail.
"""
handle1 = geometry1._guDetailHandle()
handle2 = geometry2._guDetailHandle()
details_match = int(handle1._asVoidPointer()) == int(handle2._asVoidPointer())
handle1.destroy()
handle2.destroy()
return details_match
[docs]
def geometry_has_prims_with_shared_vertex_points(geometry: hou.Geometry) -> bool:
"""Check if the geometry contains any primitives which have more than one vertex referencing the same point.
Args:
geometry: The geometry to check.
Returns:
Whether the geometry has any primitives with shared vertex points.
"""
for prim in geometry.iterPrims():
vtx_count = prim.numVertices()
vtx_points = {vertex.point().number() for vertex in prim.vertices()}
if len(vtx_points) != vtx_count:
return True
return False
[docs]
def get_points_from_list(geometry: hou.Geometry, point_list: Sequence[int]) -> tuple[hou.Point, ...]:
"""Convert a list of point numbers to hou.Point objects.
Args:
geometry: The geometry to get points for.
point_list: A list of point numbers.
Returns:
Matching points on the geometry.
"""
# Return an empty tuple if the point list is empty.
if not point_list:
return ()
# Convert the list of integers to a space separated string.
point_str = " ".join([str(i) for i in point_list])
# Glob for the specified points.
return geometry.globPoints(point_str)
[docs]
def get_prims_from_list(geometry: hou.Geometry, prim_list: Sequence[int]) -> tuple[hou.Prim, ...]:
"""Convert a list of primitive numbers to hou.Prim objects.
Args:
geometry: The geometry to get prims for.
prim_list: A list of prim numbers.
Returns:
Matching prims on the geometry.
"""
# Return an empty tuple if the prim list is empty.
if not prim_list:
return ()
# Convert the list of integers to a space separated string.
prim_str = " ".join([str(i) for i in prim_list])
# Glob for the specified prims.
return geometry.globPrims(prim_str)
[docs]
def get_primitives_with_shared_vertex_points(
geometry: hou.Geometry,
) -> tuple[hou.Prim, ...]:
"""Get any primitives in the geometry which have more than one vertex referencing the same point.
Args:
geometry: The geometry to check.
Returns:
A list of any primitives which have shared vertex points.
"""
prims = []
for prim in geometry.iterPrims():
vtx_count = prim.numVertices()
vtx_points = {vertex.point().number() for vertex in prim.vertices()}
if len(vtx_points) != vtx_count:
prims.append(prim)
return tuple(prims)
[docs]
def group_bounding_box(group: hou.EdgeGroup | hou.PointGroup | hou.PrimGroup) -> hou.BoundingBox:
"""Get the bounding box of the group.
Args:
group: The group to get the bounding box for.
Returns:
The bounding box for the group.
Raises:
TypeError: If the object is not a supported group type.
"""
match group:
case hou.EdgeGroup():
points = [point for edge in group.edges() for point in edge.points()]
case hou.PointGroup():
points = group.points()
case hou.PrimGroup():
points = [point for prim in group.prims() for point in prim.points()]
case _:
raise exceptions.UnexpectedGroupTypeError(type(group))
pos0 = points[0].position()
bbox = hou.BoundingBox(pos0[0], pos0[1], pos0[2], pos0[0], pos0[1], pos0[2])
for point in points[1:]:
bbox.enlargeToContain(point.position())
return bbox
[docs]
def num_points(geometry: hou.Geometry) -> int:
"""Get the number of points in the geometry.
This should be quicker than len(hou.Geometry.iterPoints()) since it uses
the 'pointcount' intrinsic value from the detail.
Args:
geometry: The geometry to get the point count for.
Returns:
The point count:
"""
return geometry.intrinsicValue("pointcount")
[docs]
def num_prims(geometry: hou.Geometry) -> int:
"""Get the number of primitives in the geometry.
This should be quicker than len(hou.Geometry.iterPrims()) since it uses
the 'primitivecount' intrinsic value from the detail.
Args:
geometry: The geometry to get the primitive count for.
Returns:
The primitive count:
"""
return geometry.intrinsicValue("primitivecount")
[docs]
def num_vertices(geometry: hou.Geometry) -> int:
"""Get the number of vertices in the geometry.
Args:
geometry: The geometry to get the vertex count for.
Returns:
The vertex count.
"""
return geometry.intrinsicValue("vertexcount")
[docs]
def primitive_area(prim: hou.Prim) -> float:
"""Get the area of the primitive.
This method just wraps the "measuredarea" intrinsic value.
Args:
prim: The primitive to get the area of.
Returns:
The primitive area.
"""
return prim.intrinsicValue("measuredarea")
[docs]
def primitive_bary_center(prim: hou.Prim) -> hou.Vector3:
"""Get the barycenter of the primitive.
Args:
prim: The primitive to get the center of.
Returns:
The barycenter.
"""
center = hou.Vector3()
for vertex in prim.vertices():
center += vertex.point().position()
# Construct a vector and return it.
return center / prim.numVertices()
[docs]
def primitive_bounding_box(prim: hou.Prim) -> hou.BoundingBox:
"""Get the bounding box of the primitive.
This method just wraps the "bounds" intrinsic value.
Args:
prim: The primitive to get the bounding box of.
Returns:
The primitive bounding box.
"""
bounds = prim.intrinsicValue("bounds")
# Intrinsic values are out of order for hou.BoundingBox so they need to
# be shuffled.
return hou.BoundingBox(bounds[0], bounds[2], bounds[4], bounds[1], bounds[3], bounds[5])
[docs]
def primitive_perimeter(prim: hou.Prim) -> float:
"""Get the perimeter of the primitive.
This method just wraps the "measuredperimeter" intrinsic value.
Args:
prim: The primitive to get the perimeter of.
Returns:
The primitive perimeter.
"""
return prim.intrinsicValue("measuredperimeter")
[docs]
def primitive_volume(prim: hou.Prim) -> float:
"""Get the volume of the primitive.
This method just wraps the "measuredvolume" intrinsic value.
Args:
prim: The primitive to get the volume of.
Returns:
The primitive volume.
"""
return prim.intrinsicValue("measuredvolume")
[docs]
def reverse_prim(prim: hou.Prim) -> None:
"""Reverse the vertex order of the primitive.
Args:
prim: The primitive to reverse.
Raises:
GeometryPermissionError: If the target geometry is read only.
"""
geometry = prim.geometry()
# Make sure the geometry is not read only.
if geometry.isReadOnly():
raise hou.GeometryPermissionError
verb = hou.sopNodeTypeCategory().nodeVerb("reverse")
new_geo = hou.Geometry()
verb.execute(new_geo, [geometry])
geometry.clear()
geometry.merge(new_geo)
[docs]
def set_shared_string_attrib(
attribute: hou.Attrib, value: str, *, group: hou.PointGroup | hou.PrimGroup | hou.VertexGroup | None = None
) -> None:
"""Set a string attribute value for elements.
If group is None, all elements will receive the value. If a group
is passed, only the elements in the group will be set.
Args:
attribute: The attribute to set.
value: The value to set.
group: An optional group.
Raises:
AttributeNotAStringError: If the attribute is not a string.
"""
if attribute.dataType() != hou.attribData.String:
raise exceptions.AttributeNotAStringError(attribute)
if group is None:
_set_all_shared_values(attribute, value)
else:
_set_group_shared_values(attribute, group, value)
[docs]
def shared_edges(face1: hou.Face, face2: hou.Face) -> tuple[hou.Edge, ...]:
"""Get a tuple of any shared edges between two primitives.
Args:
face1: The face to check for shared edges.
face2: The other face to check for shared edges.
Returns:
A tuple of shared edges.
"""
geometry = face1.geometry()
# A list of unique edges.
edges = set()
# Iterate over each vertex of the primitive.
for vertex in face1.vertices():
# Get the point for the vertex.
vertex_point = vertex.point()
# Iterate over all the connected points.
for connected in connected_points(vertex_point):
# Sort the points.
pt1, pt2 = sorted((vertex_point, connected), key=lambda pt: pt.number())
# Ensure the edge exists for both primitives.
if face_has_edge(face1, pt1, pt2) and face_has_edge(face2, pt1, pt2):
# Find the edge and add it to the list.
edges.add(geometry.findEdge(pt1, pt2))
return tuple(edges)