Working with Geometry¶
Detailed guide to vertices, edges, faces, loops, and the triangulation system.
The snippets assume model = skppy.load("model.skp") and use its root
Entities scope unless stated otherwise.
Vertex IDs and lookup¶
Every Vertex, Edge, and Face has an integer id unique within its
entity scope. Cross-references (e.g., edge start/end, loop edge-uses) use
these IDs.
e = model.entities
# Build a quick lookup map
vertex_map = {v.id: v for v in e.vertices}
edge_map = {ed.id: (ed.start_vertex_id, ed.end_vertex_id) for ed in e.edges}
# Resolve an edge
for edge in e.edges:
start = vertex_map[edge.start_vertex_id]
end = vertex_map[edge.end_vertex_id]
print(f"Edge {edge.id}: {start.position} -> {end.position}")
Do not use entities.vertices[id]: IDs need not be contiguous or zero-based,
especially after loading a file or deleting objects in memory.
Traversing face loops¶
Each Face has an outer_loop and zero or more inner_loops (holes).
Loops contain EdgeUse objects that specify an edge and traversal direction.
for face in e.faces:
# Outer boundary vertex IDs
outer_ids = face.outer_loop.vertex_ids(edge_map)
print(f"Face {face.id} outer:", outer_ids)
for i, hole in enumerate(face.inner_loops):
hole_ids = hole.vertex_ids(edge_map)
print(f" hole {i}:", hole_ids)
Loop.vertex_ids() handles reversed EdgeUse entries and follows the stored
loop traversal. Valid outer loops normally follow front-face winding, but the
method does not repair malformed source topology.
Face normals and planes¶
The plane equation (a, b, c, d) satisfies ax + by + cz + d = 0.
The normal is the unit vector (a, b, c):
for face in e.faces:
n = face.normal() # Vector3D
print(f"Face {face.id} normal: ({n.x:.3f}, {n.y:.3f}, {n.z:.3f})")
Triangulating faces¶
Use Face.triangulate() for a simple fan of the outer boundary. The result is
a list of (a, b, c) vertex-ID triples, not list indices:
for face in e.faces:
triangles = face.triangulate(e)
for a_id, b_id, c_id in triangles:
va = vertex_map[a_id]
vb = vertex_map[b_id]
vc = vertex_map[c_id]
print(va.position, vb.position, vc.position)
Face.triangulate() intentionally ignores inner loops. For importer-ready
faces with holes, call Entities.prepare_mesh(): it merges loops with bridge
edges and uses ear clipping. Importers that prefer n-gons can pass
split_holes_to_ngons=True to represent a single-hole face as two simple
n-gons; multiple-hole faces still triangulate.
Degenerate faces¶
Face.triangulate() returns an empty list only when fewer than three outer-loop
edge uses resolve. It is a topology convenience and does not test collinearity
or self-intersection. The prepare_mesh() path performs the stronger geometric
processing used by the Blender importer and may omit faces that cannot produce
valid polygons.
Working in world space¶
Vertex positions in a ComponentDefinition are in definition-local space.
To transform to world space, multiply by the instance transform chain:
import numpy as np
def local_to_world(
position: skppy.Vector3D, world_matrix: np.ndarray
) -> skppy.Vector3D:
"""Apply rotation, scale, translation, and homogeneous division."""
point = world_matrix @ np.array([position.x, position.y, position.z, 1.0])
if abs(point[3]) > 1.0e-12:
point = point / point[3]
return skppy.Vector3D(float(point[0]), float(point[1]), float(point[2]))
for inst in model.entities.component_instances:
defn = defn_map[inst.definition_id]
world_matrix = skppy.Transform(inst.transform).matrix
for v in defn.entities.vertices:
world_pos = local_to_world(v.position, world_matrix)
print(world_pos)
For a nested instance, compose matrices in parent-to-child order:
child_world = parent_world @ Transform(child.transform).matrix. Reusing this
matrix for all vertices in a placement avoids repeated conversion of the
13-value representation.
Curves and arcs¶
Edges that belong to a curve have a non-None curve_id. SketchUp arcs and
circles are stored as collections of straight edges sharing a curve_id:
from collections import defaultdict
curves = defaultdict(list)
for edge in e.edges:
if edge.curve_id is not None:
curves[edge.curve_id].append(edge)
for curve_id, edges in curves.items():
print(f"Curve {curve_id}: {len(edges)} segments")
Use entities.curves and entities.arc_curves when you also need polygon
state or circular parameters such as center, normal, radius, and angles.
Text and dimensions¶
Both SKP container families expose annotations through the same entity lists:
for text in e.texts:
print(text.text, text.anchor.position, text.font)
for dimension in e.linear_dimensions:
print(dimension.start.position, dimension.end.position, dimension.text)
for dimension in e.radial_dimensions:
print(dimension.target_entity_id, dimension.radius_ratio, dimension.is_diameter)
Fonts are shared model resources. font_id retains the reference even when a
source file does not provide a resolvable Font object.
PointReference.entity_id is populated when the source annotation points to an
entity that can be resolved in the same scope. A null or unavailable association
leaves it as None while preserving the measured position. drawing contains
the normalized material, layer, visibility, shadow, smoothing, and lock state.