"""
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 28, 2016
@author: jrm
"""
from typing import Any, Optional, Union
from atom.api import (
Atom,
Bool,
Coerced,
Enum,
Float,
ForwardTyped,
Instance,
List,
Tuple,
Typed,
observe,
)
from enaml.core.declarative import d_
from .geom import Direction, Point, coerce_direction, coerce_point
from .shape import ProxyShape, Shape, TopoDS_Shape
class ProxyOperation(ProxyShape):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Operation)
def set_fix(self, fix: bool):
raise NotImplementedError
class ProxyBooleanOperation(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: BooleanOperation)
def set_disabled(self, disabled: bool):
raise NotImplementedError
def set_shape1(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_shape2(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_unify(self, unify: bool):
raise NotImplementedError
def set_parallel(self, parallel: bool):
raise NotImplementedError
class ProxyCommon(ProxyBooleanOperation):
declaration = ForwardTyped(lambda: Common)
class ProxyCut(ProxyBooleanOperation):
declaration = ForwardTyped(lambda: Cut)
class ProxyFuse(ProxyBooleanOperation):
declaration = ForwardTyped(lambda: Fuse)
class ProxySplit(ProxyBooleanOperation):
declaration = ForwardTyped(lambda: Split)
class ProxyIntersection(ProxyBooleanOperation):
declaration = ForwardTyped(lambda: Intersection)
class ProxyFillet(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Fillet)
def set_disabled(self, disabled: bool):
raise NotImplementedError
def set_radius(self, r: float):
raise NotImplementedError
def set_operations(self, operations: list):
raise NotImplementedError
def set_shape_type(self, shape_type: str):
raise NotImplementedError
[docs]
class ChamferData(Atom):
distance = Float(strict=False)
distance2 = Float(strict=False)
angle = Float(strict=False)
edge = Typed(TopoDS_Shape)
face = Typed(TopoDS_Shape)
class ProxyChamfer(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Chamfer)
def set_disabled(self, disabled: bool):
raise NotImplementedError
def set_distance(self, d: float):
raise NotImplementedError
def set_distance2(self, d: float):
raise NotImplementedError
def set_operations(self, operations: list):
raise NotImplementedError
class ProxyOffset(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Offset)
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_closed(self, closed: bool):
raise NotImplementedError
def set_offset(self, offset: float):
raise NotImplementedError
def set_offset_mode(self, mode: str):
raise NotImplementedError
def set_intersection(self, enabled: bool):
raise NotImplementedError
def set_join_type(self, mode: str):
raise NotImplementedError
def set_as_face(self, enabled: bool):
raise NotImplementedError
def set_disabled(self, disabled: bool):
raise NotImplementedError
class ProxyOffsetShape(ProxyOffset):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: OffsetShape)
class ProxyThickSolid(ProxyOffset):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: ThickSolid)
def set_faces(self, faces: list):
raise NotImplementedError
class ProxyPipe(ProxyOffset):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Pipe)
def set_spline(self, spline: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_profile(self, profile: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_fill_mode(self, mode: str):
raise NotImplementedError
class ProxyAbstractRibSlot(ProxyOperation):
#: Abstract class
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_contour(self, contour: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_plane(self, plane: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_fuse(self, fuse: bool):
raise NotImplementedError
class ProxyDraftAngle(ProxyOperation):
def set_disabled(self, disabled: bool):
raise NotImplementedError
def set_angle(self, angle: float):
raise NotImplementedError
def set_faces(self, faces: list):
raise NotImplementedError
def set_operations(self, operations: list):
raise NotImplementedError
class ProxyLinearForm(ProxyAbstractRibSlot):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: LinearForm)
def set_direction1(self, direction: Optional[Union[Direction, tuple, list]]):
raise NotImplementedError
def set_modify(self, modify: bool):
raise NotImplementedError
class ProxyRevolutionForm(ProxyAbstractRibSlot):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: RevolutionForm)
def set_height1(self, height: float):
raise NotImplementedError
def set_height2(self, height: float):
raise NotImplementedError
def set_sliding(self, sliding: bool):
raise NotImplementedError
class ProxyThruSections(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: ThruSections)
def set_solid(self, solid: bool):
raise NotImplementedError
def set_ruled(self, ruled: bool):
raise NotImplementedError
def set_fix(self, fix: bool):
raise NotImplementedError
def set_precision(self, pres3d: float):
raise NotImplementedError
class ProxyTransform(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Transform)
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_operations(self, operations: list["TransformOperation"]):
raise NotImplementedError
class ProxySew(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Sew)
class ProxyGlue(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Glue)
class ProxyNormalProjection(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: NormalProjection)
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_max_distance(self, distance: float):
raise NotImplementedError
class ProxyExtend(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: Extend)
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_mode(self, mode: str):
raise NotImplementedError
def set_operations(self, operations: list):
raise NotImplementedError
class ProxyRemoveFeatures(ProxyOperation):
#: A reference to the Shape declaration.
declaration = ForwardTyped(lambda: RemoveFeatures)
def set_shape(self, shape: Optional[Union[Shape, TopoDS_Shape]]):
raise NotImplementedError
def set_disabled(self, disabled: bool):
raise NotImplementedError
def set_parallel(self, parallel: bool):
raise NotImplementedError
def set_features(self, featres: list):
raise NotImplementedError
class Operation(Shape):
"""Base class for Operations that are applied to other shapes."""
#: Reference to the implementation control
proxy = Typed(ProxyOperation)
#: Attempt to fix issues in the resulting shape
fix = d_(Bool(False))
@observe("fix")
def _update_proxy(self, change: dict[str, Any]):
if change["name"] == "axis":
dx, dy, dz = self.x, self.y, self.z
if change.get("oldvalue"):
old = change["oldvalue"].Location()
dx -= old.X()
dy -= old.Y()
dz -= old.Z()
for c in self.children:
if isinstance(c, Shape):
c.position = (c.x + dx, c.y + dy, c.z + dz)
super(Operation, self)._update_proxy(change)
class BooleanOperation(Operation):
"""A base class for a boolean operation on two or more shapes.
Attributes
----------
shape1: Shape
The first shape argument of the operation.
shape2: Shape
The second shape argument of the operation.
"""
shape1 = d_(Instance(object))
shape2 = d_(Instance(object))
#: Disable the operation (the result will be the first shape or child)
disabled = d_(Bool(False))
#: Unify using ShapeUpgrade_UnifySameDomain
unify = d_(Bool(False))
#: If True, put all children in one operation otherwise
#: perform a boolean operation with each child one at a time
parallel = d_(Bool(False))
@observe("shape1", "shape2", "unify", "parallel", "disabled")
def _update_proxy(self, change: dict[str, Any]):
super(BooleanOperation, self)._update_proxy(change)
[docs]
class Common(BooleanOperation):
"""An operation that results in the common volume of the two shapes.
This operation is repeated to give the intersection all child shapes.
Examples
----------
Common:
Box:
pass
Circle:
radius = 2
Torus:
radius = 1
"""
#: Reference to the implementation control
proxy = Typed(ProxyCommon)
[docs]
class Cut(BooleanOperation):
"""An operation that results in the subtraction of the second and
following shapes the first shape. This operation is repeated for all
additional child shapes if more than two are given.
Examples
----------
Cut:
Box:
dx = 2
dy = 2
dz = 2
Box:
pass
# etc...
"""
#: Reference to the implementation control
proxy = Typed(ProxyCut)
[docs]
class Fuse(BooleanOperation):
"""An operation that results in the addition all of the child shapes.
Examples
----------
Fuse:
Box:
pass
Box:
position = (1,0,0)
"""
#: Reference to the implementation control
proxy = Typed(ProxyFuse)
[docs]
class Split(BooleanOperation):
"""An operation that splits the first shape by all of the other shapes.
Examples
----------
Split:
# Splits the sphere along the x axis
Sphere:
radius = 2
Plane:
position = (1,0,0)
"""
#: Reference to the implementation control
proxy = Typed(ProxySplit)
[docs]
class Intersection(BooleanOperation):
"""An operation that gives the intersection by the first shape and all of
the other shapes. The result is always an Edge, Wire, or Vertex. To get a
filled shape, use the `Common` operation instead.
Examples
----------
Intersection:
# Draws the intersection wires of the box sliced by the plane
Box:
pass
Plane:
position = (1/2,0,0)
"""
#: Reference to the implementation control
proxy = Typed(ProxyIntersection)
[docs]
class Fillet(Operation):
"""Applies fillet operation to the first child shape.
Attributes
----------
shape: String
The fillet shape type apply
radius: Float
Radius of the fillet. Must be less than the face width.
operations: List of edges, optional
List of edges to apply the operation to. If not given all edges will
be used. Used in conjunction with the `topology.edges` attribute.
Examples
--------
Fillet:
#: Fillet the first 4 edges of the box (left side)
operations = [e for i, e in enumerate(box.topology.edges) if i < 4]
radius = 0.1
Box: box:
pass
"""
#: Reference to the implementation control
proxy = Typed(ProxyFillet)
#: If True, don't apply the fillet (for debugging)
disabled = d_(Bool())
#: Fillet shape type
shape_type = d_(Enum("rational", "angular", "polynomial")).tag(
view=True, group="Fillet"
)
#: Radius of fillet
radius = d_(Float(1, strict=False)).tag(view=True, group="Fillet")
#: Edges to apply fillet to and parameters
#: Leave blank to use all edges of the shape
operations = d_(List()).tag(view=True, group="Fillet")
@observe("shape_type", "radius", "operations", "disabled")
def _update_proxy(self, change: dict[str, Any]):
super(Fillet, self)._update_proxy(change)
[docs]
class Chamfer(Operation):
"""Applies Chamfer operation to the first child shape.
Attributes
----------
distance: Float
The distance of the chamfer to apply
distance2: Float
The second distance of the chamfer to apply
operations: List of edges or faces, optional
List of edges or faces to apply the operation to. If not given the first
face will be used. Used in conjunction with the `topology` attribute.
Examples
--------
Chamfer:
#: Fillet the top of the cylinder
operations = [cyl.topology.faces[0]]
distance = 0.2
Cylinder: cyl:
pass
"""
#: Reference to the implementation control
proxy = Typed(ProxyChamfer)
#: If True, don't apply the chamfer (for debugging)
disabled = d_(Bool())
#: Distance of chamfer
distance = d_(Float(1, strict=False)).tag(view=True, group="Chamfer")
#: Second of chamfer (leave 0 if not used)
distance2 = d_(Float(0, strict=False)).tag(view=True, group="Chamfer")
#: Edges or faces to apply chamfer to
operations = d_(List()).tag(view=True, group="Chamfer")
@observe("distance", "distance2", "operations", "disabled")
def _update_proxy(self, change: dict[str, Any]):
super(Chamfer, self)._update_proxy(change)
[docs]
class Offset(Operation):
"""An operation that create an Offset wire or face of the first child shape.
Attributes
----------
offset: Float
The offset distance
offset_mode: String
Defines the construction type of parallels applied to the free edges
of the shape
intersection: Bool
Intersection specifies how the algorithm must work in order to limit
the parallels to two adjacent shapes
join_type: String
Defines how to fill the holes that may appear between parallels to
the two adjacent faces
disabled: Bool
Examples
--------
See examples/operations.enaml
"""
#: Reference to the implementation control
proxy = Typed(ProxyOffset)
#: Whether the offset should be closed
closed = d_(Bool(True))
#: Offset
offset = d_(Float(1, strict=False)).tag(view=True, group="Offset")
#: Make the offset at a distance parallel to the normal plane.
normal_distance = d_(Float(0, strict=False))
#: Offset mode
offset_mode = d_(Enum("skin", "pipe", "recto_verso")).tag(view=True, group="Offset")
#: Intersection
intersection = d_(Bool(False)).tag(view=True, group="Offset")
#: Join type
join_type = d_(Enum("arc", "tangent", "intersection")).tag(
view=True, group="Offset"
)
#: If True, convert the wire into a face
as_face = d_(Bool())
#: The shape to offset if given
shape = d_(Instance((Shape, TopoDS_Shape)))
#: Disble operation
disabled = d_(Bool())
@observe(
"offset",
"offset_mode",
"intersection",
"join_type",
"closed",
"normal_distance",
"shape",
"as_face",
"disabled",
)
def _update_proxy(self, change: dict[str, Any]):
super(Offset, self)._update_proxy(change)
[docs]
class OffsetShape(Offset):
"""An operation that create an OffsetShape from the first child shape.
Attributes
----------
offset: Float
The offset distance
offset_mode: String
Defines the construction type of parallels applied to the free edges
of the shape
intersection: Bool
Intersection specifies how the algorithm must work in order to limit
the parallels to two adjacent shapes
join_type: String
Defines how to fill the holes that may appear between parallels to
the two adjacent faces
Examples
--------
See examples/operations.enaml
"""
#: Reference to the implementation control
proxy = Typed(ProxyOffsetShape)
[docs]
class ThickSolid(Offset):
"""An operation that creates a hollowed out solid from shape.
Attributes
----------
faces: List, optional
List of faces that bound the solid.
Examples
--------
ThickSolid:
#: Creates an open box with a thickness of 0.1
offset = 0.1
Box: box:
position = (4,-4,0)
# Get top face
faces << [sorted(box.topology.faces,key=top_face)[0]]
"""
#: Reference to the implementation control
proxy = Typed(ProxyThickSolid)
#: Closing faces
faces = d_(List()).tag(view=True, group="ThickSolid")
@observe("faces")
def _update_proxy(self, change: dict[str, Any]):
super(ThickSolid, self)._update_proxy(change)
[docs]
class Pipe(Operation):
"""An operation that extrudes a profile along a spline, wire, or path.
Attributes
----------
spline: Edge or Wire
The spline to extrude along.
profile: Wire
The profile to extrude.
fill_mode: String, optional
The fill mode to use.
Examples
--------
See examples/pipes.enaml
"""
#: Reference to the implementation control
proxy = Typed(ProxyPipe)
#: Spline to make the pipe along
spline = d_(Instance(object))
#: Profile to make the pipe from
profile = d_(Instance(object))
#: Fill mode
fill_mode = d_(
Enum(
None,
"corrected_frenet",
"fixed",
"frenet",
"constant_normal",
"darboux",
"guide_ac",
"guide_plan",
"guide_ac_contact",
"guide_plan_contact",
"discrete_trihedron",
)
).tag(view=True, group="Pipe")
@observe("spline", "profile", "fill_mode")
def _update_proxy(self, change: dict[str, Any]):
super(Pipe, self)._update_proxy(change)
class AbstractRibSlot(Operation):
#: Base shape
shape = d_(Instance(Shape))
#: Profile to make the pipe from
contour = d_(Instance(Shape))
#: Profile to make the pipe from
plane = d_(Instance(Shape))
#: Fuse (False to remove, True to add)
fuse = d_(Bool(False)).tag(view=True)
[docs]
class DraftAngle(Operation):
class Parameters(Atom):
face = Instance((Shape, TopoDS_Shape))
#: Angle relative to direction
angle = Float(strict=False)
#: Direction of the draft angle
direction = Coerced(
Direction, factory=lambda: Direction(0, 0, 1), coercer=coerce_direction
)
#: The point and direction of the neural plane
neutral_plane = Tuple(Point, Direction)
#: Reference to the implementation control
proxy = Typed(ProxyDraftAngle)
#: If True, don't apply the operation (for debugging)
disabled = d_(Bool())
#: Draft Angle
angle = d_(Float(strict=False))
#: List of faces to angle. Ignored if operations is given
faces = d_(List())
#: List of operations to perform. If this value is given all the
#: other parameters are ignored.
operations = d_(List(Parameters))
@observe("faces", "angle", "operations", "disabled")
def _update_proxy(self, change: dict[str, Any]):
super()._update_proxy(change)
[docs]
class ThruSections(Operation):
"""An operation that extrudes a shape by means of going through a series
of profile sections along a spline or path.
Attributes
----------
solid: Bool
If True, build a solid otherwise build a shell.
ruled: Bool
If False, smooth out the surfaces using approximation
precision: Float, optional
The precision to use for approximation.
Examples
--------
See examples/thru_sections.enaml
"""
#: Reference to the implementation control
proxy = Typed(ProxyThruSections)
#: isSolid is set to true if the construction algorithm is required
#: to build a solid or to false if it is required to build a shell
#: (the default value),
solid = d_(Bool(False)).tag(view=True, group="Through Sections")
#: ruled is set to true if the faces generated between the edges
#: of two consecutive wires are ruled surfaces or to false
#: (the default value)
#: if they are smoothed out by approximation
ruled = d_(Bool(False)).tag(view=True, group="Through Sections")
#: Apply shape fixing to the result
fix = d_(Bool(False)).tag(view=True, group="Through Sections")
#: pres3d defines the precision criterion used by the approximation
#: algorithm;
#: the default value is 1.0e-6. Use AddWire and AddVertex to define
#: the successive sections of the shell or solid to be built.
precision = d_(Float(1e-6)).tag(view=True, group="Through Sections")
@observe("solid", "ruled", "precision", "fix")
def _update_proxy(self, change: dict[str, Any]):
super(ThruSections, self)._update_proxy(change)
class TransformOperation(Atom):
#: Point
point = Coerced(Point, coercer=coerce_point)
def _default_point(self) -> Point:
return Point(0.0, 0.0, 0.0)
[docs]
class Rotate(TransformOperation):
#: Rotation axis
direction = Coerced(Direction, coercer=coerce_direction)
def _default_direction(self) -> Direction:
return Direction(0.0, 0.0, 1.0)
#: Angle
angle = Float(0.0, strict=False)
[docs]
class Translate(TransformOperation):
#: Position
x = Float(0.0, strict=False)
y = Float(0.0, strict=False)
z = Float(0.0, strict=False)
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **kwargs):
super(Translate, self).__init__(x=x, y=y, z=z, **kwargs)
[docs]
class Scale(TransformOperation):
s = Float(1.0, strict=False)
def __init__(self, s: float = 1, **kwargs):
super(Scale, self).__init__(s=s, **kwargs)
[docs]
class Mirror(TransformOperation):
#: Position
x = Float(0.0, strict=False)
y = Float(0.0, strict=False)
z = Float(0.0, strict=False)
#: Mirror as plane
plane = Bool()
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **kwargs):
super(Mirror, self).__init__(x=x, y=y, z=z, **kwargs)
[docs]
class Sew(Operation):
#: Reference to the implementation control
proxy = Typed(ProxySew)
class Glue(Operation):
#: Reference to the implementation control
proxy = Typed(ProxyGlue)
[docs]
class NormalProjection(Operation):
"""Project a wire onto a face. Requires at least one child shape. The
result is a wire.
"""
#: Reference to the implementation control
proxy = Typed(ProxyNormalProjection)
#: The face project onto
shape = d_(Instance((Shape, TopoDS_Shape)))
#: Max distance
max_distance = d_(Float(strict=False))
[docs]
class Extend(Operation):
"""Extend a curve or surface by it's parameters"""
#: Reference to the implementation control
proxy = Typed(ProxyExtend)
#: The shape to extend.
shape = d_(Instance((Shape, TopoDS_Shape)))
#: How to interpret the parameters
mode = d_(Enum("length", "parameter"))
#: List of extend operations
operations = d_(List())
@observe("shape", "mode", "operations")
def _update_proxy(self, change: dict[str, Any]):
super()._update_proxy(change)
[docs]
class RemoveFeatures(Operation):
"""Remove a feature from a shape."""
#: Reference to the implementation control
proxy = Typed(ProxyRemoveFeatures)
#: The shape to remove features from. If not set it uses the first child.
shape = d_(Instance((Shape, TopoDS_Shape)))
#: If True, put all children in one operation otherwise
#: perform a boolean operation with each child one at a time
parallel = d_(Bool(False))
#: Whether the operation should be disabled.
disabled = d_(Bool(False))
#: List of features to remove
features = d_(List())
@observe("shape", "features", "parallel", "disabled")
def _update_proxy(self, change: dict[str, Any]):
super()._update_proxy(change)