Skip to content

Commit cda76d6

Browse files
committed
Added cq_basic.py and test_basic.py for new drafted shape builders
1 parent 9d603c4 commit cda76d6

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed

cqkit/cq_basic.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#! /usr/bin/env python3
2+
#
3+
# Copyright (C) 2024 Michael Gale
4+
# This file is part of the cq-kit python module.
5+
# Permission is hereby granted, free of charge, to any person
6+
# obtaining a copy of this software and associated documentation
7+
# files (the "Software"), to deal in the Software without restriction,
8+
# including without limitation the rights to use, copy, modify, merge,
9+
# publish, distribute, sublicense, and/or sell copies of the Software,
10+
# and to permit persons to whom the Software is furnished to do so,
11+
# subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be
14+
# included in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
#
24+
# Basic shape/solid convenience functions
25+
26+
import cadquery as cq
27+
28+
from .cq_geometry import draft_dim
29+
30+
31+
def drafted_cylinder(radius, height, draft_angle=0, workplane="XY"):
32+
"""Makes a simple tapered cylinder with optional draft angle."""
33+
rt, rb = draft_dim(radius, draft_angle, height / 2, symmetric=True)
34+
return (
35+
cq.Workplane(workplane)
36+
.circle(rb)
37+
.workplane(offset=height)
38+
.circle(rt)
39+
.loft(ruled=True)
40+
)
41+
42+
43+
def drafted_hollow_cylinder(
44+
radius,
45+
height,
46+
wall_thickness,
47+
draft_angle=0,
48+
workplane="XY",
49+
has_roof=True,
50+
has_floor=False,
51+
roof_thickness=None,
52+
floor_thickness=None,
53+
):
54+
"""Makes a hollow tapered cylinder with specified wall thickness and draft angle.
55+
Different floor and roof shell thickness can be specified by overriding arguments.
56+
The exposed hollow face can be specified with any combination of has_roof and has_floor.
57+
"""
58+
exterior = drafted_cylinder(
59+
radius, height, draft_angle=draft_angle, workplane=workplane
60+
)
61+
ri = radius - wall_thickness
62+
roof = roof_thickness if roof_thickness is not None else wall_thickness
63+
floor = floor_thickness if floor_thickness is not None else wall_thickness
64+
int_height = height
65+
if has_floor:
66+
int_height -= floor
67+
if has_roof:
68+
int_height -= roof
69+
interior = drafted_cylinder(
70+
ri, int_height, draft_angle=draft_angle, workplane=workplane
71+
)
72+
if has_floor:
73+
interior = interior.translate(interior.plane.zDir * floor)
74+
return exterior.cut(interior)
75+
76+
77+
def drafted_box(
78+
length,
79+
width,
80+
height,
81+
draft_angle=0,
82+
workplane="XY",
83+
draft_length=True,
84+
draft_width=True,
85+
):
86+
"""Makes a simple tapered box with optional draft angle."""
87+
lt, lb = length, length
88+
if draft_length:
89+
lt, lb = draft_dim(length, draft_angle, height, symmetric=True)
90+
wt, wb = width, width
91+
if draft_width:
92+
wt, wb = draft_dim(width, draft_angle, height, symmetric=True)
93+
return (
94+
cq.Workplane(workplane)
95+
.rect(lb, wb)
96+
.workplane(offset=height)
97+
.rect(lt, wt)
98+
.loft(ruled=True)
99+
)
100+
101+
102+
def drafted_hollow_box(
103+
length,
104+
width,
105+
height,
106+
wall_thickness,
107+
draft_angle=0,
108+
workplane="XY",
109+
has_roof=True,
110+
has_floor=False,
111+
roof_thickness=None,
112+
floor_thickness=None,
113+
draft_length=True,
114+
draft_width=True,
115+
):
116+
"""Makes a hollow tapered box with specified wall thickness and draft angle.
117+
Different floor and roof shell thickness can be specified by overriding arguments.
118+
The exposed hollow face can be specified with any combination of has_roof and has_floor.
119+
"""
120+
exterior = drafted_box(
121+
length,
122+
width,
123+
height,
124+
draft_angle,
125+
workplane=workplane,
126+
draft_length=draft_length,
127+
draft_width=draft_width,
128+
)
129+
li, wi = length - 2 * wall_thickness, width - 2 * wall_thickness
130+
roof = roof_thickness if roof_thickness is not None else wall_thickness
131+
floor = floor_thickness if floor_thickness is not None else wall_thickness
132+
int_height = height
133+
if has_floor:
134+
int_height -= floor
135+
if has_roof:
136+
int_height -= roof
137+
interior = drafted_box(
138+
li,
139+
wi,
140+
int_height,
141+
draft_angle,
142+
workplane=workplane,
143+
draft_length=draft_length,
144+
draft_width=draft_width,
145+
)
146+
if has_floor:
147+
interior = interior.translate(interior.plane.zDir * floor)
148+
return exterior.cut(interior)
149+
150+
151+
def drafted_slot(
152+
length,
153+
radius,
154+
height,
155+
draft_angle=0,
156+
workplane="XY",
157+
draft_length=True,
158+
draft_radius=True,
159+
):
160+
"""Makes slot shape with optional tapered height."""
161+
lt, lb = length, length
162+
if draft_length:
163+
lt, lb = draft_dim(length, draft_angle, height, symmetric=True)
164+
rt, rb = radius, radius
165+
if draft_radius:
166+
rt, rb = draft_dim(radius, draft_angle, height / 2, symmetric=True)
167+
return (
168+
cq.Workplane(workplane)
169+
.slot2D(lb, 2 * rb)
170+
.workplane(offset=height)
171+
.slot2D(lt, 2 * rt)
172+
.loft(ruled=True)
173+
)
174+
175+
176+
def drafted_hollow_slot(
177+
length,
178+
radius,
179+
height,
180+
wall_thickness,
181+
draft_angle=0,
182+
workplane="XY",
183+
has_roof=True,
184+
has_floor=False,
185+
roof_thickness=None,
186+
floor_thickness=None,
187+
draft_length=True,
188+
draft_radius=True,
189+
):
190+
"""Makes a hollow tapered box with specified wall thickness and draft angle.
191+
Different floor and roof shell thickness can be specified by overriding arguments.
192+
The exposed hollow face can be specified with any combination of has_roof and has_floor.
193+
"""
194+
exterior = drafted_slot(
195+
length,
196+
radius,
197+
height,
198+
draft_angle,
199+
workplane=workplane,
200+
draft_length=draft_length,
201+
draft_radius=draft_radius,
202+
)
203+
li, ri = length - 2 * wall_thickness, radius - wall_thickness
204+
roof = roof_thickness if roof_thickness is not None else wall_thickness
205+
floor = floor_thickness if floor_thickness is not None else wall_thickness
206+
int_height = height
207+
if has_floor:
208+
int_height -= floor
209+
if has_roof:
210+
int_height -= roof
211+
interior = drafted_slot(
212+
li,
213+
ri,
214+
int_height,
215+
draft_angle,
216+
workplane=workplane,
217+
draft_length=draft_length,
218+
draft_radius=draft_radius,
219+
)
220+
if has_floor:
221+
interior = interior.translate(interior.plane.zDir * floor)
222+
return exterior.cut(interior)

tests/test_basic.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Basic shape builder tests
2+
3+
# system modules
4+
5+
# my modules
6+
from cadquery import *
7+
8+
from cqkit import *
9+
10+
from rich import inspect
11+
12+
13+
def _almost_same(x, y):
14+
if not isinstance(x, (list, tuple)):
15+
return abs(x - y) < 1e-3
16+
return all(abs(x[i] - y[i]) < 1e-3 for i in range(len(x)))
17+
18+
19+
# def test_drafted_size():
20+
# r = drafted_box(2, 3, 5, draft_angle=5)
21+
# export_step_file(r, "./tests/stepfiles/draft_box.step")
22+
# rc = drafted_cylinder(1, 5, draft_angle=5)
23+
# export_step_file(rc, "./tests/stepfiles/draft_cyl.step")
24+
25+
26+
def test_drafted_box():
27+
r = drafted_box(1, 2, 3)
28+
assert _almost_same(size_3d(r), (1, 2, 3))
29+
r = drafted_box(2, 4, 5, draft_angle=10)
30+
assert _almost_same(size_3d(r), (2.440, 4.440, 5))
31+
assert _almost_same(size_2d(r.faces("<Z")), (2.441, 4.441))
32+
assert _almost_same(size_2d(r.faces(">Z")), (1.559, 3.559))
33+
r = drafted_box(1, 2, 3, 5, draft_length=False)
34+
assert _almost_same(size_3d(r), (1, 2.131, 3))
35+
r = drafted_box(1, 2, 3, 5, draft_width=False)
36+
assert _almost_same(size_3d(r), (1.131, 2, 3))
37+
38+
39+
def test_drafted_cylinder():
40+
r = drafted_cylinder(1, 4)
41+
assert _almost_same(size_3d(r), (2, 2, 4))
42+
r1 = drafted_cylinder(1.5, height=10)
43+
assert _almost_same(size_3d(r1), (3, 3, 10))
44+
r2 = drafted_cylinder(1, height=7, draft_angle=15)
45+
assert _almost_same(size_3d(r2), (2.937, 2.937, 7))
46+
assert _almost_same(size_2d(r2.faces("<Z")), (2.937, 2.937))
47+
assert _almost_same(size_2d(r2.faces(">Z")), (1.062, 1.062))
48+
49+
50+
def test_drafted_slot():
51+
r = drafted_slot(10, 1.5, 5)
52+
assert _almost_same(size_3d(r), (10, 3, 5))
53+
r = drafted_slot(10, 1.5, 5, draft_angle=5)
54+
assert _almost_same(size_3d(r), (10.218, 3.218, 5))
55+
assert _almost_same(size_2d(r.faces("<Z")), (10.218, 3.218))
56+
assert _almost_same(size_2d(r.faces(">Z")), (9.781, 2.781))
57+
58+
r = drafted_slot(10, 1.5, 5, draft_angle=5, draft_length=False)
59+
assert _almost_same(size_3d(r), (10, 3.218, 5))
60+
r = drafted_slot(10, 1.5, 5, draft_angle=5, draft_radius=False)
61+
assert _almost_same(size_3d(r), (10.218, 3, 5))
62+
63+
64+
def test_drafted_hollow_slot():
65+
r = drafted_hollow_slot(10, 1.5, 5, 0.5, draft_angle=6)
66+
assert _almost_same(size_3d(r), (10.262, 3.262, 5))
67+
assert _almost_same(size_2d(r.faces("<Z")), (10.262, 3.262))
68+
assert _almost_same(size_2d(r.faces(">Z")), (9.737, 2.737))
69+
70+
71+
def test_drafted_hollow_box():
72+
r = drafted_hollow_box(10, 15, 20, 1.5)
73+
assert _almost_same(size_3d(r), (10, 15, 20))
74+
w0 = r.faces("<Z").wires().vals()
75+
assert len(w0) == 2
76+
wl = [wire_length(w) for w in w0]
77+
assert _almost_same(38, wl[0]) or _almost_same(50, wl[0])
78+
assert _almost_same(38, wl[1]) or _almost_same(50, wl[1])
79+
80+
r = drafted_hollow_box(10, 15, 20, 1.5, workplane="ZX")
81+
assert _almost_same(size_3d(r), (15, 20, 10))
82+
83+
r1 = drafted_hollow_box(10, 15, 20, 1.5, 5)
84+
assert _almost_same(size_3d(r1), (10.875, 15.875, 20))
85+
assert _almost_same(size_2d(r1.faces("<Z")), (10.875, 15.875))
86+
assert _almost_same(size_2d(r1.faces(">Z")), (9.125, 14.125))
87+
w1 = r1.faces(">Z").wires().vals()
88+
assert len(w1) == 1
89+
w1 = r1.faces("<Z").wires().vals()
90+
assert len(w1) == 2
91+
wl = [wire_length(w) for w in w1]
92+
assert _almost_same(41.237, wl[0]) or _almost_same(53.499, wl[0])
93+
assert _almost_same(41.237, wl[1]) or _almost_same(53.499, wl[1])
94+
re = r1.edges(FlatEdgeSelector(18.5)).vals()
95+
assert len(re) == 4
96+
97+
r2 = drafted_hollow_box(10, 15, 20, 1.5, 5, has_floor=True, has_roof=False)
98+
assert _almost_same(size_3d(r1), (10.875, 15.875, 20))
99+
assert _almost_same(size_2d(r1.faces("<Z")), (10.875, 15.875))
100+
assert _almost_same(size_2d(r1.faces(">Z")), (9.125, 14.125))
101+
w2 = r2.faces("<Z").wires().vals()
102+
assert len(w2) == 1
103+
w2 = r2.faces(">Z").wires().vals()
104+
assert len(w2) == 2
105+
wl = [wire_length(w) for w in w2]
106+
assert _almost_same(34.763, wl[0]) or _almost_same(46.5, wl[0])
107+
assert _almost_same(34.763, wl[1]) or _almost_same(46.5, wl[1])
108+
109+
r3 = drafted_hollow_box(
110+
10, 15, 20, 1.5, 5, has_floor=False, has_roof=True, roof_thickness=1
111+
)
112+
assert _almost_same(size_3d(r3), (10.875, 15.875, 20))
113+
assert _almost_same(size_2d(r3.faces("<Z")), (10.875, 15.875))
114+
assert _almost_same(size_2d(r3.faces(">Z")), (9.125, 14.125))
115+
w3 = r3.faces("<Z").wires().vals()
116+
wl = [wire_length(w) for w in w3]
117+
assert _almost_same(41.324, wl[0]) or _almost_same(53.499, wl[0])
118+
assert _almost_same(41.324, wl[1]) or _almost_same(53.499, wl[1])
119+
re = r3.wires(FlatWireSelector(19)).vals()
120+
assert len(re) == 1
121+
assert _almost_same(wire_length(re[0]), 34.675)
122+
123+
r4 = drafted_hollow_box(10, 15, 20, 1.5, 5, has_floor=False, has_roof=False)
124+
assert _almost_same(size_3d(r3), (10.875, 15.875, 20))
125+
wb = r4.faces("<Z").wires().vals()
126+
assert len(wb) == 2
127+
wl = [wire_length(w) for w in wb]
128+
assert _almost_same(41.5, wl[0]) or _almost_same(53.499, wl[0])
129+
assert _almost_same(41.5, wl[1]) or _almost_same(53.499, wl[1])
130+
wt = r4.faces(">Z").wires().vals()
131+
assert len(wt) == 2
132+
wl = [wire_length(w) for w in wt]
133+
assert _almost_same(34.5, wl[0]) or _almost_same(46.5, wl[0])
134+
assert _almost_same(34.5, wl[1]) or _almost_same(46.5, wl[1])
135+
136+
137+
def test_drafted_hollow_cylinder():
138+
r = drafted_hollow_cylinder(5, 15, 0.5)
139+
assert _almost_same(size_3d(r), (10, 10, 15))
140+
w0 = r.faces("<Z").wires().vals()
141+
assert len(w0) == 2
142+
143+
r = drafted_hollow_cylinder(5, 15, 0.5, 5)
144+
assert _almost_same(size_3d(r), (10.656, 10.656, 15))
145+
w0 = r.faces("<Z").wires().vals()
146+
assert len(w0) == 2
147+
148+
r = drafted_hollow_cylinder(
149+
5, 15, 0.5, 5, floor_thickness=2, has_floor=True, has_roof=False
150+
)
151+
assert _almost_same(size_3d(r), (10.656, 10.656, 15))
152+
wb = r.faces("<Z").wires().vals()
153+
assert len(wb) == 1
154+
wt = r.faces(">Z").wires().vals()
155+
assert len(wt) == 2
156+
re = r.wires(FlatWireSelector(2)).vals()
157+
assert len(re) == 1
158+
159+
r = drafted_hollow_cylinder(4, 10, 1, draft_angle=5, workplane="YZ")
160+
assert _almost_same(size_3d(r), (10, 8.437, 8.437))

0 commit comments

Comments
 (0)