I create a Macro to convert a 3D solid object to a LEGO model using 7 basic bricks.
I just do it to learn freecad, it is not optimum at all, many other projects are better if you really need such a converter.
Slice can takes time --> do not use a too high number as slice number (20 is a maximum...)
Here is a test on Tux and the code :
Code: Select all
import math
from FreeCAD import Base
import Draft
import Part
from PySide import QtGui, QtCore
import time
def errorDialog(msg):
diag = QtGui.QMessageBox(QtGui.QMessageBox.Critical, 'Error in macro Solid to LEGO', msg)
diag.setWindowModality(QtCore.Qt.ApplicationModal)
diag.exec_()
raise(Exception(msg))
isDebug=False
s=Gui.Selection.getSelectionEx()
if len(s)<1:
msg = 'Please select a solid to convert to LEGO'
errorDialog(msg)
s=s[0]
if not hasattr(s.Object,'Shape'):
msg = 'Please select a Solid (not a mesh) to convert to LEGO'
errorDialog(msg)
shape=s.Object.Shape
rangeX=shape.BoundBox.XMax-shape.BoundBox.XMin
rangeY=shape.BoundBox.YMax-shape.BoundBox.YMin
rangeZ=shape.BoundBox.ZMax-shape.BoundBox.ZMin
if rangeX==0 or rangeY==0 or rangeZ==0 :
msg = 'Please select a 3D object to convert to LEGO'
errorDialog(msg)
FreeCADGui.ActiveDocument.getObject(s.Object.Name).Visibility=False
class Config:
def __init__(self,sliceZ,shape):
self.sliceZ=sliceZ
self.legoWidth=8
self.legoHeight=9.6
self.legoCylHeight=1.7
self.legoCylRadius=2.4
self.rangeX=shape.BoundBox.XMax-shape.BoundBox.XMin
self.rangeY=shape.BoundBox.YMax-shape.BoundBox.YMin
self.rangeZ=shape.BoundBox.ZMax-shape.BoundBox.ZMin
self.scale=self.rangeZ/(self.sliceZ*self.legoHeight)
self.scaledLegoHeight=self.rangeZ/self.sliceZ #=self.scale*self.legoHeight
self.scaledLegoWidth=self.scale*self.legoWidth
self.xMin=shape.BoundBox.XMin-self.scaledLegoWidth
self.yMin=shape.BoundBox.YMin-self.scaledLegoWidth
self.sliceX=math.ceil(self.rangeX/self.scaledLegoWidth)+2
self.sliceY=math.ceil(self.rangeY/self.scaledLegoWidth)+2
def __repr__(self):
return "Config: '{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}'".format(self.sliceZ,self.rangeX,id(self), self.rangeY,self.rangeZ,self.scale,self.scaledLegoWidth)
config=None
class Piece:
def __init__(self, i,j,x, y,z,value,groupName):
self.i=i
self.j=j
self.x = x
self.y = y
self.z = z
self.value=value
self.groupe=groupName
self.pieceObject = None
self.shape = None
def __repr__(self):
return "Piece: '{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}'".format(self.i,self.j,id(self), self.x,self.y,self.z,self.value)
def drawValue(self):
Gui.activateWorkbench("DraftWorkbench")
text = Draft.makeText([str(self.value)],point=FreeCAD.Vector(self.x+0.25*config.scaledLegoWidth,self.y+0.25*config.scaledLegoWidth,self.z+1.01*config.scaledLegoHeight))
text.ViewObject.FontSize = 0.5*config.scaledLegoWidth
App.ActiveDocument.getObject(self.groupe).addObject(text)
def next(self):
try:
return pieces[self.i+1][self.j]
except:
return None
def upper(self):
try:
return pieces[self.i][self.j+1]
except:
return None
def getNewValue(self):
result=self.value
if self.value>0:
next=self.next()
if not(next is None) and next.value==1:
result=2
next=next.next()
if not(next is None) and next.value==1:
result=3
next=next.next()
if not(next is None) and next.value==1:
result=4
return result
def combineWithValue(self,otherValue):
result=otherValue
newValue=self.getNewValue()
if otherValue>0 and newValue>0:
result=5
if otherValue>1 and newValue>1:
result=6
if otherValue>2 and newValue>2:
result=7
if otherValue>3 and newValue>3:
result=8
return result
def resetOthers(self,newValue):
valueMod4=newValue%4
if valueMod4==0:
valueMod4=4
if valueMod4>1:
next=self.next()
next.value=0
if valueMod4>2:
next=next.next()
next.value=0
if valueMod4>3:
next=next.next()
next.value=0
if newValue>4:
upper=self.upper()
upper.value=0
if newValue>5:
next=upper.next()
next.value=0
if newValue>6:
next=next.next()
next.value=0
if newValue>7:
next=next.next()
next.value=0
def draw(self, isHorizontalLayer):
base = FreeCAD.Vector(self.x,self.y,self.z-config.scaledLegoHeight/2)
height=1
length=self.value%4
if length==0:
length=4
if self.value>4:
height=2
if isHorizontalLayer:
box=Part.makeBox(length*config.scaledLegoWidth,height*config.scaledLegoWidth,config.scaledLegoHeight,base)
base = FreeCAD.Vector(self.x+config.scaledLegoWidth/2,self.y+config.scaledLegoWidth/2,self.z+config.scaledLegoHeight/2)
union=box
for l in range(height):
for k in range(length):
cyl=Part.makeCylinder(config.legoCylRadius*config.scale,config.legoCylHeight*config.scale,base)
union=union.fuse(cyl)
base.x+=config.scaledLegoWidth
base.x=self.x+config.scaledLegoWidth/2
base.y+=config.scaledLegoWidth
else:
box=Part.makeBox(height*config.scaledLegoWidth,length*config.scaledLegoWidth,config.scaledLegoHeight,base)
base = FreeCAD.Vector(self.x+config.scaledLegoWidth/2,self.y+config.scaledLegoWidth/2,self.z+config.scaledLegoHeight/2)
union=box
for l in range(height):
for k in range(length):
cyl=Part.makeCylinder(config.legoCylRadius*config.scale,config.legoCylHeight*config.scale,base)
union=union.fuse(cyl)
base.y+=config.scaledLegoWidth
base.x+=config.scaledLegoWidth
base.y=self.y+config.scaledLegoWidth/2
self.shape=union
if isDebug:
self.pieceObject = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pieceBase' if self.groupe=='GroupeBase' else 'piece')
self.pieceObject.Shape = self.shape
App.ActiveDocument.getObject(self.groupe).addObject(self.pieceObject)
def drawFromBase(self, isHorizontalLayer):
#Copy object
self.pieceObject=App.ActiveDocument.addObject('Part::Feature','piece')
self.pieceObject.Shape=basePieces[self.value-1][isHorizontalLayer].shape
base = FreeCAD.Vector(self.x,self.y,self.z)
self.pieceObject.Placement = App.Placement(base,App.Rotation(App.Vector(0,0,1),0))
App.ActiveDocument.getObject(self.groupe).addObject(self.pieceObject)
def my_range(start, end, step):
while start <= end:
yield start
start += step
def get_ecart(nb):
div=nb/config.scaledLegoWidth
div_rounded=round(div)
return abs(div-div_rounded)
def getFaceI(face):
div=(face.BoundBox.XMin-config.xMin)/config.scaledLegoWidth
return round(div)
def getFaceJ(face):
div=(face.BoundBox.YMin-config.yMin)/config.scaledLegoWidth
return round(div)
def setPieceValue(face,value,isHorizontalLayer):
if isHorizontalLayer:
i=getFaceI(face)
j=getFaceJ(face)
else:
i=getFaceJ(face)
j=getFaceI(face)
pieces[i][j].value=value
def delete(objectName):
Gui.Selection.clearSelection()
a=FreeCADGui.ActiveDocument.getObject(objectName)
Gui.Selection.addSelection(a.Object)
import OpenSCADUtils
OpenSCADUtils.removesubtree(Gui.Selection.getSelection())
def startStep():
if isDebug:
return time.time();
def stopStep(start,msg):
if isDebug:
print("--- %s %s seconds ---" % (msg, time.time() - start))
def slice(shape, z,isHorizontalLayer):
if isDebug:
print ("z=",z)
start_time = startStep()
wires=list()
for i in shape.slice(Base.Vector(0,0,1),z):
wires.append(i)
if len(wires)<1:
print("error on z=%s no wire from slice" % z)
return
comp=Part.Compound(wires)
stopStep(start_time,"step1")
start_time = startStep()
slice=App.ActiveDocument.addObject("Part::Feature","comp")
slice.Shape=comp
slice.purgeTouched()
#Shape2DView
shape2D = Draft.makeShape2DView(slice)
stopStep(start_time,"step2")
start_time = startStep()
#Sketcher
sketch = Draft.makeSketch(shape2D)
sketch.Placement.Base.z=z
stopStep(start_time,"step3")
start_time = startStep()
f = App.ActiveDocument.addObject('Part::Extrusion', 'ExtrudeSketch')
f.Base = sketch
f.DirMode = "Normal"
f.DirLink = None
f.LengthFwd = 1.000000000000000
f.LengthRev = 1.000000000000000
f.Solid = True
f.Reversed = False
f.Symmetric = False
f.TaperAngle = 0.000000000000000
f.TaperAngleRev = 0.000000000000000
stopStep(start_time,"step4")
start_time = startStep()
App.activeDocument().recompute()
Gui.activateWorkbench("PartWorkbench")
stopStep(start_time,"step5")
start_time = startStep()
w=Part.Solid(f.Shape)
if isDebug:
Part.show(w)
stopStep(start_time,"step6")
start_time = startStep()
#Make array
pl = FreeCAD.Placement()
pl.Rotation.Q = (0.0,0.0,0.0,1.0)
pl.Base = FreeCAD.Vector(config.xMin,config.yMin,z)
rec = Draft.makeRectangle(length=config.scaledLegoWidth,height=config.scaledLegoWidth,placement=pl,face=True,support=None)
obj = Draft.makeArray(rec,FreeCAD.Vector(config.scaledLegoWidth,0,0),FreeCAD.Vector(0,config.scaledLegoWidth,0),config.sliceX,config.sliceY)
stopStep(start_time,"step7")
start_time = startStep()
#Make intersection
obj1=Part.Shell(obj.Shape.Faces)
g=w.common(obj1)
stopStep(start_time,"step8")
start_time = startStep()
if isDebug:
Part.show(g)
else:
delete(obj.Name)
delete(slice.Name)
delete(shape2D.Name)
delete(sketch.Name)
delete(f.Name)
stopStep(start_time,"step9")
start_time = startStep()
for f in g.Faces:
if f.Area>0.5*config.scaledLegoWidth*config.scaledLegoWidth:
#face is ok
setPieceValue(f,1,isHorizontalLayer)
stopStep(start_time,"step10")
start_time = startStep()
def getPiece(i,j,isHorizontalLayer):
if isHorizontalLayer:
return pieces[i][j]
else:
return pieces[j][i]
basePieces = None
def initBasePieces():
if isDebug:
group=App.ActiveDocument.addObject("App::DocumentObjectGroup","GroupeBase")
global basePieces
basePieces = [[Piece(0,0,0,0,0,i+1,"GroupeBase") for j in range(2)] for i in range(8)]
isHorizontalLayer=True
for i in range(8):
basePiece=basePieces[i][isHorizontalLayer]
basePiece.draw(isHorizontalLayer)
isHorizontalLayer=False
for i in range(8):
basePiece=basePieces[i][isHorizontalLayer]
basePiece.draw(isHorizontalLayer)
def drawAll(isHorizontalLayer):
for j in range(config.sliceY):
for i in range(config.sliceX):
piece=getPiece(i,j,isHorizontalLayer)
if (piece.value > 0):
piece.draw(isHorizontalLayer)
def drawAllFromBase(isHorizontalLayer):
for j in range(config.sliceY):
for i in range(config.sliceX):
piece=getPiece(i,j,isHorizontalLayer)
if (piece.value > 0):
piece.drawFromBase(isHorizontalLayer)
pieces=None
def scanIt(w):
# horizontal layer:
# 5 6 7 8
# 1 2 3 4
#vertical layer:
#4 8
#3 7
#2 6
#1 5
isHorizontalLayer = True
sliceZ=int(w.anz.text())
global config
config=Config(sliceZ,shape)
initBasePieces()
w.progressbar.setMinimum(shape.BoundBox.ZMin+config.scaledLegoHeight/2)
w.progressbar.setMaximum(shape.BoundBox.ZMax-config.scaledLegoHeight/2)
modelGroup=App.ActiveDocument.addObject("App::DocumentObjectGroup","Model")
cpt=1
if isDebug:
for z in my_range(shape.BoundBox.ZMin+config.scaledLegoHeight/2, shape.BoundBox.ZMax, config.scaledLegoHeight):
print(z)
# z1=[4.740459585189811]
# for z in z1:
for z in my_range(shape.BoundBox.ZMin+config.scaledLegoHeight/2, shape.BoundBox.ZMax, config.scaledLegoHeight):
start_time = startStep()
group=App.ActiveDocument.addObject("App::DocumentObjectGroup","Slice"+str(cpt))
cpt+=1
App.ActiveDocument.getObject(modelGroup.Name).addObject(group)
global pieces
if isHorizontalLayer:
pieces = [[Piece(i,j,config.xMin+i*config.scaledLegoWidth,config.yMin+j*config.scaledLegoWidth,z,0,group.Name) for j in range(config.sliceY)] for i in range(config.sliceX)]
else:
pieces = [[Piece(j,i,config.xMin+i*config.scaledLegoWidth,config.yMin+j*config.scaledLegoWidth,z,0,group.Name) for i in range(config.sliceX)] for j in range(config.sliceY)]
slice(shape,z,isHorizontalLayer)
stopStep(start_time,"step0")
start_time = startStep()
for j in range(config.sliceY):
for i in range(config.sliceX):
piece=getPiece(i,j,isHorizontalLayer)
if (piece.value == 1):
newValue=piece.getNewValue()
upperPiece=piece.upper()
if upperPiece and upperPiece.value>0:
newValue=upperPiece.combineWithValue(newValue)
piece.value=newValue
piece.resetOthers(newValue)
if isDebug:
piece.drawValue()
stopStep(start_time,"step11")
start_time = startStep()
# drawAll(isHorizontalLayer)
drawAllFromBase(isHorizontalLayer)
stopStep(start_time,"step12")
start_time = startStep()
isHorizontalLayer=not isHorizontalLayer
w.progressbar.setValue(z)
FreeCADGui.updateGui()
stopStep(start_time,"step13")
if isDebug:
delete("GroupeBase")
w.close()
def dialog():
w=QtGui.QWidget()
box = QtGui.QVBoxLayout()
w.setLayout(box)
w.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
l=QtGui.QLabel("Number of slice" )
box.addWidget(l)
w.anz = QtGui.QLineEdit()
w.anz.setText('10')
box.addWidget(w.anz)
w.r=QtGui.QPushButton("&Run")
w.r.setDefault(True)
box.addWidget(w.r)
w.r.clicked.connect(lambda :scanIt(w))
w.progressbar=QtGui.QProgressBar()
box.addWidget(w.progressbar)
w.show()
return w
w=dialog()