Files
create/src/Mod/OpenSCAD/OpenSCADFeatures.py
Johannes Wüller 13caf40036 Fix CSG computing geometry too early
onChanged() triggers for the first time before all of the properties are
loaded, so it can fail to compute properly at that stage, depending on
the load order.

However, it's not necessary to compute geometry in onChanged() at all,
because that's usually supposed to happen in execute() anyway. The
solution here is to just not do onChanged() at all.

Fixes #13558
2024-04-20 23:02:49 +02:00

700 lines
25 KiB
Python

#***************************************************************************
#* Copyright (c) 2012 Sebastian Hoogen <github@sebastianhoogen.de> *
#* *
#* 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. *
#* for detail see the LICENCE text file. *
#* *
#* This program is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
__title__ = "FreeCAD OpenSCAD Workbench - Parametric Features"
__author__ = "Sebastian Hoogen"
__url__ = ["https://www.freecad.org"]
try:
long
except NameError:
long = int
'''
This Script includes python Features to represent OpenSCAD Operations
'''
class ViewProviderTree:
"A generic View Provider for Elements with Children"
def __init__(self, obj):
obj.Proxy = self
self.Object = obj.Object
def attach(self, obj):
self.Object = obj.Object
return
def updateData(self, fp, prop):
return
def getDisplayModes(self,obj):
modes=[]
return modes
def setDisplayMode(self,mode):
return mode
def onChanged(self, vp, prop):
return
def dumps(self):
# return {'ObjectName' : self.Object.Name}
return None
def loads(self,state):
if state is not None:
import FreeCAD
doc = FreeCAD.ActiveDocument #crap
self.Object = doc.getObject(state['ObjectName'])
def claimChildren(self):
objs = []
if hasattr(self.Object.Proxy,"Base"):
objs.append(self.Object.Proxy.Base)
if hasattr(self.Object,"Base"):
objs.append(self.Object.Base)
if hasattr(self.Object,"Objects"):
objs.extend(self.Object.Objects)
if hasattr(self.Object,"Components"):
objs.extend(self.Object.Components)
if hasattr(self.Object,"Children"):
objs.extend(self.Object.Children)
return objs
def getIcon(self):
import OpenSCAD_rc
if isinstance(self.Object.Proxy,RefineShape):
return(":/icons/OpenSCAD_RefineShapeFeature.svg")
if isinstance(self.Object.Proxy,IncreaseTolerance):
return(":/icons/OpenSCAD_IncreaseToleranceFeature.svg")
if isinstance(self.Object.Proxy,MatrixTransform):
return """/* XPM */
static char * matrix_xpm[] = {
"16 16 3 1",
" c #0079FF",
". c #FFFFFF",
"+ c #000000",
" ......... .",
" ............. .",
" . . . . . .",
" . . . . . .",
" ............. .",
" . . . . . .",
" . . . . . .",
" ............. .",
" . . . . . .",
" . . . . . .",
" ............. .",
" ...........+. .",
" ..+..+..+..+. .",
" ............. .",
" ......... .",
"................"};"""
else:
return """/* XPM */
static char * openscadlogo_xpm[] = {
"16 16 33 1",
" c None",
". c #61320B",
"+ c #5D420B",
"@ c #4F4C09",
"# c #564930",
"$ c #754513",
"% c #815106",
"& c #666509",
"* c #875F55",
"= c #6E7000",
"- c #756A53",
"; c #717037",
"> c #946637",
", c #92710E",
"' c #797A0A",
") c #7C7720",
"! c #8A8603",
"~ c #88886F",
"{ c #AF8181",
"] c #999908",
"^ c #BB8D8D",
"/ c #AAAA00",
"( c #A9A880",
"_ c #B5B419",
": c #C1A9A9",
"< c #B1B19A",
"[ c #BEBE00",
"} c #B9B8B4",
"| c #CACC00",
"1 c #D4D4BC",
"2 c #DBD2D0",
"3 c #EEEEED",
"4 c #FDFFFC",
"4444444444444444",
"4444443113444444",
"4444<;']]!;<^^24",
"444(&@!]]]=&#^{3",
"44<']')@++)!&*{^",
"44)]/[|//[/]'@{{",
"42=/_|||||[]!&*{",
"4(&][|||||[/'@#}",
"3-..,|||||[)&&~4",
"^*$%.!|||[!+/](4",
"^{%%%._[[_&/[_14",
":{>%%.!//])_[_44",
"2{{%%+!]!!)]]344",
"4:{{#@&=&&@#3444",
"44224}~--~}44444",
"4444444444444444"};
"""
class OpenSCADPlaceholder:
def __init__(self,obj,children=None,arguments=None):
obj.addProperty("App::PropertyLinkList",'Children','OpenSCAD',"Base Objects")
obj.addProperty("App::PropertyString",'Arguments','OpenSCAD',"Arguments")
obj.Proxy = self
if children:
obj.Children = children
if arguments:
obj.Arguments = arguments
def execute(self,fp):
import Part
fp.Shape = Part.Compound([]) #empty Shape
class Resize:
def __init__(self,obj,target,vector):
import FreeCAD
#self.Obj = obj
self.Target = target
self.Vector = vector
#obj.addProperty("App::PropertyPythonObject","Object","Resize", \
# "Object to be resized").Object = target
obj.addProperty("Part::PropertyPartShape","Shape","Resize", "Shape of the Resize")
obj.addProperty("App::PropertyVector","Vector","Resize",
" Resize Vector").Vector = FreeCAD.Vector(vector)
obj.Proxy = self
def execute(self, fp):
import FreeCAD
mat = FreeCAD.Matrix()
mat.A11 = self.Vector[0]
mat.A22 = self.Vector[1]
mat.A33 = self.Vector[2]
fp.Shape = self.Target.Shape.transformGeometry(mat)
def dumps(self):
return None
def loads(self,state):
return None
class MatrixTransform:
def __init__(self, obj,matrix=None,child=None):
obj.addProperty("App::PropertyLink","Base","Base",
"The base object that must be tranfsformed")
obj.addProperty("App::PropertyMatrix","Matrix","Matrix", "Transformation Matrix")
obj.Proxy = self
obj.Matrix = matrix
obj.Base = child
def onChanged(self, fp, prop):
"Do something when a property has changed"
pass
def updateProperty(self, fp, prop, value):
epsilon = 0.0001
if abs(getattr(fp, prop) - value) > epsilon:
setattr(fp, prop, value)
def execute(self, fp):
if fp.Matrix and fp.Base:
sh = fp.Base.Shape#.copy()
m = sh.Placement.toMatrix().multiply(fp.Matrix)
fp.Shape = sh.transformGeometry(m)
#else:
#FreeCAD.Console.PrintMessage('base %s\nmat %s/n' % (fp.Base,fp.Matrix))
class ImportObject:
def __init__(self, obj,child=None):
obj.addProperty("App::PropertyLink", "Base", "Base",
"The base object that must be tranfsformed")
obj.Proxy = self
obj.Base = child
def onChanged(self, fp, prop):
"Do something when a property has changed"
pass
def execute(self, fp):
pass
# if fp.Base:
# fp.Shape = fp.Base.Shape.copy()
class RefineShape:
'''return a refined shape'''
def __init__(self, obj, child=None):
obj.addProperty("App::PropertyLink", "Base", "Base",
"The base object that must be refined")
obj.Proxy = self
obj.Base = child
def onChanged(self, fp, prop):
"Do something when a property has changed"
pass
def execute(self, fp):
if fp.Base and fp.Base.Shape.isValid():
import OpenSCADUtils
sh = fp.Base.Shape.removeSplitter()
fp.Shape = OpenSCADUtils.applyPlacement(sh)
class IncreaseTolerance:
'''increase the tolerance of every vertex
in the current implementation its' placement is linked'''
def __init__(self,obj,child,tolerance=0):
obj.addProperty("App::PropertyLink", "Base", "Base",
"The base object that wire must be extracted")
obj.addProperty("App::PropertyDistance","Vertex","Tolerance","Vertexes tolerance (0 default)")
obj.addProperty("App::PropertyDistance","Edge","Tolerance","Edges tolerance (0 default)")
obj.addProperty("App::PropertyDistance","Face","Tolerance","Faces tolerance (0 default)")
obj.Base = child
obj.Vertex = tolerance
obj.Edge = tolerance
obj.Face = tolerance
obj.Proxy = self
def execute(self, fp):
if fp.Base:
sh=fp.Base.Shape.copy()
# Check if property Tolerance exist and preserve support for backward compatibility
if hasattr(fp, "Tolerance") and fp.Proxy.__module__ == "OpenSCADFeatures":
for vertex in sh.Vertexes:
vertex.Tolerance = max(vertex.Tolerance,fp.Tolerance.Value)
# New properties
else:
for vertex in sh.Vertexes:
vertex.Tolerance = max(vertex.Tolerance, fp.Vertex.Value)
for edge in sh.Edges:
edge.Tolerance = max(edge.Tolerance, fp.Edge.Value)
for face in sh.Faces:
face.Tolerance = max(face.Tolerance, fp.Face.Value)
fp.Shape = sh
fp.Placement = sh.Placement
class GetWire:
'''return the first wire from a given shape'''
def __init__(self, obj, child=None):
obj.addProperty("App::PropertyLink","Base","Base",
"The base object that wire must be extracted")
obj.Proxy = self
obj.Base = child
def onChanged(self, fp, prop):
"Do something when a property has changed"
pass
def execute(self, fp):
if fp.Base:
import Part
#fp.Shape=fp.Base.Shape.Wires[0]
fp.Shape=Part.Wire(fp.Base.Shape.Wires[0]) # works with 0.13 stable
#sh = fp.Base.Shape.Wires[0].copy; sh.transformSahpe(fp.Base.Shape.Placement.toMatrix()); fp.Shape = sh #untested
class Frustum:
def __init__(self, obj,r1=1,r2=2,n=3,h=4):
obj.addProperty("App::PropertyInteger","FacesNumber","Base","Number of faces")
obj.addProperty("App::PropertyDistance","Radius1","Base","Radius of lower the inscribed control circle")
obj.addProperty("App::PropertyDistance","Radius2","Base","Radius of upper the inscribed control circle")
obj.addProperty("App::PropertyDistance","Height","Base","Height of the Frustum")
obj.FacesNumber = n
obj.Radius1 = r1
obj.Radius2= r2
obj.Height= h
obj.Proxy = self
def execute(self, fp):
if all((fp.Radius1,fp.Radius2,fp.FacesNumber,fp.Height)):
import math
import FreeCAD
import Part
#from draftlibs import fcgeo
plm = fp.Placement
wires = []
faces = []
for ir,r in enumerate((fp.Radius1,fp.Radius2)):
angle = (math.pi*2)/fp.FacesNumber
pts = [FreeCAD.Vector(r.Value,0,ir*fp.Height.Value)]
for i in range(fp.FacesNumber-1):
ang = (i+1)*angle
pts.append(FreeCAD.Vector(r.Value*math.cos(ang),\
r.Value*math.sin(ang),ir*fp.Height.Value))
pts.append(pts[0])
shape = Part.makePolygon(pts)
face = Part.Face(shape)
if ir == 0: #top face
face.reverse()
wires.append(shape)
faces.append(face)
#shellperi = Part.makeRuledSurface(*wires)
shellperi = Part.makeLoft(wires)
shell = Part.Shell(shellperi.Faces+faces)
fp.Shape = Part.Solid(shell)
fp.Placement = plm
class Twist:
def __init__(self, obj, child=None, h=1.0, angle=0.0, scale=[1.0,1.0]):
import FreeCAD
obj.addProperty("App::PropertyLink","Base","Base",
"The base object that must be transformed")
obj.addProperty("App::PropertyQuantity","Angle","Base","Twist Angle")
obj.Angle = FreeCAD.Units.Angle # assign the Angle unit
obj.addProperty("App::PropertyDistance","Height","Base","Height of the Extrusion")
obj.addProperty("App::PropertyFloatList","Scale","Base","Scale to apply during the Extrusion")
obj.Base = child
obj.Angle = angle
obj.Height = h
obj.Scale = scale
obj.Proxy = self
def execute(self, fp):
import FreeCAD
import Part
import math
import sys
if fp.Base and fp.Height and fp.Base.Shape.isValid():
solids = []
for lower_face in fp.Base.Shape.Faces:
upper_face = lower_face.copy()
face_transform = FreeCAD.Matrix()
face_transform.rotateZ(math.radians(fp.Angle.Value))
face_transform.scale(fp.Scale[0], fp.Scale[1], 1.0)
face_transform.move(FreeCAD.Vector(0,0,fp.Height.Value))
upper_face.transformShape(face_transform, False, True) # True to check for non-uniform scaling
spine = Part.makePolygon([(0,0,0),(0,0,fp.Height.Value)])
if fp.Angle.Value == 0.0:
auxiliary_spine = None
else:
num_revolutions = abs(fp.Angle.Value)/360.0
pitch = fp.Height.Value / num_revolutions
height = fp.Height.Value
radius = 1.0
if fp.Angle.Value < 0.0:
left_handed = True
else:
left_handed = False
auxiliary_spine = Part.makeHelix(pitch, height, radius, 0.0, left_handed)
faces = [lower_face,upper_face]
for wire1,wire2 in zip(lower_face.Wires,upper_face.Wires):
pipe_shell = Part.BRepOffsetAPI.MakePipeShell(spine)
pipe_shell.setSpineSupport(spine)
pipe_shell.add(wire1)
pipe_shell.add(wire2)
if auxiliary_spine:
pipe_shell.setAuxiliarySpine(auxiliary_spine,True,0)
assert(pipe_shell.isReady())
pipe_shell.build()
faces.extend(pipe_shell.shape().Faces)
try:
fullshell = Part.Shell(faces)
solid=Part.Solid(fullshell)
if solid.Volume < 0:
solid.reverse()
assert(solid.Volume >= 0)
solids.append(solid)
except Part.OCCError:
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):
import FreeCAD
import Part
import math
import 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()
solids.append(solid)
except Part.OCCError:
FreeCAD.Console.PrintWarning("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 transformed")
obj.addProperty("App::PropertyDistance","Offset","Base","Offset outwards")
obj.Base = child
obj.Offset = offset
obj.Proxy = self
def execute(self, fp):
if fp.Base and fp.Offset:
fp.Shape=fp.Base.Shape.makeOffsetShape(fp.Offset.Value,1e-6)
class CGALFeature:
def __init__(self,obj,opname=None,children=None,arguments=None):
obj.addProperty("App::PropertyLinkList",'Children','OpenSCAD',"Base Objects")
obj.addProperty("App::PropertyString",'Arguments','OpenSCAD',"Arguments")
obj.addProperty("App::PropertyString",'Operation','OpenSCAD',"Operation")
obj.Proxy = self
if opname:
obj.Operation = opname
if children:
obj.Children = children
if arguments:
obj.Arguments = arguments
def execute(self,fp):
#arguments are ignored
maxmeshpoints = None #TBD: add as property
import Part
import OpenSCAD.OpenSCADUtils
shape = OpenSCAD.OpenSCADUtils.process_ObjectsViaOpenSCADShape(fp.Document,fp.Children,\
fp.Operation, maxmeshpoints=maxmeshpoints)
if shape:
fp.Shape = shape
else:
raise ValueError
def makeSurfaceVolume(filename):
import FreeCAD
import Part
import sys
coords = []
with open(filename) as f1:
min_z = sys.float_info.max
for line in f1.readlines():
sline = line.strip()
if sline and not sline.startswith('#'):
ycoord = len(coords)
lcoords = []
for xcoord, num in enumerate(sline.split()):
fnum = float(num)
lcoords.append(FreeCAD.Vector(float(xcoord),float(ycoord),fnum))
min_z = min(fnum,min_z)
coords.append(lcoords)
num_rows = len(coords)
if num_rows == 0:
FreeCAD.Console.PrintWarning(f"No data found in surface file {filename}")
return None,0,0
num_cols = len(coords[0])
# OpenSCAD does not spline this surface, so neither do we: just create a
# bunch of faces,
# using four triangles per quadrilateral
faces = []
for row in range(num_rows - 1):
for col in range(num_cols - 1):
a = coords[row + 0][col + 0]
b = coords[row + 0][col + 1]
c = coords[row + 1][col + 1]
d = coords[row + 1][col + 0]
centroid = 0.25 * (a + b + c + d)
ab = Part.makeLine(a,b)
bc = Part.makeLine(b,c)
cd = Part.makeLine(c,d)
da = Part.makeLine(d,a)
diag_a = Part.makeLine(a, centroid)
diag_b = Part.makeLine(b, centroid)
diag_c = Part.makeLine(c, centroid)
diag_d = Part.makeLine(d, centroid)
wire1 = Part.Wire([ab,diag_a,diag_b])
wire2 = Part.Wire([bc,diag_b,diag_c])
wire3 = Part.Wire([cd,diag_c,diag_d])
wire4 = Part.Wire([da,diag_d,diag_a])
try:
face = Part.Face(wire1)
faces.append(face)
face = Part.Face(wire2)
faces.append(face)
face = Part.Face(wire3)
faces.append(face)
face = Part.Face(wire4)
faces.append(face)
except Exception:
FreeCAD.Console.PrintWarning("Failed to create the face from {},{},{},{}".format(coords[row + 0][col + 0],\
coords[row + 0][col + 1],coords[row + 1][col + 1],coords[row + 1][col + 0]))
last_row = num_rows - 1
last_col = num_cols - 1
# Create the face to close off the y-min border: OpenSCAD places the lower
# surface of the shell
# at 1 unit below the lowest coordinate in the surface
lines = []
corner1 = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z - 1)
lines.append(Part.makeLine(corner1,coords[0][0]))
for col in range(num_cols - 1):
a = coords[0][col]
b = coords[0][col + 1]
lines.append(Part.makeLine(a, b))
corner2 = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z - 1)
lines.append(Part.makeLine(corner2,coords[0][last_col]))
lines.append(Part.makeLine(corner1,corner2))
wire = Part.Wire(lines)
face = Part.Face(wire)
faces.append(face)
# Create the face to close off the y-max border
lines = []
corner1 = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z - 1)
lines.append(Part.makeLine(corner1,coords[last_row][0]))
for col in range(num_cols - 1):
a = coords[last_row][col]
b = coords[last_row][col + 1]
lines.append(Part.makeLine(a, b))
corner2 = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z - 1)
lines.append(Part.makeLine(corner2,coords[last_row][last_col]))
lines.append(Part.makeLine(corner1,corner2))
wire = Part.Wire(lines)
face = Part.Face(wire)
faces.append(face)
# Create the face to close off the x-min border
lines = []
corner1 = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z - 1)
lines.append(Part.makeLine(corner1,coords[0][0]))
for row in range(num_rows - 1):
a = coords[row][0]
b = coords[row + 1][0]
lines.append(Part.makeLine(a, b))
corner2 = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z - 1)
lines.append(Part.makeLine(corner2,coords[last_row][0]))
lines.append(Part.makeLine(corner1,corner2))
wire = Part.Wire(lines)
face = Part.Face(wire)
faces.append(face)
# Create the face to close off the x-max border
lines = []
corner1 = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z - 1)
lines.append(Part.makeLine(corner1,coords[0][last_col]))
for row in range(num_rows - 1):
a = coords[row][last_col]
b = coords[row + 1][last_col]
lines.append(Part.makeLine(a, b))
corner2 = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z - 1)
lines.append(Part.makeLine(corner2,coords[last_row][last_col]))
lines.append(Part.makeLine(corner1,corner2))
wire = Part.Wire(lines)
face = Part.Face(wire)
faces.append(face)
# Create a bottom surface to close off the shell
a = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z - 1)
b = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z - 1)
c = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z - 1)
d = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z - 1)
ab = Part.makeLine(a,b)
bc = Part.makeLine(b,c)
cd = Part.makeLine(c,d)
da = Part.makeLine(d,a)
wire = Part.Wire([ab,bc,cd,da])
face = Part.Face(wire)
faces.append(face)
s = Part.Shell(faces)
solid = Part.Solid(s)
return solid,last_col,last_row