Source code for declaracad.occ.impl.occ_shape

"""
Copyright (c) 2016-2018, CodeLV.

Distributed under the terms of the GPL v3 License.

The full license is in the file LICENSE, distributed with this software.

Created on Sep 30, 2016

@author: jrm
"""

import os
from typing import Generator, Union

from atom.api import Bool, Instance, List, Property, Str, Typed, observe, set_default
from OCCT.AIS import AIS_MultipleConnectedInteractive, AIS_Shape, AIS_TexturedShape
from OCCT.Bnd import Bnd_Box
from OCCT.BRep import BRep_Builder
from OCCT.BRepBndLib import BRepBndLib
from OCCT.gp import gp, gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCCT.TCollection import TCollection_AsciiString
from OCCT.TDF import TDF_Label
from OCCT.TopLoc import TopLoc_Location
from OCCT.TopoDS import TopoDS_Compound, TopoDS_Shape

from declaracad.core.utils import log
from declaracad.occ.algo import Mirror, Rotate, Scale, Translate
from declaracad.occ.geom import BBox, Direction, Point
from declaracad.occ.shape import (
    ProxyPart,
    ProxyRawPart,
    ProxyRawShape,
    ProxyShape,
    Shape,
)

from .topology import Topology
from .utils import color_to_quantity_color, material_to_material_aspect

DX = gp_Dir(1, 0, 0)
DXN = gp_Dir(-1, 0, 0)
DY = gp_Dir(0, 1, 0)
DYN = gp_Dir(0, -1, 0)
DZ = gp_Dir(0, 0, 1)
DZN = gp_Dir(0, 0, -1)
AX = gp_Ax1()
AX.SetDirection(gp.DX_())
AY = gp_Ax1()
AY.SetDirection(gp.DY_())
AZ = gp_Ax1()
AZ.SetDirection(gp.DZ_())
DEFAULT_AXIS = gp_Ax3(gp_Pnt(0, 0, 0), DZ, DX)


def coerce_axis(value: tuple[Point, Direction, float]) -> gp_Ax2:
    pos, dir, rotation = value
    axis = gp_Ax2(pos.proxy, dir.proxy)
    axis.Rotate(axis.Axis(), rotation)
    return axis


def coerce_shape(shape: Union[TopoDS_Shape, Shape]) -> TopoDS_Shape:
    """Coerce a declaration into a TopoDS_Shape"""
    if isinstance(shape, Shape):
        return shape.render()
    return shape


class OccShape(ProxyShape):
    #: A reference to the toolkit shape created by the proxy.
    shape = Typed(TopoDS_Shape)

    #: The shape that was shown on the screen
    ais_shape = Instance(AIS_Shape)

    #: The XCAF application label
    tdf_label = Instance(TDF_Label)

    #: Whether this is currently displayed
    displayed = Bool()

    #: Topology explorer of the shape
    topology = Typed(Topology)

    #: Class reference url
    reference = Str()

    #: Cached reference to the viewer
    def _get_viewer(self):
        parent = self.parent()
        if isinstance(parent, OccShape):
            return parent.viewer
        return parent

    viewer = Property(_get_viewer, cached=True)

    location = Typed(TopLoc_Location)

    # -------------------------------------------------------------------------
    # Initialization API
    # -------------------------------------------------------------------------
    def create_shape(self):
        """Create the toolkit shape for the proxy object.

        This method is called during the top-down pass, just before the
        'init_shape()' method is called. This method should create the
        toolkit widget and assign it to the 'widget' attribute.

        """
        raise NotImplementedError

    def init_shape(self):
        """Initialize the state of the toolkit widget.

        This method is called during the top-down pass, just after the
        'create_widget()' method is called. This method should init the
        state of the widget. The child widgets will not yet be created.

        """
        pass

    def init_layout(self):
        """Initialize the layout of the toolkit shape."""
        pass

    def activate_top_down(self):
        """Activate the proxy for the top-down pass."""
        try:
            self.create_shape()
            self.init_shape()
        except Exception as e:
            log.exception(e)
            raise e

    def activate_bottom_up(self):
        """Activate the proxy tree for the bottom-up pass."""
        self.init_layout()

    # -------------------------------------------------------------------------
    # Defaults and Observers
    # -------------------------------------------------------------------------
    def _default_topology(self):
        if self.shape is None:
            self.declaration.render()  # Force build the shape
        return Topology(shape=self.shape)

    def _observe_displayed(self, change):
        if self.displayed:
            parent = self.parent()
            if isinstance(parent, OccShape):
                parent.displayed = True

    @observe("shape")
    def on_shape_changed(self, change):
        if self.shape is not None:
            self.topology = self._default_topology()
        if self.displayed:
            self.ais_shape = self._default_ais_shape()

    def get_first_child(self):
        """Return shape to apply the operation to."""
        for child in self.children():
            if isinstance(child, OccShape):
                return child

    def child_shapes(self):
        """Iterator of all child shapes"""
        for child in self.children():
            if isinstance(child, OccShape):
                if hasattr(child, "shapes"):
                    for s in child.shapes:
                        yield s
                else:
                    yield child.shape

    def walk_shapes(
        self, ignore_display: bool = False
    ) -> Generator["OccShape", None, None]:
        """Iterator of all child shapes

        Parameters
        ----------
        ignore_display: bool
            Whether the display attribute should be ignored

        """
        if ignore_display is False and not self.declaration.display:
            return
        if isinstance(self, OccPart):
            for s in self.children():
                if isinstance(s, OccShape):
                    yield from s.walk_shapes()
        else:
            yield self

    def _default_ais_shape(self):
        """Generate the AIS shape for the viewer to display.
        This is only invoked when the viewer wants to display the shape.

        """
        d = self.declaration

        if d.texture is not None:
            texture = d.texture
            ais_shape = AIS_TexturedShape(self.shape)

            if os.path.exists(texture.path):
                path = TCollection_AsciiString(texture.path)
                ais_shape.SetTextureFileName(path)
                params = texture.repeat
                ais_shape.SetTextureRepeat(params.enabled, params.u, params.v)
                params = texture.origin
                ais_shape.SetTextureOrigin(params.enabled, params.u, params.v)
                params = texture.scale
                ais_shape.SetTextureScale(params.enabled, params.u, params.v)
                ais_shape.SetTextureMapOn()
                ais_shape.SetDisplayMode(3)
        else:
            ais_shape = AIS_Shape(self.shape)

        ais_shape.SetTransparency(d.transparency)
        if d.material.name:
            ma = material_to_material_aspect(d.material)
            ais_shape.SetMaterial(ma)
        if d.color:
            c, a = color_to_quantity_color(d.color)
            ais_shape.SetColor(c)
            if a is not None:
                ais_shape.SetTransparency(a)
        if d.line_color:
            c, a = color_to_quantity_color(d.line_color)
            aspect = ais_shape.Attributes().SeenLineAspect()
            aspect.SetColor(c)
            ais_shape.Attributes().SetSeenLineAspect(aspect)
        if d.wireframe_line_color:
            c, a = color_to_quantity_color(d.wireframe_line_color)
            aspect = ais_shape.Attributes().UnFreeBoundaryAspect()
            aspect.SetColor(c)
            ais_shape.Attributes().SetUnFreeBoundaryAspect(aspect)

        ais_shape.SetLocalTransformation(self.location.Transformation())
        return ais_shape

    def _default_location(self):
        """Get the final location based on the assembly tree."""
        location = TopLoc_Location()
        parent = self.parent()
        while isinstance(parent, OccPart):
            location = parent.location.Multiplied(location)
            parent = parent.parent()
        return location

    # -------------------------------------------------------------------------
    # Proxy API
    # -------------------------------------------------------------------------
    def get_transform(self):
        """Create a transform which rotates the default axis to align
        with the normal given by the position

        Returns
        -------
        transform: gp_Trsf

        """
        d = self.declaration
        result = gp_Trsf()
        axis = gp_Ax3(d.position.proxy, d.direction.proxy)
        axis.Rotate(axis.Axis(), d.rotation)
        result.SetDisplacement(DEFAULT_AXIS, axis)
        return result

    def set_position(self, position):
        self.create_shape()

    def set_direction(self, direction):
        self.create_shape()

    def set_axis(self, axis):
        self.create_shape()

    def set_display(self, display: bool):
        viewer = self.viewer
        if display:
            viewer.add_shape_to_display(self)
        else:
            viewer.remove_shape_from_display(self)

    def parent_shape(self):
        p = self.parent()
        if isinstance(p, OccShape):
            return p.shape

    def get_bounding_box(self, shape=None):
        shape = shape or self.shape
        if not shape:
            return BBox()
        bbox = Bnd_Box()
        BRepBndLib.Add_(shape, bbox)
        pmin, pmax = bbox.CornerMin(), bbox.CornerMax()
        return BBox(pmin.X(), pmin.Y(), pmin.Z(), pmax.X(), pmax.Y(), pmax.Z())


class OccDependentShape(OccShape):
    """Shape that is dependent on another shape"""

    def create_shape(self):
        """Create the toolkit shape for the proxy object.

        Operations depend on child or properties so they cannot be created
        in the top down pass but rather must be done in the init_layout method.

        """
        pass

    def init_layout(self):
        """Initialize the layout of the toolkit shape.

        This method is called during the bottom-up pass. This method
        should initialize the layout of the widget. The child widgets
        will be fully initialized and layed out when this is called.

        """
        try:
            self.update_shape()
        except Exception as e:
            log.exception(e)
            raise e
        # log.debug('init_layout %s shape %s' % (self, self.shape))
        assert self.shape is not None, "Shape was not created %s" % self

        # When they change re-compute
        for child in self.children():
            child.observe("shape", self.update_shape)

    def update_shape(self, change=None):
        """Must be implmented in subclasses to create the shape
        when the dependent shapes change.
        """
        raise NotImplementedError

    def child_added(self, child):
        super().child_added(child)
        if isinstance(child, OccShape):
            child.observe("shape", self.update_shape)
            if self.displayed and self.viewer:
                self.viewer.add_shape_to_display(child)

    def child_removed(self, child):
        super().child_removed(child)
        if isinstance(child, OccShape):
            child.unobserve("shape", self.update_shape)
            if child.displayed and self.viewer:
                self.viewer.remove_shape_from_display(child)

    def set_direction(self, direction):
        self.update_shape()

    def set_axis(self, axis):
        self.update_shape()


[docs] class OccPart(OccDependentShape, ProxyPart): #: A reference to the toolkit shape created by the proxy. builder = Typed(BRep_Builder, ()) #: Location location = Typed(TopLoc_Location) #: Display each sub-item ais_shape = Typed(AIS_MultipleConnectedInteractive) def get_transform(self) -> gp_Trsf: """Compute the transform for locating the part. This factors in the transforms applied to the part as well as the position, direction and rotation attributes. """ result = super().get_transform() d = self.declaration for op in d.transform: t = gp_Trsf() if isinstance(op, Translate): t.SetTranslation(gp_Vec(op.x, op.y, op.z)) elif isinstance(op, Rotate): t.SetRotation( gp_Ax1(gp_Pnt(*op.point), gp_Dir(*op.direction)), op.angle ) elif isinstance(op, Mirror): Ax = gp_Ax2 if op.plane else gp_Ax1 t.SetMirror(Ax(gp_Pnt(*op.point), gp_Dir(op.x, op.y, op.z))) elif isinstance(op, Scale): t.SetScale(gp_Pnt(*op.point), op.s) result.Multiply(t) return result def _default_location(self) -> TopLoc_Location: """Set the location of this part based on the transformation.""" return TopLoc_Location(self.get_transform()) def _default_ais_shape(self) -> AIS_MultipleConnectedInteractive: ais_obj = AIS_MultipleConnectedInteractive() viewer = self.viewer ais_context = viewer.ais_context for c in self.children(): if isinstance(c, OccShape): # HACK: WTF??? if ais_context.IsDisplayed(c.ais_shape): viewer.remove_shape_from_display(c) ais_obj.Connect(c.ais_shape) viewer.add_shape_to_display(c) else: ais_obj.Connect(c.ais_shape) ais_obj.SetLocalTransformation(self.location.Transformation()) return ais_obj def update_shape(self, change=None): """Create the toolkit shape for the proxy object.""" builder = self.builder shape = TopoDS_Compound() builder.MakeCompound(shape) for c in self.children(): if not isinstance(c, OccShape): continue if c.shape is None or not c.declaration.display: continue # Note infinite planes cannot be added to a compound! builder.Add(shape, c.shape) location = self.location = self._default_location() shape.Location(location) self.shape = shape def update_location(self, change=None): """Recompute the location of this and all nested parts. This only updates the viewer. """ self.location = self._default_location() viewer = self.viewer # Recompute locations for c in self.walk_shapes(): loc = c.location = c._default_location() viewer.ais_context.SetLocation(c.ais_shape, loc) viewer.update() def set_position(self, position): self.update_location() def set_direction(self, direction): self.update_location() def set_rotation(self, rotation): self.update_location() def set_transform(self, ops): self.update_location()
[docs] class OccRawShape(OccShape, ProxyRawShape): #: Update the class reference reference = set_default( "https://dev.opencascade.org/doc/refman/html/" "class_topo_d_s___shape.html" ) def create_shape(self): """Delegate shape creation to the declaration implementation.""" self.shape = self.declaration.create_shape(self.parent_shape()) # ------------------------------------------------------------------------- # ProxyRawShape API # ------------------------------------------------------------------------- def get_shape(self): """Retrieve the underlying toolkit shape.""" return self.shape
[docs] class OccRawPart(OccPart, ProxyRawPart): #: Update the class reference reference = set_default( "https://dev.opencascade.org/doc/refman/html/" "class_topo_d_s___shape.html" ) shapes = List(TopoDS_Shape) def create_shapes(self): """Delegate shape creation to the declaration implementation.""" self.shapes = self.declaration.create_shapes(self.parent_shape()) # ------------------------------------------------------------------------- # ProxyRawShape API # ------------------------------------------------------------------------- def get_shapes(self): """Retrieve the underlying toolkit shape.""" return self.shapes