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

skppy class

Description

Component definition

ComponentDefinition

Named geometry template. Stored once.

Component instance

ComponentInstance

Placed reference. Many instances -> one definition.

Group

Group

One-off instance with an anonymous definition.

Image

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.


Detecting shared definitions

A definition used by more than one instance is shared:

from collections import Counter

counts = Counter(
    placed.definition_id
    for placed, _defn, _world in iter_instances(model.entities, defn_map)
)

for defn_id, count in counts.most_common():
    defn = defn_map[defn_id]
    print(f"{defn.name!r}: {count} instances")

Counting the recursive iterator includes nested instances and groups. Counting only model.entities.component_instances would describe root placements only.


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.