mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
@@ -39,7 +40,6 @@ from typing import (
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import logging
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.document
|
||||
import ifcopenshell.api.pset
|
||||
@@ -58,9 +58,9 @@ from bpy_extras.io_utils import ImportHelper
|
||||
from lxml import etree
|
||||
from mathutils import Color, Vector
|
||||
|
||||
import bonsai.bim.import_ifc
|
||||
import bonsai.bim.export_ifc
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc
|
||||
import bonsai.bim.module.drawing.sheeter as sheeter
|
||||
import bonsai.bim.module.drawing.svgwriter as svgwriter
|
||||
import bonsai.core.drawing as core
|
||||
|
||||
@@ -46,6 +46,7 @@ classes = (
|
||||
operator.EnableEditingLink,
|
||||
operator.ExportIFC,
|
||||
operator.FlipClippingPlane,
|
||||
operator.HideQueriedLinkedElement,
|
||||
operator.IFCFileHandlerOperator,
|
||||
operator.ImageScalingTool,
|
||||
operator.LinkIfc,
|
||||
@@ -66,6 +67,7 @@ classes = (
|
||||
operator.RewindLibrary,
|
||||
operator.SaveLibraryFile,
|
||||
operator.SelectLibraryFile,
|
||||
operator.SelectLinkedModelElement,
|
||||
operator.SelectLinkHandle,
|
||||
operator.ToggleFilterCategories,
|
||||
operator.ToggleLinkSelectability,
|
||||
|
||||
@@ -170,6 +170,6 @@ class ProjectLibraryData:
|
||||
|
||||
|
||||
class LinksData:
|
||||
linked_data = {}
|
||||
linked_data: dict[str, Any] = {}
|
||||
enable_culling = False
|
||||
is_loaded = False
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Union
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import gpu
|
||||
@@ -54,7 +52,7 @@ class ProjectDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
def install(cls, context: bpy.types.Context) -> None:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
@@ -99,26 +97,20 @@ class ProjectDecorator:
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
selected_tris = []
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
try:
|
||||
obj = props.queried_obj
|
||||
selected_vertices = obj["selected_vertices"]
|
||||
selected_edges = obj["selected_edges"]
|
||||
selected_tris = obj["selected_tris"]
|
||||
except:
|
||||
obj = props.queried_obj
|
||||
if obj is None:
|
||||
return
|
||||
geom = tool.Project.Link.get_selected_geometry(obj)
|
||||
selected_vertices = geom.selected_vertices
|
||||
|
||||
root_obj: Union[bpy.types.Object, None] = props.queried_obj_root
|
||||
root_obj = props.queried_obj_root
|
||||
if root_obj and not (m := root_obj.matrix_world).is_identity:
|
||||
selected_vertices = [m @ Vector(v) for v in selected_vertices]
|
||||
|
||||
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)
|
||||
if geom.selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, geom.selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), geom.selected_tris)
|
||||
|
||||
|
||||
class ClippingPlaneDecorator:
|
||||
|
||||
@@ -32,6 +32,7 @@ from typing import TYPE_CHECKING, Literal, Union, get_args
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.document
|
||||
import ifcopenshell.api.nest
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.root
|
||||
@@ -48,7 +49,11 @@ import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from mathutils import Matrix, Vector
|
||||
from bpy_extras.view3d_utils import (
|
||||
region_2d_to_origin_3d,
|
||||
region_2d_to_vector_3d,
|
||||
)
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.helper
|
||||
@@ -56,14 +61,6 @@ import bonsai.core.project as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim import export_ifc, import_ifc
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.ui import IFCFileSelector
|
||||
from bonsai.bim import import_ifc
|
||||
from bonsai.bim import export_ifc
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy.app.handlers import persistent
|
||||
from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from bonsai.bim.module.project.data import LinksData, ProjectLibraryData
|
||||
@@ -1446,8 +1443,13 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Load Link"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load the selected file"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
use_cache: bool
|
||||
|
||||
def _execute(self, context):
|
||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||
@@ -1472,9 +1474,10 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
empty = bpy.data.objects.new(empty_name, None)
|
||||
empty.instance_type = "COLLECTION"
|
||||
empty.instance_collection = collection
|
||||
empty.matrix_world = Matrix(tool.Project.calculate_link_matrix(self.link))
|
||||
empty.matrix_world = tool.Project.calculate_link_matrix(self.link)
|
||||
|
||||
tool.Project.set_link_empty_handle(self.link, empty)
|
||||
assert bpy.context.scene
|
||||
bpy.context.scene.collection.objects.link(empty)
|
||||
self.link.is_loaded = True
|
||||
if tool.Ifc.get(): # For non-IFC projects, locking has no meaning
|
||||
@@ -1643,8 +1646,16 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
||||
bl_label = "Toggle Link Visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Visibility Mode",
|
||||
items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
mode: Literal["WIREFRAME", "VISIBLE"]
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -1696,8 +1707,11 @@ class EnableEditingLink(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
link = tool.Project.get_project_props().active_link
|
||||
assert link
|
||||
link.is_editing = True
|
||||
tool.Geometry.unlock_object(tool.Project.get_link_empty_handle(link))
|
||||
obj = tool.Project.get_link_empty_handle(link)
|
||||
assert obj
|
||||
tool.Geometry.unlock_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1709,9 +1723,11 @@ class DisableEditingLink(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
link = tool.Project.get_project_props().active_link
|
||||
assert link
|
||||
link.is_editing = False
|
||||
obj = tool.Project.get_link_empty_handle(link)
|
||||
obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link))
|
||||
assert obj
|
||||
obj.matrix_world = tool.Project.calculate_link_matrix(link)
|
||||
tool.Geometry.lock_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1724,8 +1740,10 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
link = tool.Project.get_project_props().active_link
|
||||
assert link
|
||||
link.is_editing = False
|
||||
obj = tool.Project.get_link_empty_handle(link)
|
||||
assert obj
|
||||
new_obj_matrix = obj.matrix_world
|
||||
|
||||
filepath = Path(tool.Ifc.resolve_uri(link.filepath))
|
||||
@@ -1761,7 +1779,7 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
link.transformation = transformation
|
||||
|
||||
obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link))
|
||||
obj.matrix_world = tool.Project.calculate_link_matrix(link)
|
||||
tool.Geometry.lock_object(obj)
|
||||
|
||||
|
||||
@@ -1783,6 +1801,43 @@ class SelectLinkHandle(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectLinkedModelElement(bpy.types.Operator):
|
||||
bl_idname = "bim.select_linked_model_element"
|
||||
bl_label = "Select Linked Model Element"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
|
||||
|
||||
guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
guid: str
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window_manager
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
guid = self.guid.strip()
|
||||
if not guid:
|
||||
self.report({"ERROR"}, "GlobalId is not provided.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
active_link = props.active_link
|
||||
assert active_link is not None
|
||||
assert active_link.is_loaded
|
||||
|
||||
guid_obj = tool.Project.Link.get_obj_by_guid(active_link, guid)
|
||||
if not guid_obj:
|
||||
filepath = active_link.filepath
|
||||
self.report({"INFO"}, f"Element with GlobalId '{guid}' not found in the linked model at '{filepath}'.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
tool.Project.Link.select_linked_element(context, guid_obj, guid)
|
||||
self.report({"INFO"}, f"Element with GlobalId '{guid}' is selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.save_project"
|
||||
bl_label = "Save IFC"
|
||||
@@ -2264,21 +2319,16 @@ class QueryLinkedElement(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
assert context.area
|
||||
return context.area.type == "VIEW_3D"
|
||||
|
||||
def execute(self, context):
|
||||
import sqlite3
|
||||
|
||||
from bpy_extras.view3d_utils import (
|
||||
region_2d_to_origin_3d,
|
||||
region_2d_to_vector_3d,
|
||||
)
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
LinksData.linked_data = {}
|
||||
props = tool.Project.get_project_props()
|
||||
props.queried_obj = None
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
assert context.screen
|
||||
for area in context.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
@@ -2286,128 +2336,89 @@ class QueryLinkedElement(bpy.types.Operator):
|
||||
elif area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
assert context.region and context.region_data
|
||||
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, instance_matrix = self.ray_cast(context, origin, direction)
|
||||
hit, location, normal, face_index, obj, instance_matrix = tool.Blender.ray_cast_scene(
|
||||
context, origin, direction
|
||||
)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
|
||||
if "guids" not in obj:
|
||||
if not tool.Project.Link.is_linked_element(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]
|
||||
props.queried_obj = obj
|
||||
props.queried_obj_root = self.find_obj_root(obj, instance_matrix)
|
||||
|
||||
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())],
|
||||
}
|
||||
self.db.close()
|
||||
|
||||
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()
|
||||
guid = tool.Project.Link.get_guid_by_face_index(obj, face_index)
|
||||
assert guid is not None
|
||||
tool.Project.Link.select_linked_element(context, obj, guid)
|
||||
|
||||
self.report({"INFO"}, f"Loaded data for {guid}")
|
||||
ProjectDecorator.install(bpy.context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def ray_cast(self, context: bpy.types.Context, origin: Vector, direction: Vector):
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
result = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
return result
|
||||
|
||||
def find_obj_root(self, obj: bpy.types.Object, matrix: Matrix) -> Union[bpy.types.Object, None]:
|
||||
collections = set(obj.users_collection)
|
||||
for o in bpy.data.objects:
|
||||
if (
|
||||
o.type != "EMPTY"
|
||||
or o.instance_type != "COLLECTION"
|
||||
or o.instance_collection not in collections
|
||||
or not np.allclose(matrix, o.matrix_world, atol=1e-4)
|
||||
):
|
||||
continue
|
||||
return o
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.mouse_x = event.mouse_region_x
|
||||
self.mouse_y = event.mouse_region_y
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class HideQueriedLinkedElement(bpy.types.Operator):
|
||||
bl_idname = "bim.hide_queried_linked_element"
|
||||
bl_label = "Hide Queried Linked Element"
|
||||
bl_description = (
|
||||
"Hide geometry for currently queried linked element.\n\n"
|
||||
"ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n"
|
||||
"(Not Yet Implemented) SHIFT+Click to hide everything but currently queried element."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
unhide_all: bool
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.unhide_all = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
props = tool.Project.get_project_props()
|
||||
|
||||
if self.unhide_all:
|
||||
return self.run_unhide_all()
|
||||
|
||||
obj = props.queried_obj
|
||||
if not obj:
|
||||
self.report({"INFO"}, "No object is queried to hide.")
|
||||
return {"FINISHED"}
|
||||
guid = props.queried_guid
|
||||
tool.Project.Link.hide_linked_element(obj, guid)
|
||||
tool.Project.Link.deselect_queried_linked_element()
|
||||
|
||||
self.report({"INFO"}, "Queried object is now hidden.")
|
||||
return {"FINISHED"}
|
||||
|
||||
def run_unhide_all(self) -> set["rna_enums.OperatorReturnItems"]:
|
||||
props = tool.Project.get_project_props()
|
||||
link = props.active_link
|
||||
if not link:
|
||||
self.report({"INFO"}, "No linked model is currently selected.")
|
||||
return {"FINISHED"}
|
||||
tool.Project.Link.unhide_all_elements(link)
|
||||
self.report({"INFO"}, "All linked model geometry is unhidden.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AppendInspectedLinkedElement(AppendLibraryElement):
|
||||
bl_idname = "bim.append_inspected_linked_element"
|
||||
bl_label = "Append Inspected Linked Element"
|
||||
bl_description = "Append inspected linked element"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
from bonsai.bim.module.project.data import LinksData
|
||||
@@ -2423,6 +2434,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement):
|
||||
return {"CANCELLED"}
|
||||
|
||||
queried_obj = props.queried_obj
|
||||
assert queried_obj
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
linked_ifc_file: ifcopenshell.file
|
||||
@@ -2680,28 +2692,25 @@ class CreateClippingPlane(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras.view3d_utils import (
|
||||
region_2d_to_origin_3d,
|
||||
region_2d_to_vector_3d,
|
||||
)
|
||||
|
||||
# Clean up deleted planes
|
||||
props = tool.Project.get_project_props()
|
||||
if len(props.clipping_planes) > 5:
|
||||
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
assert context.screen
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
assert context.region and context.region_data
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
if rv3d: # Called from a 3D viewport
|
||||
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)
|
||||
hit, location, normal, face_index, obj, matrix = tool.Blender.ray_cast_scene(context, origin, direction)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
@@ -2736,11 +2745,6 @@ class CreateClippingPlane(bpy.types.Operator):
|
||||
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
|
||||
@@ -2839,7 +2843,7 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
||||
|
||||
def clean_up_path(path: str) -> str:
|
||||
# In Blender 4.5.6 there was a bug producing unncesseary double slash prefix
|
||||
# breaking the paths. Issue is not present in 5.0+ and presumably will be solved in 4.5.7 too.
|
||||
# breaking the paths. Issue is not present in 5.0+ and is fixed in 4.5.7.
|
||||
# https://projects.blender.org/blender/blender/issues/153822
|
||||
if bpy.app.version == (4, 5, 6):
|
||||
blender_prefix = "//"
|
||||
|
||||
@@ -452,6 +452,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
)
|
||||
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
queried_guid: bpy.props.StringProperty()
|
||||
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5)
|
||||
edited_objs: bpy.props.CollectionProperty(type=EditedObj)
|
||||
@@ -550,6 +551,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
should_save_metadata_for_this_file: bool
|
||||
queried_obj: Union[bpy.types.Object, None]
|
||||
queried_obj_root: Union[bpy.types.Object, None]
|
||||
queried_guid: str
|
||||
clipping_planes: bpy.types.bpy_prop_collection_idprop[ObjProperty]
|
||||
clipping_planes_active_index: int
|
||||
edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj]
|
||||
|
||||
@@ -29,7 +29,6 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.helper import draw_attributes, prop_with_search
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.project.data import LinksData, ProjectData
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.project.prop import (
|
||||
@@ -491,6 +490,7 @@ class BIM_PT_links(Panel):
|
||||
row.operator("bim.disable_editing_link", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL")
|
||||
row.operator("bim.select_linked_model_element", icon="VIEWZOOM", text="")
|
||||
row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index
|
||||
row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index
|
||||
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
|
||||
|
||||
@@ -40,9 +40,11 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
||||
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
|
||||
("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
|
||||
("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}),
|
||||
("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}),
|
||||
("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
def draw_settings(context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool) -> None:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Query Object", icon="MOUSE_RMB")
|
||||
row = layout.row(align=True)
|
||||
@@ -61,6 +63,9 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.hide_queried_linked_element", text="Hide Queried Element", icon="EVENT_H")
|
||||
|
||||
prop = tool.Project.get_measure_tool_settings()
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -86,6 +91,7 @@ class ExploreHotkey(bpy.types.Operator):
|
||||
bl_idname = "bim.explore_hotkey"
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@@ -137,3 +143,9 @@ class ExploreHotkey(bpy.types.Operator):
|
||||
return
|
||||
|
||||
bpy.ops.bim.image_scaling_tool("INVOKE_DEFAULT")
|
||||
|
||||
def hotkey_H(self) -> None:
|
||||
bpy.ops.bim.hide_queried_linked_element()
|
||||
|
||||
def hotkey_A_H(self) -> None:
|
||||
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
|
||||
|
||||
@@ -774,6 +774,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
bsdd_load_preview_dictionaries: bool
|
||||
bsdd_load_inactive_dictionaries: bool
|
||||
bsdd_load_test_dictionaries: bool
|
||||
bsdd_baseurl: str
|
||||
should_disable_undo_on_save: bool
|
||||
should_stream: bool
|
||||
should_always_cache: bool
|
||||
@@ -789,6 +790,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
mass_time_units_in_wizard: bool
|
||||
chain_filter_with_set_operations: bool
|
||||
save_metadata_blend_file: bool
|
||||
metadata_blend_file_suffix: str
|
||||
decorator_font_scale: float
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
|
||||
@@ -2184,3 +2184,32 @@ class Blender(bonsai.core.tool.Blender):
|
||||
for f in files
|
||||
if (Path(directory) / f.name).is_file()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def ray_cast_scene(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
origin: Vector,
|
||||
direction: Vector,
|
||||
) -> tuple[bool, Vector, Vector, int, bpy.types.Object, Matrix]:
|
||||
"""
|
||||
|
||||
The returned matrix is just ``obj.matrix_world``.
|
||||
The returned object is not evaluated by the current depsgraph,
|
||||
e.g. if object is modified by the depsgraph (e.g. by modifiers)
|
||||
object has to be evaluated first (`obj.evaluated_get(depsgraph)`).
|
||||
"""
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
assert context.scene
|
||||
result = context.scene.ray_cast(
|
||||
depsgraph,
|
||||
origin,
|
||||
direction,
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def depsgraph_evaluate(cls, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated_obj = obj.evaluated_get(depsgraph)
|
||||
return evaluated_obj
|
||||
|
||||
@@ -18,21 +18,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Optional
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.document
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES
|
||||
from mathutils import Matrix
|
||||
|
||||
import bonsai.bim.schema
|
||||
import bonsai.core.aggregate
|
||||
@@ -45,7 +48,11 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings
|
||||
from bonsai.bim.module.project.prop import (
|
||||
BIMProjectProperties,
|
||||
Link,
|
||||
MeasureToolSettings,
|
||||
)
|
||||
|
||||
HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"]
|
||||
|
||||
@@ -61,20 +68,20 @@ class Project(bonsai.core.tool.Project):
|
||||
return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_link_empty_handle(cls, link) -> bpy.types.Object | None:
|
||||
def get_link_empty_handle(cls, link: Link) -> bpy.types.Object | None:
|
||||
if tool.Ifc.get():
|
||||
return tool.Ifc.get_object(tool.Ifc.get().by_id(link.ifc_definition_id))
|
||||
return link.empty_handle
|
||||
|
||||
@classmethod
|
||||
def set_link_empty_handle(cls, link, empty: bpy.types.Object) -> None:
|
||||
def set_link_empty_handle(cls, link: Link, empty: bpy.types.Object) -> None:
|
||||
if tool.Ifc.get():
|
||||
tool.Ifc.link(tool.Ifc.get().by_id(link.ifc_definition_id), empty)
|
||||
else:
|
||||
link.empty_handle = empty
|
||||
|
||||
@classmethod
|
||||
def calculate_link_matrix(cls, link) -> None:
|
||||
def calculate_link_matrix(cls, link: Link) -> Matrix:
|
||||
filepath = Path(tool.Ifc.resolve_uri(link.filepath))
|
||||
with open(filepath.with_suffix(".ifc.cache.json"), "r") as f:
|
||||
metadata = json.load(f)
|
||||
@@ -99,7 +106,7 @@ class Project(bonsai.core.tool.Project):
|
||||
rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z")
|
||||
local_matrix = rot @ np.eye(4)
|
||||
local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")]
|
||||
return np.linalg.inv(local_matrix) @ global_matrix
|
||||
return Matrix(np.linalg.inv(local_matrix) @ global_matrix)
|
||||
|
||||
@classmethod
|
||||
def append_all_types_from_template(cls, template: str) -> None:
|
||||
@@ -496,8 +503,8 @@ class Project(bonsai.core.tool.Project):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_clipping_planes_normals(cls):
|
||||
normals = []
|
||||
def get_clipping_planes_normals(cls) -> list[tuple[Vector, Vector]]:
|
||||
normals: list[tuple[Vector, Vector]] = []
|
||||
for clipping_plane in tool.Project.get_project_props().clipping_planes:
|
||||
plane = clipping_plane.obj
|
||||
if not plane or not plane.data:
|
||||
@@ -506,6 +513,7 @@ class Project(bonsai.core.tool.Project):
|
||||
if plane.mode == "EDIT":
|
||||
continue # A profile decorator or something else is used here.
|
||||
|
||||
assert isinstance(plane.data, bpy.types.Mesh)
|
||||
v1 = plane.matrix_world @ plane.data.vertices[0].co
|
||||
v2 = plane.matrix_world @ plane.data.vertices[1].co
|
||||
v3 = plane.matrix_world @ plane.data.vertices[2].co
|
||||
@@ -588,3 +596,281 @@ class Project(bonsai.core.tool.Project):
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifcopenshell.api.document.remove_information(ifc_file, information=doc)
|
||||
|
||||
class Link:
|
||||
"""Tools for working with linked models."""
|
||||
|
||||
@classmethod
|
||||
def is_linked_element(cls, obj: bpy.types.Object) -> bool:
|
||||
return "guids" in obj
|
||||
|
||||
@classmethod
|
||||
def get_obj_by_guid(cls, link: Link, guid: str) -> bpy.types.Object | None:
|
||||
assert link.is_loaded
|
||||
|
||||
handle = tool.Project.get_link_empty_handle(link)
|
||||
assert handle
|
||||
col = handle.instance_collection
|
||||
assert col
|
||||
|
||||
guid_obj = None
|
||||
for obj in col.objects:
|
||||
obj_guids: list[str] = obj["guids"]
|
||||
if guid in obj_guids:
|
||||
guid_obj = obj
|
||||
break
|
||||
|
||||
return guid_obj
|
||||
|
||||
@classmethod
|
||||
def get_linked_element_guid_ids(cls, obj: bpy.types.Object, *, skip_hidden: bool) -> npt.NDArray[np.int64]:
|
||||
obj_guid_ids: npt.NDArray[np.int64] = np.array(obj["guid_ids"])
|
||||
|
||||
if not skip_hidden:
|
||||
return obj_guid_ids
|
||||
|
||||
# 'hidden_indices' is needed, because otherwise we can't make sense of 'guid_ids',
|
||||
# since part of the geometry is hidden.
|
||||
obj_hidden_indices: list[int] = list(obj.get("hidden_indices") or [])
|
||||
|
||||
if not obj_hidden_indices:
|
||||
return obj_guid_ids
|
||||
|
||||
# Skip hidden geometry indices.
|
||||
hidden_indices_mask = np.zeros(len(obj_guid_ids), dtype=bool)
|
||||
hidden_indices_mask[obj_hidden_indices] = True
|
||||
deltas = np.diff(obj_guid_ids, prepend=0)
|
||||
deltas[~hidden_indices_mask] = 0
|
||||
obj_guid_ids -= np.cumsum(deltas)
|
||||
return obj_guid_ids
|
||||
|
||||
@classmethod
|
||||
def get_guid_by_face_index(cls, obj: bpy.types.Object, face_index: int) -> str | None:
|
||||
guids: list[str] = obj["guids"]
|
||||
guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=True)
|
||||
for guid, guid_end_index in zip(guids, guid_ids):
|
||||
if face_index < guid_end_index:
|
||||
return guid
|
||||
|
||||
@classmethod
|
||||
def get_linked_element_geom_slice(cls, obj: bpy.types.Object, guid: str) -> slice[int, int]:
|
||||
"""
|
||||
Get slice for ``obj.data.polygons`` for the provided ``guid``.
|
||||
"""
|
||||
obj_guids: list[str] = obj["guids"]
|
||||
|
||||
# Just to be safe.
|
||||
obj_hidden_indices: list[int] = obj.get("hidden_indices") or []
|
||||
index = obj_guids.index(guid)
|
||||
if index in obj_hidden_indices:
|
||||
assert False, "Unexpected. Why would you need the geometry for the hidden element?"
|
||||
obj_guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=False)
|
||||
guid_end_index = obj_guid_ids[index]
|
||||
guid_start_index = index and obj_guid_ids[index - 1]
|
||||
return slice(guid_start_index, guid_end_index)
|
||||
|
||||
@classmethod
|
||||
def hide_linked_element(cls, obj: bpy.types.Object, guid: str) -> None:
|
||||
verts = tool.Project.Link.get_linked_element_verts(obj, guid)
|
||||
|
||||
# `MeshPolygon.hide` works only in EDIT mode,
|
||||
# so we use vertex groups + Mask modifier.
|
||||
MODIFIER_VG_NAME = "BBIM_HIDE_LINKED_GEOMETRY"
|
||||
|
||||
vertex_groups = obj.vertex_groups
|
||||
vertex_group = vertex_groups.get(MODIFIER_VG_NAME)
|
||||
if vertex_group is None:
|
||||
vertex_group = vertex_groups.new(name=MODIFIER_VG_NAME)
|
||||
|
||||
modifiers = obj.modifiers
|
||||
modifier = modifiers.get(MODIFIER_VG_NAME)
|
||||
if modifier is None:
|
||||
modifier = modifiers.new(MODIFIER_VG_NAME, "MASK")
|
||||
assert isinstance(modifier, bpy.types.MaskModifier)
|
||||
modifier.vertex_group = MODIFIER_VG_NAME
|
||||
modifier.invert_vertex_group = True
|
||||
|
||||
vertex_group.add(verts, 1.0, "REPLACE")
|
||||
|
||||
hidden_indices: list[int] = list(obj.get("hidden_indices") or [])
|
||||
guid_ids: list[str] = obj["guids"]
|
||||
index = guid_ids.index(guid)
|
||||
hidden_indices.append(index)
|
||||
obj["hidden_indices"] = hidden_indices
|
||||
|
||||
@classmethod
|
||||
def unhide_all_elements(cls, link: Link) -> None:
|
||||
obj = tool.Project.get_link_empty_handle(link)
|
||||
assert obj
|
||||
col = obj.instance_collection
|
||||
assert col
|
||||
|
||||
for obj_ in col.objects:
|
||||
if "hidden_indices" not in obj_:
|
||||
continue
|
||||
obj_.vertex_groups.clear()
|
||||
obj_.modifiers.clear()
|
||||
del obj_["hidden_indices"]
|
||||
|
||||
@classmethod
|
||||
def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None:
|
||||
slice_ = cls.get_linked_element_geom_slice(obj, guid)
|
||||
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
guid_polygons = mesh.polygons[slice_]
|
||||
|
||||
selected_tris: list[tuple[int, ...]] = []
|
||||
selected_edges: list[tuple[int, ...]] = []
|
||||
|
||||
# Restart verts indices for our polygons.
|
||||
guid_vertices_set: set[int] = set()
|
||||
for polygon in guid_polygons:
|
||||
guid_vertices_set.update(polygon.vertices)
|
||||
vert_map = {k: v for v, k in enumerate(guid_vertices_set)}
|
||||
|
||||
selected_vertices = [obj.matrix_world @ mesh.vertices[vi].co for vi in vert_map]
|
||||
for polygon in guid_polygons:
|
||||
selected_tris.append(tuple(vert_map[vi] for vi 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
|
||||
|
||||
@classmethod
|
||||
def get_linked_element_verts(cls, obj: bpy.types.Object, guid: str) -> set[int]:
|
||||
slice_ = cls.get_linked_element_geom_slice(obj, guid)
|
||||
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
guid_polygons = mesh.polygons[slice_]
|
||||
|
||||
guid_vertices_set: set[int] = set()
|
||||
for polygon in guid_polygons:
|
||||
guid_vertices_set.update(polygon.vertices)
|
||||
return guid_vertices_set
|
||||
|
||||
@classmethod
|
||||
def select_linked_element(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
obj: bpy.types.Object,
|
||||
guid: str,
|
||||
instance_matrix: Matrix | None = None,
|
||||
) -> None:
|
||||
import sqlite3
|
||||
|
||||
from ifcpatch.recipes.ExtractPropertiesToSQLite import (
|
||||
ElementRow,
|
||||
PropertyRow,
|
||||
RelationshipRow,
|
||||
)
|
||||
|
||||
from bonsai.bim.module.project.data import LinksData
|
||||
from bonsai.bim.module.project.decorator import ProjectDecorator
|
||||
|
||||
# Not sure if there's a difference between `instance_matrix` coming from `ray_cast`
|
||||
# and usual `matrix_world`, maybe we can just get it from object always.
|
||||
if instance_matrix is None:
|
||||
instance_matrix = obj.matrix_world
|
||||
|
||||
cls.deselect_queried_linked_element()
|
||||
cls.set_queried_linked_element(obj, guid, instance_matrix)
|
||||
cls.select_linked_element_geom(obj, guid)
|
||||
db = sqlite3.connect(obj["db"])
|
||||
c = db.cursor()
|
||||
|
||||
c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
|
||||
element = ElementRow(*c.fetchone())
|
||||
|
||||
attributes: dict[str, Any] = {}
|
||||
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
|
||||
if element[i + 1] is not None:
|
||||
attributes[attr] = element[i + 1]
|
||||
|
||||
c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
|
||||
rows = [PropertyRow(*row) for row in c.fetchall()]
|
||||
|
||||
properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
|
||||
for row in rows:
|
||||
properties[row.pset_name][row.name] = row.value
|
||||
|
||||
c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
|
||||
relationships = [RelationshipRow(*row) for row in c.fetchall()]
|
||||
|
||||
relating_type_id = None
|
||||
|
||||
for relationship in relationships:
|
||||
if relationship[1] == "IfcRelDefinesByType":
|
||||
relating_type_id = relationship[2]
|
||||
|
||||
type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
|
||||
if relating_type_id is not None:
|
||||
c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
|
||||
rows = [PropertyRow(*row) for row in c.fetchall()]
|
||||
for row in rows:
|
||||
type_properties[row.pset_name][row.name] = row.value
|
||||
|
||||
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())],
|
||||
}
|
||||
db.close()
|
||||
|
||||
assert context.screen
|
||||
for area in 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()
|
||||
|
||||
ProjectDecorator.install(context)
|
||||
|
||||
@classmethod
|
||||
def set_queried_linked_element(cls, obj: bpy.types.Object, guid: str, instance_matrix: Matrix) -> None:
|
||||
props = tool.Project.get_project_props()
|
||||
props.queried_obj = obj
|
||||
props.queried_obj_root = cls.find_obj_root(obj, instance_matrix)
|
||||
props.queried_guid = guid
|
||||
|
||||
@classmethod
|
||||
def deselect_queried_linked_element(cls) -> None:
|
||||
props = tool.Project.get_project_props()
|
||||
obj = props.queried_obj
|
||||
props.property_unset("queried_obj")
|
||||
props.property_unset("queried_obj_root")
|
||||
props.property_unset("queried_guid")
|
||||
|
||||
if obj is not None:
|
||||
for field in cls.SelectedGeometry._fields:
|
||||
del obj[field]
|
||||
|
||||
@classmethod
|
||||
def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None:
|
||||
collections = set(obj.users_collection)
|
||||
for o in bpy.data.objects:
|
||||
if (
|
||||
o.type != "EMPTY"
|
||||
or o.instance_type != "COLLECTION"
|
||||
or o.instance_collection not in collections
|
||||
or not np.allclose(matrix, o.matrix_world, atol=1e-4)
|
||||
):
|
||||
continue
|
||||
return o
|
||||
|
||||
class SelectedGeometry(NamedTuple):
|
||||
selected_vertices: list[tuple[float, float, float]]
|
||||
selected_edges: list[tuple[int, int]]
|
||||
selected_tris: list[tuple[int, int, int]]
|
||||
|
||||
@classmethod
|
||||
def get_selected_geometry(cls, obj: bpy.types.Object) -> SelectedGeometry:
|
||||
return cls.SelectedGeometry(
|
||||
obj["selected_vertices"],
|
||||
obj["selected_edges"],
|
||||
obj["selected_tris"],
|
||||
)
|
||||
|
||||
@@ -38,6 +38,12 @@ exclude = ["test*"]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.select = [
|
||||
lint.extend-select = [
|
||||
"F401", # unused imports
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = [
|
||||
"test",
|
||||
"bonsai",
|
||||
]
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
@@ -24,10 +26,8 @@ import pytest
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import tempfile
|
||||
from bonsai.tool.blender import Blender as subject
|
||||
from test.bim.bootstrap import NewFile
|
||||
from pathlib import Path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy.stub_internal.rna_enums as rna_enums
|
||||
|
||||
@@ -21,7 +21,6 @@ import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import pytest
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.drawing
|
||||
import ifcopenshell.api.group
|
||||
@@ -31,8 +30,9 @@ import ifcopenshell.guid
|
||||
import ifcopenshell.util.element
|
||||
import mathutils
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
import pytest
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import contextlib
|
||||
import json
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import cast
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -30,6 +30,7 @@ import ifcopenshell.api.document
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.unit
|
||||
import ifcpatch
|
||||
import numpy as np
|
||||
from ifcpatch.recipes import Ifc2Sql
|
||||
|
||||
import bonsai.core.tool
|
||||
@@ -399,3 +400,50 @@ class TestLoadingIfcSqlite(NewFile):
|
||||
for element_name in elements_without_meshes:
|
||||
assert element_name in bpy.data.objects
|
||||
assert not bpy.data.objects[element_name].data
|
||||
|
||||
|
||||
class TestGettingLinkedElementGeomSlice:
|
||||
def __init__(self):
|
||||
self.test_get_first_element()
|
||||
self.test_get_middle_element()
|
||||
self.test_skip_hidden_first_element()
|
||||
self.test_skip_hidden_middle_element()
|
||||
self.test_handle_hidden_non_first_element()
|
||||
|
||||
TEST_OBJ = {
|
||||
"guids": ["aaa", "bbb", "ccc"],
|
||||
"guid_ids": [5, 10, 15],
|
||||
}
|
||||
|
||||
def test_get_first_element(self):
|
||||
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
|
||||
obj = cast(bpy.types.Object, obj)
|
||||
slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa")
|
||||
assert range(15)[slice_] == range(5)
|
||||
|
||||
def test_get_middle_element(self):
|
||||
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
|
||||
obj = cast(bpy.types.Object, obj)
|
||||
slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb")
|
||||
assert range(15)[slice_] == range(5, 10)
|
||||
|
||||
def test_skip_hidden_first_element(self):
|
||||
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
|
||||
obj = obj | {"hidden_indices": [0]}
|
||||
obj = cast(bpy.types.Object, obj)
|
||||
slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb")
|
||||
assert range(15)[slice_] == range(5)
|
||||
|
||||
def test_skip_hidden_middle_element(self):
|
||||
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
|
||||
obj = obj | {"hidden_indices": [1]}
|
||||
obj = cast(bpy.types.Object, obj)
|
||||
slice_ = subject.Link.get_linked_element_geom_slice(obj, "ccc")
|
||||
assert range(15)[slice_] == range(5, 10)
|
||||
|
||||
def test_handle_hidden_non_first_element(self):
|
||||
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
|
||||
obj = obj | {"hidden_indices": [1]}
|
||||
obj = cast(bpy.types.Object, obj)
|
||||
slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa")
|
||||
assert range(15)[slice_] == range(5)
|
||||
|
||||
@@ -61,9 +61,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
|
||||
|
||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||
|
||||
auto linear_placement = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||
profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3));
|
||||
boost::optional<Eigen::Matrix3d> rot(linear_placement->ccomponents().block<3,3>(0,0));
|
||||
Eigen::Vector3d po(
|
||||
pbde->OffsetLateral().get_value_or(0.),
|
||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
||||
pbde->OffsetVertical().get_value_or(0.),
|
||||
0.);
|
||||
|
||||
profile_offsets.push_back(po);
|
||||
|
||||
auto axis2_placement_linear = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||
boost::optional<Eigen::Matrix3d> rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0));
|
||||
profile_rotations.push_back(rot);
|
||||
}
|
||||
if (faces.size() != profile_offsets.size()) {
|
||||
|
||||
@@ -63,11 +63,18 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
|
||||
|
||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||
|
||||
auto linear_placement = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||
profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3));
|
||||
boost::optional<Eigen::Matrix3d> rot(linear_placement->ccomponents().block<3, 3>(0, 0));
|
||||
profile_rotations.push_back(rot);
|
||||
}
|
||||
Eigen::Vector3d po(
|
||||
pbde->OffsetLateral().get_value_or(0.),
|
||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
||||
pbde->OffsetVertical().get_value_or(0.),
|
||||
0.);
|
||||
|
||||
profile_offsets.push_back(po);
|
||||
|
||||
auto axis2_placement_linear = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||
boost::optional<Eigen::Matrix3d> rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0));
|
||||
profile_rotations.push_back(rot);
|
||||
}
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
|
||||
@@ -543,8 +543,8 @@ piecewise_function::const_ptr offset_function::get_offset() const { return offse
|
||||
ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) {
|
||||
auto flat = make<taxonomy::collection>();
|
||||
ifcopenshell::geometry::visit<taxonomy::collection>(deep, [&flat](taxonomy::ptr i) {
|
||||
flat->children.push_back(taxonomy::cast<taxonomy::geom_item>(clone(i)));
|
||||
});
|
||||
flat->children.push_back(std::static_pointer_cast<taxonomy::geom_item>(clone(i)));
|
||||
});
|
||||
return flat;
|
||||
}
|
||||
|
||||
|
||||
+7
-13
@@ -1622,25 +1622,19 @@ typedef item const* ptr;
|
||||
for (auto& i : deep->children) {
|
||||
// @todo Sad... now that we have templated collection members,
|
||||
// we can't generally use collection_base anymore as a cast target.
|
||||
if (auto s = taxonomy::dcast<taxonomy::collection>(i)) {
|
||||
if (auto s = std::dynamic_pointer_cast<taxonomy::collection>(i)) {
|
||||
visit<taxonomy::collection>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::loop>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::loop>(i)) {
|
||||
visit<taxonomy::loop>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::face>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::face>(i)) {
|
||||
visit<taxonomy::face>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::shell>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::shell>(i)) {
|
||||
visit<taxonomy::shell>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::solid>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::solid>(i)) {
|
||||
visit<taxonomy::solid>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::loft>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::loft>(i)) {
|
||||
visit<taxonomy::loft>(s, fn);
|
||||
}
|
||||
else if (auto s = taxonomy::dcast<taxonomy::boolean_result>(i)) {
|
||||
} else if (auto s = std::dynamic_pointer_cast<taxonomy::boolean_result>(i)) {
|
||||
visit<taxonomy::boolean_result>(s, fn);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -76,6 +76,7 @@ IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINAR
|
||||
.PHONY: build-urls
|
||||
build-urls:
|
||||
@echo "You can provide one of the 4 platforms (linux64, macos64, macosm164, win64) using 'PLATFORM=xxx'."
|
||||
@echo "And Python version using 'PYNUMBER=xx' (e.g. 'PYNUMBER=311')."
|
||||
@echo ${IOS_URL}
|
||||
@echo ${IFCCONVERT_URL}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ APPENDABLE_ASSET = Literal[
|
||||
"IfcProfileDef",
|
||||
"IfcPresentationStyle",
|
||||
]
|
||||
APPENDABLE_ASSET_TYPES = get_args(APPENDABLE_ASSET)
|
||||
APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET)
|
||||
MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet")
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import math
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import NamedTuple, Optional, Union, Any
|
||||
from typing import Any, NamedTuple, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import site
|
||||
from pathlib import Path
|
||||
|
||||
SITE = Path(site.getusersitepackages())
|
||||
SITE.mkdir(parents=True, exist_ok=True)
|
||||
REPO_PATH = Path(__file__).parent.parent.parent.parent
|
||||
REPO_PATH_SRC = REPO_PATH / "src"
|
||||
assert REPO_PATH_SRC.exists(), f"'{REPO_PATH_SRC}' doesn't exist."
|
||||
@@ -36,8 +37,8 @@ for package, repo_package_path in packages.items():
|
||||
if package_path.exists():
|
||||
# I guess it's a directory.
|
||||
shutil.rmtree(package_path)
|
||||
package_path.symlink_to(repo_package_path, True)
|
||||
print(f"Symlinking {package_path} -> {repo_package_path}")
|
||||
package_path.symlink_to(repo_package_path, True)
|
||||
|
||||
|
||||
PACKAGE_PATH = SITE / "ifcopenshell"
|
||||
|
||||
@@ -17,21 +17,47 @@
|
||||
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import NamedTuple
|
||||
|
||||
import ifcopenshell.util.element
|
||||
|
||||
import ifcpatch
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
import sqlite3 # noqa: F401
|
||||
except:
|
||||
print("No SQLite support")
|
||||
|
||||
|
||||
class Patcher:
|
||||
class ElementRow(NamedTuple):
|
||||
element_id: int
|
||||
guid: str
|
||||
class_: str
|
||||
predefined_type: str | None
|
||||
name: str | None
|
||||
description: str | None
|
||||
|
||||
|
||||
class PropertyRow(NamedTuple):
|
||||
element_id: int
|
||||
pset_name: str
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
class RelationshipRow(NamedTuple):
|
||||
element_id: int
|
||||
rel_ifc_class: str
|
||||
to_id: int
|
||||
|
||||
|
||||
class Patcher(ifcpatch.BasePatcher):
|
||||
def __init__(
|
||||
self,
|
||||
file,
|
||||
logger,
|
||||
file: ifcopenshell.file,
|
||||
logger: logging.Logger | None = None,
|
||||
):
|
||||
"""Extracts properties and relationships from a IFC-SPF model to SQLite.
|
||||
|
||||
@@ -45,10 +71,11 @@ class Patcher:
|
||||
result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"})
|
||||
ifcpatch.write(result, "output.sqlite")
|
||||
"""
|
||||
self.file = file
|
||||
self.logger = logger
|
||||
super().__init__(file, logger)
|
||||
|
||||
def patch(self):
|
||||
import sqlite3
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||
db_file = tmp.name
|
||||
self.db = sqlite3.connect(db_file)
|
||||
@@ -90,20 +117,21 @@ class Patcher:
|
||||
|
||||
elements = self.file.by_type("IfcObjectDefinition")
|
||||
|
||||
rows = []
|
||||
properties = []
|
||||
relationships = []
|
||||
rows: list[ElementRow] = []
|
||||
properties: list[PropertyRow] = []
|
||||
relationships: list[RelationshipRow] = []
|
||||
id_map = {e.id(): i for i, e in enumerate(elements)}
|
||||
|
||||
for i, element in enumerate(elements):
|
||||
rows.append(
|
||||
[
|
||||
ElementRow(
|
||||
i,
|
||||
element[0], # IfcRoot.GlobalId
|
||||
element.is_a(),
|
||||
ifcopenshell.util.element.get_predefined_type(element),
|
||||
element[2], # IfcRoot.Name
|
||||
element[3], # IfcRoot.Description
|
||||
]
|
||||
)
|
||||
)
|
||||
psets = ifcopenshell.util.element.get_psets(element, should_inherit=False)
|
||||
for pset_name, pset_data in psets.items():
|
||||
@@ -114,49 +142,55 @@ class Patcher:
|
||||
value = "True" if value else "False"
|
||||
elif not isinstance(value, str):
|
||||
value = str(value)
|
||||
properties.append([i, pset_name, prop_name, value])
|
||||
properties.append(PropertyRow(i, pset_name, prop_name, value))
|
||||
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if material:
|
||||
name = getattr(material, "Name", getattr(material, "LayerSetName", None)) or "Unnamed"
|
||||
properties.append([i, "IFC Material", "Name", name])
|
||||
properties.append([i, "IFC Material", "Class", material.is_a()])
|
||||
properties.append(PropertyRow(i, "IFC Material", "Name", name))
|
||||
properties.append(PropertyRow(i, "IFC Material", "Class", material.is_a()))
|
||||
if material.is_a("IfcMaterial"):
|
||||
materials = []
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
for idx, item in enumerate(material.MaterialLayers or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)])
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
|
||||
properties.append(
|
||||
PropertyRow(i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None))
|
||||
)
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Material", material.Name))
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Category", category))
|
||||
elif material.is_a("IfcMaterialProfileSet"):
|
||||
for idx, item in enumerate(material.MaterialProfiles or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Name", item.Name))
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Material", material.Name))
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Category", category))
|
||||
elif material.is_a("IfcMaterialConstituentSet"):
|
||||
for idx, item in enumerate(material.MaterialConstituents or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
|
||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Name", item.Name))
|
||||
properties.append(
|
||||
PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Material", material.Name)
|
||||
)
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Category", category])
|
||||
properties.append(
|
||||
PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Category", category)
|
||||
)
|
||||
elif material.is_a("IfcMaterialList"):
|
||||
for idx, material in enumerate(material.Materials):
|
||||
properties.append([i, "IFC Material", f"Material {idx + 1} Name", material.Name])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Name", material.Name))
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Material {idx + 1} Category", category])
|
||||
properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Category", category))
|
||||
|
||||
layers = ifcopenshell.util.element.get_layers(self.file, element)
|
||||
for idx, layer in enumerate(layers):
|
||||
properties.append([i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name])
|
||||
properties.append(PropertyRow(i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name))
|
||||
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
if relating_type and relating_type != element:
|
||||
relationships.append([i, "IfcRelDefinesByType", id_map[relating_type.id()]])
|
||||
relationships.append(RelationshipRow(i, "IfcRelDefinesByType", id_map[relating_type.id()]))
|
||||
|
||||
self.c.executemany("INSERT INTO elements VALUES (?, ?, ?, ?, ?, ?);", rows)
|
||||
self.c.executemany("INSERT INTO properties VALUES (?, ?, ?, ?);", properties)
|
||||
|
||||
@@ -125,7 +125,6 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
)
|
||||
"""
|
||||
super().__init__(file, logger)
|
||||
self.logger = logger
|
||||
self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower()
|
||||
self.host = host
|
||||
self.username = username
|
||||
|
||||
@@ -16,20 +16,28 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections.abc import Sequence
|
||||
from logging import Logger
|
||||
from typing import Union
|
||||
|
||||
import ifcpatch
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
|
||||
import ifcpatch
|
||||
from ifcpatch.recipes.SetFalseOrigin import Patcher as SetFalseOrigin
|
||||
|
||||
|
||||
class Patcher:
|
||||
def __init__(self, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]):
|
||||
class Patcher(ifcpatch.BasePatcher):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
logger: Logger | None = None,
|
||||
filepaths: Sequence[Union[str, ifcopenshell.file]] = (),
|
||||
):
|
||||
"""Merge two or more IFC models into one
|
||||
|
||||
Note that other than combining the two (or more) IfcProject elements into
|
||||
@@ -50,8 +58,7 @@ class Patcher:
|
||||
|
||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]})
|
||||
"""
|
||||
self.file = file
|
||||
self.logger = logger
|
||||
super().__init__(file, logger)
|
||||
self.filepaths = filepaths
|
||||
|
||||
def patch(self):
|
||||
@@ -61,6 +68,8 @@ class Patcher:
|
||||
"replace it with a list of file/filepaths."
|
||||
)
|
||||
self.filepaths = [self.filepaths]
|
||||
if len(self.filepaths) == 0:
|
||||
raise ValueError("At least one file/filepath must be provided to merge with the main model.")
|
||||
for filepath in self.filepaths:
|
||||
if isinstance(filepath, ifcopenshell.file):
|
||||
other = filepath
|
||||
|
||||
Reference in New Issue
Block a user