# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2011 Yorik van Havre * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD 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 * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** __title__ = "FreeCAD Axis System" __author__ = "Yorik van Havre" __url__ = "https://www.freecad.org" ## @package ArchGrid # \ingroup ARCH # \brief Grid system for the Arch workbench # # This module provides tools to build grid systems import math import FreeCAD import Part if FreeCAD.GuiUp: from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui from draftutils.translate import translate else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond class ArchGrid: "The Grid object" def __init__(self, obj): obj.Proxy = self self.Type = "Grid" self.setProperties(obj) def setProperties(self, obj): pl = obj.PropertiesList if not "Rows" in pl: obj.addProperty( "App::PropertyInteger", "Rows", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The number of rows"), locked=True, ) if not "Columns" in pl: obj.addProperty( "App::PropertyInteger", "Columns", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The number of columns"), locked=True, ) if not "RowSize" in pl: obj.addProperty( "App::PropertyFloatList", "RowSize", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The sizes of rows"), locked=True, ) if not "ColumnSize" in pl: obj.addProperty( "App::PropertyFloatList", "ColumnSize", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The sizes of columns"), locked=True, ) if not "Spans" in pl: obj.addProperty( "App::PropertyStringList", "Spans", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The span ranges of cells that are merged together"), locked=True, ) if not "PointsOutput" in pl: obj.addProperty( "App::PropertyEnumeration", "PointsOutput", "Grid", QT_TRANSLATE_NOOP( "Arch_Grid", "The type of 3D points produced by this grid object" ), locked=True, ) obj.PointsOutput = ["Vertices", "Edges", "Vertical Edges", "Horizontal Edges", "Faces"] if not "Width" in pl: obj.addProperty( "App::PropertyLength", "Width", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The total width of this grid"), locked=True, ) if not "Height" in pl: obj.addProperty( "App::PropertyLength", "Height", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The total height of this grid"), locked=True, ) if not "AutoWidth" in pl: obj.addProperty( "App::PropertyLength", "AutoWidth", "Grid", QT_TRANSLATE_NOOP( "Arch_Grid", "Creates automatic column divisions (set to 0 to disable)" ), locked=True, ) if not "AutoHeight" in pl: obj.addProperty( "App::PropertyLength", "AutoHeight", "Grid", QT_TRANSLATE_NOOP( "Arch_Grid", "Creates automatic row divisions (set to 0 to disable)" ), locked=True, ) if not "Reorient" in pl: obj.addProperty( "App::PropertyBool", "Reorient", "Grid", QT_TRANSLATE_NOOP( "Arch_Grid", "When in edge midpoint mode, if this grid must reorient its children along edge normals or not", ), locked=True, ) if not "HiddenFaces" in pl: obj.addProperty( "App::PropertyIntegerList", "HiddenFaces", "Grid", QT_TRANSLATE_NOOP("Arch_Grid", "The indices of faces to hide"), locked=True, ) def onDocumentRestored(self, obj): self.setProperties(obj) def getSizes(self, obj): "returns rowsizes,columnsizes,spangroups" if not obj.Height.Value: return [], [], [] if not obj.Width.Value: return [], [], [] if (not obj.Rows) and (not obj.AutoHeight.Value): return [], [], [] if (not obj.Columns) and (not obj.AutoWidth.Value): return [], [], [] # rescale rows rowsizes = [] if obj.AutoHeight.Value: if obj.AutoHeight.Value > obj.Height.Value: FreeCAD.Console.PrintError(translate("Arch", "Auto height is larger than height")) return [], [], [] rows = int(math.floor(obj.Height.Value / obj.AutoHeight.Value)) for i in range(rows): rowsizes.append(obj.AutoHeight.Value) rowsizes.append(obj.Height.Value - rows * obj.AutoHeight.Value) else: reserved_rowsize = sum(v for v in obj.RowSize) if reserved_rowsize > obj.Height.Value: FreeCAD.Console.PrintError( translate("Arch", "Total row size is larger than height") ) return [], [], [] for i in range(obj.Rows): v = 0 if i < len(obj.RowSize): v = obj.RowSize[i] rowsizes.append(v) e = len([v for v in rowsizes if v == 0]) default = obj.Height.Value - reserved_rowsize if e: default = default / e t = [] for v in rowsizes: if v: t.append(v) else: t.append(default) rowsizes = t # rescale columns columnsizes = [] if obj.AutoWidth.Value: if obj.AutoWidth.Value > obj.Width.Value: FreeCAD.Console.PrintError(translate("Arch", "Auto width is larger than width")) return [], [], [] cols = int(math.floor(obj.Width.Value / obj.AutoWidth.Value)) for i in range(cols): columnsizes.append(obj.AutoWidth.Value) columnsizes.append(obj.Width.Value - cols * obj.AutoWidth.Value) else: reserved_columnsize = sum(v for v in obj.ColumnSize) if reserved_columnsize > obj.Width.Value: FreeCAD.Console.PrintError( translate("Arch", "Total column size is larger than width") ) return [], [], [] for i in range(obj.Columns): v = 0 if i < len(obj.ColumnSize): v = obj.ColumnSize[i] columnsizes.append(v) e = len([v for v in columnsizes if v == 0]) default = obj.Width.Value - reserved_columnsize if e: default = default / e t = [] for v in columnsizes: if v: t.append(v) else: t.append(default) columnsizes = t # format span groups from [row,col,rowspan,colspan] to [faceindexes] spangroups = [] for s in obj.Spans: nspan = [] span = [int(i.strip()) for i in s.split(",")] for row in range(span[2]): for column in range(span[3]): nspan.append((span[0] + row) * obj.Columns + (span[1] + column)) spangroups.append(nspan) return rowsizes, columnsizes, spangroups def execute(self, obj): pl = obj.Placement rowsizes, columnsizes, spangroups = self.getSizes(obj) # print rowsizes,columnsizes,spangroups # create one face for each cell faces = [] facenumber = 0 rowoffset = 0 for row in rowsizes: columnoffset = 0 for column in columnsizes: v1 = FreeCAD.Vector(columnoffset, rowoffset, 0) v2 = v1.add(FreeCAD.Vector(column, 0, 0)) v3 = v2.add(FreeCAD.Vector(0, -row, 0)) v4 = v3.add(FreeCAD.Vector(-column, 0, 0)) f = Part.Face(Part.makePolygon([v1, v2, v3, v4, v1])) if not facenumber in obj.HiddenFaces: spanning = False for i in range(len(spangroups)): if facenumber in spangroups[i]: g = spangroups[i] g[g.index(facenumber)] = f spangroups[i] = g spanning = True break if not spanning: faces.append(f) facenumber += 1 columnoffset += column rowoffset -= row # join spangroups for g in spangroups: s = Part.makeShell(g) s = s.removeSplitter() faces.extend(s.Faces) if faces: obj.Shape = Part.makeCompound(faces) obj.Placement = pl def getPoints(self, obj): "returns the gridpoints" def remdupes(pts): # eliminate possible duplicates ret = [] for p in pts: if not p in ret: ret.append(p) return ret if obj.PointsOutput == "Vertices": return [v.Point for v in obj.Shape.Vertexes] elif obj.PointsOutput == "Edges": return remdupes([e.CenterOfMass for e in obj.Shape.Edges]) elif obj.PointsOutput == "Vertical Edges": rv = obj.Placement.Rotation.multVec(FreeCAD.Vector(0, 1, 0)) edges = [ e for e in obj.Shape.Edges if round(rv.getAngle(e.tangentAt(e.FirstParameter)), 4) in [0, 3.1416] ] return remdupes([e.CenterOfMass for e in edges]) elif obj.PointsOutput == "Horizontal Edges": rv = obj.Placement.Rotation.multVec(FreeCAD.Vector(1, 0, 0)) edges = [ e for e in obj.Shape.Edges if round(rv.getAngle(e.tangentAt(e.FirstParameter)), 4) in [0, 3.1416] ] return remdupes([e.CenterOfMass for e in edges]) else: return [f.CenterOfMass for f in obj.Shape.Faces] def dumps(self): return None def loads(self, state): self.Type = "Grid" class ViewProviderArchGrid: "A View Provider for the Arch Grid" def __init__(self, vobj): vobj.Proxy = self def getIcon(self): import Arch_rc return ":/icons/Arch_Grid.svg" def attach(self, vobj): self.Object = vobj.Object def setEdit(self, vobj, mode): if mode != 0: return None taskd = ArchGridTaskPanel(vobj.Object) FreeCADGui.Control.showDialog(taskd) return True def unsetEdit(self, vobj, mode): if mode != 0: return None FreeCADGui.Control.closeDialog() return True def setupContextMenu(self, vobj, menu): if FreeCADGui.activeWorkbench().name() != "BIMWorkbench": return actionEdit = QtGui.QAction(translate("Arch", "Edit"), menu) QtCore.QObject.connect(actionEdit, QtCore.SIGNAL("triggered()"), self.edit) menu.addAction(actionEdit) def edit(self): FreeCADGui.ActiveDocument.setEdit(self.Object, 0) def dumps(self): return None def loads(self, state): return None class ArchGridTaskPanel: """A TaskPanel for the Arch Grid""" def __init__(self, obj): # length, width, label self.width = 0 self.height = 0 self.spans = [] self.obj = obj self.form = QtGui.QWidget() uil = FreeCADGui.UiLoader() layout = QtGui.QVBoxLayout(self.form) hbox3 = QtGui.QHBoxLayout() layout.addLayout(hbox3) self.wLabel = QtGui.QLabel(self.form) hbox3.addWidget(self.wLabel) self.widthUi = uil.createWidget("Gui::InputField") hbox3.addWidget(self.widthUi) hbox4 = QtGui.QHBoxLayout() layout.addLayout(hbox4) self.hLabel = QtGui.QLabel(self.form) hbox4.addWidget(self.hLabel) self.heightUi = uil.createWidget("Gui::InputField") hbox4.addWidget(self.heightUi) self.title = QtGui.QLabel(self.form) layout.addWidget(self.title) # grid self.table = QtGui.QTableWidget(self.form) layout.addWidget(self.table) style = "QTableWidget { background-color: #ffffff; gridline-color: #000000; }" self.table.setStyleSheet(style) self.table.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch) # row/column buttons hbox1 = QtGui.QHBoxLayout() layout.addLayout(hbox1) self.addRowButton = QtGui.QPushButton(self.form) self.addRowButton.setIcon(QtGui.QIcon(":/icons/Arch_Add.svg")) hbox1.addWidget(self.addRowButton) self.delRowButton = QtGui.QPushButton(self.form) self.delRowButton.setIcon(QtGui.QIcon(":/icons/Arch_Remove.svg")) hbox1.addWidget(self.delRowButton) self.addColumnButton = QtGui.QPushButton(self.form) self.addColumnButton.setIcon(QtGui.QIcon(":/icons/Arch_Add.svg")) hbox1.addWidget(self.addColumnButton) self.delColumnButton = QtGui.QPushButton(self.form) self.delColumnButton.setIcon(QtGui.QIcon(":/icons/Arch_Remove.svg")) hbox1.addWidget(self.delColumnButton) # span buttons hbox2 = QtGui.QHBoxLayout() layout.addLayout(hbox2) self.spanButton = QtGui.QPushButton(self.form) self.spanButton.setIcon(QtGui.QIcon(":/icons/SpreadsheetMergeCells.svg")) hbox2.addWidget(self.spanButton) self.spanButton.setEnabled(False) self.delSpanButton = QtGui.QPushButton(self.form) self.delSpanButton.setIcon(QtGui.QIcon(":/icons/SpreadsheetSplitCell.svg")) hbox2.addWidget(self.delSpanButton) self.delSpanButton.setEnabled(False) # signals QtCore.QObject.connect(self.widthUi, QtCore.SIGNAL("valueChanged(double)"), self.setWidth) QtCore.QObject.connect(self.heightUi, QtCore.SIGNAL("valueChanged(double)"), self.setHeight) QtCore.QObject.connect(self.table, QtCore.SIGNAL("itemSelectionChanged()"), self.checkSpan) QtCore.QObject.connect(self.addRowButton, QtCore.SIGNAL("clicked()"), self.addRow) QtCore.QObject.connect(self.delRowButton, QtCore.SIGNAL("clicked()"), self.delRow) QtCore.QObject.connect(self.addColumnButton, QtCore.SIGNAL("clicked()"), self.addColumn) QtCore.QObject.connect(self.delColumnButton, QtCore.SIGNAL("clicked()"), self.delColumn) QtCore.QObject.connect(self.spanButton, QtCore.SIGNAL("clicked()"), self.addSpan) QtCore.QObject.connect(self.delSpanButton, QtCore.SIGNAL("clicked()"), self.removeSpan) QtCore.QObject.connect( self.table.horizontalHeader(), QtCore.SIGNAL("sectionDoubleClicked(int)"), self.editHorizontalHeader, ) QtCore.QObject.connect( self.table.verticalHeader(), QtCore.SIGNAL("sectionDoubleClicked(int)"), self.editVerticalHeader, ) self.update() self.retranslateUi() def retranslateUi(self, widget=None): self.form.setWindowTitle(QtGui.QApplication.translate("Arch", "Grid", None)) self.wLabel.setText(QtGui.QApplication.translate("Arch", "Total width", None)) self.hLabel.setText(QtGui.QApplication.translate("Arch", "Total height", None)) self.addRowButton.setText(QtGui.QApplication.translate("Arch", "Add Row", None)) self.delRowButton.setText(QtGui.QApplication.translate("Arch", "Delete Row", None)) self.addColumnButton.setText(QtGui.QApplication.translate("Arch", "Add Column", None)) self.delColumnButton.setText(QtGui.QApplication.translate("Arch", "Delete Column", None)) self.spanButton.setText(QtGui.QApplication.translate("Arch", "Create Span", None)) self.delSpanButton.setText(QtGui.QApplication.translate("Arch", "Remove Span", None)) self.title.setText( QtGui.QApplication.translate("Arch", "Rows", None) + ": " + str(self.table.rowCount()) + " / " + QtGui.QApplication.translate("Arch", "Columns", None) + ": " + str(self.table.columnCount()) ) def update(self): self.table.clear() if self.obj.Rows: self.table.setRowCount(self.obj.Rows) vlabels = ["0" for i in range(self.obj.Rows)] for i, v in enumerate(self.obj.RowSize): if i < len(vlabels): vlabels[i] = FreeCAD.Units.Quantity(v, FreeCAD.Units.Length).getUserPreferred()[ 0 ] self.table.setVerticalHeaderLabels(vlabels) if self.obj.Columns: self.table.setColumnCount(self.obj.Columns) hlabels = ["0" for i in range(self.obj.Columns)] for i, v in enumerate(self.obj.ColumnSize): if i < len(hlabels): hlabels[i] = FreeCAD.Units.Quantity(v, FreeCAD.Units.Length).getUserPreferred()[ 0 ] self.table.setHorizontalHeaderLabels(hlabels) self.widthUi.setText(self.obj.Width.getUserPreferred()[0]) self.heightUi.setText(self.obj.Height.getUserPreferred()[0]) self.spans = [] for s in self.obj.Spans: span = [int(i.strip()) for i in s.split(",")] if len(span) == 4: self.table.setSpan(span[0], span[1], span[2], span[3]) self.spans.append(span) def checkSpan(self): if self.table.selectedRanges(): self.spanButton.setEnabled(False) self.delSpanButton.setEnabled(False) if len(self.table.selectedRanges()) > 1: self.spanButton.setEnabled(True) for r in self.table.selectedRanges(): if (r.rowCount() * r.columnCount()) > 1: self.spanButton.setEnabled(True) elif (r.rowCount() * r.columnCount()) == 1: if ( self.table.rowSpan(r.topRow(), r.leftColumn()) > 1 or self.table.columnSpan(r.topRow(), r.leftColumn()) > 1 ): self.delSpanButton.setEnabled(True) else: self.spanButton.setEnabled(False) self.delSpanButton.setEnabled(False) def addRow(self): c = self.table.currentRow() self.table.insertRow(c + 1) self.table.setVerticalHeaderItem(c + 1, QtGui.QTableWidgetItem("0")) self.retranslateUi() def delRow(self): if self.table.selectedRanges(): self.table.removeRow(self.table.currentRow()) self.retranslateUi() def addColumn(self): c = self.table.currentColumn() self.table.insertColumn(c + 1) self.table.setHorizontalHeaderItem(c + 1, QtGui.QTableWidgetItem("0")) self.retranslateUi() def delColumn(self): if self.table.selectedRanges(): self.table.removeColumn(self.table.currentColumn()) self.retranslateUi() def addSpan(self): for r in self.table.selectedRanges(): if r.rowCount() * r.columnCount() > 1: self.table.setSpan(r.topRow(), r.leftColumn(), r.rowCount(), r.columnCount()) self.spans.append([r.topRow(), r.leftColumn(), r.rowCount(), r.columnCount()]) return if len(self.table.selectedRanges()) > 1: tr = 99999 br = 0 lc = 99999 rc = 0 for r in self.table.selectedRanges(): if r.topRow() < tr: tr = r.topRow() if r.bottomRow() > br: br = r.bottomRow() if r.leftColumn() < lc: lc = r.leftColumn() if r.rightColumn() > rc: rc = r.rightColumn() if (rc >= lc) and (br >= tr): self.table.setSpan(tr, lc, (br - tr) + 1, (rc - lc) + 1) self.spans.append([tr, lc, (br - tr) + 1, (rc - lc) + 1]) def removeSpan(self): for r in self.table.selectedRanges(): if r.rowCount() * r.columnCount() == 1: if ( self.table.rowSpan(r.topRow(), r.leftColumn()) > 1 or self.table.columnSpan(r.topRow(), r.leftColumn()) > 1 ): self.table.setSpan(r.topRow(), r.leftColumn(), 1, 1) f = None for i, s in enumerate(self.spans): if (s[0] == r.topRow()) and (s[1] == r.leftColumn()): f = i break if f is not None: self.spans.pop(f) def editHorizontalHeader(self, index): val, ok = QtGui.QInputDialog.getText(None, "Edit size", "New size") if ok: self.table.setHorizontalHeaderItem(index, QtGui.QTableWidgetItem(val)) def editVerticalHeader(self, index): val, ok = QtGui.QInputDialog.getText(None, "Edit size", "New size") if ok: self.table.setVerticalHeaderItem(index, QtGui.QTableWidgetItem(val)) def setWidth(self, d): self.width = d def setHeight(self, d): self.height = d def accept(self): self.obj.Width = self.width self.obj.Height = self.height self.obj.Rows = self.table.rowCount() self.obj.Columns = self.table.columnCount() self.obj.RowSize = [ FreeCAD.Units.Quantity(self.table.verticalHeaderItem(i).text()).Value for i in range(self.table.rowCount()) ] self.obj.ColumnSize = [ FreeCAD.Units.Quantity(self.table.horizontalHeaderItem(i).text()).Value for i in range(self.table.columnCount()) ] self.obj.Spans = [str(s)[1:-1] for s in self.spans] FreeCAD.ActiveDocument.recompute() FreeCADGui.ActiveDocument.resetEdit() return True def reject(self): FreeCADGui.ActiveDocument.resetEdit() return True