Skip to content

Commit

Permalink
[OpenSCAD] Add $fn and angle to rotate_extrude()
Browse files Browse the repository at this point in the history
As pointed out in Issue FreeCAD#4353 the OpenSCAD Workbench does not
correctly implement the angle parameter to rotate_extrude (it's a
relatively recent addition to OpenSCAD), nor does it attempt to do
anything with a specified $fn. This commit adds both features. To add
$fn handling, the code from the cylinder extrusion was mimicked,
allowing FreeCAD to create perfect, smooth representations when $fn
exceeds a user specified value in Preferences, but attempting to create
OpenSCAD's more discrete representation when using a lower $fn. Note
that this determination is made at creation time: if the user later
increases the segments parameter in the new object, it will remain
prismatic regardless of how high the value is made.
  • Loading branch information
chennes authored and wwmayer committed Mar 28, 2021
1 parent 07804a8 commit 4e58747
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 10 deletions.
90 changes: 86 additions & 4 deletions src/Mod/OpenSCAD/OpenSCADFeatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ def createGeometry(self,fp):
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")

Expand Down Expand Up @@ -455,10 +455,93 @@ def createGeometry(self,fp):
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
Expand All @@ -469,7 +552,6 @@ def execute(self, fp):
self.createGeometry(fp)

def onChanged(self, fp, prop):
pass
if prop in ["Offset"]:
self.createGeometry(fp)

Expand Down
27 changes: 26 additions & 1 deletion src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,36 @@ def test_import_rotate_extrude(self):
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")
Expand Down
35 changes: 30 additions & 5 deletions src/Mod/OpenSCAD/importCSG.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down

0 comments on commit 4e58747

Please sign in to comment.