Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
cut_list/__pycache__/
22 changes: 21 additions & 1 deletion InitGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,29 @@ def Initialize(self):
self.utilsList=["selectSolids","queryModel","moveWorkPlane","offsetWorkPlane","rotateWorkPlane","hackedL","moveHandle","dpCalc"]
self.appendToolbar("Utils",self.utilsList)
Log ('Loading Utils: done\n')

import CFrame
self.frameList=["frameIt","FrameBranchManager","insertSection","spinSect","reverseBeam","shiftBeam","pivotBeam","levelBeam","alignEdge","rotJoin","alignFlange","stretchBeam","extend","adjustFrameAngle","insertPath"]
from cut_list.cut_list_commands import cutListCommand

self.frameList=["frameIt",
"FrameBranchManager",
"insertSection",
"spinSect",
"reverseBeam",
"shiftBeam",
"pivotBeam",
"levelBeam",
"alignEdge",
"rotJoin",
"alignFlange",
"stretchBeam",
"extend",
"adjustFrameAngle",
"insertPath",
"createCutList"]

self.appendToolbar("frameTools",self.frameList)

Log ('Loading Frame tools: done\n')
import CPipe
self.pypeList=["insertPipe","insertElbow","insertReduct","insertCap","insertValve","insertFlange","insertUbolt","insertPypeLine","insertBranch","insertTank","insertRoute","breakPipe","mateEdges","flat","extend2intersection","extend1intersection","makeHeader","laydown","raiseup","attach2tube","point2point","insertAnyz"]#,"joinPype"]
Expand Down
64 changes: 64 additions & 0 deletions cut_list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Cut List Creation Command for Dodo Workbench

This Module can be used to create a cut list of beams created by the DODO-Workbench.\
The cut list can use one or more profiles (sections/sketches).\
A position number will be generated for each profile & length combination (rounded to 0.01mm).\
The script will create a new speadsheet object with each cut list.

# How to use it
[Cut_List.webm](https://github.com/FilePhil/dodo_Cutlist_Macro_Version/assets/16101101/e992a925-0a02-4560-8e7c-a22eef86234d)

# Options
## Group Parts by Size
The Option "Group Parts by Size" will count all Pieces with the same profile and Length (rounded to 0.01mm).

### Example: Group Party by Size

| Beam No. 1 | | | |
|-----------------------------|--|--|--|
| Used 2855.0 mm | | | |
| Pos. | Profil | Length | Quantity |
| 1 | 10X10 | 610,00 mm | 2 |
| 2 | 10X10 | 600,00 mm | 2 |
| 3 | 10X10 | 410,00 mm | 1 |

### Exampl: Without Group Party by Size

| Beam No. 1 | | | |
|-----------------------------|--|--|--|
| Used 2410.0 mm | | | |
| Pos. | Profil | Label | Length
| 1 | 10X10 | Structure006 | 610,00 mm |
| 1 | 10X10 | Structure017 | 610,00 mm |
| 2 | 10X10 | Structure012 | 600,00 mm |
| 2 | 10X10 | Structure018 | 600,00 mm |


## Use Nesting
The Option "Use Nesting" allows to specify the maximum length of the Stock Material and allows for optimizing of the available material.\
The cut Width will be added to each piece to account for the saw thickness.\
The list will be seperated into Sections and shows the Used Length and the Parts that can be cut from the Stock Material.\
The position number of a piece will be the same on every stock material beam.

### Example with Nesting & Group Party by Size

| Beam No. 1 | | | |
|-----------------------------|--|--|--|
| Used 2855.0 mm of 3000.0 mm | | | |
| Pos. | Profil | Length | Quantity |
| 1 | 10X10 | 610,00 mm | 2 |
| 2 | 10X10 | 600,00 mm | 2 |
| 3 | 10X10 | 410,00 mm | 1 |
| | | | |
| Beam No. 2 |
| Used 3000.0 mm of 3000.0 mm |
| Pos. | Profil | Length | Quantity|
| 3 | 10X10 | 410,00 mm | 1|
| 4 | 10X10 | 390,00 mm | 2|
| 5 | 10X10 | 210,00 mm | 2|
| 6 | 10X10 | 190,00 mm | 7|
| | | | |
| Beam No. 3 |
| Used 1365.0 mm of 3000.0 mm |
| Pos. | Profil | Length | Quantity|
| 6 | 10X10 | 190,00 mm | 7|
29 changes: 29 additions & 0 deletions cut_list/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#****************************************************************************
#* *
#* Cut List Creation for Dodo Workbench *
#* *
#* Copyright (c) 2023 FilePhil LGPL *
#* *
#* 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 *
#* *
#****************************************************************************


import os

RESOURCE_PATH = os.path.join(os.path.dirname(__file__), "resources")

30 changes: 30 additions & 0 deletions cut_list/cut_list_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import FreeCAD
import FreeCADGui
import os
from . import cut_list_ui
from . import cut_list_creation
from . import RESOURCE_PATH


class cutListCommand:
toolbarName = 'Cut List'
commandName = 'createCutList'

def GetResources(self):
Icon = os.path.join(RESOURCE_PATH, "cut_list_icon.svg")
return {'MenuText': self.commandName,
'ToolTip': "Create a new Cut List from Dodo Beams",
'Pixmap': Icon
}

def Activated(self):

cut_list_ui.openCutListDialog()

def IsActive(self):
"""If there is no active document we can't do anything."""
return not FreeCAD.ActiveDocument is None



FreeCADGui.addCommand(cutListCommand.commandName, cutListCommand())
220 changes: 220 additions & 0 deletions cut_list/cut_list_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import FreeCAD
import FreeCADGui

from dataclasses import dataclass, asdict
from typing import List

from . import resultSpreadsheet


@dataclass
class Cut:
""" Store the Infomation about the cutted Piece and provide Helper Functions
"""
label: str
profil: str
length: object
cutwidth: object
position: int = 0

def getKey(self):
""" Provide a Key to generate the Position Number
"""
return self.profil + "|" + str(round(self.length.getValueAs("mm"),2))

def totalLength(self):
return self.length + self.cutwidth

def getRow(self):
"""Provide the Information from the Cutted Piece in the form of a dict"""
return {"Label": self.label,"Profil": self.profil, "Length": self.length,"CutWidth": self.cutwidth,"Pos.":self.position}

@dataclass
class Beam:
""" Store the Infomation about the Stock Material Beam and provide Helper Functions
"""
number: int
length: object
lengthLeft: object
cuts: List[Cut]

def addCut(self,cut):
""" Try to fit the cutted piece on the Beam and provide a status if it fits
"""
if self.length.getValueAs("mm") > 0.1 and self.lengthLeft < cut.totalLength():
# Cut is not Possible on this Beam
# Ignore if Beam has no lenght
return False

self.cuts.append(cut)
self.lengthLeft -= cut.totalLength()
return True

def getCutsAsDict(self):
""" Get a easy to work with Dict List of the Beams / Cuts"""
return [x.getRow() for x in self.cuts]

def lengthUsed(self):
return self.length - self.lengthLeft


def queryStructures(profiles:list,rootObjs = None):
""" Find all Structure Elements that use one of the selected profiles
"""

resultObjs = []

if rootObjs is None:
rootObjs = FreeCAD.ActiveDocument.Objects

for obj in rootObjs:
# Follow Link Groups
if obj.TypeId == "App::LinkGroup":
resultObjs = resultObjs + queryStructures(profiles,obj.ElementList)

# TODO: A Link to a ink Group will currently not be queried

# Get the base Profile Used for the Stucture
base = getattr(obj, "Base", None)
computedLength = getattr(obj, "ComputedLength", None)

# Check if the Object is a valid Strucutre
if base is None or computedLength is None:
continue

if base.Label in profiles:
resultObjs.append(obj)

return resultObjs

def nestCuts(profiles:list,beamLength,cutwidth):
""" Nest a List of Cuts on a Standard Beam length to estimate the needed Beams.
"""

allStructures = queryStructures(profiles)

# Sort Cuts Big to Small
sortedStructures = sorted(allStructures, key=lambda x:x.ComputedLength, reverse=True)

allCuts = []
beams = [] # List of Lists to reference what is together in one beam [beamLength,[Cut1,Cut2...]]
beam = Beam(1, beamLength, beamLength,[])
beams.append(beam)

# Go through all Structure Objects and try to fit them onto the Beams
for obj in sortedStructures:

# Create Cut Object to hold all Attributes
label = obj.Label
profile = obj.Base.Label
length = round(obj.ComputedLength,2)
cutObj = Cut(label, profile, length, cutwidth)

cutKey = cutObj.getKey()

# Use the Key to define the Position Number of the Cut
if cutKey not in allCuts:
allCuts.append (cutKey)
cutObj.position = len(allCuts)
else:
cutObj.position = allCuts.index(cutKey)+1

# Try to fit the Object on exsting Beam
nofit = True
for beam in beams:
if beam.addCut(cutObj):
nofit = False
break

# Add new Beam and add the Cut to it
if nofit:
beam = Beam(len(beams)+1, beamLength,beamLength,[])
beams.append(beam)
if not beam.addCut(cutObj):
raise ValueError('Cut longer than beam!')

return beams


def createSpreadSheetReport(beams, name="Result_Nest_Profile"):
""" Create a Spreadsheet as the Result of the Cut list Generation.
Each Piece will be displayed as One Row
"""
result = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", name)

columnLabels = ["Pos.",'Profil','Label','Length']
result = resultSpreadsheet.ResultSpreadsheet(result, columnLabels)

for beam in beams:
result.printHeader(f"Beam No. {beam.number}")

# Print the Used Length if a maximum Stock Value is given
beamLength = round(beam.length,2)
if beamLength.getValueAs("mm") <= 0.1:
result.printHeader(f"Used {round(beam.lengthUsed(),2)}")
else:
result.printHeader(f"Used {round(beam.lengthUsed(),2)} of {beamLength}")

result.printColumnLabels()
result.printRows(beam.getCutsAsDict())
result.printEmptyLine()

result.recompute()

def createSpreadSheetReportGrouped(beams, name="Result_Nest_Profile"):
""" Create a Spreadsheet as the Result of the Cut list Generation.
The Pieces will be grouped by the Length and Profile.
"""

result = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", name)

columnLabels = ["Pos.","Profil","Length","Quantity"]
result = resultSpreadsheet.ResultSpreadsheet(result, columnLabels)

for beam in beams:
result.printHeader(f"Beam No. {beam.number}")

# Print the Used Length if a maximum Stock Value is given
beamLength = round(beam.length,2)
if beamLength.getValueAs("mm") <= 0.1:
result.printHeader(f"Used {round(beam.lengthUsed(),2)}")
else:
result.printHeader(f"Used {round(beam.lengthUsed(),2)} of {beamLength}")

result.printColumnLabels()

resultRows = []
usedKeys = []

for cut in beam.cuts:
# Add only one Row for each Key on the Beam
currentCutKey = cut.getKey()
if currentCutKey not in usedKeys:

roWDict = cut.getRow()
# Calculate the Number of the same pieces
roWDict["Quantity"] = len([x for x in beam.cuts if x.getKey() == currentCutKey])

resultRows.append(roWDict)
usedKeys.append(cut.getKey())

result.printRows(resultRows)
result.printEmptyLine()

result.recompute()


def createCutlist(profiles,maxBeamLength,cutWidth,GroupByLength=False):
""" Nest the Cuts to the Beams and create the Report Spreadsheet
"""

profilesLabel = "_".join(profiles)
tableName = f"Cut_List_{profilesLabel}"

beams = nestCuts(profiles,maxBeamLength,cutWidth)

if GroupByLength:
createSpreadSheetReportGrouped(beams,tableName)
else:
createSpreadSheetReport(beams,tableName)

Loading