Components and Groups¶
Practical guide to working with SketchUp’s component and group system. Unless a
snippet creates its own model, assume model = skppy.load("model.skp") and
import skppy have already run.
Definitions vs. instances¶
Concept |
|
Description |
|---|---|---|
Component definition |
|
Named geometry template. Stored once. |
Component instance |
|
Placed reference. Many instances -> one definition. |
Group |
|
One-off instance with an anonymous definition. |
Image |
|
A flat image entity. Backed by a definition with one face. |
Iterating all instances recursively¶
Definitions may contain more instances and groups, and the same definition can legitimately appear on multiple sibling paths. Track only the active recursion path to detect a true component cycle while allowing normal reuse:
def iter_instances(entities, defn_map, parent_matrix=None, active_ids=()):
"""Yield placed entity, definition, and composed 4x4 world matrix."""
import skppy
if parent_matrix is None:
parent_matrix = skppy.Transform.identity().matrix
placed_entities = [*entities.component_instances, *entities.groups]
for placed in placed_entities:
defn = defn_map.get(placed.definition_id)
if defn is None:
continue
if defn.id in active_ids:
path = " -> ".join(str(value) for value in (*active_ids, defn.id))
raise skppy.ComponentCycleError(f"Recursive definition path: {path}")
local_matrix = skppy.Transform(placed.transform).matrix
world_matrix = parent_matrix @ local_matrix
yield placed, defn, world_matrix
yield from iter_instances(
defn.entities,
defn_map,
world_matrix,
(*active_ids, defn.id),
)
defn_map = {d.id: d for d in model.definitions}
for placed, defn, world_matrix in iter_instances(model.entities, defn_map):
position = world_matrix[:3, 3]
print(f"{placed.name!r} -> {defn.name!r} at {position}")
Matrix composition uses parent @ local, matching the transform hierarchy.
Geometry inside defn.entities remains definition-local until the composed
matrix is applied.
Material inheritance¶
When a component instance has a material_id set, faces inside its definition
that have front_material_id = None use the instance’s material.
lookup = {m.id: m for m in model.materials}
for inst in model.entities.component_instances:
defn = defn_map[inst.definition_id]
mesh = defn.entities.prepare_mesh(
name=inst.name or defn.name,
material_lookup=lookup,
inherited_material_id=inst.material_id,
)
for face in mesh.faces:
print(face.material_name or "no material")
This prepares one definition for one placement. Nested placements repeat the same rule with the effective parent material; explicitly painted faces always win over the instance override.
Creating components with transforms¶
Compose rotation and translation matrices explicitly before creating each placement:
import math, skppy
model = skppy.new_model()
pillar = model.add_definition("Pillar")
e = pillar.entities
e.add_face([(0, 0, 0), (12, 0, 0), (12, 12, 0), (0, 12, 0)])
# Rotate each pillar 45 degrees around its local Z axis.
rot = skppy.Transform.from_rotation_z(math.radians(45))
for row in range(3):
for col in range(3):
translation = skppy.Transform.from_translation(row * 200, col * 200, 0)
# Apply local rotation first, then move the result into the grid.
placement = skppy.Transform(translation.matrix @ rot.matrix)
model.entities.add_instance(pillar, placement, name=f"P-{row}-{col}")
model.save("pillars.skp")
Transform stores a NumPy 4x4 matrix internally. Passing the composed matrix
back to Transform() converts it to the 13-value representation stored on the
instance.
Groups¶
Groups are created the same way as instances but skppy represents them
separately. Use Entities.groups to iterate:
for grp in model.entities.groups:
defn = defn_map[grp.definition_id]
t = skppy.Transform(grp.transform).translation()
print(f"Group {grp.name!r} at ({t.x:.1f}, {t.y:.1f}, {t.z:.1f})")
print(f" {len(defn.entities.faces)} faces inside")
Group.transform and ComponentInstance.transform are stored as plain
13-value lists. Wrap either in Transform before using matrix helpers.
Entity relationships¶
Some models store directed relationships between entities in the same root or
component-definition scope. They are available through
Entities.relationships after loading:
for relationship in model.entities.relationships:
print(relationship.source_id, "->", relationship.target_id)
The IDs refer to entities in that Entities collection. An endpoint is None
when the source file contains a dangling reference.
Attribute dictionaries¶
Model properties are available in model.attribute_dictionaries. Dictionaries
owned by a definition, material, or layer use the model-level ID map:
for definition in model.definitions:
dictionaries = model.attribute_dictionaries_by_object_id.get(definition.id, [])
for dictionary in dictionaries:
print(definition.name, dictionary.name)
Geometry and instance attributes belong to an Entities scope:
for instance in model.entities.component_instances:
dictionaries = model.entities.attribute_dictionaries_by_entity_id.get(
instance.id, []
)
for dictionary in dictionaries:
print(instance.name, dictionary.name)
These maps keep ownership explicit without exposing archive object indices.