The elevation tag's local -Z axis is intentionally parallel to its
drawing camera's view direction, making screen-space projection of that
axis always degenerate (zero XY delta). Fall through to the tag's local
+X axis, which lies in the camera plane and rotates correctly as the
user adjusts the tag's orientation. Also fix a zero-length vector crash
in svgwriter when the same degenerate case occurs during SVG export.
Adds MANUAL_DRAWING_REFERENCE to the annotation type dropdown.
Selecting it shows a dialog to choose elevation or section and
optionally assign a target drawing before placement. Tags are
protected from regeneration via EPset_Annotation.IsManualDrawingReference.
Generated with the assistance of an AI coding tool.
Adds operator to link a manual drawing reference tag to a target
drawing via IfcRelAssignsToProduct, with a pre-populated dialog
and immediate Properties panel refresh on confirm.
Generated with the assistance of an AI coding tool.
Adds operator to place manual elevation/section drawing reference
tags that survive drawing regeneration. Includes core function,
tool methods, type-selection dialog, default horizontal rotation
for elevation tags, and SVG null guard for unassigned references.
Generated with the assistance of an AI coding tool.
Introduces a boolean pset property that marks an ELEVATION or
SECTION annotation as manually placed, exempting it from
automatic deletion or regeneration during drawing sync.
Generated with the assistance of an AI coding tool.
- AddElevationAnnotation: new modal operator (inherits SetDimensionAnchor) that
creates the annotation only after the user clicks a face/layer/edge/vertex,
placing it at the picked world position with elevation tracked via BBIM_Dimension
- hotkey_S_A routes PLAN_LEVEL/SECTION_LEVEL to AddElevationAnnotation instead of
the static AddAnnotation
- _annotation_is_2d: check predefined type first so PLAN_LEVEL/SECTION_LEVEL always
use object-placement Z (not spline-point Z)
- _update_elevation_marker_z: fixed early-exit guard that skipped 2D placement update
when splines were absent; now moves the object via geometry.edit_object_placement
- SetDimensionAnchor._handle_face_pick: for elevation types, relocate the whole
annotation (XY and Z) to the new face hit, not just Z
- _zero_elevation_annotation_spline_z: flatten local spline-point Z to 0 so the
visible line and the gizmo sit at the same world elevation
- DimensionAnchorWidget: position gizmo at object origin for elevation annotations
(anchor reference point) instead of spline.points[0]
- ClickNearestDimensionAnchor: hit-test at object origin for elevation annotations
so clicking the dot correctly turns it blue
- SECTION_LEVEL default curve is now horizontal (camera X) instead of vertical
- Remove 'Edit Elevation Anchor' button; replaced by viewport green-dot workflow
- Add 'Bake to Static' and 'Make Parametric' buttons for elevation annotation types
- MakeDimensionParametric: extended to handle elevation types (single world-point
anchor at object origin rather than one anchor per spline vertex)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BakeParametricDimension (bim.bake_parametric_dimension): removes the
BBIM_Dimension pset from the active annotation, leaving the curve
geometry in place as a static dimension that no longer regenerates
when referenced elements move.
MakeDimensionParametric (bim.make_dimension_parametric): adds a
BBIM_Dimension pset to a static dimension annotation, creating one
free world-point anchor per spline vertex at the current positions.
The endpoints can then be re-anchored to IFC faces via SetDimensionAnchor.
When SetDimensionAnchor's tessellation fallback ran (element has no
IfcExtrudedAreaSolid, so get_profile_snap_candidates returns empty),
VERTEX and EDGE snaps created a static world anchor (free end) instead
of a parametric one. The anchor had a position but no guid, so it
never moved with the element.
_compute_snap_geom now includes local_m (element-local Blender
coordinates, metres) in the tessellation fallback return dict for both
VERTEX and EDGE modes. _handle_face_pick uses build_anchor_from_local_point
when local_m is present, storing a LOCAL_POINT anchor that resolves back
to world space via the element placement — so the endpoint follows the
element through moves and rotations.
DimensionLinePositionWidget was gated behind ForcePerpendicularToFace in
both the gizmo poll and the regenerate_dimension LinePosition application.
The coupling was unnecessary: _get_line_offset_direction already derives
the offset axis from cross(world_Z, dim_direction) as its primary path,
requiring the face normal only as a vertical-dimension fallback.
Remove the ForcePerpendicularToFace guard from both sites so any
anchor-based dimension shows the drag arrows and responds to LinePosition.
regenerate_dimension was only called when ForcePerpendicularToFace was
set, so normal two-anchor dimensions were left at raw polyline cursor
positions after placement. The depsgraph handler would later correct
them when the user happened to select a referenced IFC element, making
accurate placement appear to require a manual selection step.
Remove the _force_perpendicular guard so the anchor-based regeneration
always runs at the end of _create_dimension_from_polyline.
ClickNearestDimensionAnchor was firing SetDimensionAnchor immediately on
LMB PRESS and returning FINISHED, which caused Blender to re-deliver the
RELEASE to view3d.select — deselecting the annotation mid-flight. Rewrite
as a two-event modal: PRESS starts the modal, RELEASE fires SetDimensionAnchor
and exits. SetDimensionAnchor also swallows any LMB RELEASE it receives to
prevent view3d.select from stealing the active object after hand-off.
The pre-click active-object guard (skip if dimension not active_object) caused
dots to never turn blue: view3d.select was silently replacing the dimension
with the plane underneath on every line-body click, so the dimension was
never the active object at the time of the dot click. Removed — the operator
now selects the dimension itself before going modal, making each dot click
self-contained.
RADIUS_PX reduced from 60 to 15 to match the gizmo disc visual size and
prevent false triggers on line-body clicks near endpoints.
Walls viewed edge-on in plan (2-7 px screen bbox) were never hit by
Blender's raycast, so all three snap modes silently returned nothing.
- FACE: remove has_coplanar_edge Z-gate; vertical faces are now
snappable regardless of what elevation the native snap lands on
(sub-floor surfaces at Z~-7.5m were blocking all candidates)
- All modes: replace hardcoded 30 px _FACE_THRESH_D2 with a
per-candidate max_tol that matches the adaptive _SCREEN_TOL used
for bbox inclusion (~98 px for 2 px-wide walls)
- VERTEX/EDGE/LAYER: remove early `if not hit_obj: return None`;
all modes now search objs_2d_bbox with adaptive tolerance when the
primary raycast misses
- Add _get_mesh_snap_candidates fallback for tessellated elements
(IfcFacetedBrep etc.) where get_profile_snap_candidates returns []
- Add LOCAL_POINT anchor method (build_anchor_from_local_point +
resolve_anchor handler) so mesh-derived anchors store element-local
coords and follow the element through moves/rotations rather than
becoming free-floating WORLD anchors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GizmoAnchorHandle + DimensionAnchorWidget: colored dot gizmos at each
dimension curve vertex (green=anchored, orange=free); color changes to
blue while SetDimensionAnchor is in PICK_FACE mode for that vertex
- ClickNearestDimensionAnchor (LMB keymap): Python proximity operator that
fires SetDimensionAnchor pre-targeted at the nearest anchor dot within
120px, returning PASS_THROUGH for misses so normal viewport clicks are
unaffected
- SetDimensionAnchor: added anchor_index prop to enter PICK_FACE directly;
set_active_anchor called at all phase transitions (invoke, vertex-pick,
face-pick, alt-click free, ESC/RMB) so gizmo color tracks state correctly
- handler._sync_dimension_anchors_to_curve: proximity-based anchor sync
when curve vertex count changes in Edit Mode (subdivide / delete)
- depsgraph_update_post_handler: regenerates dimensions when referenced
elements move; also handles annotation curve edits directly
- Remove standalone Set Anchor button from annotation tool UI (replaced by
clicking a gizmo dot)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BBIM_Dimension.LinePosition (IfcLengthMeasure): holds the dimension line at
a fixed global coordinate along cross(world_Z, dim_direction), independent of
geometry movement
- regenerate_dimension applies LinePosition only when ForcePerpendicularToFace is
also set (the two are semantically coupled); anchor["pt"] always stores the true
surface hit so the measured length is unaffected
- BIMAnnotationProperties.line_position uses get/set callbacks instead of an update
callback to avoid the 'Writing to ID classes in this context is not allowed' error
that fires when Blender draws the tool header
- DimensionLinePositionWidget (BIM_GGT_dimension_line_position): gizmo group with
two opposing GizmoCone handles at the curve midpoint; poll requires
ForcePerpendicularToFace so the handles only appear when the feature is active
- UI: line_position field and gizmo are hidden when ForcePerpendicularToFace is off
- Psets_BBIM_Annotation.ifc: #40 LinePosition template added to BBIM_Dimension
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workspace.py:
- Move "Set Dimension Anchor" and "Regenerate" buttons from the properties
panel (ui.py) into draw_edit_object_interface in the annotation tool,
visible whenever a selected object is a dimension-type IfcAnnotation
- Change force_perpendicular_to_face from a push-button (toggle=True) to
a standard checkbox for clearer on/off state
ui.py:
- Remove the "Parametric Dimension" section (now lives in the tool header)
prop.py:
- Add _update_force_perpendicular update callback: when the checkbox is
toggled, iterates all selected dimension annotations, writes the new
ForcePerpendicularToFace value to each BBIM_Dimension pset, and calls
regenerate_dimension so the constraint is applied immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New modal operator (bim.set_dimension_anchor) anchors dimension vertices
to IFC element faces. Anchors are stored as JSON in a BBIM_DimensionTarget
pset on the IfcAnnotation and resolved via tessellation at regeneration time.
- resolve_anchor.py / regenerate_dimension.py: new ifcopenshell API modules
- bim.set_dimension_anchor: 2-phase Object Mode modal (pick vertex → pick face)
- bim.regenerate_dimensions: recomputes all parametric dimensions
- Auto-regeneration via depsgraph_update_post when referenced elements move
- placement_override reads Blender matrix_world for G-moved elements
- Plan-view annotations flattened to annotation plane (Z=0 in local space)
- IfcIndexedPolyCurve.Segments rebuilt to handle n-point chains correctly
- Add dedicated POST_PIXEL GPU callback (_draw_anchor_hover_global) using
pre-converted 2D screen coords, replacing the shared POST_VIEW callback
that caused GPU state issues and Blender freezes
- Add LAYER snap mode hover indicator showing full seam-corner outline
- Remove select_set calls from hover highlight to prevent green object outline
- Add _is_hidden() using hide_get/hide_viewport/visible_get so only scene-
visible objects are snap candidates
- Add _face_perp_ok() filter (camera-based) to prefer wall faces over
floor/ceiling faces in FACE mode; non-perp hits tracked in ray_hit_objs
so directly-hit elements always rank above proximity-found neighbours
- Add _get_current_anchor_guid() to promote the currently-bound element to
the front of the candidate list when re-picking an anchor vertex
- Add _coplanar_face_outline() to merge tessellated triangles (including
walls with window/door voids) into the correct outer face boundary;
walks all disconnected loops and returns the largest (outer perimeter),
skips meshes > 500 polygons to avoid freezing on terrain objects
- Skip _prefer_perp_face_index in FACE mode so the exact hit face is used
rather than the face most perpendicular to the camera
- Sort proximity candidates so ray-hit objects rank before bbox-only matches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch FACE/LAYER mode nearby-object filtering from 3D bbox to 2D
screen-space bbox (30px tolerance), fixing walls whose local Y extent
doesn't contain the floor hit point (e.g. wall at Z=0 with mesh not
quite reaching the floor level).
- Also run _snap_on_coplanar_faces on hit_obj itself so the blue
outline and IFC snap fire even when the cursor lands exactly on
the wall/floor boundary (hit_obj IS the wall, previously skipped).
- Store face_normal_world in coplanar face candidates and call
build_anchor_from_hit in _build_ifc_anchor for snap=="FACE", so
the anchor gets a proper FACE type with normal_local in addr.
This enables ForcePerpendicularToFace and LinePosition to work
for coplanar edge-on face snaps the same as directly-hit faces.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vertical faces perpendicular to the camera cannot be hit by raycast, so
the dimension snap tool missed them entirely. Fix by checking nearby
objects whose 3D bbox contains the floor hit point and running
mode-appropriate candidate lookup on each:
- FACE mode: _snap_on_coplanar_faces finds vertical mesh faces with a
bottom edge at the hovered Z, projects the cursor onto the face plane,
and returns a FACE candidate (blue outline + face snap point).
- LAYER mode: get_layer_snap_candidates now runs on nearby bbox objects
the same way VERTEX/EDGE mode already did, using the shared
_snap_cand_multi_cache (cleared on TAB mode switch).
- Remove clear_snap_objs() from PolylineOperator.invoke — BVH cache now
persists across invocations; per-entry staleness is checked in
create_snap_obj via matrix_world equality + vertex count, eliminating
the ~11 s full rebuild on every Shift+A press.
- Add _init_snapping_points() hook to PolylineOperator; DrawParametricDimension
overrides it with a cheap plane-intersection placeholder, deferring full
BVH detection to the first MOUSEMOVE.
- Cache matrix_world in SnapObj and replace O(N_vertices) validation loop
with O(1) matrix equality + single sample vertex check, cutting per-call
create_snap_obj cost from 22-600 ms to <0.2 ms on cache hits.
- Use scene-level BVH pierce-through in SetDimensionAnchor._compute_candidates
instead of per-object ray_cast loop (O(log N) vs O(N_objects)).
- Guard PolylineDecorator snap_mouse_point access against empty collection
to prevent IndexError before first MOUSEMOVE populates the property.
- Wrap closest_point_on_mesh in try/except RuntimeError in
_update_snap_draw_data for annotation objects with no internal mesh data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DrawParametricDimension: TAB cycles snap mode FACE→LAYER→EDGE→VERTEX during
placement; consumes both PRESS and RELEASE when not in input mode to avoid
conflict with polyline Cycle Input
- DrawParametricDimension: IFC-native snap candidate overrides polyline cursor
position in LAYER/EDGE/VERTEX modes; FACE mode shows polygon outline; reuses
_snap_draw_data / _draw_snap_indicator_global infrastructure from SetDimensionAnchor
- DrawParametricDimension: LAYER_BOUNDARY support in _update_perp_constraint,
deriving normal from LayerSetDirection (AXIS1/2/3)
- ClickNearestDimensionAnchor: scan all visible annotations instead of only
selected ones — view3d.select deselects the dimension before this operator
runs, so pre-selection check caused dots to never activate
- AnnotationTool keymap: bim.click_nearest_dimension_anchor placed before
view3d.select so it fires first when the annotation tool is active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ClickNearestDimensionAnchor: scan all selected objects instead of active
object so clicking a green dot doesn't lose to the underlying IFC geometry
- GizmoAnchorHandle: remove draw_select entirely (any entry in the select
buffer causes Blender's gizmo system to consume clicks); keep purely visual
- Scale anchor dots to scale_basis = 0.2
- Fix ReferenceError in decoration.py draw loop after undo by catching
ReferenceError and resetting DecoratorData.is_loaded
- SetDimensionAnchor: inherit tool.Ifc.Operator so IFC pset writes are
tracked for undo; finish the modal after each face write so each anchor
gets its own undo step
- Fix ReferenceError in _modal after undo when annotation RNA is freed
- handler.py: add regenerate_dims_for_layer; call it from
EditMaterialSetItem._execute so dimensions update when layer thickness changes
- regenerate_dimension.py: fix ForcePerpendicularToFace for LAYER_BOUNDARY
anchors by deriving the thickness-axis normal from LayerSetDirection
(AXIS2→Y, AXIS1→X, AXIS3→Z) instead of requiring a stored normal_local
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GizmoAnchorHandle + DimensionAnchorWidget: colored dot gizmos at each
dimension curve vertex (green=anchored, orange=free); color changes to
blue while SetDimensionAnchor is in PICK_FACE mode for that vertex
- ClickNearestDimensionAnchor (LMB keymap): Python proximity operator that
fires SetDimensionAnchor pre-targeted at the nearest anchor dot within
120px, returning PASS_THROUGH for misses so normal viewport clicks are
unaffected
- SetDimensionAnchor: added anchor_index prop to enter PICK_FACE directly;
set_active_anchor called at all phase transitions (invoke, vertex-pick,
face-pick, alt-click free, ESC/RMB) so gizmo color tracks state correctly
- handler._sync_dimension_anchors_to_curve: proximity-based anchor sync
when curve vertex count changes in Edit Mode (subdivide / delete)
- depsgraph_update_post_handler: regenerates dimensions when referenced
elements move; also handles annotation curve edits directly
- Remove standalone Set Anchor button from annotation tool UI (replaced by
clicking a gizmo dot)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BBIM_Dimension.LinePosition (IfcLengthMeasure): holds the dimension line at
a fixed global coordinate along cross(world_Z, dim_direction), independent of
geometry movement
- regenerate_dimension applies LinePosition only when ForcePerpendicularToFace is
also set (the two are semantically coupled); anchor["pt"] always stores the true
surface hit so the measured length is unaffected
- BIMAnnotationProperties.line_position uses get/set callbacks instead of an update
callback to avoid the 'Writing to ID classes in this context is not allowed' error
that fires when Blender draws the tool header
- DimensionLinePositionWidget (BIM_GGT_dimension_line_position): gizmo group with
two opposing GizmoCone handles at the curve midpoint; poll requires
ForcePerpendicularToFace so the handles only appear when the feature is active
- UI: line_position field and gizmo are hidden when ForcePerpendicularToFace is off
- Psets_BBIM_Annotation.ifc: #40 LinePosition template added to BBIM_Dimension
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workspace.py:
- Move "Set Dimension Anchor" and "Regenerate" buttons from the properties
panel (ui.py) into draw_edit_object_interface in the annotation tool,
visible whenever a selected object is a dimension-type IfcAnnotation
- Change force_perpendicular_to_face from a push-button (toggle=True) to
a standard checkbox for clearer on/off state
ui.py:
- Remove the "Parametric Dimension" section (now lives in the tool header)
prop.py:
- Add _update_force_perpendicular update callback: when the checkbox is
toggled, iterates all selected dimension annotations, writes the new
ForcePerpendicularToFace value to each BBIM_Dimension pset, and calls
regenerate_dimension so the constraint is applied immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SetDimensionAnchor — hover-select-then-confirm:
- Cursor highlights candidate IFC elements (orange Blender selection outline)
before committing; Tab cycles through overlapping/coplanar candidates
- _compute_candidates: ray-cast all IFC mesh objects; falls back to 2D
bounding-box proximity (5 cm tolerance) for plan-view picks where the
ray misses the mesh by sub-mm amounts
- _write_anchor: after anchoring a face, immediately calls
regenerate_dimension with placement_override (Blender matrix_world)
and _update_blender_curve so the curve vertex moves to the resolved point
DrawParametricDimension — ForcePerpendicularToFace live snap constraint:
- Reads force_perpendicular_to_face toggle from annotation props on invoke
- After anchor[0] is placed on a FACE, _update_perp_constraint extracts
the face normal and stores it as the constraint axis
- _apply_perp_constraint runs every modal tick after handle_snap_selection,
projecting the current snap point onto pt[0] + t*normal
- On finalize, _create_dimension_from_polyline writes ForcePerpendicularToFace
to the BBIM_Dimension pset and calls regenerate_dimension to snap the
stored curve to the constraint before the operator exits
regenerate_dimension.py:
- ForcePerpendicularToFace block: after resolving all anchors, projects
vertices 1…n onto the line through pt[0] along anchor[0]'s face normal
- _get_anchor_face_normal_world: reads normal_local from anchor fingerprint,
calls _rotate_local_to_world with placement_override; falls back to stored
world-space normal
resolve_anchor.py:
- _rotate_local_to_world: transforms an element-local direction vector to
world space using the element's placement or placement_override matrix
pset/operator.py:
- EditPset._execute: after editing a BBIM_Dimension pset on an IfcAnnotation,
auto-calls regenerate_dimension + _update_blender_curve so changes to
anchors/ForcePerpendicularToFace are reflected immediately in the viewport
prop.py / workspace.py:
- Added force_perpendicular_to_face BoolProperty to BIMAnnotationProperties
- UI toggle shown in annotation tool header for DIMENSION/RADIUS/DIAMETER/
ANGLE/PLAN_LEVEL/SECTION_LEVEL types
Psets_BBIM_Annotation.ifc:
- Added ForcePerpendicularToFace property template (#39) to BBIM_Dimension
- Extended BBIM_Dimension applicability to ANGLE, PLAN_LEVEL, SECTION_LEVEL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends parametric dimension support with a modal polyline operator that uses
Bonsai's existing snap infrastructure (same as walls/slabs) for placing anchor
points. Shift+A in the Annotation tool now routes dimension types through this
operator instead of the generic add_annotation path.
- DrawParametricDimension: PolylineOperator subclass; each confirmed snap point
is converted to a BBIM_DimensionTarget anchor via _snap_to_anchor, which reads
face_index from the snap dict for face hits and falls back to closest_point_on_mesh
for vertex/edge hits
- handle_inserting_polyline override tracks anchor list in sync with polyline
points (insert on count increase, pop on BACKSPACE)
- hotkey_S_A dispatches to bim.draw_parametric_dimension for DIMENSION/RADIUS/
DIAMETER/ANGLE/PLAN_LEVEL/SECTION_LEVEL types; all other types keep existing path
- depsgraph_update_post_handler extended to also watch is_updated_geometry so
dimensions auto-regenerate when a referenced mesh is edited in Edit Mode; the
affected element's tessellation is evicted from _dim_shape_cache so resolve_anchor
re-tessellates from the updated IFC representation on the next pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New modal operator (bim.set_dimension_anchor) anchors dimension vertices
to IFC element faces. Anchors are stored as JSON in a BBIM_DimensionTarget
pset on the IfcAnnotation and resolved via tessellation at regeneration time.
- resolve_anchor.py / regenerate_dimension.py: new ifcopenshell API modules
- bim.set_dimension_anchor: 2-phase Object Mode modal (pick vertex → pick face)
- bim.regenerate_dimensions: recomputes all parametric dimensions
- Auto-regeneration via depsgraph_update_post when referenced elements move
- placement_override reads Blender matrix_world for G-moved elements
- Plan-view annotations flattened to annotation plane (Z=0 in local space)
- IfcIndexedPolyCurve.Segments rebuilt to handle n-point chains correctly
Alternative to #8955, for #8951 (23 of 25 ifcmcp tools reach MCP clients
with an empty description because FastMCP reads each wrapper's own
__doc__, and the server.py wrappers had none).
#8955 fixes this by hand-writing a new docstring directly onto each
server.py wrapper. Most of those wrappers are thin passthroughs to
IfcSession methods in core.py, which already had short docstrings, which
themselves mostly delegate to already-documented ifcquery/ifcedit
functions -- so that fix tripled up content across three layers that can
drift out of sync.
This instead enriches the true source (the ifcquery/ifcedit library
functions, useful independently of MCP) and has core.py's IfcSession
methods copy __doc__ from their delegate via a small _use_doc()
decorator, and server.py's tool registration pull description= from the
matching IfcSession method. Methods that aren't pure passthroughs
(session lifecycle, generic API/shape dispatch) keep their own
hand-written docs. Keeps #8955's regression test.
Generated with the assistance of an AI coding tool.
Linux "basic dependencies" omitted libeigen3-dev even though
ifcgeom requires Eigen3 (find_package Eigen3 REQUIRED) and the
cmake snippet already passes -DEIGEN_DIR=/usr/include/eigen3.
macOS Homebrew line already installs eigen.
Closes#6903
Generated with the assistance of an AI coding tool.
test_file's parametrize list was filtered with `sys.argv[1] in
os.path.basename(fn)`, reading the raw process argv instead of a
pytest-native option. Under a bare `pytest` invocation sys.argv[1] is
pytest's own first CLI token, never a match, so the 138-fixture EXPRESS
rule corpus in test/fixtures/rules collapses to an empty parametrize and
pytest reports it as a single skipped test rather than an error. Under
CI's actual invocation (pytest -p no:pytest-blender -n $NPROCS test ...)
sys.argv[1] is "-p", which happens to substring-match 47 of the 138
fixtures, so CI has been silently running a coincidental 34% slice of
the corpus with no signal anything was wrong.
Replaced the module-level list comprehension with a pytest_generate_tests
hook plus a --rule CLI option (added via a new test/conftest.py). This
runs the full corpus by default under any pytest invocation, still
allows filtering to one rule for local debugging via --rule, and no
longer collides with pytest's own argv.
Verified all 138 fixtures collect and pass under the fixed harness
(63 fail- fixtures each raise a violation, 75 pass- fixtures raise none).
Generated with the assistance of an AI coding tool.
mcp 2.0.0 (unpinned in CI and in the ifcmcp[mcp] extra) renamed
mcp.server.fastmcp.FastMCP to mcp.server.mcpserver.MCPServer, which
ifcmcp does not support yet. server.py caught the resulting
ModuleNotFoundError with a bare except Exception and silently
reported it as FastMCP not installed, masking the real breakage
until the ifcmcp test suite failed in CI.
Pinned mcp to >=1.0,<2 in both ci.yml and ifcmcp's pyproject.toml
mcp extra, confirmed the full ifcmcp test suite (70 tests) passes
against mcp 1.29.0, and confirmed the genuinely-not-installed path
still raises the expected ImportError. Also narrowed the except
clause to ImportError only so an unrelated future bug in that
import block surfaces instead of being swallowed as "not installed".
Generated with the assistance of an AI coding tool.
FullBufferImpl and PagedFileImpl both open the file and then use the handle
without ever testing it:
auto stream = _wfopen(fn_wide, L"rb"); // null when the file is missing
fseek(stream, 0, SEEK_END); // null goes straight to the CRT
buf_.resize((size_t)ftell(stream));
Opening a path that does not exist therefore hands a null FILE* to the CRT. On
MSVC that does not return an error: the runtime terminates the process
immediately (fastfail, exit code 0xC0000409). No exception is thrown, no stack
unwinding starts, so a caller cannot defend with try/catch — the host
application simply dies. On glibc it is undefined behaviour as well.
This is reachable through the ordinary entry point, because guess_file_type()
answers FT_IFCSPF for a path that does not exist (its own comment calls this
"just weird, but for consistency with earlier behaviour"), so a missing path
flows into the reader rather than being reported.
The fix is to leave the reader empty when the open fails. Both implementations
then behave like a zero-length file: size() is 0 and get() throws out_of_range
for any position, so the parse fails and IfcFile::good() reports it, which is
what a caller can actually handle. PagedFileImpl's destructor already tested
fp_ for null, so the possibility was known — only the constructor did not check.
Verified by reading a non-existent path through IfcParse::IfcFile: the
constructor returns and good() reports the failure, where before the process
died with 0xC0000409 and no output.
create(timestamp=0) computed the FILE_NAME timestring with
`d.get("timestamp") or time.time()`, which treats 0 (a legitimate
epoch timestamp) as unset because 0 is falsy. The header ended up
with the current wall-clock time in FILE_NAME while IFCOWNERHISTORY
correctly stored CreationDate=0, an inconsistent pair of dates in
the same file. Switched to an explicit None check so an explicit
timestamp of 0 is honoured the same way any other explicit
timestamp is.
Generated with the assistance of an AI coding tool.
The profile mapping builds its points as
profile_helper(m4, {
{{-x, -y}, {f2}},
...
where `f2` is a `double` and profile_point's second member is a
`boost::optional<double>`. In recent Boost (somewhere between 1.85 and 1.91)
optional's converting constructor became explicit, and an explicit constructor
cannot be used in copy-initialization — which is what a braced element is. So
every one of these call sites stops compiling:
MSVC 19.4x: error C2664: cannot convert argument 2 from
'initializer list' to 'const std::vector<profile_point>&'
clang-cl 22: error: chosen constructor is explicit in copy-initialization
Twelve translation units are affected (IfcCShapeProfileDef,
IfcIShapeProfileDef, IfcLShapeProfileDef, IfcTShapeProfileDef,
IfcUShapeProfileDef, IfcZShapeProfileDef, IfcAsymmetricIShapeProfileDef,
IfcCraneRailAShapeProfileDef, IfcRectangleProfileDef,
IfcRectangleHollowProfileDef, IfcRoundedRectangleProfileDef,
IfcTrapeziumProfileDef), roughly 100 call sites in total.
Adding one overload that takes the double directly fixes all of them without
touching a single call site, and changes nothing for existing code: the
optional overload still wins wherever an optional is passed.
Verified by building schemas 2x3;4;4x3_add2 with MSVC 2022 against Boost
1.91 and OCCT 7.9.3 — IfcParse, IfcGeom, the schema mappings and
geometry_kernel_opencascade all archive cleanly. Without this, the same build
against Boost 1.85 succeeds, which is what identified Boost as the variable.
Removing translate_obj_to_z_location from the existing-IfcSpace
regeneration branch. The ShapeBuilder rewrite (d8de62308) builds
geometry in local space preserving obj.matrix_world, making the
translate call redundant — it adds z on top of the already-correct
location.z, producing 2*z.
Add test_regenerate_space_preserves_z_location to cover the
regeneration path with a non-zero Z elevation.
Generated with the assistance of an AI coding tool.
The example block was copy pasted verbatim from ExtractPropertiesToSQLite,
so it named the wrong recipe and wrote a .sqlite file. These docstrings are
what ifcpatch surfaces as CLI and UI help, so anyone following the example
for AGS2IFC got a recipe name that does not match the one they selected.
Also state that the input file is not read and that a new IFC4X3 model is
built, since that is not obvious from the signature and the recipe creates
its own project rather than patching the one passed in.
Generated with the assistance of an AI coding tool.
angle_round_threshold was only assigned inside the `distance > 0`
branch of calculate_distance_and_angle, but read unconditionally
whenever should_round is True. When the mouse sample coincides with
the last placed point (distance == 0), such as the first mouse move
after placing a wall's start point on a YZ plane view, this crashed
the modal wall tool.
angle_round_threshold is a fixed cutoff unrelated to whether distance
is currently zero, so it is now assigned once before the branch.
Fixes#8597.
Generated with the assistance of an AI coding tool.
test_returns_none_when_report_file_absent/empty build a MagicMock repo
without configuring index.unmerged_blobs(), so it returned a truthy
MagicMock and git_mergetool's load-bearing "unresolved conflicts remain"
fallback (tool/ifcgit.py:646-647) returned that list instead of None -
failing "assert [] is None". The production fallback is correct and
intentionally left untouched; the tests just misrepresented the
"mergetool resolved cleanly" scenario they are named for. Set
mock_repo.index.unmerged_blobs.return_value = {} in both.
Verified in headless Blender: test/tool/test_ifcgit.py::TestGitMergetool
2 failed / 1 passed -> 3 passed.
This change was made with the assistance of an AI tool.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DeepDiff's dictionary_item_added/set_item_added results are a
deepdiff.helper.SetOrdered instance, which subclasses orderly_set's
StableSetEq rather than the OrderedSet class json_dump_default checked
for, so the property relationship check always crashed export() with
"Object of type SetOrdered is not JSON serializable". Check against
StableSet, the common base class shared by every orderly_set set
flavour, instead.
Fixes#8905
Generated with the assistance of an AI coding tool.