3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU Lesser General Public License (LGPL)
5
# as published by the Free Software Foundation; either version 2 of
6
# the License, or (at your option) any later version.
12
from OpenSCADFeatures import *
13
from OpenSCAD2Dgeom import *
14
from OpenSCADUtils import *
15
from builtins import open as pyopen
20
def openscadmesh(doc, scadstr, objname):
25
tmpfilename = OpenSCADUtils.callopenscadstring(scadstr,'stl')
27
#mesh1 = doc.getObject(objname) #reuse imported object
28
Mesh.insert(tmpfilename)
29
os.unlink(tmpfilename)
30
mesh1 = doc.getObject(objname) #blog
31
mesh1.ViewObject.hide()
33
sh.makeShapeFromMesh(mesh1.Mesh.Topology, 0.1)
34
solid = Part.Solid(sh)
35
obj = doc.addObject("Part::FeaturePython", objname)
36
ImportObject(obj, mesh1) #This object is not mutable from the GUI
37
ViewProviderTree(obj.ViewObject)
38
solid = solid.removeSplitter()
41
obj.Shape = solid#.removeSplitter()
48
#fnmin = 12 # maximal fn for implicit polygon rendering
49
fnmin = FreeCAD.ParamGet(\
50
"User parameter:BaseApp/Preferences/Mod/OpenSCAD").GetInt('useMaxFN')
51
planedim = 1e10 #size of the square used as x-y-plane
53
def __init__(self, name, arguments=None, children=None,):
56
self.arguments = arguments or {}
57
self.children = children or []
60
str1 = 'Node(name=%s' % self.name
62
str1 += ',arguments=%s' % self.arguments
64
str1 += ',children=%s' % self.children
67
def __nonzero__(self):
68
'''A Node is not obsolete if doesn't have children.
69
Only if as neither name children or arguments'''
70
return bool(self.name or self.arguments or self.children)
73
'''return the number of children'''
74
return len(self.children)
76
def __getitem__(self, key):
77
'''direct access to the children'''
78
return self.children.__getitem__(key)
80
def rlen(self, checkmultmarix=False):
81
'''Total number of nodes'''
83
return 1+sum([ch.rlen() for ch in self.children])
87
def addtofreecad(self,doc=None,fcpar=None):
88
def center(obj,x,y,z):
89
obj.Placement = FreeCAD.Placement(\
90
FreeCAD.Vector(-x/2.0,-y/2.0,-z/2.0),\
91
FreeCAD.Rotation(0,0,0,1))
96
doc = FreeCAD.newDocument()
98
namel = self.name.lower()
99
multifeature={'union':"Part::MultiFuse",'imp_union':"Part::MultiFuse",
100
'intersection':"Part::MultiCommon"}
101
if namel in multifeature:
102
if len(self.children)>1:
103
obj = doc.addObject(multifeature[namel],namel)
104
subobjs = [child.addtofreecad(doc,obj) for child in self.children]
106
for subobj in subobjs:
107
subobj.ViewObject.hide()
108
elif len(self.children) == 1:
109
obj = self.children[0].addtofreecad(doc,fcpar or True)
112
elif namel == 'difference':
113
if len(self.children) == 1:
114
obj = self.children[0].addtofreecad(doc,fcpar or True)
116
obj = doc.addObject("Part::Cut",namel)
117
base = self.children[0].addtofreecad(doc,obj)
119
if len(self.children) == 2:
120
tool = self.children[1].addtofreecad(doc,obj)
122
tool = Node(name='imp_union',\
123
children=self.children[1:]).addtofreecad(doc,obj)
126
base.ViewObject.hide()
127
tool.ViewObject.hide()
128
elif namel == 'cube':
129
obj = doc.addObject('Part::Box', namel)
130
x,y,z = self.arguments['size']
134
if self.arguments['center']:
136
elif namel == 'sphere':
137
obj = doc.addObject("Part::Sphere", namel)
138
obj.Radius = self.arguments['r']
139
elif namel == 'cylinder':
140
h = self.arguments['h']
141
r1, r2 = self.arguments['r1'], self.arguments['r2']
142
if '$fn' in self.arguments and self.arguments['$fn'] > 2 \
143
and self.arguments['$fn']<=Node.fnmin: # polygonal
144
if r1 == r2: # prismatic
145
obj = doc.addObject("Part::Prism","prism")
146
obj.Polygon = int(self.arguments['$fn'])
147
obj.Circumradius = r1
149
if self.arguments['center']:
151
#base.ViewObject.hide()
152
elif False: #use Frustum Feature with makeRuledSurface
153
obj = doc.addObject("Part::FeaturePython",'frustum')
154
Frustum(obj,r1,r2,int(self.arguments['$fn']), h)
155
ViewProviderTree(obj.ViewObject)
156
if self.arguments['center']:
158
else: #Use Part::Loft and GetWire Feature
159
obj = doc.addObject('Part::Loft', 'frustum')
161
p1 = Draft.makePolygon(int(self.arguments['$fn']), r1)
162
p2 = Draft.makePolygon(int(self.arguments['$fn']), r2)
163
if self.arguments['center']:
164
p1.Placement = FreeCAD.Placement(\
165
FreeCAD.Vector(0.0,0.0,-h/2.0),FreeCAD.Rotation())
166
p2.Placement = FreeCAD.Placement(\
167
FreeCAD.Vector(0.0,0.0,h/2.0), FreeCAD.Rotation())
169
p2.Placement = FreeCAD.Placement(\
170
FreeCAD.Vector(0.0,0.0,h),FreeCAD.Rotation())
171
w1 = doc.addObject("Part::FeaturePython",'polygonwire1')
172
w2 = doc.addObject("Part::FeaturePython",'polygonwire2')
175
ViewProviderTree(w1.ViewObject)
176
ViewProviderTree(w2.ViewObject)
177
obj.Sections = [w1,w2]
186
obj=doc.addObject("Part::Cylinder",namel)
190
obj=doc.addObject("Part::Cone",'cone')
192
obj.Radius1, obj.Radius2 = r1, r2
193
if self.arguments['center']:
195
elif namel == 'polyhedron':
196
obj = doc.addObject("Part::Feature",namel)
197
points = self.arguments['points']
198
faces = self.arguments['triangles']
199
shell = Part.Shell([Part.Face(Part.makePolygon(\
200
[tuple(points[pointindex]) for pointindex in \
201
(face+face[0:1])])) for face in faces])
202
# obj.Shape=Part.Solid(shell).removeSplitter()
203
solid=Part.Solid(shell).removeSplitter()
207
obj.Shape=solid#.removeSplitter()
209
elif namel == 'polygon':
210
obj = doc.addObject("Part::Feature", namel)
211
points = self.arguments['points']
212
paths = self.arguments.get('paths')
214
faces = [Part.Face(Part.makePolygon([(x,y,0) for x,y in points+points[0:1]]))]
216
faces = [Part.Face(Part.makePolygon([(points[pointindex][0],points[pointindex][1],0) for \
217
pointindex in (path+path[0:1])])) for path in paths]
218
obj.Shape=subtractfaces(faces)
219
elif namel == 'square':
220
obj = doc.addObject("Part::Plane",namel)
221
x,y = self.arguments['size']
224
if self.arguments['center']:
226
elif namel == 'circle':
227
r = self.arguments['r']
229
if '$fn' in self.arguments and self.arguments['$fn'] != 0 \
230
and self.arguments['$fn']<=Node.fnmin:
231
obj = Draft.makePolygon(int(self.arguments['$fn']),r)
233
obj = Draft.makeCircle(r) # create a Face
234
#obj = doc.addObject("Part::Circle",namel);obj.Radius = r
235
elif namel == 'color':
236
if len(self.children) == 1:
237
obj = self.children[0].addtofreecad(doc,fcpar or True)
239
obj = Node(name='imp_union',\
240
children=self.children).addtofreecad(doc,fcpar or True)
241
obj.ViewObject.ShapeColor = tuple([float(p) for p in self.arguments[:3]]) #RGB
242
transp = 100 - int(math.floor(100*self.arguments[3])) #Alpha
243
obj.ViewObject.Transparency = transp
244
elif namel == 'multmatrix':
245
assert(len(self.children)>0)
246
m1l = [round(f,12) for f in sum(self.arguments,[])] #That's the original matrix
247
m1 = FreeCAD.Matrix(*tuple(m1l)) #That's the original matrix
248
if isspecialorthogonalpython(fcsubmatrix(m1)): #a Placement can represent the transformation
249
if len(self.children) == 1:
250
obj = self.children[0].addtofreecad(doc,fcpar or True)
252
obj = Node(name='imp_union',\
253
children = self.children).addtofreecad(doc,fcpar or True)
254
#FreeCAD.Console.PrintMessage('obj %s\nmat %s/n' % (obj.Placement,m1))
255
obj.Placement=FreeCAD.Placement(m1).multiply(obj.Placement)
256
else: #we need to apply the matrix transformation to the Shape using a custom PythonFeature
257
obj = doc.addObject("Part::FeaturePython",namel)
258
if len(self.children) == 1:
259
child = self.children[0].addtofreecad(doc,obj)
261
child = Node(name='imp_union',\
262
children=self.children).addtofreecad(doc,obj)
263
MatrixTransform(obj,m1,child) #This object is not mutable from the GUI
264
ViewProviderTree(obj.ViewObject)
265
#elif namel == 'import': pass #Custom Feature
266
elif namel == 'linear_extrude':
267
height = self.arguments['height']
268
twist = self.arguments.get('twist')
270
obj = doc.addObject("Part::Extrusion",namel)
272
obj = doc.addObject("Part::FeaturePython",'twist_extrude')
273
if len(self.children)==0:
274
base = Node('import',self.arguments).addtofreecad(doc,obj)
275
elif len(self.children)==1:
276
base = self.children[0].addtofreecad(doc,obj)
278
base = Node(name='imp_union',\
279
children=self.children).addtofreecad(doc,obj)
280
if False and base.isDerivedFrom('Part::MultiFuse'):
281
#does not solve all the problems
282
newobj=doc.addObject("Part::FeaturePython",'refine')
283
RefineShape(newobj,base)
284
ViewProviderTree(newobj.ViewObject)
285
base.ViewObject.hide()
289
obj.Dir = (0,0,height)
291
Twist(obj,base,height,-twist)
292
ViewProviderTree(obj.ViewObject)
293
if self.arguments['center']:
294
center(obj,0,0,height)
295
base.ViewObject.hide()
297
elif namel == 'rotate_extrude':
298
obj = doc.addObject("Part::Revolution",namel)
299
if len(self.children)==0:
300
base = Node('import',self.arguments).addtofreecad(doc,obj)
301
elif len(self.children)==1:
302
base = self.children[0].addtofreecad(doc,obj)
304
base = Node(name='imp_union',\
305
children=self.children).addtofreecad(doc,obj)
306
if False and base.isDerivedFrom('Part::MultiFuse'):
307
#creates 'Axe and meridian are confused' Errors
308
newobj=doc.addObject("Part::FeaturePython",'refine')
309
RefineShape(newobj,base)
310
ViewProviderTree(newobj.ViewObject)
311
base.ViewObject.hide()
314
obj.Axis = (0.00,1.00,0.00)
315
obj.Base = (0.00,0.00,0.00)
317
base.ViewObject.hide()
318
obj.Placement=FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Rotation(0,0,90))
319
elif namel == 'projection':
320
if self.arguments['cut']:
321
planename = 'xy_plane_used_for_project_cut'
322
obj = doc.addObject('Part::MultiCommon','projection_cut')
323
plane = doc.getObject(planename)
325
plane=doc.addObject("Part::Plane",planename)
326
plane.Length=Node.planedim*2
327
plane.Width=Node.planedim*2
328
plane.Placement = FreeCAD.Placement(FreeCAD.Vector(\
329
-Node.planedim,-Node.planedim,0),FreeCAD.Rotation(0,0,0,1))
330
#plane.ViewObject.hide()
331
subobjs = [child.addtofreecad(doc,obj) for child in self.children]
332
subobjs.append(plane)
334
for subobj in subobjs:
335
subobj.ViewObject.hide()
337
#Do a proper projection
338
raise(NotImplementedError)
339
elif namel == 'import':
340
filename = self.arguments.get('file')
341
scale = self.arguments.get('scale')
342
origin = self.arguments.get('origin')
345
docname = os.path.split(filename)[1]
346
objname,extension = docname.split('.',1)
347
if not os.path.isabs(filename):
349
global lastimportpath
350
filename = os.path.join(lastimportpath,filename)
351
except: raise #no path given
352
# Check for a mesh fileformat support by the Mesh mddule
353
if extension.lower() in reverseimporttypes()['Mesh']:
355
mesh1 = doc.getObject(objname) #reuse imported object
357
Mesh.insert(filename)
358
mesh1 = doc.getObject(objname)
359
mesh1.ViewObject.hide()
361
sh.makeShapeFromMesh(mesh1.Mesh.Topology,0.1)
362
solid = Part.Solid(sh)
363
obj = doc.addObject("Part::FeaturePython",'import_%s_%s'%(extension,objname))
364
#obj=doc.addObject('Part::Feature',)
365
ImportObject(obj,mesh1) #This object is not mutable from the GUI
366
ViewProviderTree(obj.ViewObject)
367
solid = solid.removeSplitter()
372
obj.Shape = solid#.removeSplitter()
373
elif extension in ['dxf']:
374
layera = self.arguments.get('layer')
375
featname ='import_dxf_%s_%s'%(objname,layera)
376
# reusing an already imported object does not work if the
377
# shape in not yet calculated
380
layers = dxfcache.get(id(doc),[])
382
groupobj = [go for go in layers if (not layera) or go.Label == layera]
387
layers = importDXF.processdxf(doc,filename) or importDXF.layers
388
dxfcache[id(doc)] = layers[:]
393
groupobj = [go for go in layers if (not layera) or go.Label == layera]
395
for shapeobj in groupobj[0].Group:
396
edges.extend(shapeobj.Shape.Edges)
398
f = edgestofaces(edges)
399
except Part.OCCError:
400
FreeCAD.Console.PrintError('processing of dxf import failed\nPlease rework \'%s\' manually\n' % layera)
401
f = Part.Shape() #empty Shape
402
obj = doc.addObject("Part::FeaturePython",'import_dxf_%s_%s'%(objname,layera))
403
#obj=doc.addObject('Part::Feature',)
404
ImportObject(obj,groupobj[0]) #This object is not mutable from the GUI
405
ViewProviderTree(obj.ViewObject)
409
FreeCAD.Console.ErrorMessage('Filetype of %s not supported\n' % (filename))
410
raise(NotImplementedError)
411
if obj: #handle origin and scale
412
if scale is not None and scale !=1:
413
if origin is not None and any([c != 0 for c in origin]):
414
raise(NotImplementedError)# order of transformations unknown
416
m1 = FreeCAD.Matrix()
417
m1.scale(scale,scale,scale)
418
obj = doc.addObject("Part::FeaturePython",'scale_import')
419
MatrixTransform(obj,m1,child) #This object is not mutable from the GUI
420
ViewProviderTree(obj.ViewObject)
421
elif origin is not None and any([c != 0 for c in origin]):
422
placement = FreeCAD.Placement(FreeCAD.Vector(*[-c for c in origin]),FreeCAD.Rotation())
423
obj.Placement = placement.multiply(obj.Placement)
425
FreeCAD.Console.ErrorMessage('Import of %s failed\n' % (filename))
428
elif namel == 'minkowski':
429
childrennames = [child.name.lower() for child in self.children]
430
if len(self.children) == 2 and \
431
childrennames.count('cube') == 1 and \
432
(childrennames.count('sphere') + \
433
childrennames.count('cylinder')) == 1:
434
if self.children[0].name.lower() == 'cube':
435
cube = self.children[0]
436
roundobj = self.children[1]
437
elif self.children[1].name.lower() == 'cube':
438
cube = self.children[1]
439
roundobj = self.children[0]
440
roundobjname = roundobj.name.lower()
441
issphere = roundobjname == 'sphere'
442
cubeobj = doc.addObject('Part::Box','roundedcube')
443
x,y,z = cube.arguments['size']
444
r = roundobj.arguments.get('r') or \
445
roundobj.arguments.get('r1')
446
cubeobj.Length = x+2*r
447
cubeobj.Width = y+2*r
448
cubeobj.Height = z+2*r*issphere
449
obj = doc.addObject("Part::Fillet","%s_%s"%(namel,roundobjname))
451
cubeobj.ViewObject.hide()
453
obj.Edges = [(i,r,r) for i in range(1,13)]
455
obj.Edges = [(i,r,r) for i in [1,3,5,7]]
456
if cube.arguments['center']:
457
center(cubeobj,x+2*r,y+2*r,z+2*r*issphere)
458
else: #htandle a rotated cylinder
460
raise(NotImplementedError)
461
elif childrennames.count('sphere') == 1:
462
sphereindex = childrennames.index('sphere')
463
sphere = self.children[sphereindex]
464
offset = sphere.arguments['r']
465
nonsphere = self.children[0:sphereindex]+\
466
self.sphere[sphereindex+1:]
467
obj = doc.addObject("Part::FeaturePython",'Offset')
468
if len(nonsphere) == 1:
469
child = nonsphere[0].addtofreecad(doc,obj)
471
child = Node(name='imp_union',\
472
children=nonsphere).addtofreecad(doc,obj)
473
OffsetShape(obj,child,offset)
474
ViewProviderTree(obj.ViewObject)
476
raise(NotImplementedError)
477
pass # handle rotated cylinders and select edges that
478
#radius = radius0 * m1.multiply(FreeCAD.Vector(0,0,1)).dot(edge.Curve.tangent(0)[0])
480
raise(NotImplementedError)
481
elif namel == 'surface':
482
obj = doc.addObject("Part::Feature",namel) #include filename?
483
obj.Shape,xoff,yoff=makeSurfaceVolume(self.arguments['file'])
484
if self.arguments['center']:
485
center(obj,xoff,yoff,0.0)
488
#scadstr = 'surface(file = "%s", center = %s );' % \
489
# (self.arguments['file'], 'true' if self.arguments['center'] else 'false')
490
#docname=os.path.split(self.arguments['file'])[1]
491
#objname,extension = docname.split('.',1)
492
#obj = openscadmesh(doc,scadstr,objname)
494
elif namel in ['glide','hull']:
495
raise(NotImplementedError)
496
elif namel in ['render','subdiv'] or True:
497
lenchld = len(self.children)
499
FreeCAD.Console.PrintMessage('Not recognized %s\n' % (self))
500
obj = self.children[0].addtofreecad(doc,fcpar)
502
obj = Node(name='imp_union',\
503
children=self.children).addtofreecad(doc,fcpar or True)
505
obj = doc.addObject("Part::Feature",'Not_Impl_%s'%namel)
506
if fcpar == True: #We are the last real object, our parent is not rendered.
511
obj.ViewObject.hide()
513
if True: #never refine the Shape, as it itroduces crashes
517
if obj.Type =='Part::Extrusion' and obj.Base.Type == 'Part::Part2DObjectPython' and \
518
isinstance(obj.Base.Proxy,Draft._Polygon) or \
519
(not obj.isDerivedFrom('Part::Extrusion') and \
520
not obj.isDerivedFrom('Part::Boolean') and \
521
not obj.isDerivedFrom('Part::Cut') and \
522
not obj.isDerivedFrom('Part::MultiCommon') and \
523
not obj.isDerivedFrom('Part::MultiFuse') and \
524
not obj.isDerivedFrom('Part::Revolution') ) \
525
or (obj.isDerivedFrom('Part::FeaturePython') and isinstance(obj.Proxy,RefineShape)):
528
newobj = doc.addObject("Part::FeaturePython",'refine')
529
RefineShape(newobj,obj)
530
ViewProviderTree(newobj.ViewObject)
531
obj.ViewObject.hide()
537
def flattengroups(self,name='group'):
538
"""removes group node with only one child and no arguments and empty groups"""
540
while (node.name == name and len(node.children) == 1 and len(node.arguments) == 0):
541
node = node.children[0]
542
node.children = [child for child in node.children if not (len(child.children) == 0 and child.name == name)]
544
node.children = [child.flattengroups() for child in node.children]
547
def pprint(self,level=0):
548
"""prints the indented tree"""
550
argstr = ' (%s)' % self.arguments
553
print('%s %s%s' %(' '*level,self.name,argstr))
554
for child in self.children:
555
child.pprint(level+1)
557
def pprint2(self,path='root',pathjust=24):
558
"""prints the tree. Left column contains the syntax to access a child"""
560
argstr = ' (%s)' % self.arguments
563
print('%s %s%s' %(path.ljust(pathjust),self.name,argstr))
564
for i,child in enumerate(self.children):
565
child.pprint2('%s[%d]'%(path,i),pathjust)
570
def parseexpression(e):
573
if len(el) == 0: return None
574
if el == 'true': return True
575
elif el == 'false': return False
576
elif el == 'undef': return None
577
elif e[0].isdigit() or e[0] == '-' and len(e)>1 and e[1].isdigit():
582
FreeCAD.Console.PrintMessage('%s\n' % (el))
585
elif el.startswith('"'): return e.strip('"') #string literal
586
elif el.startswith('['):
587
bopen, bclose = e.count('['), e.count(']')
592
FreeCAD.Console.PrintMessage('%s\n' % (el))
594
#assert(False) #Malformed
596
return e #Return the string
598
def parseargs(argstring):
603
for i,char in enumerate(argstring):
604
if char == '[': level += 1
605
elif char ==']': level -= 1
606
if level == 0 and (char == '=' or char == ','):
607
tok.append(''.join(a).strip())
611
tok.append(''.join(a).strip())
613
argdict = dict(zip(tok[0::2],[parseexpression(argstring) for argstring in tok[1::2]]))
615
# for key, value in re.findall(r"(\$?\w+)\s*=\s*(\[?\w+]?),?\s*",argstring):
616
# argdict[key] = parseexpression(value)
619
return parseexpression(argstring)
622
name, str2 = str1.strip().split('(',1)
623
assert('}' not in name)
624
name = name.strip('#!%* ')#remove/ignore modifiers
625
args, str3 = str2.split(')',1)
627
if str4.startswith(';'):
629
nextelement = str4[1:].lstrip()
630
return Node(name,parseargs(args)),nextelement
631
elif str4.startswith('{'):
634
for index,char in enumerate(str4):
635
if char == '{': level += 1
636
elif char == '}': level -= 1
640
childstr = str4[1:index].strip()
641
nextelement = str4[index+1:].lstrip()
642
bopen,bclose = childstr.count('{'),childstr.count('}')
643
assert(bopen == bclose)
647
childnode,childstr = parsenode(childstr)
648
children.append(childnode)
652
args = parseargs(args)
653
return Node(name,args,children),nextelement
655
def readfile(filename):
657
global lastimportpath
658
lastimportpath,relname = os.path.split(filename)
659
isopenscad = relname.lower().endswith('.scad')
661
tmpfile=callopenscad(filename)
662
if OpenSCADUtils.workaroundforissue128needed():
663
lastimportpath = os.getcwd() #https://github.com/openscad/openscad/issues/128
667
rootnode = parsenode(f.read())[0]
669
if isopenscad and tmpfile:
674
return rootnode.flattengroups()
678
docname = os.path.split(filename)[1]
679
doc = FreeCAD.newDocument(docname)
680
doc.Label = (docname.split('.',1)[0])
681
readfile(filename).addtofreecad(doc)
685
def insert(filename,docname):
687
doc = FreeCAD.getDocument(docname)
689
doc = FreeCAD.newDocument(docname)
690
readfile(filename).addtofreecad(doc)