Source code for importers

"""
build123d imports

name: importers.py
by:   Gumyr
date: March 1st, 2023

desc:
    This python module contains importers from multiple file formats.

license:

    Copyright 2022 Gumyr

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

"""

# pylint has trouble with the OCP imports
# pylint: disable=no-name-in-module, import-error

import os
from os import PathLike, fsdecode
import re
import unicodedata
from math import degrees
from pathlib import Path
from typing import Literal, Optional, TextIO, Union
import warnings

from OCP.BRep import BRep_Builder
from OCP.BRepGProp import BRepGProp
from OCP.BRepTools import BRepTools
from OCP.GProp import GProp_GProps
from OCP.Quantity import Quantity_ColorRGBA
from OCP.RWStl import RWStl
from OCP.STEPCAFControl import STEPCAFControl_Reader
from OCP.TCollection import TCollection_AsciiString, TCollection_ExtendedString
from OCP.TDataStd import TDataStd_Name
from OCP.TDF import TDF_Label, TDF_LabelSequence
from OCP.TDocStd import TDocStd_Document
from OCP.TopAbs import TopAbs_FACE
from OCP.TopExp import TopExp_Explorer
from OCP.TopoDS import (
    TopoDS_Compound,
    TopoDS_Edge,
    TopoDS_Face,
    TopoDS_Shape,
    TopoDS_Shell,
    TopoDS_Solid,
    TopoDS_Vertex,
    TopoDS_Wire,
)
from OCP.XCAFDoc import (
    XCAFDoc_ColorCurv,
    XCAFDoc_ColorGen,
    XCAFDoc_ColorSurf,
    XCAFDoc_DocumentTool,
)
from ocpsvg import ColorAndLabel, import_svg_document
from svgpathtools import svg2paths

from build123d.geometry import Color, Location
from build123d.topology import (
    Compound,
    Edge,
    Face,
    Shape,
    ShapeList,
    Shell,
    Solid,
    Vertex,
    Wire,
    downcast,
)

topods_lut = {
    TopoDS_Compound: Compound,
    TopoDS_Edge: Edge,
    TopoDS_Face: Face,
    TopoDS_Shell: Shell,
    TopoDS_Solid: Solid,
    TopoDS_Vertex: Vertex,
    TopoDS_Wire: Wire,
}


[docs] def import_brep(file_name: PathLike | str | bytes) -> Shape: """Import shape from a BREP file Args: file_name (Union[PathLike, str, bytes]): brep file Raises: ValueError: file not found Returns: Shape: build123d object """ shape = TopoDS_Shape() builder = BRep_Builder() file_name_str = fsdecode(file_name) BRepTools.Read_s(shape, file_name_str, builder) if shape.IsNull(): raise ValueError(f"Could not import {file_name_str}") return Compound.cast(shape)
[docs] def import_step(filename: PathLike | str | bytes) -> Compound: """import_step Extract shapes from a STEP file and return them as a Compound object. Args: file_name (Union[PathLike, str, bytes]): file path of STEP file to import Raises: ValueError: can't open file Returns: Compound: contents of STEP file """ def get_name(label: TDF_Label) -> str: """Extract name and format""" name = "" std_name = TDataStd_Name() if label.FindAttribute(TDataStd_Name.GetID_s(), std_name): name = TCollection_AsciiString(std_name.Get()).ToCString() # Remove characters that cause ocp_vscode to fail clean_name = "".join(ch for ch in name if unicodedata.category(ch)[0] != "C") return clean_name.translate(str.maketrans(" .()", "____")) def get_color(shape: TopoDS_Shape) -> Quantity_ColorRGBA: """Get the color - take that of the largest Face if multiple""" def get_col(obj: TopoDS_Shape) -> Quantity_ColorRGBA: col = Quantity_ColorRGBA() if ( color_tool.GetColor(obj, XCAFDoc_ColorCurv, col) or color_tool.GetColor(obj, XCAFDoc_ColorGen, col) or color_tool.GetColor(obj, XCAFDoc_ColorSurf, col) ): return col shape_color = get_col(shape) colors = {} face_explorer = TopExp_Explorer(shape, TopAbs_FACE) while face_explorer.More(): current_face = face_explorer.Current() properties = GProp_GProps() BRepGProp.SurfaceProperties_s(current_face, properties) area = properties.Mass() color = get_col(current_face) if color is not None: colors[area] = color face_explorer.Next() # If there are multiple colors, return the one from the largest face if colors: shape_color = sorted(colors.items())[-1][1] return shape_color def build_assembly(parent_tdf_label: TDF_Label | None = None) -> list[Shape]: """Recursively extract object into an assembly""" sub_tdf_labels = TDF_LabelSequence() if parent_tdf_label is None: shape_tool.GetFreeShapes(sub_tdf_labels) else: shape_tool.GetComponents_s(parent_tdf_label, sub_tdf_labels) sub_shapes: list[Shape] = [] for i in range(sub_tdf_labels.Length()): sub_tdf_label = sub_tdf_labels.Value(i + 1) if shape_tool.IsReference_s(sub_tdf_label): ref_tdf_label = TDF_Label() shape_tool.GetReferredShape_s(sub_tdf_label, ref_tdf_label) else: ref_tdf_label = sub_tdf_label sub_topo_shape = downcast(shape_tool.GetShape_s(ref_tdf_label)) if shape_tool.IsAssembly_s(ref_tdf_label): sub_shape = Compound() sub_shape.children = build_assembly(ref_tdf_label) else: sub_shape = topods_lut[type(sub_topo_shape)](sub_topo_shape) sub_shape.color = Color(get_color(sub_topo_shape)) sub_shape.label = get_name(ref_tdf_label) sub_shape.move(Location(shape_tool.GetLocation_s(sub_tdf_label))) sub_shapes.append(sub_shape) return sub_shapes if not os.path.exists(filename): raise FileNotFoundError(filename) fmt = TCollection_ExtendedString("XCAF") doc = TDocStd_Document(fmt) shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main()) reader = STEPCAFControl_Reader() reader.SetNameMode(True) reader.SetColorMode(True) reader.SetLayerMode(True) reader.ReadFile(fsdecode(filename)) reader.Transfer(doc) root = Compound() root.children = build_assembly() # Remove empty Compound wrapper if single free object if len(root.children) == 1: root = root.children[0] return root
[docs] def import_stl(file_name: PathLike | str | bytes) -> Face: """import_stl Extract shape from an STL file and return it as a Face reference object. Note that importing with this method and creating a reference is very fast while creating an editable model (with Mesher) may take minutes depending on the size of the STL file. Args: file_name (Union[PathLike, str, bytes]): file path of STL file to import Raises: ValueError: Could not import file Returns: Face: STL model """ # Read and return the shape reader = RWStl.ReadFile_s(fsdecode(file_name)) face = TopoDS_Face() BRep_Builder().MakeFace(face, reader) stl_obj = Face.cast(face) return stl_obj
[docs] def import_svg_as_buildline_code( file_name: PathLike | str | bytes, ) -> tuple[str, str]: """translate_to_buildline_code Translate the contents of the given svg file into executable build123d/BuildLine code. Args: file_name (Union[PathLike, str, bytes]): svg file name Returns: tuple[str, str]: code, builder instance name """ translator = { "Line": ["Line", "start", "end"], "CubicBezier": ["Bezier", "start", "control1", "control2", "end"], "QuadraticBezier": ["Bezier", "start", "control", "end"], "Arc": [ "EllipticalCenterArc", # "EllipticalStartArc", "start", "end", "radius", "rotation", "large_arc", "sweep", ], } file_name = fsdecode(file_name) paths_info = svg2paths(file_name) paths, _path_attributes = paths_info[0], paths_info[1] builder_name = os.path.basename(file_name).split(".")[0] builder_name = builder_name if builder_name.isidentifier() else "builder" buildline_code = [ "from build123d import *", f"with BuildLine() as {builder_name}:", ] for path in paths: for curve in path: class_name = type(curve).__name__ if class_name == "Arc": values = [curve.__dict__["center"]] values.append(curve.__dict__["radius"].real) values.append(curve.__dict__["radius"].imag) start, end = sorted( [ curve.__dict__["theta"], curve.__dict__["theta"] + curve.__dict__["delta"], ] ) values.append(start) values.append(end) values.append(degrees(curve.__dict__["phi"])) if curve.__dict__["delta"] < 0.0: values.append("AngularDirection.CLOCKWISE") else: values.append("AngularDirection.COUNTER_CLOCKWISE") # EllipticalStartArc implementation # values = [p.__dict__[parm] for parm in translator[class_name][1:3]] # values.append(p.__dict__["radius"].real) # values.append(p.__dict__["radius"].imag) # values.extend([p.__dict__[parm] for parm in translator[class_name][4:]]) else: values = [curve.__dict__[parm] for parm in translator[class_name][1:]] values_str = ",".join( [ f"({v.real}, {v.imag})" if isinstance(v, complex) else str(v) for v in values ] ) buildline_code.append(f" {translator[class_name][0]}({values_str})") return ("\n".join(buildline_code), builder_name)
[docs] def import_svg( svg_file: str | Path | TextIO, *, flip_y: bool = True, ignore_visibility: bool = False, label_by: Literal["id", "class", "inkscape:label"] | str = "id", is_inkscape_label: bool | None = None, # TODO remove for `1.0` release ) -> ShapeList[Wire | Face]: """import_svg Args: svg_file (Union[str, Path, TextIO]): svg file flip_y (bool, optional): flip objects to compensate for svg orientation. Defaults to True. ignore_visibility (bool, optional): Defaults to False. label_by (str, optional): XML attribute to use for imported shapes' `label` property. Defaults to "id". Use `inkscape:label` to read labels set from Inkscape's "Layers and Objects" panel. Raises: ValueError: unexpected shape type Returns: ShapeList[Union[Wire, Face]]: objects contained in svg """ if is_inkscape_label is not None: # TODO remove for `1.0` release msg = "`is_inkscape_label` parameter is deprecated" if is_inkscape_label: label_by = "inkscape:" + label_by msg += f", use `label_by={label_by!r}` instead" warnings.warn(msg, stacklevel=2) shapes = [] label_by = re.sub( r"^inkscape:(.+)", r"{http://www.inkscape.org/namespaces/inkscape}\1", label_by ) for face_or_wire, color_and_label in import_svg_document( svg_file, flip_y=flip_y, ignore_visibility=ignore_visibility, metadata=ColorAndLabel.Label_by(label_by), ): if isinstance(face_or_wire, TopoDS_Wire): shape = Wire(face_or_wire) elif isinstance(face_or_wire, TopoDS_Face): shape = Face(face_or_wire) else: # should not happen raise ValueError(f"unexpected shape type: {type(face_or_wire).__name__}") if shape.wrapped: shape.color = Color(*color_and_label.color_for(shape.wrapped)) shape.label = color_and_label.label shapes.append(shape) return ShapeList(shapes)