mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| feba8af4c7 | |||
| 0842df0bde | |||
| 0f1b00769e | |||
| dbd3cb6b29 | |||
| 2558518c00 | |||
| ce328a049a | |||
| 8d44b0ac4c | |||
| e0eea85cfb | |||
| 6f56d78e83 | |||
| 0c4bcb8a12 | |||
| 67753a0a3c | |||
| d02239902f | |||
| d7a7701005 | |||
| a97678d30e | |||
| 6b82cb4d5d | |||
| a3006eb028 | |||
| 48bf67f82e | |||
| 6065c0e2fc | |||
| 847f5ff4c8 | |||
| 45278467d3 | |||
| 2af1e2196f | |||
| 5c5f078221 | |||
| a5cf4cd421 | |||
| 911b7153d7 | |||
| 6cec57816e | |||
| c90f9c374f | |||
| 6caacd6024 | |||
| 2cb280932c | |||
| e089d4c78a | |||
| 155f0abbfe | |||
| 77d9688296 | |||
| 09efd68e6e | |||
| 0b3e9e7c5a | |||
| b658b74b90 | |||
| 47dd8ec4dd | |||
| 9ba1596e30 | |||
| 930020bf05 | |||
| 1fe69e5f19 | |||
| 988c8f16f3 | |||
| 080b628df5 | |||
| d0333ac7cd | |||
| e95e16c102 |
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
from . import ui, prop, operator, workspace, gizmo
|
||||
|
||||
classes = (
|
||||
operator.AppendEntireLibrary,
|
||||
@@ -25,18 +25,25 @@ classes = (
|
||||
operator.AppendLibraryElementByQuery,
|
||||
operator.AssignLibraryDeclaration,
|
||||
operator.ChangeLibraryElement,
|
||||
operator.CreateClippingPlane,
|
||||
operator.CreateProject,
|
||||
operator.DisableCulling,
|
||||
operator.DisableEditingHeader,
|
||||
operator.EditHeader,
|
||||
operator.EnableCulling,
|
||||
operator.EnableEditingHeader,
|
||||
operator.ExportIFC,
|
||||
operator.ImportIFC,
|
||||
operator.LinkIfc,
|
||||
operator.LoadLink,
|
||||
operator.LoadLinkedProject,
|
||||
operator.LoadProject,
|
||||
operator.LoadProjectElements,
|
||||
operator.NewProject,
|
||||
operator.QueryLinkedElement,
|
||||
operator.RefreshClippingPlanes,
|
||||
operator.RefreshLibrary,
|
||||
operator.ReloadLink,
|
||||
operator.RevertProject,
|
||||
operator.RewindLibrary,
|
||||
operator.SaveLibraryFile,
|
||||
@@ -48,7 +55,7 @@ classes = (
|
||||
operator.UnlinkIfc,
|
||||
operator.UnloadLink,
|
||||
operator.UnloadProject,
|
||||
operator.ReloadLink,
|
||||
workspace.QueryHotkey,
|
||||
prop.LibraryElement,
|
||||
prop.FilterCategory,
|
||||
prop.Link,
|
||||
@@ -62,6 +69,7 @@ classes = (
|
||||
ui.BIM_UL_library,
|
||||
ui.BIM_UL_filter_categories,
|
||||
ui.BIM_UL_links,
|
||||
gizmo.ClippingPlane,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +77,8 @@ addon_keymaps = []
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.QueryTool, after={"builtin.select"}, separator=True, group=False)
|
||||
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
|
||||
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
|
||||
bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu)
|
||||
@@ -88,6 +98,8 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.QueryTool)
|
||||
bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
|
||||
bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu)
|
||||
del bpy.types.Scene.BIMProjectProperties
|
||||
|
||||
@@ -24,6 +24,7 @@ from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
def refresh():
|
||||
ProjectData.is_loaded = False
|
||||
LinksData.is_loaded = False
|
||||
|
||||
|
||||
class ProjectData:
|
||||
@@ -61,3 +62,9 @@ class ProjectData:
|
||||
return f"{save_date} {':'.join(save_time.split(':')[0:2])}"
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
class LinksData:
|
||||
linked_data = {}
|
||||
enable_culling = False
|
||||
is_loaded = False
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 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 gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
class ProjectDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
selected_tris = []
|
||||
|
||||
try:
|
||||
obj = context.scene.BIMProjectProperties.queried_obj
|
||||
selected_vertices = obj["selected_vertices"]
|
||||
selected_edges = obj["selected_edges"]
|
||||
selected_tris = obj["selected_tris"]
|
||||
except:
|
||||
return
|
||||
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
|
||||
|
||||
class ClippingPlaneDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
selected_tris = []
|
||||
unselected_vertices = []
|
||||
unselected_edges = []
|
||||
unselected_tris = []
|
||||
|
||||
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
|
||||
obj = clipping_plane.obj
|
||||
if not obj or not obj.data:
|
||||
continue
|
||||
|
||||
if obj.mode == "EDIT":
|
||||
continue # A profile decorator or something else is used here.
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
obj.data.calc_loop_triangles()
|
||||
|
||||
if obj.select_get():
|
||||
offset = len(selected_vertices)
|
||||
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
else:
|
||||
offset = len(unselected_vertices)
|
||||
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
|
||||
verts = [
|
||||
tuple(obj.matrix_world @ Vector((0, 0, 0))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0, -0.5))),
|
||||
tuple(obj.matrix_world @ Vector((-0.05, 0, -0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0.05, 0, -0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, -0.05, -0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0.05, -0.45))),
|
||||
]
|
||||
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
|
||||
color = selected_elements_color if obj in context.selected_objects else special_elements_color
|
||||
self.draw_batch("LINES", verts, color, edges)
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
@@ -0,0 +1,67 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import GizmoGroup
|
||||
from mathutils import Matrix
|
||||
|
||||
|
||||
class ClippingPlane(GizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_clipping_plane"
|
||||
bl_label = "Clipping Plane"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return (
|
||||
context.selected_objects
|
||||
and obj.name.startswith("ClippingPlane")
|
||||
and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes]
|
||||
)
|
||||
|
||||
def setup(self, context):
|
||||
self.obj = None
|
||||
self.offset = 0
|
||||
self.mw = Matrix()
|
||||
self.last_mw = Matrix()
|
||||
|
||||
self.gizmo = self.gizmos.new("GIZMO_GT_arrow_3d")
|
||||
|
||||
def move_get_x():
|
||||
return self.offset
|
||||
|
||||
def move_set_x(value):
|
||||
self.obj.matrix_world.col[3] = self.mw.col[3] + (self.mw.col[2] * self.offset)
|
||||
self.last_mw = self.obj.matrix_world.copy()
|
||||
self.offset = value
|
||||
|
||||
self.gizmo.target_set_handler("offset", get=move_get_x, set=move_set_x)
|
||||
|
||||
def refresh(self, context):
|
||||
if self.obj != context.object or self.last_mw != context.object.matrix_world:
|
||||
self.obj = context.object
|
||||
self.offset = 0
|
||||
self.mw = context.object.matrix_world.copy()
|
||||
obj = context.object
|
||||
mw = context.object.matrix_world.normalized()
|
||||
mw.col[3] -= mw.col[2] * self.offset
|
||||
self.gizmo.matrix_basis = mw
|
||||
@@ -21,6 +21,8 @@ import bpy
|
||||
import time
|
||||
import logging
|
||||
import tempfile
|
||||
import subprocess
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.selector
|
||||
@@ -36,7 +38,10 @@ from blenderbim.bim.ui import IFCFileSelector
|
||||
from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim import export_ifc
|
||||
from pathlib import Path
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.module.project.data import LinksData
|
||||
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
|
||||
|
||||
|
||||
class NewProject(bpy.types.Operator):
|
||||
@@ -811,16 +816,17 @@ class ToggleFilterCategories(bpy.types.Operator):
|
||||
|
||||
class LinkIfc(bpy.types.Operator):
|
||||
bl_idname = "bim.link_ifc"
|
||||
bl_label = "Link Blend/IFC File"
|
||||
bl_label = "Link IFC"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "This will link the Blender file that is synced with the IFC file"
|
||||
bl_description = "Reference in a read-only IFC model in the background"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
|
||||
directory: bpy.props.StringProperty(subtype="DIR_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.blend;*.blend1", options={"HIDDEN"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
|
||||
def execute(self, context):
|
||||
start = time.time()
|
||||
files = [f.name for f in self.files] if self.files else [self.filepath]
|
||||
for filename in files:
|
||||
filepath = os.path.join(self.directory, filename)
|
||||
@@ -832,6 +838,7 @@ class LinkIfc(bpy.types.Operator):
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
|
||||
new.name = filepath
|
||||
bpy.ops.bim.load_link(filepath=filepath)
|
||||
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -887,6 +894,13 @@ class LoadLink(bpy.types.Operator):
|
||||
filepath = self.filepath
|
||||
if not os.path.isabs(filepath):
|
||||
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath))
|
||||
if self.filepath.lower().endswith(".blend"):
|
||||
self.link_blend(filepath)
|
||||
elif self.filepath.lower().endswith(".ifc"):
|
||||
self.link_ifc()
|
||||
return {"FINISHED"}
|
||||
|
||||
def link_blend(self, filepath):
|
||||
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
|
||||
data_to.scenes = data_from.scenes
|
||||
for scene in bpy.data.scenes:
|
||||
@@ -896,9 +910,36 @@ class LoadLink(bpy.types.Operator):
|
||||
if "IfcProject" not in child.name:
|
||||
continue
|
||||
bpy.data.scenes[0].collection.children.link(child)
|
||||
link = context.scene.BIMProjectProperties.links.get(self.filepath)
|
||||
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
|
||||
link.is_loaded = True
|
||||
return {"FINISHED"}
|
||||
|
||||
def link_ifc(self):
|
||||
blend_filepath = self.filepath + ".cache.blend"
|
||||
h5_filepath = self.filepath + ".cache.h5"
|
||||
|
||||
if not os.path.exists(blend_filepath):
|
||||
code = f"""
|
||||
import bpy
|
||||
|
||||
def run():
|
||||
bpy.ops.bim.load_linked_project(filepath="{self.filepath}")
|
||||
bpy.ops.wm.save_as_mainfile(filepath="{blend_filepath}")
|
||||
|
||||
try:
|
||||
run()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
||||
temp_file.write(code)
|
||||
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
|
||||
if run.returncode == 1:
|
||||
print("An error occurred while processing your IFC.")
|
||||
|
||||
self.link_blend(blend_filepath)
|
||||
|
||||
|
||||
class ReloadLink(bpy.types.Operator):
|
||||
@@ -1130,3 +1171,708 @@ class ImportIFC(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.load_project("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadLinkedProject(bpy.types.Operator):
|
||||
bl_idname = "bim.load_linked_project"
|
||||
bl_label = "Load a project for viewing only."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
import ifcpatch
|
||||
import multiprocessing
|
||||
import ifcopenshell.geom
|
||||
|
||||
start = time.time()
|
||||
|
||||
self.filepath = self.filepath or "/home/dion/drive/ifcs/racbasicsampleproject.ifc"
|
||||
print("Processing", self.filepath)
|
||||
|
||||
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
|
||||
self.file = ifcopenshell.open(self.filepath)
|
||||
print("Finished opening")
|
||||
|
||||
self.db_filepath = self.filepath + ".cache.sqlite"
|
||||
db = ifcpatch.execute(
|
||||
{"input": self.filepath, "file": self.file, "recipe": "ExtractPropertiesToSQLite", "arguments": []}
|
||||
)
|
||||
ifcpatch.write(db, self.db_filepath)
|
||||
print("Finished writing property database")
|
||||
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
||||
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
||||
ifc_importer.file = self.file
|
||||
ifc_importer.process_context_filter()
|
||||
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||
self.elements |= set(self.file.by_type("IfcSite"))
|
||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
||||
self.elements = list(self.elements)
|
||||
|
||||
for settings in ifc_importer.context_settings:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings, self.file, multiprocessing.cpu_count(), include=self.elements
|
||||
)
|
||||
self.meshes = {}
|
||||
self.blender_mats = {}
|
||||
blender_mats = {}
|
||||
|
||||
default_mat = np.array([[1, 1, 1, 1]], dtype=np.float32)
|
||||
chunked_guids = []
|
||||
chunked_guid_ids = []
|
||||
chunked_verts = []
|
||||
chunked_faces = []
|
||||
chunked_materials = []
|
||||
chunked_material_ids = []
|
||||
material_offset = 0
|
||||
max_slot_index = 0
|
||||
chunk_size = 10000
|
||||
r4 = np.array([[0, 0, 0, 1]])
|
||||
offset = 0
|
||||
|
||||
ci = 0
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
if len(shape.geometry.faces) > 1000: # 333 tris
|
||||
self.process_occurrence(shape)
|
||||
if not iterator.next():
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
midx = mapping[midx]
|
||||
|
||||
mat_results = []
|
||||
for mat in mats:
|
||||
mat = tuple(mat)
|
||||
blender_mat = blender_mats.get(mat, None)
|
||||
if not blender_mat:
|
||||
blender_mat = bpy.data.materials.new("Chunk")
|
||||
blender_mat.diffuse_color = mat
|
||||
blender_mats[mat] = blender_mat
|
||||
mat_results.append(blender_mat)
|
||||
|
||||
# The left over chunk
|
||||
self.create_object(
|
||||
np.concatenate(chunked_verts),
|
||||
np.concatenate(chunked_faces),
|
||||
mat_results,
|
||||
midx,
|
||||
chunked_guids,
|
||||
chunked_guid_ids,
|
||||
)
|
||||
break
|
||||
continue
|
||||
|
||||
ci += 1
|
||||
if ci % 50 == 0:
|
||||
print("Doing chunk", ci)
|
||||
|
||||
has_processed_chunk = False
|
||||
|
||||
ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))])
|
||||
mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32)
|
||||
chunked_materials.append(ms)
|
||||
chunked_material_ids.append(mi + material_offset + 1)
|
||||
material_offset += len(ms)
|
||||
|
||||
M4 = np.frombuffer(shape.transformation_buffer).reshape((4, 3))
|
||||
M4 = np.concatenate((M4.T, r4))
|
||||
vs = np.frombuffer(shape.geometry.verts_buffer).reshape((-1, 3))
|
||||
vs = np.hstack((vs, np.ones((len(vs), 1))))
|
||||
vs = (np.asmatrix(M4) * np.asmatrix(vs).T).T.A
|
||||
vs = vs[:, :3].flatten()
|
||||
fs = np.frombuffer(shape.geometry.faces_buffer, dtype=np.int32)
|
||||
chunked_verts.append(vs)
|
||||
chunked_faces.append(fs + offset)
|
||||
offset += len(vs) // 3
|
||||
|
||||
chunked_guids.append(shape.guid)
|
||||
if chunked_guid_ids:
|
||||
chunked_guid_ids.append((len(fs) // 3) + chunked_guid_ids[-1])
|
||||
else:
|
||||
chunked_guid_ids.append(len(fs) // 3)
|
||||
|
||||
if offset > chunk_size:
|
||||
has_processed_chunk = True
|
||||
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
midx = mapping[midx]
|
||||
|
||||
mat_results = []
|
||||
for mat in mats:
|
||||
mat = tuple(mat)
|
||||
blender_mat = blender_mats.get(mat, None)
|
||||
if not blender_mat:
|
||||
blender_mat = bpy.data.materials.new("Chunk")
|
||||
blender_mat.diffuse_color = mat
|
||||
blender_mats[mat] = blender_mat
|
||||
mat_results.append(blender_mat)
|
||||
|
||||
self.create_object(
|
||||
np.concatenate(chunked_verts),
|
||||
np.concatenate(chunked_faces),
|
||||
mat_results,
|
||||
midx,
|
||||
chunked_guids,
|
||||
chunked_guid_ids,
|
||||
)
|
||||
chunked_guids = []
|
||||
chunked_guid_ids = []
|
||||
chunked_verts = []
|
||||
chunked_faces = []
|
||||
chunked_materials = []
|
||||
chunked_material_ids = []
|
||||
material_offset = 0
|
||||
max_slot_index = 0
|
||||
offset = 0
|
||||
|
||||
if not iterator.next():
|
||||
if not has_processed_chunk:
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
midx = mapping[midx]
|
||||
|
||||
mat_results = []
|
||||
for mat in mats:
|
||||
mat = tuple(mat)
|
||||
blender_mat = blender_mats.get(mat, None)
|
||||
if not blender_mat:
|
||||
blender_mat = bpy.data.materials.new("Chunk")
|
||||
blender_mat.diffuse_color = mat
|
||||
blender_mats[mat] = blender_mat
|
||||
mat_results.append(blender_mat)
|
||||
|
||||
# The left over chunk
|
||||
self.create_object(
|
||||
np.concatenate(chunked_verts),
|
||||
np.concatenate(chunked_faces),
|
||||
mat_results,
|
||||
midx,
|
||||
chunked_guids,
|
||||
chunked_guid_ids,
|
||||
)
|
||||
break
|
||||
|
||||
bpy.context.scene.collection.children.link(self.collection)
|
||||
print("Finished", time.time() - start)
|
||||
break
|
||||
return {"FINISHED"}
|
||||
|
||||
def process_occurrence(self, shape):
|
||||
element = self.file.by_id(shape.id)
|
||||
matrix = shape.transformation.matrix.data
|
||||
faces = shape.geometry.faces
|
||||
verts = shape.geometry.verts
|
||||
materials = shape.geometry.materials
|
||||
material_ids = shape.geometry.material_ids
|
||||
|
||||
m = shape.transformation.matrix.data
|
||||
mat = np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
|
||||
|
||||
mesh = self.meshes.get(shape.geometry.id, None)
|
||||
if not mesh:
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
|
||||
material_to_slot = {}
|
||||
max_slot_index = 0
|
||||
|
||||
for i, material in enumerate(materials):
|
||||
alpha = 1.0
|
||||
if material.has_transparency and material.transparency > 0:
|
||||
alpha = 1.0 - material.transparency
|
||||
diffuse = material.diffuse + (alpha,)
|
||||
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
|
||||
blender_mat = self.blender_mats.get(material_name, None)
|
||||
if not blender_mat:
|
||||
blender_mat = bpy.data.materials.new(material_name)
|
||||
blender_mat.diffuse_color = diffuse
|
||||
self.blender_mats[material_name] = blender_mat
|
||||
slot_index = mesh.materials.find(material.name)
|
||||
if slot_index == -1:
|
||||
mesh.materials.append(blender_mat)
|
||||
slot_index = max_slot_index
|
||||
max_slot_index += 1
|
||||
material_to_slot[i] = slot_index
|
||||
|
||||
material_index = [(material_to_slot[i] if i != -1 else 0) for i in material_ids]
|
||||
|
||||
num_vertices = len(verts) // 3
|
||||
total_faces = len(faces)
|
||||
loop_start = range(0, total_faces, 3)
|
||||
num_loops = total_faces // 3
|
||||
loop_total = [3] * num_loops
|
||||
num_vertex_indices = len(faces)
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
mesh.loops.add(num_vertex_indices)
|
||||
mesh.loops.foreach_set("vertex_index", faces)
|
||||
mesh.polygons.add(num_loops)
|
||||
mesh.polygons.foreach_set("loop_start", loop_start)
|
||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
||||
mesh.polygons.foreach_set("material_index", material_index)
|
||||
mesh.update()
|
||||
|
||||
self.meshes[shape.geometry.id] = mesh
|
||||
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
|
||||
obj.matrix_world = Matrix(mat.tolist())
|
||||
|
||||
obj["guids"] = [shape.guid]
|
||||
obj["guid_ids"] = [len(mesh.polygons)]
|
||||
obj["db"] = self.db_filepath
|
||||
|
||||
self.collection.objects.link(obj)
|
||||
|
||||
def create_object(self, verts, faces, materials, material_ids, guids, guid_ids):
|
||||
num_vertices = len(verts) // 3
|
||||
if not num_vertices:
|
||||
return
|
||||
total_faces = len(faces)
|
||||
loop_start = range(0, total_faces, 3)
|
||||
num_loops = total_faces // 3
|
||||
loop_total = [3] * num_loops
|
||||
num_vertex_indices = len(faces)
|
||||
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
|
||||
for material in materials:
|
||||
mesh.materials.append(material)
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
mesh.loops.add(num_vertex_indices)
|
||||
mesh.loops.foreach_set("vertex_index", faces)
|
||||
mesh.polygons.add(num_loops)
|
||||
mesh.polygons.foreach_set("loop_start", loop_start)
|
||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
||||
|
||||
if material_ids.size > 0:
|
||||
mesh.polygons.foreach_set("material_index", material_ids)
|
||||
|
||||
mesh.update()
|
||||
|
||||
obj = bpy.data.objects.new("Chunk", mesh)
|
||||
obj["guids"] = list(guids)
|
||||
obj["guid_ids"] = list(guid_ids)
|
||||
obj["db"] = self.db_filepath
|
||||
|
||||
self.collection.objects.link(obj)
|
||||
|
||||
|
||||
class QueryLinkedElement(bpy.types.Operator):
|
||||
bl_idname = "bim.query_linked_element"
|
||||
bl_label = "Query Linked Element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area.type == "VIEW_3D"
|
||||
|
||||
def execute(self, context):
|
||||
import sqlite3
|
||||
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
||||
|
||||
LinksData.linked_data = {}
|
||||
bpy.context.scene.BIMProjectProperties.queried_obj = None
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
region.tag_redraw()
|
||||
elif area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
coord = (self.mouse_x, self.mouse_y)
|
||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
|
||||
if "guids" not in obj:
|
||||
self.report({"INFO"}, "Object is not a linked IFC element.")
|
||||
return {"FINISHED"}
|
||||
|
||||
guid = None
|
||||
guid_start_index = 0
|
||||
for i, guid_end_index in enumerate(obj["guid_ids"]):
|
||||
if face_index < guid_end_index:
|
||||
guid = obj["guids"][i]
|
||||
bpy.context.scene.BIMProjectProperties.queried_obj = obj
|
||||
|
||||
selected_tris = []
|
||||
selected_edges = []
|
||||
vert_indices = set()
|
||||
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
|
||||
vert_indices.update(polygon.vertices)
|
||||
vert_indices = list(vert_indices)
|
||||
vert_map = {k: v for v, k in enumerate(vert_indices)}
|
||||
selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices]
|
||||
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
|
||||
selected_tris.append(tuple(vert_map[v] for v in polygon.vertices))
|
||||
selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys))
|
||||
|
||||
obj["selected_vertices"] = selected_vertices
|
||||
obj["selected_edges"] = selected_edges
|
||||
obj["selected_tris"] = selected_tris
|
||||
|
||||
break
|
||||
guid_start_index = guid_end_index
|
||||
|
||||
self.db = sqlite3.connect(obj["db"])
|
||||
self.c = self.db.cursor()
|
||||
|
||||
self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
|
||||
element = self.c.fetchone()
|
||||
|
||||
attributes = {}
|
||||
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
|
||||
if element[i + 1] is not None:
|
||||
attributes[attr] = element[i + 1]
|
||||
|
||||
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
|
||||
rows = self.c.fetchall()
|
||||
|
||||
properties = {}
|
||||
for row in rows:
|
||||
properties.setdefault(row[1], {})[row[2]] = row[3]
|
||||
|
||||
self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
|
||||
relationships = self.c.fetchall()
|
||||
|
||||
relating_type_id = None
|
||||
|
||||
for relationship in relationships:
|
||||
if relationship[1] == "IfcRelDefinesByType":
|
||||
relating_type_id = relationship[2]
|
||||
|
||||
type_properties = {}
|
||||
if relating_type_id is not None:
|
||||
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
|
||||
rows = self.c.fetchall()
|
||||
for row in rows:
|
||||
type_properties.setdefault(row[1], {})[row[2]] = row[3]
|
||||
|
||||
LinksData.linked_data = {
|
||||
"attributes": attributes,
|
||||
"properties": [(k, properties[k]) for k in sorted(properties.keys())],
|
||||
"type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())],
|
||||
}
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
region.tag_redraw()
|
||||
elif area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({"INFO"}, f"Loaded data for {guid}")
|
||||
ProjectDecorator.install(bpy.context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def ray_cast(self, context, origin, direction):
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
result = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
return result
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.mouse_x = event.mouse_region_x
|
||||
self.mouse_y = event.mouse_region_y
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class EnableCulling(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_culling"
|
||||
bl_label = "Enable Culling"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def __init__(self):
|
||||
self.last_view_corners = None
|
||||
self.total_mousemoves = 0
|
||||
self.cullable_objects = []
|
||||
|
||||
def modal(self, context, event):
|
||||
if not LinksData.enable_culling:
|
||||
for obj in bpy.context.visible_objects:
|
||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||
obj.display_type = "SOLID"
|
||||
self.cullable_objects = []
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Even if the view is changing, there are certain scenarios where we
|
||||
# don't want to apply culling. For example, if we scroll to zoom but
|
||||
# are simultaneously moving our mouse, or if we `Zoom to Selected`. A
|
||||
# dumb but seemingly effective way is to count MOUSEMOVE events. If at
|
||||
# least 3 consecutive events occur, you're probably not doing some
|
||||
# other navigational thing.
|
||||
if event.type == "MOUSEMOVE":
|
||||
self.total_mousemoves += 1
|
||||
else:
|
||||
self.total_mousemoves = 0
|
||||
|
||||
if self.total_mousemoves > 2 and self.is_view_changed(context):
|
||||
self.total_mousemoves = 0
|
||||
camera_position = context.region_data.view_matrix.inverted().translation
|
||||
for obj in self.cullable_objects:
|
||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||
if self.is_object_in_view(obj, context, camera_position):
|
||||
obj.display_type = "SOLID"
|
||||
elif obj.display_type != "BOUNDS":
|
||||
obj.display_type = "BOUNDS"
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def is_view_changed(self, context):
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
vp_matrix = projection_matrix @ view_matrix
|
||||
|
||||
# Get NDC coordinates of the viewport corners
|
||||
viewport_corners = [Vector((-1, -1, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)), Vector((-1, 1, 0))]
|
||||
ndc_corners = [vp_matrix @ Vector((corner.x, corner.y, -1, 1)) for corner in viewport_corners]
|
||||
ndc_corners = [(corner / corner.w).xy for corner in ndc_corners]
|
||||
|
||||
if self.last_view_corners != ndc_corners:
|
||||
self.last_view_corners = ndc_corners
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_object_in_view(self, obj, context, camera_position):
|
||||
# Get the view matrix and the projection matrix from the active viewport
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
# Calculate the combined view projection matrix
|
||||
vp_matrix = projection_matrix @ view_matrix
|
||||
obj_matrix_world = obj.matrix_world
|
||||
|
||||
# Transform each corner of the bounding box using the view projection matrix
|
||||
# and check if it's inside the normalized device coordinates (NDC) space
|
||||
for corner in [obj_matrix_world @ Vector(corner) for corner in obj.bound_box]:
|
||||
ndc = vp_matrix @ corner.to_4d()
|
||||
ndc /= ndc.w
|
||||
if -1 <= ndc.x <= 1 and -1 <= ndc.y <= 1 and 0 <= ndc.z <= 1:
|
||||
# At least one corner is inside the view, so the object is visible
|
||||
break
|
||||
else:
|
||||
return False
|
||||
|
||||
# Check if the object is too far away from the camera
|
||||
object_center = obj.matrix_world.translation
|
||||
distance_threshold = 900 # 30m squared
|
||||
if (camera_position - object_center).length_squared > distance_threshold:
|
||||
# The object is too far away, so consider it not visible
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
LinksData.enable_culling = True
|
||||
self.cullable_objects = []
|
||||
for obj in bpy.context.visible_objects:
|
||||
if obj.type == "MESH" and obj.name.startswith("Ifc") and max(obj.dimensions) < 0.6:
|
||||
self.cullable_objects.append(obj)
|
||||
camera_position = context.region_data.view_matrix.inverted().translation
|
||||
if not self.is_object_in_view(obj, context, camera_position):
|
||||
obj.display_type = "BOUNDS"
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class DisableCulling(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_culling"
|
||||
bl_label = "Disable Culling"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
LinksData.enable_culling = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefreshClippingPlanes(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_clipping_planes"
|
||||
bl_label = "Refresh Clipping Planes"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def __init__(self):
|
||||
self.total_planes = 0
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
should_refresh = False
|
||||
|
||||
self.clean_deleted_planes(context)
|
||||
|
||||
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
|
||||
if clipping_plane.obj and self.is_moved(clipping_plane.obj):
|
||||
should_refresh = True
|
||||
break
|
||||
|
||||
total_planes = len(context.scene.BIMProjectProperties.clipping_planes)
|
||||
if should_refresh or total_planes != self.total_planes:
|
||||
self.refresh_clipping_planes(context)
|
||||
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
|
||||
if clipping_plane.obj:
|
||||
tool.Geometry.record_object_position(clipping_plane.obj)
|
||||
self.total_planes = total_planes
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def clean_deleted_planes(self, context):
|
||||
while True:
|
||||
for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes):
|
||||
if clipping_plane.obj:
|
||||
try:
|
||||
clipping_plane.obj.name
|
||||
except:
|
||||
context.scene.BIMProjectProperties.clipping_planes.remove(i)
|
||||
break
|
||||
else:
|
||||
context.scene.BIMProjectProperties.clipping_planes.remove(i)
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
def is_moved(self, obj):
|
||||
if not obj.BIMObjectProperties.location_checksum:
|
||||
return True # Let's be conservative
|
||||
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
|
||||
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
|
||||
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
|
||||
return False
|
||||
return True
|
||||
|
||||
def refresh_clipping_planes(self, context):
|
||||
import bmesh
|
||||
from itertools import cycle
|
||||
|
||||
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
|
||||
region = next(r for r in area.regions if r.type == "WINDOW")
|
||||
data = region.data
|
||||
|
||||
if not len(context.scene.BIMProjectProperties.clipping_planes):
|
||||
data.use_clip_planes = False
|
||||
else:
|
||||
with bpy.context.temp_override(area=area, region=region):
|
||||
bpy.ops.view3d.clip_border()
|
||||
|
||||
clip_planes = []
|
||||
for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes:
|
||||
obj = clipping_plane.obj
|
||||
if not obj:
|
||||
continue
|
||||
print("doing", obj)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
world_matrix = obj.matrix_world
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
face = bm.faces[0]
|
||||
center = world_matrix @ face.calc_center_median()
|
||||
print("center", center)
|
||||
normal = world_matrix.to_3x3() @ face.normal * -1
|
||||
center += normal * -0.01
|
||||
print("normal", normal)
|
||||
print("new center", center)
|
||||
|
||||
normal.normalize()
|
||||
distance = -center.dot(normal)
|
||||
clip_plane = (normal.x, normal.y, normal.z, distance)
|
||||
clip_planes.append(clip_plane)
|
||||
bm.free()
|
||||
|
||||
clip_planes = cycle(clip_planes)
|
||||
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
|
||||
data.update()
|
||||
region.tag_redraw()
|
||||
[a.tag_redraw() for a in bpy.context.screen.areas]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CreateClippingPlane(bpy.types.Operator):
|
||||
bl_idname = "bim.create_clipping_plane"
|
||||
bl_label = "Create Clipping Plane"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area.type == "VIEW_3D"
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
||||
|
||||
# Clean up deleted planes
|
||||
|
||||
if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5:
|
||||
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
coord = (self.mouse_x, self.mouse_y)
|
||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
|
||||
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
|
||||
|
||||
faces = [(0, 1, 2, 3)]
|
||||
|
||||
mesh = bpy.data.meshes.new(name="ClippingPlane")
|
||||
mesh.from_pydata(vertices, [], faces)
|
||||
mesh.update()
|
||||
|
||||
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
|
||||
bpy.context.collection.objects.link(plane_obj)
|
||||
z_axis = Vector((0, 0, 1))
|
||||
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
|
||||
plane_obj.matrix_world = rotation_matrix
|
||||
plane_obj.matrix_world.translation = location
|
||||
|
||||
bpy.context.scene.cursor.location = location
|
||||
|
||||
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
|
||||
new.obj = plane_obj
|
||||
|
||||
tool.Blender.set_active_object(plane_obj)
|
||||
|
||||
ClippingPlaneDecorator.install(bpy.context)
|
||||
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
def ray_cast(self, context, origin, direction):
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
result = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
return result
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.mouse_x = event.mouse_region_x
|
||||
self.mouse_y = event.mouse_region_y
|
||||
return self.execute(context)
|
||||
|
||||
@@ -20,7 +20,7 @@ import bpy
|
||||
import ifcopenshell.util.placement
|
||||
from blenderbim.bim.module.project.data import ProjectData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.prop import StrProperty, ObjProperty
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -170,6 +170,8 @@ class BIMProjectProperties(PropertyGroup):
|
||||
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema")
|
||||
template_file: EnumProperty(items=get_template_file, name="Template File")
|
||||
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
|
||||
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
|
||||
def get_library_element_index(self, lib_element):
|
||||
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
|
||||
|
||||
@@ -21,7 +21,7 @@ import blenderbim.bim
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from bpy.types import Panel, Menu, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.project.data import ProjectData
|
||||
from blenderbim.bim.module.project.data import ProjectData, LinksData
|
||||
|
||||
|
||||
class BIM_MT_project(Menu):
|
||||
@@ -307,6 +307,41 @@ class BIM_PT_links(Panel):
|
||||
"active_link_index",
|
||||
)
|
||||
|
||||
if LinksData.enable_culling:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Object Culling Enabled", icon="MOD_TRIANGULATE")
|
||||
|
||||
if not LinksData.linked_data:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Object Selected", icon="QUESTION")
|
||||
return
|
||||
|
||||
for name, value in LinksData.linked_data["attributes"].items():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=name)
|
||||
row.label(text=value)
|
||||
|
||||
for pset in LinksData.linked_data["properties"]:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text=pset[0], icon="COPY_ID")
|
||||
for name, value in pset[1].items():
|
||||
row = box.row(align=True)
|
||||
row.label(text=name)
|
||||
row.label(text=value)
|
||||
|
||||
if LinksData.linked_data["type_properties"]:
|
||||
row = self.layout.row()
|
||||
row.label(icon="LINKED", text="Type Properties")
|
||||
for pset in LinksData.linked_data["type_properties"]:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text=pset[0], icon="COPY_ID")
|
||||
for name, value in pset[1].items():
|
||||
row = box.row(align=True)
|
||||
row.label(text=name)
|
||||
row.label(text=value)
|
||||
|
||||
|
||||
class BIM_UL_library(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.project.data import LinksData
|
||||
|
||||
|
||||
class QueryTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "bim.query_tool"
|
||||
bl_label = "Query Tool"
|
||||
bl_description = "Fetch data about a linked IFC element"
|
||||
bl_icon = "ops.generic.select_circle"
|
||||
bl_widget = None
|
||||
bl_keymap = (
|
||||
("bim.query_linked_element", {"type": "RIGHTMOUSE", "value": "PRESS"}, None),
|
||||
("bim.query_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.query_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Query Object", icon="MOUSE_RMB")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Add Clipping Plane", icon="EVENT_C")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
|
||||
|
||||
|
||||
class QueryHotkey(bpy.types.Operator):
|
||||
bl_idname = "bim.query_hotkey"
|
||||
bl_label = "Query Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def execute(self, context):
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
return {"FINISHED"}
|
||||
|
||||
def hotkey_S_C(self):
|
||||
bpy.ops.bim.create_clipping_plane("INVOKE_DEFAULT")
|
||||
|
||||
def hotkey_A_C(self):
|
||||
if LinksData.enable_culling:
|
||||
bpy.ops.bim.disable_culling()
|
||||
else:
|
||||
bpy.ops.bim.enable_culling("INVOKE_DEFAULT")
|
||||
@@ -481,7 +481,8 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
|
||||
}
|
||||
}
|
||||
|
||||
BRepTools::Clean(s);
|
||||
// Temporarily commented out so we don't need to triangulate it again for clash detection.
|
||||
// BRepTools::Clean(s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,592 @@
|
||||
#include "clash_utils.h"
|
||||
#include <cassert>
|
||||
|
||||
#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON
|
||||
#define PX_MAX_F32 3.4028234663852885981170418348452e+38F
|
||||
|
||||
typedef uint32_t PxU32;
|
||||
|
||||
// Why can't I use std::clamp?
|
||||
template<typename TC>
|
||||
const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) {
|
||||
assert(!(hi < lo));
|
||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||
}
|
||||
|
||||
// Branchless slab method. Note that this can still be optimised further by batching boxes.
|
||||
// From Tavian Barnes - MIT License
|
||||
// https://tavianator.com/2022/ray_box_boundary.html
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
float tmin = 0.0, tmax = INFINITY;
|
||||
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
bool sign = std::signbit(ray->dir_inv[d]);
|
||||
float bmin = box->corners[sign][d];
|
||||
float bmax = box->corners[!sign][d];
|
||||
|
||||
float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d];
|
||||
float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d];
|
||||
|
||||
tmin = std::max(dmin, tmin);
|
||||
tmax = std::min(dmax, tmax);
|
||||
}
|
||||
|
||||
return tmin < tmax;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
const gp_Vec edge1 = vert1 - vert0;
|
||||
const gp_Vec edge2 = vert2 - vert0;
|
||||
|
||||
// Begin calculating determinant - also used to calculate U parameter
|
||||
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
|
||||
|
||||
// If determinant is near zero, ray lies in plane of triangle
|
||||
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
|
||||
if(cull)
|
||||
{
|
||||
if(det<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0;
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec);
|
||||
|
||||
const Standard_Real enlargeCoeff = enlarge*det;
|
||||
const Standard_Real uvlimit = -enlargeCoeff;
|
||||
const Standard_Real uvlimit2 = det + enlargeCoeff;
|
||||
|
||||
if(u<uvlimit || u>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec);
|
||||
if(v<uvlimit || (u+v)>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Calculate t, scale parameters, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec);
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
at = t*inv_det;
|
||||
au = u*inv_det;
|
||||
av = v*inv_det;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the non-culling branch
|
||||
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// Calculate t, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec) * inv_det;
|
||||
|
||||
at = t;
|
||||
au = u;
|
||||
av = v;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector
|
||||
{
|
||||
const gp_Vec Tx = q - p;
|
||||
const double ADotA = a.Dot(a);
|
||||
const double BDotB = b.Dot(b);
|
||||
const double ADotB = a.Dot(b);
|
||||
const double ADotT = a.Dot(Tx);
|
||||
const double BDotT = b.Dot(Tx);
|
||||
|
||||
// t parameterizes ray (p, a)
|
||||
// u parameterizes ray (q, b)
|
||||
|
||||
// Compute t for the closest point on ray (p, a) to ray (q, b)
|
||||
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
Standard_Real t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0f)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
|
||||
// find u for point on ray (q, b) closest to point at t
|
||||
Standard_Real u;
|
||||
if(BDotB!=0.0f)
|
||||
{
|
||||
u = (t*ADotB - BDotT) / BDotB;
|
||||
|
||||
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
|
||||
if(u<0.0f)
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
else if(u > 1.0f)
|
||||
{
|
||||
u = 1.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
|
||||
x = p + a * t;
|
||||
y = q + b * u;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
{
|
||||
std::array<gp_Vec, 3> Sv;
|
||||
Sv[0] = p[1] - p[0];
|
||||
Sv[1] = p[2] - p[1];
|
||||
Sv[2] = p[0] - p[2];
|
||||
|
||||
std::array<gp_Vec, 3> Tv;
|
||||
Tv[0] = q[1] - q[0];
|
||||
Tv[1] = q[2] - q[1];
|
||||
Tv[2] = q[0] - q[2];
|
||||
|
||||
gp_Vec minP, minQ;
|
||||
bool shown_disjoint = false;
|
||||
|
||||
float mindd = PX_MAX_F32;
|
||||
|
||||
for(int i=0;i<3;i++)
|
||||
{
|
||||
for(int j=0;j<3;j++)
|
||||
{
|
||||
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
|
||||
const gp_Vec V = cq - cp;
|
||||
const float dd = V.Dot(V);
|
||||
|
||||
if(dd<=mindd)
|
||||
{
|
||||
minP = cp;
|
||||
minQ = cq;
|
||||
mindd = dd;
|
||||
|
||||
int id = i+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
gp_Vec Z = p[id] - cp;
|
||||
float a = Z.Dot(V);
|
||||
id = j+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
Z = q[id] - cq;
|
||||
float b = Z.Dot(V);
|
||||
|
||||
if((a<=0.0f) && (b>=0.0f))
|
||||
return V.Dot(V);
|
||||
|
||||
if(a<=0.0f) a = 0.0f;
|
||||
else if(b>0.0f) b = 0.0f;
|
||||
|
||||
if((mindd - a + b) > 0.0f)
|
||||
shown_disjoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
|
||||
float Snl = Sn.Dot(Sn);
|
||||
|
||||
if(Snl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Tp = {(p[0] - q[0]).Dot(Sn),
|
||||
(p[0] - q[1]).Dot(Sn),
|
||||
(p[0] - q[2]).Dot(Sn)};
|
||||
|
||||
int index = -1;
|
||||
if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f))
|
||||
{
|
||||
if(Tp[0]<Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]<Tp[index]) index = 2;
|
||||
}
|
||||
else if((Tp[0]<0.0f) && (Tp[1]<0.0f) && (Tp[2]<0.0f))
|
||||
{
|
||||
if(Tp[0]>Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]>Tp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& qIndex = q[index];
|
||||
|
||||
gp_Vec V = qIndex - p[0];
|
||||
gp_Vec Z = Sn.Crossed(Sv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[1];
|
||||
Z = Sn.Crossed(Sv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[2];
|
||||
Z = Sn.Crossed(Sv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = qIndex + Sn * Tp[index]/Snl;
|
||||
cq = qIndex;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
float Tnl = Tn.Dot(Tn);
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
|
||||
(q[0] - p[1]).Dot(Tn),
|
||||
(q[0] - p[2]).Dot(Tn)};
|
||||
|
||||
int index = -1;
|
||||
if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f))
|
||||
{
|
||||
if(Sp[0]<Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]<Sp[index]) index = 2;
|
||||
}
|
||||
else if((Sp[0]<0.0f) && (Sp[1]<0.0f) && (Sp[2]<0.0f))
|
||||
{
|
||||
if(Sp[0]>Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]>Sp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& pIndex = p[index];
|
||||
|
||||
gp_Vec V = pIndex - q[0];
|
||||
gp_Vec Z = Tn.Crossed(Tv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[1];
|
||||
Z = Tn.Crossed(Tv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[2];
|
||||
Z = Tn.Crossed(Tv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = pIndex;
|
||||
cq = pIndex + Tn * Sp[index]/Tnl;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(shown_disjoint)
|
||||
{
|
||||
cp = minP;
|
||||
cq = minQ;
|
||||
return mindd;
|
||||
}
|
||||
else return 0.0f;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller
|
||||
//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
|
||||
struct Interval
|
||||
{
|
||||
Standard_Real min;
|
||||
Standard_Real max;
|
||||
gp_Vec minPoint;
|
||||
gp_Vec maxPoint;
|
||||
|
||||
Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { }
|
||||
|
||||
static bool overlapOrTouch(const Interval& a, const Interval& b)
|
||||
{
|
||||
return !(a.min > b.max || b.min > a.max);
|
||||
}
|
||||
|
||||
static Interval intersection(const Interval& a, const Interval& b)
|
||||
{
|
||||
Interval result;
|
||||
if (!overlapOrTouch(a, b))
|
||||
return result;
|
||||
|
||||
if (a.min > b.min)
|
||||
{
|
||||
result.min = a.min;
|
||||
result.minPoint = a.minPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.min = b.min;
|
||||
result.minPoint = b.minPoint;
|
||||
}
|
||||
|
||||
if (a.max < b.max)
|
||||
{
|
||||
result.max = a.max;
|
||||
result.maxPoint = a.maxPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.max = b.max;
|
||||
result.maxPoint = b.maxPoint;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void include(Standard_Real d, const gp_Vec& p)
|
||||
{
|
||||
if (d < min) { min = d; minPoint = p; }
|
||||
if (d > max) { max = d; maxPoint = p; }
|
||||
}
|
||||
};
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
{
|
||||
Interval i;
|
||||
|
||||
const bool bA = distanceA > 0;
|
||||
const bool bB = distanceB > 0;
|
||||
const bool bC = distanceC > 0;
|
||||
distanceA = std::abs(distanceA);
|
||||
distanceB = std::abs(distanceB);
|
||||
distanceC = std::abs(distanceC);
|
||||
|
||||
if (bA != bB)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bA != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bB != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
{
|
||||
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real ab = orient2d(a, b, point, x, y);
|
||||
const Standard_Real bc = orient2d(b, c, point, x, y);
|
||||
const Standard_Real ca = orient2d(c, a, point, x, y);
|
||||
|
||||
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
|
||||
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
|
||||
|
||||
if ((aaS >= 0) == (aaE >= 0))
|
||||
return false;
|
||||
|
||||
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
|
||||
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
|
||||
|
||||
if ((bbS >= 0) == (bbE >= 0))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y)
|
||||
{
|
||||
normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z()));
|
||||
|
||||
if (normal.X() >= normal.Y() && normal.X() >= normal.Z())
|
||||
{
|
||||
//x is the dominant normal direction
|
||||
x = 1;
|
||||
y = 2;
|
||||
}
|
||||
else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z())
|
||||
{
|
||||
//y is the dominant normal direction
|
||||
x = 2;
|
||||
y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//z is the dominant normal direction
|
||||
x = 0;
|
||||
y = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2)
|
||||
{
|
||||
PxU32 x = 0;
|
||||
PxU32 y = 0;
|
||||
getProjectionIndices(p1_n, x, y);
|
||||
|
||||
const Standard_Real third = (1.0f / 3.0f);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
|
||||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// Also with minor modification to return intersection points.
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
|
||||
{
|
||||
const Standard_Real tolerance = 1e-8f;
|
||||
|
||||
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
|
||||
double p1_d = -a1.Dot(p1_n);
|
||||
// const PxPlane p1(a1, b1, c1);
|
||||
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
|
||||
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
|
||||
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
|
||||
|
||||
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
|
||||
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
|
||||
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
|
||||
intersectionDirection *= 1.0f / std::sqrt(l2);
|
||||
|
||||
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
|
||||
const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection);
|
||||
|
||||
if (Interval::overlapOrTouch(i1, i2))
|
||||
{
|
||||
/*if (intersection)
|
||||
{
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
intersection->p0 = i.minPoint;
|
||||
intersection->p1 = i.maxPoint;
|
||||
}*/
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
int1 = i.minPoint;
|
||||
int2 = i.maxPoint;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <gp_Vec.hxx>
|
||||
#include <array>
|
||||
|
||||
struct ray {
|
||||
float origin[3];
|
||||
float dir[3];
|
||||
float dir_inv[3];
|
||||
};
|
||||
|
||||
struct box {
|
||||
float corners[2][3];
|
||||
};
|
||||
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
|
||||
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
|
||||
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
|
||||
@@ -181,6 +181,18 @@ class tree(ifcopenshell_wrapper.tree):
|
||||
args.append(kwargs.get("extend", -1.0e-5))
|
||||
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)]
|
||||
|
||||
def clash_intersection_many(self, set_a, set_b, tolerance=0.002, check_all=True):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
|
||||
|
||||
def clash_collision_many(self, set_a, set_b, allow_touching=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching]
|
||||
return ifcopenshell_wrapper.tree.clash_collision_many(*args)
|
||||
|
||||
def clash_clearance_many(self, set_a, set_b, clearance=0.05, check_all=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
|
||||
|
||||
|
||||
def create_shape(settings, inst, repr=None):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# IfcPatch - IFC patching utiliy
|
||||
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcPatch.
|
||||
#
|
||||
# IfcPatch is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcPatch 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
except:
|
||||
print("No SQLite support")
|
||||
|
||||
|
||||
class Patcher:
|
||||
def __init__(
|
||||
self,
|
||||
src,
|
||||
file,
|
||||
logger,
|
||||
):
|
||||
"""Extracts properties and relationships from a IFC-SPF model to SQLite.
|
||||
|
||||
This is a lossy extraction which simplifies popular properties to key
|
||||
value pairs.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"})
|
||||
ifcpatch.write(result, "output.sqlite")
|
||||
"""
|
||||
self.src = src
|
||||
self.file = file
|
||||
self.logger = logger
|
||||
|
||||
def patch(self):
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||
db_file = tmp.name
|
||||
self.db = sqlite3.connect(db_file)
|
||||
self.c = self.db.cursor()
|
||||
self.file_patched = db_file
|
||||
|
||||
self.c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS elements (
|
||||
id integer PRIMARY KEY NOT NULL UNIQUE,
|
||||
global_id text,
|
||||
ifc_class text,
|
||||
predefined_type text,
|
||||
name text,
|
||||
description text
|
||||
);
|
||||
"""
|
||||
)
|
||||
self.c.execute("CREATE INDEX IF NOT EXISTS idx_global_id ON elements (global_id);")
|
||||
self.c.execute("CREATE INDEX IF NOT EXISTS idx_ifc_class ON elements (ifc_class);")
|
||||
self.c.execute("CREATE INDEX IF NOT EXISTS idx_predefined_type ON elements (predefined_type);")
|
||||
|
||||
self.c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS relationships (
|
||||
from_id integer NOT NULL,
|
||||
type text,
|
||||
to_id integer NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
self.c.execute("CREATE INDEX IF NOT EXISTS idx_from_id ON relationships (from_id);")
|
||||
|
||||
self.c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS properties (
|
||||
element_id integer NOT NULL,
|
||||
set_name text,
|
||||
name text,
|
||||
value text
|
||||
);
|
||||
"""
|
||||
)
|
||||
self.c.execute("CREATE INDEX IF NOT EXISTS idx_element_id ON properties (element_id);")
|
||||
|
||||
elements = self.file.by_type("IfcObjectDefinition")
|
||||
|
||||
rows = []
|
||||
properties = []
|
||||
relationships = []
|
||||
id_map = {e.id(): i for i, e in enumerate(elements)}
|
||||
for i, element in enumerate(elements):
|
||||
rows.append([
|
||||
i,
|
||||
element[0],
|
||||
element.is_a(),
|
||||
ifcopenshell.util.element.get_predefined_type(element),
|
||||
element[2],
|
||||
element[3],
|
||||
])
|
||||
psets = ifcopenshell.util.element.get_psets(element, should_inherit=False)
|
||||
for pset_name, pset_data in psets.items():
|
||||
for prop_name, value in pset_data.items():
|
||||
if prop_name == "id" or value is None or value == "":
|
||||
continue
|
||||
if isinstance(value, bool):
|
||||
value = "True" if value else "False"
|
||||
elif not isinstance(value, str):
|
||||
value = str(value)
|
||||
properties.append([i, pset_name, prop_name, value])
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
if relating_type and relating_type != element:
|
||||
relationships.append([i, "IfcRelDefinesByType", id_map[relating_type.id()]])
|
||||
|
||||
self.c.executemany("INSERT INTO elements VALUES (?, ?, ?, ?, ?, ?);", rows)
|
||||
self.c.executemany("INSERT INTO properties VALUES (?, ?, ?, ?);", properties)
|
||||
self.c.executemany("INSERT INTO relationships VALUES (?, ?, ?);", relationships)
|
||||
|
||||
self.db.commit()
|
||||
self.db.close()
|
||||
@@ -74,6 +74,8 @@
|
||||
|
||||
%template(ray_intersection_results) std::vector<IfcGeom::ray_intersection_result>;
|
||||
|
||||
%template(clashes) std::vector<IfcGeom::clash>;
|
||||
|
||||
// A Template instantantation should be defined before it is used as a base class.
|
||||
// But frankly I don't care as most methods are subtlely different anyway.
|
||||
%include "../ifcgeom_schema_agnostic/IfcGeomTree.h"
|
||||
@@ -106,6 +108,80 @@
|
||||
return IfcGeom_tree_vector_to_list(ps);
|
||||
}
|
||||
|
||||
|
||||
%typemap(in) const std::vector<IfcUtil::IfcBaseClass*>& (std::vector<IfcUtil::IfcBaseClass*> temp) {
|
||||
if (!PyList_Check($input)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected a list.");
|
||||
return NULL;
|
||||
}
|
||||
$1 = &temp; // Set $1 to the address of temp, which SWIG will use as the argument in the wrapped function
|
||||
temp.reserve(PyList_Size($input)); // Pre-allocate memory for efficiency
|
||||
for (Py_ssize_t i = 0; i < PyList_Size($input); ++i) {
|
||||
PyObject* pyObj = PyList_GetItem($input, i);
|
||||
void* ptr = 0;
|
||||
int res = SWIG_ConvertPtr(pyObj, &ptr, SWIGTYPE_p_IfcUtil__IfcBaseClass, 0);
|
||||
if (!SWIG_IsOK(res)) {
|
||||
PyErr_SetString(PyExc_TypeError, "List item is not of type IfcBaseClass.");
|
||||
return NULL;
|
||||
}
|
||||
temp.push_back(reinterpret_cast<IfcUtil::IfcBaseClass*>(ptr));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<clash> clash_intersection_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double tolerance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_intersection_many(set_a_entities, set_b_entities, tolerance, check_all);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_collision_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, bool allow_touching) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_collision_many(set_a_entities, set_b_entities, allow_touching);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_clearance_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double clearance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_clearance_many(set_a_entities, set_b_entities, clearance, check_all);
|
||||
}
|
||||
|
||||
aggregate_of_instance::ptr select(IfcUtil::IfcBaseClass* e, bool completely_within = false, double extend = 0.0) const {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("Instance should be an IfcProduct");
|
||||
|
||||
@@ -203,7 +203,14 @@
|
||||
|
||||
%include "IfcGeomWrapper.i"
|
||||
%include "IfcParseWrapper.i"
|
||||
%include "std_vector.i"
|
||||
|
||||
namespace std {
|
||||
%template(float_array_3) array<double, 3>;
|
||||
%template(FloatVector) vector<float>;
|
||||
%template(IntVector) std::vector<int>;
|
||||
%template(DoubleVector) std::vector<double>;
|
||||
%template(StringVector) std::vector<std::string>;
|
||||
%template(FloatVectorVector) std::vector<std::vector<float>>;
|
||||
%template(DoubleVectorVector) std::vector<std::vector<double>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user