BIM: Report command MVP (#24078)
* 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>
This commit is contained in:
@@ -51,7 +51,7 @@ BuildRequires: gtest-devel gmock-devel
|
||||
%endif
|
||||
|
||||
# Development Libraries
|
||||
BuildRequires:boost-devel Coin4-devel eigen3-devel freeimage-devel fmt-devel libglvnd-devel libicu-devel libspnav-devel libXmu-devel med-devel mesa-libEGL-devel mesa-libGLU-devel netgen-mesher-devel netgen-mesher-devel-private opencascade-devel openmpi-devel python3 python3-devel python3-matplotlib python3-pivy python3-pybind11 python3-pyside6-devel python3-shiboken6-devel pyside6-tools qt6-qttools-static qt6-qtsvg-devel vtk-devel xerces-c-devel yaml-cpp-devel
|
||||
BuildRequires:boost-devel Coin4-devel eigen3-devel freeimage-devel fmt-devel libglvnd-devel libicu-devel libspnav-devel libXmu-devel med-devel mesa-libEGL-devel mesa-libGLU-devel netgen-mesher-devel netgen-mesher-devel-private opencascade-devel openmpi-devel python3 python3-devel python3-lark python3-matplotlib python3-pivy python3-pybind11 python3-pyside6-devel python3-shiboken6-devel pyside6-tools qt6-qttools-static qt6-qtsvg-devel vtk-devel xerces-c-devel yaml-cpp-devel
|
||||
#pcl-devel
|
||||
%if %{without bundled_smesh}
|
||||
BuildRequires: smesh-devel
|
||||
|
||||
@@ -102,6 +102,7 @@ requirements:
|
||||
- fmt
|
||||
- freetype
|
||||
- hdf5
|
||||
- lark
|
||||
- libboost-devel
|
||||
- matplotlib-base
|
||||
- noqt5
|
||||
|
||||
@@ -55,6 +55,7 @@ packages=(
|
||||
python3-dev
|
||||
python3-defusedxml
|
||||
python3-git
|
||||
python3-lark
|
||||
python3-markdown
|
||||
python3-matplotlib
|
||||
python3-packaging
|
||||
|
||||
@@ -67,6 +67,7 @@ translate = FreeCAD.Qt.translate
|
||||
# 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
|
||||
@@ -2384,3 +2385,53 @@ def _initializeArchObject(
|
||||
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
|
||||
|
||||
2457
src/Mod/BIM/ArchReport.py
Normal file
2457
src/Mod/BIM/ArchReport.py
Normal file
File diff suppressed because it is too large
Load Diff
2630
src/Mod/BIM/ArchSql.py
Normal file
2630
src/Mod/BIM/ArchSql.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
find_package(PythonInterp REQUIRED)
|
||||
|
||||
# ==============================================================================
|
||||
# File Management
|
||||
# Define the source files and resources of the BIM workbench
|
||||
@@ -56,6 +58,8 @@ SET(Arch_SRCS
|
||||
BimStatus.py
|
||||
TestArch.py
|
||||
TestArchGui.py
|
||||
ArchReport.py
|
||||
ArchSql.py
|
||||
)
|
||||
|
||||
SET(importers_SRCS
|
||||
@@ -96,6 +100,11 @@ SET(Arch_presets
|
||||
Presets/ifc_contexts_IFC2X3.json
|
||||
Presets/ifc_contexts_IFC4.json
|
||||
Presets/properties_conversion.csv
|
||||
Presets/ArchReport/QueryPresets/all_spaces.json
|
||||
Presets/ArchReport/QueryPresets/all_walls.json
|
||||
Presets/ArchReport/QueryPresets/count_by_ifc_class.json
|
||||
Presets/ArchReport/ReportPresets/room_schedule.json
|
||||
Presets/ArchReport/ReportPresets/wall_quantities.json
|
||||
)
|
||||
|
||||
SET(bimcommands_SRCS
|
||||
@@ -179,6 +188,7 @@ SET(bimcommands_SRCS
|
||||
bimcommands/BimWindow.py
|
||||
bimcommands/BimWindows.py
|
||||
bimcommands/BimWPCommands.py
|
||||
bimcommands/BimReport.py
|
||||
bimcommands/__init__.py
|
||||
)
|
||||
|
||||
@@ -242,10 +252,14 @@ SET(bimtests_SRCS
|
||||
bimtests/TestWebGLExportGui.py
|
||||
bimtests/TestArchImportersGui.py
|
||||
bimtests/TestArchBuildingPartGui.py
|
||||
bimtests/TestArchReport.py
|
||||
bimtests/TestArchReportGui.py
|
||||
)
|
||||
|
||||
set(bimtests_FIXTURES
|
||||
bimtests/fixtures/FC_site_simple-102.FCStd
|
||||
bimtests/fixtures/__init__.py
|
||||
bimtests/fixtures/BimFixtures.py
|
||||
)
|
||||
|
||||
SET(BIMGuiIcon_SVG
|
||||
@@ -260,6 +274,23 @@ SET(ImportersSample_Files
|
||||
# - ImportersSample_Files should probably be merged into bimtests_FIXTURES
|
||||
# - BIM_templates should probably be merged into Arch_presets
|
||||
|
||||
SET(ArchSql_resources
|
||||
Resources/ArchSql.lark
|
||||
Resources/ArchSqlParserGenerator.py
|
||||
)
|
||||
|
||||
set(BIM_LARK_GRAMMAR ${CMAKE_CURRENT_SOURCE_DIR}/Resources/ArchSql.lark)
|
||||
set(BIM_PARSER_GENERATOR ${CMAKE_CURRENT_SOURCE_DIR}/Resources/ArchSqlParserGenerator.py)
|
||||
|
||||
set(BIM_GENERATED_PARSER ${CMAKE_BINARY_DIR}/Mod/BIM/generated_sql_parser.py)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${BIM_GENERATED_PARSER}
|
||||
COMMAND ${PYTHON_EXECUTABLE} ${BIM_PARSER_GENERATOR} ${BIM_LARK_GRAMMAR} ${BIM_GENERATED_PARSER}
|
||||
DEPENDS ${BIM_LARK_GRAMMAR} ${BIM_PARSER_GENERATOR}
|
||||
COMMENT "Generating Arch SQL parser..."
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# Developer workflow
|
||||
# This populates the build tree using the file lists from the Install Manifest
|
||||
@@ -304,6 +335,8 @@ ADD_CUSTOM_TARGET(BIM ALL
|
||||
${BIM_templates}
|
||||
)
|
||||
|
||||
add_custom_target(ArchSqlParser ALL DEPENDS ${BIM_GENERATED_PARSER})
|
||||
|
||||
# Populate the build tree with the BIM workbench sources and test data
|
||||
fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${Arch_SRCS})
|
||||
fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${Dice3DS_SRCS})
|
||||
@@ -318,6 +351,7 @@ fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${nativeifc_SRCS})
|
||||
fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/BIM" ${BIMGuiIcon_SVG})
|
||||
fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/BIM" ${Arch_presets})
|
||||
fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/BIM" ${BIM_templates})
|
||||
fc_copy_sources(BIM "${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/BIM" ${ArchSql_resources})
|
||||
|
||||
# For generated resources, we cannot rely on `fc_copy_sources` in case
|
||||
# INSTALL_PREFER_SYMLINKS=ON has been specified, since we're generating a new
|
||||
@@ -398,3 +432,10 @@ INSTALL(
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_DATADIR}/Mod/BIM/Resources/templates"
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${BIM_GENERATED_PARSER}
|
||||
DESTINATION
|
||||
Mod/BIM
|
||||
)
|
||||
|
||||
@@ -200,6 +200,7 @@ class BIMWorkbench(Workbench):
|
||||
"BIM_Layers",
|
||||
"BIM_Material",
|
||||
"Arch_Schedule",
|
||||
"BIM_Report",
|
||||
"BIM_Preflight",
|
||||
"Draft_AnnotationStyleEditor",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "All Spaces",
|
||||
"description": "Selects all Space objects in the document.",
|
||||
"query": "SELECT * FROM document WHERE IfcType = 'Space'"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "All Walls",
|
||||
"description": "Selects all Wall objects in the document.",
|
||||
"query": "SELECT * FROM document WHERE IfcType = 'Wall'"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Count by IfcType",
|
||||
"description": "Counts all objects, grouped by their IfcType.",
|
||||
"query": "SELECT IfcType, COUNT(*) FROM document GROUP BY IfcType"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Room and Area Schedule",
|
||||
"description": "A standard schedule listing all rooms and their areas, with a final total.",
|
||||
"statements": [
|
||||
{
|
||||
"description": "Room List",
|
||||
"query_string": "SELECT Label, Area FROM document WHERE IfcType = 'Space'",
|
||||
"use_description_as_header": true,
|
||||
"include_column_names": true,
|
||||
"add_empty_row_after": false,
|
||||
"print_results_in_bold": false,
|
||||
"is_pipelined": false
|
||||
},
|
||||
{
|
||||
"description": "Total Living Area",
|
||||
"query_string": "SELECT 'Total', SUM(Area) FROM document WHERE IfcType = 'Space'",
|
||||
"use_description_as_header": false,
|
||||
"include_column_names": false,
|
||||
"add_empty_row_after": false,
|
||||
"print_results_in_bold": true,
|
||||
"is_pipelined": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Wall Quantities",
|
||||
"description": "A schedule that lists all walls and calculates their total length.",
|
||||
"statements": [
|
||||
{
|
||||
"description": "Wall List",
|
||||
"query_string": "SELECT Label, Length, Height, Width FROM document WHERE IfcType = 'Wall'",
|
||||
"use_description_as_header": true,
|
||||
"include_column_names": true,
|
||||
"add_empty_row_after": true,
|
||||
"print_results_in_bold": false,
|
||||
"is_pipelined": false
|
||||
},
|
||||
{
|
||||
"description": "Total Wall Length",
|
||||
"query_string": "SELECT 'Total Length', SUM(Length) FROM document WHERE IfcType = 'Wall'",
|
||||
"use_description_as_header": false,
|
||||
"include_column_names": false,
|
||||
"add_empty_row_after": false,
|
||||
"print_results_in_bold": true,
|
||||
"is_pipelined": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -125,6 +125,7 @@
|
||||
<file>icons/BIM_ProjectManager.svg</file>
|
||||
<file>icons/BIM_Reextrude.svg</file>
|
||||
<file>icons/BIM_Reorder.svg</file>
|
||||
<file>icons/BIM_Report.svg</file>
|
||||
<file>icons/BIM_ResetCloneColors.svg</file>
|
||||
<file>icons/BIM_Rewire.svg</file>
|
||||
<file>icons/BIM_Slab.svg</file>
|
||||
|
||||
116
src/Mod/BIM/Resources/ArchSql.lark
Normal file
116
src/Mod/BIM/Resources/ArchSql.lark
Normal file
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright (c) 2019 Daniel Furtlehner (furti)
|
||||
// Copyright (c) 2025 The FreeCAD Project
|
||||
//
|
||||
// This file is a derivative work of the sql_grammar.peg file from the
|
||||
// FreeCAD-Reporting workbench (https://github.com/furti/FreeCAD-Reporting).
|
||||
// As per the terms of the original MIT license, this derivative work is also
|
||||
// licensed under the MIT license.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, to be subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Main Rule
|
||||
start: statement
|
||||
statement: SELECT columns from_clause where_clause? group_by_clause? order_by_clause? ";"*
|
||||
|
||||
// Clauses
|
||||
// Allow FROM to accept either a simple reference (e.g., 'document')
|
||||
// or a function invocation that returns a set (e.g., CHILDREN(SELECT ...)).
|
||||
from_clause: FROM from_source
|
||||
from_source: CNAME | from_function
|
||||
from_function: CNAME "(" statement ("," operand)? ")"
|
||||
where_clause: WHERE boolean_expression
|
||||
group_by_clause: GROUP BY member_access ("," member_access)*
|
||||
order_by_clause: ORDER BY member_access ("," member_access)* (ASC | DESC)?
|
||||
columns: (column ("," column)*)
|
||||
column: (ASTERISK | operand) as_clause?
|
||||
as_clause: AS (literal | CNAME)
|
||||
// Arithmetic Expression Parsing with Operator Precedence
|
||||
expr: term (ADD term | SUB term)* // Lowest precedence: Addition/Subtraction
|
||||
term: member_access (MUL member_access | DIV member_access)* // Next precedence: Multiplication/Division
|
||||
member_access: primary ("." (CNAME | function))* // Explicitly allow a function after a dot
|
||||
primary: function | CNAME | literal | NUMBER | NULL | "(" expr ")" // The fundamental building blocks
|
||||
|
||||
// 'operand' is an alias for a full arithmetic expression.
|
||||
operand: expr
|
||||
|
||||
// Boolean Logic
|
||||
boolean_expression: boolean_or
|
||||
boolean_or: boolean_or OR boolean_and -> boolean_expression_recursive
|
||||
| boolean_and
|
||||
boolean_and: boolean_and AND boolean_term -> boolean_expression_recursive
|
||||
| boolean_term
|
||||
boolean_term: boolean_comparison | in_expression | "(" boolean_expression ")"
|
||||
boolean_comparison: operand comparison_operator operand
|
||||
in_expression: member_access IN "(" literal ("," literal)* ")"
|
||||
|
||||
// Comparison Operator
|
||||
comparison_operator: eq_op | neq_op | gt_op | lt_op | gte_op | lte_op | is_not_op | is_op | like_op
|
||||
eq_op: "="
|
||||
neq_op: "!="
|
||||
gt_op: ">"
|
||||
lt_op: "<"
|
||||
gte_op: ">="
|
||||
lte_op: "<="
|
||||
is_not_op: IS NOT
|
||||
is_op: IS
|
||||
like_op: LIKE
|
||||
|
||||
// Basic Tokens
|
||||
literal: STRING
|
||||
|
||||
// Functions
|
||||
function: CNAME "(" function_args? ")"
|
||||
function_args: ASTERISK | (operand ("," operand)*)
|
||||
|
||||
// Terminal Definitions
|
||||
SELECT: "SELECT"i
|
||||
FROM: "FROM"i
|
||||
WHERE: "WHERE"i
|
||||
AS: "AS"i
|
||||
OR: "OR"i
|
||||
AND: "AND"i
|
||||
IS: "IS"i
|
||||
NOT: "NOT"i
|
||||
IN: "IN"i
|
||||
LIKE: "LIKE"i
|
||||
GROUP: "GROUP"i
|
||||
BY: "BY"i
|
||||
ORDER: "ORDER"i
|
||||
ASC: "ASC"i
|
||||
DESC: "DESC"i
|
||||
ADD: "+"
|
||||
SUB: "-"
|
||||
MUL: "*"
|
||||
DIV: "/"
|
||||
ASTERISK.2: "*" // Higher priority
|
||||
NULL.2: "NULL"i
|
||||
STRING : /"[^"]*"|'[^']*'/
|
||||
CNAME: /[a-zA-Z_][\w\.]*/ // An identifier cannot start with a digit, allows unicode via \w
|
||||
NUMBER.2: /[0-9]+(\.[0-9]+)?/ // Higher priority
|
||||
%import common.WS
|
||||
|
||||
// Define comment terminals. The regex for multi-line is non-greedy.
|
||||
SINGLE_LINE_COMMENT: /--[^\n]*/
|
||||
MULTI_LINE_COMMENT: /\/\*[\s\S]*?\*\//
|
||||
|
||||
%ignore WS
|
||||
%ignore SINGLE_LINE_COMMENT
|
||||
%ignore MULTI_LINE_COMMENT
|
||||
52
src/Mod/BIM/Resources/ArchSqlParserGenerator.py
Normal file
52
src/Mod/BIM/Resources/ArchSqlParserGenerator.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Copyright (c) 2025 The FreeCAD Project
|
||||
|
||||
"""This script generates a standalone Python parser from the ArchSql.lark grammar."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
from lark import Lark
|
||||
from lark.tools.standalone import gen_standalone
|
||||
except ImportError:
|
||||
print("Error: The 'lark' Python package is required to generate the parser.")
|
||||
print("Please install it using: pip install lark")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python ArchSqlParserGenerator.py <input_grammar.lark> <output_parser.py>")
|
||||
return 1
|
||||
|
||||
input_file = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
|
||||
if not os.path.exists(input_file):
|
||||
print(f"Error: Input grammar file not found at '{input_file}'")
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"Generating standalone parser from '{os.path.basename(input_file)}' to '{os.path.basename(output_file)}'..."
|
||||
)
|
||||
|
||||
# 1. Read the grammar file content.
|
||||
with open(input_file, "r", encoding="utf8") as f:
|
||||
grammar_text = f.read()
|
||||
|
||||
# 2. Create an instance of the Lark parser.
|
||||
# The 'lalr' parser is recommended for performance.
|
||||
lark_instance = Lark(grammar_text, parser="lalr")
|
||||
|
||||
# 3. Open the output file and call the gen_standalone() API function.
|
||||
with open(output_file, "w", encoding="utf8") as f:
|
||||
gen_standalone(lark_instance, out=f)
|
||||
|
||||
print("Parser generation complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
244
src/Mod/BIM/Resources/icons/BIM_Report.svg
Normal file
244
src/Mod/BIM/Resources/icons/BIM_Report.svg
Normal file
@@ -0,0 +1,244 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
id="svg249"
|
||||
version="1.1"
|
||||
sodipodi:docname="BIM_Report.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1850"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview1618"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="12.890625"
|
||||
inkscape:cx="32"
|
||||
inkscape:cy="32"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg249">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid1620" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
id="linearGradient3815">
|
||||
<stop
|
||||
id="stop3817"
|
||||
offset="0"
|
||||
style="stop-color:#d3d7cf;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3819"
|
||||
offset="1"
|
||||
style="stop-color:#ffffff;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient3815"
|
||||
id="linearGradient3771"
|
||||
x1="98"
|
||||
y1="1047.3622"
|
||||
x2="81"
|
||||
y2="993.36218"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-60,-988.36218)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3771-7">
|
||||
<stop
|
||||
style="stop-color:#c4a000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3773" />
|
||||
<stop
|
||||
style="stop-color:#fce94f;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3775" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3771-7"
|
||||
id="linearGradient1775"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0175439,0,0,1.0212766,-53.052635,-0.1914894)"
|
||||
x1="35"
|
||||
y1="55"
|
||||
x2="27"
|
||||
y2="11" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD LGPL2+</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="display:inline;fill:url(#linearGradient3771);fill-opacity:1;stroke:#2e3436;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 11,3 v 58.00002 h 42 v -48 L 43,3 Z"
|
||||
id="path2991" />
|
||||
<path
|
||||
style="display:inline;fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 13,5 V 59.00002 H 51 V 13.81392 L 41.99741,5 Z"
|
||||
id="path3763" />
|
||||
<path
|
||||
style="display:inline;fill:#2e3436;fill-opacity:0.392157;stroke:#2e3436;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 43,3 V 13.00002 H 53 Z"
|
||||
id="path2993" />
|
||||
</g>
|
||||
<g
|
||||
id="g1879">
|
||||
<g
|
||||
id="layer3-2"
|
||||
style="display:inline;fill:#2b7ac4;fill-opacity:1;stroke:#0063a7;stroke-opacity:1"
|
||||
transform="translate(-9.9997206,13.000001)">
|
||||
<g
|
||||
id="layer1-3-7"
|
||||
transform="matrix(0.55823442,0,0,0.55994556,13.136501,14.655248)"
|
||||
style="display:inline;fill:#2b7ac4;fill-opacity:1;stroke:#0063a7;stroke-width:3.57725;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path
|
||||
style="fill:#2b7ac4;fill-opacity:1;stroke:#0063a7;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 21.252168,14.902792 39.166134,18.474567 51.705324,13.116903 33.791709,9.5451275 Z"
|
||||
id="path2993-0-0" />
|
||||
<path
|
||||
style="fill:#2b7ac4;fill-opacity:1;stroke:#0063a7;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.705324,13.116903 v 19.64561 l -12.53919,5.357664 v -19.64561 z"
|
||||
id="path2995-9" />
|
||||
<path
|
||||
id="path3825-3"
|
||||
d="m 21.252168,14.902791 17.913966,3.571776 v 19.64561 L 21.252168,34.548401 Z"
|
||||
style="display:inline;overflow:visible;visibility:visible;fill:#2b7ac4;fill-opacity:1;fill-rule:evenodd;stroke:#0063a7;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="layer3-0"
|
||||
style="display:inline;fill:#319db6;fill-opacity:1;stroke:#00829a;stroke-opacity:1"
|
||||
transform="translate(7.0002793,12)">
|
||||
<g
|
||||
id="layer1-3-6"
|
||||
transform="matrix(0.55823442,0,0,0.55994556,13.136501,14.655248)"
|
||||
style="display:inline;fill:#319db6;fill-opacity:1;stroke:#00829a;stroke-width:3.57725;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path
|
||||
style="fill:#319db6;fill-opacity:1;stroke:#00829a;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 21.252168,14.902792 39.166134,18.474567 51.705324,13.116903 33.791709,9.5451275 Z"
|
||||
id="path2993-0-2" />
|
||||
<path
|
||||
style="fill:#319db6;fill-opacity:1;stroke:#00829a;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.705324,13.116903 v 19.64561 l -12.53919,5.357664 v -19.64561 z"
|
||||
id="path2995-6" />
|
||||
<path
|
||||
id="path3825-1"
|
||||
d="m 21.252168,14.902791 17.913966,3.571776 v 19.64561 L 21.252168,34.548401 Z"
|
||||
style="display:inline;overflow:visible;visibility:visible;fill:#319db6;fill-opacity:1;fill-rule:evenodd;stroke:#00829a;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="layer3"
|
||||
style="display:inline;fill:#e5234b;fill-opacity:1;stroke:#c00017;stroke-opacity:1"
|
||||
transform="translate(-2.9999132,-1.0004713)">
|
||||
<g
|
||||
id="layer1-3"
|
||||
transform="matrix(0.55823442,0,0,0.55994556,13.136501,14.655248)"
|
||||
style="display:inline;fill:#e5234b;fill-opacity:1;stroke:#c00017;stroke-width:3.57725;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path
|
||||
style="fill:#e5234b;fill-opacity:1;stroke:#c00017;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 21.252168,14.902792 39.166134,18.474567 51.705324,13.116903 33.791709,9.5451275 Z"
|
||||
id="path2993-0" />
|
||||
<path
|
||||
style="fill:#e5234b;fill-opacity:1;stroke:#c00017;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.705324,13.116903 v 19.64561 l -12.53919,5.357664 v -19.64561 z"
|
||||
id="path2995" />
|
||||
<path
|
||||
id="path3825"
|
||||
d="m 21.252168,14.902791 17.913966,3.571776 v 19.64561 L 21.252168,34.548401 Z"
|
||||
style="display:inline;overflow:visible;visibility:visible;fill:#e5234b;fill-opacity:1;fill-rule:evenodd;stroke:#c00017;stroke-width:3.57725;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
style="font-variation-settings:normal;vector-effect:none;fill:url(#linearGradient1775);fill-opacity:1;stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;stop-color:#000000"
|
||||
id="rect1698"
|
||||
width="34"
|
||||
height="34"
|
||||
x="15"
|
||||
y="19" />
|
||||
<rect
|
||||
style="font-variation-settings:normal;vector-effect:none;fill:none;fill-opacity:1;stroke:#fce94f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;stop-color:#000000"
|
||||
id="rect1725"
|
||||
width="30"
|
||||
height="30"
|
||||
x="17"
|
||||
y="21" />
|
||||
<path
|
||||
style="font-variation-settings:normal;vector-effect:none;fill:url(#linearGradient1775);fill-opacity:1;stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;stop-color:#000000;opacity:1;stop-opacity:1"
|
||||
d="M 27,20 V 52"
|
||||
id="path1748"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:url(#linearGradient1775);stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-variation-settings:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke-dashoffset:0;marker:none;stop-color:#000000;stop-opacity:1"
|
||||
d="M 16,28 H 48"
|
||||
id="path1783" />
|
||||
<path
|
||||
style="fill:url(#linearGradient1775);stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-variation-settings:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke-dashoffset:0;marker:none;stop-color:#000000;stop-opacity:1"
|
||||
d="M 16,36 H 48"
|
||||
id="path1785" />
|
||||
<path
|
||||
style="fill:url(#linearGradient1775);stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-variation-settings:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke-dashoffset:0;marker:none;stop-color:#000000;stop-opacity:1"
|
||||
d="M 16,44 H 48"
|
||||
id="path1787" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -48,3 +48,4 @@ from bimtests.TestArchSchedule import TestArchSchedule
|
||||
from bimtests.TestArchTruss import TestArchTruss
|
||||
from bimtests.TestArchComponent import TestArchComponent
|
||||
from bimtests.TestWebGLExport import TestWebGLExport
|
||||
from bimtests.TestArchReport import TestArchReport
|
||||
|
||||
@@ -27,4 +27,5 @@
|
||||
from bimtests.TestArchImportersGui import TestArchImportersGui
|
||||
from bimtests.TestArchBuildingPartGui import TestArchBuildingPartGui
|
||||
from bimtests.TestArchSiteGui import TestArchSiteGui
|
||||
from bimtests.TestArchReportGui import TestArchReportGui
|
||||
from bimtests.TestWebGLExportGui import TestWebGLExportGui
|
||||
|
||||
27
src/Mod/BIM/bimcommands/BimReport.py
Normal file
27
src/Mod/BIM/bimcommands/BimReport.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Copyright (c) 2025 The FreeCAD Project
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
|
||||
class BIM_Report:
|
||||
"""The command to create a new BIM Report object."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": "BIM_Report",
|
||||
"MenuText": "BIM Report",
|
||||
"ToolTip": "Create a new BIM Report to query model data with SQL",
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
FreeCADGui.addModule("Arch")
|
||||
FreeCADGui.doCommand("Arch.makeReport()")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
|
||||
|
||||
FreeCADGui.addCommand("BIM_Report", BIM_Report())
|
||||
@@ -70,3 +70,37 @@ class TestArchBase(unittest.TestCase):
|
||||
passed as the prepend_text argument
|
||||
"""
|
||||
FreeCAD.Console.PrintMessage(prepend_text + text + end)
|
||||
|
||||
def assertDictContainsSubset(self, subset, main_dict):
|
||||
"""Asserts that one dictionary's key-value pairs are contained within another.
|
||||
|
||||
This method iterates through each key-value pair in the `subset`
|
||||
dictionary and verifies two conditions against the `main_dict`:
|
||||
1. The key exists in `main_dict`.
|
||||
2. The value associated with that key in `main_dict` is equal to the
|
||||
value in `subset`.
|
||||
|
||||
Use Case:
|
||||
This assertion is more flexible than `assertDictEqual`. It is ideal for
|
||||
tests where a function or query returns a large dictionary of results,
|
||||
but the test's scope is only to validate a specific, known subset of
|
||||
those results. It allows the test to succeed even if `main_dict`
|
||||
contains extra, irrelevant keys, thus preventing test brittleness.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subset : dict
|
||||
The dictionary containing the expected key-value pairs that must
|
||||
be found.
|
||||
main_dict : dict
|
||||
The larger, actual dictionary returned by the code under test,
|
||||
which is checked for the presence of the subset.
|
||||
|
||||
Example:
|
||||
>>> actual_results = {'Wall': 4, 'Structure': 2, 'Window': 1}
|
||||
>>> expected_subset = {'Wall': 4, 'Window': 1}
|
||||
>>> self.assertDictContainsSubset(expected_subset, actual_results) # This will pass.
|
||||
"""
|
||||
for key, value in subset.items():
|
||||
self.assertIn(key, main_dict)
|
||||
self.assertEqual(main_dict[key], value)
|
||||
|
||||
2113
src/Mod/BIM/bimtests/TestArchReport.py
Normal file
2113
src/Mod/BIM/bimtests/TestArchReport.py
Normal file
File diff suppressed because it is too large
Load Diff
193
src/Mod/BIM/bimtests/TestArchReportGui.py
Normal file
193
src/Mod/BIM/bimtests/TestArchReportGui.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""GUI tests for ArchReport features that require a running FreeCAD GUI.
|
||||
|
||||
These tests inherit from the GUI test base `TestArchBaseGui` which skips
|
||||
the whole class when `FreeCAD.GuiUp` is False.
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
import Arch
|
||||
import FreeCADGui
|
||||
import ArchReport
|
||||
|
||||
from bimtests.TestArchBaseGui import TestArchBaseGui
|
||||
|
||||
|
||||
class TestArchReportGui(TestArchBaseGui):
|
||||
"""GUI-enabled tests ported from TestArchReport.
|
||||
|
||||
These tests rely on Qt widgets and the BIM workbench UI panels.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.doc = self.document
|
||||
self.panel = None
|
||||
|
||||
# Recreate the same minimal scene that TestArchReport.setUp creates
|
||||
self.wall_ext = Arch.makeWall(length=1000, name="Exterior Wall")
|
||||
self.wall_ext.IfcType = "Wall"
|
||||
self.wall_ext.Height = FreeCAD.Units.Quantity(3000, "mm")
|
||||
|
||||
self.wall_int = Arch.makeWall(length=500, name="Interior partition wall")
|
||||
self.wall_int.IfcType = "Wall"
|
||||
self.wall_int.Height = FreeCAD.Units.Quantity(2500, "mm")
|
||||
|
||||
self.column = Arch.makeStructure(length=300, width=330, height=2000, name="Main Column")
|
||||
self.column.IfcType = "Column"
|
||||
|
||||
self.beam = Arch.makeStructure(length=2000, width=200, height=400, name="Main Beam")
|
||||
self.beam.IfcType = "Beam"
|
||||
|
||||
self.window = Arch.makeWindow(name="Living Room Window")
|
||||
self.window.IfcType = "Window"
|
||||
|
||||
self.part_box = self.doc.addObject("Part::Box", "Generic Box")
|
||||
|
||||
# Spreadsheet used by some report features
|
||||
self.spreadsheet = self.doc.addObject("Spreadsheet::Sheet", "ReportTarget")
|
||||
self.doc.recompute()
|
||||
|
||||
def tearDown(self):
|
||||
# This method is automatically called after EACH test function.
|
||||
if self.panel:
|
||||
# If a panel was created, ensure it is closed.
|
||||
FreeCADGui.Control.closeDialog()
|
||||
self.panel = None # Clear the reference
|
||||
super().tearDown()
|
||||
|
||||
def test_cheatsheet_dialog_creation(self):
|
||||
"""Tests that the Cheatsheet dialog can be created without errors."""
|
||||
api_data = Arch.getSqlApiDocumentation()
|
||||
dialog = ArchReport.CheatsheetDialog(api_data)
|
||||
self.assertIsNotNone(dialog)
|
||||
|
||||
def DISABLED_test_preview_pane_toggle_and_refresh(self):
|
||||
"""
|
||||
Tests the user workflow for the preview pane: toggling visibility,
|
||||
refreshing with a valid query, and checking the results.
|
||||
This replaces the obsolete test_task_panel_on_demand_preview.
|
||||
"""
|
||||
# 1. Arrange: Create a report object and the task panel.
|
||||
report_obj = Arch.makeReport(name="PreviewToggleTestReport")
|
||||
self.panel = ArchReport.ReportTaskPanel(report_obj)
|
||||
# Open the editor for the first (default) statement.
|
||||
self.panel._start_edit_session(row_index=0)
|
||||
|
||||
# 2. Assert Initial State: The preview pane should be hidden.
|
||||
self.assertFalse(
|
||||
self.panel.preview_pane.isVisible(), "Preview pane should be hidden by default."
|
||||
)
|
||||
|
||||
# 3. Act: Toggle the preview pane to show it.
|
||||
# A user click on a checkable button toggles its checked state.
|
||||
self.panel.btn_toggle_preview.setChecked(True)
|
||||
self.pump_gui_events()
|
||||
|
||||
# 4. Assert Visibility: The pane and its contents should now be visible.
|
||||
self.assertTrue(
|
||||
self.panel.preview_pane.isVisible(),
|
||||
"Preview pane should be visible after toggling it on.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.panel.btn_toggle_preview.text(),
|
||||
"Hide Preview",
|
||||
"Button text should update to 'Hide Preview'.",
|
||||
)
|
||||
self.assertTrue(
|
||||
self.panel.btn_refresh_preview.isVisible(),
|
||||
"Refresh button should be visible when pane is open.",
|
||||
)
|
||||
|
||||
# 5. Act: Set a valid query and refresh the preview.
|
||||
query = "SELECT Label, IfcType FROM document WHERE IfcType = 'Wall' ORDER BY Label"
|
||||
self.panel.sql_query_edit.setPlainText(query)
|
||||
self.panel.btn_refresh_preview.click()
|
||||
self.pump_gui_events()
|
||||
|
||||
# 6. Assert Correctness: The preview table should be populated correctly.
|
||||
self.assertEqual(
|
||||
self.panel.table_preview_results.columnCount(),
|
||||
2,
|
||||
"Preview table should have 2 columns for the valid query.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.panel.table_preview_results.rowCount(),
|
||||
2,
|
||||
"Preview table should have 2 rows for the two wall objects.",
|
||||
)
|
||||
# Check cell content to confirm the query ran correctly.
|
||||
self.assertEqual(self.panel.table_preview_results.item(0, 0).text(), self.wall_ext.Label)
|
||||
self.assertEqual(self.panel.table_preview_results.item(1, 1).text(), self.wall_int.IfcType)
|
||||
|
||||
# 7. Act: Toggle the preview pane to hide it again.
|
||||
self.panel.btn_toggle_preview.setChecked(False)
|
||||
self.pump_gui_events()
|
||||
|
||||
# 8. Assert Final State: The pane should be hidden.
|
||||
self.assertFalse(
|
||||
self.panel.preview_pane.isVisible(),
|
||||
"Preview pane should be hidden after toggling it off.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.panel.btn_toggle_preview.text(),
|
||||
"Show Preview",
|
||||
"Button text should revert to 'Show Preview'.",
|
||||
)
|
||||
|
||||
def DISABLED_test_preview_pane_displays_errors_gracefully(self):
|
||||
"""
|
||||
Tests that the preview pane displays a user-friendly error message when
|
||||
the query is invalid, instead of raising an exception.
|
||||
This replaces the obsolete test_preview_button_handles_errors_gracefully_in_ui.
|
||||
"""
|
||||
# 1. Arrange: Create the report and panel, then open the editor and preview.
|
||||
report_obj = Arch.makeReport(name="PreviewErrorTestReport")
|
||||
self.panel = ArchReport.ReportTaskPanel(report_obj)
|
||||
self.panel._start_edit_session(row_index=0)
|
||||
self.panel.btn_toggle_preview.setChecked(True)
|
||||
self.pump_gui_events()
|
||||
|
||||
# 2. Act: Set an invalid query and click the refresh button.
|
||||
invalid_query = "SELECT Label FRM document" # Deliberate syntax error
|
||||
self.panel.sql_query_edit.setPlainText(invalid_query)
|
||||
self.panel.btn_refresh_preview.click()
|
||||
self.pump_gui_events()
|
||||
|
||||
# 3. Assert: The preview table should be visible and display the error.
|
||||
self.assertTrue(
|
||||
self.panel.table_preview_results.isVisible(),
|
||||
"Preview table should remain visible to display the error.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.panel.table_preview_results.rowCount(),
|
||||
1,
|
||||
"Error display should occupy a single row.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.panel.table_preview_results.columnCount(),
|
||||
1,
|
||||
"Error display should occupy a single column.",
|
||||
)
|
||||
|
||||
error_item = self.panel.table_preview_results.item(0, 0)
|
||||
self.assertIsNotNone(error_item, "An error item should have been placed in the table.")
|
||||
# Check for keywords that indicate a graceful error message.
|
||||
self.assertIn(
|
||||
"Syntax Error", error_item.text(), "The error message should indicate a syntax error."
|
||||
)
|
||||
self.assertIn(
|
||||
"❌", error_item.text(), "The error message should contain a visual error indicator."
|
||||
)
|
||||
|
||||
def test_hover_tooltips(self):
|
||||
"""Tests that the SQL editor can generate tooltips."""
|
||||
editor = ArchReport.SqlQueryEditor()
|
||||
api_docs = Arch.getSqlApiDocumentation()
|
||||
editor.set_api_documentation(api_docs)
|
||||
|
||||
func_tooltip = editor._get_tooltip_for_word("CONVERT")
|
||||
self.assertIn("CONVERT(quantity, 'unit')", func_tooltip)
|
||||
self.assertIn("Utility", func_tooltip)
|
||||
|
||||
clause_tooltip = editor._get_tooltip_for_word("SELECT")
|
||||
self.assertIn("SQL Clause", clause_tooltip)
|
||||
214
src/Mod/BIM/bimtests/fixtures/BimFixtures.py
Normal file
214
src/Mod/BIM/bimtests/fixtures/BimFixtures.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Copyright (c) 2025 Furgo
|
||||
|
||||
"""Reusable BIM test fixtures.
|
||||
|
||||
Exports:
|
||||
- create_test_model(document, **overrides)
|
||||
- DEFAULTS, LABELS
|
||||
|
||||
This module centralizes the complex sample model used across multiple tests.
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
import Arch
|
||||
import Draft
|
||||
|
||||
__all__ = ["create_test_model", "DEFAULTS", "LABELS"]
|
||||
|
||||
# Canonical defaults used by the fixture
|
||||
DEFAULTS = {
|
||||
"building_length": 4000.0,
|
||||
"building_width": 3200.0,
|
||||
"ground_floor_height": 3200.0,
|
||||
"interior_wall_height": 3000.0,
|
||||
"slab_thickness": 200.0,
|
||||
"roof_overhang": 200.0,
|
||||
}
|
||||
|
||||
# Canonical labels used by the fixture for predictable queries
|
||||
LABELS = {
|
||||
"site": "Main Site",
|
||||
"building": "Main Building",
|
||||
"ground_floor": "Ground Floor",
|
||||
"upper_floor": "Upper Floor",
|
||||
"exterior_wall": "Exterior Wall",
|
||||
"interior_wall": "Interior Partition",
|
||||
"front_door": "Front Door",
|
||||
"living_window": "Living Room Window",
|
||||
"office_space": "Office Space",
|
||||
"living_space": "Living Space",
|
||||
"generic_box": "Generic Box",
|
||||
}
|
||||
|
||||
|
||||
def create_test_model(document, **overrides):
|
||||
"""Create a complete, standard BIM model in the provided document.
|
||||
|
||||
The function returns a dict of key objects for tests to reference. It
|
||||
accepts optional overrides for numeric defaults via keyword args that map
|
||||
to keys in DEFAULTS (e.g. building_length=5000.0).
|
||||
|
||||
Returns None if document is falsy.
|
||||
"""
|
||||
doc = document
|
||||
if not doc:
|
||||
FreeCAD.Console.PrintError(
|
||||
"Error: No active document found. Please create a new document first.\n"
|
||||
)
|
||||
return {}
|
||||
|
||||
# Merge defaults with overrides
|
||||
cfg = DEFAULTS.copy()
|
||||
cfg.update(overrides)
|
||||
|
||||
building_length = cfg["building_length"]
|
||||
building_width = cfg["building_width"]
|
||||
ground_floor_height = cfg["ground_floor_height"]
|
||||
interior_wall_height = cfg["interior_wall_height"]
|
||||
slab_thickness = cfg["slab_thickness"]
|
||||
roof_overhang = cfg["roof_overhang"]
|
||||
|
||||
# --- 1. BIM Hierarchy (Site, Building, Levels) ---
|
||||
site = Arch.makeSite(name=LABELS["site"])
|
||||
building = Arch.makeBuilding(name=LABELS["building"])
|
||||
site.addObject(building)
|
||||
|
||||
level_0 = Arch.makeFloor(name=LABELS["ground_floor"])
|
||||
level_0.Height = ground_floor_height
|
||||
level_1 = Arch.makeFloor(name=LABELS["upper_floor"])
|
||||
level_1.Height = ground_floor_height
|
||||
level_1.Placement.Base.z = ground_floor_height
|
||||
building.addObject(level_0)
|
||||
building.addObject(level_1)
|
||||
|
||||
# --- 2. Ground Floor Walls ---
|
||||
p1 = FreeCAD.Vector(0, 0, 0)
|
||||
p2 = FreeCAD.Vector(building_length, 0, 0)
|
||||
p3 = FreeCAD.Vector(building_length, building_width, 0)
|
||||
p4 = FreeCAD.Vector(0, building_width, 0)
|
||||
exterior_wire = Draft.makeWire([p1, p2, p3, p4], closed=True)
|
||||
exterior_wall = Arch.makeWall(
|
||||
exterior_wire, name=LABELS["exterior_wall"], height=ground_floor_height
|
||||
)
|
||||
level_0.addObject(exterior_wall)
|
||||
|
||||
p5 = FreeCAD.Vector(building_length / 2, 0, 0)
|
||||
p6 = FreeCAD.Vector(building_length / 2, building_width, 0)
|
||||
interior_wire = Draft.makeWire([p5, p6])
|
||||
interior_wall = Arch.makeWall(
|
||||
interior_wire, name=LABELS["interior_wall"], height=interior_wall_height
|
||||
)
|
||||
interior_wall.Width = 100.0
|
||||
level_0.addObject(interior_wall)
|
||||
|
||||
doc.recompute()
|
||||
|
||||
# --- 3. Openings (Doors and Windows) ---
|
||||
door = Arch.makeWindowPreset(
|
||||
"Simple door", width=900, height=2100, h1=50, h2=50, h3=50, w1=100, w2=40, o1=0, o2=0
|
||||
)
|
||||
door.Placement = FreeCAD.Placement(
|
||||
FreeCAD.Vector(800, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)
|
||||
)
|
||||
door.Label = LABELS["front_door"]
|
||||
door.Hosts = [exterior_wall]
|
||||
|
||||
window = Arch.makeWindowPreset(
|
||||
"Open 1-pane", width=1500, height=1200, h1=50, h2=50, h3=50, w1=100, w2=50, o1=0, o2=50
|
||||
)
|
||||
window.Placement = FreeCAD.Placement(
|
||||
FreeCAD.Vector(building_length, building_width / 2, 900),
|
||||
FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 270),
|
||||
)
|
||||
window.Label = LABELS["living_window"]
|
||||
window.Hosts = [exterior_wall]
|
||||
|
||||
doc.recompute()
|
||||
|
||||
# --- 4. Spaces (from Volumetric Shapes) ---
|
||||
office_box = doc.addObject("Part::Box", "OfficeVolume")
|
||||
office_box.Length = building_length / 2
|
||||
office_box.Width = building_width
|
||||
office_box.Height = interior_wall_height
|
||||
room1_space = Arch.makeSpace(office_box, name=LABELS["office_space"])
|
||||
level_0.addObject(room1_space)
|
||||
|
||||
living_box = doc.addObject("Part::Box", "LivingVolume")
|
||||
living_box.Length = building_length / 2
|
||||
living_box.Width = building_width
|
||||
living_box.Height = interior_wall_height
|
||||
living_box.Placement.Base = FreeCAD.Vector(building_length / 2, 0, 0)
|
||||
room2_space = Arch.makeSpace(living_box, name=LABELS["living_space"])
|
||||
level_0.addObject(room2_space)
|
||||
|
||||
doc.recompute()
|
||||
|
||||
# --- 5. Structural Elements ---
|
||||
column = Arch.makeStructure(
|
||||
length=200, width=200, height=interior_wall_height, name="Main Column"
|
||||
)
|
||||
column.IfcType = "Column"
|
||||
column.Placement.Base = FreeCAD.Vector(
|
||||
(building_length / 2) - 100, (building_width / 2) - 100, 0
|
||||
)
|
||||
level_0.addObject(column)
|
||||
|
||||
beam = Arch.makeStructure(length=building_length, width=150, height=300, name="Main Beam")
|
||||
beam.IfcType = "Beam"
|
||||
beam.Placement = FreeCAD.Placement(
|
||||
FreeCAD.Vector(0, building_width / 2, interior_wall_height), FreeCAD.Rotation()
|
||||
)
|
||||
level_0.addObject(beam)
|
||||
|
||||
# --- 6. Upper Floor Slab and Roof ---
|
||||
slab_profile = Draft.makeRectangle(
|
||||
length=building_length,
|
||||
height=building_width,
|
||||
placement=FreeCAD.Placement(FreeCAD.Vector(0, 0, interior_wall_height), FreeCAD.Rotation()),
|
||||
)
|
||||
slab = Arch.makeStructure(slab_profile, height=slab_thickness, name="Floor Slab")
|
||||
slab.IfcType = "Slab"
|
||||
level_1.addObject(slab)
|
||||
|
||||
roof_profile = Draft.makeRectangle(
|
||||
length=building_length + (2 * roof_overhang),
|
||||
height=building_width + (2 * roof_overhang),
|
||||
placement=FreeCAD.Placement(
|
||||
FreeCAD.Vector(-roof_overhang, -roof_overhang, ground_floor_height), FreeCAD.Rotation()
|
||||
),
|
||||
)
|
||||
doc.recompute()
|
||||
|
||||
safe_run = (max(roof_profile.Length.Value, roof_profile.Height.Value) / 2) + 100
|
||||
|
||||
roof = Arch.makeRoof(roof_profile, angles=[30.0] * 4, run=[safe_run] * 4, name="Main Roof")
|
||||
level_1.addObject(roof)
|
||||
|
||||
# --- 7. Non-BIM Object ---
|
||||
generic_box = doc.addObject("Part::Box", LABELS["generic_box"])
|
||||
generic_box.Placement.Base = FreeCAD.Vector(building_length + 1000, building_width + 1000, 0)
|
||||
|
||||
# --- 8. Custom Dynamic Property ---
|
||||
exterior_wall.addProperty("App::PropertyString", "FireRating", "BIM")
|
||||
exterior_wall.FireRating = "60 minutes"
|
||||
|
||||
# --- Final Step: Recompute and return references ---
|
||||
doc.recompute()
|
||||
|
||||
model_objects = {
|
||||
"site": site,
|
||||
"building": building,
|
||||
"ground_floor": level_0,
|
||||
"upper_floor": level_1,
|
||||
"exterior_wall": exterior_wall,
|
||||
"interior_wall": interior_wall,
|
||||
"front_door": door,
|
||||
"living_window": window,
|
||||
"office_space": room1_space,
|
||||
"living_space": room2_space,
|
||||
"column": column,
|
||||
"slab": slab,
|
||||
}
|
||||
return model_objects
|
||||
6
src/Mod/BIM/bimtests/fixtures/__init__.py
Normal file
6
src/Mod/BIM/bimtests/fixtures/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Fixtures package for BIM test helpers.
|
||||
|
||||
This package contains reusable test fixtures for the BIM module tests.
|
||||
"""
|
||||
|
||||
__all__ = []
|
||||
Reference in New Issue
Block a user