Closes #6321: Auto-snap SECTION endpoints to drawing border#7965
Closes #6321: Auto-snap SECTION endpoints to drawing border#7965theoryshaw wants to merge 25 commits into
Conversation
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.
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.
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 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.
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.
Use get_default_annotation_matrix() for manual SECTION annotations so the object is placed in the camera's annotation plane with the correct rotation, matching how auto-generated section annotations are created. Without this, annotations placed in elevation or section camera views had identity rotation, causing the IFC representation to be in the wrong coordinate system and failing to tessellate. Also guard draw_edit_object_interface against non-IFC active objects to prevent AssertionError during toolbar redraws, and skip IFC item edit mode for ELEVATION/SECTION annotation types on placement.
Replace plain enum dropdowns with prop_with_search in the AddAnnotation and AssignManualDrawingReference operator dialogs, making it easier to locate a target drawing when many drawings exist in the project. Generated with the assistance of an AI coding tool.
Extends manual drawing reference annotations (elevation and section) to also support external SVG references imported via bim.add_reference. - Add "Is a Reference" checkbox to the annotation tool sidebar, shown when Elevation or Section is the active type; checking it and pressing Add opens a dialog to optionally link the tag to a Bonsai drawing or an external SVG reference - The MANUAL_DRAWING_REFERENCE dropdown type is retained for backwards compatibility; selecting it shows a style picker (Elevation/Section) and the same linking dialog - External-reference annotations are flagged with IsDocumentReference in EPset_Annotation and linked to their IfcDocumentInformation via IfcRelAssociatesDocument; drawing-reference annotations continue to use IfcRelAssignsToProduct as before - SVG export resolves the correct reference/sheet IDs for both link types via get_reference_and_sheet_id_from_annotation - Add IsDocumentReference to the EPset_Annotation pset template
When a SECTION annotation is created or a drawing is activated, endpoint vertices are automatically placed at a configurable BorderOffset (paper-space mm, default 8) inside the camera border, scaled by the drawing scale. BorderOffset is stored in the BBIM_Section pset and visible in the Property Sets panel. An "UpdateSectionEndpoints" operator (bim.update_section_endpoints) resets endpoints back to the border offset on demand. Endpoints are also recomputed when the diagram scale is changed. Generated with the assistance of an AI coding tool.
504d0b1 to
c1d4494
Compare
BonsaiPR Conflict Resolution: PR #7798 vs PR #7965Date: 2026-04-19 ProblemPR #7798 (Manual drawing reference) (
The goal was to resolve the conflict without modifying PR #7798, which needs to stay clean for upstream submission. Step 1: Identifying the Conflicting PRPR #7798 touches the following files:
After testing PR #7798 against the Checking which build PRs touched Confirmation: PR #7798 was successfully included in the previous build Step 2: Testing Other Skipped PRsThe following open PRs were also not in the build branch and were tested against it:
PRs #7886 and #7924 have separate conflicts (not with PR #7798), left for another time per user request. Step 3: Nature of the ConflictBoth PRs add a new operator class registration to the same location in PR #7798 adds: operator.AddAnnotationType,
+ operator.AssignManualDrawingReference, # ← PR #7798
operator.AddDrawing,PR #7965 adds: operator.AddAnnotationType,
+ operator.UpdateSectionEndpoints, # ← PR #7965
operator.AddDrawing,This is a classic adjacent-line insertion conflict. The two features are independent and additive — neither is a subset of the other. Both operators need to be registered. Step 4: Rebase DirectionDecision: Rebase PR #7965 onto PR #7798.
PR #7798 is the more mature, upstream-bound PR and must not be modified. PR #7965 is a brand-new PR that should accommodate the established one. The build processes PRs in descending PR-number order (newest first). Since
Step 5: Rebase ExecutionUsed git checkout -b pr-7965-rebased origin/inset_section_endpoints
git rebase --onto origin/ManualDrawingReference origin/v0.8.0Conflict in operator.AddAnnotationType,
operator.AssignManualDrawingReference, # from PR #7798
operator.UpdateSectionEndpoints, # from PR #7965
operator.AddDrawing,Rebase completed cleanly. All other files (data.py, operator.py, prop.py, core/drawing.py, tool/drawing.py) auto-merged without conflicts. Step 6: Pushgit push origin pr-7965-rebased:inset_section_endpoints --force-with-leaseResult: Summary
|
…uld not show `0`.
…BBIM_Dimension.SuppressZeroInches Generated with the assistance of an AI coding tool.
Generated with the assistance of an AI coding tool.
…eometry 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
…imensions 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>
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>
…cularToFace toggle updates selection 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>
…pendicularToFace - 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>
…tric dimensions - 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>
… layer anchors - 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>
…for DrawParametricDimension - 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>
- 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>
…nsion 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).
Both PR #7965 (inset_section_endpoints) and PR #8083 (parametric_dimensions) independently added properties after entry #33 in Psets_BBIM_Annotation.ifc and new BoolProperty declarations after `type_name` in prop.py and workspace.py UI rows at the same locations. Resolution: keep #8083's IDs (#34-#40) unchanged, renumber #7965's IsManualDrawingReference and IsDocumentReference entries to #41 and #42, update EPset_Annotation reference list accordingly, keep both UI rows, and combine hotkey_S_A to use #8083's parametric-dimension routing with #7965's "INVOKE_DEFAULT" argument for add_annotation.
…bility Move is_manual_reference UI block to before _DIMENSION_TYPES to match the position established by PR #7965's conflict resolution already present in the build. The prior merge fix (66fcad8) placed it after _DIMENSION_TYPES, causing a 3-way merge conflict when the build's #7965 has it before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PRs #7798 (ManualDrawingReference) and #7965 (inset_section_endpoints) added operators AssignManualDrawingReference and UpdateSectionEndpoints after AddAnnotationType in __init__.py, and added ELEVATION/SECTION cases in tool/drawing.py, conflicting with parametric_dimensions at the same insertion points. Resolved by merging PR #7965 tip (738110b) — which already subsumes PR #7798's changes via their shared old-#8083 ancestry — and manually keeping both sides: AddElevationAnnotation + AssignManualDrawingReference + UpdateSectionEndpoints in __init__.py; is_manual_reference UI block from #7965 plus the narrower _DIMENSION_TYPES (no PLAN_LEVEL/SECTION_LEVEL) from current parametric_dimensions; and SECTION_LEVEL, ELEVATION, SECTION elif branches all preserved in tool/drawing.py.
Closes #6321
Summary
Prior to this change,
SECTIONannotation endpoints had no relationship to the drawing camera border. Their positions were entirely manual — a user had to place them by hand each time, and they would not update if the drawing scale changed.This PR introduces automatic placement of
SECTIONannotation endpoints: when a section annotation is created, when a drawing is activated, or when the diagram scale is changed, the two mesh vertices that form the section line are automatically positioned at a configurable inset distance (BorderOffset) from the drawing camera's orthographic border. The offset is defined in paper-space millimetres and is converted to model-space units using the drawing scale, so the same 8 mm inset looks correct at 1:50, 1:100, or any other scale.If a user manually moves one endpoint in Edit Mode, that vertex is fingerprinted and its custom position is preserved. Only endpoints that still sit at their last auto-computed position are updated on the next activation or scale change. A dedicated operator lets the user force-reset both endpoints back to the border offset at any time.
What Changed
tool/drawing.py— new geometry helpers and core update logicget_camera_dimensions(camera)— derives the camera's model-space width and height fromortho_scaleand the render resolution aspect ratio._section_ray_rect_intersections(origin, direction, half_w, half_h)— finds the twotvalues where an infinite ray through the section line's midpoint intersects the camera border rectangle.get_section_border_positions(camera, v0_world, v1_world, border_offset_mm)— uses the above two helpers to compute the two world-space positions that sitborder_offset_mm(paper mm, converted by scale) inside the camera border along the section line direction.update_section_endpoints(obj, camera)— the main entry point. ReadsBorderOffset,AutoStartPosition, andAutoEndPositionfrom theBBIM_Sectionpset. Compares each current world-space vertex position against its stored auto-position; if they match (or no auto-position is stored yet) the vertex is auto-updated. Writes the new auto-positions andBorderOffsetback to the pset, then callsbpy.ops.bim.update_representationto persist the moved vertices to the IFC geometry._parse_vector3/_format_vector3— small static helpers to serialise and deserialise aVectorto/from a comma-separated string for IFC pset storage.data.py— pset defaultsget_section_markers_display_data()now addsBorderOffset: 8.0,AutoStartPosition: "", andAutoEndPosition: ""to theBBIM_Sectionpset defaults so the values are always present and visible in the Property Sets panel even before the user changes anything.operator.py— new operator + activation hookUpdateSectionEndpoints— a newbim.update_section_endpointsoperator (label: "Reset Section to Border"). It clearsAutoStartPositionandAutoEndPositionin the pset so thatupdate_section_endpointstreats both vertices as unmodified, then callsupdate_section_endpointsto recompute. Only enabled when the active object is aSECTIONannotation and a scene camera is set.ActivateDrawingBase._execute— aftersync_referencesreloads the IFC geometry, iterates over allIfcAnnotationmembers of the drawing group withObjectType == "SECTION"and callsupdate_section_endpointsfor each one.prop.py— scale-change hookupdate_diagram_scale()(the callback for the diagram scale dropdown) now iterates the same drawing group and callsupdate_section_endpointsfor everySECTIONannotation in it, so endpoints are repositioned immediately when the scale is changed.core/drawing.py— creation hookadd_annotation()now callsupdate_section_endpointsimmediately after the new annotation is assigned to the drawing group, so a freshly created section annotation starts at the correct inset positions.__init__.pyUpdateSectionEndpointsis added to theclassestuple so it is registered with Blender.Notes
print("[SECTION] …")statements are present throughout and should be removed before final merge.AutoStartPosition/AutoEndPositionare stored in world space in this version. An object-level translate (moving the annotation object without editing vertices) will change world-space vertex positions while leaving local-spacev.counchanged, which can cause false "manually moved" detection. This is a known limitation of the current approach.BorderOffsetis intentionally exposed in the Property Sets panel rather than a custom UI panel, so users can inspect and override it without any additional UI work.Generated with the assistance of an AI coding tool.