Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/bonsai/bonsai/bim/data/assets/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,20 @@ text.GRID, tspan.GRID { /* 5mm */ font-size: 8.25px; }
.PredefinedType-STEEL { fill: url(#steel); stroke: black; stroke-width: 0.5; }
.PredefinedType-CONCRETE { fill: url(#concrete); stroke: black; stroke-width: 0.5; }
.PredefinedType-PLASTERBOARD { fill: url(#sand); stroke: black; stroke-width: 0.25; }
/* Per element classes, assigned via EPset_Annotation.Classes. Declared last so
they override the class, material and predefined type rules above. */
.fine { stroke-width: 0.18; }
.thin { stroke-width: 0.25; }
.medium { stroke-width: 0.35; }
.thick { stroke-width: 0.5; }
.strong { stroke-width: 1; }
.dashed { stroke-dasharray: 3, 2; }
.dotted { stroke-dasharray: 0.5, 1.5; }
.dashdot { stroke-dasharray: 6, 2, 1, 2; }
.hidden { stroke: none; fill: none; }
.fill-none { fill: none; }
.fill-white { fill: #ffffff; }
.fill-light { fill: #dddddd; }
.fill-grey { fill: #aaaaaa; }
.fill-dark { fill: #777777; }
.fill-solid { fill: #000000; }
4 changes: 4 additions & 0 deletions src/bonsai/bonsai/bim/module/drawing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
operator.AddAnnotation,
operator.AddAnnotationType,
operator.AddDrawing,
operator.AddElementDrawingClass,
operator.AddDrawingStyle,
operator.AddDrawingToSheet,
operator.AddReference,
Expand Down Expand Up @@ -76,6 +77,7 @@
operator.SelectElementValues,
operator.InsertFormattedLiteralPopup,
operator.AddElementValueRow,
operator.RemoveElementDrawingClass,
operator.RemoveElementValueRow,
operator.ElementValueSuggestionsPopup,
operator.FormatElementValueRow,
Expand Down Expand Up @@ -125,12 +127,14 @@
ui.BIM_PT_sheets,
ui.BIM_PT_drawings,
ui.BIM_PT_camera,
ui.BIM_PT_element_drawing_classes,
ui.BIM_PT_element_filters,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_schedules,
ui.BIM_PT_references,
ui.BIM_PT_product_assignments,
ui.BIM_PT_text,
ui.BIM_MT_element_drawing_classes,
ui.BIM_UL_drawinglist,
ui.BIM_UL_sheets,
# Core gizmos (shared across modules)
Expand Down
27 changes: 27 additions & 0 deletions src/bonsai/bonsai/bim/module/drawing/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def refresh():
ElementFiltersData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.is_loaded = False
ElementClassesData.is_loaded = False


class ProductAssignmentsData:
Expand Down Expand Up @@ -231,6 +232,32 @@ def count_documents(cls):
"title": 7.0,
}

# CSS classes shipped in default.css that may be assigned per element.
# Custom stylesheets are free to define their own, so this is only a
# convenience list for the UI, never a restriction.
ELEMENT_CLASSES = {
"Line Weight": ("fine", "thin", "medium", "thick", "strong"),
"Line Style": ("dashed", "dotted", "dashdot", "hidden"),
"Fill": ("fill-none", "fill-white", "fill-light", "fill-grey", "fill-dark", "fill-solid"),
}


class ElementClassesData:
data = {}
is_loaded = False

@classmethod
def load(cls):
cls.data = {"classes": cls.classes()}
cls.is_loaded = True

@classmethod
def classes(cls) -> list[str]:
obj = bpy.context.active_object
if not obj or not (element := tool.Ifc.get_entity(obj)):
return []
return tool.Drawing.get_element_classes(element)


class DecoratorData:
# stores 1 type of data per object
Expand Down
50 changes: 50 additions & 0 deletions src/bonsai/bonsai/bim/module/drawing/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,9 @@ def get_svg_classes(self, element, layer=None):
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)

# ─── Custom ────────────────────────────────────────────────
classes.extend(tool.Drawing.get_element_classes(element))

return classes

def is_manifold(self, obj) -> bool:
Expand Down Expand Up @@ -3647,6 +3650,53 @@ def _execute(self, context):
core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)


class AddElementDrawingClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_element_drawing_class"
bl_label = "Add Drawing Class"
bl_description = (
"Assign a CSS class to this element so it can be styled individually in drawings.\n"
"Applies to all selected IFC objects"
)
bl_options = {"REGISTER", "UNDO"}

name: bpy.props.StringProperty(name="Class", default="")

if TYPE_CHECKING:
name: str

def invoke(self, context, event):
if self.name:
return self.execute(context)
return context.window_manager.invoke_props_dialog(self)

def _execute(self, context):
name = tool.Drawing.sanitise_class_name(self.name)
if not name:
self.report({"ERROR"}, "A valid CSS class name is required.")
return {"CANCELLED"}
for obj in tool.Blender.get_selected_objects():
if element := tool.Ifc.get_entity(obj):
core.add_element_class(tool.Drawing, element=element, name=name)
self.name = ""


class RemoveElementDrawingClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_element_drawing_class"
bl_label = "Remove Drawing Class"
bl_description = "Unassign this CSS class from all selected IFC objects"
bl_options = {"REGISTER", "UNDO"}

name: bpy.props.StringProperty(name="Class")

if TYPE_CHECKING:
name: str

def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
if element := tool.Ifc.get_entity(obj):
core.remove_element_class(tool.Drawing, element=element, name=self.name)


class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_sheets"
bl_label = "Load Sheets"
Expand Down
4 changes: 1 addition & 3 deletions src/bonsai/bonsai/bim/module/drawing/svgwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,7 @@ def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
str(ifcopenshell.util.element.get_predefined_type(element))
)
classes = [global_id, element.is_a(), predefined_type]
custom_classes: str = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if custom_classes:
classes.extend(custom_classes.split())
classes.extend(tool.Drawing.get_element_classes(element))
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
Expand Down
51 changes: 51 additions & 0 deletions src/bonsai/bonsai/bim/module/drawing/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.module.drawing.data import (
ELEMENT_CLASSES,
DecoratorData,
DocumentsData,
DrawingsData,
ElementClassesData,
ElementFiltersData,
ProductAssignmentsData,
SheetsData,
Expand Down Expand Up @@ -571,6 +573,55 @@ def draw(self, context):
col.enabled = bool(ProductAssignmentsData.data["relating_product"])


class BIM_MT_element_drawing_classes(bpy.types.Menu):
bl_label = "Add Drawing Class"
bl_idname = "BIM_MT_element_drawing_classes"

def draw(self, context):
assert self.layout
for category, names in ELEMENT_CLASSES.items():
self.layout.label(text=category)
for name in names:
self.layout.operator("bim.add_element_drawing_class", text=name).name = name
self.layout.separator()
self.layout.operator("bim.add_element_drawing_class", text="Custom Class...", icon="ADD").name = ""


class BIM_PT_element_drawing_classes(Panel):
bl_label = "Drawing Classes"
bl_idname = "BIM_PT_element_drawing_classes"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 2
bl_parent_id = "BIM_PT_tab_object_metadata"

@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not context.active_object:
return False
return bool(tool.Ifc.get_entity(context.active_object))

def draw(self, context):
if not ElementClassesData.is_loaded:
ElementClassesData.load()

assert self.layout
row = self.layout.row(align=True)
row.menu("BIM_MT_element_drawing_classes", icon="ADD", text="Add Class")

classes = ElementClassesData.data["classes"]
if not classes:
self.layout.label(text="No Classes Assigned", icon="BRUSH_DATA")
return

for name in classes:
row = self.layout.row(align=True)
row.label(text=name, icon="BRUSH_DATA")
row.operator("bim.remove_element_drawing_class", icon="X", text="").name = name


def get_category_icon(category_name):
"""Get appropriate icon for each category"""
icons = {
Expand Down
8 changes: 8 additions & 0 deletions src/bonsai/bonsai/core/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,11 @@ def activate_drawing_view(
blender.activate_camera(camera)
drawing_tool.isolate_camera_collection(camera)
drawing_tool.activate_drawing(camera)


def add_element_class(drawing: type[tool.Drawing], element: ifcopenshell.entity_instance, name: str) -> None:
drawing.add_element_class(element, name)


def remove_element_class(drawing: type[tool.Drawing], element: ifcopenshell.entity_instance, name: str) -> None:
drawing.remove_element_class(element, name)
7 changes: 5 additions & 2 deletions src/bonsai/bonsai/core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ def update_document_objects(cls, document_id): pass
@interface
class Drawing:
def activate_drawing(cls, camera): pass
def add_element_class(cls, element, name): pass
def add_literal(cls, **attributes): pass
def clear_annotation_relationships(cls, drawing): pass
def copy_representation(cls, source, dest): pass
Expand Down Expand Up @@ -386,6 +387,7 @@ def get_drawing_document(cls, drawing): pass
def get_drawing_group(cls, drawing): pass
def get_drawing_references(cls, drawing): pass
def get_drawing_target_view(cls, drawing): pass
def get_element_classes(cls, element): pass
def get_group_drawing(cls, group): pass
def get_group_elements(cls, group): pass
def get_ifc_representation_class(cls, object_type): pass
Expand Down Expand Up @@ -414,20 +416,21 @@ def open_layout_svg(cls, uri): pass
def open_spreadsheet(cls, uri): pass
def open_svg(cls, filepath): pass
def reload_representation(cls, obj, representation): pass
def remove_element_class(cls, element, name): pass
def run_drawing_activate_model(cls): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def run_type_assign_type(cls, element=None, relating_type=None): pass
def sanitise_class_name(cls, name): pass
def select_assigned_product(cls, drawing): pass
def set_camera_name(cls, drawing, name): pass
def set_drawing_collection_name(cls, drawing, collection): pass
def set_element_classes(cls, element, classes): pass
def set_name(cls, element, name): pass
def setup_annotation_object(cls, obj, object_type): pass
def setup_shading_styles_path(cls, resource_path): pass
def show_decorations(cls): pass
def sync_object_placement(cls, obj): pass
def update_embedded_svg_location(cls, uri, old_location, new_location): pass


@interface
class Duplicate:
def get_decomposition_relationships(cls, objs): pass
Expand Down
40 changes: 40 additions & 0 deletions src/bonsai/bonsai/tool/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,46 @@ def get_drawing_metadata(cls, drawing: ifcopenshell.entity_instance) -> list[str
metadata_str = pset_data.get("Metadata", "") or ""
return [v_ for v in metadata_str.split(",") if (v_ := v.strip())]

@classmethod
def get_element_classes(cls, element: ifcopenshell.entity_instance) -> list[str]:
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if not isinstance(classes, str):
return []
return classes.split()

@classmethod
def set_element_classes(cls, element: ifcopenshell.entity_instance, classes: list[str]) -> None:
ifc_file = tool.Ifc.get()
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
if not pset:
if not classes:
return
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Classes": " ".join(classes)})

@classmethod
def add_element_class(cls, element: ifcopenshell.entity_instance, name: str) -> None:
name = cls.sanitise_class_name(name)
if not name:
return
classes = cls.get_element_classes(element)
if name in classes:
return
cls.set_element_classes(element, classes + [name])

@classmethod
def remove_element_class(cls, element: ifcopenshell.entity_instance, name: str) -> None:
classes = cls.get_element_classes(element)
if name not in classes:
return
cls.set_element_classes(element, [c for c in classes if c != name])

@classmethod
def sanitise_class_name(cls, name: str) -> str:
"""Strip characters that cannot appear in a CSS class name."""
name = re.sub(r"[^0-9a-zA-Z_-]+", "-", name.strip()).strip("-")
return re.sub(r"^[0-9-]+", "", name)

@classmethod
def get_annotation_z_index(cls, drawing: ifcopenshell.entity_instance) -> float:
return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "ZIndex") or 0
Expand Down
12 changes: 12 additions & 0 deletions src/bonsai/test/core/test_drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,3 +663,15 @@ def test_create_a_missing_annotation_context_on_the_fly(self, ifc, collector, dr
relating_type="element_type",
enable_editing=True,
)


class TestAddElementClass:
def test_run(self, drawing):
drawing.add_element_class("element", "dashed").should_be_called()
subject.add_element_class(drawing, element="element", name="dashed")


class TestRemoveElementClass:
def test_run(self, drawing):
drawing.remove_element_class("element", "dashed").should_be_called()
subject.remove_element_class(drawing, element="element", name="dashed")
Loading
Loading