Rebuild text annotations to have leaders in the same object and use text literals. See #1153.

This commit is contained in:
Dion Moult
2021-11-16 08:54:16 +11:00
parent fcfdc179df
commit 43869c33f6
23 changed files with 694 additions and 163 deletions
@@ -35,7 +35,6 @@ classes = (
operator.ResizeText,
operator.AddVariable,
operator.RemoveVariable,
operator.PropagateTextData,
operator.RemoveDrawing,
operator.AddDrawingStyle,
operator.RemoveDrawingStyle,
@@ -54,6 +53,9 @@ classes = (
operator.CleanWireframes,
operator.CopyGrid,
operator.AddSectionsAnnotations,
operator.EditText,
operator.DisableEditingText,
operator.EnableEditingText,
prop.Variable,
prop.Drawing,
prop.Schedule,
@@ -81,6 +83,7 @@ classes = (
def register():
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
@@ -0,0 +1,47 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.representation
import blenderbim.tool as tool
def refresh():
TextData.is_loaded = False
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 []
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
return [
{"name": "Literal", "value": text_literal.Literal},
{"name": "BoxAlignment", "value": text_literal.BoxAlignment},
]
@@ -1228,6 +1228,90 @@ class SectionViewDecorator(LevelDecorator):
self.draw_lines(context, obj, verts, [(0, 1)])
class TextDecorator(BaseDecorator):
"""Decorator for text objects
- draws the text next to the origin
"""
objecttype = "TEXT"
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
#define ARROW_ANGLE PI / 12.0
#define ARROW_SIZE 16.0
"""
)
GEOM_GLSL = """
uniform vec2 winsize;
uniform float viewportDrawingScale;
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
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 p;
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();
}
"""
def decorate(self, context, obj):
self.draw_labels(context, obj)
def draw_labels(self, context, obj):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, obj.matrix_world.translation)
self.draw_label(context, obj.BIMTextProperties.value, pos, dir, gap=0, center=False, vcenter=False)
class DecorationsHandler:
decorators_classes = [
DimensionDecorator,
@@ -1241,6 +1325,7 @@ class DecorationsHandler:
StairDecorator,
BreakDecorator,
SectionViewDecorator,
TextDecorator,
]
installed = None
@@ -32,6 +32,7 @@ import ifcopenshell.util.selector
import ifcopenshell.util.representation
import blenderbim.bim.schema
import blenderbim.tool as tool
import blenderbim.core.drawing as core
import blenderbim.bim.module.drawing.svgwriter as svgwriter
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.sheeter as sheeter
@@ -57,6 +58,13 @@ def open_with_user_command(user_command, path):
webbrowser.open("file://" + path)
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class AddDrawing(bpy.types.Operator):
bl_idname = "bim.add_drawing"
bl_label = "Add Drawing"
@@ -79,7 +87,7 @@ class AddDrawing(bpy.types.Operator):
view_collection = bpy.data.collections.new("IfcGroup/" + new.name)
views_collection.children.link(view_collection)
camera = bpy.data.objects.new(new.name, bpy.data.cameras.new(new.name))
camera.location = (0, 0, 1.7) # The view shall be 1.7m above the origin
camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
camera.data.type = "ORTHO"
camera.data.ortho_scale = 50 # The default of 6m is too small
camera.data.clip_end = 10 # A slightly more reasonable default
@@ -573,28 +581,45 @@ class AddAnnotation(bpy.types.Operator):
)
if not subcontext:
return {"FINISHED"}
if self.data_type == "text":
if context.selected_objects:
for selected_object in context.selected_objects:
obj = annotation.Annotator.add_text(context, related_element=selected_object)
else:
obj = annotation.Annotator.add_text(context)
else:
obj = annotation.Annotator.get_annotation_obj(self.obj_name, self.data_type, context)
if self.obj_name == "Break":
obj = annotation.Annotator.add_plane_to_annotation(obj, context)
else:
obj = annotation.Annotator.add_line_to_annotation(obj, context)
# TODO: reimplement bulk smart tagging
# if self.data_type == "text":
# if context.selected_objects:
# for selected_object in context.selected_objects:
# obj = annotation.Annotator.add_text(context, related_element=selected_object)
# else:
# obj = annotation.Annotator.add_text(context)
# else:
obj = annotation.Annotator.get_annotation_obj(self.object_type, self.data_type, context)
if self.object_type == "BREAKLINE":
obj = annotation.Annotator.add_plane_to_annotation(obj, context)
elif self.object_type != "TEXT":
obj = annotation.Annotator.add_line_to_annotation(obj, context)
if not obj.BIMObjectProperties.ifc_definition_id:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcAnnotation", context_id=subcontext.id())
ifc_representation_class = ""
if self.object_type == "TEXT":
ifc_representation_class = "IfcTextLiteral"
elif self.object_type == "TEXT_LEADER":
ifc_representation_class = "IfcGeometricCurveSet/IfcTextLiteral"
bpy.ops.bim.assign_class(
obj=obj.name,
ifc_class="IfcAnnotation",
context_id=subcontext.id(),
ifc_representation_class=ifc_representation_class,
)
element = tool.Ifc.get_entity(obj)
element.ObjectType = self.object_type
camera = tool.Ifc.get_entity(context.scene.camera)
group = [r for r in camera.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup
bpy.ops.bim.assign_group(product=obj.name, group=group.id())
else:
bpy.ops.bim.update_representation(obj=obj.name)
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.mode_set(mode="EDIT")
if obj.data:
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
@@ -888,29 +913,6 @@ class RemoveVariable(bpy.types.Operator):
return {"FINISHED"}
class PropagateTextData(bpy.types.Operator):
bl_idname = "bim.propagate_text_data"
bl_label = "Propagate Text Data"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
source = context.active_object
for obj in context.selected_objects:
if obj == source:
continue
obj.data.body = source.data.body
obj.data.align_x = source.data.align_x
obj.data.align_y = source.data.align_y
obj.data.BIMTextProperties.font_size = source.data.BIMTextProperties.font_size
obj.data.BIMTextProperties.symbol = source.data.BIMTextProperties.symbol
obj.data.BIMTextProperties.variables.clear()
for variable in source.data.BIMTextProperties.variables:
new_variable = obj.data.BIMTextProperties.variables.add()
new_variable.name = variable.name
new_variable.prop_key = variable.prop_key
return {"FINISHED"}
class RemoveDrawing(bpy.types.Operator):
bl_idname = "bim.remove_drawing"
bl_label = "Remove Drawing"
@@ -1380,3 +1382,30 @@ class AddSectionsAnnotations(bpy.types.Operator):
view_coll.objects.link(obj)
return {"FINISHED"}
class EditText(bpy.types.Operator, Operator):
bl_idname = "bim.edit_text"
bl_label = "Edit Text"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_text(tool.Ifc, tool.Drawing, obj=context.active_object)
class EnableEditingText(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_text"
bl_label = "Enable Editing Text"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.enable_editing_text(tool.Drawing, obj=context.active_object)
class DisableEditingText(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_text"
bl_label = "Disable Editing Text"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.disable_editing_text(tool.Drawing, obj=context.active_object)
@@ -169,6 +169,10 @@ def refreshTitleblocks(self, context):
def toggleDecorations(self, context):
toggle = self.should_draw_decorations
if toggle:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.users_collection[0]
for obj in collection.objects:
tool.Drawing.update_text_value(obj)
decoration.DecorationsHandler.install(context)
else:
decoration.DecorationsHandler.uninstall()
@@ -353,6 +357,9 @@ class BIMCameraProperties(PropertyGroup):
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
attributes: CollectionProperty(name="Attributes", type=Attribute)
value: StringProperty(name="Value", default="TEXT")
font_size: EnumProperty(
items=[
("1.8", "1.8 - Small", ""),
@@ -361,6 +368,7 @@ class BIMTextProperties(PropertyGroup):
("5.0", "5.0 - Header", ""),
("7.0", "7.0 - Title", ""),
],
default="2.5",
update=refreshFontSize,
name="Font Size",
)
@@ -24,6 +24,7 @@ import pystache
import xml.etree.ElementTree as ET
import svgwrite
import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.module.drawing.annotation as annotation
@@ -208,8 +209,7 @@ class SvgWriter:
for obj_data in self.annotations.get("solid_objs", []):
self.draw_line_annotation(obj_data, ["solid"])
if self.annotations.get("leader_obj"):
self.draw_line_annotation(self.annotations["leader_obj"], ["leader"])
self.draw_leader_annotations()
if self.annotations.get("plan_level_obj"):
matrix_world = self.annotations["plan_level_obj"].matrix_world
@@ -456,72 +456,99 @@ class SvgWriter:
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
)
def draw_leader_annotations(self):
for obj in self.annotations.get("leader_objs", []):
self.draw_line_annotation((obj, obj.data), ["leader"])
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if spline_points:
position = obj.matrix_world @ spline_points[0].co.xyz
else:
position = Vector((0, 0, 0))
self.draw_text_annotation(obj, position)
def draw_text_annotations(self):
for text_obj in self.annotations.get("text_objs", []):
self.draw_text_annotation(text_obj, text_obj.location)
def draw_text_annotation(self, text_obj, position):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
element = tool.Ifc.get_entity(text_obj)
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
for text_obj in self.annotations.get("text_objs", []):
text_position = self.project_point_onto_camera(text_obj.location)
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
text_position = self.project_point_onto_camera(position)
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
projected_x_axis = self.project_point_onto_camera(text_obj.location + local_x_axis)
angle = math.degrees(
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
Vector((1, 0))
)
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)
angle = math.degrees(
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
Vector((1, 0))
)
)
transform = "rotate({}, {}, {})".format(
angle,
(text_position * self.scale)[0],
(text_position * self.scale)[1],
)
if text_obj.BIMTextProperties.symbol != "None":
self.svg.add(
self.svg.use("#{}".format(text_obj.BIMTextProperties.symbol), insert=tuple(text_position * self.scale))
)
transform = "rotate({}, {}, {})".format(
angle,
(text_position * self.scale)[0],
(text_position * self.scale)[1],
if text_literal.BoxAlignment == "top-left":
alignment_baseline = "hanging"
text_anchor = "start"
elif text_literal.BoxAlignment == "top-middle":
alignment_baseline = "hanging"
text_anchor = "middle"
elif text_literal.BoxAlignment == "top-right":
alignment_baseline = "hanging"
text_anchor = "end"
elif text_literal.BoxAlignment == "middle-left":
alignment_baseline = "middle"
text_anchor = "start"
elif text_literal.BoxAlignment == "center":
alignment_baseline = "middle"
text_anchor = "middle"
elif text_literal.BoxAlignment == "middle-right":
alignment_baseline = "middle"
text_anchor = "end"
elif text_literal.BoxAlignment == "bottom-left":
alignment_baseline = "baseline"
text_anchor = "start"
elif text_literal.BoxAlignment == "bottom-middle":
alignment_baseline = "baseline"
text_anchor = "middle"
elif text_literal.BoxAlignment == "bottom-right":
alignment_baseline = "baseline"
text_anchor = "end"
text_body = text_literal.Literal
if text_obj.name in self.annotations.get("template_variables", {}):
text_body = pystache.render(text_body, self.annotations["template_variables"][text_obj.name])
for line_number, text_line in enumerate(text_body.split("\n")):
self.svg.add(
self.svg.text(
text_line,
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
class_=" ".join(self.get_attribute_classes(text_obj)),
**{
"font-size": annotation.Annotator.get_svg_text_size(text_obj.BIMTextProperties.font_size),
"font-family": "OpenGost Type B TT",
"text-anchor": text_anchor,
"alignment-baseline": alignment_baseline,
"dominant-baseline": alignment_baseline,
"transform": transform,
},
)
)
if text_obj.data.BIMTextProperties.symbol != "None":
self.svg.add(
self.svg.use(
"#{}".format(text_obj.data.BIMTextProperties.symbol), insert=tuple(text_position * self.scale)
)
)
if text_obj.data.align_x == "CENTER":
text_anchor = "middle"
elif text_obj.data.align_x == "RIGHT":
text_anchor = "end"
else:
text_anchor = "start"
if text_obj.data.align_y == "CENTER":
alignment_baseline = "middle"
elif text_obj.data.align_y == "TOP":
alignment_baseline = "hanging"
else:
alignment_baseline = "baseline"
text_body = text_obj.data.body
if text_obj.name in self.annotations.get("template_variables", {}):
text_body = pystache.render(text_body, self.annotations["template_variables"][text_obj.name])
for line_number, text_line in enumerate(text_body.split("\n")):
self.svg.add(
self.svg.text(
text_line,
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
class_=" ".join(self.get_attribute_classes(text_obj)),
**{
"font-size": annotation.Annotator.get_svg_text_size(
text_obj.data.BIMTextProperties.font_size
),
"font-family": "OpenGost Type B TT",
"text-anchor": text_anchor,
"alignment-baseline": alignment_baseline,
"dominant-baseline": alignment_baseline,
"transform": transform,
},
)
)
def draw_break_annotations(self, break_obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -17,7 +17,10 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
from blenderbim.bim.module.drawing.data import TextData
class BIM_PT_camera(Panel):
@@ -233,39 +236,57 @@ class BIM_PT_sheets(Panel):
class BIM_PT_text(Panel):
bl_label = "Text Paper Space"
bl_label = "IFC Text"
bl_idname = "BIM_PT_text"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
bl_context = "object"
@classmethod
def poll(cls, context):
return type(context.curve) is bpy.types.TextCurve
if not tool.Ifc.get() or not context.active_object:
return
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.active_object.data.BIMTextProperties
if not TextData.is_loaded:
TextData.load()
row = layout.row()
row.operator("bim.propagate_text_data")
self.layout.use_property_split = True
props = context.active_object.BIMTextProperties
row = layout.row()
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.edit_text", icon="CHECKMARK")
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
blenderbim.bim.helper.draw_attributes(props.attributes, self.layout)
else:
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()
row.prop(props, "font_size")
row = layout.row()
row = self.layout.row()
row.prop(props, "symbol")
row = layout.row()
row = self.layout.row()
row.prop(props, "related_element")
row = layout.row()
row = self.layout.row()
row.operator("bim.add_variable")
for index, variable in enumerate(props.variables):
row = layout.row(align=True)
row = self.layout.row(align=True)
row.prop(variable, "name")
row.operator("bim.remove_variable", icon="X", text="").index = index
row = layout.row()
row = self.layout.row()
row.prop(variable, "prop_key")
@@ -185,6 +185,7 @@ class UpdateRepresentation(bpy.types.Operator):
product, old_representation
)
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
+36
View File
@@ -0,0 +1,36 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
def enable_editing_text(drawing, obj=None):
drawing.enable_editing_text(obj)
drawing.import_text_attributes(obj)
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),
)
drawing.update_text_value(obj)
drawing.disable_editing_text(obj)
+10
View File
@@ -70,6 +70,16 @@ class Context:
def set_context(cls, context): pass
@interface
class Drawing:
def disable_editing_text(cls, obj): pass
def enable_editing_text(cls, obj): pass
def export_text_literal_attributes(cls, obj): pass
def get_text_literal(cls, obj): pass
def import_text_attributes(cls, obj): pass
def update_text_value(cls, obj): pass
@interface
class Geometry:
def change_object_data(cls, obj, data, is_global=False): pass
@@ -21,6 +21,7 @@ from blenderbim.tool.blender import Blender
from blenderbim.tool.brick import Brick
from blenderbim.tool.collector import Collector
from blenderbim.tool.context import Context
from blenderbim.tool.drawing import Drawing
from blenderbim.tool.geometry import Geometry
from blenderbim.tool.ifc import Ifc
from blenderbim.tool.material import Material
+61
View File
@@ -0,0 +1,61 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.representation
class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def disable_editing_text(cls, obj):
obj.BIMTextProperties.is_editing = False
@classmethod
def enable_editing_text(cls, obj):
obj.BIMTextProperties.is_editing = True
@classmethod
def export_text_literal_attributes(cls, obj):
return blenderbim.bim.helper.export_attributes(obj.BIMTextProperties.attributes)
@classmethod
def get_text_literal(cls, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not rep:
return
items = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
if items:
return items[0]
@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)
@classmethod
def update_text_value(cls, obj):
element = cls.get_text_literal(obj)
if element:
obj.BIMTextProperties.value = element.Literal
@@ -105,6 +105,12 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def get_ifc_representation_class(cls, element, representation):
if element.is_a("IfcAnnotation"):
if element.ObjectType == "TEXT":
return "IfcTextLiteral"
elif element.ObjectType == "TEXT_LEADER":
return "IfcGeometricCurveSet/IfcTextLiteral"
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialProfileSetUsage"):
return "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage"
@@ -151,6 +157,12 @@ 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]
@classmethod
def get_text_literal(cls, representation):
texts = [i for i in representation.Items if i.is_a("IfcTextLiteral")]
if texts:
return texts[0]
@classmethod
def get_total_representation_items(cls, obj):
return max(1, len(obj.material_slots))
+1
View File
@@ -5,6 +5,7 @@ markers =
bimtester
brick
context
drawing
geometry
material
misc
+7
View File
@@ -63,6 +63,13 @@ def context():
prophet.verify()
@pytest.fixture
def drawing():
prophet = Prophecy(blenderbim.core.tool.Drawing)
yield prophet
prophet.verify()
@pytest.fixture
def geometry():
prophet = Prophecy(blenderbim.core.tool.Geometry)
+43
View File
@@ -0,0 +1,43 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.core.drawing as subject
from test.core.bootstrap import ifc, drawing
class TestEnableEditingText:
def test_run(self, drawing):
drawing.enable_editing_text("obj").should_be_called()
drawing.import_text_attributes("obj").should_be_called()
subject.enable_editing_text(drawing, obj="obj")
class TestDisableEditingText:
def test_run(self, drawing):
drawing.disable_editing_text("obj").should_be_called()
subject.disable_editing_text(drawing, obj="obj")
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.update_text_value("obj").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(ifc, drawing, obj="obj")
+96
View File
@@ -0,0 +1,96 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
from blenderbim.tool.drawing import Drawing as subject
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Drawing)
class TestDisableEditingText(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
obj.BIMTextProperties.is_editing = True
subject.disable_editing_text(obj)
assert obj.BIMTextProperties.is_editing == False
class TestEnableEditingText(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing_text(obj)
assert obj.BIMTextProperties.is_editing == True
class TestExportTextLiteralAttributes(NewFile):
def test_run(self):
TestImportTextAttributes().test_run()
assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == {
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "BoxAlignment",
}
class TestGetTextLiteral(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcAnnotation()
element.Representation = ifc.createIfcProductDefinitionShape()
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="BoxAlignment")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
tool.Ifc.link(element, obj)
assert subject.get_text_literal(obj) == item
class TestImportTextAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcAnnotation()
element.Representation = ifc.createIfcProductDefinitionShape()
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="BoxAlignment")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
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 == "BoxAlignment"
class TestUpdateTextValue(NewFile):
def test_run(self):
TestGetTextLiteral().test_run()
obj = bpy.data.objects.get("Object")
subject.update_text_value(obj)
assert obj.BIMTextProperties.value == "Literal"
+24 -1
View File
@@ -178,7 +178,16 @@ class TestGetStyles(NewFile):
tool.Ifc.link(style, material)
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
obj.data.materials.append(material)
subject.get_styles(obj) == [style]
assert subject.get_styles(obj) == [style]
class TestGetTextLiteral(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
item = ifc.createIfcTextLiteralWithExtent()
representation = ifc.createIfcShapeRepresentation(Items=[item])
assert subject.get_text_literal(representation) == item
class TestGetCartesianPointCoordinateOffset(NewFile):
@@ -490,6 +499,20 @@ class TestGetIfcRepresentationClass(NewFile):
subject.get_ifc_representation_class(element, representation) == "IfcExtrudedAreaSolid/IfcCircleProfileDef"
)
def test_detecting_text_representations(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
element.ObjectType = "TEXT"
assert subject.get_ifc_representation_class(element, None) == "IfcTextLiteral"
def test_detecting_text_and_geometric_representations(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
element.ObjectType = "TEXT_LEADER"
assert subject.get_ifc_representation_class(element, None) == "IfcGeometricCurveSet/IfcTextLiteral"
def test_returning_null_for_non_parametric_representations(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
+29 -29
View File
@@ -18,18 +18,18 @@
import bpy
import ifcopenshell
import test.bim.bootstrap
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
from blenderbim.tool.owner import Owner as subject
class TestImplementsTool(test.bim.bootstrap.NewFile):
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Owner)
class TestSetUser(test.bim.bootstrap.NewFile):
class TestSetUser(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
@@ -38,7 +38,7 @@ class TestSetUser(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMOwnerProperties.active_user_id == user.id()
class TestGetUser(test.bim.bootstrap.NewFile):
class TestGetUser(NewFile):
def test_run(self):
assert subject.get_user() is None
TestSetUser().test_run()
@@ -56,14 +56,14 @@ class TestGetUser(test.bim.bootstrap.NewFile):
assert subject.get_user() == user2
class TestClearUser(test.bim.bootstrap.NewFile):
class TestClearUser(NewFile):
def test_run(self):
TestSetUser().test_run()
subject.clear_user()
assert bpy.context.scene.BIMOwnerProperties.active_user_id == 0
class TestSetAddress(test.bim.bootstrap.NewFile):
class TestSetAddress(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
address = ifc.createIfcPostalAddress()
@@ -71,7 +71,7 @@ class TestSetAddress(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMOwnerProperties.active_address_id == address.id()
class TestImportAddressAttributes(test.bim.bootstrap.NewFile):
class TestImportAddressAttributes(NewFile):
def test_importing_a_postal_address(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -152,7 +152,7 @@ class TestImportAddressAttributes(test.bim.bootstrap.NewFile):
assert len(props.messaging_ids) == 0
class TestClearAddress(test.bim.bootstrap.NewFile):
class TestClearAddress(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
address = ifc.createIfcPostalAddress()
@@ -161,7 +161,7 @@ class TestClearAddress(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMOwnerProperties.active_address_id == 0
class TestGetAddress(test.bim.bootstrap.NewFile):
class TestGetAddress(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -170,7 +170,7 @@ class TestGetAddress(test.bim.bootstrap.NewFile):
assert subject().get_address() == address
class TestExportAttributes(test.bim.bootstrap.NewFile):
class TestExportAttributes(NewFile):
def test_exporting_a_postal_address(self):
TestImportAddressAttributes().test_importing_a_postal_address()
assert subject().export_address_attributes() == {
@@ -201,7 +201,7 @@ class TestExportAttributes(test.bim.bootstrap.NewFile):
}
class TestAddAddressAttribute(test.bim.bootstrap.NewFile):
class TestAddAddressAttribute(NewFile):
def test_run(self):
subject().add_address_attribute("AddressLines")
subject().add_address_attribute("TelephoneNumbers")
@@ -216,7 +216,7 @@ class TestAddAddressAttribute(test.bim.bootstrap.NewFile):
assert len(props.messaging_ids) == 1
class TestRemoveAddressAttribute(test.bim.bootstrap.NewFile):
class TestRemoveAddressAttribute(NewFile):
TestAddAddressAttribute().test_run()
subject().remove_address_attribute("AddressLines", 0)
subject().remove_address_attribute("TelephoneNumbers", 0)
@@ -231,14 +231,14 @@ class TestRemoveAddressAttribute(test.bim.bootstrap.NewFile):
assert len(props.messaging_ids) == 0
class TestSetOrganisation(test.bim.bootstrap.NewFile):
class TestSetOrganisation(NewFile):
def test_run(self):
organisation = ifcopenshell.file().createIfcOrganization()
subject().set_organisation(organisation)
assert bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id()
class TestImportOrganisationAttributes(test.bim.bootstrap.NewFile):
class TestImportOrganisationAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -269,7 +269,7 @@ class TestImportOrganisationAttributes(test.bim.bootstrap.NewFile):
assert props.organisation_attributes.get("Description").string_value == ""
class TestClearOrganisation(test.bim.bootstrap.NewFile):
class TestClearOrganisation(NewFile):
def test_run(self):
props = bpy.context.scene.BIMOwnerProperties
props.active_organisation_id = 1
@@ -277,7 +277,7 @@ class TestClearOrganisation(test.bim.bootstrap.NewFile):
assert props.active_organisation_id == 0
class TestExportOrganisationAttributes(test.bim.bootstrap.NewFile):
class TestExportOrganisationAttributes(NewFile):
def test_run(self):
TestImportOrganisationAttributes().test_run()
assert subject().export_organisation_attributes() == {
@@ -287,7 +287,7 @@ class TestExportOrganisationAttributes(test.bim.bootstrap.NewFile):
}
class TestGetOrganisation(test.bim.bootstrap.NewFile):
class TestGetOrganisation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -296,14 +296,14 @@ class TestGetOrganisation(test.bim.bootstrap.NewFile):
assert subject().get_organisation() == organisation
class TestSetPerson(test.bim.bootstrap.NewFile):
class TestSetPerson(NewFile):
def test_run(self):
person = ifcopenshell.file().createIfcPerson()
subject().set_person(person)
assert bpy.context.scene.BIMOwnerProperties.active_person_id == person.id()
class TestImportPersonAttributes(test.bim.bootstrap.NewFile):
class TestImportPersonAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -346,7 +346,7 @@ class TestImportPersonAttributes(test.bim.bootstrap.NewFile):
assert props.person_attributes.get("GivenName").string_value == ""
class TestClearPerson(test.bim.bootstrap.NewFile):
class TestClearPerson(NewFile):
def test_run(self):
props = bpy.context.scene.BIMOwnerProperties
props.active_person_id = 1
@@ -354,7 +354,7 @@ class TestClearPerson(test.bim.bootstrap.NewFile):
assert props.active_person_id == 0
class TestExportPersonAttributes(test.bim.bootstrap.NewFile):
class TestExportPersonAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -385,7 +385,7 @@ class TestExportPersonAttributes(test.bim.bootstrap.NewFile):
assert result["SuffixTitles"] is None
class TestGetPerson(test.bim.bootstrap.NewFile):
class TestGetPerson(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -394,7 +394,7 @@ class TestGetPerson(test.bim.bootstrap.NewFile):
assert subject().get_person() == person
class TestAddPersonAttribute(test.bim.bootstrap.NewFile):
class TestAddPersonAttribute(NewFile):
def test_run(self):
subject().add_person_attribute("MiddleNames")
subject().add_person_attribute("PrefixTitles")
@@ -405,7 +405,7 @@ class TestAddPersonAttribute(test.bim.bootstrap.NewFile):
assert len(props.suffix_titles) == 1
class TestRemovePersonAttribute(test.bim.bootstrap.NewFile):
class TestRemovePersonAttribute(NewFile):
def test_run(self):
subject().add_person_attribute("MiddleNames")
subject().remove_person_attribute("MiddleNames", 0)
@@ -419,14 +419,14 @@ class TestRemovePersonAttribute(test.bim.bootstrap.NewFile):
assert len(props.suffix_titles) == 0
class TestSetRole(test.bim.bootstrap.NewFile):
class TestSetRole(NewFile):
def test_run(self):
role = ifcopenshell.file().createIfcActorRole()
subject().set_role(role)
assert bpy.context.scene.BIMOwnerProperties.active_role_id == role.id()
class TestImportRoleAttributes(test.bim.bootstrap.NewFile):
class TestImportRoleAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -454,7 +454,7 @@ class TestImportRoleAttributes(test.bim.bootstrap.NewFile):
assert props.role_attributes.get("Role").enum_value == "ARCHITECT"
class TestClearRole(test.bim.bootstrap.NewFile):
class TestClearRole(NewFile):
def test_run(self):
role = ifcopenshell.file().createIfcActorRole()
subject().set_role(role)
@@ -462,7 +462,7 @@ class TestClearRole(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMOwnerProperties.active_role_id == 0
class TestGetRole(test.bim.bootstrap.NewFile):
class TestGetRole(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -471,7 +471,7 @@ class TestGetRole(test.bim.bootstrap.NewFile):
assert subject().get_role() == role
class TestExportRoleAttributes(test.bim.bootstrap.NewFile):
class TestExportRoleAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
@@ -0,0 +1,10 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"text_literal": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["text_literal"], name, value)
@@ -28,8 +28,11 @@ class Usecase:
# IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids
# IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage
# IfcGeometricCurveSet/IfcTextLiteral
# IfcTextLiteral
"ifc_representation_class": None, # Whether to cast a mesh into a particular class
"profile_set_usage": None, # The material profile set if the extrusion requires it
"text_literal": None, # The text literal if the representation requires it
}
self.ifc_vertices = []
for key, value in settings.items():
@@ -117,9 +120,16 @@ class Usecase:
return self.create_lighting_representation()
def create_plan_representation(self):
if self.settings["context"].ContextIdentifier == "Annotation":
if isinstance(self.settings["geometry"], bpy.types.TextCurve):
return self.create_text_representation()
if self.settings["ifc_representation_class"] == "IfcTextLiteral":
return self.create_text_representation()
elif self.settings["ifc_representation_class"] == "IfcGeometricCurveSet/IfcTextLiteral":
shape_representation = self.create_geometric_curve_set_representation(is_2d=True)
shape_representation.RepresentationType = "Annotation2D"
items = list(shape_representation.Items)
items.append(self.create_text())
shape_representation.Items = items
return shape_representation
elif self.settings["context"].ContextIdentifier == "Annotation":
shape_representation = self.create_geometric_curve_set_representation(is_2d=True)
shape_representation.RepresentationType = "Annotation2D"
return shape_representation
@@ -143,8 +153,6 @@ class Usecase:
elif self.settings["context"].ContextIdentifier == "SurveyPoints":
pass
else:
if isinstance(self.settings["geometry"], bpy.types.TextCurve):
return self.create_text_representation()
shape_representation = self.create_geometric_curve_set_representation(is_2d=True)
shape_representation.RepresentationType = "Annotation2D"
return shape_representation
@@ -180,21 +188,8 @@ class Usecase:
)
def create_text(self):
text = self.settings["geometry"]
if text.align_y in ["TOP_BASELINE", "BOTTOM_BASELINE", "BOTTOM"]:
y = "bottom"
elif text.align_y == "CENTER":
y = "middle"
elif text.align_y == "TOP":
y = "top"
if text.align_x == "LEFT":
x = "left"
elif text.align_x == "CENTER":
x = "middle"
elif text.align_x == "RIGHT":
x = "right"
if self.settings["text_literal"]:
return self.settings["text_literal"]
origin = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
self.file.createIfcDirection((0.0, 0.0, 1.0)),
@@ -203,7 +198,7 @@ class Usecase:
# TODO: Planar extent right now is wrong ...
return self.file.createIfcTextLiteralWithExtent(
text.body, origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), f"{y}-{x}"
"TEXT", origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), "bottom-left"
)
def create_variable_representation(self):
@@ -0,0 +1,15 @@
import test.bootstrap
import ifcopenshell.api
class TestEditTextLiteral(test.bootstrap.IFC4):
def test_run(self):
text = self.file.createIfcTextLiteralWithExtent()
ifcopenshell.api.run("drawing.edit_text_literal", self.file, text_literal=text, attributes={
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "middle",
})
assert text.Literal == "Literal"
assert text.Path == "RIGHT"
assert text.BoxAlignment == "middle"