Multiple literals in 1 Ifc Text + aggregated text annotations

Added option to store multiple literals inside 1 Ifc text object.

If multiple literals share the same `BoxAlignment` then only the first one will be displayed in viewport
to prevent visual clutter (although added small asterisk symbol to indicate that there are still multiple literals).

Added option for aggregated text annotations. Example - "door-tag" symbol.
If you create text, specify "door-tag" symbol in EPset_Annotation.Symbol
\and add two literals to it then they'll automatically will fit to that door-tag in the svg drawing
(https://i.imgur.com/OXaIlWe.png).

The way it works - you define symbol in symbols.svg (or in your own symbols svg file)
and then add the `<text>` fields where text from literals will fit right in.
The imporant note: `<text>` tag must have attribute `data-type="text-template"` for it to work.
For example: `<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>`
This commit is contained in:
Andrej730
2023-03-20 15:38:09 +05:00
parent 2a0fdd214c
commit f7181aae8a
15 changed files with 480 additions and 259 deletions
@@ -5,7 +5,9 @@
</g>
<g id="door-tag">
<circle cy="1.75" r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke: black; stroke-width: 0.25;" />
<text y="5" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
</g>
<g id="elevation-arrow">
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -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,
@@ -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
@@ -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<CIRCLE_SEGS_ASTERISK; i++) {
float angle = angle_d * i;
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
}
}
void main() {
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
vec4 edge = p1w - p0w, dir = normalize(edge);
vec4 p0 = gl_in[0].gl_Position;
vec4 p0w = CLIP2WIN(p0);
vec4 p;
vec4 head[CIRCLE_SEGS_ASTERISK];
circle_head_asterisk(viewportDrawingScale * CIRCLE_SIZE, head);
vec4 head[3];
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
// start edge arrow
gl_Position = p0;
EmitVertex();
p = p0w + head[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
gl_Position = p0;
EmitVertex();
EndPrimitive();
// end edge arrow
gl_Position = p1;
EmitVertex();
p = p1w - head[1];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - head[2];
gl_Position = WIN2CLIP(p);
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
// stem, with gaps for arrows
p = p0w + head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w - head[0];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
for (int i=0; i < CIRCLE_SEGS_ASTERISK/2; i++) {
p = p0w + head[i];
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p0w + head[i+CIRCLE_SEGS_ASTERISK/2];
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
}
}
"""
@@ -2102,20 +2082,36 @@ class TextDecorator(BaseDecorator):
pos = location_3d_to_region_2d(region, region3d, obj.matrix_world.translation)
props = obj.BIMTextProperties
text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj)
box_alignment = text_data["box_alignment"]
for line_i, line in enumerate(text_data["text"].split("\\n")):
self.draw_label(
context,
line,
pos,
dir,
gap=0,
center=False,
vcenter=False,
font_size_mm=text_data["font_size"],
line_no=line_i,
box_alignment=box_alignment,
)
literals_data = text_data["Literals"]
# draw asterisk symbol to visually indicate that there are multiple literals
if len(literals_data) > 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:
@@ -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("</svg>", ""))
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"
@@ -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
@@ -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 <tspan> subtags
# otherwise alignment-baseline would be sufficient
# using dominant-baseline because we plan to use <tspan> 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]
@@ -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):
@@ -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()
+2 -6
View File
@@ -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)
+3
View File
@@ -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
+101 -17
View File
@@ -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
@@ -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")]
+2 -4
View File
@@ -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:
+11 -9
View File
@@ -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"