BIM: fix ArchRebar default name (#21681)
* BIM: ArchRebar, only import GUI modules if the GUI is up * BIM: ArchRebar, add tests * BIM: Arch.makeRebar, fix rebar default name * BIM: Arch.makeRebar, add type hints * BIM: Arch.makeRebar, add code comments * BIM: Arch.makeRebar, expand docstring
This commit is contained in:
@@ -52,6 +52,7 @@ __author__ = "Yorik van Havre"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
import FreeCAD
|
||||
from typing import Optional
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
@@ -895,44 +896,100 @@ def makeProject(sites=None, name=None):
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def makeRebar(baseobj=None, sketch=None, diameter=None, amount=1, offset=None, name=None):
|
||||
def makeRebar(
|
||||
baseobj: Optional[FreeCAD.DocumentObject] = None,
|
||||
sketch: Optional[FreeCAD.DocumentObject] = None,
|
||||
diameter: Optional[float] = None,
|
||||
amount: int = 1,
|
||||
offset: Optional[float] = None,
|
||||
name: Optional[str] = None
|
||||
) -> Optional[FreeCAD.DocumentObject]:
|
||||
"""
|
||||
Creates a reinforcement bar object.
|
||||
Creates a reinforcement bar (rebar) object.
|
||||
|
||||
The rebar's geometry is typically defined by a `sketch` object (e.g., a Sketcher::SketchObject
|
||||
or a Draft.Wire). This sketch represents the path of a single bar. The `amount` and `spacing`
|
||||
(calculated by the object) properties then determine how many such bars are created and
|
||||
distributed.
|
||||
|
||||
The `baseobj` usually acts as the structural host for the rebar. The rebar's distribution (e.g.,
|
||||
spacing, direction) can be calculated relative to this host object's dimensions if a `Host` is
|
||||
assigned and the rebar logic uses it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
baseobj : Part::FeaturePython, optional
|
||||
The structural object to host the rebar. Defaults to None.
|
||||
sketch : Part::FeaturePython, optional
|
||||
The sketch defining the rebar profile. Defaults to None.
|
||||
baseobj : FreeCAD.DocumentObject, optional
|
||||
The structural object to host the rebar (e.g., an ArchStructure._Structure created with
|
||||
`Arch.makeStructure()`). If provided with `sketch`, it's set as `rebar.Host`. If provided
|
||||
*without* a `sketch`, `rebar.Shape` is set from `baseobj.Shape`, and `rebar.Host` remains
|
||||
None. Defaults to None.
|
||||
sketch : FreeCAD.DocumentObject, optional
|
||||
An object (e.g., "Sketcher::SketchObject") whose shape defines the rebar's path. Assigned to
|
||||
`rebar.Base`. If the sketch is attached to `baseobj` before calling this function (e.g. for
|
||||
positioning purposes), this function may clear that specific attachment to avoid conflicts,
|
||||
as the rebar itself will be hosted. Defaults to None.
|
||||
diameter : float, optional
|
||||
The diameter of the rebar. Defaults to None.
|
||||
The diameter of the rebar. If None, uses Arch preferences ("RebarDiameter"). Defaults to
|
||||
None.
|
||||
amount : int, optional
|
||||
The number of rebars. Defaults to 1.
|
||||
The number of rebar instances. Defaults to 1.
|
||||
offset : float, optional
|
||||
The offset distance for the rebar. Defaults to None.
|
||||
Concrete cover distance, sets `rebar.OffsetStart` and `rebar.OffsetEnd`. If None, uses Arch
|
||||
preferences ("RebarOffset"). Defaults to None.
|
||||
name : str, optional
|
||||
The name to assign to the created rebar. Defaults to None.
|
||||
The user-visible name (Label) for the rebar. If None, defaults to "Rebar". Defaults to None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Part::FeaturePython
|
||||
The created rebar object.
|
||||
FreeCAD.DocumentObject or None
|
||||
The created rebar object, or None if creation fails.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import FreeCAD, Arch, Part, Sketcher
|
||||
>>> doc = FreeCAD.newDocument()
|
||||
>>> # Create a host structure (e.g., a concrete beam)
|
||||
>>> beam = Arch.makeStructure(length=2000, width=200, height=300)
|
||||
>>> doc.recompute() # Ensure beam's shape is ready
|
||||
>>>
|
||||
>>> # Create a sketch for the rebar path
|
||||
>>> rebar_sketch = doc.addObject('Sketcher::SketchObject')
|
||||
>>> # For positioning, attach the sketch to a face of the beam *before* makeRebar
|
||||
>>> # Programmatically select a face (e.g., the first one)
|
||||
>>> # For stable scripts, select faces by more reliable means
|
||||
>>> rebar_sketch.AttachmentSupport = (beam, ['Face1']) # Faces are 1-indexed
|
||||
>>> rebar_sketch.MapMode = "FlatFace"
|
||||
>>> # Define sketch geometry relative to the attached face's plane
|
||||
>>> rebar_sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(25, 25, 0),
|
||||
... FreeCAD.Vector(1975, 25, 0)), False)
|
||||
>>> doc.recompute() # Recompute sketch after geometry and attachment
|
||||
>>>
|
||||
>>> # Create the rebar object, linking it to the beam and using the sketch
|
||||
>>> rebar_obj = Arch.makeRebar(baseobj=beam, sketch=rebar_sketch, diameter=12,
|
||||
... amount=4, offset=25)
|
||||
>>> doc.recompute() # Trigger rebar's geometry calculation
|
||||
"""
|
||||
rebar = _initializeArchObject(
|
||||
"Part::FeaturePython",
|
||||
baseClassName="_Rebar",
|
||||
internalName="Rebar",
|
||||
defaultLabel=name if name else translate("Arch", "Project"),
|
||||
defaultLabel=name if name else translate("Arch", "Rebar"),
|
||||
moduleName="ArchRebar",
|
||||
viewProviderName="_ViewProviderRebar",
|
||||
)
|
||||
|
||||
# Initialize all relevant properties
|
||||
if baseobj and sketch:
|
||||
# Case 1: both the structural element (base object) and a sketch defining the shape and path
|
||||
# of a single rebar strand are provided. This is the most common scenario.
|
||||
if hasattr(sketch, "AttachmentSupport"):
|
||||
if sketch.AttachmentSupport:
|
||||
# If the sketch is already attached to the base object, remove that attachment.
|
||||
# Support two AttachmentSupport (PropertyLinkList) formats:
|
||||
# 1. Tuple: (baseobj, subelement)
|
||||
# 2. Direct object: baseobj
|
||||
# TODO: why is the list format not checked for here?
|
||||
# ~ 3. List: [baseobj, subelement] ~
|
||||
if isinstance(sketch.AttachmentSupport, tuple):
|
||||
if sketch.AttachmentSupport[0] == baseobj:
|
||||
sketch.AttachmentSupport = None
|
||||
@@ -943,12 +1000,15 @@ def makeRebar(baseobj=None, sketch=None, diameter=None, amount=1, offset=None, n
|
||||
sketch.ViewObject.hide()
|
||||
rebar.Host = baseobj
|
||||
elif not baseobj and sketch:
|
||||
# A obj could be based on a wire without the existence of a Structure
|
||||
# Case 2: standalone rebar strand defined by a sketch, not attached to any structural
|
||||
# element.
|
||||
rebar.Base = sketch
|
||||
if FreeCAD.GuiUp:
|
||||
sketch.ViewObject.hide()
|
||||
rebar.Host = None
|
||||
elif baseobj and not sketch:
|
||||
# Case 3: rebar strand defined by the shape of a structural element (base object). The
|
||||
# base object becomes the rebar.
|
||||
rebar.Shape = baseobj.Shape
|
||||
rebar.Diameter = diameter if diameter else params.get_param_arch("RebarDiameter")
|
||||
rebar.Amount = amount
|
||||
|
||||
@@ -49,6 +49,11 @@ if FreeCAD.GuiUp:
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCADGui
|
||||
from draftutils.translate import translate
|
||||
# TODO: check if this import is still needed, and if so, whether
|
||||
# it can be moved made conditional on the GUI being loaded
|
||||
# for Rebar addon compatibility
|
||||
from bimcommands import BimRebar
|
||||
_CommandRebar = BimRebar.Arch_Rebar
|
||||
else:
|
||||
# \cond
|
||||
def translate(ctxt,txt):
|
||||
@@ -57,10 +62,6 @@ else:
|
||||
return txt
|
||||
# \endcond
|
||||
|
||||
# for Rebar addon compatibility
|
||||
from bimcommands import BimRebar
|
||||
_CommandRebar = BimRebar.Arch_Rebar
|
||||
|
||||
|
||||
class _Rebar(ArchComponent.Component):
|
||||
|
||||
|
||||
@@ -22,18 +22,152 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Arch
|
||||
import Part
|
||||
from bimtests import TestArchBase
|
||||
|
||||
class TestArchRebar(TestArchBase.TestArchBase):
|
||||
|
||||
# TODO: remove NOT_ prefix once it is understood why Arch.makeRebar fails
|
||||
# Check https://wiki.freecad.org/Arch_Rebar#Scripting
|
||||
def NOT_test_makeRebar(self):
|
||||
"""Test the makeRebar function."""
|
||||
operation = "Testing makeRebar function"
|
||||
self.printTestMessage(operation)
|
||||
def _create_sketch(self, add_line=True, length=500.0):
|
||||
"""Helper function to create a basic sketch."""
|
||||
sketch = self.document.addObject("Sketcher::SketchObject")
|
||||
if add_line:
|
||||
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(0,0,0),
|
||||
FreeCAD.Vector(length,0,0)), False)
|
||||
self.document.recompute()
|
||||
return sketch
|
||||
|
||||
def _create_host_structure(self, length=1000.0, width=200.0, height=300.0):
|
||||
"""Helper function to create a basic host structure."""
|
||||
host = Arch.makeStructure(length=length, width=width, height=height)
|
||||
self.document.recompute()
|
||||
return host
|
||||
|
||||
def test_makeRebar_default_name(self):
|
||||
"""Test makeRebar creates rebar with default 'Rebar' Label.
|
||||
|
||||
See https://github.com/FreeCAD/FreeCAD/issues/21670
|
||||
"""
|
||||
sketch = self._create_sketch()
|
||||
rebar = Arch.makeRebar(sketch=sketch, diameter=6)
|
||||
self.document.recompute()
|
||||
|
||||
rebar = Arch.makeRebar(diameter=16, amount=5, name="TestRebar")
|
||||
self.assertIsNotNone(rebar, "makeRebar failed to create a rebar object.")
|
||||
self.assertEqual(rebar.Label, "TestRebar", "Rebar label is incorrect.")
|
||||
self.assertEqual(rebar.Label, "Rebar", "Rebar default label should be 'Rebar'.")
|
||||
self.assertTrue(rebar.Name.startswith("Rebar"),
|
||||
"Rebar internal name should start with 'Rebar'.")
|
||||
|
||||
def test_makeRebar_custom_name(self):
|
||||
"""Test makeRebar with a custom name sets Label and Mark correctly."""
|
||||
sketch = self._create_sketch()
|
||||
custom_name = "MyCustomRebar"
|
||||
rebar = Arch.makeRebar(sketch=sketch, diameter=16, amount=5, name=custom_name)
|
||||
self.document.recompute()
|
||||
|
||||
self.assertIsNotNone(rebar, "makeRebar failed to create a rebar object.")
|
||||
self.assertEqual(rebar.Label, custom_name, "Rebar label is incorrect.")
|
||||
self.assertEqual(rebar.Mark, custom_name,
|
||||
"Rebar mark should match label by default.")
|
||||
|
||||
def test_makeRebar_with_sketch_and_host(self):
|
||||
"""Test makeRebar with both sketch and host object links them correctly."""
|
||||
sketch_len = 800.0
|
||||
host_len = 1000.0
|
||||
sketch = self._create_sketch(length=sketch_len)
|
||||
host = self._create_host_structure(length=host_len)
|
||||
|
||||
rebar = Arch.makeRebar(baseobj=host, sketch=sketch, diameter=10, amount=3, offset=20)
|
||||
self.document.recompute()
|
||||
|
||||
self.assertIsNotNone(rebar, "makeRebar failed.")
|
||||
self.assertEqual(rebar.Host, host, "Rebar.Host is not set correctly.")
|
||||
self.assertEqual(rebar.Base, sketch, "Rebar.Base (sketch) is not set correctly.")
|
||||
self.assertAlmostEqual(rebar.Diameter.Value, 10.0, delta=1e-9)
|
||||
self.assertEqual(rebar.Amount, 3)
|
||||
self.assertAlmostEqual(rebar.OffsetStart.Value, 20.0, delta=1e-9)
|
||||
|
||||
self.assertTrue(hasattr(rebar, "Shape"), "Rebar should have a Shape attribute.")
|
||||
self.assertIsNotNone(rebar.Shape, "Rebar.Shape should not be None after recompute.")
|
||||
self.assertTrue(rebar.Shape.isValid(), "Rebar.Shape should be valid.")
|
||||
self.assertGreater(rebar.Shape.Length, 0, "Rebar shape seems to have no length.")
|
||||
self.assertGreater(rebar.TotalLength.Value, 0,
|
||||
"Rebar total length should be greater than 0.")
|
||||
|
||||
def test_makeRebar_with_sketch_only(self):
|
||||
"""Test makeRebar with only sketch results in no host and valid geometry."""
|
||||
sketch = self._create_sketch()
|
||||
rebar = Arch.makeRebar(sketch=sketch, diameter=8)
|
||||
self.document.recompute()
|
||||
|
||||
self.assertIsNotNone(rebar, "makeRebar failed.")
|
||||
self.assertIsNone(rebar.Host, "Rebar.Host should be None.")
|
||||
self.assertEqual(rebar.Base, sketch, "Rebar.Base (sketch) is not set correctly.")
|
||||
self.assertAlmostEqual(rebar.Diameter.Value, 8.0, delta=1e-9)
|
||||
self.assertIsNotNone(rebar.Shape)
|
||||
self.assertTrue(rebar.Shape.isValid())
|
||||
self.assertGreater(rebar.Shape.Length, 0)
|
||||
|
||||
def test_makeRebar_with_baseobj_only_shape_conversion(self):
|
||||
"""Test makeRebar with baseobj only copies shape."""
|
||||
host_shape_provider = self._create_host_structure()
|
||||
rebar = Arch.makeRebar(baseobj=host_shape_provider)
|
||||
self.document.recompute()
|
||||
|
||||
self.assertIsNotNone(rebar, "makeRebar failed.")
|
||||
self.assertIsNone(rebar.Host, "Rebar.Host should be None in this mode.")
|
||||
self.assertIsNone(rebar.Base, "Rebar.Base should be None in this mode.")
|
||||
self.assertIsNotNone(rebar.Shape, "Rebar.Shape should be set from baseobj.")
|
||||
self.assertTrue(rebar.Shape.isValid())
|
||||
self.assertGreater(rebar.Shape.Volume, 0,
|
||||
"Converted rebar shape should have volume.")
|
||||
|
||||
def test_makeRebar_properties_set_correctly(self):
|
||||
"""Test makeRebar sets dimensional and calculated properties correctly."""
|
||||
sketch = self._create_sketch(length=900)
|
||||
host = self._create_host_structure(length=1200)
|
||||
|
||||
rebar = Arch.makeRebar(
|
||||
baseobj=host,
|
||||
sketch=sketch,
|
||||
diameter=12.5,
|
||||
amount=7,
|
||||
offset=30.0
|
||||
)
|
||||
# Default label "Rebar" will be used, and Mark will also be "Rebar"
|
||||
self.document.recompute()
|
||||
|
||||
self.assertEqual(rebar.Mark, "Rebar")
|
||||
self.assertAlmostEqual(rebar.Diameter.Value, 12.5, delta=1e-9)
|
||||
self.assertEqual(rebar.Amount, 7)
|
||||
self.assertAlmostEqual(rebar.OffsetStart.Value, 30.0, delta=1e-9)
|
||||
self.assertAlmostEqual(rebar.OffsetEnd.Value, 30.0, delta=1e-9)
|
||||
self.assertEqual(rebar.Host, host)
|
||||
self.assertEqual(rebar.Base, sketch)
|
||||
|
||||
self.assertTrue(hasattr(rebar, "Length"), "Rebar should have Length property.")
|
||||
self.assertTrue(hasattr(rebar, "TotalLength"), "Rebar should have TotalLength property.")
|
||||
self.assertAlmostEqual(rebar.Length.Value, sketch.Shape.Length, delta=1e-9,
|
||||
msg="Single rebar length should match sketch length.")
|
||||
expected_total_length = rebar.Length.Value * rebar.Amount
|
||||
self.assertAlmostEqual(rebar.TotalLength.Value, expected_total_length, delta=1e-9,
|
||||
msg="TotalLength calculation is incorrect.")
|
||||
|
||||
def test_makeRebar_no_sketch_no_baseobj(self):
|
||||
"""Test makeRebar with no sketch/baseobj creates object with no meaningful shape."""
|
||||
rebar = Arch.makeRebar(diameter=10, amount=1)
|
||||
self.document.recompute()
|
||||
|
||||
self.assertIsNotNone(rebar,
|
||||
"makeRebar should create an object even with minimal inputs.")
|
||||
self.assertIsNone(rebar.Base, "Rebar.Base should be None.")
|
||||
self.assertIsNone(rebar.Host, "Rebar.Host should be None.")
|
||||
self.assertAlmostEqual(rebar.Diameter.Value, 10.0, delta=1e-9)
|
||||
self.assertEqual(rebar.Amount, 1)
|
||||
|
||||
# A Part::FeaturePython object always has a .Shape attribute.
|
||||
# For a rebar with no Base, its execute() returns early, so no geometry is made.
|
||||
# The Shape should be the default null/empty shape.
|
||||
self.assertIsNotNone(rebar.Shape, "Rebar object should always have a Shape attribute.")
|
||||
self.assertTrue(rebar.Shape.isNull(),
|
||||
"Rebar.Shape should be a null shape if no Base is provided.")
|
||||
Reference in New Issue
Block a user