Skip to content

kumiki.blueprint

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

kumiki.blueprint

Kumiki - Blueprint export module (STL and STEP)

Exports CutTimber and Frame objects to standard CAD interchange formats.

STL export uses trimesh (triangle mesh). STEP export uses OCP (OpenCascade Python bindings from cadquery-ocp) to produce exact B-rep geometry.

BRepPrimAPI_MakeBox module-attribute

BRepPrimAPI_MakeBox = getattr(_BRepPrimAPI, 'BRepPrimAPI_MakeBox')

BRepPrimAPI_MakeCylinder module-attribute

BRepPrimAPI_MakeCylinder = getattr(_BRepPrimAPI, 'BRepPrimAPI_MakeCylinder')

BRepAlgoAPI_Fuse module-attribute

BRepAlgoAPI_Fuse = getattr(_BRepAlgoAPI, 'BRepAlgoAPI_Fuse')

BRepAlgoAPI_Cut module-attribute

BRepAlgoAPI_Cut = getattr(_BRepAlgoAPI, 'BRepAlgoAPI_Cut')

BRepBuilderAPI_MakeEdge module-attribute

BRepBuilderAPI_MakeEdge = getattr(_BRepBuilderAPI, 'BRepBuilderAPI_MakeEdge')

BRepBuilderAPI_MakeWire module-attribute

BRepBuilderAPI_MakeWire = getattr(_BRepBuilderAPI, 'BRepBuilderAPI_MakeWire')

BRepBuilderAPI_MakeFace module-attribute

BRepBuilderAPI_MakeFace = getattr(_BRepBuilderAPI, 'BRepBuilderAPI_MakeFace')

BRepBuilderAPI_Transform module-attribute

BRepBuilderAPI_Transform = getattr(_BRepBuilderAPI, 'BRepBuilderAPI_Transform')

BRepPrimAPI_MakePrism module-attribute

BRepPrimAPI_MakePrism = getattr(_BRepPrimAPI, 'BRepPrimAPI_MakePrism')

BRepOffsetAPI_ThruSections module-attribute

BRepOffsetAPI_ThruSections = getattr(_BRepOffsetAPI, 'BRepOffsetAPI_ThruSections')

gp_Pnt module-attribute

gp_Pnt = getattr(_gp, 'gp_Pnt')

gp_Vec module-attribute

gp_Vec = getattr(_gp, 'gp_Vec')

gp_Dir module-attribute

gp_Dir = getattr(_gp, 'gp_Dir')

gp_Ax2 module-attribute

gp_Ax2 = getattr(_gp, 'gp_Ax2')

gp_Trsf module-attribute

gp_Trsf = getattr(_gp, 'gp_Trsf')

gp_Mat module-attribute

gp_Mat = getattr(_gp, 'gp_Mat')

gp_XYZ module-attribute

gp_XYZ = getattr(_gp, 'gp_XYZ')

TopoDS_Shape module-attribute

TopoDS_Shape = getattr(_TopoDS, 'TopoDS_Shape')

TopoDS_Compound module-attribute

TopoDS_Compound = getattr(_TopoDS, 'TopoDS_Compound')

BRep_Builder module-attribute

BRep_Builder = getattr(_BRep, 'BRep_Builder')

STEPControl_Writer module-attribute

STEPControl_Writer = getattr(_STEPControl, 'STEPControl_Writer')

STEPControl_AsIs module-attribute

STEPControl_AsIs = getattr(_STEPControl, 'STEPControl_AsIs')

STEPCAFControl_Writer module-attribute

STEPCAFControl_Writer = getattr(_STEPCAFControl, 'STEPCAFControl_Writer')

Interface_Static module-attribute

Interface_Static = getattr(_Interface, 'Interface_Static')

TopLoc_Location module-attribute

TopLoc_Location = getattr(_TopLoc, 'TopLoc_Location')

TDocStd_Document module-attribute

TDocStd_Document = getattr(_TDocStd, 'TDocStd_Document')

XCAFDoc_DocumentTool module-attribute

XCAFDoc_DocumentTool = getattr(_XCAFDoc, 'XCAFDoc_DocumentTool')

TDataStd_Name module-attribute

TDataStd_Name = getattr(_TDataStd, 'TDataStd_Name')

TCollection_ExtendedString module-attribute

TCollection_ExtendedString = getattr(_TCollection, 'TCollection_ExtendedString')

sympy_to_float

sympy_to_float(value: Union[float, int]) -> float
Source code in kumiki/blueprint.py
def sympy_to_float(value: Union[float, int]) -> float:
    return float(value)

export_cut_timber_stl

export_cut_timber_stl(cut_timber: CutTimber, filepath: Union[str, Path], *, local: bool = True) -> None

Export a single CutTimber to an STL file (metres).

Parameters:

Name Type Description Default
cut_timber CutTimber

The timber (with cuts applied) to export.

required
filepath Union[str, Path]

Destination path. Parent directories are created if needed.

required
local bool

If True (default), export in the timber's own local coordinates (bottom of timber at the origin). If False, export in global coordinates (its actual assembled position and orientation).

True
Source code in kumiki/blueprint.py
def export_cut_timber_stl(cut_timber: CutTimber, filepath: Union[str, Path], *, local: bool = True) -> None:
    """Export a single CutTimber to an STL file (metres).

    Args:
        cut_timber: The timber (with cuts applied) to export.
        filepath: Destination path. Parent directories are created if needed.
        local: If True (default), export in the timber's own local coordinates
            (bottom of timber at the origin). If False, export in global
            coordinates (its actual assembled position and orientation).
    """
    filepath = Path(filepath)
    filepath.parent.mkdir(parents=True, exist_ok=True)
    mesh = _cut_timber_to_trimesh(cut_timber, local=local)
    mesh.export(str(filepath), file_type="stl")

export_frame_stl

export_frame_stl(frame: Frame, output_dir: Union[str, Path], *, local: bool = True, combined: bool = False, include_accessories: bool = True) -> List[Path]

Export every timber and accessory in a Frame to STL files.

Parameters:

Name Type Description Default
frame Frame

The frame to export.

required
output_dir Union[str, Path]

Directory for the STL files.

required
local bool

If True (default), each individual file is exported in that part's own local coordinates (bottom at the origin). If False, individual files are exported in global coordinates (their actual assembled position and orientation). The optional combined file is always in global coordinates regardless of this flag, since a merged mesh is only meaningful with parts in their real positions.

True
combined bool

If True, also write a single _combined.stl with all timbers and accessories merged into one mesh.

False
include_accessories bool

If True, export accessory meshes. If False, export only timbers.

True

Returns:

Type Description
List[Path]

List of paths written.

Source code in kumiki/blueprint.py
def export_frame_stl(
    frame: Frame,
    output_dir: Union[str, Path],
    *,
    local: bool = True,
    combined: bool = False,
    include_accessories: bool = True,
) -> List[Path]:
    """Export every timber and accessory in a Frame to STL files.

    Args:
        frame: The frame to export.
        output_dir: Directory for the STL files.
        local: If True (default), each individual file is exported in that
            part's own local coordinates (bottom at the origin). If False,
            individual files are exported in global coordinates (their actual
            assembled position and orientation). The optional combined file is
            always in global coordinates regardless of this flag, since a
            merged mesh is only meaningful with parts in their real positions.
        combined: If True, also write a single ``_combined.stl`` with all
            timbers and accessories merged into one mesh.
        include_accessories: If True, export accessory meshes. If False,
            export only timbers.

    Returns:
        List of paths written.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    written: List[Path] = []
    used_names: set[str] = set()

    def _next_available_name(base_name: str) -> str:
        candidate = base_name
        suffix = 2
        while candidate in used_names:
            candidate = f"{base_name}_{suffix}"
            suffix += 1
        used_names.add(candidate)
        return candidate

    combined_meshes: list[trimesh.Trimesh] = []
    for i, ct in enumerate(frame.cut_timbers):
        name = _next_available_name(ct.timber.ticket.path or f"timber_{i}")
        mesh = _cut_timber_to_trimesh(ct, local=local)
        dest = output_dir / f"{name}.stl"
        mesh.export(str(dest), file_type="stl")
        written.append(dest)
        if combined:
            combined_meshes.append(mesh if not local else _cut_timber_to_trimesh(ct, local=False))

    if include_accessories:
        for i, accessory in enumerate(frame.accessories):
            ticket_name = accessory.ticket.path
            if ticket_name and ticket_name != "[no-name]":
                base_name = ticket_name
            else:
                base_name = f"accessory_{i}"
            name = _next_available_name(base_name)
            mesh = _joint_accessory_to_trimesh(accessory, local=local)
            dest = output_dir / f"{name}.stl"
            mesh.export(str(dest), file_type="stl")
            written.append(dest)
            if combined:
                combined_meshes.append(mesh if not local else _joint_accessory_to_trimesh(accessory, local=False))

    if combined and combined_meshes:
        merged = trimesh.util.concatenate(combined_meshes)
        dest = output_dir / "_combined.stl"
        merged.export(str(dest), file_type="stl")
        written.append(dest)

    return written

export_frame_obj

export_frame_obj(frame: Frame, output_dir: Union[str, Path], *, local: bool = True, combined: bool = False, include_accessories: bool = True) -> List[Path]

Export every timber and accessory in a Frame to OBJ files.

If True (default), each individual file is exported in that part's

own local coordinates (bottom at the origin). If False, individual files are exported in global coordinates. The combined file (if requested) is always in global coordinates regardless of this flag.

Source code in kumiki/blueprint.py
def export_frame_obj(
    frame: Frame,
    output_dir: Union[str, Path],
    *,
    local: bool = True,
    combined: bool = False,
    include_accessories: bool = True,
) -> List[Path]:
    """Export every timber and accessory in a Frame to OBJ files.

    local: If True (default), each individual file is exported in that part's
        own local coordinates (bottom at the origin). If False, individual
        files are exported in global coordinates. The combined file (if
        requested) is always in global coordinates regardless of this flag.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    written: List[Path] = []
    used_names: set[str] = set()

    def _next_available_name(base_name: str) -> str:
        candidate = base_name
        suffix = 2
        while candidate in used_names:
            candidate = f"{base_name}_{suffix}"
            suffix += 1
        used_names.add(candidate)
        return candidate

    combined_meshes: list[trimesh.Trimesh] = []
    for i, ct in enumerate(frame.cut_timbers):
        name = _next_available_name(ct.timber.ticket.path or f"timber_{i}")
        mesh = _cut_timber_to_trimesh(ct, local=local)
        dest = output_dir / f"{name}.obj"
        mesh.export(str(dest), file_type="obj")
        written.append(dest)
        if combined:
            combined_meshes.append(mesh if not local else _cut_timber_to_trimesh(ct, local=False))

    if include_accessories:
        for i, accessory in enumerate(frame.accessories):
            ticket_name = accessory.ticket.path
            if ticket_name and ticket_name != "[no-name]":
                base_name = ticket_name
            else:
                base_name = f"accessory_{i}"
            name = _next_available_name(base_name)
            mesh = _joint_accessory_to_trimesh(accessory, local=local)
            dest = output_dir / f"{name}.obj"
            mesh.export(str(dest), file_type="obj")
            written.append(dest)
            if combined:
                combined_meshes.append(mesh if not local else _joint_accessory_to_trimesh(accessory, local=False))

    if combined and combined_meshes:
        merged = trimesh.util.concatenate(combined_meshes)
        dest = output_dir / "_combined.obj"
        merged.export(str(dest), file_type="obj")
        written.append(dest)

    return written

export_frame_3mf

export_frame_3mf(frame: Frame, output_dir: Union[str, Path], *, local: bool = True, combined: bool = False, include_accessories: bool = True) -> List[Path]

Export individual STL members plus an optional combined 3MF scene.

If True (default), each individual STL member is exported in its

own local coordinates (bottom at the origin). If False, in global coordinates. The combined 3MF scene (if requested) is always in global coordinates regardless of this flag.

Source code in kumiki/blueprint.py
def export_frame_3mf(
    frame: Frame,
    output_dir: Union[str, Path],
    *,
    local: bool = True,
    combined: bool = False,
    include_accessories: bool = True,
) -> List[Path]:
    """Export individual STL members plus an optional combined 3MF scene.

    local: If True (default), each individual STL member is exported in its
        own local coordinates (bottom at the origin). If False, in global
        coordinates. The combined 3MF scene (if requested) is always in
        global coordinates regardless of this flag.
    """
    if not _THREEMF_AVAILABLE:
        raise ImportError(
            "3MF export requires trimesh soft dependencies 'lxml' and 'networkx'. "
            "Install them with: pip install lxml networkx"
        )

    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    written: List[Path] = []
    used_names: set[str] = set()
    scene = trimesh.Scene()

    def _next_available_name(base_name: str) -> str:
        candidate = base_name
        suffix = 2
        while candidate in used_names:
            candidate = f"{base_name}_{suffix}"
            suffix += 1
        used_names.add(candidate)
        return candidate

    for i, ct in enumerate(frame.cut_timbers):
        name = _next_available_name(ct.timber.ticket.path or f"timber_{i}")
        mesh = _cut_timber_to_trimesh(ct, local=local)
        dest = output_dir / f"{name}.stl"
        mesh.export(str(dest), file_type="stl")
        written.append(dest)
        if combined:
            scene_mesh = mesh if not local else _cut_timber_to_trimesh(ct, local=False)
            scene.add_geometry(_mesh_to_millimeters(scene_mesh), geom_name=name, node_name=name)

    if include_accessories:
        for i, accessory in enumerate(frame.accessories):
            ticket_name = accessory.ticket.path
            if ticket_name and ticket_name != "[no-name]":
                base_name = ticket_name
            else:
                base_name = f"accessory_{i}"
            name = _next_available_name(base_name)
            mesh = _joint_accessory_to_trimesh(accessory, local=local)
            dest = output_dir / f"{name}.stl"
            mesh.export(str(dest), file_type="stl")
            written.append(dest)
            if combined:
                scene_mesh = mesh if not local else _joint_accessory_to_trimesh(accessory, local=False)
                scene.add_geometry(_mesh_to_millimeters(scene_mesh), geom_name=name, node_name=name)

    if combined and len(scene.geometry) > 0:
        dest = output_dir / "_combined.3mf"
        scene.export(file_obj=str(dest), file_type="3mf")
        written.append(dest)

    return written

export_cut_timber_step

export_cut_timber_step(cut_timber: CutTimber, filepath: Union[str, Path], *, local: bool = True) -> None

Export a single CutTimber to a STEP file (millimetres).

Requires OCP (cadquery-ocp). Install with: pip install cadquery-ocp

Parameters:

Name Type Description Default
cut_timber CutTimber

The timber (with cuts applied) to export.

required
filepath Union[str, Path]

Destination path. Parent directories are created if needed.

required
local bool

If True (default), export in the timber's own local coordinates (bottom of timber at the origin). If False, export in global coordinates (its actual assembled position and orientation).

True
Source code in kumiki/blueprint.py
def export_cut_timber_step(cut_timber: CutTimber, filepath: Union[str, Path], *, local: bool = True) -> None:
    """Export a single CutTimber to a STEP file (millimetres).

    Requires OCP (cadquery-ocp). Install with: ``pip install cadquery-ocp``

    Args:
        cut_timber: The timber (with cuts applied) to export.
        filepath: Destination path. Parent directories are created if needed.
        local: If True (default), export in the timber's own local coordinates
            (bottom of timber at the origin). If False, export in global
            coordinates (its actual assembled position and orientation).
    """
    if not _OCP_AVAILABLE:
        raise ImportError(
            "OCP (cadquery-ocp) is required for STEP export. "
            "Install it with: pip install cadquery-ocp"
        )
    filepath = Path(filepath)
    filepath.parent.mkdir(parents=True, exist_ok=True)

    local_csg = cut_timber.render_timber_with_cuts_csg_local()
    export_csg = local_csg if local else adopt_csg(cut_timber.timber.transform, Transform.identity(), local_csg)
    shape = _csg_to_ocp(export_csg)
    body_name = cut_timber.timber.ticket.path or filepath.stem
    _write_step(shape, str(filepath), name=body_name)

export_frame_step

export_frame_step(frame: Frame, output_dir: Union[str, Path], *, local: bool = True, combined: bool = False, include_accessories: bool = True) -> List[Path]

Export every timber in a Frame to individual STEP files.

Requires OCP (cadquery-ocp). Install with: pip install cadquery-ocp

Geometry is in millimetres (standard STEP/CAD convention).

Parameters:

Name Type Description Default
frame Frame

The frame to export.

required
output_dir Union[str, Path]

Directory for the STEP files.

required
local bool

If True (default), each individual file is exported in that part's own local coordinates (bottom at the origin). If False, individual files are exported in global coordinates (their actual assembled position and orientation). The optional combined file is always in global coordinates regardless of this flag, since a merged compound is only meaningful with parts in their real positions.

True
combined bool

If True, also write a single _combined.step containing all timbers as a compound shape.

False
include_accessories bool

If True, export accessory solids. If False, export only timbers.

True

Returns:

Type Description
List[Path]

List of paths written.

Source code in kumiki/blueprint.py
def export_frame_step(
    frame: Frame,
    output_dir: Union[str, Path],
    *,
    local: bool = True,
    combined: bool = False,
    include_accessories: bool = True,
) -> List[Path]:
    """Export every timber in a Frame to individual STEP files.

    Requires OCP (cadquery-ocp). Install with: ``pip install cadquery-ocp``

    Geometry is in millimetres (standard STEP/CAD convention).

    Args:
        frame: The frame to export.
        output_dir: Directory for the STEP files.
        local: If True (default), each individual file is exported in that
            part's own local coordinates (bottom at the origin). If False,
            individual files are exported in global coordinates (their actual
            assembled position and orientation). The optional combined file is
            always in global coordinates regardless of this flag, since a
            merged compound is only meaningful with parts in their real
            positions.
        combined: If True, also write a single ``_combined.step`` containing
            all timbers as a compound shape.
        include_accessories: If True, export accessory solids. If False,
            export only timbers.

    Returns:
        List of paths written.
    """
    if not _OCP_AVAILABLE:
        raise ImportError(
            "OCP (cadquery-ocp) is required for STEP export. "
            "Install it with: pip install cadquery-ocp"
        )
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    written: List[Path] = []

    named_combined_shapes: list[tuple] = []
    used_names: set[str] = set()

    def _next_available_name(base_name: str) -> str:
        candidate = base_name
        suffix = 2
        while candidate in used_names:
            candidate = f"{base_name}_{suffix}"
            suffix += 1
        used_names.add(candidate)
        return candidate

    for i, ct in enumerate(frame.cut_timbers):
        name = _next_available_name(ct.timber.ticket.path or f"timber_{i}")
        local_csg = ct.render_timber_with_cuts_csg_local()
        global_csg = (
            adopt_csg(ct.timber.transform, Transform.identity(), local_csg)
            if (combined or not local) else None
        )
        if local:
            shape = _csg_to_ocp(local_csg)
        else:
            # global_csg is computed above whenever `combined or not local`,
            # which `not local` here already satisfies.
            assert global_csg is not None
            shape = _csg_to_ocp(global_csg)
        dest = output_dir / f"{name}.step"
        _write_step(shape, str(dest), name=name)
        written.append(dest)
        if combined:
            if not local:
                combined_shape = shape
            else:
                assert global_csg is not None
                combined_shape = _csg_to_ocp(global_csg)
            named_combined_shapes.append((name, combined_shape))

    if include_accessories:
        for i, accessory in enumerate(frame.accessories):
            ticket_name = accessory.ticket.path
            if ticket_name and ticket_name != "[no-name]":
                base_name = ticket_name
            else:
                base_name = f"accessory_{i}"
            name = _next_available_name(base_name)
            transform = getattr(accessory, "transform", None)
            if transform is None:
                raise ValueError(
                    f"Accessory '{accessory.ticket.path}' does not define a global transform"
                )
            local_csg = accessory.get_csg_local()
            global_csg = (
                adopt_csg(transform, Transform.identity(), local_csg)
                if (combined or not local) else None
            )
            if local:
                shape = _csg_to_ocp(local_csg)
            else:
                # global_csg is computed above whenever `combined or not local`,
                # which `not local` here already satisfies.
                assert global_csg is not None
                shape = _csg_to_ocp(global_csg)
            dest = output_dir / f"{name}.step"
            _write_step(shape, str(dest), name=name)
            written.append(dest)
            if combined:
                if not local:
                    combined_shape = shape
                else:
                    assert global_csg is not None
                    combined_shape = _csg_to_ocp(global_csg)
                named_combined_shapes.append((name, combined_shape))

    if combined and named_combined_shapes:
        dest = output_dir / "_combined.step"
        _write_step_named(named_combined_shapes, str(dest))
        written.append(dest)

    return written