mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user