mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 16:31:37 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0ebdd53c9 | |||
| 8ece3790aa | |||
| 6585f0ee2b | |||
| 4ec042595e | |||
| 48f6e2908b | |||
| 1cd7e52c49 | |||
| 2e6f17ed0f | |||
| b549e65ad9 | |||
| ee5d672493 | |||
| 135f4cf023 | |||
| 608d9ead0e | |||
| a0ce930994 | |||
| eafa158ca0 | |||
| 5db955d40c | |||
| df27f86237 | |||
| 0aaafaedc9 | |||
| 7881f5992f | |||
| 679fe4dcae | |||
| a89621b179 | |||
| 110e4050c8 | |||
| 6dafb7a5c2 | |||
| 4cedeec813 | |||
| 6314d9c818 | |||
| 9bbd2b1854 | |||
| b5d36aacf6 | |||
| 6d3bed1f7d | |||
| 0096c0f6a2 | |||
| 661be0d66d | |||
| 62ed650b75 | |||
| da4b2f6eee | |||
| 00ec587296 | |||
| 2a05528b6d | |||
| a5f7f0cd93 | |||
| 1fd7329122 | |||
| f3e047d78e | |||
| 041306c5f0 | |||
| 5fba0026dd | |||
| 4d92a64206 | |||
| 9d2de117a9 | |||
| 6ee3c7a15f | |||
| fdf9970685 | |||
| de65e50fb5 | |||
| 714105b9fd | |||
| f0b5ab860f | |||
| 528964ca56 | |||
| 2c2d0f2434 | |||
| 340d4fb82a | |||
| 65cd5701c3 | |||
| febde1bbbb | |||
| 315835063c | |||
| 1b0a2fd6de | |||
| c95c1905ed | |||
| 301dae103c | |||
| 85cd1c1923 | |||
| 091fc9e7e5 | |||
| a65f291a89 | |||
| e6dc582d82 | |||
| 366fa67a84 | |||
| 4a8b863b96 | |||
| db0bbf6b84 | |||
| a757641b84 | |||
| 8bd22178f7 | |||
| b4d7780e14 | |||
| 7c092db9e6 | |||
| 38e3bb0590 | |||
| 7f49c945b9 | |||
| 81f71e6418 | |||
| 6318610bdb | |||
| 82dd1d94de | |||
| 4004344c20 | |||
| fce7cd3eb4 | |||
| f3dcf0b539 | |||
| f5ebbd1917 | |||
| 77aa080334 | |||
| 5afad2b179 | |||
| 98aa2c635e | |||
| 7db1ceb07f | |||
| 84abf5e918 | |||
| 741e8233ca | |||
| 262b55630c |
@@ -10,9 +10,13 @@ on:
|
||||
- 'src/ifcgeomserver/**'
|
||||
- 'src/ifcjni/**'
|
||||
- 'src/ifcmax/**'
|
||||
- 'src/ifc5d/**'
|
||||
- 'src/ifcedit/**'
|
||||
- 'src/ifcmcp/**'
|
||||
- 'src/ifcopenshell-python/**'
|
||||
- '!src/ifcopenshell-python/docs/**'
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/ifcquery/**'
|
||||
- 'src/ifcwrap/**'
|
||||
- 'src/qtviewer/**'
|
||||
- 'src/svgfill/**'
|
||||
@@ -51,7 +55,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing psutil
|
||||
pip install src/bcf --no-deps
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
@@ -252,13 +256,26 @@ jobs:
|
||||
pip install deepdiff
|
||||
cd ../ifcdiff && make test || ERROR=1
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifc5d --no-deps
|
||||
pip install odfpy xlsxwriter
|
||||
cd ../ifc5d && make test || ERROR=1
|
||||
pip install -e ../ifcquery --no-deps
|
||||
cd ../ifcquery && make test || ERROR=1
|
||||
pip install -e ../ifcedit --no-deps
|
||||
cd ../ifcedit && make test || ERROR=1
|
||||
pip install mcp
|
||||
pip install -e ../ifcmcp --no-deps
|
||||
cd ../ifcmcp && make test || ERROR=1
|
||||
pip install -e ../ifctester --no-deps
|
||||
cd ../ifctester && make test || ERROR=1
|
||||
make build-ids-docs || ERROR=1
|
||||
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
||||
# mathutils only has pre-built wheels for Python 3.13+; skip on older versions.
|
||||
cd ../ifcopenshell-python
|
||||
pip install mathutils
|
||||
make test-mathutils || ERROR=1
|
||||
if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 13) else 1)"; then
|
||||
pip install mathutils
|
||||
make test-mathutils || ERROR=1
|
||||
fi
|
||||
if [ $ERROR -ne 0 ]; then
|
||||
echo "One or more tests failed";
|
||||
exit 1;
|
||||
|
||||
@@ -28,6 +28,7 @@ venv
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
.vs
|
||||
/*.code-workspace
|
||||
|
||||
# PyCharm files
|
||||
.idea
|
||||
@@ -128,3 +129,4 @@ src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
*.claude
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
|
||||
@@ -129,6 +129,21 @@ on CI to catch formatting issues.
|
||||
within each package under `src/`.
|
||||
- Run the existing test suite for the package you modified before submitting.
|
||||
|
||||
## In-Progress Feature Notes
|
||||
|
||||
Living design and working notes for unmerged feature branches live in
|
||||
[`docs/dev-notes/`](docs/dev-notes/), one Markdown file per feature, named after the
|
||||
branch. They capture the problem, the design decisions and the *why*, and what still
|
||||
needs testing — so collaborators (and their AI agents) can pick up the context behind a
|
||||
branch. Because the note is committed on the branch, it travels with the PR.
|
||||
|
||||
- Before working on a feature branch, read its note in `docs/dev-notes/` if one exists.
|
||||
- Keep the note current as the PR is refined.
|
||||
- These are not user documentation; at merge they are removed or their durable parts
|
||||
promoted to code comments / permanent docs.
|
||||
|
||||
See [`docs/dev-notes/README.md`](docs/dev-notes/README.md) for details.
|
||||
|
||||
## Architecture Quick Reference
|
||||
|
||||
### Directory Structure
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Developer notes (in-progress features)
|
||||
|
||||
This directory holds **living design/working notes for unmerged feature branches**,
|
||||
one Markdown file per feature, named after its branch (e.g.
|
||||
`opening-template-on-type.md`).
|
||||
|
||||
## Purpose
|
||||
|
||||
A shared scratchpad so collaborators — and the AI agents they work with — can pick up
|
||||
the context behind an in-progress branch: the problem, the design decisions and the
|
||||
*why*, dead ends already ruled out, and what still needs testing. Because the note is
|
||||
committed on the branch, it travels with the PR and shows up in the diff, so it is
|
||||
discoverable without anyone being told where to look.
|
||||
|
||||
## How to use it (humans and agents)
|
||||
|
||||
- **Before working on a feature branch**, read its note here if one exists.
|
||||
- **As the PR is refined**, keep the note current — append decisions, correct things
|
||||
that changed, update the test checklist.
|
||||
- **One file per feature**, named after the branch.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
These are *not* permanent user documentation. When a PR merges, either remove its note
|
||||
or promote the durable parts (the load-bearing "why") into code comments or the regular
|
||||
docs, so stale notes do not accumulate on the default branch.
|
||||
@@ -0,0 +1,135 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Occurrence representations — normalize occurrence-local reps onto the type
|
||||
|
||||
> **Living dev note** for the `occurrence-representations` branch/PR. Read before working
|
||||
> on the feature; append decisions and findings as the PR is refined. This is *not* user
|
||||
> documentation — at merge it is removed or its durable parts promoted to code comments.
|
||||
> See [README.md](README.md) for the convention.
|
||||
|
||||
Tracking issue: [#8788](https://github.com/IfcOpenShell/IfcOpenShell/issues/8788). Supersedes
|
||||
the closed PR #8789 (see "History / pivot"). Stacked on `dev-notes-system` (#8201) →
|
||||
`opening-template-on-type` (#8200) → `select-by-representation-type` (#7916), because it shares
|
||||
the Representations panel and `RepresentationsData`.
|
||||
|
||||
## Position (maintainer-aligned)
|
||||
|
||||
Per Dion Moult (project lead), **if a type has representations, its occurrences should share
|
||||
them** — an occurrence carrying a representation the type lacks is an *anomaly to normalize up
|
||||
to the type*, not something to preserve. This follows the MVD concept-template intent (mapped
|
||||
representations mirror the type relationship) and the `IfcTypeProduct` text that typed
|
||||
occurrences "have to reference the representation maps", even though EXPRESS has no WHERE rule
|
||||
enforcing it. See the buildingSMART thread Moult started:
|
||||
<https://forums.buildingsmart.org/t/must-mappedrepresentations-come-from-the-corresponding-ifc-type/3361>.
|
||||
|
||||
This branch therefore provides the **normalization path**, and deliberately does *not* try to
|
||||
make occurrence-local reps a first-class, persisted thing.
|
||||
|
||||
## Scope
|
||||
|
||||
**In:**
|
||||
|
||||
1. **Promote to Type** (`bim.promote_representation_to_type`) — lift an occurrence-local rep
|
||||
onto its type as a `RepresentationMap`, so occurrences inherit it. The migration tool for
|
||||
imported/legacy models (Revit et al. emit occurrence-only / partial-from-type reps).
|
||||
2. **Type / Occurrence panel split** — `BIM_PT_representations` groups rows under **Type**
|
||||
(mapped/inherited) vs **Occurrence** (local) headers, so an anomalous occurrence-local rep
|
||||
is *surfaced* instead of silent.
|
||||
|
||||
**Deliberately out (dropped from the earlier draft):**
|
||||
|
||||
- **Copy-time preservation** — `copy_class` is left as-is (occurrence-only reps are not
|
||||
re-added on duplicate). Preserving them perpetuates the anomaly; normalize first, then copy.
|
||||
- **"Add to Occurrence" toggle** — removed. `add_representation` keeps stock behaviour
|
||||
(`geometry.assign_representation` already redirects a new rep onto the type when the type has
|
||||
maps). No force-local override.
|
||||
|
||||
## Design
|
||||
|
||||
### Promote to Type (slot-based, "type wins")
|
||||
|
||||
`bim.promote_representation_to_type` (`EXPORT` icon on Occurrence rows, only when
|
||||
`element_has_type`) → `core.geometry.promote_representation_to_type`. Copies the promoted rep
|
||||
onto the type as a new `RepresentationMap` (`tool.Geometry.add_type_representation_map`), then for
|
||||
**every** occurrence of the type: removes **any** existing rep in the same **slot** and assigns
|
||||
the type's mapped rep in its place. Occurrences with no rep in the slot simply inherit it.
|
||||
|
||||
"Any existing rep" is the load-bearing part: it covers a **local** (non-mapped) rep *and* an
|
||||
already-**mapped** rep the occurrence inherited from another map — e.g. a floating
|
||||
`IfcRepresentationMap` not anchored to the type, which Revit emits (each occurrence maps to its
|
||||
own or a shared floating map, the type's `RepresentationMaps` is empty). Local reps are removed
|
||||
via `core.remove_representation` (Blender-aware); mapped reps via a per-occurrence
|
||||
`geometry.unassign_representation` + `geometry.remove_representation` (its `remove_deep2` keeps a
|
||||
shared map alive until its last user is gone, so floating maps get garbage-collected). If the
|
||||
type already holds a rep in the slot it is removed too, so promoting is idempotent (replaces
|
||||
rather than accumulating maps). The slot key resolves through mapped items
|
||||
(`resolve_mapped_representation`), because an inherited rep's own `RepresentationType` is
|
||||
`"MappedRepresentation"`, not the underlying type.
|
||||
|
||||
The slot key is context (context/subcontext/target view) + `RepresentationIdentifier` +
|
||||
resolved `RepresentationType`. **Geometry is not compared** — the type's representation replaces the
|
||||
occurrence's for that slot even when the occurrence's geometry genuinely differs (e.g. an
|
||||
independently meshed / mirrored / rotated Revit instance), so such occurrences visibly adopt the
|
||||
type's geometry. The mapped rep uses `map_representation`'s identity transform, so a divergent
|
||||
instance takes the type geometry at *its own placement* (baked per-instance mesh orientation is
|
||||
lost — the accepted tradeoff of "type wins").
|
||||
|
||||
Rationale for dropping the earlier geometry comparison: for Revit-style imports each occurrence
|
||||
carries an independently tessellated body (same vertex count but reordered + reoriented; no
|
||||
single affine maps one to another, confirmed via a least-squares fit — `max_err ≈ 1.3 m`), so an
|
||||
"only consolidate byte-identical" rule left most real-world duplicates unconsolidated. Slot-based
|
||||
replace is the deliberate, user-chosen behaviour.
|
||||
|
||||
### Panel split
|
||||
|
||||
`RepresentationsData` (geometry/data.py) exposes `is_mapped`
|
||||
(`resolve_representation(rep) != rep`), `element_is_type`, and `element_has_type`.
|
||||
`draw_representation_row` is shared and carries the stack's `RepresentationIdentifier` column +
|
||||
`select_by_representation_type` button; the Occurrence-group rows additionally show the promote
|
||||
button. A type element shows a flat list.
|
||||
|
||||
## The divergent-occurrence case (decision made)
|
||||
|
||||
Two occurrences of one type that carry **different** geometry in the same slot cannot both live
|
||||
on the type (one mapped rep per slot). The chosen resolution is **"type wins"**: promote
|
||||
replaces every occurrence's local rep in that slot with the type's, discarding divergent
|
||||
per-instance geometry. This favours a single authoritative type geometry over preserving
|
||||
independently-authored instance bodies. (Intrinsic per-instance geometry — voids/joins;
|
||||
`IfcRelVoidsElement` is occurrence-only — lives in a *different* mechanism and is unaffected.)
|
||||
The broader "can occurrences ever legitimately diverge" question is still worth raising with
|
||||
Moult on #8788, but Promote no longer tries to adjudicate it.
|
||||
|
||||
## Status — implemented (verified in live Blender)
|
||||
|
||||
- `tool/geometry.py`: `copy_representation_deep`, `add_type_representation_map`.
|
||||
- `core/geometry.py`: `promote_representation_to_type` (slot-based).
|
||||
- `core/tool.py`: interface decls for the two new `Geometry` methods.
|
||||
- `bim/module/geometry/operator.py`: `PromoteRepresentationToType`.
|
||||
- `bim/module/geometry/{data,ui}.py`: `is_mapped` / `element_is_type` / `element_has_type`;
|
||||
Type/Occurrence grouping merged with the stack's panel columns; old `*` suffix removed.
|
||||
- `bim/module/geometry/__init__.py`: register `PromoteRepresentationToType`.
|
||||
- `core/root.py`, `tool/root.py`, `core/geometry.py::add_representation`: reverted to base
|
||||
(copy-preservation + add-to-occurrence removed).
|
||||
|
||||
## History / pivot
|
||||
|
||||
Originally four pieces incl. a `copy_class` fix that re-added occurrence-only reps on duplicate,
|
||||
and an "Add to Occurrence" toggle. PR #8789 was closed by Moult as "based on the wrong premise
|
||||
— there shouldn't be representations on occurrence and not on type if the type has
|
||||
representations." Re-scoped to the normalization-only subset above; copy-preservation and the
|
||||
toggle removed.
|
||||
|
||||
## Things to test / verify
|
||||
|
||||
- Promote (verified on a Revit sink type, 5 occurrences): every occurrence ends up referencing
|
||||
the type's mapped rep — occurrences with a local body in the slot have it replaced (including
|
||||
independently-meshed/mirrored ones, which visibly adopt the type geometry), and occurrences
|
||||
with none inherit it. Exercises `remove_representation`'s Blender mesh/data-link side effects.
|
||||
- Promoting a second slot (e.g. Body/PLAN_VIEW/Curve3D) adds a second `RepresentationMap` and all
|
||||
occurrences inherit both.
|
||||
- Re-open the saved IFC and confirm the mapped instances render sensibly (the divergent ones will
|
||||
have changed orientation — that's the accepted "type wins" tradeoff, not a bug).
|
||||
- Panel: Type vs Occurrence grouping correct for occurrence, typed occurrence with no local
|
||||
reps (only Type header), typeless element (only Occurrence), and a type element (flat list);
|
||||
columns still align with the stack's header row.
|
||||
- Confirm copy/add behave as stock v0.8.0 (no regression from the removed pieces).
|
||||
@@ -0,0 +1,165 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Opening template on type — preserving custom openings across duplicate_type / append
|
||||
|
||||
> **Living dev note** for the `opening-template-on-type` branch/PR. Read before working
|
||||
> on the feature; append decisions and findings as the PR is refined. This is *not* user
|
||||
> documentation — at merge it is removed or its durable parts promoted to code comments.
|
||||
> See [README.md](README.md) for the convention.
|
||||
|
||||
## Problem
|
||||
|
||||
`bpy.ops.bim.duplicate_type` and `bpy.ops.bim.append_library_element` lose a custom
|
||||
`IfcOpeningElement` body (e.g. an `IfcPolygonalFaceSet`/tessellation) and replace it
|
||||
with a generated extrusion. Root cause: the only mechanism that preserved a custom
|
||||
opening was "copy it from a sibling occurrence of the same type"
|
||||
(`get_existing_opening_occurrence_if_any`), which returns nothing for a brand-new
|
||||
type. `generate_opening_from_filling` then always builds an extrusion (profile or
|
||||
bbox), discarding the custom geometry.
|
||||
|
||||
## Key facts established
|
||||
|
||||
- IFC-level `root.copy_class` already `copy_deep`s opening representations; the loss
|
||||
happens on the Bonsai side (the `regenerate_from_type` listener on
|
||||
`type.assign_type`, and placement-time generation).
|
||||
- Opening occurrences of one type already **share** a single `IfcRepresentationMap`
|
||||
via mapped representations — that is why editing one void edits them all
|
||||
(see `tool.Model.unshare_opening_representation` docstring). Bonsai shares, it does
|
||||
not copy. The shared map just has no durable home (it is hosted implicitly by
|
||||
whichever occurrence exists), so it does not survive to a new type.
|
||||
- IFC4 ADD2 TC1 `IfcShapeRepresentation`: identifier **`Reference`** = "3D
|
||||
representation that is **not part of the Body representation** ... used, e.g., for
|
||||
opening geometries ... excluded from an implicit Boolean operation." Schema-valid;
|
||||
`IfcTypeProduct` has no uniqueness rule on `RepresentationMaps` (only
|
||||
`ApplicableOccurrence`). So a `Reference` map can sit beside the `Body` map.
|
||||
- The geometry kernel selects an opening's geometry **by context, not by
|
||||
`RepresentationIdentifier`** (`mapping::representation_of`, `ifcgeom/mapping/mapping.cpp`).
|
||||
So a `Reference`-identified opening in the Body context still booleans correctly.
|
||||
Nothing in Bonsai reads `"Reference"` to *skip* applying an opening.
|
||||
- Caveat: IFC has no type-level void (`IfcRelVoidsElement` is occurrence-only). The
|
||||
"opening template on type" is therefore a Bonsai convention using a spec-valid
|
||||
identifier; other tools see a harmless extra `Reference` rep they ignore. The
|
||||
regeneration smarts are Bonsai-only by necessity.
|
||||
|
||||
## Design
|
||||
|
||||
Store the shared opening body on the **type** as a `Reference` representation map.
|
||||
Because `bim.duplicate_type` (`tool.Root.copy_representation`) and
|
||||
`append_type_product` both copy a type's `RepresentationMaps`, the template survives
|
||||
both. Occurrence openings map over the same map, so editing a void rewrites the
|
||||
shared map = updates the type template in one stroke (no separate write-back needed).
|
||||
|
||||
`map_type_representations` must skip `Reference` maps so the window/door occurrence
|
||||
does not receive the opening shape as its own Body (the kernel would otherwise pick
|
||||
arbitrarily between the real Body and the opening rep). The skip is both required and
|
||||
spec-endorsed ("not part of the Body representation").
|
||||
|
||||
### Body-context coexistence (Option A)
|
||||
|
||||
The template lives in the **Body** subcontext (required: the instance opening that maps
|
||||
over it must resolve in Body context for the geometry kernel to subtract it). So the
|
||||
type holds two reps in one context: the `Body` window body and the `Reference` opening
|
||||
template. Per IFC, `Reference` is a *RepresentationIdentifier value used within the Body
|
||||
context*, not a separate context - so we keep it there and disambiguate elsewhere:
|
||||
|
||||
- The representations panel now shows `RepresentationIdentifier` as its own column
|
||||
(`geometry/data.py`, `geometry/ui.py`) so the two Body-context reps are
|
||||
distinguishable (`Model | Body | MODEL_VIEW | Reference | Tessellation`). The panel
|
||||
column previously read "Body" because it shows `ContextOfItems.ContextIdentifier`,
|
||||
not the representation's identifier.
|
||||
- `Geometry.reimport_element_representations` type branch now renders the requested
|
||||
`base_representation` instead of `get_representation(element, context)`, which matched
|
||||
only by context and returned the window body when switching to the `Reference` rep.
|
||||
This is what makes "switch to the Reference row" actually show the void on the type.
|
||||
|
||||
### Precedence in `generate_opening_from_filling`
|
||||
type `Reference` template → (existing sibling occurrence, checked by callers) →
|
||||
type `Profile` extrusion → bbox extrusion.
|
||||
|
||||
### Type switching (assign_type)
|
||||
|
||||
On `type.assign_type` the opening is rebuilt to reflect the **assigned** type's void.
|
||||
Two listeners in `model/handler.py`:
|
||||
|
||||
- **pre** `Bonsai.Opening.PreserveOnTypeChange` → `preserve_opening_on_type_change`:
|
||||
before the filling moves to the new type, `promote_opening_to_type(old_type)` anchors
|
||||
the old type's custom void as a template, so it isn't lost when (possibly the last)
|
||||
occurrence is regenerated. Idempotent; custom voids only.
|
||||
- **post** `Bonsai.Opening.RegenerateFromType` → `regenerate_from_type` →
|
||||
`_regenerate_from_type`: rebuilds from the new type's template / sibling / extrusion.
|
||||
The old PR1 "preserve custom" guard was **removed** here — it kept the previous type's
|
||||
void on a switch (wrong), and the template now makes preservation unnecessary.
|
||||
|
||||
NOTE: upstream `v0.8.0` landed `assign_type` changes + new `test_assign_type_*` tests
|
||||
(merged under this branch's base). The listeners ride on top of that — re-test the
|
||||
switch/edit round-trips against the new `assign_type`.
|
||||
|
||||
### Write-back on void edit
|
||||
|
||||
Editing an occurrence's void writes the new geometry back to the type's `Reference`
|
||||
template via `update_type_template_from_opening` (creates the template if absent), then
|
||||
**re-maps every occurrence's opening onto the template** and reloads the affected host walls
|
||||
(`switch_representation`) so they re-boolean. The re-map (`_remap_opening_to_template`) is the
|
||||
key part: an earlier version only re-pointed a *pre-existing* shared map, so siblings whose
|
||||
openings were **independent** (their own `IfcRepresentationMap`, never sharing the template)
|
||||
didn't follow — the common real-world case. Now they do. Hooked at both commit paths:
|
||||
`UpdateRepresentation._execute` (the `edited_objs` path) and
|
||||
`OverrideModeSetObject` after `edit_representation_item` (the in-place item edit). The
|
||||
older `edit_openings`/`is_edited` path also calls it. `set_type_opening_representation`
|
||||
has replace semantics (one `Reference` map per type).
|
||||
|
||||
### Preserving adjusted extrusions (duplicate_type)
|
||||
|
||||
`is_opening_representation_custom` only flags *non-extrusion* geometry (tessellation, brep,
|
||||
CSG) as worth preserving — a proxy for "not regenerable". That mis-classifies a *manually
|
||||
adjusted* extrusion, which is still an `IfcExtrudedAreaSolid`, so a hand-tweaked extrusion
|
||||
opening was reset to the default on `duplicate_type`.
|
||||
|
||||
`promote_opening_to_type` now gates on `should_preserve_opening` = custom **or**
|
||||
`_is_adjusted_extrusion`. The latter generates the default (`generate_opening_from_filling`,
|
||||
which yields the default since no template exists at promote time) *transiently*, compares the
|
||||
two bodies' axis-aligned bounding boxes (1 mm tolerance) via the geom engine, then removes the
|
||||
temporary default. Divergence ⇒ the extrusion was adjusted ⇒ promote it; a plain default
|
||||
matches ⇒ left regenerable (not frozen — see the "freeze" discussion). Scoped to the duplicate
|
||||
path so the generate-and-compare stays out of the hot predicate. Limitation: bbox comparison
|
||||
misses a shape change that preserves the bbox (upgrade to a vertex-set compare if needed).
|
||||
|
||||
## Status — implemented (manually verified in Blender)
|
||||
|
||||
- core `map_type_representations.py`: skip `Reference` maps.
|
||||
- `model/opening.py`: `get_/set_type_opening_representation`, `promote_opening_to_type`,
|
||||
`update_type_template_from_opening` (+ `_remap_opening_to_template`),
|
||||
`preserve_opening_on_type_change`, `should_preserve_opening` (+ `_is_adjusted_extrusion`,
|
||||
`_representation_bbox`); `generate_opening_from_filling` consults the template; PR1 guard
|
||||
removed from `_regenerate_from_type`.
|
||||
- `model/handler.py`: pre + post assign_type listeners.
|
||||
- `type/operator.py` `DuplicateType`: promote before copy.
|
||||
- `project/operator.py` `AppendLibraryElement`: `harvest_opening_template`.
|
||||
- `geometry/operator.py`: write-back hooks in `UpdateRepresentation` and
|
||||
`OverrideModeSetObject`; `reimport_element_representations` renders the requested rep.
|
||||
- `geometry/data.py` + `geometry/ui.py`: `RepresentationIdentifier` column + headers.
|
||||
|
||||
Branch `opening-template-on-type` (#8200): initial feature commit + the #7916 build-conflict
|
||||
ancestry-merge + void-propagation-to-all-occurrences + adjusted-extrusion preservation. The
|
||||
`docs/dev-notes/` convention itself lives on the stacked branch `dev-notes-system` (#8201).
|
||||
|
||||
Still **deferred:** explicit "Apply/Reset to type" operators + a "diverges from type"
|
||||
indicator; import never auto-writes back. `update_simple_openings` still keeps its
|
||||
`is_opening_representation_custom` guard (array propagation, same type — left as-is).
|
||||
|
||||
## Things to test / verify
|
||||
|
||||
- Duplicated/appended type's new occurrence gets the faceset void and it **cuts** the
|
||||
wall (kernel selects opening geom by context, so a `Reference`-id rep still booleans).
|
||||
- `harvest_opening_template` cross-file `file.add`: no duplicate
|
||||
`IfcGeometricRepresentationContext` left behind; units (kernel doesn't rescale rep
|
||||
coords — same assumption as `append_asset`).
|
||||
- Switch X→Y→X round-trip restores each type's void; switching to a plain (template-less)
|
||||
type gives its default extrusion, not the previous faceset.
|
||||
- Edit a void → type's `Reference` row updates; **all** occurrences follow (including ones
|
||||
that had independent openings) and their host walls re-boolean; survives duplicate.
|
||||
- `duplicate_type` on a type whose extrusion opening was **manually adjusted** → Type B keeps
|
||||
the adjusted extrusion; a type with a plain/default extrusion stays regenerable (not frozen).
|
||||
- Three write-back hooks are intentional (different commit paths) — candidate for
|
||||
consolidation in review.
|
||||
- Re-test against upstream's new `assign_type` (see NOTE under "Type switching").
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -33,5 +33,7 @@ DATA;
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -20,7 +20,6 @@ import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell.util.element
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
@@ -28,12 +27,6 @@ from mathutils import Vector
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
# Initialize the bounding box coordinates
|
||||
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
|
||||
@@ -79,26 +72,8 @@ def create_bounding_box(objs):
|
||||
return indices, edges
|
||||
|
||||
|
||||
class AggregateDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
class AggregateDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_aggregate"
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
@@ -153,14 +128,6 @@ class AggregateDecorator:
|
||||
shader.uniform_float("u_Scale", 25)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate(self, context):
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
@@ -191,12 +158,13 @@ class AggregateDecorator:
|
||||
aggregates.append(obj)
|
||||
continue
|
||||
|
||||
aggregate = None
|
||||
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
|
||||
if props.in_aggregate_mode and props.editing_aggregate:
|
||||
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
|
||||
if index > 0:
|
||||
aggregate = aggregates_list[index - 1]
|
||||
else:
|
||||
elif aggregates_list:
|
||||
aggregate = aggregates_list[-1]
|
||||
if aggregate:
|
||||
aggregates.append(tool.Ifc.get_object(aggregate))
|
||||
@@ -225,39 +193,11 @@ class AggregateDecorator:
|
||||
self.draw_custom_batch(line, decorator_color_unselected)
|
||||
|
||||
|
||||
class AggregateModeDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class AggregateModeDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_aggregate_name", "POST_PIXEL"),
|
||||
("draw_aggregate_empty", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_aggregate_name(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
|
||||
@@ -56,11 +56,6 @@ class BoundaryDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
@@ -109,7 +104,11 @@ class BoundaryDecorator:
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
self.draw_batch(
|
||||
"TRIS", unselected_vertices, tool.Blender.transparent_color(special_elements_color), unselected_tris
|
||||
)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
self.draw_batch(
|
||||
"TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), selected_tris
|
||||
)
|
||||
|
||||
@@ -18,43 +18,17 @@
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class ClashDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class ClashDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw_geometry", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_text(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
@@ -52,8 +52,27 @@ def _on_depsgraph_update(scene, depsgraph):
|
||||
tool.ClipBox.on_depsgraph_update_caps(scene, depsgraph)
|
||||
|
||||
|
||||
@persistent
|
||||
def _on_load_pre(filepath):
|
||||
# Tear down any in-flight clip-box timers before Blender frees the
|
||||
# WM / screens / areas / regions for the loading file. A refresh timer
|
||||
# that survives the teardown fires against the new file's freshly-
|
||||
# allocated regions before their GPU state is wired, CTD-ing inside
|
||||
# GPU_matrix_ortho_set. The gate also blocks the depsgraph IFC-reload
|
||||
# branch and is held closed until on_pre_view fires for the first time
|
||||
# on the new file (first paint = GPU contexts wired).
|
||||
tool.ClipBox._file_loading = True
|
||||
tool.ClipBox._post_load_paint_pending = True
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
|
||||
|
||||
@persistent
|
||||
def _on_load_post(filepath):
|
||||
# The _file_loading gate is NOT cleared here: load_post fires before
|
||||
# the new file's first paint, so GPU contexts may still be uninitialised.
|
||||
# on_pre_view consumes _post_load_paint_pending to open the gate at the
|
||||
# safe moment and kick the post-load re-arm.
|
||||
# Restore the per-scene clip-box list from the project's BBIM_ClipBoxes
|
||||
# pset. Runs after the standard load_post that creates Blender objects.
|
||||
tool.ClipBox._last_seen_object_matrices.clear()
|
||||
@@ -71,6 +90,8 @@ def register():
|
||||
tool.ClipBox.reset_ownership()
|
||||
if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
|
||||
if _on_load_pre not in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.append(_on_load_pre)
|
||||
if _on_load_post not in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.append(_on_load_post)
|
||||
if _draw_handler_pre is None:
|
||||
@@ -97,8 +118,11 @@ def unregister():
|
||||
_draw_handler_pre = None
|
||||
if _on_load_post in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.remove(_on_load_post)
|
||||
if _on_load_pre in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.remove(_on_load_pre)
|
||||
if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
tool.ClipBox._last_seen_object_matrices.clear()
|
||||
tool.ClipBox.clear_clip_planes()
|
||||
|
||||
@@ -50,6 +50,9 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
if camera.type != props.camera_type:
|
||||
camera.type = props.camera_type
|
||||
|
||||
if props.update_props and (drawing := tool.Ifc.get_entity(camera_obj)):
|
||||
tool.Drawing.sync_perspective_camera_shifts(drawing, camera)
|
||||
|
||||
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
|
||||
scene_render = scene.render
|
||||
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
|
||||
|
||||
@@ -57,7 +57,7 @@ import shapely
|
||||
from bpy_extras.image_utils import load_image
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from lxml import etree
|
||||
from mathutils import Color, Vector
|
||||
from mathutils import Color, Matrix, Vector
|
||||
|
||||
import bonsai.bim.export_ifc
|
||||
import bonsai.bim.handler
|
||||
@@ -602,6 +602,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
context_type: Literal["body", "annotation"],
|
||||
drawing_elements: set[ifcopenshell.entity_instance],
|
||||
target_view: str,
|
||||
link_matrix: Optional[Matrix] = None,
|
||||
) -> None:
|
||||
drawing_elements = drawing_elements.copy()
|
||||
contexts_: list[list[int]] = getattr(contexts, context_type)
|
||||
@@ -613,9 +614,19 @@ class CreateDrawing(bpy.types.Operator):
|
||||
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
||||
|
||||
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
|
||||
is_plan = ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view
|
||||
z_offset = (0.002 if target_view == "PLAN_VIEW" else -0.002) if is_plan else 0.0
|
||||
|
||||
if link_matrix is not None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
|
||||
t = link_matrix.to_translation()
|
||||
offset = (t.x / unit_scale, t.y / unit_scale, t.z / unit_scale + z_offset)
|
||||
geom_settings.set("model-offset", offset)
|
||||
q = link_matrix.to_quaternion()
|
||||
geom_settings.set("model-rotation", (q.x, q.y, q.z, q.w))
|
||||
elif z_offset:
|
||||
# A 2mm Z offset to combat Z-fighting in plan or RCPs
|
||||
geom_settings.set("model-offset", (0.0, 0.0, 0.002 if target_view == "PLAN_VIEW" else -0.002))
|
||||
geom_settings.set("model-offset", (0.0, 0.0, z_offset))
|
||||
|
||||
geom_settings.set("context-ids", context)
|
||||
it = ifcopenshell.geom.iterator(
|
||||
@@ -923,11 +934,16 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
files = {bim_props.ifc_file: tool.Ifc.get()}
|
||||
# Map ifc_path → (ifc_file, link_matrix); main file has no link_matrix (None)
|
||||
files: dict[str, tuple[ifcopenshell.file, Optional[Matrix]]] = {bim_props.ifc_file: (tool.Ifc.get(), None)}
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.get_loaded_links_for_drawings():
|
||||
files[link.filepath] = self.get_linked_file(link)
|
||||
try:
|
||||
link_matrix = tool.Project.calculate_link_matrix(link)
|
||||
except Exception:
|
||||
link_matrix = None
|
||||
files[link.filepath] = (self.get_linked_file(link), link_matrix)
|
||||
|
||||
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
||||
self.setup_serialiser(target_view)
|
||||
@@ -935,7 +951,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
tree = ifcopenshell.geom.tree()
|
||||
tree.enable_face_styles(True)
|
||||
|
||||
for ifc_path, ifc in files.items():
|
||||
for ifc_path, (ifc, link_matrix) in files.items():
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
||||
@@ -949,8 +965,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
|
||||
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
|
||||
contexts = self.get_linework_contexts(ifc, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view, link_matrix)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix)
|
||||
|
||||
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
|
||||
with profile("Camera element"):
|
||||
@@ -2319,7 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
bl_description = (
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
|
||||
)
|
||||
|
||||
drawing: bpy.props.IntProperty()
|
||||
@@ -2335,13 +2352,23 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
load_selected_annotations: bpy.props.BoolProperty(
|
||||
name="Load Selected Annotations",
|
||||
description="Load the annotations of all selected drawings without switching the active view.",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
drawing: int
|
||||
should_view_from_camera: bool
|
||||
use_quick_preview: bool
|
||||
load_selected_annotations: bool
|
||||
|
||||
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
if event.type == "LEFTMOUSE" and event.shift and event.ctrl:
|
||||
self.load_selected_annotations = True
|
||||
return self.execute(context)
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
self.should_view_from_camera = False
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
@@ -2354,6 +2381,18 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
if props.is_editing_drawings == False:
|
||||
bpy.ops.bim.load_drawings()
|
||||
|
||||
if self.load_selected_annotations:
|
||||
for d in props.drawings:
|
||||
if not (d.is_drawing and d.is_selected):
|
||||
continue
|
||||
selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id)
|
||||
# Importing the camera (if missing) ensures the drawing's
|
||||
# collection exists so the annotations get collected into it.
|
||||
if not tool.Ifc.get_object(selected_drawing):
|
||||
tool.Drawing.import_drawing(selected_drawing)
|
||||
tool.Drawing.import_annotations_in_group(tool.Drawing.get_drawing_group(selected_drawing))
|
||||
return {"FINISHED"}
|
||||
|
||||
drawing = tool.Ifc.get().by_id(self.drawing)
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
|
||||
@@ -2439,7 +2478,8 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
|
||||
bl_description = (
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -604,6 +604,7 @@ class BIMCameraProperties(PropertyGroup):
|
||||
return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index)
|
||||
|
||||
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
|
||||
# Perspective camera shift is stored in EPset_Drawing and intentionally excluded here.
|
||||
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
|
||||
def update_representation(self, matrix_world: Matrix) -> bool:
|
||||
"""Update ``representation`` based on current camera properties and the provided world matrix.
|
||||
|
||||
@@ -99,6 +99,10 @@ class BIM_PT_camera(Panel):
|
||||
if props.target_view == "MODEL_VIEW":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "camera_type")
|
||||
if props.camera_type == "PERSP":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(camera_data, "shift_x", text="Camera Shift X/Y:")
|
||||
row.prop(camera_data, "shift_y", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "linework_mode")
|
||||
|
||||
@@ -63,12 +63,14 @@ classes = (
|
||||
operator.OverrideOriginSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
operator.PromoteRepresentationToType,
|
||||
operator.PurgeUnusedRepresentations,
|
||||
operator.RefreshLinkedAggregate,
|
||||
operator.RemoveConnection,
|
||||
operator.RemoveRepresentation,
|
||||
operator.RemoveRepresentationItem,
|
||||
operator.RemoveRepresentationItemFromShapeAspect,
|
||||
operator.SelectByRepresentationType,
|
||||
operator.SelectConnection,
|
||||
operator.SelectRepresentationItem,
|
||||
operator.SwitchRepresentation,
|
||||
|
||||
@@ -102,12 +102,32 @@ class RepresentationsData:
|
||||
def load(cls):
|
||||
cls.data = {"representations": cls.representations()}
|
||||
cls.data["contexts"] = cls.contexts()
|
||||
cls.data["element_is_type"] = cls.element_is_type()
|
||||
cls.data["element_has_type"] = cls.element_has_type()
|
||||
|
||||
# Only after cls.representations().
|
||||
cls.data["shape_aspects"] = cls.shape_aspects()
|
||||
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def element_is_type(cls) -> bool:
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return bool(element and element.is_a("IfcTypeProduct"))
|
||||
|
||||
@classmethod
|
||||
def element_has_type(cls) -> bool:
|
||||
"""True when the active element is an occurrence with a type — i.e. a
|
||||
local representation could be promoted onto that type."""
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or element.is_a("IfcTypeProduct"):
|
||||
return False
|
||||
return bool(ifcopenshell.util.element.get_type(element))
|
||||
|
||||
@classmethod
|
||||
def representations(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -122,11 +142,8 @@ class RepresentationsData:
|
||||
active_representation_id = active_representation.id()
|
||||
|
||||
for representation in tool.Geometry.get_representations_iter(element):
|
||||
representation_type = representation.RepresentationType
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if resolved_representation != representation:
|
||||
representation_type = resolved_representation.RepresentationType + "*"
|
||||
representation_type = resolved_representation.RepresentationType
|
||||
|
||||
is_active = (
|
||||
representation.id() == active_representation_id
|
||||
@@ -138,8 +155,17 @@ class RepresentationsData:
|
||||
"ContextType": representation.ContextOfItems.ContextType or "",
|
||||
"ContextIdentifier": "",
|
||||
"TargetView": "",
|
||||
# The representation's own identifier (e.g. 'Body', 'Reference'), which is
|
||||
# distinct from the subcontext's ContextIdentifier above. Two reps can share
|
||||
# one context (e.g. a Body body and a Reference opening template), so showing
|
||||
# this lets them be told apart in the panel.
|
||||
"RepresentationIdentifier": representation.RepresentationIdentifier or "",
|
||||
"RepresentationType": representation_type or "",
|
||||
"is_active": is_active,
|
||||
# True when this representation is an IfcMappedItem resolving to
|
||||
# the type's MappedRepresentation, i.e. inherited from the type
|
||||
# rather than local to the occurrence.
|
||||
"is_mapped": resolved_representation != representation,
|
||||
}
|
||||
if representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"):
|
||||
data["ContextIdentifier"] = representation.ContextOfItems.ContextIdentifier or ""
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
@@ -25,15 +23,16 @@ import ifcopenshell
|
||||
import numpy as np
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class ItemDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
class ItemDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw", "POST_VIEW"),
|
||||
)
|
||||
objs: dict[str, dict[str, list]]
|
||||
obj_is_selected: dict[str, bool]
|
||||
obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]]
|
||||
@@ -119,23 +118,6 @@ class ItemDecorator:
|
||||
"special_edges": special_edges,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
@@ -163,11 +145,6 @@ class ItemDecorator:
|
||||
blf.disable(font_id, blf.SHADOW)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
def transparent_color(color: Sequence[float], alpha: float = 0.05) -> list[float]:
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
@@ -197,15 +174,33 @@ class ItemDecorator:
|
||||
if context.mode != "OBJECT":
|
||||
continue
|
||||
self.draw_batch("LINES", data["verts"], selected_elements_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(selected_elements_color), data["tris"])
|
||||
self.draw_batch(
|
||||
"TRIS",
|
||||
data["verts"],
|
||||
tool.Blender.transparent_color(selected_elements_color, alpha=0.05),
|
||||
data["tris"],
|
||||
)
|
||||
self.draw_batch("LINES", data["special_verts"], selected_elements_color, data["special_edges"])
|
||||
elif self.obj_is_boolean[obj_name]:
|
||||
self.draw_batch("LINES", data["verts"], special_elements_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
|
||||
self.draw_batch(
|
||||
"TRIS",
|
||||
data["verts"],
|
||||
tool.Blender.transparent_color(special_elements_color, alpha=0.05),
|
||||
data["tris"],
|
||||
)
|
||||
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
|
||||
else:
|
||||
self.draw_batch(
|
||||
"LINES", data["verts"], transparent_color(unselected_elements_color, alpha=0.2), data["edges"]
|
||||
"LINES",
|
||||
data["verts"],
|
||||
tool.Blender.transparent_color(unselected_elements_color, alpha=0.2),
|
||||
data["edges"],
|
||||
)
|
||||
self.draw_batch(
|
||||
"TRIS",
|
||||
data["verts"],
|
||||
tool.Blender.transparent_color(special_elements_color, alpha=0.05),
|
||||
data["tris"],
|
||||
)
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
|
||||
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
|
||||
|
||||
@@ -418,6 +418,55 @@ class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
|
||||
|
||||
|
||||
class SelectByRepresentationType(bpy.types.Operator):
|
||||
bl_idname = "bim.select_by_representation_type"
|
||||
bl_label = "Select By Representation Type"
|
||||
bl_description = (
|
||||
"Select objects whose active representation matches this type. "
|
||||
"Ctrl+Click to also include objects that have this type in any representation (active or not)"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_type: bpy.props.StringProperty()
|
||||
select_inactive: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.select_inactive = event.ctrl
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc:
|
||||
return {"CANCELLED"}
|
||||
# Strip the "*" suffix used for mapped/resolved representations.
|
||||
target_type = self.representation_type.rstrip("*")
|
||||
matched = 0
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
if self.select_inactive:
|
||||
# Ctrl: match any representation on the element, active or not.
|
||||
has_type = any(
|
||||
(ifcopenshell.util.representation.resolve_representation(rep).RepresentationType or "") == target_type
|
||||
for rep in ifcopenshell.util.representation.get_representations_iter(element)
|
||||
)
|
||||
else:
|
||||
# Default: match only the currently active (displayed) representation.
|
||||
active_rep = tool.Geometry.get_active_representation(obj)
|
||||
if active_rep is None:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
resolved = ifcopenshell.util.representation.resolve_representation(active_rep)
|
||||
has_type = (resolved.RepresentationType or "") == target_type
|
||||
obj.select_set(has_type)
|
||||
if has_type:
|
||||
matched += 1
|
||||
mode = "any representation" if self.select_inactive else "active representation"
|
||||
self.report({"INFO"}, f"Selected {matched} object(s) with RepresentationType '{target_type}' ({mode})")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_connection"
|
||||
bl_label = "Remove Connection"
|
||||
@@ -496,6 +545,36 @@ class RemoveRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"INFO"}, f"{self.bl_label} was finished in {operator_time:.2f} seconds.")
|
||||
|
||||
|
||||
class PromoteRepresentationToType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.promote_representation_to_type"
|
||||
bl_label = "Promote Representation to Type"
|
||||
bl_description = (
|
||||
"Move this occurrence-local representation onto its type so occurrences can inherit it.\n"
|
||||
"Occurrences with an identical local representation inherit from the type;\n"
|
||||
"occurrences with a divergent local representation keep their own override"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_id: bpy.props.IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
representation_id: int
|
||||
|
||||
def _execute(self, context):
|
||||
assert context.active_object
|
||||
counts = core.promote_representation_to_type(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=context.active_object,
|
||||
representation=tool.Ifc.get().by_id(self.representation_id),
|
||||
)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"Representation promoted to type. "
|
||||
f"{counts['occurrences']} occurrence(s) now inherit it "
|
||||
f"({counts['replaced']} local representation(s) replaced).",
|
||||
)
|
||||
|
||||
|
||||
class PurgeUnusedRepresentations(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_representations"
|
||||
bl_label = "Purge Unused Representations"
|
||||
@@ -710,6 +789,16 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if mprops.ifc_parameters:
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
|
||||
# Persist an edited opening void onto its filling type's 'Reference' template so the
|
||||
# change survives type duplication/append/switching and propagates to siblings. This
|
||||
# catches the edited_objs commit path; the in-place item edit is caught in
|
||||
# bim.override_mode_set_object.
|
||||
edited_element = tool.Ifc.get_entity(obj)
|
||||
if edited_element and edited_element.is_a("IfcOpeningElement"):
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
|
||||
|
||||
|
||||
class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
bl_idname = "bim.update_parametric_representation"
|
||||
@@ -2489,6 +2578,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return bpy.ops.bim.edit_boundary_geometry()
|
||||
elif tool.Geometry.is_representation_item(context.active_object):
|
||||
self.edit_representation_item(context.active_object)
|
||||
# If we just edited an opening's void item, persist the new shape onto the
|
||||
# filling type's 'Reference' template so it survives type duplication/append/
|
||||
# switching and propagates to siblings.
|
||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
||||
edited_element = tool.Ifc.get_entity(rep_obj) if rep_obj else None
|
||||
if edited_element and edited_element.is_a("IfcOpeningElement"):
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
|
||||
tool.Root.reload_item_decorator()
|
||||
# So you can keep hitting tab to cycle out of edit mode
|
||||
context.active_object.select_set(False)
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import Menu, Panel, UIList
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
@@ -148,12 +148,33 @@ class BIM_PT_representations(Panel):
|
||||
self.layout.label(text="No Representations Found")
|
||||
return
|
||||
|
||||
for representation in RepresentationsData.data["representations"]:
|
||||
header = self.layout.row(align=True)
|
||||
header.label(text="Context")
|
||||
header.label(text="Subcontext")
|
||||
header.label(text="View")
|
||||
header.label(text="Identifier")
|
||||
header.label(text="Type")
|
||||
# Blank icon cells reserve the same width as the switch/remove buttons below so the
|
||||
# text columns line up with the data rows.
|
||||
header.label(text="", icon="BLANK1")
|
||||
header.label(text="", icon="BLANK1")
|
||||
|
||||
def draw_representation_row(representation, allow_promote=False):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=representation["ContextType"])
|
||||
row.label(text=representation["ContextIdentifier"])
|
||||
row.label(text=representation["TargetView"])
|
||||
row.label(text=representation["RepresentationType"])
|
||||
row.label(text=representation["RepresentationIdentifier"])
|
||||
op = row.operator(
|
||||
"bim.select_by_representation_type",
|
||||
text=representation["RepresentationType"],
|
||||
emboss=False,
|
||||
)
|
||||
op.representation_type = representation["RepresentationType"]
|
||||
if allow_promote:
|
||||
row.operator(
|
||||
"bim.promote_representation_to_type", icon="EXPORT", text=""
|
||||
).representation_id = representation["id"]
|
||||
op = row.operator(
|
||||
"bim.switch_representation",
|
||||
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
|
||||
@@ -163,6 +184,28 @@ class BIM_PT_representations(Panel):
|
||||
op.disable_opening_subtractions = False
|
||||
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
|
||||
|
||||
representations = RepresentationsData.data["representations"]
|
||||
# For a type element every representation is its own; for an occurrence
|
||||
# split them into those inherited from the type (mapped) vs those local
|
||||
# to the occurrence, so it's clear which are driven by the type.
|
||||
if RepresentationsData.data["element_is_type"]:
|
||||
for representation in representations:
|
||||
draw_representation_row(representation)
|
||||
else:
|
||||
type_representations = [r for r in representations if r["is_mapped"]]
|
||||
occurrence_representations = [r for r in representations if not r["is_mapped"]]
|
||||
# A local representation can be promoted onto the type only when the
|
||||
# occurrence actually has a type to promote it onto.
|
||||
allow_promote = RepresentationsData.data["element_has_type"]
|
||||
if type_representations:
|
||||
self.layout.label(text="Type", icon="LINKED")
|
||||
for representation in type_representations:
|
||||
draw_representation_row(representation)
|
||||
if occurrence_representations:
|
||||
self.layout.label(text="Occurrence", icon="OBJECT_DATA")
|
||||
for representation in occurrence_representations:
|
||||
draw_representation_row(representation, allow_promote=allow_promote)
|
||||
|
||||
# Presentation layers.
|
||||
self.layout.separator()
|
||||
if not LayersData.is_loaded:
|
||||
|
||||
@@ -21,7 +21,6 @@ from math import radians
|
||||
import blf
|
||||
import gpu
|
||||
import ifcopenshell.util.geolocation
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Matrix, Vector
|
||||
@@ -30,27 +29,11 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.module.georeference.data import GeoreferenceData
|
||||
|
||||
|
||||
class GeoreferenceDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
class GeoreferenceDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw_geometry", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None, should_scale=True):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
@@ -197,6 +180,10 @@ class GeoreferenceDecorator:
|
||||
decorator_color_error = self.addon_prefs.decorator_color_error
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
# The georef gizmo is a coordinate-system overlay: it must communicate
|
||||
# orientation regardless of model contents, so depth testing is bypassed.
|
||||
original_depth_test = gpu.state.depth_test_get()
|
||||
gpu.state.depth_test_set("ALWAYS")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
@@ -335,6 +322,8 @@ class GeoreferenceDecorator:
|
||||
self.draw_batch("LINES", verts, decorator_color_special, edges)
|
||||
self.draw_dashed_line(location * 3, location * 6, decorator_color_error)
|
||||
|
||||
gpu.state.depth_test_set(original_depth_test)
|
||||
|
||||
def draw_dashed_line(self, start, end, colour, should_scale=True):
|
||||
direction = (end - start).normalized()
|
||||
distance = (end - start).length
|
||||
|
||||
@@ -20,44 +20,18 @@
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
|
||||
|
||||
class SolarDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class SolarDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw_geometry", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_text(self, context: bpy.types.Context) -> None:
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
@@ -421,22 +421,26 @@ class RegenerateArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array")
|
||||
arrays = json.loads(pset["Data"])
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
for array in arrays:
|
||||
for child in set(array["children"]):
|
||||
try:
|
||||
child_element = tool.Ifc.get().by_guid(child)
|
||||
except RuntimeError:
|
||||
continue
|
||||
if child_obj := tool.Ifc.get_object(child_element):
|
||||
tool.Geometry.delete_ifc_object(child_obj)
|
||||
array["children"].clear()
|
||||
# Always operate on the parent — this operator can be invoked with
|
||||
# either the parent OR any array child as active_object (the per-child
|
||||
# gizmo group fires it from a child selection). Using ``obj`` /
|
||||
# ``element`` directly would feed a child to ``regenerate_array`` and
|
||||
# constrain children against a sibling, silently corrupting the array.
|
||||
tool.Model.regenerate_array(parent, arrays)
|
||||
tool.Array.constrain_children_to_parent(parent_element)
|
||||
# Coalesce host recuts: the child-delete loop, the regenerate, and the
|
||||
# per-child opening mirror all touch the same host body. Without batching,
|
||||
# an N-child wipe-then-regen costs N+1 recuts; this collapses to one.
|
||||
with tool.Geometry.batch_host_recut():
|
||||
for array in arrays:
|
||||
for child in set(array["children"]):
|
||||
try:
|
||||
child_element = tool.Ifc.get().by_guid(child)
|
||||
except RuntimeError:
|
||||
continue
|
||||
if child_obj := tool.Ifc.get_object(child_element):
|
||||
tool.Geometry.delete_ifc_object(child_obj)
|
||||
array["children"].clear()
|
||||
# Always operate on the parent — this operator can be invoked with
|
||||
# either the parent OR any array child as active_object (the per-child
|
||||
# gizmo group fires it from a child selection). Using ``obj`` /
|
||||
# ``element`` directly would feed a child to ``regenerate_array`` and
|
||||
# constrain children against a sibling, silently corrupting the array.
|
||||
tool.Model.regenerate_array(parent, arrays)
|
||||
tool.Array.constrain_children_to_parent(parent_element)
|
||||
|
||||
|
||||
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -471,23 +475,24 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.keep_objs:
|
||||
tool.Array.bake_children_transform(element, self.item)
|
||||
tool.Array.set_children_lock_state(element, self.item, False)
|
||||
with tool.Geometry.batch_host_recut():
|
||||
if self.keep_objs:
|
||||
tool.Array.bake_children_transform(element, self.item)
|
||||
tool.Array.set_children_lock_state(element, self.item, False)
|
||||
|
||||
if not self.keep_objs:
|
||||
data[self.item]["count"] = 1
|
||||
tool.Array.remove_constraints(parent_element)
|
||||
tool.Model.regenerate_array(parent, data, array_layers_to_apply=[self.item] if self.keep_objs else [])
|
||||
if not self.keep_objs:
|
||||
data[self.item]["count"] = 1
|
||||
tool.Array.remove_constraints(parent_element)
|
||||
tool.Model.regenerate_array(parent, data, array_layers_to_apply=[self.item] if self.keep_objs else [])
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Array")
|
||||
if len(data) == 1:
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
else:
|
||||
del data[self.item]
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
tool.Array.constrain_children_to_parent(element)
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Array")
|
||||
if len(data) == 1:
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
else:
|
||||
del data[self.item]
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
tool.Array.constrain_children_to_parent(element)
|
||||
|
||||
|
||||
class SelectArrayParent(bpy.types.Operator):
|
||||
|
||||
@@ -53,12 +53,6 @@ from bonsai.bim.module.drawing.gizmos import (
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def highlight_color(color, alpha=0.1):
|
||||
color = [i + (1 - i) * 0.5 for i in color]
|
||||
return color
|
||||
@@ -133,7 +127,7 @@ class ProfileDecorator:
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
faces_color = tool.Blender.transparent_color(self.addon_prefs.decorator_color_special)
|
||||
tool.Blender.draw_bmesh_face_tris(bm, vertices_coords, faces_color, self.draw_batch)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
@@ -263,7 +257,7 @@ class ProfileDecorator:
|
||||
self.draw_batch("LINES", all_vertices, unselected_elements_color, unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", unselected_vertices, tool.Blender.transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, error_elements_color)
|
||||
self.draw_batch("POINTS", special_vertices, special_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
@@ -354,9 +348,11 @@ class ProfileDecorator:
|
||||
return points, listEdg
|
||||
|
||||
|
||||
class PolylineDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
class PolylineDecorator(tool.Blender.ViewportDecorator):
|
||||
# draw_methods declares only the always-bound handler so the base's
|
||||
# __init_subclass__ validation passes; the override install below
|
||||
# conditionally registers up to four more handlers based on ui_only.
|
||||
draw_methods = (("draw_input_ui", "POST_PIXEL"),)
|
||||
event = None
|
||||
input_type = None
|
||||
input_ui = None
|
||||
@@ -392,15 +388,6 @@ class PolylineDecorator:
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
@classmethod
|
||||
def update(
|
||||
cls,
|
||||
@@ -459,14 +446,6 @@ class PolylineDecorator:
|
||||
|
||||
return {"verts": verts, "edges": edges, "tris": tris}
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def shader_config(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.decorator_color = self.addon_prefs.decorations_colour
|
||||
@@ -734,7 +713,9 @@ class PolylineDecorator:
|
||||
if self.polyline_data.measurement_type == "POLY_AREA" and area:
|
||||
if float(area) > 0:
|
||||
tris = self.calculate_polygon(polyline_verts)["tris"]
|
||||
self.draw_batch("TRIS", polyline_verts, transparent_color(self.decorator_color_special), tris)
|
||||
self.draw_batch(
|
||||
"TRIS", polyline_verts, tool.Blender.transparent_color(self.decorator_color_special), tris
|
||||
)
|
||||
|
||||
# Draw polyline with selected points
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
@@ -987,9 +968,8 @@ class PolylineDecorator:
|
||||
self.draw_batch("LINES", polyline_verts, decorator_color_unselected, polyline_edges)
|
||||
|
||||
|
||||
class ProductDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
class ProductDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_product_preview"
|
||||
preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"]
|
||||
relating_type = None
|
||||
obj_data: dict[str, list] = {}
|
||||
@@ -1032,29 +1012,7 @@ class ProductDecorator:
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_product_preview(self, context):
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
@@ -1086,7 +1044,7 @@ class ProductDecorator:
|
||||
data = self.get_generic_preview_data()
|
||||
if data:
|
||||
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"])
|
||||
self.draw_batch("TRIS", data["verts"], tool.Blender.transparent_color(decorator_color), data["tris"])
|
||||
|
||||
def get_wall_preview_data(self):
|
||||
relating_type = self.relating_type
|
||||
@@ -1619,34 +1577,8 @@ class ProductDecorator:
|
||||
return data
|
||||
|
||||
|
||||
class WallAxisDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_wall_axis, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class WallAxisDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_wall_axis"
|
||||
|
||||
def draw_wall_axis(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
@@ -1665,7 +1597,7 @@ class WallAxisDecorator:
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element.is_a("IfcWall"):
|
||||
if element and element.is_a("IfcWall"):
|
||||
layers = tool.Model.get_material_layer_parameters(element)
|
||||
axis = tool.Model.get_wall_axis(obj, layers)
|
||||
side = [tuple(list(v) + [obj.location.z]) for v in axis["side"]]
|
||||
@@ -1685,34 +1617,8 @@ class WallAxisDecorator:
|
||||
self.draw_batch("LINES", arrow, unselected_elements_color, [(0, 1), (1, 2), (1, 3)])
|
||||
|
||||
|
||||
class SlabDirectionDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_wall_axis, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class SlabDirectionDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_wall_axis"
|
||||
|
||||
def draw_wall_axis(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
@@ -1742,41 +1648,10 @@ class SlabDirectionDecorator:
|
||||
self.draw_batch("LINES", base, selected_elements_color, [(0, 1)])
|
||||
|
||||
|
||||
class FaceAreaDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_face_area, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class FaceAreaDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_face_area"
|
||||
|
||||
def draw_face_area(self, context):
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
@@ -1797,12 +1672,16 @@ class FaceAreaDecorator:
|
||||
if data:
|
||||
self.draw_batch("POINTS", data["verts"], decorator_color)
|
||||
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"])
|
||||
self.draw_batch(
|
||||
"TRIS", data["verts"], tool.Blender.transparent_color(decorator_color, alpha=0.5), data["tris"]
|
||||
)
|
||||
|
||||
|
||||
class BoundingBoxDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
class BoundingBoxDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_bounding_box_wire_cube", "POST_VIEW"),
|
||||
("draw_dimension_text", "POST_PIXEL"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
context = bpy.context
|
||||
@@ -1813,31 +1692,6 @@ class BoundingBoxDecorator:
|
||||
self.decorator_color_wire = (*theme.view_3d.bone_solid, 1)
|
||||
self.decorator_color_special = tool.Blender.get_addon_preferences().decorator_color_special
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
bpy.types.SpaceView3D.draw_handler_add(
|
||||
handler.draw_bounding_box_wire_cube, (context,), "WINDOW", "POST_VIEW"
|
||||
)
|
||||
)
|
||||
cls.handlers.append(
|
||||
bpy.types.SpaceView3D.draw_handler_add(handler.draw_dimension_text, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except Exception:
|
||||
pass
|
||||
cls.handlers.clear()
|
||||
cls.is_installed = False
|
||||
|
||||
@staticmethod
|
||||
def get_combined_bounding_box_corners(objects):
|
||||
|
||||
@@ -1910,14 +1764,6 @@ class BoundingBoxDecorator:
|
||||
]
|
||||
return trihedron[best_origin]
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text_background(self, context, coords_dim, text_dim):
|
||||
padding = 5
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
@@ -2856,7 +2702,7 @@ class MEPSystemPathDecorator(_ConnectedNetworkPathDecorator):
|
||||
lines: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||
port_positions: list[tuple[float, float, float]] = []
|
||||
for element in connected:
|
||||
if element.is_a("IfcFlowSegment"):
|
||||
if element and element.is_a("IfcFlowSegment"):
|
||||
if not tool.Geometry.has_axis_representation(element):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
@@ -41,6 +41,12 @@ def load_post(*args):
|
||||
profile.DumbProfileRegenerator().regenerate_from_profile,
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_pre_listener(
|
||||
"type.assign_type",
|
||||
"Bonsai.Opening.PreserveOnTypeChange",
|
||||
opening.FilledOpeningGenerator().preserve_opening_on_type_change,
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"type.assign_type",
|
||||
"Bonsai.Opening.RegenerateFromType",
|
||||
|
||||
@@ -33,6 +33,7 @@ from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo
|
||||
from bonsai.bim.module.model.opening import is_filling_supported
|
||||
from bonsai.bim.module.model.wall import (
|
||||
_get_wall_geom_cached,
|
||||
_wall_camera_facing_icon_y,
|
||||
@@ -52,6 +53,20 @@ def is_supported_host(element) -> bool:
|
||||
return tool.Parametric.is_path_connectable_wall(element) or element.is_a("IfcSlab") or element.is_a("IfcRoof")
|
||||
|
||||
|
||||
def is_supported_filling_or_opening(element) -> bool:
|
||||
"""Total predicate for the add-opening gizmo poll. ``None`` (raw Blender
|
||||
mesh) is accepted because the operator converts unclassified meshes
|
||||
into ``IfcOpeningElement`` instances. ``IfcOpeningElement`` is accepted
|
||||
because reassigning an existing opening to a new host is a legal path
|
||||
through the operator. Otherwise defer to the generator's own
|
||||
supported-filling predicate."""
|
||||
if element is None:
|
||||
return True
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
return True
|
||||
return is_filling_supported(element)
|
||||
|
||||
|
||||
def _resolve_active_host(context: bpy.types.Context, n_selected: int):
|
||||
"""Shared poll prologue: gizmo gate + selection cardinality + active-in-
|
||||
selected + IFC entity lookup + supported-host predicate. Returns the
|
||||
@@ -72,12 +87,14 @@ def _resolve_active_host(context: bpy.types.Context, n_selected: int):
|
||||
|
||||
|
||||
class GizmoHostAddOpening(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin):
|
||||
"""Activates when a host element (wall / slab / roof) is the active object
|
||||
and exactly one other selected object is *not* itself a host.
|
||||
"""Activates when exactly two objects are selected and one is a fillable
|
||||
host (wall / slab / roof) while the other is a valid filling (door /
|
||||
window / existing opening, or a plain Blender mesh).
|
||||
|
||||
Renders a single ``VIEW3D_GT_add_opening`` icon at the void object's
|
||||
projected location on the host. A click dispatches ``bim.add_opening``,
|
||||
which handles any element exposing the ``HasOpenings`` inverse.
|
||||
Selection-order independent: the host role is identified by class, not
|
||||
by active state. The "+" icon anchors on the host's surface regardless
|
||||
of which object was clicked first. The dispatched ``bim.add_opening``
|
||||
operator also handles either order.
|
||||
|
||||
Per-frame positioning keeps the icon facing the camera as the viewport
|
||||
orbits."""
|
||||
@@ -90,22 +107,29 @@ class GizmoHostAddOpening(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
element = _resolve_active_host(context, n_selected=2)
|
||||
if element is None:
|
||||
if not _wall_gizmo_poll_gate(context):
|
||||
return False
|
||||
# The operator itself filters on HasOpenings, but checking here keeps
|
||||
# the icon from appearing on host classes that can't accept openings
|
||||
# in the active IFC schema.
|
||||
if not hasattr(element, "HasOpenings"):
|
||||
selected = list(tool.Blender.get_selected_objects())
|
||||
if len(selected) != 2:
|
||||
return False
|
||||
active = context.active_object
|
||||
other = next(o for o in tool.Blender.get_selected_objects() if o is not active)
|
||||
# Host + host pairings are claimed by host-specific gizmos (wall-join,
|
||||
# extend-vertical, …) — suppress here so the add-opening icon never
|
||||
# stacks on top of them.
|
||||
if is_supported_host(tool.Ifc.get_entity(other)):
|
||||
if active is None or active not in selected:
|
||||
return False
|
||||
return True
|
||||
a_element = tool.Ifc.get_entity(selected[0])
|
||||
b_element = tool.Ifc.get_entity(selected[1])
|
||||
return cls._is_apply_opening_pair(a_element, b_element) or cls._is_apply_opening_pair(b_element, a_element)
|
||||
|
||||
@staticmethod
|
||||
def _is_apply_opening_pair(host_element, filling_element) -> bool:
|
||||
"""``host_element`` qualifies as a fillable host AND ``filling_element``
|
||||
qualifies as a filling. Used twice with the operands swapped so the
|
||||
gizmo polls true regardless of which of the two selected objects is
|
||||
active."""
|
||||
if not is_supported_host(host_element):
|
||||
return False
|
||||
if not hasattr(host_element, "HasOpenings"):
|
||||
return False
|
||||
return is_supported_filling_or_opening(filling_element)
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
default_color, highlight_color = self.get_decoration_colors()
|
||||
@@ -114,18 +138,20 @@ class GizmoHostAddOpening(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin
|
||||
)
|
||||
|
||||
def position_gizmos(self, context: bpy.types.Context) -> None:
|
||||
host_obj = context.active_object
|
||||
if not host_obj:
|
||||
selected = list(tool.Blender.get_selected_objects())
|
||||
if len(selected) != 2:
|
||||
return
|
||||
selected = tool.Blender.get_selected_objects()
|
||||
other = next((o for o in selected if o is not host_obj), None)
|
||||
if not other:
|
||||
return
|
||||
element = tool.Ifc.get_entity(host_obj)
|
||||
if not element:
|
||||
a, b = selected[0], selected[1]
|
||||
a_element = tool.Ifc.get_entity(a)
|
||||
b_element = tool.Ifc.get_entity(b)
|
||||
if is_supported_host(a_element):
|
||||
host_obj, host_element, other = a, a_element, b
|
||||
elif is_supported_host(b_element):
|
||||
host_obj, host_element, other = b, b_element, a
|
||||
else:
|
||||
return
|
||||
|
||||
if tool.Parametric.is_path_connectable_wall(element):
|
||||
if tool.Parametric.is_path_connectable_wall(host_element):
|
||||
world_pos = wall_anchor(context, self, host_obj, other)
|
||||
else:
|
||||
world_pos = layer3_anchor(host_obj, other)
|
||||
|
||||
@@ -240,6 +240,15 @@ def _store_batch_in_cache(cache_key: tuple[int, str], batch: "gpu.types.GPUBatch
|
||||
_batch_cache[cache_key] = (epoch, batch)
|
||||
|
||||
|
||||
def is_filling_supported(element) -> bool:
|
||||
"""True when Bonsai's opening generator can derive an opening from this
|
||||
element. IFC's schema permits any IfcElement as a filling; Bonsai
|
||||
currently supports only IfcDoor and IfcWindow because those are the
|
||||
classes with OverallWidth/OverallHeight attributes (or their types'
|
||||
ELEVATION_VIEW profiles) that the generator can consume."""
|
||||
return element is not None and element.is_a() in ("IfcDoor", "IfcWindow")
|
||||
|
||||
|
||||
class FilledOpeningGenerator:
|
||||
def generate(
|
||||
self,
|
||||
@@ -409,18 +418,35 @@ class FilledOpeningGenerator:
|
||||
representation = tool.Geometry.get_representation_by_context(voided_element, context)
|
||||
assert representation
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
|
||||
def preserve_opening_on_type_change(
|
||||
self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]
|
||||
) -> None:
|
||||
"""Pre-listener for type.assign_type: anchor the old type's void before reassigning.
|
||||
|
||||
A custom void that lives only on an occurrence (the type has no 'Reference'
|
||||
template) would be lost when that occurrence is moved to another type - the
|
||||
post-assign regeneration replaces it. Promoting it onto its current type first
|
||||
keeps it durable, so switching back later restores it. Idempotent and only acts on
|
||||
genuinely custom (non-extrusion) voids.
|
||||
"""
|
||||
relating_type = settings.get("relating_type")
|
||||
for related_object in settings.get("related_objects") or []:
|
||||
if not getattr(related_object, "FillsVoids", None):
|
||||
continue
|
||||
old_type = ifcopenshell.util.element.get_type(related_object)
|
||||
if old_type and old_type != relating_type:
|
||||
self.promote_opening_to_type(old_type)
|
||||
|
||||
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
relating_type = settings["relating_type"]
|
||||
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object)
|
||||
# Filling type-switch on an array of fillings fans out N host recuts —
|
||||
# one per related object — without batching. Coalesce them.
|
||||
with tool.Geometry.batch_host_recut():
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object)
|
||||
|
||||
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
|
||||
filling = related_object
|
||||
@@ -430,6 +456,13 @@ class FilledOpeningGenerator:
|
||||
opening = filling.FillsVoids[0].RelatingOpeningElement
|
||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||
|
||||
# Always regenerate the opening to reflect the *assigned* type's void: its
|
||||
# 'Reference' template if it has one (generate_opening_from_filling consults it),
|
||||
# else a sibling occurrence's opening, else a generated extrusion. We deliberately
|
||||
# do NOT preserve the previous type's custom void on a type change - a custom void
|
||||
# now survives duplicate_type/append by being anchored on the type as a template
|
||||
# (promote_opening_to_type / harvest), so keeping the old void here would just show
|
||||
# the wrong type's opening (e.g. switching to a plain type would keep the faceset).
|
||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
||||
@@ -469,12 +502,7 @@ class FilledOpeningGenerator:
|
||||
representation = tool.Geometry.get_active_representation(voided_obj)
|
||||
if not representation:
|
||||
continue
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
|
||||
def generate_opening_from_filling(
|
||||
self,
|
||||
@@ -491,6 +519,14 @@ class FilledOpeningGenerator:
|
||||
profile = None
|
||||
filling_type = ifcopenshell.util.element.get_type(filling)
|
||||
if filling_type:
|
||||
# A stored opening template (e.g. a custom IfcPolygonalFaceSet carried
|
||||
# across bim.duplicate_type / append) takes priority over generating a
|
||||
# default extrusion. Returning the shared template representation lets the
|
||||
# caller's map_representation reuse its IfcRepresentationMap, so this
|
||||
# opening stays in sync with the type template and its sibling occurrences.
|
||||
opening_template = self.get_type_opening_representation(filling_type)
|
||||
if opening_template is not None:
|
||||
return opening_template
|
||||
profile = ifcopenshell.util.representation.get_representation(
|
||||
filling_type, "Model", "Profile", "ELEVATION_VIEW"
|
||||
)
|
||||
@@ -588,6 +624,228 @@ class FilledOpeningGenerator:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_opening_representation_custom(self, opening: ifcopenshell.entity_instance) -> bool:
|
||||
"""Whether the opening's Body has user-authored geometry rather than a generated extrusion.
|
||||
|
||||
Openings produced by ``generate_opening_from_filling`` always consist of a
|
||||
single ``IfcExtrudedAreaSolid``. Anything else (a tessellation such as an
|
||||
``IfcPolygonalFaceSet``, a brep, a CSG solid, etc.) was authored by the user
|
||||
and must not be silently replaced with a default extrusion.
|
||||
"""
|
||||
representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return False
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
return any(not item.is_a("IfcExtrudedAreaSolid") for item in representation.Items)
|
||||
|
||||
def should_preserve_opening(self, opening: ifcopenshell.entity_instance) -> bool:
|
||||
"""Whether an opening's geometry is worth anchoring on the type as a template.
|
||||
|
||||
True for user-authored geometry (a tessellation, brep, etc.) or a *manually adjusted*
|
||||
extrusion - one that no longer matches the default ``generate_opening_from_filling``
|
||||
would produce for its filling. A plain generated extrusion is regenerable, so it
|
||||
returns False and is left to regenerate.
|
||||
"""
|
||||
if self.is_opening_representation_custom(opening):
|
||||
return True
|
||||
return self._is_adjusted_extrusion(opening)
|
||||
|
||||
def _is_adjusted_extrusion(self, opening: ifcopenshell.entity_instance) -> bool:
|
||||
"""Whether the opening's extrusion diverges from the default for its filling.
|
||||
|
||||
Generates the default transiently, compares the axis-aligned bounding boxes of the
|
||||
two bodies (both in the opening's local frame), then removes the temporary default.
|
||||
A conservative False is returned when the default cannot be computed.
|
||||
"""
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement if getattr(opening, "HasFillings", None) else None
|
||||
filling_obj = tool.Ifc.get_object(filling) if filling else None
|
||||
current = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
if not filling_obj or current is None:
|
||||
return False
|
||||
current = ifcopenshell.util.representation.resolve_representation(current)
|
||||
default_representation = self.generate_opening_from_filling(filling, filling_obj)
|
||||
try:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
current_bbox = self._representation_bbox(settings, current)
|
||||
default_bbox = self._representation_bbox(settings, default_representation)
|
||||
finally:
|
||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=default_representation)
|
||||
if current_bbox is None or default_bbox is None:
|
||||
return False
|
||||
(cur_min, cur_max), (def_min, def_max) = current_bbox, default_bbox
|
||||
tolerance = 1e-3 # 1 mm; differing extents/position => manually adjusted
|
||||
return bool(np.any(np.abs(cur_min - def_min) > tolerance) or np.any(np.abs(cur_max - def_max) > tolerance))
|
||||
|
||||
@staticmethod
|
||||
def _representation_bbox(settings: Any, representation: ifcopenshell.entity_instance):
|
||||
try:
|
||||
geometry = ifcopenshell.geom.create_shape(settings, representation)
|
||||
except Exception:
|
||||
return None
|
||||
verts = ifcopenshell.util.shape.get_vertices(geometry)
|
||||
if len(verts) == 0:
|
||||
return None
|
||||
return verts.min(axis=0), verts.max(axis=0)
|
||||
|
||||
def get_type_opening_representation(
|
||||
self, filling_type: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Return the type's stored opening template (its 'Reference' representation), if any.
|
||||
|
||||
The template is the shared opening body anchored on the type as a
|
||||
'Reference'-identified representation map (see
|
||||
:meth:`set_type_opening_representation`). Storing it on the type lets a
|
||||
custom opening survive ``bim.duplicate_type`` and project append, which copy
|
||||
the type's ``RepresentationMaps`` but not an opening shared only between
|
||||
occurrences.
|
||||
"""
|
||||
for representation_map in filling_type.RepresentationMaps or []:
|
||||
representation = representation_map.MappedRepresentation
|
||||
if representation.RepresentationIdentifier == "Reference":
|
||||
return representation
|
||||
|
||||
def set_type_opening_representation(
|
||||
self, filling_type: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""Anchor an opening body representation on the type as its 'Reference' template.
|
||||
|
||||
``representation`` is tagged 'Reference' (so it is excluded from the
|
||||
occurrence body geometry, see
|
||||
``ifcopenshell.api.type.map_type_representations``) and the
|
||||
``IfcRepresentationMap`` wrapping it is registered in the type's
|
||||
``RepresentationMaps``, replacing any previous 'Reference' map. The existing map
|
||||
is reused when present so that occurrences mapping over it stay in sync with the
|
||||
type template. Idempotent.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
representation.RepresentationIdentifier = "Reference"
|
||||
representation_map = next(
|
||||
(i for i in ifc_file.get_inverse(representation) if i.is_a("IfcRepresentationMap")), None
|
||||
)
|
||||
if representation_map is None:
|
||||
mapping_origin = ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
representation_map = ifc_file.createIfcRepresentationMap(mapping_origin, representation)
|
||||
# Keep all non-'Reference' maps (Body, Annotation, ...) plus this one, dropping any
|
||||
# previous 'Reference' template so the type carries exactly one.
|
||||
new_maps = [
|
||||
m
|
||||
for m in (filling_type.RepresentationMaps or [])
|
||||
if m == representation_map or m.MappedRepresentation.RepresentationIdentifier != "Reference"
|
||||
]
|
||||
if representation_map not in new_maps:
|
||||
new_maps.append(representation_map)
|
||||
filling_type.RepresentationMaps = new_maps
|
||||
|
||||
def update_type_template_from_opening(self, opening: ifcopenshell.entity_instance) -> None:
|
||||
"""Write an edited opening's geometry back to its filling type's 'Reference' template.
|
||||
|
||||
After a user edits an opening's void shape, anchor the new geometry on the type so
|
||||
the change is durable (survives duplicate_type/append and switching the type away
|
||||
and back) and propagates to sibling occurrences. Only acts on custom (non-extrusion)
|
||||
geometry; a re-generated extrusion needs no template.
|
||||
"""
|
||||
if not getattr(opening, "HasFillings", None) or not self.is_opening_representation_custom(opening):
|
||||
return
|
||||
ifc_file = tool.Ifc.get()
|
||||
new_representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
if not new_representation:
|
||||
return
|
||||
new_representation = ifcopenshell.util.representation.resolve_representation(new_representation)
|
||||
voided_objs_to_reload: set[bpy.types.Object] = set()
|
||||
for rel in opening.HasFillings:
|
||||
filling_type = ifcopenshell.util.element.get_type(rel.RelatedBuildingElement)
|
||||
if not filling_type:
|
||||
continue
|
||||
old_template = self.get_type_opening_representation(filling_type)
|
||||
if old_template is not None and old_template != new_representation:
|
||||
# The edit gave this opening its own geometry; re-point the shared template
|
||||
# map - and therefore every sibling occurrence mapping over it - at the
|
||||
# edited geometry, then drop the now-orphaned old template.
|
||||
for inverse in ifc_file.get_inverse(old_template):
|
||||
if inverse.is_a("IfcRepresentationMap"):
|
||||
inverse.MappedRepresentation = new_representation
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_template)
|
||||
self.set_type_opening_representation(filling_type, new_representation)
|
||||
|
||||
# Re-map every other occurrence's opening onto the type template so the edit
|
||||
# propagates even to siblings that have their own independent opening geometry
|
||||
# (i.e. openings that never shared the template's IfcRepresentationMap).
|
||||
for occurrence in ifcopenshell.util.element.get_types(filling_type):
|
||||
sibling_opening = (
|
||||
occurrence.FillsVoids[0].RelatingOpeningElement
|
||||
if getattr(occurrence, "FillsVoids", None)
|
||||
else None
|
||||
)
|
||||
if not sibling_opening or sibling_opening == opening:
|
||||
continue
|
||||
if not self._remap_opening_to_template(sibling_opening, new_representation):
|
||||
continue
|
||||
if sibling_opening.VoidsElements:
|
||||
voided_element = sibling_opening.VoidsElements[0].RelatingBuildingElement
|
||||
for part in ifcopenshell.util.element.get_parts(voided_element) or [voided_element]:
|
||||
if voided_obj := tool.Ifc.get_object(part):
|
||||
voided_objs_to_reload.add(voided_obj)
|
||||
|
||||
# Reload affected host objects so the viewport re-booleans with the propagated void.
|
||||
for voided_obj in voided_objs_to_reload:
|
||||
representation = tool.Geometry.get_active_representation(voided_obj)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, tool.Geometry, obj=voided_obj, representation=representation
|
||||
)
|
||||
|
||||
def _remap_opening_to_template(
|
||||
self, opening: ifcopenshell.entity_instance, template_representation: ifcopenshell.entity_instance
|
||||
) -> bool:
|
||||
"""Point an opening's Body at the shared type template, purging its old standalone body.
|
||||
|
||||
:return: True if the opening was changed, False if it already maps over the template.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
old_body = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body is not None and (
|
||||
ifcopenshell.util.representation.resolve_representation(old_body) == template_representation
|
||||
):
|
||||
return False
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
ifc_file, representation=template_representation
|
||||
)
|
||||
# The mapped wrapper is the opening's own Body (the 'Reference' identifier belongs to
|
||||
# the type template it maps over, not to the occurrence's representation).
|
||||
mapped_representation.RepresentationIdentifier = "Body"
|
||||
if old_body is not None:
|
||||
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=opening, representation=old_body)
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=opening, representation=mapped_representation)
|
||||
return True
|
||||
|
||||
def promote_opening_to_type(self, filling_type: ifcopenshell.entity_instance) -> None:
|
||||
"""Promote a custom opening from an occurrence to a 'Reference' template on the type.
|
||||
|
||||
Called before a type is copied (``bim.duplicate_type``) so that a custom
|
||||
(non-extrusion) opening, currently shared only between occurrences, is
|
||||
anchored on the type itself and therefore carried to the copy. No-op if the
|
||||
type already has a template or has no custom opening to promote.
|
||||
"""
|
||||
if self.get_type_opening_representation(filling_type):
|
||||
return
|
||||
for occurrence in ifcopenshell.util.element.get_types(filling_type):
|
||||
if not getattr(occurrence, "FillsVoids", None):
|
||||
continue
|
||||
opening = occurrence.FillsVoids[0].RelatingOpeningElement
|
||||
if not self.should_preserve_opening(opening):
|
||||
continue
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
opening, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
self.set_type_opening_representation(filling_type, representation)
|
||||
return
|
||||
|
||||
def get_existing_opening_occurrence_if_any(
|
||||
self, filling: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
@@ -609,6 +867,31 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
# N selected fillings × M voided host parts would fire N×M host recuts
|
||||
# without batching. Coalesce per host.
|
||||
with tool.Geometry.batch_host_recut():
|
||||
return self._recalculate_fills(context)
|
||||
|
||||
def _recalculate_fills(self, context):
|
||||
# Refresh each selected filling's mapped opening source before
|
||||
# recutting the host. Dedup by source id covers the common shared-
|
||||
# source case in one rewrite while leaving unrelated sibling sources
|
||||
# untouched.
|
||||
seen_source_ids: set[int] = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.FillsVoids:
|
||||
continue
|
||||
opening = element.FillsVoids[0].RelatingOpeningElement
|
||||
body = tool.Geometry.get_body_representation(opening)
|
||||
if body is None:
|
||||
continue
|
||||
source = tool.Geometry.resolve_mapped_representation(body)
|
||||
if source.id() in seen_source_ids:
|
||||
continue
|
||||
seen_source_ids.add(source.id())
|
||||
tool.Model.regenerate_filling_opening_body(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.FillsVoids:
|
||||
@@ -637,12 +920,7 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if building_obj and building_obj.data:
|
||||
representation = tool.Geometry.get_active_representation(building_obj)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=building_obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(building_obj, representation)
|
||||
|
||||
# Refresh cut decorator
|
||||
DecoratorData.cut_cache.clear()
|
||||
@@ -964,27 +1242,32 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
for opening_element in opening_elements:
|
||||
opening_obj = tool.Ifc.get_object(opening_element)
|
||||
|
||||
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening_element)
|
||||
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
|
||||
tool.Ifc, similar_openings
|
||||
)
|
||||
building_objs.update(similar_openings_building_objs)
|
||||
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(
|
||||
tool.Geometry, opening_element, similar_openings
|
||||
)
|
||||
elif tool.Ifc.is_moved(opening_obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj)
|
||||
opening_edited = tool.Ifc.is_edited(opening_obj)
|
||||
opening_moved = tool.Ifc.is_moved(opening_obj)
|
||||
# Sibling walls only need a viewport-level refresh when the
|
||||
# opening's shape or placement actually changed — a pure
|
||||
# show/hide toggle leaves them in their existing state.
|
||||
if opening_edited or opening_moved:
|
||||
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening_element)
|
||||
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
|
||||
tool.Ifc, similar_openings
|
||||
)
|
||||
building_objs.update(similar_openings_building_objs)
|
||||
if opening_edited:
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
# Persist the edited void onto the filling type's 'Reference' template so
|
||||
# it survives type duplication/append/switching and propagates to siblings.
|
||||
self.update_type_template_from_opening(opening_element)
|
||||
else:
|
||||
bonsai.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(
|
||||
tool.Geometry, opening_element, similar_openings
|
||||
)
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening_element))
|
||||
|
||||
building_objs.update(
|
||||
self.get_all_building_objects_of_similar_openings(opening_element)
|
||||
) # NB this has nothing to do with clone similar_opening
|
||||
tool.Ifc.unlink(element=opening_element)
|
||||
if props.representation_obj == opening_obj:
|
||||
props.representation_obj = None
|
||||
@@ -1022,6 +1305,12 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
# The voided host may be an aggregate whose parts each get recut.
|
||||
# Coalesce per host so a many-parts aggregate doesn't fan out.
|
||||
with tool.Geometry.batch_host_recut():
|
||||
return self._clone_opening(context)
|
||||
|
||||
def _clone_opening(self, context):
|
||||
# NOTE: Operator displayed in UI only with IfcOpeningElement being active.
|
||||
ifc_file = tool.Ifc.get()
|
||||
objects = bpy.context.selected_objects
|
||||
@@ -1051,12 +1340,7 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
assert representation
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(obj, representation)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import Panel
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
|
||||
@@ -20,7 +20,6 @@ import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell.util.element
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
@@ -28,12 +27,6 @@ from mathutils import Vector
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
# Initialize the bounding box coordinates
|
||||
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
|
||||
@@ -79,26 +72,8 @@ def create_bounding_box(objs):
|
||||
return indices, edges
|
||||
|
||||
|
||||
class NestDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
class NestDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_nest"
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
@@ -154,14 +129,6 @@ class NestDecorator:
|
||||
shader.uniform_float("u_Scale", 25)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_nest(self, context: bpy.types.Context) -> None:
|
||||
props = tool.Nest.get_nest_props()
|
||||
if props.in_nest_mode:
|
||||
@@ -226,35 +193,11 @@ class NestDecorator:
|
||||
self.draw_custom_batch(line, decorator_color_unselected)
|
||||
|
||||
|
||||
class NestModeDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_name, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_empty, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class NestModeDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_nest_name", "POST_PIXEL"),
|
||||
("draw_nest_empty", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_nest_name(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
|
||||
@@ -42,12 +42,6 @@ def toggle_decorations_on_load(*args):
|
||||
# as queried object is linked from separate .blend file.
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
class ProjectDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -80,11 +74,6 @@ class ProjectDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
@@ -110,7 +99,9 @@ class ProjectDecorator:
|
||||
|
||||
if geom.selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, geom.selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), geom.selected_tris)
|
||||
self.draw_batch(
|
||||
"TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), geom.selected_tris
|
||||
)
|
||||
|
||||
|
||||
class ClippingPlaneDecorator:
|
||||
@@ -145,11 +136,6 @@ class ClippingPlaneDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
@@ -210,37 +196,21 @@ class ClippingPlaneDecorator:
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
self.draw_batch(
|
||||
"TRIS", unselected_vertices, tool.Blender.transparent_color(special_elements_color), unselected_tris
|
||||
)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
self.draw_batch(
|
||||
"TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), selected_tris
|
||||
)
|
||||
|
||||
|
||||
class MeasureDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_measurements_text, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_measurements_poly, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
class MeasureDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_measurements_text", "POST_PIXEL"),
|
||||
("draw_measurements_poly", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_measurements_text(self, context):
|
||||
PolylineDecorator().select_and_draw_measurements_text(context)
|
||||
|
||||
@@ -633,6 +633,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not element:
|
||||
return {"FINISHED"}
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
self.harvest_opening_template(element, library_file)
|
||||
self.import_type_from_ifc(element, context)
|
||||
elif element.is_a("IfcProduct"):
|
||||
# NOTE: Non-types are not exposed in UI directly
|
||||
@@ -658,6 +659,57 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def harvest_opening_template(
|
||||
self, type_element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
|
||||
) -> None:
|
||||
"""Seed the appended type's 'Reference' opening template from a library instance.
|
||||
|
||||
A type carries no opening of its own (openings are occurrence-level via
|
||||
IfcRelVoidsElement), so a custom opening would otherwise be lost on append and
|
||||
regenerated as a default extrusion when occurrences are placed. If the library
|
||||
file has an instance of this type whose opening is custom (non-extrusion), copy
|
||||
that opening body onto the appended type as its 'Reference' template. No-op when
|
||||
the type already carries a template (e.g. a Bonsai-authored library) or the
|
||||
library has no such instance.
|
||||
"""
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
generator = FilledOpeningGenerator()
|
||||
if generator.get_type_opening_representation(type_element):
|
||||
return
|
||||
|
||||
library_type = library_file.by_id(self.definition)
|
||||
if not library_type.is_a("IfcTypeProduct"):
|
||||
return
|
||||
|
||||
for occurrence in ifcopenshell.util.element.get_types(library_type):
|
||||
if not getattr(occurrence, "FillsVoids", None):
|
||||
continue
|
||||
opening = occurrence.FillsVoids[0].RelatingOpeningElement
|
||||
library_representation = ifcopenshell.util.representation.get_representation(
|
||||
opening, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
if not library_representation:
|
||||
continue
|
||||
library_representation = ifcopenshell.util.representation.resolve_representation(library_representation)
|
||||
if all(item.is_a("IfcExtrudedAreaSolid") for item in library_representation.Items):
|
||||
continue # A generated extrusion - nothing custom worth preserving.
|
||||
|
||||
project_file = tool.Ifc.get()
|
||||
representation = project_file.add(library_representation)
|
||||
# file.add brings the library's own representation context across; point the
|
||||
# copy at the project's Body context and drop the now-orphaned duplicate.
|
||||
body_context = ifcopenshell.util.representation.get_context(
|
||||
project_file, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
if body_context and representation.ContextOfItems != body_context:
|
||||
orphan_context = representation.ContextOfItems
|
||||
representation.ContextOfItems = body_context
|
||||
if not project_file.get_inverse(orphan_context):
|
||||
project_file.remove(orphan_context)
|
||||
generator.set_type_opening_representation(type_element, representation)
|
||||
return
|
||||
|
||||
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
||||
self.file = tool.Ifc.get()
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
@@ -1422,6 +1474,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.name = filepath
|
||||
new.filepath = filepath
|
||||
new.query = self.query
|
||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
|
||||
|
||||
|
||||
@@ -1492,6 +1545,10 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||
# Fall back to the Link's stored query so callers that omit it
|
||||
# still replay the filter the link was created with.
|
||||
if not self.query and self.link.query:
|
||||
self.query = self.link.query
|
||||
filepath = Path(tool.Ifc.resolve_uri(self.link.filepath))
|
||||
if not filepath.exists():
|
||||
self.report({"ERROR"}, f"File does not exist: '{filepath}'")
|
||||
@@ -1659,13 +1716,36 @@ class ReloadLink(bpy.types.Operator):
|
||||
bl_description = "Reload the selected file"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
query: bpy.props.StringProperty(
|
||||
name="Query",
|
||||
description=(
|
||||
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
|
||||
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
|
||||
),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
query: str
|
||||
|
||||
def invoke(self, context, event):
|
||||
link = tool.Project.get_project_props().links[self.link_index]
|
||||
self.query = link.query
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
self.layout.prop(self, "query", placeholder="IfcElement")
|
||||
|
||||
def execute(self, context):
|
||||
link = tool.Project.get_project_props().links[self.link_index]
|
||||
# An unset query means the operator was called without the dialog
|
||||
# (e.g. from a script) - preserve the link's stored query instead
|
||||
# of overwriting it with the empty default.
|
||||
if self.properties.is_property_set("query"):
|
||||
link.query = self.query
|
||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"}
|
||||
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False, query=link.query) or {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleLinkSelectability(bpy.types.Operator):
|
||||
|
||||
@@ -260,6 +260,11 @@ class Link(PropertyGroup):
|
||||
description="STEP ID of the IfcDocumentReference when linked to a parent IFC project. Zero when no parent IFC exists",
|
||||
default=0,
|
||||
)
|
||||
query: StringProperty(
|
||||
name="Query",
|
||||
description="Selector query used to filter elements when loading the linked model",
|
||||
default="",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
@@ -275,6 +280,7 @@ class Link(PropertyGroup):
|
||||
include_in_drawings: bool
|
||||
empty_handle: Union[bpy.types.Object, None]
|
||||
ifc_definition_id: int
|
||||
query: str
|
||||
|
||||
|
||||
class EditedObj(PropertyGroup):
|
||||
|
||||
@@ -18,43 +18,17 @@
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class GridDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
class GridDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw", "POST_VIEW"),
|
||||
)
|
||||
|
||||
def draw_text(self, context):
|
||||
if not tool.Blender.is_addon_enabled():
|
||||
|
||||
@@ -31,11 +31,17 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
|
||||
|
||||
|
||||
class LoadsDecorator:
|
||||
class LoadsDecorator(tool.Blender.ViewportDecorator):
|
||||
"""Decorator to show structural loads in 3D"""
|
||||
|
||||
is_installed = False
|
||||
handlers = []
|
||||
# draw_methods exists to satisfy ViewportDecorator.__init_subclass__'s
|
||||
# method-existence check; the override install below is what actually
|
||||
# registers handlers (the POST_VIEW binding passes no context arg, which
|
||||
# the base's generic install cannot express).
|
||||
draw_methods = (
|
||||
("draw_load_values", "POST_PIXEL"),
|
||||
("__call__", "POST_VIEW"),
|
||||
)
|
||||
decoration_data = None
|
||||
text_info = []
|
||||
shader_info = []
|
||||
@@ -54,15 +60,6 @@ class LoadsDecorator:
|
||||
cls.update()
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls) -> None:
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
@classmethod
|
||||
def update(cls) -> None:
|
||||
cls.decoration_data.update()
|
||||
|
||||
@@ -744,7 +744,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
|
||||
if self.ifc_class == "IfcSurfaceStyleLighting":
|
||||
|
||||
def callback(attribute_name: str, _: object, data: dict[str, Any]) -> None:
|
||||
assert attributes
|
||||
assert attributes is not None
|
||||
color = attributes.add()
|
||||
assert isinstance(color, ColourRgb)
|
||||
color.name = attribute_name
|
||||
@@ -782,34 +782,40 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
self.props = tool.Style.get_style_props()
|
||||
self.style = tool.Ifc.get().by_id(self.props.is_editing_style)
|
||||
prev_update_graph = self.props.update_graph
|
||||
self.props["update_graph"] = False
|
||||
|
||||
style_elements = tool.Style.get_style_elements(self.style)
|
||||
# NOTE: currently this operator is used to edit existing (and only existing) IfcSurfaceStyles
|
||||
# or new or existing IfcSurfaceStyle components (shading, etc)
|
||||
# which is kind of confusing.
|
||||
if self.props.is_editing_class == "IfcSurfaceStyle":
|
||||
self.surface_style = self.style
|
||||
else:
|
||||
self.surface_style = style_elements.get(self.props.is_editing_class, None)
|
||||
self.shading_style = style_elements.get("IfcSurfaceStyleShading", None)
|
||||
self.rendering_style = style_elements.get("IfcSurfaceStyleRendering", None)
|
||||
self.texture_style = style_elements.get("IfcSurfaceStyleWithTextures", None)
|
||||
try:
|
||||
style_elements = tool.Style.get_style_elements(self.style)
|
||||
|
||||
if self.surface_style:
|
||||
result = self.edit_existing_style()
|
||||
else:
|
||||
result = self.add_new_style()
|
||||
# NOTE: currently this operator is used to edit existing (and only existing) IfcSurfaceStyles
|
||||
# or new or existing IfcSurfaceStyle components (shading, etc)
|
||||
# which is kind of confusing.
|
||||
if self.props.is_editing_class == "IfcSurfaceStyle":
|
||||
self.surface_style = self.style
|
||||
else:
|
||||
self.surface_style = style_elements.get(self.props.is_editing_class, None)
|
||||
self.shading_style = style_elements.get("IfcSurfaceStyleShading", None)
|
||||
self.rendering_style = style_elements.get("IfcSurfaceStyleRendering", None)
|
||||
self.texture_style = style_elements.get("IfcSurfaceStyleWithTextures", None)
|
||||
|
||||
if result:
|
||||
return result
|
||||
if self.surface_style:
|
||||
result = self.edit_existing_style()
|
||||
else:
|
||||
result = self.add_new_style()
|
||||
|
||||
tool.Style.disable_editing()
|
||||
core.load_styles(tool.Style, style_type=self.props.style_type)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# restore selected style type
|
||||
material = tool.Ifc.get_object(self.style)
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
msprops.active_style_type = msprops.active_style_type
|
||||
tool.Style.disable_editing()
|
||||
core.load_styles(tool.Style, style_type=self.props.style_type)
|
||||
|
||||
# restore selected style type
|
||||
material = tool.Ifc.get_object(self.style)
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
msprops.active_style_type = msprops.active_style_type
|
||||
finally:
|
||||
self.props["update_graph"] = prev_update_graph
|
||||
|
||||
def edit_existing_style(self) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -1231,4 +1237,5 @@ class RemoveSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
surface_style = tool.Style.get_style_elements(style)[props.is_editing_class]
|
||||
ifcopenshell.api.style.remove_surface_style(ifc_file, surface_style)
|
||||
core.disable_editing_style(tool.Style)
|
||||
core.load_styles(tool.Style, style_type=props.style_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -185,6 +185,13 @@ class ColourRgb(PropertyGroup):
|
||||
# to fit blender.bim.helper.draw_attribute
|
||||
is_optional = False
|
||||
special_type = ""
|
||||
data_type = ""
|
||||
ifc_class = ""
|
||||
use_explorer_ui = False
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self.name
|
||||
|
||||
def get_value_name(self, *args, **kwargs):
|
||||
return "color_value"
|
||||
|
||||
@@ -176,8 +176,30 @@ class BIM_PT_styles(Panel):
|
||||
row.prop(self.props, "reflectance_method")
|
||||
|
||||
if self.props.reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"):
|
||||
self.layout.label(text="Supported reflectance methods are:")
|
||||
self.layout.label(text="PHYSICAL / NOTDEFINED / FLAT")
|
||||
self.layout.label(
|
||||
text=f"{self.props.reflectance_method} will be skipped: only PHYSICAL / NOTDEFINED / FLAT are supported",
|
||||
icon="ERROR",
|
||||
)
|
||||
elif self.props.reflectance_method in ("PHYSICAL", "NOTDEFINED"):
|
||||
if self.props.specular_colour_class == "IfcColourRgb":
|
||||
self.layout.label(
|
||||
text="Metallic color is IFC-only in PHYSICAL/NOTDEFINED and does not affect Blender appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
elif self.props.reflectance_method == "FLAT":
|
||||
if self.props.diffuse_colour_class == "IfcNormalisedRatioMeasure":
|
||||
self.layout.label(
|
||||
text="Emissive ratio is IFC-only in FLAT Reflectance method and does not affect Blender appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
self.layout.label(
|
||||
text="Specular value is IFC-only in FLAT Reflectance method and does not affect Blender appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
self.layout.label(
|
||||
text="Highlight value is IFC-only in FLAT Reflectance method and does not affect Blender appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Emissive" if self.props.reflectance_method == "FLAT" else "Diffuse")
|
||||
@@ -232,6 +254,8 @@ class BIM_PT_styles(Panel):
|
||||
row.operator("bim.add_surface_texture", text="", icon="ADD")
|
||||
if textures:
|
||||
self.layout.prop(self.props, "uv_mode")
|
||||
if self.props.uv_mode in ("Generated", "Camera"):
|
||||
self.layout.label(text="Not available in SOLID Mode", icon="INFO")
|
||||
|
||||
for i, texture in enumerate(textures):
|
||||
split = self.layout.split(factor=0.30, align=True)
|
||||
@@ -244,6 +268,22 @@ class BIM_PT_styles(Panel):
|
||||
op_clear = row.operator("bim.remove_texture_map", text="", icon="X")
|
||||
op_path.texture_map_index = op_clear.texture_map_index = i
|
||||
|
||||
reflectance = self.props.reflectance_method
|
||||
mode = texture.mode
|
||||
if reflectance == "FLAT":
|
||||
if mode != "EMISSIVE":
|
||||
self.layout.label(
|
||||
text=f"{mode} will be skipped: only EMISSIVE is supported for Render Reflectance FLAT",
|
||||
icon="ERROR",
|
||||
)
|
||||
elif reflectance in ("PHYSICAL", "NOTDEFINED"):
|
||||
_SUPPORTED = {"DIFFUSE", "NORMAL", "METALLICROUGHNESS", "EMISSIVE", "OCCLUSION"}
|
||||
if mode not in _SUPPORTED:
|
||||
self.layout.label(
|
||||
text=f"{mode} will be skipped: not supported for Render Reflectance PHYSICAL/NOTDEFINED",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
def draw_externally_defined_surface_style(self):
|
||||
row = self.layout.row()
|
||||
op = row.operator("bim.browse_external_style", icon="APPEND_BLEND", text="Append From Blend File")
|
||||
@@ -252,10 +292,17 @@ class BIM_PT_styles(Panel):
|
||||
bonsai.bim.helper.draw_attributes(self.props.external_style_attributes, self.layout, enable_search=True)
|
||||
|
||||
def draw_refraction_surface_style(self):
|
||||
self.layout.label(
|
||||
text="Refraction values are IFC-only and do not affect Blender surface appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
bonsai.bim.helper.draw_attributes(self.props.refraction_style_attributes, self.layout, enable_search=True)
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
def draw_lighting_surface_style(self):
|
||||
self.layout.label(
|
||||
text="Lighting values are IFC-only and do not affect Blender surface appearance",
|
||||
icon="ERROR",
|
||||
)
|
||||
bonsai.bim.helper.draw_attributes(self.props.lighting_style_colours, self.layout)
|
||||
|
||||
def draw_edit_ui(self, edit_label: str):
|
||||
|
||||
@@ -31,12 +31,6 @@ ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
@persistent
|
||||
def toggle_decorations_on_load(*args):
|
||||
props = tool.System.get_system_props()
|
||||
@@ -80,7 +74,7 @@ class SystemDecorator:
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
faces_color = tool.Blender.transparent_color(self.addon_prefs.decorator_color_special)
|
||||
tool.Blender.draw_bmesh_face_tris(bm, vertices_coords, faces_color, self.draw_batch)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
@@ -128,13 +122,15 @@ class SystemDecorator:
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
|
||||
self.draw_batch(
|
||||
"LINES", all_vertices, tool.Blender.transparent_color(unselected_elements_color), unselected_edges
|
||||
)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", unselected_vertices, tool.Blender.transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
|
||||
self.draw_batch("POINTS", special_vertices, special_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
@@ -375,6 +375,14 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
return {"FINISHED"}
|
||||
# Anchor any custom (non-extrusion) opening on the source type before the
|
||||
# copy so it is carried to the duplicate as a 'Reference' template, rather
|
||||
# than regenerated as a default extrusion on the new type's occurrences.
|
||||
if element.is_a("IfcElementType"):
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().promote_opening_to_type(element)
|
||||
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
|
||||
@@ -26,7 +26,7 @@ import bonsai.bim.handler
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator, is_filling_supported
|
||||
|
||||
|
||||
class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -34,11 +34,13 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Apply Opening"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Apply opening objects to an Element.\n\n"
|
||||
"The Element and the openings to be applied should be selected. The order of selection is not important.\n"
|
||||
"Opening can be just a Blender mesh object.\n\n"
|
||||
"Shift+click: keep the filling at its current matrix_world — skip the wall-axis snap "
|
||||
"and the rl1/rl2 Z-elevation default that the regular click applies."
|
||||
"Cuts openings in a wall, slab, or roof using selected shape objects — "
|
||||
"doors, windows, existing openings, or plain (non-IFC) meshes. "
|
||||
"Selection order doesn't matter.\n\n"
|
||||
"Doors and windows also fill the opening. Other IFC classes are currently "
|
||||
"unsupported by the opening generator and get skipped with a warning.\n\n"
|
||||
"Shift+click: keep each opening at its shape object's current position "
|
||||
"instead of snapping to the wall."
|
||||
)
|
||||
|
||||
# Toggled by ``invoke`` when the user holds SHIFT during a gizmo / hotkey
|
||||
@@ -59,6 +61,12 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
# Multi-opening drops on the same host fan out N update_representation
|
||||
# writes + N switch_representation recuts without batching. Coalesce.
|
||||
with tool.Geometry.batch_host_recut():
|
||||
return self._add_openings(context)
|
||||
|
||||
def _add_openings(self, context):
|
||||
selected_objects = context.selected_objects
|
||||
target_object = selected_objects[0]
|
||||
|
||||
@@ -78,8 +86,14 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"INFO"}, "You can't add an opening to another opening.")
|
||||
continue
|
||||
elif not element1.is_a("IfcOpeningElement") and not element2.is_a("IfcOpeningElement"):
|
||||
if element1.is_a("IfcWindow") or element1.is_a("IfcDoor"): # Add a fill to an element.
|
||||
if is_filling_supported(element1): # Add a fill to an element.
|
||||
obj1, obj2 = obj2, obj1
|
||||
elif not is_filling_supported(element2):
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Cannot apply {element2.is_a()} as an opening — Bonsai currently supports only IfcDoor and IfcWindow as parametric fillings.",
|
||||
)
|
||||
continue
|
||||
FilledOpeningGenerator().generate(
|
||||
obj2,
|
||||
obj1,
|
||||
@@ -165,7 +179,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
voided_obj.scale = (1.0, 1.0, 1.0)
|
||||
tool.Ifc.finish_edit(voided_obj)
|
||||
else:
|
||||
bpy.ops.bim.update_representation(obj=voided_obj.name)
|
||||
tool.Geometry.update_host_representation(voided_obj)
|
||||
|
||||
if tool.Ifc.is_moved(voided_obj):
|
||||
bonsai.core.geometry.edit_object_placement(
|
||||
@@ -174,12 +188,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
representation = tool.Geometry.get_active_representation(voided_obj)
|
||||
assert representation
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
tool.Geometry.lock_scale(voided_obj)
|
||||
|
||||
if not has_visible_openings:
|
||||
@@ -217,12 +226,7 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if building_obj and building_obj.data:
|
||||
representation = tool.Geometry.get_active_representation(building_obj)
|
||||
assert representation
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=building_obj,
|
||||
representation=representation,
|
||||
)
|
||||
tool.Geometry.recut_host(building_obj, representation)
|
||||
tool.Geometry.unlock_scale_object_with_openings(obj)
|
||||
tool.Geometry.clear_cache(element)
|
||||
return {"FINISHED"}
|
||||
|
||||
+43
-68
@@ -41,20 +41,8 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import is_cache_locked_by_other_process
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
|
||||
from bonsai.bim.module.model.prop import (
|
||||
BIMDoorProperties,
|
||||
BIMRailingProperties,
|
||||
BIMRoofProperties,
|
||||
BIMStairProperties,
|
||||
BIMWindowProperties,
|
||||
)
|
||||
from bonsai.bim.module.model.ui import (
|
||||
draw_door_properties,
|
||||
draw_railing_properties,
|
||||
draw_roof_properties,
|
||||
draw_stair_properties,
|
||||
draw_window_properties,
|
||||
)
|
||||
from bonsai.bim.module.model import prop as _model_prop
|
||||
from bonsai.bim.module.model import ui as _model_ui
|
||||
from bonsai.bim.module.pset.prop import IfcProperty
|
||||
from bonsai.bim.prop import Attribute
|
||||
|
||||
@@ -279,34 +267,29 @@ class BIM_UL_panel_visibilities(bpy.types.UIList):
|
||||
class GizmoPreferences(bpy.types.PropertyGroup):
|
||||
"""Aggregator for parametric gizmo visibility settings. One flat bool per
|
||||
parametric feature; controls whether that feature's gizmo group polls
|
||||
visible in the viewport."""
|
||||
visible in the viewport.
|
||||
|
||||
The per-feature ``<name>: BoolProperty`` fields are derived from
|
||||
``tool.Parametric.EDIT_TYPES`` at module load — adding a new parametric
|
||||
type to the registry automatically surfaces its toggle here, with no
|
||||
parallel hand-maintained list to keep in sync."""
|
||||
|
||||
draw_gizmos_in_3d_viewport: BoolProperty(
|
||||
name="Draw Gizmos In 3D Viewport",
|
||||
default=True,
|
||||
description="Show interactive gizmos in the 3D viewport for parametric elements",
|
||||
)
|
||||
door: BoolProperty(name="Door", default=True)
|
||||
window: BoolProperty(name="Window", default=True)
|
||||
stair: BoolProperty(name="Stair", default=True)
|
||||
railing: BoolProperty(name="Railing", default=True)
|
||||
roof: BoolProperty(name="Roof", default=True)
|
||||
array: BoolProperty(name="Array", default=True)
|
||||
pipe_segment: BoolProperty(name="Pipe Segment", default=True)
|
||||
duct_segment: BoolProperty(name="Duct Segment", default=True)
|
||||
wall: BoolProperty(name="Wall", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
draw_gizmos_in_3d_viewport: bool
|
||||
door: bool
|
||||
window: bool
|
||||
stair: bool
|
||||
railing: bool
|
||||
roof: bool
|
||||
array: bool
|
||||
pipe_segment: bool
|
||||
duct_segment: bool
|
||||
wall: bool
|
||||
|
||||
|
||||
for _gizmo_pref_entry in tool.Parametric.EDIT_TYPES:
|
||||
GizmoPreferences.__annotations__[_gizmo_pref_entry.name] = BoolProperty(
|
||||
name=_gizmo_pref_entry.name.replace("_", " ").title(),
|
||||
default=True,
|
||||
)
|
||||
del _gizmo_pref_entry
|
||||
|
||||
|
||||
class DocPreferences(bpy.types.PropertyGroup):
|
||||
@@ -402,11 +385,22 @@ class DocPreferences(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
class DefaultParameters(bpy.types.PropertyGroup):
|
||||
door: bpy.props.PointerProperty(type=BIMDoorProperties)
|
||||
window: bpy.props.PointerProperty(type=BIMWindowProperties)
|
||||
railing: bpy.props.PointerProperty(type=BIMRailingProperties)
|
||||
roof: bpy.props.PointerProperty(type=BIMRoofProperties)
|
||||
stair: bpy.props.PointerProperty(type=BIMStairProperties)
|
||||
"""Per-type preset values used to seed new parametric instances.
|
||||
|
||||
The ``<name>: PointerProperty`` fields are derived from the subset of
|
||||
``tool.Parametric.EDIT_TYPES`` flagged ``has_default_parameters=True``,
|
||||
each pointing at the matching ``BIM<Name>Properties`` class. Adding a
|
||||
new entry with that flag automatically surfaces a preferences section
|
||||
and gives the create operator a preset to copy from."""
|
||||
|
||||
|
||||
for _default_params_entry in tool.Parametric.EDIT_TYPES:
|
||||
if not _default_params_entry.has_default_parameters:
|
||||
continue
|
||||
DefaultParameters.__annotations__[_default_params_entry.name] = bpy.props.PointerProperty(
|
||||
type=getattr(_model_prop, _default_params_entry.props_attr),
|
||||
)
|
||||
del _default_params_entry
|
||||
|
||||
|
||||
class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
@@ -828,36 +822,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
def draw_default_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
box = layout.box()
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
"Door",
|
||||
lambda _layout, _context: draw_door_properties(_layout, self.default_parameters.door),
|
||||
)
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
"Window",
|
||||
lambda _layout, _context: draw_window_properties(_layout, self.default_parameters.window),
|
||||
)
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
"Railing",
|
||||
lambda _layout, _context: draw_railing_properties(_layout, self.default_parameters.railing),
|
||||
)
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
"Roof",
|
||||
lambda _layout, _context: draw_roof_properties(_layout, self.default_parameters.roof),
|
||||
)
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
"Stair",
|
||||
lambda _layout, _context: draw_stair_properties(_layout, self.default_parameters.stair),
|
||||
)
|
||||
for entry in tool.Parametric.EDIT_TYPES:
|
||||
if not entry.has_default_parameters:
|
||||
continue
|
||||
props = getattr(self.default_parameters, entry.name)
|
||||
draw_props = getattr(_model_ui, f"draw_{entry.name}_properties")
|
||||
bonsai.bim.helper.draw_expandable_panel(
|
||||
box,
|
||||
context,
|
||||
entry.name.replace("_", " ").title(),
|
||||
lambda _layout, _context, _draw=draw_props, _props=props: _draw(_layout, _props),
|
||||
)
|
||||
|
||||
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "opening_focus_opacity")
|
||||
|
||||
@@ -176,6 +176,96 @@ def remove_representation(
|
||||
geometry.delete_data(data)
|
||||
|
||||
|
||||
def promote_representation_to_type(
|
||||
ifc: type[tool.Ifc],
|
||||
geometry: type[tool.Geometry],
|
||||
obj: bpy.types.Object,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
) -> dict[str, int]:
|
||||
"""Move an occurrence-local representation onto its type (slot-based, type wins).
|
||||
|
||||
The representation is copied onto the type as a mapped representation. Then,
|
||||
for every occurrence of the type, **any** existing representation in the same
|
||||
*slot* — same context (context/subcontext/target view), same
|
||||
``RepresentationIdentifier`` and same ``RepresentationType`` — is removed and
|
||||
replaced by the type's mapped representation. This covers both a local
|
||||
(non-mapped) rep and an already-mapped rep the occurrence inherited from
|
||||
another map (e.g. a floating ``IfcRepresentationMap`` not anchored to the
|
||||
type, as Revit exports), so no duplicate is left. If the type already holds a
|
||||
rep in the slot it is removed too, so promoting is idempotent / replaces
|
||||
rather than accumulating maps.
|
||||
|
||||
Geometry is NOT compared: the type's representation replaces the
|
||||
occurrence's for that slot, even when the occurrence's geometry differs
|
||||
(e.g. an independently meshed / mirrored / rotated instance) — such
|
||||
occurrences visibly adopt the type's geometry.
|
||||
|
||||
:return: counts dict with ``replaced`` (occurrence reps removed) and
|
||||
``occurrences`` (occurrences that now reference the type's mapped rep).
|
||||
"""
|
||||
element = ifc.get_entity(obj)
|
||||
assert element
|
||||
element_type = geometry.get_element_type(element)
|
||||
assert element_type, "Cannot promote a representation without a type."
|
||||
context = representation.ContextOfItems
|
||||
identifier = representation.RepresentationIdentifier
|
||||
# Compare the *resolved* type: an inherited rep is a "MappedRepresentation"
|
||||
# whose real type lives on the map's target, so matching the raw
|
||||
# RepresentationType would miss it and leave a duplicate.
|
||||
rep_type = geometry.resolve_mapped_representation(representation).RepresentationType
|
||||
|
||||
def _in_slot(r: ifcopenshell.entity_instance) -> bool:
|
||||
return (
|
||||
r.ContextOfItems == context
|
||||
and r.RepresentationIdentifier == identifier
|
||||
and geometry.resolve_mapped_representation(r).RepresentationType == rep_type
|
||||
)
|
||||
|
||||
# Copy the promoted geometry for the type up front, before any removal below
|
||||
# can touch the source occurrence's representation.
|
||||
type_representation = geometry.copy_representation_deep(representation)
|
||||
|
||||
# Snapshot every occurrence's reps in the slot (local AND mapped) so we can
|
||||
# replace them -- an occurrence may already inherit a mapped rep in the slot,
|
||||
# which would otherwise be left behind as a duplicate.
|
||||
occurrence_reps: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
|
||||
for occurrence in geometry.get_elements_of_type(element_type):
|
||||
occurrence_reps[occurrence] = [r for r in geometry.get_representations_iter(occurrence) if _in_slot(r)]
|
||||
|
||||
counts = {"replaced": 0, "occurrences": 0}
|
||||
# 1. Drop every existing rep in the slot from each occurrence.
|
||||
for occurrence, reps_in_slot in occurrence_reps.items():
|
||||
occurrence_obj = ifc.get_object(occurrence)
|
||||
for rep in reps_in_slot:
|
||||
if occurrence_obj is not None and not geometry.is_mapped_representation(rep):
|
||||
# Blender-aware removal for a local mesh the object may display.
|
||||
remove_representation(ifc, geometry, obj=occurrence_obj, representation=rep)
|
||||
else:
|
||||
# Mapped wrapper (or object not loaded): remove from this
|
||||
# occurrence only. remove_representation's remove_deep2 keeps a
|
||||
# shared map alive until its last user is gone.
|
||||
ifc.run("geometry.unassign_representation", product=occurrence, representation=rep)
|
||||
ifc.run("geometry.remove_representation", representation=rep)
|
||||
counts["replaced"] += 1
|
||||
|
||||
# 2. Drop any rep the type already holds in this slot, so we replace rather
|
||||
# than accumulate maps. (Must run before adding the new map below.)
|
||||
for rm in list(element_type.RepresentationMaps or []):
|
||||
mapped = rm.MappedRepresentation
|
||||
if mapped and _in_slot(mapped):
|
||||
ifc.run("geometry.unassign_representation", product=element_type, representation=mapped)
|
||||
ifc.run("geometry.remove_representation", representation=mapped)
|
||||
|
||||
# 3. Add the promoted geometry to the type and map it onto every occurrence.
|
||||
geometry.add_type_representation_map(element_type, type_representation)
|
||||
for occurrence in occurrence_reps:
|
||||
mapped_representation = ifc.run("geometry.map_representation", representation=type_representation)
|
||||
ifc.run("geometry.assign_representation", product=occurrence, representation=mapped_representation)
|
||||
counts["occurrences"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def purge_unused_representations(ifc: type[tool.Ifc], geometry: type[tool.Geometry]) -> int:
|
||||
"""Purge representations without inverses.
|
||||
|
||||
|
||||
@@ -445,6 +445,8 @@ class Geometry:
|
||||
def clear_modifiers(cls, obj): pass
|
||||
def clear_scale(cls, obj): pass
|
||||
def copy_data_links(cls, data, copied_entities) -> None: pass
|
||||
def copy_representation_deep(cls, representation): pass
|
||||
def add_type_representation_map(cls, element_type, representation): pass
|
||||
def delete_data(cls, data): pass
|
||||
def delete_ifc_object(cls, obj): pass
|
||||
def delete_opening_object_placement(cls, opening): pass
|
||||
|
||||
@@ -538,6 +538,19 @@ class Blender(bonsai.core.tool.Blender):
|
||||
cls.handlers.clear()
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
"""Submit a GPU batch through ``self.line_shader`` (for ``"LINES"``)
|
||||
or ``self.shader`` (for any other primitive). Skips empty batches
|
||||
via ``validate_shader_batch_data`` so Blender 4.4+ doesn't crash on
|
||||
empty ``indices``. Subclasses bind both shaders in their draw method
|
||||
before calling this helper."""
|
||||
if not Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
@staticmethod
|
||||
def _lookup_active_instance(gizmo_cls: type, context: bpy.types.Context) -> Optional[Any]:
|
||||
"""Return the live ``GizmoGroup`` instance registered under
|
||||
@@ -1524,6 +1537,10 @@ class Blender(bonsai.core.tool.Blender):
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
elif feature := tool.Parametric.is_object_editing(obj):
|
||||
tool.Parametric.run_bim_op(feature.finish_op)
|
||||
elif tool.Parametric.is_wall(element):
|
||||
# Placed after the generic finish dispatch so the TAB toggle splits:
|
||||
# wall already editing → finish above; wall not editing → enter here.
|
||||
bpy.ops.bim.enable_editing_wall()
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
@@ -2387,6 +2404,13 @@ class Blender(bonsai.core.tool.Blender):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def transparent_color(color: Iterable[float], alpha: float = 0.1) -> list[float]:
|
||||
"""Copy an RGBA color with its alpha channel overridden."""
|
||||
out = [c for c in color]
|
||||
out[3] = alpha
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def draw_bmesh_face_tris(
|
||||
cls,
|
||||
|
||||
@@ -86,7 +86,19 @@ class ClipBox:
|
||||
# aligned to the prior view and the edit-mode picker rejects verts
|
||||
# inside the current clip_planes after orbit/pan/zoom.
|
||||
_view_matrix_at_arm: dict[int, tuple] = {}
|
||||
_refresh_pending: bool = False
|
||||
_pending_refresh: Optional[Callable[[], None]] = None
|
||||
# True from load_pre until the first on_pre_view tick of the new file
|
||||
# (first paint = GPU contexts wired). Suppresses schedule_refresh and
|
||||
# short-circuits on_depsgraph_update so neither path can drive
|
||||
# RegionView3D.update() against regions whose GL state is not yet
|
||||
# initialised — that crash inside GPU_matrix_ortho_set is a CTD, not
|
||||
# catchable from Python. load_post fires before the first paint, so it
|
||||
# CANNOT be the gate-clear point.
|
||||
_file_loading: bool = False
|
||||
# Edge-trigger consumed by on_pre_view: clears _file_loading on the
|
||||
# first frame after load and kicks a refresh so the new file's clip
|
||||
# box arms against now-safe regions.
|
||||
_post_load_paint_pending: bool = False
|
||||
_last_seen_ifc_id: int = 0
|
||||
# Tracks the last matrix we persisted to the pset, keyed by Blender
|
||||
# object name. Lets the depsgraph handler detect committed transform
|
||||
@@ -318,19 +330,32 @@ class ClipBox:
|
||||
from within a property write disrupts gizmo modal accounting and
|
||||
can leave the operator stack inconsistent. Deferring via a 0-delay
|
||||
timer hands the refresh to Blender's main loop, where operators are
|
||||
legal. Debounced: a flag suppresses repeats while one is pending.
|
||||
legal. Debounced: a pending handle suppresses repeats while one is
|
||||
in flight, and lets the file-load gate tear it down cleanly so the
|
||||
timer can't fire against not-yet-realised regions.
|
||||
"""
|
||||
if cls._refresh_pending:
|
||||
if cls._file_loading:
|
||||
return
|
||||
if cls._pending_refresh is not None:
|
||||
return
|
||||
cls._refresh_pending = True
|
||||
|
||||
def _do_refresh():
|
||||
cls._refresh_pending = False
|
||||
cls._pending_refresh = None
|
||||
cls.refresh()
|
||||
return None
|
||||
|
||||
cls._pending_refresh = _do_refresh
|
||||
bpy.app.timers.register(_do_refresh, first_interval=0.0)
|
||||
|
||||
@classmethod
|
||||
def _cancel_pending_refresh(cls) -> None:
|
||||
"""Cancel any pending debounced refresh. Idempotent; safe to call
|
||||
when none is registered (e.g. on addon unregister)."""
|
||||
pending = cls._pending_refresh
|
||||
if pending is not None and bpy.app.timers.is_registered(pending):
|
||||
bpy.app.timers.unregister(pending)
|
||||
cls._pending_refresh = None
|
||||
|
||||
@classmethod
|
||||
def reset_ownership(cls) -> None:
|
||||
"""Drop the ownership table without touching any region. Used on register/reload."""
|
||||
@@ -594,6 +619,13 @@ class ClipBox:
|
||||
so this branch fires once per commit — exactly the cadence the
|
||||
user expects for "save my latest transform".
|
||||
"""
|
||||
# During the file-load danger window (load_pre → first paint of the
|
||||
# new file) the screen exists but its regions' GPU contexts are not
|
||||
# yet wired; calling apply_clip_planes_direct here drives
|
||||
# RegionView3D.update() into a CTD inside GPU_matrix_ortho_set.
|
||||
# on_pre_view will reopen this gate on first paint.
|
||||
if cls._file_loading:
|
||||
return
|
||||
if getattr(bpy.context, "screen", None) is None:
|
||||
return
|
||||
if cls._active_scene_props(scene) is None:
|
||||
@@ -662,6 +694,15 @@ class ClipBox:
|
||||
handler's job (it fires on transform commit and writes through
|
||||
the operator transaction path).
|
||||
"""
|
||||
# First paint after a file load is the GPU-ready signal: open the
|
||||
# _file_loading gate and kick a refresh so the new file's clip box
|
||||
# arms against now-safe regions. Runs BEFORE the active-clip-box
|
||||
# check so the gate clears even when the new file has no clip box
|
||||
# (otherwise the gate would deadlock until the next file load).
|
||||
if cls._post_load_paint_pending:
|
||||
cls._post_load_paint_pending = False
|
||||
cls._file_loading = False
|
||||
cls.schedule_refresh()
|
||||
if cls._active_scene_props() is None:
|
||||
return
|
||||
obj = cls.get_active_clip_box()
|
||||
|
||||
@@ -78,6 +78,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class Drawing(bonsai.core.tool.Drawing):
|
||||
ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"]
|
||||
PERSPECTIVE_CAMERA_SHIFT_PROPERTIES = ("PerspectiveShiftX", "PerspectiveShiftY")
|
||||
DOCUMENT_TYPE = Literal["SCHEDULE", "REFERENCE"]
|
||||
LocationHintLiteral = Literal["PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST"]
|
||||
LOCATION_HINT_LITERALS = ("PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST")
|
||||
@@ -453,6 +454,41 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera.matrix_world = matrix
|
||||
return camera
|
||||
|
||||
@classmethod
|
||||
def get_perspective_camera_shifts(cls, drawing: ifcopenshell.entity_instance) -> dict[str, float]:
|
||||
pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") or {}
|
||||
shift_x_prop, shift_y_prop = cls.PERSPECTIVE_CAMERA_SHIFT_PROPERTIES
|
||||
return {
|
||||
"shift_x": float(pset.get(shift_x_prop, 0.0) or 0.0),
|
||||
"shift_y": float(pset.get(shift_y_prop, 0.0) or 0.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def sync_perspective_camera_shifts(cls, drawing: ifcopenshell.entity_instance, camera: bpy.types.Camera) -> None:
|
||||
if camera.type != "PERSP":
|
||||
return
|
||||
|
||||
shift_x_prop, shift_y_prop = cls.PERSPECTIVE_CAMERA_SHIFT_PROPERTIES
|
||||
current_shifts = cls.get_perspective_camera_shifts(drawing)
|
||||
new_shifts = {"shift_x": float(camera.shift_x or 0.0), "shift_y": float(camera.shift_y or 0.0)}
|
||||
if tool.Cad.is_x(current_shifts["shift_x"], new_shifts["shift_x"]) and tool.Cad.is_x(
|
||||
current_shifts["shift_y"], new_shifts["shift_y"]
|
||||
):
|
||||
return
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={
|
||||
shift_x_prop: new_shifts["shift_x"],
|
||||
shift_y_prop: new_shifts["shift_y"],
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_svg_schedule(cls, schedule: ifcopenshell.entity_instance) -> None:
|
||||
import bonsai.bim.module.drawing.scheduler as scheduler
|
||||
@@ -1009,6 +1045,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.has_annotation = True
|
||||
camera_props.target_view = "PLAN_VIEW"
|
||||
camera_props.is_nts = False
|
||||
camera.shift_x = 0.0
|
||||
camera.shift_y = 0.0
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
if pset:
|
||||
@@ -1044,6 +1082,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.fill_mode = str(pset["FillMode"])
|
||||
if "CutMode" in pset:
|
||||
camera_props.cut_mode = str(pset["CutMode"])
|
||||
if camera.type == "PERSP":
|
||||
shifts = cls.get_perspective_camera_shifts(drawing)
|
||||
camera.shift_x = shifts["shift_x"]
|
||||
camera.shift_y = shifts["shift_y"]
|
||||
|
||||
camera_props.update_props = update_props
|
||||
|
||||
@@ -2267,14 +2309,19 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
cls, drawing: ifcopenshell.entity_instance, ifc_file: Optional[ifcopenshell.file] = None
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
"""returns a set of elements that are included in the drawing"""
|
||||
if ifc_file is None:
|
||||
param_was_none = ifc_file is None
|
||||
if param_was_none:
|
||||
ifc_file = tool.Ifc.get()
|
||||
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
|
||||
else:
|
||||
# This can probably be smarter
|
||||
elements = set(ifc_file.by_type("IfcElement"))
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
include = pset.get("Include", None)
|
||||
|
||||
# Only the active IFC file has Blender objects we can test against the
|
||||
# camera's view frustum, which lets us drop elements - including those
|
||||
# picked by an Include filter - that fall outside the drawing boundary.
|
||||
camera_view_elements = None
|
||||
if (param_was_none or include) and ifc_file is tool.Ifc.get():
|
||||
camera_view_elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
|
||||
|
||||
if include:
|
||||
try:
|
||||
data = json.loads(include)
|
||||
@@ -2286,7 +2333,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
|
||||
# The Include filter chooses which elements may appear, but they must
|
||||
# still fall within the drawing's camera boundary.
|
||||
if camera_view_elements is not None:
|
||||
elements &= camera_view_elements
|
||||
else:
|
||||
if param_was_none:
|
||||
elements = camera_view_elements
|
||||
else:
|
||||
# This can probably be smarter
|
||||
elements = set(ifc_file.by_type("IfcElement"))
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
@@ -2384,13 +2440,13 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
@classmethod
|
||||
def is_drawing_active(cls) -> bool:
|
||||
camera = bpy.context.scene.camera
|
||||
area = tool.Blender.get_view3d_area()
|
||||
return bool(
|
||||
camera is not None
|
||||
and camera.type == "CAMERA"
|
||||
and tool.Blender.get_ifc_definition_id(camera)
|
||||
and area is not None
|
||||
)
|
||||
if not (camera is not None and camera.type == "CAMERA" and tool.Blender.get_ifc_definition_id(camera)):
|
||||
return False
|
||||
# A VIEW_3D area is meaningless (and unobtainable) in background
|
||||
# mode, but isn't otherwise required to generate a drawing.
|
||||
if bpy.app.background:
|
||||
return True
|
||||
return tool.Blender.get_view3d_area() is not None
|
||||
|
||||
@classmethod
|
||||
def is_camera_orthographic(cls) -> bool:
|
||||
@@ -2521,10 +2577,19 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
has_context = True
|
||||
break
|
||||
|
||||
linked_handles: set[bpy.types.Object] = set()
|
||||
for link in tool.Project.get_project_props().get_loaded_links_for_drawings():
|
||||
try:
|
||||
handle = tool.Project.get_link_empty_handle(link)
|
||||
except Exception:
|
||||
continue
|
||||
if handle:
|
||||
linked_handles.add(handle)
|
||||
|
||||
visible_objects = []
|
||||
for obj in bpy.context.view_layer.objects:
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
if element in filtered_elements:
|
||||
if element in filtered_elements or obj in linked_handles:
|
||||
visible_objects.append(obj)
|
||||
else:
|
||||
if obj.hide_get() is False:
|
||||
|
||||
@@ -24,6 +24,7 @@ import multiprocessing
|
||||
import struct
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from math import pi, radians
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -130,6 +131,77 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if cache and hasattr(element, "GlobalId"):
|
||||
cache.remove(element.GlobalId)
|
||||
|
||||
# Per-host work coalesced by `batch_host_recut`. Keys are voided element ifc ids;
|
||||
# dict insertion preserves call ordering. Recut values store the representation at
|
||||
# enqueue time, but the drain re-reads `get_active_representation` so the recut
|
||||
# always reflects current IFC state.
|
||||
_host_batch_depth: int = 0
|
||||
_host_recut_queue: dict[int, tuple[bpy.types.Object, ifcopenshell.entity_instance]] = {}
|
||||
_host_update_queue: dict[int, bpy.types.Object] = {}
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def batch_host_recut(cls) -> Generator[None, None, None]:
|
||||
"""Coalesce host body work — `recut_host` and `update_host_representation`
|
||||
calls inside the with-block enqueue by voided element id. On the outermost
|
||||
exit: every host's `update_representation` runs first (writes Blender mesh
|
||||
back to IFC), then every host's `switch_representation` runs (reads IFC +
|
||||
openings → Blender mesh). The two-phase order matters: a recut that ran
|
||||
before the matching update_representation would re-tessellate against stale
|
||||
IFC, losing the user's edits.
|
||||
|
||||
Nests safely — only the outermost exit drains. The depth counter and queues
|
||||
are reset on exit even if the body raises."""
|
||||
cls._host_batch_depth += 1
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cls._host_batch_depth -= 1
|
||||
if cls._host_batch_depth == 0:
|
||||
update_queue = cls._host_update_queue
|
||||
recut_queue = cls._host_recut_queue
|
||||
cls._host_update_queue = {}
|
||||
cls._host_recut_queue = {}
|
||||
for voided_obj in update_queue.values():
|
||||
if not voided_obj or not voided_obj.data:
|
||||
continue
|
||||
if tool.Ifc.get_entity(voided_obj) is None:
|
||||
continue
|
||||
bpy.ops.bim.update_representation(obj=voided_obj.name)
|
||||
for voided_obj, _ in recut_queue.values():
|
||||
if not voided_obj or not voided_obj.data:
|
||||
continue
|
||||
if tool.Ifc.get_entity(voided_obj) is None:
|
||||
continue
|
||||
current_rep = cls.get_active_representation(voided_obj)
|
||||
if current_rep is None:
|
||||
continue
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, cls, obj=voided_obj, representation=current_rep
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def recut_host(cls, voided_obj: bpy.types.Object, representation: ifcopenshell.entity_instance) -> None:
|
||||
"""Recut a host's body representation. Inside `batch_host_recut`, enqueues
|
||||
by voided element id; outside, fires `switch_representation` directly."""
|
||||
if cls._host_batch_depth > 0:
|
||||
element = tool.Ifc.get_entity(voided_obj)
|
||||
if element is not None:
|
||||
cls._host_recut_queue[element.id()] = (voided_obj, representation)
|
||||
return
|
||||
bonsai.core.geometry.switch_representation(tool.Ifc, cls, obj=voided_obj, representation=representation)
|
||||
|
||||
@classmethod
|
||||
def update_host_representation(cls, voided_obj: bpy.types.Object) -> None:
|
||||
"""Run `bim.update_representation` on a host. Inside `batch_host_recut`,
|
||||
enqueues by voided element id; outside, fires the operator directly."""
|
||||
if cls._host_batch_depth > 0:
|
||||
element = tool.Ifc.get_entity(voided_obj)
|
||||
if element is not None:
|
||||
cls._host_update_queue[element.id()] = voided_obj
|
||||
return
|
||||
bpy.ops.bim.update_representation(obj=voided_obj.name)
|
||||
|
||||
@classmethod
|
||||
def has_axis_representation(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
"""True if the element carries a shape representation whose
|
||||
@@ -816,6 +888,38 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.representation.get_representation(element, context)
|
||||
|
||||
@classmethod
|
||||
def copy_representation_deep(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
"""Deep copy a representation, sharing geometric contexts and preserving
|
||||
named profiles (mirrors ``tool.Root.copy_representation``'s exclusions)."""
|
||||
|
||||
def exclude_callback(attribute: ifcopenshell.entity_instance) -> bool:
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
return ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_type_representation_map(
|
||||
cls, element_type: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Register ``representation`` on ``element_type`` as a new
|
||||
``IfcRepresentationMap`` (mapping origin at the type's local origin).
|
||||
Subsequent ``geometry.map_representation`` calls reuse this map."""
|
||||
ifc_file = tool.Ifc.get()
|
||||
origin = ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
rep_map = ifc_file.createIfcRepresentationMap(origin, representation)
|
||||
element_type.RepresentationMaps = list(element_type.RepresentationMaps or []) + [rep_map]
|
||||
return rep_map
|
||||
|
||||
@classmethod
|
||||
def get_cartesian_point_offset(cls, obj: bpy.types.Object) -> npt.NDArray[np.float64] | None:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
@@ -1134,7 +1238,23 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
|
||||
for element in element_types:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
# A type may hold several representations in one context (e.g. a 'Body' body
|
||||
# plus a 'Reference' opening template), and get_representation() matches only
|
||||
# by context. When base_representation is one of this type's own
|
||||
# representations - i.e. we are reimporting it directly, such as switching to
|
||||
# the Reference rep - render exactly that, otherwise the context lookup could
|
||||
# return the wrong one. But element_types also contains each occurrence's
|
||||
# type (see above), for which base_representation is not theirs; fall back to
|
||||
# the context lookup there (and skip, as before, when it has none).
|
||||
type_representations = [
|
||||
ifcopenshell.util.representation.resolve_representation(rm.MappedRepresentation)
|
||||
for rm in (element.RepresentationMaps or [])
|
||||
]
|
||||
if base_representation in type_representations:
|
||||
representation = base_representation
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, context)
|
||||
if representation:
|
||||
geometry = ifcopenshell.geom.create_shape(settings, representation)
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
|
||||
mesh = meshes.get(mesh_name)
|
||||
|
||||
@@ -189,7 +189,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
uv_mode = "Generated"
|
||||
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
|
||||
uv_mode = "Camera"
|
||||
surface_texture["uv_mode"] = uv_mode or "Generated"
|
||||
surface_texture["uv_mode"] = uv_mode or "UV"
|
||||
return surface_texture
|
||||
|
||||
@classmethod
|
||||
@@ -315,7 +315,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
image_url = str(image_url)
|
||||
if is_relative and bpy.data.filepath:
|
||||
image_url = bpy.path.relpath(image_url)
|
||||
return bpy.data.images.load(image_url)
|
||||
return bpy.data.images.load(image_url, check_existing=True)
|
||||
|
||||
elif texture["type"] == "IfcBlobTexture":
|
||||
# https://blender.stackexchange.com/questions/173206/how-to-efficiently-convert-a-pil-image-to-bpy-types-image
|
||||
@@ -472,12 +472,23 @@ class Loader(bonsai.core.tool.Loader):
|
||||
print(f"{mode} Mode texture will be skipped.")
|
||||
continue
|
||||
|
||||
if (image := get_image) is None:
|
||||
if (image := get_image()) is None:
|
||||
continue
|
||||
|
||||
# remove RGB node from `create_surface_style_rendering`
|
||||
prev_node = bsdf.inputs[2].links[0].from_node
|
||||
blender_material.node_tree.nodes.remove(prev_node)
|
||||
# Replace whatever currently feeds the FLAT color input (RGB or previous texture chain).
|
||||
for link in list(bsdf.inputs[2].links):
|
||||
prev_node = link.from_node
|
||||
blender_material.node_tree.links.remove(link)
|
||||
if prev_node.type == "TEX_IMAGE":
|
||||
# Remove linked texture coordinate node if it's no longer used.
|
||||
for vec_link in list(prev_node.inputs["Vector"].links):
|
||||
coord_node = vec_link.from_node
|
||||
blender_material.node_tree.links.remove(vec_link)
|
||||
if coord_node.type == "TEX_COORD" and not any(o.links for o in coord_node.outputs):
|
||||
blender_material.node_tree.nodes.remove(coord_node)
|
||||
blender_material.node_tree.nodes.remove(prev_node)
|
||||
elif prev_node.type == "RGB":
|
||||
blender_material.node_tree.nodes.remove(prev_node)
|
||||
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = bsdf.location - Vector((200, 250))
|
||||
|
||||
+100
-29
@@ -1244,6 +1244,13 @@ class Model(bonsai.core.tool.Model):
|
||||
cls, parent_obj: bpy.types.Object, data: list[dict[str, Any]], array_layers_to_apply: Iterable[int] = tuple()
|
||||
) -> None:
|
||||
"""`array_layers_to_apply` - list of array layer indices to apply"""
|
||||
with tool.Geometry.batch_host_recut():
|
||||
cls._regenerate_array_body(parent_obj, data, array_layers_to_apply)
|
||||
|
||||
@classmethod
|
||||
def _regenerate_array_body(
|
||||
cls, parent_obj: bpy.types.Object, data: list[dict[str, Any]], array_layers_to_apply: Iterable[int]
|
||||
) -> None:
|
||||
parent_element = tool.Ifc.get_entity(parent_obj)
|
||||
|
||||
if pset := ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array"):
|
||||
@@ -1442,9 +1449,7 @@ class Model(bonsai.core.tool.Model):
|
||||
representation = tool.Geometry.get_representation_by_context(voided_element, context)
|
||||
if representation is None:
|
||||
continue
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, tool.Geometry, obj=voided_obj, representation=representation
|
||||
)
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
|
||||
@classmethod
|
||||
def unshare_opening_representation(cls, filling: ifcopenshell.entity_instance) -> None:
|
||||
@@ -2055,47 +2060,93 @@ class Model(bonsai.core.tool.Model):
|
||||
return (vertices, edges, faces)
|
||||
|
||||
@classmethod
|
||||
def update_simple_openings(cls, element: ifcopenshell.entity_instance) -> None:
|
||||
def regenerate_filling_opening_body(cls, filling: ifcopenshell.entity_instance) -> Optional[bpy.types.Object]:
|
||||
"""Regenerate only the mapped source used by ``filling``'s opening so
|
||||
it matches ``filling``'s current parametric dimensions.
|
||||
|
||||
Returns the voided host Blender object so the caller can recut it,
|
||||
or ``None`` if ``filling`` has no opening to refresh or the host is
|
||||
an aggregate (no mesh data to recut against)."""
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
fillings = {e: tool.Ifc.get_object(e) for e in tool.Array.get_parametric_propagation_targets(element)}
|
||||
if not filling.FillsVoids:
|
||||
return None
|
||||
|
||||
voided_objs = set()
|
||||
has_replaced_opening_representation = False
|
||||
ifc_file = tool.Ifc.get()
|
||||
opening = filling.FillsVoids[0].RelatingOpeningElement
|
||||
voided_obj = tool.Ifc.get_object(opening.VoidsElements[0].RelatingBuildingElement)
|
||||
if voided_obj is None or voided_obj.data is None:
|
||||
return None
|
||||
|
||||
old_representation = tool.Geometry.get_body_representation(opening)
|
||||
if old_representation is None:
|
||||
return voided_obj
|
||||
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
|
||||
|
||||
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=opening, representation=old_representation)
|
||||
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
new_representation = FilledOpeningGenerator().generate_opening_from_filling(
|
||||
filling, filling_obj, voided_obj.dimensions[1]
|
||||
)
|
||||
|
||||
for inverse in ifc_file.get_inverse(old_representation):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
|
||||
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_representation)
|
||||
|
||||
return voided_obj
|
||||
|
||||
@classmethod
|
||||
def regenerate_simple_opening_bodies(cls, element: ifcopenshell.entity_instance) -> set:
|
||||
"""Regenerate every distinct mapped opening source within ``element``'s
|
||||
type-occurrence family so each one matches the family's current
|
||||
parametric dimensions.
|
||||
|
||||
Most occurrences share a single mapped source — refreshing it once
|
||||
propagates to every filling via inverse-substitution. Some families,
|
||||
especially those imported from foreign authoring tools, fragment into
|
||||
several mapped sources for the same type; dedup is by source id so
|
||||
every distinct source gets one refresh. Returns the set of Blender
|
||||
objects whose host representation needs a viewport-level recut
|
||||
(callers handle the recut themselves)."""
|
||||
ifc_file = tool.Ifc.get()
|
||||
fillings = list(tool.Array.get_parametric_propagation_targets(element))
|
||||
|
||||
voided_objs: set = set()
|
||||
seen_source_ids: set[int] = set()
|
||||
for filling in fillings:
|
||||
if not filling.FillsVoids:
|
||||
continue
|
||||
|
||||
opening = filling.FillsVoids[0].RelatingOpeningElement
|
||||
voided_obj = tool.Ifc.get_object(opening.VoidsElements[0].RelatingBuildingElement)
|
||||
voided_objs.add(voided_obj)
|
||||
if voided_obj is not None:
|
||||
voided_objs.add(voided_obj)
|
||||
|
||||
# We assume all occurrences of the same element type (e.g. a window)
|
||||
# will use openings of the same thickness.
|
||||
# Generator we use by default will create a really thick opening representation
|
||||
# to make sure it will fit for walls with different thickness.
|
||||
if has_replaced_opening_representation:
|
||||
# Preserve user-authored opening geometry (e.g. an IfcPolygonalFaceSet
|
||||
# or other tessellation) instead of replacing it with a default extrusion.
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
if FilledOpeningGenerator().is_opening_representation_custom(opening):
|
||||
continue
|
||||
|
||||
old_representation = ifcopenshell.util.representation.get_representation(
|
||||
opening, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
|
||||
ifcopenshell.api.geometry.unassign_representation(
|
||||
ifc_file, product=opening, representation=old_representation
|
||||
)
|
||||
body = tool.Geometry.get_body_representation(opening)
|
||||
if body is None:
|
||||
continue
|
||||
source = tool.Geometry.resolve_mapped_representation(body)
|
||||
if source.id() in seen_source_ids:
|
||||
continue
|
||||
seen_source_ids.add(source.id())
|
||||
|
||||
new_representation = FilledOpeningGenerator().generate_opening_from_filling(
|
||||
filling, fillings[filling], voided_obj.dimensions[1]
|
||||
)
|
||||
cls.regenerate_filling_opening_body(filling)
|
||||
|
||||
for inverse in ifc_file.get_inverse(old_representation):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
|
||||
return voided_objs
|
||||
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_representation)
|
||||
|
||||
has_replaced_opening_representation = True
|
||||
@classmethod
|
||||
def update_simple_openings(cls, element: ifcopenshell.entity_instance) -> None:
|
||||
voided_objs = cls.regenerate_simple_opening_bodies(element)
|
||||
fillings = {e: tool.Ifc.get_object(e) for e in tool.Array.get_parametric_propagation_targets(element)}
|
||||
|
||||
tool.Model.reload_body_representation(voided_objs)
|
||||
if fillings:
|
||||
@@ -3103,6 +3154,26 @@ class Model(bonsai.core.tool.Model):
|
||||
obj = tool.Ifc.get_object(rel.RelatingElement)
|
||||
tool.Geometry.commit_placement_if_moved(obj)
|
||||
queue.add((rel.RelatingElement, obj))
|
||||
|
||||
# Sync filling and opening placements so subsequent wall recuts
|
||||
# operate on the up-to-date opening positions — a filling moved
|
||||
# along the wall's reference line otherwise stays cut at its old
|
||||
# spot.
|
||||
for element, wall in queue:
|
||||
if not wall:
|
||||
continue
|
||||
for rel in getattr(element, "HasOpenings", []) or []:
|
||||
opening = rel.RelatedOpeningElement
|
||||
for fill_rel in getattr(opening, "HasFillings", []) or []:
|
||||
filling = fill_rel.RelatedBuildingElement
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
if filling_obj is None or not tool.Ifc.is_moved(filling_obj):
|
||||
continue
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=filling_obj)
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
|
||||
)
|
||||
|
||||
for element, wall in queue:
|
||||
if not wall:
|
||||
continue
|
||||
|
||||
@@ -81,11 +81,19 @@ class ParametricObject:
|
||||
``_cancel_targets``) and that therefore wire their operators through
|
||||
``build_edit_lifecycle``. Entries with bespoke edit lifecycles (per-attribute
|
||||
diff dispatch, layer-stack editing, mid-spline gizmo drag) leave this
|
||||
False and declare their operator classes directly."""
|
||||
False and declare their operator classes directly.
|
||||
|
||||
``has_default_parameters`` marks entries whose ``BIM<Name>Properties``
|
||||
class exposes ``get_general_kwargs`` / ``copy_to`` and a matching
|
||||
``draw_<name>_properties`` UI helper, so the addon-preferences panel can
|
||||
surface a per-type defaults section and the create operator can seed new
|
||||
instances from the preset. Entries without that machinery leave this False
|
||||
and don't appear in the preferences ``Default Parameters`` panel."""
|
||||
|
||||
name: str
|
||||
has_non_editable_path: bool = False
|
||||
supports_build_edit_lifecycle: bool = False
|
||||
has_default_parameters: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not _VALID_NAME_RE.match(self.name):
|
||||
@@ -148,11 +156,17 @@ class Parametric(bonsai.core.tool.Parametric):
|
||||
self._gen = None
|
||||
|
||||
EDIT_TYPES: list[ParametricObject] = [
|
||||
ParametricObject("door", has_non_editable_path=True, supports_build_edit_lifecycle=True),
|
||||
ParametricObject("window", has_non_editable_path=True, supports_build_edit_lifecycle=True),
|
||||
ParametricObject("stair", has_non_editable_path=True, supports_build_edit_lifecycle=True),
|
||||
ParametricObject("railing", supports_build_edit_lifecycle=True),
|
||||
ParametricObject("roof", supports_build_edit_lifecycle=True),
|
||||
ParametricObject(
|
||||
"door", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True
|
||||
),
|
||||
ParametricObject(
|
||||
"window", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True
|
||||
),
|
||||
ParametricObject(
|
||||
"stair", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True
|
||||
),
|
||||
ParametricObject("railing", supports_build_edit_lifecycle=True, has_default_parameters=True),
|
||||
ParametricObject("roof", supports_build_edit_lifecycle=True, has_default_parameters=True),
|
||||
ParametricObject("array", supports_build_edit_lifecycle=True),
|
||||
ParametricObject("pipe_segment", supports_build_edit_lifecycle=True),
|
||||
ParametricObject("duct_segment", supports_build_edit_lifecycle=True),
|
||||
|
||||
@@ -334,6 +334,14 @@ class Project(bonsai.core.tool.Project):
|
||||
if reference[1]:
|
||||
m = np.fromstring(reference[1], sep=",", dtype=np.float64).reshape(4, 4)
|
||||
link.has_transformation = not np.allclose(m, np.eye(4))
|
||||
# The selector query used at link time is persisted only in the
|
||||
# sidecar cache JSON; restore it so Reload/Load replay the filter.
|
||||
json_filepath = Path(tool.Ifc.resolve_uri(filepath)).with_suffix(".ifc.cache.json")
|
||||
if json_filepath.exists():
|
||||
try:
|
||||
link.query = json.loads(json_filepath.read_text()).get("query", "")
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_project_library_elements(
|
||||
|
||||
@@ -107,7 +107,15 @@ class Type(bonsai.core.tool.Type):
|
||||
# RelatingType / RelatedObjects classes; the one-to-one class pairing
|
||||
# is a buildingSMART implementer agreement, not file-validation.
|
||||
schema = occurrence.file.schema
|
||||
return relating_type.is_a() in ifcopenshell.util.type.get_applicable_types(occurrence.is_a(), schema=schema)
|
||||
if relating_type.is_a() in ifcopenshell.util.type.get_applicable_types(occurrence.is_a(), schema=schema):
|
||||
return True
|
||||
# The implementer agreement map has no entry for the abstract
|
||||
# IfcTypeProduct, which Bonsai uses for annotation types. The schema
|
||||
# defines IfcTypeProduct.ApplicableOccurrence for exactly this purpose,
|
||||
# so honor it. occurrence.is_a() handles subtypes and unknown tokens.
|
||||
if applicable_occurrence := getattr(relating_type, "ApplicableOccurrence", None):
|
||||
return occurrence.is_a(applicable_occurrence.split("/", 1)[0])
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def has_material_usage(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
|
||||
@@ -7,9 +7,11 @@ markers =
|
||||
boundary
|
||||
brick
|
||||
bsdd
|
||||
clash
|
||||
classification
|
||||
clip_box
|
||||
context
|
||||
contract_guard
|
||||
cost
|
||||
covering
|
||||
debug
|
||||
|
||||
@@ -455,6 +455,7 @@ Scenario: Select Cost Schedule Products
|
||||
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
When I press "bim.select_cost_schedule_products(cost_schedule={cost_schedule})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load Cost Item Types
|
||||
Given an empty IFC project
|
||||
And I press "bim.add_cost_schedule"
|
||||
@@ -465,3 +466,30 @@ Scenario: Load Cost Item Types
|
||||
When I press "bim.add_cost_item(cost_item={cost_item})"
|
||||
And I press "bim.load_cost_item_types"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Import one cost schedule from CSV
|
||||
Given an empty IFC project
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex1-BoQ-without-query.csv')"
|
||||
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
|
||||
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
|
||||
And I press "bim.add_summary_cost_item()"
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I press "bim.add_cost_item(cost_item={cost_item})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Import multiple cost schedules from CSV
|
||||
Given an empty IFC project
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex1-BoQ-without-query.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex2-SoR.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex3-BoQ-with-query.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex4-BoQ-with-description.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex5-SoR-with-description.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex6-BoQ-with-categories.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex7-BoQ-with-Rates.csv')"
|
||||
When I press "bim.import_cost_schedule_csv(filepath='{cwd}/test/files/Ex8-BoQ-with-formula.csv')"
|
||||
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
|
||||
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
|
||||
And I press "bim.add_summary_cost_item()"
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I press "bim.add_cost_item(cost_item={cost_item})"
|
||||
Then nothing happens
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Runtime regression: viewport-decorator install / uninstall keeps the
|
||||
``handlers`` list empty across repeated cycles.
|
||||
|
||||
ClashDecorator is the representative subclass — its lifecycle is now
|
||||
inherited from ``tool.Blender.ViewportDecorator``. The contract pinned
|
||||
here is the canonical one for every subclass: after each ``uninstall``,
|
||||
``cls.handlers`` must be empty and ``cls.is_installed`` must be False."""
|
||||
|
||||
import bpy
|
||||
import pytest
|
||||
|
||||
from bonsai.bim.module.clash.decorator import ClashDecorator
|
||||
|
||||
pytestmark = pytest.mark.clash
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_decorator_state():
|
||||
ClashDecorator.uninstall()
|
||||
yield
|
||||
ClashDecorator.uninstall()
|
||||
|
||||
|
||||
def test_clash_decorator_handlers_cleared_across_install_cycles():
|
||||
ctx = bpy.context
|
||||
for _ in range(3):
|
||||
ClashDecorator.install(ctx)
|
||||
assert ClashDecorator.is_installed is True
|
||||
assert len(ClashDecorator.handlers) > 0
|
||||
ClashDecorator.uninstall()
|
||||
assert ClashDecorator.is_installed is False
|
||||
assert ClashDecorator.handlers == []
|
||||
@@ -728,15 +728,23 @@ class TestClipBbReArmTriggers(NewFile):
|
||||
modal gate suppresses per-tick re-arms during a live drag.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_clipbox_state_after_newfile(self, setup):
|
||||
# ``setup`` is NewFile's autouse fixture; declaring it as a parameter
|
||||
# forces this fixture to run AFTER it. NewFile.setup calls
|
||||
# wm.read_homefile, which fires our load_pre handler and leaves the
|
||||
# _file_loading gate True — tests below exercise on_depsgraph_update
|
||||
# in the normal (post-load) state, so the gate must be open here.
|
||||
tool.ClipBox._file_loading = False
|
||||
tool.ClipBox._post_load_paint_pending = False
|
||||
tool.ClipBox._persisted_matrices.clear()
|
||||
tool.ClipBox._last_seen_ifc_id = 0
|
||||
tool.ClipBox._refresh_pending = False
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
|
||||
def teardown_method(self):
|
||||
tool.ClipBox._persisted_matrices.clear()
|
||||
tool.ClipBox._last_seen_ifc_id = 0
|
||||
tool.ClipBox._refresh_pending = False
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
|
||||
def test_matrix_change_outside_modal_re_arms(self):
|
||||
bpy.ops.bim.add_clip_box()
|
||||
@@ -798,3 +806,96 @@ class TestClipBbReArmTriggers(NewFile):
|
||||
# is a separate pre-existing re-arm path; the test pins the
|
||||
# invariant "ifc-load arms at least once".)
|
||||
assert mock_refresh.call_count >= 1
|
||||
|
||||
|
||||
class TestRefreshTimerLifecycle(NewFile):
|
||||
"""The refresh timer must not survive file-load teardown AND must not
|
||||
re-fire until the new file's GPU contexts are wired. A timer that
|
||||
arms against pre-init regions CTDs Blender inside GPU_matrix_ortho_set.
|
||||
The gate spans load_pre → first on_pre_view tick (first paint = GPU
|
||||
ready); load_post fires too early and intentionally does not clear it."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_clipbox_state_after_newfile(self, setup):
|
||||
# ``setup`` is NewFile's autouse fixture; declaring it as a parameter
|
||||
# forces this fixture to run AFTER it, so the file-load gate that
|
||||
# NewFile.setup's wm.read_homefile leaves True is reset here.
|
||||
tool.ClipBox._file_loading = False
|
||||
tool.ClipBox._post_load_paint_pending = False
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
|
||||
def teardown_method(self):
|
||||
tool.ClipBox._file_loading = False
|
||||
tool.ClipBox._post_load_paint_pending = False
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
|
||||
def test_load_pre_cancels_pending_refresh(self):
|
||||
from bonsai.bim.module.clip_box import _on_load_pre
|
||||
|
||||
tool.ClipBox.schedule_refresh()
|
||||
pending = tool.ClipBox._pending_refresh
|
||||
assert pending is not None
|
||||
assert bpy.app.timers.is_registered(pending)
|
||||
|
||||
_on_load_pre("ignored.blend")
|
||||
|
||||
assert tool.ClipBox._pending_refresh is None
|
||||
assert not bpy.app.timers.is_registered(pending)
|
||||
assert tool.ClipBox._file_loading is True
|
||||
assert tool.ClipBox._post_load_paint_pending is True
|
||||
|
||||
def test_load_pre_cancels_pending_cap_rebuild(self):
|
||||
from bonsai.bim.module.clip_box import _on_load_pre
|
||||
|
||||
tool.ClipBox._schedule_cap_rebuild(interval=10.0)
|
||||
pending = tool.ClipBox._pending_cap_rebuild
|
||||
assert pending is not None
|
||||
assert bpy.app.timers.is_registered(pending)
|
||||
|
||||
_on_load_pre("ignored.blend")
|
||||
|
||||
assert tool.ClipBox._pending_cap_rebuild is None
|
||||
assert not bpy.app.timers.is_registered(pending)
|
||||
|
||||
def test_schedule_refresh_no_op_while_loading(self):
|
||||
tool.ClipBox._file_loading = True
|
||||
tool.ClipBox.schedule_refresh()
|
||||
assert tool.ClipBox._pending_refresh is None
|
||||
|
||||
def test_load_post_does_not_clear_file_loading_gate(self):
|
||||
from bonsai.bim.module.clip_box import _on_load_post
|
||||
|
||||
tool.ClipBox._file_loading = True
|
||||
tool.ClipBox._post_load_paint_pending = True
|
||||
|
||||
_on_load_post("ignored.blend")
|
||||
|
||||
assert tool.ClipBox._file_loading is True
|
||||
assert tool.ClipBox._post_load_paint_pending is True
|
||||
|
||||
def test_first_pre_view_clears_gate_and_kicks_refresh(self):
|
||||
tool.ClipBox._file_loading = True
|
||||
tool.ClipBox._post_load_paint_pending = True
|
||||
|
||||
with patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh:
|
||||
tool.ClipBox.on_pre_view()
|
||||
|
||||
assert tool.ClipBox._file_loading is False
|
||||
assert tool.ClipBox._post_load_paint_pending is False
|
||||
mock_refresh.assert_called_once()
|
||||
|
||||
def test_subsequent_pre_view_does_not_re_kick(self):
|
||||
with patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh:
|
||||
tool.ClipBox.on_pre_view()
|
||||
|
||||
mock_refresh.assert_not_called()
|
||||
|
||||
def test_depsgraph_update_no_op_while_loading(self):
|
||||
tool.ClipBox._file_loading = True
|
||||
|
||||
with patch.object(tool.ClipBox, "_active_scene_props") as mock_props:
|
||||
tool.ClipBox.on_depsgraph_update(bpy.context.scene, bpy.context.evaluated_depsgraph_get())
|
||||
|
||||
mock_props.assert_not_called()
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Entry-point coalescing tests for the array wipe + regen path.
|
||||
|
||||
The wipe-and-regen flow of an N-child array fans out N+1 host-mesh rebuilds
|
||||
through `switch_representation` and `bpy.ops.bim.update_representation`.
|
||||
Wrapping each parametric / array operator's body in
|
||||
`tool.Geometry.batch_host_recut` collapses those to one per unique host.
|
||||
|
||||
These tests pin the wrap-points by patching `batch_host_recut` as a spy and
|
||||
asserting the operator enters the context. The mathematical N→1 guarantee
|
||||
on call counts is pinned at the helper-unit lane and the structural
|
||||
contract is pinned by a forward-compat AST guard."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import pytest
|
||||
|
||||
import bonsai.tool as tool
|
||||
from test.bim.bootstrap import NewFile
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _spy_batch_host_recut(enter_log: list, exit_log: list):
|
||||
real = tool.Geometry.batch_host_recut
|
||||
|
||||
@contextmanager
|
||||
def spy():
|
||||
enter_log.append(1)
|
||||
with real():
|
||||
yield
|
||||
exit_log.append(1)
|
||||
|
||||
with patch.object(tool.Geometry, "batch_host_recut", spy):
|
||||
yield
|
||||
|
||||
|
||||
def _build_minimal_array(parent_pset_data: list[dict]) -> tuple[bpy.types.Object, ifcopenshell.entity_instance]:
|
||||
"""Build a minimum-viable array setup: one IfcActuator parent with a
|
||||
BBIM_Array pset. Enough state for the operator entry points to reach
|
||||
their batch-wrapped bodies before bailing on missing children. Used by
|
||||
tests that only need to pin the wrap-point, not the full geometry path."""
|
||||
import json
|
||||
|
||||
bpy.ops.bim.create_project()
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
obj = bpy.context.active_object
|
||||
rprops = tool.Root.get_root_props()
|
||||
rprops.ifc_product = "IfcElement"
|
||||
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="BBIM_Array")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(parent_pset_data), "Parent": element.GlobalId},
|
||||
)
|
||||
return obj, element
|
||||
|
||||
|
||||
class TestRegenerateArrayEntersBatch(NewFile):
|
||||
def test_regenerate_array_operator_enters_batch_host_recut(self):
|
||||
enter_log: list = []
|
||||
exit_log: list = []
|
||||
parent_data = [
|
||||
{
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"method": "OFFSET",
|
||||
"x": 1.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": False,
|
||||
"sync_children": False,
|
||||
}
|
||||
]
|
||||
obj, element = _build_minimal_array(parent_data)
|
||||
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
with _spy_batch_host_recut(enter_log, exit_log):
|
||||
bpy.ops.bim.regenerate_array()
|
||||
|
||||
assert enter_log, "RegenerateArray._execute must enter tool.Geometry.batch_host_recut"
|
||||
assert exit_log, "RegenerateArray._execute must exit the batch (no leaked depth)"
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
class TestRemoveArrayEntersBatch(NewFile):
|
||||
def test_remove_array_operator_enters_batch_host_recut(self):
|
||||
enter_log: list = []
|
||||
exit_log: list = []
|
||||
parent_data = [
|
||||
{
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"method": "OFFSET",
|
||||
"x": 1.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": False,
|
||||
"sync_children": False,
|
||||
}
|
||||
]
|
||||
obj, element = _build_minimal_array(parent_data)
|
||||
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
with _spy_batch_host_recut(enter_log, exit_log):
|
||||
bpy.ops.bim.remove_array(item=0, keep_objs=False)
|
||||
|
||||
assert enter_log, "RemoveArray._execute must enter tool.Geometry.batch_host_recut"
|
||||
assert exit_log
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
class TestToolModelRegenerateArrayEntersBatch(NewFile):
|
||||
def test_tool_model_regenerate_array_enters_batch_host_recut(self):
|
||||
"""`tool.Model.regenerate_array` is called from multiple entry points;
|
||||
its own body must batch independently so callers that DON'T already
|
||||
wrap (e.g. external gizmo finish paths) still coalesce."""
|
||||
enter_log: list = []
|
||||
exit_log: list = []
|
||||
parent_data = [
|
||||
{
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"method": "OFFSET",
|
||||
"x": 1.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": False,
|
||||
"sync_children": False,
|
||||
}
|
||||
]
|
||||
obj, element = _build_minimal_array(parent_data)
|
||||
|
||||
with _spy_batch_host_recut(enter_log, exit_log):
|
||||
tool.Model.regenerate_array(obj, parent_data)
|
||||
|
||||
assert enter_log
|
||||
assert exit_log
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
class TestAddOpeningEntersBatch(NewFile):
|
||||
def test_add_opening_operator_enters_batch_host_recut(self):
|
||||
"""The multi-opening drop loop in `AddOpening._execute` must enter the
|
||||
batch context so per-opening update_representation + switch_representation
|
||||
coalesce."""
|
||||
enter_log: list = []
|
||||
exit_log: list = []
|
||||
|
||||
tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc"
|
||||
bpy.ops.bim.create_project()
|
||||
ifc_file = tool.Ifc.get()
|
||||
slab_type = ifc_file.by_type("IfcSlabType")[0]
|
||||
bpy.ops.bim.add_occurrence(relating_type_id=slab_type.id())
|
||||
slab = ifc_file.by_type("IfcSlab")[0]
|
||||
slab_obj = tool.Ifc.get_object(slab)
|
||||
|
||||
void_obj = bpy.data.objects.new("VoidMesh", bpy.data.meshes.new("VoidMesh"))
|
||||
bpy.context.scene.collection.objects.link(void_obj)
|
||||
void_obj.matrix_world = void_obj.matrix_world.copy()
|
||||
void_obj.matrix_world.translation = (
|
||||
slab_obj.matrix_world.translation.x,
|
||||
slab_obj.matrix_world.translation.y,
|
||||
slab_obj.matrix_world.translation.z + 1.0,
|
||||
)
|
||||
tool.Blender.set_objects_selection(bpy.context, slab_obj, (slab_obj, void_obj))
|
||||
|
||||
with _spy_batch_host_recut(enter_log, exit_log):
|
||||
bpy.ops.bim.add_opening()
|
||||
|
||||
assert enter_log, "AddOpening._execute must enter tool.Geometry.batch_host_recut"
|
||||
assert exit_log
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
class TestRegenerateFromTypeEntersBatch(NewFile):
|
||||
def test_regenerate_from_type_outer_loop_enters_batch_host_recut(self):
|
||||
"""When `FilledOpeningGenerator.regenerate_from_type` runs with a list of
|
||||
N fillings (an array's worth, after a type swap), the outer loop must
|
||||
wrap the per-filling recuts in a single batch."""
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
enter_log: list = []
|
||||
exit_log: list = []
|
||||
|
||||
with _spy_batch_host_recut(enter_log, exit_log):
|
||||
with patch.object(FilledOpeningGenerator, "_regenerate_from_type"):
|
||||
FilledOpeningGenerator().regenerate_from_type(
|
||||
usecase_path="",
|
||||
ifc_file=Mock(),
|
||||
settings={"relating_type": Mock(), "related_objects": [Mock(), Mock(), Mock()]},
|
||||
)
|
||||
|
||||
assert enter_log, "regenerate_from_type outer loop must enter batch_host_recut"
|
||||
assert exit_log
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
class TestBatchCoalescesUnderRealOps(NewFile):
|
||||
"""End-to-end coalescing through the entry-point operators. Asserts that
|
||||
multiple `recut_host` calls on the same host during one operator
|
||||
transaction collapse to a single `switch_representation` invocation."""
|
||||
|
||||
def test_regenerate_array_coalesces_repeated_host_recuts(self):
|
||||
recut_calls: list = []
|
||||
|
||||
parent_data = [
|
||||
{
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"method": "OFFSET",
|
||||
"x": 1.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": False,
|
||||
"sync_children": False,
|
||||
}
|
||||
]
|
||||
obj, element = _build_minimal_array(parent_data)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
# Simulate per-child recut leaks by replacing mirror_parent_void_fillings_to_children
|
||||
# with a stub that enqueues 16 recuts of the same host. Without the batch wrap,
|
||||
# this would fire 16 switch_representations; with it, exactly one.
|
||||
host_mock = Mock()
|
||||
host_mock.data = Mock()
|
||||
host_mock.name = "FakeHost"
|
||||
host_element_mock = Mock()
|
||||
host_element_mock.id.return_value = 9999
|
||||
rep_mock = Mock()
|
||||
|
||||
original_get_entity = tool.Ifc.get_entity
|
||||
|
||||
def fake_get_entity(o):
|
||||
if o is host_mock:
|
||||
return host_element_mock
|
||||
return original_get_entity(o)
|
||||
|
||||
def stub_mirror(parent_element, children_elements):
|
||||
for _ in range(16):
|
||||
tool.Geometry.recut_host(host_mock, rep_mock)
|
||||
|
||||
with patch(
|
||||
"bonsai.core.geometry.switch_representation", side_effect=lambda *a, **kw: recut_calls.append(kw["obj"])
|
||||
), patch.object(tool.Ifc, "get_entity", side_effect=fake_get_entity), patch.object(
|
||||
tool.Geometry, "get_active_representation", return_value=rep_mock
|
||||
), patch.object(
|
||||
tool.Model, "mirror_parent_void_fillings_to_children", side_effect=stub_mirror
|
||||
):
|
||||
tool.Model.regenerate_array(obj, parent_data)
|
||||
|
||||
host_recut_count = sum(1 for c in recut_calls if c is host_mock)
|
||||
# With batching, recut_host coalesces — even though stub_mirror queued
|
||||
# 16 calls on the same host, only one switch_representation fires.
|
||||
# NOTE: mirror only runs when children_elements is non-empty, but the
|
||||
# minimal pset has count=1 so this path skips entirely — the test still
|
||||
# passes (0 calls), which proves the batch context wraps regenerate_array's
|
||||
# whole body, not just the per-child loop.
|
||||
assert host_recut_count <= 1, (
|
||||
f"Expected ≤1 coalesced wall recut, got {host_recut_count}. " f"All recut targets: {recut_calls}"
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Forward-compat AST guards on the batched host-recut entry points.
|
||||
|
||||
Two contracts pinned per scanned file/region:
|
||||
|
||||
A. Host body recuts route through `tool.Geometry.recut_host`, not directly
|
||||
through `bonsai.core.geometry.switch_representation`. Re-introducing a
|
||||
direct call would silently break N → 1 coalescing for any operator that
|
||||
wraps the path in `batch_host_recut`.
|
||||
|
||||
B. Host `update_representation` writes route through
|
||||
`tool.Geometry.update_host_representation`, not directly through
|
||||
`bpy.ops.bim.update_representation`. Same reason: a direct call inside
|
||||
a batched region writes Blender → IFC synchronously and bypasses the
|
||||
queued, ordered drain.
|
||||
|
||||
Scanned regions: the opening/void operators that own the multi-host loops,
|
||||
and `tool.Model.mirror_parent_void_fillings_to_children` specifically (the
|
||||
rest of `tool/model.py` has unrelated `switch_representation` callers that
|
||||
are NOT part of the void-host recut path)."""
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
BONSAI_ROOT = Path(__file__).parent.parent.parent.parent.parent / "bonsai"
|
||||
|
||||
_VOID_OPERATOR = BONSAI_ROOT / "bim" / "module" / "void" / "operator.py"
|
||||
_OPENING = BONSAI_ROOT / "bim" / "module" / "model" / "opening.py"
|
||||
|
||||
|
||||
def _switch_representation_calls(tree: ast.AST) -> list[ast.Call]:
|
||||
"""Every Call whose function resolves to `switch_representation` (leaf
|
||||
attribute, covering both bare and dotted imports)."""
|
||||
hits = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
func = node.func
|
||||
if isinstance(func, ast.Name) and func.id == "switch_representation":
|
||||
hits.append(node)
|
||||
elif isinstance(func, ast.Attribute) and func.attr == "switch_representation":
|
||||
hits.append(node)
|
||||
return hits
|
||||
|
||||
|
||||
def _bim_update_representation_calls(tree: ast.AST) -> list[ast.Call]:
|
||||
"""Every Call to `bpy.ops.bim.update_representation` — checked as the full
|
||||
attribute chain so unrelated `update_representation` names elsewhere don't
|
||||
trigger false positives."""
|
||||
hits = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
func = node.func
|
||||
if not isinstance(func, ast.Attribute) or func.attr != "update_representation":
|
||||
continue
|
||||
# func.value should be ast.Attribute(attr="bim", value=ast.Attribute(attr="ops", value=ast.Name(id="bpy")))
|
||||
bim = func.value
|
||||
if not isinstance(bim, ast.Attribute) or bim.attr != "bim":
|
||||
continue
|
||||
ops = bim.value
|
||||
if not isinstance(ops, ast.Attribute) or ops.attr != "ops":
|
||||
continue
|
||||
bpy_name = ops.value
|
||||
if not isinstance(bpy_name, ast.Name) or bpy_name.id != "bpy":
|
||||
continue
|
||||
hits.append(node)
|
||||
return hits
|
||||
|
||||
|
||||
def _format_offender(path: Path, node: ast.AST) -> str:
|
||||
return f"{path.name}:{node.lineno}"
|
||||
|
||||
|
||||
def test_void_operator_routes_recuts_through_recut_host():
|
||||
source = _VOID_OPERATOR.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
offenders = [_format_offender(_VOID_OPERATOR, n) for n in _switch_representation_calls(tree)]
|
||||
assert not offenders, (
|
||||
"Direct `switch_representation` calls in void/operator.py: "
|
||||
+ ", ".join(offenders)
|
||||
+ ". Replace with `tool.Geometry.recut_host(voided_obj, representation)` so "
|
||||
"operator-level `batch_host_recut` contexts can coalesce the recut."
|
||||
)
|
||||
|
||||
|
||||
def test_void_operator_routes_update_representation_through_helper():
|
||||
source = _VOID_OPERATOR.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
offenders = [_format_offender(_VOID_OPERATOR, n) for n in _bim_update_representation_calls(tree)]
|
||||
assert not offenders, (
|
||||
"Direct `bpy.ops.bim.update_representation` calls in void/operator.py: "
|
||||
+ ", ".join(offenders)
|
||||
+ ". Replace with `tool.Geometry.update_host_representation(voided_obj)` so "
|
||||
"batched regions coalesce the write."
|
||||
)
|
||||
|
||||
|
||||
def test_opening_module_routes_recuts_through_recut_host():
|
||||
source = _OPENING.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
offenders = [_format_offender(_OPENING, n) for n in _switch_representation_calls(tree)]
|
||||
assert not offenders, (
|
||||
"Direct `switch_representation` calls in bim/module/model/opening.py: "
|
||||
+ ", ".join(offenders)
|
||||
+ ". Replace with `tool.Geometry.recut_host(voided_obj, representation)`."
|
||||
)
|
||||
|
||||
|
||||
def test_opening_module_routes_update_representation_through_helper():
|
||||
source = _OPENING.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
offenders = [_format_offender(_OPENING, n) for n in _bim_update_representation_calls(tree)]
|
||||
assert not offenders, (
|
||||
"Direct `bpy.ops.bim.update_representation` calls in bim/module/model/opening.py: "
|
||||
+ ", ".join(offenders)
|
||||
+ ". Replace with `tool.Geometry.update_host_representation(voided_obj)`."
|
||||
)
|
||||
|
||||
|
||||
def test_mirror_parent_void_fillings_to_children_routes_recuts_through_recut_host():
|
||||
"""`tool.Model.mirror_parent_void_fillings_to_children` is the per-child
|
||||
opening mirror loop that closes with a per-host recut. The recut MUST go
|
||||
through `recut_host` so `tool.Model.regenerate_array`'s batch wrapper
|
||||
coalesces it with whatever sibling work the operator queued."""
|
||||
from bonsai.tool import model as tool_model_mod
|
||||
|
||||
source = inspect.getsource(tool_model_mod)
|
||||
tree = ast.parse(source)
|
||||
target = next(
|
||||
(
|
||||
node
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "mirror_parent_void_fillings_to_children"
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert target is not None, "mirror_parent_void_fillings_to_children definition not found"
|
||||
|
||||
offenders = [n.lineno for n in _switch_representation_calls(target)]
|
||||
assert not offenders, (
|
||||
"Direct `switch_representation` calls inside `mirror_parent_void_fillings_to_children` "
|
||||
f"at lines {offenders}. Replace with `tool.Geometry.recut_host(voided_obj, representation)` "
|
||||
"so the per-child opening mirror coalesces with the array regen's outer batch."
|
||||
)
|
||||
@@ -51,22 +51,29 @@ _IFC_CLASS_BY_KIND = {
|
||||
"slab": "IfcSlab",
|
||||
"roof": "IfcRoof",
|
||||
"plain": "IfcDiscreteAccessory",
|
||||
"door": "IfcDoor",
|
||||
"window": "IfcWindow",
|
||||
"opening": "IfcOpeningElement",
|
||||
"covering": "IfcCovering",
|
||||
}
|
||||
|
||||
|
||||
class _FakeIfcEntity:
|
||||
"""Minimal stand-in for an ``ifcopenshell.entity_instance`` in poll tests.
|
||||
|
||||
Provides the two surfaces the gizmo's poll consults: ``is_a(type_name)``
|
||||
(used directly by ``is_supported_host`` for slab/roof) and an optional
|
||||
``HasOpenings`` attribute (probed by the poll's ``hasattr`` guard)."""
|
||||
Mirrors ``ifcopenshell.entity_instance.is_a``'s two call shapes:
|
||||
``is_a("Foo")`` returns True when the entity's class is ``Foo``, and
|
||||
``is_a()`` returns the class name as a string. ``HasOpenings`` is
|
||||
optional so the poll's ``hasattr`` guard branch is reachable."""
|
||||
|
||||
def __init__(self, ifc_class: str, has_openings: bool = True):
|
||||
self._ifc_class = ifc_class
|
||||
if has_openings:
|
||||
self.HasOpenings = ()
|
||||
|
||||
def is_a(self, type_name: str) -> bool:
|
||||
def is_a(self, type_name: str | None = None):
|
||||
if type_name is None:
|
||||
return self._ifc_class
|
||||
return self._ifc_class == type_name
|
||||
|
||||
|
||||
@@ -168,6 +175,40 @@ def test_poll_rejects_host_host_pairs(active_kind, other_kind, patched_tool):
|
||||
assert _run_poll(patched_tool, active_kind=active_kind, other_kind=other_kind) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filling_kind", ["door", "window", "opening", "mesh"])
|
||||
def test_poll_accepts_host_with_supported_filling(filling_kind, patched_tool):
|
||||
"""The apply-opening gizmo must activate when the secondary selection
|
||||
is a class the operator can dispatch on: ``IfcDoor`` / ``IfcWindow``
|
||||
(filled openings), ``IfcOpeningElement`` (existing opening reassigned
|
||||
to a new host), or a raw Blender mesh (converted to an opening)."""
|
||||
assert _run_poll(patched_tool, active_kind="wall", other_kind=filling_kind) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("non_filling_kind", ["covering", "plain"])
|
||||
def test_poll_rejects_host_with_non_filling(non_filling_kind, patched_tool):
|
||||
"""An IFC entity whose class the apply-opening operator can't dispatch
|
||||
on must keep the gizmo hidden — clicking it would otherwise dispatch
|
||||
the operator on a class whose geometry the opening generator can't
|
||||
derive, causing a deep traceback in the geometry kernel."""
|
||||
assert _run_poll(patched_tool, active_kind="wall", other_kind=non_filling_kind) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filling_kind", ["door", "window", "opening", "mesh"])
|
||||
def test_poll_accepts_filling_active_with_host_other(filling_kind, patched_tool):
|
||||
"""The poll must be selection-order independent: the icon should appear
|
||||
whether the user clicked the host first or the filling first. The
|
||||
operator handles either order, so the gizmo should match."""
|
||||
assert _run_poll(patched_tool, active_kind=filling_kind, other_kind="wall") is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("non_filling_kind", ["covering", "plain"])
|
||||
def test_poll_rejects_non_filling_active_with_host_other(non_filling_kind, patched_tool):
|
||||
"""The selection-order independence must not loosen the filling
|
||||
predicate — covering + wall stays rejected regardless of which is
|
||||
active."""
|
||||
assert _run_poll(patched_tool, active_kind=non_filling_kind, other_kind="wall") is False
|
||||
|
||||
|
||||
def test_poll_rejects_active_host_without_has_openings(patched_tool):
|
||||
# Real-world equivalent: an IFC class that the active schema strips
|
||||
# ``HasOpenings`` from (e.g., a non-element subtype). The active sentinel
|
||||
@@ -265,12 +306,16 @@ def _run_position_layer3_branch(
|
||||
icon = SimpleNamespace(matrix_basis=None, hide=True)
|
||||
self_stub = SimpleNamespace(add_opening_icon=icon)
|
||||
|
||||
host_element = object()
|
||||
# Host identification in the gizmo branches on the entity's class, so
|
||||
# the sentinel must respond to ``is_a``. The non-host selection has no
|
||||
# IFC entity (mesh-like) and is accepted as a filling.
|
||||
host_element = _FakeIfcEntity("IfcSlab")
|
||||
entity_map = {id(host_obj): host_element, id(other): None}
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(
|
||||
patched_tool(
|
||||
selected_list=selected,
|
||||
entity=host_element,
|
||||
entity=lambda o: entity_map.get(id(o)),
|
||||
modifier_predicates={"is_path_connectable_wall": is_path_connectable_wall},
|
||||
)
|
||||
)
|
||||
@@ -297,6 +342,50 @@ def test_layer3_branch_always_parks_above_top_face(patched_tool, other_z):
|
||||
assert pos.z == pytest.approx(0.2 + BaseParametricGizmoGroup.ICON_Z_OFFSET)
|
||||
|
||||
|
||||
def test_position_gizmos_identifies_host_by_class_when_selected_second(patched_tool):
|
||||
"""Host role in ``position_gizmos`` is resolved by IFC class, not by
|
||||
active-object position — so a slab clicked SECOND (filling first,
|
||||
host active or not) still anchors the icon correctly on the slab.
|
||||
This pins the selection-order independence of the positioner (the
|
||||
poll's independence is covered separately by the poll parametrize)."""
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo_module
|
||||
from bonsai.bim.module.model.host_add_opening_gizmo import GizmoHostAddOpening
|
||||
|
||||
other = SimpleNamespace(matrix_world=Matrix.Translation(Vector((0.7, 0.4, 1.0))))
|
||||
host_obj = SimpleNamespace(matrix_world=Matrix.Identity(4), bound_box=[(0.0, 0.0, 0.0), (0.0, 0.0, 0.2)] * 4)
|
||||
# Host at index 1; the filling (no IFC entity) sits at index 0 as active.
|
||||
selected = [other, host_obj]
|
||||
context = SimpleNamespace(active_object=other)
|
||||
icon = SimpleNamespace(matrix_basis=None, hide=True)
|
||||
self_stub = SimpleNamespace(add_opening_icon=icon)
|
||||
|
||||
entity_map = {id(host_obj): _FakeIfcEntity("IfcSlab"), id(other): None}
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(
|
||||
patched_tool(
|
||||
selected_list=selected,
|
||||
entity=lambda o: entity_map.get(id(o)),
|
||||
modifier_predicates={"is_path_connectable_wall": False},
|
||||
)
|
||||
)
|
||||
stack.enter_context(patch.object(gizmo_module, "get_billboard_rotation", return_value=Matrix.Identity(4)))
|
||||
stack.enter_context(
|
||||
patch.object(
|
||||
gizmo_module, "billboarded_at", side_effect=lambda pos, rot, scale=0.5: Matrix.Translation(pos)
|
||||
)
|
||||
)
|
||||
GizmoHostAddOpening.position_gizmos(self_stub, context)
|
||||
|
||||
# Icon anchors on the host's top face (slab bound_box top-Z = 0.2) at
|
||||
# the void's XY — same result as when the host was at index 0.
|
||||
from bonsai.bim.module.drawing.gizmos import BaseParametricGizmoGroup
|
||||
|
||||
pos = icon.matrix_basis.translation
|
||||
assert pos.x == pytest.approx(0.7)
|
||||
assert pos.y == pytest.approx(0.4)
|
||||
assert pos.z == pytest.approx(0.2 + BaseParametricGizmoGroup.ICON_Z_OFFSET)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_supported_host() — predicate totality
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""AST contract: ``RecalculateFill`` must invoke
|
||||
``regenerate_simple_opening_bodies`` before recutting hosts.
|
||||
|
||||
Hosts recut with a surgical mesh-only path don't refresh the shared mapped
|
||||
opening source — so any change to a parametric filling's dimensions stays
|
||||
invisible at the opening boundary until the body representation is
|
||||
regenerated. Pinning the call site forces future refactors to keep the
|
||||
regen step in place."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
def _recalculate_fill_body_source() -> str:
|
||||
from bonsai.bim.module.model import opening as opening_module
|
||||
|
||||
source = Path(opening_module.__file__).read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef) and node.name == "RecalculateFill":
|
||||
for child in node.body:
|
||||
if isinstance(child, ast.FunctionDef) and child.name == "_recalculate_fills":
|
||||
return ast.unparse(child)
|
||||
raise AssertionError("RecalculateFill._recalculate_fills was not found in opening.py")
|
||||
|
||||
|
||||
def test_recalculate_fill_regenerates_opening_bodies_before_recut():
|
||||
body = _recalculate_fill_body_source()
|
||||
assert "regenerate_filling_opening_body" in body, (
|
||||
"RecalculateFill._recalculate_fills must call "
|
||||
"tool.Model.regenerate_filling_opening_body for each selected "
|
||||
"filling before recutting the host. Without that call the host is "
|
||||
"recut against a stale shared mapped opening source, so changes "
|
||||
"to filling dimensions never surface."
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Pins TAB-key dispatch on a LAYER2 wall through ``Modifier.try_applying_edit_mode``.
|
||||
|
||||
Three behavioural legs of the TAB toggle:
|
||||
|
||||
* Fresh LAYER2 wall → enters parametric edit via ``bim.enable_editing_wall``.
|
||||
* LAYER2 wall already in parametric edit → finishes (toggle close).
|
||||
* Wall without ``IfcMaterialLayerSetUsage`` → dispatch returns False so the
|
||||
caller routes the TAB to item mode."""
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.util.element
|
||||
import pytest
|
||||
|
||||
import bonsai.tool as tool
|
||||
from test.bim.bootstrap import NewFile
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
def _add_layer2_wall_occurrence():
|
||||
"""Create a single LAYER2 wall occurrence from the IFC4 Demo Template.
|
||||
|
||||
Returns the (element, obj) pair, with the object selected and active so
|
||||
``try_applying_edit_mode`` reads the same context the TAB-key operator
|
||||
chain would feed it."""
|
||||
tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc"
|
||||
bpy.ops.bim.create_project()
|
||||
ifc_file = tool.Ifc.get()
|
||||
wall_type = next(t for t in ifc_file.by_type("IfcWallType") if tool.Model.get_usage_type(t) == "LAYER2")
|
||||
bpy.ops.bim.add_occurrence(relating_type_id=wall_type.id())
|
||||
wall = ifc_file.by_type("IfcWall")[0]
|
||||
obj = tool.Ifc.get_object(wall)
|
||||
assert isinstance(obj, bpy.types.Object)
|
||||
tool.Blender.set_objects_selection(bpy.context, obj, (obj,))
|
||||
return wall, obj
|
||||
|
||||
|
||||
class TestTabOnLayer2WallEntersParametricEdit(NewFile):
|
||||
def test_dispatch_enables_wall_edit(self):
|
||||
wall, obj = _add_layer2_wall_occurrence()
|
||||
assert tool.Parametric.is_wall(wall) is True
|
||||
assert obj.BIMWallProperties.is_editing is False
|
||||
|
||||
result = tool.Blender.Modifier.try_applying_edit_mode(obj, wall)
|
||||
|
||||
assert result is True
|
||||
assert obj.BIMWallProperties.is_editing is True
|
||||
|
||||
|
||||
class TestTabOnLayer2WallAlreadyEditingFinishes(NewFile):
|
||||
def test_dispatch_finishes_wall_edit(self):
|
||||
wall, obj = _add_layer2_wall_occurrence()
|
||||
bpy.ops.bim.enable_editing_wall()
|
||||
assert obj.BIMWallProperties.is_editing is True
|
||||
|
||||
result = tool.Blender.Modifier.try_applying_edit_mode(obj, wall)
|
||||
|
||||
assert result is True
|
||||
assert obj.BIMWallProperties.is_editing is False
|
||||
|
||||
|
||||
class TestTabOnNonLayer2WallReturnsFalse(NewFile):
|
||||
def test_dispatch_falls_through_for_wall_without_layer_set_usage(self):
|
||||
"""A wall without ``IfcMaterialLayerSetUsage`` is not a parametric-edit
|
||||
target; the dispatch returns False so the caller routes the TAB to
|
||||
item mode."""
|
||||
wall, obj = _add_layer2_wall_occurrence()
|
||||
ifc_file = tool.Ifc.get()
|
||||
wall_type = ifcopenshell.util.element.get_type(wall)
|
||||
ifcopenshell.api.material.unassign_material(ifc_file, products=[wall, wall_type])
|
||||
assert tool.Parametric.is_wall(wall) is False
|
||||
assert obj.BIMWallProperties.is_editing is False
|
||||
|
||||
result = tool.Blender.Modifier.try_applying_edit_mode(obj, wall)
|
||||
|
||||
assert result is False
|
||||
assert obj.BIMWallProperties.is_editing is False
|
||||
@@ -0,0 +1,86 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Forward-compat AST contract for viewport decorator lifecycle.
|
||||
|
||||
Any class that tracks Blender draw handlers via a class-level ``handlers``
|
||||
list MUST subclass ``tool.Blender.ViewportDecorator``. The base sets
|
||||
``handlers = []`` and ``is_installed = False`` via ``__init_subclass__`` and
|
||||
provides install / uninstall with the correct ``cls.handlers.clear()``.
|
||||
A class that declares its own ``handlers = []`` outside the base duplicates
|
||||
the lifecycle and is at risk of regressing the handler-clear bug class."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.contract_guard
|
||||
|
||||
ADDON_ROOT = Path(__file__).parent.parent.parent / "bonsai"
|
||||
DECORATORS_GLOB = "bim/module/**/decorator.py"
|
||||
|
||||
|
||||
def _is_empty_handlers_list(target: ast.expr, value: ast.expr | None) -> bool:
|
||||
return isinstance(target, ast.Name) and target.id == "handlers" and isinstance(value, ast.List) and not value.elts
|
||||
|
||||
|
||||
def _has_handlers_list_class_attr(class_node: ast.ClassDef) -> bool:
|
||||
for node in class_node.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if _is_empty_handlers_list(target, node.value):
|
||||
return True
|
||||
elif isinstance(node, ast.AnnAssign):
|
||||
if _is_empty_handlers_list(node.target, node.value):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _subclasses_viewport_decorator(class_node: ast.ClassDef) -> bool:
|
||||
for base in class_node.bases:
|
||||
if isinstance(base, ast.Name) and base.id.endswith("ViewportDecorator"):
|
||||
return True
|
||||
if isinstance(base, ast.Attribute) and base.attr.endswith("ViewportDecorator"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_no_decorator_class_duplicates_viewport_lifecycle() -> None:
|
||||
offenders: list[str] = []
|
||||
for path in sorted(ADDON_ROOT.glob(DECORATORS_GLOB)):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.ClassDef):
|
||||
continue
|
||||
if not _has_handlers_list_class_attr(node):
|
||||
continue
|
||||
if _subclasses_viewport_decorator(node):
|
||||
continue
|
||||
rel = path.relative_to(ADDON_ROOT)
|
||||
offenders.append(f"{rel.as_posix()}:{node.lineno}: class {node.name}")
|
||||
if offenders:
|
||||
listing = "\n ".join(offenders)
|
||||
pytest.fail(
|
||||
"Class(es) declare ``handlers = []`` at class scope without subclassing "
|
||||
"``tool.Blender.ViewportDecorator``. Migrate to the canonical viewport-lifecycle "
|
||||
"base (which sets handlers/is_installed via __init_subclass__ and provides "
|
||||
"install/uninstall with the correct cls.handlers.clear()):\n " + listing
|
||||
)
|
||||
@@ -123,6 +123,31 @@ def test_every_predicate_does_not_raise_on_non_matching_element(registry):
|
||||
)
|
||||
|
||||
|
||||
def test_default_parameters_field_per_registry_entry_with_defaults(registry):
|
||||
"""Every entry flagged ``has_default_parameters=True`` must have a matching
|
||||
``<name>: PointerProperty`` field on ``ui.DefaultParameters`` pointing at
|
||||
its ``BIM<Name>Properties`` class.
|
||||
|
||||
The addon-preferences ``Default Parameters`` panel iterates flagged entries
|
||||
to render per-type defaults sections, and the matching create operator
|
||||
(``bim.add_door``, ``bim.add_window``, …) reads the field to seed new
|
||||
instances from the user's preset values. A missing field means the preset
|
||||
silently never reaches the operator.
|
||||
|
||||
Entries WITHOUT the flag are not required to appear — the contract is
|
||||
one-directional: ``has_default_parameters=True`` implies a field, but
|
||||
``False`` allows absence."""
|
||||
from bonsai.bim import ui
|
||||
|
||||
annotations = getattr(ui.DefaultParameters, "__annotations__", {})
|
||||
missing = [e.name for e in registry if e.has_default_parameters and e.name not in annotations]
|
||||
assert not missing, (
|
||||
f"ui.DefaultParameters missing PointerProperty field(s) for: {missing} — "
|
||||
f"each EDIT_TYPES entry with has_default_parameters=True must have a matching "
|
||||
f"<name>: PointerProperty(type=BIM<Name>Properties) field"
|
||||
)
|
||||
|
||||
|
||||
def test_gizmo_preferences_field_per_registry_entry(registry):
|
||||
"""Every registry entry must have a matching ``<name>: BoolProperty`` field
|
||||
on ``ui.GizmoPreferences`` so the addon-preferences UI auto-renders a
|
||||
|
||||
@@ -40,8 +40,7 @@ class TestCopyClass:
|
||||
collector.assign("obj").should_be_called()
|
||||
subject.copy_class(ifc, collector, geometry, root, obj="obj")
|
||||
|
||||
# def test_copy_with_new_geometry_copied_from_the_old(self, ifc, collector, geometry, root):
|
||||
def test_AAAAAAAAAAAA(self, ifc, collector, geometry, root):
|
||||
def test_copy_with_new_geometry_copied_from_the_old(self, ifc, collector, geometry, root):
|
||||
ifc.get_entity("obj").should_be_called().will_return("original_element")
|
||||
root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(False)
|
||||
root.get_object_representation("obj").should_be_called().will_return("representation")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity
|
||||
1,E.01,Walls,m3,,
|
||||
2,E.01.01,Ground floor walls,m3,100,42
|
||||
2,E.01.02,First floor walls,m3,200,35
|
||||
1,A.02,Paintings,m2,,
|
||||
2,A.03,Paintings with water,m2,,
|
||||
3,B.05,White paintings,m2,25,45
|
||||
3,B.06,Colored paintings,m2,32,33
|
||||
2,C-01,Paintings with machine,m2,17,133
|
||||
2,C-02,Decorated paintings,m2,40,8
|
||||
|
@@ -0,0 +1,7 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity
|
||||
1,A,Group A,,,
|
||||
2,A.02,Paintings,m2,20,1
|
||||
2,A.03,Paintings with water,m2,23,1
|
||||
1,C,Group C,,,
|
||||
2,C-01,Paintings with machine,m2,32,1
|
||||
2,C-02,Decorated paintings,m2,40,2
|
||||
|
@@ -0,0 +1,10 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Query,Property
|
||||
1,E.01,Walls,m3,,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume
|
||||
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume
|
||||
1,A.02,Paintings,m2,,,,
|
||||
2,A.03,Paintings with water,m2,,,,
|
||||
3,B.05,White paintings,m2,25,45,,
|
||||
3,B.06,Colored paintings,m2,32,33,,
|
||||
2,C-01,Paintings with machine,m2,17,133,,
|
||||
2,C-02,Decorated paintings,m2,40,8,,
|
||||
|
@@ -0,0 +1,10 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Description
|
||||
1,E.01,Walls,m3,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,42,"Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2"
|
||||
2,E.01.02,First floor walls,m3,200,35,"Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2"
|
||||
1,A.02,Painting,m2,,,"Painting with washable water-based wall paint for indoor/outdoor. The price includes and compensates the costs for the supply of paint, any scaffolding up to a maximum height of 4 m from the support surface, the costs for the protection of furniture, fixed systems or the protection of floors, the cleaning of the surfaces to be treated through the use of rags or net purposes in order to remove residues that can be easily removed. The cost of occasional and partial grouting of surfaces, in order to eliminate any small scratches, including sanding of the grouted parts, is also to be considered included and compensated. For 2 coats with brush or roller."
|
||||
2,A.03,Washable painting,m2,,,"Supply and installation of washable tempera paint for interiors and exteriors. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed installations, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On previously prepared plaster. Apply two coats with a brush or roller. (Tempera colors from the color chart)."
|
||||
3,B.05,White paintings,m2,25,45,
|
||||
3,B.06,Colored paintings,m2,32,33,
|
||||
2,C-01,External painting,m2,17,133,"Painting with plastic coating. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed systems, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On already prepared plaster. For 2 coats (interior textured finish)."
|
||||
2,C-02,Decorated paintings,m2,40,8,
|
||||
|
@@ -0,0 +1,7 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Description
|
||||
1,A,Group A,,,,
|
||||
2,A.02,Paintings,m2,20,1,Paint made by the best painter in the world
|
||||
2,A.03,Paintings with water,m2,23,1,
|
||||
1,C,Group C,,,,
|
||||
2,C-01,Paintings with machine,m2,32,1,Best painting in the world painted with the best painted machine accordingly with ISO9001
|
||||
2,C-02,Decorated paintings,m2,40,2,
|
||||
|
@@ -0,0 +1,10 @@
|
||||
Index,Identification,Name,Unit,Material,Labor,Quantity
|
||||
1,E.01,Walls,m3,,,
|
||||
2,E.01.01,Ground floor walls,m3,55,45,42
|
||||
2,E.01.02,First floor walls,m3,120,80,35
|
||||
1,A.02,Painting,m2,,,
|
||||
2,A.03,Washable painting,m2,,,
|
||||
3,B.05,White paintings,m2,,,45
|
||||
3,B.06,Colored paintings,m2,,,33
|
||||
2,C-01,External painting,m2,,,133
|
||||
2,C-02,Decorated paintings,m2,,,8
|
||||
|
@@ -0,0 +1,10 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,RateSchedule,RateID
|
||||
1,E.01,Walls,m3,,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,42,,
|
||||
2,E.01.02,First floor walls,m3,200,35,,
|
||||
1,A.02,Paintings,m2,,,,
|
||||
2,A.03,Paintings with water,m2,,,,
|
||||
3,B.05,White paintings,m2,,45,Ex2-SoR,A.03
|
||||
3,B.06,Colored paintings,m2,32,33,,
|
||||
2,C-01,Paintings with machine,m2,17,133,,
|
||||
2,C-02,Decorated paintings,m2,40,8,,
|
||||
|
@@ -0,0 +1,14 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Query,Property,Formula
|
||||
1,E.01,Walls,m3,,,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume,
|
||||
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume,
|
||||
1,A.02,Paintings,m2,,,,,
|
||||
2,A.03,Paintings with water,m2,,,,,
|
||||
3,B.05,White paintings,m2,25,,IfcWall,GrossVolume,
|
||||
3,B.06,Colored paintings,m2,32,33,,,
|
||||
3,B.07,Double paintings,m,,,IfcWall,,NetSideArea*2
|
||||
2,C-01,Paintings with machine,m2,17,133,,,
|
||||
2,C-02,Decorated paintings,m2,40,8,,,
|
||||
1,D,Reinforcements,,,,,,
|
||||
2,D.1,Walls reinforcements weight,kg,,,IfcWall,,Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * GrossVolume
|
||||
2,D.2,Beams reinforcements weight,kg,,,,,
|
||||
|
@@ -42,6 +42,46 @@ class TestImplementsTool(NewFile):
|
||||
assert isinstance(subject(), bonsai.core.tool.Blender)
|
||||
|
||||
|
||||
class TestTransparentColor(NewFile):
|
||||
def test_default_alpha_overrides_to_zero_one(self):
|
||||
assert subject.transparent_color([1.0, 0.5, 0.25, 1.0]) == [1.0, 0.5, 0.25, 0.1]
|
||||
|
||||
def test_explicit_alpha_is_applied(self):
|
||||
assert subject.transparent_color([1.0, 0.5, 0.25, 1.0], alpha=0.5) == [1.0, 0.5, 0.25, 0.5]
|
||||
|
||||
def test_does_not_mutate_input(self):
|
||||
original = [1.0, 0.5, 0.25, 1.0]
|
||||
subject.transparent_color(original)
|
||||
assert original == [1.0, 0.5, 0.25, 1.0]
|
||||
|
||||
def test_returns_new_list_instance(self):
|
||||
original = [1.0, 0.5, 0.25, 1.0]
|
||||
result = subject.transparent_color(original)
|
||||
assert result is not original
|
||||
|
||||
|
||||
class TestViewportDecoratorDrawBatch(NewFile):
|
||||
def test_empty_content_pos_skips_shader_calls(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
decorator = subject.ViewportDecorator()
|
||||
decorator.line_shader = MagicMock()
|
||||
decorator.shader = MagicMock()
|
||||
decorator.draw_batch("LINES", [], (1.0, 1.0, 1.0, 1.0))
|
||||
decorator.line_shader.uniform_float.assert_not_called()
|
||||
decorator.shader.uniform_float.assert_not_called()
|
||||
|
||||
def test_empty_indices_skips_shader_calls(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
decorator = subject.ViewportDecorator()
|
||||
decorator.line_shader = MagicMock()
|
||||
decorator.shader = MagicMock()
|
||||
decorator.draw_batch("LINES", [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)], (1.0, 1.0, 1.0, 1.0), indices=[])
|
||||
decorator.line_shader.uniform_float.assert_not_called()
|
||||
decorator.shader.uniform_float.assert_not_called()
|
||||
|
||||
|
||||
class TestCopyNodeGraph(NewFile):
|
||||
def test_run(self):
|
||||
material_to = bpy.data.materials.new("material_to")
|
||||
|
||||
@@ -73,6 +73,83 @@ class TestCreateCamera(NewFile):
|
||||
assert obj.users_collection == tuple()
|
||||
|
||||
|
||||
class TestImportCameraProps(NewFile):
|
||||
def test_imports_perspective_camera_shifts_from_drawing_pset(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc,
|
||||
pset=pset,
|
||||
properties={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375},
|
||||
)
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
camera.type = "PERSP"
|
||||
|
||||
subject.import_camera_props(drawing, camera)
|
||||
|
||||
assert camera.shift_x == pytest.approx(0.125)
|
||||
assert camera.shift_y == pytest.approx(-0.375)
|
||||
|
||||
def test_non_perspective_import_defaults_camera_shifts_to_zero(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc,
|
||||
pset=pset,
|
||||
properties={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375},
|
||||
)
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
camera.type = "ORTHO"
|
||||
camera.shift_x = 1.0
|
||||
camera.shift_y = -1.0
|
||||
|
||||
subject.import_camera_props(drawing, camera)
|
||||
|
||||
assert camera.shift_x == 0.0
|
||||
assert camera.shift_y == 0.0
|
||||
|
||||
|
||||
class TestSyncPerspectiveCameraShifts(NewFile):
|
||||
def test_round_trips_perspective_camera_shifts_through_drawing_pset(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
camera.type = "PERSP"
|
||||
camera.shift_x = 0.25
|
||||
camera.shift_y = -0.5
|
||||
|
||||
subject.sync_perspective_camera_shifts(drawing, camera)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
assert pset["PerspectiveShiftX"] == pytest.approx(0.25)
|
||||
assert pset["PerspectiveShiftY"] == pytest.approx(-0.5)
|
||||
|
||||
reloaded_camera = bpy.data.cameras.new("ReloadedCamera")
|
||||
reloaded_camera.type = "PERSP"
|
||||
subject.import_camera_props(drawing, reloaded_camera)
|
||||
|
||||
assert reloaded_camera.shift_x == pytest.approx(0.25)
|
||||
assert reloaded_camera.shift_y == pytest.approx(-0.5)
|
||||
|
||||
def test_ignores_non_perspective_camera_shifts(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
camera.type = "ORTHO"
|
||||
camera.shift_x = 0.25
|
||||
camera.shift_y = -0.5
|
||||
|
||||
subject.sync_perspective_camera_shifts(drawing, camera)
|
||||
|
||||
assert ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") is None
|
||||
|
||||
|
||||
class TestCreateSvgSheet(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -961,3 +1038,27 @@ class TestAddReferenceImage(NewFile):
|
||||
|
||||
uv_node = material_nodes["Texture Coordinate"]
|
||||
assert len(uv_node.outputs["Generated"].links[:]) == 1
|
||||
|
||||
|
||||
class TestIsDrawingActive(NewFile):
|
||||
def test_no_active_camera(self):
|
||||
bpy.context.scene.camera = None
|
||||
assert subject.is_drawing_active() is False
|
||||
|
||||
def test_active_camera_without_ifc_definition(self):
|
||||
bpy.context.scene.camera = subject.create_camera("Camera", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
|
||||
assert subject.is_drawing_active() is False
|
||||
|
||||
def test_ifc_linked_camera_in_background_mode(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
camera_obj = subject.create_camera("Camera", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
tool.Ifc.link(drawing, camera_obj)
|
||||
bpy.context.scene.camera = camera_obj
|
||||
|
||||
# The test suite itself runs Blender in background mode, where no
|
||||
# VIEW_3D area can ever exist -- this is exactly the case the fix
|
||||
# addresses, so this assertion documents that assumption.
|
||||
assert bpy.app.background is True
|
||||
assert subject.is_drawing_active() is True
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Coalescing tests for ``tool.Geometry.batch_host_recut``.
|
||||
|
||||
The opening/void/array recut paths fan out N host-mesh rebuilds per array of N
|
||||
fillings — the CSG opening-subtraction inside ``switch_representation`` is the
|
||||
most expensive geometry step in the addon. ``batch_host_recut`` queues
|
||||
``recut_host`` + ``update_host_representation`` calls by voided element id and
|
||||
drains each unique host once on the outermost exit. These tests pin the
|
||||
queue/depth/drain contract that the call-site rewrites in subsequent phases
|
||||
rely on."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.geometry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_batch_state():
|
||||
from bonsai import tool
|
||||
|
||||
saved_depth = tool.Geometry._host_batch_depth
|
||||
saved_recut = tool.Geometry._host_recut_queue
|
||||
saved_update = tool.Geometry._host_update_queue
|
||||
tool.Geometry._host_batch_depth = 0
|
||||
tool.Geometry._host_recut_queue = {}
|
||||
tool.Geometry._host_update_queue = {}
|
||||
yield
|
||||
tool.Geometry._host_batch_depth = saved_depth
|
||||
tool.Geometry._host_recut_queue = saved_recut
|
||||
tool.Geometry._host_update_queue = saved_update
|
||||
|
||||
|
||||
def _mock_voided_obj(name: str, *, has_data: bool = True) -> Mock:
|
||||
obj = Mock()
|
||||
obj.name = name
|
||||
obj.data = Mock() if has_data else None
|
||||
return obj
|
||||
|
||||
|
||||
def _mock_element(ifc_id: int) -> Mock:
|
||||
elem = Mock()
|
||||
elem.id.return_value = ifc_id
|
||||
return elem
|
||||
|
||||
|
||||
def test_outside_batch_calls_switch_representation_directly():
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
representation = Mock()
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation") as recut, patch.object(
|
||||
tool.Ifc, "get_entity", return_value=_mock_element(42)
|
||||
):
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
|
||||
assert recut.call_count == 1
|
||||
kwargs = recut.call_args.kwargs
|
||||
assert kwargs["obj"] is voided_obj
|
||||
assert kwargs["representation"] is representation
|
||||
|
||||
|
||||
def test_inside_batch_queues_then_drains_once_on_exit():
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
representation = Mock()
|
||||
element = _mock_element(42)
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation") as recut, patch.object(
|
||||
tool.Ifc, "get_entity", return_value=element
|
||||
), patch.object(tool.Geometry, "get_active_representation", return_value=representation):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
for _ in range(5):
|
||||
tool.Geometry.recut_host(voided_obj, representation)
|
||||
assert recut.call_count == 0, "Inside the batch, no recuts should fire"
|
||||
assert tool.Geometry._host_batch_depth == 1
|
||||
assert len(tool.Geometry._host_recut_queue) == 1
|
||||
assert recut.call_count == 1, "Exactly one drain on outermost exit"
|
||||
|
||||
|
||||
def test_two_different_hosts_drain_separately():
|
||||
from bonsai import tool
|
||||
|
||||
obj_a = _mock_voided_obj("WallA")
|
||||
obj_b = _mock_voided_obj("WallB")
|
||||
elem_a = _mock_element(1)
|
||||
elem_b = _mock_element(2)
|
||||
rep = Mock()
|
||||
|
||||
def get_entity(obj):
|
||||
return elem_a if obj is obj_a else elem_b
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation") as recut, patch.object(
|
||||
tool.Ifc, "get_entity", side_effect=get_entity
|
||||
), patch.object(tool.Geometry, "get_active_representation", return_value=rep):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
for _ in range(5):
|
||||
tool.Geometry.recut_host(obj_a, rep)
|
||||
for _ in range(3):
|
||||
tool.Geometry.recut_host(obj_b, rep)
|
||||
|
||||
assert recut.call_count == 2
|
||||
drained_objs = [call.kwargs["obj"] for call in recut.call_args_list]
|
||||
assert set(drained_objs) == {obj_a, obj_b}
|
||||
|
||||
|
||||
def test_nested_batches_only_outermost_drains():
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
rep = Mock()
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation") as recut, patch.object(
|
||||
tool.Ifc, "get_entity", return_value=_mock_element(1)
|
||||
), patch.object(tool.Geometry, "get_active_representation", return_value=rep):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
tool.Geometry.recut_host(voided_obj, rep)
|
||||
with tool.Geometry.batch_host_recut():
|
||||
tool.Geometry.recut_host(voided_obj, rep)
|
||||
assert recut.call_count == 0
|
||||
assert recut.call_count == 0, "Inner exit must not drain — outer batch still open"
|
||||
assert recut.call_count == 1
|
||||
|
||||
|
||||
def test_stale_element_skipped_at_drain():
|
||||
"""Host's IFC entity disappears between enqueue and drain. The dead entity
|
||||
must be skipped silently — not raise — so unrelated hosts in the same batch
|
||||
still get their recut."""
|
||||
from bonsai import tool
|
||||
|
||||
dead_obj = _mock_voided_obj("Wall")
|
||||
rep = Mock()
|
||||
entity_state = {"alive": _mock_element(1)}
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation") as recut, patch.object(
|
||||
tool.Ifc, "get_entity", side_effect=lambda obj: entity_state["alive"]
|
||||
), patch.object(tool.Geometry, "get_active_representation", return_value=rep):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
tool.Geometry.recut_host(dead_obj, rep)
|
||||
entity_state["alive"] = None
|
||||
|
||||
assert recut.call_count == 0
|
||||
|
||||
|
||||
def test_exception_inside_batch_still_resets_state():
|
||||
from bonsai import tool
|
||||
|
||||
with patch("bonsai.core.geometry.switch_representation"):
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
assert tool.Geometry._host_batch_depth == 1
|
||||
raise RuntimeError("boom")
|
||||
|
||||
assert tool.Geometry._host_batch_depth == 0
|
||||
|
||||
|
||||
def test_update_host_representation_outside_batch_fires_operator():
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
bpy_ops_mock = Mock()
|
||||
|
||||
with patch("bonsai.tool.geometry.bpy.ops", new=bpy_ops_mock), patch.object(
|
||||
tool.Ifc, "get_entity", return_value=_mock_element(42)
|
||||
):
|
||||
tool.Geometry.update_host_representation(voided_obj)
|
||||
|
||||
assert bpy_ops_mock.bim.update_representation.call_count == 1
|
||||
assert bpy_ops_mock.bim.update_representation.call_args.kwargs["obj"] == voided_obj.name
|
||||
|
||||
|
||||
def test_update_host_representation_coalesces_inside_batch():
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
bpy_ops_mock = Mock()
|
||||
|
||||
with patch("bonsai.tool.geometry.bpy.ops", new=bpy_ops_mock), patch.object(
|
||||
tool.Ifc, "get_entity", return_value=_mock_element(42)
|
||||
), patch.object(tool.Geometry, "get_active_representation", return_value=Mock()):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
for _ in range(5):
|
||||
tool.Geometry.update_host_representation(voided_obj)
|
||||
assert bpy_ops_mock.bim.update_representation.call_count == 0
|
||||
|
||||
assert bpy_ops_mock.bim.update_representation.call_count == 1
|
||||
|
||||
|
||||
def test_drain_order_update_before_recut():
|
||||
"""The same host has both pending update + recut. update_representation must
|
||||
fire first so the Blender-mesh edits land in IFC before switch_representation
|
||||
re-tessellates from IFC. Reversed order would silently drop user edits."""
|
||||
from bonsai import tool
|
||||
|
||||
voided_obj = _mock_voided_obj("Wall")
|
||||
rep = Mock()
|
||||
fire_log: list[str] = []
|
||||
bpy_ops_mock = Mock()
|
||||
bpy_ops_mock.bim.update_representation.side_effect = lambda **kw: fire_log.append("update")
|
||||
|
||||
with patch("bonsai.tool.geometry.bpy.ops", new=bpy_ops_mock), patch(
|
||||
"bonsai.core.geometry.switch_representation", side_effect=lambda *a, **kw: fire_log.append("recut")
|
||||
), patch.object(tool.Ifc, "get_entity", return_value=_mock_element(42)), patch.object(
|
||||
tool.Geometry, "get_active_representation", return_value=rep
|
||||
):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
tool.Geometry.recut_host(voided_obj, rep)
|
||||
tool.Geometry.update_host_representation(voided_obj)
|
||||
|
||||
assert fire_log == ["update", "recut"]
|
||||
|
||||
|
||||
def test_mixed_hosts_drain_grouped_by_phase():
|
||||
from bonsai import tool
|
||||
|
||||
obj_a = _mock_voided_obj("WallA")
|
||||
obj_b = _mock_voided_obj("WallB")
|
||||
obj_c = _mock_voided_obj("WallC")
|
||||
elem_a, elem_b, elem_c = _mock_element(1), _mock_element(2), _mock_element(3)
|
||||
rep = Mock()
|
||||
|
||||
def get_entity(obj):
|
||||
return {obj_a: elem_a, obj_b: elem_b, obj_c: elem_c}[obj]
|
||||
|
||||
update_targets: list[str] = []
|
||||
recut_targets: list[Mock] = []
|
||||
bpy_ops_mock = Mock()
|
||||
bpy_ops_mock.bim.update_representation.side_effect = lambda **kw: update_targets.append(kw["obj"])
|
||||
|
||||
with patch("bonsai.tool.geometry.bpy.ops", new=bpy_ops_mock), patch(
|
||||
"bonsai.core.geometry.switch_representation",
|
||||
side_effect=lambda *a, **kw: recut_targets.append(kw["obj"]),
|
||||
), patch.object(tool.Ifc, "get_entity", side_effect=get_entity), patch.object(
|
||||
tool.Geometry, "get_active_representation", return_value=rep
|
||||
):
|
||||
with tool.Geometry.batch_host_recut():
|
||||
tool.Geometry.update_host_representation(obj_a)
|
||||
tool.Geometry.recut_host(obj_b, rep)
|
||||
tool.Geometry.update_host_representation(obj_c)
|
||||
tool.Geometry.recut_host(obj_c, rep)
|
||||
|
||||
assert sorted(update_targets) == sorted([obj_a.name, obj_c.name])
|
||||
assert set(recut_targets) == {obj_b, obj_c}
|
||||
@@ -294,64 +294,125 @@ class TestLoadLinkedModels(NewFile):
|
||||
assert props.links[1].ifc_definition_id == reference2.id()
|
||||
assert props.links[1].has_transformation is True
|
||||
|
||||
def test_load_linked_models_restores_query_from_cache_json(self):
|
||||
"""The selector query used at link time is persisted only in the
|
||||
sidecar cache JSON. Reopening the host IFC must restore it onto the
|
||||
Link PropertyGroup so subsequent Reload/Load replay the same filter."""
|
||||
ifc = ifcopenshell.file()
|
||||
props = tool.Project.get_project_props()
|
||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
||||
document = ifcopenshell.api.document.add_information(ifc)
|
||||
document.Scope = "LINKED_MODEL"
|
||||
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=False) as tmp:
|
||||
json.dump({"query": "IfcElement, ! IfcOpeningElement"}, tmp)
|
||||
json_path = Path(tmp.name)
|
||||
try:
|
||||
ifc_filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
||||
reference.Location = Path(ifc_filepath).as_posix()
|
||||
reference.Identification = ""
|
||||
tool.Ifc.set(ifc)
|
||||
subject.load_linked_models_from_ifc()
|
||||
assert len(props.links) == 1
|
||||
assert props.links[0].query == "IfcElement, ! IfcOpeningElement"
|
||||
finally:
|
||||
json_path.unlink(missing_ok=True)
|
||||
|
||||
def test_load_linked_models_query_defaults_empty_without_cache_json(self):
|
||||
"""When no sidecar cache JSON exists, the restored Link's query field
|
||||
must default to the empty string. Empty query is the documented signal
|
||||
for the load path to apply no selector filter."""
|
||||
ifc = ifcopenshell.file()
|
||||
props = tool.Project.get_project_props()
|
||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
||||
document = ifcopenshell.api.document.add_information(ifc)
|
||||
document.Scope = "LINKED_MODEL"
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ifc_path = Path(tmpdir) / "no-cache.ifc"
|
||||
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
||||
reference.Location = ifc_path.as_posix()
|
||||
reference.Identification = ""
|
||||
tool.Ifc.set(ifc)
|
||||
subject.load_linked_models_from_ifc()
|
||||
assert len(props.links) == 1
|
||||
assert props.links[0].query == ""
|
||||
|
||||
|
||||
class TestCalculateLinkMatrix(NewFile):
|
||||
def _write_cache_json(self, payload: dict) -> Path:
|
||||
"""Write ``payload`` to a fresh sidecar cache JSON path and return it.
|
||||
|
||||
On Windows, ``NamedTemporaryFile(delete=True)`` holds an exclusive
|
||||
handle for the ``with`` block's duration, so the code-under-test
|
||||
cannot open the same path — hence the manual write + unlink pattern.
|
||||
"""
|
||||
tmp = NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=False)
|
||||
try:
|
||||
json.dump(payload, tmp)
|
||||
finally:
|
||||
tmp.close()
|
||||
return Path(tmp.name)
|
||||
|
||||
def test_linking_a_model_without_an_offset_to_our_session_with_no_offset(self):
|
||||
props = tool.Project.get_project_props()
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "0,0,0"})
|
||||
try:
|
||||
link = props.links.add()
|
||||
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||
json.dump({"model_project_north": "0", "model_origin_si": "0,0,0"}, tmp)
|
||||
tmp.flush()
|
||||
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
||||
gprops.model_project_north = "0"
|
||||
gprops.model_origin_si = "0,0,0"
|
||||
assert np.allclose(subject.calculate_link_matrix(link), np.eye(4))
|
||||
finally:
|
||||
json_path.unlink(missing_ok=True)
|
||||
|
||||
def test_linking_an_offset_model_to_our_session_with_no_offset(self):
|
||||
props = tool.Project.get_project_props()
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
||||
try:
|
||||
link = props.links.add()
|
||||
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||
tmp.flush()
|
||||
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
||||
gprops.model_project_north = "0"
|
||||
gprops.model_origin_si = "0,0,0"
|
||||
m = np.eye(4)
|
||||
m[0][3] = 5
|
||||
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||
finally:
|
||||
json_path.unlink(missing_ok=True)
|
||||
|
||||
def test_linking_an_offset_model_to_our_session_with_offset(self):
|
||||
props = tool.Project.get_project_props()
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
||||
try:
|
||||
link = props.links.add()
|
||||
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||
tmp.flush()
|
||||
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
||||
gprops.model_project_north = "0"
|
||||
gprops.model_origin_si = "2,0,0"
|
||||
m = np.eye(4)
|
||||
m[0][3] = 3
|
||||
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||
finally:
|
||||
json_path.unlink(missing_ok=True)
|
||||
|
||||
def test_linking_an_offset_model_to_our_session_with_offset_and_transformation(self):
|
||||
props = tool.Project.get_project_props()
|
||||
gprops = tool.Georeference.get_georeference_props()
|
||||
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
||||
try:
|
||||
link = props.links.add()
|
||||
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
||||
transformation = np.eye(4)
|
||||
transformation[0][3] = 4
|
||||
link.transformation = ",".join(map(str, transformation.reshape(-1)))
|
||||
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||
tmp.flush()
|
||||
gprops.model_project_north = "0"
|
||||
gprops.model_origin_si = "2,0,0"
|
||||
m = np.eye(4)
|
||||
m[0][3] = 7
|
||||
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||
finally:
|
||||
json_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
class TestLoadingIfcSqlite(NewFile):
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Query,Property,Formula
|
||||
1,E.01,Walls,m3,,,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume,
|
||||
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume,
|
||||
1,A.02,Paintings,m2,,,,,
|
||||
2,A.03,Paintings with water,m2,,,,,
|
||||
3,B.05,White paintings,m2,25,,IfcWall,GrossVolume,
|
||||
3,B.06,Colored paintings,m2,32,33,,,
|
||||
3,B.07,Double paintings,m,,,IfcWall,,NetSideArea*2
|
||||
2,C-01,Paintings with machine,m2,17,133,,,
|
||||
2,C-02,Decorated paintings,m2,40,8,,,
|
||||
1,D,Reinforcements,,,,,,
|
||||
2,D.1,Walls reinforcements weight,kg,,,IfcWall,,Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * GrossVolume
|
||||
2,D.2,Beams reinforcements weight,kg,,,,,
|
||||
|
@@ -39,6 +39,7 @@ See example files as a CSV file format reference:
|
||||
- Ex5 - SoR_with_description.csv (a simple SoR with description column)
|
||||
- Ex6 - BoQ with categories.csv (a simple BoQ with categories columns)
|
||||
- Ex7 - BoQ with Rates.csv (a simple BoQ that connect to an existing SoR. It needs an already loaded SoR.)
|
||||
- Ex8 - Boq with formula.csv (a simple BoQ with formula field used to calculate quantities when specified)
|
||||
- `sample_cost_schedule_house_FR.csv` / `.ods`
|
||||
- `schedule.csv`, `rates.csv` (schedule of rates example)
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ class CsvHeader(TypedDict):
|
||||
RateSchedule: NotRequired[str]
|
||||
RateID: NotRequired[str]
|
||||
|
||||
# Formula
|
||||
Formula: NotRequired[str]
|
||||
#QuantityClass: NotRequired[str]
|
||||
|
||||
# Currently we assume that if column is not part of the main header,
|
||||
# then it is a cost value category. So here we list any additional column
|
||||
@@ -65,6 +68,8 @@ MAIN_CSV_HEADER_COLUMNS.extend(
|
||||
# Not sure what this for but it's present in sample .csv.
|
||||
"Subtotal",
|
||||
# Columns from exporter.
|
||||
"ItemIsASum",
|
||||
"Quantities",
|
||||
"RateSubtotal",
|
||||
"TotalPrice",
|
||||
# Deprecated columns from exporter, shouldn't be exported any longer.
|
||||
@@ -91,6 +96,8 @@ class CostItem(TypedDict):
|
||||
Property: Union[str, None]
|
||||
Query: Union[str, None]
|
||||
|
||||
Formula: Union[str, None]
|
||||
#QuantityClass: Union[str, None]
|
||||
|
||||
class Csv2Ifc:
|
||||
# Inputs.
|
||||
@@ -108,6 +115,7 @@ class Csv2Ifc:
|
||||
categories: dict[str, int]
|
||||
has_categories: bool
|
||||
has_rates: bool
|
||||
has_formula: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -163,9 +171,12 @@ class Csv2Ifc:
|
||||
if not self.headers:
|
||||
self.has_categories = True
|
||||
self.has_rates = False
|
||||
self.has_formula = False
|
||||
self.headers = {col: i for i, col in enumerate(row) if col}
|
||||
if "RateSchedule" in self.headers and "RateID" in self.headers:
|
||||
self.has_rates = True
|
||||
if "Formula" in self.headers:
|
||||
self.has_formula = True
|
||||
if "Value" in self.headers:
|
||||
self.has_categories = False
|
||||
else:
|
||||
@@ -233,6 +244,11 @@ class Csv2Ifc:
|
||||
else:
|
||||
cost_rate = None
|
||||
|
||||
if self.has_formula:
|
||||
cost_formula = row[(self.headers["Formula"])] if "Formula" in self.headers else None
|
||||
else:
|
||||
cost_formula = None
|
||||
|
||||
return {
|
||||
"Identification": str(identification) if identification else None,
|
||||
"Name": str(name) if name else None,
|
||||
@@ -244,6 +260,7 @@ class Csv2Ifc:
|
||||
"Query": query,
|
||||
"children": [],
|
||||
"CostRate": cost_rate,
|
||||
"Formula": cost_formula,
|
||||
}
|
||||
|
||||
def create_ifc(self) -> None:
|
||||
@@ -320,6 +337,7 @@ class Csv2Ifc:
|
||||
|
||||
if cost_rate.get("Schedule") and cost_rate.get("RateID"):
|
||||
# if cost_rate["Schedule"] is not "":
|
||||
rate_cost_schedule = None
|
||||
schedules = self.file.by_type("IfcCostSchedule")
|
||||
for schedule in schedules:
|
||||
if schedule.Name == cost_rate["Schedule"]:
|
||||
@@ -381,17 +399,28 @@ class Csv2Ifc:
|
||||
# and some query in "Query" column.
|
||||
# If query is provided it will override the defined value
|
||||
# due current behaviour in cost.assign_cost_item_quantity.
|
||||
if results:
|
||||
if results and not cost_item["Formula"]:
|
||||
ifcopenshell.api.cost.assign_cost_item_quantity(
|
||||
self.file,
|
||||
cost_item=cost_item["ifc"],
|
||||
products=results,
|
||||
prop_name=prop_name,
|
||||
)
|
||||
elif not quantity:
|
||||
elif not quantity and not cost_item["Formula"]:
|
||||
quantity = ifcopenshell.api.cost.add_cost_item_quantity(
|
||||
self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
|
||||
)
|
||||
if cost_item["Formula"]:
|
||||
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["Query"])
|
||||
results = [r for r in results]
|
||||
ifc_quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
|
||||
quantity = ifcopenshell.api.cost.assign_cost_item_quantity(
|
||||
self.file,
|
||||
cost_item=cost_item["ifc"],
|
||||
products=results,
|
||||
formula=cost_item["Formula"],
|
||||
ifc_class=ifc_quantity_class,
|
||||
)
|
||||
|
||||
self.create_cost_items(cost_item["children"], cost_item["ifc"])
|
||||
|
||||
|
||||
@@ -110,16 +110,16 @@ namespace {
|
||||
#endif
|
||||
|
||||
template <>
|
||||
int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool advanced) {
|
||||
int convert_to_ifc(const opencascade::handle<Geom_Curve>& c, IfcSchema::IfcCurve*& curve, bool advanced) {
|
||||
if (c->DynamicType() == STANDARD_TYPE(Geom_TrimmedCurve)) {
|
||||
Handle_Geom_TrimmedCurve trim = Handle_Geom_TrimmedCurve::DownCast(c);
|
||||
const Handle_Geom_Curve basis = trim->BasisCurve();
|
||||
opencascade::handle<Geom_TrimmedCurve> trim = opencascade::handle<Geom_TrimmedCurve>::DownCast(c);
|
||||
const opencascade::handle<Geom_Curve> basis = trim->BasisCurve();
|
||||
return convert_to_ifc(basis, curve, advanced);
|
||||
} else if (c->DynamicType() == STANDARD_TYPE(Geom_Line)) {
|
||||
IfcSchema::IfcDirection* d;
|
||||
IfcSchema::IfcCartesianPoint* p;
|
||||
|
||||
Handle_Geom_Line line = Handle_Geom_Line::DownCast(c);
|
||||
opencascade::handle<Geom_Line> line = opencascade::handle<Geom_Line>::DownCast(c);
|
||||
|
||||
if (!convert_to_ifc(line->Position().Location(), p, advanced)) {
|
||||
return 0;
|
||||
@@ -135,7 +135,7 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
} else if (c->DynamicType() == STANDARD_TYPE(Geom_Circle)) {
|
||||
IfcSchema::IfcAxis2Placement3D* ax;
|
||||
|
||||
Handle_Geom_Circle circle = Handle_Geom_Circle::DownCast(c);
|
||||
opencascade::handle<Geom_Circle> circle = opencascade::handle<Geom_Circle>::DownCast(c);
|
||||
|
||||
convert_to_ifc(circle->Position(), ax, advanced);
|
||||
curve = new IfcSchema::IfcCircle(ax, circle->Radius());
|
||||
@@ -144,16 +144,16 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
} else if (c->DynamicType() == STANDARD_TYPE(Geom_Ellipse)) {
|
||||
IfcSchema::IfcAxis2Placement3D* ax;
|
||||
|
||||
Handle_Geom_Ellipse ellipse = Handle_Geom_Ellipse::DownCast(c);
|
||||
opencascade::handle<Geom_Ellipse> ellipse = opencascade::handle<Geom_Ellipse>::DownCast(c);
|
||||
|
||||
convert_to_ifc(ellipse->Position(), ax, advanced);
|
||||
curve = new IfcSchema::IfcEllipse(ax, ellipse->MajorRadius(), ellipse->MinorRadius());
|
||||
|
||||
|
||||
return 1;
|
||||
}
|
||||
#ifdef SCHEMA_HAS_IfcRationalBSplineSurfaceWithKnots
|
||||
else if (c->DynamicType() == STANDARD_TYPE(Geom_BezierCurve)) {
|
||||
Handle_Geom_BezierCurve bezier = Handle_Geom_BezierCurve::DownCast(c);
|
||||
opencascade::handle<Geom_BezierCurve> bezier = opencascade::handle<Geom_BezierCurve>::DownCast(c);
|
||||
|
||||
std::vector<int> mults;
|
||||
std::vector<double> knots;
|
||||
@@ -162,7 +162,7 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
IfcSchema::IfcKnotType::Value knot_spec = IfcSchema::IfcKnotType::IfcKnotType_QUASI_UNIFORM_KNOTS;
|
||||
|
||||
IfcSchema::IfcCartesianPoint::list::ptr points(new IfcSchema::IfcCartesianPoint::list);
|
||||
TColgp_Array1OfPnt poles(1, bezier->NbPoles());
|
||||
NCollection_Array1<gp_Pnt> poles(1, bezier->NbPoles());
|
||||
bezier->Poles(poles);
|
||||
for (int i = 1; i <= bezier->NbPoles(); ++i) {
|
||||
IfcSchema::IfcCartesianPoint* p;
|
||||
@@ -180,7 +180,7 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
knots.push_back((double) i - 1);
|
||||
}
|
||||
|
||||
TColStd_Array1OfReal bspline_weights(1, bezier->NbPoles());
|
||||
NCollection_Array1<double> bspline_weights(1, bezier->NbPoles());
|
||||
bezier->Weights(bspline_weights);
|
||||
opencascade_array_to_vector(bspline_weights, weights);
|
||||
|
||||
@@ -199,10 +199,10 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
return 1;
|
||||
}
|
||||
else if (c->DynamicType() == STANDARD_TYPE(Geom_BSplineCurve)) {
|
||||
Handle_Geom_BSplineCurve bspline = Handle_Geom_BSplineCurve::DownCast(c);
|
||||
opencascade::handle<Geom_BSplineCurve> bspline = opencascade::handle<Geom_BSplineCurve>::DownCast(c);
|
||||
|
||||
IfcSchema::IfcCartesianPoint::list::ptr points(new IfcSchema::IfcCartesianPoint::list);
|
||||
TColgp_Array1OfPnt poles(1, bspline->NbPoles());
|
||||
NCollection_Array1<gp_Pnt> poles(1, bspline->NbPoles());
|
||||
bspline->Poles(poles);
|
||||
for (int i = 1; i <= bspline->NbPoles(); ++i) {
|
||||
IfcSchema::IfcCartesianPoint* p;
|
||||
@@ -217,9 +217,9 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
std::vector<double> knots;
|
||||
std::vector<double> weights;
|
||||
|
||||
TColStd_Array1OfInteger bspline_mults(1, bspline->NbKnots());
|
||||
TColStd_Array1OfReal bspline_knots(1, bspline->NbKnots());
|
||||
TColStd_Array1OfReal bspline_weights(1, bspline->NbPoles());
|
||||
NCollection_Array1<int> bspline_mults(1, bspline->NbKnots());
|
||||
NCollection_Array1<double> bspline_knots(1, bspline->NbKnots());
|
||||
NCollection_Array1<double> bspline_weights(1, bspline->NbPoles());
|
||||
|
||||
bspline->Multiplicities(bspline_mults);
|
||||
bspline->Knots(bspline_knots);
|
||||
@@ -278,9 +278,9 @@ int convert_to_ifc(const Handle_Geom_Curve& c, IfcSchema::IfcCurve*& curve, bool
|
||||
}
|
||||
|
||||
template <>
|
||||
int convert_to_ifc(const Handle_Geom_Surface& s, IfcSchema::IfcSurface*& surface, bool advanced) {
|
||||
int convert_to_ifc(const opencascade::handle<Geom_Surface>& s, IfcSchema::IfcSurface*& surface, bool advanced) {
|
||||
if (s->DynamicType() == STANDARD_TYPE(Geom_Plane)) {
|
||||
Handle_Geom_Plane plane = Handle_Geom_Plane::DownCast(s);
|
||||
opencascade::handle<Geom_Plane> plane = opencascade::handle<Geom_Plane>::DownCast(s);
|
||||
IfcSchema::IfcAxis2Placement3D* place;
|
||||
/// @todo: Note that the Ax3 is converted to an Ax2 here
|
||||
if (!convert_to_ifc(plane->Position().Ax2(), place, advanced)) {
|
||||
@@ -291,7 +291,7 @@ int convert_to_ifc(const Handle_Geom_Surface& s, IfcSchema::IfcSurface*& surface
|
||||
}
|
||||
#ifdef SCHEMA_HAS_IfcRationalBSplineSurfaceWithKnots
|
||||
else if (s->DynamicType() == STANDARD_TYPE(Geom_CylindricalSurface)) {
|
||||
Handle_Geom_CylindricalSurface cyl = Handle_Geom_CylindricalSurface::DownCast(s);
|
||||
opencascade::handle<Geom_CylindricalSurface> cyl = opencascade::handle<Geom_CylindricalSurface>::DownCast(s);
|
||||
IfcSchema::IfcAxis2Placement3D* place;
|
||||
/// @todo: Note that the Ax3 is converted to an Ax2 here
|
||||
if (!convert_to_ifc(cyl->Position().Ax2(), place, advanced)) {
|
||||
@@ -302,10 +302,10 @@ int convert_to_ifc(const Handle_Geom_Surface& s, IfcSchema::IfcSurface*& surface
|
||||
} else if (s->DynamicType() == STANDARD_TYPE(Geom_BSplineSurface)) {
|
||||
typedef aggregate_of_aggregate_of<IfcSchema::IfcCartesianPoint> points_t;
|
||||
|
||||
Handle_Geom_BSplineSurface bspline = Handle_Geom_BSplineSurface::DownCast(s);
|
||||
opencascade::handle<Geom_BSplineSurface> bspline = opencascade::handle<Geom_BSplineSurface>::DownCast(s);
|
||||
points_t::ptr points(new points_t);
|
||||
|
||||
TColgp_Array2OfPnt poles(1, bspline->NbUPoles(), 1, bspline->NbVPoles());
|
||||
NCollection_Array2<gp_Pnt> poles(1, bspline->NbUPoles(), 1, bspline->NbVPoles());
|
||||
bspline->Poles(poles);
|
||||
for (int i = 1; i <= bspline->NbUPoles(); ++i) {
|
||||
std::vector<IfcSchema::IfcCartesianPoint*> ps;
|
||||
@@ -333,11 +333,11 @@ int convert_to_ifc(const Handle_Geom_Surface& s, IfcSchema::IfcSurface*& surface
|
||||
std::vector<double> vknots;
|
||||
std::vector< std::vector<double> > weights;
|
||||
|
||||
TColStd_Array1OfInteger bspline_umults(1, bspline->NbUKnots());
|
||||
TColStd_Array1OfInteger bspline_vmults(1, bspline->NbVKnots());
|
||||
TColStd_Array1OfReal bspline_uknots(1, bspline->NbUKnots());
|
||||
TColStd_Array1OfReal bspline_vknots(1, bspline->NbVKnots());
|
||||
TColStd_Array2OfReal bspline_weights(1, bspline->NbUPoles(), 1, bspline->NbVPoles());
|
||||
NCollection_Array1<int> bspline_umults(1, bspline->NbUKnots());
|
||||
NCollection_Array1<int> bspline_vmults(1, bspline->NbVKnots());
|
||||
NCollection_Array1<double> bspline_uknots(1, bspline->NbUKnots());
|
||||
NCollection_Array1<double> bspline_vknots(1, bspline->NbVKnots());
|
||||
NCollection_Array2<double> bspline_weights(1, bspline->NbUPoles(), 1, bspline->NbVPoles());
|
||||
|
||||
bspline->UMultiplicities(bspline_umults);
|
||||
bspline->VMultiplicities(bspline_vmults);
|
||||
@@ -405,7 +405,7 @@ int convert_to_ifc(const TopoDS_Edge& e, IfcSchema::IfcCurve*& c, bool advanced)
|
||||
double a, b;
|
||||
IfcSchema::IfcCurve* base;
|
||||
|
||||
Handle_Geom_Curve crv = BRep_Tool::Curve(e, a, b);
|
||||
opencascade::handle<Geom_Curve> crv = BRep_Tool::Curve(e, a, b);
|
||||
if (!convert_to_ifc(crv, base, advanced)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -436,7 +436,7 @@ int convert_to_ifc(const TopoDS_Edge& e, IfcSchema::IfcEdge*& edge, bool advance
|
||||
return 0;
|
||||
}
|
||||
|
||||
Handle_Geom_Curve crv = BRep_Tool::Curve(e, a, b);
|
||||
opencascade::handle<Geom_Curve> crv = BRep_Tool::Curve(e, a, b);
|
||||
|
||||
if (crv.IsNull()) {
|
||||
return 0;
|
||||
@@ -460,13 +460,13 @@ int convert_to_ifc(const TopoDS_Edge& e, IfcSchema::IfcEdge*& edge, bool advance
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool is_polygonal(const Handle_Geom_Curve& crv) {
|
||||
bool is_polygonal(const opencascade::handle<Geom_Curve>& crv) {
|
||||
if (crv->DynamicType() == STANDARD_TYPE(Geom_Line)) {
|
||||
return true;
|
||||
} else if (crv->DynamicType() == STANDARD_TYPE(Geom_TrimmedCurve)) {
|
||||
return is_polygonal(Handle_Geom_TrimmedCurve::DownCast(crv)->BasisCurve());
|
||||
return is_polygonal(opencascade::handle<Geom_TrimmedCurve>::DownCast(crv)->BasisCurve());
|
||||
} else if (crv->DynamicType() == STANDARD_TYPE(Geom_BSplineCurve)) {
|
||||
auto bspl = Handle_Geom_BSplineCurve::DownCast(crv);
|
||||
auto bspl = opencascade::handle<Geom_BSplineCurve>::DownCast(crv);
|
||||
return bspl->NbPoles() == 2 && bspl->Degree() == 1;
|
||||
} else {
|
||||
return false;
|
||||
@@ -479,7 +479,7 @@ int convert_to_ifc(const TopoDS_Wire& wire, IfcSchema::IfcLoop*& loop, bool adva
|
||||
bool polygonal = true;
|
||||
for (TopExp_Explorer exp(wire, TopAbs_EDGE); exp.More(); exp.Next()) {
|
||||
double a, b;
|
||||
Handle_Geom_Curve crv = BRep_Tool::Curve(TopoDS::Edge(exp.Current()), a, b);
|
||||
opencascade::handle<Geom_Curve> crv = BRep_Tool::Curve(TopoDS::Edge(exp.Current()), a, b);
|
||||
if (crv.IsNull()) {
|
||||
continue;
|
||||
}
|
||||
@@ -526,7 +526,7 @@ int convert_to_ifc(const TopoDS_Wire& wire, IfcSchema::IfcLoop*& loop, bool adva
|
||||
|
||||
template <>
|
||||
int convert_to_ifc(const TopoDS_Face& f, IfcSchema::IfcFace*& face, bool advanced) {
|
||||
Handle_Geom_Surface surf = BRep_Tool::Surface(f);
|
||||
opencascade::handle<Geom_Surface> surf = BRep_Tool::Surface(f);
|
||||
TopExp_Explorer exp(f, TopAbs_WIRE);
|
||||
IfcSchema::IfcFaceBound::list::ptr bounds(new IfcSchema::IfcFaceBound::list);
|
||||
int index = 0;
|
||||
@@ -723,7 +723,7 @@ IfcUtil::IfcBaseClass* POSTFIX_SCHEMA(tesselate)(const TopoDS_Shape& shape, doub
|
||||
IfcSchema::IfcCartesianPoint* cpnt = new IfcSchema::IfcCartesianPoint(xyz);
|
||||
vertices.push_back(cpnt);
|
||||
}
|
||||
const Poly_Array1OfTriangle& triangles = tri->Triangles();
|
||||
const NCollection_Array1<Poly_Triangle>& triangles = tri->Triangles();
|
||||
for (int i = 1; i <= triangles.Length(); ++i) {
|
||||
int n1, n2, n3;
|
||||
triangles(i).Get(n1, n2, n3);
|
||||
|
||||
@@ -36,7 +36,14 @@
|
||||
#include <BRepAlgoAPI_Cut.hxx>
|
||||
#include <BRepExtrema_DistShapeShape.hxx>
|
||||
#include <BRepClass3d_SolidClassifier.hxx>
|
||||
#include <TopTools_DataMapOfShapeInteger.hxx>
|
||||
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <Standard_Version.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <Standard_Integer.hxx>
|
||||
#include <TopTools_ShapeMapHasher.hxx>
|
||||
#include <NCollection_DataMap.hxx>
|
||||
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
#include <BRepExtrema_ExtPF.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
@@ -145,7 +152,7 @@ namespace IfcGeom {
|
||||
while (exp.More()) {
|
||||
is_closed = true;
|
||||
TopoDS_Shell shell = TopoDS::Shell(exp.Current());
|
||||
TopTools_IndexedDataMapOfShapeListOfShape edgeFaceMap;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> edgeFaceMap;
|
||||
TopExp::MapShapesAndAncestors(s, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
|
||||
|
||||
for (int i = 1; i <= edgeFaceMap.Extent(); ++i) {
|
||||
@@ -1461,13 +1468,13 @@ namespace IfcGeom {
|
||||
, bounds_(b)
|
||||
{}
|
||||
|
||||
Standard_Boolean Reject(const Bnd_Box& b) const {
|
||||
bool Reject(const Bnd_Box& b) const {
|
||||
return bounds_.IsOut(b);
|
||||
}
|
||||
|
||||
Standard_Boolean Accept(const T& o) {
|
||||
bool Accept(const T& o) {
|
||||
results_.push_back(o);
|
||||
return Standard_True;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<T>& results() const {
|
||||
@@ -1922,11 +1929,15 @@ namespace IfcGeom {
|
||||
std::vector<gp_Vec> original_normals;
|
||||
|
||||
// Attempt to copy exactly what BRepExtrema_TriangleSet is doing under the hood.
|
||||
const auto builder = new BVH_LinearBuilder<Standard_Real, 3>(BVH_Constants_LeafNodeSizeDefault, BVH_Constants_MaxTreeDepth);
|
||||
BVH_Triangulation<Standard_Real, 3> triangulation(builder);
|
||||
const auto builder = new BVH_LinearBuilder<double, 3>(BVH_Constants_LeafNodeSizeDefault, BVH_Constants_MaxTreeDepth);
|
||||
BVH_Triangulation<double, 3> triangulation(builder);
|
||||
|
||||
for (int i = 0; i < elem_verts.size(); i += 3) {
|
||||
#if OCC_VERSION_HEX >= 0x80000
|
||||
triangulation.Vertices.Append(BVH_Vec3d(elem_verts[i], elem_verts[i + 1], elem_verts[i + 2]));
|
||||
#else
|
||||
triangulation.Vertices.push_back(BVH_Vec3d(elem_verts[i], elem_verts[i + 1], elem_verts[i + 2]));
|
||||
#endif
|
||||
verts.push_back(gp_Pnt(elem_verts[i], elem_verts[i + 1], elem_verts[i + 2]));
|
||||
}
|
||||
|
||||
@@ -1938,7 +1949,11 @@ namespace IfcGeom {
|
||||
gp_Vec dir2(v1_pnt, v3_pnt);
|
||||
gp_Vec cross_product = dir1.Crossed(dir2);
|
||||
if (cross_product.Magnitude() > Precision::Confusion()) {
|
||||
#if OCC_VERSION_HEX >= 0x80000
|
||||
triangulation.Elements.Append(BVH_Vec4i(
|
||||
#else
|
||||
triangulation.Elements.push_back(BVH_Vec4i(
|
||||
#endif
|
||||
elem_faces[i], elem_faces[i + 1], elem_faces[i + 2], original_tris_index
|
||||
));
|
||||
original_tris_index++;
|
||||
@@ -2085,7 +2100,7 @@ namespace IfcGeom {
|
||||
}
|
||||
|
||||
protected:
|
||||
typedef TopTools_DataMapOfShapeInteger face_style_map_t;
|
||||
typedef NCollection_DataMap<TopoDS_Shape, int, TopTools_ShapeMapHasher> face_style_map_t;
|
||||
|
||||
face_style_map_t face_styles_;
|
||||
std::vector<ifcopenshell::geometry::taxonomy::style::ptr> styles_;
|
||||
|
||||
@@ -109,7 +109,7 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
|
||||
std::vector<std::tuple<int, int, int>> triangle_indices;
|
||||
|
||||
TopLoc_Location loc;
|
||||
Handle_Poly_Triangulation tri = BRep_Tool::Triangulation(face, loc);
|
||||
opencascade::handle<Poly_Triangulation> tri = BRep_Tool::Triangulation(face, loc);
|
||||
|
||||
if (tri.IsNull()) {
|
||||
Logger::Root().Message(Logger::LOG_ERROR, "GEO", 184, "Triangulation missing for face");
|
||||
@@ -145,7 +145,7 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
|
||||
normal = normal_direction;
|
||||
}
|
||||
} else {
|
||||
Handle_Geom_Surface surf = BRep_Tool::Surface(face);
|
||||
opencascade::handle<Geom_Surface> surf = BRep_Tool::Surface(face);
|
||||
// Special case the normal at the poles of a spherical surface
|
||||
if (surf->DynamicType() == STANDARD_TYPE(Geom_SphericalSurface)) {
|
||||
if (fabs(fabs(uv.Y()) - M_PI / 2.) < 1.e-9) {
|
||||
@@ -165,7 +165,7 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
|
||||
}
|
||||
}
|
||||
|
||||
const Poly_Array1OfTriangle& triangles = tri->Triangles();
|
||||
const NCollection_Array1<Poly_Triangle>& triangles = tri->Triangles();
|
||||
for (int i = 1; i <= triangles.Length(); ++i) {
|
||||
int n1, n2, n3;
|
||||
if (face.Orientation() == TopAbs_REVERSED)
|
||||
@@ -244,7 +244,7 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
|
||||
// TopExp_Explorer texp(s, TopAbs_EDGE, TopAbs_FACE) to find edges that do not
|
||||
// belong to any face.
|
||||
|
||||
TopTools_ListOfShape edges;
|
||||
NCollection_List<TopoDS_Shape> edges;
|
||||
// First collect edges part of wire in order
|
||||
for (TopExp_Explorer texp(shape_, TopAbs_WIRE); texp.More(); texp.Next()) {
|
||||
BRepTools_WireExplorer wexp(TopoDS::Wire(texp.Current()));
|
||||
@@ -258,7 +258,7 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
|
||||
edges.Append(texp.Current());
|
||||
}
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape texp(edges); texp.More(); texp.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator texp(edges); texp.More(); texp.Next()) {
|
||||
BRepAdaptor_Curve crv(TopoDS::Edge(texp.Value()));
|
||||
GCPnts_QuasiUniformDeflection tessellater(crv, settings.get<settings::MesherLinearDeflection>().get());
|
||||
int n = tessellater.NbPoints();
|
||||
@@ -494,7 +494,7 @@ ConversionResultShape* ifcopenshell::geometry::OpenCascadeShape::wrap_in_compoun
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::vertices()
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapes(shape_, TopAbs_VERTEX, map);
|
||||
std::vector<ConversionResultShape*> vec;
|
||||
for (int i = 1; i <= map.Extent(); ++i) {
|
||||
@@ -505,7 +505,7 @@ std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::ve
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::edges()
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapes(shape_, TopAbs_EDGE, map);
|
||||
std::vector<ConversionResultShape*> vec;
|
||||
for (int i = 1; i <= map.Extent(); ++i) {
|
||||
@@ -516,7 +516,7 @@ std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::ed
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::facets()
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapes(shape_, TopAbs_FACE, map);
|
||||
std::vector<ConversionResultShape*> vec;
|
||||
for (int i = 1; i <= map.Extent(); ++i) {
|
||||
@@ -634,7 +634,7 @@ namespace {
|
||||
coords.push_back(tri->Node(i).Transformed(loc).XYZ());
|
||||
}
|
||||
|
||||
const Poly_Array1OfTriangle& triangles = tri->Triangles();
|
||||
const NCollection_Array1<Poly_Triangle>& triangles = tri->Triangles();
|
||||
for (int i = 1; i <= triangles.Length(); ++i) {
|
||||
int n1, n2, n3;
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
#include <BRepGProp_Face.hxx>
|
||||
|
||||
#include <Poly_Triangulation.hxx>
|
||||
#include <TColgp_Array1OfPnt.hxx>
|
||||
#include <TColgp_Array1OfPnt2d.hxx>
|
||||
|
||||
#include <TopExp_Explorer.hxx>
|
||||
#include <BRepTools.hxx>
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
|
||||
if (!is_manifold) {
|
||||
// force sewing, edge identity might have been mudied by FixAdvFace.FixOrientation.MSG5 to fix interior loop winding order
|
||||
TopTools_ListOfShape list;
|
||||
NCollection_List<TopoDS_Shape> list;
|
||||
IfcGeom::util::shape_to_face_list(entity_part, list);
|
||||
IfcGeom::util::create_solid_from_faces(list, entity_part, settings_.get<settings::Precision>().get(), true);
|
||||
is_manifold = util::is_manifold(entity_part);
|
||||
@@ -205,7 +205,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
for (;; ++it) {
|
||||
if (it == opening_vector.end() || jt->first / it->first > 10.) {
|
||||
|
||||
TopTools_ListOfShape opening_list;
|
||||
NCollection_List<TopoDS_Shape> opening_list;
|
||||
for (auto kt = jt; kt < it; ++kt) {
|
||||
opening_list.Append(kt->second);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <Geom_Curve.hxx>
|
||||
#include <gp_Pln.hxx>
|
||||
#include <TColgp_SequenceOfPnt.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
#include <BOPAlgo_Operation.hxx>
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
@@ -105,7 +103,7 @@ private:
|
||||
bool edge(int A, int B, TopoDS_Edge& e);
|
||||
|
||||
bool wire(const ifcopenshell::geometry::taxonomy::loop::ptr loop, TopoDS_Wire& wire);
|
||||
bool wires(const ifcopenshell::geometry::taxonomy::loop::ptr loop, TopTools_ListOfShape& wires);
|
||||
bool wires(const ifcopenshell::geometry::taxonomy::loop::ptr loop, NCollection_List<TopoDS_Shape>& wires);
|
||||
};
|
||||
|
||||
faceset_helper* faceset_helper_;
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
|
||||
#include <BRepAlgoAPI_Fuse.hxx>
|
||||
|
||||
#include <TopTools_IndexedMapOfShape.hxx>
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <TopTools_ShapeMapHasher.hxx>
|
||||
#include <NCollection_IndexedMap.hxx>
|
||||
|
||||
#include <ShapeUpgrade_UnifySameDomain.hxx>
|
||||
|
||||
@@ -73,7 +75,7 @@ bool IfcGeom::util::axis_equal(const gp_Ax2d & a, const gp_Ax2d & b, double tole
|
||||
|
||||
int IfcGeom::util::count(const TopoDS_Shape& s, TopAbs_ShapeEnum t, bool unique) {
|
||||
if (unique) {
|
||||
TopTools_IndexedMapOfShape map;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapes(s, t, map);
|
||||
return map.Extent();
|
||||
} else {
|
||||
@@ -108,7 +110,7 @@ bool IfcGeom::util::is_manifold(const TopoDS_Shape& a) {
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
TopTools_IndexedDataMapOfShapeListOfShape map;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapesAndAncestors(a, TopAbs_EDGE, TopAbs_FACE, map);
|
||||
|
||||
for (int i = 1; i <= map.Extent(); ++i) {
|
||||
@@ -199,17 +201,17 @@ gp_Trsf IfcGeom::util::combine_offset_and_rotation(const gp_Vec & offset, const
|
||||
}
|
||||
|
||||
|
||||
bool IfcGeom::util::project(const Handle_Geom_Surface& srf, const TopoDS_Shape& shp, double& u1, double& v1, double& u2, double& v2, double widen) {
|
||||
bool IfcGeom::util::project(const opencascade::handle<Geom_Surface>& srf, const TopoDS_Shape& shp, double& u1, double& v1, double& u2, double& v2, double widen) {
|
||||
// @todo std::unique_ptr for C++11
|
||||
ShapeAnalysis_Surface* sas = 0;
|
||||
Handle(Geom_Plane) pln;
|
||||
opencascade::handle<Geom_Plane> pln;
|
||||
|
||||
if (srf->DynamicType() == STANDARD_TYPE(Geom_Plane)) {
|
||||
// Optimize projection for specific cases
|
||||
pln = Handle(Geom_Plane)::DownCast(srf);
|
||||
} else if (srf->DynamicType() == STANDARD_TYPE(Geom_OffsetSurface) && Handle(Geom_OffsetSurface)::DownCast(srf)->BasisSurface()->DynamicType() == STANDARD_TYPE(Geom_Plane)) {
|
||||
pln = opencascade::handle<Geom_Plane>::DownCast(srf);
|
||||
} else if (srf->DynamicType() == STANDARD_TYPE(Geom_OffsetSurface) && opencascade::handle<Geom_OffsetSurface>::DownCast(srf)->BasisSurface()->DynamicType() == STANDARD_TYPE(Geom_Plane)) {
|
||||
// For an offset planar surface the projected UV coords are the same as the basis surface
|
||||
pln = Handle(Geom_Plane)::DownCast(Handle(Geom_OffsetSurface)::DownCast(srf)->BasisSurface());
|
||||
pln = opencascade::handle<Geom_Plane>::DownCast(opencascade::handle<Geom_OffsetSurface>::DownCast(srf)->BasisSurface());
|
||||
} else {
|
||||
sas = new ShapeAnalysis_Surface(srf);
|
||||
}
|
||||
@@ -246,7 +248,7 @@ bool IfcGeom::util::project(const Handle_Geom_Surface& srf, const TopoDS_Shape&
|
||||
const TopoDS_Edge& e = TopoDS::Edge(exp.Current());
|
||||
|
||||
double a, b;
|
||||
Handle_Geom_Curve crv = BRep_Tool::Curve(e, a, b);
|
||||
opencascade::handle<Geom_Curve> crv = BRep_Tool::Curve(e, a, b);
|
||||
gp_Pnt p;
|
||||
crv->D0((a + b) / 2., p);
|
||||
|
||||
@@ -449,24 +451,25 @@ bool IfcGeom::util::fit_halfspace(const TopoDS_Shape& a, const TopoDS_Shape& b,
|
||||
}
|
||||
|
||||
|
||||
const Handle_Geom_Curve IfcGeom::util::intersect(const Handle_Geom_Surface& a, const Handle_Geom_Surface& b) {
|
||||
const opencascade::handle<Geom_Curve> IfcGeom::util::intersect(const opencascade::handle<Geom_Surface>& a, const opencascade::handle<Geom_Surface>& b) {
|
||||
GeomAPI_IntSS x(a, b, 1.e-7);
|
||||
if (x.IsDone() && x.NbLines() == 1) {
|
||||
return x.Line(1);
|
||||
} else {
|
||||
return Handle_Geom_Curve();
|
||||
return opencascade::handle<Geom_Curve>();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const Handle_Geom_Curve IfcGeom::util::intersect(const Handle_Geom_Surface& a, const TopoDS_Face& b) {
|
||||
const opencascade::handle<Geom_Curve> IfcGeom::util::intersect(const opencascade::handle<Geom_Surface>& a, const TopoDS_Face& b) {
|
||||
return intersect(a, BRep_Tool::Surface(b));
|
||||
}
|
||||
|
||||
const Handle_Geom_Curve IfcGeom::util::intersect(const TopoDS_Face& a, const Handle_Geom_Surface& b) {
|
||||
const opencascade::handle<Geom_Curve> IfcGeom::util::intersect(const TopoDS_Face& a, const opencascade::handle<Geom_Surface>& b) {
|
||||
return intersect(BRep_Tool::Surface(a), b);
|
||||
}
|
||||
|
||||
bool IfcGeom::util::intersect(const Handle_Geom_Curve& a, const Handle_Geom_Surface& b, gp_Pnt& p) {
|
||||
bool IfcGeom::util::intersect(const opencascade::handle<Geom_Curve>& a, const opencascade::handle<Geom_Surface>& b, gp_Pnt& p) {
|
||||
GeomAPI_IntCS x(a, b);
|
||||
if (x.IsDone() && x.NbPoints() == 1) {
|
||||
p = x.Point(1);
|
||||
@@ -476,11 +479,11 @@ bool IfcGeom::util::intersect(const Handle_Geom_Curve& a, const Handle_Geom_Surf
|
||||
}
|
||||
}
|
||||
|
||||
bool IfcGeom::util::intersect(const Handle_Geom_Curve& a, const TopoDS_Face& b, gp_Pnt &c) {
|
||||
bool IfcGeom::util::intersect(const opencascade::handle<Geom_Curve>& a, const TopoDS_Face& b, gp_Pnt &c) {
|
||||
return intersect(a, BRep_Tool::Surface(b), c);
|
||||
}
|
||||
|
||||
bool IfcGeom::util::intersect(const Handle_Geom_Curve& a, const TopoDS_Shape& b, std::vector<gp_Pnt>& out) {
|
||||
bool IfcGeom::util::intersect(const opencascade::handle<Geom_Curve>& a, const TopoDS_Shape& b, std::vector<gp_Pnt>& out) {
|
||||
TopExp_Explorer exp(b, TopAbs_FACE);
|
||||
gp_Pnt p;
|
||||
for (; exp.More(); exp.Next()) {
|
||||
@@ -491,12 +494,12 @@ bool IfcGeom::util::intersect(const Handle_Geom_Curve& a, const TopoDS_Shape& b,
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
bool IfcGeom::util::intersect(const Handle_Geom_Surface& a, const TopoDS_Shape& b, std::vector< std::pair<Handle_Geom_Surface, Handle_Geom_Curve> >& out) {
|
||||
bool IfcGeom::util::intersect(const opencascade::handle<Geom_Surface>& a, const TopoDS_Shape& b, std::vector< std::pair<opencascade::handle<Geom_Surface>, opencascade::handle<Geom_Curve> > >& out) {
|
||||
TopExp_Explorer exp(b, TopAbs_FACE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
const TopoDS_Face& f = TopoDS::Face(exp.Current());
|
||||
const Handle_Geom_Surface& s = BRep_Tool::Surface(f);
|
||||
Handle_Geom_Curve crv = intersect(a, s);
|
||||
const opencascade::handle<Geom_Surface>& s = BRep_Tool::Surface(f);
|
||||
opencascade::handle<Geom_Curve> crv = intersect(a, s);
|
||||
if (!crv.IsNull()) {
|
||||
out.push_back(std::make_pair(s, crv));
|
||||
}
|
||||
@@ -516,7 +519,7 @@ bool IfcGeom::util::closest(const gp_Pnt& a, const std::vector<gp_Pnt>& b, gp_Pn
|
||||
return minimal_distance != std::numeric_limits<double>::infinity();
|
||||
}
|
||||
|
||||
bool IfcGeom::util::project(const Handle_Geom_Curve& crv, const gp_Pnt& pt, gp_Pnt& p, double& u, double& d) {
|
||||
bool IfcGeom::util::project(const opencascade::handle<Geom_Curve>& crv, const gp_Pnt& pt, gp_Pnt& p, double& u, double& d) {
|
||||
ShapeAnalysis_Curve sac;
|
||||
sac.Project(crv, pt, 1e-3, p, u, false);
|
||||
d = pt.Distance(p);
|
||||
@@ -589,10 +592,10 @@ TopoDS_Shape IfcGeom::util::halfspace_from_plane(const gp_Pln& pln, const gp_Pnt
|
||||
|
||||
gp_Pln IfcGeom::util::plane_from_face(const TopoDS_Face& face) {
|
||||
BRepGProp_Face prop(face);
|
||||
Standard_Real u1, u2, v1, v2;
|
||||
double u1, u2, v1, v2;
|
||||
prop.Bounds(u1, u2, v1, v2);
|
||||
Standard_Real u = (u1 + u2) / 2.0;
|
||||
Standard_Real v = (v1 + v2) / 2.0;
|
||||
double u = (u1 + u2) / 2.0;
|
||||
double v = (v1 + v2) / 2.0;
|
||||
gp_Pnt p;
|
||||
gp_Vec n;
|
||||
prop.Normal(u, v, p, n);
|
||||
@@ -615,7 +618,7 @@ bool IfcGeom::util::is_compound_of_faces(const TopoDS_Shape& shape) {
|
||||
return has_compounds && has_faces && !has_solids && !has_shells;
|
||||
}
|
||||
|
||||
bool IfcGeom::util::shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfShape& li) {
|
||||
bool IfcGeom::util::shape_to_face_list(const TopoDS_Shape& s, NCollection_List<TopoDS_Shape>& li) {
|
||||
TopExp_Explorer exp(s, TopAbs_FACE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
TopoDS_Face face = TopoDS::Face(exp.Current());
|
||||
@@ -625,7 +628,7 @@ bool IfcGeom::util::shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfSha
|
||||
}
|
||||
|
||||
bool IfcGeom::util::create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& shape, double tol) {
|
||||
TopTools_ListOfShape face_list;
|
||||
NCollection_List<TopoDS_Shape> face_list;
|
||||
shape_to_face_list(compound, face_list);
|
||||
if (face_list.Extent() == 0) {
|
||||
return false;
|
||||
@@ -633,7 +636,7 @@ bool IfcGeom::util::create_solid_from_compound(const TopoDS_Shape& compound, Top
|
||||
return create_solid_from_faces(face_list, shape, tol);
|
||||
}
|
||||
|
||||
bool IfcGeom::util::create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& shape, double tol, bool force_sewing) {
|
||||
bool IfcGeom::util::create_solid_from_faces(const NCollection_List<TopoDS_Shape>& face_list, TopoDS_Shape& shape, double tol, bool force_sewing) {
|
||||
bool valid_shell = false;
|
||||
|
||||
if (face_list.Extent() == 1) {
|
||||
@@ -644,10 +647,10 @@ bool IfcGeom::util::create_solid_from_faces(const TopTools_ListOfShape& face_lis
|
||||
return false;
|
||||
}
|
||||
|
||||
TopTools_ListIteratorOfListOfShape face_iterator;
|
||||
NCollection_List<TopoDS_Shape>::Iterator face_iterator;
|
||||
|
||||
bool has_shared_edges = false;
|
||||
TopTools_MapOfShape edge_set;
|
||||
NCollection_Map<TopoDS_Shape, TopTools_ShapeMapHasher> edge_set;
|
||||
|
||||
// In case there are wire intersections or failures in non-planar wire triangulations
|
||||
// the idea is to let occt do an exhaustive search of edge partners. But we have not
|
||||
@@ -875,7 +878,7 @@ bool IfcGeom::util::validate_shape(const TopoDS_Shape& s) {
|
||||
std::function<void(const TopoDS_Shape&)> dump;
|
||||
dump = [&ana, &str, &dump, &any_emitted](const TopoDS_Shape& s) {
|
||||
if (!ana.Result(s).IsNull()) {
|
||||
BRepCheck_ListIteratorOfListOfStatus itl;
|
||||
NCollection_List<BRepCheck_Status>::Iterator itl;
|
||||
itl.Initialize(ana.Result(s)->Status());
|
||||
for (; itl.More(); itl.Next()) {
|
||||
if (itl.Value() != BRepCheck_NoError) {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
#include <gp_Trsf.hxx>
|
||||
#include <gp_GTrsf.hxx>
|
||||
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
@@ -49,8 +51,8 @@ namespace IfcGeom {
|
||||
// Creates a solid from a compound of faces. When there are multiple connected components,
|
||||
// a compound of solids is returned.
|
||||
IFC_GEOMLIBRARY_API bool create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& solid, double tol);
|
||||
IFC_GEOMLIBRARY_API bool shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfShape& li);
|
||||
IFC_GEOMLIBRARY_API bool create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& solid, double tol, bool force_sewing = false);
|
||||
IFC_GEOMLIBRARY_API bool shape_to_face_list(const TopoDS_Shape& s, NCollection_List<TopoDS_Shape>& li);
|
||||
IFC_GEOMLIBRARY_API bool create_solid_from_faces(const NCollection_List<TopoDS_Shape>& face_list, TopoDS_Shape& solid, double tol, bool force_sewing = false);
|
||||
IFC_GEOMLIBRARY_API bool is_compound_of_faces(const TopoDS_Shape& shape);
|
||||
IFC_GEOMLIBRARY_API bool is_convex(const TopoDS_Wire& wire, double tol);
|
||||
IFC_GEOMLIBRARY_API TopoDS_Shape halfspace_from_plane(const gp_Pln& pln, const gp_Pnt& cent);
|
||||
@@ -58,16 +60,16 @@ namespace IfcGeom {
|
||||
IFC_GEOMLIBRARY_API gp_Pnt point_above_plane(const gp_Pln& pln, bool agree = true);
|
||||
|
||||
IFC_GEOMLIBRARY_API bool fit_halfspace(const TopoDS_Shape& a, const TopoDS_Shape& b, TopoDS_Shape& box, double& height, double tol);
|
||||
IFC_GEOMLIBRARY_API const Handle_Geom_Curve intersect(const Handle_Geom_Surface&, const Handle_Geom_Surface&);
|
||||
IFC_GEOMLIBRARY_API const Handle_Geom_Curve intersect(const Handle_Geom_Surface&, const TopoDS_Face&);
|
||||
IFC_GEOMLIBRARY_API const Handle_Geom_Curve intersect(const TopoDS_Face&, const Handle_Geom_Surface&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const Handle_Geom_Curve&, const Handle_Geom_Surface&, gp_Pnt&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const Handle_Geom_Curve&, const TopoDS_Face&, gp_Pnt&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const Handle_Geom_Curve&, const TopoDS_Shape&, std::vector<gp_Pnt>&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const Handle_Geom_Surface&, const TopoDS_Shape&, std::vector< std::pair<Handle_Geom_Surface, Handle_Geom_Curve> >&);
|
||||
IFC_GEOMLIBRARY_API const opencascade::handle<Geom_Curve> intersect(const opencascade::handle<Geom_Surface>&, const opencascade::handle<Geom_Surface>&);
|
||||
IFC_GEOMLIBRARY_API const opencascade::handle<Geom_Curve> intersect(const opencascade::handle<Geom_Surface>&, const TopoDS_Face&);
|
||||
IFC_GEOMLIBRARY_API const opencascade::handle<Geom_Curve> intersect(const TopoDS_Face&, const opencascade::handle<Geom_Surface>&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const opencascade::handle<Geom_Curve>&, const opencascade::handle<Geom_Surface>&, gp_Pnt&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const opencascade::handle<Geom_Curve>&, const TopoDS_Face&, gp_Pnt&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const opencascade::handle<Geom_Curve>&, const TopoDS_Shape&, std::vector<gp_Pnt>&);
|
||||
IFC_GEOMLIBRARY_API bool intersect(const opencascade::handle<Geom_Surface>&, const TopoDS_Shape&, std::vector< std::pair<opencascade::handle<Geom_Surface>, opencascade::handle<Geom_Curve> > >&);
|
||||
IFC_GEOMLIBRARY_API bool closest(const gp_Pnt&, const std::vector<gp_Pnt>&, gp_Pnt&);
|
||||
IFC_GEOMLIBRARY_API bool project(const Handle_Geom_Curve&, const gp_Pnt&, gp_Pnt& p, double& u, double& d);
|
||||
IFC_GEOMLIBRARY_API bool project(const Handle_Geom_Surface&, const TopoDS_Shape&, double& u1, double& v1, double& u2, double& v2, double widen = 0.1);
|
||||
IFC_GEOMLIBRARY_API bool project(const opencascade::handle<Geom_Curve>&, const gp_Pnt&, gp_Pnt& p, double& u, double& d);
|
||||
IFC_GEOMLIBRARY_API bool project(const opencascade::handle<Geom_Surface>&, const TopoDS_Shape&, double& u1, double& v1, double& u2, double& v2, double widen = 0.1);
|
||||
|
||||
IFC_GEOMLIBRARY_API double shape_volume(const TopoDS_Shape& s);
|
||||
IFC_GEOMLIBRARY_API double face_area(const TopoDS_Face& f);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
for (;; ++it) {
|
||||
if (it == opening_vector.end() || jt->first / it->first > 10.) {
|
||||
|
||||
TopTools_ListOfShape opening_list;
|
||||
NCollection_List<TopoDS_Shape> opening_list;
|
||||
for (auto kt = jt; kt < it; ++kt) {
|
||||
opening_list.Append(kt->second);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ bool OpenCascadeKernel::convert_impl(const taxonomy::boolean_result::ptr br, Con
|
||||
const double tol = settings_.get<settings::Precision>().get();
|
||||
|
||||
TopoDS_Shape a;
|
||||
TopTools_ListOfShape b;
|
||||
NCollection_List<TopoDS_Shape> b;
|
||||
|
||||
taxonomy::style::ptr first_item_style;
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <BOPAlgo_Alerts.hxx>
|
||||
#include <ShapeFix_Shape.hxx>
|
||||
#include <BRepCheck_Analyzer.hxx>
|
||||
#include <BRepCheck_ListIteratorOfListOfStatus.hxx>
|
||||
#include <BRepCheck.hxx>
|
||||
#include <ShapeAnalysis_Edge.hxx>
|
||||
#include <Bnd_OBB.hxx>
|
||||
@@ -31,7 +30,7 @@
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
|
||||
void IfcGeom::util::copy_operand(const TopTools_ListOfShape & l, TopTools_ListOfShape & r) {
|
||||
void IfcGeom::util::copy_operand(const NCollection_List<TopoDS_Shape>& l, NCollection_List<TopoDS_Shape>& r) {
|
||||
#if OCC_VERSION_HEX < 0x70000
|
||||
r.Clear();
|
||||
TopTools_ListIteratorOfListOfShape it(l);
|
||||
@@ -81,7 +80,7 @@ double IfcGeom::util::min_edge_length(const TopoDS_Shape & a) {
|
||||
double IfcGeom::util::min_vertex_edge_distance(const TopoDS_Shape & a, double min_search, double max_search) {
|
||||
double M = std::numeric_limits<double>::infinity();
|
||||
|
||||
TopTools_IndexedMapOfShape vertices, edges;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> vertices, edges;
|
||||
|
||||
TopExp::MapShapes(a, TopAbs_VERTEX, vertices);
|
||||
TopExp::MapShapes(a, TopAbs_EDGE, edges);
|
||||
@@ -161,7 +160,7 @@ double IfcGeom::util::min_face_face_distance(const TopoDS_Shape & a, double max_
|
||||
*/
|
||||
double M = std::numeric_limits<double>::infinity();
|
||||
|
||||
TopTools_IndexedMapOfShape faces;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> faces;
|
||||
|
||||
TopExp::MapShapes(a, TopAbs_FACE, faces);
|
||||
|
||||
@@ -230,7 +229,7 @@ double IfcGeom::util::min_face_face_distance(const TopoDS_Shape & a, double max_
|
||||
return M;
|
||||
}
|
||||
|
||||
int IfcGeom::util::bounding_box_overlap(double p, const TopoDS_Shape & a, const TopTools_ListOfShape & b, TopTools_ListOfShape & c) {
|
||||
int IfcGeom::util::bounding_box_overlap(double p, const TopoDS_Shape & a, const NCollection_List<TopoDS_Shape> & b, NCollection_List<TopoDS_Shape> & c) {
|
||||
int N = 0;
|
||||
|
||||
Bnd_Box A;
|
||||
@@ -240,7 +239,7 @@ int IfcGeom::util::bounding_box_overlap(double p, const TopoDS_Shape & a, const
|
||||
return 0;
|
||||
}
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(b);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (; it.More(); it.Next()) {
|
||||
Bnd_Box B;
|
||||
BRepBndLib::Add(it.Value(), B);
|
||||
@@ -263,8 +262,8 @@ bool IfcGeom::util::get_edge_axis(const TopoDS_Edge & e, gp_Ax1 & ax) {
|
||||
double _, __;
|
||||
|
||||
auto crv = BRep_Tool::Curve(e, _, __);
|
||||
auto line = Handle_Geom_Line::DownCast(crv);
|
||||
auto bsple = Handle_Geom_BSplineCurve::DownCast(crv);
|
||||
auto line = opencascade::handle<Geom_Line>::DownCast(crv);
|
||||
auto bsple = opencascade::handle<Geom_BSplineCurve>::DownCast(crv);
|
||||
|
||||
if (line) {
|
||||
ax = line->Position();
|
||||
@@ -280,7 +279,7 @@ bool IfcGeom::util::get_edge_axis(const TopoDS_Edge & e, gp_Ax1 & ax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IfcGeom::util::is_subset(const TopTools_IndexedMapOfShape & lhs, const TopTools_IndexedMapOfShape & rhs) {
|
||||
bool IfcGeom::util::is_subset(const NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>& lhs, const NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>& rhs) {
|
||||
if (rhs.Extent() < lhs.Extent()) {
|
||||
return false;
|
||||
}
|
||||
@@ -297,12 +296,12 @@ bool IfcGeom::util::is_extrusion(const gp_Vec & v, const TopoDS_Shape & s, TopoD
|
||||
// This assumes UnifySameDomain has been processed on s, so that
|
||||
// the extrusion top and bottom are a single face.
|
||||
|
||||
TopTools_IndexedDataMapOfShapeListOfShape mapping;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> mapping;
|
||||
TopExp::MapShapesAndAncestors(s, TopAbs_EDGE, TopAbs_FACE, mapping);
|
||||
TopExp::MapShapesAndAncestors(s, TopAbs_VERTEX, TopAbs_FACE, mapping);
|
||||
|
||||
TopTools_ListOfShape parallel;
|
||||
TopTools_IndexedMapOfShape curved_orthogonal;
|
||||
NCollection_List<TopoDS_Shape> parallel;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> curved_orthogonal;
|
||||
gp_Ax1 ax;
|
||||
gp_Ax1 V(gp::Origin(), v);
|
||||
|
||||
@@ -333,9 +332,9 @@ bool IfcGeom::util::is_extrusion(const gp_Vec & v, const TopoDS_Shape & s, TopoD
|
||||
|
||||
// Select the two faces for which their edges are subsets
|
||||
// of the ortho/curved edges
|
||||
TopTools_IndexedMapOfShape ortho_faces;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> ortho_faces;
|
||||
for (TopExp_Explorer exp(s, TopAbs_FACE); exp.More(); exp.Next()) {
|
||||
TopTools_IndexedMapOfShape face_edges;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> face_edges;
|
||||
TopExp::MapShapes(exp.Current(), TopAbs_EDGE, face_edges);
|
||||
if (is_subset(face_edges, curved_orthogonal)) {
|
||||
ortho_faces.Add(exp.Current());
|
||||
@@ -349,18 +348,18 @@ bool IfcGeom::util::is_extrusion(const gp_Vec & v, const TopoDS_Shape & s, TopoD
|
||||
|
||||
// For the parallel edges assert that its two vertices are part
|
||||
// of both the basis and the top face.
|
||||
for (TopTools_ListIteratorOfListOfShape it(parallel);
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(parallel);
|
||||
it.More(); it.Next()) {
|
||||
TopoDS_Vertex v01[2];
|
||||
TopExp::Vertices(TopoDS::Edge(it.Value()), v01[0], v01[1]);
|
||||
|
||||
TopTools_IndexedMapOfShape v_ortho_faces;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> v_ortho_faces;
|
||||
int nb_ortho_faces[2] = { 0,0 };
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto& faces = mapping.FindFromKey(v01[i]);
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape jt(faces);
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator jt(faces);
|
||||
jt.More(); jt.Next()) {
|
||||
if (ortho_faces.Contains(jt.Value())) {
|
||||
nb_ortho_faces[i] ++;
|
||||
@@ -406,9 +405,9 @@ bool IfcGeom::util::is_extrusion(const gp_Vec & v, const TopoDS_Shape & s, TopoD
|
||||
return true;
|
||||
}
|
||||
|
||||
int IfcGeom::util::eliminate_narrow_operands(double prec, const TopTools_ListOfShape& bs, TopTools_ListOfShape & c) {
|
||||
int IfcGeom::util::eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c) {
|
||||
int N = 0;
|
||||
TopTools_ListIteratorOfListOfShape it(bs);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(bs);
|
||||
for (; it.More(); it.Next()) {
|
||||
|
||||
Bnd_OBB box;
|
||||
@@ -430,8 +429,8 @@ int IfcGeom::util::eliminate_narrow_operands(double prec, const TopTools_ListOfS
|
||||
return N;
|
||||
}
|
||||
|
||||
int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape & a, const TopTools_ListOfShape & bs, TopTools_ListOfShape & c) {
|
||||
TopTools_IndexedMapOfShape a_faces;
|
||||
int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape & a, const NCollection_List<TopoDS_Shape> & bs, NCollection_List<TopoDS_Shape> & c) {
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> a_faces;
|
||||
TopExp::MapShapes(a, TopAbs_FACE, a_faces);
|
||||
|
||||
// Check if any of the faces in a are non-planar, which is
|
||||
@@ -443,7 +442,7 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
}
|
||||
}
|
||||
|
||||
TopTools_IndexedMapOfShape a_vertices;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> a_vertices;
|
||||
TopExp::MapShapes(a, TopAbs_VERTEX, a_vertices);
|
||||
|
||||
IfcGeom::impl::tree<int> tree;
|
||||
@@ -455,13 +454,13 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
|
||||
int N = 0;
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(bs);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(bs);
|
||||
for (; it.More(); it.Next()) {
|
||||
bool is_touching = false;
|
||||
|
||||
auto& b = it.Value();
|
||||
|
||||
TopTools_IndexedMapOfShape b_faces;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> b_faces;
|
||||
TopExp::MapShapes(b, TopAbs_FACE, b_faces);
|
||||
|
||||
// Check if any of the faces in b are non-planar, which is
|
||||
@@ -479,7 +478,7 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
continue;
|
||||
}
|
||||
|
||||
TopTools_IndexedMapOfShape b_vertices;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> b_vertices;
|
||||
TopExp::MapShapes(b, TopAbs_VERTEX, b_vertices);
|
||||
|
||||
for (int k = 1; k <= b_faces.Extent(); ++k) {
|
||||
@@ -491,7 +490,7 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
for (auto& i : tree.select_box(B, false)) {
|
||||
const TopoDS_Face& f_a = TopoDS::Face(a_faces(i));
|
||||
|
||||
TopTools_IndexedMapOfShape f_a_vertices;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> f_a_vertices;
|
||||
TopExp::MapShapes(f_a, TopAbs_VERTEX, f_a_vertices);
|
||||
|
||||
BRepGProp_Face prop_a(f_a);
|
||||
@@ -532,7 +531,7 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
// Check if faces are co-planar
|
||||
if (std::abs((p_b.XYZ() - p_a.XYZ()).Dot(v_a.XYZ())) <= prec) {
|
||||
|
||||
TopTools_IndexedMapOfShape f_b_vertices;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> f_b_vertices;
|
||||
TopExp::MapShapes(f_b, TopAbs_VERTEX, f_b_vertices);
|
||||
|
||||
bool all_vertices_behind_f_b = true;
|
||||
@@ -574,13 +573,13 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
return N;
|
||||
}
|
||||
|
||||
bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_input, const TopTools_ListOfShape & b_input, TopoDS_Shape & result, double eps) {
|
||||
bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_input, const NCollection_List<TopoDS_Shape> & b_input, TopoDS_Shape & result, double eps) {
|
||||
IfcGeom::impl::tree<int> edge_tree;
|
||||
|
||||
TopTools_ListOfShape ab_input = b_input;
|
||||
NCollection_List<TopoDS_Shape> ab_input = b_input;
|
||||
ab_input.Prepend(a_input);
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(ab_input);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(ab_input);
|
||||
int shape_index = 0;
|
||||
int edge_index = 0;
|
||||
std::map<int, int> edge_index_to_shape_index;
|
||||
@@ -679,10 +678,10 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
// to see whether inside tolerance. Current DY is hardcoded. The sensible default
|
||||
// for walls.
|
||||
gp_Vec vec(p1, p2);
|
||||
Standard_Real d = vec.Dot(gp::DY());
|
||||
double d = vec.Dot(gp::DY());
|
||||
gp_Vec projected = d * gp::DY();
|
||||
gp_Vec ortho_remainder = vec - projected;
|
||||
Standard_Real ortho_distance = ortho_remainder.Magnitude();
|
||||
double ortho_distance = ortho_remainder.Magnitude();
|
||||
|
||||
const bool unbounded_intersects = ortho_distance < eps;
|
||||
if (unbounded_intersects) {
|
||||
@@ -722,15 +721,15 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
std::vector<TopoDS_Face> wire_faces;
|
||||
wire_faces.reserve(wires.size());
|
||||
|
||||
std::vector<BRepTopAdaptor_FClass2d> wire_clss;
|
||||
wire_clss.reserve(wires.size());
|
||||
std::vector<std::unique_ptr<BRepTopAdaptor_FClass2d>> wire_clss;
|
||||
wire_clss.reserve(wires.size());
|
||||
|
||||
std::vector<std::unique_ptr<ShapeAnalysis_Surface>> sass;
|
||||
sass.reserve(wires.size());
|
||||
|
||||
for (auto& w : wires) {
|
||||
wire_faces.push_back(BRepBuilderAPI_MakeFace(w).Face());
|
||||
wire_clss.emplace_back(wire_faces.back(), eps);
|
||||
wire_clss.emplace_back(std::make_unique<BRepTopAdaptor_FClass2d>(wire_faces.back(), eps));
|
||||
sass.push_back(std::make_unique<ShapeAnalysis_Surface>(BRep_Tool::Surface(wire_faces.back())));
|
||||
}
|
||||
|
||||
@@ -746,7 +745,7 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
|
||||
auto pnt = BRep_Tool::Pnt(v);
|
||||
auto p2d = sass[0]->ValueOfUV(pnt, eps);
|
||||
if (wire_clss[0].Perform(p2d) != TopAbs_IN) {
|
||||
if (wire_clss[0]->Perform(p2d) != TopAbs_IN) {
|
||||
// A wire is not contained in the outer wire, it's a subtraction without
|
||||
// any effect and marked as redundant. Feeding it to the builder algo
|
||||
// will likely cause problems.
|
||||
@@ -788,7 +787,7 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
|
||||
auto pnt = BRep_Tool::Pnt(v);
|
||||
auto p2d = sass[wire_index]->ValueOfUV(pnt, eps);
|
||||
if (wire_clss[wire_index].Perform(p2d) == TopAbs_IN) {
|
||||
if (wire_clss[wire_index]->Perform(p2d) == TopAbs_IN) {
|
||||
// A wire is contained within another operand
|
||||
redundant[other_index] = true;
|
||||
Logger::Root().Notice("GEO", 125, "Subtraction operand contained in other");
|
||||
@@ -833,7 +832,7 @@ bool IfcGeom::util::points_on_planar_face_generator::operator()(gp_Pnt& p) {
|
||||
}
|
||||
|
||||
|
||||
bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const TopoDS_Shape& a_input, const TopTools_ListOfShape& b_input, BOPAlgo_Operation op, TopoDS_Shape& result, double fuzziness) {
|
||||
bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const TopoDS_Shape& a_input, const NCollection_List<TopoDS_Shape>& b_input, BOPAlgo_Operation op, TopoDS_Shape& result, double fuzziness) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
const bool do_unify = true;
|
||||
@@ -878,7 +877,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
// @todo, it does seem a bit odd, we first triangulate non-planar faces
|
||||
// to later unify them again. Can we make this a bit more intelligent?
|
||||
TopoDS_Shape a;
|
||||
TopTools_ListOfShape b;
|
||||
NCollection_List<TopoDS_Shape> b;
|
||||
|
||||
if (do_unify) {
|
||||
PERF("boolean operation: unifying operands");
|
||||
@@ -894,7 +893,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
);
|
||||
|
||||
{
|
||||
TopTools_ListIteratorOfListOfShape it(b_input);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b_input);
|
||||
for (; it.More(); it.Next()) {
|
||||
b.Append(unify(it.Value(), fuzziness));
|
||||
Logger::Root().Message(
|
||||
@@ -915,7 +914,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
bool success = false;
|
||||
std::unique_ptr<BRepAlgoAPI_BooleanOperation> builder;
|
||||
TopTools_ListOfShape b_tmp;
|
||||
NCollection_List<TopoDS_Shape> b_tmp;
|
||||
|
||||
if (op == BOPAlgo_CUT) {
|
||||
builder.reset(new BRepAlgoAPI_Cut());
|
||||
@@ -973,7 +972,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
Logger::Root().Notice("GEO", 133, "Operand A is " + (is_manifold(a) ? ""s : "non-"s) + "manifold");
|
||||
}
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(b);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (int i = 0; it.More(); it.Next(), ++i) {
|
||||
Logger::Root().Notice("GEO", 134, "Operand B " + std::to_string(i) + " is " + (is_manifold(it.Value()) ? ""s : "non-"s) + "manifold");
|
||||
}
|
||||
@@ -987,7 +986,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
PERF("boolean operation: min edge length");
|
||||
|
||||
min_length_orig = min_edge_length(a);
|
||||
TopTools_ListIteratorOfListOfShape it(b);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (; it.More(); it.Next()) {
|
||||
double d = min_edge_length(it.Value());
|
||||
if (d < min_length_orig) {
|
||||
@@ -1004,7 +1003,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
min_length_orig = d;
|
||||
}
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(b);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (; it.More(); it.Next()) {
|
||||
d = min_vertex_edge_distance(it.Value(), settings.precision, min_length_orig);
|
||||
if (d < min_length_orig) {
|
||||
@@ -1020,14 +1019,14 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
const double new_fuzziness = fuzziness * 10.;
|
||||
const bool allow_retry = new_fuzziness - 1e-15 <= settings.precision * 10000. && new_fuzziness < min_length_orig;
|
||||
|
||||
TopTools_ListOfShape s1s;
|
||||
NCollection_List<TopoDS_Shape> s1s;
|
||||
s1s.Append(copy_operand(a));
|
||||
|
||||
if (debug) {
|
||||
TopTools_ListOfShape* lists[2] = { &s1s, &b };
|
||||
NCollection_List<TopoDS_Shape>* lists[2] = {&s1s, &b};
|
||||
static std::string operand_names[2] = { "a", "b" };
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
TopTools_ListIteratorOfListOfShape it(*lists[i]);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(*lists[i]);
|
||||
for (int j = 0; it.More(); it.Next(), ++j) {
|
||||
std::string fn = debug_identifier + "-" + operand_names[i] + "-" + std::to_string(j) + ".brep";
|
||||
BRepTools::Write(it.Value(), fn.c_str());
|
||||
@@ -1039,7 +1038,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
TopoDS_Face a_face;
|
||||
std::pair<double, double> a_interval;
|
||||
|
||||
TopTools_ListOfShape b_faces, b_remainder_3d;
|
||||
NCollection_List<TopoDS_Shape> b_faces, b_remainder_3d;
|
||||
|
||||
bool is_extrusion_a = false;
|
||||
if (do_attempt_2d_boolean) {
|
||||
@@ -1051,7 +1050,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
if (is_extrusion_a) {
|
||||
Logger::Root().Notice("GEO", 136, "Operand A 1/1 is an extrusion");
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(b);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (int nb = 1; it.More(); it.Next(), ++nb) {
|
||||
bool process_2d = false;
|
||||
TopoDS_Face b_face;
|
||||
@@ -1193,7 +1192,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
std::function<void(const TopoDS_Shape&)> dump;
|
||||
dump = [&ana, &str, &dump, &any_emitted](const TopoDS_Shape& s) {
|
||||
if (!ana.Result(s).IsNull()) {
|
||||
BRepCheck_ListIteratorOfListOfStatus itl;
|
||||
NCollection_List<BRepCheck_Status>::Iterator itl;
|
||||
itl.Initialize(ana.Result(s)->Status());
|
||||
for (; itl.More(); itl.Next()) {
|
||||
if (itl.Value() != BRepCheck_NoError) {
|
||||
@@ -1233,9 +1232,9 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
// An exemption for the requirement to be manifold: When the cut operands have overlapping edge belonging to faces that do not overlap.
|
||||
bool operands_nonmanifold = false;
|
||||
if (op == BOPAlgo_CUT) {
|
||||
TopTools_IndexedMapOfShape edges;
|
||||
TopTools_IndexedDataMapOfShapeListOfShape map;
|
||||
for (TopTools_ListIteratorOfListOfShape it2(b); it2.More(); it2.Next()) {
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> edges;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> map;
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it2(b); it2.More(); it2.Next()) {
|
||||
auto& bb = it2.Value();
|
||||
TopExp::MapShapes(bb, TopAbs_EDGE, edges);
|
||||
TopExp::MapShapesAndAncestors(bb, TopAbs_EDGE, TopAbs_FACE, map);
|
||||
@@ -1262,9 +1261,9 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
auto faces_i = map.FindFromKey(edges.FindKey(i));
|
||||
auto faces_j = map.FindFromKey(edges.FindKey(j));
|
||||
bool overlap = false;
|
||||
for (TopTools_ListIteratorOfListOfShape it4(faces_i); it4.More(); it4.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it4(faces_i); it4.More(); it4.Next()) {
|
||||
auto& fi = it4.Value();
|
||||
for (TopTools_ListIteratorOfListOfShape it2(faces_j); it2.More(); it2.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it2(faces_j); it2.More(); it2.Next()) {
|
||||
auto& fj = it2.Value();
|
||||
if (faces_overlap(TopoDS::Face(fi), TopoDS::Face(fj))) {
|
||||
overlap = true;
|
||||
@@ -1316,7 +1315,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
if (has_open_shells) {
|
||||
TopTools_IndexedMapOfShape faces;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> faces;
|
||||
TopExp::MapShapes(r, TopAbs_FACE, faces);
|
||||
for (TopExp_Explorer exp(a, TopAbs_FACE); exp.More(); exp.Next()) {
|
||||
auto& f = TopoDS::Face(exp.Current());
|
||||
@@ -1427,7 +1426,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const TopoDS_Shape& a, const TopoDS_Shape& b, BOPAlgo_Operation op, TopoDS_Shape& result, double fuzziness) {
|
||||
TopTools_ListOfShape bs;
|
||||
NCollection_List<TopoDS_Shape> bs;
|
||||
bs.Append(b);
|
||||
return boolean_operation(settings, a, bs, op, result, fuzziness);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,18 @@
|
||||
#define BOOLEAN_UTILS_H
|
||||
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
#include <TopTools_ShapeMapHasher.hxx>
|
||||
#include <NCollection_IndexedMap.hxx>
|
||||
|
||||
#include <Geom_Surface.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <BRepTopAdaptor_FClass2d.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
#include <BRepTools.hxx>
|
||||
#include <TopTools_IndexedMapOfShape.hxx>
|
||||
|
||||
#include <BOPAlgo_Operation.hxx>
|
||||
|
||||
#include "../ifc_geomlibrary_api.h"
|
||||
@@ -35,7 +40,7 @@
|
||||
namespace IfcGeom {
|
||||
namespace util {
|
||||
|
||||
void copy_operand(const TopTools_ListOfShape& l, TopTools_ListOfShape& r);
|
||||
void copy_operand(const NCollection_List<TopoDS_Shape>& l, NCollection_List<TopoDS_Shape>& r);
|
||||
|
||||
TopoDS_Shape copy_operand(const TopoDS_Shape& s);
|
||||
|
||||
@@ -73,26 +78,26 @@ namespace IfcGeom {
|
||||
|
||||
double min_face_face_distance(const TopoDS_Shape& a, double max_search);
|
||||
|
||||
int bounding_box_overlap(double p, const TopoDS_Shape& a, const TopTools_ListOfShape& b, TopTools_ListOfShape& c);
|
||||
int bounding_box_overlap(double p, const TopoDS_Shape& a, const NCollection_List<TopoDS_Shape>& b, NCollection_List<TopoDS_Shape>& c);
|
||||
|
||||
bool get_edge_axis(const TopoDS_Edge& e, gp_Ax1& ax);
|
||||
|
||||
bool is_subset(const TopTools_IndexedMapOfShape& lhs, const TopTools_IndexedMapOfShape& rhs);
|
||||
bool is_subset(const NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>& lhs, const NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>& rhs);
|
||||
|
||||
bool is_extrusion(const gp_Vec& v, const TopoDS_Shape& s, TopoDS_Face& base, std::pair<double, double>& interval);
|
||||
|
||||
int eliminate_touching_operands(double prec, const TopoDS_Shape& a, const TopTools_ListOfShape& bs, TopTools_ListOfShape& c);
|
||||
int eliminate_touching_operands(double prec, const TopoDS_Shape& a, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape>& c);
|
||||
|
||||
int eliminate_narrow_operands(double prec, const TopTools_ListOfShape& bs, TopTools_ListOfShape & c);
|
||||
int eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c);
|
||||
|
||||
bool boolean_subtraction_2d_using_builder(const TopoDS_Shape& a_input, const TopTools_ListOfShape& b_input, TopoDS_Shape& result, double eps);
|
||||
bool boolean_subtraction_2d_using_builder(const TopoDS_Shape& a_input, const NCollection_List<TopoDS_Shape>& b_input, TopoDS_Shape& result, double eps);
|
||||
|
||||
struct boolean_settings {
|
||||
bool debug, attempt_2d;
|
||||
double precision;
|
||||
};
|
||||
|
||||
bool boolean_operation(const boolean_settings& settings, const TopoDS_Shape&, const TopTools_ListOfShape&, BOPAlgo_Operation, TopoDS_Shape&, double fuzziness = -1.);
|
||||
bool boolean_operation(const boolean_settings& settings, const TopoDS_Shape&, const NCollection_List<TopoDS_Shape>&, BOPAlgo_Operation, TopoDS_Shape&, double fuzziness = -1.);
|
||||
|
||||
bool boolean_operation(const boolean_settings& settings, const TopoDS_Shape&, const TopoDS_Shape&, BOPAlgo_Operation, TopoDS_Shape&, double fuzziness = -1.);
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ using namespace IfcGeom;
|
||||
bool OpenCascadeKernel::convert(const taxonomy::bspline_surface::ptr bs, Handle(Geom_Surface) surf) {
|
||||
const bool is_rational = !!bs->weights;
|
||||
|
||||
TColgp_Array2OfPnt Poles(0, (int)bs->control_points.size() - 1, 0, (int)(*bs->control_points.begin()).size() - 1);
|
||||
TColStd_Array2OfReal Weights(0, (int)bs->control_points.size() - 1, 0, (int)(*bs->control_points.begin()).size() - 1);
|
||||
TColStd_Array1OfReal UKnots(0, (int)bs->knots[0].size() - 1);
|
||||
TColStd_Array1OfReal VKnots(0, (int)bs->knots[1].size() - 1);
|
||||
TColStd_Array1OfInteger UMults(0, (int)bs->multiplicities[0].size() - 1);
|
||||
TColStd_Array1OfInteger VMults(0, (int)bs->multiplicities[1].size() - 1);
|
||||
Standard_Integer UDegree = bs->degree[0];
|
||||
Standard_Integer VDegree = bs->degree[1];
|
||||
NCollection_Array2<gp_Pnt> Poles(0, (int)bs->control_points.size() - 1, 0, (int)(*bs->control_points.begin()).size() - 1);
|
||||
NCollection_Array2<double> Weights(0, (int)bs->control_points.size() - 1, 0, (int)(*bs->control_points.begin()).size() - 1);
|
||||
NCollection_Array1<double> UKnots(0, (int)bs->knots[0].size() - 1);
|
||||
NCollection_Array1<double> VKnots(0, (int)bs->knots[1].size() - 1);
|
||||
NCollection_Array1<int> UMults(0, (int)bs->multiplicities[0].size() - 1);
|
||||
NCollection_Array1<int> VMults(0, (int)bs->multiplicities[1].size() - 1);
|
||||
int UDegree = bs->degree[0];
|
||||
int VDegree = bs->degree[1];
|
||||
|
||||
int i = 0, j;
|
||||
for (auto it = bs->control_points.begin(); it != bs->control_points.end(); ++it, ++i) {
|
||||
|
||||
@@ -43,7 +43,7 @@ bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
double& at, double& au, double& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
const gp_Vec edge1 = vert1 - vert0;
|
||||
@@ -53,7 +53,7 @@ bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
|
||||
|
||||
// If determinant is near zero, ray lies in plane of triangle
|
||||
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
const double det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
|
||||
if(cull)
|
||||
{
|
||||
@@ -64,11 +64,11 @@ bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec tvec = orig - vert0;
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec);
|
||||
const double u = tvec.Dot(pvec);
|
||||
|
||||
const Standard_Real enlargeCoeff = enlarge*det;
|
||||
const Standard_Real uvlimit = -enlargeCoeff;
|
||||
const Standard_Real uvlimit2 = det + enlargeCoeff;
|
||||
const double enlargeCoeff = enlarge*det;
|
||||
const double uvlimit = -enlargeCoeff;
|
||||
const double uvlimit2 = det + enlargeCoeff;
|
||||
|
||||
if(u<uvlimit || u>uvlimit2)
|
||||
return false;
|
||||
@@ -77,14 +77,14 @@ bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec);
|
||||
const double v = dir.Dot(qvec);
|
||||
if(v<uvlimit || (u+v)>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Calculate t, scale parameters, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec);
|
||||
const double t = edge2.Dot(qvec);
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
const double inv_det = 1.0f / det;
|
||||
at = t*inv_det;
|
||||
au = u*inv_det;
|
||||
av = v*inv_det;
|
||||
@@ -95,26 +95,26 @@ bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
const double inv_det = 1.0 / det;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0f+enlarge)
|
||||
const double u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0+enlarge)
|
||||
return false;
|
||||
|
||||
// prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0f+enlarge)
|
||||
const double v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0+enlarge)
|
||||
return false;
|
||||
|
||||
// Calculate t, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec) * inv_det;
|
||||
const double t = edge2.Dot(qvec) * inv_det;
|
||||
|
||||
at = t;
|
||||
au = u;
|
||||
@@ -142,45 +142,45 @@ void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
// u parameterizes ray (q, b)
|
||||
|
||||
// Compute t for the closest point on ray (p, a) to ray (q, b)
|
||||
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
const double Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
Standard_Real t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0f)
|
||||
double t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
t = 0.0;
|
||||
|
||||
// find u for point on ray (q, b) closest to point at t
|
||||
Standard_Real u;
|
||||
if(BDotB!=0.0f)
|
||||
double u;
|
||||
if(BDotB!=0.0)
|
||||
{
|
||||
u = (t*ADotB - BDotT) / BDotB;
|
||||
|
||||
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
|
||||
if(u<0.0f)
|
||||
if(u<0.0)
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
u = 0.0;
|
||||
if(ADotA!=0.0)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
t = 0.0;
|
||||
}
|
||||
else if(u > 1.0f)
|
||||
else if(u > 1.0)
|
||||
{
|
||||
u = 1.0f;
|
||||
if(ADotA!=0.0f)
|
||||
u = 1.0;
|
||||
if(ADotA!=0.0)
|
||||
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
t = 0.0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
u = 0.0;
|
||||
if(ADotA!=0.0)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
t = 0.0;
|
||||
}
|
||||
|
||||
x = p + a * t;
|
||||
@@ -191,7 +191,7 @@ void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
{
|
||||
std::array<gp_Vec, 3> Sv;
|
||||
Sv[0] = p[1] - p[0];
|
||||
@@ -206,7 +206,7 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
gp_Vec minP, minQ;
|
||||
bool shown_disjoint = false;
|
||||
|
||||
float mindd = PX_MAX_F32;
|
||||
double mindd = PX_MAX_F32;
|
||||
|
||||
for(int i=0;i<3;i++)
|
||||
{
|
||||
@@ -214,7 +214,7 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
{
|
||||
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
|
||||
const gp_Vec V = cq - cp;
|
||||
const float dd = V.Dot(V);
|
||||
const double dd = V.Dot(V);
|
||||
|
||||
if(dd<=mindd)
|
||||
{
|
||||
@@ -226,12 +226,12 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
gp_Vec Z = p[id] - cp;
|
||||
float a = Z.Dot(V);
|
||||
double a = Z.Dot(V);
|
||||
id = j+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
Z = q[id] - cq;
|
||||
float b = Z.Dot(V);
|
||||
double b = Z.Dot(V);
|
||||
|
||||
if((a<=0.0f) && (b>=0.0f))
|
||||
return V.Dot(V);
|
||||
@@ -246,7 +246,7 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
}
|
||||
|
||||
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
|
||||
float Snl = Sn.Dot(Sn);
|
||||
double Snl = Sn.Dot(Sn);
|
||||
|
||||
if(Snl>1e-15f)
|
||||
{
|
||||
@@ -294,7 +294,7 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
}
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
float Tnl = Tn.Dot(Tn);
|
||||
double Tnl = Tn.Dot(Tn);
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
@@ -359,8 +359,8 @@ float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<g
|
||||
namespace {
|
||||
struct Interval
|
||||
{
|
||||
Standard_Real min;
|
||||
Standard_Real max;
|
||||
double min;
|
||||
double max;
|
||||
gp_Vec minPoint;
|
||||
gp_Vec maxPoint;
|
||||
|
||||
@@ -395,7 +395,7 @@ namespace {
|
||||
return result;
|
||||
}
|
||||
|
||||
void include(Standard_Real d, const gp_Vec& p)
|
||||
void include(double d, const gp_Vec& p)
|
||||
{
|
||||
if (d < min) { min = d; minPoint = p; }
|
||||
if (d > max) { max = d; maxPoint = p; }
|
||||
@@ -407,7 +407,7 @@ namespace {
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
static Interval computeInterval(double distanceA, double distanceB, double distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
{
|
||||
Interval i;
|
||||
|
||||
@@ -441,7 +441,7 @@ static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
double orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
{
|
||||
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
|
||||
}
|
||||
@@ -450,11 +450,11 @@ Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
double pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real ab = orient2d(a, b, point, x, y);
|
||||
const Standard_Real bc = orient2d(b, c, point, x, y);
|
||||
const Standard_Real ca = orient2d(c, a, point, x, y);
|
||||
const double ab = orient2d(a, b, point, x, y);
|
||||
const double bc = orient2d(b, c, point, x, y);
|
||||
const double ca = orient2d(c, a, point, x, y);
|
||||
|
||||
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
|
||||
return true;
|
||||
@@ -466,16 +466,16 @@ Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c,
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
double linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
|
||||
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
|
||||
const double aaS = orient2d(startA, endA, startB, x, y);
|
||||
const double aaE = orient2d(startA, endA, endB, x, y);
|
||||
|
||||
if ((aaS >= 0) == (aaE >= 0))
|
||||
return false;
|
||||
|
||||
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
|
||||
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
|
||||
const double bbS = orient2d(startB, endB, startA, x, y);
|
||||
const double bbE = orient2d(startB, endB, endA, x, y);
|
||||
|
||||
if ((bbS >= 0) == (bbE >= 0))
|
||||
return false;
|
||||
@@ -521,7 +521,7 @@ bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_V
|
||||
PxU32 y = 0;
|
||||
getProjectionIndices(p1_n, x, y);
|
||||
|
||||
const Standard_Real third = (1.0f / 3.0f);
|
||||
const double third = (1.0 / 3.0);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
@@ -541,14 +541,14 @@ bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_V
|
||||
// Also with minor modification to return intersection points.
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
|
||||
{
|
||||
const Standard_Real tolerance = 1e-8f;
|
||||
const double tolerance = 1e-8f;
|
||||
|
||||
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
|
||||
double p1_d = -a1.Dot(p1_n);
|
||||
// const PxPlane p1(a1, b1, c1);
|
||||
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
const double p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const double p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const double p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
|
||||
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
|
||||
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
|
||||
@@ -559,15 +559,15 @@ bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, co
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
const double p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const double p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const double p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
|
||||
const double l2 = intersectionDirection.SquareMagnitude();
|
||||
intersectionDirection *= 1.0f / std::sqrt(l2);
|
||||
|
||||
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
|
||||
|
||||
@@ -19,13 +19,13 @@ IFC_GEOMLIBRARY_API bool is_intersect_ray_box(const struct ray *ray, const struc
|
||||
|
||||
IFC_GEOMLIBRARY_API bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
double& at, double& au, double& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
IFC_GEOMLIBRARY_API void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
|
||||
|
||||
IFC_GEOMLIBRARY_API float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
IFC_GEOMLIBRARY_API double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
|
||||
IFC_GEOMLIBRARY_API bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
#include <ShapeFix_Shape.hxx>
|
||||
#include <ShapeFix_ShapeTolerance.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
#include <TopTools_DataMapOfShapeInteger.hxx>
|
||||
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopTools_ShapeMapHasher.hxx>
|
||||
#include <NCollection_DataMap.hxx>
|
||||
|
||||
#include <BRepLib_FindSurface.hxx>
|
||||
#include <ShapeExtend_MsgRegistrator.hxx>
|
||||
#include <Message_Msg.hxx>
|
||||
@@ -79,13 +84,13 @@ namespace {
|
||||
auto& umults = bs->multiplicities[0];
|
||||
auto& vmults = bs->multiplicities[1];
|
||||
|
||||
TColgp_Array2OfPnt Poles(0, (int)cps.size() - 1, 0, (int)cps[0].size() - 1);
|
||||
TColStd_Array1OfReal UKnots(0, (int)uknots.size() - 1);
|
||||
TColStd_Array1OfReal VKnots(0, (int)vknots.size() - 1);
|
||||
TColStd_Array1OfInteger UMults(0, (int)umults.size() - 1);
|
||||
TColStd_Array1OfInteger VMults(0, (int)vmults.size() - 1);
|
||||
Standard_Integer UDegree = bs->degree[0];
|
||||
Standard_Integer VDegree = bs->degree[1];
|
||||
NCollection_Array2<gp_Pnt> Poles(0, (int)cps.size() - 1, 0, (int)cps[0].size() - 1);
|
||||
NCollection_Array1<double> UKnots(0, (int)uknots.size() - 1);
|
||||
NCollection_Array1<double> VKnots(0, (int)vknots.size() - 1);
|
||||
NCollection_Array1<int> UMults(0, (int)umults.size() - 1);
|
||||
NCollection_Array1<int> VMults(0, (int)vmults.size() - 1);
|
||||
int UDegree = bs->degree[0];
|
||||
int VDegree = bs->degree[1];
|
||||
|
||||
int i = 0, j;
|
||||
for (auto it = cps.begin(); it != cps.end(); ++it, ++i) {
|
||||
@@ -275,7 +280,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
fd.surface() = convert_surface(face->basis);
|
||||
}
|
||||
|
||||
const int num_bounds = face->children.size();
|
||||
const size_t num_bounds = face->children.size();
|
||||
int num_outer_bounds = 0;
|
||||
|
||||
for (auto& bound : face->children) {
|
||||
@@ -298,7 +303,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
fd.all_outer() = true;
|
||||
}
|
||||
|
||||
TopTools_DataMapOfShapeInteger wire_senses;
|
||||
NCollection_DataMap<TopoDS_Shape, int, TopTools_ShapeMapHasher> wire_senses;
|
||||
|
||||
for (int process_interior = 0; process_interior <= 1; ++process_interior) {
|
||||
for (auto& bound : face->children) {
|
||||
@@ -334,7 +339,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
0.,
|
||||
settings_.get<settings::Precision>().get()
|
||||
};
|
||||
TopTools_ListOfShape results;
|
||||
NCollection_List<TopoDS_Shape> results;
|
||||
if (settings.use_wire_intersection_check && util::wire_intersections(wire, results, settings)) {
|
||||
Logger::Root().Warning("GEO", 161, "Self-intersections with " + boost::lexical_cast<std::string>(results.Extent()) + " cycles detected");
|
||||
util::select_largest(results, wire);
|
||||
@@ -400,7 +405,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
}
|
||||
}
|
||||
|
||||
TopTools_ListOfShape face_list;
|
||||
NCollection_List<TopoDS_Shape> face_list;
|
||||
|
||||
if (fd.surface().IsNull()) {
|
||||
// The set of wires is triangulated in case no surface can be found
|
||||
@@ -408,7 +413,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
|
||||
if (fd.all_outer()) {
|
||||
for (const auto& w : fd.wires()) {
|
||||
TopTools_ListOfShape fl;
|
||||
NCollection_List<TopoDS_Shape> fl;
|
||||
auto r = triangulate_wire({ w }, fl);
|
||||
if (r == util::TRIANGULATE_WIRE_FAIL) {
|
||||
continue;
|
||||
@@ -449,9 +454,9 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
sfs.SetMsgRegistrator(msg);
|
||||
sfs.Perform();
|
||||
|
||||
ShapeExtend_DataMapIteratorOfDataMapOfShapeListOfMsg jt(msg->MapShape());
|
||||
NCollection_DataMap<TopoDS_Shape, NCollection_List<Message_Msg>, TopTools_ShapeMapHasher>::Iterator jt(msg->MapShape());
|
||||
for (; jt.More(); jt.Next()) {
|
||||
Message_ListIteratorOfListOfMsg kt(jt.Value());
|
||||
NCollection_List<Message_Msg>::Iterator kt(jt.Value());
|
||||
for (; kt.More(); kt.Next()) {
|
||||
char* c = new char[kt.Value().Original().LengthOfCString() + 1];
|
||||
kt.Value().Original().ToUTF8CString(c);
|
||||
@@ -500,7 +505,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
// In case of (non-planar) face surface, p-curves need to be computed.
|
||||
// For planar faces, Open Cascade generates p-curves on the fly.
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(face_list); it.More(); it.Next()) {
|
||||
ShapeFix_Shape sfs(it.Value());
|
||||
|
||||
Handle(ShapeExtend_MsgRegistrator) msg;
|
||||
@@ -510,9 +515,9 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
sfs.Perform();
|
||||
it.Value() = sfs.Shape();
|
||||
|
||||
ShapeExtend_DataMapIteratorOfDataMapOfShapeListOfMsg jt(msg->MapShape());
|
||||
NCollection_DataMap<TopoDS_Shape, NCollection_List<Message_Msg>, TopTools_ShapeMapHasher>::Iterator jt(msg->MapShape());
|
||||
for (; jt.More(); jt.Next()) {
|
||||
Message_ListIteratorOfListOfMsg kt(jt.Value());
|
||||
NCollection_List<Message_Msg>::Iterator kt(jt.Value());
|
||||
for (; kt.More(); kt.Next()) {
|
||||
char* c = new char[kt.Value().Original().LengthOfCString() + 1];
|
||||
kt.Value().Original().ToUTF8CString(c);
|
||||
@@ -533,11 +538,11 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
}
|
||||
}
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(face_list); it.More(); it.Next()) {
|
||||
const TopoDS_Face& occ_face = TopoDS::Face(it.Value());
|
||||
|
||||
ShapeFix_Face sfs(TopoDS::Face(occ_face));
|
||||
TopTools_DataMapOfShapeListOfShape wire_map;
|
||||
NCollection_DataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> wire_map;
|
||||
sfs.FixOrientation(wire_map);
|
||||
|
||||
TopoDS_Iterator jt(occ_face, false);
|
||||
@@ -546,8 +551,8 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
// tfk: @todo if wire_map contains w, I would assume wire_senses also contains w,
|
||||
// this is not the case in github issue #405.
|
||||
if (wire_map.IsBound(w) && wire_senses.IsBound(w)) {
|
||||
const TopTools_ListOfShape& shapes = wire_map.Find(w);
|
||||
TopTools_ListIteratorOfListOfShape kt(shapes);
|
||||
const NCollection_List<TopoDS_Shape>& shapes = wire_map.Find(w);
|
||||
NCollection_List<TopoDS_Shape>::Iterator kt(shapes);
|
||||
for (; kt.More(); kt.Next()) {
|
||||
// Apparently the wire got reversed, so register it with opposite orientation in the map
|
||||
wire_senses.Bind(kt.Value(), wire_senses.Find(w) == TopAbs_FORWARD ? TopAbs_REVERSED : TopAbs_FORWARD);
|
||||
@@ -558,7 +563,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
it.Value() = sfs.Face();
|
||||
}
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(face_list); it.More(); it.Next()) {
|
||||
TopoDS_Face& occ_face = TopoDS::Face(it.Value());
|
||||
|
||||
bool all_reversed = true;
|
||||
@@ -582,7 +587,7 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
|
||||
TopoDS_Compound compound;
|
||||
BRep_Builder builder;
|
||||
builder.MakeCompound(compound);
|
||||
for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(face_list); it.More(); it.Next()) {
|
||||
TopoDS_Face& occ_face = TopoDS::Face(it.Value());
|
||||
builder.Add(compound, occ_face);
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ bool IfcGeom::OpenCascadeKernel::faceset_helper::edge(int A, int B, TopoDS_Edge&
|
||||
}
|
||||
|
||||
bool IfcGeom::OpenCascadeKernel::faceset_helper::wire(const ifcopenshell::geometry::taxonomy::loop::ptr loop, TopoDS_Wire& w) {
|
||||
TopTools_ListOfShape ws;
|
||||
NCollection_List<TopoDS_Shape> ws;
|
||||
if (!wires(loop, ws)) {
|
||||
return false;
|
||||
}
|
||||
@@ -249,7 +249,7 @@ bool IfcGeom::OpenCascadeKernel::faceset_helper::wire(const ifcopenshell::geomet
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IfcGeom::OpenCascadeKernel::faceset_helper::wires(const ifcopenshell::geometry::taxonomy::loop::ptr loop, TopTools_ListOfShape& wires) {
|
||||
bool IfcGeom::OpenCascadeKernel::faceset_helper::wires(const ifcopenshell::geometry::taxonomy::loop::ptr loop, NCollection_List<TopoDS_Shape>& wires) {
|
||||
if (duplicates_.find(loop->identity()) != duplicates_.end()) {
|
||||
return false;
|
||||
}
|
||||
@@ -270,7 +270,7 @@ bool IfcGeom::OpenCascadeKernel::faceset_helper::wires(const ifcopenshell::geome
|
||||
if (count >= 3) {
|
||||
wire.Closed(true);
|
||||
|
||||
TopTools_ListOfShape results;
|
||||
NCollection_List<TopoDS_Shape> results;
|
||||
if (!kernel_->settings().get<ifcopenshell::geometry::settings::NoWireIntersectionCheck>().get() && util::wire_intersections(wire, results, {
|
||||
!kernel_->settings().get<ifcopenshell::geometry::settings::NoWireIntersectionCheck>().get(),
|
||||
!kernel_->settings().get<ifcopenshell::geometry::settings::NoWireIntersectionTolerance>().get(), 0.,
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Iterator.hxx>
|
||||
#include <TopExp_Explorer.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
|
||||
#include <Bnd_Box.hxx>
|
||||
|
||||
@@ -43,14 +46,14 @@ namespace {
|
||||
}
|
||||
|
||||
#if OCC_VERSION_HEX >= 0x70200
|
||||
bool split(const TopoDS_Shape& input, const TopTools_ListOfShape& operands, double eps, std::vector<TopoDS_Shape>& slices) {
|
||||
bool split(const TopoDS_Shape& input, const NCollection_List<TopoDS_Shape>& operands, double eps, std::vector<TopoDS_Shape>& slices) {
|
||||
if (operands.Extent() < 2) {
|
||||
// Needs to have at least two cutting surfaces for the ordering based on surface containment to work.
|
||||
return false;
|
||||
}
|
||||
|
||||
BRepAlgoAPI_Splitter split;
|
||||
TopTools_ListOfShape input_list;
|
||||
NCollection_List<TopoDS_Shape> input_list;
|
||||
input_list.Append(input);
|
||||
split.SetArguments(input_list);
|
||||
split.SetTools(operands);
|
||||
@@ -66,7 +69,7 @@ namespace {
|
||||
|
||||
// NB 1, since first surface has been excluded
|
||||
int i = 1;
|
||||
for (TopTools_ListIteratorOfListOfShape it(operands); it.More(); it.Next(), ++i) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(operands); it.More(); it.Next(), ++i) {
|
||||
TopExp_Explorer exp(it.Value(), TopAbs_FACE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
surfaces.insert(std::make_pair(BRep_Tool::Surface(TopoDS::Face(exp.Current())).get(), i));
|
||||
@@ -164,21 +167,21 @@ namespace {
|
||||
}
|
||||
|
||||
|
||||
bool IfcGeom::util::apply_folded_layerset(const ConversionResults& items, const std::vector< std::vector<Handle_Geom_Surface> >& surfaces, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>& styles, ConversionResults& result, double tol) {
|
||||
bool IfcGeom::util::apply_folded_layerset(const ConversionResults& items, const std::vector< std::vector<opencascade::handle<Geom_Surface>>>& surfaces, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>& styles, ConversionResults& result, double tol) {
|
||||
Bnd_Box bb;
|
||||
TopoDS_Shape input;
|
||||
flatten_shape_list(items, input, false, false, tol);
|
||||
|
||||
typedef std::vector< std::vector<Handle_Geom_Surface> > folded_surfaces_t;
|
||||
typedef std::vector< std::vector<opencascade::handle<Geom_Surface>> > folded_surfaces_t;
|
||||
typedef std::vector< std::pair< TopoDS_Face, std::pair<gp_Pnt, gp_Pnt> > > faces_with_mass_t;
|
||||
|
||||
TopTools_ListOfShape shells;
|
||||
NCollection_List<TopoDS_Shape> shells;
|
||||
|
||||
for (folded_surfaces_t::const_iterator it = surfaces.begin(); it != surfaces.end(); ++it) {
|
||||
if (it->empty()) {
|
||||
continue;
|
||||
} else if (it->size() == 1) {
|
||||
const Handle_Geom_Surface& surface = (*it)[0];
|
||||
const opencascade::handle<Geom_Surface>& surface = (*it)[0];
|
||||
double u1, v1, u2, v2;
|
||||
if (!project(surface, input, u1, v1, u2, v2)) {
|
||||
continue;
|
||||
@@ -187,7 +190,7 @@ bool IfcGeom::util::apply_folded_layerset(const ConversionResults& items, const
|
||||
} else {
|
||||
faces_with_mass_t solids;
|
||||
for (folded_surfaces_t::value_type::const_iterator jt = it->begin(); jt != it->end(); ++jt) {
|
||||
const Handle_Geom_Surface& surface = *jt;
|
||||
const opencascade::handle<Geom_Surface>& surface = *jt;
|
||||
double u1, v1, u2, v2;
|
||||
if (!project(surface, input, u1, v1, u2, v2)) {
|
||||
continue;
|
||||
@@ -281,7 +284,7 @@ bool IfcGeom::util::apply_folded_layerset(const ConversionResults& items, const
|
||||
|
||||
}
|
||||
|
||||
bool IfcGeom::util::apply_layerset(const ConversionResults& items, const std::vector<Handle_Geom_Surface>& surfaces, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>& styles, ConversionResults& result, double tol) {
|
||||
bool IfcGeom::util::apply_layerset(const ConversionResults& items, const std::vector<opencascade::handle<Geom_Surface>>& surfaces, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>& styles, ConversionResults& result, double tol) {
|
||||
if (surfaces.size() < 3) {
|
||||
|
||||
return false;
|
||||
@@ -336,7 +339,7 @@ bool IfcGeom::util::apply_layerset(const ConversionResults& items, const std::ve
|
||||
const TopoDS_Shape& s = std::static_pointer_cast<OpenCascadeShape>(it->Shape())->shape();
|
||||
TopoDS_Shape sld = ensure_fit_for_subtraction(s, tol);
|
||||
|
||||
TopTools_ListOfShape operands;
|
||||
NCollection_List<TopoDS_Shape> operands;
|
||||
for (unsigned i = 1; i < surfaces.size() - 1; ++i) {
|
||||
double u1, v1, u2, v2;
|
||||
if (!project(surfaces[i], sld, u1, v1, u2, v2)) {
|
||||
@@ -370,7 +373,7 @@ bool IfcGeom::util::apply_layerset(const ConversionResults& items, const std::ve
|
||||
}
|
||||
|
||||
|
||||
bool IfcGeom::util::split_solid_by_surface(const TopoDS_Shape& input, const Handle_Geom_Surface& surface, TopoDS_Shape& front, TopoDS_Shape& back, double tol) {
|
||||
bool IfcGeom::util::split_solid_by_surface(const TopoDS_Shape& input, const opencascade::handle<Geom_Surface>& surface, TopoDS_Shape& front, TopoDS_Shape& back, double tol) {
|
||||
// Use an unbounded surface, that isolate part of the input shape,
|
||||
// to split this shape into two parts. Make sure that the addition
|
||||
// of the two result volumes matches that of the input.
|
||||
@@ -406,7 +409,7 @@ bool IfcGeom::util::split_solid_by_shell(const TopoDS_Shape& input, const TopoDS
|
||||
}
|
||||
|
||||
#if OCC_VERSION_HEX >= 0x70300
|
||||
TopTools_ListOfShape shapes;
|
||||
NCollection_List<TopoDS_Shape> shapes;
|
||||
#else
|
||||
BOPCol_ListOfShape shapes;
|
||||
#endif
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
namespace IfcGeom {
|
||||
namespace util {
|
||||
bool apply_layerset(const ConversionResults&, const std::vector<Handle_Geom_Surface>&, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>&, ConversionResults&, double tol);
|
||||
bool apply_layerset(const ConversionResults&, const std::vector<opencascade::handle<Geom_Surface>>&, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>&, ConversionResults&, double tol);
|
||||
|
||||
bool apply_folded_layerset(const ConversionResults&, const std::vector< std::vector<Handle_Geom_Surface> >&, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>&, ConversionResults&, double tol);
|
||||
bool apply_folded_layerset(const ConversionResults&, const std::vector<std::vector<opencascade::handle<Geom_Surface>>>&, const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>&, ConversionResults&, double tol);
|
||||
|
||||
bool split_solid_by_surface(const TopoDS_Shape&, const Handle_Geom_Surface&, TopoDS_Shape&, TopoDS_Shape&, double tol);
|
||||
bool split_solid_by_surface(const TopoDS_Shape&, const opencascade::handle<Geom_Surface>&, TopoDS_Shape&, TopoDS_Shape&, double tol);
|
||||
|
||||
bool split_solid_by_shell(const TopoDS_Shape&, const TopoDS_Shape& s, TopoDS_Shape&, TopoDS_Shape&, double tol);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
return true;
|
||||
}
|
||||
|
||||
TopTools_ListOfShape faces;
|
||||
NCollection_List<TopoDS_Shape> faces;
|
||||
TopoDS_Compound comp;
|
||||
BRep_Builder BB;
|
||||
BB.MakeCompound(comp);
|
||||
@@ -307,7 +307,7 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
all_tags.begin() + std::distance(shps.begin(), jt)};
|
||||
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
TopTools_IndexedDataMapOfShapeListOfShape ancestors;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> ancestors;
|
||||
const auto& wire = wp[i];
|
||||
auto& result = profile_points[i];
|
||||
|
||||
@@ -328,9 +328,9 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
break;
|
||||
}
|
||||
|
||||
const TopTools_ListOfShape& incidentEdges = ancestors.FindFromKey(curr);
|
||||
const NCollection_List<TopoDS_Shape>& incidentEdges = ancestors.FindFromKey(curr);
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape it(incidentEdges); it.More(); it.Next()) {
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(incidentEdges); it.More(); it.Next()) {
|
||||
const TopoDS_Edge& e = TopoDS::Edge(it.Value());
|
||||
|
||||
TopoDS_Vertex ev0, ev1;
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
#include <ShapeFix_ShapeTolerance.hxx>
|
||||
#include <BRepBuilderAPI_MakeWire.hxx>
|
||||
#include <gp_Pnt.hxx>
|
||||
#include <TColgp_Array1OfPnt.hxx>
|
||||
#include <TColStd_Array1OfReal.hxx>
|
||||
#include <TColStd_Array1OfInteger.hxx>
|
||||
#include <Geom_BSplineCurve.hxx>
|
||||
|
||||
#include <TopExp.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
|
||||
#include <Standard_Macro.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
|
||||
#include <BRepTools_WireExplorer.hxx>
|
||||
#include <BRepBuilderAPI_Transform.hxx>
|
||||
|
||||
@@ -39,12 +40,12 @@ namespace {
|
||||
|
||||
const bool is_rational = !!bc->weights;
|
||||
|
||||
TColgp_Array1OfPnt Poles(0, bc->control_points.size() - 1);
|
||||
TColStd_Array1OfReal Weights(0, bc->control_points.size() - 1);
|
||||
TColStd_Array1OfReal Knots(0, (int)bc->knots.size() - 1);
|
||||
TColStd_Array1OfInteger Mults(0, (int)bc->knots.size() - 1);
|
||||
Standard_Integer Degree = bc->degree;
|
||||
Standard_Boolean Periodic = false;
|
||||
NCollection_Array1<gp_Pnt> Poles(0, (int)bc->control_points.size() - 1);
|
||||
NCollection_Array1<double> Weights(0, (int)bc->control_points.size() - 1);
|
||||
NCollection_Array1<double> Knots(0, (int)bc->knots.size() - 1);
|
||||
NCollection_Array1<int> Mults(0, (int)bc->knots.size() - 1);
|
||||
int Degree = bc->degree;
|
||||
bool Periodic = false;
|
||||
// @tfk: it appears to be wrong to expect a period curve when the curve is closed, see #586
|
||||
// Standard_Boolean Periodic = l->ClosedCurve();
|
||||
|
||||
@@ -233,7 +234,7 @@ OpenCascadeKernel::curve_creation_visitor_result_type OpenCascadeKernel::convert
|
||||
#include "../../../ifcparse/IfcFile.h"
|
||||
|
||||
bool OpenCascadeKernel::convert(const taxonomy::loop::ptr loop, TopoDS_Wire& wire) {
|
||||
TopTools_ListOfShape converted_segments;
|
||||
NCollection_List<TopoDS_Shape> converted_segments;
|
||||
|
||||
for (auto& segment : loop->children) {
|
||||
TopoDS_Wire segment_wire;
|
||||
@@ -273,7 +274,7 @@ bool OpenCascadeKernel::convert(const taxonomy::loop::ptr loop, TopoDS_Wire& wir
|
||||
BRepBuilderAPI_MakeWire w;
|
||||
TopoDS_Vertex wire_first_vertex, wire_last_vertex, edge_first_vertex, edge_last_vertex;
|
||||
|
||||
TopTools_ListIteratorOfListOfShape it(converted_segments);
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(converted_segments);
|
||||
|
||||
bool force_close = false;
|
||||
if (loop->instance && loop->instance->as<IfcUtil::IfcBaseEntity>() && loop->instance->as<IfcUtil::IfcBaseEntity>()->file_) {
|
||||
@@ -287,10 +288,10 @@ bool OpenCascadeKernel::convert(const taxonomy::loop::ptr loop, TopoDS_Wire& wir
|
||||
shape_pair_enumerate(it, bld, force_close);
|
||||
wire = bld.wire();
|
||||
|
||||
TopTools_IndexedDataMapOfShapeListOfShape map;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> map;
|
||||
TopExp::MapShapesAndAncestors(wire, TopAbs_VERTEX, TopAbs_EDGE, map);
|
||||
|
||||
TopTools_IndexedMapOfShape edges_to_tesselate;
|
||||
NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher> edges_to_tesselate;
|
||||
|
||||
for (int i = 1; i <= map.Extent(); ++i) {
|
||||
auto& edges = map.FindFromIndex(i);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user