Commit Graph

21164 Commits

Author SHA1 Message Date
Petru Conduraru 69d12aa87e Bonsai: uniquify drawing name on rename so SVGs don't overwrite (#5635)
A drawing's SVG filename is derived from its Name
(get_default_drawing_path -> drawings/<name>.svg). New drawings are
de-duplicated by ensure_unique_drawing_name in core.add_drawing, but the
rename path core.update_drawing_name applied the raw name with no
uniqueness check. Renaming one drawing to another's name then made both
resolve to the same SVG path: update_drawing_name moved one SVG onto the
other and pointed both IfcDocumentReference.Location values at the same
file, overwriting the drawing and crossing the sheet references.

Give ensure_unique_drawing_name an optional `ignore` entity (so a drawing
can keep its own name) and call it from update_drawing_name with
ignore=drawing, mirroring add_drawing. Default stays None, so existing
callers are unchanged.

Verified live in headless Blender: renaming a second drawing to a taken
name now yields "<name>-X" and a distinct SVG path (paths no longer
collide). Core test_drawing.py::TestUpdateDrawingName passes; the tool
ensure_unique_drawing_name ignore case is confirmed live.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:51:35 +03:00
Andrej730 84b2cf6db0 dev-setup: use Python 3.13 bonsai-0.8.6-alpha2607221437 2026-07-22 18:47:44 +05:00
Andrej730 113643c916 dev_environment.py: add --skip-binaries flag 2026-07-22 18:36:24 +05:00
Andrej730 f0e6cfecc1 cmake: skip compiled extensions when installing ifcwrap sources 2026-07-22 18:31:48 +05:00
Andrej730 7338736898 ColumnPSetsOfSets.ifc: restore original schema
It seems it was switched to ifc2x3 by accident.
Related - a7738ee 6c590bf00
2026-07-22 17:24:40 +05:00
Andrej730 e9e2f89649 Revert "Sync ifcopenshell_wrapper.pyi with sync_stub.py"
This reverts commit b61f809731.

This commit was probably using not updated build, currently latest build is e333c1c and can confirm that it has `logger_or_root` added and `delete_same_facet_edge_pairs` removed.
bonsai-0.8.6-alpha2607221019
2026-07-22 15:19:16 +05:00
Petru Conduraru 4ceadd8f10 Fix IfcFooting Qto_FootingBaseQuantities axis mapping per predefined type #4783
Footings are authored two ways with different local axis conventions. Beam-like
footings (STRIP_FOOTING, FOOTING_BEAM) are a profile extruded along local Z, so
Length is local Z and the cross section sits on local X (Width, horizontal) and
local Y (Height, vertical). Slab-like footings (PAD_FOOTING, PILE_CAP) have their
footprint on local X/Y and their thickness (Height) on local Z.

The engine rule set is keyed per IfcFooting and cannot branch on predefined type,
so the previous static rule (Height=net_get_z, Length=net_get_max_xy, Width=null)
swapped Length and Height for beam-like footings and never emitted Width.

Add predefined-type-aware get_footing_length/width/height to the IfcOpenShell and
Blender calculators, and point the IfcFooting rule at them in all four IFC4/IFC4X3
ios/Blender rule files.

Confirmed by authoring footings through the real Bonsai generators and measuring
world-axis orientation: a beam-like footing with a 0.3 wide by 0.6 tall cross
section and 6.0 run reports Length 6.0, Width 0.3, Height 0.6, with the 0.3
physically horizontal and 0.6 physically vertical; a 2.0x1.5x0.3 pad reports
Length 2.0, Width 1.5, Height 0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607220505
2026-07-22 07:05:04 +02:00
falken10vdl 0cfc77030d Merge pull request #8843 from IfcOpenShell/bonsai-6680-material-rename
Bonsai: right-click Rename Material on material rows (#6680)
bonsai-0.8.6-alpha2607212131
2026-07-21 23:31:03 +02:00
Petru Conduraru efac8a0ec0 ifc5d: measure openings in their real orientation on both take-off engines
See #6835. Qto_OpeningElementBaseQuantities came out axis-scrambled for
openings authored in a Z-up local frame (X along the voided wall, Y
through it, Z vertical), which is how Bonsai authors every wall opening:

- The IfcOpenShell engine mapped Height to the local Y extent and Depth
  to the local Z extent, so a 0.9 x 2.0 door opening with Bonsai's
  default 1.2m void depth reported Height 1.2 and Depth 2.0, and Area
  (max side area) picked the through-wall side, 2.4 instead of 1.8.
  This matches the wrong Height=1.2/Area=1.2 screenshots reported for a
  1x1 window opening in #6835.
- The Blender engine mapped opening Width to get_length, which returns
  the longest bounding box edge, i.e. the opening height for typical
  door openings (the same defect 4adaf0d fixed for IfcDoor Width), and
  get_opening_depth used min(x, y), which returns the opening width
  whenever the width is smaller than the void depth.

The IfcOpenShell engine now has opening-aware internal calculators
(get_opening_width/height/depth/area) that detect horizontal (slab
style) openings with the same heuristic as the Blender calculator, so
slab opening depths keep reporting the slab thickness. The Blender
ruleset uses get_x for opening Width, and get_opening_depth measures the
through-element Y extent for vertical openings.

Door and window quantities themselves are addressed separately: the
Blender engine door Width was fixed in 4adaf0d, and the remaining
door/window defects (door not quantified on the IfcOpenShell engine,
inflated areas) are fixed by the attribute-based calculators in #8389.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607211958
2026-07-21 21:58:35 +02:00
Petru Conduraru 7ab0628c54 Bonsai: refresh material data unconditionally instead of forcing a redraw
falken10vdl reviewed 16b1b4e7b1 on #8843 and pointed out that tagging
every area for redraw was overkill. The actual problem was that the
Object Material panel and the scene Materials list read from plain
python caches (ObjectMaterialData and MaterialsData) that only get
invalidated when the Materials editing UI list is reloaded, which
never happens while you are not in editing mode. The redraw itself was
never the issue, closing the rename dialog already triggers one.

Removed the tag_redraw loop from RenameMaterial and instead call the
existing bonsai.bim.module.material.data.refresh() function from
core.rename_material, unconditionally, through a new tool.Material.refresh()
method. This is the same invalidate-on-next-load mechanism already used
by every other module's Data classes, just wired up for this operator
too, instead of introducing a new one.

Also updates the core tests to prescribe the new unconditional refresh()
call, and adds tool-layer coverage for tool.Material.refresh().

Generated with the assistance of an AI coding tool.
2026-07-21 21:08:21 +03:00
Petru Conduraru 16b1b4e7b1 Bonsai: refresh the UI after renaming a material
theoryshaw tested #8843 and asked for the new name to show up right
away instead of needing a manual refresh. The Object Material panel
and the scene Materials list both already re-read live IFC data on
their next draw (tool.Ifc.Operator purges those caches after every
IFC-mutating operator), so the button text was correct on the next
redraw. What was missing was the redraw itself: the material name is
a plain button label, not an RNA property Blender tracks, so nothing
told the Properties editor to repaint after the rename dialog closed.
Tag every area for redraw once the rename completes, the same pattern
used elsewhere in Bonsai for popup-triggered edits that need an
immediate repaint.

Also adds core-layer test coverage for rename_material, which had
none.

Generated with the assistance of an AI coding tool.
2026-07-21 17:11:56 +03:00
Petru Conduraru 8c667b8ae0 Bonsai: right-click rename on a material name (#6680)
Adds a "Rename Material" entry to the context menu that already
extends every button in the properties editor (UI_MT_button_context_menu),
triggered when right-clicking a material name button
(bim.select_by_material) that points to a real IfcMaterial. This
gives a quick entry point to renaming from the Object Material panel
without navigating to the scene Materials list.

This follows the pattern that #6680's thread converged on: theoryshaw
requested a right-click entry (rather than a pencil icon or
double-click) that keeps the existing single-click select-by-material
behaviour intact. falken10vdl is the issue's assignee; this is offered
as a starting point for that discussion, not a replacement for it.

Generated with the assistance of an AI coding tool.
2026-07-21 15:54:59 +03:00
Ryan Schultz e52e5e2e58 Bonsai: add category-level select-all to the Drawings list (#8826)
Add an "Is Selected" checkbox to each target-view category header in
BIM_UL_drawinglist that toggles selection for all drawings in the
category. The toggle only affects drawings currently visible in the
list (honoring the show_drawings_on_sheets_only filter), and the header
checkbox reflects the aggregate selection state of its drawings.

Also make category headers more obvious: wrap them in a box() for a
distinct inset background and make the header name clickable to
expand/contract the category (same as the disclosure triangle).

Ref: #8825

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607210139
2026-07-20 20:39:22 -05:00
Ryan Schultz 2d59ea1988 Bonsai: add toggle to show only drawings placed on sheets (#8824)
Adds a "Show Only Drawings on Sheets" toggle below the drawing list. When
enabled, the list is filtered to drawings referenced by at least one sheet
(target-view headers with no sheeted drawings are hidden too), and
bim.select_all_drawings only acts on the visible/filtered drawings.

A drawing is considered sheeted when its drawing document Location matches a
document reference Location on any SHEET-scoped IfcDocumentInformation.
Filtering is computed live so it reflects sheet edits without reloading.

Closes #8823

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607210004
2026-07-20 19:03:56 -05:00
Petru Conduraru 55a2430d71 docs: cover the Blender 5.1 / Python 3.13 transition in installation guides (#8781)
* docs: cover the Blender 5.1 / Python 3.13 transition in installation guides

The system requirements still listed Blender 4.3-4.5 with Python 3.11
only, and nothing documented the pitfall from issue 7623: importing
preferences into a Blender whose Python version changed carries over an
incompatible Bonsai build that silently fails to load. Document the two
Python generations, that Get Extensions picks the matching build
automatically while manual zip installs do not, and the
uninstall-reinstall step that resolves the upgrade case.

Generated with the assistance of an AI coding tool.

* docs: keep it simple, only Blender 5.1 and 5.2 with Python 3.13

Per review, drop the descriptive text and the Python 3.11 line.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607200628 bonsai-0.8.6-alpha2607200627
2026-07-20 16:27:51 +10:00
Petru Conduraru 04a2535a98 Preserve the real cause when the ifcopenshell wrapper fails to load (#8785)
* Keep real cause in wrapper ImportError

When the compiled wrapper exists for the current interpreter but fails
to load (for example a glibc version mismatch, as on AWS Lambda in
issue 5927), the bare except rewrote the error into the misleading
"IfcOpenShell not built for '<platform>'" message. Environments such
as AWS Lambda or the Blender add-on dialog only surface the final
exception message, so the actual cause was invisible and undiagnosable.

Keep the "not built for" message only when no matching binary is
present, and otherwise include the original loader error, chaining the
cause in both branches.

This change was AI-generated.

Fixes #5927

* Simplify wrapper import failure to a single message

Per review feedback, drop the filesystem scan and the two message
variants. Always raise the classic "IfcOpenShell not built for
'<platform>'" message with the original exception appended in
parentheses, still chained as the cause. Environments that only show
the final exception message (AWS Lambda, the Blender add-on dialog)
now surface the real loader error, such as the glibc version mismatch
in issue 5927, without any extra logic.

This change was AI-generated.
2026-07-20 16:27:22 +10:00
Petru Conduraru 727b5f3475 Bonsai: add Hour zoom level to the interactive Gantt chart
The jsGantt-improved library that renders Bonsai's Gantt chart already
ships full support for an "Hour" granularity (column width, header
labels in every bundled language, hour-aware rendering math). Bonsai's
config only exposed Day/Week/Month/Quarter, with a comment claiming
Hour caused browser issues even with vUseSingleCell enabled.

Headless Chrome testing against the same library version shows that
claim no longer holds once vUseSingleCell is active (as Bonsai already
configures it at 10000): Hour-format charts render without errors from
typical schedules up through fairly extreme ones (5000 tasks across a
3 year span rendered in about 2.4s). The failure mode the old comment
described only reproduces with vUseSingleCell disabled, which is not
how Bonsai runs it.

Task start/finish times already flow through to the chart unmodified
as raw ISO datetimes (tool/sequence.py create_new_task_json), so any
schedule authored with real hour-level timestamps, for example an
imported MS Project/P6/Excel schedule or one written directly through
ifcopenshell-python, can now be viewed at hour granularity. Verified
live with a night shift schedule crossing midnight, rendered correctly
with no console errors.

Note: Bonsai's own "Edit Task Time" UI currently always snaps
ScheduleStart/ScheduleFinish to 09:00/17:00 regardless of the hour
entered (ifcopenshell/api/sequence/edit_task_time.py), and work
calendars only encode working days, not working hours. So authoring a
genuine hour-precision schedule through that UI is still not possible;
this change only unlocks viewing hour-level data that already exists
in the model. Fixing the editor and calendar model is a separate,
larger design decision for a maintainer.

Addresses #2772.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607200229
2026-07-20 10:19:42 +10:00
Bartok a45f2fae61 docs(ifc2ca): fix script paths in README
Point scriptSalome.py at templates/salome/ and the bonded scripts at
_deprecated/, matching the current tree so README links resolve.

Generated with the assistance of an AI coding tool.
2026-07-20 09:53:35 +10:00
dependabot[bot] 1ae50b8cce build(deps): bump ruff from 0.15.12 to 0.15.22 (#8212)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 09:52:46 +10:00
dependabot[bot] 9fda996ebe build(deps): bump ruff from 0.15.12 to 0.15.22 (#8497)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 09:52:38 +10:00
dependabot[bot] 7a7a250942 build(deps): bump ruff from 0.15.12 to 0.15.22
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 09:52:07 +10:00
Petru Conduraru bc1fb2a88d Bonsai: add one click copy of annotations to another drawing (#8719)
* Bonsai: move annotations between drawings when reassigning their group

Assigning an IfcAnnotation to a group that represents another drawing
previously left the annotation in both drawings at once: it stayed in
its old drawing group, its Blender object stayed in the old drawing
collection, and it kept the old camera depth, so the reassignment
appeared to do nothing useful. Issue #2966 documents the seven step
manual workaround users needed instead.

The assign group operator now detects when the target group represents
a drawing (via the new tool.Drawing.get_group_drawing, the inverse of
get_drawing_group), unassigns the annotation from its previous drawing
group, moves its object into the new drawing collection, and places it
on the new drawing camera plane. The target camera is imported on
demand when it has not been loaded yet, matching the pattern used by
the activate drawing operator.

Generated with the assistance of an AI coding tool.

* Bonsai: add one click copy of annotations to another drawing (#2966)

Duplicating an annotation into a different drawing used to require a
seven step manual process: loading groups in scene properties, copying
the object, fixing its group assignment by hand, and repositioning it
onto the target camera plane. A plain Blender duplicate is not enough
because the copy keeps pointing at the same IFC entity, and the Shift D
override, while it does create a genuine new entity through
root.copy_class, leaves the duplicate in the source drawing group,
collection, and camera depth.

The new copy annotation to drawing operator packages the proven recipe
already used by duplicate drawing into one action: duplicate through
tool.Geometry.duplicate_ifc_objects, unassign the copy from the source
drawing group, assign it to the chosen target group, place it on the
target camera plane at the same world XY, and file it into the target
drawing collection. The originals are left untouched and the user's
selection is restored. The target camera is imported on demand when it
has not been loaded yet.

The operator shows a target drawing dropdown and is reachable from the
annotation tool sidebar when an annotation is selected, and from the
drawings panel. Annotations already in the target drawing are skipped
and reported.

The orchestration lives in core.drawing.copy_annotations_to_drawing
with prophecy tests covering the copy, the skip, and the camera import
branches. Verified live in headless Blender 5.1: the copy is a new
IfcAnnotation with its own GlobalId and IfcTextLiteral, both texts are
editable independently, and everything survives save and reload with
each annotation loading in its own drawing.

Generated with the assistance of an AI coding tool.
2026-07-20 09:50:14 +10:00
Petru Conduraru c55a79b8b5 bonsai: allow overriding which classes join in section linework (#4395) (#8617)
Fixes #4395.

Root cause: the SVG cut-linework merge step that fuses adjacent
elements' cut polygons together (per the pset-driven JoinCriteria
setting) was hardcoded to only IfcWall and IfcSlab. IfcCovering cut
shapes were skipped unconditionally, so adjacent coverings never
joined, leaving a visible seam/broken corner in section drawings
regardless of JoinCriteria.

Fix: added an EPset_Drawing.JoinClasses property, following the
exact same user-overridable pattern already used by
EPset_Drawing.BringToFront - a comma-separated list of IFC classes
to join, defaulting to "IfcWall,IfcSlab" (unchanged behavior) when
unset. Users can override per-drawing to add IfcCovering (or any
other class) when they want it joined too. Kept this opt-in rather
than hardcoding IfcCovering into the default list, since joining a
thin finish layer the same way as a thick wall/slab could produce
unwanted mitring in some cases - the user decides per drawing.

Verified live against the reporter's own attached file
(ifcovering joining.ifc) and its cached section linework: with
JoinClasses unset, two separate closed paths reproduce the reported
seam exactly. With JoinClasses = "IfcWall,IfcSlab,IfcCovering", the
two coverings merge into a single closed polygon with the internal
seam removed. Confirmed IfcSlab join behavior is unchanged in both
runs.

Generated with the assistance of an AI coding tool.

Co-authored-by: Dion Moult <dionmoult@gmail.com>
2026-07-20 09:41:09 +10:00
sboddy b669baf793 Propagate deflection settings on reload (#8484)
reimport_element_representations() built a fresh
ifcopenshell.geom.settings() without copying deflection_tolerance /
angular_tolerance from the IfcImportSettings it had just
constructed, and never passed geometry_library to either the
iterator() or create_shape() calls it makes. As a result, exiting
Item/edit mode (which reaches this function via
switch_representation) silently fell back to IfcOpenShell's
hard-coded mesher defaults (0.001 linear deflection, ~50x finer than
the project's default of 0.05) and the default geometry kernel,
instead of the project's configured tolerance and Geometry Library.

This made geometry visibly change quality after a no-op Tab into and
back out of edit mode, since the reload path was unintentionally far
more precise (and used a different kernel) than the initial import.
Both settings, and geometry_library, are now taken from the
IfcImportSettings instance already built at the top of the function,
so a reload matches the original import.

Refs #5685.

Generated with the assistance of an AI coding tool.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 09:37:04 +10:00
Petru Conduraru 32ac20e8e3 Bonsai: refresh the arc/circle decorator immediately after duplicating a loop
theoryshaw's follow-up on #6944: after the profile/curve reconstruction fix
(previous commit), the arc/circle marker for a freshly Shift+D-duplicated
loop wouldn't appear until leaving and re-entering Edit Mode.

Root cause: ProfileDecorator groups arc/circle vertices purely by
IFCARCINDEX/IFCCIRCLE vertex-group index every draw call (it has no cache
to go stale, it fully recomputes from the live edit-mesh bmesh each frame).
Duplicating a loop copies its vertex-group weights onto the new geometry,
since Blender allocates no new group for a duplicate, so the source loop
and its live duplicate land in the same dict entry. That entry then fails
the "exactly 2 verts per circle / 3 per arc" check and is skipped entirely,
so BOTH the original and the duplicate stop being drawn until the mesh is
reimported and gets fresh, distinct groups.

Verified live in headless Blender: built a bmesh with an IFCCIRCLE loop and
an IFCARCINDEX loop, then ran bmesh.ops.duplicate on each (the same
bmesh-level operation underlying Shift+D) and called ProfileDecorator's
draw method directly. Before this change, duplicating either loop dropped
both the original and the duplicate from the decorator (0 circle/arc
batches drawn instead of 2). After, both draw immediately, with no change
to the non-duplicated case (still 1) or to genuinely distinct loops (5
independent circles still resolve to 5, not merged). 500-circle timing is
unchanged (~14.3ms/draw before and after), so the added connectivity split
is not a hot-path regression.

Added test/bim/module/model/test_profile_decorator_duplicate_loop.py
pinning the new _connected_components helper's behavior for single and
duplicated circle/arc loops.

This contribution was produced with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 0d5ea02169 Bonsai: fix the same duplicate-loop vertex-group bug in auto_detect_curves
auto_detect_profiles had the identical defect fixed in the previous
commit: duplicating a circle/arc loop in Edit Mode reuses the same
IFCCIRCLE/IFCARCINDEX vertex group index for the new geometry, and this
sibling function (used for curve/annotation editing rather than profile
voids) tallied group membership across the whole mesh instead of per
loop, so it also rejected a legitimately duplicated loop as malformed.

Applied the identical fix: scope the group-count sanity check to each
connected edge loop, computed after the loops are built rather than in
the initial whole-mesh vertex pass. Kept the existing forked-loop check
(more than 2 edges per vertex) in the first pass since it is unrelated
to group counting.

Verified live in headless Blender: constructed two 2-vertex IFCCIRCLE
loops sharing one vertex group index (the exact state Blender's Edit
Mode duplicate produces) and called auto_detect_curves directly.
Before this change it returned (False, "CIRCLE"); after, it returns two
valid IfcCircle curves.

Generated with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 0a027d47a3 Bonsai: fix profile reconstruction after duplicating a circle/arc in Edit Mode
Duplicating a circular or filleted-arc void in the profile CAD editor
(Shift+D on the loop's vertices) reused the same IFCCIRCLE/IFCARCINDEX
vertex group index for the new geometry, since Blender's mesh duplicate
copies vertex group weights but does not allocate a new group. On exit
from Edit Mode, auto_detect_profiles tallied group membership across the
whole mesh rather than per loop, so a group meant to hold exactly 2 (circle)
or 3 (arc) vertices ended up with double that, failing its sanity check
and blocking the edit with an "INVALID PROFILE" popup. Fixes #6944.

Scope the sanity check to each connected edge loop instead, matching how
the loops are actually converted into IfcCircle/arc segments below. Also
explicitly reject an arc/circle vertex tagged onto an isolated vertex with
no edges at all, which the old whole-mesh count also caught.

Verified live in headless Blender against the issue's repro file
(IfcFurniture "Slab.004", IfcArbitraryProfileDefWithVoids with three
IfcCircle voids): entering the profile editor, duplicating one void's
2-vertex loop and moving it produced an "INVALID PROFILE" popup before
this change, and now produces a valid profile (the original 3 voids
intact, plus the duplicate as a 4th void or a separate solid profile
depending on whether it still falls inside the outer boundary).
test/tool/test_model.py passes unchanged (32 passed, 1 pre-existing
unrelated failure present on both before and after).

Generated with the assistance of an AI coding tool.
2026-07-20 09:34:07 +10:00
Petru Conduraru 6014bbd877 ci-lint: black-format two files that drifted on v0.8.0
Both files were merged unformatted and fail the Black formatter step
on every branch, keeping ci-lint red repo-wide.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607192322 bonsai-0.8.6-alpha2607192327 bonsai-0.8.6-alpha2607192323
2026-07-20 09:24:34 +10:00
Petru Conduraru 6d6d92b849 Fix all remaining ty type-check failures on ci-lint
The ci-lint workflow's ty steps fail on every branch because base
v0.8.0 has four diagnostics.

ty check (bonsai):

- root/operator.py: bpy.data.objects.get() can return None, so
  UnlinkObject._execute could put None in its objects list and crash
  on the first attribute access when an unknown object name is passed.
  Handle the miss explicitly, which also satisfies the declared
  list[bpy.types.Object] type.
- tool/sequence.py: ty does not narrow Literal types through
  membership tests on list literals, so the assert_never() exhaustive
  check was flagged. Use tuple literals, which ty narrows, keeping the
  exhaustiveness check intact.

ty check (ios):

- draw.py: arrange_polygons was called through conditional argument
  splats that let the same call site work against pre-April-2026
  wrappers lacking arrange_polygon_settings and the logger parameter.
  No runtime bug for current builds, but the dynamic splats cannot be
  typed against the fixed 3-parameter signature. Drop the old-build
  workaround and call the current signature directly, following the
  precedent of 3d8115ebc5 which dropped similar old-build workarounds
  in ifcopenshell.file. Verified against a current wrapper build that
  the direct call arranges polygons and serializes to SVG, with and
  without a logger.
- Optimise.py: igraph is an optional dependency with a guarded import
  and a toposort fallback, but it was missing from the ios type-check
  venv so ty could not resolve it. Add it to type-check-requirements
  next to the toposort fallback that is already listed.

After this, poe ty-bonsai and poe ty-ios both pass cleanly.

Generated with the assistance of an AI coding tool.
2026-07-20 09:23:48 +10:00
Petru Conduraru bb49822f2e Bonsai: make dxf2ifc.py example script skip unsupported DXF entities
The script called Polyline.get_mode() on every modelspace entity, but
that method only exists on POLYLINE entities, so any typical DXF
containing lines, circles or text crashed with AttributeError before
converting anything. Test for POLYLINE polyface meshes with
dxftype()/is_poly_face_mesh instead and skip other entities with a
message, only create the spatial containment relation when products
exist, and take the input/output paths from the command line (matching
obj2ifc.py) instead of a hardcoded input.dxf/test.ifc.

Fixes #2151

This change was written with the assistance of an AI coding tool.
2026-07-20 09:22:44 +10:00
Bruno Postle 21ea58b0e6 Fix Bonsai polyline not enough values to unpack error
Typo was introduced in b35f99e
bonsai-0.8.6-alpha2607192250
2026-07-19 23:50:19 +01:00
Ryan Schultz b66d8b2c4d Fix #6652: Extend grab selection to include BBIM_Array members (#7968)
When grabbing an array child, the selection now expands to include
the array parent and all sibling children before the move operator
runs. Mirrors existing behavior for aggregates and nests.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607191900
2026-07-19 14:00:44 -05:00
Stephen Boddy f9be61c10b Bump build 821cf7b > e333c1c bonsai-0.8.6-alpha2607191816 2026-07-19 18:47:57 +01:00
Stephen Boddy b61f809731 Sync ifcopenshell_wrapper.pyi with sync_stub.py
Ran the new sync_stub.py against a real local build: adds
context.delete_same_facet_edge_pairs (present on the compiled wrapper,
missing from the stub) and drops the module-level logger_or_root
(present in the stub, no longer exists on the wrapper at all).

Nothing else changes - no license header rewrite, no docstring loss,
none of the 14 hand-curated named-parameter constructor/function
signatures touched, unlike the wholesale regeneration this replaces.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607191513
2026-07-19 16:13:13 +01:00
Stephen Boddy 948ffce7e9 Add sync_stub.py, a minimal-diff stub syncer
generate_stub.py (this branch's earlier commit) regenerates
ifcopenshell_wrapper.pyi wholesale from the compiled wrapper: it
reliably fixes real drift, but it also discards everything that isn't
mechanically recoverable from the wrapper alone - the license header,
docstrings, and hand-curated named-parameter signatures for
SWIG-overloaded constructors/functions (SWIG itself always emits
generic `*args` for those, so a regenerator can't tell a deliberate
curation from real drift and just overwrites it).

sync_stub.py takes the smaller-blast-radius approach: it only adds
top-level symbols/class members that are genuinely missing, and only
removes ones that are genuinely gone, cross-checking against
validate_stub.py's own full canonicalisation (via the newly-exposed
get_names_tree()) so it never mistakes a property()/staticmethod()-
wrapped member for something absent just because its own narrower
parser skips that form. Anything that exists on both sides under the
same name but with a different signature - exactly where curation
lives - is left untouched and reported for a human to review instead
of guessed at.

Verified against a real local build: applying it to the current
ifcopenshell_wrapper.pyi produces a small, targeted diff (add one
missing method, drop one stale function) with the license header,
docstrings, and all 14 curated constructor/function signatures
preserved byte-for-byte, versus generate_stub.py's ~1000-line
wholesale rewrite for the same underlying fix.

Generated with the assistance of an AI coding tool.
2026-07-19 16:13:13 +01:00
Petru Conduraru c68e4a0eee Size entity attribute storage to schema arity, not token count
When a STEP instance has fewer attribute tokens than its schema declares
(commonly from corrupted/malformed syntax), parse_context::construct()
sized the in-memory attribute storage to the smaller token count instead
of the schema's attribute count. This left the storage's last N attribute
slots simply nonexistent rather than blank, so any later read of one of
those trailing attributes by index threw an uncaught IfcParse::IfcException
("Index N is out of range for storage of size N") that terminated the
whole process (SIGABRT) instead of being handled as a parse warning.

Fix: when the schema declaration is known, size the storage to the
schema's attribute count. Indices beyond the number of tokens found are
left at their existing default-constructed blank value (the storage
constructor already blank-initializes every slot), so a truncated
instance now degrades to blank values for its missing trailing
attributes, matching the parser's existing "expected N attribute values,
found M" warning intent instead of crashing.

Reproduced with the fuzzing script attached to #5679: single-byte
mutations of a minimal IFC4 file that corrupt the IFCPROJECT instance's
token stream reliably aborted IfcConvert with this exact exception before
the fix, and now parse with a logged syntax error and exit code 0.

Fixes #5679

Generated with the assistance of an AI coding tool.
2026-07-19 10:29:48 +02:00
Petru Conduraru 6603c8459a Fix pythonocc-core viewer compatibility in geom.occ_utils and geom.app (#1037, #1098)
set_shape_transparency() called AIS_InteractiveContext.SetTransparency(),
whose argument count is inconsistent across pythonocc-core versions
(reported as a TypeError in #1037). Set transparency directly on the AIS
object instead, the same stable pattern already used elsewhere in this
file (display_shape() calls ais.SetTransparency() directly, never through
the Context), then call Context.UpdateCurrentViewer() to refresh.

app.py's viewer used a "SetSelectionPriority(counter)"/"SelectionPriority()"
pair as an ad hoc unique key to map a displayed AIS object back to its IFC
product. On modern pythonocc-core this crashed with AttributeError because
.GetObject() (needed to unwrap the old handle-based API) no longer exists
on AIS objects (#1098, PR #1113 partially patched one of the two call
sites but left the one in HandleSelection unguarded).

Live pythonocc-core 7.9.3 testing showed the GetObject() guard alone is
not sufficient: SetSelectionPriority/SelectionPriority themselves have
been removed from AIS_InteractiveObject entirely in modern OCCT (only
AIS_Trihedron keeps a same-named but unrelated method for datum parts),
so gating the .GetObject() call with the existing USE_OCCT_HANDLE flag
would still crash the first time a shape is selected. Verified live that
AIS objects retain correct __eq__/__hash__ (matching the underlying OCCT
instance) across separate SWIG wrapper instances, so ais_to_product is
now keyed directly by the AIS object itself, removing the dependency on
the removed OCCT API and the GetObject()/handle distinction altogether.

Verified live against pythonocc-core 7.9.3 (conda-forge) using real
AIS_Shape objects obtained from ifcopenshell.geom.occ_utils.display_shape()
and a real IFC file: reproduced both the original TypeError (#1037) and
AttributeError (#1098), confirmed both fixes resolve them, and confirmed
the ais_to_product dict lookup round trips correctly through a real
Context.Select()/SelectedInteractive() call. Could not exercise the full
Qt-embedded viewer.finished()/HandleSelection() flow end to end because
this pythonocc-core build segfaults natively when creating a second GL
context inside a Qt widget on this macOS host, a pre-existing environment
issue unrelated to this diff (reproduces identically with unpatched code,
before any touched line executes).

AI-generated, reviewed and tested by Petru Conduraru.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607190810
2026-07-19 10:10:09 +02:00
Petru Conduraru 824c1fc280 ifcwrap: keep geometry's owning element alive to fix silent data corruption (#1124)
create_shape() returns a Python-owned Element (SWIG_POINTER_OWN in the
boost::variant out typemap). Its .geometry property calls Element::geometry(),
which returns a reference into the element's boost::shared_ptr<Representation>
_geometry member. SWIG wraps that reference as a non-owning pointer, so the
returned Triangulation/BRep/Serialization proxy does not keep the element alive.

When a caller keeps only .geometry (e.g. create_shape(s, e).geometry) and drops
the parent element, Python garbage-collects the element, destroying its
shared_ptr and freeing the underlying representation. Subsequent reads of
verts/faces then return freed memory: empty or implausible float/int garbage,
non-deterministically depending on GC and allocator timing. This is silent data
corruption, not a crash, and has bitten users since 2020.

Fix: in the TriangulationElement/SerializedElement/BRepElement pythoncode, wrap
the geometry getter so the returned geometry stores a backreference to its
owning element (result._parent = self). This makes the parent's lifetime at
least as long as the geometry's, automatically and transparently, so no caller
has to remember to hold the element. This is aothms's suggested backreference,
applied generically in the binding rather than left as a workaround.

Reproduced deterministically (washBasin fixture): before, verts len 0 vs 133500
across repeated GC-pressure runs; after, 133500 every run for all three element
types. test_create_shape passes; no regressions.

Note: tree.select_ray()'s ray_intersection_result (2024 follow-up in #1124) is a
separate ownership mechanism (std::vector element reference + std::array member
pointer) and is left as follow-up scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607190804
2026-07-19 10:04:48 +02:00
carlopav 705af7ba3a drawing: compute cut/fill intersection once per CutDecorator object
recalculate_cut() and recalculate_fill() each ran is_intersecting_camera(),
which builds a bmesh and scans every vertex. When a redraw recalculated both
(camera moved, cache miss, or the object selected) that was two full
intersection tests per object per frame for the same answer.

Compute it once in decorate() and pass it to both, and skip the test
entirely when neither recalculation is needed. Never more tests than before,
identical result since the camera can't move within a frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607190415
2026-07-19 14:15:42 +10:00
carlopav 074fc26e8f drawing: evaluate camera movement once per CutDecorator redraw
is_camera_moved() runs eval()/numpy over the camera matrix and, as a side
effect, refreshes the stored checksum the first time it returns True. It was
called up to twice per object inside decorate(), so on a frame where the
camera actually moved the first call updated the checksum and every later
call - the fill check on the same object, and both checks on all remaining
objects - then saw an already-current checksum and returned False. Only the
first object's cut got recalculated; its fill and every other element stayed
stale until something else invalidated the cache.

Evaluate it once at the top of __call__ and reuse the flag. This halves the
per-object eval overhead on the common path (viewport navigation with the
camera object stationary) and, when the camera does move, correctly
recalculates the cut and fill for every intersecting element instead of just
the first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:15:42 +10:00
Ryan Schultz a90064929b Bonsai: preserve occurrence geometry/material/styles when deleting a type
Deleting a type used to strip its occurrences: any that displayed the
type's mapped representation lost their geometry, and inherited material
and presentation styles were dropped too.

The no-SHIFT "Delete Type" path now bakes each occurrence's geometry,
styles, and inherited material onto the occurrence before the type is
removed:
- Refactor UnassignType's unmap logic into a reusable
  UnassignType.unassign_and_unmap(), and extend it to re-attach styled
  items (copy_deep only follows forward refs, so IfcStyledItem is lost)
  and bake down any inherited (non-owned) material.
- Add RemoveType._detach_type_material_set(): unhook the type's
  IfcMaterialLayerSet/ProfileSet association cascade-free before deletion,
  so remove_product's aggressive unassign_material never fires and the
  occurrences' layer/profile-set usages survive intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607182233
2026-07-18 17:32:49 -05:00
Ryan Schultz 397f13e71c Bonsai: add Delete Type button to Type Attributes panel
Adds a trash button in BIM_PT_type_attributes that deletes the relating
type via bim.remove_type. SHIFT+Click also deletes every occurrence of
the type in the project, behind a confirmation dialog showing the count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bonsai-0.8.6-alpha2607182028
2026-07-18 15:26:42 -05:00
Petru Conduraru 7ebdd046b6 Optimise IfcPatch recipe: make toposort backend configurable
aothms asked for the toposort dependency ordering used by the dedup
walk to try igraph's C-backed topological_sorting() first, since it
should shave off additional time on top of the non-recursive
get_info fix. Falls back to the pure python toposort package with a
warning if igraph is not installed.

Generated with the assistance of an AI coding tool.
bonsai-0.8.6-alpha2607182013
2026-07-18 22:13:29 +02:00
Petru Conduraru 57cfd9d1fd Fix #1043. Optimise IfcPatch recipe: avoid redundant recursive get_info
The 2020 profiling in issue #1043 found the Optimise recipe's dedup
loop spent almost all of its time in entity_instance.get_info(recursive=True):
because the topological sort already guarantees every referenced entity
is folded before the entity that references it, recomputing each
already-folded subtree's canonical value from scratch for every parent
that points to it is wasted work. Confirmed this is still exactly the
bottleneck in the current codebase, unchanged since 2020 (get_info's
recursive path still walks the whole subtree on every call).

Applied aothms's suggested fix from the issue thread: canonicalize each
entity with a non-recursive get_info, and for referenced entities substitute
the already-computed identity of their folded replacement (looked up in
instance_mapping) instead of re-expanding the subtree. Also limited the
toposort dependency graph to direct references (max_levels=1), since a
topological sort only needs direct edges, not the full transitive closure
traverse() was computing for every entity.

Benchmarked before and after on real IFC test fixtures and a larger
synthetic file with heavily shared geometry (thousands of walls sharing
a handful of profile/point subtrees, mirroring the sharing pattern
described in the issue):

- test/input/geometrygym_great_court_roof.ifc (56989 entities): 9.9s -> 1.7s
- test/input/acad2010_objects.ifc (16296 entities): 3.7s -> 0.4s
- synthetic 120083-entity fixture with heavy geometry sharing: 19.2s -> 3.4s

Verified correctness by comparing the full canonical (recursive get_info)
multiset of the optimized output between the old and new implementation on
all three fixtures: identical results, same fold counts.

Added test_Optimise.py covering the core scenario from the issue: entities
built from separate, value-identical non-rooted subtrees fold to a shared
instance, while entities with distinct values do not.

Generated with the assistance of an AI coding tool.
2026-07-18 22:13:29 +02:00
Petru Conduraru e333c1c100 ifcgeom: build the swept-area directrix from the offset curve far from origin (#4848)
IfcSurfaceCurveSweptAreaSolid regressed in 0.8 for geometry far from the
origin (for example parapets on a georeferenced building), which went
missing or glitched.

The kernel offsets the directrix toward the origin when it is far away
(mean.norm() > 1e2), storing the offset copy in a local curve variable and
setting applied_temporary_offset so the finished solid is translated back by
+mean. But the wire was still built from scs->curve, the un-offset original,
so the offset never took effect and the result was translated by +mean from
its correct location. Build the wire from curve instead. When no offset is
applied curve aliases scs->curve, so near-origin geometry is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:04:02 +02:00
sboddy 97a85fe5a7 Merge pull request #8608 from sboddy/feature-svg-edge-classification-3668-4
Classify projection edges in SVG elevations (#3668)
bonsai-0.8.6-alpha2607181934 bonsai-0.8.6-alpha2607181931
2026-07-18 20:32:37 +01:00
Bruno Postle 8ee52c466f Fix null reference bind in header parsing
references_to_resolve is never set while parsing header
entities, so binding a reference to it was UB, caught by
UBSan on any file with a header.

Generated with the assistance of an AI coding tool.
2026-07-18 21:31:46 +02:00
sboddy fe4fdd091d Merge pull request #8554 from sboddy/fixes-for-ci-tests
Fix ifcopenshell-python test drift (4 CI failures traced to root cause)
2026-07-18 20:31:05 +01:00
Bruno Postle 56121ca061 Fix null-pointer derefs in reference resolution
Two related bugs in read_from_stream's reference-resolution
loop, both reachable from malformed input:

- has_attribute_value<IfcBaseClass*> only checks the stored
  slot's type, not that it's non-null (e.g. an explicit $
  value), so the following get_attribute_value() call could
  return null and inst->declaration() crashed on it.
- byid_[ref] default-inserts (and returns) a null pointer
  when the owning instance id isn't present, which was then
  dereferenced unconditionally via ->data().

Added regression tests using the two minimized crash inputs
that found these.

Generated with the assistance of an AI coding tool.
2026-07-18 21:30:56 +02:00
Bartok 7b613a0bcc docs(readme): use https for IfcOpenShell website link 2026-07-18 20:38:02 +02:00