mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Annotation Tool
Added Annotation Tool that allows you create annotation types and their occurences. It will also allow us to get rid of "Annotation" panel in N-panel. I've recorded bunch of short videos to demonstrate the features: - bulk tagging - currently works only for "Text" and "Stair arrow" type of annotations, adds annotations to all selected objects, automatically assigns products to the annotations https://user-images.githubusercontent.com/9417531/231968358-28cbb762-3349-470a-beef-d1ab21882769.mp4 - creating annotation type with representation - all occurences will share the same representation which makes bulk tagging really powerful if you use variables like {{PredefinedType}}, {{GlobalId}}, {{Name}}, {{Description}}, {{ObjectType}}, {{Tag}} https://user-images.githubusercontent.com/9417531/231970500-333514c6-6406-4ef5-a9e7-fddfcc9a2436.mp4 - creating annotation type without representation - it allows occurences to have unique text or line geometry but still share the same classes from inherited property set Examples: creating text type without representation to create title case annotations https://user-images.githubusercontent.com/9417531/231969361-bda5f924-a8c8-4f5a-ba61-461aa7101f73.mp4 creating type for oblique dimension annotations https://user-images.githubusercontent.com/9417531/231969902-38e8c3f4-45a6-4c98-bba4-2956288ce10a.mp4 - readjusting - if you created some annotations using bulk tagging but the products were changed / moved afterwards now you can select those annotations and "readjust" them according to new product's position / measurements https://user-images.githubusercontent.com/9417531/231968975-ee8d6589-60ee-4575-9be2-b0ce68d43ffa.mp4 PS In this commit I've also moved all icons to 1 .blend file and added instructions how to regenerate them.
This commit is contained in:
@@ -5,14 +5,14 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#6,#7));
|
||||
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#9,#10,#11,#12,#13));
|
||||
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator, handler, gizmos
|
||||
from . import ui, prop, operator, handler, gizmos, workspace
|
||||
|
||||
classes = (
|
||||
operator.ActivateDrawing,
|
||||
@@ -80,6 +80,7 @@ classes = (
|
||||
prop.Literal,
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
prop.BIMAnnotationProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_annotation_utilities,
|
||||
@@ -95,11 +96,15 @@ classes = (
|
||||
gizmos.DimensionLabelGizmo,
|
||||
gizmos.ExtrusionGuidesGizmo,
|
||||
gizmos.ExtrusionWidget,
|
||||
workspace.Hotkey,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=True)
|
||||
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
|
||||
bpy.types.Scene.BIMAnnotationProperties = bpy.props.PointerProperty(type=prop.BIMAnnotationProperties)
|
||||
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
|
||||
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
|
||||
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
@@ -109,7 +114,10 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.AnnotationTool)
|
||||
del bpy.types.Scene.DocProperties
|
||||
del bpy.types.Scene.BIMAnnotationProperties
|
||||
del bpy.types.Camera.BIMCameraProperties
|
||||
del bpy.types.Object.BIMAssignedProductProperties
|
||||
del bpy.types.Object.BIMTextProperties
|
||||
|
||||
@@ -77,17 +77,20 @@ class Annotator:
|
||||
co1, co2, _, _ = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1
|
||||
co2 = obj.matrix_world.inverted() @ co2
|
||||
|
||||
if isinstance(obj.data, bpy.types.Mesh):
|
||||
obj.data.vertices.add(2)
|
||||
obj.data.vertices[-2].co = co1
|
||||
obj.data.vertices[-1].co = co2
|
||||
obj.data.edges.add(1)
|
||||
obj.data.edges[-1].vertices = (obj.data.vertices[-2].index, obj.data.vertices[-1].index)
|
||||
|
||||
if isinstance(obj.data, bpy.types.Curve):
|
||||
polyline = obj.data.splines.new("POLY")
|
||||
polyline.points.add(1)
|
||||
polyline.points[-2].co = list(co1) + [1]
|
||||
polyline.points[-1].co = list(co2) + [1]
|
||||
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
@@ -114,11 +117,13 @@ class Annotator:
|
||||
matrix_world[1][3] = co1.y
|
||||
matrix_world[2][3] = co1.z
|
||||
collection = camera.users_collection[0]
|
||||
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
@@ -127,17 +132,20 @@ class Annotator:
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
if object_type != "ANGLE":
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type and obj.type == object_type.upper():
|
||||
return obj
|
||||
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
elif data_type == "curve":
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
@@ -148,14 +156,13 @@ class Annotator:
|
||||
if not camera:
|
||||
camera = bpy.context.scene.camera
|
||||
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
y = (
|
||||
camera.data.ortho_scale
|
||||
* (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x)
|
||||
/ 4
|
||||
)
|
||||
else:
|
||||
y = camera.data.ortho_scale / 4
|
||||
y = camera.data.ortho_scale / 4
|
||||
|
||||
res_x = bpy.context.scene.render.resolution_x
|
||||
res_y = bpy.context.scene.render.resolution_y
|
||||
if res_x > res_y:
|
||||
y *= res_y / res_x
|
||||
|
||||
y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0))
|
||||
x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0))
|
||||
return (
|
||||
|
||||
@@ -31,6 +31,7 @@ def refresh():
|
||||
DrawingsData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
AnnotationData.is_loaded = False
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -220,7 +221,7 @@ class DecoratorData:
|
||||
return result
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
return None
|
||||
|
||||
props = obj.BIMTextProperties
|
||||
@@ -276,3 +277,28 @@ class DecoratorData:
|
||||
|
||||
cls.data[obj.name] = dimension_style
|
||||
return dimension_style
|
||||
|
||||
|
||||
class AnnotationData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.props = bpy.context.scene.BIMAnnotationProperties
|
||||
cls.data["relating_types"] = cls.get_relating_types()
|
||||
|
||||
@classmethod
|
||||
def get_relating_types(cls):
|
||||
object_type = cls.props.object_type
|
||||
relating_types = []
|
||||
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
|
||||
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
|
||||
relating_types.append(relating_type)
|
||||
|
||||
enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types]
|
||||
|
||||
# item to create anootations without relating types
|
||||
enum_items.insert(0, ("0", "-", ""))
|
||||
return enum_items
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,7 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.drawing as core
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.decoration as decoration
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
from pathlib import Path
|
||||
from blenderbim.bim.prop import Attribute, StrProperty
|
||||
@@ -465,3 +465,55 @@ class BIMTextProperties(PropertyGroup):
|
||||
class BIMAssignedProductProperties(PropertyGroup):
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
|
||||
|
||||
|
||||
# ObjectType: annotation_name, description, icon, data_type
|
||||
ANNOTATION_TYPES_DATA = {
|
||||
"DIMENSION": ("Dimension", "", "FIXED_SIZE", "curve"),
|
||||
"ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
|
||||
"RADIUS": ("Radius", "", "FORWARD", "curve"),
|
||||
"DIAMETER": ("Diameter", "", "ARROW_LEFTRIGHT", "curve"),
|
||||
"TEXT": ("Text", "", "SMALL_CAPS", "empty"),
|
||||
"TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"),
|
||||
"STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
|
||||
"HIDDEN_LINE": ("Hidden", "", "CON_TRACKTO", "mesh"),
|
||||
"PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
|
||||
"SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
|
||||
"BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
|
||||
"LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"),
|
||||
"BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
|
||||
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
|
||||
"FALL": ("Fall", "", "SORT_ASC", "curve"),
|
||||
}
|
||||
|
||||
annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)]
|
||||
|
||||
|
||||
def get_annotation_data_prop(prop_name):
|
||||
def function(self, context):
|
||||
if not AnnotationData.is_loaded:
|
||||
AnnotationData.load()
|
||||
return AnnotationData.data[prop_name]
|
||||
|
||||
return function
|
||||
|
||||
|
||||
def update_annotation_object_type(self, context):
|
||||
self.relating_type_id = "0"
|
||||
# changing enum doesn't trigger refresh by itself
|
||||
AnnotationData.is_loaded = False
|
||||
|
||||
|
||||
class BIMAnnotationProperties(PropertyGroup):
|
||||
object_type: bpy.props.EnumProperty(
|
||||
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
|
||||
)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
name="Relating Annotation Type", items=get_annotation_data_prop("relating_types")
|
||||
)
|
||||
create_representation_for_type: bpy.props.BoolProperty(
|
||||
name="Create Representation For Type",
|
||||
default=False,
|
||||
description='Whether "Add type" should define a representation for the type \n'
|
||||
"or allow occurences to have their own",
|
||||
)
|
||||
|
||||
@@ -371,7 +371,7 @@ class BIM_PT_text(Panel):
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
|
||||
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
@@ -474,7 +474,6 @@ class BIM_PT_annotation_utilities(Panel):
|
||||
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
|
||||
op.object_type = "STAIR_ARROW"
|
||||
op.data_type = "curve"
|
||||
op.description = "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation"
|
||||
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
|
||||
op.object_type = "HIDDEN_LINE"
|
||||
op.data_type = "mesh"
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# 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 os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from bpy.types import WorkSpaceTool
|
||||
|
||||
# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.handler
|
||||
|
||||
|
||||
# declaring it here to avoid circular import problems
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AnnotationTool(WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
|
||||
bl_idname = "bim.annotation_tool"
|
||||
bl_label = "Annotation Tool"
|
||||
bl_description = "Gives you Annotation related superpowers"
|
||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
|
||||
bl_widget = None
|
||||
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
|
||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
||||
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.annotation_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
("bim.annotation_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
|
||||
("bim.annotation_hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
|
||||
("bim.annotation_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
|
||||
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
|
||||
("bim.annotation_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
("bim.annotation_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
|
||||
("bim.annotation_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
|
||||
("bim.annotation_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
|
||||
("bim.annotation_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
("bim.annotation_hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
|
||||
("bim.annotation_hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
|
||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
|
||||
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
# Unlike operators, Blender doesn't treat workspace tools as a class, so we'll create our own.
|
||||
AnnotationToolUI.draw(context, layout)
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon=modifiers[modifier])
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
op = row.operator("bim.annotation_hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op, row
|
||||
|
||||
|
||||
# TODO: move to operator
|
||||
def create_annotation_type(context):
|
||||
# just empty to store parameters
|
||||
props = context.scene.BIMAnnotationProperties
|
||||
object_type = props.object_type
|
||||
create_representation = props.create_representation_for_type
|
||||
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
|
||||
|
||||
if props.create_representation_for_type:
|
||||
obj = tool.Drawing.create_annotation_object(drawing, object_type)
|
||||
else:
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
|
||||
obj.name = f"{object_type}_TYPE"
|
||||
obj.location = context.scene.cursor.location
|
||||
tool.Drawing.ensure_annotation_in_drawing_plane(obj)
|
||||
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing))
|
||||
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcTypeProduct",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=create_representation,
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
|
||||
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
|
||||
|
||||
# TODO: move to operator
|
||||
def create_annotation_occurence(context):
|
||||
# object_type = context.scene.BIMAnnotationProperties.object_type
|
||||
props = context.scene.BIMAnnotationProperties
|
||||
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
|
||||
object_type = props.object_type
|
||||
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
obj = tool.Drawing.create_annotation_object(drawing, object_type)
|
||||
obj.name = relating_type.Name
|
||||
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing))
|
||||
relating_type_repr = tool.Drawing.get_annotation_representation(relating_type)
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=not bool(relating_type_repr),
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
|
||||
tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element])
|
||||
tool.Collector.assign(obj)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
|
||||
|
||||
def create_annotation():
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
create_type_occurence = props.relating_type_id != "0"
|
||||
if create_type_occurence:
|
||||
create_annotation_occurence(bpy.context)
|
||||
else:
|
||||
object_type = props.object_type
|
||||
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
|
||||
|
||||
|
||||
class AnnotationToolUI:
|
||||
@classmethod
|
||||
def draw(cls, context, layout):
|
||||
cls.layout = layout
|
||||
cls.props = context.scene.BIMAnnotationProperties
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not AnnotationData.is_loaded:
|
||||
AnnotationData.load()
|
||||
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
if context.active_object and context.selected_objects:
|
||||
cls.draw_edit_object_interface(context)
|
||||
elif not context.selected_objects:
|
||||
cls.draw_create_object_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_create_object_interface(cls):
|
||||
row = cls.layout.row(align=True)
|
||||
op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type")
|
||||
selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT"
|
||||
row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon)
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_basic_annotation_tool_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
object_type = cls.props.object_type
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_VOLUME")
|
||||
prop_with_search(row, cls.props, "object_type", text="")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
|
||||
create_type_occurence = cls.props.relating_type_id != "0"
|
||||
label = "Add Type Occurence" if create_type_occurence else "Add Annotation"
|
||||
add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation")
|
||||
|
||||
if object_type in ("TEXT", "STAIR_ARROW"):
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Bulk Tag",
|
||||
"S_T",
|
||||
"Create new annotations and automatically adjust them to the selected objects",
|
||||
)
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
|
||||
)
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.annotation_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
self.props = context.scene.BIMAnnotationProperties
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
|
||||
def invoke(self, context, event):
|
||||
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
|
||||
self.props = context.scene.BIMAnnotationProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def hotkey_S_T(self):
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
object_type = props.object_type
|
||||
|
||||
related_objects = bpy.context.selected_objects
|
||||
for related_object in related_objects:
|
||||
create_annotation()
|
||||
obj = bpy.context.active_object
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
||||
|
||||
def hotkey_S_A(self):
|
||||
create_annotation()
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if not bpy.context.object:
|
||||
return
|
||||
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
bpy.ops.bim.edit_text_popup()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
|
||||
related_product = tool.Drawing.get_assigned_product(element)
|
||||
if not related_product:
|
||||
continue
|
||||
related_object = tool.Ifc.get_object(related_product)
|
||||
|
||||
tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object)
|
||||
|
||||
def hotkey_S_C(self):
|
||||
create_annotation_type(bpy.context)
|
||||
@@ -68,9 +68,7 @@ class UnassignType(bpy.types.Operator):
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
objs = (
|
||||
[bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
|
||||
)
|
||||
objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or element.is_a("IfcElementType"):
|
||||
@@ -90,7 +88,10 @@ class UnassignType(bpy.types.Operator):
|
||||
else:
|
||||
# We must unmap representations.
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), resolved_representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
|
||||
tool.Ifc.get(),
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
@@ -227,6 +228,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
context=body,
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
|
||||
elif template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
obj = bpy.data.objects.new(name, None)
|
||||
@@ -260,6 +262,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif template == "LAYERSET_AXIS3":
|
||||
axis = "AXIS3"
|
||||
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis})
|
||||
|
||||
elif template == "PROFILESET":
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
obj = bpy.data.objects.new(name, None)
|
||||
|
||||
@@ -321,7 +321,6 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
|
||||
)
|
||||
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
|
||||
collector.assign(obj)
|
||||
drawing_tool.setup_annotation_object(obj, object_type)
|
||||
drawing_tool.enable_editing(obj)
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class Blender:
|
||||
@@ -175,9 +176,17 @@ class Blender:
|
||||
"max_y": bound_box[6][1],
|
||||
"min_z": bound_box[0][2],
|
||||
"max_z": bound_box[6][2],
|
||||
"center": (Vector(bound_box[6]) - Vector(bound_box[0])) / 2,
|
||||
}
|
||||
return bbox_dict
|
||||
|
||||
@classmethod
|
||||
def select_and_activate_single_object(cls, context, active_object):
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
context.view_layer.objects.active = active_object
|
||||
active_object.select_set(True)
|
||||
|
||||
## BMESH UTILS ##
|
||||
@classmethod
|
||||
def apply_bmesh(cls, mesh, bm, obj=None):
|
||||
|
||||
@@ -40,6 +40,7 @@ from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS
|
||||
|
||||
from mathutils import Vector
|
||||
import collections
|
||||
|
||||
|
||||
class Drawing(blenderbim.core.tool.Drawing):
|
||||
@@ -80,8 +81,28 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
|
||||
return obj
|
||||
|
||||
# TODO: add a way for users to run this method to readjust the annotations
|
||||
# based on non-modifier stairs
|
||||
@classmethod
|
||||
def ensure_annotation_in_drawing_plane(cls, obj, camera=None):
|
||||
"""Make sure annotation object is going to be visible in the camera view"""
|
||||
|
||||
def get_camera_from_annotation_object(obj):
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
if not entity:
|
||||
return
|
||||
for rel in entity.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToGroup"):
|
||||
for e in rel.RelatedObjects:
|
||||
if e.ObjectType == "DRAWING":
|
||||
return tool.Ifc.get_object(e)
|
||||
|
||||
if not camera:
|
||||
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
|
||||
|
||||
current_pos = camera.matrix_world.inverted() @ obj.location
|
||||
current_pos.z = -camera.data.clip_start
|
||||
current_pos = camera.matrix_world @ current_pos
|
||||
obj.location = current_pos
|
||||
|
||||
@classmethod
|
||||
def setup_annotation_object(cls, obj, object_type, related_object=None):
|
||||
"""Finish object's adjustments after both object and entity are created"""
|
||||
@@ -89,27 +110,70 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if not related_object:
|
||||
related_object = bpy.context.active_object
|
||||
related_entity = tool.Ifc.get_entity(related_object)
|
||||
if not related_entity:
|
||||
return
|
||||
|
||||
obj_entity = tool.Ifc.get_entity(obj)
|
||||
assign_product = False
|
||||
|
||||
if object_type == "STAIR_ARROW":
|
||||
if related_entity and related_entity.is_a("IfcStairFlight"):
|
||||
stair, stair_entity = related_object, related_entity
|
||||
arrow = obj
|
||||
arrow_element = tool.Ifc.get_entity(arrow)
|
||||
arrow.parent = stair
|
||||
if related_entity.is_a("IfcStairFlight"):
|
||||
stair, arrow = related_object, obj
|
||||
|
||||
# place the arrow
|
||||
# NOTE: may not work correctly in EDIT mode
|
||||
bbox = tool.Blender.get_object_bounding_box(stair)
|
||||
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
|
||||
arrow.location = Vector((bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"]))
|
||||
arrow.location = stair.matrix_world @ Vector(
|
||||
(bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"])
|
||||
)
|
||||
last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"]))
|
||||
|
||||
arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1))
|
||||
arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1))
|
||||
|
||||
if not cls.get_assigned_product(arrow_element):
|
||||
tool.Ifc.run("drawing.assign_product", relating_product=stair_entity, related_object=arrow_element)
|
||||
pass
|
||||
cls.ensure_annotation_in_drawing_plane(obj)
|
||||
assign_product = True
|
||||
|
||||
elif object_type == "TEXT":
|
||||
bbox = tool.Blender.get_object_bounding_box(related_object)
|
||||
|
||||
obj.location = related_object.matrix_world @ bbox["center"]
|
||||
cls.ensure_annotation_in_drawing_plane(obj)
|
||||
assign_product = True
|
||||
|
||||
if assign_product and not cls.get_assigned_product(obj_entity):
|
||||
tool.Ifc.run("drawing.assign_product", relating_product=related_entity, related_object=obj_entity)
|
||||
|
||||
if object_type == "TEXT":
|
||||
tool.Drawing.update_text_value(obj)
|
||||
|
||||
@classmethod
|
||||
def is_annotation_object_type(cls, element, object_types):
|
||||
if not isinstance(object_types, collections.abc.Iterable):
|
||||
object_types = [object_types]
|
||||
|
||||
element_type = element.is_a()
|
||||
|
||||
if element_type == "IfcAnnotation" and element.ObjectType in object_types:
|
||||
return True
|
||||
|
||||
if element_type == "IfcTypeProduct" and element.ApplicableOccurrence.startswith("IfcAnnotation/"):
|
||||
applicable_object_type = element.ApplicableOccurrence.split("/")[1]
|
||||
if applicable_object_type in object_types:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_annotation_representation(cls, element):
|
||||
rep = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "Annotation"
|
||||
) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
|
||||
if not rep:
|
||||
return None
|
||||
|
||||
rep = tool.Geometry.resolve_mapped_representation(rep)
|
||||
return rep
|
||||
|
||||
@classmethod
|
||||
def create_camera(cls, name, matrix):
|
||||
@@ -376,13 +440,11 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
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")
|
||||
rep = cls.get_annotation_representation(element)
|
||||
if not rep:
|
||||
return
|
||||
items = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
||||
return [] if return_list else None
|
||||
|
||||
items = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
||||
if not items:
|
||||
return [] if return_list else None
|
||||
if return_list:
|
||||
@@ -395,9 +457,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if not element:
|
||||
return
|
||||
|
||||
rep = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "Annotation"
|
||||
) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
|
||||
rep = cls.get_annotation_representation(element)
|
||||
if not rep:
|
||||
return
|
||||
|
||||
@@ -435,9 +495,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if not element:
|
||||
return
|
||||
|
||||
rep = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "Annotation"
|
||||
) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
|
||||
rep = cls.get_annotation_representation(element)
|
||||
|
||||
if not rep:
|
||||
return
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -420,7 +420,6 @@ class TestAddAnnotation:
|
||||
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
||||
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
|
||||
collector.assign("obj").should_be_called()
|
||||
drawing.setup_annotation_object("obj", "object_type").should_be_called()
|
||||
drawing.enable_editing("obj").should_be_called()
|
||||
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user