Skip to content

kumiki.csg.symmetry

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.DEFAULT_TOLERANCE -- you do not need to import from the submodule path shown in the heading above.

kumiki.csg.symmetry

Find which cut timbers are the same piece: identical, rotated, or mirrored (a reflection).

For frame authors who want to solve or draw a piece once for all its copies. It reports matches and what differs between them (labels, reference features, tags); what to do with them is the caller's choice.

Two timbers are compared in their own local frames (x width, y height, z length):

  • Their CSG trees must have the same shape (node types and children, in order).
  • Each of 16 symmetries (flips of the three axes, and swapping width with height) is tried by walking both trees in step, mapping the second's primitives through it. Directions, sizes, radii and profile shapes must agree; positions give equations linear in one unknown shift, solved at the end.
  • A symmetry fits when every equation holds. Determinant +1 is a rotation, -1 a reflection.

Trees are compared, not solids, so a match is never claimed falsely, but two pieces built differently may match under fewer symmetries than their shapes would (e.g. peg-hole cutters running from opposite faces).

DEFAULT_TOLERANCE module-attribute

DEFAULT_TOLERANCE = 1e-06

Equation module-attribute

Equation = Tuple[np.ndarray, float]

SymmetryKind

Bases: Enum

IDENTICAL class-attribute instance-attribute

IDENTICAL = 'identical'

ROTATION class-attribute instance-attribute

ROTATION = 'rotation'

REFLECTION class-attribute instance-attribute

REFLECTION = 'reflection'

Symmetry dataclass

Symmetry(matrix: ndarray, shift: ndarray, kind: SymmetryKind, description: str)

A map from one piece onto another in their local frames: point p of the second is at matrix @ p + shift on the first.

matrix instance-attribute

matrix: ndarray

shift instance-attribute

shift: ndarray

kind instance-attribute

description instance-attribute

description: str

TimberMatch dataclass

TimberMatch(cut: CutTimber, kind: SymmetryKind, symmetries: Tuple[Symmetry, ...], differences: Tuple[str, ...])

A timber matching its group's representative.

kind is IDENTICAL if the identity fits, else ROTATION if any rotation does, else REFLECTION (a handed piece). symmetries lists all that fit. differences are what the geometry doesn't settle (labels, reference features, tags); they never prevent a match.

cut instance-attribute

cut: CutTimber

kind instance-attribute

symmetries instance-attribute

symmetries: Tuple[Symmetry, ...]

differences instance-attribute

differences: Tuple[str, ...]

SymmetricTimberGroup dataclass

SymmetricTimberGroup(representative: CutTimber, matches: Tuple[TimberMatch, ...])

One distinct piece: the first timber found of it, and every timber that matches it.

representative instance-attribute

representative: CutTimber

matches instance-attribute

matches: Tuple[TimberMatch, ...]

csg_symmetries

csg_symmetries(a: CutCSG, b: CutCSG, tolerance: float = DEFAULT_TOLERANCE) -> List[Symmetry]

Return every Symmetry mapping tree b onto tree a (each in its own local frame): identity first, then rotations, then reflections.

Source code in kumiki/csg/symmetry.py
def csg_symmetries(a: CutCSG, b: CutCSG, tolerance: float = DEFAULT_TOLERANCE) -> List[Symmetry]:
    """Return every Symmetry mapping tree `b` onto tree `a` (each in its own local frame): identity first, then rotations, then reflections."""
    found = []
    for matrix in _CANDIDATES:
        try:
            shift = _shift(_walk(a, b, matrix, tolerance), tolerance)
        except _Mismatch:
            continue
        if shift is not None:
            found.append(Symmetry(matrix=matrix, shift=shift, kind=_kind(matrix), description=_describe(matrix)))
    return found

timber_symmetries

timber_symmetries(a: CutTimber, b: CutTimber, tolerance: float = DEFAULT_TOLERANCE) -> List[Symmetry]

Return every Symmetry mapping cut timber b onto a. With a twice, the piece's own symmetries.

Source code in kumiki/csg/symmetry.py
def timber_symmetries(a: 'CutTimber', b: 'CutTimber', tolerance: float = DEFAULT_TOLERANCE) -> List[Symmetry]:
    """Return every Symmetry mapping cut timber `b` onto `a`. With `a` twice, the piece's own symmetries."""
    return csg_symmetries(a.render_timber_with_cuts_csg_local(), b.render_timber_with_cuts_csg_local(), tolerance)

match_timbers

match_timbers(cut_timbers: Sequence[CutTimber], tolerance: float = DEFAULT_TOLERANCE) -> List[SymmetricTimberGroup]

Return the distinct pieces among cut_timbers, in the order first met.

Each timber joins the first group whose representative it matches (timber_symmetries), else starts its own. Only timbers with the same tree shape are compared.

Source code in kumiki/csg/symmetry.py
def match_timbers(cut_timbers: Sequence['CutTimber'], tolerance: float = DEFAULT_TOLERANCE) -> List[SymmetricTimberGroup]:
    """Return the distinct pieces among `cut_timbers`, in the order first met.

    Each timber joins the first group whose representative it matches (timber_symmetries), else starts
    its own. Only timbers with the same tree shape are compared.
    """
    groups: List[Tuple['CutTimber', Tuple, List[TimberMatch]]] = []
    for cut in cut_timbers:
        shape = _shape(cut.render_timber_with_cuts_csg_local())
        for representative, representative_shape, matches in groups:
            if representative_shape != shape:
                continue
            symmetries = timber_symmetries(representative, cut, tolerance)
            if not symmetries:
                continue
            kinds = {symmetry.kind for symmetry in symmetries}
            kind = next(kind for kind in SymmetryKind if kind in kinds)
            matches.append(TimberMatch(cut=cut, kind=kind, symmetries=tuple(symmetries),
                                       differences=_differences(representative, cut)))
            break
        else:
            groups.append((cut, shape, []))
    return [SymmetricTimberGroup(representative=representative, matches=tuple(matches))
            for representative, _shape_of, matches in groups]