Skip to content

kumiki.patternbook

Flat import path

kumiki/__init__.py re-exports everything on this page via from kumiki import *, so every name below is also available directly as kumiki.PatternLambda -- you do not need to import from the submodule path shown in the heading above.

kumiki.patternbook

PatternBook - Helper structure for generating and organizing example patterns

This module provides a convenient way to organize multiple patterns (frames or CSG objects) and raise them at different positions for visualization and testing.

PatternLambda module-attribute

PatternLambda = Callable[..., Union[Frame, CutCSG]]

Pattern dataclass

Pattern(path: str, lambda_: PatternLambda, tags: List[str] = list(), pattern_type: Literal['frame', 'csg'] = 'frame')

A single renderable pattern in the new pattern system.

hierarchical path like "corner_joints/cut_plain_miter_joint".

Each path segment is an implicit tag for filtering.

lambda_: callable(center: V3, **kwargs) -> Frame | CutCSG tags: explicit tags. Special values: 'main' (default display when file opened), 'poop' (hide from sidebar). pattern_type: 'frame' or 'csg'

path instance-attribute

path: str

lambda_ instance-attribute

lambda_: PatternLambda

tags class-attribute instance-attribute

tags: List[str] = field(default_factory=list)

pattern_type class-attribute instance-attribute

pattern_type: Literal['frame', 'csg'] = 'frame'

name property

name: str

path_segments property

path_segments: List[str]

all_tags

all_tags() -> List[str]

Return all tags including implicit path segment tags.

Source code in kumiki/patternbook.py
def all_tags(self) -> List[str]:
    """Return all tags including implicit path segment tags."""
    return list(self.path_segments) + list(self.tags)

raise_at

raise_at(center: Optional[Any] = None, **kwargs: Any) -> Any

Raise this pattern at the given center (defaults to origin).

Source code in kumiki/patternbook.py
def raise_at(self, center: Optional[Any] = None, **kwargs: Any) -> Any:
    """Raise this pattern at the given center (defaults to origin)."""
    if center is None:
        center = create_v3(scalar(0), scalar(0), scalar(0))
    return self.lambda_(center, **kwargs)

PatternMetadata dataclass

PatternMetadata(pattern_name: str, pattern_group_names: List[str] = list(), pattern_type: Literal['frame', 'csg'] = 'frame')

Metadata describing a pattern in the pattern book.

Attributes:

Name Type Description
pattern_name str

Unique name for this pattern

pattern_group_names List[str]

List of group names to organize related patterns

pattern_type Literal['frame', 'csg']

Type of pattern - either 'frame' or 'csg'

pattern_name instance-attribute

pattern_name: str

pattern_group_names class-attribute instance-attribute

pattern_group_names: List[str] = field(default_factory=list)

pattern_type class-attribute instance-attribute

pattern_type: Literal['frame', 'csg'] = 'frame'

__post_init__

__post_init__()

Validate pattern type.

Source code in kumiki/patternbook.py
def __post_init__(self):
    """Validate pattern type."""
    if self.pattern_type not in ['frame', 'csg']:
        raise ValueError(f"pattern_type must be 'frame' or 'csg', got: {self.pattern_type}")

PatternBook dataclass

PatternBook(patterns: List[Tuple[PatternMetadata, PatternLambda]] = list())

A collection of patterns with functions to raise them at different positions.

Patterns can be either Frame objects or CutCSG objects, and can be organized into groups for batch visualization with spacing.

Attributes:

Name Type Description
patterns List[Tuple[PatternMetadata, PatternLambda]]

List of (PatternMetadata, PatternLambda) pairs

patterns class-attribute instance-attribute

patterns: List[Tuple[PatternMetadata, PatternLambda]] = field(default_factory=list)

__post_init__

__post_init__()

Validate pattern names are unique.

Source code in kumiki/patternbook.py
def __post_init__(self):
    """Validate pattern names are unique."""
    names = [metadata.pattern_name for metadata, _ in self.patterns]
    if len(names) != len(set(names)):
        duplicates = [name for name in names if names.count(name) > 1]
        raise ValueError(f"Duplicate pattern names found: {set(duplicates)}")

raise_pattern

raise_pattern(pattern_name: str, center: Optional[V3] = None, **pattern_kwargs: Any) -> Union[Frame, CutCSG]

Raise a single pattern by name at the specified center location.

Parameters:

Name Type Description Default
pattern_name str

Name of the pattern to raise

required
center Optional[V3]

Center location for the pattern (default: origin)

None

Returns:

Type Description
Union[Frame, CutCSG]

Frame or CutCSG object at the specified location

Raises:

Type Description
ValueError

If pattern_name is not found

Source code in kumiki/patternbook.py
def raise_pattern(
    self,
    pattern_name: str,
    center: Optional[V3] = None,
    **pattern_kwargs: Any,
) -> Union[Frame, CutCSG]:
    """
    Raise a single pattern by name at the specified center location.

    Args:
        pattern_name: Name of the pattern to raise
        center: Center location for the pattern (default: origin)

    Returns:
        Frame or CutCSG object at the specified location

    Raises:
        ValueError: If pattern_name is not found
    """
    if center is None:
        center = create_v3(scalar(0), scalar(0), scalar(0))

    # Find the pattern by name
    for metadata, pattern_lambda in self.patterns:
        if metadata.pattern_name == pattern_name:
            return pattern_lambda(center, **pattern_kwargs)

    # Pattern not found
    available_names = [m.pattern_name for m, _ in self.patterns]
    raise ValueError(f"Pattern '{pattern_name}' not found. Available patterns: {available_names}")

raise_pattern_group

raise_pattern_group(group_name: str, separation_distance: Union[float, int], start_center: Optional[V3] = None) -> Union[Frame, List[CutCSG]]

Raise all patterns in a group, separated by the specified distance along the X-axis.

For frame patterns: Breaks apart individual frames and builds a megaframe from all of them. For CSG patterns: Returns a list of CSG objects.

Parameters:

Name Type Description Default
group_name str

Name of the group to raise

required
separation_distance Union[float, int]

Distance between pattern centers along X-axis

required
start_center Optional[V3]

Starting center location (default: origin)

None

Returns:

Type Description
Union[Frame, List[CutCSG]]

For frame patterns: A single Frame containing all cut timbers from all patterns

Union[Frame, List[CutCSG]]

For CSG patterns: A list of CutCSG objects

Raises:

Type Description
ValueError

If group_name is not found or if frame and CSG patterns are mixed

Source code in kumiki/patternbook.py
def raise_pattern_group(
    self, 
    group_name: str, 
    separation_distance: Union[float, int],
    start_center: Optional[V3] = None
) -> Union[Frame, List[CutCSG]]:
    """
    Raise all patterns in a group, separated by the specified distance along the X-axis.

    For frame patterns: Breaks apart individual frames and builds a megaframe from all of them.
    For CSG patterns: Returns a list of CSG objects.

    Args:
        group_name: Name of the group to raise
        separation_distance: Distance between pattern centers along X-axis
        start_center: Starting center location (default: origin)

    Returns:
        For frame patterns: A single Frame containing all cut timbers from all patterns
        For CSG patterns: A list of CutCSG objects

    Raises:
        ValueError: If group_name is not found or if frame and CSG patterns are mixed
    """
    separation_distance, start_center = self._normalize_spacing_args(
        separation_distance,
        start_center,
    )

    # Find all patterns in the group (check if group_name is in pattern_group_names list)
    group_patterns = [
        (metadata, pattern_lambda) 
        for metadata, pattern_lambda in self.patterns 
        if group_name in metadata.pattern_group_names
    ]

    if not group_patterns:
        available_groups = self.list_groups()
        raise ValueError(f"Group '{group_name}' not found. Available groups: {available_groups}")

    # Check that all patterns in the group have the same type
    pattern_types = set(metadata.pattern_type for metadata, _ in group_patterns)
    if len(pattern_types) > 1:
        raise ValueError(
            f"Cannot mix frame and CSG patterns in the same group. "
            f"Group '{group_name}' contains types: {pattern_types}"
        )

    pattern_type = list(pattern_types)[0]

    # Raise all patterns with appropriate spacing
    results = self._raise_entries_with_spacing(
        group_patterns,
        separation_distance,
        start_center,
    )

    # Process results based on pattern type
    if pattern_type == 'frame':
        # Combine all frames into a megaframe
        frame_results = [result for result in results if isinstance(result, Frame)]
        if len(frame_results) != len(results):
            raise TypeError(
                f"Group '{group_name}' contains non-frame results while pattern_type='frame'"
            )
        return self._combine_frames(frame_results, group_name)
    else:  # pattern_type == 'csg'
        # Return list of CSG objects
        csg_results = [result for result in results if isinstance(result, CutCSG)]
        if len(csg_results) != len(results):
            raise TypeError(
                f"Group '{group_name}' contains non-CSG results while pattern_type='csg'"
            )
        return csg_results

raise_patternbook_as_frame

raise_patternbook_as_frame(separation_distance: Union[float, int] = scalar(2), start_center: Optional[V3] = None) -> Frame

Raise this PatternBook as a single combined Frame.

Prefers raise_pattern_group when one group covers all patterns. Falls back to raising each frame pattern by position and combining.

Source code in kumiki/patternbook.py
def raise_patternbook_as_frame(
    self,
    separation_distance: Union[float, int] = scalar(2),
    start_center: Optional[V3] = None,
) -> Frame:
    """Raise this PatternBook as a single combined Frame.

    Prefers ``raise_pattern_group`` when one group covers all patterns.
    Falls back to raising each frame pattern by position and combining.
    """
    pattern_names = self.list_patterns()
    if not pattern_names:
        raise ValueError("PatternBook is empty")

    if len(pattern_names) == 1:
        single = self.raise_pattern(pattern_names[0], center=start_center)
        return self._coerce_pattern_result_to_frame(single, pattern_names[0])

    separation_distance, start_center = self._normalize_spacing_args(
        separation_distance,
        start_center,
    )

    all_pattern_set = set(pattern_names)
    umbrella_group: Optional[str] = None
    for group_name in self.list_groups():
        if set(self.get_patterns_in_group(group_name)) >= all_pattern_set:
            umbrella_group = group_name
            break

    if umbrella_group is not None:
        grouped_result = self.raise_pattern_group(
            umbrella_group,
            separation_distance=separation_distance,
            start_center=start_center,
        )
        return self._coerce_pattern_result_to_frame(grouped_result, umbrella_group)

    results = self._raise_entries_with_spacing(
        self.patterns,
        separation_distance,
        start_center,
    )
    return self._combine_pattern_results_as_frame(results, "all_patterns")

list_patterns

list_patterns() -> List[str]

List all pattern names in the book.

Returns:

Type Description
List[str]

List of pattern names

Source code in kumiki/patternbook.py
def list_patterns(self) -> List[str]:
    """
    List all pattern names in the book.

    Returns:
        List of pattern names
    """
    return [metadata.pattern_name for metadata, _ in self.patterns]

list_groups

list_groups() -> List[str]

List all unique group names in the book.

Returns:

Type Description
List[str]

List of group names (flattened from all patterns)

Source code in kumiki/patternbook.py
def list_groups(self) -> List[str]:
    """
    List all unique group names in the book.

    Returns:
        List of group names (flattened from all patterns)
    """
    groups = set()
    for metadata, _ in self.patterns:
        groups.update(metadata.pattern_group_names)
    return sorted(list(groups))

get_patterns_in_group

get_patterns_in_group(group_name: str) -> List[str]

Get all pattern names in a specific group.

Parameters:

Name Type Description Default
group_name str

Name of the group

required

Returns:

Type Description
List[str]

List of pattern names in the group

Source code in kumiki/patternbook.py
def get_patterns_in_group(self, group_name: str) -> List[str]:
    """
    Get all pattern names in a specific group.

    Args:
        group_name: Name of the group

    Returns:
        List of pattern names in the group
    """
    return [
        metadata.pattern_name 
        for metadata, _ in self.patterns 
        if group_name in metadata.pattern_group_names
    ]

merge

merge(other: PatternBook) -> PatternBook

Merge another PatternBook into a new PatternBook.

Parameters:

Name Type Description Default
other PatternBook

Another PatternBook to merge with this one

required

Returns:

Type Description
PatternBook

A new PatternBook containing patterns from both books

Raises:

Type Description
ValueError

If there are duplicate pattern names

Source code in kumiki/patternbook.py
def merge(self, other: 'PatternBook') -> 'PatternBook':
    """
    Merge another PatternBook into a new PatternBook.

    Args:
        other: Another PatternBook to merge with this one

    Returns:
        A new PatternBook containing patterns from both books

    Raises:
        ValueError: If there are duplicate pattern names
    """
    all_patterns = self.patterns + other.patterns
    return PatternBook(patterns=all_patterns)

merge_multiple staticmethod

merge_multiple(pattern_books: List[PatternBook]) -> PatternBook

Merge multiple PatternBooks into a single PatternBook.

Parameters:

Name Type Description Default
pattern_books List[PatternBook]

List of PatternBooks to merge

required

Returns:

Type Description
PatternBook

A new PatternBook containing patterns from all books

Raises:

Type Description
ValueError

If there are duplicate pattern names

Source code in kumiki/patternbook.py
@staticmethod
def merge_multiple(pattern_books: List['PatternBook']) -> 'PatternBook':
    """
    Merge multiple PatternBooks into a single PatternBook.

    Args:
        pattern_books: List of PatternBooks to merge

    Returns:
        A new PatternBook containing patterns from all books

    Raises:
        ValueError: If there are duplicate pattern names
    """
    all_patterns = []
    for book in pattern_books:
        all_patterns.extend(book.patterns)
    return PatternBook(patterns=all_patterns)

make_pattern_from_joint

make_pattern_from_joint(joint_func: Callable[..., Joint]) -> PatternLambda

Convert a joint function (no args, returns a joint with cut_timbers and jointAccessories) to a pattern lambda that accepts center and returns a Frame with all timbers and accessories translated by center.

Source code in kumiki/patternbook.py
def make_pattern_from_joint(joint_func: Callable[..., Joint]) -> PatternLambda:
    """
    Convert a joint function (no args, returns a joint with cut_timbers and jointAccessories)
    to a pattern lambda that accepts center and returns a Frame with all timbers and
    accessories translated by center.
    """
    def pattern_lambda(center: V3, **pattern_kwargs: Any) -> Frame:
        joint = joint_func(**pattern_kwargs)
        translated_timbers: List[CutTimber] = []
        # Build translated cuttings dict in parallel so that the Cutting objects
        # stored in the joint and in the CutTimbers are identical (same identity),
        # which is required by serialize_layers's identity-based cut_indices lookup.
        translated_cuttings: Dict[str, Any] = {}
        for name, cutting in joint.cuttings.items():
            new_position = cutting.timber.get_bottom_position_global() + center
            # Preserve the concrete timber subclass (RoundTimber, MeshTimber, etc.) — only override the transform.
            translated_timber = replace(
                cutting.timber,
                transform=Transform(position=new_position, orientation=cutting.timber.orientation),
            )
            translated_cut = replace(cutting, timber=translated_timber)
            translated_timbers.append(CutTimber(timber=translated_timber, cuts=[translated_cut]))
            translated_cuttings[name] = translated_cut

        translated_accessories: List[Accessory] = []
        if joint.jointAccessories:
            for accessory in joint.jointAccessories.values():
                if isinstance(accessory, Peg):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation
                    )
                    translated_accessories.append(Peg(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        size=accessory.size,
                        shape=accessory.shape,
                        forward_length=accessory.forward_length,
                        stickout_length=accessory.stickout_length
                    ))
                elif isinstance(accessory, Wedge):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation
                    )
                    translated_accessories.append(Wedge(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        base_width=accessory.base_width,
                        tip_width=accessory.tip_width,
                        height=accessory.height,
                        length=accessory.length,
                        stickout_length=accessory.stickout_length
                    ))
                elif isinstance(accessory, CSGAccessory):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation,
                    )
                    translated_accessories.append(CSGAccessory(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        positive_csg=accessory.positive_csg,
                    ))
                else:
                    translated_accessories.append(accessory)

        translated_joint = replace(joint, cuttings=translated_cuttings)
        return Frame(
            cut_timbers=translated_timbers,
            accessories=translated_accessories,
            source_joints=[translated_joint],
        )

    setattr(pattern_lambda, "__signature__", _build_pattern_lambda_signature(joint_func))
    return pattern_lambda

make_pattern_from_frame

make_pattern_from_frame(frame_func: Callable[..., Frame]) -> PatternLambda

Convert a Frame-returning function (no args) to a pattern lambda that accepts center and returns a Frame with all timbers and accessories translated by center.

Source code in kumiki/patternbook.py
def make_pattern_from_frame(frame_func: Callable[..., Frame]) -> PatternLambda:
    """
    Convert a Frame-returning function (no args) to a pattern lambda that accepts center
    and returns a Frame with all timbers and accessories translated by center.
    """
    def pattern_lambda(center: V3, **pattern_kwargs: Any) -> Frame:
        frame = frame_func(**pattern_kwargs)
        translated_timbers = []
        for cut_timber in frame.cut_timbers:
            new_position = cut_timber.timber.get_bottom_position_global() + center
            # Preserve the concrete timber subclass (RoundTimber, MeshTimber, etc.) — only override the transform.
            translated_timber = replace(
                cut_timber.timber,
                transform=Transform(position=new_position, orientation=cut_timber.timber.orientation),
            )
            translated_timbers.append(CutTimber(timber=translated_timber, cuts=cut_timber.cuts))

        translated_accessories: List[Accessory] = []
        if frame.accessories:
            for accessory in frame.accessories:
                if isinstance(accessory, Peg):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation
                    )
                    translated_accessories.append(Peg(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        size=accessory.size,
                        shape=accessory.shape,
                        forward_length=accessory.forward_length,
                        stickout_length=accessory.stickout_length
                    ))
                elif isinstance(accessory, Wedge):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation
                    )
                    translated_accessories.append(Wedge(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        base_width=accessory.base_width,
                        tip_width=accessory.tip_width,
                        height=accessory.height,
                        length=accessory.length,
                        stickout_length=accessory.stickout_length
                    ))
                elif isinstance(accessory, CSGAccessory):
                    translated_transform = Transform(
                        position=accessory.transform.position + center,
                        orientation=accessory.transform.orientation,
                    )
                    translated_accessories.append(CSGAccessory(
                        ticket=accessory.ticket,
                        transform=translated_transform,
                        positive_csg=accessory.positive_csg,
                    ))
                else:
                    translated_accessories.append(accessory)

        return Frame(cut_timbers=translated_timbers, accessories=translated_accessories)

    setattr(pattern_lambda, "__signature__", _build_pattern_lambda_signature(frame_func))
    return pattern_lambda

make_pattern_from_csg

make_pattern_from_csg(csg_func: Callable[..., CutCSG]) -> PatternLambda

Convert a CSG-returning function (no args) to a pattern lambda that accepts center and returns the CSG translated by center. Consistent with frame/joint patterns: the returned CSG is positioned at the given center.

Source code in kumiki/patternbook.py
def make_pattern_from_csg(csg_func: Callable[..., CutCSG]) -> PatternLambda:
    """
    Convert a CSG-returning function (no args) to a pattern lambda that accepts center
    and returns the CSG translated by center. Consistent with frame/joint patterns:
    the returned CSG is positioned at the given center.
    """
    def pattern_lambda(center: V3, **pattern_kwargs: Any) -> CutCSG:
        return translate_csg(csg_func(**pattern_kwargs), center)

    setattr(pattern_lambda, "__signature__", _build_pattern_lambda_signature(csg_func))
    return pattern_lambda