BIM: add task panel box for wall options (#26758)
* BIM: add Wall options task box to ArchWall's edit task panel * BIM: update Align property live on the task box * BIM: make wall options cancellable * BIM: make the Component task box actions reversible, in particular debasing a wall * BIM: implement baseless walls creation (#24595) * BIM: Implement smart base removal for Walls Previously, removing the Base object from an Arch Wall would cause the wall to reset its position to the document origin and could lead to unintended geometric changes for complex walls. This commit introduces a "smart debasing" mechanism integrated into the Component Task Panel's "Remove" button: - For walls based on a single straight line, the operation now preserves the wall's global position and parametric `Length`, making it an independent object. - For walls with complex bases (multi-segment, curved), a warning dialog is now presented to the user, explaining the consequences (shape alteration and position reset) before allowing the operation to proceed. This is supported by new API functions `Arch.is_debasable()` and `Arch.debaseWall()`, which contain the core logic for the feature. Fixes: https://github.com/FreeCAD/FreeCAD/issues/24453 * BIM: Move wall debasing logic into ArchWall proxy The logic for handling the removal of a wall's base object was previously implemented directly within the generic `ComponentTaskPanel` in `ArchComponent.py`. This created a tight coupling, forcing the generic component UI to have specific knowledge about the `ArchWall` type. This commit refactors the implementation to follow a more object-oriented and polymorphic design: 1. A new overridable method, `handleComponentRemoval(subobject)`, has been added to the base `ArchComponent` proxy class. Its default implementation maintains the standard removal behavior. 2. The `_Wall` proxy class in `ArchWall.py` now overrides this method. All wall-specific debasing logic, including the eligibility check and the user-facing warning dialog, now resides entirely within this override. 3. The `ComponentTaskPanel.removeElement` method has been simplified. It is now a generic dispatcher that calls `handleComponentRemoval` on the proxy of the object being edited, with no specific knowledge of object types. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * BIM: Add interactive creation of baseless walls Introduce a new workflow for creating Arch Walls without a dependency on a baseline object (e.g., a Draft Line). - The `Arch_Wall` command is enhanced with a "No baseline" mode, controlled by a new "Walls baseline" preference, allowing users to create placement-driven walls directly in the 3D view. - The existing `debaseWall` function has been refactored for correctness and consistency with the new baseless wall geometry. Co-authored-by: Yorik van Havre <yorik@uncreated.net> * BIM: Refactor structure of the Arch Wall command Refactor the `Arch_Wall` GUI command (`BimWall.py`) for improved readability, maintainability, and architectural clarity. - A `WallBaselineMode` Enum is introduced to replace the original integer values, making the code self-documenting. - The monolithic `create_wall` method is broken down into smaller, single-responsibility helper functions for each creation mode. - The `addDefault` method has been removed, with its logic integrated into the new structure. * BIM: Add Draft Stretch support for baseless walls This commit makes the new baseless Arch Walls graphically editable using the `Draft_Stretch` tool. - An API for stretching (`calc_endpoints` and `set_from_endpoints`) has been added to the `ArchWall` proxy. - The `Draft_Stretch` tool is now aware of baseless walls and calls this new proxy API to perform the stretch operation, enabling users to stretch them. Co-authored-by: Yorik van Havre <yorik@uncreated.net> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * BIM: address CodeQL warnings * BIM: Fix wall alignment for GUI creation of baseless walls Fixes an issue whereby creating a baseless wall from the GUI would ignore the selected `Align` property, always resulting in a center-aligned wall. - The underlying geometry generation for baseless walls now correctly honors the `Align` property passed by the GUI and API. - To ensure predictable behavior, the implementation uses the same geometric convention as walls built from a base object, making the `Align` property work uniformly for all wall types. - This also corrects the behavior of the `Arch.makeWall` function for baseless walls. - Update unit tests to test wall alignment. * BIM: Refactor wall geometry generation for improved clarity and maintainability Improves the internal logic for wall geometry creation, addressing CodeQL warnings and enhancing overall maintainability without changing external behavior. - The `build_base_from_scratch` method is refactored to unify the separate logic paths for single- and multi-layer walls, reducing code duplication. - A local helper function is introduced to create face geometry, for better modularity and readability. - In the `_Wall.execute` method, the control flow that relied on implicit type checking is replaced with an explicit strategy pattern for fusing solids, making the logic more robust. - Variable names are made more descriptive. - A NumPy-style docstring is added to better document the function. * Draft: fix stretching of rotated baseless walls * BIM: add unit test for stretching baseless walls * BIM: add regression tests for working-plane-relative coordinates and reuse of base sketches * BIM: Fix baseless wall creation to respect the working plane Corrects an issue where baseless walls were created using global coordinates instead of being relative to the active Draft working plane. The calculated local placement of the wall is now correctly transformed into the global coordinate system by multiplying it with the working plane's placement. * BIM: Ensure unique baselines for subsequent wall creation Fixes a bug where creating multiple walls with baselines would incorrectly reuse the same underlying Sketch or Draft Line object. The object retrieval logic after the `doCommand` call now correctly uses `ActiveObject` to get a reliable reference to the new object instead of relying on a hardcoded name. * BIM: Make the wall's base object label translatable * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * BIM: use singular for consistency with other labels Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com> * Fix typo * BIM: address reviewer's comments about improving object reference passing between Python and FreeCAD contexts, and functions * BIM: remove defensive programming: the callback is only executed as a result of a user's GUI action * BIM: use the params API to define WallBaseline parameter * BIM: add Arch Wall tests for joining wall logic * BIM: add joining logic * BIM: re-add ArchSketch support * BIM: re-add multimaterial support on wall creation * BIM: address CodeQL warning, remove module duplication * BIM: fix check for SketchArch module when creating sketch-based walls --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yorik van Havre <yorik@uncreated.net> Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com> * CI: upgrade Ubuntu native build to 24.04 * CI: reenable Ubuntu native build * CI: abandon PySide6 pip approach, try KDE Neon repos * BIM: fix mistake in casting return value * BIM: add wall label to better identify transaction in the Undo stack * BIM: remove superfluous statement * BIM: add explanatory comment about additional transaction pending flag * BIM: Refactor Wall transaction logic to use explicit argument passing This replaces the `Proxy.InTransaction` flag mechanism with a cleaner transaction architecture based on explicit argument passing. The motivation for managing the transaction mechanism is that previously, removing a Wall's base object via the Task Panel triggered `debaseWall`, which opened and committed its own transaction immediately. This nested commit finalized the document state prematurely, rendering the Task Panel's "Cancel" button ineffective. The Task Panel now owns the transaction lifecycle for the editing session. It propagates a `manage_transaction=False` flag down to the logic layer, preventing nested transactions from committing prematurely. Key changes: - Arch.py: `debaseWall` now accepts `manage_transaction` (default True). Setting it to False allows the Task Panel to disable the nested transaction and include the debasing operation into its own transaction context instead. - ArchComponent.py: - `ComponentTaskPanel` now manages the transaction lifecycle (Open in init, Commit in accept, Abort in reject). - `ComponentTaskPanel.removeElement` now passes `manage_transaction=False` when calling the object's `handleComponentRemoval` proxy method. - Updated `Component.handleComponentRemoval` signature to accept the `manage_transaction` argument. - ArchWall.py: - Updated `_Wall.handleComponentRemoval` to pass the `manage_transaction` flag to `debaseWall`. - Cleaned up `WallTaskPanel` by removing the deprecated `InTransaction` logic and redundant overrides. * BIM: provide immediate visual feedback on additions and subtractions * BIM: remove low-level API transaction management * BIM: Refactor transaction management in ComponentTaskPanel The C++ GUI layer implicitly manages the transaction lifecycle when entering and exiting edit mode. Explicitly opening a transaction in init and committing in accept() is redundant, as the backend establishes the transaction name and performs the final commit during the resetEdit() cleanup phase. The reject() method retains an explicit abortTransaction() call to signal a rollback, which prevents the backend from committing session changes by default. Docstrings are added to clarify this implicit interaction between the Python UI and the C++ document management logic. * Wall Options in title case * QtGui.QApplication.translate -> translate --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yorik van Havre <yorik@uncreated.net> Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com>
This commit is contained in:
@@ -2231,7 +2231,6 @@ def debaseWall(wall):
|
||||
return False
|
||||
|
||||
doc = wall.Document
|
||||
doc.openTransaction(f"Debase Wall: {wall.Label}")
|
||||
|
||||
try:
|
||||
# --- Calculation of the final placement ---
|
||||
@@ -2296,11 +2295,8 @@ def debaseWall(wall):
|
||||
doc.recompute()
|
||||
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
FreeCAD.Console.PrintError(f"Error debasing wall '{wall.Label}': {e}\n")
|
||||
return False
|
||||
finally:
|
||||
doc.commitTransaction()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -2092,6 +2092,10 @@ class ComponentTaskPanel:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the task panel. The transaction context is implicitly opened by the C++ layer
|
||||
when entering edit mode.
|
||||
"""
|
||||
# the panel has a tree widget that contains categories
|
||||
# for the subcomponents, such as additions, subtractions.
|
||||
# the categories are shown only if they are not empty.
|
||||
@@ -2176,6 +2180,8 @@ class ComponentTaskPanel:
|
||||
)
|
||||
self.update()
|
||||
|
||||
self.doc = FreeCAD.ActiveDocument
|
||||
|
||||
def isAllowedAlterSelection(self):
|
||||
"""Indicate whether this task dialog allows other commands to modify
|
||||
the selection while it is open.
|
||||
@@ -2201,9 +2207,9 @@ class ComponentTaskPanel:
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
"""Add the standard ok button."""
|
||||
"""Add the standard Ok/Cancel buttons."""
|
||||
|
||||
return QtGui.QDialogButtonBox.Ok
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
def check(self, wid, col):
|
||||
"""This method is run as the callback when the user selects an item in the tree.
|
||||
@@ -2319,6 +2325,7 @@ class ComponentTaskPanel:
|
||||
mod = a
|
||||
for o in FreeCADGui.Selection.getSelection():
|
||||
addToComponent(self.obj, o, mod)
|
||||
self.obj.recompute()
|
||||
self.update()
|
||||
|
||||
def removeElement(self):
|
||||
@@ -2340,18 +2347,29 @@ class ComponentTaskPanel:
|
||||
# Fallback for older proxies that might not have the method
|
||||
removeFromComponent(self.obj, element_to_remove)
|
||||
|
||||
self.obj.recompute()
|
||||
self.update()
|
||||
|
||||
def accept(self):
|
||||
"""This method runs as a callback when the user selects the ok button.
|
||||
|
||||
Recomputes the document, and leave edit mode.
|
||||
"""
|
||||
|
||||
The transaction is implicitly committed by the C++ layer during resetEdit.
|
||||
"""
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
FreeCADGui.ActiveDocument.resetEdit()
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""
|
||||
Aborts the edit session. An explicit abort is required to prevent the C++ layer from
|
||||
committing changes during resetEdit.
|
||||
"""
|
||||
self.doc.abortTransaction()
|
||||
FreeCADGui.ActiveDocument.resetEdit()
|
||||
return True
|
||||
|
||||
def editObject(self, wid, col):
|
||||
"""This method is run when the user double clicks on an item in the tree widget.
|
||||
|
||||
|
||||
@@ -1780,6 +1780,75 @@ class _Wall(ArchComponent.Component):
|
||||
return base_faces, placement
|
||||
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
|
||||
class WallTaskPanel(ArchComponent.ComponentTaskPanel):
|
||||
def __init__(self, obj):
|
||||
ArchComponent.ComponentTaskPanel.__init__(self)
|
||||
self.obj = obj
|
||||
self.wallWidget = QtGui.QWidget()
|
||||
self.wallWidget.setWindowTitle(translate("Arch", "Wall Options"))
|
||||
|
||||
layout = QtGui.QFormLayout(self.wallWidget)
|
||||
|
||||
self.length = FreeCADGui.UiLoader().createWidget("Gui::InputField")
|
||||
self.length.setProperty("unit", "mm")
|
||||
self.length.setText(obj.Length.UserString)
|
||||
layout.addRow(translate("Arch", "Length"), self.length)
|
||||
|
||||
self.width = FreeCADGui.UiLoader().createWidget("Gui::InputField")
|
||||
self.width.setProperty("unit", "mm")
|
||||
self.width.setText(obj.Width.UserString)
|
||||
layout.addRow(translate("Arch", "Width"), self.width)
|
||||
|
||||
self.height = FreeCADGui.UiLoader().createWidget("Gui::InputField")
|
||||
self.height.setProperty("unit", "mm")
|
||||
self.height.setText(obj.Height.UserString)
|
||||
layout.addRow(translate("Arch", "Height"), self.height)
|
||||
|
||||
self.alignLayout = QtGui.QHBoxLayout()
|
||||
self.alignLeft = QtGui.QRadioButton(translate("Arch", "Left"))
|
||||
self.alignCenter = QtGui.QRadioButton(translate("Arch", "Center"))
|
||||
self.alignRight = QtGui.QRadioButton(translate("Arch", "Right"))
|
||||
self.alignLayout.addWidget(self.alignLeft)
|
||||
self.alignLayout.addWidget(self.alignCenter)
|
||||
self.alignLayout.addWidget(self.alignRight)
|
||||
self.alignLayout.addStretch()
|
||||
|
||||
self.alignGroup = QtGui.QButtonGroup(self.wallWidget)
|
||||
self.alignGroup.addButton(self.alignLeft)
|
||||
self.alignGroup.addButton(self.alignCenter)
|
||||
self.alignGroup.addButton(self.alignRight)
|
||||
self.alignGroup.buttonClicked.connect(self.setAlign)
|
||||
|
||||
if obj.Align == "Left":
|
||||
self.alignLeft.setChecked(True)
|
||||
elif obj.Align == "Right":
|
||||
self.alignRight.setChecked(True)
|
||||
else:
|
||||
self.alignCenter.setChecked(True)
|
||||
|
||||
layout.addRow(translate("Arch", "Alignment"), self.alignLayout)
|
||||
|
||||
# Wall Options first, then Components (inherited self.form)
|
||||
self.form = [self.wallWidget, self.form]
|
||||
|
||||
def setAlign(self, button):
|
||||
if button == self.alignLeft:
|
||||
self.obj.Align = "Left"
|
||||
elif button == self.alignRight:
|
||||
self.obj.Align = "Right"
|
||||
else:
|
||||
self.obj.Align = "Center"
|
||||
self.obj.recompute()
|
||||
|
||||
def accept(self):
|
||||
self.obj.Length = self.length.text()
|
||||
self.obj.Width = self.width.text()
|
||||
self.obj.Height = self.height.text()
|
||||
return super().accept()
|
||||
|
||||
|
||||
class _ViewProviderWall(ArchComponent.ViewProviderComponent):
|
||||
"""The view provider for the wall object.
|
||||
|
||||
@@ -1965,6 +2034,14 @@ class _ViewProviderWall(ArchComponent.ViewProviderComponent):
|
||||
return "Wireframe"
|
||||
return ArchComponent.ViewProviderComponent.setDisplayMode(self, mode)
|
||||
|
||||
def setEdit(self, vobj, mode):
|
||||
if mode != 0:
|
||||
return None
|
||||
taskd = WallTaskPanel(vobj.Object)
|
||||
taskd.update()
|
||||
FreeCADGui.Control.showDialog(taskd)
|
||||
return True
|
||||
|
||||
def setupContextMenu(self, vobj, menu):
|
||||
|
||||
if FreeCADGui.activeWorkbench().name() != "BIMWorkbench":
|
||||
|
||||
Reference in New Issue
Block a user