mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-20 20:22:09 +00:00
1102 lines
45 KiB
Python
1102 lines
45 KiB
Python
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# This file is part of BlenderBIM Add-on.
|
|
#
|
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import bpy
|
|
import bmesh
|
|
import json
|
|
import time
|
|
import logging
|
|
import textwrap
|
|
import shutil
|
|
import platform
|
|
import subprocess
|
|
import tempfile
|
|
import webbrowser
|
|
import ifcopenshell
|
|
import blenderbim.bim.handler
|
|
import blenderbim.tool as tool
|
|
from . import schema
|
|
from blenderbim.bim import import_ifc
|
|
from blenderbim.bim.ifc import IfcStore
|
|
from blenderbim.bim.prop import StrProperty
|
|
from blenderbim.bim.ui import IFCFileSelector
|
|
from blenderbim.bim.helper import get_enum_items
|
|
from mathutils import Vector, Matrix, Euler
|
|
from math import radians
|
|
from pathlib import Path
|
|
from collections import namedtuple
|
|
from typing import List
|
|
|
|
|
|
class SetTab(bpy.types.Operator):
|
|
bl_idname = "bim.set_tab"
|
|
# NOTE: bl_label is set to empty string intentionally
|
|
# to avoid showing the operator's name in the tooltips, see #3704
|
|
bl_label = ""
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
tab: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def description(cls, context, operator):
|
|
return next((t[1] for t in blenderbim.bim.prop.get_tab(None, context) if t[0] == operator.tab), "")
|
|
|
|
def execute(self, context):
|
|
if context.area.spaces.active.search_filter:
|
|
return {"FINISHED"}
|
|
tool.Blender.setup_tabs()
|
|
aprops = tool.Blender.get_area_props(context)
|
|
aprops.tab = self.tab
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SwitchTab(bpy.types.Operator):
|
|
bl_idname = "bim.switch_tab"
|
|
bl_label = "Switch Tab"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Switches to the last used tab"
|
|
|
|
def execute(self, context):
|
|
if context.area.spaces.active.search_filter:
|
|
return {"FINISHED"}
|
|
tool.Blender.setup_tabs()
|
|
aprops = tool.Blender.get_area_props(context)
|
|
aprops.tab = aprops.alt_tab
|
|
return {"FINISHED"}
|
|
|
|
|
|
class OpenUri(bpy.types.Operator):
|
|
bl_idname = "bim.open_uri"
|
|
bl_label = "Open URI"
|
|
uri: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
webbrowser.open(self.uri)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SelectURIAttribute(bpy.types.Operator):
|
|
bl_idname = "bim.select_uri_attribute"
|
|
bl_label = "Select URI Attribute"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Select a local file"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
data_path: bpy.props.StringProperty(name="Data Path")
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
|
|
def execute(self, context):
|
|
# data_path contains the latter half of the path to the string_value property
|
|
# I have no idea how to find out the former half, so let's just use brute force.
|
|
data_path = self.data_path.replace(".string_value", "")
|
|
attribute = None
|
|
try:
|
|
attribute = eval(f"bpy.context.scene.{data_path}")
|
|
except:
|
|
try:
|
|
attribute = eval(f"bpy.context.active_object.{data_path}")
|
|
except:
|
|
try:
|
|
attribute = eval(f"bpy.context.active_object.active_material.{data_path}")
|
|
except:
|
|
# Do you know a better way?
|
|
pass
|
|
if attribute:
|
|
filepath = self.filepath
|
|
if self.use_relative_path:
|
|
filepath = os.path.relpath(filepath, os.path.dirname(tool.Ifc.get_path()))
|
|
attribute.string_value = filepath
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
|
|
bl_idname = "bim.select_ifc_file"
|
|
bl_label = "Select IFC File"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = f"Select a different IFC file.\n{tool.Blender.operator_invoke_filepath_hotkeys_description}"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
|
|
def execute(self, context):
|
|
if self.is_existing_ifc_file():
|
|
context.scene.BIMProperties.ifc_file = self.get_filepath()
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
filepath = Path(context.scene.BIMProperties.ifc_file)
|
|
res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath)
|
|
if res is not None:
|
|
return res
|
|
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class SelectDataDir(bpy.types.Operator):
|
|
bl_idname = "bim.select_data_dir"
|
|
bl_label = "Select Data Directory"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Select the directory that contains all IFC data es. PSet, styles, etc..."
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
|
|
def execute(self, context):
|
|
context.scene.BIMProperties.data_dir = os.path.dirname(self.filepath)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class SelectSchemaDir(bpy.types.Operator):
|
|
bl_idname = "bim.select_schema_dir"
|
|
bl_label = "Select Schema Directory"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Select the directory containing the IFC schema specification"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
|
|
def execute(self, context):
|
|
context.scene.BIMProperties.schema_dir = os.path.dirname(self.filepath)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class FileAssociate(bpy.types.Operator):
|
|
bl_idname = "bim.file_associate"
|
|
bl_label = "Associate BlenderBIM with *.ifc files"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Creates a Desktop launcher and associates it with IFC files"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if platform.system() in ("Linux", "Windows"):
|
|
return True
|
|
cls.poll_message_set("Option available only on Windows & Linux.")
|
|
# TODO Darwin
|
|
# https://stackoverflow.com/questions/1082889/how-to-change-filetype-association-in-the-registry
|
|
return False
|
|
|
|
def draw(self, context):
|
|
# NOTE: really weird thing on windows that typing this command in cmd works
|
|
# when even if you create .bat with the command below and run it as administrator it won't
|
|
# Haven't found a workaround yet to automate process completely.
|
|
command = "ASSOC .IFC=BLENDERBIM"
|
|
self.layout.label(text="On the next step to create file association ")
|
|
self.layout.label(text="the system console will be opened ")
|
|
self.layout.label(text=f"and you will be asked to type command")
|
|
self.layout.label(text=f"{command}")
|
|
self.layout.label(text="to create an association.")
|
|
|
|
def invoke(self, context, event):
|
|
if platform.system() == "Windows":
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
else:
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop")
|
|
binary_path = bpy.app.binary_path
|
|
if platform.system() == "Linux":
|
|
destdir = os.path.join(os.environ["HOME"], ".local")
|
|
self.install_desktop_linux(src_dir=src_dir, destdir=destdir, binary_path=binary_path)
|
|
elif platform.system() == "Windows":
|
|
self.install_desktop_windows(src_dir, binary_path)
|
|
self.report({"INFO"}, "Associations established.")
|
|
return {"FINISHED"}
|
|
|
|
def install_desktop_windows(self, src_dir, binary_path):
|
|
# very important to clear this regitstry key before creating new association
|
|
# tried to do the regitsry change from powershell/cmd - but even admin rights are not enough
|
|
# this is why we're using .reg
|
|
reg_change_path = os.path.join(src_dir, "windows_bbim_association.reg")
|
|
subprocess.run(["cmd", "/c", "call", reg_change_path])
|
|
|
|
ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1")
|
|
# NOTE: call powershell with RunAs to get admin rights from user
|
|
subprocess.run(["powershell", "-ExecutionPolicy", "Bypass", "-File", ps_script_path, binary_path], shell=True)
|
|
|
|
def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"):
|
|
"""Creates linux file assocations and launcher icon"""
|
|
|
|
for rel_path in (
|
|
"bin",
|
|
"share/icons/hicolor/128x128/apps",
|
|
"share/icons/hicolor/128x128/mimetypes",
|
|
"share/applications",
|
|
"share/mime/packages",
|
|
):
|
|
os.makedirs(os.path.join(destdir, rel_path), exist_ok=True)
|
|
|
|
shutil.copy(
|
|
os.path.join(src_dir, "blenderbim.png"),
|
|
os.path.join(destdir, "share/icons/hicolor/128x128/apps"),
|
|
)
|
|
shutil.copy(
|
|
os.path.join(src_dir, "blenderbim.desktop"),
|
|
os.path.join(destdir, "share/applications"),
|
|
)
|
|
shutil.copy(
|
|
os.path.join(src_dir, "blenderbim.xml"),
|
|
os.path.join(destdir, "share/mime/packages"),
|
|
)
|
|
shutil.copyfile(
|
|
os.path.join(src_dir, "x-ifc_128x128.png"),
|
|
os.path.join(destdir, "share/icons/hicolor/128x128/mimetypes", "x-ifc.png"),
|
|
)
|
|
|
|
# copy and rewrite wrapper script
|
|
with open(os.path.join(src_dir, "blenderbim"), "r") as wrapper_template:
|
|
filedata = wrapper_template.read()
|
|
filedata = filedata.replace("#BLENDER_EXE=/opt/blender-3.3/blender", 'BLENDER_EXE="' + binary_path + '"')
|
|
with open(os.path.join(destdir, "bin", "blenderbim"), "w") as wrapper:
|
|
wrapper.write(filedata)
|
|
|
|
os.chmod(os.path.join(destdir, "bin", "blenderbim"), 0o755)
|
|
|
|
self.refresh_system_linux(destdir=destdir)
|
|
|
|
def refresh_system_linux(self, destdir="/tmp"):
|
|
"""Attempt to update mime and desktop databases"""
|
|
try:
|
|
subprocess.call(["update-mime-database", os.path.join(destdir, "share/mime")])
|
|
except:
|
|
pass
|
|
try:
|
|
subprocess.call(["update-desktop-database", os.path.join(destdir, "share/applications")])
|
|
except:
|
|
pass
|
|
|
|
|
|
class FileUnassociate(bpy.types.Operator):
|
|
bl_idname = "bim.file_unassociate"
|
|
bl_label = "Remove BlenderBIM *.ifc association"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Removes Desktop launcher and unassociates it with IFC files"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if platform.system() in ("Linux", "Windows"):
|
|
return True
|
|
cls.poll_message_set("Option available only on Windows & Linux.")
|
|
return False
|
|
|
|
def execute(self, context):
|
|
if platform.system() == "Linux":
|
|
destdir = os.path.join(os.environ["HOME"], ".local")
|
|
self.uninstall_desktop_linux(destdir=destdir)
|
|
elif platform.system() == "Windows":
|
|
self.uninstall_desktop_windows()
|
|
return {"FINISHED"}
|
|
|
|
def uninstall_desktop_windows(self):
|
|
# NOTE: call powershell with RunAs to get admin rights from user
|
|
cmd = [
|
|
"powershell",
|
|
"-Command",
|
|
"Start-Process -Verb RunAs -Wait cmd -ArgumentList '/c reg delete HKCR\\BLENDERBIM /f'",
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
self.report({"INFO"}, "Association removed.")
|
|
|
|
def uninstall_desktop_linux(self, destdir="/tmp"):
|
|
"""Removes linux file assocations and launcher icon"""
|
|
for rel_path in (
|
|
"share/icons/hicolor/128x128/apps/blenderbim.png",
|
|
"share/icons/hicolor/128x128/mimetypes/x-ifc.png",
|
|
"share/applications/blenderbim.desktop",
|
|
"share/mime/packages/blenderbim.xml",
|
|
"bin/blenderbim",
|
|
):
|
|
try:
|
|
os.remove(os.path.join(destdir, rel_path))
|
|
except:
|
|
pass
|
|
|
|
self.refresh_system_linux(destdir=destdir)
|
|
|
|
def refresh_system_linux(self, destdir="/tmp"):
|
|
"""Attempt to update mime and desktop databases"""
|
|
try:
|
|
subprocess.call(["update-mime-database", os.path.join(destdir, "share/mime")])
|
|
except:
|
|
pass
|
|
try:
|
|
subprocess.call(["update-desktop-database", os.path.join(destdir, "share/applications")])
|
|
except:
|
|
pass
|
|
|
|
|
|
class OpenUpstream(bpy.types.Operator):
|
|
bl_idname = "bim.open_upstream"
|
|
bl_label = "Open Upstream Reference"
|
|
page: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
if self.page == "home":
|
|
webbrowser.open("https://blenderbim.org/")
|
|
elif self.page == "docs":
|
|
webbrowser.open("https://blenderbim.org/docs/")
|
|
elif self.page == "wiki":
|
|
webbrowser.open("https://wiki.osarch.org/index.php?title=Category:BlenderBIM_Add-on")
|
|
elif self.page == "community":
|
|
webbrowser.open("https://community.osarch.org/")
|
|
elif self.page == "fund":
|
|
webbrowser.open("https://opencollective.com/opensourcebim")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BIM_OT_add_section_plane(bpy.types.Operator):
|
|
"""Add a temporary empty object as a section cutaway. Cull all geometry rendering below the empty's local Z axis"""
|
|
|
|
bl_idname = "bim.add_section_plane"
|
|
bl_label = "Add Temporary Section Cutaway"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
obj = self.create_section_obj(context)
|
|
if not self.has_section_override_node():
|
|
self.create_section_compare_node()
|
|
self.create_section_override_node(obj, context)
|
|
else:
|
|
self.append_obj_to_section_override_node(obj)
|
|
self.add_default_material_if_none_exists(context)
|
|
self.override_materials()
|
|
return {"FINISHED"}
|
|
|
|
def create_section_obj(self, context):
|
|
section = bpy.data.objects.new("Section", None)
|
|
section.empty_display_type = "SINGLE_ARROW"
|
|
section.empty_display_size = 5
|
|
section.show_in_front = True
|
|
if (
|
|
context.active_object
|
|
and context.active_object.select_get()
|
|
and isinstance(context.active_object.data, bpy.types.Camera)
|
|
):
|
|
section.matrix_world = (
|
|
context.active_object.matrix_world @ Euler((radians(180.0), 0.0, 0.0), "XYZ").to_matrix().to_4x4()
|
|
)
|
|
else:
|
|
section.rotation_euler = Euler((radians(180.0), 0.0, 0.0), "XYZ")
|
|
section.location = context.scene.cursor.location
|
|
collection = bpy.data.collections.get("Sections")
|
|
if not collection:
|
|
collection = bpy.data.collections.new("Sections")
|
|
context.scene.collection.children.link(collection)
|
|
collection.objects.link(section)
|
|
return section
|
|
|
|
def has_section_override_node(self):
|
|
return bpy.data.node_groups.get("Section Override")
|
|
|
|
def create_section_compare_node(self):
|
|
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
|
|
if bpy.app.version >= (4, 0, 0):
|
|
input_value = group.interface.new_socket(name="Value", in_out="INPUT", socket_type="NodeSocketFloat")
|
|
input_value.default_value = 1.0 # Mandatory multiplier for the last node group
|
|
group.interface.new_socket(name="Vector", in_out="INPUT", socket_type="NodeSocketVector")
|
|
group.interface.new_socket(name="Line Decorator", in_out="INPUT", socket_type="NodeSocketFloat")
|
|
group.interface.new_socket(name="Value", in_out="OUTPUT", socket_type="NodeSocketFloat")
|
|
group.interface.new_socket(name="Line Decorator", in_out="OUTPUT", socket_type="NodeSocketFloat")
|
|
else:
|
|
group.inputs.new("NodeSocketFloat", "Value")
|
|
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
|
|
group.inputs.new("NodeSocketVector", "Vector")
|
|
group.inputs.new("NodeSocketFloat", "Line Decorator")
|
|
group.outputs.new("NodeSocketFloat", "Value")
|
|
group.outputs.new("NodeSocketFloat", "Line Decorator")
|
|
|
|
group_input = group.nodes.new(type="NodeGroupInput")
|
|
group_input.location = 0, 50
|
|
|
|
separate_xyz = group.nodes.new(type="ShaderNodeSeparateXYZ")
|
|
separate_xyz.location = 200, 0
|
|
|
|
greater = group.nodes.new(type="ShaderNodeMath")
|
|
greater.operation = "GREATER_THAN"
|
|
greater.inputs[1].default_value = 0
|
|
greater.location = 400, 0
|
|
|
|
compare = group.nodes.new(type="ShaderNodeMath")
|
|
compare.operation = "COMPARE"
|
|
compare.inputs[1].default_value = 0
|
|
compare.inputs[2].default_value = 0.04
|
|
compare.location = 400, -200
|
|
|
|
multiply = group.nodes.new(type="ShaderNodeMath")
|
|
multiply.operation = "MULTIPLY"
|
|
multiply.inputs[0].default_value = 1
|
|
multiply.location = 600, 150
|
|
|
|
add_line_decorator = group.nodes.new(type="ShaderNodeMath")
|
|
add_line_decorator.operation = "ADD"
|
|
add_line_decorator.location = 600, -200
|
|
|
|
multiply_line_decorator = group.nodes.new(type="ShaderNodeMath")
|
|
multiply_line_decorator.operation = "MULTIPLY"
|
|
multiply_line_decorator.location = 800, -200
|
|
|
|
group_output = group.nodes.new(type="NodeGroupOutput")
|
|
group_output.location = 1000, 0
|
|
|
|
group.links.new(group_input.outputs["Value"], multiply.inputs[0])
|
|
group.links.new(group_input.outputs["Vector"], separate_xyz.inputs[0])
|
|
group.links.new(separate_xyz.outputs[2], greater.inputs[0])
|
|
group.links.new(greater.outputs[0], multiply.inputs[1])
|
|
group.links.new(multiply.outputs[0], group_output.inputs["Value"])
|
|
group.links.new(separate_xyz.outputs[2], compare.inputs[0])
|
|
group.links.new(compare.outputs[0], add_line_decorator.inputs[0])
|
|
group.links.new(group_input.outputs["Line Decorator"], add_line_decorator.inputs[1])
|
|
group.links.new(multiply.outputs[0], multiply_line_decorator.inputs[0])
|
|
group.links.new(add_line_decorator.outputs[0], multiply_line_decorator.inputs[1])
|
|
group.links.new(multiply_line_decorator.outputs[0], group_output.inputs["Line Decorator"])
|
|
|
|
def create_section_override_node(self, obj, context):
|
|
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
|
|
if bpy.app.version >= (4, 0, 0):
|
|
group.interface.new_socket(name="Shader", in_out="INPUT", socket_type="NodeSocketShader")
|
|
group.interface.new_socket(name="Shader", in_out="OUTPUT", socket_type="NodeSocketShader")
|
|
else:
|
|
group.inputs.new("NodeSocketShader", "Shader")
|
|
group.outputs.new("NodeSocketShader", "Shader")
|
|
links = group.links
|
|
nodes = group.nodes
|
|
|
|
group_input = nodes.new(type="NodeGroupInput")
|
|
group_output = nodes.new(type="NodeGroupOutput")
|
|
group_output.location = 800, 250
|
|
|
|
mix_decorator = group.nodes.new(type="ShaderNodeMixShader")
|
|
mix_decorator.name = "Line Decorator Mix"
|
|
mix_decorator.inputs[0].default_value = 0 # Directly pass input shader when there is no cutaway
|
|
mix_decorator.location = group_output.location - Vector((200, 0))
|
|
|
|
mix_section = group.nodes.new(type="ShaderNodeMixShader")
|
|
mix_section.name = "Section Mix"
|
|
mix_section.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
|
|
mix_section.location = mix_decorator.location - Vector((200, 200))
|
|
|
|
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
|
|
transparent.location = mix_section.location - Vector((200, 100))
|
|
|
|
mix_backfacing = nodes.new(type="ShaderNodeMixShader")
|
|
mix_backfacing.location = mix_section.location - Vector((200, 0))
|
|
|
|
group_input.location = mix_backfacing.location - Vector((200, 50))
|
|
|
|
backfacing = nodes.new(type="ShaderNodeNewGeometry")
|
|
backfacing.location = mix_backfacing.location + Vector((-200, 200))
|
|
|
|
emission = nodes.new(type="ShaderNodeEmission")
|
|
emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1]
|
|
emission.location = mix_backfacing.location - Vector((200, 150))
|
|
|
|
cut_obj = nodes.new(type="ShaderNodeTexCoord")
|
|
cut_obj.object = obj
|
|
cut_obj.location = backfacing.location - Vector((200, 200))
|
|
|
|
section_compare = nodes.new(type="ShaderNodeGroup")
|
|
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
|
|
section_compare.name = "Last Section Compare"
|
|
section_compare.location = backfacing.location + Vector((0, 200))
|
|
|
|
links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
|
|
links.new(backfacing.outputs["Backfacing"], mix_backfacing.inputs[0])
|
|
links.new(group_input.outputs["Shader"], mix_backfacing.inputs[1])
|
|
links.new(emission.outputs["Emission"], mix_backfacing.inputs[2])
|
|
links.new(section_compare.outputs["Value"], mix_section.inputs[0])
|
|
links.new(transparent.outputs[0], mix_section.inputs[1])
|
|
links.new(mix_backfacing.outputs["Shader"], mix_section.inputs[2])
|
|
links.new(section_compare.outputs["Line Decorator"], mix_decorator.inputs[0])
|
|
links.new(mix_section.outputs["Shader"], mix_decorator.inputs[1])
|
|
links.new(mix_decorator.outputs["Shader"], group_output.inputs["Shader"])
|
|
|
|
def append_obj_to_section_override_node(self, obj):
|
|
group = bpy.data.node_groups.get("Section Override")
|
|
try:
|
|
last_section_node = next(
|
|
n
|
|
for n in group.nodes
|
|
if isinstance(n, bpy.types.ShaderNodeGroup)
|
|
and n.node_tree.name == "Section Compare"
|
|
and not n.inputs[0].links
|
|
)
|
|
offset = Vector((0, 0))
|
|
except StopIteration:
|
|
last_section_node = group.nodes.get("Section Mix")
|
|
offset = Vector((200, 0))
|
|
section_compare = group.nodes.new(type="ShaderNodeGroup")
|
|
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
|
|
section_compare.location = last_section_node.location - Vector((200, 0)) - offset
|
|
|
|
cut_obj = group.nodes.new(type="ShaderNodeTexCoord")
|
|
cut_obj.object = obj
|
|
cut_obj.location = last_section_node.location - Vector((400, 150)) - offset
|
|
|
|
group.links.new(section_compare.outputs["Value"], last_section_node.inputs[0])
|
|
group.links.new(
|
|
section_compare.outputs["Line Decorator"],
|
|
(
|
|
last_section_node.inputs["Line Decorator"]
|
|
if "Line Decorator" in last_section_node.inputs
|
|
else group.nodes.get("Line Decorator Mix").inputs[0]
|
|
),
|
|
)
|
|
group.links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
|
|
|
|
section_compare.name = "Last Section Compare"
|
|
|
|
def add_default_material_if_none_exists(self, context):
|
|
material = bpy.data.materials.get("Section Override")
|
|
if not material:
|
|
material = bpy.data.materials.new("Section Override")
|
|
material.use_nodes = True
|
|
|
|
if context.scene.BIMProperties.should_section_selected_objects:
|
|
objects = list(context.selected_objects)
|
|
else:
|
|
objects = list(context.visible_objects)
|
|
|
|
for obj in objects:
|
|
aggregate = obj.instance_collection
|
|
if aggregate and "IfcRelAggregates/" in aggregate.name:
|
|
for part in aggregate.objects:
|
|
objects.append(part)
|
|
if not (obj.data and hasattr(obj.data, "materials") and obj.data.materials and obj.data.materials[0]):
|
|
if obj.data and hasattr(obj.data, "materials"):
|
|
if len(obj.material_slots):
|
|
obj.material_slots[0].material = material
|
|
else:
|
|
obj.data.materials.append(material)
|
|
|
|
def override_materials(self):
|
|
override = bpy.data.node_groups.get("Section Override")
|
|
for material in bpy.data.materials:
|
|
material.use_nodes = True
|
|
if material.node_tree.nodes.get("Section Override"):
|
|
continue
|
|
material.blend_method = "HASHED"
|
|
material.shadow_method = "HASHED"
|
|
material_output = tool.Blender.get_material_node(material, "OUTPUT_MATERIAL", {"is_active_output": True})
|
|
if not material_output:
|
|
continue
|
|
from_socket = material_output.inputs[0].links[0].from_socket
|
|
section_override = material.node_tree.nodes.new(type="ShaderNodeGroup")
|
|
section_override.name = "Section Override"
|
|
section_override.node_tree = override
|
|
material.node_tree.links.new(from_socket, section_override.inputs[0])
|
|
material.node_tree.links.new(section_override.outputs[0], material_output.inputs[0])
|
|
|
|
|
|
class BIM_OT_remove_section_plane(bpy.types.Operator):
|
|
"""Remove selected section plane. No effect if executed on a regular object"""
|
|
|
|
bl_idname = "bim.remove_section_plane"
|
|
bl_label = "Remove Temporary Section Cutaway"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.active_object and bpy.data.node_groups.get("Section Override")
|
|
|
|
def execute(self, context):
|
|
name = context.active_object.name
|
|
section_override = bpy.data.node_groups.get("Section Override")
|
|
tex_coords = next(
|
|
(
|
|
n
|
|
for n in section_override.nodes
|
|
if isinstance(n, bpy.types.ShaderNodeTexCoord) and n.object.name == name
|
|
),
|
|
None,
|
|
)
|
|
if tex_coords is not None:
|
|
section_compare = tex_coords.outputs["Object"].links[0].to_node
|
|
if section_compare.inputs[0].links:
|
|
previous_section_compare = section_compare.inputs[0].links[0].from_node
|
|
next_section_compare = section_compare.outputs[0].links[0].to_node
|
|
section_override.links.new(previous_section_compare.outputs[0], next_section_compare.inputs[0])
|
|
section_override.links.new(
|
|
previous_section_compare.outputs[1],
|
|
(
|
|
next_section_compare.inputs["Line Decorator"]
|
|
if "Line Decorator" in next_section_compare.inputs
|
|
else next_section_compare.inputs[0]
|
|
),
|
|
)
|
|
self.offset_previous_nodes(section_compare, offset_x=200)
|
|
section_override.nodes.remove(section_compare)
|
|
section_override.nodes.remove(tex_coords)
|
|
bpy.data.objects.remove(context.active_object)
|
|
|
|
return {"FINISHED"}
|
|
|
|
def offset_previous_nodes(self, section_compare, offset_x=0, offset_y=0):
|
|
if section_compare.inputs[0].links:
|
|
previous_section_compare = section_compare.inputs[0].links[0].from_node
|
|
previous_section_compare.location += Vector((offset_x, offset_y))
|
|
if previous_section_compare.inputs["Vector"].links:
|
|
previous_section_compare.inputs["Vector"].links[0].from_node.location += Vector((offset_x, offset_y))
|
|
self.offset_previous_nodes(previous_section_compare, offset_x, offset_y)
|
|
|
|
def purge_all_section_data(self, context):
|
|
bpy.data.materials.remove(bpy.data.materials.get("Section Override"))
|
|
for material in bpy.data.materials:
|
|
if not material.node_tree:
|
|
continue
|
|
override = material.node_tree.nodes.get("Section Override")
|
|
if not override:
|
|
continue
|
|
material.node_tree.links.new(
|
|
override.inputs[0].links[0].from_socket, override.outputs[0].links[0].to_socket
|
|
)
|
|
material.node_tree.nodes.remove(override)
|
|
bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Override"))
|
|
bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Compare"))
|
|
with context.temp_override(selected_objects=[context.active_object]):
|
|
bpy.ops.object.delete()
|
|
|
|
|
|
class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.reload_ifc_file"
|
|
bl_label = "Reload IFC File"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Reload an updated IFC file"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
|
|
|
def _execute(self, context):
|
|
import ifcdiff
|
|
|
|
old = tool.Ifc.get()
|
|
new = ifcopenshell.open(self.filepath)
|
|
|
|
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=[])
|
|
ifc_diff.diff()
|
|
|
|
changed_elements = set([k for k, v in ifc_diff.change_register.items() if "geometry_changed" in v])
|
|
|
|
for global_id in ifc_diff.deleted_elements | changed_elements:
|
|
element = tool.Ifc.get().by_guid(global_id)
|
|
obj = tool.Ifc.get_object(element)
|
|
if obj:
|
|
bpy.data.objects.remove(obj)
|
|
|
|
# STEP IDs may change, but we assume the GlobalID to be constant
|
|
obj_map = {}
|
|
for obj in bpy.data.objects:
|
|
if obj.library:
|
|
continue
|
|
element = tool.Ifc.get_entity(obj)
|
|
if element and hasattr(element, "GlobalId"):
|
|
obj_map[obj.name] = element.GlobalId
|
|
|
|
delta_elements = [new.by_guid(global_id) for global_id in ifc_diff.added_elements | changed_elements]
|
|
tool.Ifc.set(new)
|
|
|
|
for obj in bpy.data.objects:
|
|
if obj.library:
|
|
continue
|
|
global_id = obj_map.get(obj.name)
|
|
if global_id:
|
|
try:
|
|
tool.Ifc.link(new.by_guid(global_id), obj)
|
|
except:
|
|
# Still prototyping, so things like types definitely won't work
|
|
print("Could not relink", obj)
|
|
|
|
start = time.time()
|
|
logger = logging.getLogger("ImportIFC")
|
|
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
|
|
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
|
|
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
|
|
logging.basicConfig(
|
|
filename=path_log,
|
|
filemode="a",
|
|
level=logging.DEBUG,
|
|
)
|
|
settings = import_ifc.IfcImportSettings.factory(context, self.filepath, logger)
|
|
settings.has_filter = True
|
|
settings.should_filter_spatial_elements = False
|
|
settings.elements = delta_elements
|
|
settings.logger.info("Starting import")
|
|
ifc_importer = import_ifc.IfcImporter(settings)
|
|
ifc_importer.execute()
|
|
settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start))
|
|
print("Import finished in {:.2f} seconds".format(time.time() - start))
|
|
|
|
context.scene.BIMProperties.ifc_file = self.filepath
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class AddIfcFile(bpy.types.Operator):
|
|
bl_idname = "bim.add_ifc_file"
|
|
bl_label = "Add IFC File"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
context.scene.DocProperties.ifc_files.add()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class RemoveIfcFile(bpy.types.Operator):
|
|
bl_idname = "bim.remove_ifc_file"
|
|
bl_label = "Remove IFC File"
|
|
index: bpy.props.IntProperty()
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
context.scene.DocProperties.ifc_files.remove(self.index)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BIM_OT_open_webbrowser(bpy.types.Operator):
|
|
bl_idname = "bim.open_webbrowser"
|
|
bl_description = "Open the URL in your Web Browser"
|
|
bl_label = "Open URL"
|
|
|
|
url: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
import webbrowser
|
|
|
|
webbrowser.open(self.url)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SelectExternalMaterialDir(bpy.types.Operator):
|
|
bl_idname = "bim.select_external_material_dir"
|
|
bl_label = "Select Material File"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
|
|
def execute(self, context):
|
|
# TODO: this is dead code, awaiting reimplementation. See #1222.
|
|
context.active_object.active_material.BIMMaterialProperties.location = self.filepath
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class FetchExternalMaterial(bpy.types.Operator):
|
|
bl_idname = "bim.fetch_external_material"
|
|
bl_label = "Fetch External Material"
|
|
|
|
def execute(self, context):
|
|
# TODO: this is dead code, awaiting reimplementation. See #1222.
|
|
location = context.active_object.active_material.BIMMaterialProperties.location
|
|
if location[-6:] != ".mpass":
|
|
return {"FINISHED"}
|
|
if not os.path.isabs(location):
|
|
location = os.path.join(context.scene.BIMProperties.data_dir, location)
|
|
with open(location) as f:
|
|
self.material_pass = json.load(f)
|
|
if context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass:
|
|
self.fetch_eevee_or_cycles("eevee", context)
|
|
elif context.scene.render.engine == "CYCLES" and "cycles" in self.material_pass:
|
|
self.fetch_eevee_or_cycles("cycles", context)
|
|
return {"FINISHED"}
|
|
|
|
def fetch_eevee_or_cycles(self, name, context):
|
|
identification = context.active_object.active_material.BIMMaterialProperties.identification
|
|
uri = self.material_pass[name]["uri"]
|
|
if not os.path.isabs(uri):
|
|
uri = os.path.join(context.scene.BIMProperties.data_dir, uri)
|
|
bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material"))
|
|
for material in bpy.data.materials:
|
|
if material.name == identification and material.library:
|
|
context.active_object.material_slots[0].material = material
|
|
return
|
|
|
|
|
|
class FetchObjectPassport(bpy.types.Operator):
|
|
bl_idname = "bim.fetch_object_passport"
|
|
bl_label = "Fetch Object Passport"
|
|
|
|
def execute(self, context):
|
|
# TODO: this is dead code, awaiting reimplementation. See #1222.
|
|
for reference in context.active_object.BIMObjectProperties.document_references:
|
|
reference = context.scene.BIMProperties.document_references[reference.name]
|
|
if reference.location[-6:] == ".blend":
|
|
self.fetch_blender(reference, context)
|
|
return {"FINISHED"}
|
|
|
|
def fetch_blender(self, reference, context):
|
|
bpy.ops.wm.link(filename=reference.name, directory=os.path.join(reference.location, "Mesh"))
|
|
context.active_object.data = bpy.data.meshes[reference.name]
|
|
|
|
|
|
def update_enum_property_search_prop(self, context):
|
|
for i, prop in enumerate(self.collection_names):
|
|
if prop.name == self.dummy_name:
|
|
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
|
|
predefined_type = self.collection_predefined_types[i].name
|
|
if self.first_launch:
|
|
self.first_launch = False
|
|
else:
|
|
context.window.screen = context.window.screen
|
|
if predefined_type:
|
|
try:
|
|
setattr(context.data, "ifc_predefined_type", predefined_type)
|
|
except TypeError: # User clicked on a suggestion, but it's not a predefined type
|
|
pass
|
|
break
|
|
|
|
|
|
class BIM_OT_enum_property_search(bpy.types.Operator):
|
|
bl_idname = "bim.enum_property_search"
|
|
bl_label = "Search For Property"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
|
dummy_name: bpy.props.StringProperty(name="Property", update=update_enum_property_search_prop)
|
|
collection_names: bpy.props.CollectionProperty(type=StrProperty)
|
|
collection_identifiers: bpy.props.CollectionProperty(type=StrProperty)
|
|
collection_predefined_types: bpy.props.CollectionProperty(type=StrProperty)
|
|
prop_name: bpy.props.StringProperty()
|
|
|
|
def invoke(self, context, event):
|
|
self.clear_collections()
|
|
self.data = context.data
|
|
items = get_enum_items(self.data, self.prop_name, context)
|
|
if items is None:
|
|
return {"FINISHED"}
|
|
self.add_items_regular(items)
|
|
self.add_items_suggestions()
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
# Mandatory to access context.data in update :
|
|
self.layout.context_pointer_set(name="data", data=self.data)
|
|
self.layout.prop_search(self, "dummy_name", self, "collection_names")
|
|
|
|
def execute(self, context):
|
|
return {"FINISHED"}
|
|
|
|
def clear_collections(self):
|
|
self.collection_names.clear()
|
|
self.collection_identifiers.clear()
|
|
|
|
def add_item(self, identifier: str, name: str, predefined_type: str = ""):
|
|
self.collection_identifiers.add().name = identifier
|
|
self.collection_names.add().name = name
|
|
self.collection_predefined_types.add().name = predefined_type
|
|
|
|
def add_items_regular(self, items):
|
|
self.identifiers = []
|
|
for item in items:
|
|
self.identifiers.append(item[0])
|
|
self.add_item(identifier=item[0], name=item[1])
|
|
if item[0] == getattr(self.data, self.prop_name):
|
|
self.dummy_name = item[1] # We found the current enum name
|
|
|
|
def add_items_suggestions(self):
|
|
getter_suggestions = getattr(self.data, "getter_enum_suggestions", None)
|
|
if getter_suggestions is not None:
|
|
mapping = getter_suggestions.get(self.prop_name)
|
|
if mapping is None:
|
|
return
|
|
for key, suggestions in mapping().items():
|
|
if key in self.identifiers:
|
|
if not isinstance(suggestions, (tuple, list)):
|
|
suggestions = [suggestions]
|
|
for suggestion in suggestions:
|
|
predefined_type = suggestion.get("predefined_type", "NOTDEFINED").upper()
|
|
name = suggestion.get("name")
|
|
self.add_item(
|
|
identifier=key,
|
|
name=f"{key} > {name if name else predefined_type }",
|
|
predefined_type=predefined_type,
|
|
)
|
|
|
|
|
|
class EditBlenderCollection(bpy.types.Operator):
|
|
bl_idname = "bim.edit_blender_collection"
|
|
bl_label = "Add or Remove Blender Collection Item"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
option: bpy.props.StringProperty(description="add or remove item from collection")
|
|
collection: bpy.props.StringProperty(description="collection to be edited")
|
|
index: bpy.props.IntProperty(description="index of item to be removed")
|
|
|
|
def execute(self, context):
|
|
if self.option == "add":
|
|
getattr(context.bim_prop_group, self.collection).add()
|
|
else:
|
|
getattr(context.bim_prop_group, self.collection).remove(self.index)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BIM_OT_show_description(bpy.types.Operator):
|
|
bl_idname = "bim.show_description"
|
|
bl_label = "Description"
|
|
attr_name: bpy.props.StringProperty()
|
|
description: bpy.props.StringProperty()
|
|
url: bpy.props.StringProperty()
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=450)
|
|
|
|
def execute(self, context):
|
|
return {"FINISHED"}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wrapper = textwrap.TextWrapper(width=80)
|
|
for line in wrapper.wrap(self.attr_name + " : " + self.description):
|
|
layout.label(text=line)
|
|
if self.url:
|
|
url_op = layout.operator("bim.open_webbrowser", icon="URL", text="Online IFC Documentation")
|
|
url_op.url = self.url
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return properties.description
|
|
|
|
|
|
CuttingPlaneData = namedtuple("CuttingPlaneData", ["co", "normal"])
|
|
|
|
|
|
class ClippingPlaneCutWithCappings(bpy.types.Operator):
|
|
bl_idname = "bim.clipping_plane_cut_with_cappings"
|
|
bl_label = "Cut With Clipping Planes"
|
|
bl_description = "Cut selected objects with clipping planes and create cappings"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes]
|
|
if not cutting_planes:
|
|
self.report({"INFO"}, "No cutting planes found.")
|
|
return {"FINISHED"}
|
|
cutting_planes_data = self.get_cutting_plane_data(cutting_planes)
|
|
|
|
wm = context.window_manager
|
|
objects_processed, t0 = 0, time.time()
|
|
wm.progress_begin(0, len(context.selected_objects))
|
|
for obj_i, obj in enumerate(context.selected_objects):
|
|
if obj.type != "MESH":
|
|
continue
|
|
|
|
if obj in cutting_planes:
|
|
continue
|
|
|
|
RevertClippingPlaneCut.revert_object_mesh(self, obj)
|
|
|
|
# localize matrix to consider object's transform
|
|
ws_to_ls = obj.matrix_world.inverted()
|
|
rotation = ws_to_ls.to_quaternion()
|
|
|
|
mesh = obj.data
|
|
bm = tool.Blender.get_bmesh_for_mesh(mesh)
|
|
object_changed = False
|
|
|
|
for plane_data in cutting_planes_data:
|
|
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
|
plane_co = ws_to_ls @ plane_data.co
|
|
plane_no = rotation @ plane_data.normal
|
|
# clear_outer -> remove everything in direction of the normal
|
|
results = bmesh.ops.bisect_plane(
|
|
bm,
|
|
geom=geom,
|
|
dist=10e-4,
|
|
plane_co=plane_co,
|
|
plane_no=plane_no,
|
|
clear_outer=True,
|
|
)
|
|
edges_to_fill = [e for e in results["geom_cut"] if isinstance(e, bmesh.types.BMEdge)]
|
|
object_changed = object_changed or edges_to_fill
|
|
bmesh.ops.contextual_create(bm, geom=edges_to_fill)
|
|
|
|
# don't swap mesh if it wasn't affected by any of the cutting planes
|
|
if object_changed:
|
|
temp_mesh = bpy.data.meshes.new("temp_cut")
|
|
temp_mesh.BIMMeshProperties.replaced_mesh = mesh
|
|
for material in mesh.materials:
|
|
temp_mesh.materials.append(material)
|
|
obj.data = temp_mesh
|
|
tool.Blender.apply_bmesh(temp_mesh, bm, obj)
|
|
|
|
objects_processed += 1
|
|
wm.progress_update(obj_i)
|
|
|
|
self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec")
|
|
|
|
return {"FINISHED"}
|
|
|
|
def get_cutting_plane_data(self, cutting_planes: List[bpy.types.Object]) -> List[CuttingPlaneData]:
|
|
cutting_planes_data = []
|
|
|
|
for obj in cutting_planes:
|
|
cutting_matrix = obj.matrix_world
|
|
plane_data = CuttingPlaneData(cutting_matrix.translation, cutting_matrix.col[2].to_3d())
|
|
cutting_planes_data.append(plane_data)
|
|
|
|
return cutting_planes_data
|
|
|
|
# NOTE: unused, will be used later for cutting boxes support
|
|
def get_box_cutting_plane_data(self, obj: bpy.types.Object) -> List[CuttingPlaneData]:
|
|
matrix_world = obj.matrix_world
|
|
rotation = matrix_world.to_quaternion() # avoid scale for normals
|
|
cutting_planes_data = []
|
|
for p in obj.data.polygons:
|
|
plane_data = CuttingPlaneData(matrix_world @ p.center, rotation @ p.normal)
|
|
cutting_planes_data.append(plane_data)
|
|
return cutting_planes_data
|
|
|
|
|
|
class RevertClippingPlaneCut(bpy.types.Operator):
|
|
bl_idname = "bim.revert_clipping_plane_cut"
|
|
bl_label = "Revert Clipping Plane Cut"
|
|
bl_description = "Revert clipping plane cut (switch back to the original mesh)"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
wm = context.window_manager
|
|
|
|
objects_processed, t0 = 0, time.time()
|
|
wm.progress_begin(0, len(context.selected_objects))
|
|
for obj_i, obj in enumerate(context.selected_objects):
|
|
if obj.type != "MESH":
|
|
continue
|
|
self.revert_object_mesh(obj)
|
|
objects_processed += 1
|
|
wm.progress_update(obj_i)
|
|
wm.progress_end()
|
|
|
|
self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec")
|
|
return {"FINISHED"}
|
|
|
|
def revert_object_mesh(self, obj):
|
|
mesh = obj.data
|
|
replaced_mesh = mesh.BIMMeshProperties.replaced_mesh
|
|
if replaced_mesh:
|
|
obj.data = replaced_mesh
|
|
tool.Blender.remove_data_block(mesh, do_unlink=False)
|