#!/usr/bin/env python # SPDX-License-Identifier: LGPL-2.1-or-later # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License (LGPL) # as published by the Free Software Foundation; either version 2 of # the License, or (at your option) any later version. import FreeCAD import math import re from OpenSCADFeatures import * from OpenSCAD2Dgeom import * from OpenSCADUtils import * from builtins import open as pyopen def openscadmesh(doc, scadstr, objname): import Part import Mesh import os import OpenSCADUtils tmpfilename = OpenSCADUtils.callopenscadstring(scadstr,'stl') if tmpfilename: Mesh.insert(tmpfilename) os.unlink(tmpfilename) mesh1 = doc.getObject(objname) #blog mesh1.ViewObject.hide() sh=Part.Shape() sh.makeShapeFromMesh(mesh1.Mesh.Topology, 0.1) solid = Part.Solid(sh) obj = doc.addObject("Part::FeaturePython", objname) ImportObject(obj, mesh1) #This object is not mutable from the GUI ViewProviderTree(obj.ViewObject) solid = solid.removeSplitter() if solid.Volume < 0: solid.complement() obj.Shape = solid return obj else: print(scadstr) class Node: #fnmin = 12 # maximal fn for implicit polygon rendering fnmin = FreeCAD.ParamGet(\ "User parameter:BaseApp/Preferences/Mod/OpenSCAD").GetInt('useMaxFN') planedim = 1e10 #size of the square used as x-y-plane def __init__(self, name, arguments=None, children=None,): pass self.name = name self.arguments = arguments or {} self.children = children or [] def __repr__(self): str1 = 'Node(name=%s' % self.name if self.arguments: str1 += ',arguments=%s' % self.arguments if self.children: str1 += ',children=%s' % self.children return str1+')' def __nonzero__(self): '''A Node is not obsolete if doesn't have children. Only if as neither name children or arguments''' return bool(self.name or self.arguments or self.children) def __len__(self): '''return the number of children''' return len(self.children) def __getitem__(self, key): '''direct access to the children''' return self.children.__getitem__(key) def rlen(self, checkmultmarix=False): '''Total number of nodes''' if self.children: return 1+sum([ch.rlen() for ch in self.children]) else: return 1 def addtofreecad(self,doc=None,fcpar=None): def center(obj,x,y,z): obj.Placement = FreeCAD.Placement(\ FreeCAD.Vector(-x/2.0,-y/2.0,-z/2.0),\ FreeCAD.Rotation(0,0,0,1)) import FreeCAD import Part if not doc: doc = FreeCAD.newDocument() obj = None namel = self.name.lower() multifeature={'union':"Part::MultiFuse",'imp_union':"Part::MultiFuse", 'intersection':"Part::MultiCommon"} if namel in multifeature: if len(self.children)>1: obj = doc.addObject(multifeature[namel],namel) subobjs = [child.addtofreecad(doc,obj) for child in self.children] obj.Shapes = subobjs for subobj in subobjs: subobj.ViewObject.hide() elif len(self.children) == 1: obj = self.children[0].addtofreecad(doc,fcpar or True) else: obj = fcpar elif namel == 'difference': if len(self.children) == 1: obj = self.children[0].addtofreecad(doc,fcpar or True) else: obj = doc.addObject("Part::Cut",namel) base = self.children[0].addtofreecad(doc,obj) if len(self.children) == 2: tool = self.children[1].addtofreecad(doc,obj) else: tool = Node(name='imp_union',\ children=self.children[1:]).addtofreecad(doc,obj) obj.Base = base obj.Tool = tool base.ViewObject.hide() tool.ViewObject.hide() elif namel == 'cube': obj = doc.addObject('Part::Box', namel) x,y,z = self.arguments['size'] obj.Length = x obj.Width = y obj.Height = z if self.arguments['center']: center(obj,x,y,z) elif namel == 'sphere': obj = doc.addObject("Part::Sphere", namel) obj.Radius = self.arguments['r'] elif namel == 'cylinder': h = self.arguments['h'] r1, r2 = self.arguments['r1'], self.arguments['r2'] if '$fn' in self.arguments and self.arguments['$fn'] > 2 \ and self.arguments['$fn']<=Node.fnmin: # polygonal if r1 == r2: # prismatic obj = doc.addObject("Part::Prism","prism") obj.Polygon = int(self.arguments['$fn']) obj.Circumradius = r1 obj.Height = h if self.arguments['center']: center(obj,0,0,h) elif False: #use Frustum Feature with makeRuledSurface obj = doc.addObject("Part::FeaturePython",'frustum') Frustum(obj,r1,r2,int(self.arguments['$fn']), h) ViewProviderTree(obj.ViewObject) if self.arguments['center']: center(obj,0,0,h) else: #Use Part::Loft and GetWire Feature obj = doc.addObject('Part::Loft', 'frustum') import Draft p1 = Draft.makePolygon(int(self.arguments['$fn']), r1) p2 = Draft.makePolygon(int(self.arguments['$fn']), r2) if self.arguments['center']: p1.Placement = FreeCAD.Placement(\ FreeCAD.Vector(0.0,0.0,-h/2.0),FreeCAD.Rotation()) p2.Placement = FreeCAD.Placement(\ FreeCAD.Vector(0.0,0.0,h/2.0), FreeCAD.Rotation()) else: p2.Placement = FreeCAD.Placement(\ FreeCAD.Vector(0.0,0.0,h),FreeCAD.Rotation()) w1 = doc.addObject("Part::FeaturePython",'polygonwire1') w2 = doc.addObject("Part::FeaturePython",'polygonwire2') GetWire(w1,p1) GetWire(w2,p2) ViewProviderTree(w1.ViewObject) ViewProviderTree(w2.ViewObject) obj.Sections = [w1,w2] obj.Solid = True obj.Ruled = True p1.ViewObject.hide() p2.ViewObject.hide() w1.ViewObject.hide() w2.ViewObject.hide() else: if r1 == r2: obj=doc.addObject("Part::Cylinder",namel) obj.Height = h obj.Radius = r1 else: obj=doc.addObject("Part::Cone",'cone') obj.Height = h obj.Radius1, obj.Radius2 = r1, r2 if self.arguments['center']: center(obj,0,0,h) elif namel == 'polyhedron': obj = doc.addObject("Part::Feature",namel) points = self.arguments['points'] faces = self.arguments['triangles'] shell = Part.Shell([Part.Face(Part.makePolygon(\ [tuple(points[pointindex]) for pointindex in \ (face+face[0:1])])) for face in faces]) # obj.Shape=Part.Solid(shell).removeSplitter() solid=Part.Solid(shell).removeSplitter() if solid.Volume < 0: # solid.complement() solid.reverse() obj.Shape=solid#.removeSplitter() elif namel == 'polygon': obj = doc.addObject("Part::Feature", namel) points = self.arguments['points'] paths = self.arguments.get('paths') if not paths: faces = [Part.Face(Part.makePolygon([(x,y,0) for x,y in points+points[0:1]]))] else: faces = [Part.Face(Part.makePolygon([(points[pointindex][0],points[pointindex][1],0) for \ pointindex in (path+path[0:1])])) for path in paths] obj.Shape=subtractfaces(faces) elif namel == 'square': obj = doc.addObject("Part::Plane",namel) x,y = self.arguments['size'] obj.Length = x obj.Width = y if self.arguments['center']: center(obj,x,y,0) elif namel == 'circle': r = self.arguments['r'] import Draft if '$fn' in self.arguments and self.arguments['$fn'] != 0 \ and self.arguments['$fn']<=Node.fnmin: obj = Draft.makePolygon(int(self.arguments['$fn']),r) else: obj = Draft.makeCircle(r) # create a Face #obj = doc.addObject("Part::Circle",namel);obj.Radius = r elif namel == 'color': if len(self.children) == 1: obj = self.children[0].addtofreecad(doc,fcpar or True) else: obj = Node(name='imp_union',\ children=self.children).addtofreecad(doc,fcpar or True) obj.ViewObject.ShapeColor = tuple([float(p) for p in self.arguments[:3]]) #RGB transp = 100 - int(math.floor(100*self.arguments[3])) #Alpha obj.ViewObject.Transparency = transp elif namel == 'multmatrix': assert(len(self.children)>0) m1l = [round(f,12) for f in sum(self.arguments,[])] #That's the original matrix m1 = FreeCAD.Matrix(*tuple(m1l)) #That's the original matrix if isspecialorthogonalpython(fcsubmatrix(m1)): #a Placement can represent the transformation if len(self.children) == 1: obj = self.children[0].addtofreecad(doc,fcpar or True) else: obj = Node(name='imp_union',\ children = self.children).addtofreecad(doc,fcpar or True) #FreeCAD.Console.PrintMessage('obj %s\nmat %s/n' % (obj.Placement,m1)) obj.Placement=FreeCAD.Placement(m1).multiply(obj.Placement) else: #we need to apply the matrix transformation to the Shape using a custom PythonFeature obj = doc.addObject("Part::FeaturePython",namel) if len(self.children) == 1: child = self.children[0].addtofreecad(doc,obj) else: child = Node(name='imp_union',\ children=self.children).addtofreecad(doc,obj) MatrixTransform(obj,m1,child) #This object is not mutable from the GUI ViewProviderTree(obj.ViewObject) #elif namel == 'import': pass #Custom Feature elif namel == 'linear_extrude': height = self.arguments['height'] twist = self.arguments.get('twist') if not twist: obj = doc.addObject("Part::Extrusion",namel) else: #twist obj = doc.addObject("Part::FeaturePython",'twist_extrude') if len(self.children)==0: base = Node('import',self.arguments).addtofreecad(doc,obj) elif len(self.children)==1: base = self.children[0].addtofreecad(doc,obj) else: base = Node(name='imp_union',\ children=self.children).addtofreecad(doc,obj) if False and base.isDerivedFrom('Part::MultiFuse'): #does not solve all the problems newobj=doc.addObject("Part::FeaturePython",'refine') RefineShape(newobj,base) ViewProviderTree(newobj.ViewObject) base.ViewObject.hide() base=newobj if not twist: obj.Base = base obj.Dir = (0,0,height) else: #twist Twist(obj,base,height,-twist) ViewProviderTree(obj.ViewObject) if self.arguments['center']: center(obj,0,0,height) base.ViewObject.hide() elif namel == 'rotate_extrude': obj = doc.addObject("Part::Revolution",namel) if len(self.children)==0: base = Node('import',self.arguments).addtofreecad(doc,obj) elif len(self.children)==1: base = self.children[0].addtofreecad(doc,obj) else: base = Node(name='imp_union',\ children=self.children).addtofreecad(doc,obj) if False and base.isDerivedFrom('Part::MultiFuse'): #creates 'Axe and meridian are confused' Errors newobj=doc.addObject("Part::FeaturePython",'refine') RefineShape(newobj,base) ViewProviderTree(newobj.ViewObject) base.ViewObject.hide() base=newobj obj.Source= base obj.Axis = (0.00,1.00,0.00) obj.Base = (0.00,0.00,0.00) obj.Angle = 360.00 base.ViewObject.hide() obj.Placement=FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Rotation(0,0,90)) elif namel == 'projection': if self.arguments['cut']: planename = 'xy_plane_used_for_project_cut' obj = doc.addObject('Part::MultiCommon','projection_cut') plane = doc.getObject(planename) if not plane: plane=doc.addObject("Part::Plane",planename) plane.Length=Node.planedim*2 plane.Width=Node.planedim*2 plane.Placement = FreeCAD.Placement(FreeCAD.Vector(\ -Node.planedim,-Node.planedim,0),FreeCAD.Rotation(0,0,0,1)) #plane.ViewObject.hide() subobjs = [child.addtofreecad(doc,obj) for child in self.children] subobjs.append(plane) obj.Shapes = subobjs for subobj in subobjs: subobj.ViewObject.hide() else: #Do a proper projection raise(NotImplementedError) elif namel == 'import': filename = self.arguments.get('file') scale = self.arguments.get('scale') origin = self.arguments.get('origin') if filename: import os docname = os.path.split(filename)[1] objname,extension = docname.split('.',1) if not os.path.isabs(filename): try: global lastimportpath filename = os.path.join(lastimportpath,filename) except: raise #no path given # Check for a mesh fileformat support by the Mesh mddule if extension.lower() in reverseimporttypes()['Mesh']: import Mesh mesh1 = doc.getObject(objname) #reuse imported object if not mesh1: Mesh.insert(filename) mesh1 = doc.getObject(objname) mesh1.ViewObject.hide() sh = Part.Shape() sh.makeShapeFromMesh(mesh1.Mesh.Topology,0.1) solid = Part.Solid(sh) obj = doc.addObject("Part::FeaturePython",'import_%s_%s'%(extension,objname)) ImportObject(obj,mesh1) #This object is not mutable from the GUI ViewProviderTree(obj.ViewObject) solid = solid.removeSplitter() if solid.Volume < 0: #sh.reverse() #sh = sh.copy() solid.complement() obj.Shape = solid#.removeSplitter() elif extension in ['dxf']: layera = self.arguments.get('layer') featname ='import_dxf_%s_%s'%(objname,layera) # reusing an already imported object does not work if the # shape in not yet calculated import importDXF global dxfcache layers = dxfcache.get(id(doc),[]) if layers: groupobj = [go for go in layers if (not layera) or go.Label == layera] else: groupobj = None if not groupobj: groupname = objname layers = importDXF.processdxf(doc,filename) or importDXF.layers dxfcache[id(doc)] = layers[:] for l in layers: for o in l.Group: o.ViewObject.hide() l.ViewObject.hide() groupobj = [go for go in layers if (not layera) or go.Label == layera] edges = [] for shapeobj in groupobj[0].Group: edges.extend(shapeobj.Shape.Edges) try: f = edgestofaces(edges) except Part.OCCError: FreeCAD.Console.PrintError('processing of dxf import failed\nRework \'%s\' manually\n' % layera) f = Part.Shape() #empty Shape obj = doc.addObject("Part::FeaturePython",'import_dxf_%s_%s'%(objname,layera)) ImportObject(obj,groupobj[0]) #This object is not mutable from the GUI ViewProviderTree(obj.ViewObject) obj.Shape=f else: FreeCAD.Console.ErrorMessage('Filetype of %s not supported\n' % (filename)) raise(NotImplementedError) if obj: #handle origin and scale if scale is not None and scale !=1: if origin is not None and any([c != 0 for c in origin]): raise(NotImplementedError)# order of transformations unknown child = obj m1 = FreeCAD.Matrix() m1.scale(scale,scale,scale) obj = doc.addObject("Part::FeaturePython",'scale_import') MatrixTransform(obj,m1,child) #This object is not mutable from the GUI ViewProviderTree(obj.ViewObject) elif origin is not None and any([c != 0 for c in origin]): placement = FreeCAD.Placement(FreeCAD.Vector(*[-c for c in origin]),FreeCAD.Rotation()) obj.Placement = placement.multiply(obj.Placement) else: FreeCAD.Console.ErrorMessage('Import of %s failed\n' % (filename)) elif namel == 'minkowski': childrennames = [child.name.lower() for child in self.children] if len(self.children) == 2 and \ childrennames.count('cube') == 1 and \ (childrennames.count('sphere') + \ childrennames.count('cylinder')) == 1: if self.children[0].name.lower() == 'cube': cube = self.children[0] roundobj = self.children[1] elif self.children[1].name.lower() == 'cube': cube = self.children[1] roundobj = self.children[0] roundobjname = roundobj.name.lower() issphere = roundobjname == 'sphere' cubeobj = doc.addObject('Part::Box','roundedcube') x,y,z = cube.arguments['size'] r = roundobj.arguments.get('r') or \ roundobj.arguments.get('r1') cubeobj.Length = x+2*r cubeobj.Width = y+2*r cubeobj.Height = z+2*r*issphere obj = doc.addObject("Part::Fillet","%s_%s"%(namel,roundobjname)) obj.Base = cubeobj cubeobj.ViewObject.hide() if issphere: obj.Edges = [(i,r,r) for i in range(1,13)] else:#cylinder obj.Edges = [(i,r,r) for i in [1,3,5,7]] if cube.arguments['center']: center(cubeobj,x+2*r,y+2*r,z+2*r*issphere) else: #htandle a rotated cylinder #OffsetShape raise(NotImplementedError) elif childrennames.count('sphere') == 1: sphereindex = childrennames.index('sphere') sphere = self.children[sphereindex] offset = sphere.arguments['r'] nonsphere = self.children[0:sphereindex]+\ self.sphere[sphereindex+1:] obj = doc.addObject("Part::FeaturePython",'Offset') if len(nonsphere) == 1: child = nonsphere[0].addtofreecad(doc,obj) else: child = Node(name='imp_union',\ children=nonsphere).addtofreecad(doc,obj) OffsetShape(obj,child,offset) ViewProviderTree(obj.ViewObject) elif False: raise(NotImplementedError) pass # handle rotated cylinders and select edges that #radius = radius0 * m1.multiply(FreeCAD.Vector(0,0,1)).dot(edge.Curve.tangent(0)[0]) else: raise(NotImplementedError) elif namel == 'surface': obj = doc.addObject("Part::Feature",namel) #include filename? obj.Shape,xoff,yoff=makeSurfaceVolume(self.arguments['file']) if self.arguments['center']: center(obj,xoff,yoff,0.0) return obj elif namel in ['glide','hull']: raise(NotImplementedError) elif namel in ['render','subdiv'] or True: lenchld = len(self.children) if lenchld == 1: FreeCAD.Console.PrintMessage('Not recognized %s\n' % (self)) obj = self.children[0].addtofreecad(doc,fcpar) elif lenchld >1: obj = Node(name='imp_union',\ children=self.children).addtofreecad(doc,fcpar or True) else: obj = doc.addObject("Part::Feature",'Not_Impl_%s'%namel) if fcpar == True: #We are the last real object, our parent is not rendered. return obj if fcpar: try: obj.ViewObject.hide() except: raise if True: #never refine the Shape, as it itroduces crashes return obj else: #refine Shape import Draft if obj.Type =='Part::Extrusion' and obj.Base.Type == 'Part::Part2DObjectPython' and \ isinstance(obj.Base.Proxy,Draft._Polygon) or \ (not obj.isDerivedFrom('Part::Extrusion') and \ not obj.isDerivedFrom('Part::Boolean') and \ not obj.isDerivedFrom('Part::Cut') and \ not obj.isDerivedFrom('Part::MultiCommon') and \ not obj.isDerivedFrom('Part::MultiFuse') and \ not obj.isDerivedFrom('Part::Revolution') ) \ or (obj.isDerivedFrom('Part::FeaturePython') and isinstance(obj.Proxy,RefineShape)): return obj else: newobj = doc.addObject("Part::FeaturePython",'refine') RefineShape(newobj,obj) ViewProviderTree(newobj.ViewObject) obj.ViewObject.hide() return newobj else: doc.recompute() def flattengroups(self,name='group'): """removes group node with only one child and no arguments and empty groups""" node = self while (node.name == name and len(node.children) == 1 and len(node.arguments) == 0): node = node.children[0] node.children = [child for child in node.children if not (len(child.children) == 0 and child.name == name)] if node.children: node.children = [child.flattengroups() for child in node.children] return node def pprint(self,level=0): """prints the indented tree""" if self.arguments: argstr = ' (%s)' % self.arguments else: argstr = '' print('%s %s%s' %(' '*level,self.name,argstr)) for child in self.children: child.pprint(level+1) def pprint2(self,path='root',pathjust=24): """prints the tree. Left column contains the syntax to access a child""" if self.arguments: argstr = ' (%s)' % self.arguments else: argstr = '' print('%s %s%s' %(path.ljust(pathjust),self.name,argstr)) for i,child in enumerate(self.children): child.pprint2('%s[%d]'%(path,i),pathjust) def parseexpression(e): e = e.strip() el = e.lower() if len(el) == 0: return None if el == 'true': return True elif el == 'false': return False elif el == 'undef': return None elif e[0].isdigit() or e[0] == '-' and len(e)>1 and e[1].isdigit(): try: return float(e) except ValueError: import FreeCAD FreeCAD.Console.PrintMessage('%s\n' % (el)) return 1.0 elif el.startswith('"'): return e.strip('"') #string literal elif el.startswith('['): bopen, bclose = e.count('['), e.count(']') if bopen == bclose: return eval(el) else: import FreeCAD FreeCAD.Console.PrintMessage('%s\n' % (el)) else: return e #Return the string def parseargs(argstring): if '=' in argstring: level = 0 tok = [] a = [] for i,char in enumerate(argstring): if char == '[': level += 1 elif char ==']': level -= 1 if level == 0 and (char == '=' or char == ','): tok.append(''.join(a).strip()) a= [] else: a.append(char) tok.append(''.join(a).strip()) #print(tok) argdict = dict(zip(tok[0::2],[parseexpression(argstring) for argstring in tok[1::2]])) return argdict else: return parseexpression(argstring) def parsenode(str1): name, str2 = str1.strip().split('(',1) assert('}' not in name) name = name.strip('#!%* ')#remove/ignore modifiers args, str3 = str2.split(')',1) str4 = str3.lstrip() if str4.startswith(';'): #has no children nextelement = str4[1:].lstrip() return Node(name,parseargs(args)),nextelement elif str4.startswith('{'): #has children level = 0 for index,char in enumerate(str4): if char == '{': level += 1 elif char == '}': level -= 1 if level == 0: break #end of children childstr = str4[1:index].strip() nextelement = str4[index+1:].lstrip() bopen,bclose = childstr.count('{'),childstr.count('}') assert(bopen == bclose) children= [] while childstr: try: childnode,childstr = parsenode(childstr) children.append(childnode) except ValueError: raise if args: args = parseargs(args) return Node(name,args,children),nextelement def readfile(filename): import os global lastimportpath lastimportpath,relname = os.path.split(filename) isopenscad = relname.lower().endswith('.scad') if isopenscad: tmpfile=callopenscad(filename) if OpenSCADUtils.workaroundforissue128needed(): lastimportpath = os.getcwd() #https://github.com/openscad/openscad/issues/128 f = pyopen(tmpfile) else: f = pyopen(filename) rootnode = parsenode(f.read())[0] f.close() if isopenscad and tmpfile: try: os.unlink(tmpfile) except OSError: pass return rootnode.flattengroups() def open(filename): import os docname = os.path.split(filename)[1] doc = FreeCAD.newDocument(docname) doc.Label = (docname.split('.',1)[0]) readfile(filename).addtofreecad(doc) #doc.recompute() return doc def insert(filename,docname): try: doc = FreeCAD.getDocument(docname) except NameError: doc = FreeCAD.newDocument(docname) readfile(filename).addtofreecad(doc) global dxfcache dxfcache = {}