* BIM: BIM Report MVP
* BIM: add Arch.selectObjects() API call and tests
* BIM: adopted ArchSchedule spreadsheet update and linking patterns
* BIM: SELECT with dynamic headers and dynamic data population
* BIM: fix deletion, dependencies, and serialization issues
* BIM: transition from individual query to multiple-query statements
* BIM: Mostly fix serialization issues
* BIM: Report editor UI/UX improvements
- Make edits directly on table (Description field only, for now)
- Sync table edits with query editor (Description field only, for now)
- Fix Property group name for spreadsheet object
- Add tooltips for all task panel widgets
- Put Description input and label on the same row
- Edit statement is now a static title
- Open report editor upon creation
- Remove overview table's redundant edit button
- Put report query options in a box
- Open spreadsheet after accepting report changes
* BIM: Aggregarion and grouping - implement GROUP BY with COUNT(*)
* BIM: Aggregation and grouping - implement SUM, MIN, MAX functions
* BIM: Aggregation and grouping - implement validation
* BIM: add reporting presets support
* BIM: align description and query preset inputs vertically
* BIM: write units on their own spreadsheet cell
* BIM: update test suite to new SQL engine behaviour
* BIM: fix various bugs: return values should not be stringized, non-grouped query with mixed extractors, handle nested properties
* BIM: expand Report test suite, fix property count vs count all bug
* BIM: add report engine SQL's IN clause support
* BIM: enable running Report presets tests from the build directory
* BIM: make spreadsheet more useful for analysis, units on headers
* BIM: update BIM Report icons
* BIM: Add key option columns to report overview table
* BIM: add syntax highlighting support to report query editor
* BIM: Add lark build dependency for SQL parser generator
* BIM: Install the generated parser
* BIM: implement TYPE() function
* BIM: simplify function registry, make it OOP
* BIM: add CHILDREN function
* BIM: improve SQL engine error descriptions
* BIM: Implement ORDER BY clause
* BIM: Implement column aliasing (AS...)
* BIM: improve error reporting to pinpoint exact token names
* BIM: implement CONCAT, LOWER, UPPER functions. Improve exception handling
* BIM: refactor to improve initialization readability and maintenance
* BIM: Improve query editor syntax highlighting
* BIM: Address CodeQL warnings
* BIM: Enable scalar functions in WHERE clause and refactor transformer
- Enable the use of scalar functions (e.g., `LOWER`, `TYPE`) in `WHERE` clause comparisons.
- Refactor the Lark transformer to correctly handle `NUMBER`, `NULL`, and `ASTERISK` terminals using dedicated methods.
- Refactor error handling to raise a custom `BimSqlSyntaxError` exception instead of returning values on failure.
- Improve syntax error messages to be more specific and user-friendly by inspecting the failing token.
- Fix regressions in `AggregateFunction` and `TypeFunction` handling that were introduced during the refactoring.
- Update the test suite to assert for specific exceptions, aligning with the new error handling API.
* BIM: Implement arithmetic operations in SELECT clause
- Update grammar with `expr`, `term`, and `factor` rules to support operator precedence.
- Introduce `ArithmeticOperation` class to represent calculations as a recursive tree.
- Add transformer methods to build the calculation tree from the grammar rules.
- Implement recursive evaluation that correctly normalizes `Quantity` and `float` types before calculation.
* BIM: Add CONVERT() function for unit conversions
- Implement a new ConvertFunction class to handle unit conversions in the SELECT clause.
- Leverage the core Quantity.getValueAs() FreeCAD API method to perform the conversion logic.
- Add a unit test to verify both successful conversions and graceful failure on incompatible units.
* BIM: add self-documenting supported SQL syntax helper
* BIM: document internal and external API functions
* BIM: Finalize and rename public ArchSql API, update all consumers
- Rename public API functions for clarity and consistency
(run_query_for_objects -> select, get_sql_keywords -> getSqlKeywords,
etc.).
- Establish Arch.py as the official API facade by exposing all public
SQL functions and exceptions via a safe wildcard import from ArchSql,
governed by __all__.
- Refactor all consumers (ArchReport.py, bimtests/TestArchReport.py) to
use the new function names and access the API exclusively through the
Arch facade (e.g., Arch.select).
- Finalize the error handling architecture:
- Arch.select now logs and re-raises exceptions for consumers to
handle.
- Arch.count remains "safe," catching exceptions and returning an
error tuple for the UI.
- Refactor the test suite to correctly assert the behavior of the new
"unsafe" (select) and "safe" (count) APIs, including verifying
specific exception messages.
* BIM: add results preview on demand on the query editor
* BIM: add documentation quick reference button
* BIM: Handle query errors gracefully in Report preview UI
Refactor the SQL engine's error handling to improve the Report preview
UI. Invalid queries from the "Preview Results" button no longer print
tracebacks to the console. Instead, errors are now caught and displayed
contextually within the preview table, providing clear feedback without
console noise.
- `ArchSql.select()` no longer logs errors to the console; it now only
raises a specific exception on failure, delegating handling to the
caller.
- The `ReportTaskPanel`'s preview button handler now wraps its call to
`Arch.select()` in a `try...except` block.
- Caught exceptions are formatted and displayed in the preview table
widget.
- Add new unit test
* BIM: consolidate internal API into a more semantically and functionally meaningful function
* BIM: Implement two-phase execution for SQL engine performance
Refactors the ArchSql engine to improve performance and internal API
semantics. The `Arch.count()` function, used for UI validation, now
executes faster by avoiding unnecessary data extraction.
This is achieved by splitting query execution into two phases. The first
phase performs fast filtering and grouping (`FROM`/`WHERE`/`GROUP BY`).
The second, slower phase processes the `SELECT` columns. The
`Arch.count()` function now only runs the first phase, while
`Arch.select()` runs both.
- Introduces `_run_query(query_string, mode)` as the mode-driven
internal entry point for the SQL engine
- The `SelectStatement` class is refactored with new internal methods:
`_get_grouped_data()` (Phase 1) and `_process_select_columns()`
(Phase 2).
- `Arch.count()` now uses a fast path that includes a "sample execution"
on a single object to correctly validate the full query without
performance loss.
- The internal `execute()` method of `SelectStatement` is now a
coordinator for the two-phase process.
* BIM: Implement autocompletion in Report SQL editor
Introduces an autocompletion feature for the SQL query editor in the BIM
Report task panel. The completer suggests SQL keywords, functions, and
property names to improve query writing speed and accuracy.
- Adds a custom `SqlQueryEditor` subclass that manages all
autocompletion logic within its `keyPressEvent`.
- The completion model is populated with static SQL keywords and
functions, plus all unique top-level property names dynamically
scanned from every object in the document.
- A blocklist is used to filter out common non-queryable properties
(e.g., `Visibility`, `Proxy`) from the suggestions.
- The editor manually calculates the completer popup's width based on
content (`sizeHintForColumn`) to resolve a Qt rendering issue and
ensure suggestions are always visible.
- The first suggestion is now pre-selected, allowing a single `Tab`
press to accept the completion.
* BIM: remove unused import
* BIM: support SQL comments in queries
* BIM: support non-ASCII characters in queries
* BIM: Allow ORDER BY to accept a comma-separated list of columns for multi-level sorting.
* BIM: fix two-way overview/editor sync
* BIM: refactor to simplify editor modification events
* BIM: add tooltips to overview table
* BIM: add tooltips to query editor, enrich syntax data with signature and snippets
* BIM: implement PARENT function
* BIM: Enable property access on SQL function results
Previously, the SQL engine could only access properties directly from
the main object in a given row. This made it impossible to query
attributes of objects returned by functions, such as getting the name of
a parent with PARENT(*). This commit evolves the SQL grammar to handle
chained member access (.) with correct operator precedence. The parser's
transformer and execution logic were updated to recursively resolve
these chains, enabling more intuitive queries like SELECT
PARENT(*).Label and standard nested property access like Shape.Volume.
* BIM: refactor function registration, internationalize API metadata
* BIM: remove outdated Report alias
* BIM: refactor selectObjects to use latest API, move implementation to ArchSql
* BIM: improve friendly token names for error reporting
* BIM: implement chained functions e.g. PARENT(*).PARENT(*)
* BIM: add further tests for property access, fix bug with non-literal AS clause argument
* BIM: Implement full expression support for GROUP BY
The SQL engine's GROUP BY clause was previously limited to simple
property names, failing on queries that used functions (e.g., `GROUP BY
TYPE(*)`). This has been fixed by allowing the SQL transformer and
validator to correctly process function expressions.
The `SelectStatement.validate()` method now uses a canonical signature
to compare SELECT columns against GROUP BY expressions, ensuring
correctness for both simple properties and complex functions.
New regression tests have also been added to validate `GROUP BY`
functionality with chained functions (PPA) and multiple columns.
* BIM: Make arithmetic engine robust against missing properties
Previously, a query containing an arithmetic expression in the `WHERE`
clause would cause a fatal error if it processed an object that was
missing one of the properties used in the calculation.
This has been fixed by making the `ArithmeticOperation` class NULL-safe.
The engine now correctly handles `None` values returned by property
lookups, propagating them through the calculation as per standard SQL
behavior.
As a result, the engine no longer crashes on such queries. It now
gracefully evaluates all objects, simply filtering out those for which
the arithmetic expression cannot be resolved to a valid number. This
significantly improves the robustness of the query engine.
* BIM: Finalize ORDER BY logic and fix query regressions
- The `ORDER BY` clause is now governed by a single, predictable rule:
it can only refer to column names or aliases that exist in the final
`SELECT` list. The engine's transformer and execution logic have been
updated to enforce and correctly implement this, fixing several test
failures related to sorting by aliases and expressions.
- Updated test suite to align with the new engine rules. Add new
dedicated unit tests that explicitly document the supported (aliased
expression) and unsupported (raw expression) syntax for the `ORDER BY`
clause.
* BIM: Implement backend for pipelined report execution
Adds the backend architecture for multi-step, pipelined queries. The
core SQL engine can now execute a statement against the results of a
previous one, enabling complex sequential filtering.
- The internal `_run_query` function was refactored to be
pipeline-aware, allowing it to operate on a pre-filtered list of
source objects.
- A new `execute_pipeline` orchestrator was added to manage the data
flow between statements, controlled by a new `is_pipelined` flag in
the `ReportStatement` data model.
- The public API was extended with `selectObjectsFromPipeline` for
scripting, and `count()` was enhanced to support contextual validation
for the UI.
Pipelines phase 2
Pipelines phase 3
* BIM: refactor to avoid circular imports
* BIM: Address key CodeQl check errors/notes
* BIM: Refactor Task Panel UI/UX with explicit editing workflow
Refactor the report editor UI to improve usability and prevent data
loss. Editing a statement is now an explicit action, triggered by a new
"Edit Selected" button. This prevents accidental changes and enables a
new transactional workflow with "Save" and "Discard" buttons for each
edit session. A checkbox for "Apply & Next" has been added to streamline
editing multiple statements.
The results preview feature has been redesigned into a self-contained,
closable pane controlled by a "Show Preview" toggle. This pane includes
its own contextual "Refresh" button, reducing UI clutter. All action
button groups have been consistently right-aligned to improve layout and
workflow.
* BIM: Integrate pipeline execution and stabilize UI workflow
Completes the implementation of the pipelined statements feature by
integrating the new backend orchestrator with the ArchReport object. It
also includes a series of critical bug fixes that were discovered during
end-to-end testing, resulting in a more stable user experience.
The primary feature integration consists of refactoring the
_ArchReport.execute method. It now uses the ArchSql.execute_pipeline
generator, enabling the report to correctly process multi-step pipelines
and honor the user-configured data flow.
Severalbugs and regressions were fixed:
- Backend: A major flaw was fixed where FROM-clause functions (like
CHILDREN) were not pipeline-aware. The engine's GROUP BY validator was
also corrected to enforce its original, safer design of not supporting
aliases.
- UI Workflow: A feedback loop that caused the editor cursor to reset
was resolved. The transactional logic for the Save, Discard, and "Save
& Add New" actions was corrected to prevent data loss and ensure
predictable behavior. The Add and Duplicate actions no longer auto-open
the editor, creating a more consistent workflow.
- UI State: Fixed regressions related to the explicit editing model,
including incorrect statement loading (Edit Selected) and state
management when a report is reloaded from a file.
* BIM: add presets manager
* BIM: Fix 'still touched after recompute' bug in some of the tests
* BIM: cleanup tests, fixed presets tests to new presets locations
* BIM: Move test model to its own module for reusability
* BIM: Move GUI tests to their own module
- Disable two of the tests: they pass, but cause a segmentation fault on
teardown. They need to be investigated before enabling them on CI.
* BIM: fix bug in interpreting CONVERT string from GROUP BY
* BIM: Migrate signal connections from lambdas to Qt slots
Refactors all signal connections in the `ReportTaskPanel` to use the
`@QtCore.Slot()` decorator instead of lambda wrappers.
- Resolves CodeQL warning "Unnecessary lambda".
- Adheres to PySide/Qt best practices for signal/slot handling.
- Improves code clarity by using explicit, named methods for
connections.
- Prevents potential runtime `TypeError` exceptions by correctly
managing slot signatures for signals that emit arguments (e.g.,
`clicked(bool)`).
- Introduces simple slot wrappers where necessary to cleanly separate UI
event handling from core logic.
* BIM: Address CodeQl warnings
* BIM: Add CHILDREN_RECURSIVE(subquery, max_depth) SQL function to find all descendants of an object set.
- Create internal _get_bim_type and _is_bim_group helpers to remove all Draft module dependencies from the SQL engine.
- Implement a new _traverse_architectural_hierarchy function using a deque-based BFS algorithm to prevent infinite loops.
- The CHILDREN and CHILDREN_RECURSIVE functions now use the new traversal engine.
- The traversal engine now transparently navigates generic groups but excludes them from the results.
- Correct ParentFunction logic to validate containment by checking the parent's .Group list instead of the child's .InList.
- Add unit tests for recursive traversal, depth limiting, and transparent group handling.
- Update test_group_by_multiple_mixed_columns to match the corrected behavior of the TYPE(*) function.
* BIM: Add __repr__ and __str__ for the ArchReport proxy
* BIM: Align report quantity headers with internal units
- Change default `Quantity` headers to use the property's internal unit
(e.g., `mm`) instead of the user's global preferred unit (e.g., `m`).
- Fixes inconsistency where the header unit (e.g., `m²`) did not match
the raw data's unit (e.g., `mm²`), making the default behavior
predictable.
- Implement by parsing `str(Quantity)` as a workaround for the C++
`Unit::getString()` method not being exposed to the Python API.
- Add a unit test that temporarily changes the global schema to verify
the fix is independent of user preferences.
* BIM: remove dual import ArchSql and from ArchSql import
* BIM: Fix IfcRole => IfcType
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* CI: add Lark as a build dependency
* BIM: Replace QRegExp with QRegularExpression for regex patterns
QRegExp is no longer available in PySide6. Make the code compatible with
both PySide2/Qt5 and PySide6/Qt6.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* CI: move lark dependency to pixi's requirements.host
* BIM: Correct numeric comparisons in SQL WHERE clause
Refactor the `BooleanComparison.evaluate` method to handle numeric and
string-based comparisons separately. Previously, all comparisons were
being performed on string-converted values, causing numerically
incorrect results for `Quantity` objects formatted with "smart" units
(e.g., `"10.0 m"` was incorrectly evaluated as less than `"8000"`).
- The `evaluate` method now normalizes `Quantity` objects to their raw
`.Value` first.
- If both operands are numeric, a direct numerical comparison is
performed for all operators (`=`, `!=`, `>`, `<`, `>=`, `<=`).
- String-based comparisons (for `like`, or as a fallback for other
types) are handled in a separate path.
- Add unit test to lock down this behavior.
* BIM: autocompleter improvements
- Add a trailing space when autocompleting the keywords that need it
- Autocomplete multi-word keywords as a unit
* BIM: Improve live validation user experience
- Do not throw syntax error when in the middle of autocompleting a
keyword
- Introduce typing state and show incomplete query status
* BIM: change double-click action to edit in statements overview
Also allow quick editing the statement description with F2
* BIM: Improve user strings for consistency
* BIM: show count of returned objects in statements table
* BIM: disable preview on invalid queries
* BIM: define slot so that tests can run headless
* BIM: Update unit test to adapt to new behaviour
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2438 lines
89 KiB
Python
2438 lines
89 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2011 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * 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 *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
"""Core API for architectural and Building Information Modeling (BIM) in FreeCAD.
|
|
|
|
Provides tools for creating parametric architectural elements (walls, windows,
|
|
structures) and managing BIM data. Serves as the foundation for both the BIM
|
|
Workbench and third-party extensions.
|
|
|
|
## Features
|
|
- Parametric architectural components (walls, floors, roofs, windows)
|
|
- BIM data support (materials, IFC properties, classification systems)
|
|
- Integration with FreeCAD's core (Part, Draft) and other workbenches
|
|
- Object creation utilities for architectural workflows
|
|
|
|
## Usage
|
|
Designed for:
|
|
1. Internal API for FreeCAD's built-in BIM commands
|
|
2. Public API for add-on developers creating extension macros, workbenches, or
|
|
other specialized BIM tools
|
|
|
|
## Examples
|
|
```python
|
|
import Arch
|
|
wall = Arch.makeWall(length=5000, width=200, height=3000) # mm units
|
|
wall.recompute()
|
|
```
|
|
"""
|
|
__title__ = "FreeCAD Arch API"
|
|
__author__ = "Yorik van Havre"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
import FreeCAD
|
|
from typing import Optional
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
|
|
FreeCADGui.updateLocale()
|
|
|
|
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
|
|
translate = FreeCAD.Qt.translate
|
|
|
|
|
|
# Importing all members from these modules enables us to use them directly by
|
|
# simply importing the Arch module, as if they were part of this module.
|
|
from ArchCommands import *
|
|
from ArchWindowPresets import *
|
|
from ArchSql import *
|
|
|
|
# TODO: migrate this one
|
|
# Currently makeStructure, makeStructuralSystem need migration
|
|
from ArchStructure import *
|
|
|
|
|
|
# make functions
|
|
|
|
|
|
def makeAxis(num=1, size=1000, name=None):
|
|
"""
|
|
Creates an axis set in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
num : int, optional
|
|
The number of axes to create. Defaults to 1.
|
|
size : float, optional
|
|
The interval distance between axes. Defaults to 1000.
|
|
name : str, optional
|
|
The name to assign to the created axis object. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created axis object.
|
|
"""
|
|
import ArchAxis
|
|
|
|
if not FreeCAD.ActiveDocument:
|
|
FreeCAD.Console.PrintError("No active document. Aborting\n")
|
|
return
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Axis")
|
|
obj.Label = name if name else translate("Arch", "Axes")
|
|
ArchAxis._Axis(obj)
|
|
if FreeCAD.GuiUp:
|
|
ArchAxis._ViewProviderAxis(obj.ViewObject)
|
|
if num:
|
|
dist = []
|
|
angles = []
|
|
for i in range(num):
|
|
if i == 0:
|
|
dist.append(0)
|
|
else:
|
|
dist.append(float(size))
|
|
angles.append(float(0))
|
|
obj.Distances = dist
|
|
obj.Angles = angles
|
|
FreeCAD.ActiveDocument.recompute()
|
|
return obj
|
|
|
|
|
|
def makeAxisSystem(axes, name=None):
|
|
"""
|
|
Creates an axis system from the given list of axes.
|
|
|
|
Parameters
|
|
----------
|
|
axes : list of Part::FeaturePython
|
|
A list of axis objects to include in the axis system.
|
|
name : str, optional
|
|
The name to assign to the created axis system. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::FeaturePython
|
|
The created axis system object.
|
|
"""
|
|
import ArchAxisSystem
|
|
|
|
if not isinstance(axes, list):
|
|
axes = [axes]
|
|
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "AxisSystem")
|
|
obj.Label = name if name else translate("Arch", "Axis System")
|
|
ArchAxisSystem._AxisSystem(obj)
|
|
obj.Axes = axes
|
|
if FreeCAD.GuiUp:
|
|
ArchAxisSystem._ViewProviderAxisSystem(obj.ViewObject)
|
|
FreeCAD.ActiveDocument.recompute()
|
|
return obj
|
|
|
|
|
|
def makeBuildingPart(objectslist=None, baseobj=None, name=None):
|
|
"""
|
|
Creates a building part including the given objects in the list.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the building part. Defaults to None.
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the building part. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created building part. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::GeometryPython
|
|
The created building part object.
|
|
"""
|
|
import ArchBuildingPart
|
|
|
|
obj = FreeCAD.ActiveDocument.addObject("App::GeometryPython", "BuildingPart")
|
|
# obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","BuildingPart")
|
|
obj.Label = name if name else translate("Arch", "BuildingPart")
|
|
ArchBuildingPart.BuildingPart(obj)
|
|
obj.IfcType = "Building Element Part"
|
|
if FreeCAD.GuiUp:
|
|
ArchBuildingPart.ViewProviderBuildingPart(obj.ViewObject)
|
|
if objectslist:
|
|
if isinstance(objectslist, (list, tuple)):
|
|
obj.addObjects(objectslist)
|
|
else:
|
|
obj.addObject(objectslist)
|
|
return obj
|
|
|
|
|
|
def makeFloor(objectslist=None, baseobj=None, name=None):
|
|
"""
|
|
Creates a floor/level in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the floor. Defaults to None.
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the floor. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created floor. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::GeometryPython
|
|
The created floor object.
|
|
"""
|
|
obj = makeBuildingPart(objectslist)
|
|
obj.Label = name if name else translate("Arch", "Level")
|
|
obj.IfcType = "Building Storey"
|
|
obj.CompositionType = "ELEMENT"
|
|
return obj
|
|
|
|
|
|
def makeBuilding(objectslist=None, baseobj=None, name=None):
|
|
"""
|
|
Creates a building in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the building. Defaults to None.
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the building. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created building. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::GeometryPython
|
|
The created building object.
|
|
"""
|
|
import ArchBuildingPart
|
|
|
|
obj = makeBuildingPart(objectslist)
|
|
obj.Label = name if name else translate("Arch", "Building")
|
|
obj.IfcType = "Building"
|
|
obj.CompositionType = "ELEMENT"
|
|
t = QT_TRANSLATE_NOOP("App::Property", "The type of this building")
|
|
obj.addProperty("App::PropertyEnumeration", "BuildingType", "Building", t, locked=True)
|
|
obj.BuildingType = ArchBuildingPart.BuildingTypes
|
|
if FreeCAD.GuiUp:
|
|
obj.ViewObject.ShowLevel = False
|
|
obj.ViewObject.ShowLabel = False
|
|
return obj
|
|
|
|
|
|
def make2DDrawing(objectslist=None, baseobj=None, name=None):
|
|
"""
|
|
Creates a 2D drawing view in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the drawing. Defaults to None.
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the drawing. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created drawing. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::GeometryPython
|
|
The created 2D drawing object.
|
|
"""
|
|
obj = makeBuildingPart(objectslist)
|
|
obj.Label = name if name else translate("Arch", "Drawing")
|
|
obj.IfcType = "Annotation"
|
|
obj.ObjectType = "DRAWING"
|
|
obj.setEditorMode("Area", 2)
|
|
obj.setEditorMode("Height", 2)
|
|
obj.setEditorMode("LevelOffset", 2)
|
|
obj.setEditorMode("OnlySolids", 2)
|
|
obj.setEditorMode("HeightPropagate", 2)
|
|
if FreeCAD.GuiUp:
|
|
obj.ViewObject.DisplayOffset = FreeCAD.Placement()
|
|
obj.ViewObject.ShowLevel = False
|
|
return obj
|
|
|
|
|
|
def convertFloors(floor=None):
|
|
"""
|
|
Converts the given floor or building into building parts.
|
|
|
|
Parameters
|
|
----------
|
|
floor : Part::FeaturePython, optional
|
|
The floor or building to convert. If None, all Arch floors in the active document
|
|
are converted. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
"""
|
|
import Draft
|
|
import ArchBuildingPart
|
|
|
|
todel = []
|
|
if floor:
|
|
objset = [floor]
|
|
else:
|
|
objset = FreeCAD.ActiveDocument.Objects
|
|
for obj in objset:
|
|
if Draft.getType(obj) in ["Floor", "Building"]:
|
|
nobj = makeBuildingPart(obj.Group)
|
|
if Draft.getType(obj) == "Floor":
|
|
nobj.IfcType = "Building Storey"
|
|
nobj.CompositionType = "ELEMENT"
|
|
else:
|
|
nobj.IfcType = "Building"
|
|
nobj.CompositionType = "ELEMENT"
|
|
t = QT_TRANSLATE_NOOP("App::Property", "The type of this building")
|
|
nobj.addProperty(
|
|
"App::PropertyEnumeration", "BuildingType", "Building", t, locked=True
|
|
)
|
|
nobj.BuildingType = ArchBuildingPart.BuildingTypes
|
|
label = obj.Label
|
|
for parent in obj.InList:
|
|
if hasattr(parent, "Group"):
|
|
if obj in parent.Group:
|
|
parent.addObject(nobj)
|
|
# g = parent.Group
|
|
# g.append(nobj)
|
|
# parent.Group = g
|
|
todel.append(obj.Name)
|
|
if obj.ViewObject:
|
|
# some bug makes this trigger even efter the object has been deleted...
|
|
obj.ViewObject.Proxy.Object = None
|
|
# in case FreeCAD doesn't allow 2 objs with same label
|
|
obj.Label = obj.Label + " to delete"
|
|
nobj.Label = label
|
|
for n in todel:
|
|
from draftutils import todo
|
|
|
|
todo.ToDo.delay(FreeCAD.ActiveDocument.removeObject, n)
|
|
|
|
|
|
def makeCurtainWall(baseobj=None, name=None):
|
|
"""
|
|
Creates a curtain wall object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the curtain wall. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created curtain wall. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created curtain wall object.
|
|
"""
|
|
curtainWall = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="CurtainWall",
|
|
internalName="CurtainWall",
|
|
defaultLabel=name if name else translate("Arch", "Curtain Wall"),
|
|
viewProviderName="ViewProviderCurtainWall",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
curtainWall.Base = baseobj
|
|
if FreeCAD.GuiUp:
|
|
baseobj.ViewObject.hide()
|
|
|
|
return curtainWall
|
|
|
|
|
|
def makeEquipment(baseobj=None, placement=None, name=None):
|
|
"""
|
|
Creates an equipment object from the given base object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython or Mesh::Feature, optional
|
|
The base object for the equipment. Defaults to None.
|
|
placement : FreeCAD.Placement, optional
|
|
The placement of the equipment. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created equipment. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created equipment object.
|
|
"""
|
|
equipment = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Equipment",
|
|
internalName="Equipment",
|
|
defaultLabel=name if name else translate("Arch", "Equipment"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
if baseobj.isDerivedFrom("Mesh::Feature"):
|
|
equipment.Mesh = baseobj
|
|
else:
|
|
equipment.Base = baseobj
|
|
if placement:
|
|
equipment.Placement = placement
|
|
|
|
if FreeCAD.GuiUp and baseobj:
|
|
baseobj.ViewObject.hide()
|
|
return equipment
|
|
|
|
|
|
def makeFence(section, post, path):
|
|
"""
|
|
Creates a fence object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
section : Part::FeaturePython
|
|
The section profile of the fence.
|
|
post : Part::FeaturePython
|
|
The post profile of the fence.
|
|
path : Part::FeaturePython
|
|
The path along which the fence is created.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created fence object.
|
|
"""
|
|
fence = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Fence",
|
|
internalName="Fence",
|
|
defaultLabel=translate("Arch", "Fence"),
|
|
)
|
|
fence.Section = section
|
|
fence.Post = post
|
|
fence.Path = path
|
|
if FreeCAD.GuiUp:
|
|
import ArchFence
|
|
|
|
ArchFence.hide(section)
|
|
ArchFence.hide(post)
|
|
ArchFence.hide(path)
|
|
return fence
|
|
|
|
|
|
def makeFrame(baseobj, profile, name=None):
|
|
"""Creates a frame object from a base sketch (or any other object containing wires) and a
|
|
profile object (an extrudable 2D object containing faces or closed wires).
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython
|
|
The base object containing wires to define the frame.
|
|
profile : Part::FeaturePython
|
|
The profile object, an extrudable 2D object containing faces or closed wires.
|
|
name : str, optional
|
|
The name to assign to the created frame. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created frame object.
|
|
"""
|
|
frame = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Frame",
|
|
internalName="Frame",
|
|
defaultLabel=name if name else translate("Arch", "Frame"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
frame.Base = baseobj
|
|
if profile:
|
|
frame.Profile = profile
|
|
if FreeCAD.GuiUp:
|
|
profile.ViewObject.hide()
|
|
|
|
return frame
|
|
|
|
|
|
def makeGrid(name=None):
|
|
"""
|
|
Creates a grid object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
name : str, optional
|
|
The name to assign to the created grid. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created grid object.
|
|
"""
|
|
grid = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="ArchGrid",
|
|
internalName="Grid",
|
|
defaultLabel=name if name else translate("Arch", "Grid"),
|
|
moduleName="ArchGrid",
|
|
viewProviderName="ViewProviderArchGrid",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if FreeCAD.GuiUp:
|
|
grid.ViewObject.Transparency = 85
|
|
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
return grid
|
|
|
|
|
|
def makeMaterial(name=None, color=None, transparency=None):
|
|
"""
|
|
Creates a material object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
name : str, optional
|
|
The name to assign to the created material. Defaults to None.
|
|
color : tuple of float, optional
|
|
The RGB color of the material. Defaults to None.
|
|
transparency : float, optional
|
|
The transparency level of the material. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::MaterialObjectPython
|
|
The created material object.
|
|
"""
|
|
material = _initializeArchObject(
|
|
"App::MaterialObjectPython",
|
|
baseClassName="_ArchMaterial",
|
|
internalName="Material",
|
|
defaultLabel=name if name else translate("Arch", "Material"),
|
|
)
|
|
getMaterialContainer().addObject(material)
|
|
|
|
# Initialize all relevant properties
|
|
if color:
|
|
r, g, b = color[:3]
|
|
material.Color = (r, g, b)
|
|
if len(color) > 3:
|
|
alpha = color[3]
|
|
material.Transparency = alpha * 100
|
|
if transparency:
|
|
material.Transparency = transparency
|
|
|
|
return material
|
|
|
|
|
|
def makeMultiMaterial(name=None):
|
|
"""
|
|
Creates a multi-material object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
name : str, optional
|
|
The name to assign to the created multi-material. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::FeaturePython
|
|
The created multi-material object.
|
|
"""
|
|
multimaterial = _initializeArchObject(
|
|
"App::FeaturePython",
|
|
baseClassName="_ArchMultiMaterial",
|
|
internalName="MultiMaterial",
|
|
defaultLabel=name if name else translate("Arch", "MultiMaterial"),
|
|
moduleName="ArchMaterial",
|
|
)
|
|
getMaterialContainer().addObject(multimaterial)
|
|
|
|
return multimaterial
|
|
|
|
|
|
def getMaterialContainer():
|
|
"""
|
|
Returns a group object to store materials in the active document.
|
|
|
|
Returns
|
|
-------
|
|
App::DocumentObjectGroupPython
|
|
The material container object.
|
|
"""
|
|
# Check if a container already exists
|
|
for obj in FreeCAD.ActiveDocument.Objects:
|
|
if obj.Name == "MaterialContainer":
|
|
return obj
|
|
|
|
# If no container exists, create one
|
|
materialContainer = _initializeArchObject(
|
|
"App::DocumentObjectGroupPython",
|
|
baseClassName="_ArchMaterialContainer",
|
|
internalName="MaterialContainer",
|
|
defaultLabel=translate("Arch", "Materials"),
|
|
moduleName="ArchMaterial",
|
|
)
|
|
|
|
return materialContainer
|
|
|
|
|
|
def getDocumentMaterials():
|
|
"""
|
|
Retrieves all material objects in the active document.
|
|
|
|
Returns
|
|
-------
|
|
list of App::MaterialObjectPython
|
|
A list of all material objects in the document.
|
|
"""
|
|
for obj in FreeCAD.ActiveDocument.Objects:
|
|
if obj.Name == "MaterialContainer":
|
|
materials = []
|
|
for o in obj.Group:
|
|
if o.isDerivedFrom("App::MaterialObjectPython"):
|
|
materials.append(o)
|
|
return materials
|
|
return []
|
|
|
|
|
|
def makePanel(baseobj=None, length=0, width=0, thickness=0, placement=None, name=None):
|
|
"""
|
|
Creates a panel element based on the given profile object and the given
|
|
extrusion thickness. If no base object is given, you can also specify
|
|
length and width for a simple cubic object.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base profile object for the panel. Defaults to None.
|
|
length : float, optional
|
|
The length of the panel. Defaults to 0.
|
|
width : float, optional
|
|
The width of the panel. Defaults to 0.
|
|
thickness : float, optional
|
|
The thickness of the panel. Defaults to 0.
|
|
placement : FreeCAD.Placement, optional
|
|
The placement of the panel. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created panel. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created panel object.
|
|
"""
|
|
panel = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Panel",
|
|
internalName="Panel",
|
|
defaultLabel=name if name else translate("Arch", "Panel"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
panel.Base = baseobj
|
|
if FreeCAD.GuiUp:
|
|
panel.Base.ViewObject.hide()
|
|
if width:
|
|
panel.Width = width
|
|
if thickness:
|
|
panel.Thickness = thickness
|
|
if length:
|
|
panel.Length = length
|
|
|
|
return panel
|
|
|
|
|
|
def makePanelCut(panel, name=None):
|
|
"""
|
|
Creates a 2D view of the given panel in the 3D space, positioned at the origin.
|
|
|
|
Parameters
|
|
----------
|
|
panel : Part::FeaturePython
|
|
The panel object to create a 2D view for.
|
|
name : str, optional
|
|
The name to assign to the created panel cut. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created panel cut object.
|
|
"""
|
|
view = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="PanelCut",
|
|
internalName="PanelCut",
|
|
defaultLabel=name if name else translate("Arch", f"View of {panel.Label}"),
|
|
moduleName="ArchPanel",
|
|
viewProviderName="ViewProviderPanelCut",
|
|
)
|
|
view.Source = panel
|
|
return view
|
|
|
|
|
|
def makePanelSheet(panels=[], name=None):
|
|
"""
|
|
Creates a sheet with the given panel cuts in the 3D space, positioned at the origin.
|
|
|
|
Parameters
|
|
----------
|
|
panels : list of Part::FeaturePython, optional
|
|
A list of panel cuts to include in the sheet. Defaults to an empty list.
|
|
name : str, optional
|
|
The name to assign to the created panel sheet. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created panel sheet object.
|
|
"""
|
|
sheet = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="PanelSheet",
|
|
internalName="PanelSheet",
|
|
defaultLabel=name if name else translate("Arch", "PanelSheet"),
|
|
moduleName="ArchPanel",
|
|
viewProviderName="ViewProviderPanelSheet",
|
|
)
|
|
if panels:
|
|
sheet.Group = panels
|
|
return sheet
|
|
|
|
|
|
def makePipe(baseobj=None, diameter=0, length=0, placement=None, name=None):
|
|
"""
|
|
Creates a pipe object from the given base object or specified dimensions.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the pipe. Defaults to None.
|
|
diameter : float, optional
|
|
The diameter of the pipe. Defaults to 0.
|
|
length : float, optional
|
|
The length of the pipe. Defaults to 0.
|
|
placement : FreeCAD.Placement, optional
|
|
The placement of the pipe. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created pipe. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created pipe object.
|
|
"""
|
|
pipe = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_ArchPipe",
|
|
internalName="Pipe",
|
|
defaultLabel=name if name else translate("Arch", "Pipe"),
|
|
viewProviderName="_ViewProviderPipe",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
pipe.Diameter = diameter if diameter else params.get_param_arch("PipeDiameter")
|
|
pipe.Width = pipe.Diameter
|
|
pipe.Height = pipe.Diameter
|
|
|
|
if baseobj:
|
|
pipe.Base = baseobj
|
|
else:
|
|
pipe.Length = length if length else 1000
|
|
|
|
if placement:
|
|
pipe.Placement = placement
|
|
|
|
if FreeCAD.GuiUp:
|
|
if baseobj:
|
|
baseobj.ViewObject.hide()
|
|
|
|
return pipe
|
|
|
|
|
|
def makePipeConnector(pipes, radius=0, name=None):
|
|
"""
|
|
Creates a connector between the given pipes.
|
|
|
|
Parameters
|
|
----------
|
|
pipes : list of Part::FeaturePython
|
|
A list of pipe objects to connect.
|
|
radius : float, optional
|
|
The curvature radius of the connector. Defaults to 0, which uses the diameter of the first
|
|
pipe.
|
|
name : str, optional
|
|
The name to assign to the created connector. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created pipe connector object.
|
|
"""
|
|
pipeConnector = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_ArchPipeConnector",
|
|
internalName="Connector",
|
|
defaultLabel=name if name else translate("Arch", "Connector"),
|
|
moduleName="ArchPipe",
|
|
viewProviderName="_ViewProviderPipe",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
pipeConnector.Pipes = pipes
|
|
if radius:
|
|
pipeConnector.Radius = radius
|
|
elif pipes[0].ProfileType == "Circle":
|
|
pipeConnector.Radius = pipes[0].Diameter
|
|
else:
|
|
pipeConnector.Radius = max(pipes[0].Height, pipes[0].Width)
|
|
|
|
return pipeConnector
|
|
|
|
|
|
def makeProfile(profile=[0, "REC", "REC100x100", "R", 100, 100]):
|
|
"""
|
|
Creates a profile object based on the given profile data.
|
|
|
|
Parameters
|
|
----------
|
|
profile : list, optional
|
|
A list defining the profile data. Defaults to [0, 'REC', 'REC100x100', 'R', 100, 100].
|
|
The list should contain the following elements:
|
|
|
|
0. listOrder: str
|
|
The order of the profile data. Currently not used.
|
|
1. profileSubClass: str
|
|
The subclass of a given profile class (e.g. 'REC' for the 'C' class).
|
|
2. profileName: str
|
|
The name of the profile (e.g., 'REC100x100').
|
|
3. profileClass: str
|
|
The class of the profile (e.g., 'REC', 'C', 'H', etc.).
|
|
4. dimensionsList: int
|
|
A variable set of arguments that define the dimensions of the profile. Their
|
|
interpretation and count depends on the type of profile. Not implemented
|
|
as a list, it's a variable number of arguments within the main profile
|
|
argument. For instance, a C profile will define outside diameter and thickness,
|
|
whereas a H profile will define width, height, web thickness, and flange thickness.
|
|
See https://wiki.freecad.org/Arch_Profile for more details on profile presets.
|
|
|
|
Returns
|
|
-------
|
|
Part::Part2DObjectPython
|
|
The created profile object.
|
|
"""
|
|
import ArchProfile
|
|
|
|
if not FreeCAD.ActiveDocument:
|
|
FreeCAD.Console.PrintError("No active document. Aborting\n")
|
|
return
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython", "Profile")
|
|
|
|
profileName, profileClass = profile[2:4]
|
|
|
|
match profileClass:
|
|
case "C":
|
|
ArchProfile._ProfileC(obj, profile)
|
|
case "H":
|
|
ArchProfile._ProfileH(obj, profile)
|
|
case "R":
|
|
ArchProfile._ProfileR(obj, profile)
|
|
case "RH":
|
|
ArchProfile._ProfileRH(obj, profile)
|
|
case "U":
|
|
ArchProfile._ProfileU(obj, profile)
|
|
case "L":
|
|
ArchProfile._ProfileL(obj, profile)
|
|
case "T":
|
|
ArchProfile._ProfileT(obj, profile)
|
|
case "TSLOT":
|
|
ArchProfile._ProfileTSLOT(obj, profile)
|
|
case _:
|
|
print("Profile not supported")
|
|
|
|
if FreeCAD.GuiUp:
|
|
ArchProfile.ViewProviderProfile(obj.ViewObject)
|
|
|
|
# Initialize all relevant properties
|
|
obj.Label = profileName + "_"
|
|
|
|
return obj
|
|
|
|
|
|
def makeProject(sites=None, name=None):
|
|
"""Create an Arch project.
|
|
|
|
If sites are provided, add them as children of the new project.
|
|
|
|
.. deprecated:: 1.0.0
|
|
|
|
Parameters
|
|
----------
|
|
sites: list of <Part::FeaturePython>, optional
|
|
Sites to add as children of the project. Ultimately this could be
|
|
anything, however.
|
|
name: str, optional
|
|
The label for the project.
|
|
|
|
Returns
|
|
-------
|
|
<Part::FeaturePython>
|
|
The created project.
|
|
|
|
Notes
|
|
-----
|
|
This function is deprecated and will be removed in a future version.
|
|
The NativeIFC project is the new way to create IFC projects.
|
|
"""
|
|
project = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Project",
|
|
internalName="Project",
|
|
defaultLabel=name if name else translate("Arch", "Project"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if sites:
|
|
project.Group = sites
|
|
|
|
return project
|
|
|
|
|
|
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 (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 : 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. If None, uses Arch preferences ("RebarDiameter"). Defaults to
|
|
None.
|
|
amount : int, optional
|
|
The number of rebar instances. Defaults to 1.
|
|
offset : float, optional
|
|
Concrete cover distance, sets `rebar.OffsetStart` and `rebar.OffsetEnd`. If None, uses Arch
|
|
preferences ("RebarOffset"). Defaults to None.
|
|
name : str, optional
|
|
The user-visible name (Label) for the rebar. If None, defaults to "Rebar". Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
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", "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
|
|
elif sketch.AttachmentSupport == baseobj:
|
|
sketch.AttachmentSupport = None
|
|
rebar.Base = sketch
|
|
if FreeCAD.GuiUp:
|
|
sketch.ViewObject.hide()
|
|
rebar.Host = baseobj
|
|
elif not baseobj and sketch:
|
|
# 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
|
|
rebar.Document.recompute()
|
|
if offset is not None:
|
|
rebar.OffsetStart = offset
|
|
rebar.OffsetEnd = offset
|
|
else:
|
|
rebar.OffsetStart = params.get_param_arch("RebarOffset")
|
|
rebar.OffsetEnd = params.get_param_arch("RebarOffset")
|
|
rebar.Mark = rebar.Label
|
|
|
|
return rebar
|
|
|
|
|
|
def makeReference(filepath=None, partname=None, name=None):
|
|
"""
|
|
Creates an Arch reference object.
|
|
|
|
Parameters
|
|
----------
|
|
filepath : str, optional
|
|
The file path of the external reference. Defaults to None.
|
|
partname : str, optional
|
|
The name of the part in the external file. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created reference. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created reference object.
|
|
"""
|
|
reference = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="ArchReference",
|
|
internalName="ArchReference",
|
|
defaultLabel=name if name else translate("Arch", "External Reference"),
|
|
moduleName="ArchReference",
|
|
viewProviderName="ViewProviderArchReference",
|
|
)
|
|
|
|
if filepath:
|
|
reference.File = filepath
|
|
if partname:
|
|
reference.Part = partname
|
|
|
|
import Draft
|
|
|
|
Draft.select(reference)
|
|
|
|
return reference
|
|
|
|
|
|
def makeRoof(
|
|
baseobj=None,
|
|
facenr=0,
|
|
angles=[45.0],
|
|
run=[250.0],
|
|
idrel=[-1],
|
|
thickness=[50.0],
|
|
overhang=[100.0],
|
|
name=None,
|
|
):
|
|
"""
|
|
Creates a roof object based on a closed wire or an object.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the roof. Defaults to None.
|
|
facenr : int, optional
|
|
The face number to use as the base. Defaults to 0.
|
|
angles : list of float, optional
|
|
The angles for each edge of the roof. Defaults to [45.0].
|
|
run : list of float, optional
|
|
The run distances for each edge. Defaults to [250.0].
|
|
idrel : list of int, optional
|
|
The relative IDs for each edge. Defaults to [-1].
|
|
thickness : list of float, optional
|
|
The thickness of the roof for each edge. Defaults to [50.0].
|
|
overhang : list of float, optional
|
|
The overhang distances for each edge. Defaults to [100.0].
|
|
name : str, optional
|
|
The name to assign to the created roof. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created roof object.
|
|
|
|
Notes
|
|
-----
|
|
1. If the base object is a solid the roof uses its shape.
|
|
2. The angles, run, idrel, thickness, and overhang lists are automatically
|
|
completed to match the number of edges in the wire.
|
|
"""
|
|
import Part
|
|
import ArchRoof
|
|
|
|
baseWire = None
|
|
|
|
roof = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Roof",
|
|
internalName="Roof",
|
|
defaultLabel=name if name else translate("Arch", "Roof"),
|
|
moduleName="ArchRoof",
|
|
viewProviderName="_ViewProviderRoof",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
roof.Base = baseobj
|
|
if hasattr(roof.Base, "Shape"):
|
|
if roof.Base.Shape.Solids:
|
|
if FreeCAD.GuiUp:
|
|
roof.Base.ViewObject.hide()
|
|
else:
|
|
if roof.Base.Shape.Faces and roof.Face:
|
|
baseWire = roof.Base.Shape.Faces[roof.Face - 1].Wires[0]
|
|
if FreeCAD.GuiUp:
|
|
roof.Base.ViewObject.hide()
|
|
elif roof.Base.Shape.Wires:
|
|
baseWire = roof.Base.Shape.Wires[0]
|
|
if FreeCAD.GuiUp:
|
|
roof.Base.ViewObject.hide()
|
|
if baseWire:
|
|
if baseWire.isClosed():
|
|
if FreeCAD.GuiUp:
|
|
roof.Base.ViewObject.hide()
|
|
edges = Part.__sortEdges__(baseWire.Edges)
|
|
ln = len(edges)
|
|
roof.Angles = ArchRoof.adjust_list_len(angles, ln, angles[0])
|
|
roof.Runs = ArchRoof.adjust_list_len(run, ln, run[0])
|
|
roof.IdRel = ArchRoof.adjust_list_len(idrel, ln, idrel[0])
|
|
roof.Thickness = ArchRoof.adjust_list_len(thickness, ln, thickness[0])
|
|
roof.Overhang = ArchRoof.adjust_list_len(overhang, ln, overhang[0])
|
|
|
|
roof.Face = facenr
|
|
|
|
return roof
|
|
|
|
|
|
def makeSchedule():
|
|
"""
|
|
Creates a schedule object in the active document.
|
|
|
|
Returns
|
|
-------
|
|
App::FeaturePython
|
|
The created schedule object.
|
|
"""
|
|
schedule = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
internalName="Schedule",
|
|
baseClassName="_ArchSchedule",
|
|
defaultLabel=translate("Arch", "Schedule"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if hasattr(schedule, "CreateSpreadsheet") and schedule.CreateSpreadsheet:
|
|
schedule.Proxy.getSpreadSheet(schedule, force=True)
|
|
|
|
return schedule
|
|
|
|
|
|
def makeSectionPlane(objectslist=None, name=None):
|
|
"""
|
|
Creates a section plane object including the given objects.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the section plane. If no object is given, the whole
|
|
document will be considered. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created section plane. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::FeaturePython
|
|
The created section plane object.
|
|
"""
|
|
import Draft
|
|
from WorkingPlane import get_working_plane
|
|
|
|
sectionPlane = _initializeArchObject(
|
|
"App::FeaturePython",
|
|
baseClassName="_SectionPlane",
|
|
internalName="Section",
|
|
defaultLabel=name if name else translate("Arch", "Section"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if objectslist:
|
|
sectionPlane.Objects = objectslist
|
|
boundBox = FreeCAD.BoundBox()
|
|
for obj in Draft.get_group_contents(objectslist):
|
|
if hasattr(obj, "Shape") and hasattr(obj.Shape, "BoundBox"):
|
|
boundBox.add(obj.Shape.BoundBox)
|
|
sectionPlane.Placement = get_working_plane().get_placement()
|
|
sectionPlane.Placement.Base = boundBox.Center
|
|
if FreeCAD.GuiUp:
|
|
margin = boundBox.XLength * 0.1
|
|
sectionPlane.ViewObject.DisplayLength = boundBox.XLength + margin
|
|
sectionPlane.ViewObject.DisplayHeight = boundBox.YLength + margin
|
|
return sectionPlane
|
|
|
|
|
|
def makeSite(objectslist=None, baseobj=None, name=None):
|
|
"""
|
|
Creates a site object including the given objects.
|
|
|
|
Parameters
|
|
----------
|
|
objectslist : list of Part::FeaturePython, optional
|
|
A list of objects to include in the site. Defaults to None.
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the site. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created site. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created site object.
|
|
"""
|
|
site = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Site",
|
|
internalName="Site",
|
|
defaultLabel=name if name else translate("Arch", "Site"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if objectslist:
|
|
site.Group = objectslist
|
|
if baseobj:
|
|
import Part
|
|
|
|
if isinstance(baseobj, Part.Shape):
|
|
site.Shape = baseobj
|
|
else:
|
|
site.Terrain = baseobj
|
|
|
|
return site
|
|
|
|
|
|
def makeSpace(objects=None, baseobj=None, name=None):
|
|
"""Creates a space object from the given objects.
|
|
|
|
Parameters
|
|
----------
|
|
objects : object or List(<SelectionObject>) or App::PropertyLinkSubList, optional
|
|
The object or selection set that defines the space. If a single object is given,
|
|
it becomes the base shape for the object. If the object or selection set contains
|
|
subelements, these will be used as the boundaries to create the space. By default None.
|
|
baseobj : object or List(<SelectionObject>) or App::PropertyLinkSubList, optional
|
|
Currently unimplemented, it replaces and behaves in the same way as the objects parameter
|
|
if defined. By default None.
|
|
name : str, optional
|
|
The user-facing name to assign to the space object's label. By default None, in
|
|
which case the label is set to "Space".
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created space object.
|
|
|
|
Notes
|
|
-----
|
|
The objects parameter can be passed using either of these different formats:
|
|
|
|
1. Single object (e.g. a Part::Feature document object). Will be used as the space's base
|
|
shape.::
|
|
objects = <Part::Feature>
|
|
2. List of selection objects, as provided by ``Gui.Selection.getSelectionEx()``. This
|
|
requires the GUI to be active. The `SubObjects` property of each selection object in the
|
|
list defines the space's boundaries. If the list contains a single selection object without
|
|
subobjects, or with only one subobject, the object in its ``Object`` property is used as
|
|
the base shape.::
|
|
objects = [<SelectionObject>, ...]
|
|
3. A list of tuples that can be assigned to an ``App::PropertyLinkSubList`` property. Each
|
|
tuple contains a document object and a nested tuple of subobjects that define the boundaries.
|
|
If the list contains a single tuple without a nested subobjects tuple, or a subobjects tuple
|
|
with only one subobject, the object in the tuple is used as the base shape.::
|
|
objects = [(obj1, ("Face1")), (obj2, ("Face1")), ...]
|
|
objects = [(obj, ("Face1", "Face2", "Face3", "Face4"))]
|
|
"""
|
|
space = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Space",
|
|
internalName="Space",
|
|
defaultLabel=name if name else translate("Arch", "Space"),
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
objects = baseobj
|
|
if objects:
|
|
if not isinstance(objects, list):
|
|
objects = [objects]
|
|
|
|
isSingleObject = lambda objs: len(objs) == 1
|
|
|
|
# We assume that the objects list is not a mixed set. The type of the first
|
|
# object will determine the type of the set.
|
|
# Input to this function can come into three different formats. First convert it
|
|
# to a common format: [ (<Part::Feature>, ["Face1", ...]), ... ]
|
|
if hasattr(objects[0], "isDerivedFrom") and objects[0].isDerivedFrom(
|
|
"Gui::SelectionObject"
|
|
):
|
|
# Selection set: convert to common format
|
|
# [<SelectionObject>, ...]
|
|
objects = [(obj.Object, obj.SubElementNames) for obj in objects]
|
|
elif isinstance(objects[0], tuple) or isinstance(objects[0], list):
|
|
# Tuple or list of object with subobjects: pass unmodified
|
|
# [ (<Part::Feature>, ["Face1", ...]), ... ]
|
|
pass
|
|
else:
|
|
# Single object: assume anything else passed is a single object with no
|
|
# boundaries.
|
|
# [ <Part::Feature> ]
|
|
objects = [(objects[0], [])]
|
|
|
|
if isSingleObject(objects):
|
|
# For a single object, having boundaries is determined by them being defined
|
|
# as more than one subelement (e.g. two faces)
|
|
boundaries = [obj for obj in objects if len(obj[1]) > 1]
|
|
else:
|
|
boundaries = [obj for obj in objects if obj[1]]
|
|
|
|
if isSingleObject(objects) and not boundaries:
|
|
space.Base = objects[0][0]
|
|
if FreeCAD.GuiUp:
|
|
objects[0][0].ViewObject.hide()
|
|
else:
|
|
space.Proxy.addSubobjects(space, boundaries)
|
|
return space
|
|
|
|
|
|
def addSpaceBoundaries(space, subobjects):
|
|
"""Adds the given subobjects as defining boundaries of the given space.
|
|
|
|
Parameters
|
|
----------
|
|
space : ArchSpace._Space
|
|
Arch space object to add the boundaries to.
|
|
subobjects : List(<SelectionObject>) or App::PropertyLinkSubList
|
|
List of boundaries to add to the space.
|
|
|
|
Notes
|
|
-----
|
|
The subobjects parameter can be passed using either of these different formats:
|
|
|
|
1. List of selection objects, as provided by ``Gui.Selection.getSelectionEx()``. This
|
|
requires the GUI to be active. The `SubObjects` property of each selection object in the
|
|
list defines the boundaries to add to the space.::
|
|
subobjects = [<SelectionObject>, ...]
|
|
2. A list of tuples that can be assigned to an ``App::PropertyLinkSubList`` property. Each
|
|
tuple contains a document object and a nested tuple of subobjects that define the boundaries
|
|
to add.::
|
|
subobjects = [(obj1, ("Face1")), (obj2, ("Face1")), ...]
|
|
subobjects = [(obj, ("Face1", "Face2", "Face3", "Face4"))]
|
|
"""
|
|
import Draft
|
|
|
|
if Draft.getType(space) == "Space":
|
|
space.Proxy.addSubobjects(space, subobjects)
|
|
|
|
|
|
def removeSpaceBoundaries(space, subobjects):
|
|
"""Remove the given subobjects as defining boundaries of the given space.
|
|
|
|
Parameters
|
|
----------
|
|
space : ArchSpace._Space
|
|
Arch space object to remove the boundaries from.
|
|
subobjects : List(<SelectionObject>) or App::PropertyLinkSubList
|
|
List of boundaries to remove from the space.
|
|
|
|
Notes
|
|
-----
|
|
The subobjects parameter can be passed using either of these different formats:
|
|
|
|
1. List of selection objects, as provided by ``Gui.Selection.getSelectionEx()``. This
|
|
requires the GUI to be active. The `SubObjects` property of each selection object in the
|
|
list defines the boundaries to remove from the space.::
|
|
subobjects = [<SelectionObject>, ...]
|
|
2. A list of tuples that can be assigned to an ``App::PropertyLinkSubList`` property. Each
|
|
tuple contains a document object and a nested tuple of subobjects that define the boundaries
|
|
to remove.::
|
|
subobjects = [(obj1, ("Face1")), (obj2, ("Face1")), ...]
|
|
subobjects = [(obj, ("Face1", "Face2", "Face3", "Face4"))]
|
|
"""
|
|
import Draft
|
|
|
|
if Draft.getType(space) == "Space":
|
|
space.Proxy.removeSubobjects(space, subobjects)
|
|
|
|
|
|
def makeStairs(baseobj=None, length=None, width=None, height=None, steps=None, name=None):
|
|
"""
|
|
Creates a stairs object with the given attributes.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the stairs. Defaults to None.
|
|
length : float, optional
|
|
The length of the stairs. Defaults to None.
|
|
width : float, optional
|
|
The width of the stairs. Defaults to None.
|
|
height : float, optional
|
|
The height of the stairs. Defaults to None.
|
|
steps : int, optional
|
|
The number of steps. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created stairs. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created stairs object.
|
|
"""
|
|
import ArchStairs
|
|
|
|
if not FreeCAD.ActiveDocument:
|
|
FreeCAD.Console.PrintError("No active document. Aborting\n")
|
|
return
|
|
|
|
stairs = []
|
|
additions = []
|
|
label = name if name else translate("Arch", "Stairs")
|
|
|
|
def setProperty(obj, length, width, height, steps):
|
|
"""setProperty(obj,length,width,height,steps): sets up the basic properties for this stair"""
|
|
obj.Length = length if length else params.get_param_arch("StairsLength")
|
|
obj.Width = width if width else params.get_param_arch("StairsWidth")
|
|
obj.Height = height if height else params.get_param_arch("StairsHeight")
|
|
obj.Structure = "Massive"
|
|
obj.StructureThickness = 150
|
|
obj.DownSlabThickness = 150
|
|
obj.UpSlabThickness = 150
|
|
if steps:
|
|
obj.NumberOfSteps = steps
|
|
|
|
obj.RailingOffsetLeft = 60
|
|
obj.RailingOffsetRight = 60
|
|
obj.RailingHeightLeft = 900
|
|
obj.RailingHeightRight = 900
|
|
|
|
if baseobj:
|
|
if not isinstance(baseobj, list):
|
|
baseobj = [baseobj]
|
|
lenSelection = len(baseobj)
|
|
if lenSelection > 1:
|
|
stair = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stairs")
|
|
stair.Label = label
|
|
ArchStairs._Stairs(stair)
|
|
stairs.append(stair)
|
|
i = 1
|
|
else:
|
|
i = 0
|
|
|
|
for baseobjI in baseobj:
|
|
stair = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stairs")
|
|
stair.Label = label
|
|
ArchStairs._Stairs(stair)
|
|
stairs.append(stair)
|
|
stairs[i].Base = baseobjI
|
|
if steps:
|
|
stepsI = steps
|
|
else:
|
|
stepsI = 20
|
|
setProperty(stairs[i], None, width, height, stepsI)
|
|
|
|
if lenSelection > 1: # More than 1 segment
|
|
# All semgments in a complex stairs moved together by default
|
|
# regardless MoveWithHost setting in system setting
|
|
stair.MoveWithHost = True
|
|
|
|
# All segment goes to Additions (rather than previously 1st segment
|
|
# went to Base) - for consistent in MoveWithHost behaviour
|
|
if i > 0:
|
|
additions.append(stairs[i])
|
|
if i > 1:
|
|
stairs[i].LastSegment = stairs[i - 1]
|
|
# else:
|
|
# Below made '1st segment' of a complex stairs went to Base
|
|
# Remarked below out, 2025.8.31.
|
|
# Seems no other Arch object create an Arch object as its Base
|
|
# and use a 'master' Arch(Stairs) object like Stairs. Base is
|
|
# not moved together with host upon onChanged(), unlike
|
|
# behaviour in objects of Additions.
|
|
#
|
|
# if len(stairs) > 1: # i.e. length >1, have a 'master' staircase created
|
|
# stairs[0].Base = stairs[1]
|
|
|
|
i += 1
|
|
if lenSelection > 1:
|
|
stairs[0].Additions = additions
|
|
else:
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Stairs")
|
|
obj.Label = label
|
|
ArchStairs._Stairs(obj)
|
|
setProperty(obj, length, width, height, steps)
|
|
stairs.append(obj)
|
|
if FreeCAD.GuiUp:
|
|
if baseobj:
|
|
for stair in stairs:
|
|
ArchStairs._ViewProviderStairs(stair.ViewObject)
|
|
for bo in baseobj:
|
|
bo.ViewObject.hide()
|
|
else:
|
|
ArchStairs._ViewProviderStairs(obj.ViewObject)
|
|
if stairs:
|
|
for stair in stairs:
|
|
stair.recompute()
|
|
makeRailing(stairs)
|
|
# return stairs - all other functions expect one object as return value
|
|
return stairs[0]
|
|
else:
|
|
obj.recompute()
|
|
return obj
|
|
|
|
|
|
def makeRailing(stairs):
|
|
"""
|
|
Creates railings for the given stairs.
|
|
|
|
Parameters
|
|
----------
|
|
stairs : list of Part::FeaturePython
|
|
The stairs objects to add railings to.
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
"""
|
|
|
|
def makeRailingLorR(stairs, side="L"):
|
|
"""makeRailingLorR(stairs,side="L"): Creates a railing on the given side of the stairs, L or
|
|
R"""
|
|
for stair in reversed(stairs):
|
|
if side == "L":
|
|
outlineLR = stair.OutlineLeft
|
|
outlineLRAll = stair.OutlineLeftAll
|
|
stairRailingLR = "RailingLeft"
|
|
elif side == "R":
|
|
outlineLR = stair.OutlineRight
|
|
outlineLRAll = stair.OutlineRightAll
|
|
stairRailingLR = "RailingRight"
|
|
if outlineLR or outlineLRAll:
|
|
lrRail = makePipe(
|
|
baseobj=None,
|
|
diameter=0,
|
|
length=0,
|
|
placement=None,
|
|
name=translate("Arch", "Railing"),
|
|
)
|
|
# All semgments in a complex stairs moved together by default
|
|
# regardless Move With Host setting in system setting
|
|
lrRail.MoveWithHost = True
|
|
if outlineLRAll:
|
|
setattr(stair, stairRailingLR, lrRail)
|
|
break
|
|
elif outlineLR:
|
|
setattr(stair, stairRailingLR, lrRail)
|
|
|
|
if stairs is None:
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
sel0 = sel[0]
|
|
stairs = []
|
|
# TODO currently consider 1st selected object, then would tackle multiple objects?
|
|
if Draft.getType(sel[0]) == "Stairs":
|
|
stairs.append(sel0)
|
|
if Draft.getType(sel0.Base) == "Stairs":
|
|
stairs.append(sel0.Base)
|
|
additions = sel0.Additions
|
|
for additionsI in additions:
|
|
if Draft.getType(additionsI) == "Stairs":
|
|
stairs.append(additionsI)
|
|
else:
|
|
stairs.append(sel[0])
|
|
else:
|
|
print("No Stairs object selected")
|
|
return
|
|
|
|
makeRailingLorR(stairs, "L")
|
|
makeRailingLorR(stairs, "R")
|
|
|
|
|
|
def makeTruss(baseobj=None, name=None):
|
|
"""
|
|
Creates a truss object from the given base object.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : Part::FeaturePython, optional
|
|
The base object for the truss. Defaults to None.
|
|
name : str, optional
|
|
The name to assign to the created truss. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The created truss object.
|
|
"""
|
|
truss = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="Truss",
|
|
internalName="Truss",
|
|
defaultLabel=name if name else translate("Arch", "Truss"),
|
|
moduleName="ArchTruss",
|
|
viewProviderName="ViewProviderTruss",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
truss.Base = baseobj
|
|
if FreeCAD.GuiUp:
|
|
baseobj.ViewObject.hide()
|
|
|
|
return truss
|
|
|
|
|
|
def makeWall(
|
|
baseobj=None,
|
|
height=None,
|
|
length=None,
|
|
width=None,
|
|
align=None,
|
|
offset=None,
|
|
face=None,
|
|
name=None,
|
|
):
|
|
"""Create a wall based on a given object, and returns the generated wall.
|
|
|
|
TODO: It is unclear what defines which units this function uses.
|
|
|
|
Parameters
|
|
----------
|
|
baseobj: <Part::Feature>, optional
|
|
The base object with which to build the wall. This can be a sketch, a
|
|
draft object, a face, or a solid. It can also be left as None.
|
|
height: float, optional
|
|
The height of the wall.
|
|
length: float, optional
|
|
The length of the wall. Not used if the wall is based off an object.
|
|
Will use Arch default if left empty.
|
|
width: float, optional
|
|
The width of the wall. Not used if the base object is a face. Will use
|
|
Arch default if left empty.
|
|
align: str, optional
|
|
Either "Center", "Left", or "Right". Effects the alignment of the wall
|
|
on its baseline.
|
|
face: int, optional
|
|
The index number of a face on the given baseobj, to base the wall on.
|
|
name: str, optional
|
|
The name to give to the created wall.
|
|
|
|
Returns
|
|
-------
|
|
<Part::FeaturePython>
|
|
Returns the generated wall.
|
|
|
|
Notes
|
|
-----
|
|
1. Creates a new <Part::FeaturePython> object, and turns it into a parametric wall
|
|
object. This <Part::FeaturePython> object does not yet have any shape.
|
|
2. The wall then uses the baseobj.Shape as the basis to extrude out a wall shape,
|
|
giving the new <Part::FeaturePython> object a shape.
|
|
3. It then hides the original baseobj.
|
|
"""
|
|
import Draft
|
|
|
|
wall = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Wall",
|
|
internalName="Wall",
|
|
defaultLabel=name if name else translate("Arch", "Wall"),
|
|
moduleName="ArchWall",
|
|
viewProviderName="_ViewProviderWall",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if baseobj:
|
|
if hasattr(baseobj, "Shape") or baseobj.isDerivedFrom("Mesh::Feature"):
|
|
wall.Base = baseobj
|
|
else:
|
|
FreeCAD.Console.PrintWarning(
|
|
str(translate("Arch", "Walls can only be based on Part or Mesh objects"))
|
|
)
|
|
if face:
|
|
wall.Face = face
|
|
if length:
|
|
wall.Length = length
|
|
wall.Width = width if width else params.get_param_arch("WallWidth")
|
|
wall.Height = height if height else params.get_param_arch("WallHeight")
|
|
wall.Align = (
|
|
align if align else ["Center", "Left", "Right"][params.get_param_arch("WallAlignment")]
|
|
)
|
|
|
|
if wall.Base and FreeCAD.GuiUp:
|
|
if Draft.getType(wall.Base) != "Space":
|
|
wall.Base.ViewObject.hide()
|
|
|
|
return wall
|
|
|
|
|
|
def joinWalls(walls, delete=False, deletebase=False):
|
|
"""Join the given list of walls into one sketch-based wall.
|
|
|
|
Take the first wall in the list, and adds on the other walls in the list.
|
|
Return the modified first wall.
|
|
|
|
Setting delete to True, will delete the other walls. Only join walls
|
|
if the walls have the same width, height and alignment.
|
|
|
|
Parameters
|
|
----------
|
|
walls : list of <Part::FeaturePython>
|
|
List containing the walls to add to the first wall in the list. Walls must
|
|
be based off a base object.
|
|
delete : bool, optional
|
|
If True, deletes the other walls in the list. Defaults to False.
|
|
deletebase : bool, optional
|
|
If True, and delete is True, the base of the other walls is also deleted
|
|
Defaults to False.
|
|
|
|
Returns
|
|
-------
|
|
Part::FeaturePython
|
|
The joined wall object.
|
|
"""
|
|
import Part
|
|
import Draft
|
|
import ArchWall
|
|
|
|
if not walls:
|
|
return None
|
|
if not isinstance(walls, list):
|
|
walls = [walls]
|
|
if not ArchWall.areSameWallTypes(walls):
|
|
return None
|
|
deleteList = []
|
|
base = walls.pop()
|
|
if base.Base:
|
|
if base.Base.Shape.Faces:
|
|
return None
|
|
# Use ArchSketch if SketchArch add-on is present
|
|
if Draft.getType(base.Base) == "ArchSketch":
|
|
sk = base.Base
|
|
else:
|
|
try:
|
|
import ArchSketchObject
|
|
|
|
newSk = ArchSketchObject.makeArchSketch()
|
|
except:
|
|
if Draft.getType(base.Base) != "Sketcher::SketchObject":
|
|
newSk = FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject", "WallTrace")
|
|
else:
|
|
newSk = None
|
|
if newSk:
|
|
sk = Draft.makeSketch(base.Base, autoconstraints=True, addTo=newSk)
|
|
base.Base = sk
|
|
else:
|
|
sk = base.Base
|
|
for w in walls:
|
|
if w.Base and not w.Base.Shape.Faces:
|
|
for hostedObj in w.Proxy.getHosts(w):
|
|
if hasattr(hostedObj, "Host"):
|
|
hostedObj.Host = base
|
|
else:
|
|
tmp = hostedObj.Hosts
|
|
if delete:
|
|
tmp.remove(w)
|
|
if not base in tmp:
|
|
tmp.append(base)
|
|
hostedObj.Hosts = tmp
|
|
tmp = []
|
|
for add in w.Additions:
|
|
if not add in base.Additions:
|
|
tmp.append(add)
|
|
if delete:
|
|
w.Additions = None
|
|
base.Additions += tmp
|
|
tmp = []
|
|
for sub in w.Subtractions:
|
|
if not sub in base.Subtractions:
|
|
tmp.append(sub)
|
|
if delete:
|
|
w.Subtractions = None
|
|
base.Subtractions += tmp
|
|
for e in w.Base.Shape.Edges:
|
|
l = e.Curve
|
|
if isinstance(l, Part.Line):
|
|
l = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point)
|
|
sk.addGeometry(l)
|
|
deleteList.append(w.Name)
|
|
if deletebase:
|
|
deleteList.append(w.Base.Name)
|
|
if delete:
|
|
for n in deleteList:
|
|
FreeCAD.ActiveDocument.removeObject(n)
|
|
FreeCAD.ActiveDocument.recompute()
|
|
if base.Base and FreeCAD.GuiUp:
|
|
base.ViewObject.show()
|
|
return base
|
|
|
|
|
|
def makeWindow(
|
|
baseobj: Optional[FreeCAD.DocumentObject] = None,
|
|
width: Optional[float] = None,
|
|
height: Optional[float] = None,
|
|
parts: Optional[list[str]] = None,
|
|
name: Optional[str] = None,
|
|
) -> FreeCAD.DocumentObject:
|
|
"""
|
|
Creates an Arch Window object, which can represent either a window or a door.
|
|
|
|
The created object can be based on a 2D profile (e.g., a Sketch), have its
|
|
dimensions set directly, or be defined by custom components. It can be
|
|
inserted into host objects like Walls, creating openings. The IfcType of
|
|
the object can be set to "Window" or "Door" accordingly (presets often
|
|
handle this automatically).
|
|
|
|
Parameters
|
|
----------
|
|
baseobj : FreeCAD.DocumentObject, optional
|
|
The base object for the window/door.
|
|
If `baseobj` is an existing `Arch.Window` (or Door), it will be cloned.
|
|
If `baseobj` is a 2D object with wires (e.g., `Sketcher::SketchObject`,
|
|
`Draft.Wire`), these wires are used to define the geometry.
|
|
If `parts` is None, default components are generated from `baseobj.Shape.Wires`:
|
|
- If one closed wire: `["Default", "Frame", "Wire0", "1", "0"]` (or "Solid panel").
|
|
- If multiple closed wires (e.g., Wire0 outer, Wire1 inner):
|
|
`["Default", "Frame", "Wire0,Wire1", "1", "0"]` (Wire1 cuts Wire0).
|
|
The `Normal` direction is derived from `baseobj.Placement`.
|
|
Defaults to None.
|
|
width : float, optional
|
|
The total width of the window/door.
|
|
If `baseobj` is None, this value is used by `ensureBase()` on first
|
|
recompute to create a default sketch with a "Width" constraint.
|
|
If `baseobj` is a sketch with a "Width" named constraint, setting
|
|
`window_or_door.Width` will drive this sketch constraint. `makeWindow` itself
|
|
does not initially set the object's `Width` *from* a sketch's constraint.
|
|
Defaults to None (or an Arch preference value if `baseobj` is None).
|
|
height : float, optional
|
|
The total height of the window/door.
|
|
If `baseobj` is None, this value is used by `ensureBase()` on first
|
|
recompute to create a default sketch with a "Height" constraint.
|
|
If `baseobj` is a sketch with a "Height" named constraint, setting
|
|
`window_or_door.Height` will drive this sketch constraint. `makeWindow` itself
|
|
does not initially set the object's `Height` *from* a sketch's constraint.
|
|
Defaults to None (or an Arch preference value if `baseobj` is None).
|
|
parts : list[str], optional
|
|
A list defining custom components for the window/door. The list is flat, with
|
|
every 5 elements describing one component:
|
|
`["Name1", "Type1", "WiresStr1", "ThickStr1", "OffsetStr1", ...]`
|
|
- `Name`: User-defined name (e.g., "OuterFrame").
|
|
- `Type`: Component type (e.g., "Frame", "Glass panel", "Solid panel").
|
|
See `ArchWindow.WindowPartTypes`.
|
|
- `WiresStr`: Comma-separated string defining wire usage from `baseobj.Shape.Wires`
|
|
(0-indexed) and optionally hinge/opening from `baseobj.Shape.Edges` (1-indexed).
|
|
Example: `"Wire0,Wire1,Edge8,Mode1"`.
|
|
- `"WireN"`: Uses Nth wire for the base face.
|
|
- `"WireN,WireM"`: WireN is base, WireM is cutout.
|
|
- `"EdgeK"`: Kth edge is hinge.
|
|
- `"ModeL"`: Lth opening mode from `ArchWindow.WindowOpeningModes`.
|
|
- `ThickStr`: Thickness as string (e.g., `"50.0"`). Appending `"+V"`
|
|
adds the object's `Frame` property value.
|
|
- `OffsetStr`: Offset along normal as string (e.g., `"25.0"`). Appending `"+V"`
|
|
adds the object's `Offset` property value.
|
|
Defaults to None. If None and `baseobj` is a sketch, default parts
|
|
are generated as described under `baseobj`.
|
|
name : str, optional
|
|
The name (label) for the created window/door. If None, a default localized
|
|
name ("Window" or "Door", depending on context or subsequent changes) is used.
|
|
Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
FreeCAD.DocumentObject
|
|
The created Arch Window object (which is a `Part::FeaturePython` instance,
|
|
configurable to represent a window or a door).
|
|
|
|
See Also
|
|
--------
|
|
ArchWindowPresets.makeWindowPreset : Create window/door from predefined types.
|
|
ArchWall.addComponents : Add a window/door to a wall (creates opening).
|
|
|
|
Notes
|
|
-----
|
|
- **Dual purpose (window/door)**: despite its name, this function is the primary
|
|
way to programmatically create both windows and doors in the BIM workbench.
|
|
The distinction is often made by setting the `IfcType` property of the
|
|
created object to "Window" or "Door", and by the chosen components or preset.
|
|
- **Sketch-based dimensions**: If `baseobj` is a `Sketcher::SketchObject`
|
|
with named constraints "Width" and "Height", these sketch constraints will be
|
|
parametrically driven by the created object's `Width` and `Height` properties
|
|
respectively *after* the object is created and its properties are changed.
|
|
`makeWindow` itself does not initially populate the object's `Width`/`Height` from
|
|
these sketch constraints if `width`/`height` arguments are not passed to it.
|
|
The object's internal `Width` and `Height` properties are the drivers.
|
|
- **Object from dimensions (No `baseobj` initially)**: if `baseobj` is `None` but
|
|
`width` and `height` are provided, `makeWindow` creates an Arch Window object.
|
|
Upon the first `doc.recompute()`, the `ensureBase()` mechanism generates
|
|
an internal sketch (`obj.Base`) with "Width" and "Height" constraints
|
|
driven by `obj.Width` and `obj.Height`. However, `obj.WindowParts`
|
|
will remain undefined, resulting in a shapeless object until `WindowParts`
|
|
are manually set.
|
|
- **`obj.Frame` and `obj.Offset` properties**: these main properties of the
|
|
created object (e.g., `my_window.Frame = 50.0`) provide the values used when
|
|
`"+V"` is specified in the `ThicknessString` or `OffsetString` of a component
|
|
within the `parts` list.
|
|
- **Hosting and openings**: to create an opening in a host object (e.g., `Arch.Wall`),
|
|
set `obj.Hosts = [my_wall]`. The opening's shape is typically derived
|
|
from `obj.HoleWire` (defaulting to the largest wire of `obj.Base`) and
|
|
extruded by `obj.HoleDepth` (if 0, tries to match host thickness).
|
|
A custom `obj.Subvolume` can also define the opening shape.
|
|
- **Component management**: components and their geometry are primarily
|
|
managed by the `_Window` class and its methods in `ArchWindow.py`.
|
|
- **Initialization from sketch `baseobj`**: when `baseobj` is a sketch
|
|
(e.g., `Sketcher::SketchObject`) and `parts` is `None` or provided:
|
|
- The `window.Shape` (geometric representation) is correctly generated
|
|
at the global position and orientation defined by `baseobj.Placement`.
|
|
- However, the created window object's own `window.Placement` property is
|
|
**not** automatically initialized from `baseobj.Placement` and typically
|
|
remains at the identity placement (origin, no rotation).
|
|
- Similarly, the `window.Width` and `window.Height` properties are **not**
|
|
automatically populated from the dimensions of the `baseobj` sketch.
|
|
These properties will default to 0.0 or values from Arch preferences
|
|
(if `width`/`height` arguments to `makeWindow` are also `None`).
|
|
- If you need the `window` object's `Placement`, `Width`, or `Height`
|
|
properties to reflect the `baseobj` sketch for subsequent operations
|
|
(e.g., if other systems query these specific window properties, or if
|
|
you intend to parametrically drive the sketch via these window properties),
|
|
you may need to set them manually after `makeWindow` is called:
|
|
- The `ArchWindow._Window.execute()` method, when recomputing the window,
|
|
*does* use `window.Base.Shape` (the sketch's shape in its global position)
|
|
to generate the window's geometry. The `ArchWindow._Window.getSubVolume()`
|
|
method also correctly uses `window.Base.Shape` and the window object's
|
|
(identity) `Placement` for creating the cutting volume.
|
|
|
|
Examples
|
|
--------
|
|
>>> import FreeCAD as App
|
|
>>> import Draft, Arch, Sketcher, Part
|
|
>>> doc = App.newDocument("ArchWindowDoorExamples")
|
|
|
|
>>> # Ex1: Basic window from sketch and parts definition, oriented to XZ (vertical) plane
|
|
>>> sketch_ex1 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx1_Vertical')
|
|
>>> # Define geometry in sketch's local XY plane (width along local X, height along local Y)
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(1000,0,0))) # Wire0 - Outer
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,0,0), App.Vector(1000,1200,0)))
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,1200,0), App.Vector(0,1200,0)))
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,1200,0), App.Vector(0,0,0)))
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,100,0), App.Vector(900,100,0))) # Wire1 - Inner
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,100,0), App.Vector(900,1100,0)))
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,1100,0), App.Vector(100,1100,0)))
|
|
>>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,1100,0), App.Vector(100,100,0)))
|
|
>>> doc.recompute() # Update sketch Wires
|
|
>>> # Orient sketch: Rotate +90 deg around X-axis to place sketch's XY onto global XZ.
|
|
>>> # Sketch's local Y (height) now aligns with global Z. Sketch normal is global -Y.
|
|
>>> sketch_ex1.Placement.Rotation = App.Rotation(App.Vector(1,0,0), 90)
|
|
>>> doc.recompute() # Apply sketch placement
|
|
>>> window_ex1 = Arch.makeWindow(baseobj=sketch_ex1, name="MyWindowEx1_Vertical")
|
|
>>> # Window Normal will be derived as global +Y, extrusion along +Y.
|
|
>>> window_ex1.WindowParts = [
|
|
... "Frame", "Frame", "Wire0,Wire1", "60", "0", # Frame from Wire0-Wire1
|
|
... "Glass", "Glass panel", "Wire1", "10", "25" # Glass from Wire1, offset in Normal dir
|
|
... ]
|
|
>>> doc.recompute()
|
|
|
|
>>> # Ex2: Window from sketch with named "Width"/"Height" constraints (on default XY plane)
|
|
>>> sketch_ex2 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx2_Named')
|
|
>>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(800,0,0))) # Edge 0
|
|
>>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,0,0), App.Vector(800,600,0))) # Edge 1
|
|
>>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,600,0), App.Vector(0,600,0))) # Complete Wire0
|
|
>>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,600,0), App.Vector(0,0,0)))
|
|
>>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2, 800))
|
|
>>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Width")
|
|
>>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2, 600))
|
|
>>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Height")
|
|
>>> doc.recompute()
|
|
>>> window_ex2 = Arch.makeWindow(baseobj=sketch_ex2, name="MyWindowEx2_Parametric")
|
|
>>> window_ex2.WindowParts = ["Frame", "Frame", "Wire0", "50", "0"]
|
|
>>> doc.recompute()
|
|
>>> print(f"Ex2 Initial - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}")
|
|
>>> window_ex2.Width = 950 # This drives the sketch constraint
|
|
>>> doc.recompute()
|
|
>>> print(f"Ex2 Updated - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}")
|
|
|
|
>>> # Ex3: Window from dimensions only (initially shapeless, sketch on XY plane)
|
|
>>> window_ex3 = Arch.makeWindow(width=700, height=900, name="MyWindowEx3_Dims")
|
|
>>> print(f"Ex3 Initial - Base: {window_ex3.Base}, Shape isNull: {window_ex3.Shape.isNull()}")
|
|
>>> doc.recompute() # ensureBase creates the sketch on XY plane
|
|
>>> print(f"Ex3 After Recompute - Base: {window_ex3.Base.Name if window_ex3.Base else 'None'}, Shape isNull: {window_ex3.Shape.isNull()}")
|
|
>>> window_ex3.WindowParts = ["SimpleFrame", "Frame", "Wire0", "40", "0"] # Wire0 from auto-generated sketch
|
|
>>> doc.recompute()
|
|
>>> print(f"Ex3 After Parts - Shape isNull: {window_ex3.Shape.isNull()}")
|
|
|
|
>>> # Ex4: Door created using an ArchWindowPresets function
|
|
>>> # Note: Arch.makeWindowPreset calls Arch.makeWindow internally
|
|
>>> door_ex4_preset = makeWindowPreset(
|
|
... "Simple door", width=900, height=2100,
|
|
... h1=50, h2=0, h3=0, w1=70, w2=40, o1=0, o2=0 # Preset-specific params
|
|
... )
|
|
>>> if door_ex4_preset:
|
|
... door_ex4_preset.Label = "MyDoorEx4_Preset"
|
|
... doc.recompute()
|
|
|
|
>>> # Ex5: Door created from a sketch, with IfcType manually set (sketch on XY plane)
|
|
>>> sketch_ex5_door = doc.addObject('Sketcher::SketchObject', 'DoorSketchEx5')
|
|
>>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(850,0,0))) # Wire0
|
|
>>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,0,0), App.Vector(850,2050,0)))
|
|
>>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,2050,0), App.Vector(0,2050,0)))
|
|
>>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,2050,0), App.Vector(0,0,0)))
|
|
>>> doc.recompute()
|
|
>>> door_ex5_manual = Arch.makeWindow(baseobj=sketch_ex5_door, name="MyDoorEx5_Manual")
|
|
>>> door_ex5_manual.WindowParts = ["DoorPanel", "Solid panel", "Wire0", "40", "0"]
|
|
>>> door_ex5_manual.IfcType = "Door" # Explicitly define as a Door
|
|
>>> doc.recompute()
|
|
|
|
>>> # Ex6: Hosting the vertical window from Ex1 in an Arch.Wall
|
|
>>> wall_ex6 = Arch.makeWall(None, length=4000, width=200, height=2400)
|
|
>>> wall_ex6.Label = "WallForOpening_Ex6"
|
|
>>> # Window_ex1 is already oriented (its sketch placement was set in Ex1).
|
|
>>> # Now, just position the window object itself.
|
|
>>> window_ex1.Placement.Base = App.Vector(1500, wall_ex6.Width.Value / 2, 900) # X, Y (center of wall), Z (sill)
|
|
>>> window_ex1.HoleDepth = 0 # Use wall's thickness for the opening depth
|
|
>>> doc.recompute() # Apply window placement and HoleDepth
|
|
>>> window_ex1.Hosts = [wall_ex6]
|
|
>>> doc.recompute() # Wall recomputes to create the opening
|
|
"""
|
|
import Draft
|
|
import DraftGeomUtils
|
|
from draftutils import todo
|
|
|
|
if baseobj and Draft.getType(baseobj) == "Window" and FreeCAD.ActiveDocument:
|
|
window = Draft.clone(baseobj)
|
|
return window
|
|
|
|
window = _initializeArchObject(
|
|
"Part::FeaturePython",
|
|
baseClassName="_Window",
|
|
internalName="Window",
|
|
defaultLabel=name if name else translate("Arch", "Window"),
|
|
moduleName="ArchWindow",
|
|
viewProviderName="_ViewProviderWindow",
|
|
)
|
|
|
|
# Initialize all relevant properties
|
|
if width:
|
|
window.Width = width
|
|
if height:
|
|
window.Height = height
|
|
if baseobj:
|
|
# 2025.5.25
|
|
# Historically, this normal was deduced by the orientation of the Base Sketch and hardcoded
|
|
# in the Normal property. Now with the new AutoNormalReversed property/flag, set True as
|
|
# default, the auto Normal previously in opposite direction to is now consistent with that
|
|
# previously hardcoded. With the normal set to 'auto', window object would not suffer weird
|
|
# shape if the Base Sketch is rotated by some reason. Keep the property be 'auto' (0,0,0)
|
|
# here.
|
|
# obj.Normal = baseobj.Placement.Rotation.multVec(FreeCAD.Vector(0, 0, -1))
|
|
window.Base = baseobj
|
|
if parts is not None:
|
|
window.WindowParts = parts
|
|
else:
|
|
if baseobj:
|
|
linked_obj = baseobj.getLinkedObject(True)
|
|
if (
|
|
linked_obj.isDerivedFrom("Part::Part2DObject")
|
|
or Draft.getType(linked_obj) in ["BezCurve", "BSpline", "Wire"]
|
|
) and DraftGeomUtils.isPlanar(baseobj.Shape):
|
|
# "BezCurve", "BSpline" and "Wire" objects created with < v1.1 are
|
|
# "Part::Part2DObject" objects. In all versions these objects need not be planar.
|
|
if baseobj.Shape.Wires:
|
|
part_type = "Frame"
|
|
if len(baseobj.Shape.Wires) == 1:
|
|
part_type = "Solid panel"
|
|
wires = []
|
|
for i, wire in enumerate(baseobj.Shape.Wires):
|
|
if wire.isClosed():
|
|
wires.append(f"Wire{i}")
|
|
wires_str = ",".join(wires)
|
|
part_name = "Default"
|
|
part_frame_thickness = "1" # mm
|
|
part_offset = "0" # mm
|
|
window.WindowParts = [
|
|
part_name,
|
|
part_type,
|
|
wires_str,
|
|
part_frame_thickness,
|
|
part_offset,
|
|
]
|
|
else:
|
|
# Bind properties from base obj if they exist
|
|
for prop in ["Height", "Width", "Subvolume", "Tag", "Description", "Material"]:
|
|
for baseobj_prop in baseobj.PropertiesList:
|
|
if (baseobj_prop == prop) or baseobj_prop.endswith(f"_{prop}"):
|
|
window.setExpression(prop, f"{baseobj.Name}.{baseobj_prop}")
|
|
|
|
if window.Base and FreeCAD.GuiUp:
|
|
from ArchWindow import recolorize
|
|
|
|
window.Base.ViewObject.DisplayMode = "Wireframe"
|
|
window.Base.ViewObject.hide()
|
|
todo.ToDo.delay(recolorize, [window.Document.Name, window.Name])
|
|
|
|
return window
|
|
|
|
|
|
def is_debasable(wall):
|
|
"""Determines if an Arch Wall can be cleanly converted to a baseless state.
|
|
|
|
This function checks if a given wall is a valid candidate for a parametric
|
|
"debasing" operation, where its dependency on a Base object is removed and
|
|
it becomes driven by its own Length and Placement properties.
|
|
|
|
Parameters
|
|
----------
|
|
wall : FreeCAD.DocumentObject
|
|
The Arch Wall object to check.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if the wall is a valid candidate for debasing, otherwise ``False``.
|
|
|
|
Notes
|
|
-----
|
|
A wall is considered debasable if its ``Base`` object's final shape consists
|
|
of exactly one single, straight edge. This check is generic and works for
|
|
any base object that provides a valid ``.Shape`` property, including
|
|
``Draft.Line`` and ``Sketcher::SketchObject`` objects.
|
|
"""
|
|
import Part
|
|
import Draft
|
|
|
|
# Ensure the object is actually a wall
|
|
if Draft.getType(wall) != "Wall":
|
|
return False
|
|
|
|
# Check for a valid Base object with a geometric Shape
|
|
if not hasattr(wall, "Base") or not wall.Base:
|
|
return False
|
|
if not hasattr(wall.Base, "Shape") or wall.Base.Shape.isNull():
|
|
return False
|
|
|
|
base_shape = wall.Base.Shape
|
|
|
|
# The core condition: the final shape must contain exactly one edge.
|
|
# This correctly handles Sketches with multiple lines or construction geometry.
|
|
if len(base_shape.Edges) != 1:
|
|
return False
|
|
|
|
# The single edge must be a straight line.
|
|
edge = base_shape.Edges[0]
|
|
if not isinstance(edge.Curve, (Part.Line, Part.LineSegment)):
|
|
return False
|
|
|
|
# If all checks pass, the wall is debasable.
|
|
return True
|
|
|
|
|
|
def debaseWall(wall):
|
|
"""
|
|
Converts a line-based Arch Wall to be parametrically driven by its own
|
|
properties (Length, Width, Height) and Placement, removing its dependency
|
|
on a Base object.
|
|
|
|
This operation preserves the wall's exact size and global position.
|
|
It is only supported for walls based on a single, straight line.
|
|
|
|
Returns True on success, False otherwise.
|
|
"""
|
|
import FreeCAD
|
|
|
|
if not is_debasable(wall):
|
|
FreeCAD.Console.PrintWarning(f"Wall '{wall.Label}' is not eligible for debasing.\n")
|
|
return False
|
|
|
|
doc = wall.Document
|
|
doc.openTransaction(f"Debase Wall: {wall.Label}")
|
|
try:
|
|
# Calculate the final state before making any changes
|
|
# A wall's final position and orientation are derived from its Base object and its own
|
|
# properties like Width and Align. To make the wall independent, this final state must be
|
|
# captured. A simple transfer of the Base object's Placement property is unreliable for two
|
|
# main reasons:
|
|
# - Ambiguity: A Draft.Line's direction is defined by its vertex coordinates, so a line at
|
|
# 45° can have a 0° rotation in its Placement.
|
|
# - Coordinate Systems: A universal method is needed to handle both Draft objects (defined
|
|
# in global coordinates) and Sketch objects (defined in a local coordinate system).
|
|
#
|
|
# The solution is to use the wall.Proxy.basewires internal attribute. It is non-persistent
|
|
# and populated on the fly by calling getExtrusionData(). It contains the baseline edge
|
|
# already transformed into the document's global coordinate system. From the vertex
|
|
# positions of this globally-aware edge, the new wall's final Placement is calculated.
|
|
extrusion_data = wall.Proxy.getExtrusionData(wall)
|
|
if not extrusion_data or not hasattr(wall.Proxy, "basewires") or not wall.Proxy.basewires:
|
|
raise Exception("Could not retrieve extrusion data to calculate global placement.")
|
|
|
|
# In addition to the baseline edge, getExtrusionData() also provides the extrusion vector,
|
|
# which is used to determine the wall's vertical orientation.
|
|
extrusion_vector = extrusion_data[1]
|
|
baseline_edge = wall.Proxy.basewires[0][0]
|
|
|
|
# Now determine the wall's rotation inferred from its local axes:
|
|
# - The local X axis is along the baseline edge (length).
|
|
# - The local Z axis is along the extrusion vector (height).
|
|
# - The local Y axis is the cross product of X and Z (width, perpendicular to both the
|
|
# - above).
|
|
# Once the local axes are known, a FreeCAD.Rotation matrix can be constructed.
|
|
z_axis = extrusion_vector.normalize()
|
|
x_axis = (baseline_edge.lastVertex().Point - baseline_edge.firstVertex().Point).normalize()
|
|
y_axis = z_axis.cross(x_axis).normalize()
|
|
final_rotation = FreeCAD.Rotation(x_axis, y_axis, z_axis)
|
|
|
|
# This will be the debased wall's local coordinate system origin (0, 0, 0).
|
|
# The wall's Align property (Left, Center, Right) determines how the wall's
|
|
# Width offsets the final position from the centerline.
|
|
centerline_position = baseline_edge.CenterOfMass
|
|
align_offset_distance = 0
|
|
if wall.Align == "Left":
|
|
align_offset_distance = wall.Width.Value / 2.0
|
|
elif wall.Align == "Right":
|
|
align_offset_distance = -wall.Width.Value / 2.0
|
|
|
|
# Convert the offset distance into a vector in the width direction (local Y axis).
|
|
align_offset_vector = y_axis * align_offset_distance
|
|
final_position = centerline_position - align_offset_vector
|
|
|
|
final_placement = FreeCAD.Placement(final_position, final_rotation)
|
|
|
|
# Store properties before unlinking
|
|
height = wall.Height.Value
|
|
length = wall.Length.Value
|
|
width = wall.Width.Value
|
|
|
|
# 1. Apply the final placement first.
|
|
wall.Placement = final_placement
|
|
|
|
# 2. Now, remove the base. The recompute triggered by this change
|
|
# will already have the correct placement to work with.
|
|
wall.Base = None
|
|
|
|
# 3. Clear internal caches and set final properties.
|
|
if hasattr(wall.Proxy, "connectEdges"):
|
|
wall.Proxy.connectEdges = []
|
|
|
|
wall.Height = height
|
|
wall.Length = length
|
|
wall.Width = width
|
|
|
|
# 4. Add an explicit recompute to ensure the final state is settled.
|
|
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
|
|
|
|
|
|
def _initializeArchObject(
|
|
objectType,
|
|
baseClassName=None,
|
|
internalName=None,
|
|
defaultLabel=None,
|
|
moduleName=None,
|
|
viewProviderName=None,
|
|
):
|
|
"""
|
|
Initializes a new Arch object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
objectType : str
|
|
The type of object to create (e.g., "Part::FeaturePython").
|
|
baseClassName : str
|
|
The name of the base class to initialize the object (e.g., "_ArchSchedule").
|
|
internalName : str, optional
|
|
The internal name to assign to the object.
|
|
defaultLabel : str, optional
|
|
The default label to assign to the object if no name is provided.
|
|
moduleName : str, optional
|
|
The name of the module containing the base class and view provider. If not provided,
|
|
it is inferred from baseClassName.
|
|
viewProviderName : str, optional
|
|
The name of the view provider class to initialize the object's view. If not provided,
|
|
it is inferred from baseClassName.
|
|
|
|
Returns
|
|
-------
|
|
App.DocumentObject
|
|
The created object, or None if no active document exists.
|
|
"""
|
|
if not FreeCAD.ActiveDocument:
|
|
FreeCAD.Console.PrintError("No active document. Aborting\n")
|
|
return None
|
|
|
|
import importlib
|
|
|
|
# Infer moduleName and viewProviderName if not provided
|
|
if not moduleName:
|
|
moduleName = "Arch" + baseClassName.lstrip("_").strip("Arch")
|
|
if not viewProviderName:
|
|
viewProviderName = "_ViewProvider" + baseClassName.lstrip("_")
|
|
|
|
obj = FreeCAD.ActiveDocument.addObject(objectType, internalName)
|
|
if not obj:
|
|
return None
|
|
|
|
obj.Label = defaultLabel
|
|
|
|
try:
|
|
# Import module and initialize base class
|
|
module = importlib.import_module(moduleName)
|
|
baseClass = getattr(module, baseClassName, None)
|
|
if not baseClass:
|
|
FreeCAD.Console.PrintError(
|
|
f"Base class '{baseClassName}' not found in module '{moduleName}'.\n"
|
|
)
|
|
return None
|
|
baseClass(obj)
|
|
|
|
# Initialize view provider
|
|
if FreeCAD.GuiUp:
|
|
viewProvider = getattr(module, viewProviderName, None)
|
|
if not viewProvider:
|
|
FreeCAD.Console.PrintWarning(
|
|
f"View provider '{viewProviderName}' not found in module '{moduleName}'.\n"
|
|
)
|
|
else:
|
|
viewProvider(obj.ViewObject)
|
|
|
|
except ImportError as e:
|
|
FreeCAD.Console.PrintError(f"Failed to import module '{moduleName}': {e}\n")
|
|
return None
|
|
|
|
return obj
|
|
|
|
|
|
def makeReport(name=None):
|
|
"""
|
|
Creates a BIM Report object in the active document.
|
|
|
|
Parameters
|
|
----------
|
|
name : str, optional
|
|
The name to assign to the created report object. Defaults to None.
|
|
|
|
Returns
|
|
-------
|
|
App::FeaturePython
|
|
The created report object.
|
|
"""
|
|
|
|
# Use the helper to create the main object. Note that we pass the
|
|
# correct class and module names.
|
|
report_obj = _initializeArchObject(
|
|
objectType="App::FeaturePython",
|
|
baseClassName="_ArchReport",
|
|
internalName="ArchReport",
|
|
defaultLabel=name if name else translate("Arch", "Report"),
|
|
moduleName="ArchReport",
|
|
viewProviderName="ViewProviderReport",
|
|
)
|
|
|
|
# The helper returns None if there's no document, so we can exit early.
|
|
if not report_obj:
|
|
return None
|
|
|
|
# Initialize the Statements property
|
|
# Report object proxy needs its Statements list initialized before getSpreadSheet is called,
|
|
# as getSpreadSheet calls execute() which now relies on obj.Statements.
|
|
# Initialize with one default statement to provide a starting point for the user.
|
|
default_stmt = ReportStatement(description=translate("Arch", "New Statement"))
|
|
report_obj.Statements = [default_stmt.dumps()]
|
|
|
|
# Initialize a spreadsheet if the report requests one. The report is responsible for how the
|
|
# association is stored (we use a non-dependent ``ReportName`` on the sheet and persist the
|
|
# report's ``Target`` link when the report creates the sheet).
|
|
if hasattr(report_obj, "Proxy") and hasattr(report_obj.Proxy, "getSpreadSheet"):
|
|
_ = report_obj.Proxy.getSpreadSheet(report_obj, force=True)
|
|
|
|
if FreeCAD.GuiUp:
|
|
# Automatically open the task panel for the new report
|
|
FreeCADGui.ActiveDocument.setEdit(report_obj.Name, 0)
|
|
|
|
return report_obj
|