1
#***************************************************************************
2
#* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
4
#* This program is free software; you can redistribute it and/or modify *
5
#* it under the terms of the GNU Lesser General Public License (LGPL) *
6
#* as published by the Free Software Foundation; either version 2 of *
7
#* the License, or (at your option) any later version. *
8
#* for detail see the LICENSE text file. *
10
#* This program is distributed in the hope that it will be useful, *
11
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
12
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13
#* GNU Library General Public License for more details. *
15
#* You should have received a copy of the GNU Library General Public *
16
#* License along with this program; if not, write to the Free Software *
17
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
20
#***************************************************************************
30
from os.path import join
32
__title__ = "ImportCSG OpenSCAD App unit tests"
33
__author__ = "Chris Hennes"
34
__url__ = "https://www.freecad.org"
37
class TestImportCSG(unittest.TestCase):
39
MODULE = 'test_importCSG' # file name without extension
40
temp_dir = tempfile.TemporaryDirectory()
44
self.test_dir = join(FreeCAD.getHomePath(), "Mod", "OpenSCAD", "OpenSCADTest", "data")
46
def test_open_scad(self):
47
testfile = join(self.test_dir, "CSG.scad")
48
doc = importCSG.open(testfile)
50
# Doc should now contain three solids: a union, an intersection, and a difference
51
union = doc.getObject("union")
52
intersection = doc.getObject("intersection")
53
difference = doc.getObject("difference")
55
self.assertTrue (union is not None)
56
self.assertTrue (intersection is not None)
57
self.assertTrue (difference is not None)
59
FreeCAD.closeDocument("CSG")
61
def test_open_csg(self):
62
testfile = join(self.test_dir, "CSG.csg")
63
doc = importCSG.open(testfile)
65
# Doc should now contain three solids: a union, an intersection, and a difference
66
union = doc.getObject("union")
67
intersection = doc.getObject("intersection")
68
difference = doc.getObject("difference")
70
self.assertTrue (union is not None)
71
self.assertTrue (intersection is not None)
72
self.assertTrue (difference is not None)
74
FreeCAD.closeDocument("CSG")
76
def utility_create_scad(self, scadCode, name):
77
filename = self.temp_dir.name + os.path.sep + name + ".scad"
78
print (f"Creating {filename}")
79
f = open(filename,"w+")
82
return importCSG.open(filename)
84
def test_import_sphere(self):
85
doc = self.utility_create_scad("sphere(10.0);","sphere")
86
sphere = doc.getObject("sphere")
87
self.assertTrue (sphere is not None)
88
self.assertTrue (sphere.Radius == 10.0)
89
FreeCAD.closeDocument(doc.Name)
91
def test_import_cylinder(self):
92
doc = self.utility_create_scad("cylinder(50.0,d=10.0);","cylinder")
93
cylinder = doc.getObject("cylinder")
94
self.assertTrue (cylinder is not None)
95
self.assertTrue (cylinder.Radius == 5.0)
96
self.assertTrue (cylinder.Height == 50.0)
97
FreeCAD.closeDocument(doc.Name)
99
def test_import_cube(self):
100
doc = self.utility_create_scad("cube([1.0,2.0,3.0]);","cube")
101
cube = doc.getObject("cube")
102
self.assertTrue (cube is not None)
103
self.assertTrue (cube.Length == 1.0)
104
self.assertTrue (cube.Width == 2.0)
105
self.assertTrue (cube.Height == 3.0)
106
FreeCAD.closeDocument(doc.Name)
108
def test_import_circle(self):
109
doc = self.utility_create_scad("circle(10.0);","circle")
110
circle = doc.getObject("circle")
111
self.assertTrue (circle is not None)
112
self.assertTrue (circle.Radius == 10.0)
113
FreeCAD.closeDocument(doc.Name)
115
def test_import_square(self):
116
doc = self.utility_create_scad("square([1.0,2.0]);","square")
117
square = doc.getObject("square")
118
self.assertTrue (square is not None)
119
self.assertTrue (square.Length == 1.0)
120
self.assertTrue (square.Width == 2.0)
121
FreeCAD.closeDocument(doc.Name)
123
def test_import_text(self):
124
# This uses the DXF importer that may pop-up modal dialogs
125
# if not all 3rd party libraries are installed
129
doc = self.utility_create_scad("text(\"X\");","text") # Keep it short to keep the test fast-ish
130
text = doc.getObject("text")
131
self.assertTrue (text is not None)
132
FreeCAD.closeDocument(doc.Name)
134
return # We may not have the DXF importer available
136
# Try a number with a set script:
137
doc = self.utility_create_scad("text(\"2\",script=\"latin\");","two_text")
138
text = doc.getObject("text")
139
self.assertTrue (text is not None)
140
FreeCAD.closeDocument(doc.Name)
142
# Leave off the script (which is supposed to default to "latin")
143
doc = self.utility_create_scad("text(\"1\");","one_text")
144
text = doc.getObject("text")
145
self.assertTrue (text is not None)
146
FreeCAD.closeDocument(doc.Name)
148
def test_import_polygon_nopath(self):
149
doc = self.utility_create_scad("polygon(points=[[0,0],[100,0],[130,50],[30,50]]);","polygon_nopath")
150
polygon = doc.getObject("polygon")
151
self.assertTrue (polygon is not None)
152
self.assertAlmostEqual (polygon.Shape.Area, 5000.0)
153
FreeCAD.closeDocument(doc.Name)
155
def test_import_polygon_path(self):
156
doc = self.utility_create_scad("polygon([[0,0],[100,0],[130,50],[30,50]], paths=[[0,1,2,3]]);","polygon_path")
157
wire = doc.ActiveObject # With paths, the polygon gets created as a wire...
158
self.assertTrue (wire is not None)
159
self.assertAlmostEqual (wire.Shape.Area, 5000.0)
160
FreeCAD.closeDocument(doc.Name)
162
def test_import_polyhedron(self):
163
doc = self.utility_create_scad(
166
points=[ [10,10,0],[10,-10,0],[-10,-10,0],[-10,10,0], // the four points at base
167
[0,0,10] ], // the apex point
168
faces=[ [0,1,4],[1,2,4],[2,3,4],[3,0,4], // each triangle side
169
[1,0,3],[2,1,3] ] // two triangles for square base
173
polyhedron = doc.ActiveObject # With paths, the polygon gets created as a wire...
174
self.assertTrue (polyhedron is not None)
175
self.assertAlmostEqual (polyhedron.Shape.Volume, 1333.3333, 4)
176
FreeCAD.closeDocument(doc.Name)
178
def test_import_difference(self):
179
doc = self.utility_create_scad("difference() { cube(15, center=true); sphere(10); }", "difference")
180
object = doc.ActiveObject
181
self.assertTrue (object is not None)
182
self.assertAlmostEqual (object.Shape.Volume, 266.1323, 3)
183
FreeCAD.closeDocument(doc.Name)
185
def test_import_intersection(self):
186
doc = self.utility_create_scad("intersection() { cube(15, center=true); sphere(10); }", "intersection")
187
object = doc.ActiveObject
188
self.assertTrue (object is not None)
189
self.assertAlmostEqual (object.Shape.Volume, 3108.8677, 3)
190
FreeCAD.closeDocument(doc.Name)
192
def test_import_union(self):
193
doc = self.utility_create_scad("union() { cube(15, center=true); sphere(10); }", "union")
194
object = doc.ActiveObject
195
self.assertTrue (object is not None)
196
self.assertAlmostEqual (object.Shape.Volume, 4454.9224, 3)
197
FreeCAD.closeDocument(doc.Name)
199
def test_import_rotate_extrude(self):
200
doc = self.utility_create_scad("rotate_extrude() translate([10, 0]) square(5);", "rotate_extrude_simple")
201
object = doc.ActiveObject
202
self.assertTrue (object is not None)
203
self.assertAlmostEqual (object.Shape.Volume, 1963.4954, 3)
204
FreeCAD.closeDocument(doc.Name)
206
doc = self.utility_create_scad("translate([0, 30, 0]) rotate_extrude() polygon( points=[[0,0],[8,4],[4,8],[4,12],[12,16],[0,20]] );", "rotate_extrude_no_hole")
207
object = doc.ActiveObject
208
self.assertTrue (object is not None)
209
self.assertAlmostEqual (object.Shape.Volume, 2412.7431, 3)
210
FreeCAD.closeDocument(doc.Name)
212
# Bug #4353 - https://tracker.freecad.org/view.php?id=4353
213
doc = self.utility_create_scad("rotate_extrude($fn=4, angle=180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_low_fn")
214
object = doc.ActiveObject
215
self.assertTrue (object is not None)
216
self.assertAlmostEqual (object.Shape.Volume, 9.0, 5)
217
FreeCAD.closeDocument(doc.Name)
219
doc = self.utility_create_scad("rotate_extrude($fn=4, angle=-180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_low_fn_negative_angle")
220
object = doc.ActiveObject
221
self.assertTrue (object is not None)
222
self.assertAlmostEqual (object.Shape.Volume, 9.0, 5)
223
FreeCAD.closeDocument(doc.Name)
225
doc = self.utility_create_scad("rotate_extrude(angle=180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_angle")
226
object = doc.ActiveObject
227
self.assertTrue (object is not None)
228
self.assertAlmostEqual (object.Shape.Volume, 4.5*math.pi, 5)
229
FreeCAD.closeDocument(doc.Name)
231
doc = self.utility_create_scad("rotate_extrude(angle=-180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_negative_angle")
232
object = doc.ActiveObject
233
self.assertTrue (object is not None)
234
self.assertAlmostEqual (object.Shape.Volume, 4.5*math.pi, 5)
235
FreeCAD.closeDocument(doc.Name)
237
def test_import_linear_extrude(self):
238
doc = self.utility_create_scad("linear_extrude(height = 20) square([20, 10], center = true);", "linear_extrude_simple")
239
object = doc.ActiveObject
240
self.assertTrue (object is not None)
241
self.assertAlmostEqual (object.Shape.Volume, 4000.000, 3)
242
FreeCAD.closeDocument(doc.Name)
244
doc = self.utility_create_scad("linear_extrude(height = 20, twist = 90) square([20, 10], center = true);", "linear_extrude_twist")
245
object = doc.ActiveObject
246
self.assertTrue (object is not None)
247
self.assertAlmostEqual (object.Shape.Volume, 4000.000, 2)
248
FreeCAD.closeDocument(doc.Name)
250
doc = self.utility_create_scad("linear_extrude(height = 20, scale = 0.2) square([20, 10], center = true);", "linear_extrude_scale")
251
object = doc.ActiveObject
252
self.assertTrue (object is not None)
256
expected_volume = (h/3) * (a1+a2+math.sqrt(a1*a2))
257
self.assertAlmostEqual (object.Shape.Volume, expected_volume, 3)
258
FreeCAD.closeDocument(doc.Name)
260
doc = self.utility_create_scad("linear_extrude(height = 20, twist = 180, scale=0.2) square([20, 10], center = true);", "linear_extrude_twist_scale")
261
object = doc.ActiveObject
262
self.assertTrue (object is not None)
263
self.assertAlmostEqual (object.Shape.Volume, expected_volume, 2)
264
FreeCAD.closeDocument(doc.Name)
266
def test_import_rotate_extrude_file(self):
267
# OpenSCAD doesn't seem to have this feature at this time (March 2021)
270
# There is a problem with the DXF code right now, it doesn't like this square.
271
# def test_import_import_dxf(self):
272
# testfile = join(self.test_dir, "Square.dxf").replace('\\','/')
273
# doc = self.utility_create_scad("import(\"{}\");".format(testfile), "import_dxf");
274
# object = doc.ActiveObject
275
# self.assertTrue (object is not None)
276
# FreeCAD.closeDocument(doc.Name)
278
def test_import_import_stl(self):
279
preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD")
280
transfer_mechanism = preferences.GetInt('transfermechanism',0)
281
if transfer_mechanism == 2:
282
print ("Cannot test STL import, communication with OpenSCAD is via pipes")
283
print ("If either OpenSCAD or FreeCAD are installed as sandboxed packages,")
284
print ("use of import is not possible.")
286
testfile = join(self.test_dir, "Cube.stl").replace('\\','/')
287
doc = self.utility_create_scad("import(\"{}\");".format(testfile), "import_stl");
288
object = doc.ActiveObject
289
self.assertTrue (object is not None)
290
FreeCAD.closeDocument(doc.Name)
292
def test_import_resize(self):
293
doc = self.utility_create_scad("resize([2,2,2]) cube();", "resize_simple")
294
object = doc.ActiveObject
295
self.assertTrue (object is not None)
296
self.assertAlmostEqual (object.Shape.Volume, 8.000000, 6)
297
FreeCAD.closeDocument(doc.Name)
299
doc = self.utility_create_scad("resize([2,2,0]) cube();", "resize_with_zero")
300
object = doc.ActiveObject
301
self.assertTrue (object is not None)
302
self.assertAlmostEqual (object.Shape.Volume, 4.000000, 6)
303
FreeCAD.closeDocument(doc.Name)
305
doc = self.utility_create_scad("resize([2,0,0], auto=true) cube();", "resize_with_auto")
306
object = doc.ActiveObject
307
self.assertTrue (object is not None)
308
self.assertAlmostEqual (object.Shape.Volume, 8.000000, 6)
309
FreeCAD.closeDocument(doc.Name)
311
doc = self.utility_create_scad("resize([2,2,2]) cube([2,2,2]);", "resize_no_change")
312
object = doc.ActiveObject
313
self.assertTrue (object is not None)
314
self.assertAlmostEqual (object.Shape.Volume, 8.000000, 6)
315
FreeCAD.closeDocument(doc.Name)
317
doc = self.utility_create_scad("resize([2,2,2]) cube([4,8,12]);", "resize_non_uniform")
318
object = doc.ActiveObject
319
self.assertTrue (object is not None)
320
self.assertAlmostEqual (object.Shape.Volume, 8.000000, 6)
321
FreeCAD.closeDocument(doc.Name)
323
# Make sure to test something that isn't just a box (where the bounding box is trivial)
324
doc = self.utility_create_scad("""
325
resize(newsize = [0,0,10], auto = [0,0,0]) {
326
sphere($fn = 96, $fa = 12, $fs = 2, r = 8.5);
327
}""", "resize_non_uniform_sphere")
328
object = doc.ActiveObject
329
self.assertTrue (object is not None)
330
object.Shape.tessellate(0.025) # To ensure the bounding box calculation is correct
331
self.assertAlmostEqual (object.Shape.BoundBox.XLength, 2*8.5, 1)
332
self.assertAlmostEqual (object.Shape.BoundBox.YLength, 2*8.5, 1)
333
self.assertAlmostEqual (object.Shape.BoundBox.ZLength, 10.0, 1)
334
FreeCAD.closeDocument(doc.Name)
336
def test_import_surface(self):
337
# Workaround for absolute vs. relative path issue
338
# Inside the OpenSCAD file an absolute path name to Surface.dat is used
339
# but by using the OpenSCAD executable to create a CSG file it's converted
340
# into a path name relative to the output filename.
341
# In order to open the CAG file correctly the cwd must be temporarily changed
342
with tempfile.TemporaryDirectory() as temp_dir:
346
testfile = join(self.test_dir, "Surface.dat").replace('\\','/')
347
doc = self.utility_create_scad(f"surface(file = \"{testfile}\", center = true, convexity = 5);", "surface_simple_dat")
348
object = doc.ActiveObject
349
self.assertTrue (object is not None)
350
self.assertAlmostEqual (object.Shape.Volume, 275.000000, 6)
351
self.assertAlmostEqual (object.Shape.BoundBox.XMin, -4.5, 6)
352
self.assertAlmostEqual (object.Shape.BoundBox.XMax, 4.5, 6)
353
self.assertAlmostEqual (object.Shape.BoundBox.YMin, -4.5, 6)
354
self.assertAlmostEqual (object.Shape.BoundBox.YMax, 4.5, 6)
355
FreeCAD.closeDocument(doc.Name)
357
testfile = join(self.test_dir, "Surface.dat").replace('\\','/')
358
doc = self.utility_create_scad(f"surface(file = \"{testfile}\", convexity = 5);", "surface_uncentered_dat")
359
object = doc.ActiveObject
360
self.assertTrue (object is not None)
361
self.assertAlmostEqual (object.Shape.Volume, 275.000000, 6)
362
self.assertAlmostEqual (object.Shape.BoundBox.XMin, 0, 6)
363
self.assertAlmostEqual (object.Shape.BoundBox.XMax, 9, 6)
364
self.assertAlmostEqual (object.Shape.BoundBox.YMin, 0, 6)
365
self.assertAlmostEqual (object.Shape.BoundBox.YMax, 9, 6)
366
FreeCAD.closeDocument(doc.Name)
368
testfile = join(self.test_dir, "Surface2.dat").replace('\\','/')
369
doc = self.utility_create_scad(f"surface(file = \"{testfile}\", center = true, convexity = 5);", "surface_rectangular_dat")
370
object = doc.ActiveObject
371
self.assertTrue (object is not None)
372
self.assertAlmostEqual (object.Shape.Volume, 24.5500000, 6)
373
self.assertAlmostEqual (object.Shape.BoundBox.XMin, -2, 6)
374
self.assertAlmostEqual (object.Shape.BoundBox.XMax, 2, 6)
375
self.assertAlmostEqual (object.Shape.BoundBox.YMin, -1.5, 6)
376
self.assertAlmostEqual (object.Shape.BoundBox.YMax, 1.5, 6)
377
FreeCAD.closeDocument(doc.Name)
380
def test_import_projection(self):
381
base_shape = "linear_extrude(height=5,center=true,twist=90,scale=0.5){square([1,1],center=true);}"
382
hole = "cube([0.25,0.25,6],center=true);"
383
cut_shape = f"difference() {{ {base_shape} {hole} }}"
385
doc = self.utility_create_scad(f"projection(cut=true) {base_shape}", "projection_slice_square")
386
object = doc.getObject("projection_cut")
387
self.assertTrue (object is not None)
388
self.assertAlmostEqual (object.Shape.Area, 0.75*0.75, 3)
389
FreeCAD.closeDocument(doc.Name)
391
doc = self.utility_create_scad(f"projection(cut=true) {cut_shape}", "projection_slice_square_with_hole")
392
object = doc.getObject("projection_cut")
393
self.assertTrue (object is not None)
394
self.assertAlmostEqual (object.Shape.Area, 0.75*0.75 - 0.25*0.25, 3)
395
FreeCAD.closeDocument(doc.Name)
397
# Unimplemented functionality:
399
# With cut=false, the twisted unit square projects to a circle of radius sqrt(0.5)
400
#doc = self.utility_create_scad(f"projection(cut=false) {base_shape}", "projection_circle")
401
#object = doc.getObject("projection")
402
#self.assertTrue (object is not None)
403
#self.assertAlmostEqual (object.Shape.Area, 2*math.pi*math.sqrt(2), 3)
404
#FreeCAD.closeDocument(doc.Name)
406
#doc = self.utility_create_scad(f"projection(cut=false) {cut_shape}", "projection_circle_with_hole")
407
#object = doc.getObject("projection")
408
#self.assertTrue (object is not None)
409
#self.assertAlmostEqual (object.Shape.Area, 2*math.pi*math.sqrt(0.5) - 0.125, 3)
410
#FreeCAD.closeDocument(doc.Name)
412
def test_import_hull(self):
415
def test_import_minkowski(self):
418
def test_import_offset(self):
421
def test_empty_union(self):
422
content = """union() {
423
color(c = [0.30, 0.50, 0.80, 0.50]) {
427
translate(v = [23.0, -9.50, 13.60]) {
444
translate(v = [2.50, 2.50, 9.50]) {
445
cylinder(h = 19.0, r = 2.50, center = true, $fn = 100);
448
translate(v = [11.50, 2.50, 9.50]) {
449
cylinder(h = 19.0, r = 2.50, center = true, $fn = 100);
452
translate(v = [11.50, 6.30, 9.50]) {
453
cylinder(h = 19.0, r = 2.50, center = true, $fn = 100);
456
translate(v = [2.50, 6.30, 9.50]) {
457
cylinder(h = 19.0, r = 2.50, center = true, $fn = 100);
460
translate(v = [2.50, 0.0, 0.0]) {
461
cube(size = [9.0, 8.80, 19.0]);
464
translate(v = [0.0, 2.50, 0.0]) {
465
cube(size = [14.0, 3.80, 19.0]);
469
translate(v = [-1.0, 8.40, 3.50]) {
470
cube(size = [30.0, 10.0, 12.0]);
473
translate(v = [4.0, 4.0, -0.10]) {
474
cylinder($fn = 30, h = 20.0, r = 1.750, r1 = 1.80);
477
translate(v = [9.0, 4.40, -0.10]) {
478
cylinder($fn = 30, h = 20.0, r = 2.150, r1 = 2.20);
481
translate(v = [12.30, 2.10, -0.10]) {
482
cube(size = [5.0, 2.20, 20.0]);
485
translate(v = [1.90, 7.60, -0.10]) {
486
cube(size = [2.20, 5.0, 20.0]);
489
translate(v = [3.60, 7.20, 1.80]) {
490
cube(size = [30.0, 10.0, 2.0]);
493
translate(v = [0, 0, 13.40]) {
494
translate(v = [3.60, 7.20, 1.80]) {
495
cube(size = [30.0, 10.0, 2.0]);
506
doc = self.utility_create_scad(content, "empty_union")
507
self.assertEqual (len(doc.RootObjects), 1)
508
FreeCAD.closeDocument(doc.Name)
510
def test_complex_fuse_no_placement(self):
511
# Issue #7878 - https://github.com/FreeCAD/FreeCAD/issues/7878
515
multmatrix([[1, 0, 0, 0], [0, 1, 0, -127], [0, 0, 1, -6], [0, 0, 0, 1]]) {
519
cube(size = [4, 106.538, 12], center = false);
521
polyhedron(points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1]], faces = [[0, 1, 2], [5, 4, 3], [3, 1, 0], [1, 3, 4], [0, 2, 3], [5, 3, 2], [4, 2, 1], [2, 4, 5]], convexity = 1);
525
polyhedron(points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1]], faces = [[0, 1, 2], [5, 4, 3], [3, 1, 0], [1, 3, 4], [0, 2, 3], [5, 3, 2], [4, 2, 1], [2, 4, 5]], convexity = 1);
526
polyhedron(points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1]], faces = [[0, 1, 2], [5, 4, 3], [3, 1, 0], [1, 3, 4], [0, 2, 3], [5, 3, 2], [4, 2, 1], [2, 4, 5]], convexity = 1);
529
multmatrix([[1, 0, 0, 6.4], [0, 1, 0, -125], [0, 0, 1, -40], [0, 0, 0, 1]]) {
531
cylinder($fn = 0, $fa = 12, $fs = 2, h = 80, r1 = 8, r2 = 8, center = false);
532
multmatrix([[1, 0, 0, -14.4], [0, 1, 0, -8], [0, 0, 1, -5], [0, 0, 0, 1]]) {
533
cube(size = [8, 16, 90], center = false);
539
doc = self.utility_create_scad(csg_data, "complex-fuse")
540
self.assertEqual (doc.RootObjects[0].Placement, FreeCAD.Placement())
541
FreeCAD.closeDocument(doc.Name)