mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
PR7500 squashed commit
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
|
||||
* { stroke-linecap: round; stroke-linejoin: round; }
|
||||
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; }
|
||||
a text, a tspan { fill: blue !important; text-decoration: underline;}
|
||||
a:hover { cursor: pointer; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
|
||||
@@ -65,6 +65,17 @@ classes = (
|
||||
operator.EnableEditingText,
|
||||
operator.ExcludeAnnotation,
|
||||
operator.ExpandSheet,
|
||||
operator.ToggleElementValuesPanel,
|
||||
operator.ToggleElementValuesCategory,
|
||||
operator.SelectElementValues,
|
||||
operator.InsertFormattedLiteralPopup,
|
||||
operator.AddElementValueRow,
|
||||
operator.RemoveElementValueRow,
|
||||
operator.ElementValueSuggestionsPopup,
|
||||
operator.FormatElementValueRow,
|
||||
operator.ApplyElementValueRowsToLiteral,
|
||||
operator.ShowCategoryHelp,
|
||||
operator.ShowElementValuesInstructions,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadReferences,
|
||||
operator.LoadSchedules,
|
||||
@@ -89,8 +100,10 @@ classes = (
|
||||
operator.SelectAllDrawings,
|
||||
operator.SelectAllSheets,
|
||||
operator.SelectAssignedProduct,
|
||||
operator.SelectSimilarTextLiteralValue,
|
||||
operator.ToggleTargetView,
|
||||
operator.OpenDocumentationWebUi,
|
||||
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
||||
prop.Variable,
|
||||
prop.Drawing,
|
||||
prop.Document,
|
||||
@@ -98,7 +111,9 @@ classes = (
|
||||
prop.Sheet,
|
||||
prop.DocProperties,
|
||||
prop.BIMCameraProperties,
|
||||
prop.ElementValueRow,
|
||||
prop.LiteralProps,
|
||||
prop.LiteralApplySettings,
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
prop.BIMAnnotationProperties,
|
||||
@@ -143,7 +158,6 @@ def menu_func(self, context):
|
||||
if element and element.is_a("IfcAnnotation") and element.ObjectType in ["SECTION", "ELEVATION"]:
|
||||
self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing")
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
|
||||
@@ -156,7 +170,7 @@ def register():
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -22,6 +22,7 @@ import json
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.selector
|
||||
import bonsai.tool as tool
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
@@ -335,19 +336,17 @@ class DecoratorData:
|
||||
|
||||
@classmethod
|
||||
def get_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
|
||||
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
|
||||
returns font size in mm for current ifc text object"""
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
# getting font size
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
# use `regular` as default
|
||||
|
||||
# get font size
|
||||
classes = pset_data.get("Classes", None) or "regular"
|
||||
classes_split = classes.split()
|
||||
# prioritize smaller font sizes just like in svg
|
||||
font_size_type = next(
|
||||
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
|
||||
)
|
||||
@@ -361,15 +360,22 @@ class DecoratorData:
|
||||
literals = tool.Drawing.get_text_literal(obj, return_list=True)
|
||||
assert isinstance(literals, list)
|
||||
literals_data: list[dict[str, Any]] = []
|
||||
product = tool.Drawing.get_assigned_product(element) or element
|
||||
|
||||
product = cls.get_product_for_element_values(obj, element)
|
||||
|
||||
for literal in literals:
|
||||
literal_value = literal.Literal
|
||||
|
||||
try:
|
||||
current_value = cls.evaluate_formatting_expressions(literal_value)
|
||||
current_value = tool.Drawing.replace_text_literal_variables(current_value, product)
|
||||
except Exception:
|
||||
current_value = literal_value
|
||||
|
||||
literal_data = {
|
||||
"Literal": literal_value,
|
||||
"BoxAlignment": literal.BoxAlignment,
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(
|
||||
literal_value, product, reverse_list, list_separator
|
||||
),
|
||||
"CurrentValue": current_value,
|
||||
}
|
||||
literals_data.append(literal_data)
|
||||
|
||||
@@ -382,6 +388,419 @@ class DecoratorData:
|
||||
"List_Separator": list_separator,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_product_for_element_values(cls, text_obj: bpy.types.Object, element: ifcopenshell.entity_instance):
|
||||
"""Get the product to use for element values - either ProductUsed or assigned product"""
|
||||
props = tool.Drawing.get_text_props(text_obj)
|
||||
if props.literals:
|
||||
for literal_props in props.literals:
|
||||
if hasattr(literal_props, "product_used") and literal_props.product_used:
|
||||
return tool.Ifc.get_entity(literal_props.product_used)
|
||||
|
||||
assigned_product = tool.Drawing.get_assigned_product(element)
|
||||
if assigned_product:
|
||||
return assigned_product
|
||||
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def evaluate_formatting_expressions(cls, text: str) -> str:
|
||||
"""Evaluate formatting expressions wrapped in backticks using ifcopenshell.util.selector.format"""
|
||||
import re
|
||||
|
||||
def evaluate_expression(match):
|
||||
try:
|
||||
expression = match.group(1)
|
||||
result = ifcopenshell.util.selector.format(expression)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return match.group(0)
|
||||
|
||||
return re.sub(r"``([^`]+)``", evaluate_expression, text)
|
||||
|
||||
@classmethod
|
||||
def get_element_value_by_key(cls, element: ifcopenshell.entity_instance, key: str):
|
||||
"""Get element value by its key using IfcOpenShell selector syntax"""
|
||||
try:
|
||||
# Basic keys
|
||||
if key == "id":
|
||||
return element.id()
|
||||
elif key == "class":
|
||||
return element.is_a()
|
||||
elif key == "predefined_type":
|
||||
return ifcopenshell.util.element.get_predefined_type(element)
|
||||
|
||||
# Direct attributes
|
||||
elif hasattr(element, key):
|
||||
return getattr(element, key)
|
||||
|
||||
# Material keys
|
||||
elif key.startswith("material"):
|
||||
return cls._get_material_value(element, key)
|
||||
|
||||
# Type keys
|
||||
elif key.startswith("type."):
|
||||
if hasattr(element, "IsTypedBy") and element.IsTypedBy:
|
||||
element_type = element.IsTypedBy[0].RelatingType
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(element_type, attr_name):
|
||||
return getattr(element_type, attr_name)
|
||||
|
||||
elif key == "types.count":
|
||||
if hasattr(element, "IsTypedBy") and element.IsTypedBy:
|
||||
element_type = element.IsTypedBy[0].RelatingType
|
||||
occurrence_count = 0
|
||||
if hasattr(element_type, "Types"):
|
||||
for rel in element_type.Types:
|
||||
if hasattr(rel, "RelatedObjects"):
|
||||
occurrence_count += len(rel.RelatedObjects)
|
||||
return occurrence_count
|
||||
|
||||
elif key == "occurrences.count":
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
occurrence_count = 0
|
||||
if hasattr(element, "Types"):
|
||||
for rel in element.Types:
|
||||
if hasattr(rel, "RelatedObjects"):
|
||||
occurrence_count += len(rel.RelatedObjects)
|
||||
return occurrence_count
|
||||
|
||||
# Spatial keys
|
||||
elif key.startswith("container."):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(container, attr_name):
|
||||
return getattr(container, attr_name)
|
||||
|
||||
elif key.startswith("space."):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcSpace"):
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(current, attr_name):
|
||||
return getattr(current, attr_name)
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
|
||||
elif key.startswith("storey."):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcBuildingStorey"):
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(current, attr_name):
|
||||
return getattr(current, attr_name)
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
|
||||
elif key.startswith("building."):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcBuilding"):
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(current, attr_name):
|
||||
return getattr(current, attr_name)
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
|
||||
elif key.startswith("site."):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcSite"):
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(current, attr_name):
|
||||
return getattr(current, attr_name)
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
|
||||
# Parent keys
|
||||
elif key.startswith("parent."):
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
if parent:
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(parent, attr_name):
|
||||
return getattr(parent, attr_name)
|
||||
|
||||
# Group, system, zone keys
|
||||
elif key.startswith(("group.", "system.", "zone.")):
|
||||
prefix = key.split(".")[0]
|
||||
attr_name = key.split(".", 1)[1]
|
||||
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for assignment in element.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToGroup"):
|
||||
group = assignment.RelatingGroup
|
||||
if (
|
||||
prefix == "group"
|
||||
and group.is_a("IfcGroup")
|
||||
and not group.is_a("IfcSystem")
|
||||
and not group.is_a("IfcZone")
|
||||
):
|
||||
if hasattr(group, attr_name):
|
||||
return getattr(group, attr_name)
|
||||
elif prefix == "system" and group.is_a("IfcSystem"):
|
||||
if hasattr(group, attr_name):
|
||||
return getattr(group, attr_name)
|
||||
elif prefix == "zone" and group.is_a("IfcZone"):
|
||||
if hasattr(group, attr_name):
|
||||
return getattr(group, attr_name)
|
||||
|
||||
elif key in ("groups.count", "systems.count", "zones.count"):
|
||||
prefix = key.split(".")[0]
|
||||
count = 0
|
||||
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for assignment in element.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToGroup"):
|
||||
group = assignment.RelatingGroup
|
||||
if (
|
||||
prefix == "groups"
|
||||
and group.is_a("IfcGroup")
|
||||
and not group.is_a("IfcSystem")
|
||||
and not group.is_a("IfcZone")
|
||||
):
|
||||
count += 1
|
||||
elif prefix == "systems" and group.is_a("IfcSystem"):
|
||||
count += 1
|
||||
elif prefix == "zones" and group.is_a("IfcZone"):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
# Classification keys
|
||||
elif key.startswith("classification."):
|
||||
import re
|
||||
|
||||
match = re.match(r"classification\.(\d+)\.(\w+)", key)
|
||||
if match:
|
||||
idx = int(match.group(1))
|
||||
attr_name = match.group(2)
|
||||
classifications = ifcopenshell.util.classification.get_references(element)
|
||||
if classifications and 0 <= idx < len(classifications):
|
||||
classification = classifications[idx]
|
||||
if hasattr(classification, attr_name):
|
||||
return getattr(classification, attr_name)
|
||||
|
||||
elif key == "classification.count":
|
||||
classifications = ifcopenshell.util.classification.get_references(element)
|
||||
return len(classifications) if classifications else 0
|
||||
|
||||
# Profile keys
|
||||
elif key.startswith("profiles."):
|
||||
import re
|
||||
|
||||
if key == "profiles.count":
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if material and material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
return len(material.MaterialProfiles)
|
||||
return 0
|
||||
|
||||
match = re.match(r"profiles\.(\d+)\.(\w+)", key)
|
||||
if match:
|
||||
idx = int(match.group(1))
|
||||
attr_name = match.group(2)
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if material and material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
if 0 <= idx < len(material.MaterialProfiles):
|
||||
profile_item = material.MaterialProfiles[idx]
|
||||
if hasattr(profile_item, "Profile") and profile_item.Profile:
|
||||
profile = profile_item.Profile
|
||||
if hasattr(profile, attr_name):
|
||||
return getattr(profile, attr_name)
|
||||
return None
|
||||
|
||||
elif key.startswith("profile."):
|
||||
attr_name = key.split(".", 1)[1]
|
||||
if hasattr(element, "Representation") and element.Representation:
|
||||
for representation in element.Representation.Representations:
|
||||
if hasattr(representation, "Items"):
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcExtrudedAreaSolid") and hasattr(item, "SweptArea"):
|
||||
profile = item.SweptArea
|
||||
if hasattr(profile, attr_name):
|
||||
return getattr(profile, attr_name)
|
||||
return None
|
||||
|
||||
# Style keys
|
||||
elif key.startswith("styles."):
|
||||
import re
|
||||
|
||||
if key == "styles.count":
|
||||
try:
|
||||
styles = ifcopenshell.util.element.get_styles(element)
|
||||
return len(styles) if styles else 0
|
||||
except:
|
||||
return 0
|
||||
|
||||
match = re.match(r"styles\.(\d+)\.(\w+)", key)
|
||||
if match:
|
||||
idx = int(match.group(1))
|
||||
attr_name = match.group(2)
|
||||
try:
|
||||
styles = ifcopenshell.util.element.get_styles(element)
|
||||
if styles and 0 <= idx < len(styles):
|
||||
style = styles[idx]
|
||||
if attr_name == "Color" and style.is_a("IfcSurfaceStyle"):
|
||||
# Extract color from surface style
|
||||
if hasattr(style, "Styles"):
|
||||
for surface_style_elem in style.Styles:
|
||||
if surface_style_elem.is_a("IfcSurfaceStyleRendering"):
|
||||
if hasattr(surface_style_elem, "SurfaceColour"):
|
||||
color = surface_style_elem.SurfaceColour
|
||||
return f"RGB({color.Red:.2f}, {color.Green:.2f}, {color.Blue:.2f})"
|
||||
elif hasattr(style, attr_name):
|
||||
return getattr(style, attr_name)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Coordinate keys
|
||||
elif key in ("x", "y", "z"):
|
||||
if hasattr(element, "ObjectPlacement") and element.ObjectPlacement:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
if matrix is not None:
|
||||
if key == "x":
|
||||
return matrix[0][3]
|
||||
elif key == "y":
|
||||
return matrix[1][3]
|
||||
elif key == "z":
|
||||
return matrix[2][3]
|
||||
return None
|
||||
|
||||
elif key in ("easting", "northing", "elevation"):
|
||||
if hasattr(element, "ObjectPlacement") and element.ObjectPlacement:
|
||||
try:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
if matrix is not None:
|
||||
ifc_file = element.wrapped_data.file
|
||||
project = ifc_file.by_type("IfcProject")[0] if ifc_file.by_type("IfcProject") else None
|
||||
|
||||
if project:
|
||||
for context in project.RepresentationContexts or []:
|
||||
if context.is_a("IfcGeometricRepresentationContext") and hasattr(
|
||||
context, "HasCoordinateOperation"
|
||||
):
|
||||
for coord_op in context.HasCoordinateOperation:
|
||||
if coord_op.is_a("IfcMapConversion"):
|
||||
if key == "easting":
|
||||
return matrix[0][3] + coord_op.Eastings
|
||||
elif key == "northing":
|
||||
return matrix[1][3] + coord_op.Northings
|
||||
elif key == "elevation":
|
||||
return matrix[2][3] + coord_op.OrthogonalHeight
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Property sets / Quantity sets (must be checked last to avoid conflicts)
|
||||
elif "." in key:
|
||||
parts = key.split(".", 1)
|
||||
if len(parts) == 2:
|
||||
pset_name, prop_name = parts
|
||||
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
|
||||
qsets = ifcopenshell.util.element.get_psets(element, qtos_only=True)
|
||||
all_psets = {**psets, **qsets}
|
||||
|
||||
if pset_name in all_psets and prop_name in all_psets[pset_name]:
|
||||
return all_psets[pset_name][prop_name]
|
||||
|
||||
# Check for regex patterns (e.g., /Pset_.*Common/)
|
||||
import re
|
||||
|
||||
if pset_name.startswith("/") and pset_name.endswith("/"):
|
||||
pattern = pset_name[1:-1]
|
||||
try:
|
||||
regex = re.compile(pattern)
|
||||
for actual_pset_name in all_psets.keys():
|
||||
if regex.match(actual_pset_name):
|
||||
if prop_name in all_psets[actual_pset_name]:
|
||||
return all_psets[actual_pset_name][prop_name]
|
||||
elif prop_name.startswith("/") and prop_name.endswith("/"):
|
||||
prop_pattern = prop_name[1:-1]
|
||||
prop_regex = re.compile(prop_pattern)
|
||||
for actual_prop_name, prop_value in all_psets[actual_pset_name].items():
|
||||
if prop_regex.match(actual_prop_name):
|
||||
return prop_value
|
||||
except re.error:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting value for key '{key}': {e}")
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_material_value(cls, element: ifcopenshell.entity_instance, key: str):
|
||||
"""Extract material values using correct selector syntax"""
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if not material:
|
||||
return None
|
||||
|
||||
if key == "materials.count":
|
||||
if material.is_a("IfcMaterialLayerSetUsage") and material.ForLayerSet:
|
||||
layer_set = material.ForLayerSet
|
||||
if hasattr(layer_set, "MaterialLayers"):
|
||||
return len(layer_set.MaterialLayers)
|
||||
elif material.is_a("IfcMaterialLayerSet") and hasattr(material, "MaterialLayers"):
|
||||
return len(material.MaterialLayers)
|
||||
elif material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
return len(material.MaterialProfiles)
|
||||
elif material.is_a("IfcMaterialConstituentSet") and hasattr(material, "MaterialConstituents"):
|
||||
return len(material.MaterialConstituents)
|
||||
elif material.is_a("IfcMaterial"):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if key == "material.Name":
|
||||
return getattr(material, "Name", None)
|
||||
|
||||
elif key.startswith("material.item."):
|
||||
import re
|
||||
|
||||
match = re.match(r"material\.item\.(\d+)\.(\w+)", key)
|
||||
if match:
|
||||
item_idx = int(match.group(1))
|
||||
prop_name = match.group(2)
|
||||
|
||||
items = cls._get_material_items(material)
|
||||
if items and 0 <= item_idx < len(items):
|
||||
item = items[item_idx]
|
||||
if prop_name == "Name" and hasattr(item, "Material") and item.Material:
|
||||
return getattr(item.Material, "Name", None)
|
||||
elif prop_name == "LayerThickness" and hasattr(item, "LayerThickness"):
|
||||
return getattr(item, "LayerThickness", None)
|
||||
elif hasattr(item, prop_name):
|
||||
return getattr(item, prop_name, None)
|
||||
|
||||
match = re.match(r"material\.item\.Material\.Name\.(\d+)", key)
|
||||
if match:
|
||||
item_idx = int(match.group(1))
|
||||
items = cls._get_material_items(material)
|
||||
if items and 0 <= item_idx < len(items):
|
||||
item = items[item_idx]
|
||||
if hasattr(item, "Material") and item.Material:
|
||||
return getattr(item.Material, "Name", None)
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_material_items(cls, material):
|
||||
"""Get material items (layers, profiles, or constituents) from material"""
|
||||
if material.is_a("IfcMaterialLayerSetUsage") and hasattr(material, "ForLayerSet") and material.ForLayerSet:
|
||||
if hasattr(material.ForLayerSet, "MaterialLayers"):
|
||||
return material.ForLayerSet.MaterialLayers
|
||||
elif material.is_a("IfcMaterialLayerSet") and hasattr(material, "MaterialLayers"):
|
||||
return material.MaterialLayers
|
||||
elif material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
return material.MaterialProfiles
|
||||
elif material.is_a("IfcMaterialConstituentSet") and hasattr(material, "MaterialConstituents"):
|
||||
return material.MaterialConstituents
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
"""used by Ifc Annotations with ObjectType:
|
||||
@@ -514,3 +933,390 @@ class AnnotationData:
|
||||
)
|
||||
|
||||
return sorted(relating_types, key=lambda x: x["name"])
|
||||
|
||||
|
||||
class ElementValuesData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_flattened_keys(cls, available_keys: dict[str, list[tuple[str, str]]]) -> list[tuple[str, str]]:
|
||||
"""Flatten all keys from all categories into a single list for numbering"""
|
||||
all_keys_flat = []
|
||||
for cat_name, cat_keys in available_keys.items():
|
||||
for cat_key, cat_desc in cat_keys:
|
||||
all_keys_flat.append((cat_key, cat_desc))
|
||||
return all_keys_flat
|
||||
|
||||
@classmethod
|
||||
def get_available_element_value_keys(
|
||||
cls, element: ifcopenshell.entity_instance
|
||||
) -> dict[str, list[tuple[str, str]]]:
|
||||
"""Get all available selector syntax keys for the element"""
|
||||
keys = {}
|
||||
|
||||
keys["Basic"] = cls._get_basic_keys(element)
|
||||
|
||||
keys["Attributes"] = cls._get_attribute_keys(element)
|
||||
|
||||
keys["Property Sets"] = cls._get_pset_keys(element)
|
||||
keys["Quantity Sets"] = cls._get_qset_keys(element)
|
||||
|
||||
keys["Type"] = cls._get_type_keys(element)
|
||||
keys["Spatial"] = cls._get_spatial_keys(element)
|
||||
keys["Parent"] = cls._get_parent_keys(element)
|
||||
keys["Groups"] = cls._get_group_keys(element)
|
||||
keys["Systems"] = cls._get_system_keys(element)
|
||||
keys["Zones"] = cls._get_zone_keys(element)
|
||||
|
||||
keys["Material"] = cls._get_material_keys(element)
|
||||
keys["Styles"] = cls._get_style_keys(element)
|
||||
|
||||
keys["Classification"] = cls._get_classification_keys(element)
|
||||
|
||||
keys["Profiles"] = cls._get_profile_keys(element)
|
||||
|
||||
keys["Coordinates"] = cls._get_coordinate_keys()
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_basic_keys(cls, element):
|
||||
keys = []
|
||||
keys.append(("id", f"IFC ID: {element.id()}"))
|
||||
keys.append(("class", f"IFC Class: {element.is_a()}"))
|
||||
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if predefined_type and predefined_type != "NOTDEFINED":
|
||||
keys.append(("predefined_type", f"Predefined Type: {predefined_type}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_attribute_keys(cls, element):
|
||||
keys = []
|
||||
excluded_attrs = {"id", "type", "GlobalId", "OwnerHistory", "ObjectPlacement", "Representation"}
|
||||
|
||||
for attr_name in element.get_info().keys():
|
||||
if attr_name not in excluded_attrs:
|
||||
attr_value = getattr(element, attr_name, None)
|
||||
if attr_value is not None:
|
||||
keys.append((attr_name, f"{attr_name}: {attr_value}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_pset_keys(cls, element):
|
||||
keys = []
|
||||
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
|
||||
if psets:
|
||||
for pset_name, props in psets.items():
|
||||
for prop_name, prop_value in props.items():
|
||||
if isinstance(prop_value, (str, int, float, bool)):
|
||||
keys.append((f"{pset_name}.{prop_name}", f"{pset_name}.{prop_name}: {prop_value}"))
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_qset_keys(cls, element):
|
||||
keys = []
|
||||
qsets = ifcopenshell.util.element.get_psets(element, qtos_only=True)
|
||||
if qsets:
|
||||
for qset_name, quantities in qsets.items():
|
||||
for qty_name, qty_value in quantities.items():
|
||||
if isinstance(qty_value, (str, int, float)):
|
||||
keys.append((f"{qset_name}.{qty_name}", f"{qset_name}.{qty_name}: {qty_value}"))
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_material_keys(cls, element):
|
||||
keys = []
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if not material:
|
||||
return keys
|
||||
|
||||
if hasattr(material, "Name") and material.Name:
|
||||
keys.append(("material.Name", f"Material: {material.Name}"))
|
||||
|
||||
material_count = 0
|
||||
if material.is_a("IfcMaterialLayerSetUsage") and material.ForLayerSet:
|
||||
layer_set = material.ForLayerSet
|
||||
if hasattr(layer_set, "MaterialLayers"):
|
||||
material_count = len(layer_set.MaterialLayers)
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if layer.Material and layer.Material.Name:
|
||||
keys.append(
|
||||
(f"material.item.Material.Name.{i}", f"Layer {i+1} Material Name: {layer.Material.Name}")
|
||||
)
|
||||
if hasattr(layer, "LayerThickness") and layer.LayerThickness:
|
||||
keys.append(
|
||||
(f"material.item.{i}.LayerThickness", f"Layer {i+1} Thickness: {layer.LayerThickness}")
|
||||
)
|
||||
|
||||
elif material.is_a("IfcMaterialLayerSet") and hasattr(material, "MaterialLayers"):
|
||||
material_count = len(material.MaterialLayers)
|
||||
for i, layer in enumerate(material.MaterialLayers):
|
||||
if layer.Material and layer.Material.Name:
|
||||
keys.append(
|
||||
(f"material.item.Material.Name.{i}", f"Layer {i+1} Material Name: {layer.Material.Name}")
|
||||
)
|
||||
if hasattr(layer, "LayerThickness") and layer.LayerThickness:
|
||||
keys.append((f"material.item.{i}.LayerThickness", f"Layer {i+1} Thickness: {layer.LayerThickness}"))
|
||||
|
||||
elif material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
material_count = len(material.MaterialProfiles)
|
||||
for i, profile in enumerate(material.MaterialProfiles):
|
||||
if profile.Material and profile.Material.Name:
|
||||
keys.append(
|
||||
(f"material.item.Material.Name.{i}", f"Profile {i+1} Material Name: {profile.Material.Name}")
|
||||
)
|
||||
|
||||
elif material.is_a("IfcMaterialConstituentSet") and hasattr(material, "MaterialConstituents"):
|
||||
material_count = len(material.MaterialConstituents)
|
||||
for i, constituent in enumerate(material.MaterialConstituents):
|
||||
if constituent.Material and constituent.Material.Name:
|
||||
keys.append(
|
||||
(
|
||||
f"material.item.Material.Name.{i}",
|
||||
f"Constituent {i+1} Material Name: {constituent.Material.Name}",
|
||||
)
|
||||
)
|
||||
|
||||
elif material.is_a("IfcMaterial"):
|
||||
material_count = 1
|
||||
|
||||
if material_count > 0:
|
||||
keys.append(("materials.count", f"Material Count: {material_count}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_style_keys(cls, element):
|
||||
"""Get presentation styles from the element"""
|
||||
keys = []
|
||||
|
||||
styles = ifcopenshell.util.element.get_styles(element)
|
||||
if styles:
|
||||
keys.append(("styles.count", f"Style Count: {len(styles)}"))
|
||||
for i, style in enumerate(styles):
|
||||
if hasattr(style, "Name") and style.Name:
|
||||
keys.append((f"styles.{i}.Name", f"Style {i+1} Name: {style.Name}"))
|
||||
if style.is_a("IfcSurfaceStyle") and hasattr(style, "Styles"):
|
||||
for surface_style_elem in style.Styles:
|
||||
if surface_style_elem.is_a("IfcSurfaceStyleRendering"):
|
||||
if hasattr(surface_style_elem, "SurfaceColour"):
|
||||
color = surface_style_elem.SurfaceColour
|
||||
if hasattr(color, "Red") and hasattr(color, "Green") and hasattr(color, "Blue"):
|
||||
rgb = f"RGB({color.Red:.2f}, {color.Green:.2f}, {color.Blue:.2f})"
|
||||
keys.append((f"styles.{i}.Color", f"Style {i+1} Color: {rgb}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_type_keys(cls, element):
|
||||
keys = []
|
||||
|
||||
if hasattr(element, "IsTypedBy") and element.IsTypedBy:
|
||||
element_type = element.IsTypedBy[0].RelatingType
|
||||
if hasattr(element_type, "Name") and element_type.Name:
|
||||
keys.append(("type.Name", f"Type Name: {element_type.Name}"))
|
||||
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
occurrence_count = 0
|
||||
if hasattr(element, "Types"):
|
||||
for rel in element.Types:
|
||||
if hasattr(rel, "RelatedObjects"):
|
||||
occurrence_count += len(rel.RelatedObjects)
|
||||
keys.append(("occurrences.count", f"Occurrence Count: {occurrence_count}"))
|
||||
|
||||
elif hasattr(element, "IsTypedBy") and element.IsTypedBy:
|
||||
element_type = element.IsTypedBy[0].RelatingType
|
||||
occurrence_count = 0
|
||||
if hasattr(element_type, "Types"):
|
||||
for rel in element_type.Types:
|
||||
if hasattr(rel, "RelatedObjects"):
|
||||
occurrence_count += len(rel.RelatedObjects)
|
||||
keys.append(("types.count", f"Type Occurrence Count: {occurrence_count}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_spatial_keys(cls, element):
|
||||
keys = []
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container and hasattr(container, "Name") and container.Name:
|
||||
keys.append(("container.Name", f"Container: {container.Name}"))
|
||||
|
||||
space = None
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcSpace"):
|
||||
space = current
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
if space and hasattr(space, "Name") and space.Name:
|
||||
keys.append(("space.Name", f"Space: {space.Name}"))
|
||||
|
||||
storey = None
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcBuildingStorey"):
|
||||
storey = current
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
if storey and hasattr(storey, "Name") and storey.Name:
|
||||
keys.append(("storey.Name", f"Storey: {storey.Name}"))
|
||||
|
||||
building = None
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcBuilding"):
|
||||
building = current
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
if building and hasattr(building, "Name") and building.Name:
|
||||
keys.append(("building.Name", f"Building: {building.Name}"))
|
||||
|
||||
site = None
|
||||
current = container
|
||||
while current:
|
||||
if current.is_a("IfcSite"):
|
||||
site = current
|
||||
break
|
||||
current = ifcopenshell.util.element.get_aggregate(current)
|
||||
if site and hasattr(site, "Name") and site.Name:
|
||||
keys.append(("site.Name", f"Site: {site.Name}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_parent_keys(cls, element):
|
||||
keys = []
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
if parent and hasattr(parent, "Name") and parent.Name:
|
||||
keys.append((f"parent.name", f"Parent: {parent.Name}"))
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_group_keys(cls, element):
|
||||
keys = []
|
||||
groups = []
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for assignment in element.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToGroup"):
|
||||
group = assignment.RelatingGroup
|
||||
if group.is_a("IfcGroup") and not group.is_a("IfcSystem") and not group.is_a("IfcZone"):
|
||||
groups.append(group)
|
||||
if group.Name:
|
||||
keys.append((f"group.Name", f"Group: {group.Name}"))
|
||||
|
||||
if groups:
|
||||
keys.append(("groups.count", f"Group Count: {len(groups)}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_system_keys(cls, element):
|
||||
keys = []
|
||||
systems = []
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for assignment in element.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToGroup"):
|
||||
group = assignment.RelatingGroup
|
||||
if group.is_a("IfcSystem"):
|
||||
systems.append(group)
|
||||
if group.Name:
|
||||
keys.append((f"system.Name", f"System: {group.Name}"))
|
||||
|
||||
if systems:
|
||||
keys.append(("systems.count", f"System Count: {len(systems)}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_zone_keys(cls, element):
|
||||
keys = []
|
||||
zones = []
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for assignment in element.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToGroup"):
|
||||
group = assignment.RelatingGroup
|
||||
if group.is_a("IfcZone"):
|
||||
zones.append(group)
|
||||
if group.Name:
|
||||
keys.append((f"zone.Name", f"Zone: {group.Name}"))
|
||||
|
||||
if zones:
|
||||
keys.append(("zones.count", f"Zone Count: {len(zones)}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_classification_keys(cls, element):
|
||||
keys = []
|
||||
classifications = ifcopenshell.util.classification.get_references(element)
|
||||
if classifications:
|
||||
for i, classification in enumerate(classifications):
|
||||
if hasattr(classification, "Name") and classification.Name:
|
||||
keys.append((f"classification.{i}.Name", f"Classification {i+1}: {classification.Name}"))
|
||||
if hasattr(classification, "Identification") and classification.Identification:
|
||||
keys.append(
|
||||
(
|
||||
f"classification.{i}.Identification",
|
||||
f"Classification {i+1} ID: {classification.Identification}",
|
||||
)
|
||||
)
|
||||
|
||||
keys.append(("classification.count", f"Classification Count: {len(classifications)}"))
|
||||
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _get_coordinate_keys(cls):
|
||||
return [
|
||||
("x", "X Coordinate"),
|
||||
("y", "Y Coordinate"),
|
||||
("z", "Z Coordinate"),
|
||||
("easting", "Easting"),
|
||||
("northing", "Northing"),
|
||||
("elevation", "Elevation"),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_profile_keys(cls, element):
|
||||
"""Get profile definitions from the element"""
|
||||
keys = []
|
||||
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if material:
|
||||
profiles = []
|
||||
if material.is_a("IfcMaterialProfileSet") and hasattr(material, "MaterialProfiles"):
|
||||
for profile in material.MaterialProfiles:
|
||||
if hasattr(profile, "Profile") and profile.Profile:
|
||||
profiles.append(profile.Profile)
|
||||
|
||||
if profiles:
|
||||
keys.append(("profiles.count", f"Profile Count: {len(profiles)}"))
|
||||
for i, profile in enumerate(profiles):
|
||||
if hasattr(profile, "ProfileName") and profile.ProfileName:
|
||||
keys.append((f"profiles.{i}.ProfileName", f"Profile {i+1} Name: {profile.ProfileName}"))
|
||||
if hasattr(profile, "ProfileType") and profile.ProfileType:
|
||||
keys.append((f"profiles.{i}.ProfileType", f"Profile {i+1} Type: {profile.ProfileType}"))
|
||||
|
||||
if hasattr(element, "Representation") and element.Representation:
|
||||
for representation in element.Representation.Representations:
|
||||
if hasattr(representation, "Items"):
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcExtrudedAreaSolid") and hasattr(item, "SweptArea"):
|
||||
profile = item.SweptArea
|
||||
if hasattr(profile, "ProfileName") and profile.ProfileName:
|
||||
keys.append(("profile.ProfileName", f"Swept Profile Name: {profile.ProfileName}"))
|
||||
if hasattr(profile, "ProfileType") and profile.ProfileType:
|
||||
keys.append(("profile.ProfileType", f"Swept Profile Type: {profile.ProfileType}"))
|
||||
break
|
||||
|
||||
return keys
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ import bonsai.bim.module.drawing.annotation as annotation
|
||||
import bonsai.bim.module.drawing.decoration as decoration
|
||||
from mathutils import Matrix
|
||||
from bonsai.bim.prop import BIMFilterGroup
|
||||
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData, ElementValuesData
|
||||
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
from pathlib import Path
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
@@ -687,6 +687,105 @@ BOX_ALIGNMENT_POSITIONS = [
|
||||
]
|
||||
|
||||
|
||||
class ElementValueRow(PropertyGroup):
|
||||
"""Represents a single element value row with category, key, and formatted value"""
|
||||
|
||||
category: EnumProperty(
|
||||
name="Category",
|
||||
items=[
|
||||
("Basic", "Basic", "Basic element information"),
|
||||
("Attributes", "Attributes", "IFC Attributes"),
|
||||
("Property Sets", "Property Sets", "Property Sets"),
|
||||
("Quantity Sets", "Quantity Sets", "Quantity Sets"),
|
||||
("Type", "Type", "Type information"),
|
||||
("Spatial", "Spatial", "Spatial relationships"),
|
||||
("Parent", "Parent", "Parent relationships"),
|
||||
("Classification", "Classification", "Classifications"),
|
||||
("Groups", "Groups", "Group assignments"),
|
||||
("Systems", "Systems", "System assignments"),
|
||||
("Zones", "Zones", "Zone assignments"),
|
||||
("Material", "Material", "Material information"),
|
||||
("Styles", "Styles", "Style information"),
|
||||
("Profiles", "Profiles", "Profile information"),
|
||||
("Coordinates", "Coordinates", "Coordinate information"),
|
||||
("Custom String", "Custom String", "Custom text (no element key)"),
|
||||
],
|
||||
default="Basic",
|
||||
)
|
||||
|
||||
element_key: StringProperty(
|
||||
name="Element Key", description="The element value key (e.g., 'id', 'Name', 'Pset_WallCommon.Reference')", default=""
|
||||
)
|
||||
|
||||
formatted_value: StringProperty(
|
||||
name="Formatted Value",
|
||||
description="The formatted value string with selector syntax (e.g., '{{id}}' or '``upper({{Name}})``')",
|
||||
default="",
|
||||
)
|
||||
|
||||
separator: StringProperty(
|
||||
name="Separator",
|
||||
description="Text to insert before this value when concatenating (e.g., ' - ', ', ', '\\n')",
|
||||
default=" - ",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
category: str
|
||||
element_key: str
|
||||
formatted_value: str
|
||||
separator: str
|
||||
|
||||
|
||||
def get_category_items_with_counts(self, context):
|
||||
"""Generate category items with counts dynamically"""
|
||||
category_metadata = [
|
||||
("Basic", "Basic", "Basic element information", "OBJECT_DATA"),
|
||||
("Attributes", "Attributes", "IFC Attributes", "PROPERTIES"),
|
||||
("Property Sets", "Property Sets", "Property Sets", "ALIGN_JUSTIFY"),
|
||||
("Quantity Sets", "Quantity Sets", "Quantity Sets", "SNAP_VOLUME"),
|
||||
("Type", "Type", "Type information", "FILE_VOLUME"),
|
||||
("Spatial", "Spatial", "Spatial relationships", "HOME"),
|
||||
("Parent", "Parent", "Parent relationships", "FILE_PARENT"),
|
||||
("Classification", "Classification", "Classifications", "BOOKMARKS"),
|
||||
("Groups", "Groups", "Group assignments", "OUTLINER_COLLECTION"),
|
||||
("Systems", "Systems", "System assignments", "SYSTEM"),
|
||||
("Zones", "Zones", "Zone assignments", "MESH_CIRCLE"),
|
||||
("Material", "Material", "Material information", "MATERIAL"),
|
||||
("Styles", "Styles", "Style information", "COLOR"),
|
||||
("Profiles", "Profiles", "Profile information", "OUTLINER_DATA_CURVES"),
|
||||
("Coordinates", "Coordinates", "Coordinate information", "EMPTY_ARROWS"),
|
||||
("Custom String", "Custom String", "Add custom text (no element key)", "SMALL_CAPS"),
|
||||
]
|
||||
|
||||
obj = context.active_object
|
||||
|
||||
if obj and tool.Ifc.get_entity(obj):
|
||||
try:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
text_element = element
|
||||
|
||||
if hasattr(self, 'product_used'):
|
||||
if self.product_used:
|
||||
element = tool.Ifc.get_entity(self.product_used)
|
||||
else:
|
||||
assigned = tool.Drawing.get_assigned_product(text_element)
|
||||
if assigned:
|
||||
element = assigned
|
||||
|
||||
available_keys = ElementValuesData.get_available_element_value_keys(element)
|
||||
items = []
|
||||
for i, (identifier, base_name, description, icon) in enumerate(category_metadata):
|
||||
count = len(available_keys.get(identifier, []))
|
||||
display_name = f"{base_name} ({count})" if count > 0 else base_name
|
||||
items.append((identifier, display_name, description, icon, i))
|
||||
|
||||
return items
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return [(id, name, desc, icon, i) for i, (id, name, desc, icon) in enumerate(category_metadata)]
|
||||
|
||||
|
||||
class LiteralProps(PropertyGroup):
|
||||
def set_box_alignment(self, new_value):
|
||||
markers = new_value.count(True)
|
||||
@@ -725,11 +824,57 @@ class LiteralProps(PropertyGroup):
|
||||
}
|
||||
return text_data
|
||||
|
||||
show_element_values: bpy.props.BoolProperty(
|
||||
name="Show Element Values", description="Show/hide the element values panel", default=False
|
||||
)
|
||||
|
||||
expanded_category: bpy.props.StringProperty(
|
||||
name="Expanded Category", description="Currently expanded category in the element values panel", default=""
|
||||
)
|
||||
|
||||
element_values_filter: bpy.props.StringProperty(
|
||||
name="Element Values Filter", description="Search filter for element values", default=""
|
||||
)
|
||||
|
||||
product_used: PointerProperty(
|
||||
name="Product Used",
|
||||
type=bpy.types.Object,
|
||||
description="Object to use for fetching element values. If empty, uses assigned product",
|
||||
)
|
||||
|
||||
element_value_rows: CollectionProperty(
|
||||
name="Element Value Rows",
|
||||
type=ElementValueRow,
|
||||
description="Collection of element value rows for building the literal value"
|
||||
)
|
||||
|
||||
category_for_adding: EnumProperty(
|
||||
name="Category for Adding",
|
||||
items=get_category_items_with_counts,
|
||||
default=0,
|
||||
description="Category to use when adding a new element value row"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
||||
ifc_definition_id: int
|
||||
element_value_rows: bpy.types.bpy_prop_collection_idprop[ElementValueRow]
|
||||
category_for_adding: str
|
||||
|
||||
|
||||
class LiteralApplySettings(PropertyGroup):
|
||||
literal_index: IntProperty(name="Literal Index")
|
||||
apply_text_to_all: BoolProperty(name="Apply Text to All", default=False)
|
||||
apply_path_to_all: BoolProperty(name="Apply Path to All", default=False)
|
||||
apply_box_alignment_to_all: BoolProperty(name="Apply Box Alignment to All", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
literal_index: int
|
||||
apply_text_to_all: bool
|
||||
apply_path_to_all: bool
|
||||
apply_box_alignment_to_all: bool
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
@@ -763,6 +908,24 @@ class BIMTextProperties(PropertyGroup):
|
||||
description="Non-default symbol to use for this text.",
|
||||
)
|
||||
|
||||
apply_font_size_to_all: BoolProperty(
|
||||
name="Apply Font Size to All", description="Apply font size changes to all selected text objects", default=False
|
||||
)
|
||||
apply_newline_to_all: BoolProperty(
|
||||
name="Apply Newline to All", description="Apply newline changes to all selected text objects", default=False
|
||||
)
|
||||
|
||||
literal_apply_settings: CollectionProperty(name="Literal Apply Settings", type=LiteralApplySettings)
|
||||
|
||||
def ensure_literal_apply_settings(self, literal_count: int):
|
||||
"""Ensure we have apply settings for all literals"""
|
||||
while len(self.literal_apply_settings) > literal_count:
|
||||
self.literal_apply_settings.remove(len(self.literal_apply_settings) - 1)
|
||||
|
||||
while len(self.literal_apply_settings) < literal_count:
|
||||
setting = self.literal_apply_settings.add()
|
||||
setting.literal_index = len(self.literal_apply_settings) - 1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||
@@ -772,6 +935,8 @@ class BIMTextProperties(PropertyGroup):
|
||||
list_separator: str
|
||||
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
||||
custom_symbol: str
|
||||
apply_font_size_to_all: bool
|
||||
apply_newline_to_all: bool
|
||||
|
||||
def get_symbol(self) -> Union[str, None]:
|
||||
if self.symbol == "NO SYMBOL":
|
||||
|
||||
@@ -42,6 +42,7 @@ from mathutils import geometry, Vector
|
||||
from typing import Optional, Self, Union
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
|
||||
class External(svgwrite.container.Group):
|
||||
@@ -61,6 +62,120 @@ class External(svgwrite.container.Group):
|
||||
return self.xml
|
||||
|
||||
|
||||
def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
||||
"""
|
||||
Parse markdown (links, breaks, bold, italic, bullet points) from text and return structured information.
|
||||
Returns a list of dicts with keys:
|
||||
- 'text': the text content
|
||||
- 'url': link target if present, else None
|
||||
- 'break': True if this is a line break (\n or <br>), else False
|
||||
- 'bold': True if bold, else False
|
||||
- 'italic': True if italic, else False
|
||||
- \u2022 for bullet points (you start a bullet point list by prefixing \n before the first - bullet point)
|
||||
|
||||
Example:
|
||||
Hello **bold** *italic* [World](http://ex.com)\n
|
||||
- Bullet 1\n
|
||||
- Bullet 2 <br>Another [Link](http://foo.com)
|
||||
[
|
||||
{'text': 'Hello ', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': 'bold', 'url': None, 'break': False, 'bold': True, 'italic': False},
|
||||
{'text': ' ', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': 'italic', 'url': None, 'break': False, 'bold': False, 'italic': True},
|
||||
{'text': ' ', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': 'World', 'url': 'http://ex.com', 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': None, 'url': None, 'break': True, 'bold': False, 'italic': False},
|
||||
{'text': '\u2022 ', 'url': None, 'break': True, 'bold': False, 'italic': False},
|
||||
{'text': 'Bullet 1', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': None, 'url': None, 'break': True, 'bold': False, 'italic': False},
|
||||
{'text': '\u2022 ', 'url': None, 'break': True, 'bold': False, 'italic': False},
|
||||
{'text': 'Bullet 2 ', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': None, 'url': None, 'break': True, 'bold': False, 'italic': False},
|
||||
{'text': 'Another ', 'url': None, 'break': False, 'bold': False, 'italic': False},
|
||||
{'text': 'Link', 'url': 'http://foo.com', 'break': False, 'bold': False, 'italic': False}
|
||||
]
|
||||
"""
|
||||
md = MarkdownIt("commonmark")
|
||||
tokens = md.parse(text)
|
||||
segments = []
|
||||
bold = False
|
||||
italic = False
|
||||
link_opening = None
|
||||
link_text = None
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
token = tokens[i]
|
||||
if token.type == "bullet_list_open":
|
||||
i += 1
|
||||
while i < len(tokens) and tokens[i].type != "bullet_list_close":
|
||||
if tokens[i].type == "list_item_open":
|
||||
segments.append({"text": "\u2022 ", "url": None, "break": True, "bold": False, "italic": False})
|
||||
j = i + 1
|
||||
while j < len(tokens) and tokens[j].type != "list_item_close":
|
||||
if tokens[j].type == "inline":
|
||||
for child in tokens[j].children or []:
|
||||
if child.type == "softbreak":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
elif child.type == "html_inline" and child.content.strip().lower() == "<br>":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
elif child.type == "strong_open":
|
||||
bold = True
|
||||
elif child.type == "strong_close":
|
||||
bold = False
|
||||
elif child.type == "em_open":
|
||||
italic = True
|
||||
elif child.type == "em_close":
|
||||
italic = False
|
||||
elif child.type == "link_open":
|
||||
link_opening = child
|
||||
elif child.type == "text" and link_opening:
|
||||
link_text = child.content
|
||||
elif child.type == "link_close" and link_opening:
|
||||
url = link_opening.attrGet("href")
|
||||
if url and link_text:
|
||||
segments.append({"text": link_text, "url": url, "break": False, "bold": bold, "italic": italic})
|
||||
link_opening = None
|
||||
link_text = None
|
||||
elif child.type == "text" and not link_opening:
|
||||
segments.append({"text": child.content, "url": None, "break": False, "bold": bold, "italic": italic})
|
||||
j += 1
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
i += 1
|
||||
elif token.type == "inline":
|
||||
for child in token.children or []:
|
||||
if child.type == "softbreak":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
elif child.type == "html_inline" and child.content.strip().lower() == "<br>":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
elif child.type == "strong_open":
|
||||
bold = True
|
||||
elif child.type == "strong_close":
|
||||
bold = False
|
||||
elif child.type == "em_open":
|
||||
italic = True
|
||||
elif child.type == "em_close":
|
||||
italic = False
|
||||
elif child.type == "link_open":
|
||||
link_opening = child
|
||||
elif child.type == "text" and link_opening:
|
||||
link_text = child.content
|
||||
elif child.type == "link_close" and link_opening:
|
||||
url = link_opening.attrGet("href")
|
||||
if url and link_text:
|
||||
segments.append({"text": link_text, "url": url, "break": False, "bold": bold, "italic": italic})
|
||||
link_opening = None
|
||||
link_text = None
|
||||
elif child.type == "text" and not link_opening:
|
||||
segments.append({"text": child.content, "url": None, "break": False, "bold": bold, "italic": italic})
|
||||
i += 1
|
||||
segments = [seg for seg in segments if seg.get("text") is not None or seg.get("break", False)]
|
||||
if not segments:
|
||||
return [{"text": text, "url": None, "break": False, "bold": False, "italic": False}]
|
||||
return segments
|
||||
|
||||
|
||||
class SvgWriter:
|
||||
metadata: list[str]
|
||||
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
|
||||
@@ -893,19 +1008,78 @@ class SvgWriter:
|
||||
text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
text_tags = self.create_text_tag(
|
||||
text,
|
||||
text_position_svg,
|
||||
angle,
|
||||
text_literal.BoxAlignment,
|
||||
classes_str,
|
||||
fill_bg=fill_bg,
|
||||
line_number_start=line_number,
|
||||
newline_at=newline_at,
|
||||
)
|
||||
for tag in text_tags:
|
||||
self.svg.add(tag)
|
||||
line_number += len(tag.elements)
|
||||
|
||||
text_segments = parse_markdown_it(text)
|
||||
|
||||
if len(text_segments) == 1 and text_segments[0]["url"] is None and not text_segments[0].get("break", False):
|
||||
text_tags = self.create_text_tag(
|
||||
text,
|
||||
text_position_svg,
|
||||
angle,
|
||||
text_literal.BoxAlignment,
|
||||
classes_str,
|
||||
fill_bg=fill_bg,
|
||||
line_number_start=line_number,
|
||||
newline_at=newline_at,
|
||||
)
|
||||
for tag in text_tags:
|
||||
self.svg.add(tag)
|
||||
line_number += len(text_tags)
|
||||
else:
|
||||
base_text_attrs = SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment)
|
||||
text_position_svg_str = ", ".join(map(str, text_position_svg))
|
||||
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
|
||||
|
||||
text_tag = self.svg.text("", transform=text_transform, class_=classes_str, **base_text_attrs)
|
||||
|
||||
line_idx = 0
|
||||
new_line = True
|
||||
bullet_next = False
|
||||
for idx, segment in enumerate(text_segments):
|
||||
if segment.get("break", False):
|
||||
if segment.get("text") == "\u2022 ":
|
||||
bullet_next = True
|
||||
line_idx += 1
|
||||
new_line = True
|
||||
continue
|
||||
if segment["text"] is None:
|
||||
continue
|
||||
text_content = segment["text"]
|
||||
if bullet_next:
|
||||
text_content = "\u2022 " + (text_content or "")
|
||||
bullet_next = False
|
||||
|
||||
if new_line:
|
||||
dy, x, y = f"{line_idx}em", 0, 0
|
||||
new_line = False
|
||||
else:
|
||||
dy, x, y = None, None, None
|
||||
|
||||
tspan = self.svg.tspan(text_content, class_=classes_str)
|
||||
if segment.get("bold", False):
|
||||
tspan.attribs["font-weight"] = "bold"
|
||||
if segment.get("italic", False):
|
||||
tspan.attribs["font-style"] = "italic"
|
||||
if dy is not None:
|
||||
tspan.attribs["dy"] = dy
|
||||
if x is not None:
|
||||
tspan.attribs["x"] = x
|
||||
if y is not None:
|
||||
tspan.attribs["y"] = y
|
||||
|
||||
if segment["url"]:
|
||||
link_element = self.svg.a(href=segment["url"], target="_blank")
|
||||
link_element.add(tspan)
|
||||
text_tag.add(link_element)
|
||||
else:
|
||||
text_tag.add(tspan)
|
||||
|
||||
if fill_bg:
|
||||
fill_bg_tag = self.add_fill_bg(text_tag)
|
||||
self.svg.add(fill_bg_tag)
|
||||
|
||||
self.svg.add(text_tag)
|
||||
line_number += 1
|
||||
|
||||
def draw_empty_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
|
||||
@@ -28,8 +28,9 @@ from bonsai.bim.module.drawing.data import (
|
||||
DrawingsData,
|
||||
ElementFiltersData,
|
||||
DecoratorData,
|
||||
ElementValuesData,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Union, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.drawing.prop import DocProperties, Drawing, Sheet
|
||||
@@ -475,21 +476,20 @@ class BIM_PT_sheets(Panel):
|
||||
|
||||
op = row3.operator("bim.activate_drawing_from_sheet", icon="OUTLINER_OB_CAMERA", text="")
|
||||
|
||||
|
||||
if active_sheet.reference_type == "DRAWING":
|
||||
drawingnamesvg = active_sheet.name
|
||||
drawingname = drawingnamesvg.split(".svg")[0]
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifc_annotations = ifc_file.by_type("IfcAnnotation")
|
||||
drawingid = None
|
||||
|
||||
for annotation in ifc_file.by_type("IfcAnnotation"):
|
||||
for annotation in ifc_annotations:
|
||||
if annotation.ObjectType != "DRAWING":
|
||||
continue
|
||||
|
||||
Annotation_Name = annotation.Name.replace(",", "") # Remove commas
|
||||
if Annotation_Name == drawingname:
|
||||
drawingid = annotation.id()
|
||||
break
|
||||
|
||||
if drawingid is not None:
|
||||
op.drawing = drawingid
|
||||
|
||||
@@ -551,6 +551,61 @@ class BIM_PT_product_assignments(Panel):
|
||||
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
||||
|
||||
|
||||
def get_category_icon(category_name):
|
||||
"""Get appropriate icon for each category"""
|
||||
icons = {
|
||||
"Basic": "OBJECT_DATA",
|
||||
"Attributes": "PROPERTIES",
|
||||
"Property Sets": "PROPERTIES",
|
||||
"Quantity Sets": "SNAP_VOLUME",
|
||||
"Type": "OUTLINER_OB_MESH",
|
||||
"Spatial": "HOME",
|
||||
"Parent": "FILE_PARENT",
|
||||
"Classification": "BOOKMARKS",
|
||||
"Groups": "GROUP",
|
||||
"Systems": "SYSTEM",
|
||||
"Zones": "MESH_CIRCLE",
|
||||
"Material": "MATERIAL",
|
||||
"Profiles": "MESH_DATA",
|
||||
"Coordinates": "EMPTY_ARROWS",
|
||||
}
|
||||
return icons.get(category_name, "DOT")
|
||||
|
||||
|
||||
def get_current_product_for_element_values(obj: bpy.types.Object, literal_props) -> Optional[bpy.types.Object]:
|
||||
"""Get the product to use for fetching element values - either explicitly set or assigned product"""
|
||||
if hasattr(literal_props, "product_used") and literal_props.product_used:
|
||||
return literal_props.product_used
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
return tool.Drawing.get_assigned_product(element) or obj
|
||||
return obj
|
||||
|
||||
|
||||
def get_category_icon(category: str) -> str:
|
||||
"""Get the icon for an element value category"""
|
||||
icons = {
|
||||
"Basic": "OBJECT_DATA",
|
||||
"Attributes": "PROPERTIES",
|
||||
"Property Sets": "ALIGN_JUSTIFY",
|
||||
"Quantity Sets": "SNAP_VOLUME",
|
||||
"Type": "FILE_VOLUME",
|
||||
"Spatial": "HOME",
|
||||
"Parent": "FILE_PARENT",
|
||||
"Classification": "BOOKMARKS",
|
||||
"Groups": "OUTLINER_COLLECTION",
|
||||
"Systems": "SYSTEM",
|
||||
"Zones": "MESH_CIRCLE",
|
||||
"Material": "MATERIAL",
|
||||
"Styles": "COLOR",
|
||||
"Profiles": "OUTLINER_DATA_CURVES",
|
||||
"Coordinates": "EMPTY_ARROWS",
|
||||
"Custom String": "SMALL_CAPS",
|
||||
}
|
||||
return icons.get(category, "DOT")
|
||||
|
||||
|
||||
class BIM_PT_text(Panel):
|
||||
bl_label = "Text"
|
||||
bl_idname = "BIM_PT_text"
|
||||
@@ -563,10 +618,10 @@ class BIM_PT_text(Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not context.active_object:
|
||||
return
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return False
|
||||
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||
|
||||
def draw_text_editing_ui(
|
||||
@@ -592,6 +647,8 @@ class BIM_PT_text(Panel):
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
row.prop(props, "apply_font_size_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "newline_at")
|
||||
row = self.layout.row(align=True)
|
||||
@@ -604,10 +661,13 @@ class BIM_PT_text(Panel):
|
||||
if props.symbol == "CUSTOM SYMBOL":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "custom_symbol", text="")
|
||||
row.prop(props, "apply_newline_to_all", text="", icon="COPYDOWN")
|
||||
select_op = row.operator("bim.select_similar_text_literal_value", text="", icon="RESTRICT_SELECT_OFF")
|
||||
select_op.literal_value = str(props.newline_at)
|
||||
select_op.attribute_type = "newline"
|
||||
|
||||
for i, literal_props in enumerate(props.literals):
|
||||
box = self.layout.box()
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Literal[{i}]:")
|
||||
@@ -617,25 +677,140 @@ class BIM_PT_text(Panel):
|
||||
row.operator("bim.order_text_literal_down", icon="TRIA_DOWN", text="").literal_prop_id = i
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
# skip BoxAlignment since we're going to format it ourselves
|
||||
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
|
||||
popup_active_attribute = attributes[0] if popup_mode else None
|
||||
bonsai.bim.helper.draw_attributes(attributes, box, popup_active_attribute=popup_active_attribute)
|
||||
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
|
||||
row = box.row(align=True)
|
||||
row.prop(literal_props.attributes[0], "string_value", text="Literal")
|
||||
|
||||
expand_icon = "DOWNARROW_HLT" if getattr(literal_props, "show_element_values", False) else "RIGHTARROW"
|
||||
op = row.operator("bim.toggle_element_values_panel", icon=expand_icon, text="")
|
||||
op.literal_prop_id = i
|
||||
|
||||
row.prop(props.literal_apply_settings[i], "apply_text_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assigned_element = tool.Drawing.get_assigned_product(element) or element
|
||||
resolved_value = tool.Drawing.replace_text_literal_variables(
|
||||
literal_props.attributes[0].string_value,
|
||||
assigned_element,
|
||||
props.reverse_list,
|
||||
props.list_separator,
|
||||
)
|
||||
row = box.row(align=True)
|
||||
row.label(text="CurrentValue:")
|
||||
row.label(text=str(resolved_value))
|
||||
|
||||
# Show the element values panel if expanded
|
||||
if getattr(literal_props, "show_element_values", False):
|
||||
values_box = box.box()
|
||||
|
||||
help_row = values_box.row(align=True)
|
||||
help_row.operator("bim.show_element_values_instructions", text="Instructions", icon="QUESTION")
|
||||
|
||||
element_values_row = values_box.row(align=True)
|
||||
element_values_row.label(text="Element Values:", icon="PROPERTIES")
|
||||
element_values_row.prop(literal_props, "product_used", text="", icon="EYEDROPPER")
|
||||
|
||||
current_product = get_current_product_for_element_values(obj, literal_props)
|
||||
|
||||
product_name = current_product.name if (current_product and hasattr(current_product, "name")) else "Unknown"
|
||||
source_row = values_box.row()
|
||||
source_row.label(text=f"Source: {product_name}", icon="OBJECT_DATA")
|
||||
|
||||
element = tool.Ifc.get_entity(current_product) if current_product else None
|
||||
if element:
|
||||
add_row = values_box.row(align=True)
|
||||
add_row.prop(literal_props, "category_for_adding", text="")
|
||||
|
||||
op = add_row.operator("bim.add_element_value_row", text="Add Element", icon="ADD")
|
||||
op.literal_prop_id = i
|
||||
|
||||
if len(literal_props.element_value_rows) > 0:
|
||||
for row_idx, value_row in enumerate(literal_props.element_value_rows):
|
||||
row = values_box.row(align=True)
|
||||
|
||||
is_custom_string = value_row.category == "Custom String"
|
||||
|
||||
if is_custom_string:
|
||||
category_icon = get_category_icon(value_row.category)
|
||||
row.prop(value_row, "element_key", text="", icon=category_icon)
|
||||
else:
|
||||
split = row.split(factor=0.25, align=True)
|
||||
|
||||
sep_col = split.row(align=True)
|
||||
sep_col.prop(value_row, "separator", text="")
|
||||
|
||||
key_col = split.row(align=True)
|
||||
category_icon = get_category_icon(value_row.category)
|
||||
key_col.prop(value_row, "element_key", text="", icon=category_icon)
|
||||
|
||||
op = row.operator("bim.element_value_suggestions_popup", text="", icon="VIEWZOOM")
|
||||
op.literal_prop_id = i
|
||||
op.row_index = row_idx
|
||||
op.category = value_row.category
|
||||
|
||||
op = row.operator("bim.format_element_value_row", text="", icon="SHADERFX")
|
||||
op.literal_prop_id = i
|
||||
op.row_index = row_idx
|
||||
|
||||
op = row.operator("bim.remove_element_value_row", text="", icon="X")
|
||||
op.literal_prop_id = i
|
||||
op.row_index = row_idx
|
||||
|
||||
apply_row = values_box.row()
|
||||
apply_row.scale_y = 1.2
|
||||
op = apply_row.operator("bim.apply_element_value_rows_to_literal", text="Apply to Literal", icon="CHECKMARK")
|
||||
op.literal_prop_id = i
|
||||
else:
|
||||
error_row = values_box.row()
|
||||
error_row.label(text="Selected object has no IFC data", icon="ERROR")
|
||||
|
||||
if len(literal_props.attributes) > 1:
|
||||
attr = literal_props.attributes[1]
|
||||
row = box.row(align=True)
|
||||
if getattr(attr, "data_type", None) == "enum" and getattr(attr, "enum_items", None):
|
||||
row.prop(attr, "enum_value", text="Path")
|
||||
select_value = attr.enum_value
|
||||
else:
|
||||
row.prop(attr, "string_value", text="Path")
|
||||
select_value = attr.string_value
|
||||
if i < len(props.literal_apply_settings):
|
||||
row.prop(props.literal_apply_settings[i], "apply_path_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
other_attributes = [a for a in literal_props.attributes[2:] if a.name != "BoxAlignment"]
|
||||
if other_attributes:
|
||||
bonsai.bim.helper.draw_attributes(other_attributes, box)
|
||||
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(3)]
|
||||
for i in range(9):
|
||||
cols[i % 3].prop(
|
||||
cols = [row.column(align=True) for j in range(3)]
|
||||
for j in range(9):
|
||||
cols[j % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=i,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[i] else "RADIOBUT_OFF",
|
||||
index=j,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[j] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.label(text=" Text box alignment:")
|
||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
||||
alignment_label_row = col.row(align=True)
|
||||
alignment_label_row.label(text=" Text box alignment:")
|
||||
if i < len(props.literal_apply_settings):
|
||||
alignment_label_row.prop(
|
||||
props.literal_apply_settings[i], "apply_box_alignment_to_all", text="", icon="COPYDOWN"
|
||||
)
|
||||
|
||||
box_alignment_value = (
|
||||
literal_props.attributes[
|
||||
next(
|
||||
(idx for idx, attr in enumerate(literal_props.attributes) if attr.name == "BoxAlignment"),
|
||||
-1,
|
||||
)
|
||||
].string_value
|
||||
if any(attr.name == "BoxAlignment" for attr in literal_props.attributes)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
col.label(text=f" {box_alignment_value}")
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
@@ -652,10 +827,21 @@ class BIM_PT_text(Panel):
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="FontSize")
|
||||
row.label(text=str(text_data["FontSize"]))
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value", text=str(text_data["FontSize"]), emboss=False
|
||||
)
|
||||
click_op.literal_value = str(text_data["FontSize"])
|
||||
click_op.attribute_type = "font_size"
|
||||
click_op.display_text = str(text_data["FontSize"])
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Newline_At")
|
||||
row.label(text=str(text_data["Newline_At"]))
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value", text=str(text_data["Newline_At"]), emboss=False
|
||||
)
|
||||
click_op.literal_value = str(text_data["Newline_At"])
|
||||
click_op.attribute_type = "newline"
|
||||
click_op.display_text = str(text_data["Newline_At"])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Reverse_List")
|
||||
row.label(text=str(text_data["Reverse_List"]))
|
||||
@@ -663,12 +849,30 @@ class BIM_PT_text(Panel):
|
||||
row.label(text="List_Separator")
|
||||
row.label(text=str(text_data["List_Separator"]))
|
||||
|
||||
for literal_data in text_data["Literals"]:
|
||||
for i, literal_data in enumerate(text_data["Literals"]):
|
||||
box = self.layout.box()
|
||||
box.label(text=f"Literal[{i}]:")
|
||||
|
||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||
for attribute in literal_data:
|
||||
row = box.row(align=True)
|
||||
row.label(text=attribute)
|
||||
row.label(text=literal_data[attribute])
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value",
|
||||
text=str(literal_data[attribute]),
|
||||
emboss=False,
|
||||
)
|
||||
click_op.literal_value = str(literal_data[attribute])
|
||||
click_op.literal_index = i
|
||||
if attribute == "Literal":
|
||||
click_op.attribute_type = "literal"
|
||||
elif attribute == "Path":
|
||||
click_op.attribute_type = "path"
|
||||
elif attribute == "BoxAlignment":
|
||||
click_op.attribute_type = "box_alignment"
|
||||
else:
|
||||
click_op.attribute_type = "text"
|
||||
click_op.display_text = str(literal_data[attribute])
|
||||
|
||||
|
||||
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
|
||||
@@ -259,6 +259,9 @@ class AnnotationToolUI:
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
|
||||
)
|
||||
row = cls.layout.row(align=True)
|
||||
props = tool.Drawing.get_document_props()
|
||||
row.operator("bim.filter_selected_objects_if_intersected_by_camera", text="Filter by Camera")
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -92,7 +92,12 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) ->
|
||||
|
||||
# Support weird buildingSMART dictionary mappings which behave like enums
|
||||
items: list[tuple[str, str, str]] = []
|
||||
data = json.loads(prop.enum_items)
|
||||
if not prop.enum_items:
|
||||
return items
|
||||
try:
|
||||
data = json.loads(prop.enum_items)
|
||||
except Exception:
|
||||
return items
|
||||
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
@@ -633,7 +638,7 @@ class BIMProperties(PropertyGroup):
|
||||
],
|
||||
name="Time Unit",
|
||||
default="HOUR",
|
||||
)
|
||||
)
|
||||
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
|
||||
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
|
||||
|
||||
@@ -774,15 +779,6 @@ class BIMFacet(PropertyGroup):
|
||||
pset: StringProperty(name="Pset")
|
||||
value: StringProperty(name="Value")
|
||||
type: StringProperty(name="Type")
|
||||
filter_mode: EnumProperty(
|
||||
name="Filter Mode",
|
||||
items=[
|
||||
("ADD", "Add", "Add elements to the result set (query entire IFC file)", "ADD", 0),
|
||||
("SUBTRACT", "Subtract", "Subtract matching elements from previous results", "REMOVE", 1),
|
||||
("FILTER", "Filter", "Filter down previous results to matching elements", "FILTER", 2),
|
||||
],
|
||||
default="ADD",
|
||||
)
|
||||
comparison: EnumProperty(
|
||||
items=[
|
||||
("=", "equal to", ""),
|
||||
@@ -800,7 +796,6 @@ class BIMFacet(PropertyGroup):
|
||||
pset: str
|
||||
value: str
|
||||
type: str
|
||||
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
|
||||
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
||||
|
||||
|
||||
|
||||
@@ -139,21 +139,32 @@ get_element_grammar = lark.Lark(
|
||||
)
|
||||
|
||||
format_grammar = lark.Lark(
|
||||
"""start: function
|
||||
"""start: expression
|
||||
|
||||
function: round | number | int | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER
|
||||
?expression: add_sub
|
||||
?add_sub: mul_div
|
||||
| add_sub "+" mul_div -> add
|
||||
| add_sub "-" mul_div -> subtract
|
||||
?mul_div: function
|
||||
| mul_div "*" function -> multiply
|
||||
| mul_div "/" function -> divide
|
||||
|
||||
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | NUMBER | "(" expression ")"
|
||||
|
||||
round: "round(" function "," NUMBER ")"
|
||||
number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
|
||||
int: "int(" function ")"
|
||||
variable: "{{" query_path "}}"
|
||||
query_path: /[^}]+/
|
||||
|
||||
round: "round(" expression "," NUMBER ")"
|
||||
number: "number(" expression ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
|
||||
int: "int(" expression ")"
|
||||
format_length: metric_length | imperial_length
|
||||
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
|
||||
imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
||||
lower: "lower(" function ")"
|
||||
upper: "upper(" function ")"
|
||||
title: "title(" function ")"
|
||||
concat: "concat(" function ("," function)* ")"
|
||||
substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")"
|
||||
metric_length: "metric_length(" expression "," NUMBER "," NUMBER ")"
|
||||
imperial_length: "imperial_length(" expression "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
||||
lower: "lower(" expression ")"
|
||||
upper: "upper(" expression ")"
|
||||
title: "title(" expression ")"
|
||||
concat: "concat(" expression ("," expression)* ")"
|
||||
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
|
||||
boolean: TRUE | FALSE
|
||||
|
||||
TRUE: "true" | "True" | "TRUE"
|
||||
@@ -189,9 +200,83 @@ format_grammar = lark.Lark(
|
||||
|
||||
|
||||
class FormatTransformer(lark.Transformer):
|
||||
def __init__(self, element=None):
|
||||
"""Initialize transformer with optional element for variable substitution"""
|
||||
super().__init__()
|
||||
self.element = element
|
||||
|
||||
def start(self, args):
|
||||
return args[0]
|
||||
|
||||
def expression(self, args):
|
||||
return args[0]
|
||||
|
||||
def variable(self, args):
|
||||
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
||||
if self.element is None:
|
||||
return "0" # Default value if no element context
|
||||
|
||||
query_path = args[0]
|
||||
try:
|
||||
value = get_element_value(self.element, query_path)
|
||||
if value is None:
|
||||
return "0"
|
||||
# Convert to string for further processing
|
||||
return str(value)
|
||||
except:
|
||||
return "0" # Return default on error
|
||||
|
||||
def query_path(self, args):
|
||||
"""Extract the query path from variable"""
|
||||
return str(args[0]).strip()
|
||||
|
||||
def add(self, args):
|
||||
"""Handle addition operation"""
|
||||
left, right = args
|
||||
try:
|
||||
left_val = float(left) if left != "None" and left is not None else 0.0
|
||||
right_val = float(right) if right != "None" and right is not None else 0.0
|
||||
result = left_val + right_val
|
||||
# Return integer if result has no decimal part
|
||||
if result % 1 == 0:
|
||||
return str(int(result))
|
||||
return str(result)
|
||||
except (ValueError, TypeError):
|
||||
# If can't convert to numbers, concatenate as strings
|
||||
return str(left) + str(right)
|
||||
|
||||
def subtract(self, args):
|
||||
"""Handle subtraction operation"""
|
||||
left, right = args
|
||||
left_val = float(left) if left != "None" and left is not None else 0.0
|
||||
right_val = float(right) if right != "None" and right is not None else 0.0
|
||||
result = left_val - right_val
|
||||
if result % 1 == 0:
|
||||
return str(int(result))
|
||||
return str(result)
|
||||
|
||||
def multiply(self, args):
|
||||
"""Handle multiplication operation"""
|
||||
left, right = args
|
||||
left_val = float(left) if left != "None" and left is not None else 0.0
|
||||
right_val = float(right) if right != "None" and right is not None else 0.0
|
||||
result = left_val * right_val
|
||||
if result % 1 == 0:
|
||||
return str(int(result))
|
||||
return str(result)
|
||||
|
||||
def divide(self, args):
|
||||
"""Handle division operation"""
|
||||
left, right = args
|
||||
left_val = float(left) if left != "None" and left is not None else 0.0
|
||||
right_val = float(right) if right != "None" and right is not None else 1.0
|
||||
if right_val == 0:
|
||||
return "inf" # or raise an error, or return "0"
|
||||
result = left_val / right_val
|
||||
if result % 1 == 0:
|
||||
return str(int(result))
|
||||
return str(result)
|
||||
|
||||
def function(self, args):
|
||||
return args[0]
|
||||
|
||||
@@ -211,7 +296,7 @@ class FormatTransformer(lark.Transformer):
|
||||
return str(args[0]).title()
|
||||
|
||||
def concat(self, args):
|
||||
return "".join(args)
|
||||
return "".join(str(arg) for arg in args)
|
||||
|
||||
def substr(self, args):
|
||||
if len(args) == 3:
|
||||
@@ -241,13 +326,14 @@ class FormatTransformer(lark.Transformer):
|
||||
return str(result)
|
||||
|
||||
def number(self, args):
|
||||
if isinstance(args[0], str):
|
||||
args[0] = float(args[0]) if "." in args[0] else int(args[0])
|
||||
arg_val = args[0]
|
||||
if isinstance(arg_val, str):
|
||||
arg_val = float(arg_val) if "." in arg_val else int(arg_val)
|
||||
if len(args) >= 3 and args[2]:
|
||||
return "{:,}".format(args[0]).replace(".", "*").replace(",", args[2]).replace("*", args[1])
|
||||
return "{:,}".format(arg_val).replace(".", "*").replace(",", args[2]).replace("*", args[1])
|
||||
elif len(args) >= 2 and args[1]:
|
||||
return "{}".format(args[0]).replace(".", args[1])
|
||||
return "{:,}".format(args[0])
|
||||
return "{}".format(arg_val).replace(".", args[1])
|
||||
return "{:,}".format(arg_val)
|
||||
|
||||
def format_length(self, args):
|
||||
return args[0]
|
||||
@@ -287,7 +373,8 @@ class FormatTransformer(lark.Transformer):
|
||||
)
|
||||
|
||||
def int(self, args: list[str]) -> str:
|
||||
return str(int(float(args[0])))
|
||||
value = 0.0 if args[0] == "None" else args[0] or 0.0
|
||||
return str(int(float(value)))
|
||||
|
||||
|
||||
class GetElementTransformer(lark.Transformer):
|
||||
@@ -313,8 +400,18 @@ class GetElementTransformer(lark.Transformer):
|
||||
return args[1:-1].replace("\\", "")
|
||||
|
||||
|
||||
def format(query: str) -> str:
|
||||
return FormatTransformer().transform(format_grammar.parse(query))
|
||||
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
|
||||
"""Format a query string with optional element context for variable substitution.
|
||||
|
||||
:param query: Format query string (can include {{variable}} placeholders)
|
||||
:param element: Optional IFC element for variable substitution
|
||||
:return: Formatted string
|
||||
|
||||
Example:
|
||||
format("{{z}} / 2", element) # Substitutes element's z value
|
||||
format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation
|
||||
"""
|
||||
return FormatTransformer(element).transform(format_grammar.parse(query))
|
||||
|
||||
|
||||
def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any:
|
||||
@@ -1163,4 +1260,4 @@ class FacetTransformer(lark.Transformer):
|
||||
|
||||
if comparison.startswith("!"):
|
||||
return not result
|
||||
return result
|
||||
return result
|
||||
Reference in New Issue
Block a user