"""
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