diff --git a/src/Mod/OpenSCAD/OpenSCADFeatures.py b/src/Mod/OpenSCAD/OpenSCADFeatures.py index dc3cd8ad76..a8cf3a5506 100644 --- a/src/Mod/OpenSCAD/OpenSCADFeatures.py +++ b/src/Mod/OpenSCAD/OpenSCADFeatures.py @@ -389,8 +389,8 @@ class Frustum: class Twist: def __init__(self, obj,child=None,h=1.0,angle=0.0,scale=[1.0,1.0]): obj.addProperty("App::PropertyLink","Base","Base", - "The base object that must be tranfsformed") - obj.addProperty("App::PropertyAngle","Angle","Base","Twist angle") #degree or rad + "The base object that must be transformed") + obj.addProperty("App::PropertyAngle","Angle","Base","Twist Angle in degrees") #degree or rad obj.addProperty("App::PropertyDistance","Height","Base","Height of the Extrusion") obj.addProperty("App::PropertyFloatList","Scale","Base","Scale to apply during the Extrusion") @@ -455,10 +455,93 @@ class Twist: solids.append(Part.Compound(faces)) fp.Shape=Part.Compound(solids) + + +class PrismaticToroid: + def __init__(self, obj,child=None,angle=360.0,n=3): + obj.addProperty("App::PropertyLink","Base","Base", + "The 2D face that will be swept") + obj.addProperty("App::PropertyAngle","Angle","Base","Angle to sweep through") + obj.addProperty("App::PropertyInteger","Segments","Base","Number of segments per 360° (OpenSCAD's \"$fn\")") + + obj.Base = child + obj.Angle = angle + obj.Segments = n + obj.Proxy = self + + def execute(self, fp): + self.createGeometry(fp) + + def onChanged(self, fp, prop): + if prop in ["Angle","Segments"]: + self.createGeometry(fp) + + def createGeometry(self,fp): + import FreeCAD,Part,math,sys + if fp.Base and fp.Angle and fp.Segments and fp.Base.Shape.isValid(): + solids = [] + min_sweep_angle_per_segment = 360.0 / fp.Segments # This is how OpenSCAD defines $fn + num_segments = math.floor(abs(fp.Angle) / min_sweep_angle_per_segment) + num_ribs = num_segments + 1 + sweep_angle_per_segment = fp.Angle / num_segments # Always >= min_sweep_angle_per_segment + + # From the OpenSCAD documentation: + # The 2D shape must lie completely on either the right (recommended) or the left side of the Y-axis. + # More precisely speaking, every vertex of the shape must have either x >= 0 or x <= 0. If the shape + # spans the X axis a warning appears in the console windows and the rotate_extrude() is ignored. If + # the 2D shape touches the Y axis, i.e. at x=0, it must be a line that touches, not a point. + + for start_face in fp.Base.Shape.Faces: + ribs = [] + end_face = start_face + for rib in range(num_ribs): + angle = rib * sweep_angle_per_segment + intermediate_face = start_face.copy() + face_transform = FreeCAD.Matrix() + face_transform.rotateY (math.radians (angle)) + intermediate_face.transformShape (face_transform) + if rib == num_ribs-1: + end_face = intermediate_face + + edges = [] + for edge in intermediate_face.OuterWire.Edges: + if edge.BoundBox.XMin != 0.0 or edge.BoundBox.XMax != 0.0: + edges.append(edge) + + ribs.append(Part.Wire(edges)) + + faces = [] + shell = Part.makeShellFromWires (ribs) + for face in shell.Faces: + faces.append(face) + + if abs(fp.Angle) < 360.0 and faces: + if fp.Angle > 0: + faces.append(start_face.reversed()) # Reversed so the normal faces out of the shell + faces.append(end_face) + else: + faces.append(start_face) + faces.append(end_face.reversed()) # Reversed so the normal faces out of the shell + + try: + shell = Part.makeShell(faces) + shell.sewShape() + shell.fix(1e-7,1e-7,1e-7) + clean_shell = shell.removeSplitter() + solid = Part.makeSolid (clean_shell) + if solid.Volume < 0: + solid.reverse() + print (f"Solid volume is {solid.Volume}") + solids.append(solid) + except Part.OCCError: + print ("Could not create solid: creating compound instead") + solids.append(Part.Compound(faces)) + fp.Shape = Part.Compound(solids) + class OffsetShape: def __init__(self, obj,child=None,offset=1.0): obj.addProperty("App::PropertyLink","Base","Base", - "The base object that must be tranfsformed") + "The base object that must be transformed") obj.addProperty("App::PropertyDistance","Offset","Base","Offset outwards") obj.Base = child @@ -469,7 +552,6 @@ class OffsetShape: self.createGeometry(fp) def onChanged(self, fp, prop): - pass if prop in ["Offset"]: self.createGeometry(fp) diff --git a/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py b/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py index fbbbd564ee..d7ac8afd48 100644 --- a/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py +++ b/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py @@ -232,11 +232,36 @@ polyhedron( self.assertAlmostEqual (object.Shape.Volume, 1963.4954, 3) FreeCAD.closeDocument(doc.Name) - doc = self.utility_create_scad("translate([0, 30, 0]) rotate_extrude($fn = 80) polygon( points=[[0,0],[8,4],[4,8],[4,12],[12,16],[0,20]] );", "rotate_extrude_no_hole") + 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") object = doc.ActiveObject self.assertTrue (object is not None) self.assertAlmostEqual (object.Shape.Volume, 2412.7431, 3) FreeCAD.closeDocument(doc.Name) + + # Bug #4353 - https://tracker.freecadweb.org/view.php?id=4353 + doc = self.utility_create_scad("rotate_extrude($fn=4, angle=180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_low_fn") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 9.0, 5) + FreeCAD.closeDocument(doc.Name) + + doc = self.utility_create_scad("rotate_extrude($fn=4, angle=-180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_low_fn_negative_angle") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 9.0, 5) + FreeCAD.closeDocument(doc.Name) + + doc = self.utility_create_scad("rotate_extrude(angle=180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_angle") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 4.5*math.pi, 5) + FreeCAD.closeDocument(doc.Name) + + doc = self.utility_create_scad("rotate_extrude(angle=-180) polygon([[0,0],[3,3],[0,3]]);", "rotate_extrude_negative_angle") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 4.5*math.pi, 5) + FreeCAD.closeDocument(doc.Name) def test_import_linear_extrude(self): doc = self.utility_create_scad("linear_extrude(height = 20) square([20, 10], center = true);", "linear_extrude_simple") diff --git a/src/Mod/OpenSCAD/importCSG.py b/src/Mod/OpenSCAD/importCSG.py index 1e35cd25a0..1b52fe0a6e 100644 --- a/src/Mod/OpenSCAD/importCSG.py +++ b/src/Mod/OpenSCAD/importCSG.py @@ -164,7 +164,7 @@ def processcsg(filename): # Build the parser if printverbose: print('Load Parser') # No debug out otherwise Linux has protection exception - parser = yacc.yacc(debug=0) + parser = yacc.yacc(debug=False) if printverbose: print('Parser Loaded') # Give the lexer some input #f=open('test.scad', 'r') @@ -667,7 +667,7 @@ def p_intersection_action(p): p[0] = [mycommon] if printverbose: print("End Intersection") -def process_rotate_extrude(obj): +def process_rotate_extrude(obj, angle): newobj=doc.addObject("Part::FeaturePython",'RefineRotateExtrude') RefineShape(newobj,obj) if gui: @@ -682,20 +682,45 @@ def process_rotate_extrude(obj): myrev.Source = newobj myrev.Axis = (0.00,1.00,0.00) myrev.Base = (0.00,0.00,0.00) - myrev.Angle = 360.00 + myrev.Angle = angle myrev.Placement=FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Rotation(0,0,90)) if gui: newobj.ViewObject.hide() return(myrev) +def process_rotate_extrude_prism(obj, angle, n): + newobj=doc.addObject("Part::FeaturePython",'PrismaticToroid') + PrismaticToroid(newobj, obj, angle, n) + newobj.Placement=FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Rotation(0,0,90)) + if gui: + if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\ + GetBool('useViewProviderTree'): + from OpenSCADFeatures import ViewProviderTree + ViewProviderTree(newobj.ViewObject) + else: + newobj.ViewObject.Proxy = 0 + obj.ViewObject.hide() + return(newobj) + def p_rotate_extrude_action(p): 'rotate_extrude_action : rotate_extrude LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE' - if printverbose: print("Rotate Extrude") + if printverbose: print("Rotate Extrude") + angle = 360.0 + if 'angle' in p[3]: + angle = float(p[3]['angle']) + n = int(round(float(p[3]['$fn']))) + fnmax = FreeCAD.ParamGet(\ + "User parameter:BaseApp/Preferences/Mod/OpenSCAD").\ + GetInt('useMaxFN', 16) if (len(p[6]) > 1) : part = fuse(p[6],"Rotate Extrude Union") else : part = p[6][0] - p[0] = [process_rotate_extrude(part)] + + if n < 3 or fnmax != 0 and n > fnmax: + p[0] = [process_rotate_extrude(part,angle)] + else: + p[0] = [process_rotate_extrude_prism(part,angle,n)] if printverbose: print("End Rotate Extrude") def p_rotate_extrude_file(p):