diff --git a/src/blenderbim/blenderbim/bim/data/templates/symbols.svg b/src/blenderbim/blenderbim/bim/data/templates/symbols.svg
index ea0e4b3630..8b09f0e441 100644
--- a/src/blenderbim/blenderbim/bim/data/templates/symbols.svg
+++ b/src/blenderbim/blenderbim/bim/data/templates/symbols.svg
@@ -5,7 +5,9 @@
+
+
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py
index 8286edcba8..652647b2b7 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py
@@ -40,6 +40,8 @@ classes = (
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
+ operator.AddTextLiteral,
+ operator.RemoveTextLiteral,
operator.DuplicateDrawing,
operator.EditAssignedProduct,
operator.EditText,
@@ -71,6 +73,7 @@ classes = (
prop.Sheet,
prop.DocProperties,
prop.BIMCameraProperties,
+ prop.Literal,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
ui.BIM_PT_camera,
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/data.py b/src/blenderbim/blenderbim/bim/module/drawing/data.py
index 0d695ad672..5ce3aeab2b 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/data.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/data.py
@@ -24,7 +24,6 @@ import blenderbim.tool as tool
def refresh():
ProductAssignmentsData.is_loaded = False
- TextData.is_loaded = False
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
@@ -51,27 +50,6 @@ class ProductAssignmentsData:
return f"{rel.RelatingProduct.is_a()}/{name}"
-class TextData:
- data = {}
- is_loaded = False
-
- @classmethod
- def load(cls):
- cls.data = {"attributes": cls.attributes()}
- cls.is_loaded = True
-
- @classmethod
- def attributes(cls):
- element = tool.Ifc.get_entity(bpy.context.active_object)
- if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
- return []
- text_literal = tool.Drawing.get_text_literal(bpy.context.active_object)
- return [
- {"name": "Literal", "value": text_literal.Literal},
- {"name": "BoxAlignment", "value": text_literal.BoxAlignment},
- ]
-
-
class SheetsData:
data = {}
is_loaded = False
@@ -199,8 +177,8 @@ class DecoratorData:
return result
element = tool.Ifc.get_entity(obj)
- if not element:
- return
+ if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
+ return None
props = obj.BIMTextProperties
# getting font size
@@ -217,10 +195,22 @@ class DecoratorData:
font_size = FONT_SIZES[font_size_type]
# other attributes
- literal = tool.Drawing.get_text_literal(obj)
- box_alignment = literal.BoxAlignment
- text = props.value
+ props_literals = props.literals
+ props_literals_n = len(props.literals)
+ literals = tool.Drawing.get_text_literal(obj, return_list=True)
+ literals_data = []
+ for i, literal in enumerate(literals):
+ literal_data = {
+ "Literal": literal.Literal,
+ "BoxAlignment": literal.BoxAlignment,
+ }
+ if i < props_literals_n:
+ literal_data["CurrentValue"] = props_literals[i].value
+ else:
+ literal_data["CurrentValue"] = literal.Literal
- text_data = {"text": text, "font_size": font_size, "box_alignment": box_alignment}
+ literals_data.append(literal_data)
+
+ text_data = {"Literals": literals_data, "FontSize": font_size}
cls.data[obj.name] = text_data
return text_data
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
index feb43be55c..4ab398f270 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
@@ -2028,8 +2028,8 @@ class TextDecorator(BaseDecorator):
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
- #define ARROW_ANGLE PI / 12.0
- #define ARROW_SIZE 16.0
+ #define CIRCLE_SIZE 4.0
+ #define CIRCLE_SEGS_ASTERISK 6
"""
)
@@ -2040,54 +2040,34 @@ class TextDecorator(BaseDecorator):
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
+ void circle_head_asterisk(in float size, out vec4 head[CIRCLE_SEGS_ASTERISK]) {
+ float angle_d = PI * 2 / CIRCLE_SEGS_ASTERISK;
+ for(int i = 0; i 1:
+ verts = [obj.location]
+ idxs = [(0, 0)]
+ self.draw_lines(context, obj, verts, idxs)
+
+ box_alignment_used = []
+ for literal_data in literals_data:
+ box_alignment = literal_data["BoxAlignment"]
+ # Skip literals with the same box alignment to prevent visual clutter in viewport
+ # User is still indicated by the asterisk symbol
+ if box_alignment in box_alignment_used:
+ continue
+ box_alignment_used.append(box_alignment)
+
+ for line_i, line in enumerate(literal_data["CurrentValue"].split("\\n")):
+ self.draw_label(
+ context,
+ line,
+ pos,
+ dir,
+ gap=0,
+ center=False,
+ vcenter=False,
+ font_size_mm=text_data["FontSize"],
+ line_no=line_i,
+ box_alignment=box_alignment,
+ )
class DecorationsHandler:
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
index 58289bcede..fdc15123d2 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
@@ -39,12 +39,12 @@ import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
-from blenderbim.bim.module.drawing.data import DecoratorData, TextData
+from blenderbim.bim.module.drawing.data import DecoratorData
import blenderbim.bim.export_ifc
from lxml import etree
from mathutils import Vector
from timeit import default_timer as timer
-from blenderbim.bim.module.drawing.prop import RasterStyleProperty
+from blenderbim.bim.module.drawing.prop import RasterStyleProperty, Literal
from blenderbim.bim.ifc import IfcStore
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -197,6 +197,14 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
+ pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
+ related_paths = {
+ "Stylesheet": pset.get("Stylesheet"),
+ "Markers": pset.get("Markers"),
+ "Symbols": pset.get("Symbols"),
+ "Patterns": pset.get("Patterns"),
+ }
+ self.svg_writer.define_related_paths(**related_paths)
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
@@ -230,10 +238,7 @@ class CreateDrawing(bpy.types.Operator):
# Hacky :)
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
with open(svg_path, "w") as outfile:
- pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
- self.svg_writer.create_blank_svg(svg_path).define_boilerplate(
- pset.get("Stylesheet"), pset.get("Markers"), pset.get("Symbols"), pset.get("Patterns")
- )
+ self.svg_writer.create_blank_svg(svg_path).define_boilerplate()
boilerplate = self.svg_writer.svg.tostring()
outfile.write(boilerplate.replace("", ""))
if underlay:
@@ -1211,30 +1216,39 @@ class EditTextPopup(bpy.types.Operator):
# shares most of the code with BIM_PT_text.draw()
# need to keep them in sync or move to some common function
- if not TextData.is_loaded:
- TextData.load()
props = context.active_object.BIMTextProperties
- # skip BoxAlignment since we're going to format it ourselves
- attributes = [a for a in props.attributes if a.name != "BoxAlignment"]
- # set first attribute with text to be active by default
- blenderbim.bim.helper.draw_attributes(attributes, self.layout, popup_active_attribute=attributes[0])
+ row = self.layout.row(align=True)
+ row.operator("bim.add_text_literal", icon="ADD", text="Add literal")
+
row = self.layout.row(align=True)
row.prop(props, "font_size")
- # a bit hacky way to align box alignment widget
- rows = [self.layout.row(align=True) for i in range(3)]
- for i in range(9):
- if i % 3 == 0:
- split = rows[i // 3].split(factor=0.1, align=True)
- split.column()
- split.prop(props, "box_alignment", text="", index=i)
+ for i, literal_props in enumerate(props.literals):
+ box = self.layout.box()
+ row = self.layout.row(align=True)
- text_lines = ["Text box alignment:", props.attributes["BoxAlignment"].string_value, ""]
- for i in range(3):
- split = rows[i].split(factor=0.1, align=False)
- split.column()
- split.label(text=text_lines[i])
+ row = box.row(align=True)
+ row.label(text=f"Literal[{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"]
+ blenderbim.bim.helper.draw_attributes(attributes, box)
+
+ # a bit hacky way to align box alignment widget
+ rows = [box.row(align=True) for i in range(3)]
+ for i in range(9):
+ if i % 3 == 0:
+ split = rows[i // 3].split(factor=0.1, align=True)
+ split.column()
+ split.prop(literal_props, "box_alignment", text="", index=i)
+
+ text_lines = ["Text box alignment:", literal_props.attributes["BoxAlignment"].string_value, ""]
+ for i in range(3):
+ split = rows[i].split(factor=0.1, align=False)
+ split.column()
+ split.label(text=text_lines[i])
def cancel(self, context):
# disable editing when dialog is closed
@@ -1259,7 +1273,7 @@ class EditText(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.edit_text(tool.Ifc, tool.Drawing, obj=context.active_object)
+ core.edit_text(tool.Drawing, obj=context.active_object)
tool.Blender.update_viewport()
@@ -1285,6 +1299,54 @@ class DisableEditingText(bpy.types.Operator, Operator):
tool.Blender.update_viewport()
+class AddTextLiteral(bpy.types.Operator):
+ bl_idname = "bim.add_text_literal"
+ bl_label = "Add text literal"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def execute(self, context):
+ obj = context.active_object
+
+ # similar to `tool.Drawing.import_text_attributes`
+ literal_props = obj.BIMTextProperties.literals.add()
+ literal_attributes = literal_props.attributes
+ literal_attr_values = {
+ "Literal": "Literal",
+ "Path": "RIGHT",
+ "BoxAlignment": "bottom_left",
+ }
+ # emulates `blenderbim.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)`
+ for attr_name in literal_attr_values:
+ attr = literal_attributes.add()
+ attr.name = attr_name
+ if attr_name == "Path":
+ attr.data_type = "enum"
+ attr.enum_items = '["DOWN", "LEFT", "RIGHT", "UP"]'
+ attr.enum_value = literal_attr_values[attr_name]
+
+ else:
+ attr.data_type = "string"
+ attr.string_value = literal_attr_values[attr_name]
+
+ box_alignment_mask = [False] * 9
+ box_alignment_mask[6] = True # bottom_left box_alignment
+ literal_props.box_alignment = box_alignment_mask
+ return {"FINISHED"}
+
+
+class RemoveTextLiteral(bpy.types.Operator):
+ bl_idname = "bim.remove_text_literal"
+ bl_label = "Remove text literal"
+ bl_options = {"REGISTER", "UNDO"}
+
+ literal_prop_id: bpy.props.IntProperty()
+
+ def execute(self, context):
+ obj = context.active_object
+ obj.BIMTextProperties.literals.remove(self.literal_prop_id)
+ return {"FINISHED"}
+
+
class EditAssignedProduct(bpy.types.Operator, Operator):
bl_idname = "bim.edit_assigned_product"
bl_label = "Edit Text Product"
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
index d336053656..5f8d107b27 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
@@ -392,7 +392,7 @@ BOX_ALIGNMENT_POSITIONS = [
]
-class BIMTextProperties(PropertyGroup):
+class Literal(PropertyGroup):
def set_box_alignment(self, new_value):
markers = new_value.count(True)
if not markers:
@@ -416,13 +416,28 @@ class BIMTextProperties(PropertyGroup):
def get_box_alignment(self):
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
- is_editing: BoolProperty(name="Is Editing", default=False)
attributes: CollectionProperty(name="Attributes", type=Attribute)
- test_prop: StringProperty(name="test_prop", default="TEXT")
# Current text value with evaluated experessions stored in `value`.
# The original (Literal) value stored in `attributes['Literal']`
# and can be accessed with `get_text()`
value: StringProperty(name="Value", default="TEXT")
+ box_alignment: BoolVectorProperty(
+ name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
+ )
+ ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
+
+ def get_literal_edited_data(self):
+ text_data = {
+ "CurrentValue": self.attributes["Literal"].string_value,
+ "Literal": self.attributes["Literal"].string_value,
+ "BoxAlignment": self.attributes["BoxAlignment"].string_value,
+ }
+ return text_data
+
+
+class BIMTextProperties(PropertyGroup):
+ is_editing: BoolProperty(name="Is Editing", default=False)
+ literals: CollectionProperty(name="Literals", type=Literal)
font_size: EnumProperty(
items=[
("1.8", "1.8 - Small", ""),
@@ -434,19 +449,19 @@ class BIMTextProperties(PropertyGroup):
default="2.5",
name="Font Size",
)
- box_alignment: BoolVectorProperty(
- name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
- )
def get_text_edited_data(self):
"""should be called only if `is_editing`
- etherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
+ otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
because this data could be out of date
"""
+ literals_data = []
+ for literal in self.literals:
+ literal_data = literal.get_literal_edited_data()
+ literals_data.append(literal_data)
text_data = {
- "text": self.attributes["Literal"].string_value,
- "font_size": float(self.font_size),
- "box_alignment": self.attributes["BoxAlignment"].string_value,
+ "Literals": literals_data,
+ "FontSize": float(self.font_size),
}
return text_data
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
index 6366beb7a4..80e4698e71 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
@@ -65,8 +65,10 @@ class SvgWriter:
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
+ self.related_paths = None
def create_blank_svg(self, output_path):
+ self.define_related_paths() # making sure all paths are defined
self.calculate_scale()
self.svg = svgwrite.Drawing(
output_path,
@@ -92,11 +94,32 @@ class SvgWriter:
)
return self
- def define_boilerplate(self, stylesheet=None, markers=None, symbols=None, patterns=None):
- self.add_stylesheet(stylesheet)
- self.add_markers(markers)
- self.add_symbols(symbols)
- self.add_patterns(patterns)
+ def define_related_paths(self, **related_paths):
+ if not self.related_paths:
+ self.related_paths = {}
+
+ if not related_paths:
+ related_paths = {
+ "Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
+ "Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
+ "Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
+ "Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
+ }
+ for path_name in list(related_paths.keys()):
+ if path_name in self.related_paths:
+ del related_paths[path_name]
+
+ for path_name in related_paths:
+ uri = related_paths[path_name]
+ custom_path = tool.Ifc.resolve_uri(uri)
+ if custom_path:
+ self.related_paths[path_name] = custom_path
+
+ def define_boilerplate(self):
+ self.add_stylesheet()
+ self.add_markers()
+ self.add_symbols()
+ self.add_patterns()
return self
def calculate_scale(self):
@@ -108,25 +131,29 @@ class SvgWriter:
self.width = self.raw_width * self.svg_scale
self.height = self.raw_height * self.svg_scale
- def add_stylesheet(self, uri):
- default_stylesheet = os.path.join(self.data_dir, "styles", f"default.css")
- with open(tool.Ifc.resolve_uri(uri) or default_stylesheet, "r") as stylesheet:
+ def add_stylesheet(self):
+ with open(self.related_paths["Stylesheet"], "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
- def add_markers(self, uri):
- tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "markers.svg"))
+ def add_markers(self):
+ tree = ET.parse(self.related_paths["Markers"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
- def add_symbols(self, uri):
- tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "symbols.svg"))
+ def add_symbols(self):
+ tree = ET.parse(self.related_paths["Symbols"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
- def add_patterns(self, uri):
- tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "patterns.svg"))
+ def find_xml_symbol_by_id(self, id):
+ tree = ET.parse(self.related_paths["Symbols"])
+ xml_symbol = tree.find(f'.//*[@id="{id}"]')
+ return External(xml_symbol) if xml_symbol else None
+
+ def add_patterns(self):
+ tree = ET.parse(self.related_paths["Patterns"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -530,7 +557,7 @@ class SvgWriter:
edge_verts_svg.append(symbol_position_svg)
edge_dir = (edge_verts_svg[1] - edge_verts_svg[0]).normalized()
- angle = degrees( edge_dir.xy.angle_signed( Vector([1,0]) ) )
+ angle = degrees(edge_dir.xy.angle_signed(Vector([1, 0])))
for v_i, symbol_position_svg in zip(edge.vertices, edge_verts_svg):
current_marker_position = "start" if v_i == 0 else "end"
@@ -562,10 +589,7 @@ class SvgWriter:
)
self.svg.add(
self.svg.text(
- sheet_id,
- insert=(text_position[0], text_position[1] + 2.5),
- class_="SECTION",
- **text_style
+ sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="SECTION", **text_style
)
)
@@ -592,8 +616,14 @@ class SvgWriter:
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
- self.svg.add(self.svg.text(reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style))
- self.svg.add(self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style))
+ self.svg.add(
+ self.svg.text(
+ reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
+ )
+ )
+ self.svg.add(
+ self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
+ )
def get_reference_and_sheet_id_from_annotation(self, element):
reference_id = "-"
@@ -616,10 +646,12 @@ class SvgWriter:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
element = tool.Ifc.get_entity(text_obj)
- text_literal = tool.Drawing.get_text_literal(text_obj)
+ text_literals = tool.Drawing.get_text_literal(text_obj, return_list=True)
+ product = tool.Drawing.get_assigned_product(element)
text_position = self.project_point_onto_camera(position)
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
+ text_position_svg = text_position * self.svg_scale
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
projected_x_axis = self.project_point_onto_camera(position + local_x_axis)
@@ -629,62 +661,79 @@ class SvgWriter:
)
)
- transform = "rotate({}, {}, {})".format(
- angle,
- (text_position * self.svg_scale)[0],
- (text_position * self.svg_scale)[1],
- )
+ transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
+ classes_str = " ".join(self.get_attribute_classes(text_obj))
symbol = tool.Drawing.get_annotation_symbol(element)
if symbol:
- self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.svg_scale)))
+ symbol_svg = self.find_xml_symbol_by_id(symbol)
+ if symbol_svg:
+ symbol_xml = symbol_svg.get_xml()
+ template_text_fields = symbol_xml.findall('.//text[@data-type="text-template"]')
+ # if there is a simple with template text fields
+ # then we just populate it's fields with the data from text literals
+ if template_text_fields:
+ symbol_xml.attrib["transform"] = f"translate({', '.join(map(str, text_position_svg))})"
+ symbol_xml.attrib.pop("id")
+ for i, field in enumerate(template_text_fields):
+ field.text = tool.Drawing.replace_text_literal_variables(text_literals[i].Literal, product)
+ field.attrib["class"] = classes_str
+ self.svg.add(symbol_svg)
+ return None
+ else:
+ self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
- box_alignment = text_literal.BoxAlignment
+ def get_box_alignment_parameters(box_alignment):
+ # reference for alignment values:
+ # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
+ # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
+ vertical_alignment = {
+ "top": "hanging",
+ "bottom": "baseline",
+ "center": "middle",
+ "middle": "middle",
+ }
+ alignment_baseline = vertical_alignment[
+ next(align for align in vertical_alignment if align in box_alignment)
+ ]
+ horizontal_alignment = {
+ "left": "start",
+ "right": "end",
+ "center": "middle",
+ "middle": "middle",
+ }
+ text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
- # reference for alignment values:
- # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
- # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
- vertical_alignment = {
- "top": "hanging",
- "bottom": "baseline",
- "center": "middle",
- "middle": "middle",
- }
- alignment_baseline = vertical_alignment[ next(align for align in vertical_alignment if align in box_alignment) ]
- horizontal_alignment = {
- "left": "start",
- "right": "end",
- "center": "middle",
- "middle": "middle",
- }
- text_anchor = horizontal_alignment[ next(align for align in horizontal_alignment if align in box_alignment) ]
-
-
- # after pretty indentation some redundant spaces can occur in svg tags
- # this is why we apply "font-size: 0;" to the text tag to remove those spaces
- # and add clases to the tspan tags
- # ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
- product = tool.Drawing.get_assigned_product(element)
- text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
- text_tag = self.svg.text(
- "",
- **{
- "text-anchor": text_anchor,
- # using dominant-baseline because we plan to use subtags
- # otherwise alignment-baseline would be sufficient
+ # using dominant-baseline because we plan to use subtags
+ # otherwise alignment-baseline would be sufficient
+ return {
"dominant-baseline": alignment_baseline,
- "transform": transform,
- "style": "font-size: 0;",
- },
- )
- self.svg.add(text_tag)
+ "text-anchor": text_anchor,
+ }
- classes = " ".join(self.get_attribute_classes(text_obj))
- for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
- t_span = self.svg.tspan(text_line, insert=(text_position * self.svg_scale), class_=classes)
- # doing it here and not in tspan constructor because it adds unnecessary spaces
- t_span.update({"dy": f"{line_number}em"})
- text_tag.add(t_span)
+ for text_literal in text_literals:
+ # after pretty indentation some redundant spaces can occur in svg tags
+ # this is why we apply "font-size: 0;" to the text tag to remove those spaces
+ # and add clases to the tspan tags
+ # ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
+
+ text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
+ text_tag = self.svg.text(
+ "",
+ **{
+ "transform": transform,
+ "style": "font-size: 0;",
+ "insert": text_position_svg,
+ },
+ **get_box_alignment_parameters(text_literal.BoxAlignment),
+ )
+ self.svg.add(text_tag)
+
+ for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
+ t_span = self.svg.tspan(text_line, class_=classes_str)
+ # doing it here and not in tspan constructor because constructor adds unnecessary spaces
+ t_span.update({"dy": f"{line_number}em"})
+ text_tag.add(t_span)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -762,7 +811,7 @@ class SvgWriter:
points = obj.data.splines[0].points
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
- points_chunked = [points[i:i+3] for i in range(len(points)-2)]
+ points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
for points_chunk in points_chunked:
points_2d = [view3d_utils.location_3d_to_region_2d(region, region_3d, p.co.xyz) for p in points_chunk]
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py
index 99e06f19dc..8f8a6e34c3 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py
@@ -20,7 +20,13 @@ import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
-from blenderbim.bim.module.drawing.data import ProductAssignmentsData, TextData, SheetsData, SchedulesData, DrawingsData
+from blenderbim.bim.module.drawing.data import (
+ ProductAssignmentsData,
+ SheetsData,
+ SchedulesData,
+ DrawingsData,
+ DecoratorData,
+)
class BIM_PT_camera(Panel):
@@ -340,10 +346,8 @@ class BIM_PT_text(Panel):
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
def draw(self, context):
- if not TextData.is_loaded:
- TextData.load()
-
- props = context.active_object.BIMTextProperties
+ obj = context.active_object
+ props = obj.BIMTextProperties
if props.is_editing:
# shares most of the code with EditTextPopup.draw()
@@ -351,38 +355,53 @@ class BIM_PT_text(Panel):
row = self.layout.row(align=True)
row.operator("bim.edit_text", icon="CHECKMARK")
+ row.operator("bim.add_text_literal", icon="ADD", text="")
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
- # skip BoxAlignment since we're going to format it ourselves
- attributes = [a for a in props.attributes if a.name != "BoxAlignment"]
- blenderbim.bim.helper.draw_attributes(attributes, self.layout)
+
row = self.layout.row(align=True)
row.prop(props, "font_size")
- # a bit hacky way to align box alignment widget
- rows = [self.layout.row(align=True) for i in range(3)]
- for i in range(9):
- if i % 3 == 0:
- split = rows[i // 3].split(factor=0.1, align=True)
- split.column()
- split.prop(props, "box_alignment", text="", index=i)
+ for i, literal_props in enumerate(props.literals):
+ box = self.layout.box()
+ row = self.layout.row(align=True)
- text_lines = ["Text box alignment:", props.attributes["BoxAlignment"].string_value, ""]
- for i in range(3):
- split = rows[i].split(factor=0.1, align=False)
- split.column()
- split.label(text=text_lines[i])
+ row = box.row(align=True)
+ row.label(text=f"Literal[{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"]
+ blenderbim.bim.helper.draw_attributes(attributes, box)
+
+ # a bit hacky way to align box alignment widget
+ rows = [box.row(align=True) for i in range(3)]
+ for i in range(9):
+ if i % 3 == 0:
+ split = rows[i // 3].split(factor=0.1, align=True)
+ split.column()
+ split.prop(literal_props, "box_alignment", text="", index=i)
+
+ text_lines = ["Text box alignment:", literal_props.attributes["BoxAlignment"].string_value, ""]
+ for i in range(3):
+ split = rows[i].split(factor=0.1, align=False)
+ split.column()
+ split.label(text=text_lines[i])
else:
+ text_data = DecoratorData.get_ifc_text_data(obj)
+
row = self.layout.row()
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
- for attribute in TextData.data["attributes"]:
- row = self.layout.row(align=True)
- row.label(text=attribute["name"])
- row.label(text=attribute["value"])
-
row = self.layout.row(align=True)
- row.label(text="Current value:")
- row.label(text=props.value)
+ row.label(text="FontSize")
+ row.label(text=str(text_data["FontSize"]))
+
+ for literal_data in text_data["Literals"]:
+ box = self.layout.box()
+ for attribute in literal_data:
+ row = box.row(align=True)
+ row.label(text=attribute)
+ row.label(text=literal_data[attribute])
class BIM_PT_annotation_utilities(Panel):
diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py
index 14286d5426..9eb3ab72c0 100644
--- a/src/blenderbim/blenderbim/bim/module/model/workspace.py
+++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py
@@ -25,7 +25,7 @@ import blenderbim.bim.module.type.prop as type_prop
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
-from blenderbim.bim.module.drawing.data import TextData
+from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.module.model.prop import get_ifc_class
@@ -334,7 +334,8 @@ class BimToolUI:
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
- elif (TextData.is_loaded or not TextData.load()) and TextData.data["attributes"]:
+
+ elif DecoratorData.get_ifc_text_data(bpy.context.object):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
@@ -574,7 +575,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_roof_path()
- elif (TextData.is_loaded or not TextData.load()) and TextData.data["attributes"]:
+ elif DecoratorData.get_ifc_text_data(bpy.context.object):
bpy.context.object.select_set(True)
bpy.ops.bim.edit_text_popup()
diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py
index 1932132124..cf45f59e8b 100644
--- a/src/blenderbim/blenderbim/core/drawing.py
+++ b/src/blenderbim/blenderbim/core/drawing.py
@@ -26,12 +26,8 @@ def disable_editing_text(drawing, obj=None):
drawing.disable_editing_text(obj)
-def edit_text(ifc, drawing, obj=None):
- ifc.run(
- "drawing.edit_text_literal",
- text_literal=drawing.get_text_literal(obj),
- attributes=drawing.export_text_literal_attributes(obj),
- )
+def edit_text(drawing, obj=None):
+ drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_value(obj)
drawing.update_text_size_pset(obj)
drawing.disable_editing_text(obj)
diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py
index e4b4444f58..5887e98e62 100644
--- a/src/blenderbim/blenderbim/core/tool.py
+++ b/src/blenderbim/blenderbim/core/tool.py
@@ -257,6 +257,9 @@ class Drawing:
def get_name(cls, element): pass
def get_schedule_location(cls, schedule): pass
def get_text_literal(cls, obj): pass
+ def remove_literal_from_annotation(cls, obj, literal): pass
+ def synchronise_ifc_and_text_attributes(cls, obj): pass
+ def add_literal_to_annotation(cls, obj, Literal='Literal', Path='RIGHT', BoxAlignment='bottom-left'): pass
def import_assigned_product(cls, obj): pass
def import_drawings(cls): pass
def import_schedules(cls): pass
diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py
index 1491cc5c17..95717916c9 100644
--- a/src/blenderbim/blenderbim/tool/drawing.py
+++ b/src/blenderbim/blenderbim/tool/drawing.py
@@ -187,7 +187,11 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def export_text_literal_attributes(cls, obj):
- return blenderbim.bim.helper.export_attributes(obj.BIMTextProperties.attributes)
+ literals = []
+ for literal_props in obj.BIMTextProperties.literals:
+ literal_data = blenderbim.bim.helper.export_attributes(literal_props.attributes)
+ literals.append(literal_data)
+ return literals
@classmethod
def get_annotation_context(cls, target_view):
@@ -300,7 +304,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return "A" + str(number).zfill(2)
@classmethod
- def get_text_literal(cls, obj):
+ def get_text_literal(cls, obj, return_list=False):
element = tool.Ifc.get_entity(obj)
if not element:
return
@@ -310,8 +314,80 @@ class Drawing(blenderbim.core.tool.Drawing):
if not rep:
return
items = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
- if items:
- return items[0]
+
+ if not items:
+ return [] if return_list else None
+ if return_list:
+ return items
+ return items[0]
+
+ @classmethod
+ def remove_literal_from_annotation(cls, obj, literal):
+ element = tool.Ifc.get_entity(obj)
+ if not element:
+ return
+
+ rep = ifcopenshell.util.representation.get_representation(
+ element, "Plan", "Annotation"
+ ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
+ if not rep:
+ return
+
+ ifc_file = tool.Ifc.get()
+ rep.Items = [l for l in rep.Items if l != literal]
+ ifcopenshell.util.element.remove_deep2(ifc_file, literal)
+
+ @classmethod
+ def synchronise_ifc_and_text_attributes(cls, obj):
+ literals = cls.get_text_literal(obj, return_list=True)
+ literals_attributes = cls.export_text_literal_attributes(obj)
+ defined_ifc_ids = [l.ifc_definition_id for l in obj.BIMTextProperties.literals]
+ ifc_file = tool.Ifc.get()
+
+ for ifc_definition_id, attributes in zip(defined_ifc_ids, literals_attributes):
+ # making sure all literals from text edit exist in ifc
+ if ifc_definition_id == 0:
+ literal = cls.add_literal_to_annotation(obj, **attributes)
+ else:
+ literal = ifc_file.by_id(ifc_definition_id)
+ tool.Ifc.run(
+ "drawing.edit_text_literal",
+ text_literal=literal,
+ attributes=attributes,
+ )
+
+ # remove from ifc the literals that were removed during the edit
+ for literal in literals:
+ if literal.id() not in defined_ifc_ids:
+ cls.remove_literal_from_annotation(obj, literal)
+
+ @classmethod
+ def add_literal_to_annotation(cls, obj, Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left"):
+ element = tool.Ifc.get_entity(obj)
+ if not element:
+ return
+
+ rep = ifcopenshell.util.representation.get_representation(
+ element, "Plan", "Annotation"
+ ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
+
+ if not rep:
+ return
+
+ ifc_file = tool.Ifc.get()
+
+ origin = ifc_file.createIfcAxis2Placement3D(
+ ifc_file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
+ ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
+ ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
+ )
+
+ ifc_literal = ifc_file.createIfcTextLiteralWithExtent(
+ Literal, origin, Path, ifc_file.createIfcPlanarExtent(1000, 1000), BoxAlignment
+ )
+
+ rep.Items = rep.Items + (ifc_literal,)
+ return ifc_literal
@classmethod
def get_assigned_product(cls, element):
@@ -461,17 +537,20 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def import_text_attributes(cls, obj):
props = obj.BIMTextProperties
- props.attributes.clear()
- text = cls.get_text_literal(obj)
- blenderbim.bim.helper.import_attributes2(text, props.attributes)
+ props.literals.clear()
- box_alignment_mask = [False] * 9
- position_string = props.attributes["BoxAlignment"].string_value
- box_alignment_mask[BOX_ALIGNMENT_POSITIONS.index(position_string)] = True
- props.box_alignment = box_alignment_mask
+ for ifc_literal in cls.get_text_literal(obj, return_list=True):
+ literal_props = props.literals.add()
+ blenderbim.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)
+
+ box_alignment_mask = [False] * 9
+ position_string = literal_props.attributes["BoxAlignment"].string_value
+ box_alignment_mask[BOX_ALIGNMENT_POSITIONS.index(position_string)] = True
+ literal_props.box_alignment = box_alignment_mask
+ literal_props.ifc_definition_id = ifc_literal.id()
text_data = DecoratorData.get_ifc_text_data(obj)
- props.font_size = str(text_data["font_size"])
+ props.font_size = str(text_data["FontSize"])
@classmethod
def import_assigned_product(cls, obj):
@@ -537,11 +616,16 @@ class Drawing(blenderbim.core.tool.Drawing):
def update_text_value(cls, obj):
props = obj.BIMTextProperties
- ifc_literal = cls.get_text_literal(obj)
- if not ifc_literal:
+ literals = cls.get_text_literal(obj, return_list=True)
+ if not literals:
return
- product = cls.get_assigned_product(tool.Ifc.get_entity(obj))
- props.value = cls.replace_text_literal_variables(ifc_literal.Literal, product)
+
+ if not props.literals:
+ cls.import_text_attributes(obj)
+
+ for i, literal in enumerate(literals):
+ product = cls.get_assigned_product(tool.Ifc.get_entity(obj))
+ props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
@classmethod
def update_text_size_pset(cls, obj):
@@ -960,7 +1044,7 @@ class Drawing(blenderbim.core.tool.Drawing):
for variable in re.findall("{{.*?}}", text):
text = text.replace(
variable, str(ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) or "")
- )
+ )
return text
@classmethod
diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py
index 0a5adcba8c..8b9744e7ec 100644
--- a/src/blenderbim/blenderbim/tool/geometry.py
+++ b/src/blenderbim/blenderbim/tool/geometry.py
@@ -153,6 +153,7 @@ class Geometry(blenderbim.core.tool.Geometry):
def get_styles(cls, obj):
return [tool.Style.get_style(s.material) for s in obj.material_slots if s.material]
+ # TODO: multiple Literals?
@classmethod
def get_text_literal(cls, representation):
texts = [i for i in representation.Items if i.is_a("IfcTextLiteral")]
diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py
index 565ea30e2e..f66b13c596 100644
--- a/src/blenderbim/test/core/test_drawing.py
+++ b/src/blenderbim/test/core/test_drawing.py
@@ -35,13 +35,11 @@ class TestDisableEditingText:
class TestEditText:
def test_run(self, ifc, drawing):
- drawing.get_text_literal("obj").should_be_called().will_return("text")
- drawing.export_text_literal_attributes("obj").should_be_called().will_return("attributes")
- ifc.run("drawing.edit_text_literal", text_literal="text", attributes="attributes").should_be_called()
+ drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_value("obj").should_be_called()
drawing.update_text_size_pset("obj").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
- subject.edit_text(ifc, drawing, obj="obj")
+ subject.edit_text(drawing, obj="obj")
class TestEnableEditingAssignedProduct:
diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py
index 6c538a2565..f91d6cea25 100644
--- a/src/blenderbim/test/tool/test_drawing.py
+++ b/src/blenderbim/test/tool/test_drawing.py
@@ -204,11 +204,11 @@ class TestEnsureUniqueIdentification(NewFile):
class TestExportTextLiteralAttributes(NewFile):
def test_run(self):
TestImportTextAttributes().test_run()
- assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == {
+ assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [{
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "bottom-left",
- }
+ }]
class TestGetAnnotationContext(NewFile):
@@ -433,6 +433,7 @@ class TestGetTextLiteral(NewFile):
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
+ element.ObjectType = "TEXT" # TODO: double check if it's valid to set this
tool.Ifc.link(element, obj)
assert subject.get_text_literal(obj) == item
@@ -520,12 +521,13 @@ class TestImportTextAttributes(NewFile):
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
+ element.ObjectType = "TEXT" # TODO: double check if it's valid to set this
tool.Ifc.link(element, obj)
subject.import_text_attributes(obj)
- props = obj.BIMTextProperties
- assert props.attributes.get("Literal").string_value == "Literal"
- assert props.attributes.get("Path").enum_value == "RIGHT"
- assert props.attributes.get("BoxAlignment").string_value == "bottom-left"
+ literal_props = obj.BIMTextProperties.literals[0]
+ assert literal_props.attributes.get("Literal").string_value == "Literal"
+ assert literal_props.attributes.get("Path").enum_value == "RIGHT"
+ assert literal_props.attributes.get("BoxAlignment").string_value == "bottom-left"
class TestImportAssignedProduct(NewFile):
@@ -596,7 +598,7 @@ class TestUpdateTextValue(NewFile):
TestGetTextLiteral().test_run()
obj = bpy.data.objects.get("Object")
subject.update_text_value(obj)
- assert obj.BIMTextProperties.value == "Literal"
+ assert obj.BIMTextProperties.literals[0].value == "Literal"
def test_using_attribute_variables(self):
TestGetTextLiteral().test_run()
@@ -610,7 +612,7 @@ class TestUpdateTextValue(NewFile):
ifc.by_type("IfcTextLiteralWithExtent")[0].Literal = "Foo {{Name}} Bar"
subject.update_text_value(obj)
- assert obj.BIMTextProperties.value == "Foo Baz Bar"
+ assert obj.BIMTextProperties.literals[0].value == "Foo Baz Bar"
def test_using_property_variables(self):
TestGetTextLiteral().test_run()
@@ -626,4 +628,4 @@ class TestUpdateTextValue(NewFile):
ifc.by_type("IfcTextLiteralWithExtent")[0].Literal = "Foo {{Custom_Pset.Key}} Bar"
subject.update_text_value(obj)
- assert obj.BIMTextProperties.value == "Foo Baz Bar"
+ assert obj.BIMTextProperties.literals[0].value == "Foo Baz Bar"