Skip to content

Commit fe01012

Browse files
committed
Add a Hole primitive that allows easy chamfering of top and bottom edges
1 parent 53624cc commit fe01012

File tree

6 files changed

+235
-4
lines changed

6 files changed

+235
-4
lines changed

declaracad/occ/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
Export,
8585
Face,
8686
HalfSpace,
87+
Hole,
8788
Material,
8889
Part,
8990
Prism,

declaracad/occ/impl/occ_factories.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ def occ_half_space_factory():
127127
return OccHalfSpace
128128

129129

130+
def occ_hole():
131+
from .occ_hole import OccHole
132+
133+
return OccHole
134+
135+
130136
def occ_hyperbola_factory():
131137
from .occ_hyperbola import OccHyperbola
132138

@@ -414,6 +420,7 @@ def occ_export_factory():
414420
"Tube": occ_tube_factory,
415421
"Wedge": occ_wedge_factory,
416422
#: Primatives
423+
"Hole": occ_hole,
417424
"HalfSpace": occ_half_space_factory,
418425
"Revol": occ_revol_factory,
419426
#: Operations

declaracad/occ/impl/occ_fillet.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,17 @@ def fillet_polyline(self, child: OccPolyline) -> TopoDS_Shape:
135135
radius, wire = filleted_wires[i]
136136
topo = Topology(shape=wire)
137137
if last_radius is None and radius is None:
138-
edges.append(topo.edges[0]) # Add common edge with no fillets
138+
edges.append(topo.edges[0]) # Add common edge with no fillets
139139
else:
140140
common = BRepAlgoAPI_Common(last_wire, wire)
141141
common.Build()
142142
common_edges = Topology(shape=common.Shape()).edges
143143
assert common_edges, "Fillet radius too large"
144144
edges.extend(common_edges)
145145
if i == last_index:
146-
edges.extend(topo.edges[1:]) # Add all remaining edges
146+
edges.extend(topo.edges[1:]) # Add all remaining edges
147147
elif radius is not None:
148-
edges.append(topo.edges[1]) # Add only the filleted edge
148+
edges.append(topo.edges[1]) # Add only the filleted edge
149149
last_wire = wire
150150
last_topo = topo
151151
last_radius = radius

declaracad/occ/impl/occ_hole.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Copyright (c) 2025, Jairus Martin.
3+
4+
Distributed under the terms of the GPL v3 License.
5+
6+
The full license is in the file LICENSE, distributed with this software.
7+
8+
"""
9+
10+
from typing import Optional
11+
12+
from OCCT.BRepBuilderAPI import (
13+
BRepBuilderAPI_MakeFace,
14+
BRepBuilderAPI_MakePolygon,
15+
BRepBuilderAPI_Transform,
16+
)
17+
from OCCT.BRepPrimAPI import BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeRevol
18+
from OCCT.gp import gp_Ax3, gp_Pnt, gp_Trsf
19+
20+
from declaracad.occ.shape import ProxyHole
21+
22+
from .occ_shape import AZ, DEFAULT_AXIS, OccShape, coerce_axis
23+
from .topology import Topology
24+
25+
26+
class OccHole(OccShape, ProxyHole):
27+
reference = ""
28+
29+
def create_shape(self):
30+
d = self.declaration
31+
r = d.diameter / 2
32+
h = d.depth
33+
if d.top_edge.distance or d.bottom_edge.distance:
34+
poly = BRepBuilderAPI_MakePolygon()
35+
36+
poly.Add(gp_Pnt(0, 0, 0))
37+
if edge_style := d.bottom_edge:
38+
d1 = abs(edge_style.distance)
39+
d2 = abs(edge_style.distance2 or d1)
40+
if edge_style.kind == "chamfer":
41+
d1 = -d1
42+
y = r + d1
43+
if y < 0:
44+
raise ValueError(
45+
f"Bottom edge chamfer distance cannot be > hole radius. Got R={r} D={d1}"
46+
)
47+
elif y > 0:
48+
poly.Add(gp_Pnt(0, y, 0))
49+
# If y == 0 skip the point
50+
poly.Add(gp_Pnt(0, r, d2))
51+
z = d2
52+
else:
53+
poly.Add(gp_Pnt(0, r, 0))
54+
z = 0
55+
56+
if edge_style := d.top_edge:
57+
d1 = abs(edge_style.distance)
58+
d2 = abs(edge_style.distance2 or d1)
59+
if edge_style.kind == "chamfer":
60+
d1 = -d1
61+
poly.Add(gp_Pnt(0, r, h - d2))
62+
y = r + d1
63+
if y < 0:
64+
raise ValueError(
65+
f"Top edge chamfer cannot be > hole radius. Got R={r} D={d1}"
66+
)
67+
elif y > 0:
68+
poly.Add(gp_Pnt(0, y, h))
69+
# If y == 0 skip the point
70+
else:
71+
poly.Add(gp_Pnt(0, r, h))
72+
poly.Add(gp_Pnt(0, 0, h))
73+
poly.Close()
74+
face = BRepBuilderAPI_MakeFace(poly.Wire()).Face()
75+
revol = BRepPrimAPI_MakeRevol(face, AZ, False)
76+
t = self.get_transform()
77+
shape = BRepBuilderAPI_Transform(revol.Shape(), t, False).Shape()
78+
else:
79+
cylinder = BRepPrimAPI_MakeCylinder(coerce_axis(d.axis), r, h)
80+
shape = cylinder.Shape()
81+
82+
self.shape = shape
83+
84+
def set_diameter(self, diameter: float):
85+
self.create_shape()
86+
87+
def set_depth(self, depth: float):
88+
self.create_shape()
89+
90+
def set_top_edge(self, value):
91+
self.create_shape()
92+
93+
def set_bottom_edge(self, value):
94+
self.create_shape()

declaracad/occ/shape.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
@author: jrm
1212
"""
1313
from math import pi
14-
from typing import Any, ClassVar, Optional
14+
from typing import Any, ClassVar, Optional, Union
1515

1616
from atom.api import (
17+
Atom,
1718
Bool,
1819
Coerced,
1920
Dict,
21+
Enum,
2022
Event,
2123
Float,
2224
FloatRange,
@@ -246,6 +248,43 @@ def set_itx(self, itx):
246248
raise NotImplementedError
247249

248250

251+
class HoleEdgeStyle(Atom):
252+
kind = Enum("chamfer", "cone")
253+
distance = Float(strict=False)
254+
distance2 = Float(strict=False)
255+
256+
def __init__(self, *args, **kwargs):
257+
if len(args) == 1 and isinstance(args[0], tuple):
258+
value = args[0]
259+
n = len(value)
260+
if n == 3:
261+
kind, distance, distance2 = value
262+
super().__init__(kind=kind, distance=distance, distance2=distance2)
263+
return
264+
elif n == 2:
265+
kind, distance = value
266+
super().__init__(kind=kind, distance=distance)
267+
return
268+
super().__init__(*args, **kwargs)
269+
270+
271+
class ProxyHole(ProxyShape):
272+
#: A reference to the shape declaration.
273+
declaration = ForwardTyped(lambda: Hole)
274+
275+
def set_diameter(self, diameter: float):
276+
raise NotImplementedError
277+
278+
def set_depth(self, depth: float):
279+
raise NotImplementedError
280+
281+
def set_top_edge(self, value: HoleEdgeStyle):
282+
raise NotImplementedError
283+
284+
def set_bottom_edge(self, value: HoleEdgeStyle):
285+
raise NotImplementedError
286+
287+
249288
class ProxyRevol(ProxyShape):
250289
#: A reference to the shape declaration.
251290
declaration = ForwardTyped(lambda: Revol)
@@ -1045,6 +1084,52 @@ def _update_proxy(self, change: dict[str, Any]):
10451084
super(Wedge, self)._update_proxy(change)
10461085

10471086

1087+
class Hole(Shape):
1088+
"""A primitive Hole shape.
1089+
1090+
Attributes
1091+
----------
1092+
1093+
diameter: Float
1094+
The diameter of the hole.
1095+
depth: Float
1096+
The depth of the hole.
1097+
top_edge: Float | Tuple[Float, Float]
1098+
If negative, add a chamfer to the top of the hole. If positive add a cone.
1099+
bottom_edge: Float | Tuple[Float, Float]
1100+
If negative, add a chamfer on the bottom of the hole. If positive add a cone.
1101+
1102+
Examples
1103+
--------
1104+
1105+
Hole:
1106+
diameter = 4
1107+
depth = 20
1108+
top_edge = ('chamfer', 0.5)
1109+
bottom_edge =('cone', 0.5)
1110+
1111+
"""
1112+
1113+
#: Proxy shape
1114+
proxy = Typed(ProxyHole)
1115+
1116+
#: Hole diameter
1117+
diameter = d_(Float(1, strict=False)).tag(view=True)
1118+
1119+
#: Hole depth
1120+
depth = d_(Float(1, strict=False)).tag(view=True)
1121+
1122+
#: Top edge style
1123+
top_edge = d_(Coerced(HoleEdgeStyle))
1124+
1125+
#: Bottom chamfer of the hole
1126+
bottom_edge = d_(Coerced(HoleEdgeStyle))
1127+
1128+
@observe("diameter", "depth", "top_edge", "bottom_edge")
1129+
def _update_proxy(self, change: dict[str, Any]):
1130+
super()._update_proxy(change)
1131+
1132+
10481133
class Revol(Shape):
10491134
"""A Revol creates a shape by revolving a profile about an axis.
10501135

examples/holes.enaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Created in DeclaraCAD
2+
from declaracad.occ.api import *
3+
4+
enamldef Assembly(Part):
5+
Axis:
6+
pass
7+
Cut:
8+
Box: box:
9+
position = (-dx/2, -dy/2)
10+
dx = 50
11+
dy = 60
12+
dz = 10
13+
Hole: center_through_hole:
14+
# A hole with no top_edge or bottom_edge styles
15+
# defined is the same as a Cylinder except the diameter
16+
# and depth are used instead of radius and height
17+
diameter = 10
18+
depth = box.dz
19+
Hole: center_hole:
20+
# Defining the top edge as "cone" 1 adds a cone
21+
# to the top of the hole which when cut from the box
22+
# makes it chamfered by the cone distance.
23+
position = (0, 0, 2)
24+
diameter = 20
25+
depth = box.dz-self.z
26+
top_edge = ("cone", 1)
27+
bottom_edge = ("chamfer", 2)
28+
29+
Looper: corner_holes:
30+
attr offset = 8
31+
iterable = [
32+
(box.dx/2-offset, box.dy/2-offset),
33+
(-box.dx/2+offset, box.dy/2-offset),
34+
(-box.dx/2+offset, -box.dy/2+offset),
35+
(box.dx/2-offset, -box.dy/2+offset),
36+
]
37+
Hole:
38+
position = loop.item
39+
diameter = 6
40+
depth = box.dz
41+
# Defining the top edge as "cone" 1 adds a cone
42+
# to the top of the hole which when cut from the box
43+
# makes it chamfered by the cone distance.
44+
top_edge = ("cone", 1)

0 commit comments

Comments
 (0)