mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +00:00
PR7500 squashed commit
This commit is contained in:
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
* { stroke-linecap: round; stroke-linejoin: round; }
|
* { 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; }
|
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; }
|
.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; }
|
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ classes = (
|
|||||||
operator.EnableEditingText,
|
operator.EnableEditingText,
|
||||||
operator.ExcludeAnnotation,
|
operator.ExcludeAnnotation,
|
||||||
operator.ExpandSheet,
|
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.LoadDrawings,
|
||||||
operator.LoadReferences,
|
operator.LoadReferences,
|
||||||
operator.LoadSchedules,
|
operator.LoadSchedules,
|
||||||
@@ -89,8 +100,10 @@ classes = (
|
|||||||
operator.SelectAllDrawings,
|
operator.SelectAllDrawings,
|
||||||
operator.SelectAllSheets,
|
operator.SelectAllSheets,
|
||||||
operator.SelectAssignedProduct,
|
operator.SelectAssignedProduct,
|
||||||
|
operator.SelectSimilarTextLiteralValue,
|
||||||
operator.ToggleTargetView,
|
operator.ToggleTargetView,
|
||||||
operator.OpenDocumentationWebUi,
|
operator.OpenDocumentationWebUi,
|
||||||
|
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
||||||
prop.Variable,
|
prop.Variable,
|
||||||
prop.Drawing,
|
prop.Drawing,
|
||||||
prop.Document,
|
prop.Document,
|
||||||
@@ -98,7 +111,9 @@ classes = (
|
|||||||
prop.Sheet,
|
prop.Sheet,
|
||||||
prop.DocProperties,
|
prop.DocProperties,
|
||||||
prop.BIMCameraProperties,
|
prop.BIMCameraProperties,
|
||||||
|
prop.ElementValueRow,
|
||||||
prop.LiteralProps,
|
prop.LiteralProps,
|
||||||
|
prop.LiteralApplySettings,
|
||||||
prop.BIMTextProperties,
|
prop.BIMTextProperties,
|
||||||
prop.BIMAssignedProductProperties,
|
prop.BIMAssignedProductProperties,
|
||||||
prop.BIMAnnotationProperties,
|
prop.BIMAnnotationProperties,
|
||||||
@@ -143,7 +158,6 @@ def menu_func(self, context):
|
|||||||
if element and element.is_a("IfcAnnotation") and element.ObjectType in ["SECTION", "ELEVATION"]:
|
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")
|
self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing")
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
|
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import json
|
|||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.representation
|
import ifcopenshell.util.representation
|
||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.unit
|
||||||
|
import ifcopenshell.util.selector
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
@@ -335,19 +336,17 @@ class DecoratorData:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
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"""
|
returns font size in mm for current ifc text object"""
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
|
|
||||||
# getting font size
|
# getting font size
|
||||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||||
# use `regular` as default
|
|
||||||
|
|
||||||
# get font size
|
# get font size
|
||||||
classes = pset_data.get("Classes", None) or "regular"
|
classes = pset_data.get("Classes", None) or "regular"
|
||||||
classes_split = classes.split()
|
classes_split = classes.split()
|
||||||
# prioritize smaller font sizes just like in svg
|
|
||||||
font_size_type = next(
|
font_size_type = next(
|
||||||
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
|
(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)
|
literals = tool.Drawing.get_text_literal(obj, return_list=True)
|
||||||
assert isinstance(literals, list)
|
assert isinstance(literals, list)
|
||||||
literals_data: list[dict[str, Any]] = []
|
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:
|
for literal in literals:
|
||||||
literal_value = literal.Literal
|
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_data = {
|
||||||
"Literal": literal_value,
|
"Literal": literal_value,
|
||||||
"BoxAlignment": literal.BoxAlignment,
|
"BoxAlignment": literal.BoxAlignment,
|
||||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(
|
"CurrentValue": current_value,
|
||||||
literal_value, product, reverse_list, list_separator
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
literals_data.append(literal_data)
|
literals_data.append(literal_data)
|
||||||
|
|
||||||
@@ -382,6 +388,419 @@ class DecoratorData:
|
|||||||
"List_Separator": list_separator,
|
"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
|
@classmethod
|
||||||
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||||
"""used by Ifc Annotations with ObjectType:
|
"""used by Ifc Annotations with ObjectType:
|
||||||
@@ -514,3 +933,390 @@ class AnnotationData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return sorted(relating_types, key=lambda x: x["name"])
|
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
|
import bonsai.bim.module.drawing.decoration as decoration
|
||||||
from mathutils import Matrix
|
from mathutils import Matrix
|
||||||
from bonsai.bim.prop import BIMFilterGroup
|
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 bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from bonsai.bim.prop import Attribute, StrProperty
|
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):
|
class LiteralProps(PropertyGroup):
|
||||||
def set_box_alignment(self, new_value):
|
def set_box_alignment(self, new_value):
|
||||||
markers = new_value.count(True)
|
markers = new_value.count(True)
|
||||||
@@ -725,11 +824,57 @@ class LiteralProps(PropertyGroup):
|
|||||||
}
|
}
|
||||||
return text_data
|
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:
|
if TYPE_CHECKING:
|
||||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||||
value: str
|
value: str
|
||||||
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
||||||
ifc_definition_id: int
|
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):
|
class BIMTextProperties(PropertyGroup):
|
||||||
@@ -763,6 +908,24 @@ class BIMTextProperties(PropertyGroup):
|
|||||||
description="Non-default symbol to use for this text.",
|
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:
|
if TYPE_CHECKING:
|
||||||
is_editing: bool
|
is_editing: bool
|
||||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||||
@@ -772,6 +935,8 @@ class BIMTextProperties(PropertyGroup):
|
|||||||
list_separator: str
|
list_separator: str
|
||||||
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
||||||
custom_symbol: str
|
custom_symbol: str
|
||||||
|
apply_font_size_to_all: bool
|
||||||
|
apply_newline_to_all: bool
|
||||||
|
|
||||||
def get_symbol(self) -> Union[str, None]:
|
def get_symbol(self) -> Union[str, None]:
|
||||||
if self.symbol == "NO SYMBOL":
|
if self.symbol == "NO SYMBOL":
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from mathutils import geometry, Vector
|
|||||||
from typing import Optional, Self, Union
|
from typing import Optional, Self, Union
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from markdown_it import MarkdownIt
|
||||||
|
|
||||||
|
|
||||||
class External(svgwrite.container.Group):
|
class External(svgwrite.container.Group):
|
||||||
@@ -61,6 +62,120 @@ class External(svgwrite.container.Group):
|
|||||||
return self.xml
|
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:
|
class SvgWriter:
|
||||||
metadata: list[str]
|
metadata: list[str]
|
||||||
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
|
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
|
||||||
@@ -893,19 +1008,78 @@ class SvgWriter:
|
|||||||
text = tool.Drawing.replace_text_literal_variables(
|
text = tool.Drawing.replace_text_literal_variables(
|
||||||
text_literal.Literal, product or element, reverse_list, list_separator
|
text_literal.Literal, product or element, reverse_list, list_separator
|
||||||
)
|
)
|
||||||
text_tags = self.create_text_tag(
|
|
||||||
text,
|
text_segments = parse_markdown_it(text)
|
||||||
text_position_svg,
|
|
||||||
angle,
|
if len(text_segments) == 1 and text_segments[0]["url"] is None and not text_segments[0].get("break", False):
|
||||||
text_literal.BoxAlignment,
|
text_tags = self.create_text_tag(
|
||||||
classes_str,
|
text,
|
||||||
fill_bg=fill_bg,
|
text_position_svg,
|
||||||
line_number_start=line_number,
|
angle,
|
||||||
newline_at=newline_at,
|
text_literal.BoxAlignment,
|
||||||
)
|
classes_str,
|
||||||
for tag in text_tags:
|
fill_bg=fill_bg,
|
||||||
self.svg.add(tag)
|
line_number_start=line_number,
|
||||||
line_number += len(tag.elements)
|
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:
|
def draw_empty_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
|
||||||
x_offset = self.raw_width / 2
|
x_offset = self.raw_width / 2
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ from bonsai.bim.module.drawing.data import (
|
|||||||
DrawingsData,
|
DrawingsData,
|
||||||
ElementFiltersData,
|
ElementFiltersData,
|
||||||
DecoratorData,
|
DecoratorData,
|
||||||
|
ElementValuesData,
|
||||||
)
|
)
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Union, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bonsai.bim.module.drawing.prop import DocProperties, Drawing, Sheet
|
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="")
|
op = row3.operator("bim.activate_drawing_from_sheet", icon="OUTLINER_OB_CAMERA", text="")
|
||||||
|
|
||||||
|
|
||||||
if active_sheet.reference_type == "DRAWING":
|
if active_sheet.reference_type == "DRAWING":
|
||||||
drawingnamesvg = active_sheet.name
|
drawingnamesvg = active_sheet.name
|
||||||
drawingname = drawingnamesvg.split(".svg")[0]
|
drawingname = drawingnamesvg.split(".svg")[0]
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
|
ifc_annotations = ifc_file.by_type("IfcAnnotation")
|
||||||
drawingid = None
|
drawingid = None
|
||||||
|
for annotation in ifc_annotations:
|
||||||
for annotation in ifc_file.by_type("IfcAnnotation"):
|
|
||||||
if annotation.ObjectType != "DRAWING":
|
if annotation.ObjectType != "DRAWING":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
Annotation_Name = annotation.Name.replace(",", "") # Remove commas
|
Annotation_Name = annotation.Name.replace(",", "") # Remove commas
|
||||||
if Annotation_Name == drawingname:
|
if Annotation_Name == drawingname:
|
||||||
drawingid = annotation.id()
|
drawingid = annotation.id()
|
||||||
break
|
break
|
||||||
|
|
||||||
if drawingid is not None:
|
if drawingid is not None:
|
||||||
op.drawing = drawingid
|
op.drawing = drawingid
|
||||||
|
|
||||||
@@ -551,6 +551,61 @@ class BIM_PT_product_assignments(Panel):
|
|||||||
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
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):
|
class BIM_PT_text(Panel):
|
||||||
bl_label = "Text"
|
bl_label = "Text"
|
||||||
bl_idname = "BIM_PT_text"
|
bl_idname = "BIM_PT_text"
|
||||||
@@ -563,10 +618,10 @@ class BIM_PT_text(Panel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not tool.Ifc.get() or not context.active_object:
|
if not tool.Ifc.get() or not context.active_object:
|
||||||
return
|
return False
|
||||||
element = tool.Ifc.get_entity(context.active_object)
|
element = tool.Ifc.get_entity(context.active_object)
|
||||||
if not element:
|
if not element:
|
||||||
return
|
return False
|
||||||
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||||
|
|
||||||
def draw_text_editing_ui(
|
def draw_text_editing_ui(
|
||||||
@@ -592,6 +647,8 @@ class BIM_PT_text(Panel):
|
|||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.prop(props, "font_size")
|
row.prop(props, "font_size")
|
||||||
|
row.prop(props, "apply_font_size_to_all", text="", icon="COPYDOWN")
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.prop(props, "newline_at")
|
row.prop(props, "newline_at")
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
@@ -604,10 +661,13 @@ class BIM_PT_text(Panel):
|
|||||||
if props.symbol == "CUSTOM SYMBOL":
|
if props.symbol == "CUSTOM SYMBOL":
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.prop(props, "custom_symbol", text="")
|
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):
|
for i, literal_props in enumerate(props.literals):
|
||||||
box = self.layout.box()
|
box = self.layout.box()
|
||||||
row = self.layout.row(align=True)
|
|
||||||
|
|
||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.label(text=f"Literal[{i}]:")
|
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.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
|
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||||
|
|
||||||
# skip BoxAlignment since we're going to format it ourselves
|
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
|
||||||
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
|
row = box.row(align=True)
|
||||||
popup_active_attribute = attributes[0] if popup_mode else None
|
row.prop(literal_props.attributes[0], "string_value", text="Literal")
|
||||||
bonsai.bim.helper.draw_attributes(attributes, box, popup_active_attribute=popup_active_attribute)
|
|
||||||
|
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)
|
row = box.row(align=True)
|
||||||
cols = [row.column(align=True) for i in range(3)]
|
cols = [row.column(align=True) for j in range(3)]
|
||||||
for i in range(9):
|
for j in range(9):
|
||||||
cols[i % 3].prop(
|
cols[j % 3].prop(
|
||||||
literal_props,
|
literal_props,
|
||||||
"box_alignment",
|
"box_alignment",
|
||||||
text="",
|
text="",
|
||||||
index=i,
|
index=j,
|
||||||
icon="RADIOBUT_ON" if literal_props.box_alignment[i] else "RADIOBUT_OFF",
|
icon="RADIOBUT_ON" if literal_props.box_alignment[j] else "RADIOBUT_OFF",
|
||||||
)
|
)
|
||||||
|
|
||||||
col = row.column(align=True)
|
col = row.column(align=True)
|
||||||
col.label(text=" Text box alignment:")
|
alignment_label_row = col.row(align=True)
|
||||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
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):
|
def draw(self, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
@@ -652,10 +827,21 @@ class BIM_PT_text(Panel):
|
|||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.label(text="FontSize")
|
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 = self.layout.row(align=True)
|
||||||
row.label(text="Newline_At")
|
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 = self.layout.row(align=True)
|
||||||
row.label(text="Reverse_List")
|
row.label(text="Reverse_List")
|
||||||
row.label(text=str(text_data["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="List_Separator")
|
||||||
row.label(text=str(text_data["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 = 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:
|
for attribute in literal_data:
|
||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.label(text=attribute)
|
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):
|
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ class AnnotationToolUI:
|
|||||||
add_layout_hotkey_operator(
|
add_layout_hotkey_operator(
|
||||||
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
|
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):
|
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
|
# Support weird buildingSMART dictionary mappings which behave like enums
|
||||||
items: list[tuple[str, str, str]] = []
|
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):
|
if isinstance(data, dict):
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
@@ -774,15 +779,6 @@ class BIMFacet(PropertyGroup):
|
|||||||
pset: StringProperty(name="Pset")
|
pset: StringProperty(name="Pset")
|
||||||
value: StringProperty(name="Value")
|
value: StringProperty(name="Value")
|
||||||
type: StringProperty(name="Type")
|
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(
|
comparison: EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
("=", "equal to", ""),
|
("=", "equal to", ""),
|
||||||
@@ -800,7 +796,6 @@ class BIMFacet(PropertyGroup):
|
|||||||
pset: str
|
pset: str
|
||||||
value: str
|
value: str
|
||||||
type: str
|
type: str
|
||||||
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
|
|
||||||
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -139,21 +139,32 @@ get_element_grammar = lark.Lark(
|
|||||||
)
|
)
|
||||||
|
|
||||||
format_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
|
||||||
|
|
||||||
round: "round(" function "," NUMBER ")"
|
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | NUMBER | "(" expression ")"
|
||||||
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
|
format_length: metric_length | imperial_length
|
||||||
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
|
metric_length: "metric_length(" expression "," NUMBER "," NUMBER ")"
|
||||||
imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
imperial_length: "imperial_length(" expression "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
|
||||||
lower: "lower(" function ")"
|
lower: "lower(" expression ")"
|
||||||
upper: "upper(" function ")"
|
upper: "upper(" expression ")"
|
||||||
title: "title(" function ")"
|
title: "title(" expression ")"
|
||||||
concat: "concat(" function ("," function)* ")"
|
concat: "concat(" expression ("," expression)* ")"
|
||||||
substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")"
|
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
|
||||||
boolean: TRUE | FALSE
|
boolean: TRUE | FALSE
|
||||||
|
|
||||||
TRUE: "true" | "True" | "TRUE"
|
TRUE: "true" | "True" | "TRUE"
|
||||||
@@ -189,9 +200,83 @@ format_grammar = lark.Lark(
|
|||||||
|
|
||||||
|
|
||||||
class FormatTransformer(lark.Transformer):
|
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):
|
def start(self, args):
|
||||||
return args[0]
|
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):
|
def function(self, args):
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
@@ -211,7 +296,7 @@ class FormatTransformer(lark.Transformer):
|
|||||||
return str(args[0]).title()
|
return str(args[0]).title()
|
||||||
|
|
||||||
def concat(self, args):
|
def concat(self, args):
|
||||||
return "".join(args)
|
return "".join(str(arg) for arg in args)
|
||||||
|
|
||||||
def substr(self, args):
|
def substr(self, args):
|
||||||
if len(args) == 3:
|
if len(args) == 3:
|
||||||
@@ -241,13 +326,14 @@ class FormatTransformer(lark.Transformer):
|
|||||||
return str(result)
|
return str(result)
|
||||||
|
|
||||||
def number(self, args):
|
def number(self, args):
|
||||||
if isinstance(args[0], str):
|
arg_val = args[0]
|
||||||
args[0] = float(args[0]) if "." in args[0] else int(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]:
|
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]:
|
elif len(args) >= 2 and args[1]:
|
||||||
return "{}".format(args[0]).replace(".", args[1])
|
return "{}".format(arg_val).replace(".", args[1])
|
||||||
return "{:,}".format(args[0])
|
return "{:,}".format(arg_val)
|
||||||
|
|
||||||
def format_length(self, args):
|
def format_length(self, args):
|
||||||
return args[0]
|
return args[0]
|
||||||
@@ -287,7 +373,8 @@ class FormatTransformer(lark.Transformer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def int(self, args: list[str]) -> str:
|
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):
|
class GetElementTransformer(lark.Transformer):
|
||||||
@@ -313,8 +400,18 @@ class GetElementTransformer(lark.Transformer):
|
|||||||
return args[1:-1].replace("\\", "")
|
return args[1:-1].replace("\\", "")
|
||||||
|
|
||||||
|
|
||||||
def format(query: str) -> str:
|
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
|
||||||
return FormatTransformer().transform(format_grammar.parse(query))
|
"""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:
|
def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any:
|
||||||
|
|||||||
Reference in New Issue
Block a user