diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a02364a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+cut_list/__pycache__/
\ No newline at end of file
diff --git a/InitGui.py b/InitGui.py
index 338c7d5..6377008 100644
--- a/InitGui.py
+++ b/InitGui.py
@@ -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"]
diff --git a/cut_list/README.md b/cut_list/README.md
new file mode 100644
index 0000000..ad63af2
--- /dev/null
+++ b/cut_list/README.md
@@ -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|
diff --git a/cut_list/__init__.py b/cut_list/__init__.py
new file mode 100644
index 0000000..5957893
--- /dev/null
+++ b/cut_list/__init__.py
@@ -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")
+
diff --git a/cut_list/cut_list_commands.py b/cut_list/cut_list_commands.py
new file mode 100644
index 0000000..01d9902
--- /dev/null
+++ b/cut_list/cut_list_commands.py
@@ -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())
diff --git a/cut_list/cut_list_creation.py b/cut_list/cut_list_creation.py
new file mode 100644
index 0000000..4161264
--- /dev/null
+++ b/cut_list/cut_list_creation.py
@@ -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)
+
\ No newline at end of file
diff --git a/cut_list/cut_list_ui.py b/cut_list/cut_list_ui.py
new file mode 100644
index 0000000..7b6838f
--- /dev/null
+++ b/cut_list/cut_list_ui.py
@@ -0,0 +1,106 @@
+import FreeCAD,FreeCADGui,Part
+import os
+
+from FreeCAD import Units
+from PySide import QtCore
+
+from . import RESOURCE_PATH
+from . import cut_list_creation
+
+
+class cutListUI:
+ def __init__(self):
+ uiPath = os.path.join(RESOURCE_PATH, "cut_list_dialog.ui")
+ # this will create a Qt widget from our ui file
+ self.form = FreeCADGui.PySideUic.loadUi(uiPath)
+
+ # Set the Default Values and allow only Positiv Values
+ maxStockDefault = Units.parseQuantity("6m")
+ cutWidthDefault = Units.parseQuantity("5mm")
+
+ self.form.max_stock_length.setProperty("value",maxStockDefault)
+ self.form.max_stock_length.setProperty("minimum",0.0)
+
+ self.form.cut_width.setProperty("value",cutWidthDefault)
+ self.form.cut_width.setProperty("minimum",0.0)
+
+ # Set Default Options
+ self.form.use_nesting.stateChanged.connect(self.useNestingToggle)
+
+ self.form.use_nesting.setChecked(False)
+
+ self.form.use_group_by_size.setChecked(True)
+
+ # Update the UI
+ self.useNestingToggle()
+ self.UpdateProfileList()
+ self.selectProfilefromSelection()
+
+
+ def selectProfilefromSelection(self):
+ """ Select the selected Profile if some is used
+ """
+ for selected in FreeCADGui.Selection.getSelection():
+ selLabel = selected.Label
+ found = self.form.profile_list.findItems(selLabel,QtCore.Qt.MatchExactly)
+ if len(found)>0:
+ item = found[0]
+ item.setSelected(True)
+
+
+
+ def useNestingToggle(self):
+ """ Toggle the Nesting Options depending on the Need
+ """
+ state = self.form.use_nesting.checkState()
+ self.form.max_stock_length.setProperty("enabled",state)
+ self.form.cut_width.setProperty("enabled",state)
+
+ def UpdateProfileList(self):
+ """ Add all Sketches/Profiles/Sections that can be used for the Beam Creation
+ """
+
+ sketches = [s.Label for s in FreeCAD.ActiveDocument.Objects if s.TypeId=="Sketcher::SketchObject"]
+ obj2D = [s.Label for s in FreeCAD.ActiveDocument.Objects if hasattr (s,"Shape") and s.Shape.Faces and not s.Shape.Solids]
+
+ self.form.profile_list.clear()
+ self.form.profile_list.addItems(sketches)
+
+ self.form.profile_list.addItems(obj2D)
+
+
+ def accept(self):
+ """ Start the Creation of the Cut List
+ """
+ # Get all selected Profiles
+ sel = self.form.profile_list.currentItem()
+ profils = []
+ for item in self.form.profile_list.selectedItems():
+ profils.append(item.text())
+
+ if profils == []:
+ # Do not try to create a cut list with an empty selection
+ # TODO: Add Message Box
+ return
+ # Get teh Nesting Information or set the default
+ if self.form.use_nesting.checkState() == False:
+ maxStockLength = Units.parseQuantity("0mm")
+ cutWidth = Units.parseQuantity("0mm")
+ else:
+ maxStockLength = self.form.max_stock_length.property('value')
+ cutWidth = self.form.cut_width.property('value')
+
+ # Generate the Cut List
+ cut_list_creation.createCutlist(profils,
+ maxStockLength,
+ cutWidth,
+ self.form.use_group_by_size.checkState())
+
+ FreeCADGui.Control.closeDialog()
+
+ def reject(self):
+ FreeCADGui.Control.closeDialog()
+
+def openCutListDialog():
+ panel = cutListUI()
+ FreeCADGui.Control.showDialog(panel)
\ No newline at end of file
diff --git a/cut_list/resources/cut_list_dialog.ui b/cut_list/resources/cut_list_dialog.ui
new file mode 100644
index 0000000..854a84f
--- /dev/null
+++ b/cut_list/resources/cut_list_dialog.ui
@@ -0,0 +1,257 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 450
+ 643
+
+
+
+
+ 0
+ 0
+
+
+
+ Create Cut List
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+ true
+ false
+
+
+
+ Select Profile
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 10
+
+
+
+
+ 0
+ 150
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractScrollArea::AdjustIgnored
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ true
+
+
+ QAbstractItemView::MultiSelection
+
+
+ QListView::Fixed
+
+
+ QListView::SinglePass
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+ true
+ false
+
+
+
+ Cut List Options
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LeftToRight
+
+
+ Group Parts by Size
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+ true
+ false
+
+
+
+ Nesting Options
+
+
+ 10
+
+
+
+ -
+
+
-
+
+
+ Maxmium Stock Length
+
+
+ 10
+
+
+
+ -
+
+
+ Cut Width
+
+
+ 10
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LeftToRight
+
+
+ Use Nesting
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+ Gui::QuantitySpinBox
+ QDoubleSpinBox
+
+
+
+
+
+
diff --git a/cut_list/resources/cut_list_icon.svg b/cut_list/resources/cut_list_icon.svg
new file mode 100644
index 0000000..a144344
--- /dev/null
+++ b/cut_list/resources/cut_list_icon.svg
@@ -0,0 +1,98 @@
+
+
+
+
diff --git a/cut_list/resultSpreadsheet.py b/cut_list/resultSpreadsheet.py
new file mode 100644
index 0000000..d6b5e97
--- /dev/null
+++ b/cut_list/resultSpreadsheet.py
@@ -0,0 +1,176 @@
+# Heavily Inspired Code from
+# https://github.com/furti/FreeCAD-Reporting/blob/master/report.py
+
+import FreeCAD
+import FreeCADGui
+import string
+
+
+COLUMN_NAMES = list(string.ascii_uppercase)
+
+def literalText(text):
+ return "'%s" % (text)
+
+def lineRange(startColumn, endColumn, lineNumber):
+ return cellRange(startColumn, lineNumber, endColumn, lineNumber)
+
+
+def cellRange(startColumn, startLine, endColumn, endLine):
+ return '%s%s:%s%s' % (startColumn, startLine, endColumn, endLine)
+
+def nextColumnName(actualColumnName):
+ if actualColumnName is None:
+ return COLUMN_NAMES[0]
+
+ nextIndex = COLUMN_NAMES.index(actualColumnName) + 1
+
+ if nextIndex >= len(COLUMN_NAMES):
+ nextIndex -= len(COLUMN_NAMES)
+
+ return COLUMN_NAMES[nextIndex]
+
+
+class ResultSpreadsheet(object):
+
+ def __init__(self, spreadsheet, columnLabels):
+
+ self.spreadsheet = spreadsheet
+ self.lineNumber = 1
+ self.maxColumn = None
+ self.columnLabels = columnLabels
+
+ def getColumnName(self, label):
+ if label is None:
+ return COLUMN_NAMES[0]
+
+ nextIndex = self.columnLabels.index(label)
+
+ if nextIndex >= len(COLUMN_NAMES):
+ nextIndex -= len(COLUMN_NAMES)
+
+ return COLUMN_NAMES[nextIndex]
+
+
+ def clearAll(self):
+ self.spreadsheet.clearAll()
+
+ def printEmptyLine(self):
+ self.lineNumber += 1
+
+ def printHeader(self, header='HAEADER'):
+ spreadsheet = self.spreadsheet
+
+ if header is None or header == '':
+ return
+
+ headerCell = 'A%s' % (self.lineNumber)
+
+ self.setCellValue(headerCell, header)
+ spreadsheet.setStyle(headerCell, 'bold|underline', 'add')
+
+ spreadsheet.mergeCells(lineRange('A', COLUMN_NAMES[len(self.columnLabels)-1], self.lineNumber))
+
+ self.lineNumber += 1
+ self.updateMaxColumn('A')
+
+ self.clearLine(self.lineNumber)
+
+ def printColumnLabels(self):
+ spreadsheet = self.spreadsheet
+
+ columnName = None
+
+ for columnLabel in self.columnLabels:
+ columnName = self.getColumnName(columnLabel)
+ cellName = f"{columnName}{self.lineNumber}"
+
+ self.setCellValue(cellName, columnLabel)
+
+ spreadsheet.setStyle(
+ lineRange('A', columnName, self.lineNumber), 'bold', 'add')
+
+ self.lineNumber += 1
+ self.updateMaxColumn(columnName)
+
+ self.clearLine(self.lineNumber)
+
+ def printRows(self, rows):
+ lineNumberBefore = self.lineNumber
+
+ columnName = 'A'
+ for row in rows:
+ columnName = None
+
+ for columnlabel, value in row.items():
+ if columnlabel in self.columnLabels:
+ columnName = self.getColumnName(columnlabel)
+ cellName = f"{columnName}{self.lineNumber}"
+
+ self.setCellValue(cellName, value)
+
+ self.lineNumber += 1
+
+ #if printResultInBold:
+ # self.spreadsheet.setStyle(
+ # cellRange('A', lineNumberBefore, columnName, self.lineNumber), 'bold', 'add')
+
+ self.clearLine(self.lineNumber)
+
+
+ self.updateMaxColumn(columnName)
+
+ def setCellValue(self, cell, value):
+ if value is None:
+ convertedValue = ''
+ elif isinstance(value, FreeCAD.Units.Quantity):
+ convertedValue = value.UserString
+ else:
+ convertedValue = str(value)
+
+ convertedValue = literalText(convertedValue)
+
+ self.spreadsheet.set(cell, convertedValue)
+
+ def recompute(self):
+ self.spreadsheet.recompute()
+
+ def updateMaxColumn(self, columnName):
+ if self.maxColumn is None:
+ self.maxColumn = columnName
+ else:
+ actualIndex = COLUMN_NAMES.index(self.maxColumn)
+ columnIndex = COLUMN_NAMES.index(columnName)
+
+ if actualIndex < columnIndex:
+ self.maxColumn = columnName
+
+ def clearUnusedCells(self, column, line):
+ if line is not None and line > self.lineNumber:
+ for lineNumberToDelete in range(line, self.lineNumber, -1):
+ self.clearLine(lineNumberToDelete)
+
+ if column is not None:
+ columnIndex = COLUMN_NAMES.index(column)
+ maxColumnIndex = COLUMN_NAMES.index(self.maxColumn)
+
+ if columnIndex > maxColumnIndex:
+ for columnIndexToDelete in range(columnIndex, maxColumnIndex, -1):
+ self.clearColumn(COLUMN_NAMES[columnIndexToDelete], line)
+
+ def clearLine(self, lineNumberToDelete):
+
+ column = None
+
+ while column is None or column != self.maxColumn:
+ column = nextColumnName(column)
+ cellName = f"{column}{lineNumberToDelete}"
+
+
+ self.spreadsheet.clear(cellName)
+
+ def clearColumn(self, columnToDelete, maxLineNumber):
+
+ for lineNumber in range(1, maxLineNumber):
+ cellName = f"{columnToDelete}{lineNumber + 1}"
+
+ self.spreadsheet.clear(cellName)