Bonsai: position drawings on sheets without leaving Blender

Sheet items could only be placed by the automatic row-packing layout in
SheetBuilder.add_drawing/add_document. Any correction meant opening the
layout SVG in Inkscape via bim.open_layout, which is the round trip that
D&D Phase 1 Item 07 asks to remove.

Adds bim.edit_sheet_item_position, which reads the current position of
the selected drawing, schedule or reference from the sheet layout SVG,
offers it as X/Y in millimetres, and writes the whole group back so the
view title keeps its offset from the content.

Scale is deliberately not exposed. build_drawings/build_documents inline
the referenced SVG under a plain translate(), so the image width/height
only drives the clip rect and cannot scale the view.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-20 12:41:53 +03:00
parent 55a2430d71
commit 3a0e8ccd31
4 changed files with 116 additions and 0 deletions
@@ -63,6 +63,7 @@ classes = (
operator.EditAssignedProduct,
operator.EditElementFilter,
operator.EditSheet,
operator.EditSheetItemPosition,
operator.EditText,
operator.EditTextPopup,
operator.EnableAddAnnotationType,
@@ -2165,6 +2165,54 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.remove_drawing_from_sheet(ifc_file.by_id(self.reference))
class EditSheetItemPosition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_sheet_item_position"
bl_label = "Edit Position On Sheet"
bl_description = "Set the position of the selected drawing, schedule or reference on its sheet layout"
bl_options = {"REGISTER", "UNDO"}
x: bpy.props.FloatProperty(name="X", description="Distance in mm from the left edge of the sheet", unit="NONE")
y: bpy.props.FloatProperty(name="Y", description="Distance in mm from the top edge of the sheet", unit="NONE")
@classmethod
def poll(cls, context):
active_item = tool.Drawing.get_active_sheet_item()
if active_item is None or active_item.is_sheet:
cls.poll_message_set("No sheet item selected.")
return False
if active_item.reference_type not in ("DRAWING", "SCHEDULE", "REFERENCE"):
cls.poll_message_set("Only drawings, schedules and references can be positioned.")
return False
return True
def invoke(self, context, event):
assert context.window_manager
position = self.get_position()
if position is None:
self.report({"ERROR"}, "Item was not found in the sheet layout.")
return {"CANCELLED"}
self.x, self.y = position
return context.window_manager.invoke_props_dialog(self)
def get_position(self):
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
reference = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
sheet = tool.Drawing.get_reference_document(reference)
if sheet is None:
return None
return sheeter.SheetBuilder().get_sheet_item_position(reference, sheet)
def _execute(self, context):
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
reference = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
sheet = tool.Drawing.get_reference_document(reference)
if sheet is None or not sheeter.SheetBuilder().set_sheet_item_position(reference, sheet, self.x, self.y):
self.report({"ERROR"}, "Item was not found in the sheet layout.")
return {"CANCELLED"}
class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.create_sheets"
bl_label = "Create Sheets"
@@ -24,6 +24,7 @@ import urllib.parse
import uuid
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Union
from xml.dom import minidom
import ifcopenshell.util.geolocation
@@ -211,6 +212,71 @@ class SheetBuilder:
layout_tree.write(layout_path)
def find_sheet_item(
self, layout_root: ET.Element, reference: ifcopenshell.entity_instance
) -> Union[ET.Element, None]:
for g in layout_root.findall(f"{SVG}g"):
if g.attrib.get("data-id") == str(reference.id()):
return g
return None
def get_sheet_item_position(
self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance
) -> Union[Vector, None]:
"""Position in mm of a drawing/schedule/reference placed on a sheet layout."""
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
if not os.path.exists(layout_path):
return None
layout_root = ET.parse(layout_path).getroot()
view = self.find_sheet_item(layout_root, reference)
if view is None:
return None
content = self.get_sheet_item_content(view)
if content is None:
return None
return Vector((self.convert_to_mm(content.attrib["x"]), self.convert_to_mm(content.attrib["y"])))
def set_sheet_item_position(
self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance, x: float, y: float
) -> bool:
"""Move a sheet item and its view title to an absolute position in mm."""
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
if not os.path.exists(layout_path):
return False
layout_tree = ET.parse(layout_path)
view = self.find_sheet_item(layout_tree.getroot(), reference)
if view is None:
return False
content = self.get_sheet_item_content(view)
if content is None:
return False
offset = Vector((x, y)) - Vector(
(self.convert_to_mm(content.attrib["x"]), self.convert_to_mm(content.attrib["y"]))
)
for image in view.findall(f"{SVG}image"):
image.attrib["x"] = str(round(self.convert_to_mm(image.attrib["x"]) + offset.x, 4))
image.attrib["y"] = str(round(self.convert_to_mm(image.attrib["y"]) + offset.y, 4))
layout_tree.write(layout_path)
return True
def get_sheet_item_content(self, view: ET.Element) -> Union[ET.Element, None]:
for image in view.findall(f"{SVG}image"):
if image.attrib.get("data-type") in ("foreground", "content"):
return image
return None
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
@@ -516,6 +516,7 @@ class BIM_PT_sheets(Panel):
row3.separator(factor=0.5, type="SPACE")
row3.operator("bim.edit_sheet", icon="GREASEPENCIL", text="")
row3.operator("bim.edit_sheet_item_position", icon="ORIENTATION_VIEW", text="")
row3.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="")
row3.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="")
row3.operator("bim.add_reference_to_sheet", icon="IMAGE_REFERENCE", text="")