gumyr / build123d

A python CAD programming library
Apache License 2.0
554 stars 93 forks source link

Colors for compound object (when including an imported step file) don't render in OCP Cad Viewer, but exported step shows colors. #757

Open seltzered opened 1 week ago

seltzered commented 1 week ago

Per conversation with @jdegenstein on discord I'm filing this issue, and the demo repo at https://github.com/seltzered/build123d-cad-viewer-issue-demo has an example.

Basically in the example scenario I've noticed that in building a simple compound object (with setting different colors for vairous parts) and viewing it with OCP Cad Viewer, things appear okay when using creating a Compound object made up of only BuildPart objects, but when I add in an imported step file to the Compound object I notice there's the colors seem to all be the default BUT the exported step file appears fine in another CAD application (e.g. FreeCAD). See the screenshot below for a rough idea of the issue or the repo above to try.

Environment: Linux (fedora 40) x86, build123d 0.7.0, ocp_vscode 2.5.3, vscode 1.94.2 image

gumyr commented 1 week ago

When you say "when I add in" how exactly did you do this?

seltzered commented 1 week ago

When you say "when I add in" how exactly did you do this?

Lines 13, 29, and 60 of https://github.com/seltzered/build123d-cad-viewer-issue-demo/blob/main/cirque_23mm.py illustrate how a step file was imported and added to the object.

A compound object (imported via step file) was part of another compound object (that also has parts created with BuildPart().

gumyr commented 1 week ago

I think I understand the problem which is basically described by the Warning generated:

exporters3d.py:127: UserWarning: Unknown Compound type, color not set
  warnings.warn("Unknown Compound type, color not set")

If one looks at the exporter code here:

        # For Part, Sketch and Curve objects color needs to be applied to the wrapped
        # object not just the Compound wrapper
        sub_node_labels = []
        if isinstance(node, Compound) and not node.children:
            sub_nodes = []
            if isinstance(node, Part):
                explorer = TopExp_Explorer(node.wrapped, ta.TopAbs_SOLID)
            elif isinstance(node, Sketch):
                explorer = TopExp_Explorer(node.wrapped, ta.TopAbs_FACE)
            elif isinstance(node, Curve):
                explorer = TopExp_Explorer(node.wrapped, ta.TopAbs_EDGE)
            else:
                warnings.warn("Unknown Compound type, color not set")
                explorer = TopExp_Explorer()  # don't know what to look for

            while explorer.More():
                sub_nodes.append(explorer.Current())
                explorer.Next()

            sub_node_labels = [
                shape_tool.FindShape(sub_node, findInstance=False)
                for sub_node in sub_nodes
            ]

Here the compound object is being explored by using the TopExp_Explorer class which requires the user provide the type of object they are looking for. As the Compound was generated by some other S/W there is no way to know what to look for.

However, there is a better way to do this. I'm currently working on the following:

    def first_level_shapes(
        self, _shapes: list[TopoDS_Shape] = None
    ) -> ShapeList[Shape]:
        """first_level_shapes

        This method iterates through the immediate children of the compound and
        collects all non-compound shapes (e.g., vertices, edges, faces, solids).
        If a child shape is itself a compound, the method recursively explores it,
        retrieving all first-level shapes within any nested compounds.

        Note: the _shapes parameter is not to be assigned by the user.

        Returns:
            ShapeList[Shape]: Shapes contained within the Compound
        """
        if _shapes is None:
            _shapes = []
        iterator = TopoDS_Iterator()
        iterator.Initialize(self.wrapped)
        while iterator.More():
            child = Shape.cast(iterator.Value())
            if isinstance(child, Compound):
                child.first_level_shapes(_shapes)
            else:
                _shapes.append(child)
            iterator.Next()
        return ShapeList(_shapes)

which will find those shapes without having to know in advance what type they are. Using this new functionality it should be possible to improve the exporter and avoid the loss of color.