mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-18 03:19:53 +00:00
3787 lines
157 KiB
Python
3787 lines
157 KiB
Python
# Bonsai - OpenBIM Blender Add-on
|
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# This file is part of Bonsai.
|
|
#
|
|
# Bonsai 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.
|
|
#
|
|
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import bpy
|
|
import json
|
|
import time
|
|
import bmesh
|
|
import shutil
|
|
import hashlib
|
|
import shapely
|
|
import shapely.ops
|
|
import subprocess
|
|
import numpy as np
|
|
import multiprocessing
|
|
import ifcopenshell
|
|
import ifcopenshell.api
|
|
import ifcopenshell.api.document
|
|
import ifcopenshell.api.pset
|
|
import ifcopenshell.api.style
|
|
import ifcopenshell.ifcopenshell_wrapper
|
|
import ifcopenshell.geom
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.representation
|
|
import ifcopenshell.util.selector
|
|
import ifcopenshell.util.unit
|
|
import bonsai.bim.helper
|
|
import bonsai.bim.handler
|
|
import bonsai.tool as tool
|
|
import bonsai.core.geometry
|
|
import bonsai.core.drawing as core
|
|
import bonsai.bim.module.drawing.svgwriter as svgwriter
|
|
import bonsai.bim.module.drawing.annotation as annotation
|
|
import bonsai.bim.module.drawing.sheeter as sheeter
|
|
import bonsai.bim.export_ifc
|
|
from bpy_extras.io_utils import ImportHelper
|
|
from bonsai.bim.module.drawing.decoration import CutDecorator
|
|
from bonsai.bim.module.drawing.data import DecoratorData
|
|
from typing import NamedTuple, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict, get_args
|
|
from lxml import etree
|
|
from math import radians
|
|
from mathutils import Vector, Color, Matrix
|
|
from timeit import default_timer as timer
|
|
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
|
|
from bonsai.bim.ifc import IfcStore
|
|
from pathlib import Path
|
|
from bpy_extras.image_utils import load_image
|
|
|
|
if TYPE_CHECKING:
|
|
from bonsai.bim.module.drawing.prop import RenderType
|
|
from bonsai.bim.module.project.prop import Link
|
|
from bpy.stub_internal import rna_enums
|
|
|
|
cwd = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
|
class profile:
|
|
"""
|
|
A python context manager timing utility
|
|
"""
|
|
|
|
def __init__(self, task):
|
|
self.task = task
|
|
|
|
def __enter__(self):
|
|
self.start = timer()
|
|
|
|
def __exit__(self, *args):
|
|
print(self.task, timer() - self.start)
|
|
|
|
|
|
class LineworkContexts(NamedTuple):
|
|
body: list[list[int]]
|
|
annotation: list[list[int]]
|
|
|
|
|
|
class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_annotation_type"
|
|
bl_label = "Add Annotation Type"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_annotation_props()
|
|
object_type = props.object_type
|
|
has_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 = props.type_name
|
|
ifc_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW")
|
|
element = tool.Drawing.run_root_assign_class(
|
|
obj=obj,
|
|
ifc_class="IfcTypeProduct",
|
|
predefined_type=object_type,
|
|
should_add_representation=has_representation,
|
|
context=ifc_context,
|
|
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
|
)
|
|
if representation := tool.Drawing.get_representation(element, ifc_context):
|
|
tool.Drawing.reload_representation(obj=obj, representation=representation)
|
|
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
|
|
|
|
if props.create_representation_for_type and object_type == "IMAGE":
|
|
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
|
|
|
|
|
class EnableAddAnnotationType(bpy.types.Operator):
|
|
bl_idname = "bim.enable_add_annotation_type"
|
|
bl_label = "Enable Add Annotation Type"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
|
tool.Drawing.get_annotation_props().is_adding_type = True
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DisableAddAnnotationType(bpy.types.Operator):
|
|
bl_idname = "bim.disable_add_annotation_type"
|
|
bl_label = "Disable Add Annotation Type"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
|
tool.Drawing.get_annotation_props().is_adding_type = False
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_drawing"
|
|
bl_label = "Add Drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Add a drawing view to the IFC project.\n\n"
|
|
"For all views besides MODEL_VIEW camera will be placed at the current cursor position,\n"
|
|
"for MODEL_VIEW camera will be aligned to the current viewport position and orientation."
|
|
)
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
hint = props.location_hint
|
|
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
|
|
hint = int(hint)
|
|
else:
|
|
assert hint in tool.Drawing.LOCATION_HINT_LITERALS
|
|
core.add_drawing(
|
|
tool.Ifc,
|
|
tool.Collector,
|
|
tool.Drawing,
|
|
target_view=props.target_view,
|
|
location_hint=hint,
|
|
)
|
|
|
|
# TODO: Why need to resync active drawing, if it wasn't changed.
|
|
drawing = props.get_active_drawing()
|
|
if drawing is None:
|
|
return
|
|
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
|
|
|
|
|
class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.duplicate_drawing"
|
|
bl_label = "Duplicate Drawing"
|
|
bl_description = "Make a copy of currently selected drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
drawing: bpy.props.IntProperty()
|
|
should_duplicate_annotations: bpy.props.BoolProperty(name="Should Duplicate Annotations", default=False)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
assert context.window_manager
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
assert self.layout
|
|
row = self.layout
|
|
row.prop(self, "should_duplicate_annotations")
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
core.duplicate_drawing(
|
|
tool.Ifc,
|
|
tool.Blender,
|
|
tool.Drawing,
|
|
tool.Geometry,
|
|
drawing=tool.Ifc.get().by_id(self.drawing),
|
|
should_duplicate_annotations=self.should_duplicate_annotations,
|
|
)
|
|
|
|
|
|
class CreateDrawing(bpy.types.Operator):
|
|
"""Creates/refreshes a .svg drawing
|
|
|
|
Only available if :
|
|
- IFC file is created
|
|
- Camera is in Orthographic mode"""
|
|
|
|
bl_idname = "bim.create_drawing"
|
|
bl_label = "Create Drawing"
|
|
bl_description = (
|
|
"Creates/refreshes a .svg drawing based on currently active camera\n"
|
|
+ 'and open with default system viewer or using "svg_command" or\n'
|
|
+ '"pdf_command" from the Bonsai preferences (if provided).\n\n'
|
|
+ "SHIFT+CLICK to create/refresh all shown checked drawings, but doesn't\n"
|
|
+ "open them for viewing.\n\n"
|
|
+ "Add the CTRL modifier to optionally open drawings to view them as\n"
|
|
+ "they are created"
|
|
)
|
|
print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
name="Print All",
|
|
default=False,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
name="Open in Viewer",
|
|
default=False,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
name="Sync Before Creating Drawing",
|
|
description="Could save some time if you're sure IFC and current Blender session are already in sync",
|
|
default=True,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
print_all: bool
|
|
open_viewer: bool
|
|
sync: bool
|
|
|
|
drawing_name: str
|
|
is_manifold_cache: dict[str, bool]
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not tool.Ifc.get():
|
|
return False
|
|
if not tool.Drawing.is_drawing_active():
|
|
cls.poll_message_set("No active drawing.")
|
|
return False
|
|
assert context.scene
|
|
assert (camera_obj := context.scene.camera)
|
|
if tool.Drawing.get_camera_props(camera_obj).linework_mode == "FREESTYLE" and not hasattr(
|
|
context.scene, "svg_export"
|
|
):
|
|
cls.poll_message_set(
|
|
"Freestyle SVG Exporter extension is not installed (required for 'Freestyle' linework mode)."
|
|
)
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
# printing all drawings on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.print_all = True
|
|
if event.type == "LEFTMOUSE" and event.ctrl:
|
|
self.open_viewer = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
self.props = tool.Drawing.get_document_props()
|
|
assert context.scene and context.scene.camera
|
|
|
|
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
|
|
original_drawing_id = None
|
|
if self.print_all:
|
|
original_drawing_id = active_drawing_id
|
|
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
|
|
else:
|
|
drawings_to_print = [active_drawing_id]
|
|
|
|
for drawing_i, drawing_id in enumerate(drawings_to_print):
|
|
self.drawing_index = drawing_i
|
|
if self.print_all:
|
|
bpy.ops.bim.activate_drawing(drawing=drawing_id, should_view_from_camera=False)
|
|
|
|
self.camera = context.scene.camera
|
|
assert (camera_element := tool.Ifc.get_entity(self.camera))
|
|
self.camera_element = camera_element
|
|
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
|
|
self.file = tool.Ifc.get()
|
|
|
|
with profile("Drawing generation process"):
|
|
with profile("Initialize drawing generation process"):
|
|
self.cprops = tool.Drawing.get_camera_props(self.camera)
|
|
self.drawing = self.file.by_id(drawing_id)
|
|
self.drawing_name = self.drawing.Name
|
|
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
|
|
self.get_scale(context)
|
|
if self.cprops.update_representation(self.camera.matrix_world):
|
|
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
|
|
# Reassign props as data is recreated during the update.
|
|
self.cprops = tool.Drawing.get_camera_props(self.camera)
|
|
|
|
camera_dims = self.get_camera_dimensions()
|
|
self.svg_writer = svgwriter.SvgWriter(
|
|
camera_width=camera_dims[0],
|
|
camera_height=camera_dims[1],
|
|
camera=self.camera,
|
|
camera_projection=(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))),
|
|
scale=self.scale,
|
|
human_scale=self.human_scale,
|
|
)
|
|
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
|
|
|
|
underlay_svg = None
|
|
linework_svg = None
|
|
annotation_svg = None
|
|
|
|
with profile("Generate underlay"):
|
|
if ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
|
|
drawing_style = self.cprops.get_active_drawing_style()
|
|
if not drawing_style:
|
|
self.report(
|
|
{"ERROR"},
|
|
f"Failed to create drawing '{self.drawing.Name}' - drawing has underlay but there's no active drawing underlay style.",
|
|
)
|
|
return {"FINISHED"}
|
|
underlay_svg = self.generate_underlay(context)
|
|
|
|
with profile("Generate linework"):
|
|
if tool.Drawing.is_camera_orthographic():
|
|
if self.cprops.linework_mode == "OPENCASCADE":
|
|
linework_svg = self.generate_linework(context)
|
|
elif self.cprops.linework_mode == "FREESTYLE":
|
|
linework_svg = self.generate_freestyle_linework(context)
|
|
elif self.cprops.linework_mode == "FREESTYLE":
|
|
linework_svg = self.generate_freestyle_linework(context)
|
|
|
|
with profile("Generate annotation"):
|
|
if tool.Drawing.is_camera_orthographic():
|
|
annotation_svg = self.generate_annotation(context)
|
|
|
|
with profile("Combine SVG layers"):
|
|
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
|
|
|
if self.open_viewer:
|
|
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(self.drawing))
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, drawing_uri)
|
|
if not self.open_viewer:
|
|
self.report({"INFO"}, f"{len(drawings_to_print)} drawings created...")
|
|
if self.print_all:
|
|
assert original_drawing_id is not None
|
|
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, should_view_from_camera=False)
|
|
return {"FINISHED"}
|
|
|
|
def get_camera_dimensions(self) -> tuple[float, float]:
|
|
assert bpy.context.scene
|
|
render = bpy.context.scene.render
|
|
assert isinstance(self.camera.data, bpy.types.Camera)
|
|
if self.is_landscape(render):
|
|
width = self.camera.data.ortho_scale
|
|
height = width / render.resolution_x * render.resolution_y
|
|
else:
|
|
height = self.camera.data.ortho_scale
|
|
width = height / render.resolution_y * render.resolution_x
|
|
return width, height
|
|
|
|
def combine_svgs(
|
|
self, context: bpy.types.Context, underlay: Optional[str], linework: Optional[str], annotation: Optional[str]
|
|
) -> str:
|
|
# Hacky :)
|
|
svg_path = self.get_svg_path()
|
|
with open(svg_path, "w") as outfile:
|
|
self.svg_writer.create_blank_svg(svg_path).define_boilerplate()
|
|
boilerplate = self.svg_writer.svg.tostring()
|
|
outfile.write(boilerplate.replace("</svg>", ""))
|
|
if underlay:
|
|
with open(underlay) as infile:
|
|
for i, line in enumerate(infile):
|
|
if i < 2:
|
|
continue
|
|
elif "</svg>" in line:
|
|
continue
|
|
outfile.write(line)
|
|
shutil.copyfile(os.path.splitext(underlay)[0] + ".png", os.path.splitext(svg_path)[0] + "-underlay.png")
|
|
if linework:
|
|
with open(linework) as infile:
|
|
should_skip = False
|
|
for i, line in enumerate(infile):
|
|
if i == 0:
|
|
continue
|
|
if "</svg>" in line:
|
|
continue
|
|
elif "<defs>" in line:
|
|
should_skip = True
|
|
continue
|
|
elif "</defs>" in line:
|
|
should_skip = False
|
|
continue
|
|
elif should_skip:
|
|
continue
|
|
outfile.write(line)
|
|
if annotation:
|
|
with open(annotation) as infile:
|
|
for i, line in enumerate(infile):
|
|
if i < 2:
|
|
continue
|
|
if "</svg>" in line:
|
|
continue
|
|
outfile.write(line)
|
|
outfile.write("</svg>")
|
|
return svg_path
|
|
|
|
def generate_underlay(self, context: bpy.types.Context) -> Union[str, None]:
|
|
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
|
|
return
|
|
svg_path = self.get_svg_path(cache_type="underlay")
|
|
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
|
|
return svg_path
|
|
|
|
assert context.scene and context.view_layer and context.screen
|
|
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
|
|
assert (drawing_style := self.cprops.get_active_drawing_style())
|
|
|
|
tool.Blender.sync_render_visibility()
|
|
|
|
if drawing_style.render_type == "DEFAULT":
|
|
bpy.ops.render.render(write_still=True)
|
|
else:
|
|
previous_visibility: dict[str, bool] = {}
|
|
collection = tool.Blender.get_object_bim_props(self.camera).collection
|
|
assert collection
|
|
# Hide annotations.
|
|
for obj in collection.objects:
|
|
if context.view_layer.objects.get(obj.name):
|
|
previous_visibility[obj.name] = obj.hide_get()
|
|
obj.hide_set(True)
|
|
|
|
# Hide objects that shouldn't appear on render - empties, camera, grids, etc.
|
|
for obj in context.visible_objects:
|
|
if (
|
|
(not obj.data and not obj.instance_collection)
|
|
or isinstance(obj.data, bpy.types.Camera)
|
|
or "IfcGrid/" in obj.name
|
|
or "IfcGridAxis/" in obj.name
|
|
or "IfcOpeningElement/" in obj.name
|
|
):
|
|
if context.view_layer.objects.get(obj.name):
|
|
previous_visibility[obj.name] = obj.hide_get()
|
|
obj.hide_set(True)
|
|
|
|
assert (space := tool.Blender.get_view3d_space())
|
|
previous_shading = space.shading.type
|
|
previous_format = context.scene.render.image_settings.file_format
|
|
space.shading.type = "RENDERED"
|
|
context.scene.render.image_settings.file_format = "PNG"
|
|
|
|
with tool.Blender.bonsai_crash_txt("render.opengl"):
|
|
bpy.ops.render.opengl(write_still=True)
|
|
|
|
space.shading.type = previous_shading
|
|
context.scene.render.image_settings.file_format = previous_format
|
|
|
|
for name, value in previous_visibility.items():
|
|
bpy.data.objects[name].hide_set(value)
|
|
|
|
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
|
|
return svg_path
|
|
|
|
def get_linework_contexts(self, ifc: ifcopenshell.file, target_view: str) -> LineworkContexts:
|
|
plan_body_target_contexts: list[int] = []
|
|
plan_body_model_contexts: list[int] = []
|
|
model_body_target_contexts: list[int] = []
|
|
model_body_model_contexts: list[int] = []
|
|
|
|
plan_annotation_target_contexts: list[int] = []
|
|
plan_annotation_model_contexts: list[int] = []
|
|
model_annotation_target_contexts: list[int] = []
|
|
model_annotation_model_contexts: list[int] = []
|
|
|
|
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
|
|
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
|
|
if rep_context.ContextType == "Plan":
|
|
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
|
|
if rep_context.TargetView == target_view:
|
|
plan_body_target_contexts.append(rep_context.id())
|
|
elif rep_context.TargetView == "MODEL_VIEW":
|
|
plan_body_model_contexts.append(rep_context.id())
|
|
elif rep_context.ContextIdentifier == "Annotation":
|
|
if rep_context.TargetView == target_view:
|
|
plan_annotation_target_contexts.append(rep_context.id())
|
|
elif rep_context.TargetView == "MODEL_VIEW":
|
|
plan_annotation_model_contexts.append(rep_context.id())
|
|
elif rep_context.ContextType == "Model":
|
|
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
|
|
if rep_context.TargetView == target_view:
|
|
model_body_target_contexts.append(rep_context.id())
|
|
elif rep_context.TargetView == "MODEL_VIEW":
|
|
model_body_model_contexts.append(rep_context.id())
|
|
elif rep_context.ContextIdentifier == "Annotation":
|
|
if rep_context.TargetView == target_view:
|
|
model_annotation_target_contexts.append(rep_context.id())
|
|
elif rep_context.TargetView == "MODEL_VIEW":
|
|
model_annotation_model_contexts.append(rep_context.id())
|
|
elif rep_context.ContextType == "Model":
|
|
# You should never purely assign to a "Model" context, but
|
|
# if you do, this is what we assume your intention is.
|
|
model_body_model_contexts.append(rep_context.id())
|
|
continue
|
|
|
|
body_contexts = (
|
|
[
|
|
plan_body_target_contexts,
|
|
plan_body_model_contexts,
|
|
model_body_target_contexts,
|
|
model_body_model_contexts,
|
|
]
|
|
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
|
|
else [
|
|
model_body_target_contexts,
|
|
model_body_model_contexts,
|
|
]
|
|
)
|
|
|
|
annotation_contexts = (
|
|
[
|
|
plan_annotation_target_contexts,
|
|
plan_annotation_model_contexts,
|
|
model_annotation_target_contexts,
|
|
model_annotation_model_contexts,
|
|
]
|
|
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
|
|
else [
|
|
model_annotation_target_contexts,
|
|
model_annotation_model_contexts,
|
|
]
|
|
)
|
|
|
|
return LineworkContexts(body_contexts, annotation_contexts)
|
|
|
|
def serialize_contexts_elements(
|
|
self,
|
|
ifc: ifcopenshell.file,
|
|
tree: ifcopenshell.geom.tree,
|
|
contexts: LineworkContexts,
|
|
context_type: Literal["body", "annotation"],
|
|
drawing_elements: set[ifcopenshell.entity_instance],
|
|
target_view: str,
|
|
) -> None:
|
|
drawing_elements = drawing_elements.copy()
|
|
contexts_: list[list[int]] = getattr(contexts, context_type)
|
|
for context in contexts_:
|
|
with profile(f"Processing {context_type} context"):
|
|
if not context or not drawing_elements:
|
|
continue
|
|
geom_settings = ifcopenshell.geom.settings()
|
|
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
|
|
|
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
|
|
# A 2mm Z offset to combat Z-fighting in plan or RCPs
|
|
geom_settings.set("model-offset", (0.0, 0.0, 0.002 if target_view == "PLAN_VIEW" else -0.002))
|
|
|
|
geom_settings.set("context-ids", context)
|
|
it = ifcopenshell.geom.iterator(
|
|
geom_settings, ifc, multiprocessing.cpu_count(), include=drawing_elements
|
|
)
|
|
processed = set()
|
|
for elem in it:
|
|
processed.add(ifc.by_id(elem.id))
|
|
self.serialiser.write(elem)
|
|
tree.add_element(elem)
|
|
drawing_elements -= processed
|
|
|
|
def generate_bisect_linework(self, context: bpy.types.Context, root) -> None:
|
|
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
|
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
raw_width, raw_height = self.get_camera_dimensions()
|
|
x_offset = raw_width / 2
|
|
y_offset = raw_height / 2
|
|
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
|
|
|
|
for obj in context.visible_objects:
|
|
if obj.type != "MESH":
|
|
continue
|
|
if not (element := tool.Ifc.get_entity(obj)):
|
|
continue
|
|
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
|
continue
|
|
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
|
|
|
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
|
|
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
|
|
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
|
|
|
|
lines = []
|
|
for edge in edges:
|
|
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
|
|
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
|
|
coords = [start, end]
|
|
d = " ".join(
|
|
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
|
|
)
|
|
d = "M{}".format(d[1:])
|
|
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
|
|
path.attrib["d"] = d
|
|
group.append(g)
|
|
|
|
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
|
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
|
if "projection" in el.get("class", "").split():
|
|
continue
|
|
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
|
if not (obj := tool.Ifc.get_object(element)):
|
|
continue
|
|
if not (material := ifcopenshell.util.element.get_material(element)):
|
|
continue
|
|
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
|
|
continue
|
|
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
|
|
if material.is_a("IfcMaterialLayerSetUsage"):
|
|
usage = material
|
|
layer_set = material.ForLayerSet
|
|
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
|
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
|
elif material.is_a("IfcMaterialLayerSet"):
|
|
usage = None
|
|
layer_set = material
|
|
offset = 0
|
|
sense_factor = 1
|
|
|
|
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
|
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
raw_width, raw_height = self.get_camera_dimensions()
|
|
x_offset = raw_width / 2
|
|
y_offset = raw_height / 2
|
|
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
|
|
|
|
mesh = obj.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh)
|
|
|
|
# Slice our mesh into a 2D drawing cut (2D is always easier)
|
|
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
|
|
plane_co = camera_matrix.translation
|
|
plane_no = camera_matrix.col[2].xyz
|
|
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
|
bmesh.ops.bisect_plane(
|
|
bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=True
|
|
)
|
|
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
|
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
|
|
|
|
prev_co = None
|
|
if not usage:
|
|
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
|
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
|
co = Vector((0.0, 0.0, offset))
|
|
elif usage.LayerSetDirection == "AXIS2":
|
|
co = Vector((0.0, offset, 0.0))
|
|
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
|
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
|
elif usage.LayerSetDirection == "AXIS3":
|
|
co = Vector((0.0, 0.0, offset))
|
|
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
|
no = Vector([0.0, 0.0, 1.0])
|
|
elif usage.LayerSetDirection == "AXIS1":
|
|
co = Vector((0.0, 0.0, offset))
|
|
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
|
no = Vector([1.0, 0.0, 0.0])
|
|
no *= sense_factor
|
|
last_i = len(layer_set.MaterialLayers) - 1
|
|
for i, layer in enumerate(layer_set.MaterialLayers):
|
|
prev_co = co.copy()
|
|
co += no * layer.LayerThickness * self.unit_scale
|
|
|
|
bm_fill = bm.copy()
|
|
if i != last_i:
|
|
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
|
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
|
|
if i != 0:
|
|
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
|
bmesh.ops.bisect_plane(
|
|
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
|
|
)
|
|
|
|
bm_fill.verts.ensure_lookup_table()
|
|
bm_fill.edges.ensure_lookup_table()
|
|
verts = [tuple(obj.matrix_world @ v.co) for v in bm_fill.verts]
|
|
edges = [[v.index for v in e.verts] for e in bm_fill.edges]
|
|
|
|
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
|
|
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
|
|
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
|
|
g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id())
|
|
|
|
lines = []
|
|
for edge in edges:
|
|
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
|
|
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
|
|
coords = [start, end]
|
|
d = " ".join(
|
|
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
|
|
)
|
|
d = "M{}".format(d[1:])
|
|
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
|
|
path.attrib["d"] = d
|
|
group.append(g)
|
|
|
|
bm.free()
|
|
|
|
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
|
|
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
|
return
|
|
svg_path = self.get_svg_path(cache_type="linework")
|
|
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
|
return svg_path
|
|
|
|
context.scene.render.use_freestyle = True
|
|
context.scene.svg_export.use_svg_export = True
|
|
|
|
linesets = context.view_layer.freestyle_settings.linesets
|
|
if len(linesets) == 1 and linesets[0].name == "LineSet":
|
|
context.view_layer.freestyle_settings.crease_angle = radians(140)
|
|
context.view_layer.freestyle_settings.use_culling = True
|
|
lineset = linesets[0]
|
|
lineset.edge_type_negation = "EXCLUSIVE"
|
|
lineset.select_silhouette = False
|
|
lineset.select_crease = False
|
|
lineset.select_border = False
|
|
lineset.select_edge_mark = False
|
|
lineset.select_contour = False
|
|
lineset.select_external_contour = False
|
|
lineset.select_material_boundary = False
|
|
lineset.select_suggestive_contour = True
|
|
lineset.select_ridge_valley = True
|
|
|
|
edge_mesh = bpy.data.meshes.new("Temp Merged Edges")
|
|
edge_obj = bpy.data.objects.new("Temp Merged Edges", edge_mesh)
|
|
context.scene.collection.objects.link(edge_obj)
|
|
edge_bm = bmesh.new()
|
|
|
|
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
|
|
for obj in bpy.context.view_layer.objects:
|
|
is_visible = obj.name in visible_object_names
|
|
obj.hide_render = not is_visible
|
|
if (
|
|
is_visible
|
|
and obj.type == "MESH"
|
|
and len(obj.data.edges)
|
|
and not len(obj.data.polygons)
|
|
and not obj.name.startswith("IfcAnnotation")
|
|
):
|
|
tmp_mesh = None
|
|
try:
|
|
tmp_mesh = obj.data.copy()
|
|
tmp_mesh.transform(obj.matrix_world)
|
|
edge_bm.from_mesh(tmp_mesh)
|
|
finally:
|
|
if tmp_mesh:
|
|
bpy.data.meshes.remove(tmp_mesh)
|
|
|
|
ret = bmesh.ops.extrude_edge_only(edge_bm, edges=edge_bm.edges)
|
|
verts_extruded = [e for e in ret["geom"] if isinstance(e, bmesh.types.BMVert)]
|
|
|
|
cam_z = self.camera.matrix_world.to_3x3() @ self.camera.data.view_frame(scene=None)[-1].normalized()
|
|
cam_z *= 0.001
|
|
|
|
for v in verts_extruded:
|
|
v.co += cam_z
|
|
|
|
edge_bm.to_mesh(edge_mesh)
|
|
edge_bm.free()
|
|
|
|
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
|
|
context.scene.render.filepath = svg_path[0:-4]
|
|
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
|
|
bpy.ops.render.render(write_still=False)
|
|
|
|
os.replace(actual_path, svg_path)
|
|
|
|
bpy.data.objects.remove(edge_obj)
|
|
bpy.data.meshes.remove(edge_mesh)
|
|
|
|
context.scene.render.use_freestyle = False
|
|
context.scene.svg_export.use_svg_export = False
|
|
|
|
tree = etree.parse(svg_path)
|
|
root = tree.getroot()
|
|
|
|
freestyle_width = float(root.attrib["width"])
|
|
freestyle_height = float(root.attrib["height"])
|
|
svg_width = self.svg_writer.width
|
|
svg_height = self.svg_writer.height
|
|
|
|
group = root.find(".//{http://www.w3.org/2000/svg}g")
|
|
group.attrib["class"] = "projection"
|
|
|
|
# Resize Freestyle to our proper width / height and purge all other attributes
|
|
for path in root.findall(".//{http://www.w3.org/2000/svg}path"):
|
|
for key in path.attrib:
|
|
if key == "fill":
|
|
continue
|
|
elif key != "d":
|
|
del path.attrib[key]
|
|
continue
|
|
d = path.attrib[key]
|
|
coords = d.strip().split()[1:]
|
|
new_d = "M"
|
|
for i in range(0, len(coords), 2):
|
|
x = float(coords[i][:-1])
|
|
y = float(coords[i + 1])
|
|
x = x / freestyle_width * svg_width
|
|
y = y / freestyle_height * svg_height
|
|
new_d += f" {x},{y}"
|
|
path.attrib["d"] = new_d
|
|
pass
|
|
|
|
if tool.Drawing.is_camera_orthographic():
|
|
self.generate_bisect_linework(context, root)
|
|
if self.cprops.generate_material_layers:
|
|
self.generate_material_layers(context, root)
|
|
self.merge_linework_and_add_metadata(root)
|
|
self.move_elements_to_top(root)
|
|
|
|
with open(svg_path, "wb") as svg:
|
|
svg.write(etree.tostring(root))
|
|
|
|
return svg_path
|
|
|
|
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
|
|
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
|
return
|
|
svg_path = self.get_svg_path(cache_type="linework")
|
|
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
|
return svg_path
|
|
|
|
# in case of printing multiple drawings we need to sync just once
|
|
if self.sync and self.drawing_index == 0:
|
|
with profile("sync"):
|
|
# All very hackish whilst prototyping
|
|
exporter = bonsai.bim.export_ifc.IfcExporter(None)
|
|
exporter.file = tool.Ifc.get()
|
|
invalidated_elements = exporter.sync_all_objects()
|
|
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
|
|
if cache := IfcStore.get_cache():
|
|
[cache.remove(guid) for guid in invalidated_guids]
|
|
|
|
# If we have already calculated it in the SVG in the past, don't recalculate
|
|
edited_guids = set()
|
|
for obj in IfcStore.edited_objs:
|
|
element = tool.Ifc.get_entity(obj)
|
|
edited_guids.add(element.GlobalId) if hasattr(element, "GlobalId") else None
|
|
cached_linework = set()
|
|
if os.path.isfile(svg_path):
|
|
tree = etree.parse(svg_path)
|
|
root = tree.getroot()
|
|
cached_linework = {
|
|
el.get("{http://www.ifcopenshell.org/ns}guid")
|
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]")
|
|
}
|
|
cached_linework -= edited_guids
|
|
|
|
bim_props = tool.Blender.get_bim_props()
|
|
prefs = tool.Blender.get_addon_preferences()
|
|
files = {bim_props.ifc_file: tool.Ifc.get()}
|
|
|
|
props = tool.Project.get_project_props()
|
|
for link in props.get_loaded_links_for_drawings():
|
|
files[link.name] = self.get_linked_file(link)
|
|
|
|
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
|
self.setup_serialiser(target_view)
|
|
|
|
tree = ifcopenshell.geom.tree()
|
|
tree.enable_face_styles(True)
|
|
|
|
for ifc_path, ifc in files.items():
|
|
# Don't use draw.main() just whilst we're prototyping and experimenting
|
|
# TODO: hash paths are never used
|
|
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
|
ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
|
|
|
|
self.serialiser.setFile(ifc)
|
|
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
|
|
|
|
# Get all representation contexts to see what we're dealing with.
|
|
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
|
|
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
|
|
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
|
|
contexts = self.get_linework_contexts(ifc, target_view)
|
|
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
|
|
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
|
|
|
|
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
|
|
with profile("Camera element"):
|
|
# The camera must always be included, regardless of any include/exclude filters.
|
|
geom_settings = ifcopenshell.geom.settings()
|
|
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
|
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
|
|
for elem in it:
|
|
self.serialiser.write(elem)
|
|
|
|
with profile("Finalizing"):
|
|
self.serialiser.finalize()
|
|
results = self.svg_buffer.get_value()
|
|
|
|
root = etree.fromstring(results)
|
|
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
if group is None:
|
|
with open(svg_path, "wb") as svg:
|
|
svg.write(etree.tostring(root))
|
|
|
|
return svg_path
|
|
|
|
if self.cprops.cut_mode == "BISECT":
|
|
self.remove_cut_linework(root)
|
|
self.generate_bisect_linework(context, root)
|
|
if self.cprops.generate_material_layers:
|
|
self.generate_material_layers(context, root)
|
|
self.merge_linework_and_add_metadata(root)
|
|
self.move_elements_to_top(root)
|
|
elif self.cprops.cut_mode == "OPENCASCADE":
|
|
self.move_projection_to_bottom(root)
|
|
if self.cprops.generate_material_layers:
|
|
self.generate_material_layers(context, root)
|
|
self.merge_linework_and_add_metadata(root)
|
|
self.move_elements_to_top(root)
|
|
|
|
if self.cprops.fill_mode == "SHAPELY":
|
|
# shapely variant
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
|
|
raycast_objs = set()
|
|
elements_with_faces = set()
|
|
for element in drawing_elements.copy():
|
|
if element.is_a("IfcAnnotation"):
|
|
continue
|
|
obj = tool.Ifc.get_object(element)
|
|
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
|
elements_with_faces.add(element.GlobalId)
|
|
raycast_objs.add(obj)
|
|
|
|
projections = root.xpath(
|
|
".//svg:g[contains(@class, 'projection')]", namespaces={"svg": "http://www.w3.org/2000/svg"}
|
|
)
|
|
|
|
boundary_lines = []
|
|
for projection in projections:
|
|
global_id = projection.attrib["{http://www.ifcopenshell.org/ns}guid"]
|
|
if global_id not in elements_with_faces:
|
|
continue
|
|
for path in projection.findall("./{http://www.w3.org/2000/svg}path"):
|
|
start, end = [[float(o) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
|
|
if start == end:
|
|
continue
|
|
# Extension by 0.5mm is necessary to ensure lines overlap with other diagonal lines
|
|
start, end = tool.Drawing.extend_line(start, end, 0.5)
|
|
boundary_lines.append(shapely.LineString([start, end]))
|
|
|
|
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
|
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
|
|
|
for polygon in closed_polygons.geoms:
|
|
# Less than 1mm2 is not worth styling on sheet
|
|
if polygon.area < 1:
|
|
continue
|
|
centroid = polygon.centroid
|
|
centroid = centroid if polygon.contains(centroid) else polygon.representative_point()
|
|
if centroid:
|
|
centroid3d = self.drawing_to_model_co(centroid.x, centroid.y)
|
|
inside_elements = [
|
|
e for e in tree.select(self.pythonize(centroid3d)) if not e.is_a("IfcAnnotation")
|
|
]
|
|
if not inside_elements:
|
|
camera_dir = self.camera.matrix_world.col[2].to_3d() * -1
|
|
# We previously used tree.select_ray, but raycasting in Blender is 100x faster.
|
|
raycast_results = self.cast_rays_and_get_best_object(raycast_objs, centroid3d, camera_dir)
|
|
raycast_element = None
|
|
if raycast_obj := raycast_results[0]:
|
|
raycast_element = tool.Ifc.get_entity(raycast_obj)
|
|
|
|
if raycast_element:
|
|
path = etree.Element("path")
|
|
d = (
|
|
"M"
|
|
+ " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]])
|
|
+ " Z"
|
|
)
|
|
for interior in polygon.interiors:
|
|
d += (
|
|
" M"
|
|
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
|
|
+ " Z"
|
|
)
|
|
path.attrib["d"] = d
|
|
classes = self.get_svg_classes(raycast_element)
|
|
classes.append("surface")
|
|
path.set("class", " ".join(list(classes)))
|
|
group.insert(0, path)
|
|
|
|
if self.cprops.fill_mode == "SVGFILL":
|
|
results = etree.tostring(root).decode("utf8")
|
|
svg_data_1 = results
|
|
from xml.dom.minidom import parseString
|
|
|
|
def yield_groups(n):
|
|
if n.nodeType == n.ELEMENT_NODE and n.tagName == "g":
|
|
yield n
|
|
for c in n.childNodes:
|
|
yield from yield_groups(c)
|
|
|
|
dom1 = parseString(svg_data_1)
|
|
svg1 = dom1.childNodes[0]
|
|
groups1 = [g for g in yield_groups(svg1) if "projection" in g.getAttribute("class")]
|
|
|
|
ls_groups = ifcopenshell.ifcopenshell_wrapper.svg_to_line_segments(results, "projection")
|
|
|
|
for i, (ls, g1) in enumerate(zip(ls_groups, groups1)):
|
|
projection, g1 = g1, g1.parentNode
|
|
|
|
svgfill_context = ifcopenshell.ifcopenshell_wrapper.context(
|
|
ifcopenshell.ifcopenshell_wrapper.EXACT_CONSTRUCTIONS, 1.0e-3
|
|
)
|
|
|
|
# EXACT_CONSTRUCTIONS is significantly faster than FILTERED_CARTESIAN_QUOTIENT
|
|
# remove duplicates (without tolerance)
|
|
ls = [l for l in map(tuple, set(map(frozenset, ls))) if len(l) == 2 and l[0] != l[1]]
|
|
svgfill_context.add(ls)
|
|
|
|
num_passes = 0
|
|
|
|
for iteration in range(num_passes + 1):
|
|
# initialize empty group, note that in the current approach only one
|
|
# group is stored
|
|
ps = ifcopenshell.ifcopenshell_wrapper.svg_groups_of_polygons()
|
|
if iteration != 0 or svgfill_context.build():
|
|
svgfill_context.write(ps)
|
|
|
|
if iteration != num_passes:
|
|
pairs = svgfill_context.get_face_pairs()
|
|
semantics = [None] * (max(pairs) + 1)
|
|
|
|
# Reserialize cells into an SVG string
|
|
svg_data_2 = ifcopenshell.ifcopenshell_wrapper.polygons_to_svg(ps, True)
|
|
|
|
# We parse both SVG files to create on document with the combination of sections from
|
|
# the output directly from the serializer and the cells found from the hidden line
|
|
# rendering
|
|
dom2 = parseString(svg_data_2)
|
|
svg2 = dom2.childNodes[0]
|
|
# file 2 only has the groups we are interested in.
|
|
# in fact in the approach, it's only a single group
|
|
|
|
g2 = next(yield_groups(svg2))
|
|
|
|
# Loop over the cell paths
|
|
for pi, p in enumerate(g2.getElementsByTagName("path")):
|
|
d = p.getAttribute("d")
|
|
coords = [co[1:].split(",") for co in d.split() if co[1:]]
|
|
polygon = shapely.Polygon(coords)
|
|
# 1mm2 polygons aren't worth styling in paperspace. Raycasting is expensive!
|
|
if polygon.area < 1:
|
|
continue
|
|
# point inside is an attribute that comes from line_segments_to_polygons() polygons_to_svg?
|
|
# it is an arbitrary point guaranteed to be inside the polygon and outside
|
|
# of any potential inner bounds. We can use this to construct a ray to find
|
|
# the face of the IFC element that the cell belongs to.
|
|
assert p.hasAttribute("ifc:pointInside")
|
|
|
|
xy = list(map(float, p.getAttribute("ifc:pointInside").split(",")))
|
|
|
|
centroid3d = self.drawing_to_model_co(*xy)
|
|
|
|
inside_elements = [
|
|
e for e in tree.select(self.pythonize(centroid3d)) if not e.is_a("IfcAnnotation")
|
|
]
|
|
if inside_elements:
|
|
elements = None
|
|
if iteration != num_passes:
|
|
semantics[pi] = (inside_elements[0], -1)
|
|
else:
|
|
camera_dir = self.camera.matrix_world.col[2].to_3d() * -1
|
|
elements = [
|
|
e
|
|
for e in tree.select_ray(self.pythonize(centroid3d), self.pythonize(camera_dir))
|
|
if not e.instance.is_a("IfcAnnotation")
|
|
]
|
|
|
|
if elements:
|
|
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
|
|
classes.append("projection")
|
|
classes.append("surface")
|
|
|
|
if iteration != num_passes:
|
|
semantics[pi] = elements[0]
|
|
else:
|
|
classes = ["projection"]
|
|
|
|
p.setAttribute("style", "foo")
|
|
p.setAttribute("class", " ".join(classes))
|
|
|
|
if iteration != num_passes:
|
|
to_remove = []
|
|
|
|
for he_idx in range(0, len(pairs), 2):
|
|
# @todo instead of ray_distance, better do (x.point - y.point).dot(x.normal)
|
|
# to see if they're coplanar, because ray-distance will be different in case
|
|
# of element surfaces non-orthogonal to the view direction
|
|
|
|
def format(x):
|
|
if x is None:
|
|
return None
|
|
elif isinstance(x, tuple):
|
|
# found to be inside element using tree.select() no face or style info
|
|
return x
|
|
else:
|
|
return (x.instance.is_a(), x.ray_distance, tuple(x.position))
|
|
|
|
pp = pairs[he_idx : he_idx + 2]
|
|
if pp == (-1, -1):
|
|
continue
|
|
data = list(map(format, map(semantics.__getitem__, pp)))
|
|
if None not in data and data[0][0] == data[1][0] and abs(data[0][1] - data[1][1]) < 1.0e-5:
|
|
to_remove.append(he_idx // 2)
|
|
# Print edge index and semantic data
|
|
# print(he_idx // 2, *data)
|
|
|
|
svgfill_context.merge(to_remove)
|
|
|
|
# Swap the XML nodes from the files
|
|
# Remove the original hidden line node we still have in the serializer output
|
|
g1.removeChild(projection)
|
|
g2.setAttribute("class", "projection")
|
|
# Find the children of the projection node parent
|
|
children = [x for x in g1.childNodes if x.nodeType == x.ELEMENT_NODE]
|
|
if children:
|
|
# Insert the new semantically enriched cell-based projection node
|
|
# *before* the node with sections from the serializer. SVG derives
|
|
# draw order from node order in the DOM so sections are draw over
|
|
# the projections.
|
|
g1.insertBefore(g2, children[0])
|
|
else:
|
|
# This generally shouldn't happen
|
|
g1.appendChild(g2)
|
|
|
|
results = dom1.toxml()
|
|
results = results.encode("ascii", "xmlcharrefreplace")
|
|
root = etree.fromstring(results)
|
|
|
|
# Spaces are handled as a special case, since they are often overlayed
|
|
# in addition to elements. For example, a space should not obscure
|
|
# other elements in projection. Spaces should also not override cut
|
|
# elements but instead be drawn in addition to cut elements.
|
|
spaces = tool.Drawing.get_drawing_spaces(self.camera_element)
|
|
|
|
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
|
|
|
x_offset = self.svg_writer.raw_width / 2
|
|
y_offset = self.svg_writer.raw_height / 2
|
|
|
|
for space in spaces:
|
|
obj = tool.Ifc.get_object(space)
|
|
if not obj or not tool.Drawing.is_intersecting_camera(obj, self.camera):
|
|
continue
|
|
verts, edges = tool.Drawing.bisect_mesh(obj, self.camera)
|
|
verts = [self.svg_writer.project_point_onto_camera(Vector(v)) for v in verts]
|
|
line_strings = [
|
|
shapely.LineString(
|
|
[
|
|
(
|
|
(x_offset + verts[e[0]][0]) * self.svg_writer.svg_scale,
|
|
(y_offset - verts[e[0]][1]) * self.svg_writer.svg_scale,
|
|
),
|
|
(
|
|
(x_offset + verts[e[1]][0]) * self.svg_writer.svg_scale,
|
|
(y_offset - verts[e[1]][1]) * self.svg_writer.svg_scale,
|
|
),
|
|
]
|
|
)
|
|
for e in edges
|
|
]
|
|
closed_polygons = shapely.polygonize(line_strings)
|
|
for polygon in closed_polygons.geoms:
|
|
classes = self.get_svg_classes(space)
|
|
path = etree.Element("path")
|
|
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
|
for interior in polygon.interiors:
|
|
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
|
path.attrib["d"] = d
|
|
path.set("class", " ".join(list(classes)))
|
|
group.append(path)
|
|
|
|
with open(svg_path, "wb") as svg:
|
|
svg.write(etree.tostring(root))
|
|
|
|
return svg_path
|
|
|
|
def setup_serialiser(self, target_view):
|
|
self.svg_settings = ifcopenshell.geom.settings()
|
|
self.svg_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
self.svg_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
|
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
|
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
|
self.serialiser = ifcopenshell.geom.serializers.svg(
|
|
self.svg_buffer, self.svg_settings, self.serialiser_settings
|
|
)
|
|
self.serialiser.setWithoutStoreys(True)
|
|
self.serialiser.setPolygonal(True)
|
|
self.serialiser.setUseHlrPoly(True)
|
|
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
|
|
self.serialiser.setProfileThreshold(10000)
|
|
self.serialiser.setUseNamespace(True)
|
|
self.serialiser.setAlwaysProject(True)
|
|
self.serialiser.setAutoElevation(False)
|
|
self.serialiser.setAutoSection(False)
|
|
self.serialiser.setPrintSpaceNames(False)
|
|
self.serialiser.setPrintSpaceAreas(False)
|
|
self.serialiser.setDrawDoorArcs(False)
|
|
self.serialiser.setNoCSS(True)
|
|
self.serialiser.setElevationRefGuid(self.camera_element.GlobalId)
|
|
self.serialiser.setScale(self.scale)
|
|
self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS)
|
|
self.serialiser.setUsePrefiltering(True) # See #3359
|
|
self.serialiser.setUnifyInputs(True)
|
|
self.serialiser.setSegmentProjection(True)
|
|
if target_view == "REFLECTED_PLAN_VIEW":
|
|
self.serialiser.setMirrorY(True)
|
|
# tree = ifcopenshell.geom.tree()
|
|
# This instructs the tree to explode BReps into faces and return
|
|
# the style of the face when running tree.select_ray()
|
|
# tree.enable_face_styles(True)
|
|
|
|
def get_svg_classes(self, element, layer=None):
|
|
classes = [element.is_a()]
|
|
|
|
# ─── Material ──────────────────────────────────────────────
|
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
material_name = ""
|
|
if material:
|
|
if material.is_a("IfcMaterialLayerSet"):
|
|
material_name = material.LayerSetName or "null"
|
|
else:
|
|
material_name = getattr(material, "Name", "null") or "null"
|
|
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
|
classes.append(f"material-{material_name}")
|
|
else:
|
|
classes.append("material-null")
|
|
|
|
# ─── Layer ─────────────────────────────────────────────────
|
|
if layer:
|
|
classes.append(layer.is_a())
|
|
layer_material = layer.Material
|
|
layer_material_name = layer_material.Name or "null"
|
|
layer_material_name = tool.Drawing.canonicalise_class_name(layer_material_name)
|
|
classes.append(f"layer-material-{layer_material_name}")
|
|
|
|
# Add material category if available
|
|
if hasattr(layer_material, "Category") and layer_material.Category:
|
|
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
|
classes.append(f"layer-material-category-{layer_material_category}")
|
|
|
|
# ─── Metadata ──────────────────────────────────────────────
|
|
for key in self.metadata:
|
|
value = ifcopenshell.util.selector.get_element_value(element, key)
|
|
if value:
|
|
classes.append(
|
|
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
|
)
|
|
|
|
# ─── Target View ───────────────────────────────────────────
|
|
if getattr(self.cprops, "target_view", None):
|
|
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
|
classes.append(f"target-view-{target_view_class}")
|
|
|
|
return classes
|
|
|
|
def is_manifold(self, obj) -> bool:
|
|
result = self.is_manifold_cache.get(obj.data.name, None)
|
|
if result is not None:
|
|
return result
|
|
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
|
for edge in bm.edges:
|
|
if not edge.is_manifold:
|
|
bm.free()
|
|
self.is_manifold_cache[obj.data.name] = False
|
|
return False
|
|
self.is_manifold_cache[obj.data.name] = True
|
|
return True
|
|
|
|
def get_linked_file(self, link: "Link") -> ifcopenshell.file:
|
|
link_path = link.name
|
|
ifc_file = IfcStore.session_files.get(link_path, None)
|
|
if ifc_file is not None:
|
|
return ifc_file
|
|
resolved_path = tool.Ifc.resolve_uri(link_path)
|
|
ifc_file = IfcStore.session_files[link_path] = ifcopenshell.open(resolved_path)
|
|
return ifc_file
|
|
|
|
def get_element_by_guid(self, guid: str) -> Union[ifcopenshell.entity_instance, None]:
|
|
try:
|
|
return tool.Ifc.get().by_guid(guid)
|
|
except RuntimeError:
|
|
props = tool.Project.get_project_props()
|
|
for link in props.get_loaded_links_for_drawings():
|
|
ifc_file = self.get_linked_file(link)
|
|
try:
|
|
return ifc_file.by_guid(guid)
|
|
except RuntimeError:
|
|
continue
|
|
|
|
def get_element_by_id(self, step_id: Any) -> Union[ifcopenshell.entity_instance, None]:
|
|
try:
|
|
step_id = int(step_id)
|
|
except:
|
|
return
|
|
try:
|
|
return tool.Ifc.get().by_id(step_id)
|
|
except RuntimeError:
|
|
props = tool.Project.get_project_props()
|
|
for link in props.get_loaded_links_for_drawings():
|
|
ifc_file = self.get_linked_file(link)
|
|
try:
|
|
return ifc_file.by_id(step_id)
|
|
except RuntimeError:
|
|
continue
|
|
|
|
def remove_cut_linework(self, root):
|
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
|
if "projection" not in el.get("class", "").split():
|
|
el.getparent().remove(el)
|
|
|
|
def merge_linework_and_add_metadata(self, root):
|
|
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
|
|
if join_criteria:
|
|
join_criteria = join_criteria.split(",")
|
|
else:
|
|
# Drawing convention states that same objects classes with the same material are merged when cut.
|
|
join_criteria = [
|
|
"class",
|
|
"material.Name",
|
|
"/Pset_.*Common/.Status",
|
|
"EPset_Status.Status",
|
|
"EPset_Status.UserDefinedStatus",
|
|
]
|
|
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
joined_paths = {}
|
|
self.is_manifold_cache = {}
|
|
|
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
|
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
|
layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id"))
|
|
|
|
if "projection" in el.get("class", "").split():
|
|
classes = self.get_svg_classes(element)
|
|
classes.append("projection")
|
|
el.set("class", " ".join(classes))
|
|
continue
|
|
else:
|
|
classes = self.get_svg_classes(element, layer)
|
|
classes.append("cut")
|
|
el.set("class", " ".join(classes))
|
|
|
|
obj = tool.Ifc.get_object(element)
|
|
|
|
if not obj: # This is a linked model object. For now, do nothing.
|
|
continue
|
|
|
|
if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in (
|
|
"IfcMaterialLayerSet",
|
|
"IfcMaterialLayerSetUsage",
|
|
):
|
|
pass # These are always manifold
|
|
elif not self.is_manifold(obj):
|
|
continue
|
|
|
|
# An element group will contain a bunch of paths representing the
|
|
# cut of that element. However IfcOpenShell may not correctly
|
|
# create closed paths. We post-process all paths with shapely to
|
|
# ensure things that should be closed (i.e.
|
|
# shapely.polygonize_full) are, and things which aren't are left
|
|
# alone (e.g. dangles, cuts, invalids). See #3421.
|
|
line_strings = []
|
|
old_paths = []
|
|
has_open_paths = False
|
|
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
|
for subpath in path.attrib["d"].split("M")[1:]:
|
|
subpath = "M" + subpath.strip()
|
|
coords = [[float(o) for o in co[1:].split(",")] for co in subpath.split()]
|
|
if coords[0] != coords[-1]:
|
|
has_open_paths = True
|
|
line_strings.append(shapely.LineString(coords))
|
|
old_paths.append(path)
|
|
|
|
results = []
|
|
if has_open_paths:
|
|
unioned_line_strings = shapely.union_all(shapely.GeometryCollection(line_strings))
|
|
if hasattr(unioned_line_strings, "geoms"):
|
|
results = shapely.polygonize_full(unioned_line_strings.geoms)
|
|
|
|
# If we succeeded in generating new path geometry, remove all the
|
|
# old paths and add new ones.
|
|
if results:
|
|
for path in old_paths:
|
|
path.getparent().remove(path)
|
|
|
|
# polygonize_full will create polygons for everything, including
|
|
# interior "holes". As a result we do two passes. The first pass
|
|
# records polygon interior rings. The second pass uses this to
|
|
# check if the exterior ring matches an interior ring. If it does,
|
|
# it's a hole. Skip it!
|
|
|
|
interior_hashes = set()
|
|
for result in results:
|
|
for geom in result.geoms:
|
|
if isinstance(geom, shapely.Polygon):
|
|
for interior in geom.interiors:
|
|
# Sorted because coordinate ordering may differ,
|
|
# and frozenset because shapely sometimes emits
|
|
# duplicate coordinates.
|
|
interior_hashes.add(hash(frozenset(sorted(interior.coords))))
|
|
elif isinstance(geom, shapely.LineString):
|
|
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
|
|
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
|
|
path.attrib["d"] = d
|
|
|
|
for result in results:
|
|
for geom in result.geoms:
|
|
if isinstance(geom, shapely.Polygon):
|
|
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
|
|
if hash(frozenset(sorted(geom.exterior.coords))) in interior_hashes:
|
|
# This is a "hole", as its exterior perfectly matches an interior.
|
|
continue
|
|
d = (
|
|
"M"
|
|
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
|
|
+ " Z"
|
|
)
|
|
for interior in geom.interiors:
|
|
d += (
|
|
" M"
|
|
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
|
|
+ " Z"
|
|
)
|
|
path.attrib["d"] = d
|
|
|
|
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
|
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
|
continue
|
|
|
|
keys = []
|
|
for query in join_criteria:
|
|
key = ifcopenshell.util.selector.get_element_value(element, query)
|
|
if isinstance(key, (list, tuple)):
|
|
keys.extend(key)
|
|
else:
|
|
keys.append(key)
|
|
|
|
if layer:
|
|
for query in join_criteria:
|
|
key = ifcopenshell.util.selector.get_element_value(layer, query)
|
|
if isinstance(key, (list, tuple)):
|
|
keys.extend(key)
|
|
else:
|
|
keys.append(key)
|
|
|
|
hash_keys = hash(tuple(keys))
|
|
|
|
if el.findall("{http://www.w3.org/2000/svg}path"):
|
|
joined_paths.setdefault(hash_keys, []).append(el)
|
|
|
|
for key, els in joined_paths.items():
|
|
queue = []
|
|
|
|
for el in els:
|
|
classes = set(el.attrib["class"].split())
|
|
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
|
is_closed_polygon = False
|
|
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
|
# Temporary hack.
|
|
if "d" not in path.attrib:
|
|
continue
|
|
for subpath in path.attrib["d"].split("M")[1:]:
|
|
subpath_co = "M" + subpath.strip(" Z")
|
|
# Round due to inaccuracies from Blender meshes and bisection
|
|
coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()]
|
|
if subpath.strip().lower().endswith("z"):
|
|
coords.append(coords[0])
|
|
if len(coords) > 2 and coords[0] == coords[-1]:
|
|
is_closed_polygon = True
|
|
queue.append((shapely.Polygon(coords), classes))
|
|
if is_closed_polygon:
|
|
el.getparent().remove(el)
|
|
|
|
while queue:
|
|
polygon, polygon_classes = queue.pop()
|
|
for polygon2, polygon2_classes in queue[:]:
|
|
try:
|
|
merged_polygon = shapely.union(polygon, polygon2)
|
|
except:
|
|
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
|
|
continue
|
|
if type(merged_polygon) == shapely.Polygon:
|
|
polygon = merged_polygon
|
|
polygon_classes.update(polygon2_classes)
|
|
queue.remove((polygon2, polygon2_classes))
|
|
|
|
g = etree.Element("g")
|
|
path = etree.SubElement(g, "path")
|
|
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
|
for interior in polygon.interiors:
|
|
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
|
path.attrib["d"] = d
|
|
g.set("class", " ".join(list(polygon_classes)))
|
|
group.append(g)
|
|
|
|
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
|
camera_xy = np.array((x, -y)) / self.scale / 1000
|
|
camera_xy += np.array((self.cprops.width / -2, self.cprops.height / 2)) # top left offset
|
|
return self.camera.matrix_world @ Vector(camera_xy).to_3d()
|
|
|
|
def pythonize(self, arr):
|
|
return tuple(map(float, arr))
|
|
|
|
def cast_rays_and_get_best_object(
|
|
self, objs_to_raycast: list[bpy.types.Object], ray_origin, ray_direction
|
|
) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
|
|
# This could be optimised even further with 2D box culling
|
|
best_length_squared = 1.0
|
|
best_obj = None
|
|
best_hit = None
|
|
best_face_index = None
|
|
|
|
for obj in objs_to_raycast:
|
|
matrix_inv = obj.matrix_world.inverted()
|
|
ray_origin_obj = matrix_inv @ ray_origin
|
|
ray_direction_obj = ray_direction.to_4d()
|
|
ray_direction_obj[3] = 0.0
|
|
ray_direction_obj = (matrix_inv @ ray_direction_obj).to_3d()
|
|
|
|
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
|
|
|
|
if success:
|
|
hit = obj.matrix_world @ location
|
|
length_squared = (hit - ray_origin).length_squared
|
|
if best_obj is None or length_squared < best_length_squared:
|
|
best_length_squared = length_squared
|
|
best_obj = obj
|
|
best_hit = hit
|
|
best_face_index = face_index
|
|
|
|
if best_obj is not None:
|
|
return best_obj, best_hit, best_face_index
|
|
return None, None, None
|
|
|
|
def move_projection_to_bottom(self, root):
|
|
# IfcConvert puts the projection afterwards which is not correct since
|
|
# projection should be drawn underneath the cut.
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
projections = root.xpath(
|
|
".//svg:g[contains(@class, 'projection')]", namespaces={"svg": "http://www.w3.org/2000/svg"}
|
|
)
|
|
for projection in projections:
|
|
projection.getparent().remove(projection)
|
|
group.insert(0, projection)
|
|
|
|
def move_elements_to_top(self, root):
|
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
|
bringtofront = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "BringToFront") or ""
|
|
bringtofront = [item.strip() for item in bringtofront.split(",") if item.strip()]
|
|
|
|
# Iterate through classes in order of preference
|
|
for class_name in bringtofront:
|
|
xpath_query = f".//svg:g[contains(@class, '{class_name}')]"
|
|
elements_to_move = root.xpath(xpath_query, namespaces={"svg": "http://www.w3.org/2000/svg"})
|
|
|
|
# Move each element to the end of the group (effectively placing them at the top visually)
|
|
for element in elements_to_move:
|
|
element.getparent().remove(element)
|
|
group.append(element)
|
|
|
|
def generate_annotation(self, context: bpy.types.Context) -> Union[str, None]:
|
|
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasAnnotation"):
|
|
return
|
|
svg_path = self.get_svg_path(cache_type="annotation")
|
|
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
|
|
return svg_path
|
|
|
|
elements = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(self.camera_element))
|
|
filtered_drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
|
|
filtered_drawing_annotations = {e for e in filtered_drawing_elements if e.is_a("IfcAnnotation")}
|
|
elements = {e for e in elements if e in filtered_drawing_elements}
|
|
elements = list(elements | filtered_drawing_annotations)
|
|
|
|
annotations = sorted(
|
|
elements,
|
|
key=lambda a: (
|
|
tool.Drawing.get_annotation_z_index(a),
|
|
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
|
|
),
|
|
)
|
|
|
|
precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision")
|
|
if not precision:
|
|
precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "ImperialPrecision")
|
|
|
|
decimal_places = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "DecimalPlaces")
|
|
self.svg_writer.metadata = self.metadata
|
|
self.svg_writer.create_blank_svg(svg_path).draw_annotations(annotations, precision, decimal_places).save()
|
|
|
|
return svg_path
|
|
|
|
def get_scale(self, context):
|
|
diagram_scale = tool.Drawing.get_diagram_scale(self.camera)
|
|
self.human_scale = diagram_scale["HumanScale"]
|
|
self.scale = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
|
|
|
if ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "IsNTS"):
|
|
self.human_scale = "NTS"
|
|
|
|
def is_landscape(self, render):
|
|
return render.resolution_x > render.resolution_y
|
|
|
|
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
|
|
if hasattr(element, "Name") and element.Name:
|
|
return element.Name
|
|
elif hasattr(element, "LayerSetName") and element.LayerSetName:
|
|
return element.LayerSetName
|
|
return "mat-" + str(element.id())
|
|
|
|
def get_svg_path(self, cache_type: Optional[str] = None) -> str:
|
|
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
|
|
assert drawing_path
|
|
drawings_dir = os.path.dirname(drawing_path)
|
|
|
|
if cache_type:
|
|
drawings_dir = os.path.join(drawings_dir, "cache")
|
|
os.makedirs(drawings_dir, exist_ok=True)
|
|
filename = tool.Drawing.sanitise_filename(f"{self.drawing_name}-{cache_type}.svg")
|
|
return os.path.join(drawings_dir, filename)
|
|
os.makedirs(drawings_dir, exist_ok=True)
|
|
return drawing_path
|
|
|
|
|
|
class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_annotation"
|
|
bl_label = "Add Annotation"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
description: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return tool.Ifc.get() and context.scene.camera
|
|
|
|
@classmethod
|
|
def description(cls, context, operator):
|
|
return operator.description or ""
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_annotation_props()
|
|
dprops = tool.Drawing.get_document_props()
|
|
if not (drawing := dprops.get_active_drawing()):
|
|
self.report({"WARNING"}, "No active drawing.")
|
|
return
|
|
|
|
obj = core.add_annotation(
|
|
tool.Ifc,
|
|
tool.Collector,
|
|
tool.Drawing,
|
|
drawing=drawing,
|
|
object_type=props.object_type,
|
|
relating_type=tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None,
|
|
enable_editing=True,
|
|
)
|
|
if props.object_type == "IMAGE":
|
|
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
|
|
|
|
|
class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_sheet"
|
|
bl_label = "Add Sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Add a sheet to the project"
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock)
|
|
|
|
|
|
class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.duplicate_sheet"
|
|
bl_label = "Duplicate Sheet"
|
|
bl_description = "Make a copy of currently selected sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
drawing: bpy.props.IntProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
# Unconditionally disable until implemented
|
|
cls.poll_message_set("Not implemented yet.")
|
|
return False
|
|
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
def _execute(self, context):
|
|
pass
|
|
"""
|
|
self.props = tool.Drawing.get_document_props()
|
|
core.duplicate_sheet(
|
|
tool.Ifc,
|
|
tool.Drawing,
|
|
sheet=tool.Ifc.get().by_id(self.sheet),
|
|
)
|
|
try:
|
|
sheet = tool.Ifc.get().by_id(self.props.active_sheet_id)
|
|
core.sync_references(tool.Ifc, tool.Collector, tool.Sheet, drawing=sheet)
|
|
except:
|
|
pass
|
|
"""
|
|
|
|
|
|
class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.open_layout"
|
|
bl_label = "Open Sheet Layout"
|
|
bl_description = (
|
|
"Opens selected .svg layout with default system viewer\n"
|
|
+ 'or using "layout_svg_command" from the Bonsai preferences\n'
|
|
+ "(if provided)"
|
|
)
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
self.props = tool.Drawing.get_document_props()
|
|
sheet_item = tool.Drawing.get_active_sheet_item()
|
|
assert sheet_item
|
|
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.update_sheet_drawing_sizes(sheet)
|
|
core.open_layout(tool.Drawing, sheet=sheet)
|
|
|
|
|
|
class SelectAllSheets(bpy.types.Operator):
|
|
bl_idname = "bim.select_all_sheets"
|
|
bl_label = "Select All Sheets"
|
|
view: bpy.props.StringProperty()
|
|
bl_description = "Select all sheets in the sheet list.\n\n" + "SHIFT+CLICK to deselect all sheets"
|
|
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
|
|
|
|
def invoke(self, context, event):
|
|
# deselect all sheets on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.select_all = False
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
for sheet in props.sheets:
|
|
if sheet.is_selected != self.select_all:
|
|
sheet.is_selected = self.select_all
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OpenSheet(bpy.types.Operator):
|
|
bl_idname = "bim.open_sheet"
|
|
bl_label = "Open Sheet"
|
|
bl_description = (
|
|
"Opens selected sheet with default system viewer\n"
|
|
+ 'or using "svg_command" or "pdf_command" from\n'
|
|
+ "the Bonsai preferences (if provided).\n\n"
|
|
+ "SHIFT+CLICK to open all shown checked sheets"
|
|
)
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
open_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
|
|
cls.poll_message_set("No sheet selected.")
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
# opening all sheets on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.open_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
self.props = tool.Drawing.get_document_props()
|
|
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
|
|
|
|
if self.open_all:
|
|
sheets = [
|
|
tool.Ifc.get().by_id(s.ifc_definition_id) for s in self.props.sheets if s.is_sheet and s.is_selected
|
|
]
|
|
else:
|
|
sheet_item = tool.Drawing.get_active_sheet_item()
|
|
assert sheet_item
|
|
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
|
|
|
|
sheet_uris: list[str] = []
|
|
sheets_not_found: list[str] = []
|
|
warnings: list[tool.Drawing.SheetWarningType] = []
|
|
|
|
for sheet in sheets:
|
|
if not sheet.is_a("IfcDocumentInformation"):
|
|
continue
|
|
warnings.extend(sheets_warnings := tool.Drawing.validate_sheet_files(sheet))
|
|
if sheets_warnings:
|
|
continue
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
references = sheet_builder.build(sheet)
|
|
sheet_uri = references["SHEET"]
|
|
if svg2pdf_command:
|
|
sheet_uri = os.path.splitext(sheet_uri)[0] + ".pdf"
|
|
sheet_uris.append(sheet_uri)
|
|
if not os.path.exists(sheet_uri):
|
|
sheets_not_found.append(sheet.Name)
|
|
|
|
if warnings:
|
|
self.report({"ERROR"}, f"There were errors opening sheets. See system console for the details.")
|
|
print("-" * 10)
|
|
print("\n".join(str(w) for w in warnings))
|
|
|
|
if sheets_not_found:
|
|
msg = "Some sheets .svg/.pdf files were not found, need to create them first: \n{}.".format(
|
|
"\n".join(sheets_not_found)
|
|
)
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
for sheet_uri in sheet_uris:
|
|
if svg2pdf_command:
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, sheet_uri)
|
|
else:
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, sheet_uri)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_drawing_to_sheet"
|
|
bl_label = "Add Selected Drawing To Sheet"
|
|
bl_description = "Add the drawing selected in the\nDrawings list below to the sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
if not props.sheets:
|
|
cls.poll_message_set("No sheets available.")
|
|
return False
|
|
if not tool.Blender.get_user_data_dir():
|
|
cls.poll_message_set("BIM data directory not set.")
|
|
return False
|
|
return True
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
active_drawing = props.drawings[props.active_drawing_index]
|
|
assert active_drawing
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
active_sheet = tool.Drawing.get_active_sheet()
|
|
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
|
|
drawing_reference = tool.Drawing.get_drawing_document(drawing)
|
|
|
|
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
|
if not sheet.is_a("IfcDocumentInformation"):
|
|
return
|
|
|
|
references = tool.Drawing.get_document_references(sheet)
|
|
|
|
has_drawing = False
|
|
for reference in references:
|
|
if reference.Location == drawing_reference.Location:
|
|
has_drawing = True
|
|
break
|
|
if has_drawing:
|
|
return
|
|
|
|
if not tool.Drawing.does_file_exist(tool.Drawing.get_document_uri(drawing_reference)):
|
|
self.report({"ERROR"}, "The drawing must be generated before adding to a sheet.")
|
|
return
|
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
|
|
attributes = tool.Drawing.generate_reference_attributes(
|
|
reference,
|
|
Identification=str(
|
|
len(
|
|
[
|
|
r
|
|
for r in references
|
|
if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE")
|
|
]
|
|
)
|
|
+ 1
|
|
),
|
|
Location=drawing_reference.Location,
|
|
Description="DRAWING",
|
|
)
|
|
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.add_drawing(reference, drawing, sheet)
|
|
|
|
tool.Drawing.import_sheets()
|
|
|
|
|
|
class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_drawing_from_sheet"
|
|
bl_label = "Remove Drawing From Sheet"
|
|
bl_description = "Remove currently selected drawing from sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
reference: bpy.props.IntProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
active_item = tool.Drawing.get_active_sheet_item()
|
|
if active_item is None:
|
|
return False
|
|
|
|
if active_item.reference_type == "TITLEBLOCK":
|
|
cls.poll_message_set("No effect deleting titleblock reference.")
|
|
return False
|
|
return True
|
|
|
|
def _execute(self, context):
|
|
ifc_file = tool.Ifc.get()
|
|
tool.Drawing.remove_drawing_from_sheet(ifc_file.by_id(self.reference))
|
|
|
|
|
|
class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.create_sheets"
|
|
bl_label = "Create Sheets"
|
|
bl_description = (
|
|
"Build and open selected sheet from the sheet layout and\n"
|
|
+ "optionally create .pdf and .dxf from commands in\n"
|
|
+ "the Bonsai preferences (if provided).\n\n"
|
|
+ "SHIFT+CLICK to create all shown checked sheets, but doesn't\n"
|
|
+ "open them for viewing\n\n"
|
|
+ "Add the CTRL modifier to optionally open sheets to view them as\n"
|
|
+ "they are created"
|
|
)
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
create_all: bpy.props.BoolProperty(name="Create All", default=False, options={"SKIP_SAVE"})
|
|
open_viewer: bpy.props.BoolProperty(name="Open in Viewer", default=False, options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
|
|
cls.poll_message_set("No sheet selected.")
|
|
return False
|
|
prefs = tool.Blender.get_addon_preferences()
|
|
return props.sheets and prefs.data_dir
|
|
|
|
def invoke(self, context, event):
|
|
# opening all sheets on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.create_all = True
|
|
if event.type == "LEFTMOUSE" and event.ctrl:
|
|
self.open_viewer = True
|
|
return self.execute(context)
|
|
|
|
def _execute(self, context):
|
|
scene = context.scene
|
|
props = tool.Drawing.get_document_props()
|
|
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
|
|
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
if self.create_all:
|
|
sheets = [tool.Ifc.get().by_id(s.ifc_definition_id) for s in props.sheets if s.is_sheet and s.is_selected]
|
|
else:
|
|
sheet_item = tool.Drawing.get_active_sheet_item()
|
|
assert sheet_item
|
|
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
|
|
|
|
warnings: list[tool.Drawing.SheetWarningType] = []
|
|
n_sheets_created = 0
|
|
for sheet in sheets:
|
|
|
|
warnings.extend(sheet_warnings := tool.Drawing.validate_sheet_files(sheet))
|
|
if sheet_warnings:
|
|
continue
|
|
|
|
# Update any drawing boundary changes
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.update_sheet_drawing_sizes(sheet)
|
|
|
|
if not sheet.is_a("IfcDocumentInformation"):
|
|
return
|
|
|
|
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
|
|
references = sheet_builder.build(sheet)
|
|
|
|
# These variables will be made available to the evaluated commands
|
|
svg = references["SHEET"]
|
|
pdf = os.path.splitext(svg)[0] + ".pdf"
|
|
replacements = {
|
|
"svg": svg,
|
|
"basename": os.path.basename(svg),
|
|
"path": os.path.dirname(svg),
|
|
"pdf": pdf,
|
|
"eps": os.path.splitext(svg)[0] + ".eps",
|
|
"dxf": os.path.splitext(svg)[0] + ".dxf",
|
|
}
|
|
|
|
has_sheet_reference = False
|
|
for reference in tool.Drawing.get_document_references(sheet):
|
|
reference_description = tool.Drawing.get_reference_description(reference)
|
|
if reference_description == "SHEET":
|
|
has_sheet_reference = True
|
|
|
|
if not has_sheet_reference:
|
|
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
|
|
ifcopenshell.api.document.edit_reference(
|
|
ifc_file,
|
|
reference=reference,
|
|
attributes=tool.Drawing.generate_reference_attributes(
|
|
reference, Location=tool.Ifc.get_uri(svg, use_relative_path=True), Description="SHEET"
|
|
),
|
|
)
|
|
|
|
if svg2pdf_command:
|
|
# With great power comes great responsibility. Example:
|
|
# [["inkscape", "svg", "-o", "pdf"]]
|
|
commands = json.loads(svg2pdf_command)
|
|
for command in commands:
|
|
subprocess.run([replacements.get(c, c) for c in command])
|
|
|
|
if svg2dxf_command:
|
|
# With great power comes great responsibility. Example:
|
|
# [["inkscape", "svg", "-o", "eps"], ["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]]
|
|
commands = json.loads(svg2dxf_command)
|
|
for command in commands:
|
|
command[0] = shutil.which(command[0]) or command[0]
|
|
subprocess.run([replacements.get(c, c) for c in command])
|
|
|
|
if self.open_viewer:
|
|
if svg2pdf_command:
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf)
|
|
else:
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg)
|
|
|
|
n_sheets_created += 1
|
|
|
|
if not self.open_viewer:
|
|
self.report({"INFO"}, f"{n_sheets_created} sheets created...")
|
|
|
|
if warnings:
|
|
self.report({"ERROR"}, f"There were errors creating sheets. See system console for the details.")
|
|
print("-" * 10)
|
|
print("\n".join(str(w) for w in warnings))
|
|
|
|
|
|
class SelectAllDrawings(bpy.types.Operator):
|
|
bl_idname = "bim.select_all_drawings"
|
|
bl_label = "Select All Drawings"
|
|
view: bpy.props.StringProperty()
|
|
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
|
|
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
|
|
|
|
def invoke(self, context, event):
|
|
# deselect all drawings on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.select_all = False
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
for drawing in props.drawings:
|
|
if drawing.is_selected != self.select_all:
|
|
drawing.is_selected = self.select_all
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OpenDrawing(bpy.types.Operator):
|
|
bl_idname = "bim.open_drawing"
|
|
bl_label = "Open Drawing"
|
|
view: bpy.props.StringProperty()
|
|
bl_description = (
|
|
"Opens selected .svg drawing with default system viewer\n"
|
|
+ 'or using "svg_command" from the Bonsai preferences (if provided).\n\n'
|
|
+ "SHIFT+CLICK to open all shown checked drawings"
|
|
)
|
|
open_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
# opening all drawings on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.open_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
self.props = tool.Drawing.get_document_props()
|
|
if self.open_all:
|
|
drawings = [
|
|
tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected
|
|
]
|
|
else:
|
|
drawings = [tool.Ifc.get().by_id(self.props.drawings.get(self.view).ifc_definition_id)]
|
|
|
|
drawing_uris = []
|
|
drawings_not_found = []
|
|
|
|
for drawing in drawings:
|
|
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
|
drawing_uris.append(drawing_uri)
|
|
if not os.path.exists(drawing_uri):
|
|
drawings_not_found.append(drawing.Name)
|
|
|
|
if drawings_not_found:
|
|
msg = "Some drawings .svg files were not found, need to create them first: \n{}.".format(
|
|
"\n".join(drawings_not_found)
|
|
)
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
for drawing_uri in drawing_uris:
|
|
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, drawing_uri)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ActivateModel(bpy.types.Operator):
|
|
bl_idname = "bim.activate_model"
|
|
bl_label = "Activate Model"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Activate the model view.\n\n"
|
|
"Show all objects (and apply status filters if they were enabled before) and hide all annotations."
|
|
)
|
|
|
|
def execute(self, context):
|
|
start_time = time.time()
|
|
|
|
dprops = tool.Drawing.get_document_props()
|
|
dprops.active_drawing_id = 0
|
|
model_props = tool.Model.get_model_props()
|
|
if model_props.show_cut_decorator:
|
|
CutDecorator.uninstall()
|
|
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
if not bpy.app.background:
|
|
with context.temp_override(**tool.Blender.get_viewport_context()):
|
|
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
|
|
|
|
elements = {e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))}
|
|
|
|
def refine_elements(
|
|
elements_mutable: set[ifcopenshell.entity_instance],
|
|
) -> dict[ifcopenshell.entity_instance, tuple[ifcopenshell.entity_instance, bpy.types.Object]]:
|
|
"""
|
|
:return: element -> (representation, obj)
|
|
"""
|
|
# TODO: in the future reimport_element_representations should have an option
|
|
# not recalculate elements completely, but get the from cache, to speed up the process further.
|
|
refined_elements: dict[
|
|
ifcopenshell.entity_instance, tuple[ifcopenshell.entity_instance, bpy.types.Object]
|
|
] = {}
|
|
elements = elements_mutable
|
|
while elements:
|
|
element = elements.pop()
|
|
model = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if not model:
|
|
continue
|
|
assert isinstance(obj := tool.Ifc.get_object(element), bpy.types.Object)
|
|
current_representation = tool.Geometry.get_active_representation(obj)
|
|
if current_representation == model:
|
|
continue
|
|
|
|
# reimport_element_representations automatically reloads all elements sharing representation.
|
|
# So we should avoid reloading same elements twice.
|
|
resolved_model = ifcopenshell.util.representation.resolve_representation(model)
|
|
elements_sharing_representation = ifcopenshell.util.element.get_elements_by_representation(
|
|
ifc_file, resolved_model
|
|
)
|
|
|
|
refined_elements[element] = (model, obj)
|
|
elements = elements - elements_sharing_representation
|
|
return refined_elements
|
|
|
|
refined_elements = refine_elements(elements)
|
|
for _, (model, obj) in refined_elements.items():
|
|
bonsai.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=model,
|
|
)
|
|
|
|
tool.Blender.reset_object_visibility()
|
|
tool.Drawing.hide_all_drawing_collections()
|
|
tool.Blender.update_viewport()
|
|
bonsai.bim.handler.refresh_ui_data()
|
|
|
|
operator_time = time.time() - start_time
|
|
if operator_time > 10:
|
|
self.report({"INFO"}, f"{self.bl_label} was finished in {operator_time:.2f} seconds.")
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ActivateDrawingBase(tool.Ifc.Operator):
|
|
# Ifc Operator is necessary, because sync_references may create or remove IFC elements.
|
|
bl_label = "Activate Drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Activates the selected drawing view.\n\n"
|
|
+ "ALT+CLICK to keep the viewport position.\n\n"
|
|
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
|
)
|
|
|
|
drawing: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
|
should_view_from_camera: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
name="Should View From Camera",
|
|
description="Move view to the activated drawing's camera position.",
|
|
default=True,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
use_quick_preview: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
name="Use Quick Preview",
|
|
description="Just move the camera to the drawing view, without loading anything else.",
|
|
default=False,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
drawing: int
|
|
should_view_from_camera: bool
|
|
use_quick_preview: bool
|
|
|
|
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
|
if event.type == "LEFTMOUSE" and event.alt:
|
|
self.should_view_from_camera = False
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.use_quick_preview = True
|
|
return self.execute(context)
|
|
|
|
def _execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
|
assert context.scene
|
|
props = tool.Drawing.get_document_props()
|
|
if props.is_editing_drawings == False:
|
|
bpy.ops.bim.load_drawings()
|
|
|
|
drawing = tool.Ifc.get().by_id(self.drawing)
|
|
dprops = tool.Drawing.get_document_props()
|
|
|
|
if self.use_quick_preview:
|
|
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
|
|
return {"FINISHED"}
|
|
|
|
viewport_position = None
|
|
|
|
v3d_space = tool.Blender.get_view3d_space()
|
|
assert v3d_space
|
|
if self.should_view_from_camera:
|
|
# Since we must switch to drawing camera, undo local camera in viewport.
|
|
# The code is needed to undo `else` branch from activating other cameras.
|
|
v3d_space.use_local_camera = False
|
|
else:
|
|
# Since we use `scene.camera` to store active drawing,
|
|
# the only way to preserve previous drawing camera position is making it local.
|
|
r3d = v3d_space.region_3d
|
|
assert r3d
|
|
if r3d.view_perspective == "CAMERA":
|
|
if v3d_space.use_local_camera:
|
|
# Nothing to do, local camera is already set by user.
|
|
pass
|
|
else:
|
|
previous_camera = context.scene.camera
|
|
if previous_camera:
|
|
v3d_space.camera = previous_camera
|
|
v3d_space.use_local_camera = True
|
|
else:
|
|
viewport_position = tool.Blender.get_viewport_position()
|
|
|
|
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
|
|
|
|
if not self.should_view_from_camera:
|
|
if viewport_position is None:
|
|
# Local camera takes priority over active scene camera,
|
|
# so nothing to do after drawing view activation.
|
|
pass
|
|
else:
|
|
tool.Blender.set_viewport_position(viewport_position)
|
|
|
|
dprops.active_drawing_id = self.drawing
|
|
dprops.drawing_styles.clear()
|
|
bpy.ops.bim.reload_drawing_styles()
|
|
bpy.ops.bim.activate_drawing_style()
|
|
|
|
if tool.Drawing.is_camera_orthographic():
|
|
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
|
|
model_props = tool.Model.get_model_props()
|
|
if model_props.show_cut_decorator:
|
|
CutDecorator.install(context)
|
|
tool.Drawing.show_decorations()
|
|
|
|
# Save drawing bounds to the .ifc file
|
|
camera = context.scene.camera
|
|
assert camera
|
|
camera_props = tool.Drawing.get_camera_props(camera)
|
|
# Check if this is a reflected ceiling camera and preserve its scale
|
|
camera_element = tool.Ifc.get_entity(camera)
|
|
is_reflected = False
|
|
if camera_element:
|
|
is_reflected = (
|
|
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
|
|
== "REFLECTED_PLAN_VIEW"
|
|
)
|
|
if is_reflected and camera.scale != (-1, -1, -1):
|
|
camera.scale = (-1, -1, -1)
|
|
|
|
if camera_props.update_representation(camera.matrix_world):
|
|
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
|
# Restore the scale after update if needed
|
|
if is_reflected:
|
|
camera.scale = (-1, -1, -1)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
|
|
bl_idname = "bim.activate_drawing"
|
|
bl_label = "Activate Drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Activates the selected drawing view.\n\n"
|
|
+ "ALT+CLICK to keep the viewport position.\n\n"
|
|
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
|
)
|
|
|
|
|
|
class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
|
|
bl_idname = "bim.activate_drawing_from_sheet"
|
|
bl_label = "Activate Drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Activates the selected drawing view.\n\n"
|
|
+ "ALT+CLICK to keep the viewport position.\n\n"
|
|
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"):
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
|
|
class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_drawing"
|
|
bl_label = "Remove Drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Remove currently selected drawing.\n\n" + "SHIFT+CLICK to remove all selected drawings"
|
|
|
|
drawing: bpy.props.IntProperty()
|
|
remove_all: bpy.props.BoolProperty(name="Remove All", default=False, options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
# removing all selected drawings on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.remove_all = True
|
|
return self.execute(context)
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if self.remove_all:
|
|
drawings = [
|
|
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
|
|
]
|
|
else:
|
|
if not self.drawing:
|
|
self.report({"ERROR"}, "No drawing selected")
|
|
return {"CANCELLED"}
|
|
drawings = [tool.Ifc.get().by_id(self.drawing)]
|
|
|
|
for drawing in drawings:
|
|
sheet_references = tool.Drawing.get_sheet_references(drawing)
|
|
for reference in sheet_references:
|
|
tool.Drawing.remove_drawing_from_sheet(reference)
|
|
core.remove_drawing(tool.Ifc, tool.Drawing, drawing=drawing)
|
|
|
|
# In case we removed the active drawing.
|
|
if not context.scene.camera and props.should_draw_decorations:
|
|
props.should_draw_decorations = False
|
|
|
|
|
|
class DrawingStyleJson(TypedDict):
|
|
render_type: "RenderType"
|
|
raster_style: dict[str, Any]
|
|
|
|
|
|
class ReloadDrawingStyles(bpy.types.Operator):
|
|
bl_idname = "bim.reload_drawing_styles"
|
|
bl_label = "Reload Drawing Styles"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Reload drawing styles for the active camera from the related JSON file."
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
assert (drawing := props.get_active_drawing())
|
|
drawing_pset_data = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
|
|
|
assert context.scene and (camera := context.scene.camera)
|
|
camera_props = tool.Drawing.get_camera_props(camera)
|
|
|
|
if "ShadingStyles" not in drawing_pset_data:
|
|
self.report({"ERROR"}, "Could not find shading styles path in EPset_Drawing.ShadingStyles.")
|
|
return {"CANCELLED"}
|
|
|
|
rel_path = drawing_pset_data["ShadingStyles"]
|
|
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
|
|
|
|
json_path = Path(tool.Ifc.resolve_uri(rel_path))
|
|
if not json_path.exists():
|
|
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / "shading_styles.json")
|
|
print(
|
|
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
|
|
f"Default BBIM resource will be copied from {ootb_resource}"
|
|
)
|
|
if ootb_resource.exists():
|
|
os.makedirs(json_path.parent, exist_ok=True)
|
|
shutil.copy(ootb_resource, json_path)
|
|
|
|
with open(json_path, "r") as fi:
|
|
shading_styles_json: dict[str, DrawingStyleJson] = json.load(fi)
|
|
|
|
drawing_styles = props.drawing_styles
|
|
drawing_styles.clear()
|
|
styles = [style for style in shading_styles_json]
|
|
for style_name in styles:
|
|
style_data = shading_styles_json[style_name]
|
|
drawing_style = drawing_styles.add()
|
|
drawing_style["name"] = style_name # setting as attribute to avoid triggering setter
|
|
drawing_style.render_type = style_data["render_type"]
|
|
drawing_style.raster_style = json.dumps(style_data["raster_style"])
|
|
|
|
if current_style is not None:
|
|
if current_style not in styles:
|
|
self.report({"WARNING"}, f"Could not find style {current_style} in EPset_Drawing.ShadingStyles.")
|
|
else:
|
|
camera_props.active_drawing_style_index = styles.index(current_style)
|
|
return {"FINISHED"}
|
|
|
|
|
|
# NOTE: Ifc Operator is not necessary for add and remove,
|
|
# as underlying save operator creates ifc undo step for us,
|
|
# but we keep it to make it more safe in case operators composition will change.
|
|
class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_drawing_style"
|
|
bl_label = "Add Drawing Style"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
assert context.scene and (camera_obj := context.scene.camera)
|
|
props = tool.Drawing.get_document_props()
|
|
drawing_styles = props.drawing_styles
|
|
new = drawing_styles.add()
|
|
# drawing style is saved to ifc on rename
|
|
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
|
|
camera_props = tool.Drawing.get_camera_props(camera_obj)
|
|
camera_props.active_drawing_style_index = len(drawing_styles) - 1
|
|
return {"FINISHED"}
|
|
|
|
|
|
class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_drawing_style"
|
|
bl_label = "Remove Drawing Style"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
index: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
assert context.scene and (camera_obj := context.scene.camera)
|
|
props = tool.Drawing.get_document_props()
|
|
props.drawing_styles.remove(self.index)
|
|
camera_props = tool.Drawing.get_camera_props(camera_obj)
|
|
camera_props.active_drawing_style_index = max(self.index - 1, 0)
|
|
bpy.ops.bim.save_drawing_styles_data()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.save_drawing_style"
|
|
bl_label = "Save Drawing Style"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Save current render settings to currently selected drawing style. Also resaves styles data to IFC"
|
|
|
|
index: bpy.props.StringProperty()
|
|
# TODO: check undo redo
|
|
|
|
def _execute(self, context):
|
|
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used later in eval
|
|
scene = context.scene
|
|
assert scene
|
|
style = {}
|
|
eval_namespace = {"context": context, "scene": scene, "space": space}
|
|
|
|
def add_prop_to_style(
|
|
prop_path: str, context: bpy.types.Context, scene: bpy.types.Scene, space: bpy.types.SpaceView3D
|
|
) -> None:
|
|
value = eval(prop_path)
|
|
if not isinstance(value, str):
|
|
try:
|
|
value = tuple(value)
|
|
except TypeError:
|
|
pass
|
|
style[prop_path] = value
|
|
|
|
for prop in RasterStyleProperty:
|
|
if prop.name.startswith("EVAL_PROP"):
|
|
prop_path = prop.value
|
|
add_prop_to_style(prop_path, **eval_namespace)
|
|
else:
|
|
props_source_path = prop.value
|
|
props_source = eval(props_source_path)
|
|
for prop_name in dir(props_source):
|
|
if prop_name.startswith("__"):
|
|
continue
|
|
|
|
prop_path = f"{props_source_path}.{prop_name}"
|
|
prop_value = eval(prop_path)
|
|
if (
|
|
not isinstance(prop_value, (int, float, bool, str, Color, Vector))
|
|
or props_source.is_property_readonly(prop_name)
|
|
or prop_path in RASTER_STYLE_PROPERTIES_EXCLUDE
|
|
):
|
|
continue
|
|
|
|
add_prop_to_style(prop_path, **eval_namespace)
|
|
|
|
if self.index:
|
|
index = int(self.index)
|
|
else:
|
|
assert (camera := scene.camera)
|
|
props = tool.Drawing.get_camera_props(camera)
|
|
index = props.active_drawing_style_index
|
|
props = tool.Drawing.get_document_props()
|
|
props.drawing_styles[index].raster_style = json.dumps(style)
|
|
|
|
bpy.ops.bim.save_drawing_styles_data()
|
|
return {"FINISHED"}
|
|
|
|
|
|
# TODO: operator is not exposed to UI, move it to tool.
|
|
class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.save_drawing_styles_data"
|
|
bl_label = "Save Drawing Styles Data"
|
|
bl_description = "Save current drawing styles settings to IFC and JSON."
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
skip_updating_current_style: bpy.props.BoolProperty(default=False)
|
|
rename_style: bpy.props.BoolProperty(default=False)
|
|
rename_style_from: bpy.props.StringProperty(default="")
|
|
rename_style_to: bpy.props.StringProperty(default="")
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
assert (drawing := props.get_active_drawing())
|
|
drawing_pset_data = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
|
drawing_styles = props.drawing_styles
|
|
|
|
rel_path = drawing_pset_data["ShadingStyles"]
|
|
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
|
|
json_path = Path(tool.Ifc.resolve_uri(rel_path))
|
|
if not json_path.exists():
|
|
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
|
|
return {"CANCELLED"}
|
|
|
|
styles_data: dict[str, DrawingStyleJson] = {}
|
|
for style in drawing_styles:
|
|
styles_data[style.name] = {"render_type": style.render_type, "raster_style": json.loads(style.raster_style)}
|
|
|
|
with open(json_path, "w") as fo:
|
|
json.dump(styles_data, fo, indent=4)
|
|
|
|
# TODO: currently it doesn't update current style for the other drawings
|
|
# handling case when current style is not present in styles saved in ifc
|
|
if not self.skip_updating_current_style and current_style not in styles_data and current_style is not None:
|
|
# style was renamed
|
|
if self.rename_style and current_style == self.rename_style_from:
|
|
new_style_name = self.rename_style_to
|
|
|
|
# style was removed
|
|
else:
|
|
new_style_name = None
|
|
|
|
ifc_file = tool.Ifc.get()
|
|
assert (drawing := props.get_active_drawing())
|
|
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
|
assert pset
|
|
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name})
|
|
bonsai.bim.handler.refresh_ui_data()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.activate_drawing_style"
|
|
bl_label = "Activate Drawing Style"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
has_errors_during_activation = False
|
|
has_warnings_during_activation = False
|
|
|
|
def _execute(self, context):
|
|
scene = context.scene
|
|
assert scene and (camera := scene.camera)
|
|
camera_props = tool.Drawing.get_camera_props(camera)
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
drawing_style = camera_props.get_active_drawing_style()
|
|
if drawing_style is None:
|
|
self.report({"ERROR"}, "Could not find active drawing style")
|
|
return {"CANCELLED"}
|
|
self.drawing_style = drawing_style
|
|
|
|
self.set_raster_style(context)
|
|
|
|
props = tool.Drawing.get_document_props()
|
|
assert (drawing := props.get_active_drawing())
|
|
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
|
assert pset
|
|
ifcopenshell.api.pset.edit_pset(
|
|
ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
|
|
)
|
|
bonsai.bim.handler.refresh_ui_data()
|
|
|
|
if self.has_warnings_during_activation:
|
|
self.report(
|
|
{"WARNING"},
|
|
"There were warnings setting some drawing style properies, see system console for the details.",
|
|
)
|
|
if self.has_errors_during_activation:
|
|
self.report(
|
|
{"WARNING"},
|
|
"There were errors setting some drawing style properies, see system console for the details.",
|
|
)
|
|
|
|
return {"FINISHED"}
|
|
|
|
def set_raster_style(self, context: bpy.types.Context) -> None:
|
|
scene = context.scene # Do not remove. It is used in exec later
|
|
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
|
|
style = json.loads(self.drawing_style.raster_style)
|
|
|
|
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
|
|
warning = False
|
|
skip = False
|
|
# @25.05.12
|
|
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
|
|
value = "BLENDER_EEVEE_NEXT"
|
|
print(
|
|
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
|
|
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
|
|
)
|
|
warning = True
|
|
# @25.05.12
|
|
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
|
value = "THEME"
|
|
print(
|
|
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
|
|
"since Blender 4.0 and should be replaced with 'THEME' in shading_styles.json."
|
|
)
|
|
warning = True
|
|
# @25.05.12
|
|
elif path in ("scene.render.simplify_shadows", "scene.render.simplify_shadows_render"):
|
|
print(
|
|
f"Warning: Property '{path}' is removed "
|
|
"since Blender 4.2 and should be also removed from shading_styles.json."
|
|
)
|
|
warning = True
|
|
skip = True
|
|
# @25.05.12
|
|
elif path in ("space.overlay.backwire_opacity", "space.overlay.show_edges"):
|
|
print(
|
|
f"Warning: Property '{path}' is removed "
|
|
"since Blender 4.1 and should be also removed from shading_styles.json."
|
|
)
|
|
warning = True
|
|
skip = True
|
|
# @25.05.12
|
|
elif path in ("space.overlay.show_occlude_wire",):
|
|
print(
|
|
f"Warning: Property '{path}' is removed "
|
|
"since Blender 3.6 and should be also removed from shading_styles.json."
|
|
)
|
|
warning = True
|
|
skip = True
|
|
|
|
return path, value, warning, skip
|
|
|
|
for path, value in style.items():
|
|
path, value, warning, skip = preprocess(path, value)
|
|
self.has_warnings_during_activation |= warning
|
|
|
|
if skip:
|
|
continue
|
|
|
|
try:
|
|
if isinstance(value, str):
|
|
exec(f"{path} = '{value}'")
|
|
else:
|
|
exec(f"{path} = {value}")
|
|
except Exception as e:
|
|
# Differences in Blender versions mean result in failures here
|
|
print(f"Failed to set drawing style property '{path}' to '{value}'. Error: '{str(e)}'")
|
|
self.has_errors_during_activation = True
|
|
if "PYTEST_VERSION" in os.environ:
|
|
raise
|
|
|
|
|
|
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_sheet"
|
|
bl_label = "Remove Sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Remove currently selected sheet"
|
|
sheet: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
|
|
|
|
|
|
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|
bl_idname = "bim.add_schedule"
|
|
bl_label = "Add Schedule"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
|
|
|
|
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
|
|
|
def _execute(self, context):
|
|
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
|
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
|
|
|
|
|
|
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_schedule"
|
|
bl_label = "Remove Schedule"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Remove the currently selected schedule"
|
|
|
|
schedule: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", document=tool.Ifc.get().by_id(self.schedule))
|
|
|
|
|
|
class OpenSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.open_schedule"
|
|
bl_label = "Open Schedule"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Open the currently selected schedule \nin the default system viewer"
|
|
|
|
schedule: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.open_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
|
|
|
|
|
|
class BuildSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.build_schedule"
|
|
bl_label = "Build Schedule"
|
|
bl_description = "Create a .svg file of the selected schedule\nand open it with default system viewer"
|
|
schedule: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.build_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
|
|
|
|
|
|
class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_schedule_to_sheet"
|
|
bl_label = "Add Schedule To Sheet"
|
|
bl_description = "Add the schedule selected in the\nSchedules list below to the sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not props.schedules:
|
|
cls.poll_message_set("No schedule selected.")
|
|
return False
|
|
if not props.sheets:
|
|
cls.poll_message_set("No sheets available.")
|
|
return False
|
|
if not tool.Blender.get_user_data_dir():
|
|
cls.poll_message_set("BIM data directory not set.")
|
|
return False
|
|
return True
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
active_schedule = props.schedules[props.active_schedule_index]
|
|
active_sheet = tool.Drawing.get_active_sheet()
|
|
ifc_file = tool.Ifc.get()
|
|
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
|
|
else:
|
|
schedule_location = tool.Drawing.get_path_with_ext(schedule.HasDocumentReferences[0].Location, "svg")
|
|
|
|
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
|
if not sheet.is_a("IfcDocumentInformation"):
|
|
return
|
|
|
|
references = tool.Drawing.get_document_references(sheet)
|
|
|
|
has_schedule = False
|
|
for reference in references:
|
|
if reference.Location == schedule_location:
|
|
has_schedule = True
|
|
break
|
|
if has_schedule:
|
|
return
|
|
|
|
if not tool.Drawing.does_file_exist(tool.Ifc.resolve_uri(schedule_location)):
|
|
self.report({"ERROR"}, "The schedule must be generated before adding to a sheet.")
|
|
return
|
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
|
|
attributes = tool.Drawing.generate_reference_attributes(
|
|
reference,
|
|
Identification=str(
|
|
len(
|
|
[
|
|
r
|
|
for r in references
|
|
if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE")
|
|
]
|
|
)
|
|
+ 1
|
|
),
|
|
Location=schedule_location,
|
|
Description="SCHEDULE",
|
|
)
|
|
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
|
|
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.add_document(reference, schedule, sheet)
|
|
|
|
tool.Drawing.import_sheets()
|
|
|
|
|
|
class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.add_reference_to_sheet"
|
|
bl_label = "Add Reference To Sheet"
|
|
bl_description = "Add the reference selected in the\nReferences list below to the sheet"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not props.references:
|
|
cls.poll_message_set("No reference selected.")
|
|
return False
|
|
if not props.sheets:
|
|
cls.poll_message_set("No sheets available.")
|
|
return False
|
|
if not tool.Blender.get_user_data_dir():
|
|
cls.poll_message_set("BIM data directory not set.")
|
|
return False
|
|
return True
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
active_reference = props.references[props.active_reference_index]
|
|
active_sheet = tool.Drawing.get_active_sheet()
|
|
ifc_file = tool.Ifc.get()
|
|
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
extref_location = tool.Drawing.get_path_with_ext(extref.DocumentReferences[0].Location, "svg")
|
|
else:
|
|
extref_location = tool.Drawing.get_path_with_ext(extref.HasDocumentReferences[0].Location, "svg")
|
|
|
|
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
|
if not sheet.is_a("IfcDocumentInformation"):
|
|
return
|
|
|
|
references = tool.Drawing.get_document_references(sheet)
|
|
|
|
has_extref = False
|
|
for reference in references:
|
|
if reference.Location == extref_location:
|
|
has_extref = True
|
|
break
|
|
if has_extref:
|
|
return
|
|
|
|
if not tool.Drawing.does_file_exist(tool.Ifc.resolve_uri(extref_location)):
|
|
self.report({"ERROR"}, f"Cannot find reference svg by path {extref_location}.")
|
|
return
|
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
|
|
attributes = tool.Drawing.generate_reference_attributes(
|
|
reference,
|
|
Identification=str(
|
|
len(
|
|
[
|
|
r
|
|
for r in references
|
|
if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE")
|
|
]
|
|
)
|
|
+ 1
|
|
),
|
|
Location=extref_location,
|
|
Description="REFERENCE",
|
|
)
|
|
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
|
|
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.add_document(reference, extref, sheet)
|
|
|
|
tool.Drawing.import_sheets()
|
|
|
|
|
|
class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|
bl_idname = "bim.add_reference"
|
|
bl_label = "Add Reference"
|
|
bl_description = "Import a .svg file to the project as a reference"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
|
filename_ext = ".svg"
|
|
|
|
def _execute(self, context):
|
|
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
|
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
|
|
|
|
|
|
class RemoveReference(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.remove_reference"
|
|
bl_label = "Remove Reference"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Remove the currently selected reference\nfrom the project"
|
|
|
|
reference: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.remove_document(tool.Ifc, tool.Drawing, "REFERENCE", document=tool.Ifc.get().by_id(self.reference))
|
|
|
|
|
|
class OpenReference(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.open_reference"
|
|
bl_label = "Open Reference"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Open the reference into default system viewer"
|
|
|
|
reference: bpy.props.IntProperty()
|
|
|
|
def _execute(self, context):
|
|
core.open_reference(tool.Drawing, reference=tool.Ifc.get().by_id(self.reference))
|
|
|
|
|
|
class CleanWireframes(bpy.types.Operator):
|
|
bl_idname = "bim.clean_wireframes"
|
|
bl_label = "Clean Wireframes"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
if context.selected_objects:
|
|
objects = context.selected_objects
|
|
else:
|
|
objects = context.scene.objects
|
|
for obj in (o for o in objects if o.type == "MESH"):
|
|
if "EDGE_SPLIT" not in (m.type for m in obj.modifiers):
|
|
obj.modifiers.new("EdgeSplit", "EDGE_SPLIT")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EditTextPopup(bpy.types.Operator):
|
|
bl_idname = "bim.edit_text_popup"
|
|
bl_label = "Edit Text"
|
|
first_run: bpy.props.BoolProperty(default=True)
|
|
|
|
def draw(self, context):
|
|
from bonsai.bim.module.drawing.ui import BIM_PT_text
|
|
|
|
BIM_PT_text.draw_text_editing_ui(self, context, popup_mode=True)
|
|
|
|
def cancel(self, context):
|
|
# disable editing when dialog is closed
|
|
bpy.ops.bim.disable_editing_text()
|
|
|
|
def execute(self, context):
|
|
# can't use invoke() because this operator
|
|
# will be run indirectly by hotkey
|
|
# so we use execute() and track whether it's the first run of the operator
|
|
if self.first_run:
|
|
bpy.ops.bim.enable_editing_text()
|
|
self.first_run = False
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
else:
|
|
bpy.ops.bim.edit_text()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EditText(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.edit_text"
|
|
bl_label = "Edit Text"
|
|
bl_description = "Save changes to the text annotation and\ndisable the text editing options"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.edit_text(tool.Drawing, obj=context.active_object)
|
|
tool.Blender.update_viewport()
|
|
|
|
|
|
class EnableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.enable_editing_text"
|
|
bl_label = "Enable Editing Text"
|
|
bl_description = "Enable the text editing options for this\ntext annotation"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.enable_editing_text(tool.Drawing, obj=context.active_object)
|
|
|
|
|
|
class DisableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_text"
|
|
bl_label = "Disable Editing Text"
|
|
bl_description = "Discard changes to the text annotation\nand disable the text editing options"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
obj = context.active_object
|
|
core.disable_editing_text(tool.Drawing, obj=obj)
|
|
|
|
# force update this object's font size for viewport display
|
|
DecoratorData.data.pop(obj.name, None)
|
|
tool.Blender.update_viewport()
|
|
|
|
|
|
class AddTextLiteral(bpy.types.Operator):
|
|
bl_idname = "bim.add_text_literal"
|
|
bl_label = "Add Text Literal"
|
|
bl_description = "Add another text literal to the\ntext annotation"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
assert obj
|
|
|
|
# similar to `tool.Drawing.import_text_attributes`
|
|
props = tool.Drawing.get_text_props(obj)
|
|
literal_props = props.literals.add()
|
|
literal_attributes = literal_props.attributes
|
|
literal_attr_values = {
|
|
"Literal": "Literal",
|
|
"Path": "RIGHT",
|
|
"BoxAlignment": "bottom_left",
|
|
}
|
|
# emulates `bonsai.bim.helper.import_attributes(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_description = "Delete the text literal from the\ntext annotation"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
literal_prop_id: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
assert obj
|
|
props = tool.Drawing.get_text_props(obj)
|
|
props.literals.remove(self.literal_prop_id)
|
|
tool.Blender.update_viewport()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OrderTextLiteralUp(bpy.types.Operator):
|
|
bl_idname = "bim.order_text_literal_up"
|
|
bl_label = "Move Text Literal Up"
|
|
bl_description = "Move the text literal up in the\norder of literals"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
literal_prop_id: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
assert obj
|
|
props = tool.Drawing.get_text_props(obj)
|
|
props.literals.move(self.literal_prop_id, self.literal_prop_id - 1)
|
|
tool.Blender.update_viewport()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OrderTextLiteralDown(bpy.types.Operator):
|
|
bl_idname = "bim.order_text_literal_down"
|
|
bl_label = "Move Text Literal Down"
|
|
bl_description = "Move the text literal down in the\norder of literals"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
literal_prop_id: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
assert obj
|
|
props = tool.Drawing.get_text_props(obj)
|
|
props.literals.move(self.literal_prop_id, self.literal_prop_id + 1)
|
|
tool.Blender.update_viewport()
|
|
return {"FINISHED"}
|
|
|
|
|
|
# Ifc Operator is unnecessary, because suboperator is handling IFC changes.
|
|
class AssignSelectedObjectAsProduct(bpy.types.Operator):
|
|
bl_idname = "bim.assign_selected_as_product"
|
|
bl_label = "Assign Selected Object As Product"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if len(context.selected_objects) != 2:
|
|
cls.poll_message_set("2 objects need to be selected")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
assert bpy.context.view_layer
|
|
objs = context.selected_objects[:]
|
|
obj1, obj2 = objs
|
|
element1 = tool.Ifc.get_entity(obj1)
|
|
element2 = tool.Ifc.get_entity(obj2)
|
|
assert element1 and element2
|
|
if element1.is_a("IfcAnnotation"):
|
|
other_selected_object = obj2
|
|
bpy.context.view_layer.objects.active = obj1
|
|
elif element2.is_a("IfcAnnotation"):
|
|
other_selected_object = obj1
|
|
bpy.context.view_layer.objects.active = obj2
|
|
else:
|
|
self.report({"ERROR"}, "One of the selected objects must be IfcAnnotation.")
|
|
return {"CANCELLED"}
|
|
|
|
assert (active_obj := context.active_object)
|
|
props = tool.Drawing.get_object_assigned_product_props(active_obj)
|
|
props.relating_product = other_selected_object
|
|
bpy.ops.bim.edit_assigned_product()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.edit_assigned_product"
|
|
bl_label = "Edit Text Product"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
product = None
|
|
assert (obj := context.active_object)
|
|
props = tool.Drawing.get_object_assigned_product_props(obj)
|
|
if props.relating_product:
|
|
product = tool.Ifc.get_entity(props.relating_product)
|
|
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=obj, product=product)
|
|
tool.Blender.update_viewport()
|
|
|
|
|
|
class EnableEditingAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.enable_editing_assigned_product"
|
|
bl_label = "Enable Editing Assigned Product"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
assert context.active_object
|
|
core.enable_editing_assigned_product(tool.Drawing, obj=context.active_object)
|
|
|
|
|
|
class DisableEditingAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_assigned_product"
|
|
bl_label = "Disable Editing Assigned Product"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
assert context.active_object
|
|
core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)
|
|
|
|
|
|
class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.load_sheets"
|
|
bl_label = "Load Sheets"
|
|
bl_description = "Load the saved sheets in this IFC project"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.load_sheets(tool.Drawing)
|
|
|
|
props = tool.Drawing.get_document_props()
|
|
warnings: list[tool.Drawing.SheetWarningType] = []
|
|
for sheet_prop in props.sheets:
|
|
if not sheet_prop.is_sheet:
|
|
continue
|
|
|
|
sheet = tool.Ifc.get().by_id(sheet_prop.ifc_definition_id)
|
|
document_uri = tool.Drawing.get_document_uri(sheet)
|
|
assert document_uri is not None
|
|
|
|
filepath = Path(document_uri)
|
|
if not filepath.is_file():
|
|
res = core.regenerate_sheet(tool.Drawing, sheet)
|
|
if res:
|
|
warnings.extend(res)
|
|
|
|
if warnings:
|
|
self.report({"WARNING"}, f"There were warnings loading sheets. See system console for the details.")
|
|
print("-" * 10)
|
|
print("\n".join(str(w) for w in warnings))
|
|
|
|
|
|
class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.edit_sheet"
|
|
bl_label = "Edit Sheet / Drawing"
|
|
bl_description = "Edit details of sheet or drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
identification: bpy.props.StringProperty()
|
|
name: bpy.props.StringProperty()
|
|
|
|
document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"]
|
|
|
|
def invoke(self, context, event):
|
|
assert context.window_manager
|
|
sheet_item = tool.Drawing.get_active_sheet_item()
|
|
assert sheet_item
|
|
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
|
if sheet.is_a("IfcDocumentInformation"):
|
|
self.document_type = "SHEET"
|
|
self.name = sheet.Name
|
|
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
|
|
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
|
|
self.document_type = "TITLEBLOCK"
|
|
else:
|
|
self.document_type = "EMBEDDED"
|
|
self.identification = sheet.Identification
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
assert self.layout
|
|
props = tool.Drawing.get_document_props()
|
|
if self.document_type == "SHEET":
|
|
row = self.layout.row()
|
|
row.prop(self, "identification", text="Identification")
|
|
row = self.layout.row()
|
|
row.prop(self, "name", text="Name")
|
|
elif self.document_type == "TITLEBLOCK":
|
|
row = self.layout.row()
|
|
row.prop(props, "titleblock", text="Titleblock")
|
|
elif self.document_type == "EMBEDDED":
|
|
row = self.layout.row()
|
|
row.prop(self, "identification", text="Identification")
|
|
|
|
def _execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
ifc_file = tool.Ifc.get()
|
|
sheet_item = tool.Drawing.get_active_sheet_item()
|
|
assert sheet_item
|
|
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
|
if self.document_type == "SHEET":
|
|
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
|
|
elif self.document_type == "EMBEDDED":
|
|
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
|
|
elif self.document_type == "TITLEBLOCK":
|
|
titleblock = props.titleblock
|
|
reference = sheet
|
|
sheet = tool.Drawing.get_reference_document(reference)
|
|
assert sheet
|
|
ifcopenshell.api.document.edit_reference(
|
|
ifc_file,
|
|
reference=reference,
|
|
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
|
|
)
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
sheet_builder.change_titleblock(sheet, titleblock)
|
|
tool.Drawing.import_sheets()
|
|
|
|
|
|
class DisableEditingSheets(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_sheets"
|
|
bl_label = "Disable Editing Sheets"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.disable_editing_sheets(tool.Drawing)
|
|
|
|
|
|
class LoadSchedules(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.load_schedules"
|
|
bl_label = "Load Schedules"
|
|
bl_description = "Load the saved schedules in this IFC project"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.load_schedules(tool.Drawing)
|
|
|
|
|
|
class DisableEditingSchedules(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_schedules"
|
|
bl_label = "Disable Editing Schedules"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.disable_editing_schedules(tool.Drawing)
|
|
|
|
|
|
class LoadReferences(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.load_references"
|
|
bl_label = "Load References"
|
|
bl_description = "Load the saved references in this IFC project"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.load_references(tool.Drawing)
|
|
|
|
|
|
class DisableEditingReferences(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_references"
|
|
bl_label = "Disable Editing References"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.disable_editing_references(tool.Drawing)
|
|
|
|
|
|
class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.load_drawings"
|
|
bl_label = "Load Drawings"
|
|
bl_description = "Load the saved drawings in this IFC project"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.load_drawings(tool.Drawing)
|
|
|
|
|
|
class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.disable_editing_drawings"
|
|
bl_label = "Disable Editing Drawings"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.disable_editing_drawings(tool.Drawing)
|
|
|
|
|
|
ToggleOption = Literal["EXPAND", "CONTRACT"]
|
|
|
|
|
|
class ToggleTargetView(bpy.types.Operator):
|
|
bl_idname = "bim.toggle_target_view"
|
|
bl_label = "Toggle Target View"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
target_view: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
|
toggle_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
default=False,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
|
items=[(i, i, "") for i in get_args(ToggleOption)]
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
target_view: str
|
|
toggle_all: bool
|
|
option: ToggleOption
|
|
|
|
@classmethod
|
|
def description(cls, context, properties) -> str:
|
|
option: ToggleOption = properties.option
|
|
if option == "EXPAND":
|
|
return "Expand target view.\n\nSHIFT+CLICK to expand all view categories."
|
|
else:
|
|
return "Contract target view.\n\nSHIFT+CLICK to contract all view categories."
|
|
|
|
def invoke(self, context, event):
|
|
# Toggling all categories on shift+click.
|
|
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.toggle_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
expanded = self.option == "EXPAND"
|
|
for drawing in props.drawings:
|
|
if drawing.is_drawing:
|
|
continue
|
|
if self.toggle_all or drawing.target_view == self.target_view:
|
|
drawing.is_expanded = expanded
|
|
core.load_drawings(tool.Drawing)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ExpandSheet(bpy.types.Operator):
|
|
bl_idname = "bim.expand_sheet"
|
|
bl_label = "Expand Sheet"
|
|
bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets."
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
sheet: bpy.props.IntProperty()
|
|
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
|
|
|
|
def invoke(self, context, event):
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.expand_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
|
|
sheet.is_expanded = True
|
|
core.load_sheets(tool.Drawing)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ContractSheet(bpy.types.Operator):
|
|
bl_idname = "bim.contract_sheet"
|
|
bl_label = "Contract Sheet"
|
|
bl_description = (
|
|
"Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets."
|
|
)
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
sheet: bpy.props.IntProperty()
|
|
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
|
|
|
|
def invoke(self, context, event):
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.expand_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
|
|
sheet.is_expanded = False
|
|
core.load_sheets(tool.Drawing)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SelectAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.select_assigned_product"
|
|
bl_label = "Select Assigned Product"
|
|
bl_description = "Select the product this element is assigned to"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
core.select_assigned_product(tool.Drawing, context)
|
|
|
|
|
|
class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.enable_editing_element_filter"
|
|
bl_label = "Element Filter Mode"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
filter_mode: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.filter_mode == "NONE":
|
|
return "Cancel filter"
|
|
return "Enable editing options for the include or exclude filter"
|
|
|
|
def _execute(self, context):
|
|
assert context.scene
|
|
obj = context.scene.camera
|
|
if not obj:
|
|
return
|
|
assert (camera := context.scene.camera)
|
|
props = tool.Drawing.get_camera_props(camera)
|
|
props.filter_mode = self.filter_mode
|
|
element = tool.Ifc.get_entity(obj)
|
|
assert element
|
|
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
|
|
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
|
|
try:
|
|
tool.Search.import_filter_query(query, filter_groups)
|
|
except:
|
|
pass
|
|
|
|
|
|
class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.edit_element_filter"
|
|
bl_label = "Edit Element Filter"
|
|
bl_description = "Saves the filter"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
filter_mode: bpy.props.StringProperty()
|
|
|
|
def _execute(self, context):
|
|
assert context.scene
|
|
obj = context.scene.camera
|
|
assert obj
|
|
props = tool.Drawing.get_camera_props(obj)
|
|
element = tool.Ifc.get_entity(obj)
|
|
assert element
|
|
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
|
assert pset
|
|
if self.filter_mode == "INCLUDE":
|
|
query = tool.Search.export_filter_query(props.include_filter_groups) or None
|
|
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
|
|
elif self.filter_mode == "EXCLUDE":
|
|
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
|
|
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
|
|
props.filter_mode = "NONE"
|
|
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
|
|
|
|
|
|
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|
bl_idname = "bim.add_reference_image"
|
|
bl_label = "Add Reference Image"
|
|
bl_description = "Add or import reference image to the IFC project"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
|
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
|
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
|
|
|
override_existing_image: bpy.props.BoolProperty(
|
|
name="Override Existing Image",
|
|
default=True,
|
|
description=(
|
|
"Override image if it was previously loaded to Blender. If disabled, will always create a new image"
|
|
),
|
|
)
|
|
use_existing_object_by_name: bpy.props.StringProperty(
|
|
name="Use Existing Object By Name",
|
|
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
if Path(tool.Ifc.get_path()).is_file():
|
|
layout.prop(self, "use_relative_path")
|
|
else:
|
|
self.use_relative_path = False
|
|
layout.label(text="Save the .ifc file first ")
|
|
layout.label(text="to use relative paths.")
|
|
layout.prop(self, "override_existing_image")
|
|
layout.prop(self, "use_existing_object_by_name")
|
|
|
|
def _execute(self, context):
|
|
abs_path = Path(self.filepath).absolute().resolve()
|
|
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
|
|
ifc_file = tool.Ifc.get()
|
|
|
|
if self.override_existing_image:
|
|
params = {"check_existing": True, "force_reload": True}
|
|
else:
|
|
params = {"check_existing": False}
|
|
image = load_image(abs_path.name, abs_path.parent, **params)
|
|
|
|
def bm_add_image_plane(mesh):
|
|
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
|
|
plane_scale = (Vector(image.size) / min(image.size)).to_3d()
|
|
matrix = Matrix.LocRotScale(None, None, plane_scale)
|
|
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
|
|
tool.Blender.apply_bmesh(mesh, bm)
|
|
|
|
if self.use_existing_object_by_name:
|
|
obj = bpy.data.objects[self.use_existing_object_by_name]
|
|
bm_add_image_plane(obj.data)
|
|
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
|
|
else:
|
|
temp_mesh = bpy.data.meshes.new("temp_mesh")
|
|
bm_add_image_plane(temp_mesh)
|
|
obj = bpy.data.objects.new(image_filepath.stem, temp_mesh)
|
|
tool.Drawing.run_root_assign_class(
|
|
obj=obj,
|
|
ifc_class="IfcAnnotation",
|
|
predefined_type="IMAGE",
|
|
should_add_representation=True,
|
|
context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"),
|
|
ifc_representation_class=None,
|
|
)
|
|
tool.Blender.remove_data_block(temp_mesh)
|
|
|
|
tool.Blender.set_active_object(obj)
|
|
|
|
material = bpy.data.materials.new(name=image_filepath.stem)
|
|
obj.data.materials.append(None) # new slot
|
|
obj.material_slots[0].material = material
|
|
bpy.ops.bim.add_style()
|
|
|
|
style = tool.Ifc.get_entity(material)
|
|
assert style
|
|
tool.Style.assign_style_to_object(style, obj)
|
|
|
|
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
|
|
# we don't support IfcSurfaceStyleWithTextures without Rendering yet
|
|
shading_attributes = {
|
|
"SurfaceColour": {
|
|
"Red": 1.0,
|
|
"Green": 1.0,
|
|
"Blue": 1.0,
|
|
},
|
|
"Transparency": 0.0,
|
|
"ReflectanceMethod": "NOTDEFINED",
|
|
}
|
|
ifcopenshell.api.style.add_surface_style(
|
|
tool.Ifc.get(),
|
|
style=style,
|
|
ifc_class="IfcSurfaceStyleRendering",
|
|
attributes=shading_attributes,
|
|
)
|
|
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
|
|
textures = [texture]
|
|
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
|
|
ifcopenshell.api.style.add_surface_style(
|
|
ifc_file,
|
|
style=style,
|
|
ifc_class="IfcSurfaceStyleWithTextures",
|
|
attributes={"Textures": textures},
|
|
)
|
|
tool.Style.reload_material_from_ifc(material)
|
|
tool.Geometry.record_object_materials(obj)
|
|
|
|
|
|
class ConvertSVGToDXF(bpy.types.Operator):
|
|
bl_idname = "bim.convert_svg_to_dxf"
|
|
bl_label = "Convert SVG to DXF"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
view: bpy.props.StringProperty()
|
|
bl_description = "Convert selected drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all shown checked drawings"
|
|
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if not tool.Drawing.get_active_drawing_item():
|
|
cls.poll_message_set("No drawing selected.")
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
# convert all drawings on shift+click
|
|
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
|
if event.type == "LEFTMOUSE" and event.shift:
|
|
self.convert_all = True
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
props = tool.Drawing.get_document_props()
|
|
if self.convert_all:
|
|
drawings = [
|
|
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
|
|
]
|
|
else:
|
|
drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)]
|
|
|
|
drawing_uris: list[Path] = []
|
|
drawings_not_found: list[str] = []
|
|
|
|
for drawing in drawings:
|
|
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
|
if drawing_uri is None or not os.path.exists(drawing_uri):
|
|
drawings_not_found.append(drawing.Name)
|
|
else:
|
|
drawing_uris.append(Path(drawing_uri))
|
|
|
|
if drawings_not_found:
|
|
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
|
|
"\n".join(drawings_not_found)
|
|
)
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
for drawing_uri in drawing_uris:
|
|
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
|
|
|
|
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OpenDocumentationWebUi(bpy.types.Operator):
|
|
bl_idname = "bim.open_documentation_web_ui"
|
|
bl_label = "Open Documentation Web UI"
|
|
bl_description = "Open the documentation web UI page"
|
|
|
|
def execute(self, context):
|
|
if not tool.Web.get_web_props().is_connected:
|
|
bpy.ops.bim.connect_websocket_server(page="documentation")
|
|
else:
|
|
bpy.ops.bim.open_web_browser(page="documentation")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.exclude_annotation"
|
|
bl_label = "Exclude Annotation"
|
|
bl_description = "Excludes the automatic annotation reference from the drawing"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def _execute(self, context):
|
|
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
|
|
return
|
|
for obj in tool.Blender.get_selected_objects(include_active=False):
|
|
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
|
|
if referenced_element := tool.Drawing.get_annotation_element(element):
|
|
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
|
|
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|