remove_representation_item assumed a representation item's parent is
always the IfcShapeRepresentation (or an IfcBooleanResult). Items that
live inside a container item were detached from nothing, so they stayed
in the file and were re-imported on leaving item mode (#6591).
The import side is already general. The C++ mapper tags every mapped
IfcRepresentationItem with its instance id, and container items are
mapped through the same map_to_collection helper as
IfcRepresentation.Items, so their children become individually editable
items with no special casing. Deletion now mirrors that: any inverse
that is itself a representation item is treated as a container, and the
item is cleared from whichever attribute references it. A container left
without contents is removed in turn.
This covers IfcGeometricSet/IfcGeometricCurveSet Elements,
IfcShellBasedSurfaceModel SbsmBoundary and IfcFaceBasedSurfaceModel
FbsmFaces, rather than only the case reported in #6591. Items shared by
several containers are detached from all of them, which matches item
mode presenting one object per item id.
Fixes#6591
Generated with the assistance of an AI coding tool.
* 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.
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: 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.
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>
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>
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.
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.
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.
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.
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.
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.
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.
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>
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>
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>
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>
The crease and sharp weighting seemed flipped to my sensibilities, so
now crease is heavier than sharp. I also added a commented out block
for debug colours in case someone wants to quickly use bright colours
to diagnose future problems.
Add UseEdgeClassification, RenderCreases, ValleyAngleMinDegrees,
RenderSharp, RidgeAngleMinDegrees, and RenderFlush to EPset_Drawing,
following the existing HasUnderlay/DPI/PerspectiveShiftX pattern.
The master toggle defaults off, preserving current linework output;
the three dependent controls only show in the panel once it's on.
Removes the previous dormant, transient operator-redo properties for
the ridge/valley thresholds and flush-edge toggle, which were never
persisted per-drawing or exposed in any panel, replacing them with
the persistent camera properties read in setup_serialiser().
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds boundary/outline/sharp/crease/flush classification of HLR
projection edges in SvgSerializer, so CSS can style silhouettes,
ridges, and valleys differently instead of drawing every edge
identically (fixes the "ugly faceted sphere" problem from #3668).
Classification happens pre-HLR on the original solid's real face
topology (three prior attempts tried to classify HLR's own output,
which carries no face topology at all and can't be correlated back
by edge identity). Each class's visible portion is then extracted via
HLRBRep_HLRToShape::VCompound(S)/OutLineVCompound(S), the same
per-shape filtering mechanism already used for per-product
segmentation, applied per class instead. Classes are tagged directly
on individual <path> elements so Bonsai's merge_linework_and_add_metadata
group-level class rewrite in operator.py never touches them.
New settings: svg-ridge-angle-min-degrees, svg-valley-angle-min-degrees,
svg-emit-flush-edges (ConversionSettings.h), wired through Bonsai's
CreateDrawing operator and exposed via its redo panel.
Refs #3668.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.
Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.
Closes#8664
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When converting a wall representation to a parametric extrusion via the
Representation Utilities buttons, an element that has openings would report
"has openings - representation cannot be updated" and stop, without telling
the user there is an ALT+click path that bakes the openings into the new
representation. Point the message at that path so the error is actionable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Bonsai: place auto-generated opening boundaries at their real position #8237
auto_generate_boundaries (single-space mode) built each opening/filling boundary
from the opening's LOCAL geometry (get_vertices) but first did
mat.translation = (0, 0, 0) on its placement matrix. Because the vertices are
local, that placement translation is exactly what carries the opening to its
real location, so zeroing it collapsed every window/door boundary onto the
origin. This is why the auto path misplaced window boundaries while the
single-element path (create_element_boundary) placed them correctly, as
@MDHering observed with the two modes. Keep the full placement matrix.
Verified on the reporter's file: the opening's real placement is (0.1, 1.5, 1.0);
a vertex went from (0.6, 0, 0) under the old code to (0.7, 1.5, 1.0) with the fix,
i.e. moved by exactly the (0.1, 1.5, 1.0) that was being discarded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove superfluous comment from #8237 fix
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CyrilWaechter <cyril@biminsight.ch>
versionURLs in brand.html used http:// while the docs sites are
served over https://, so currentURL.includes(url) never matched and
the <select> never reflected/switched to Unstable. Fixes#8023.
Generated with the assistance of an AI coding tool.
Two TestImplementsTool failures on v0.8.0:
- test_cost.py: Cost could not be instantiated because
core.tool.Cost declared abstract get_direct_cost_item_products, which
tool.cost.Cost never implements. The method is dead (zero call sites;
get_cost_item_products(is_deep=False) already covers the 'direct'
case), so remove the abstract declaration.
- test_ifcgit.py: tool.ifcgit.IfcGit was not declared as a subclass of
its core.tool.IfcGit interface (unlike every sibling tool class), so
the isinstance check failed. Add the base class (and the
bonsai.core.tool import it needs). All 50 interface methods are
already implemented on the concrete class.
No behaviour change. Verified in headless Blender: isinstance(Cost(), core.tool.Cost) and isinstance(IfcGit(), core.tool.IfcGit) both True (were TypeError / False); repo abstract-vs-impl diff confirms all IfcGit abstracts are implemented.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generate_annotation built the annotation list from a set union and sorted it by
ZIndex and TEXT-ness only. Annotations that tied on that key kept set iteration
order, which follows entity hash (step id plus the process memory address), so
the order of tied annotations (for example a label and its background fill)
shuffled between Blender restarts and flipped their draw order.
Add the stable IFC step id as a final tiebreaker so the order is total and
session independent. Behavior preserving, no z-layer semantics changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several BDD scenarios hardcode absolute representation-item object names
whose trailing number is the IFC STEP line id
(f"Item/{item.is_a()}/{item.id()}"). Those ids drift when file-creation
order changes; a recent shift moved all of them by a uniform -4, so the
scenarios failed with "Item/.../NN does not exist".
The failing step (the_object_name_exists in test_feature.py) dumps the
full bpy.data.objects listing on failure, so the correct current ids are
recoverable directly from the CI log (run 29208793599, tested commit
36e21e882f, an ancestor of HEAD with only a .gitignore commit between).
Renumber to match:
IfcExtrudedAreaSolid/77->73, IfcPolygonalFaceSet/76->72,
IfcVertexPoint/69->65, IfcEdge/72->68, IfcFace/74->70.
Verified against the CI failure dump (a local build produces different
ids, so this is validated by CI's own object listing rather than a local
run). boolean.feature also hardcodes IfcHalfSpaceSolid/90 and panel text
[91] downstream of the failing assertion, which CI never reached and so
never dumped; left as-is to avoid guessing - they will print a fresh dump
next run for a follow-up if still stale.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported: Blender segfaults when clicking Cancel on the "newer
autosave found" recovery popup shown by LoadProject at startup.
Root cause: LoadProject.execute()/invoke() triggered the recovery
popup via bpy.ops.bim.load_autosaved_recovery_popup("INVOKE_DEFAULT",
...) and returned that call's result ({'RUNNING_MODAL'}) as their own
return value, without LoadProject itself ever calling
modal_handler_add(). Blender's window manager takes a RUNNING_MODAL
return as a promise the operator registered its own modal handler;
since it hadn't, the WM's operator bookkeeping was left corrupted -
silently, since this is heap/state corruption rather than an
immediate crash. It only surfaced later, when the real modal operator
(the popup) closed and the WM reconciled its modal stack, which lines
up with the crash occurring specifically on dialog close regardless
of which button was pressed. check_autosave_recovery() now returns a
plain bool and fires the popup fire-and-forget; LoadProject reports
its own honest {"FINISHED"}.
Also hardened, as defense in depth: LoadAutosavedRecoveryPopup's
execute()/cancel() call back into bim.load_project(...), which (with
should_start_fresh_session) calls wm.read_homefile() and tears down
the window manager/screens. Doing that synchronously from inside this
popup's own execute()/cancel() - itself invoked from deep inside
Blender's modal handling for the popup's button click - risks the
same class of use-after-free as the timer bug fixed in the previous
commit. The reload is now deferred by one timer tick so it runs after
the popup's modal handling has fully unwound, and the deferred
callback closes over plain values rather than `self`, since the
operator instance may not survive past cancel()/execute() returning.
This defer-only change was tried and tested first, on the (incorrect)
assumption it was the root cause: it produced a byte-for-byte
identical crash backtrace on retest, which is what pointed at the
RUNNING_MODAL bug above as the actual cause - the defer change alone
was insufficient because the corruption happens when the popup is
first shown, not when it's closed.
Generated with the assistance of an AI coding tool.
The periodic autosave timer called reset_timer() at the end of its
own callback, which unregistered the timer that was still executing
(itself). Blender frees the timer's internal registry entry on that
manual unregister, then frees it again when the callback returns
None - a double free that corrupts the heap and can crash Blender
later, once the corrupted memory is reused.
Reschedule by returning the next interval from the callback instead,
which is the safe, documented way to repeat a bpy.app.timers
callback. External reset_timer() calls (from SaveProject,
LoadProject, AutosavePrompt) are unaffected since they run from a
separate call stack (UI events), not from inside the timer.
Found while investigating a segfault reported when cancelling the
autosave recovery popup; not itself the cause of that crash (see the
following commit), but the same reentrant-unregister pattern and a
real, independent latent bug in the periodic reminder path.
Generated with the assistance of an AI coding tool.
The project-unit to Blender-unit mapping in format_distance only knew
FOOT/INCH/METRE/DECIMETRE/CENTIMETRE/MILLIMETRE, so creating a project
with Kilometers or Miles in the New Project Wizard crashed with
KeyError: 'KILOMETRE' (or 'MILE') as soon as the spatial tree formatted
an elevation. Add the missing Blender-supported units (kilometre, mile,
micrometre) and fall through gracefully for anything else (for example
HECTOMETRE) so unknown units use the adaptive formatting branch instead
of raising.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project loading set scene length_unit to f"{Prefix}METERS", but Blender's
enum only defines KILOMETERS, CENTIMETERS, MILLIMETERS and MICROMETERS.
A model with a DECIMETRE (or HECTO/DECA/etc.) length unit therefore raised
on the enum assignment and the file failed to open. Guard with the set of
supported values and fall back to ADAPTIVE display for the rest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The quickstart ended with three empty sections whose bodies were only
"TODO" (placing occurrences, changing locations, modeling a building),
which read as a dead end on docs.bonsaibim.org. The page now ends on the
completed save-and-view flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera, then selects their cameras with
the first as active. SHIFT+CTRL+ALT+CLICK also selects the
loaded annotation objects. The drawing camera is imported
when missing so annotations land in the correct collection.
Loading is idempotent.
Generated with the assistance of an AI coding tool.
- autosave.py: black formatting (blank line) and ruff's
collections.abc.Callable import fix.
- project/__init__.py, tool/__init__.py: ruff import-sort fixes. The
autosave import in tool/__init__.py is deliberately kept last (must
come after tool.drawing, per its existing comment) via `# isort: skip`
rather than letting ruff move it, which would reintroduce that bug.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The recovery popup used invoke_popup, which is dismissed the instant
the mouse leaves its bounds - closing the prompt without loading
either file, and with no visible feedback that anything happened.
Switches to invoke_props_dialog, which blocks the rest of the UI and
is only dismissed by an explicit action. Since Blender always renders
both a fixed "Cancel" button and one labelled by confirm_text on that
dialog type, the prompt is reframed as a direct Yes/Cancel question
("Do you want to load the autosaved version instead?") instead of
adding separate Load Original/Load Autosave buttons on top of those.
Folds the load logic directly into the popup's execute()/cancel(), so
the now-redundant LoadAutosavedRecovery operator is removed.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>