Всем привет. Красивая фэшн мебель, предметы роскоши и модный лухари интерьер - это то, что позволяет пустить пыль в глаза вашим гостям и прослыть хозяевам интересными людьми. Но как все это раздобыть, если у вас ипотека и бюджет ограничен?
Этими вопросами я и задался, когда решил построить свой личный CNC станок.
Если конкретнее, я увидел на Pinterest всяческие модные штуки типа этой:

Воссоздать такую красоту нам поможет формула волны, известная из школьного курса y=sin(x), правда я модифицировал ее, добавив затухание, и спустя пару часов с момента, как я впервые увидел язык ruby, в редакторе SketchUp у меня оформился вот такой результат (листинг рассмотрим в другой раз):

Но, если речь идет об объектах серьезных размеров, хотелось бы как-то сократить количество материала. Посмотрите на следующий объект:

Здесь появляется следующая проблема: как преобразовать 3D объект в каркас из листового материала, части которого стыкуются между собой, желательно без крепежа, паз в паз? Не секрет, что на рынке игрушек есть разнообразные конструкторы, разработчики которых решили эту проблему:

в открытом виде такой код мне обнаружить не удалось, поэтому пришлось писать свой собственный высококачественный лапшекод, за который "coder-nazi" накидают мне минусов в карму, думая что этого достаточно для его автоматического улучшения. Перед тем как вы это увидите, отвлекитесь от детских конструкторов и взгляните, что могут сделать толковые ребята в более серьезных масштабах:

И наконец, его величество код! Исполнять в консоли редактора FreeCad.
App.ActiveDocument.addObject("Part::Sphere","Sphere") App.ActiveDocument.ActiveObject.Label = "Sphere" s_offset = 200 App.ActiveDocument.Sphere.Radius = s_offset #App.ActiveDocument.Sphere.Placement = App.Placement(App.Vector(500,500,500),App.Rotation(App.Vector(0,0,1),0)) App.ActiveDocument.recompute() def isEmpty(sh): if sh == None: return True if sh.Shape.BoundBox.ZMin == sys.float_info.max and sh.Shape.BoundBox.ZMax == -sys.float_info.max: return True return False def slice(name): if name == "": name = "Sphere" sx = 1310 sy = 1510 offset = 10000 rHeigth = 1400 rThickness = 4 rLength = 1500 stepx = 80 stepy = 80 dx = 10 dy = 10 mx = {} my = {} n = 0 copy = FreeCAD.ActiveDocument.copyObject(FreeCAD.activeDocument().getObject(name),False) copy.Placement = App.Placement(App.Vector(s_offset,offset+s_offset,s_offset),App.Rotation(App.Vector(0,0,1),0)) while dx < sx: box = App.ActiveDocument.addObject("Part::Box","Box"+str(n)) box.Length=rThickness box.Width=rLength box.Height=rHeigth box.Placement=App.Placement(App.Vector(dx,offset,0),App.Rotation(App.Vector(0,0,1),0)) common = App.activeDocument().addObject("Part::MultiCommon","CommonX"+str(n)) common.Shapes = [copy,box] #common.Placement=App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,0,1),0)) #copy.Visibility=False #box.Visibility=False #common.ShapeColor=copy.ShapeColor #common.DisplayMode=copy.DisplayMode mx[n] = common dx = dx + stepx n = n + 1 colx = n n = 0 while dy < sy: box = App.ActiveDocument.addObject("Part::Box","Box"+str(n)) box.Length=rLength box.Width=rThickness box.Height=rHeigth box.Placement=App.Placement(App.Vector(0,offset+dy,0),App.Rotation(App.Vector(0,0,1),0)) common = App.activeDocument().addObject("Part::MultiCommon","CommonY"+str(n)) common.Shapes = [copy,box] #common.Placement=App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,0,1),0)) #copy.Visibility=False #box.Visibility=False #common.ShapeColor=copy.ShapeColor #common.DisplayMode=copy.DisplayMode my[n] = common dy = dy + stepy n = n + 1 coly = n print("!!!---") App.ActiveDocument.recompute() inter = {} for i in range(0,colx): for j in range(0,coly): if not (isEmpty(mx[i]) or isEmpty(my[j])): common = App.activeDocument().addObject("Part::MultiCommon","ComX"+str(i)+"Y"+str(j)) common.Shapes = [mx[i],my[j]] inter[(i,j)] = common #print(str(i)+" "+ str(j)+" " + str(common.Shape.BoundBox.ZMax)+" "+str(common.Shape.BoundBox.ZMax)) App.ActiveDocument.recompute() for i in range(0,colx): for j in range(0,coly): if not isEmpty(inter.get((i,j))): com = inter.get((i,j)) d = (com.Shape.BoundBox.ZMax - com.Shape.BoundBox.ZMin)/2 box = App.ActiveDocument.addObject("Part::Box","Box"+str(n)) box.Length= com.Shape.BoundBox.XMax - com.Shape.BoundBox.XMin box.Width= com.Shape.BoundBox.YMax - com.Shape.BoundBox.YMin box.Height=d+rThickness/2 box.Placement=App.Placement(App.Vector(com.Shape.BoundBox.XMin,com.Shape.BoundBox.YMin,com.Shape.BoundBox.ZMin),App.Rotation(App.Vector(0,0,1),0)) box1 = App.ActiveDocument.copyObject(box, False) box1.Placement=App.Placement(App.Vector(com.Shape.BoundBox.XMin,com.Shape.BoundBox.YMin,com.Shape.BoundBox.ZMin+d-rThickness/2),App.Rotation(App.Vector(0,0,1),0)) com2 = App.activeDocument().addObject("Part::MultiCommon","com2_") com2.Shapes = [com,box] com3 = App.activeDocument().addObject("Part::MultiCommon","com3_") com3.Shapes = [com,box1] comX = App.activeDocument().addObject("Part::Cut","CutX"+str(i)) comX.Base = mx[i] comX.Tool = com2 mx[i] = comX comY = App.activeDocument().addObject("Part::Cut","CutY"+str(j)) comY.Base = my[j] comY.Tool = com3 my[j] = comY #print(str(i)+" "+ str(j)) App.ActiveDocument.recompute() for i in range(0,colx): if not (isEmpty(mx[i])): mx[i].Placement=App.Placement(App.Vector(stepx*i,20000,0),App.Rotation(App.Vector(0,1,0),90)) for j in range(0,coly): if not (isEmpty(my[j])): my[j].Placement=App.Placement(App.Vector(20000,stepy*j,0),App.Rotation(App.Vector(1,0,0),90)) #App.ActiveDocument.addObject('Part::Feature','Line002').Shape=App.ActiveDocument.Line002.Shape.removeSplitter() #App.ActiveDocument.Common.Shape.BoundBox.ZMax App.ActiveDocument.recompute() x1=rThickness y1=rThickness yMax = 0 #from typing import Dict, List all = list()#List[DocumentObject] for i in range(0,colx): if not (isEmpty(mx[i])): if (x1+(mx[i].Shape.BoundBox.XMax-mx[i].Shape.BoundBox.XMin)) > sx: x1 = 0 y1 = yMax+rThickness*3 x=mx[i].Shape.BoundBox.XMin y=mx[i].Shape.BoundBox.YMin z=mx[i].Shape.BoundBox.ZMin mx[i].Placement.move(FreeCAD.Base.Vector(-x+x1+rThickness*3,-y+y1,-z)) x1=mx[i].Shape.BoundBox.XMax if (mx[i].Shape.BoundBox.YMax>yMax): yMax = mx[i].Shape.BoundBox.YMax all.append(App.activeDocument().getObject(mx[i].Name)) for j in range(0,coly): if not (isEmpty(my[j])): if (x1+(my[j].Shape.BoundBox.XMax-my[j].Shape.BoundBox.XMin)) > sx: x1 = 0 y1 = yMax+rThickness*3 x=my[j].Shape.BoundBox.XMin y=my[j].Shape.BoundBox.YMin z=my[j].Shape.BoundBox.ZMin my[j].Placement.move(FreeCAD.Base.Vector(-x+x1+rThickness*3,-y+y1,-z)) #App.ActiveDocument.recompute() x1=my[j].Shape.BoundBox.XMax #print "--"+str(x1) #y1=my[j].Shape.BoundBox.YMax if (my[j].Shape.BoundBox.YMax>yMax): yMax = my[j].Shape.BoundBox.YMax all.append(App.activeDocument().getObject(my[j].Name)) #print all Gui.activeDocument().getObject(name).Visibility=False App.activeDocument().addObject("Part::Compound","Compound") App.activeDocument().Compound.Links = all App.ActiveDocument.recompute() return "" slice("")
Для начала, создается сфера (App.ActiveDocument.addObject("Part::Sphere","Sphere"))

Далее, она нарезается горизонтально решеткой рассекающих Box'ов:

Особенность ее - в том, что на половине высоты каждого пересечения у продольных и поперечных ребер вырезается паз. Так как у меня фрезер, а не лазерный станок, пазы следует увеличить, иначе за счет радиуса фрезы при вставке деталей будет пазы окажутся недостаточными. Но ребра еще предстоит разложить на листе материала:

На основе этого compaund'а из всех деталей можно построить программу для станка. Программно я этого сделать не смог (может, кто-то подскажет как это сделать), поэтому следует вручную добавить Job, выставить параметры фрезы, и добавить тех. операцию profile (к которой было бы неплохо добавить "Перемычки для удержания деталей в заготовке", иначе детали разбросает после отделения от листа):

Отсюда уже можно экспортировать G-код для станка:

А результат уже был показан ранее:

Видео-версия:
