Macro to convert a solid to a LEGO model

Need help, or want to share a macro? Post here!
jfc941640
Posts: 9
Joined: Mon Oct 28, 2019 6:20 pm

Macro to convert a solid to a LEGO model

Postby jfc941640 » Wed Nov 27, 2019 9:42 pm

Since I received help on this forum, now it is time to share the result !

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()
Attachments
tux.png
tux.png (61.97 KiB) Viewed 836 times
Tux01.png
Tux01.png (32.92 KiB) Viewed 836 times
Tux02.png
Tux02.png (43.16 KiB) Viewed 836 times
User avatar
microelly2
Posts: 4508
Joined: Tue Nov 12, 2013 4:06 pm
Contact:

Re: Macro to convert a solid to a LEGO model

Postby microelly2 » Thu Nov 28, 2019 7:05 am

a good macro to get children motivated to learn FreeCAD. :D
chrisb
Posts: 20660
Joined: Tue Mar 17, 2015 9:14 am

Re: Macro to convert a solid to a LEGO model

Postby chrisb » Thu Nov 28, 2019 8:41 am

A thirty year old friend of mine is still building Lego models, and has told me about the collectioners scene, where people need their boxes at least twice: one for really building it and another to be kept in its untouched state: all plastic bags still sealed, ...

So there may be a FreeCAD market for grown-ups as well.
User avatar
microelly2
Posts: 4508
Joined: Tue Nov 12, 2013 4:06 pm
Contact:

Re: Macro to convert a solid to a LEGO model

Postby microelly2 » Thu Nov 28, 2019 8:43 am

chrisb wrote:
Thu Nov 28, 2019 8:41 am
So there may be a FreeCAD market for grown-ups as well.
I remember the assembly animation workbench.
may be there is a way to get a assembly instruction generator for the legos too.
User avatar
Kunda1
Posts: 6220
Joined: Thu Jan 05, 2017 9:03 pm

Re: Macro to convert a solid to a LEGO model

Postby Kunda1 » Thu Nov 28, 2019 11:46 am

OP please post a thread to the Users Showcase subforum with s link to this thread for discussion
Want to contribute back to FC? Checkout:
#lowhangingfruit | Use the Source, Luke. | How to Help FreeCAD | How to report FC bugs and features
jfc941640
Posts: 9
Joined: Mon Oct 28, 2019 6:20 pm

Re: Macro to convert a solid to a LEGO model

Postby jfc941640 » Thu Nov 28, 2019 8:54 pm

microelly2 wrote:
Thu Nov 28, 2019 8:43 am
microelly2
Great suggestion microelly2 the animation !
You make me discover a new hidden function of freecad.
I think it should be simple, all parts are a copy of the corresponding base part.
jfc941640
Posts: 9
Joined: Mon Oct 28, 2019 6:20 pm

Re: Macro to convert a solid to a LEGO model

Postby jfc941640 » Thu Nov 28, 2019 10:41 pm

Here it is with 1 placer --> slowwww ;-)

Code: Select all

import math
from FreeCAD import Base
import Draft
import Part
from PySide import QtGui, QtCore
import time
from Animation import *
import Placer

placer=Placer.createPlacer("PiecePlacer",None)
m=createManager()
placer.x='x0+(x1-x0)*time'
placer.y='y0+(y1-y0)*time'
placer.z='z0+(z1-z0)*time'
m.addObject(placer)

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

          placer.x1 = self.x
          placer.y1 = self.y
          placer.z1 = self.z
          placer.target=self.pieceObject
          
          m.Proxy.run()

#          from time import sleep
#          sleep(2)
#          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()
User avatar
Kunda1
Posts: 6220
Joined: Thu Jan 05, 2017 9:03 pm

Re: Macro to convert a solid to a LEGO model

Postby Kunda1 » Fri Nov 29, 2019 8:12 pm

fosselius wrote::bell:
@fosselius you may be interested in this
Want to contribute back to FC? Checkout:
#lowhangingfruit | Use the Source, Luke. | How to Help FreeCAD | How to report FC bugs and features
User avatar
fosselius
Posts: 347
Joined: Sat Apr 23, 2016 10:03 am

Re: Macro to convert a solid to a LEGO model

Postby fosselius » Sat Nov 30, 2019 10:46 am

awesome!
cron