tpaviot / pythonocc-core

Python package for 3D geometry CAD/BIM/CAM
GNU Lesser General Public License v3.0
1.31k stars 372 forks source link

Building pythonocc for geometry info and visualization only. #840

Open efirvida opened 4 years ago

efirvida commented 4 years ago

Dear all. I want to use PythonOCC to create a small library for reading (step, iges, brep) CAD files, extract info as volume, centroid, inertia properties, bounding box..... and also export to json, and stl, file to render for the web. But I don't need any of the modeling tools. Is there any advice to build it only with these features, in order to reduce package size, and build time?

tpaviot commented 4 years ago

You have to compile pythonocc by your own. The CMakeLists.txt file enables to choose which packages to wrap. Modeling tools are mandatory, but you can set the PYTHONOCC_WRAP_OCAF and PYTHONOCC_WRAP_VISU to OFF (see https://github.com/tpaviot/pythonocc-core/blob/master/CMakeLists.txt#L92), that will result in a smaller package size, and you won't need opengl as well.

efirvida commented 4 years ago

You have to compile pythonocc by your own. The CMakeLists.txt file enables to choose which packages to wrap. Modeling tools are mandatory, but you can set the PYTHONOCC_WRAP_OCAF and PYTHONOCC_WRAP_VISU to OFF (see https://github.com/tpaviot/pythonocc-core/blob/master/CMakeLists.txt#L92), that will result in a smaller package size, and you won't need opengl as well.

And offscreen rendering will work with these functions disabled?

tpaviot commented 4 years ago

No, offscreen rendering won't work, it requires opengl and, at least, a virtual frame buffer. But export to webgl will be possible (threejs, x3d), they only need a mesher

CsatiZoltan commented 4 years ago

@efirvida If you release it as open source, could you post the link here? I would be interested.

efirvida commented 4 years ago

@efirvida If you release it as open-source, could you post the link here? I would be interested.

There is no problem with sharing the code. It's nothing from the other world. It is still under development but is functional:

import uuid
import enum
from subprocess import check_output

# OCC IMPORT
from OCC.Core.BRepTools import breptools_Read
from OCC.Core.BRep import BRep_Builder
from OCC.Core.TopoDS import TopoDS_Shape

from OCC.Core.BRepGProp import brepgprop_VolumeProperties
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.GProp import GProp_GProps
from OCC.Extend.DataExchange import read_stl_file, read_step_file, read_iges_file

from OCC.Core.Tesselator import ShapeTesselator
from OCC.Display.WebGl import x3dom_renderer, threejs_renderer
from OCC.Core.Quantity import Quantity_Color, Quantity_TOC_RGB
from OCC.Display.SimpleGui import init_display

class CADFormats(enum.Enum):
    CAD = ["step", "stp", "iges", "igs", "brep"]

# READERS
class Readers(ABC):
    @abstractmethod
    def __call__(self, file: str):
        ...

class StepReader(Readers):
    def __call__(self, file):
        return read_step_file(file)

class IgesReader(Readers):
    def __call__(self, file):
        return read_iges_file(file)

class BrepReader(Readers):
    def __call__(self, file):
        shape = TopoDS_Shape()
        builder = BRep_Builder()
        breptools_Read(shape, file, builder)
        return shape

CAD_READERS = {
    "brep": BrepReader,
    "step": StepReader,
    "stp": StepReader,
    "iges": IgesReader,
    "igs": IgesReader,
}

class Model:
    def __init__(self, file):
        self.file = file
        self.model = None

    def __call__(self):
        return self.read()

    @property
    def ttype(self):
        """Get model type as defined in ``CADFormats`` enumerator
        based on ``file`` extension"""

        _ext = self.file.split(".")[-1]
        type = {
            "format": _format
            for _format in CADFormats
            if _format.value and _ext in _format.value
        }
        if type:
            return type
        return CADFormats.UNDEFINED

    def __hash__(self) -> str:
        return self.hash

    @property
    def hash(self) -> str:
        hash_output = check_output(["sha1sum", self.file]).decode()
        return hash_output.split()[0]

    def read(self):
        if self.ttype["format"] == CADFormats.CAD:
            self.model = CADModel()(self.file)
        return self

    @property
    def info(self) -> dict:
        self.model.info.update({"hash": self.hash})
        return self.model.info

    def to_png(self, output_file: str = None) -> None:
        if not output_file:
            output_file = f"shp_{self.hash}.png"
        self.model.to_png(output_file)

    def to_web(self, output_file: str = None) -> None:
        ext = "json" if self.ttype["format"] == CADFormats.CAD else "stl"
        if not output_file:
            output_file = f"shp_{self.hash}.{ext}"
        self.model.to_web(output_file, self.hash)

class CADModel:
    def __call__(self, file):
        self.file = file
        return self.read()

    def read(self):
        _ext = self.file.split(".")[-1]
        reader = CAD_READERS[_ext]()
        self.__shape = reader(self.file)
        return self

    @property
    def info(self) -> dict:
        # Compute inertia properties
        props = GProp_GProps()
        brepgprop_VolumeProperties(self.__shape, props)
        volume = props.Mass()

        # Get inertia properties
        cog = props.CentreOfMass()
        cog_x, cog_y, cog_z = cog.Coord()

        # _m = props.MatrixOfInertia()
        # matrix_of_inertia = [
        #     (_m.Row(row + 1).X(), _m.Row(row + 1).Y(), _m.Row(row + 1).Z())
        #     for row in range(3)
        # ]

        box = self._get_boundingbox(use_mesh=False)

        shape_info = {
            "volume": volume,
            "center_of_mass": (cog_x, cog_y, cog_z),
            "bounding_box": box["box"],
            "shape_size": box["size"],
        }
        return shape_info

    def _get_boundingbox(self, tol=1e-6, use_mesh=True) -> tuple:
        """ return the bounding box of the TopoDS_Shape `shape`
        Parameters
        ----------
        shape : TopoDS_Shape or a subclass such as TopoDS_Face
            the shape to compute the bounding box from
        tol: float
            tolerance of the computed boundingbox
        use_mesh : bool
            a flag that tells whether or not the shape has first to be meshed before the bbox
            computation. This produces more accurate results
        """
        bbox = Bnd_Box()
        bbox.SetGap(tol)
        if use_mesh:
            mesh = BRepMesh_IncrementalMesh()
            mesh.SetParallelDefault(True)
            mesh.SetShape(self.__shape)
            mesh.Perform()
            if not mesh.IsDone():
                raise AssertionError("Mesh not done.")
        brepbndlib_Add(self.__shape, bbox, use_mesh)

        xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
        return {
            "box": ((xmin, ymin, zmin), (xmax, ymax, zmax)),
            "size": {
                "X": abs(xmax - xmin),
                "Y": abs(ymax - ymin),
                "Z": abs(zmax - zmin),
            },
        }

    def to_png(self, output_file: str) -> None:
        display, *_ = init_display()
        display.DisplayShape(self.__shape)
        display.FitAll()
        display.View.Dump(output_file)

    def to_web(self, output_file: str, hash: str = None) -> None:
        if not hash:
            shape_uuid = uuid.uuid4().hex
            hash = "shp%s" % shape_uuid
        tess = ShapeTesselator(self.__shape)
        tess.Compute(compute_edges=False, mesh_quality=1.0)
        web_shape = tess.ExportShapeToThreejsJSONString(hash)
        with open(output_file, "w") as web_file:
            web_file.write(web_shape)

    def __str__(self):
        info = self.info
        center_of_mass = (
            f"\tX: {info['center_of_mass'][0]}",
            f"\tY: {info['center_of_mass'][1]}",
            f"\tZ: {info['center_of_mass'][2]}",
        )
        matrix_of_inertia = (
            f"\t{info['matrix_of_inertia'][0]}",
            f"\t{info['matrix_of_inertia'][1]}",
            f"\t{info['matrix_of_inertia'][2]}",
        )
        bounding_box = (
            f"\tMIN: {info['bounding_box'][0]}",
            f"\tMAX: {info['bounding_box'][1]}",
        )

        _str = (
            f"Volume: {info['volume']}",
            f"Size: {info['shape_size']}",
            "Center of mass:\n" + "\n".join(center_of_mass),
            "Matrix of inertia:\n" + "\n".join(matrix_of_inertia),
            "Bounding box:\n" + "\n".join(bounding_box),
        )
        return "\n".join(_str)
CsatiZoltan commented 4 years ago

@efirvida Thank you.

If you called the to_png method of CADModel from another function, could you prevent the graphics from being closed? This is related to my question #855.

efirvida commented 4 years ago

@efirvida Thank you.

If you called the to_png method of CADModel from another function, could you prevent the graphics from being closed? This is related to my question #855.

I made this only to save screenshoot of the 3d model. On my project I don need the window, so to run it well I also use "PYTHONOCC_OFFSCREEN_RENDERER=1" as an environment variable, to completely hide the windows, which is not your case. In your your example, I think you just need to call start_display() after display.DisplayShape(...

im44pos commented 1 year ago

@efirvida If you release it as open-source, could you post the link here? I would be interested.

There is no problem with sharing the code. It's nothing from the other world. It is still under development but is functional:

import uuid
import enum
from subprocess import check_output

# OCC IMPORT
from OCC.Core.BRepTools import breptools_Read
from OCC.Core.BRep import BRep_Builder
from OCC.Core.TopoDS import TopoDS_Shape

from OCC.Core.BRepGProp import brepgprop_VolumeProperties
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.GProp import GProp_GProps
from OCC.Extend.DataExchange import read_stl_file, read_step_file, read_iges_file

from OCC.Core.Tesselator import ShapeTesselator
from OCC.Display.WebGl import x3dom_renderer, threejs_renderer
from OCC.Core.Quantity import Quantity_Color, Quantity_TOC_RGB
from OCC.Display.SimpleGui import init_display

class CADFormats(enum.Enum):
    CAD = ["step", "stp", "iges", "igs", "brep"]

# READERS
class Readers(ABC):
    @abstractmethod
    def __call__(self, file: str):
        ...

class StepReader(Readers):
    def __call__(self, file):
        return read_step_file(file)

class IgesReader(Readers):
    def __call__(self, file):
        return read_iges_file(file)

class BrepReader(Readers):
    def __call__(self, file):
        shape = TopoDS_Shape()
        builder = BRep_Builder()
        breptools_Read(shape, file, builder)
        return shape

CAD_READERS = {
    "brep": BrepReader,
    "step": StepReader,
    "stp": StepReader,
    "iges": IgesReader,
    "igs": IgesReader,
}

class Model:
    def __init__(self, file):
        self.file = file
        self.model = None

    def __call__(self):
        return self.read()

    @property
    def ttype(self):
        """Get model type as defined in ``CADFormats`` enumerator
        based on ``file`` extension"""

        _ext = self.file.split(".")[-1]
        type = {
            "format": _format
            for _format in CADFormats
            if _format.value and _ext in _format.value
        }
        if type:
            return type
        return CADFormats.UNDEFINED

    def __hash__(self) -> str:
        return self.hash

    @property
    def hash(self) -> str:
        hash_output = check_output(["sha1sum", self.file]).decode()
        return hash_output.split()[0]

    def read(self):
        if self.ttype["format"] == CADFormats.CAD:
            self.model = CADModel()(self.file)
        return self

    @property
    def info(self) -> dict:
        self.model.info.update({"hash": self.hash})
        return self.model.info

    def to_png(self, output_file: str = None) -> None:
        if not output_file:
            output_file = f"shp_{self.hash}.png"
        self.model.to_png(output_file)

    def to_web(self, output_file: str = None) -> None:
        ext = "json" if self.ttype["format"] == CADFormats.CAD else "stl"
        if not output_file:
            output_file = f"shp_{self.hash}.{ext}"
        self.model.to_web(output_file, self.hash)

class CADModel:
    def __call__(self, file):
        self.file = file
        return self.read()

    def read(self):
        _ext = self.file.split(".")[-1]
        reader = CAD_READERS[_ext]()
        self.__shape = reader(self.file)
        return self

    @property
    def info(self) -> dict:
        # Compute inertia properties
        props = GProp_GProps()
        brepgprop_VolumeProperties(self.__shape, props)
        volume = props.Mass()

        # Get inertia properties
        cog = props.CentreOfMass()
        cog_x, cog_y, cog_z = cog.Coord()

        # _m = props.MatrixOfInertia()
        # matrix_of_inertia = [
        #     (_m.Row(row + 1).X(), _m.Row(row + 1).Y(), _m.Row(row + 1).Z())
        #     for row in range(3)
        # ]

        box = self._get_boundingbox(use_mesh=False)

        shape_info = {
            "volume": volume,
            "center_of_mass": (cog_x, cog_y, cog_z),
            "bounding_box": box["box"],
            "shape_size": box["size"],
        }
        return shape_info

    def _get_boundingbox(self, tol=1e-6, use_mesh=True) -> tuple:
        """ return the bounding box of the TopoDS_Shape `shape`
        Parameters
        ----------
        shape : TopoDS_Shape or a subclass such as TopoDS_Face
            the shape to compute the bounding box from
        tol: float
            tolerance of the computed boundingbox
        use_mesh : bool
            a flag that tells whether or not the shape has first to be meshed before the bbox
            computation. This produces more accurate results
        """
        bbox = Bnd_Box()
        bbox.SetGap(tol)
        if use_mesh:
            mesh = BRepMesh_IncrementalMesh()
            mesh.SetParallelDefault(True)
            mesh.SetShape(self.__shape)
            mesh.Perform()
            if not mesh.IsDone():
                raise AssertionError("Mesh not done.")
        brepbndlib_Add(self.__shape, bbox, use_mesh)

        xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
        return {
            "box": ((xmin, ymin, zmin), (xmax, ymax, zmax)),
            "size": {
                "X": abs(xmax - xmin),
                "Y": abs(ymax - ymin),
                "Z": abs(zmax - zmin),
            },
        }

    def to_png(self, output_file: str) -> None:
        display, *_ = init_display()
        display.DisplayShape(self.__shape)
        display.FitAll()
        display.View.Dump(output_file)

    def to_web(self, output_file: str, hash: str = None) -> None:
        if not hash:
            shape_uuid = uuid.uuid4().hex
            hash = "shp%s" % shape_uuid
        tess = ShapeTesselator(self.__shape)
        tess.Compute(compute_edges=False, mesh_quality=1.0)
        web_shape = tess.ExportShapeToThreejsJSONString(hash)
        with open(output_file, "w") as web_file:
            web_file.write(web_shape)

    def __str__(self):
        info = self.info
        center_of_mass = (
            f"\tX: {info['center_of_mass'][0]}",
            f"\tY: {info['center_of_mass'][1]}",
            f"\tZ: {info['center_of_mass'][2]}",
        )
        matrix_of_inertia = (
            f"\t{info['matrix_of_inertia'][0]}",
            f"\t{info['matrix_of_inertia'][1]}",
            f"\t{info['matrix_of_inertia'][2]}",
        )
        bounding_box = (
            f"\tMIN: {info['bounding_box'][0]}",
            f"\tMAX: {info['bounding_box'][1]}",
        )

        _str = (
            f"Volume: {info['volume']}",
            f"Size: {info['shape_size']}",
            "Center of mass:\n" + "\n".join(center_of_mass),
            "Matrix of inertia:\n" + "\n".join(matrix_of_inertia),
            "Bounding box:\n" + "\n".join(bounding_box),
        )
        return "\n".join(_str)

I get an "NameError: name 'ABC' is not defined" on "class Readers(ABC):"

Tanneguydv commented 1 year ago

add the following line to import : from abc import ABC, abstractmethod

im44pos commented 1 year ago

add the following line to import : from abc import ABC, abstractmethod

@Tanneguydv Thank you.