mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-27 15:26:46 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04cf9c870b | |||
| bf9bddc867 | |||
| 20a2fda066 | |||
| f03afb20f3 | |||
| e0df22ffec | |||
| 96302a5c07 | |||
| 38afce1628 | |||
| cd5afacc97 | |||
| c2623e5dce | |||
| 42ecd7e127 | |||
| c80a7cfcc9 | |||
| 6d8f1a3980 | |||
| 5c4c61bb15 | |||
| d8af8b58bb | |||
| 3c0405249b | |||
| 7f57649d10 | |||
| fbde23870e | |||
| 7b968030ca | |||
| b5d672ba42 | |||
| efa8da83ba | |||
| 64cf7237db | |||
| cf9a94a799 | |||
| 46c1a8c8ff | |||
| ac7f2aeb62 | |||
| 9378a763f3 | |||
| 0bc83fedae | |||
| f7c03db752 | |||
| b6b8c21741 | |||
| c8af51cfd2 | |||
| 77de3820e7 | |||
| adc5bea4e9 | |||
| 1eddcb9ee0 | |||
| eae542f90f | |||
| fab0d05279 | |||
| d2e495a69f | |||
| 02058efa4d | |||
| ed79cbb1cc | |||
| 0065c5fba8 | |||
| 1ab24e8362 | |||
| 232b4ec34d | |||
| 75e6147dbc | |||
| 8d7b293381 | |||
| 19c8afb652 | |||
| 84dcee6b36 | |||
| dadcf03f55 | |||
| 413886ac6b | |||
| 377f68c020 | |||
| b718a37db2 | |||
| e21043b038 | |||
| 395598d8e3 | |||
| 813518bcb5 | |||
| 4ce7a898c9 | |||
| 78f235ea7c | |||
| bda2f50f9d | |||
| 5e5ba5b3d4 | |||
| 22b0e9dcc5 | |||
| 68420d43ea | |||
| 924e337c90 | |||
| 04599a10d2 | |||
| 84c931bb5f | |||
| 572aedb3eb | |||
| 1db86c24e3 | |||
| 74adde3030 | |||
| f68e8f0e00 | |||
| 98386984f3 | |||
| 7b35ed97ca | |||
| b3302ed679 | |||
| e12f060af8 | |||
| 6d4f47a2ca | |||
| e49ab7e0cc | |||
| ccfe7ab80a | |||
| 7b95358975 | |||
| bcc50aaadc | |||
| 30f63b7d67 | |||
| ead13e23bc | |||
| 24257d2a85 | |||
| 5c4dbb924d | |||
| 88e1b3d2f9 | |||
| f67b80c6a6 | |||
| c7fd42f2cf | |||
| bed03bf83c | |||
| 1e49400e93 | |||
| 608c0dc05d | |||
| 921dbe9dae | |||
| a7e0837f96 | |||
| 071c80f845 | |||
| 06d823b9d8 | |||
| b15e33e5e9 |
@@ -137,7 +137,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -362,7 +362,9 @@ endif
|
||||
# Provides express rule validation for ifcopenshell.validate
|
||||
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
|
||||
# Provides Brickschema functionality
|
||||
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
|
||||
# Required for SVG to DXF conversion
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
|
||||
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -75,6 +75,10 @@ def draw_attribute(attribute, layout, copy_operator=None):
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
if attribute.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
if attribute.name == "GlobalId":
|
||||
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
||||
|
||||
if copy_operator:
|
||||
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
|
||||
op.name = attribute.name
|
||||
@@ -271,6 +275,9 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.type = sprops.facet
|
||||
op.index = i
|
||||
op.module = module
|
||||
op = row.operator("bim.remove_filter_group", text="", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
for j, ifc_filter in enumerate(filter_group.filters):
|
||||
if ifc_filter.type == "entity":
|
||||
@@ -317,11 +324,6 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.index = j
|
||||
op.module = module
|
||||
|
||||
row = box.row()
|
||||
op = row.operator("bim.remove_filter_group", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
|
||||
# TODO this should move into ifcopenshell.util
|
||||
|
||||
|
||||
@@ -1164,6 +1164,9 @@ class IfcImporter:
|
||||
|
||||
return obj
|
||||
|
||||
def load_existing_meshes(self) -> None:
|
||||
self.meshes.update({m.name: m for m in bpy.data.meshes})
|
||||
|
||||
def get_representation_item_material_name(self, item):
|
||||
if not item.StyledByItem:
|
||||
return
|
||||
|
||||
@@ -120,20 +120,46 @@ class EditAttributes(bpy.types.Operator, Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GenerateGlobalId(bpy.types.Operator):
|
||||
class GenerateGlobalId(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.generate_global_id"
|
||||
bl_label = "Regenerate GlobalId"
|
||||
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
|
||||
if index >= 0:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes[index]
|
||||
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# using all selected objects on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.use_selected = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.use_selected:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcRoot"):
|
||||
continue
|
||||
element.GlobalId = ifcopenshell.guid.new()
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
|
||||
return {"FINISHED"}
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcRoot"):
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.use_selected and obj in context.selected_objects:
|
||||
# guid value was already regenerated, just update the ui prop
|
||||
guid_value = element.GlobalId
|
||||
else:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes.add()
|
||||
global_id.name = "GlobalId"
|
||||
global_id.data_type = "string"
|
||||
global_id.string_value = ifcopenshell.guid.new()
|
||||
guid_value = ifcopenshell.guid.new()
|
||||
|
||||
props.attributes["GlobalId"].string_value = guid_value
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ from math import pi
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
|
||||
@@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator):
|
||||
row.prop(props, "resolution")
|
||||
|
||||
def hotkey_S_C(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
@@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_trim_extend()
|
||||
|
||||
def hotkey_S_F(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
|
||||
@@ -17,138 +17,36 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.classification
|
||||
import json
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
ClassificationsData.is_loaded = False
|
||||
ClassificationReferencesData.is_loaded = False
|
||||
MaterialClassificationsData.is_loaded = False
|
||||
CostClassificationsData.is_loaded = False
|
||||
ClashData.is_loaded = False
|
||||
|
||||
|
||||
class ClassificationsData:
|
||||
class ClashData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["has_classification_file"] = cls.has_classification_file()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["available_classifications"] = cls.available_classifications()
|
||||
cls.data = {}
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
|
||||
@classmethod
|
||||
def has_classification_file(cls):
|
||||
return bool(IfcStore.classification_file)
|
||||
|
||||
@classmethod
|
||||
def classifications(cls):
|
||||
results = []
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
data = element.get_info()
|
||||
if tool.Ifc.get().schema == "IFC2X3" and element.EditionDate:
|
||||
data["EditionDate"] = ifcopenshell.util.date.ifc2datetime(data["EditionDate"])
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def available_classifications(cls):
|
||||
if not IfcStore.classification_file:
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ReferencesData:
|
||||
@classmethod
|
||||
def active_classification_library(cls):
|
||||
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
|
||||
return False
|
||||
props = bpy.context.scene.BIMClassificationProperties
|
||||
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
|
||||
if name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]:
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def classifications(cls):
|
||||
return [(str(e.id()), e.Name, "") for e in tool.Ifc.get().by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ClassificationReferencesData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "OBJECT"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class MaterialClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "MATERIAL"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class CostClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "COST"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
|
||||
@@ -16,31 +16,37 @@
|
||||
# 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 blf
|
||||
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
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
|
||||
class ClashDecorator:
|
||||
installed = None
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
@@ -48,7 +54,27 @@ class ClashDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
def draw_text(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
|
||||
|
||||
text = context.scene.BIMClashProperties.active_clash_text
|
||||
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
|
||||
|
||||
font_id = 0
|
||||
blf.size(font_id, 12)
|
||||
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
|
||||
color = self.addon_prefs.decorations_colour
|
||||
blf.color(font_id, *color)
|
||||
if coords_2d:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
coords_2d -= Vector((w * .5, 0))
|
||||
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
|
||||
blf.draw(font_id, text) # Set your text here
|
||||
|
||||
def draw_geometry(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
|
||||
|
||||
@@ -69,6 +69,7 @@ class ImportClashSets(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
context.scene.BIMClashProperties.clash_sets.clear()
|
||||
for clash_set in tool.Clash.get_clash_sets():
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new.name = clash_set["name"]
|
||||
@@ -85,14 +86,14 @@ class ImportClashSets(bpy.types.Operator):
|
||||
new_source = new.a.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
if "b" in clash_set and clash_set["b"]:
|
||||
for clash_source in clash_set["b"]:
|
||||
new_source = new.b.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
@@ -208,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
# if extension != ".json":
|
||||
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -220,6 +224,9 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
self.props = context.scene.BIMClashProperties
|
||||
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
|
||||
|
||||
@@ -276,14 +283,7 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
if extension == ".json":
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
result = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
||||
for clash in result["clashes"].values():
|
||||
blender_clash = self.props.active_clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -379,6 +379,7 @@ class SelectClash(bpy.types.Operator):
|
||||
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
|
||||
self.props.p1 = clash["p1"]
|
||||
self.props.p2 = clash["p2"]
|
||||
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,11 +33,12 @@ from bpy.props import (
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
name: StringProperty(name="File")
|
||||
selector: StringProperty(name="Selector")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
("i", "Include", "Only the selected objects are included for clashing"),
|
||||
("e", "Exclude", "All objects except the selected objects are included for clashing"),
|
||||
("a", "All Elements", "All elements will be used for clashing"),
|
||||
("i", "Include", "Only the selected elements are included for clashing"),
|
||||
("e", "Exclude", "All elements except the selected elements are included for clashing"),
|
||||
],
|
||||
name="Mode",
|
||||
)
|
||||
@@ -97,6 +98,7 @@ class BIMClashProperties(PropertyGroup):
|
||||
)
|
||||
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
active_clash_text: StringProperty(name="Active Clash Text")
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.module.clash.data import ClashData
|
||||
|
||||
|
||||
class BIM_PT_ifcclash(Panel):
|
||||
@@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_clash_detection"
|
||||
|
||||
def draw(self, context):
|
||||
if not ClashData.is_loaded:
|
||||
ClashData.load()
|
||||
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
@@ -78,6 +83,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
for index, source in enumerate(clash_set.a):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
@@ -85,9 +91,10 @@ class BIM_PT_ifcclash(Panel):
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
|
||||
@@ -96,6 +103,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
for index, source in enumerate(clash_set.b):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
@@ -103,9 +111,10 @@ class BIM_PT_ifcclash(Panel):
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
|
||||
)
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "should_create_clash_snapshots")
|
||||
|
||||
@@ -75,6 +75,9 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.profile_import_ifc")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.generate_global_id")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.operator("bim.create_shape_from_step_id").should_include_curves = False
|
||||
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
|
||||
|
||||
@@ -38,6 +38,7 @@ class DiffData:
|
||||
cls.data["total_deleted"] = cls.total_deleted()
|
||||
cls.data["total_changed"] = cls.total_changed()
|
||||
cls.data["changes"] = cls.changes()
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
@@ -84,3 +85,18 @@ class DiffData:
|
||||
elif element.GlobalId in diff["deleted"]:
|
||||
changes["Deleted"] = True
|
||||
return changes
|
||||
|
||||
@classmethod
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
|
||||
@@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
bl_label = "Execute IFC Diff"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
@@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
|
||||
relationships = [r.relationship for r in self.props.diff_relationships]
|
||||
query = self.props.diff_filter_elements
|
||||
query = tool.Search.export_filter_query(self.props.filter_groups) or None
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
|
||||
ifc_diff.diff()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
|
||||
old_file: StringProperty(default="", name="Old IFC File")
|
||||
new_file: StringProperty(default="", name="New IFC File")
|
||||
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
|
||||
diff_filter_elements: StringProperty(default="", name="Filter")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
|
||||
active_file: EnumProperty(
|
||||
items=[
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import json
|
||||
|
||||
|
||||
class BIM_PT_diff(Panel):
|
||||
@@ -93,9 +93,7 @@ class BIM_PT_diff(Panel):
|
||||
remove.collection = "diff_relationships"
|
||||
remove.index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diff_filter_elements")
|
||||
row.operator("bim.ifc_selector", icon="FILTER", text="")
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.execute_ifc_diff")
|
||||
|
||||
@@ -41,6 +41,7 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ContractTargetView,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableAddAnnotationType,
|
||||
|
||||
@@ -188,7 +188,6 @@ def format_distance(
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
tx_dist = ""
|
||||
@@ -299,7 +298,9 @@ def parse_diagram_scale(camera):
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
def ortho_view_frame(camera, margin=0.015):
|
||||
def ortho_view_frame(
|
||||
camera: bpy.types.Camera, margin: float = 0.015
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
"""Calculates 2d bounding box of camera view area.
|
||||
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
@@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.bim.export_ifc
|
||||
from blenderbim.bim.module.drawing.decoration import CutDecorator
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||
from typing import NamedTuple, List
|
||||
from typing import NamedTuple, List, Union
|
||||
from lxml import etree
|
||||
from mathutils import Vector, Color, Matrix
|
||||
from timeit import default_timer as timer
|
||||
@@ -2685,3 +2685,50 @@ class AddReferenceImage(bpy.types.Operator, Operator):
|
||||
)
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
|
||||
class ConvertSVGToDXF(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_svg_to_dxf"
|
||||
bl_label = "Convert SVG to DXF"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
view: bpy.props.StringProperty()
|
||||
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
|
||||
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# convert all drawings on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.open_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
if self.convert_all:
|
||||
drawings = [
|
||||
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
|
||||
]
|
||||
else:
|
||||
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
|
||||
|
||||
drawing_uris: list[Path] = []
|
||||
drawings_not_found: list[str] = []
|
||||
|
||||
for drawing in drawings:
|
||||
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
||||
if drawing_uri is None or not os.path.exists(drawing_uri):
|
||||
drawings_not_found.append(drawing.Name)
|
||||
else:
|
||||
drawing_uris.append(Path(drawing_uri))
|
||||
|
||||
if drawings_not_found:
|
||||
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
|
||||
"\n".join(drawings_not_found)
|
||||
)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
for drawing_uri in drawing_uris:
|
||||
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
|
||||
|
||||
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel):
|
||||
col = row.column()
|
||||
col.alignment = "RIGHT"
|
||||
|
||||
convert_to_dxf = row.row(align=True)
|
||||
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
|
||||
op.view = active_drawing.name
|
||||
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
|
||||
|
||||
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
|
||||
|
||||
open_drawing_button = row.row(align=True)
|
||||
|
||||
@@ -21,12 +21,14 @@ import bmesh
|
||||
import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Helper:
|
||||
def __init__(self, file):
|
||||
def __init__(self, file: ifcopenshell.file):
|
||||
self.file = file
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
@@ -34,7 +36,7 @@ class Helper:
|
||||
# edge that shares a single vertex only with that face to find the extrusion
|
||||
# edge. A face with the normal facing down is prioritised. A limited
|
||||
# dissolve ensure that faces are quads and not tris.
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
@@ -115,7 +117,9 @@ class Helper:
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
|
||||
def auto_detect_arbitrary_profile_with_voids(
|
||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
||||
) -> Union[tuple, dict]:
|
||||
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
@@ -138,28 +142,31 @@ class Helper:
|
||||
total_groups = 0
|
||||
is_circle = False
|
||||
for group_type, group_indices in groups.items():
|
||||
for group_index in group_indices:
|
||||
if group_index in vert[deform_layer]:
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if not group_indices:
|
||||
continue
|
||||
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
|
||||
if not is_special:
|
||||
continue
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if total_groups > 1: # A vert can only belong to one group
|
||||
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
|
||||
elif is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = set(bm.edges)
|
||||
@@ -269,7 +276,7 @@ class Helper:
|
||||
inner_loops.remove(outer_loop)
|
||||
|
||||
# Copy vectors to prevent random data mangling after bmesh is freed.
|
||||
points = [Vector(list(v.co)) for v in bm.verts]
|
||||
points = [v.co.copy() for v in bm.verts]
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
|
||||
@@ -908,7 +908,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
index += 1
|
||||
pset = tool.Ifc.get().by_id(pset['id'])
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -976,26 +976,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
add_linked_aggregate_pset(element, index)
|
||||
index +=1
|
||||
index += 1
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
add_linked_aggregate_pset(part, index)
|
||||
index += 1
|
||||
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
|
||||
def add_linked_aggregate_pset(part, index):
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -1029,21 +1026,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
][0]
|
||||
|
||||
|
||||
number = len(group_elements) - 1
|
||||
number = f"{number:02d}"
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
pattern1 = r'_\d'
|
||||
pattern1 = r"_\d"
|
||||
if re.findall(pattern1, new_obj.name):
|
||||
split_name = new_obj.name.split("_")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
continue
|
||||
pattern2 = r'\.\d{3}'
|
||||
pattern2 = r"\.\d{3}"
|
||||
if re.findall(pattern2, new_obj.name):
|
||||
split_name = new_obj.name.split(".")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1065,7 +1061,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
@@ -1075,17 +1071,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i['Index'] for i in psets if i]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
@@ -1134,9 +1128,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(element).name
|
||||
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
@@ -1144,20 +1138,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names | get_original_names(part)
|
||||
else:
|
||||
try:
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
except:
|
||||
continue
|
||||
index = pset['Index']
|
||||
continue
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(part).name
|
||||
|
||||
|
||||
return original_names
|
||||
|
||||
def set_original_name(obj, original_names):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
if ifcopenshell.util.element.get_parts(
|
||||
element
|
||||
): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
aggregate = element
|
||||
|
||||
|
||||
group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
@@ -1166,18 +1162,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
]
|
||||
if not group:
|
||||
return
|
||||
|
||||
|
||||
group = group[0].id()
|
||||
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
|
||||
index = pset["Index"]
|
||||
|
||||
try:
|
||||
obj.name = original_names[group][index]
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
|
||||
def get_element_assembly(element):
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
@@ -1263,9 +1257,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
|
||||
original_names = get_original_names(element)
|
||||
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
@@ -1280,24 +1274,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly"):
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
|
||||
if not new_aggregate:
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
set_original_name(new_obj, original_names)
|
||||
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
|
||||
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
|
||||
if props.is_editing_item_style:
|
||||
# NOTE: we currently support 1 item having just 1 style
|
||||
# when IfcStyledItem can actually have multiple styles
|
||||
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
|
||||
else:
|
||||
|
||||
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material"
|
||||
bl_label = "Add Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Material Name")
|
||||
name: bpy.props.StringProperty(default="Default")
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
@@ -23,6 +23,7 @@ from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Union
|
||||
|
||||
|
||||
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
|
||||
return color
|
||||
|
||||
|
||||
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
|
||||
"""returns tuple boolean (whether vertex is in any of the groups)
|
||||
and related group index"""
|
||||
for group_index in vertex[deform_layer].keys():
|
||||
if group_index in groups:
|
||||
return True, group_index
|
||||
return False, None
|
||||
|
||||
|
||||
class ProfileDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -148,12 +140,12 @@ class ProfileDecorator:
|
||||
# deform_layer is None if there are no verts assigned to vertex groups
|
||||
# even if there are vertex groups in the obj.vertex_groups
|
||||
if deform_layer:
|
||||
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
if is_arc:
|
||||
arcs.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
if is_circle:
|
||||
circles.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
@@ -20,9 +20,12 @@ import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.type
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
@@ -698,9 +701,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
new_footprint = ifcopenshell.api.run(
|
||||
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
|
||||
@@ -113,6 +113,10 @@ class CreateProject(bpy.types.Operator):
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
for mesh in bpy.data.meshes:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
for mat in bpy.data.materials:
|
||||
bpy.data.materials.remove(mat)
|
||||
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
|
||||
tool.Blender.register_toolbar()
|
||||
|
||||
@@ -856,7 +860,10 @@ class LinkIfc(bpy.types.Operator):
|
||||
continue
|
||||
new = context.scene.BIMProjectProperties.links.add()
|
||||
if self.use_relative_path:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
try:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
except:
|
||||
pass # Perhaps on another drive or something
|
||||
new.name = filepath
|
||||
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
|
||||
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
||||
@@ -1287,6 +1294,8 @@ class LoadLinkedProject(bpy.types.Operator):
|
||||
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
|
||||
self.process_occurrence(shape)
|
||||
if not iterator.next():
|
||||
if not chunked_verts:
|
||||
break
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
|
||||
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim
|
||||
from typing import Literal, Union, Optional
|
||||
|
||||
|
||||
AxisType = Literal["x", "y", "z"]
|
||||
VectorTuple = tuple[float, float, float]
|
||||
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
|
||||
|
||||
|
||||
class QtoCalculator:
|
||||
@@ -45,7 +53,7 @@ class QtoCalculator:
|
||||
else:
|
||||
self.mapping_dict[key][item] = None
|
||||
|
||||
def calculate_quantity(self, qto_name, quantity_name, obj):
|
||||
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
"""calculates the value of the quantity in the project units"""
|
||||
string = "self.mapping_dict[qto_name][quantity_name](obj"
|
||||
if isinstance(mapper[qto_name][quantity_name], dict):
|
||||
@@ -54,11 +62,13 @@ class QtoCalculator:
|
||||
args = ""
|
||||
string += args
|
||||
string += ")"
|
||||
value = eval(string)
|
||||
value: float = eval(string)
|
||||
|
||||
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
|
||||
|
||||
def guess_quantity(self, prop_name, alternative_prop_names, obj):
|
||||
def guess_quantity(
|
||||
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
|
||||
) -> Union[float, None]:
|
||||
"""guess the value of the quantity by name, returns the value in the project units"""
|
||||
prop_name = prop_name.lower()
|
||||
alternative_prop_names = [p.lower() for p in alternative_prop_names]
|
||||
@@ -89,7 +99,7 @@ class QtoCalculator:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
unit_type_keywords = {
|
||||
unit_type_keywords: dict[str, QuanityTypes] = {
|
||||
"length": "Q_LENGTH",
|
||||
"width": "Q_LENGTH",
|
||||
"height": "Q_LENGTH",
|
||||
@@ -102,10 +112,10 @@ class QtoCalculator:
|
||||
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
|
||||
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
|
||||
|
||||
def get_units(self, o, vg_index):
|
||||
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
|
||||
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
|
||||
|
||||
def get_linear_length(self, o):
|
||||
def get_linear_length(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the length of the longest edge of the object bounding box
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -116,7 +126,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x, y, z)
|
||||
|
||||
def get_length(self, o, vg_index=None, main_axis: str = ""):
|
||||
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
|
||||
if vg_index is None:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
@@ -141,23 +151,23 @@ class QtoCalculator:
|
||||
length += self.get_edge_distance(o, e)
|
||||
return length
|
||||
|
||||
def get_stair_length(self, obj):
|
||||
def get_stair_length(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj)
|
||||
height = self.get_height(obj)
|
||||
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
|
||||
return stair_length
|
||||
|
||||
def get_net_stair_area(self, obj):
|
||||
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
|
||||
return OBB_net_footprint_area
|
||||
|
||||
def get_gross_stair_area(self, obj):
|
||||
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
|
||||
return OBB_gross_footprint_area
|
||||
|
||||
def get_parametric_axis(self, obj):
|
||||
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
|
||||
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
|
||||
if relating_type:
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -172,7 +182,7 @@ class QtoCalculator:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_covering_gross_area(self, obj):
|
||||
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -181,7 +191,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_gross_footprint_area(obj)
|
||||
|
||||
def get_covering_net_area(self, obj):
|
||||
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_net_footprint_area(obj)
|
||||
@@ -190,7 +200,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_net_footprint_area(obj)
|
||||
|
||||
def get_covering_width(self, obj):
|
||||
def get_covering_width(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_height(obj)
|
||||
@@ -199,7 +209,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_width(self, o):
|
||||
def get_width(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the width of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -209,7 +219,7 @@ class QtoCalculator:
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
return min(x, y)
|
||||
|
||||
def get_height(self, o):
|
||||
def get_height(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -217,32 +227,32 @@ class QtoCalculator:
|
||||
"""
|
||||
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
def get_opening_height(self, obj):
|
||||
def get_opening_height(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_width(obj)
|
||||
else:
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_opening_depth(self, obj):
|
||||
def get_opening_depth(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_height(obj)
|
||||
else:
|
||||
return self.get_width(obj)
|
||||
|
||||
def get_opening_mapping_area(self, obj):
|
||||
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_net_footprint_area(obj)
|
||||
else:
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
def get_finish_ceiling_height(self, obj):
|
||||
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
space_height = self.get_height(obj)
|
||||
floor_height = self.get_finish_floor_height(obj)
|
||||
ceiling_height = self.get_ceiling_height(obj)
|
||||
finish_ceiling_height = space_height - floor_height - ceiling_height
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_finish_floor_height(self, obj):
|
||||
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_floor_height = 0
|
||||
@@ -258,7 +268,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_floor_height
|
||||
|
||||
def get_ceiling_height(self, obj):
|
||||
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_ceiling_height = 0
|
||||
@@ -274,7 +284,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_net_perimeter(self, o):
|
||||
def get_net_perimeter(self, o: bpy.types.Object) -> float:
|
||||
parsed_edges = []
|
||||
shared_edges = []
|
||||
perimeter = 0
|
||||
@@ -289,7 +299,7 @@ class QtoCalculator:
|
||||
perimeter -= self.get_edge_key_distance(o, edge_key)
|
||||
return perimeter
|
||||
|
||||
def get_gross_perimeter(self, o):
|
||||
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(o)
|
||||
mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("GrossObj", mesh)
|
||||
@@ -297,15 +307,15 @@ class QtoCalculator:
|
||||
self.delete_obj(gross_obj)
|
||||
return gross_perimeter
|
||||
|
||||
def get_space_net_perimeter(self, obj):
|
||||
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
pass
|
||||
|
||||
def get_rectangular_perimeter(self, obj):
|
||||
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj, main_axis="x")
|
||||
height = self.get_height(obj)
|
||||
return (length + height) * 2
|
||||
|
||||
def get_lowest_polygons(self, o):
|
||||
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
lowest_polygons = []
|
||||
lowest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -321,7 +331,7 @@ class QtoCalculator:
|
||||
lowest_z = z
|
||||
return lowest_polygons
|
||||
|
||||
def get_highest_polygons(self, o):
|
||||
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
highest_polygons = []
|
||||
highest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -337,13 +347,13 @@ class QtoCalculator:
|
||||
highest_z = z
|
||||
return highest_polygons
|
||||
|
||||
def get_edge_key_distance(self, obj, edge_key):
|
||||
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
|
||||
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
|
||||
|
||||
def get_edge_distance(self, obj, edge):
|
||||
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
|
||||
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
|
||||
|
||||
def get_net_floor_area(self, obj):
|
||||
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -359,7 +369,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_floor_area
|
||||
|
||||
def get_gross_ceiling_area(self, obj):
|
||||
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_top_area(obj)
|
||||
@@ -375,7 +385,7 @@ class QtoCalculator:
|
||||
|
||||
return total_gross_ceiling_area
|
||||
|
||||
def get_net_ceiling_area(self, obj):
|
||||
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_net_top_area(obj)
|
||||
@@ -395,7 +405,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_ceiling_area
|
||||
|
||||
def get_space_net_volume(self, obj):
|
||||
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_volume(obj)
|
||||
@@ -410,7 +420,7 @@ class QtoCalculator:
|
||||
|
||||
return total_space_net_volume
|
||||
|
||||
def get_net_footprint_area(self, o):
|
||||
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -421,7 +431,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_gross_footprint_area(self, o):
|
||||
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -437,7 +447,7 @@ class QtoCalculator:
|
||||
self.delete_mesh(mesh)
|
||||
return gross_footprint_area
|
||||
|
||||
def get_net_roofprint_area(self, o):
|
||||
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
|
||||
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
|
||||
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
|
||||
|
||||
@@ -449,7 +459,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_side_area(self, o):
|
||||
def get_side_area(self, o: bpy.types.Object) -> float:
|
||||
# There are a few dumb options for this, but this seems the dumbest
|
||||
# until I get more practical experience on what works best.
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
@@ -457,8 +467,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x * z, y * z)
|
||||
|
||||
def get_cross_section_area(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
@@ -474,7 +483,7 @@ class QtoCalculator:
|
||||
return area
|
||||
# TODO handle other types of sections, and then fall back to mesh parsing
|
||||
|
||||
def get_gross_surface_area(self, o, vg_index=None):
|
||||
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
|
||||
if vg_index is None:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_surface_area(o)
|
||||
@@ -492,29 +501,29 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_net_surface_area(self, obj):
|
||||
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
return self.get_mesh_area(obj.data)
|
||||
|
||||
def get_mesh_area(self, mesh):
|
||||
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
|
||||
area = 0
|
||||
for polygon in mesh.polygons:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def is_polygon_in_vg(self, polygon, vertices_in_vg):
|
||||
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
|
||||
for v in polygon.vertices:
|
||||
if v not in vertices_in_vg:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_net_volume(self, o):
|
||||
def get_net_volume(self, o: bpy.types.Object) -> float:
|
||||
o_mesh = bmesh.new()
|
||||
o_mesh.from_mesh(o.data)
|
||||
volume = o_mesh.calc_volume()
|
||||
o_mesh.free()
|
||||
return volume
|
||||
|
||||
def get_gross_volume(self, o):
|
||||
def get_gross_volume(self, o: bpy.types.Object) -> float:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_volume(o)
|
||||
|
||||
@@ -529,16 +538,18 @@ class QtoCalculator:
|
||||
|
||||
return gross_volume
|
||||
|
||||
def has_openings(self, obj):
|
||||
def has_openings(
|
||||
self, obj: bpy.types.Object
|
||||
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return element and getattr(element, "HasOpenings", [])
|
||||
|
||||
def get_obj_decompositions(self, obj):
|
||||
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
return decompositions
|
||||
|
||||
def get_gross_weight(self, obj):
|
||||
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -546,7 +557,7 @@ class QtoCalculator:
|
||||
gross_weight = obj_mass_density * gross_volume
|
||||
return gross_weight
|
||||
|
||||
def get_net_weight(self, obj):
|
||||
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -554,7 +565,7 @@ class QtoCalculator:
|
||||
net_weight = obj_mass_density * net_volume
|
||||
return net_weight
|
||||
|
||||
def get_obj_mass_density(self, obj):
|
||||
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
material = ifcopenshell.util.element.get_material(entity)
|
||||
if material is None:
|
||||
@@ -629,7 +640,7 @@ class QtoCalculator:
|
||||
# volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
# return volume
|
||||
|
||||
def get_opening_type(self, opening, obj):
|
||||
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
|
||||
"""_summary_: Returns the opening type - OPENING / RECESS
|
||||
|
||||
:param blender-object opening: blender opening object
|
||||
@@ -649,14 +660,22 @@ class QtoCalculator:
|
||||
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
|
||||
|
||||
def get_opening_area(
|
||||
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
|
||||
):
|
||||
self,
|
||||
obj: bpy.types.Object,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
min_area: int = 0,
|
||||
ignore_recesses: bool = False,
|
||||
) -> float:
|
||||
"""_summary_: Returns the lateral area of the openings in the object.
|
||||
|
||||
:param obj: blender object
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
|
||||
defaults to 0
|
||||
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
|
||||
:return float: Opening Area
|
||||
"""
|
||||
@@ -702,14 +721,14 @@ class QtoCalculator:
|
||||
|
||||
def get_lateral_area(
|
||||
self,
|
||||
obj,
|
||||
obj: bpy.types.Object,
|
||||
subtract_openings: bool = True,
|
||||
exclude_end_areas: bool = False,
|
||||
exclude_side_areas: bool = False,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
main_axis: str = "",
|
||||
):
|
||||
) -> float:
|
||||
"""_summary_
|
||||
|
||||
:param blender-object obj: blender object, bpy.types.Object
|
||||
@@ -760,7 +779,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area + total_opening_area
|
||||
|
||||
def get_gross_side_area(self, obj):
|
||||
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
|
||||
if not self.has_openings(obj):
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
@@ -768,15 +787,15 @@ class QtoCalculator:
|
||||
|
||||
return gross_side_area
|
||||
|
||||
def get_net_side_area(self, obj):
|
||||
def get_net_side_area(self, obj: bpy.types.Object) -> float:
|
||||
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
|
||||
return net_side_area
|
||||
|
||||
def get_outer_surface_area(self, obj):
|
||||
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
|
||||
return outer_surface_area
|
||||
|
||||
def get_end_area(self, obj):
|
||||
def get_end_area(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
gross_mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
|
||||
@@ -790,7 +809,7 @@ class QtoCalculator:
|
||||
|
||||
return end_area
|
||||
|
||||
def get_gross_top_area(self, obj, angle: int = 45):
|
||||
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
|
||||
"""_summary_: Returns the gross top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
@@ -827,12 +846,14 @@ class QtoCalculator:
|
||||
return area + opening_area
|
||||
|
||||
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
|
||||
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
|
||||
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
|
||||
"""_summary_: Returns the net top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
|
||||
Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
|
||||
defaults to True
|
||||
:return float: Net Top Area
|
||||
"""
|
||||
z_axis = (0, 0, 1)
|
||||
@@ -852,11 +873,11 @@ class QtoCalculator:
|
||||
|
||||
return area
|
||||
|
||||
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
|
||||
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
|
||||
"""_summary_: Returns the projected area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
|
||||
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
|
||||
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
|
||||
:return float: Projected Area
|
||||
"""
|
||||
@@ -891,7 +912,7 @@ class QtoCalculator:
|
||||
return projected_polygon.area + void_area
|
||||
return projected_polygon.area
|
||||
|
||||
def get_OBB_object(self, obj):
|
||||
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -932,7 +953,7 @@ class QtoCalculator:
|
||||
|
||||
return new_OBB_object
|
||||
|
||||
def get_AABB_object(self, obj):
|
||||
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -988,12 +1009,12 @@ class QtoCalculator:
|
||||
|
||||
def get_bisected_obj(
|
||||
self,
|
||||
obj,
|
||||
plane_co_pos,
|
||||
plane_no_pos,
|
||||
plane_co_neg,
|
||||
plane_no_neg,
|
||||
):
|
||||
obj: bpy.types.Object,
|
||||
plane_co_pos: VectorTuple,
|
||||
plane_no_pos: VectorTuple,
|
||||
plane_co_neg: VectorTuple,
|
||||
plane_no_neg: VectorTuple,
|
||||
) -> bpy.types.Object:
|
||||
"""_summary_: Returns the object bisected by two planes.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1031,11 +1052,12 @@ class QtoCalculator:
|
||||
|
||||
return bis_obj
|
||||
|
||||
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
|
||||
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
|
||||
"""_summary_: Returns the total contact area of the object with other objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return float: Total contact area of the object with other objects.
|
||||
"""
|
||||
total_contact_area = 0
|
||||
@@ -1046,11 +1068,12 @@ class QtoCalculator:
|
||||
|
||||
return total_contact_area
|
||||
|
||||
def get_touching_objects(self, obj, class_filter):
|
||||
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
|
||||
"""_summary_: Returns a list of objects that are touching the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return list: List of touching objects
|
||||
"""
|
||||
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
|
||||
@@ -1094,7 +1117,7 @@ class QtoCalculator:
|
||||
|
||||
return touching_objects
|
||||
|
||||
def get_contact_area(self, object1, object2):
|
||||
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the contact area between two objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1109,7 +1132,13 @@ class QtoCalculator:
|
||||
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
|
||||
return total_area
|
||||
|
||||
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
|
||||
def get_intersection_between_polygons(
|
||||
self,
|
||||
object1: bpy.types.Object,
|
||||
poly1: bpy.types.MeshPolygon,
|
||||
object2: bpy.types.Object,
|
||||
poly2: bpy.types.MeshPolygon,
|
||||
) -> float:
|
||||
"""_summary_: Returns the intersection between two polygons.
|
||||
|
||||
:param blender-object object1: Blender Object
|
||||
@@ -1152,7 +1181,9 @@ class QtoCalculator:
|
||||
# TopologicalError - Generated Geometry might be invalid
|
||||
return 0
|
||||
|
||||
def create_shapely_polygon(self, obj, polygon, trans_matrix):
|
||||
def create_shapely_polygon(
|
||||
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
|
||||
) -> Polygon:
|
||||
"""_summary_: Create a shapely polygon
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1171,12 +1202,14 @@ class QtoCalculator:
|
||||
polygon_tuples.append((x, y))
|
||||
return Polygon(polygon_tuples)
|
||||
|
||||
def get_gross_element_mesh(self, element):
|
||||
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
return self.create_mesh_from_shape(element, settings)
|
||||
|
||||
def create_mesh_from_shape(self, element, settings=None):
|
||||
def create_mesh_from_shape(
|
||||
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
|
||||
) -> bpy.types.Mesh:
|
||||
if settings is None:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
@@ -1203,12 +1236,12 @@ class QtoCalculator:
|
||||
mesh.update()
|
||||
return mesh
|
||||
|
||||
def get_bmesh_from_mesh(self, mesh):
|
||||
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
return bm
|
||||
|
||||
def get_object_main_axis(self, o):
|
||||
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
|
||||
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -1227,18 +1260,18 @@ class QtoCalculator:
|
||||
else:
|
||||
return "x"
|
||||
|
||||
def is_opening_horizontal(self, o):
|
||||
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
return z < x and z < y
|
||||
|
||||
def delete_mesh(self, mesh):
|
||||
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
|
||||
mesh.user_clear()
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
def delete_obj(self, obj):
|
||||
def delete_obj(self, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
|
||||
return ""
|
||||
|
||||
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if "is_expanded" not in pset:
|
||||
@@ -114,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
elif props.active_pset_id != pset_id:
|
||||
row.label(text=pset["Name"], icon="COPY_ID")
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
if pset["is_expanded"]:
|
||||
if props.active_pset_id == pset_id:
|
||||
for prop in props.properties:
|
||||
@@ -211,7 +215,7 @@ class BIM_PT_object_psets(Panel):
|
||||
if ObjectPsetsData.data["inherited_psets"]:
|
||||
self.layout.label(text="Type:")
|
||||
for pset in ObjectPsetsData.data["inherited_psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_object_qtos(Panel):
|
||||
@@ -246,7 +250,7 @@ class BIM_PT_object_qtos(Panel):
|
||||
|
||||
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
|
||||
|
||||
|
||||
if ObjectQtosData.data["qtos"]:
|
||||
self.layout.label(text="Instance:")
|
||||
for qto in ObjectQtosData.data["qtos"]:
|
||||
@@ -255,7 +259,7 @@ class BIM_PT_object_qtos(Panel):
|
||||
if ObjectQtosData.data["inherited_qsets"]:
|
||||
self.layout.label(text="Type:")
|
||||
for qset in ObjectQtosData.data["inherited_qsets"]:
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_material_psets(Panel):
|
||||
|
||||
@@ -34,6 +34,7 @@ classes = (
|
||||
operator.RemovePropTemplate,
|
||||
operator.RemovePsetTemplate,
|
||||
operator.RemovePsetTemplateFile,
|
||||
operator.SavePsetTemplateFile,
|
||||
prop.PsetTemplate,
|
||||
prop.EnumerationValues,
|
||||
prop.PropTemplate,
|
||||
|
||||
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
bl_idname = "bim.unlink_object"
|
||||
bl_label = "Unlink Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Object Name")
|
||||
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
|
||||
if obj.data:
|
||||
obj_copy.data = obj.data.copy()
|
||||
|
||||
# prevent unlinking materials that might be used elsewhere
|
||||
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
|
||||
for material_slot in obj_copy.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
if material in replacements:
|
||||
material_replacement = replacements[material]
|
||||
|
||||
# no need to copy non-ifc materials as unlinking won't do anything to them
|
||||
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
|
||||
replacements[material] = material
|
||||
continue
|
||||
|
||||
else:
|
||||
material_replacement = material.copy()
|
||||
replacements[material] = material_replacement
|
||||
|
||||
material_slot.material = material_replacement
|
||||
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
obj = obj_copy
|
||||
|
||||
@@ -24,21 +24,15 @@ classes = (
|
||||
operator.ActivateIfcClassFilter,
|
||||
operator.AddFilter,
|
||||
operator.AddFilterGroup,
|
||||
operator.AddToIfcGroup,
|
||||
operator.ColourByProperty,
|
||||
operator.EditFilterQuery,
|
||||
operator.FilterModelElements,
|
||||
operator.IfcSelector,
|
||||
operator.LoadColourscheme,
|
||||
operator.LoadQuery,
|
||||
operator.LoadSearch,
|
||||
operator.OpenQueryLibrary,
|
||||
operator.RemoveFilter,
|
||||
operator.RemoveFilterGroup,
|
||||
operator.ResetObjectColours,
|
||||
operator.SaveColourscheme,
|
||||
operator.SaveSearch,
|
||||
operator.SaveSelectorQuery,
|
||||
operator.Search,
|
||||
operator.SelectByProperty,
|
||||
operator.SelectFilterElements,
|
||||
@@ -51,11 +45,6 @@ classes = (
|
||||
prop.BIMFilterClasses,
|
||||
prop.BIMFilterBuildingStoreys,
|
||||
prop.BIMSearchProperties,
|
||||
prop.SearchCollection,
|
||||
prop.SearchQueryFilter,
|
||||
prop.SearchQuery,
|
||||
prop.SearchQueryGroup,
|
||||
prop.IfcSelectorProperties,
|
||||
ui.BIM_PT_search,
|
||||
ui.BIM_PT_filter,
|
||||
ui.BIM_PT_colour_by_property,
|
||||
@@ -63,15 +52,12 @@ classes = (
|
||||
ui.BIM_UL_colourscheme,
|
||||
ui.BIM_UL_ifc_class_filter,
|
||||
ui.BIM_UL_ifc_building_storey_filter,
|
||||
ui.BIM_PT_IFCSelector,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
|
||||
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMSearchProperties
|
||||
del bpy.types.Scene.IfcSelectorProperties
|
||||
|
||||
@@ -271,25 +271,29 @@ class ColourByProperty(Operator):
|
||||
colourscheme = {}
|
||||
|
||||
if len(props.colourscheme):
|
||||
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
|
||||
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
value = str(ifcopenshell.util.selector.get_element_value(element, query))
|
||||
if value not in colourscheme:
|
||||
colourscheme[value] = next(colours)[0:3]
|
||||
obj.color = (*colourscheme[value], 1)
|
||||
if value in colourscheme:
|
||||
colourscheme[value]["total"] += 1
|
||||
else:
|
||||
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
|
||||
obj.color = (*colourscheme[value]["colour"], 1)
|
||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
||||
if areas:
|
||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
||||
|
||||
props.colourscheme.clear()
|
||||
for value, colour in colourscheme.items():
|
||||
for value in sorted(colourscheme.keys()):
|
||||
data = colourscheme[value]
|
||||
new = props.colourscheme.add()
|
||||
new.name = str(value)
|
||||
new.colour = colour[0:3]
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"][0:3]
|
||||
return {"FINISHED"}
|
||||
|
||||
def store_state(self, context):
|
||||
@@ -351,16 +355,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
|
||||
query = props.colourscheme_query
|
||||
|
||||
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
|
||||
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
|
||||
if group:
|
||||
group = group[0]
|
||||
description = json.loads(group.Description)
|
||||
description["colourscheme"] = coulour_scheme
|
||||
description["colourscheme"] = colourscheme
|
||||
description["colourscheme_query"] = query
|
||||
group.Description = json.dumps(description)
|
||||
else:
|
||||
description = json.dumps(
|
||||
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
|
||||
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
|
||||
)
|
||||
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
|
||||
|
||||
@@ -380,10 +384,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
|
||||
description = json.loads(group.Description)
|
||||
props.colourscheme_query = description.get("colourscheme_query")
|
||||
props.colourscheme.clear()
|
||||
for name, colour in description.get("colourscheme", {}).items():
|
||||
for name, data in description.get("colourscheme", {}).items():
|
||||
new = props.colourscheme.add()
|
||||
new.name = name
|
||||
new.colour = colour
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"]
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMSearchProperties
|
||||
@@ -623,223 +628,6 @@ class ShowAllElements(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FilterModelElements(Operator):
|
||||
"""Filter model elements based on selection"""
|
||||
|
||||
bl_idname = "bim.filter_model_elements"
|
||||
bl_label = "Filter Model Elements"
|
||||
option: StringProperty("select|isolate|hide")
|
||||
|
||||
def execute(self, context):
|
||||
selection_props = context.scene.IfcSelectorProperties
|
||||
selection = (
|
||||
selection_props.selector_query_syntax
|
||||
if selection_props.manual_override
|
||||
else self.add_groups(selection_props)
|
||||
)
|
||||
selection_props.selector_query_syntax = selection
|
||||
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
def add_groups(self, selector: str) -> str:
|
||||
selection = ""
|
||||
for group_index, group in enumerate(selector.groups):
|
||||
if group_index != 0:
|
||||
selection += " | "
|
||||
selection += "(" if len(selector.groups) > 1 else ""
|
||||
selection = self.add_queries(selection, group)
|
||||
|
||||
selection += ")" if len(selector.groups) > 1 else ""
|
||||
return selection
|
||||
|
||||
def add_queries(self, selection: str, group: str) -> str:
|
||||
for query_index, query in enumerate(group.queries):
|
||||
if query_index != 0:
|
||||
selection += " & " if query.and_or == "and" else " | "
|
||||
|
||||
if query.selector == "IFC Class":
|
||||
active_option = query.active_option.split(": ")[1]
|
||||
selection += f".{active_option}"
|
||||
selection = self.add_filters(selection, query)
|
||||
|
||||
elif query.selector == "GlobalId":
|
||||
selection += f"#{query.value}"
|
||||
|
||||
elif query.selector == "IfcElementType":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"* #{query.sub_options[index].global_id}"
|
||||
|
||||
elif query.selector == "IfcSpatialElement":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"@ #{query.sub_options[index].global_id}"
|
||||
return selection
|
||||
|
||||
def add_filters(self, selection: str, query: str) -> str:
|
||||
for f_index, f in enumerate(query.filters):
|
||||
if f_index != 0:
|
||||
selection += " & " if f.and_or == "and" else " | "
|
||||
selection += f".{query.active_option}"
|
||||
|
||||
if f.value in ("True", "False"):
|
||||
value = f.value
|
||||
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
|
||||
value = f.value
|
||||
else:
|
||||
value = f'"{f.value}"'
|
||||
|
||||
selection += "["
|
||||
|
||||
if f.selector == "IfcPropertySet":
|
||||
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
elif f.selector == "Attribute":
|
||||
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
|
||||
pattern = re.compile(r"^[0-9]+:")
|
||||
match = pattern.search(f.active_option)
|
||||
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
selection += "]"
|
||||
return selection
|
||||
|
||||
|
||||
# This needs to be moved into ui code, I know ;) - vulevukusej
|
||||
class IfcSelector(Operator):
|
||||
"""Select elements in model with IFC Selector"""
|
||||
|
||||
bl_idname = "bim.ifc_selector"
|
||||
bl_label = "Select elements with IFC Selector"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
from . import ui
|
||||
|
||||
ui.IfcSelectorUI.draw(context, self.layout)
|
||||
|
||||
|
||||
class SaveSelectorQuery(Operator):
|
||||
bl_idname = "bim.save_selector_query"
|
||||
bl_label = "Save Selector Query"
|
||||
save_name: StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "save_name")
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
new = ifc_selector.query_library.add()
|
||||
new.name = self.save_name
|
||||
new.query = ifc_selector.selector_query_syntax
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenQueryLibrary(Operator):
|
||||
"""Open Query Library"""
|
||||
|
||||
bl_idname = "bim.open_query_library"
|
||||
bl_label = "Open Query Library"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
|
||||
for index, query in enumerate(ifc_selector.query_library):
|
||||
row = layout.row(align=True)
|
||||
row.prop(query, "query", text=query.name)
|
||||
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "query_library"
|
||||
op.index = index
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadQuery(Operator):
|
||||
bl_idname = "bim.load_query"
|
||||
bl_label = "Load Query"
|
||||
index: IntProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
close_operator_panel(event)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddToIfcGroup(Operator):
|
||||
bl_idname = "bim.add_to_ifc_group"
|
||||
bl_label = "Add to IFC Group"
|
||||
group_name: StringProperty(name="Group Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
bpy.ops.bim.load_groups()
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_group")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_groups",
|
||||
"",
|
||||
self.props,
|
||||
"groups",
|
||||
self.props,
|
||||
"active_group_index",
|
||||
)
|
||||
|
||||
if self.props.active_group_id:
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name in ["Name", "Description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
|
||||
def execute(self, context):
|
||||
active_group_index = self.props.active_group_index
|
||||
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
|
||||
|
||||
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
|
||||
|
||||
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
|
||||
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name == "Description":
|
||||
if "*selector*" not in attribute.string_value:
|
||||
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
|
||||
else:
|
||||
new_description = attribute.string_value.split("*selector*")
|
||||
new_description[1] = selector_query_syntax
|
||||
attribute.string_value = "*selector*".join(new_description)
|
||||
|
||||
bpy.ops.bim.edit_group()
|
||||
bpy.ops.bim.disable_group_editing_ui()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectSimilar(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.select_similar"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
@@ -78,7 +78,7 @@ def update_is_container_selected(self, context):
|
||||
else:
|
||||
for obj in context.selected_objects:
|
||||
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
|
||||
if (container and container.Name == self.name) or (not container and self.name== "None"):
|
||||
if (container and container.Name == self.name) or (not container and self.name == "None"):
|
||||
obj.select_set(False)
|
||||
new = self.unselected_objects.add()
|
||||
new.obj = obj
|
||||
@@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup):
|
||||
|
||||
class BIMColour(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
total: IntProperty(name="Total")
|
||||
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
|
||||
|
||||
|
||||
@@ -138,139 +139,3 @@ def get_classes(self, ifc_product):
|
||||
declarations = util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
return [(c, c, "") for c in sorted(names)]
|
||||
|
||||
|
||||
def load_sub_options(self, context):
|
||||
if self.selector not in ["IfcClass"]:
|
||||
self.load_option = "sub_options"
|
||||
load_selection_options(self, context)
|
||||
|
||||
|
||||
def load_selection_options(self, context):
|
||||
ifc = IfcStore.file
|
||||
load_option = self.load_option
|
||||
op = getattr(self, load_option)
|
||||
op.clear()
|
||||
options = []
|
||||
|
||||
if load_option == "options":
|
||||
self.sub_options.clear()
|
||||
if self.selector == "IFC Class":
|
||||
options = get_classes(self, "IfcElement")
|
||||
options.append(("IfcSpace", "IfcSpace", ""))
|
||||
elif self.selector == "IfcSpatialElement":
|
||||
options = get_classes(self, "IfcSpatialElement")
|
||||
elif self.selector == "IfcElementType":
|
||||
options = get_classes(self, "IfcElementType")
|
||||
elif self.selector == "GlobalId":
|
||||
return
|
||||
elif self.selector == "IfcPropertySet":
|
||||
psets = Selector.parse(ifc, ".IfcPropertySet")
|
||||
options = set([o.Name for o in psets])
|
||||
elif self.selector == "Attribute":
|
||||
attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]
|
||||
for a in attributes:
|
||||
options.append((a, a, ""))
|
||||
|
||||
elif load_option == "sub_options":
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
active_option = self.active_option.split(": ")[1]
|
||||
options = Selector.parse(ifc, f".{active_option}")
|
||||
|
||||
elif self.selector == "IfcPropertySet":
|
||||
options = set()
|
||||
active_pset = self.active_option.split(": ")[1]
|
||||
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
|
||||
for pset in psets_in_file:
|
||||
for prop in pset.HasProperties:
|
||||
options.add(prop.Name)
|
||||
|
||||
for index, option in enumerate(options):
|
||||
new = op.add()
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
if self.load_option == "sub_options":
|
||||
new.name = f"{index}: {option.Name}"
|
||||
new.global_id = option.GlobalId
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
elif self.selector == "IfcPropertySet":
|
||||
new.name = f"{index}: {option}"
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
self.load_option = "options"
|
||||
|
||||
|
||||
def load_spatial_elements(self, context):
|
||||
ifc = IfcStore.file
|
||||
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
|
||||
collection = getattr(self, "sub_spatial_elements", None)
|
||||
collection.clear()
|
||||
for index, c in enumerate(col_items):
|
||||
new = collection.add()
|
||||
new.name = f"{index}-{c.Name}"
|
||||
|
||||
|
||||
class SearchCollection(PropertyGroup):
|
||||
name: StringProperty()
|
||||
long_name: StringProperty()
|
||||
global_id: StringProperty()
|
||||
query: StringProperty()
|
||||
|
||||
|
||||
class IfcSelector:
|
||||
and_or: EnumProperty(
|
||||
items=[(i, i, i) for i in ["and", "or"]],
|
||||
)
|
||||
negation: BoolProperty(name="not")
|
||||
comparison: EnumProperty(
|
||||
items=[
|
||||
("=", "equal to", ""),
|
||||
("*=", "contains", ""),
|
||||
(">=", "greater than or equal to", ""),
|
||||
("<=", "lesser than or equal to", ""),
|
||||
(">", "greater than", ""),
|
||||
("<", "less than", ""),
|
||||
],
|
||||
)
|
||||
load_option: StringProperty(
|
||||
default="options", description="controls whether or not options or sub_options are loaded"
|
||||
)
|
||||
options: CollectionProperty(type=SearchCollection)
|
||||
active_option: StringProperty(update=load_sub_options)
|
||||
sub_options: CollectionProperty(type=SearchCollection)
|
||||
active_sub_option: StringProperty()
|
||||
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
|
||||
|
||||
|
||||
class SearchQueryFilter(PropertyGroup, IfcSelector):
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
|
||||
name="Filter selection by",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQuery(PropertyGroup, IfcSelector):
|
||||
filters: CollectionProperty(type=SearchQueryFilter)
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
|
||||
name="Selector type",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQueryGroup(PropertyGroup, IfcSelector):
|
||||
queries: CollectionProperty(type=SearchQuery)
|
||||
|
||||
|
||||
class IfcSelectorProperties(PropertyGroup, IfcSelector):
|
||||
groups: CollectionProperty(type=SearchQueryGroup)
|
||||
selector_query_syntax: StringProperty()
|
||||
|
||||
query_library: CollectionProperty(type=SearchCollection)
|
||||
active_query: StringProperty()
|
||||
|
||||
active_query: StringProperty()
|
||||
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
|
||||
|
||||
@@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList):
|
||||
if not item:
|
||||
return
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
row.label(text=f"{item.name} ({item.total})")
|
||||
row.prop(item, "colour", text="")
|
||||
|
||||
|
||||
@@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
|
||||
split = split.column()
|
||||
split.scale_x = 0.5
|
||||
split.label(text=str(item.total))
|
||||
|
||||
|
||||
class BIM_PT_IFCSelector(Panel):
|
||||
bl_label = "Selector"
|
||||
bl_idname = "BIM_PT_ifc_selector"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("bim.ifc_selector")
|
||||
|
||||
|
||||
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
|
||||
class IfcSelectorUI:
|
||||
@classmethod
|
||||
def draw(self, context, layout):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
row = layout.row()
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add selection group")
|
||||
op.option = "add"
|
||||
op.collection = "groups"
|
||||
layout.separator()
|
||||
|
||||
self.draw_query_group_ui(self, ifc_selector, layout)
|
||||
|
||||
if len(ifc_selector.groups) != 0:
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
|
||||
row.prop(ifc_selector, "manual_override", text="")
|
||||
|
||||
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
|
||||
select.option = "select"
|
||||
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
|
||||
isolate.option = "isolate"
|
||||
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
|
||||
hide.option = "hide"
|
||||
unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF")
|
||||
unhide.option = "unhide"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator("bim.save_selector_query", text="Save Query")
|
||||
op = row.operator("bim.open_query_library", text="Load Query")
|
||||
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF")
|
||||
|
||||
def draw_query_group_ui(self, ifc_selector, layout):
|
||||
for index, group in enumerate(ifc_selector.groups):
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text="or") if index != 0 else None
|
||||
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.label(text=f"Group #{str(index+1)}")
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
|
||||
op.option = "add"
|
||||
op.collection = "queries"
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
|
||||
op.option = "remove"
|
||||
op.collection = "groups"
|
||||
op.index = index
|
||||
|
||||
self.draw_query_ui(self, group, box)
|
||||
|
||||
def draw_query_ui(self, group, box):
|
||||
for index, query in enumerate(group.queries):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
|
||||
row.prop(query, "and_or", text="") if index != 0 else None
|
||||
if query.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.prop(query, "selector", text="")
|
||||
|
||||
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
|
||||
row.label(text="Equals")
|
||||
row.prop_search(query, "active_option", query, "options", text="")
|
||||
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
|
||||
query.sub_options
|
||||
) != 0 else None
|
||||
self.draw_filter_ui(self, box, query)
|
||||
|
||||
elif query.selector in ["GlobalId", "Attribute"]:
|
||||
row.prop(query, "negation", text="")
|
||||
row.prop(query, "comparison", text="")
|
||||
row.prop(query, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "queries"
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = (
|
||||
row.operator("bim.edit_blender_collection", text="Add filter")
|
||||
if query.selector == "IFC Class"
|
||||
else None
|
||||
)
|
||||
if op:
|
||||
op.option = "add"
|
||||
op.collection = "filters"
|
||||
|
||||
def draw_filter_ui(self, box, query):
|
||||
for filter_index, f in enumerate(query.filters):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "and_or", text="") if filter_index != 0 else None
|
||||
if f.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "selector", text="")
|
||||
|
||||
if f.selector == "Attribute":
|
||||
row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True)
|
||||
row.prop(
|
||||
f,
|
||||
"negation",
|
||||
)
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
elif f.selector == "IfcPropertySet":
|
||||
row.prop_search(f, "active_option", f, "options", text="")
|
||||
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
|
||||
row.prop(f, "negation")
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "filters"
|
||||
op.index = filter_index
|
||||
|
||||
@@ -388,13 +388,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
|
||||
should_delete_existing_annotation = False
|
||||
should_create_annotation = False
|
||||
|
||||
# remove annotation only if the reference object was changed
|
||||
# otherwise we rely on the existing annotation
|
||||
if annotation:
|
||||
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
|
||||
should_delete_existing_annotation = True
|
||||
elif not reference_obj:
|
||||
should_delete_existing_annotation = True
|
||||
|
||||
if reference_obj and (should_delete_existing_annotation or not annotation):
|
||||
if (should_delete_existing_annotation or not annotation):
|
||||
should_create_annotation = True
|
||||
|
||||
if should_delete_existing_annotation:
|
||||
|
||||
@@ -153,6 +153,17 @@ class Bsdd:
|
||||
def should_load_preview_domains(cls): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Clash:
|
||||
def export_clash_sets(cls): pass
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id): pass
|
||||
def get_clash_set(cls, name): pass
|
||||
def get_clash_sets(cls): pass
|
||||
def import_active_clashes(cls): pass
|
||||
def load_clash_sets(cls, fn): pass
|
||||
def look_at(cls, target, location): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Collector:
|
||||
def assign(cls, obj): pass
|
||||
@@ -508,7 +519,7 @@ class Misc:
|
||||
class Model:
|
||||
def convert_si_to_unit(cls, value): pass
|
||||
def convert_unit_to_si(cls, value): pass
|
||||
def export_curve(cls, position, edge_indices, points=None): pass
|
||||
def export_curve(cls, position, edge_indices): pass
|
||||
def export_points(cls, position, indices): pass
|
||||
def export_profile(cls, obj, position=None): pass
|
||||
def generate_occurrence_name(cls, element_type, ifc_class): pass
|
||||
|
||||
@@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary
|
||||
from blenderbim.tool.brick import Brick
|
||||
from blenderbim.tool.bsdd import Bsdd
|
||||
from blenderbim.tool.cad import Cad
|
||||
from blenderbim.tool.clash import Clash
|
||||
from blenderbim.tool.collector import Collector
|
||||
from blenderbim.tool.context import Context
|
||||
from blenderbim.tool.debug import Debug
|
||||
|
||||
@@ -569,6 +569,18 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
|
||||
return bm_a
|
||||
|
||||
@classmethod
|
||||
def bmesh_check_vertex_in_groups(
|
||||
cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int]
|
||||
) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]:
|
||||
"""returns tuple boolean (whether vertex is in any of the groups) and related group index"""
|
||||
for group_index in vertex[deform_layer].keys():
|
||||
# ignore vertex groups assignments produced by edge subdivision near arcs
|
||||
# they usually have weight = 0.5
|
||||
if group_index in groups and vertex[deform_layer][group_index] == 1.0:
|
||||
return True, group_index
|
||||
return False, None
|
||||
|
||||
@classmethod
|
||||
def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
|
||||
ao = context.active_object
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# 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 os
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
from contextlib import contextmanager
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class Clash(blenderbim.core.tool.Clash):
|
||||
|
||||
@classmethod
|
||||
def export_clash_sets(cls):
|
||||
clash_sets = []
|
||||
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
|
||||
a = []
|
||||
b = []
|
||||
for ab in ["a", "b"]:
|
||||
for data in getattr(clash_set, ab):
|
||||
clash_source = {"file": data.name}
|
||||
query = tool.Search.export_filter_query(data.filter_groups)
|
||||
if query and data.mode != "a":
|
||||
clash_source["selector"] = query
|
||||
clash_source["mode"] = data.mode
|
||||
if ab == "a":
|
||||
a.append(clash_source)
|
||||
elif ab == "b":
|
||||
b.append(clash_source)
|
||||
clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b}
|
||||
if clash_set.mode == "intersection":
|
||||
clash_set_data["tolerance"] = clash_set.tolerance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
elif clash_set.mode == "collision":
|
||||
clash_set_data["allow_touching"] = clash_set.allow_touching
|
||||
elif clash_set.mode == "clearance":
|
||||
clash_set_data["clearance"] = clash_set.clearance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
clash_sets.append(clash_set_data)
|
||||
return clash_sets
|
||||
|
||||
@classmethod
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id):
|
||||
clashes = clash_set.get("clashes", None)
|
||||
if not clashes:
|
||||
return
|
||||
return clashes.get(f"{a_global_id}-{b_global_id}", None)
|
||||
|
||||
@classmethod
|
||||
def get_clash_set(cls, name):
|
||||
for clash_set in ClashStore.clash_sets:
|
||||
if clash_set["name"] == name:
|
||||
return clash_set
|
||||
|
||||
@classmethod
|
||||
def get_clash_sets(cls):
|
||||
return ClashStore.clash_sets
|
||||
|
||||
@classmethod
|
||||
def import_active_clashes(cls):
|
||||
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set
|
||||
if not clash_set:
|
||||
return
|
||||
clash_set.clashes.clear()
|
||||
result = tool.Clash.get_clash_set(clash_set.name)
|
||||
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
|
||||
blender_clash = clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
|
||||
@classmethod
|
||||
def load_clash_sets(cls, fn):
|
||||
with open(fn) as f:
|
||||
ClashStore.clash_sets = json.load(f)
|
||||
|
||||
@classmethod
|
||||
def look_at(cls, target, location):
|
||||
camera_location = location
|
||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
||||
region = next(region for region in area.regions if region.type == "WINDOW")
|
||||
space = next(space for space in area.spaces if space.type == "VIEW_3D")
|
||||
override = {"area": area, "region": region, "space_data": space}
|
||||
space.region_3d.view_location = target
|
||||
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
|
||||
space.region_3d.view_distance = (camera_location - target).length
|
||||
space.shading.show_xray = True
|
||||
|
||||
|
||||
class ClashStore:
|
||||
clash_sets = None
|
||||
path = None
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
ClashStore.clash_sets = None
|
||||
ClashStore.path = None
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import collections
|
||||
import bpy
|
||||
import math
|
||||
import json
|
||||
@@ -27,27 +28,31 @@ import shutil
|
||||
import logging
|
||||
import shapely
|
||||
import platform
|
||||
from shapely.ops import unary_union
|
||||
import mathutils
|
||||
import subprocess
|
||||
import numpy as np
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import blenderbim.bim.import_ifc
|
||||
import blenderbim.bim.module.drawing.sheeter as sheeter
|
||||
import blenderbim.bim.module.drawing.scheduler as scheduler
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.core.root
|
||||
from shapely.ops import unary_union
|
||||
from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS, ANNOTATION_TYPES_DATA
|
||||
from lxml import etree
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
from fractions import Fraction
|
||||
import collections
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Union, Iterable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Drawing(blenderbim.core.tool.Drawing):
|
||||
@@ -247,7 +252,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
bpy.data.collections.remove(collection, do_unlink=True)
|
||||
|
||||
@classmethod
|
||||
def delete_drawing_elements(cls, elements):
|
||||
def delete_drawing_elements(cls, elements: Iterable[ifcopenshell.entity_instance]) -> None:
|
||||
for element in elements:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
@@ -258,7 +263,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
tool.Blender.remove_data_block(obj_data)
|
||||
|
||||
@classmethod
|
||||
def delete_object(cls, obj):
|
||||
def delete_object(cls, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
@classmethod
|
||||
@@ -366,7 +371,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_annotation_context(cls, target_view, object_type=None):
|
||||
def get_annotation_context(
|
||||
cls, target_view: str, object_type: Optional[str] = None
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
# checking PLAN target view and annotation type that doesn't require 3d
|
||||
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in (
|
||||
"FALL",
|
||||
@@ -381,7 +388,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def get_document_uri(cls, document, description=None):
|
||||
def get_document_uri(
|
||||
cls, document: ifcopenshell.entity_instance, description: Optional[str] = None
|
||||
) -> Union[str, None]:
|
||||
if getattr(document, "Location", None):
|
||||
if os.path.isabs(document.Location):
|
||||
return document.Location
|
||||
@@ -426,7 +435,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return rel.RelatingGroup
|
||||
|
||||
@classmethod
|
||||
def get_drawing_document(cls, drawing):
|
||||
def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
for rel in drawing.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
return rel.RelatingDocument
|
||||
@@ -593,7 +602,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return rel.RelatingProduct
|
||||
|
||||
@classmethod
|
||||
def import_annotations_in_group(cls, group):
|
||||
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
|
||||
elements = set(
|
||||
[e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"]
|
||||
)
|
||||
@@ -603,6 +612,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
ifc_importer.file = tool.Ifc.get()
|
||||
ifc_importer.calculate_unit_scale()
|
||||
ifc_importer.process_context_filter()
|
||||
ifc_importer.load_existing_meshes()
|
||||
ifc_importer.material_creator.load_existing_materials()
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
for obj in ifc_importer.added_data.values():
|
||||
@@ -612,7 +622,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
# but BIMCameraProperties are only synced with EPsetDrawing at drawing import
|
||||
# therefore camera props can differ from pset if the user changed them from pset.
|
||||
@classmethod
|
||||
def import_drawing(cls, drawing):
|
||||
def import_drawing(cls, drawing: ifcopenshell.entity_instance) -> bpy.types.Object:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.STRICT_TOLERANCE, True)
|
||||
|
||||
@@ -865,7 +875,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
should_add_representation=True,
|
||||
context=None,
|
||||
ifc_representation_class=None,
|
||||
):
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
@@ -1051,7 +1061,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
shutil.copy(ootb_resource, resource_path)
|
||||
|
||||
@classmethod
|
||||
def get_potential_reference_elements(cls, drawing):
|
||||
def get_potential_reference_elements(
|
||||
cls, drawing: ifcopenshell.entity_instance
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
elements = []
|
||||
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
|
||||
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||
@@ -1072,7 +1084,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def get_drawing_reference_annotation(cls, drawing, reference_element):
|
||||
def get_drawing_reference_annotation(
|
||||
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, bool, None]:
|
||||
if drawing == reference_element:
|
||||
return True
|
||||
for element in cls.get_group_elements(cls.get_drawing_group(drawing)):
|
||||
@@ -1088,26 +1102,50 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if reference_element.is_a("IfcGridAxis"):
|
||||
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||
|
||||
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
|
||||
obj = tool.Ifc.get_object(drawing)
|
||||
if obj is None:
|
||||
obj = cls.import_drawing(drawing)
|
||||
# there is no need for other drawing annotations in that case
|
||||
# but we import them anyway so we won't break that drawing activation
|
||||
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
|
||||
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(reference_element)
|
||||
target_view = psets.get("EPset_Drawing", {}).get("TargetView", None)
|
||||
if target_view == "ELEVATION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
||||
elif target_view == "SECTION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcBuildingStorey"):
|
||||
return cls.generate_storey_annotation(drawing, reference_element, context)
|
||||
|
||||
@classmethod
|
||||
def generate_storey_annotation(cls, drawing, reference_element, context):
|
||||
def generate_storey_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1162,13 +1200,18 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_section_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_section_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1179,7 +1222,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
mat.translation += annotation_offset
|
||||
return mat
|
||||
|
||||
def clip_to_camera_boundary(mesh, bounds):
|
||||
def clip_to_camera_boundary(mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]) -> Union[bpy.types.Mesh, None]:
|
||||
mesh.verts.ensure_lookup_table()
|
||||
points = [v.co for v in mesh.verts[0:2]]
|
||||
points = helper.clip_segment(bounds, points)
|
||||
@@ -1240,12 +1283,17 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_elevation_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_elevation_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1272,7 +1320,12 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_grid_axis_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
@@ -1300,7 +1353,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
mesh.to_mesh(obj.data)
|
||||
return obj
|
||||
|
||||
def to_camera_coords(obj, mesh):
|
||||
def to_camera_coords(obj: bpy.types.Object, mesh: bpy.types.Mesh) -> tuple[bpy.types.Object, bpy.types.Mesh]:
|
||||
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
|
||||
obj.matrix_world = camera.matrix_world
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
@@ -1357,7 +1410,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def is_perpendicular(cls, a, b):
|
||||
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||
a_quaternion = a.matrix_world.to_quaternion()
|
||||
b_quaternion = b.matrix_world.to_quaternion()
|
||||
@@ -1367,7 +1420,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_camera_block(cls, obj):
|
||||
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
|
||||
raster_x = obj.data.BIMCameraProperties.raster_x
|
||||
raster_y = obj.data.BIMCameraProperties.raster_y
|
||||
|
||||
@@ -1400,7 +1453,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return {"verts": verts, "faces": faces}
|
||||
|
||||
@classmethod
|
||||
def is_intersecting(cls, a, b):
|
||||
def is_intersecting(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||
a_block = cls.get_camera_block(a)
|
||||
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
|
||||
b_block = cls.get_camera_block(b)
|
||||
@@ -1622,7 +1675,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
bpy.ops.bim.activate_model()
|
||||
|
||||
@classmethod
|
||||
def activate_drawing(cls, camera):
|
||||
def activate_drawing(cls, camera: bpy.types.Object) -> None:
|
||||
area = tool.Blender.get_view3d_area()
|
||||
is_local_view = area.spaces[0].local_view is not None
|
||||
if is_local_view:
|
||||
@@ -1704,7 +1757,6 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if subcontext:
|
||||
subcontexts.append(context_filter)
|
||||
|
||||
|
||||
# switch representations and hide elements without representations
|
||||
for element in filtered_elements:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
@@ -1943,3 +1995,60 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
rotate180z = mathutils.Matrix.Rotation(math.radians(180.0), 4, "Z")
|
||||
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() @ rotate180z
|
||||
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
|
||||
|
||||
@classmethod
|
||||
def convert_svg_to_dxf(cls, svg_filepath: Path, dxf_filepath: Path) -> None:
|
||||
import ezdxf
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
IFC = "{http://www.ifcopenshell.org/ns}"
|
||||
|
||||
doc = ezdxf.new("R2010")
|
||||
msp = doc.modelspace()
|
||||
svg = ET.parse(svg_filepath).getroot()
|
||||
|
||||
def finalize_dxf():
|
||||
doc.saveas(dxf_filepath)
|
||||
|
||||
drawing = svg.findall(f"{SVG}g[@{IFC}name]")
|
||||
if not drawing:
|
||||
finalize_dxf()
|
||||
return
|
||||
drawing = drawing[0]
|
||||
|
||||
NUMBER = r"-?\d+\.?\d+"
|
||||
COORD = rf"{NUMBER},{NUMBER}"
|
||||
POLYLINE_PATTERN = rf"M{COORD} (?:L{COORD} ?)+Z? ?"
|
||||
MULTI_POLYLINE_PATTERN = rf"^({POLYLINE_PATTERN})+$"
|
||||
|
||||
for element_g in drawing.findall(f"{SVG}g"):
|
||||
paths = element_g.findall(f"{SVG}path")
|
||||
|
||||
for path in paths:
|
||||
path = path.attrib["d"]
|
||||
|
||||
if not re.match(MULTI_POLYLINE_PATTERN, path):
|
||||
# print(f'Path "{path}" doesn\'t match expected pattern {MULTI_POLYLINE_PATTERN}')
|
||||
continue
|
||||
|
||||
for polyline_path in re.findall(POLYLINE_PATTERN, path):
|
||||
points = re.findall(rf"{NUMBER}", polyline_path)
|
||||
points = [float(p) for p in points]
|
||||
POINT_SIZE = 2
|
||||
|
||||
grouped_points = []
|
||||
for i in range(0, len(points), POINT_SIZE):
|
||||
point = points[i : i + POINT_SIZE]
|
||||
point[1] *= -1
|
||||
grouped_points.append(point)
|
||||
points = grouped_points
|
||||
|
||||
# Z marks closed polylines
|
||||
is_closed_polyline = polyline_path.rstrip().endswith("Z")
|
||||
if is_closed_polyline or len(points) > 2:
|
||||
msp.add_lwpolyline(points, close=is_closed_polyline)
|
||||
else: # LINE
|
||||
msp.add_line(*points)
|
||||
|
||||
finalize_dxf()
|
||||
|
||||
@@ -79,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc):
|
||||
return IfcStore.get_schema()
|
||||
|
||||
@classmethod
|
||||
def get_entity(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
|
||||
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance:
|
||||
ifc = IfcStore.get_file()
|
||||
props = getattr(obj, "BIMObjectProperties", None)
|
||||
if ifc and props and props.ifc_definition_id:
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import collections
|
||||
import collections.abc
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -36,23 +37,26 @@ from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
|
||||
from ifcopenshell.util.shape_builder import V, ShapeBuilder
|
||||
from typing import Optional, Union, TypeVar, Any
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Model(blenderbim.core.tool.Model):
|
||||
@classmethod
|
||||
def convert_si_to_unit(cls, value):
|
||||
def convert_si_to_unit(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v / cls.unit_scale for v in value]
|
||||
return value / cls.unit_scale
|
||||
|
||||
@classmethod
|
||||
def convert_unit_to_si(cls, value):
|
||||
def convert_unit_to_si(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v * cls.unit_scale for v in value]
|
||||
return value * cls.unit_scale
|
||||
|
||||
@classmethod
|
||||
def convert_data_to_project_units(cls, data, non_si_props=[]):
|
||||
def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for prop_name in data:
|
||||
if prop_name in non_si_props:
|
||||
@@ -65,7 +69,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def convert_data_to_si_units(cls, data, non_si_props=[]):
|
||||
def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for prop_name in data:
|
||||
if prop_name in non_si_props:
|
||||
@@ -78,34 +82,35 @@ class Model(blenderbim.core.tool.Model):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def export_curve(cls, position, edge_indices, points=None):
|
||||
def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance:
|
||||
position_i = position.inverted()
|
||||
ifc_file = tool.Ifc.get()
|
||||
if len(edge_indices) == 2:
|
||||
diameter = edge_indices[0]
|
||||
p1 = cls.bm.verts[diameter[0]].co
|
||||
p2 = cls.bm.verts[diameter[1]].co
|
||||
center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
|
||||
radius = cls.convert_si_to_unit((p1 - p2).length / 2)
|
||||
return tool.Ifc.get().createIfcCircle(
|
||||
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
|
||||
return ifc_file.createIfcCircle(
|
||||
ifc_file.createIfcAxis2Placement2D(ifc_file.createIfcCartesianPoint(center[0:2])), radius
|
||||
)
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
points = []
|
||||
for edge in edge_indices:
|
||||
local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d()
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
|
||||
points.append(ifc_file.createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
|
||||
points.append(points[0])
|
||||
return tool.Ifc.get().createIfcPolyline(points)
|
||||
return ifc_file.createIfcPolyline(points)
|
||||
segments = []
|
||||
for segment in edge_indices:
|
||||
if len(segment) == 2:
|
||||
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
||||
segments.append(ifc_file.createIfcLineIndex([i + 1 for i in segment]))
|
||||
elif len(segment) == 3:
|
||||
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
|
||||
return tool.Ifc.get().createIfcIndexedPolyCurve(cls.points, segments, False)
|
||||
segments.append(ifc_file.createIfcArcIndex([i + 1 for i in segment]))
|
||||
return ifc_file.createIfcIndexedPolyCurve(cls.points, segments, False)
|
||||
|
||||
@classmethod
|
||||
def export_points(cls, position, indices):
|
||||
def export_points(cls, position: Matrix, indices: list[Vector]) -> ifcopenshell.entity_instance:
|
||||
position_i = position.inverted()
|
||||
points = []
|
||||
for point in indices:
|
||||
@@ -114,7 +119,10 @@ class Model(blenderbim.core.tool.Model):
|
||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
|
||||
@classmethod
|
||||
def export_profile(cls, obj, position=None):
|
||||
def export_profile(
|
||||
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Returns `None` in case if profile was invalid."""
|
||||
if position is None:
|
||||
position = Matrix()
|
||||
|
||||
@@ -150,7 +158,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return profile
|
||||
|
||||
@classmethod
|
||||
def export_surface(cls, obj):
|
||||
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
||||
|
||||
edge1 = p2 - p1
|
||||
@@ -202,7 +210,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return surface
|
||||
|
||||
@classmethod
|
||||
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str):
|
||||
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
if props.occurrence_name_style == "CLASS":
|
||||
return ifc_class[3:]
|
||||
@@ -216,7 +224,8 @@ class Model(blenderbim.core.tool.Model):
|
||||
return "Instance"
|
||||
|
||||
@classmethod
|
||||
def get_extrusion(cls, representation):
|
||||
def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""return first found IfcExtrudedAreaSolid"""
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
|
||||
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from mathutils import Vector
|
||||
from ifcopenshell import util
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim.schema
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
|
||||
class Qto(blenderbim.core.tool.Qto):
|
||||
@classmethod
|
||||
def get_radius_of_selected_vertices(cls, obj):
|
||||
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
|
||||
selected_verts = [v.co for v in obj.data.vertices if v.select]
|
||||
total = Vector()
|
||||
for v in selected_verts:
|
||||
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return max([(v - circle_center).length for v in selected_verts])
|
||||
|
||||
@classmethod
|
||||
def set_qto_result(cls, result):
|
||||
def set_qto_result(cls, result: float) -> None:
|
||||
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
|
||||
|
||||
@classmethod
|
||||
def add_object_base_qto(cls, obj):
|
||||
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
return cls.add_product_base_qto(product)
|
||||
|
||||
@classmethod
|
||||
def add_product_base_qto(cls, product):
|
||||
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
base_quantity_name = cls.get_applicable_base_quantity_name(product)
|
||||
if base_quantity_name:
|
||||
return tool.Ifc.run(
|
||||
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_quantity_names(cls, qto_name):
|
||||
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
|
||||
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
|
||||
return (
|
||||
[property.Name for property in pset_template.HasPropertyTemplates]
|
||||
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_base_quantity_name(cls, product=None):
|
||||
def get_applicable_base_quantity_name(
|
||||
cls, product: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> Union[str, None]:
|
||||
if not product:
|
||||
return
|
||||
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
|
||||
@@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
|
||||
|
||||
@classmethod
|
||||
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
|
||||
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
|
||||
|
||||
@classmethod
|
||||
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
|
||||
def get_new_guessed_quantity(
|
||||
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
|
||||
) -> Union[float, None]:
|
||||
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
|
||||
|
||||
@classmethod
|
||||
def get_rounded_value(cls, new_quantity):
|
||||
def get_rounded_value(cls, new_quantity: float) -> float:
|
||||
return round(new_quantity, 3)
|
||||
|
||||
@classmethod
|
||||
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
|
||||
def get_calculated_object_quantities(
|
||||
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
|
||||
) -> dict[str, float]:
|
||||
return {
|
||||
quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj))
|
||||
quantity_name: cls.get_rounded_value(value)
|
||||
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
|
||||
if cls.has_calculator(qto_name, quantity_name)
|
||||
and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None
|
||||
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def has_calculator(cls, qto_name, quantity_name):
|
||||
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
|
||||
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
|
||||
|
||||
@classmethod
|
||||
def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None):
|
||||
def convert_to_project_units(
|
||||
cls,
|
||||
value: float,
|
||||
qto_name: Optional[str] = None,
|
||||
quantity_name: Optional[str] = None,
|
||||
quantity_type: Optional[QuanityTypes] = None,
|
||||
) -> Union[float, None]:
|
||||
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
|
||||
to let method figure the `quantity_type` from the templates
|
||||
|
||||
`quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME`
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
quantity_to_unit_types = {
|
||||
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_guessed_quantities(cls, obj, pset_qto_properties):
|
||||
def get_guessed_quantities(
|
||||
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
|
||||
) -> dict[str, float]:
|
||||
calculated_quantities = {}
|
||||
for pset_qto_property in pset_qto_properties:
|
||||
quantity_name = pset_qto_property.get_info()["Name"]
|
||||
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return calculated_quantities
|
||||
|
||||
@classmethod
|
||||
def get_base_qto(cls, product):
|
||||
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not hasattr(product, "IsDefinedBy"):
|
||||
return
|
||||
for rel in product.IsDefinedBy or []:
|
||||
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
@classmethod
|
||||
def get_related_cost_item_quantities(cls, product):
|
||||
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
|
||||
"""_summary_: Returns the related cost item and related quantities of the product
|
||||
|
||||
:param ifc-instance product: ifc instance
|
||||
|
||||
@@ -21,6 +21,8 @@ import ifcopenshell
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.material
|
||||
import blenderbim.core.style
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
@@ -20,10 +20,17 @@ class Search(blenderbim.core.tool.Search):
|
||||
return bpy.context.scene.BIMSearchProperties.filter_groups
|
||||
elif module == "csv":
|
||||
return bpy.context.scene.CsvProperties.filter_groups
|
||||
elif module == "diff":
|
||||
return bpy.context.scene.DiffProperties.filter_groups
|
||||
elif module == "drawing_include":
|
||||
return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups
|
||||
elif module == "drawing_exclude":
|
||||
return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups
|
||||
elif module.startswith("clash"):
|
||||
_, clash_set_index, ab, clash_source_index = module.split("_")
|
||||
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[
|
||||
int(clash_source_index)
|
||||
].filter_groups
|
||||
|
||||
@classmethod
|
||||
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
|
||||
@@ -189,6 +196,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
||||
|
||||
def query(self, args):
|
||||
keys, comparison, value = args
|
||||
if comparison == "=":
|
||||
comparison = ""
|
||||
return {"type": "query", "name": keys, "value": f"{comparison}{value}"}
|
||||
|
||||
def comparison(self, args):
|
||||
|
||||
@@ -150,9 +150,9 @@ class Spatial(blenderbim.core.tool.Spatial):
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
obj.select_set(True)
|
||||
if unhide:
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def filter_products(cls, products, action):
|
||||
|
||||
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
|
||||
return obj.name
|
||||
|
||||
@classmethod
|
||||
def get_style(cls, obj):
|
||||
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if obj.BIMMaterialProperties.ifc_style_id:
|
||||
try:
|
||||
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
|
||||
|
||||
+37
-5
@@ -1,12 +1,44 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
color: #7cbf33;
|
||||
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
|
||||
background-clip: border-box;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toc-tree .reference:hover {
|
||||
color: #d98014;
|
||||
}
|
||||
.sidebar-brand-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.blockbutton {
|
||||
border: 1px solid var(--color-admonition-title--seealso);
|
||||
background-color: var(--color-admonition-title-background--seealso);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.blockbutton a {
|
||||
display: block;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-radius: 5px;
|
||||
background: #40b74c;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.blockbutton a:hover {
|
||||
background: #70ba35;
|
||||
}
|
||||
.highlight .hll {
|
||||
background-color: #ffc2;
|
||||
}
|
||||
section img {
|
||||
display: block;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "BlenderBIM Add-on"
|
||||
copyright = "2020-2023 IfcOpenShell Contributors"
|
||||
copyright = "2020-2024 IfcOpenShell Contributors"
|
||||
author = "IfcOpenShell Contributors"
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
@@ -48,7 +48,7 @@ release = "0.0.231104"
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"]
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
|
||||
|
||||
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
|
||||
autosectionlabel_prefix_document = True
|
||||
@@ -75,3 +75,59 @@ html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
# Code block styles. Dark styling helps important code examples "pop" on the
|
||||
# page even on light themes.
|
||||
pygments_style = "one-dark"
|
||||
pygments_dark_style = "one-dark"
|
||||
|
||||
html_logo = "https://blenderbim.org/assets/images/blender/blender-logo.png"
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
|
||||
"source_branch": "v0.7.0",
|
||||
"source_directory": "src/blenderbim/docs/",
|
||||
"light_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#f7f7f6",
|
||||
"color-background-secondary": "#eeeeec",
|
||||
"color-background-border": "#cfd0cb",
|
||||
"color-foreground-primary": "#2e3436",
|
||||
"color-sidebar-item-background--hover": "#f7f7f6",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
"dark_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#2e3436",
|
||||
"color-background-border": "#2e3436",
|
||||
"color-foreground-primary": "#eeeeec",
|
||||
"color-sidebar-item-background--hover": "#2e3436",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
|
||||
"footer_icons": [
|
||||
{
|
||||
"name": "IfcOpenShell",
|
||||
"url": "https://ifcopenshell.org",
|
||||
"html": """
|
||||
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs",
|
||||
"html": """
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ You will need to choose which build to download.
|
||||
- If you are on Blender >=4.1, choose py311
|
||||
- If you are on Blender >=3.1 and <=4.0, choose py10
|
||||
- If you are on Blender >=2.93 and <3.1, choose py39
|
||||
- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win``
|
||||
depending on your operating system
|
||||
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or
|
||||
``win`` depending on your operating system
|
||||
|
||||
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
|
||||
but it happens.
|
||||
@@ -43,11 +43,11 @@ compile IfcOpenShell is out of scope of this document.
|
||||
|
||||
You can create your own package by using the Makefile as shown below. You can
|
||||
choose between a ``PLATFORM`` of ``linux``, ``macos``, ``macosm1``, and ``win``.
|
||||
You can choose between a ``PYVERSION`` of ``py39``, or ``py310``.
|
||||
You can choose between a ``PYVERSION`` of ``py311``, ``py310``, or ``py39``.
|
||||
::
|
||||
|
||||
$ cd src/blenderbim
|
||||
$ make dist PLATFORM=linux PYVERSION=py310
|
||||
$ make dist PLATFORM=linux PYVERSION=py311
|
||||
$ ls dist/
|
||||
|
||||
This will give you a fully packaged Blender add-on zip that you can distribute
|
||||
@@ -258,10 +258,12 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
|
||||
ifcclash
|
||||
bimtester
|
||||
ifccobie
|
||||
ifcdiff
|
||||
ifccsv
|
||||
ifcdiff
|
||||
ifc4d
|
||||
ifc5d
|
||||
ifcpatch
|
||||
ifcp6
|
||||
ifctester
|
||||
pystache
|
||||
svgwrite
|
||||
dateutil
|
||||
@@ -276,17 +278,13 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
|
||||
elementpath
|
||||
six
|
||||
lark-parser
|
||||
hppfcl
|
||||
behave
|
||||
parse
|
||||
parse_type
|
||||
xlsxwriter
|
||||
odfpy
|
||||
defusedxml
|
||||
boto3
|
||||
botocore
|
||||
jmespath
|
||||
s3transfer
|
||||
ifcjson
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -6,9 +6,13 @@ the Python version shipped by the Blender Foundation for the most recent three
|
||||
major Blender versions:
|
||||
|
||||
- 64-bit Linux on Python 3.10
|
||||
- 64-bit MacOS on Python 3.10
|
||||
- 64-bit MacOS M1 on Python 3.10
|
||||
- 64-bit Linux on Python 3.11
|
||||
- 64-bit MacOS Intel on Python 3.10
|
||||
- 64-bit MacOS Intel on Python 3.11
|
||||
- 64-bit MacOS Silicon on Python 3.10
|
||||
- 64-bit MacOS Silicon on Python 3.11
|
||||
- 64-bit Windows on Python 3.10
|
||||
- 64-bit Windows on Python 3.11
|
||||
|
||||
Note that developer builds may exist for older versions of Python but there will
|
||||
be no guarantee of the uptime or stability of these builds.
|
||||
|
||||
@@ -12,6 +12,8 @@ Writing technical documentation
|
||||
All documentation is written in ReStructured Text and is available in the
|
||||
`BlenderBIM Add-on docs directory
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs>`_.
|
||||
You can press the edit button on the top right on any documentation page to
|
||||
quickly edit their content.
|
||||
|
||||
You can link to `external websites
|
||||
<https://docs.readthedocs.io/en/stable/guides/cross-referencing-with-sphinx.html>`_.
|
||||
@@ -86,3 +88,27 @@ Tables can be very annoying to format. You can use a CSV table instead.
|
||||
|
||||
"ABC", "01", "02"
|
||||
"DEF", "03", "04"
|
||||
|
||||
Building documentation
|
||||
----------------------
|
||||
|
||||
If you want to build the documentation locally, the documentation system uses
|
||||
`Sphinx <https://www.sphinx-doc.org/en/master/>`_. First, install the theme and
|
||||
theme dependencies:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install furo
|
||||
$ pip install sphinx-autoapi
|
||||
$ pip install sphinx-copybutton
|
||||
|
||||
Now you can generate the documentation:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd /path/to/ifcopenshell/src/blenderbim/docs/
|
||||
$ make html
|
||||
$ cd _build/html
|
||||
$ python -m http.server
|
||||
|
||||
You will now have a local webserver running hosting the documentation.
|
||||
|
||||
@@ -70,8 +70,8 @@ In the top left **File** menu, Save your new IFC model on your computer.
|
||||
Congratulations! You have now created your first OpenBIM model from Blender! You
|
||||
can open the IFC file in any other program, and you will see something similar
|
||||
to the image below. Three simple open source online viewers you can test with
|
||||
are `IfcPipeline <https://view.ifcopenshell.org>`__, `IFC.JS
|
||||
<https://ifcjs.github.io/web-ifc-viewer/example/index>`__, and `3DViewer
|
||||
are `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
|
||||
<https://platform.thatopen.com/app>`__, and `3DViewer
|
||||
<https://3dviewer.net/>`__.
|
||||
|
||||
.. image:: images/ifc-pipeline.png
|
||||
|
||||
@@ -25,7 +25,7 @@ Installation
|
||||
|
||||
.. warning::
|
||||
|
||||
If you are not using Blender version >=3.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
|
||||
If you are not using Blender version >=4.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
|
||||
|
||||
3. **Install the BlenderBIM Add-on**
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,138 @@
|
||||
# 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 os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# When run from Blender
|
||||
BLEND_DIR = os.path.dirname(bpy.data.filepath)
|
||||
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Entourage Library.ifc")
|
||||
|
||||
|
||||
class LibraryGenerator:
|
||||
def generate(self, library_name, output_filename):
|
||||
ifcopenshell.api.pre_listeners = {}
|
||||
ifcopenshell.api.post_listeners = {}
|
||||
|
||||
self.materials = {}
|
||||
|
||||
self.file = ifcopenshell.api.run("project.create_file")
|
||||
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
|
||||
self.library = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
|
||||
self.representations = {
|
||||
"Model/Body/MODEL_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Plan/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Plan",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=plan,
|
||||
),
|
||||
"Model/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Model/Body/SECTION_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="SECTION_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
}
|
||||
|
||||
# Manually modeled trees
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.type == "MESH":
|
||||
continue
|
||||
if "Plan/" in obj.name or "Model/" in obj.name:
|
||||
continue
|
||||
representations = {"Model/Body/MODEL_VIEW": obj.name}
|
||||
for rep_key in self.representations.keys():
|
||||
rep_obj = bpy.data.objects.get(obj.name + " " + rep_key)
|
||||
if rep_obj:
|
||||
representations[rep_key] = rep_obj.name
|
||||
self.create_type("IfcBuildingElementProxyType", obj.name, representations)
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_type(self, ifc_class, name, representations):
|
||||
element = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class=ifc_class, predefined_type="ENTOURAGE", name=name
|
||||
)
|
||||
for rep_name, obj_name in representations.items():
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_representation",
|
||||
self.file,
|
||||
context=self.representations[rep_name],
|
||||
blender_object=obj,
|
||||
geometry=obj.data,
|
||||
total_items=max(1, len(obj.material_slots)),
|
||||
)
|
||||
styles = []
|
||||
for slot in obj.material_slots:
|
||||
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
|
||||
ifcopenshell.api.run(
|
||||
"style.add_surface_style",
|
||||
self.file,
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleShading",
|
||||
attributes=tool.Style.get_surface_shading_attributes(slot.material),
|
||||
)
|
||||
styles.append(style)
|
||||
if styles:
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", self.file, product=element, representation=representation
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LibraryGenerator().generate("Entourage Assets Library", output_filename=OUT_PATH)
|
||||
@@ -438,7 +438,7 @@ Scenario: Override duplicate move - copying an aggregate
|
||||
And the object "IfcElementAssembly/Assembly.001" exists
|
||||
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001"
|
||||
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying objects with connection
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -467,7 +467,7 @@ Scenario: Override duplicate move - copying objects with connection
|
||||
Then the object "IfcSlab/Slab.001" exists
|
||||
And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]"
|
||||
Then the object "{wall_name}" has a connection with "{slab_name}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying walls with mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -477,24 +477,24 @@ Scenario: Override duplicate move - copying walls with mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
When I deselect all objects
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcWall/Wall.002" exists
|
||||
And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]"
|
||||
Then the object "IfcWall/Wall.003" exists
|
||||
And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]"
|
||||
Then the object "{wall_name1}" has a connection with "{wall_name2}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move linked - without active IFC data
|
||||
Given an empty Blender session
|
||||
And I add a cube
|
||||
@@ -544,14 +544,14 @@ Scenario: Override paste buffer - with active IFC data
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "view3d.copybuffer"
|
||||
# IFC elements unlinked on paste for safety
|
||||
And I press "bim.override_paste_buffer"
|
||||
Then the object "IfcWall/Cube" exists
|
||||
And the object "IfcWall/Cube" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" exists
|
||||
And the object "IfcWall/Cube.001" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
And the object "IfcBuildingStorey/My Storey.001" exists
|
||||
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
|
||||
And the object "Cube.001" exists
|
||||
And the object "Cube.001" is not an IFC element
|
||||
And the object "My Storey.001" exists
|
||||
And the object "My Storey.001" is not an IFC element
|
||||
|
||||
Scenario: Duplicate linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -559,8 +559,8 @@ Scenario: Duplicate linked aggregate
|
||||
When I duplicate linked aggregate the selected objects
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcElementAssembly/Assembly_02" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
|
||||
And the object "IfcElementAssembly/Assembly_01" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group
|
||||
|
||||
Scenario: Refresh linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -588,7 +588,7 @@ Scenario: Refresh linked aggregate - after deleting an object
|
||||
When I refresh linked aggregate the selected object
|
||||
Then the object "IfcWall/Wall_01" does not exist
|
||||
And the object "IfcWall/Wall_02" exists
|
||||
|
||||
|
||||
Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
And the object "IfcWall/Wall_01" is selected
|
||||
@@ -606,4 +606,4 @@ Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcWall/Wall_03.001" exists
|
||||
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
When I press "bim.hotkey(hotkey='S_A')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Extend one wall to another
|
||||
Given an empty IFC project
|
||||
@@ -122,35 +122,17 @@ Scenario: Extend one wall to another
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_E')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-2,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - first wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Change the height of a wall
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -387,10 +387,10 @@ Scenario: Edit pset length property
|
||||
|
||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
|
||||
Scenario: Edit qset length property
|
||||
Given an empty IFC project
|
||||
And I press "mesh.add_clever_stair"
|
||||
@@ -401,15 +401,14 @@ Scenario: Edit qset length property
|
||||
|
||||
# Testing Q_LENGTH type of prop
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -24,7 +24,7 @@ Scenario: Unlink object
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material"
|
||||
And I press "bim.add_style()"
|
||||
When I press "bim.unlink_object(obj='IfcWall/Cube')"
|
||||
Then the object "Cube" is not an IFC element
|
||||
And the material "Material" is an IFC style
|
||||
|
||||
@@ -8,9 +8,9 @@ Scenario: Select all walls
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I add a new item to "scene.IfcSelectorProperties.groups"
|
||||
And I add a new item to "scene.IfcSelectorProperties.groups[0].queries"
|
||||
And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class"
|
||||
And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall"
|
||||
When I press "bim.filter_model_elements(option='select')"
|
||||
And I press "bim.add_filter_group(module='search')"
|
||||
And I set "scene.BIMSearchProperties.facet" to "entity"
|
||||
And I press "bim.add_filter(index=0, type='entity', module='search')"
|
||||
And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall"
|
||||
When I press "bim.search"
|
||||
Then the object "IfcWall/Cube" is selected
|
||||
|
||||
@@ -18,7 +18,7 @@ Scenario: Remove style
|
||||
When I press "bim.remove_style(style={style})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Add style
|
||||
Scenario: Add style to current blender material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
@@ -33,32 +33,39 @@ Scenario: Unlink style
|
||||
When I press "bim.unlink_style"
|
||||
Then the material "Material" is not an IFC style
|
||||
|
||||
Scenario: Enable editing style
|
||||
Scenario: Add a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
When I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
When I press "bim.add_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Scenario: Disable adding a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
When I press "bim.disable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
When I press "bim.disable_adding_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Edit style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.edit_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.disable_editing_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load styles
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -51,7 +51,7 @@ def replace_variables(value):
|
||||
|
||||
|
||||
def is_x(number, x):
|
||||
return abs(number - x) < 1e-6
|
||||
return abs(number - x) < 1e-5
|
||||
|
||||
|
||||
def vectors_are_equal(v1, v2):
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import datetime
|
||||
from datetime import timedelta, date
|
||||
from typing import List
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.date
|
||||
@@ -25,7 +26,7 @@ import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class MSP2Ifc:
|
||||
def __init__(self):
|
||||
def __init__(self, optionalColumns: List[str] = []):
|
||||
self.xml = None
|
||||
self.file = None
|
||||
self.ns = None
|
||||
@@ -33,6 +34,7 @@ class MSP2Ifc:
|
||||
self.project = {}
|
||||
self.calendars = {}
|
||||
self.tasks = {}
|
||||
self.optionalColumns = optionalColumns
|
||||
self.resources = {}
|
||||
self.RESOURCE_TYPES_MAPPING = {"1": "LABOR", "0": "MATERIAL", "2": None}
|
||||
|
||||
@@ -106,6 +108,18 @@ class MSP2Ifc:
|
||||
"ifc": None,
|
||||
}
|
||||
|
||||
# retrieve optional columns
|
||||
# If first column = "all" then retrieve all columns
|
||||
if len(self.optionalColumns) and self.optionalColumns[0] == "all":
|
||||
self.optionalColumns = [child.tag.split("}")[1] for child in task]
|
||||
|
||||
for column in self.optionalColumns:
|
||||
if not self.tasks[task_id].get(column):
|
||||
self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None
|
||||
|
||||
|
||||
|
||||
|
||||
def parse_calendar_xml(self, project):
|
||||
def parse_working_times(day):
|
||||
working_times = []
|
||||
@@ -200,7 +214,8 @@ class MSP2Ifc:
|
||||
def create_tasks(self, work_schedule):
|
||||
for task_id in self.tasks:
|
||||
task = self.tasks[task_id]
|
||||
if task["OutlineLevel"] == 0:
|
||||
# Outline Level can be None or 0
|
||||
if not task["OutlineLevel"]:
|
||||
self.create_task(task, work_schedule=work_schedule)
|
||||
|
||||
def create_work_schedule(self):
|
||||
@@ -268,6 +283,18 @@ class MSP2Ifc:
|
||||
for subtask_id in task["subtasks"]:
|
||||
self.create_task(self.tasks[subtask_id], parent_task=task)
|
||||
|
||||
|
||||
# create pset for optional columns
|
||||
if len(self.optionalColumns):
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task")
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
self.file,
|
||||
pset=pset,
|
||||
properties= {name: str(task[name]) for name in self.optionalColumns if task[name]}
|
||||
)
|
||||
|
||||
def process_working_week(self, week, calendar):
|
||||
day_map = {
|
||||
"1": 7, # Sunday
|
||||
|
||||
@@ -117,7 +117,7 @@ class Clasher:
|
||||
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
|
||||
start = time.time()
|
||||
self.settings.logger.info("Creating iterator")
|
||||
if not mode or not selector:
|
||||
if not mode or mode == "a" or not selector:
|
||||
elements = set(ifc_file.by_type("IfcElement"))
|
||||
elements -= set(ifc_file.by_type("IfcFeatureElement"))
|
||||
elif mode == "e":
|
||||
@@ -135,8 +135,7 @@ class Clasher:
|
||||
self.logger.info(f"Adding objects {name}")
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
self.tree.add_element(iterator.get_native(), should_triangulate=True)
|
||||
# self.tree.add_element(iterator.get())
|
||||
self.tree.add_element(iterator.get())
|
||||
shape = iterator.get()
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
@@ -240,13 +240,19 @@ class IfcCsv:
|
||||
index = attributes.index(data["name"])
|
||||
formatting_indices[index] = data["format"]
|
||||
|
||||
for row in self.results:
|
||||
for index, format_query in formatting_indices.items():
|
||||
for index, format_query in formatting_indices.items():
|
||||
for row in self.results:
|
||||
if row[index] == null:
|
||||
continue
|
||||
row[index] = '"' + str(row[index]).replace('"', '\\"') + '"'
|
||||
row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index]))
|
||||
|
||||
if self.summaries[index] is not None:
|
||||
summary_label, summary_value = self.summaries[index].split(": ")
|
||||
summary_value = '"' + str(summary_value).replace('"', '\\"') + '"'
|
||||
summary_value = ifcopenshell.util.selector.format(format_query.replace("{{value}}", summary_value))
|
||||
self.summaries[index] = summary_label + ": " + str(summary_value)
|
||||
|
||||
def sort_results(self, sort, attributes, include_global_id):
|
||||
if not self.results:
|
||||
return
|
||||
|
||||
@@ -54,7 +54,7 @@ class IfcDiff:
|
||||
that comparisons will take longer.
|
||||
:type is_shallow: bool
|
||||
:param filter_elements: An IFC filter query if you only want to compare a
|
||||
subset of elements. For example: ``.IfcWall`` to only compare walls.
|
||||
subset of elements. For example: ``IfcWall`` to only compare walls.
|
||||
:type filter_elements: string
|
||||
|
||||
Example::
|
||||
@@ -83,9 +83,12 @@ class IfcDiff:
|
||||
self.precision = self.get_precision()
|
||||
|
||||
if self.filter_elements:
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements))
|
||||
new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements))
|
||||
old_elements = set(
|
||||
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.old, self.filter_elements)
|
||||
)
|
||||
new_elements = set(
|
||||
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.new, self.filter_elements)
|
||||
)
|
||||
else:
|
||||
old_elements = self.old.by_type("IfcElement")
|
||||
if self.old.schema == "IFC2X3":
|
||||
|
||||
@@ -28,7 +28,19 @@
|
||||
bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
|
||||
TopoDS_Face f;
|
||||
gp_Trsf2d trsf2d;
|
||||
if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
bool is_mirror = false;
|
||||
#ifdef SCHEMA_HAS_IfcMirroredProfileDef
|
||||
if (l->as<IfcSchema::IfcMirroredProfileDef>()) {
|
||||
trsf2d.SetMirror(gp::Origin2d());
|
||||
is_mirror = true;
|
||||
}
|
||||
#endif
|
||||
if (!is_mirror) {
|
||||
if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (convert_face(l->ParentProfile(), f)) {
|
||||
gp_Trsf trsf = trsf2d;
|
||||
face = BRepBuilderAPI_Transform(f, trsf).Shape();
|
||||
return true;
|
||||
|
||||
@@ -859,7 +859,9 @@ IfcSchema::IfcRepresentation* IfcGeom::Kernel::representation_mapped_to(const If
|
||||
}
|
||||
}
|
||||
} catch (const IfcParse::IfcException& e) {
|
||||
Logger::Error(e);
|
||||
// @todo the remove_boundingboxes() in IfcConvert leave dangling IfcRepresentationMap
|
||||
// causing this error to be logged.
|
||||
Logger::Error(e, representation);
|
||||
// @todo reset representation_mapped_to to zero?
|
||||
}
|
||||
return representation_mapped_to;
|
||||
|
||||
@@ -371,6 +371,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
|
||||
triangles(i).Get(n3, n2, n1);
|
||||
else triangles(i).Get(n1, n2, n3);
|
||||
|
||||
if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) {
|
||||
Logger::Warning("Mesher generated a degenerate triangle, ignoring");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* An alternative would be to calculate normals based
|
||||
* on the coordinates of the mesh vertices */
|
||||
/*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,588 @@
|
||||
#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
|
||||
namespace {
|
||||
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);
|
||||
@@ -109,7 +109,7 @@ endif
|
||||
mkdir -p dist/ifcopenshell
|
||||
cp -r ifcopenshell/* dist/ifcopenshell/
|
||||
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
|
||||
ifeq ($(PLATFORM), win)
|
||||
|
||||
+37
-5
@@ -1,12 +1,44 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
color: #7cbf33;
|
||||
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
|
||||
background-clip: border-box;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toc-tree .reference:hover {
|
||||
color: #d98014;
|
||||
}
|
||||
.sidebar-brand-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.blockbutton {
|
||||
border: 1px solid var(--color-admonition-title--seealso);
|
||||
background-color: var(--color-admonition-title-background--seealso);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.blockbutton a {
|
||||
display: block;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-radius: 5px;
|
||||
background: #40b74c;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.blockbutton a:hover {
|
||||
background: #70ba35;
|
||||
}
|
||||
.highlight .hll {
|
||||
background-color: #ffc2;
|
||||
}
|
||||
section img {
|
||||
display: block;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# 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
|
||||
# IfcOpenShell 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.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# IfcOpenShell 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.
|
||||
# GNU Lesser 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/>.
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
@@ -36,7 +36,7 @@ sys.path.insert(0, os.path.abspath('..'))
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "IfcOpenShell"
|
||||
copyright = "2011-2023, IfcOpenShell Contributors"
|
||||
copyright = "2011-2024, IfcOpenShell Contributors"
|
||||
author = "IfcOpenShell Contributors"
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
@@ -54,7 +54,7 @@ release = "0.7.0"
|
||||
# - No subnav making it really hard to navigate
|
||||
# - Kinda hacky setup https://stackoverflow.com/questions/2701998/sphinx-autodoc-is-not-automatic-enough
|
||||
# - I couldn't customise the template to show submodules above members which makes API discovery hard for users
|
||||
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel"]
|
||||
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
|
||||
|
||||
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
|
||||
autosectionlabel_prefix_document = True
|
||||
@@ -67,6 +67,8 @@ autoapi_type = 'python'
|
||||
|
||||
# autoapi works by reading source code instead of importing modules
|
||||
autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
|
||||
# autoapi_dirs = ['../../ifcdiff']
|
||||
# autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util']
|
||||
# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
|
||||
|
||||
# These are auto-generated based on the IFC schema, so exclude them
|
||||
@@ -113,3 +115,54 @@ html_css_files = ["custom.css"]
|
||||
# page even on light themes.
|
||||
pygments_style = "one-dark"
|
||||
pygments_dark_style = "one-dark"
|
||||
|
||||
html_logo = "https://ifcopenshell.org/assets/images/logo.png"
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
|
||||
"source_branch": "v0.7.0",
|
||||
"source_directory": "src/ifcopenshell-python/docs/",
|
||||
"light_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#f7f7f6",
|
||||
"color-background-secondary": "#eeeeec",
|
||||
"color-background-border": "#cfd0cb",
|
||||
"color-foreground-primary": "#2e3436",
|
||||
"color-sidebar-item-background--hover": "#f7f7f6",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
"dark_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#2e3436",
|
||||
"color-background-border": "#2e3436",
|
||||
"color-foreground-primary": "#eeeeec",
|
||||
"color-sidebar-item-background--hover": "#2e3436",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
|
||||
"footer_icons": [
|
||||
{
|
||||
"name": "IfcOpenShell",
|
||||
"url": "https://ifcopenshell.org",
|
||||
"html": """
|
||||
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/docs",
|
||||
"html": """
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ Pre-built packages
|
||||
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
|
||||
+----------------+----------------+----------------+----------------+------------------+
|
||||
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and run IfcConvert using the command line.
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
Geometry tree
|
||||
=============
|
||||
|
||||
IfcOpenShell includes a utility to build a unbalanced binary tree of geometry
|
||||
and their bounding boxes. After a tree is built, you can efficiently select
|
||||
geometry by specifying a point, radius, or bounding box.
|
||||
IfcOpenShell includes a utility to build trees of geometry and their bounding
|
||||
boxes. Geometry trees can be used to efficiently select geometry or collide
|
||||
geometry with one another.
|
||||
|
||||
.. image:: images/geometry-tree.png
|
||||
|
||||
The most efficient way to build tree is by using the iterator, as shown in the
|
||||
example below:
|
||||
The most efficient way to build a tree is by using the iterator. If the native
|
||||
OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively,
|
||||
if triangulation is added to the tree, a **BVH Tree** is built. The type of
|
||||
tree determines the type of operation you can perform.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -21,27 +23,208 @@ example below:
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
# Use triangulation to build a BVH tree
|
||||
tree.add_element(iterator.get())
|
||||
|
||||
# Alternatively, use this code to build an unbalanced binary tree
|
||||
# tree.add_element(iterator.get_native())
|
||||
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
Once built, there are three methods you can use to select elements in the tree:
|
||||
``select_box``, ``select``, and ``select_ray``.
|
||||
Clashing or selecting geometry from a geometry tree
|
||||
---------------------------------------------------
|
||||
|
||||
``select_box`` lets you query for elements that contain a point or another
|
||||
element. However, it only checks the bounding box of elements instead of their
|
||||
exact geometry. This is the fastest approach and is recommended if you don't
|
||||
need precise geometry selection.
|
||||
With a **BVH Tree**, you can efficiently clash sets of elements with other
|
||||
elements. You can find elements that intersect, collide, or are within a
|
||||
clearance distance threshold of one another. There are three methods you can
|
||||
use to clash elements in the tree. Each function collides one set of elements
|
||||
with another set of elements.
|
||||
|
||||
``select`` lets you query for elements that contain a point, a sphere, or
|
||||
another element. ``select`` is similar to select box, but additionally
|
||||
considers the actual geometry of the object. This is slower but more precise.
|
||||
- `Detecting intersection clashes between elements`_ detects when an element intersects with another
|
||||
element. This is the most common type of clash detection used when
|
||||
coordinating designs. For example, you might want to know if any pipes go
|
||||
through structural columns or beams.
|
||||
- `Detecting collision clashes between elements`_ detects when an element
|
||||
touches another element. It is the fastest type of clash detection but does
|
||||
not consider the distance that an element goes inside another element. This
|
||||
considers surfaces only so it works on non-manifold geometry but will not
|
||||
detect if an element is completely within another element.
|
||||
- `Detecting clearance clashes between elements`_ detects when an element comes
|
||||
near to another element within a clearance threshold. This is the slowest
|
||||
type of clash detection. It works on non-manifold geometry and does not
|
||||
consider inside vs outside. Elements like pipe and ducts with insulation,
|
||||
structural openings, and equipment will typically require clearance checks.
|
||||
|
||||
``select_ray`` lets you query for elements that intersect with a ray.
|
||||
With a **UB Tree**, you can efficiently select geometry by specifying a point,
|
||||
radius, or bounding box. There are three methods you can use to select elements
|
||||
in the tree.
|
||||
|
||||
- `Selecting elements using bounding boxes`_ lets you query for elements that
|
||||
contain a point or another element. However, it only checks the bounding box
|
||||
of elements instead of their exact geometry. This is the fastest approach and
|
||||
is recommended if you don't need precise geometry selection.
|
||||
- `Selecting elements using precise geometry`_ lets you query for elements that
|
||||
contain a point, a sphere, or another element. This is similar to selecting
|
||||
using bounding boxes, but additionally considers the actual geometry of the
|
||||
element. This is slower but more precise.
|
||||
- `Selecting elements using a ray`_ lets you query for elements that intersect
|
||||
with a ray.
|
||||
|
||||
Detecting intersection clashes between elements
|
||||
-----------------------------------------------
|
||||
|
||||
``clash_intersection_many`` detects when an element intersects with or is
|
||||
contained within another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_intersection_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
tolerance=0.002, # Any protrusions less than 2mm are ignored
|
||||
check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# Potential clash types that can be detected are protrusions, pierces, and collisions
|
||||
clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type],
|
||||
|
||||
# P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the protrusion or piercing distance in meters.
|
||||
# It is also the distance between P1 and P2.
|
||||
distance = clash.distance
|
||||
|
||||
If you specify a ``tolerance`` value, intersections with a protrusion distance
|
||||
smaller than this tolerance are excluded. It is recommended to specify a
|
||||
non-zero tolerance to distinguish between when elements merely touch (e.g. a
|
||||
GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through
|
||||
a beam).
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as an
|
||||
intersection is found. This is faster but may not return the worst-case
|
||||
protrusion distance. If you are not interested in the protrusion distance, it
|
||||
is recommended to set this to ``False``. If you want the protrusion distance,
|
||||
such as to prioritise which clashes are more severe, set this to ``True``.
|
||||
|
||||
This includes:
|
||||
|
||||
1. When an element X protrudes inside element Y, where element Y is manifold.
|
||||
In this case, a protrusion distance is calculated as the deepest point of
|
||||
element X to the closest surface of element Y. ``P1`` is defined as the XYZ
|
||||
coordinate on element X, and ``P2`` is defined as the nearest point on the
|
||||
surface of element Y.
|
||||
2. When an element X pierces element Y, such that an edge of element X enters
|
||||
element Y and leaves through another face. In this case, a piercing distance
|
||||
is calculated as the distance where that edge is inside element Y. ``P1`` is
|
||||
defined as the point on an edge of element X which enters element Y, and
|
||||
``P2`` is the point where that edge leaves element Y.
|
||||
3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so
|
||||
instead when X and Y have any touching face. This is the same as the
|
||||
``clash_collision_many`` check below. The distance is considered to be zero
|
||||
and ignores your specified tolerance. ``P1`` and ``P2`` are equal and
|
||||
represent an arbitrary XYZ point where the two elements touch.
|
||||
|
||||
Detecting collision clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_collision_many`` detects when the surface of an element collides with
|
||||
another element. The surfaces may either merely touch (e.g. are coplanar) or
|
||||
intersect.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_collision_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
allow_touching=True, # Include results where faces merely touch but do not intersect
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents two possible arbitrary points where a collision is found.
|
||||
# P1 may or may not be equal to P2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
A collision between two surface triangles may be "touching" or "intersecting".
|
||||
Two touching triangles may be coplanar or merely have a single edge or vertex
|
||||
touching the other triangle. An intersecting triangle will have at least one
|
||||
edge that goes through the other triangle.
|
||||
|
||||
Detecting clearance clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_clearance_many`` detects with the surface of an element comes within a
|
||||
clearance distance threshold of another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_clearance_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
clearance=0.1, # Any surface closer than than 100mm is a clash
|
||||
check_all=False, # Stop measuring distances once the first clearance violation is found per element.
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents the two XYZ coordinates between element1 and element2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the distance between element1 and element2 that is less than the clearance.
|
||||
# It is the distance between P1 and P2. It cannot be less than 0.
|
||||
distance = clash.distance
|
||||
|
||||
You cannot specify a ``clearance`` less than 0.
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as a
|
||||
clearance violation is found. This is faster but may not return the worst-case
|
||||
distance. If you only interested whether there is a clearance issue, it is
|
||||
recommended to set this to ``False``. If you want the exact worst case
|
||||
clearance distance, such as to prioritise which clashes are more severe, set
|
||||
this to ``True``.
|
||||
|
||||
Selecting elements using bounding boxes
|
||||
---------------------------------------
|
||||
|
||||
Elements may be queried using an axis aligned bounding box. An axis aligned
|
||||
bounding box is the bounding box using global XYZ axes, not the element's local
|
||||
XYZ axes. If you have a vertical construction project, this means that your
|
||||
model should be oriented to project north to get the best results.
|
||||
|
||||
You may select all elements that have a bounding box containing the point with
|
||||
XYZ coordinates of ``(0., 0., 0.)``.
|
||||
|
||||
@@ -128,11 +311,11 @@ geometry. It will return:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
elements = tree.select_box(wall, completely_within=True)
|
||||
elements = tree.select(wall, completely_within=True)
|
||||
|
||||
# Alternatively, you may also specify an extension to dilate the geometry
|
||||
# of the wall.
|
||||
elements = tree.select_box(wall, completely_within=True, extend=5.)
|
||||
elements = tree.select(wall, completely_within=True, extend=5.)
|
||||
|
||||
Selecting elements using a ray
|
||||
------------------------------
|
||||
|
||||
@@ -30,10 +30,6 @@ the API.
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
|
||||
+=============+================+================+================+===================+=====================+
|
||||
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
|
||||
@@ -43,36 +39,26 @@ the API.
|
||||
| Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
|
||||
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win32.zip
|
||||
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win32.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win32.zip
|
||||
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win64.zip
|
||||
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win64.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win64.zip
|
||||
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win32.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
|
||||
Python path. If you're not sure where your Python path is, run the following
|
||||
|
||||
@@ -233,4 +233,4 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
|
||||
"``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings."
|
||||
"``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``."
|
||||
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
|
||||
"``imperial_length({{value}}, {{precision}}, {{unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "Rounds ``{{value}}`` to the nearest ``1/{{precision}}`` inch then displays using fractional feet and inches. The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{unit}}``."
|
||||
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "``The {{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot`` or just inches if ``{{output_unit}}`` is set to ``inch``."
|
||||
|
||||
@@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required.
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
3. Install OpenCascade Technology (OCCT).
|
||||
3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other
|
||||
versions may have unexpected behaviour.
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -87,3 +87,4 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool
|
||||
|
||||
introduction/introduction_to_bim
|
||||
introduction/introduction_to_ifc
|
||||
introduction/how_to_contribute
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
How to contribute
|
||||
=================
|
||||
|
||||
IfcOpenShell is made possible by the contributions of hundreds of people.
|
||||
Contributions range from developing code, writing documentation, creating
|
||||
tutorials, responding to requests, reporting bugs, and marketing.
|
||||
|
||||
Writing documentation
|
||||
---------------------
|
||||
|
||||
A great way to contribute without writing code is to help writing
|
||||
documentation.
|
||||
|
||||
All documentation is written in ReStructured Text and is available in the
|
||||
`IfcOpenShell docs directory
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/docs>`_.
|
||||
You can press the edit button on the top right on any documentation page to
|
||||
quickly edit their content.
|
||||
|
||||
Python API documentation is autogenerated from docstrings present in the source
|
||||
code of the respective Python module.
|
||||
|
||||
If you want to build the documentation locally, the documentation system uses
|
||||
`Sphinx <https://www.sphinx-doc.org/en/master/>`_. First, install the theme and
|
||||
theme dependencies:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install furo
|
||||
$ pip install sphinx-autoapi
|
||||
$ pip install sphinx-copybutton
|
||||
|
||||
Now you can generate the documentation:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd /path/to/ifcopenshell/src/ifcopenshell-python/docs/
|
||||
$ make html
|
||||
$ cd _build/html
|
||||
$ python -m http.server
|
||||
|
||||
You will now have a local webserver running hosting the documentation.
|
||||
|
||||
.. tip::
|
||||
|
||||
It may take some time to generate the API documentation. You can edit the
|
||||
`list of autoapi directories
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/ifcopenshell-python/docs/conf.py#L69>`_
|
||||
to make this faster
|
||||
@@ -53,15 +53,29 @@ class Usecase:
|
||||
if hasattr(self.settings["product"], "PredefinedType"):
|
||||
if hasattr(self.settings["product"], "ElementType"):
|
||||
if (
|
||||
not self.settings["product"].ElementType
|
||||
self.settings["product"].ElementType is None
|
||||
and self.settings["product"].PredefinedType == "USERDEFINED"
|
||||
):
|
||||
self.settings["product"].PredefinedType = "NOTDEFINED"
|
||||
elif (
|
||||
self.settings["product"].ElementType
|
||||
and self.settings["product"].PredefinedType != "USERDEFINED"
|
||||
):
|
||||
self.settings["product"].PredefinedType = "USERDEFINED"
|
||||
elif hasattr(self.settings["product"], "ObjectType"):
|
||||
if (
|
||||
not self.settings["product"].ObjectType
|
||||
relating_type = ifcopenshell.util.element.get_type(self.settings["product"])
|
||||
if relating_type and relating_type.PredefinedType != "NOTDEFINED":
|
||||
self.settings["product"].ObjectType = None
|
||||
self.settings["product"].PredefinedType = None
|
||||
elif (
|
||||
self.settings["product"].ObjectType is None
|
||||
and self.settings["product"].PredefinedType == "USERDEFINED"
|
||||
):
|
||||
self.settings["product"].PredefinedType = "NOTDEFINED"
|
||||
elif (
|
||||
self.settings["product"].ObjectType
|
||||
and self.settings["product"].PredefinedType != "USERDEFINED"
|
||||
):
|
||||
self.settings["product"].PredefinedType = "USERDEFINED"
|
||||
if hasattr(self.settings["product"], "OwnerHistory"):
|
||||
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": self.settings["product"]})
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.date
|
||||
from ifcopenshell.util.data import WorkTimeDatesInterface
|
||||
|
||||
|
||||
class Usecase:
|
||||
@@ -55,10 +54,12 @@ class Usecase:
|
||||
self.settings = {"work_time": work_time, "attributes": attributes or {}}
|
||||
|
||||
def execute(self):
|
||||
work_time_dates = WorkTimeDatesInterface(self.settings["work_time"])
|
||||
for name, value in self.settings["attributes"].items():
|
||||
if name in ("Start", "Finish", "StartDate", "FinishDate"):
|
||||
if name in ("Start", "StartDate"):
|
||||
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
|
||||
setattr(work_time_dates, name, value)
|
||||
self.settings["work_time"][4] = value
|
||||
elif name in ("Finish", "FinishDate"):
|
||||
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
|
||||
self.settings["work_time"][5] = value
|
||||
else:
|
||||
setattr(self.settings["work_time"], name, value)
|
||||
|
||||
@@ -30,7 +30,7 @@ import functools
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Union, Any, Callable
|
||||
from typing import Union, Any, Callable, TypeVar
|
||||
|
||||
from . import ifcopenshell_wrapper
|
||||
from . import settings
|
||||
@@ -40,6 +40,8 @@ try:
|
||||
except ImportError as e:
|
||||
logging = type("logger", (object,), {"exception": staticmethod(lambda s: print(s))})
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def set_derived_attribute(*args):
|
||||
raise TypeError("Unable to set derived attribute")
|
||||
@@ -272,16 +274,16 @@ class entity_instance(object):
|
||||
"""
|
||||
return self.wrapped_data.get_argument_name(attr_idx)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
index = self.wrapped_data.get_argument_index(key)
|
||||
self[index] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: int) -> Any:
|
||||
if key < 0 or key >= len(self):
|
||||
raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a()))
|
||||
return entity_instance.wrap_value(self.wrapped_data.get_argument(key), self.wrapped_data.file)
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
def __setitem__(self, idx: int, value: T) -> T:
|
||||
if self.wrapped_data.file and self.wrapped_data.file.transaction:
|
||||
self.wrapped_data.file.transaction.store_edit(self, idx, value)
|
||||
|
||||
|
||||
@@ -195,6 +195,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: settings, inst: entity_instance, repr: Optional[entity_instance] = None
|
||||
|
||||
@@ -93,37 +93,3 @@ class Clipping:
|
||||
|
||||
second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False)
|
||||
return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
|
||||
|
||||
|
||||
class WorkTimeDatesInterface:
|
||||
def __init__(self, work_time: ifcopenshell.entity_instance):
|
||||
"""Utility class for IfcWorkTime dates allowing refer
|
||||
to it's dates as `.Start`/`.Finish` or `.StartDate`/`.FinishDate`
|
||||
regardless of the schema version
|
||||
"""
|
||||
if hasattr(work_time, "StartDate"): # since IFC4X3
|
||||
self.start_attr, self.finish_attr = "StartDate", "FinishDate"
|
||||
else:
|
||||
self.start_attr, self.finish_attr = "Start", "Finish"
|
||||
|
||||
self.work_time = work_time
|
||||
|
||||
@property
|
||||
def Start(self):
|
||||
return getattr(self.work_time, self.start_attr)
|
||||
|
||||
@Start.setter
|
||||
def Start(self, value):
|
||||
return setattr(self.work_time, self.start_attr, value)
|
||||
|
||||
StartDate = Start
|
||||
|
||||
@property
|
||||
def Finish(self):
|
||||
return getattr(self.work_time, self.finish_attr)
|
||||
|
||||
@Finish.setter
|
||||
def Finish(self, value):
|
||||
return setattr(self.work_time, self.finish_attr, value)
|
||||
|
||||
FinishDate = Finish
|
||||
|
||||
@@ -179,12 +179,14 @@ def get_property_definition(
|
||||
if not definition:
|
||||
return
|
||||
|
||||
ifc_class = definition.is_a()
|
||||
|
||||
if prop:
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
return get_quantity(definition.Quantities, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
return get_property(definition.HasProperties, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
return get_property(definition.Properties, prop, verbose=verbose)
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
@@ -196,12 +198,12 @@ def get_property_definition(
|
||||
return
|
||||
|
||||
props = {}
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
props.update(get_quantities(definition.Quantities, verbose=verbose))
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
props.update(get_properties(definition.HasProperties, verbose=verbose))
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
props.update(get_properties(definition.Properties, verbose=verbose))
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
props.update(get_quantities(definition[5], verbose=verbose))
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
props.update(get_properties(definition[4], verbose=verbose))
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
props.update(get_properties(definition[2], verbose=verbose))
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
# definition.is_a('IfcPreDefinedPropertySet'):
|
||||
@@ -216,7 +218,7 @@ def get_quantity(
|
||||
quantities: list[ifcopenshell.entity_instance], name: str, verbose=False
|
||||
) -> Union[Any, dict[str, Any]]:
|
||||
for quantity in quantities or []:
|
||||
if quantity.Name != name:
|
||||
if quantity[0] != name:
|
||||
continue
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
result = quantity[3]
|
||||
@@ -234,23 +236,23 @@ def get_quantities(quantities: list[ifcopenshell.entity_instance], verbose=False
|
||||
results = {}
|
||||
for quantity in quantities or []:
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
results[quantity.Name] = quantity[3]
|
||||
results[quantity[0]] = quantity[3]
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
elif quantity.is_a("IfcPhysicalComplexQuantity"):
|
||||
data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_quantities(quantity.HasQuantities)
|
||||
del data["HasQuantities"]
|
||||
results[quantity.Name] = data
|
||||
results[quantity[0]] = data
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
return results
|
||||
|
||||
@@ -262,7 +264,7 @@ def get_property(
|
||||
if prop.Name != name:
|
||||
continue
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
result = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
result = prop[2].wrappedValue if prop[2] else None
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
result = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
@@ -286,35 +288,36 @@ def get_property(
|
||||
def get_properties(properties: list[ifcopenshell.entity_instance], verbose=False) -> dict[str, dict[str, Any]]:
|
||||
results = {}
|
||||
for prop in properties or []:
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
results[prop.Name] = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
ifc_class = prop.is_a()
|
||||
if ifc_class == "IfcPropertySingleValue":
|
||||
results[prop[0]] = prop[2].wrappedValue if prop[2] else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyEnumeratedValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyListValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyBoundedValue"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyBoundedValue":
|
||||
data = prop.get_info()
|
||||
del data["Unit"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyTableValue"):
|
||||
results[prop.Name] = prop.get_info()
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyTableValue":
|
||||
results[prop[0]] = prop.get_info()
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcComplexProperty"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcComplexProperty":
|
||||
data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_properties(prop.HasProperties)
|
||||
del data["HasProperties"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import pathlib
|
||||
import ifcopenshell
|
||||
@@ -25,10 +26,10 @@ from ifcopenshell.entity_instance import entity_instance
|
||||
from functools import lru_cache
|
||||
from typing import List, Generator, Optional
|
||||
|
||||
templates = {}
|
||||
templates: dict[str, PsetQto] = {}
|
||||
|
||||
|
||||
def get_template(schema):
|
||||
def get_template(schema: str) -> PsetQto:
|
||||
global templates
|
||||
if schema not in templates:
|
||||
templates[schema] = PsetQto(schema)
|
||||
@@ -36,11 +37,13 @@ def get_template(schema):
|
||||
|
||||
|
||||
class PsetQto:
|
||||
# fmt: off
|
||||
templates_path = {
|
||||
"IFC2X3": "Pset_IFC2X3.ifc",
|
||||
"IFC4": "Pset_IFC4_ADD2.ifc",
|
||||
"IFC4X3": "Pset_IFC4X3.ifc"
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, schema: str, templates=None) -> None:
|
||||
self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
|
||||
|
||||
@@ -387,7 +387,30 @@ def set_element_value(
|
||||
|
||||
if isinstance(key, str) and hasattr(element, key):
|
||||
if getattr(element, key) != value:
|
||||
return setattr(element, key, value)
|
||||
try:
|
||||
# Try our luck
|
||||
return setattr(element, key, value)
|
||||
except:
|
||||
# Try to cast
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(
|
||||
element.wrapped_data.declaration()
|
||||
.as_entity()
|
||||
.attribute_by_index(element.wrapped_data.get_argument_index(key))
|
||||
)
|
||||
if data_type == "string":
|
||||
value = str(value)
|
||||
elif data_type == "float":
|
||||
value = float(value)
|
||||
elif data_type == "integer":
|
||||
value = int(value)
|
||||
elif data_type == "boolean":
|
||||
if value in ("True", "true", "TRUE", "Yes", "1"):
|
||||
value = True
|
||||
elif value in ("False", "false", "FALSE", "No", "0"):
|
||||
value = True
|
||||
else:
|
||||
value = bool(value)
|
||||
return setattr(element, key, value)
|
||||
else:
|
||||
# Try to extract pset
|
||||
if isinstance(key, re.Pattern):
|
||||
@@ -674,31 +697,37 @@ class FacetTransformer(lark.Transformer):
|
||||
if isinstance(element_value, (list, tuple)):
|
||||
return any(self.compare(ev, comparison, value) for ev in element_value)
|
||||
elif isinstance(value, str):
|
||||
if isinstance(element_value, int):
|
||||
value = int(value)
|
||||
elif isinstance(element_value, float):
|
||||
value = float(value)
|
||||
try:
|
||||
if isinstance(element_value, int):
|
||||
value = int(value)
|
||||
elif isinstance(element_value, float):
|
||||
value = float(value)
|
||||
|
||||
if isinstance(element_value, (int, float)):
|
||||
operator = comparison.lstrip("!")
|
||||
if operator == ">=":
|
||||
result = element_value >= value
|
||||
elif operator == "<=":
|
||||
result = element_value <= value
|
||||
elif operator == ">":
|
||||
result = element_value > value
|
||||
elif operator == "<":
|
||||
result = element_value < value
|
||||
else:
|
||||
result = element_value == value # Tolerance?
|
||||
elif isinstance(element_value, str):
|
||||
operator = comparison.lstrip("!")
|
||||
if operator == "*=":
|
||||
result = value in element_value
|
||||
if isinstance(element_value, (int, float)):
|
||||
operator = comparison.lstrip("!")
|
||||
if operator == ">=":
|
||||
result = element_value >= value
|
||||
elif operator == "<=":
|
||||
result = element_value <= value
|
||||
elif operator == ">":
|
||||
result = element_value > value
|
||||
elif operator == "<":
|
||||
result = element_value < value
|
||||
else:
|
||||
result = element_value == value # Tolerance?
|
||||
elif isinstance(element_value, str):
|
||||
operator = comparison.lstrip("!")
|
||||
if operator == "*=":
|
||||
result = value in element_value
|
||||
else:
|
||||
result = element_value == value
|
||||
else:
|
||||
result = element_value == value
|
||||
else:
|
||||
result = element_value == value
|
||||
except:
|
||||
# Potentially they are trying to compare a value which cannot
|
||||
# be legally casted to the element_value, or cannot use the
|
||||
# `in` or more / less than comparison operators.
|
||||
result = False
|
||||
elif isinstance(value, re.Pattern):
|
||||
result = bool(value.match(element_value)) if element_value is not None else False
|
||||
elif value in (None, True, False):
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import datetime
|
||||
import ifcopenshell.util.date
|
||||
from ifcopenshell.util.data import WorkTimeDatesInterface
|
||||
from math import floor
|
||||
from functools import lru_cache
|
||||
from collections import namedtuple
|
||||
@@ -182,15 +181,14 @@ def is_day_in_work_time(day, work_time):
|
||||
is_day_in_work_time = True
|
||||
if isinstance(day, datetime.datetime):
|
||||
day = datetime.date(day.year, day.month, day.day)
|
||||
work_time_dates = WorkTimeDatesInterface(work_time)
|
||||
if work_time_dates.Start:
|
||||
start = ifcopenshell.util.date.ifc2datetime(work_time_dates.Start)
|
||||
if work_time[4]:
|
||||
start = ifcopenshell.util.date.ifc2datetime(work_time[4])
|
||||
if day > start:
|
||||
is_day_in_work_time = True
|
||||
else:
|
||||
is_day_in_work_time = False
|
||||
if work_time_dates.Finish:
|
||||
finish = ifcopenshell.util.date.ifc2datetime(work_time_dates.Finish)
|
||||
if work_time[5]:
|
||||
finish = ifcopenshell.util.date.ifc2datetime(work_time[5])
|
||||
if day < finish:
|
||||
is_day_in_work_time = True
|
||||
else:
|
||||
@@ -207,17 +205,16 @@ def is_work_time_applicable_to_day(work_time, day):
|
||||
if isinstance(day, datetime.datetime):
|
||||
day = datetime.date(day.year, day.month, day.day)
|
||||
recurrence = work_time.RecurrencePattern
|
||||
work_time_dates = WorkTimeDatesInterface(work_time)
|
||||
if recurrence.RecurrenceType == "DAILY":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return True
|
||||
if not work_time_dates.Start:
|
||||
if not work_time[4]:
|
||||
return False
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "WEEKLY":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return (day.weekday() + 1) in recurrence.WeekdayComponent
|
||||
if not work_time_dates.Start:
|
||||
if not work_time[4]:
|
||||
return False
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH":
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from fractions import Fraction
|
||||
from math import pi
|
||||
from typing import Tuple, Iterable, Any, Union, Literal
|
||||
from typing import Tuple, Iterable, Any, Union, Literal, Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
@@ -525,7 +525,7 @@ def convert_unit(value, from_unit, to_unit):
|
||||
)
|
||||
|
||||
|
||||
def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float:
|
||||
"""Converts between length, area, and volume units
|
||||
|
||||
In this case, you manually specify the names and (optionally) prefixes to
|
||||
@@ -534,12 +534,12 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
|
||||
:param value: The numeric value you want to convert
|
||||
:type value: float
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type from_prefix: str,optional
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type from_prefix: str, optional
|
||||
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type from_unit: str
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type to_prefix: str,optional
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type to_prefix: str, optional
|
||||
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type to_unit: str
|
||||
:return: The converted value.
|
||||
@@ -566,7 +566,7 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
return value
|
||||
|
||||
|
||||
def calculate_unit_scale(ifc_file, unit_type="LENGTHUNIT"):
|
||||
def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float:
|
||||
"""Returns a unit scale factor to convert to and from IFC project units and SI units.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -26,8 +26,11 @@ import json
|
||||
import functools
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import Union, Iterator, Any, Optional
|
||||
from logging import Logger
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.ifcopenshell_wrapper
|
||||
import ifcopenshell.express.rule_executor
|
||||
|
||||
named_type = ifcopenshell.ifcopenshell_wrapper.named_type
|
||||
@@ -38,6 +41,10 @@ enumeration_type = ifcopenshell.ifcopenshell_wrapper.enumeration_type
|
||||
entity_type = ifcopenshell.ifcopenshell_wrapper.entity
|
||||
select_type = ifcopenshell.ifcopenshell_wrapper.select_type
|
||||
attribute = ifcopenshell.ifcopenshell_wrapper.attribute
|
||||
inverse_attribute = ifcopenshell.ifcopenshell_wrapper.inverse_attribute
|
||||
schema_definition = ifcopenshell.ifcopenshell_wrapper.schema_definition
|
||||
|
||||
attribute_types = Union[simple_type, named_type, enumeration_type, select_type, aggregation_type, type_declaration]
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
@@ -76,8 +83,8 @@ simple_type_python_mapping = {
|
||||
}
|
||||
|
||||
|
||||
def annotate_inst_attr_pos(inst, pos):
|
||||
def get_pos():
|
||||
def annotate_inst_attr_pos(inst: ifcopenshell.entity_instance, pos: int) -> str:
|
||||
def get_pos() -> Iterator[int]:
|
||||
depth = 0
|
||||
idx = -1
|
||||
for c in str(inst):
|
||||
@@ -104,23 +111,24 @@ def annotate_inst_attr_pos(inst, pos):
|
||||
return "".join(" ^"[i == pos] for i in get_pos())
|
||||
|
||||
|
||||
def format(val):
|
||||
def format(val: Any) -> str:
|
||||
if isinstance(val, tuple) and val and isinstance(val[0], ifcopenshell.entity_instance):
|
||||
return "[\n%s\n ]" % "\n".join(" {}. {}".format(*x) for x in enumerate(val, start=1))
|
||||
else:
|
||||
return repr(val)
|
||||
|
||||
|
||||
def assert_valid_inverse(attr, val, schema):
|
||||
def assert_valid_inverse(
|
||||
attr: inverse_attribute, val: tuple[ifcopenshell.entity_instance], schema: schema_definition
|
||||
) -> bool:
|
||||
b1, b2 = attr.bound1(), attr.bound2()
|
||||
|
||||
|
||||
if (b1, b2) == (-1, -1):
|
||||
invalid = len(val) != 1
|
||||
else:
|
||||
invalid = len(val) < b1 or (b2 != -1 and len(val) > b2)
|
||||
|
||||
if invalid:
|
||||
|
||||
if invalid:
|
||||
ent_ref = attr.entity_reference().name()
|
||||
attr_ref = attr.attribute_reference().name()
|
||||
aggr = attr.type_of_aggregation_string().upper()
|
||||
@@ -128,24 +136,26 @@ def assert_valid_inverse(attr, val, schema):
|
||||
if aggr:
|
||||
aggr_str = f'{aggr} [{b1}:{"?" if b2 == -1 else b2}] OF '
|
||||
else:
|
||||
aggr_str = ''
|
||||
aggr_str = ""
|
||||
|
||||
attr_formatted = f"{attr.name()} : {aggr_str}{ent_ref} FOR {attr_ref}"
|
||||
|
||||
raise ValidationError(f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n", attr.name())
|
||||
raise ValidationError(
|
||||
f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n", attr.name()
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
select_members_cache = {}
|
||||
select_members_cache: dict[tuple[str, str], set[str]] = {}
|
||||
|
||||
|
||||
def get_select_members(schema, ty):
|
||||
def get_select_members(schema: schema_definition, ty: select_type) -> set[str]:
|
||||
cache_key = schema.name(), ty.name()
|
||||
from_cache = select_members_cache.get(cache_key)
|
||||
if from_cache:
|
||||
return from_cache
|
||||
|
||||
def inner(ty):
|
||||
def inner(ty: select_type) -> Iterator[str]:
|
||||
if isinstance(ty, select_type):
|
||||
for st in ty.select_list():
|
||||
yield from inner(st)
|
||||
@@ -166,7 +176,13 @@ def get_select_members(schema, ty):
|
||||
return v
|
||||
|
||||
|
||||
def assert_valid(attr_type, val, schema, no_throw=False, attr=None):
|
||||
def assert_valid(
|
||||
attr_type: attribute_types,
|
||||
val: Any,
|
||||
schema: schema_definition,
|
||||
no_throw=False,
|
||||
attr: Optional[attribute] = None,
|
||||
):
|
||||
type_wrappers = (named_type,)
|
||||
if not isinstance(val, ifcopenshell.entity_instance):
|
||||
# If val is not an entity instance we need to
|
||||
@@ -209,19 +225,26 @@ def assert_valid(attr_type, val, schema, no_throw=False, attr=None):
|
||||
elif isinstance(attr_type, aggregation_type):
|
||||
b1, b2 = attr_type.bound1(), attr_type.bound2()
|
||||
ty = attr_type.type_of_element()
|
||||
invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) or not all(assert_valid(ty, v, schema, attr=attr) for v in val)
|
||||
invalid = (
|
||||
len(val) < b1
|
||||
or (b2 != -1 and len(val) > b2)
|
||||
or not all(assert_valid(ty, v, schema, attr=attr) for v in val)
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("Not impl %s %s" % (type(attr_type), attr_type))
|
||||
|
||||
if no_throw:
|
||||
return not invalid
|
||||
elif invalid:
|
||||
raise ValidationError(f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n", *([attr.name()] if attr else []))
|
||||
raise ValidationError(
|
||||
f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n",
|
||||
*([attr.name()] if attr else []),
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def log_internal_cpp_errors(filename, logger):
|
||||
def log_internal_cpp_errors(filename: str, logger: Logger) -> None:
|
||||
import re
|
||||
import bisect
|
||||
|
||||
@@ -231,7 +254,6 @@ def log_internal_cpp_errors(filename, logger):
|
||||
msgs = list(map(json.loads, filter(None, log.split("\n"))))
|
||||
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
|
||||
if chr_offsets:
|
||||
|
||||
# The file is opened in binary mode, in order
|
||||
# to correspond with the offsets reported by
|
||||
# IfcOpenShell C++
|
||||
@@ -246,17 +268,17 @@ def log_internal_cpp_errors(filename, logger):
|
||||
m = chr_offset_re.sub("", msg["message"])
|
||||
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('instance', line)
|
||||
logger.set_state('attribute', None)
|
||||
logger.set_state("instance", line)
|
||||
logger.set_state("attribute", None)
|
||||
logger.error("%s:\n\n%s" % (m, line))
|
||||
else:
|
||||
logger.error("For instance:\n %s\n%s", line, m)
|
||||
|
||||
|
||||
entity_attribute_map = {}
|
||||
entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute]]] = {}
|
||||
|
||||
|
||||
def get_entity_attributes(schema, entity):
|
||||
def get_entity_attributes(schema: schema_definition, entity: str) -> tuple[entity_type, tuple[attribute]]:
|
||||
cache_key = schema.name(), entity
|
||||
from_cache = entity_attribute_map.get(cache_key)
|
||||
if from_cache:
|
||||
@@ -271,7 +293,7 @@ def get_entity_attributes(schema, entity):
|
||||
return entity_attrs
|
||||
|
||||
|
||||
def validate(f, logger, express_rules=False):
|
||||
def validate(f: Union[ifcopenshell.file, str], logger: Logger, express_rules=False) -> None:
|
||||
"""
|
||||
For an IFC population model `f` (or filepath to such a file) validate whether the entity attribute values are correctly supplied. As this
|
||||
is a function that is applied after a file has been parsed, certain types of errors in syntax, duplicate
|
||||
@@ -300,11 +322,10 @@ def validate(f, logger, express_rules=False):
|
||||
|
||||
filename = None
|
||||
|
||||
if hasattr(logger, 'set_state'):
|
||||
logger.set_state('type', 'schema')
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state("type", "schema")
|
||||
|
||||
if not isinstance(f, ifcopenshell.file):
|
||||
|
||||
# get_log() clears log existing output
|
||||
ifcopenshell.get_log()
|
||||
# @todo restore log format
|
||||
@@ -314,31 +335,49 @@ def validate(f, logger, express_rules=False):
|
||||
try:
|
||||
f = ifcopenshell.open(f)
|
||||
except ifcopenshell.SchemaError as e:
|
||||
current_dir_files = {fn.lower(): fn for fn in os.listdir('.')}
|
||||
schema_name = str(e).split(' ')[-1].lower()
|
||||
exists = current_dir_files.get(schema_name + '.exp')
|
||||
current_dir_files = {fn.lower(): fn for fn in os.listdir(".")}
|
||||
schema_name = str(e).split(" ")[-1].lower()
|
||||
exists = current_dir_files.get(schema_name + ".exp")
|
||||
if exists:
|
||||
schema = ifcopenshell.express.parse(exists)
|
||||
ifcopenshell.register_schema(schema)
|
||||
|
||||
f = ifcopenshell.open(f)
|
||||
else:
|
||||
logger.error(f'Unsupported schema: {schema_name}')
|
||||
logger.error(f"Unsupported schema: {schema_name}")
|
||||
return
|
||||
|
||||
log_internal_cpp_errors(filename, logger)
|
||||
|
||||
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema_identifier)
|
||||
used_guids: dict[str, ifcopenshell.entity_instance] = dict()
|
||||
|
||||
for inst in f:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('instance', inst)
|
||||
logger.set_state("instance", inst)
|
||||
|
||||
if hasattr(inst, "GlobalId"):
|
||||
guid = inst.GlobalId
|
||||
if guid is not None and guid in used_guids:
|
||||
rule = "Rule IfcRoot.UR1:\n The attribute GlobalId should be unique"
|
||||
previous_element = used_guids[guid]
|
||||
logger.error(
|
||||
"On instance:\n %s\n %s\n%s\nViolated by:\n %s\n %s",
|
||||
inst,
|
||||
annotate_inst_attr_pos(inst, 0),
|
||||
rule,
|
||||
previous_element,
|
||||
annotate_inst_attr_pos(previous_element, 0),
|
||||
)
|
||||
else:
|
||||
used_guids[guid] = inst
|
||||
|
||||
entity, attrs = get_entity_attributes(schema, inst.is_a())
|
||||
|
||||
if entity.is_abstract():
|
||||
e = "Entity %s is abstract" % entity.name()
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', None)
|
||||
logger.set_state("attribute", None)
|
||||
logger.error(e)
|
||||
else:
|
||||
logger.error("For instance:\n %s\n%s", inst, e)
|
||||
@@ -351,7 +390,7 @@ def validate(f, logger, express_rules=False):
|
||||
pass
|
||||
except:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', f"{entity.name()}.{attrs[i].name()}")
|
||||
logger.set_state("attribute", f"{entity.name()}.{attrs[i].name()}")
|
||||
logger.error("Invalid attribute value")
|
||||
else:
|
||||
logger.error(
|
||||
@@ -365,10 +404,9 @@ def validate(f, logger, express_rules=False):
|
||||
|
||||
if not has_invalid_value:
|
||||
for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())):
|
||||
|
||||
if is_derived and not isinstance(val, ifcopenshell.ifcopenshell_wrapper.attribute_value_derived):
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||
logger.set_state("attribute", f"{entity.name()}.{attr.name()}")
|
||||
logger.error("Attribute is derived in subtype")
|
||||
else:
|
||||
logger.error(
|
||||
@@ -380,7 +418,7 @@ def validate(f, logger, express_rules=False):
|
||||
|
||||
if val is None and not attr.optional() and not is_derived:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||
logger.set_state("attribute", f"{entity.name()}.{attr.name()}")
|
||||
logger.error("Attribute not optional")
|
||||
else:
|
||||
logger.error(
|
||||
@@ -396,7 +434,7 @@ def validate(f, logger, express_rules=False):
|
||||
assert_valid(attr_type, val, schema, attr=attr)
|
||||
except ValidationError as e:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', e.attribute)
|
||||
logger.set_state("attribute", e.attribute)
|
||||
logger.error(str(e))
|
||||
else:
|
||||
logger.error(
|
||||
@@ -411,7 +449,7 @@ def validate(f, logger, express_rules=False):
|
||||
val = getattr(inst, attr.name())
|
||||
except Exception as e:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||
logger.set_state("attribute", f"{entity.name()}.{attr.name()}")
|
||||
logger.error(str(e))
|
||||
else:
|
||||
logger.error("For instance:\n %s\n%s", inst, e)
|
||||
@@ -420,7 +458,7 @@ def validate(f, logger, express_rules=False):
|
||||
assert_valid_inverse(attr, val, schema)
|
||||
except ValidationError as e:
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||
logger.set_state("attribute", f"{entity.name()}.{attr.name()}")
|
||||
logger.error(str(e))
|
||||
else:
|
||||
logger.error("For instance:\n %s\n%s", inst, e)
|
||||
@@ -437,9 +475,9 @@ def validate(f, logger, express_rules=False):
|
||||
ifcopenshell.ifcopenshell_wrapper.set_feature("use_attribute_value_derived", attribute_value_derived_org)
|
||||
|
||||
if express_rules:
|
||||
if hasattr(logger, 'set_state'):
|
||||
logger.set_state('instance', None)
|
||||
logger.set_state('attribute', None)
|
||||
if hasattr(logger, "set_state"):
|
||||
logger.set_state("instance", None)
|
||||
logger.set_state("attribute", None)
|
||||
ifcopenshell.express.rule_executor.run(f, logger)
|
||||
|
||||
|
||||
@@ -461,14 +499,16 @@ if __name__ == "__main__":
|
||||
validate(fn, logger, "--rules" in flags)
|
||||
|
||||
if "--json" in flags:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
conv = str
|
||||
if "--spf" in flags:
|
||||
conv = lambda x: x.to_string() if isinstance(x, ifcopenshell.entity_instance) else str(x)
|
||||
if "--fields" in flags:
|
||||
|
||||
def conv(x):
|
||||
if isinstance(x, ifcopenshell.entity_instance):
|
||||
return x.get_info(scalar_only=True)
|
||||
else:
|
||||
return str(x)
|
||||
|
||||
print("\n".join(json.dumps(x, default=conv) for x in logger.statements))
|
||||
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
geometry = ["mathutils", "shapely"]
|
||||
date = ["isodate"]
|
||||
date = ["isodate", "dateutil"]
|
||||
[project.urls]
|
||||
"Homepage" = "http://ifcopenshell.org"
|
||||
"Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues"
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import datetime
|
||||
import test.bootstrap
|
||||
import ifcopenshell.api
|
||||
from ifcopenshell.util.data import WorkTimeDatesInterface
|
||||
|
||||
|
||||
class TestEditWorkTime(test.bootstrap.IFC4):
|
||||
@@ -35,13 +34,12 @@ class TestEditWorkTime(test.bootstrap.IFC4):
|
||||
"Finish": datetime.datetime(2020, 2, 1),
|
||||
}
|
||||
ifcopenshell.api.run("sequence.edit_work_time", self.file, work_time=work_time, attributes=attributes)
|
||||
worktime_dates = WorkTimeDatesInterface(work_time)
|
||||
assert work_time.Name == attributes["Name"]
|
||||
assert work_time.DataOrigin == attributes["DataOrigin"]
|
||||
assert work_time.UserDefinedDataOrigin == attributes["UserDefinedDataOrigin"]
|
||||
assert work_time.RecurrencePattern == attributes["RecurrencePattern"]
|
||||
assert worktime_dates.Start == "2020-01-01"
|
||||
assert worktime_dates.Finish == "2020-02-01"
|
||||
assert work_time.Start == "2020-01-01"
|
||||
assert work_time.Finish == "2020-02-01"
|
||||
|
||||
|
||||
class TestEditWorkTimeIFC4X3(test.bootstrap.IFC4X3):
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
||||
FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03','');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$);
|
||||
#2=IFCTELECOMADDRESS(.USERDEFINED.,$,'WEBPAGE',$,$,$,$,'https://ifcopenshell.org');
|
||||
#3=IFCORGANIZATION('IfcOpenShell','IfcOpenShell','IfcOpenShell is an open source software library that helps users and software developers to work with IFC data.',(#1),(#2));
|
||||
#4=IFCAPPLICATION(#3,'v0.7.0-e38eafd03','IfcOpenShell','IfcOpenShell');
|
||||
#5=IFCPERSON('HSeldon','Seldon','Hari',$,$,$,$,$);
|
||||
#6=IFCORGANIZATION('APTR','Aperture Science',$,$,$);
|
||||
#7=IFCPERSONANDORGANIZATION(#5,#6,$);
|
||||
#8=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210);
|
||||
#9=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',#8,$,$,$,$,$,$,$);
|
||||
#10=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210);
|
||||
#11=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',#10,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
||||
FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03','');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$);
|
||||
#2=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user