mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8338cc72db | |||
| 67b636f5fb | |||
| 3a754c8ecc | |||
| d64d7cf27a | |||
| 6ff4c95ac7 | |||
| 0b6af5f848 | |||
| 3ded4dff5f | |||
| 452631b997 | |||
| c041dd13ad | |||
| 994ccb94f4 | |||
| c7ba643ef7 | |||
| 0b6f3113a7 | |||
| 730efcae49 | |||
| 62b297757e | |||
| 0533a00631 | |||
| 7fe2e93dea | |||
| acc9bd736f | |||
| 478612b6ae | |||
| a24d683613 | |||
| 0b5d7cdbf3 | |||
| b8e20ec1a5 | |||
| 1cd243f13c | |||
| 0a2adbb632 | |||
| 8882c99455 | |||
| 7890ded698 | |||
| 7d2d9b2bcf | |||
| 75ae501ff9 | |||
| 880fbe9533 | |||
| c2684f4fbf | |||
| afdc1e6679 | |||
| 8935a17844 | |||
| 93c59e81c6 | |||
| d423274e62 | |||
| 7958dd837b | |||
| e42a638220 |
@@ -280,7 +280,7 @@ ENDIF()
|
||||
# Use the found libTKernel as a template for all other OCC libraries
|
||||
# TODO Extract this into macro/function
|
||||
foreach(lib ${OPENCASCADE_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convetion too.
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE TKerneld "${lib}" lib_path "${libTKernel}")
|
||||
string(REPLACE TKernel "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCASCADE_LIBRARIES "${lib_path}")
|
||||
@@ -355,7 +355,7 @@ IF(COLLADA_SUPPORT AND BUILD_CONVERT)
|
||||
|
||||
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
|
||||
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convetion too.
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
|
||||
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
|
||||
|
||||
@@ -450,7 +450,7 @@ class BcfXml:
|
||||
for bitmap in viewpoint.bitmaps:
|
||||
bitmap_el = self._create_element(parent, "Bitmap")
|
||||
|
||||
text_map = {"Bitmap": bitmap.bitmap_type, "Reference": bitmap.reference}
|
||||
text_map = {"Bitmap": bitmap.bitmap_format, "Reference": bitmap.reference}
|
||||
for key, value in text_map.items():
|
||||
self._create_element(bitmap_el, key, text=value)
|
||||
|
||||
@@ -718,7 +718,7 @@ class BcfXml:
|
||||
for item in visinfo["Bitmap"]:
|
||||
bitmap = bcf.v2.data.Bitmap()
|
||||
bitmap.reference = item["Reference"]
|
||||
bitmap.bitmap_type = item["Bitmap"].upper()
|
||||
bitmap.bitmap_format = item["Bitmap"].upper()
|
||||
self.set_vector(bitmap.location, item["Location"])
|
||||
self.set_vector(bitmap.normal, item["Normal"])
|
||||
self.set_vector(bitmap.up, item["Up"])
|
||||
|
||||
@@ -157,7 +157,7 @@ class Bitmap:
|
||||
def __init__(self):
|
||||
self.reference = "" # Only in BCF-XML
|
||||
self.bitmap_data = None # Only in BCF-API
|
||||
self.bitmap_type = "PNG" # Enum of png or jpg
|
||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
||||
self.location = Point()
|
||||
self.normal = Direction()
|
||||
self.up = Direction()
|
||||
|
||||
+21
-21
@@ -479,22 +479,22 @@ class BcfXml:
|
||||
def write_viewpoint_bitmaps(self, viewpoint, parent):
|
||||
if not viewpoint.bitmaps:
|
||||
return
|
||||
if viewpoint.bitmaps:
|
||||
bitmaps_parent = self._create_element(parent, "Bitmaps")
|
||||
for bitmap in viewpoint.bitmaps:
|
||||
bitmap_el = self._create_element(bitmaps_parent, "Bitmap")
|
||||
text_map = {"Format": bitmap.bitmap_type, "Reference": bitmap.reference}
|
||||
for key, value in text_map.items():
|
||||
self._create_element(bitmap_el, key, text=value)
|
||||
bitmaps_el = self._create_element(parent, "Bitmaps")
|
||||
for bitmap in viewpoint.bitmaps:
|
||||
bitmap_el = self._create_element(bitmaps_el, "Bitmap")
|
||||
|
||||
location_el = self._create_element(bitmap_el, "Location")
|
||||
self.write_vector(location_el, bitmap.location)
|
||||
normal_el = self._create_element(bitmap_el, "Normal")
|
||||
self.write_vector(normal_el, bitmap.normal)
|
||||
up_el = self._create_element(bitmap_el, "Up")
|
||||
self.write_vector(up_el, bitmap.up)
|
||||
text_map = {"Format": bitmap.bitmap_format, "Reference": bitmap.reference}
|
||||
for key, value in text_map.items():
|
||||
self._create_element(bitmap_el, key, text=value)
|
||||
|
||||
self._create_element(bitmap_el, "Height", text=bitmap.height)
|
||||
location_el = self._create_element(bitmap_el, "Location")
|
||||
self.write_vector(location_el, bitmap.location)
|
||||
normal_el = self._create_element(bitmap_el, "Normal")
|
||||
self.write_vector(normal_el, bitmap.normal)
|
||||
up_el = self._create_element(bitmap_el, "Up")
|
||||
self.write_vector(up_el, bitmap.up)
|
||||
|
||||
self._create_element(bitmap_el, "Height", text=bitmap.height)
|
||||
|
||||
def write_vector(self, parent, from_obj):
|
||||
self._create_element(parent, "X", text=from_obj.x)
|
||||
@@ -691,7 +691,7 @@ class BcfXml:
|
||||
components = bcf.v3.data.Components()
|
||||
data = visinfo["Components"]
|
||||
if "Selection" in data and "Component" in data["Selection"]:
|
||||
for item in data["Selection"]["Component"]:
|
||||
for item in data["Selection"].get("Component", []):
|
||||
components.selection.append(self.get_component(item))
|
||||
if "Visibility" in data:
|
||||
component_visibility = bcf.v3.data.ComponentVisibility()
|
||||
@@ -730,7 +730,7 @@ class BcfXml:
|
||||
self.set_vector(camera.camera_direction, data["CameraDirection"])
|
||||
self.set_vector(camera.camera_up_vector, data["CameraUpVector"])
|
||||
camera.view_to_world_scale = data["ViewToWorldScale"]
|
||||
camera.aspect_ration = data["AspectRatio"]
|
||||
camera.aspect_ratio = data["AspectRatio"]
|
||||
return camera
|
||||
|
||||
def get_viewpoint_perspective_camera(self, visinfo):
|
||||
@@ -742,14 +742,14 @@ class BcfXml:
|
||||
self.set_vector(camera.camera_direction, data["CameraDirection"])
|
||||
self.set_vector(camera.camera_up_vector, data["CameraUpVector"])
|
||||
camera.field_of_view = data["FieldOfView"]
|
||||
camera.aspect_ration = data["AspectRatio"]
|
||||
camera.aspect_ratio = data["AspectRatio"]
|
||||
return camera
|
||||
|
||||
def get_viewpoint_lines(self, visinfo):
|
||||
if "Lines" not in visinfo:
|
||||
return []
|
||||
lines = []
|
||||
for item in visinfo["Lines"]["Line"]:
|
||||
for item in visinfo["Lines"].get("Line", []):
|
||||
line = bcf.v3.data.Line()
|
||||
self.set_vector(line.start_point, item["StartPoint"])
|
||||
self.set_vector(line.end_point, item["EndPoint"])
|
||||
@@ -768,13 +768,13 @@ class BcfXml:
|
||||
return planes
|
||||
|
||||
def get_viewpoint_bitmaps(self, visinfo):
|
||||
if "Bitmap" not in visinfo:
|
||||
if "Bitmaps" not in visinfo:
|
||||
return []
|
||||
bitmaps = []
|
||||
for item in visinfo["Bitmaps"]["Bitmap"]:
|
||||
for item in visinfo["Bitmaps"].get("Bitmap"):
|
||||
bitmap = bcf.v3.data.Bitmap()
|
||||
bitmap.reference = item["Reference"]
|
||||
bitmap.bitmap_type = item["Format"].upper()
|
||||
bitmap.bitmap_format = item["Format"].upper()
|
||||
self.set_vector(bitmap.location, item["Location"])
|
||||
self.set_vector(bitmap.normal, item["Normal"])
|
||||
self.set_vector(bitmap.up, item["Up"])
|
||||
|
||||
@@ -159,7 +159,7 @@ class Bitmap:
|
||||
def __init__(self):
|
||||
self.reference = "" # Only in BCF-XML
|
||||
self.bitmap_data = None # Only in BCF-API
|
||||
self.bitmap_type = "PNG" # Enum of png or jpg
|
||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
||||
self.location = Point()
|
||||
self.normal = Direction()
|
||||
self.up = Direction()
|
||||
|
||||
+17
-3
@@ -13,10 +13,10 @@ endif
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
ifeq ($(PYVERSION), py37)
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-ff7219b-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-81ad689-$(PLATFORM)64.zip
|
||||
endif
|
||||
ifeq ($(PYVERSION), py39)
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-ff7219b-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-81ad689-$(PLATFORM)64.zip
|
||||
endif
|
||||
cd dist/working && unzip ifcblender*
|
||||
cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
@@ -28,7 +28,7 @@ endif
|
||||
|
||||
# Provides IfcConvert for construction documentation
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-517b819-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-81ad689-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip IfcConvert*
|
||||
ifeq ($(PLATFORM), win)
|
||||
cp -r dist/working/IfcConvert.exe dist/blenderbim/libs/
|
||||
@@ -92,6 +92,20 @@ endif
|
||||
cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides networkx graph analysis for project dependency calculations
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz
|
||||
cd dist/working && tar -xzvf networkx*
|
||||
cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by networkx
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz
|
||||
cd dist/working && tar -xzvf decorator*
|
||||
cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides jsgantt-improved supports for web-based construction sequencing gantt charts
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
|
||||
|
||||
@@ -31,6 +31,7 @@ if bpy is not None:
|
||||
"sequence": None,
|
||||
"group": None,
|
||||
"structural": None,
|
||||
"boundary": None,
|
||||
"material": None,
|
||||
"style": None,
|
||||
"layer": None,
|
||||
@@ -124,6 +125,7 @@ if bpy is not None:
|
||||
bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
|
||||
|
||||
for module in modules.values():
|
||||
@@ -145,6 +147,7 @@ if bpy is not None:
|
||||
del bpy.types.Mesh.BIMMeshProperties
|
||||
del bpy.types.Curve.BIMMeshProperties
|
||||
del bpy.types.Camera.BIMMeshProperties
|
||||
del bpy.types.PointLight.BIMMeshProperties
|
||||
bpy.types.SCENE_PT_unit.remove(ui.ifc_units)
|
||||
|
||||
for module in reversed(list(modules.values())):
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
<script type="text/javascript">
|
||||
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
|
||||
g.setOptions({
|
||||
vCaptionType: 'Complete', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vQuarterColWidth: 36,
|
||||
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
|
||||
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
|
||||
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
|
||||
vLang: 'en',
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vTotalHeight: 1000,
|
||||
});
|
||||
var json_data = `
|
||||
{{{json_data}}}
|
||||
`;
|
||||
|
||||
@@ -49,7 +49,6 @@ class IfcExporter:
|
||||
json.dump(jsonData, outfile, indent=None if self.ifc_export_settings.json_compact else 4)
|
||||
|
||||
def set_header(self):
|
||||
# TODO: add all metadata, pending bug #747
|
||||
self.file.wrapped_data.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
||||
self.file.wrapped_data.header.file_name.time_stamp = (
|
||||
datetime.datetime.utcnow()
|
||||
@@ -62,16 +61,6 @@ class IfcExporter:
|
||||
self.file.wrapped_data.header.file_name.originating_system = "{} {}".format(
|
||||
self.get_application_name(), self.get_application_version()
|
||||
)
|
||||
# TODO: reimplement. See #1222.
|
||||
# if self.owner_history:
|
||||
# if self.schema_version == "IFC2X3":
|
||||
# self.file.wrapped_data.header.file_name.authorization = self.owner_history.OwningUser.ThePerson.Id
|
||||
# else:
|
||||
# self.file.wrapped_data.header.file_name.authorization = (
|
||||
# self.owner_history.OwningUser.ThePerson.Identification
|
||||
# )
|
||||
# else:
|
||||
# self.file.wrapped_data.header.file_name.authorization = "Nobody"
|
||||
|
||||
def sync_object_placements_and_deletions(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
@@ -55,6 +55,15 @@ def name_callback(obj, data):
|
||||
AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
|
||||
def active_object_callback():
|
||||
obj = bpy.context.active_object
|
||||
for obj in bpy.context.selected_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
if IfcStore.id_map[obj.BIMObjectProperties.ifc_definition_id] != obj:
|
||||
bpy.ops.bim.copy_class(obj=obj.name)
|
||||
|
||||
|
||||
def subscribe_to(object, data_path, callback):
|
||||
subscribe_to = object.path_resolve(data_path, False)
|
||||
bpy.msgbus.subscribe_rna(
|
||||
@@ -176,15 +185,6 @@ def create_application_organisation(ifc):
|
||||
)
|
||||
|
||||
|
||||
def active_object_callback():
|
||||
obj = bpy.context.active_object
|
||||
for obj in bpy.context.selected_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
if IfcStore.id_map[obj.BIMObjectProperties.ifc_definition_id] != obj:
|
||||
bpy.ops.bim.copy_class(obj=obj.name)
|
||||
|
||||
|
||||
@persistent
|
||||
def setDefaultProperties(scene):
|
||||
global global_subscription_owner
|
||||
|
||||
@@ -14,6 +14,7 @@ class IfcStore:
|
||||
pset_template_file = None
|
||||
library_path = ""
|
||||
library_file = None
|
||||
element_listeners = set()
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
@@ -47,6 +48,23 @@ class IfcStore:
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema)
|
||||
return IfcStore.schema
|
||||
|
||||
@staticmethod
|
||||
def get_element(id_or_guid):
|
||||
if isinstance(id_or_guid, int):
|
||||
map_object = IfcStore.id_map
|
||||
else:
|
||||
map_object = IfcStore.guid_map
|
||||
try:
|
||||
obj = map_object[id_or_guid]
|
||||
obj.type # In case the object has been deleted, this triggers an exception
|
||||
except:
|
||||
return
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def add_element_listener(callback):
|
||||
IfcStore.element_listeners.add(callback)
|
||||
|
||||
@staticmethod
|
||||
def link_element(element, obj):
|
||||
IfcStore.id_map[element.id()] = obj
|
||||
@@ -55,6 +73,8 @@ class IfcStore:
|
||||
obj.BIMObjectProperties.ifc_definition_id = element.id()
|
||||
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
|
||||
blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback)
|
||||
for listener in IfcStore.element_listeners:
|
||||
listener(element, obj)
|
||||
|
||||
@staticmethod
|
||||
def unlink_element(element=None, obj=None):
|
||||
|
||||
@@ -23,7 +23,6 @@ from pathlib import Path
|
||||
from itertools import cycle
|
||||
from datetime import datetime
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from . import schema
|
||||
|
||||
|
||||
class FileCopy(threading.Thread):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
import blenderbim.bim.schema # refactor
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import bpy
|
||||
from . import ui
|
||||
|
||||
classes = (
|
||||
ui.BIM_PT_boundary,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
pass
|
||||
|
||||
|
||||
def unregister():
|
||||
pass
|
||||
@@ -0,0 +1,34 @@
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.boundary.data import Data
|
||||
|
||||
|
||||
class BIM_PT_boundary(Panel):
|
||||
bl_label = "IFC Space Boundaries"
|
||||
bl_idname = "BIM_PT_boundary"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if IfcStore.get_file().by_id(props.ifc_definition_id).is_a() not in ["IfcSpace", "IfcExternalSpatialElement"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
self.oprops = context.active_object.BIMObjectProperties
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
for boundary_id in Data.spaces.get(self.oprops.ifc_definition_id, []):
|
||||
boundary = Data.boundaries[boundary_id]
|
||||
row = self.layout.row()
|
||||
row.label(text=f"{boundary_id}", icon="GHOST_ENABLED")
|
||||
@@ -332,6 +332,7 @@ class EnableEditingCostItemValues(bpy.types.Operator):
|
||||
props = context.scene.BIMCostProperties
|
||||
props.active_cost_item_id = self.cost_item
|
||||
props.cost_item_editing_type = "VALUES"
|
||||
bpy.ops.bim.disable_editing_cost_item_value()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ classes = (
|
||||
prop.BIMCameraProperties,
|
||||
prop.BIMTextProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_drawings,
|
||||
ui.BIM_PT_schedules,
|
||||
ui.BIM_PT_sheets,
|
||||
|
||||
@@ -4,6 +4,7 @@ import bpy
|
||||
import json
|
||||
import time
|
||||
import bmesh
|
||||
import shutil
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import ifcopenshell.util.selector
|
||||
@@ -92,11 +93,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.profile_code("Start drawing generation process")
|
||||
self.props = context.scene.DocProperties
|
||||
self.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
|
||||
base_svg = self.ifc_to_svg(context)
|
||||
self.profile_code("Generate base layer")
|
||||
annotation_svg = self.annotation_to_svg(context)
|
||||
self.profile_code("Generate annotation layer")
|
||||
svg_path = self.combine_svgs(context, base_svg, annotation_svg)
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
self.profile_code("Generate underlay")
|
||||
linework_svg = self.generate_linework(context)
|
||||
self.profile_code("Generate linework")
|
||||
annotation_svg = self.generate_annotation(context)
|
||||
self.profile_code("Generate annotation")
|
||||
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
||||
self.profile_code("Combine SVG layers")
|
||||
open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
|
||||
print("Total Time: {:.2f}".format(time.time() - start))
|
||||
@@ -108,34 +111,121 @@ class CreateDrawing(bpy.types.Operator):
|
||||
print("{} :: {:.2f}".format(message, time.time() - self.time))
|
||||
self.time = time.time()
|
||||
|
||||
def combine_svgs(self, context, base, annotation):
|
||||
def combine_svgs(self, context, underlay, linework, annotation):
|
||||
# Hacky :)
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
|
||||
with open(svg_path, "w") as outfile:
|
||||
with open(base) as infile:
|
||||
should_skip = False
|
||||
for line in infile:
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
elif "<defs>" in line:
|
||||
should_skip = True
|
||||
continue
|
||||
elif "</style>" in line:
|
||||
should_skip = False
|
||||
continue
|
||||
elif should_skip:
|
||||
continue
|
||||
outfile.write(line)
|
||||
with open(annotation) as infile:
|
||||
for i, line in enumerate(infile):
|
||||
if i == 0 or i == 1:
|
||||
continue
|
||||
outfile.write(line)
|
||||
has_boilerplate = False
|
||||
if underlay:
|
||||
with open(underlay) as infile:
|
||||
for line in infile:
|
||||
if "<svg " in line:
|
||||
line = line.replace('">', '" xmlns:ifc="http://www.ifcopenshell.org/ns">')
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
outfile.write(line)
|
||||
shutil.copyfile(underlay[0:-4] + ".png", svg_path[0:-4] + "-underlay.png")
|
||||
has_boilerplate = True
|
||||
if linework:
|
||||
with open(linework) as infile:
|
||||
should_skip = False
|
||||
for i, line in enumerate(infile):
|
||||
if has_boilerplate and i == 0:
|
||||
continue
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
elif "<defs>" in line:
|
||||
should_skip = True
|
||||
continue
|
||||
elif "</style>" in line:
|
||||
should_skip = False
|
||||
continue
|
||||
elif should_skip:
|
||||
continue
|
||||
outfile.write(line)
|
||||
has_boilerplate = True
|
||||
if annotation:
|
||||
with open(annotation) as infile:
|
||||
for i, line in enumerate(infile):
|
||||
if has_boilerplate and i in [0, 1]:
|
||||
continue
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
outfile.write(line)
|
||||
outfile.write("</svg>")
|
||||
return svg_path
|
||||
|
||||
def ifc_to_svg(self, context):
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-base.svg")
|
||||
if os.path.isfile(svg_path) and not self.props.should_regenerate_base_layer:
|
||||
def generate_underlay(self, context):
|
||||
if not self.props.has_underlay:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg")
|
||||
bpy.context.scene.render.filepath = svg_path[0:-4] + ".png"
|
||||
drawing_style = bpy.context.scene.DocProperties.drawing_styles[
|
||||
self.camera.data.BIMCameraProperties.active_drawing_style_index
|
||||
]
|
||||
|
||||
if drawing_style.render_type == "DEFAULT":
|
||||
bpy.ops.render.render(write_still=True)
|
||||
else:
|
||||
previous_visibility = {}
|
||||
for obj in self.camera.users_collection[0].objects:
|
||||
previous_visibility[obj.name] = obj.hide_get()
|
||||
obj.hide_set(True)
|
||||
for obj in bpy.context.visible_objects:
|
||||
if (
|
||||
(not obj.data and not obj.instance_collection)
|
||||
or isinstance(obj.data, bpy.types.Camera)
|
||||
or "IfcGrid/" in obj.name
|
||||
or "IfcGridAxis/" in obj.name
|
||||
or "IfcOpeningElement/" in obj.name
|
||||
):
|
||||
previous_visibility[obj.name] = obj.hide_get()
|
||||
obj.hide_set(True)
|
||||
|
||||
space = self.get_view_3d()
|
||||
previous_shading = space.shading.type
|
||||
previous_format = bpy.context.scene.render.image_settings.file_format
|
||||
space.shading.type = "RENDERED"
|
||||
bpy.context.scene.render.image_settings.file_format = "PNG"
|
||||
bpy.ops.render.opengl(write_still=True)
|
||||
space.shading.type = previous_shading
|
||||
bpy.context.scene.render.image_settings.file_format = previous_format
|
||||
|
||||
for name, value in previous_visibility.items():
|
||||
bpy.data.objects[name].hide_set(value)
|
||||
|
||||
svg_writer = svgwriter.SvgWriter()
|
||||
if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
|
||||
human_scale, fraction = self.camera.data.BIMCameraProperties.custom_diagram_scale.split("|")
|
||||
else:
|
||||
human_scale, fraction = self.camera.data.BIMCameraProperties.diagram_scale.split("|")
|
||||
if self.camera.data.BIMCameraProperties.is_nts:
|
||||
svg_writer.human_scale = "NTS"
|
||||
else:
|
||||
svg_writer.human_scale = human_scale
|
||||
render = bpy.context.scene.render
|
||||
if self.is_landscape():
|
||||
width = self.camera.data.ortho_scale
|
||||
height = width / render.resolution_x * render.resolution_y
|
||||
else:
|
||||
height = self.camera.data.ortho_scale
|
||||
width = height / render.resolution_y * render.resolution_x
|
||||
svg_writer.output = svg_path
|
||||
svg_writer.data_dir = bpy.context.scene.BIMProperties.data_dir
|
||||
svg_writer.vector_style = drawing_style.vector_style
|
||||
svg_writer.camera = self.camera
|
||||
svg_writer.camera_width = width
|
||||
svg_writer.camera_height = height
|
||||
svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
|
||||
svg_writer.background_image = bpy.context.scene.render.filepath
|
||||
svg_writer.write("underlay")
|
||||
return svg_path
|
||||
|
||||
def generate_linework(self, context):
|
||||
if not self.props.has_linework:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-linework.svg")
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
ifcconvert_path = os.path.join(cwd, "..", "..", "..", "libs", "IfcConvert")
|
||||
subprocess.run(
|
||||
@@ -161,9 +251,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
)
|
||||
return svg_path
|
||||
|
||||
def annotation_to_svg(self, context):
|
||||
def generate_annotation(self, context):
|
||||
if not self.props.has_annotation:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-annotation.svg")
|
||||
if os.path.isfile(svg_path) and not self.props.should_regenerate_annotation_layer:
|
||||
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
|
||||
return svg_path
|
||||
|
||||
camera = self.camera
|
||||
@@ -237,12 +329,21 @@ class CreateDrawing(bpy.types.Operator):
|
||||
svg_writer.annotations["attributes"] = [a.name for a in drawing_style.attributes]
|
||||
svg_writer.annotations["annotation_objs"] = self.get_annotation(svg_writer)
|
||||
|
||||
svg_writer.write()
|
||||
svg_writer.write("annotation")
|
||||
return svg_writer.output
|
||||
|
||||
def is_landscape(self):
|
||||
return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y
|
||||
|
||||
def get_view_3d(self):
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type != "VIEW_3D":
|
||||
continue
|
||||
for space in area.spaces:
|
||||
if space.type != "VIEW_3D":
|
||||
continue
|
||||
return space
|
||||
|
||||
def get_annotation(self, svg_writer):
|
||||
results = []
|
||||
x = svg_writer.camera_width / 2
|
||||
@@ -486,8 +587,14 @@ class ActivateView(bpy.types.Operator):
|
||||
camera = bpy.context.scene.DocProperties.drawings[self.drawing_index].camera
|
||||
if not camera:
|
||||
return {"FINISHED"}
|
||||
bpy.context.scene.camera = camera
|
||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
||||
is_local_view = area.spaces[0].local_view is not None
|
||||
if is_local_view:
|
||||
bpy.ops.view3d.localview()
|
||||
bpy.context.scene.camera = camera
|
||||
bpy.ops.view3d.localview()
|
||||
else:
|
||||
bpy.context.scene.camera = camera
|
||||
area.spaces[0].region_3d.view_perspective = "CAMERA"
|
||||
views_collection = bpy.data.collections.get("Views")
|
||||
for collection in views_collection.children:
|
||||
@@ -950,9 +1057,9 @@ class RefreshDrawingList(bpy.types.Operator):
|
||||
for obj in bpy.context.scene.objects:
|
||||
if not isinstance(obj.data, bpy.types.Camera):
|
||||
continue
|
||||
if "IfcAnnotation/" in obj.name and obj.users_collection[0].name == obj.name:
|
||||
if "IfcAnnotation/" in obj.name:
|
||||
new = bpy.context.scene.DocProperties.drawings.add()
|
||||
new.name = obj.name.split("/")[1]
|
||||
new.name = "/".join(obj.name.split("/")[1:])
|
||||
new.camera = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -99,9 +99,11 @@ def updateDrawingName(self, context):
|
||||
return
|
||||
if self.camera.name == self.name:
|
||||
return
|
||||
self.camera.name = "IfcGroup/{}".format(self.name)
|
||||
self.camera.users_collection[0].name = self.camera.name
|
||||
self.name = self.camera.name.split("/")[1]
|
||||
self.camera.name = "IfcAnnotation/{}".format(self.name)
|
||||
unique_name = "/".join(self.camera.name.split("/")[1:])
|
||||
self.camera.users_collection[0].name = "IfcGroup/{}".format(unique_name)
|
||||
if self.name != unique_name:
|
||||
self.name = unique_name
|
||||
|
||||
|
||||
def refreshActiveDrawingIndex(self, context):
|
||||
@@ -198,10 +200,12 @@ class DrawingStyle(PropertyGroup):
|
||||
|
||||
|
||||
class DocProperties(PropertyGroup):
|
||||
should_regenerate_base_layer: BoolProperty(name="Regenerate Base Layer", default=True)
|
||||
should_regenerate_annotation_layer: BoolProperty(name="Regenerate Annotation Layer", default=True)
|
||||
should_recut: BoolProperty(name="Should Recut", default=True)
|
||||
should_recut_selected: BoolProperty(name="Should Recut Selected Only", default=False)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False)
|
||||
has_linework: BoolProperty(name="Linework", default=True)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True)
|
||||
should_use_underlay_cache: BoolProperty(name="Use Underlay Cache", default=False)
|
||||
should_use_linework_cache: BoolProperty(name="Use Linework Cache", default=False)
|
||||
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
|
||||
should_extract: BoolProperty(name="Should Extract", default=True)
|
||||
drawings: CollectionProperty(name="Drawings", type=Drawing)
|
||||
active_drawing_index: IntProperty(name="Active Drawing Index", update=refreshActiveDrawingIndex)
|
||||
|
||||
@@ -13,7 +13,7 @@ class SheetBuilder:
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, name, titleblock_name):
|
||||
sheet_path = "{}sheets/{}.svg".format(self.data_dir, name)
|
||||
sheet_path = os.path.join(self.data_dir, f"{name}.svg")
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
@@ -121,9 +121,9 @@ class SheetBuilder:
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet_name):
|
||||
os.makedirs("{}build/{}/".format(self.data_dir, sheet_name), exist_ok=True)
|
||||
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
|
||||
|
||||
sheet_path = "{}sheets/{}.svg".format(self.data_dir, sheet_name)
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
@@ -140,7 +140,7 @@ class SheetBuilder:
|
||||
self.build_drawings(root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'), sheet_name)
|
||||
self.build_schedules(root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'))
|
||||
|
||||
with open("{}build/{}/{}.svg".format(self.data_dir, sheet_name, sheet_name), "wb") as output:
|
||||
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
|
||||
tree.write(output)
|
||||
|
||||
def build_drawings(self, drawings, sheet_name):
|
||||
@@ -155,8 +155,9 @@ class SheetBuilder:
|
||||
view.append(self.parse_embedded_svg(foreground, {}))
|
||||
|
||||
# Add background
|
||||
background_path = "{}sheets/{}".format(self.data_dir, self.get_href(background))
|
||||
copy(background_path, "{}build/{}/".format(self.data_dir, sheet_name))
|
||||
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
|
||||
|
||||
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
|
||||
# Add view title
|
||||
foreground_path = self.get_href(foreground)
|
||||
@@ -202,7 +203,7 @@ class SheetBuilder:
|
||||
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
|
||||
)
|
||||
svg_path = self.get_href(image)
|
||||
with open("{}sheets/{}".format(self.data_dir, svg_path), "r") as template:
|
||||
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
|
||||
embedded = ET.fromstring(pystache.render(template.read(), data))
|
||||
# viewBox should not be nested
|
||||
embedded.attrib["viewBox"] = ""
|
||||
|
||||
@@ -45,9 +45,10 @@ class SvgWriter:
|
||||
self.vector_style = None
|
||||
self.human_scale = "NTS"
|
||||
self.annotations = {}
|
||||
self.background_image = None
|
||||
self.scale = 1 / 100 # 1:100
|
||||
|
||||
def write(self):
|
||||
def write(self, layer):
|
||||
self.calculate_scale()
|
||||
self.svg = svgwrite.Drawing(
|
||||
self.output,
|
||||
@@ -58,14 +59,16 @@ class SvgWriter:
|
||||
data_scale=self.human_scale,
|
||||
)
|
||||
|
||||
self.add_stylesheet()
|
||||
self.add_markers()
|
||||
self.add_symbols()
|
||||
self.add_patterns()
|
||||
# self.draw_background_image()
|
||||
# self.draw_background_elements()
|
||||
# self.draw_cut_polygons()
|
||||
self.draw_annotations()
|
||||
if layer == "underlay":
|
||||
self.draw_background_image()
|
||||
elif layer == "annotation":
|
||||
self.add_stylesheet()
|
||||
self.add_markers()
|
||||
self.add_symbols()
|
||||
self.add_patterns()
|
||||
# self.draw_background_elements()
|
||||
# self.draw_cut_polygons()
|
||||
self.draw_annotations()
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def calculate_scale(self):
|
||||
@@ -76,32 +79,31 @@ class SvgWriter:
|
||||
self.height = self.raw_height * self.scale
|
||||
|
||||
def add_stylesheet(self):
|
||||
with open("{}styles/{}.css".format(self.data_dir, self.vector_style), "r") as stylesheet:
|
||||
with open(os.path.join(self.data_dir, "styles", f"{self.vector_style}.css"), "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self):
|
||||
tree = ET.parse("{}templates/markers.svg".format(self.data_dir))
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root.getchildren():
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_symbols(self):
|
||||
tree = ET.parse("{}templates/symbols.svg".format(self.data_dir))
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root.getchildren():
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_patterns(self):
|
||||
tree = ET.parse("{}templates/patterns.svg".format(self.data_dir))
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root.getchildren():
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_background_image(self):
|
||||
return # TODO reimplement for artistic drawing options
|
||||
self.svg.add(
|
||||
self.svg.image(
|
||||
os.path.join("..", "diagrams", os.path.basename(self.ifc_cutter.background_image)),
|
||||
os.path.join("..", "diagrams", os.path.basename(self.background_image)),
|
||||
**{"width": self.width, "height": self.height}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -27,16 +27,17 @@ class BIM_PT_camera(Panel):
|
||||
dprops = bpy.context.scene.DocProperties
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
|
||||
layout.label(text="Generation Options:")
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_underlay", icon="OUTLINER_OB_IMAGE")
|
||||
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_linework", icon="IMAGE_DATA")
|
||||
row.prop(dprops, "should_use_linework_cache", text="", icon="FILE_REFRESH")
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_regenerate_base_layer")
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_regenerate_annotation_layer")
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_recut")
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_recut_selected")
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_extract")
|
||||
|
||||
@@ -68,7 +69,31 @@ class BIM_PT_camera(Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "custom_diagram_scale")
|
||||
|
||||
layout.label(text="Drawing Styles:")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = context.active_object.name.split("/")[1]
|
||||
|
||||
|
||||
class BIM_PT_drawing_underlay(Panel):
|
||||
bl_label = "Drawing Underlay"
|
||||
bl_idname = "BIM_PT_drawing_underlay"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "data"
|
||||
bl_parent_id = "BIM_PT_camera"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
engine = context.engine
|
||||
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
dprops = bpy.context.scene.DocProperties
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_drawing_style")
|
||||
@@ -105,10 +130,6 @@ class BIM_PT_camera(Panel):
|
||||
row.operator("bim.save_drawing_style")
|
||||
row.operator("bim.activate_drawing_style")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.create_drawing", text="Create Drawing")
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = context.active_object.name.split("/")[1]
|
||||
|
||||
|
||||
class BIM_PT_drawings(Panel):
|
||||
@@ -238,6 +259,7 @@ class BIM_PT_text(Panel):
|
||||
class BIM_PT_annotation_utilities(Panel):
|
||||
bl_idname = "BIM_PT_annotation_utilities"
|
||||
bl_label = "Annotation"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BlenderBIM"
|
||||
|
||||
@@ -200,11 +200,12 @@ class SwitchRepresentation(bpy.types.Operator):
|
||||
if self.oprops.ifc_definition_id not in VoidData.products:
|
||||
VoidData.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
|
||||
for opening_id in VoidData.products[self.oprops.ifc_definition_id]:
|
||||
if opening_id in IfcStore.id_map:
|
||||
opening = IfcStore.id_map[opening_id]
|
||||
modifier = self.element_obj.modifiers.new("IfcOpeningElement", "BOOLEAN")
|
||||
modifier.operation = "DIFFERENCE"
|
||||
modifier.object = opening
|
||||
opening = IfcStore.get_element(opening_id)
|
||||
if not opening:
|
||||
continue
|
||||
modifier = self.element_obj.modifiers.new("IfcOpeningElement", "BOOLEAN")
|
||||
modifier.operation = "DIFFERENCE"
|
||||
modifier.object = opening
|
||||
else:
|
||||
for modifier in self.element_obj.modifiers:
|
||||
if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name:
|
||||
@@ -269,7 +270,7 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
if product.is_a("IfcGridAxis"):
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": product})
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": product})
|
||||
return
|
||||
|
||||
bpy.ops.bim.edit_object_placement(obj=obj.name)
|
||||
|
||||
@@ -42,7 +42,7 @@ class AddMaterial(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
obj = bpy.data.materials.get(self.obj) if self.obj else bpy.context.active_object.active_material
|
||||
self.file = IfcStore.get_file()
|
||||
result = ifcopenshell.api.run("material.add_material", self.file, **{"Name": obj.name})
|
||||
result = ifcopenshell.api.run("material.add_material", self.file, **{"name": obj.name})
|
||||
obj.BIMObjectProperties.ifc_definition_id = result.id()
|
||||
Data.load(IfcStore.get_file())
|
||||
material_prop_purge()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
import blenderbim.bim.schema # refactor
|
||||
from ifcopenshell.api.material.data import Data
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import bpy
|
||||
from . import operator, ui, grid, stair, door, window, slab, opening, pie
|
||||
from . import handler, prop, ui, grid, product, wall, slab, stair, door, window, opening, pie, workspace
|
||||
|
||||
classes = (
|
||||
operator.AddTypeInstance,
|
||||
operator.JoinWall,
|
||||
operator.AlignWall,
|
||||
product.AddTypeInstance,
|
||||
wall.AddWall,
|
||||
wall.JoinWall,
|
||||
wall.AlignWall,
|
||||
wall.FlipWall,
|
||||
wall.SplitWall,
|
||||
prop.BIMModelProperties,
|
||||
ui.BIM_PT_authoring,
|
||||
ui.BIM_PT_authoring_architectural,
|
||||
ui.BIM_PT_misc_utilities,
|
||||
@@ -12,7 +16,6 @@ classes = (
|
||||
stair.BIM_OT_add_object,
|
||||
door.BIM_OT_add_object,
|
||||
window.BIM_OT_add_object,
|
||||
slab.BIM_OT_add_object,
|
||||
opening.BIM_OT_add_object,
|
||||
pie.OpenPieClass,
|
||||
pie.PieUpdateContainer,
|
||||
@@ -32,12 +35,14 @@ addon_keymaps = []
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_tool(workspace.WallTool, after={"builtin.scale_cage"}, separator=True, group=True)
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(slab.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
|
||||
@@ -47,11 +52,13 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_tool(workspace.WallTool)
|
||||
del bpy.types.Scene.BIMModelProperties
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(slab.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button)
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
|
||||
@@ -57,10 +57,10 @@ def add_object(self, context):
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis",
|
||||
self.file,
|
||||
**{"AxisTag": tag, "AxisCurve": obj, "UVWAxes": "UAxes", "Grid": grid},
|
||||
**{"axis_tag": tag, "uvw_axes": "UAxes", "grid": grid},
|
||||
)
|
||||
IfcStore.link_element(result, obj)
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": result})
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": result})
|
||||
obj.BIMObjectProperties.ifc_definition_id = result.id()
|
||||
|
||||
axes_collection = bpy.data.collections.new("VAxes")
|
||||
@@ -83,10 +83,10 @@ def add_object(self, context):
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis",
|
||||
self.file,
|
||||
**{"AxisTag": tag, "AxisCurve": obj, "UVWAxes": "VAxes", "Grid": grid},
|
||||
**{"axis_tag": tag, "uvw_axes": "VAxes", "grid": grid},
|
||||
)
|
||||
IfcStore.link_element(result, obj)
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": result})
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": result})
|
||||
obj.BIMObjectProperties.ifc_definition_id = result.id()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
from blenderbim.bim.module.model import product, wall, slab
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(*args):
|
||||
ifcopenshell.api.add_post_listener("geometry.add_representation", None, product.generate_box)
|
||||
|
||||
ifcopenshell.api.add_post_listener("geometry.add_representation", None, wall.generate_axis)
|
||||
ifcopenshell.api.add_post_listener("geometry.add_representation", None, wall.calculate_quantities)
|
||||
ifcopenshell.api.add_pre_listener("material.edit_layer", None, wall.DumbWallPlaner().regenerate_from_layer)
|
||||
ifcopenshell.api.add_pre_listener("type.assign_type", None, wall.DumbWallPlaner().regenerate_from_type)
|
||||
|
||||
IfcStore.add_element_listener(slab.element_listener)
|
||||
ifcopenshell.api.add_pre_listener("material.edit_layer", None, slab.DumbSlabPlaner().regenerate_from_layer)
|
||||
ifcopenshell.api.add_pre_listener("type.assign_type", None, slab.DumbSlabPlaner().regenerate_from_type)
|
||||
@@ -0,0 +1,89 @@
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
from . import wall, slab
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
|
||||
|
||||
class AddTypeInstance(bpy.types.Operator):
|
||||
bl_idname = "bim.add_type_instance"
|
||||
bl_label = "Add Type Instance"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
relating_type: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
tprops = context.scene.BIMTypeProperties
|
||||
ifc_class = self.ifc_class or tprops.ifc_class
|
||||
relating_type = self.relating_type or tprops.relating_type
|
||||
if not ifc_class or not relating_type:
|
||||
return {"FINISHED"}
|
||||
self.file = IfcStore.get_file()
|
||||
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0]
|
||||
if ifc_class == "IfcWallType":
|
||||
obj = wall.DumbWallGenerator(self.file.by_id(int(relating_type))).generate()
|
||||
if obj:
|
||||
return {"FINISHED"}
|
||||
elif ifc_class == "IfcSlabType":
|
||||
obj = slab.DumbSlabGenerator(self.file.by_id(int(relating_type))).generate()
|
||||
if obj:
|
||||
return {"FINISHED"}
|
||||
# A cube
|
||||
verts = [
|
||||
Vector((-1, -1, -1)),
|
||||
Vector((-1, -1, 1)),
|
||||
Vector((-1, 1, -1)),
|
||||
Vector((-1, 1, 1)),
|
||||
Vector((1, -1, -1)),
|
||||
Vector((1, -1, 1)),
|
||||
Vector((1, 1, -1)),
|
||||
Vector((1, 1, 1)),
|
||||
]
|
||||
edges = []
|
||||
faces = [
|
||||
[0, 2, 3, 1],
|
||||
[2, 3, 7, 6],
|
||||
[4, 5, 7, 6],
|
||||
[0, 1, 5, 4],
|
||||
[1, 3, 7, 5],
|
||||
[0, 2, 6, 4],
|
||||
]
|
||||
mesh = bpy.data.meshes.new(name="Instance")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = bpy.data.objects.new("Instance", mesh)
|
||||
obj.location = context.scene.cursor.location
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
bpy.ops.bim.assign_type(relating_type=int(tprops.relating_type), related_object=obj.name)
|
||||
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def generate_box(usecase_path, ifc_file, **settings):
|
||||
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
|
||||
if not box_context:
|
||||
return
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW")
|
||||
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
|
||||
if old_box:
|
||||
bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name)
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = box_context
|
||||
new_box = ifcopenshell.api.run(
|
||||
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation",
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
**{"product": product, "representation": new_box}
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import bpy
|
||||
import ifcopenshell.util.type
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
relating_types_enum = []
|
||||
|
||||
|
||||
def purge():
|
||||
global relating_types_enum
|
||||
relating_types_enum = []
|
||||
|
||||
def getRelatingTypes(self, context):
|
||||
global relating_types_enum
|
||||
if len(relating_types_enum) < 1:
|
||||
elements = IfcStore.get_file().by_type("IfcWallType")
|
||||
relating_types_enum.extend((str(e.id()), e.Name, "") for e in elements)
|
||||
return relating_types_enum
|
||||
|
||||
class BIMModelProperties(PropertyGroup):
|
||||
relating_type: EnumProperty(items=getRelatingTypes, name="Relating Type")
|
||||
@@ -1,46 +1,174 @@
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty
|
||||
from mathutils import Vector
|
||||
import bmesh
|
||||
import math
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import mathutils.geometry
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from math import pi, degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
verts = [
|
||||
Vector((0, 0, 0)),
|
||||
Vector((0, self.width, 0)),
|
||||
Vector((self.length, self.width, 0)),
|
||||
Vector((self.length, 0, 0)),
|
||||
]
|
||||
edges = []
|
||||
faces = [[0, 1, 2, 3]]
|
||||
def element_listener(element, obj):
|
||||
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
|
||||
|
||||
mesh = bpy.data.meshes.new(name="Dumb Slab")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = bpy.data.objects.new("Slab", mesh)
|
||||
modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY")
|
||||
modifier.use_even_offset = True
|
||||
modifier.offset = 1
|
||||
modifier.thickness = self.depth
|
||||
obj.name = "Slab"
|
||||
context.view_layer.active_layer_collection.collection.objects.link(obj)
|
||||
if IfcStore.get_file():
|
||||
|
||||
def mode_callback(obj, data):
|
||||
for obj in bpy.context.selected_objects + [bpy.context.active_object]:
|
||||
if (
|
||||
obj.mode != "EDIT"
|
||||
or not obj.data
|
||||
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
|
||||
or not obj.BIMObjectProperties.ifc_definition_id
|
||||
or not bpy.context.scene.BIMProjectProperties.is_authoring
|
||||
):
|
||||
return
|
||||
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if parametric and parametric["Engine"] != "BlenderBIM.DumbSlab":
|
||||
return
|
||||
modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"]
|
||||
if modifier:
|
||||
return
|
||||
depth = obj.dimensions.z
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bm.faces.ensure_lookup_table()
|
||||
non_bottom_faces = []
|
||||
for face in bm.faces:
|
||||
if face.normal.z > -0.9:
|
||||
non_bottom_faces.append(face)
|
||||
else:
|
||||
face.normal_flip()
|
||||
bmesh.ops.delete(bm, geom=non_bottom_faces, context="FACES")
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bm.free()
|
||||
modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY")
|
||||
modifier.use_even_offset = True
|
||||
modifier.offset = 1
|
||||
modifier.thickness = depth
|
||||
|
||||
|
||||
class DumbSlabGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
|
||||
def generate(self):
|
||||
self.file = IfcStore.get_file()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
thicknesses = []
|
||||
for rel in self.relating_type.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesMaterial"):
|
||||
material = rel.RelatingMaterial
|
||||
if material.is_a("IfcMaterialLayerSet"):
|
||||
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
|
||||
break
|
||||
if not thicknesses:
|
||||
return
|
||||
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.depth = sum(thicknesses) * unit_scale
|
||||
self.width = 3
|
||||
self.length = 3
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
return self.derive_from_cursor()
|
||||
|
||||
def derive_from_cursor(self):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
return self.create_slab()
|
||||
|
||||
def create_slab(self):
|
||||
verts = [
|
||||
Vector((0, 0, 0)),
|
||||
Vector((0, self.width, 0)),
|
||||
Vector((self.length, self.width, 0)),
|
||||
Vector((self.length, 0, 0)),
|
||||
]
|
||||
edges = []
|
||||
faces = [[0, 3, 2, 1]]
|
||||
|
||||
mesh = bpy.data.meshes.new(name="Dumb Slab")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = bpy.data.objects.new("Slab", mesh)
|
||||
modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY")
|
||||
modifier.use_even_offset = True
|
||||
modifier.offset = 1
|
||||
modifier.thickness = self.depth
|
||||
obj.name = "Slab"
|
||||
obj.location = self.location
|
||||
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
obj.location[2] = self.collection_obj.location[2] - self.depth
|
||||
else:
|
||||
obj.location[2] -= self.depth
|
||||
self.collection.objects.link(obj)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSlab", predefined_type="FLOOR")
|
||||
obj.location = context.scene.cursor.location
|
||||
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbSlab"})
|
||||
MaterialData.load(self.file)
|
||||
obj.select_set(True)
|
||||
return obj
|
||||
|
||||
|
||||
class BIM_OT_add_object(Operator):
|
||||
bl_idname = "mesh.add_slab"
|
||||
bl_label = "Dumb Slab"
|
||||
class DumbSlabPlaner:
|
||||
def regenerate_from_layer(self, usecase_path, ifc_file, **settings):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
layer = settings["layer"]
|
||||
thickness = settings["attributes"].get("LayerThickness")
|
||||
if thickness is None or layer.LayerThickness == thickness:
|
||||
return
|
||||
delta_thickness = thickness - layer.LayerThickness
|
||||
for layer_set in layer.ToMaterialLayerSet:
|
||||
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||
if not total_thickness:
|
||||
continue
|
||||
for inverse in ifc_file.get_inverse(layer_set):
|
||||
if not inverse.is_a("IfcMaterialLayerSetUsage"):
|
||||
continue
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
for rel in ifc_file.get_inverse(inverse):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, delta_thickness)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, delta_thickness)
|
||||
|
||||
length: FloatProperty(name="Length", default=2)
|
||||
width: FloatProperty(name="Width", default=2)
|
||||
depth: FloatProperty(name="Depth", default=0.2)
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, **settings):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
if not new_material.is_a("IfcMaterialLayerSet"):
|
||||
return
|
||||
obj = IfcStore.get_element(settings["related_object"].id())
|
||||
if not obj:
|
||||
return
|
||||
current_thickness = obj.dimensions.z / self.unit_scale
|
||||
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
|
||||
if current_thickness == new_thickness:
|
||||
return
|
||||
self.change_thickness(settings["related_object"], new_thickness - current_thickness)
|
||||
|
||||
def execute(self, context):
|
||||
add_object(self, context)
|
||||
return {"FINISHED"}
|
||||
def change_thickness(self, element, delta_thickness):
|
||||
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
|
||||
return
|
||||
|
||||
obj = IfcStore.get_element(element.id())
|
||||
if not obj:
|
||||
return
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
|
||||
modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"]
|
||||
if modifier:
|
||||
modifier = modifier[0]
|
||||
else:
|
||||
pass
|
||||
modifier.thickness += delta_thickness * self.unit_scale
|
||||
obj.location[2] -= delta_thickness * self.unit_scale
|
||||
|
||||
@@ -36,6 +36,9 @@ class BIM_PT_authoring_architectural(Panel):
|
||||
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.flip_wall", icon="ORIENTATION_NORMAL", text="Flip")
|
||||
row.operator("bim.split_wall", icon="MOD_PHYSICS", text="Split")
|
||||
|
||||
|
||||
class BIM_PT_misc_utilities(Panel):
|
||||
|
||||
+372
-73
@@ -1,60 +1,28 @@
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import mathutils.geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
from math import pi, degrees
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
class AddTypeInstance(bpy.types.Operator):
|
||||
bl_idname = "bim.add_type_instance"
|
||||
bl_label = "Add Type Instance"
|
||||
class AddWall(bpy.types.Operator):
|
||||
bl_idname = "bim.add_wall"
|
||||
bl_label = "Add Wall"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
join_type: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
tprops = context.scene.BIMTypeProperties
|
||||
if not tprops.ifc_class or not tprops.relating_type:
|
||||
return {"FINISHED"}
|
||||
self.file = IfcStore.get_file()
|
||||
instance_class = ifcopenshell.util.type.get_applicable_entities(tprops.ifc_class, self.file.schema)[0]
|
||||
if instance_class in ["IfcWall", "IfcWallStandardCase"]:
|
||||
obj = DumbWallGenerator(self.file.by_id(int(tprops.relating_type))).generate()
|
||||
if obj:
|
||||
return {"FINISHED"}
|
||||
# A cube
|
||||
verts = [
|
||||
Vector((-1, -1, -1)),
|
||||
Vector((-1, -1, 1)),
|
||||
Vector((-1, 1, -1)),
|
||||
Vector((-1, 1, 1)),
|
||||
Vector((1, -1, -1)),
|
||||
Vector((1, -1, 1)),
|
||||
Vector((1, 1, -1)),
|
||||
Vector((1, 1, 1)),
|
||||
]
|
||||
edges = []
|
||||
faces = [
|
||||
[0, 2, 3, 1],
|
||||
[2, 3, 7, 6],
|
||||
[4, 5, 7, 6],
|
||||
[0, 1, 5, 4],
|
||||
[1, 3, 7, 5],
|
||||
[0, 2, 6, 4],
|
||||
]
|
||||
mesh = bpy.data.meshes.new(name="Instance")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = bpy.data.objects.new("Instance", mesh)
|
||||
obj.location = context.scene.cursor.location
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
bpy.ops.bim.assign_type(relating_type=int(tprops.relating_type), related_object=obj.name)
|
||||
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
|
||||
props = context.scene.BIMModelProperties
|
||||
bpy.ops.bim.add_type_instance(ifc_class="IfcWallType", relating_type=int(props.relating_type))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -107,21 +75,133 @@ class AlignWall(bpy.types.Operator):
|
||||
if self.align_type == "CENTERLINE":
|
||||
aligner.align_centerline()
|
||||
elif self.align_type == "EXTERIOR":
|
||||
aligner.align_exterior()
|
||||
aligner.align_first_layer()
|
||||
elif self.align_type == "INTERIOR":
|
||||
aligner.align_interior()
|
||||
aligner.align_last_layer()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def recalculate_dumb_wall_origin(wall):
|
||||
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
|
||||
if (wall.matrix_world.translation - new_origin).length > 0.001:
|
||||
wall.data.transform(
|
||||
Matrix.Translation(
|
||||
(wall.matrix_world.inverted().to_quaternion() @ (wall.matrix_world.translation - new_origin))
|
||||
)
|
||||
class FlipWall(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_wall"
|
||||
bl_label = "Flip Wall"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
if len(selected_objs) == 0:
|
||||
return {"FINISHED"}
|
||||
for obj in selected_objs:
|
||||
DumbWallFlipper(obj).flip()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SplitWall(bpy.types.Operator):
|
||||
bl_idname = "bim.split_wall"
|
||||
bl_label = "Split Wall"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
if len(selected_objs) == 0:
|
||||
return {"FINISHED"}
|
||||
for obj in selected_objs:
|
||||
DumbWallSplitter(obj, bpy.context.scene.cursor.location).split()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def recalculate_dumb_wall_origin(wall, new_origin=None):
|
||||
if new_origin is None:
|
||||
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
|
||||
if (wall.matrix_world.translation - new_origin).length < 0.001:
|
||||
return
|
||||
wall.data.transform(
|
||||
Matrix.Translation(
|
||||
(wall.matrix_world.inverted().to_quaternion() @ (wall.matrix_world.translation - new_origin))
|
||||
)
|
||||
wall.matrix_world.translation = new_origin
|
||||
)
|
||||
wall.matrix_world.translation = new_origin
|
||||
|
||||
|
||||
class DumbWallSplitter:
|
||||
def __init__(self, wall, point):
|
||||
self.wall = wall
|
||||
self.point = point
|
||||
|
||||
def split(self):
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
self.point = self.determine_split_point()
|
||||
if not self.point:
|
||||
return
|
||||
new_wall = self.duplicate_wall()
|
||||
self.snap_end_face_to_point(self.wall, "max")
|
||||
self.snap_end_face_to_point(new_wall, "min")
|
||||
|
||||
def determine_split_point(self):
|
||||
start = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
|
||||
end = self.wall.matrix_world @ Vector(self.wall.bound_box[4])
|
||||
point, distance = mathutils.geometry.intersect_point_line(self.point, start, end)
|
||||
if round(distance, 2) <= 0 or round(distance, 2) >= 1:
|
||||
return # The split point is not on the wall
|
||||
return point
|
||||
|
||||
def duplicate_wall(self):
|
||||
new = self.wall.copy()
|
||||
self.wall.users_collection[0].objects.link(new)
|
||||
bpy.ops.bim.copy_class(obj=new.name)
|
||||
return new
|
||||
|
||||
def snap_end_face_to_point(self, wall, which_end):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(wall.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
min_face, max_face = self.get_wall_end_faces(wall, bm)
|
||||
face = min_face if which_end == "min" else max_face
|
||||
local_point = wall.matrix_world.inverted() @ self.point
|
||||
for vert in face.verts:
|
||||
vert.co.x = local_point.x
|
||||
bm.to_mesh(wall.data)
|
||||
wall.data.update()
|
||||
bm.free()
|
||||
IfcStore.edited_objs.add(wall)
|
||||
|
||||
# An end face is a quad that is on one end of the wall or the other. It must
|
||||
# have at least one vertex on either extreme X-axis, and a non-insignificant
|
||||
# X component of its face normal
|
||||
def get_wall_end_faces(self, wall, bm):
|
||||
min_face = None
|
||||
max_face = None
|
||||
min_x = min([v[0] for v in wall.bound_box])
|
||||
max_x = max([v[0] for v in wall.bound_box])
|
||||
bm.faces.ensure_lookup_table()
|
||||
for f in bm.faces:
|
||||
for v in f.verts:
|
||||
if v.co.x == min_x and abs(f.normal.x) > 0.1:
|
||||
min_face = f
|
||||
elif v.co.x == max_x and abs(f.normal.x) > 0.1:
|
||||
max_face = f
|
||||
if min_face and max_face:
|
||||
break
|
||||
return min_face, max_face
|
||||
|
||||
|
||||
class DumbWallFlipper:
|
||||
# A flip switches the origin from the min XY corner to the max XY corner, and rotates the origin by 180.
|
||||
def __init__(self, wall):
|
||||
self.wall = wall
|
||||
|
||||
def flip(self):
|
||||
if (
|
||||
self.wall.matrix_world.translation - self.wall.matrix_world @ Vector(self.wall.bound_box[0])
|
||||
).length < 0.001:
|
||||
recalculate_dumb_wall_origin(self.wall, self.wall.matrix_world @ Vector(self.wall.bound_box[7]))
|
||||
self.rotate_wall_180()
|
||||
else:
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
|
||||
def rotate_wall_180(self):
|
||||
flip_matrix = Matrix.Rotation(pi, 4, "Z")
|
||||
self.wall.data.transform(flip_matrix)
|
||||
self.wall.rotation_euler.rotate(flip_matrix)
|
||||
|
||||
|
||||
class DumbWallAligner:
|
||||
@@ -133,37 +213,79 @@ class DumbWallAligner:
|
||||
self.reference_wall = reference_wall
|
||||
|
||||
def align_centerline(self):
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
recalculate_dumb_wall_origin(self.reference_wall)
|
||||
self.align_rotation()
|
||||
|
||||
width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
|
||||
reference_width = (Vector(self.reference_wall.bound_box[3]) - Vector(self.reference_wall.bound_box[0])).y
|
||||
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, (reference_width / 2) - (width / 2), 0))
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -(reference_width / 2) - (width / 2), 0))
|
||||
else:
|
||||
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, (reference_width / 2) - (width / 2), 0))
|
||||
|
||||
self.align(
|
||||
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0]),
|
||||
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4]),
|
||||
offset,
|
||||
)
|
||||
|
||||
def align_exterior(self):
|
||||
wall_width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
|
||||
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -wall_width, 0))
|
||||
self.align(
|
||||
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3]),
|
||||
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7]),
|
||||
offset,
|
||||
)
|
||||
def align_last_layer(self):
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
recalculate_dumb_wall_origin(self.reference_wall)
|
||||
self.align_rotation()
|
||||
|
||||
def align_interior(self):
|
||||
self.align(self.reference_wall.matrix_world.translation, self.reference_wall.matrix_world @ Vector((1, 0, 0)))
|
||||
if self.is_rotation_flipped():
|
||||
DumbWallFlipper(self.wall).flip()
|
||||
bpy.context.view_layer.update()
|
||||
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3])
|
||||
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7])
|
||||
|
||||
wall_width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
|
||||
|
||||
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -wall_width, 0))
|
||||
self.align(start, end, offset)
|
||||
|
||||
def align_first_layer(self):
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
recalculate_dumb_wall_origin(self.reference_wall)
|
||||
self.align_rotation()
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
DumbWallFlipper(self.wall).flip()
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0])
|
||||
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4])
|
||||
|
||||
self.align(start, end)
|
||||
|
||||
def align(self, start, end, offset=None):
|
||||
if offset is None:
|
||||
offset = Vector()
|
||||
recalculate_dumb_wall_origin(self.wall)
|
||||
recalculate_dumb_wall_origin(self.reference_wall)
|
||||
offset = Vector((0, 0, 0))
|
||||
point, distance = mathutils.geometry.intersect_point_line(self.wall.matrix_world.translation, start, end)
|
||||
new_origin = point + offset
|
||||
self.wall.matrix_world.translation[0] = new_origin[0]
|
||||
self.wall.matrix_world.translation[1] = new_origin[1]
|
||||
self.wall.rotation_euler[2] = self.reference_wall.rotation_euler[2]
|
||||
|
||||
def align_rotation(self):
|
||||
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
||||
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
||||
angle = reference.angle_signed(wall)
|
||||
if round(degrees(angle) % 360) in (0, 180):
|
||||
return
|
||||
elif angle > (pi / 2):
|
||||
self.wall.rotation_euler[2] -= pi - angle
|
||||
else:
|
||||
self.wall.rotation_euler[2] += angle
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
def is_rotation_flipped(self):
|
||||
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
||||
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
||||
angle = reference.angle_signed(wall)
|
||||
return round(degrees(angle) % 360) == 180
|
||||
|
||||
|
||||
class DumbWallJoiner:
|
||||
@@ -557,8 +679,185 @@ class DumbWallGenerator:
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcWall")
|
||||
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, Name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, Properties={"Engine": "BlenderBIM.DumbWall"})
|
||||
ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage")
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbWall"})
|
||||
MaterialData.load(self.file)
|
||||
obj.select_set(True)
|
||||
return obj
|
||||
|
||||
|
||||
def generate_axis(usecase_path, ifc_file, **settings):
|
||||
axis_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Axis", "GRAPH_VIEW")
|
||||
if not axis_context:
|
||||
return
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
old_axis = ifcopenshell.util.representation.get_representation(product, "Model", "Axis", "GRAPH_VIEW")
|
||||
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
|
||||
if old_axis:
|
||||
bpy.ops.bim.remove_representation(representation_id=old_axis.id(), obj=obj.name)
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = axis_context
|
||||
|
||||
mesh = bpy.data.meshes.new("Temporary Axis")
|
||||
start = Vector(obj.bound_box[0])
|
||||
end = Vector(obj.bound_box[4])
|
||||
mesh.from_pydata([start, end], [(0, 1)], [])
|
||||
|
||||
new_settings["geometry"] = mesh
|
||||
new_axis = ifcopenshell.api.run(
|
||||
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation",
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
**{"product": product, "representation": new_axis}
|
||||
)
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def calculate_quantities(usecase_path, ifc_file, **settings):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
qto = ifcopenshell.api.run(
|
||||
"pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_WallBaseQuantities"
|
||||
)
|
||||
length = obj.dimensions[0] / unit_scale
|
||||
width = obj.dimensions[1] / unit_scale
|
||||
height = obj.dimensions[2] / unit_scale
|
||||
|
||||
if product.HasOpenings:
|
||||
# TODO: calculate gross / net
|
||||
gross_volume = 0
|
||||
net_volume = 0
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
gross_volume = bm.calc_volume()
|
||||
net_volume = gross_volume
|
||||
bm.free()
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_qto",
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
qto=qto,
|
||||
properties={
|
||||
"Length": round(length, 2),
|
||||
"Width": round(width, 2),
|
||||
"Height": round(height, 2),
|
||||
"GrossVolume": round(gross_volume, 2),
|
||||
"NetVolume": round(net_volume, 2),
|
||||
},
|
||||
)
|
||||
PsetData.load(ifc_file, obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
|
||||
class DumbWallPlaner:
|
||||
def regenerate_from_layer(self, usecase_path, ifc_file, **settings):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
layer = settings["layer"]
|
||||
thickness = settings["attributes"].get("LayerThickness")
|
||||
if thickness is None or layer.LayerThickness == thickness:
|
||||
return
|
||||
delta_thickness = thickness - layer.LayerThickness
|
||||
for layer_set in layer.ToMaterialLayerSet:
|
||||
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||
if not total_thickness:
|
||||
continue
|
||||
for inverse in ifc_file.get_inverse(layer_set):
|
||||
if not inverse.is_a("IfcMaterialLayerSetUsage"):
|
||||
continue
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
for rel in ifc_file.get_inverse(inverse):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, delta_thickness)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, delta_thickness)
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, **settings):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
if not new_material.is_a("IfcMaterialLayerSet"):
|
||||
return
|
||||
obj = IfcStore.get_element(settings["related_object"].id())
|
||||
if not obj:
|
||||
return
|
||||
current_thickness = obj.dimensions.y / self.unit_scale
|
||||
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
|
||||
if current_thickness == new_thickness:
|
||||
return
|
||||
self.change_thickness(settings["related_object"], new_thickness - current_thickness)
|
||||
|
||||
def change_thickness(self, element, delta_thickness):
|
||||
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
|
||||
obj = IfcStore.get_element(element.id())
|
||||
if not obj:
|
||||
return
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
min_face, max_face = self.get_wall_end_faces(obj, bm)
|
||||
|
||||
self.thicken_face(min_face, delta_thickness)
|
||||
self.thicken_face(max_face, delta_thickness)
|
||||
|
||||
bm.to_mesh(obj.data)
|
||||
obj.data.update()
|
||||
bm.free()
|
||||
|
||||
def thicken_face(self, face, delta_thickness):
|
||||
slide_magnitude = abs(delta_thickness) / 2 * self.unit_scale
|
||||
for vert in face.verts:
|
||||
slide_vector = None
|
||||
for edge in vert.link_edges:
|
||||
other_vert = edge.verts[1] if edge.verts[0] == vert else edge.verts[0]
|
||||
if delta_thickness > 0:
|
||||
potential_slide_vector = vert.co - other_vert.co
|
||||
else:
|
||||
potential_slide_vector = other_vert.co - vert.co
|
||||
if abs(potential_slide_vector.x) > 0.9 or abs(potential_slide_vector.z) > 0.9:
|
||||
continue
|
||||
slide_vector = potential_slide_vector
|
||||
break
|
||||
if not slide_vector:
|
||||
continue
|
||||
slide_vector *= slide_magnitude / abs(slide_vector.y)
|
||||
vert.co += slide_vector
|
||||
|
||||
# An end face is a quad that is on one end of the wall or the other. It must
|
||||
# have at least one vertex on either extreme X-axis, and a non-insignificant
|
||||
# X component of its face normal
|
||||
def get_wall_end_faces(self, wall, bm):
|
||||
min_face = None
|
||||
max_face = None
|
||||
min_x = min([v[0] for v in wall.bound_box])
|
||||
max_x = max([v[0] for v in wall.bound_box])
|
||||
bm.faces.ensure_lookup_table()
|
||||
for f in bm.faces:
|
||||
for v in f.verts:
|
||||
if v.co.x == min_x and abs(f.normal.x) > 0.1:
|
||||
min_face = f
|
||||
elif v.co.x == max_x and abs(f.normal.x) > 0.1:
|
||||
max_face = f
|
||||
if min_face and max_face:
|
||||
break
|
||||
return min_face, max_face
|
||||
@@ -0,0 +1,58 @@
|
||||
import bpy
|
||||
from bpy.types import WorkSpaceTool
|
||||
|
||||
|
||||
class WallTool(WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
|
||||
bl_idname = "bim.wall_tool"
|
||||
bl_label = "Wall Tool"
|
||||
bl_description = "Gives you wall related superpowers"
|
||||
bl_icon = "ops.generic.select_circle"
|
||||
bl_widget = None
|
||||
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
|
||||
bl_keymap = (
|
||||
# ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
|
||||
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
|
||||
("bim.add_wall", {"type": "A", "value": "PRESS", "shift": True}, {"properties": []}),
|
||||
("bim.join_wall", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("join_type", "T")]}),
|
||||
("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}),
|
||||
("bim.join_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("join_type", "V")]}),
|
||||
("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
|
||||
("bim.split_wall", {"type": "S", "value": "PRESS", "shift": True}, {"properties": []}),
|
||||
(
|
||||
"bim.align_wall",
|
||||
{"type": "X", "value": "PRESS", "shift": True},
|
||||
{"properties": [("align_type", "EXTERIOR")]},
|
||||
),
|
||||
(
|
||||
"bim.align_wall",
|
||||
{"type": "C", "value": "PRESS", "shift": True},
|
||||
{"properties": [("align_type", "CENTERLINE")]},
|
||||
),
|
||||
(
|
||||
"bim.align_wall",
|
||||
{"type": "V", "value": "PRESS", "shift": True},
|
||||
{"properties": [("align_type", "INTERIOR")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
props = context.scene.BIMModelProperties
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "relating_type", text="")
|
||||
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Add", icon="EVENT_A")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
row.label(text="Butt", icon="EVENT_T")
|
||||
row.label(text="Mitre", icon="EVENT_Y")
|
||||
row.label(text="Flip", icon="EVENT_F")
|
||||
row.label(text="Split", icon="EVENT_S")
|
||||
row.label(text="", icon="EVENT_X")
|
||||
row.label(text="", icon="EVENT_C")
|
||||
row.label(text="", icon="EVENT_V")
|
||||
row.label(text="Align")
|
||||
@@ -1,196 +1,5 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from math import pi
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def generate_box(usecase_path, ifc_file, **settings):
|
||||
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
|
||||
if not box_context:
|
||||
return
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW")
|
||||
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
|
||||
if old_box:
|
||||
bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name)
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = box_context
|
||||
new_box = ifcopenshell.api.run(
|
||||
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation",
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
**{"product": product, "representation": new_box}
|
||||
)
|
||||
|
||||
|
||||
def generate_dumb_wall_axis(usecase_path, ifc_file, **settings):
|
||||
axis_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Axis", "GRAPH_VIEW")
|
||||
if not axis_context:
|
||||
return
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
old_axis = ifcopenshell.util.representation.get_representation(product, "Model", "Axis", "GRAPH_VIEW")
|
||||
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
|
||||
if old_axis:
|
||||
bpy.ops.bim.remove_representation(representation_id=old_axis.id(), obj=obj.name)
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = axis_context
|
||||
|
||||
mesh = bpy.data.meshes.new("Temporary Axis")
|
||||
start = Vector(obj.bound_box[0])
|
||||
end = Vector(obj.bound_box[4])
|
||||
mesh.from_pydata([start, end], [(0, 1)], [])
|
||||
|
||||
new_settings["geometry"] = mesh
|
||||
new_axis = ifcopenshell.api.run(
|
||||
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation",
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
**{"product": product, "representation": new_axis}
|
||||
)
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def calculate_dumb_quantities(usecase_path, ifc_file, **settings):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
obj = settings["blender_object"]
|
||||
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
qto = ifcopenshell.api.run(
|
||||
"pset.add_qto", ifc_file, should_run_listeners=False, product=product, Name="Qto_WallBaseQuantities"
|
||||
)
|
||||
length = obj.dimensions[0] / unit_scale
|
||||
width = obj.dimensions[1] / unit_scale
|
||||
height = obj.dimensions[2] / unit_scale
|
||||
|
||||
if product.HasOpenings:
|
||||
# TODO: calculate gross / net
|
||||
gross_volume = 0
|
||||
net_volume = 0
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
gross_volume = bm.calc_volume()
|
||||
net_volume = gross_volume
|
||||
bm.free()
|
||||
|
||||
ifcopenshell.api.run("pset.edit_qto", ifc_file, should_run_listeners=False, qto=qto, Properties={
|
||||
"Length": round(length, 2),
|
||||
"Width": round(width, 2),
|
||||
"Height": round(height, 2),
|
||||
"GrossVolume": round(gross_volume, 2),
|
||||
"NetVolume": round(net_volume, 2)
|
||||
})
|
||||
PsetData.load(ifc_file, obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
|
||||
class DumbWallPlaner:
|
||||
def regenerate_dumb_wall_thicknesses(self, usecase_path, ifc_file, **settings):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
layer = settings["layer"]
|
||||
thickness = settings["attributes"].get("LayerThickness")
|
||||
if thickness is None or layer.LayerThickness == thickness:
|
||||
return
|
||||
delta_thickness = thickness - layer.LayerThickness
|
||||
for layer_set in layer.ToMaterialLayerSet:
|
||||
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers]) * self.unit_scale
|
||||
if not total_thickness:
|
||||
continue
|
||||
for inverse in ifc_file.get_inverse(layer_set):
|
||||
if not inverse.is_a("IfcMaterialLayerSetUsage"):
|
||||
continue
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
for rel in ifc_file.get_inverse(inverse):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.regenerate_wall_thickness(element, delta_thickness)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.regenerate_wall_thickness(element, delta_thickness)
|
||||
|
||||
def regenerate_wall_thickness(self, element, delta_thickness):
|
||||
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
|
||||
return
|
||||
try:
|
||||
obj = IfcStore.id_map[element.id()]
|
||||
obj.name # In case the object has been deleted
|
||||
except:
|
||||
return
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
min_face, max_face = self.get_wall_end_faces(obj, bm)
|
||||
|
||||
self.thicken_face(min_face, delta_thickness)
|
||||
self.thicken_face(max_face, delta_thickness)
|
||||
|
||||
bm.to_mesh(obj.data)
|
||||
obj.data.update()
|
||||
bm.free()
|
||||
|
||||
def thicken_face(self, face, delta_thickness):
|
||||
slide_magnitude = abs(delta_thickness) / 2 * self.unit_scale
|
||||
for vert in face.verts:
|
||||
slide_vector = None
|
||||
for edge in vert.link_edges:
|
||||
other_vert = edge.verts[1] if edge.verts[0] == vert else edge.verts[0]
|
||||
if delta_thickness > 0:
|
||||
potential_slide_vector = vert.co - other_vert.co
|
||||
else:
|
||||
potential_slide_vector = other_vert.co - vert.co
|
||||
if abs(potential_slide_vector.x) > 0.9 or abs(potential_slide_vector.z) > 0.9:
|
||||
continue
|
||||
slide_vector = potential_slide_vector
|
||||
break
|
||||
if not slide_vector:
|
||||
continue
|
||||
slide_vector *= (slide_magnitude / abs(slide_vector.y))
|
||||
vert.co += slide_vector
|
||||
|
||||
# An end face is a quad that is on one end of the wall or the other. It must
|
||||
# have at least one vertex on either extreme X-axis, and a non-insignificant
|
||||
# X component of its face normal
|
||||
def get_wall_end_faces(self, wall, bm):
|
||||
min_face = None
|
||||
max_face = None
|
||||
min_x = min([v[0] for v in wall.bound_box])
|
||||
max_x = max([v[0] for v in wall.bound_box])
|
||||
bm.faces.ensure_lookup_table()
|
||||
for f in bm.faces:
|
||||
for v in f.verts:
|
||||
if v.co.x == min_x and abs(f.normal.x) > 0.1:
|
||||
min_face = f
|
||||
elif v.co.x == max_x and abs(f.normal.x) > 0.1:
|
||||
max_face = f
|
||||
if min_face and max_face:
|
||||
break
|
||||
return min_face, max_face
|
||||
|
||||
|
||||
class ActivateParametricEngine(bpy.types.Operator):
|
||||
@@ -198,14 +7,4 @@ class ActivateParametricEngine(bpy.types.Operator):
|
||||
bl_label = "Activate Parametric Engine"
|
||||
|
||||
def execute(self, context):
|
||||
ifcopenshell.api.add_post_listener("geometry.add_representation", IfcStore.get_file(), generate_box)
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", IfcStore.get_file(), generate_dumb_wall_axis
|
||||
)
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", IfcStore.get_file(), calculate_dumb_quantities
|
||||
)
|
||||
ifcopenshell.api.add_pre_listener(
|
||||
"material.edit_layer", IfcStore.get_file(), DumbWallPlaner().regenerate_dumb_wall_thicknesses
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
|
||||
@@ -46,7 +47,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
||||
"output": context.scene.BIMPatchProperties.ifc_patch_output,
|
||||
"recipe": context.scene.BIMPatchProperties.ifc_patch_recipes,
|
||||
"arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"),
|
||||
"log": context.scene.BIMProperties.data_dir + "process.log",
|
||||
"log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
|
||||
}
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -13,6 +13,9 @@ classes = (
|
||||
operator.UnassignLibraryDeclaration,
|
||||
operator.SaveLibraryFile,
|
||||
operator.AppendLibraryElement,
|
||||
operator.EnableEditingHeader,
|
||||
operator.DisableEditingHeader,
|
||||
operator.EditHeader,
|
||||
prop.LibraryElement,
|
||||
prop.BIMProjectProperties,
|
||||
ui.BIM_PT_project,
|
||||
|
||||
@@ -255,3 +255,60 @@ class AppendLibraryElement(bpy.types.Operator):
|
||||
ifc_importer.type_collection = type_collection
|
||||
ifc_importer.create_type_product(element)
|
||||
ifc_importer.place_objects_in_spatial_tree()
|
||||
|
||||
|
||||
class EnableEditingHeader(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_header"
|
||||
bl_label = "Enable Editing Header"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMProjectProperties
|
||||
props.is_editing = True
|
||||
|
||||
mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
|
||||
if "[" in mvd:
|
||||
props.mvd = mvd.split("[")[1][0:-1]
|
||||
else:
|
||||
props.mvd = ""
|
||||
|
||||
author = self.file.wrapped_data.header.file_name.author
|
||||
if author:
|
||||
props.author_name = author[0]
|
||||
if len(author) > 1:
|
||||
props.author_email = author[1]
|
||||
|
||||
organisation = self.file.wrapped_data.header.file_name.organization
|
||||
if organisation:
|
||||
props.organisation_name = organisation[0]
|
||||
if len(organisation) > 1:
|
||||
props.organisation_email = organisation[1]
|
||||
|
||||
props.authorisation = self.file.wrapped_data.header.file_name.authorization
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditHeader(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_header"
|
||||
bl_label = "Edit Header"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMProjectProperties
|
||||
props.is_editing = True
|
||||
|
||||
self.file.wrapped_data.header.file_description.description = (f'ViewDefinition[{props.mvd}]',)
|
||||
self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email)
|
||||
self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email)
|
||||
self.file.wrapped_data.header.file_name.authorization = props.authorisation
|
||||
bpy.ops.bim.disable_editing_header()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingHeader(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_header"
|
||||
bl_label = "Disable Editing Header"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMProjectProperties.is_editing = False
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -21,6 +21,13 @@ class LibraryElement(PropertyGroup):
|
||||
|
||||
class BIMProjectProperties(PropertyGroup):
|
||||
is_authoring: BoolProperty(name="Enable Authoring Mode", default=True)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
mvd: StringProperty(name="MVD")
|
||||
author_name: StringProperty(name="Author")
|
||||
author_email: StringProperty(name="Author Email")
|
||||
organisation_name: StringProperty(name="Organisation")
|
||||
organisation_email: StringProperty(name="Organisation Email")
|
||||
authorisation: StringProperty(name="Authoriser")
|
||||
active_library_element: StringProperty(name="Enable Authoring Mode", default="")
|
||||
library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty)
|
||||
library_elements: CollectionProperty(name="Library Elements", type=LibraryElement)
|
||||
|
||||
@@ -28,10 +28,40 @@ class BIM_PT_project(Panel):
|
||||
row.label(text=os.path.basename(props.ifc_file) or "No File Found")
|
||||
|
||||
if IfcStore.get_file():
|
||||
row.prop(pprops, "is_authoring", icon="GREASEPENCIL", text="")
|
||||
row.prop(pprops, "is_authoring", icon="MODIFIER", text="")
|
||||
if pprops.is_editing:
|
||||
row.operator("bim.edit_header", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_header", icon="CANCEL", text="")
|
||||
else:
|
||||
row.operator("bim.enable_editing_header", icon="GREASEPENCIL", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IFC Schema", icon="FILE_CACHE")
|
||||
row.label(text=IfcStore.get_file().schema)
|
||||
|
||||
if pprops.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "mvd")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "author_name")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "author_email")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "organisation_name")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "organisation_email")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(pprops, "authorisation")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IFC MVD", icon="FILE_HIDDEN")
|
||||
mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
|
||||
if "[" in mvd:
|
||||
mvd = mvd.split("[")[1][0:-1]
|
||||
row.label(text=mvd)
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="File Not Loaded", icon="ERROR")
|
||||
|
||||
@@ -187,8 +187,8 @@ class EditPset(bpy.types.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"pset": self.file.by_id(pset_id),
|
||||
"Name": props.active_pset_name,
|
||||
"Properties": properties,
|
||||
"name": props.active_pset_name,
|
||||
"properties": properties,
|
||||
},
|
||||
)
|
||||
else:
|
||||
@@ -197,8 +197,8 @@ class EditPset(bpy.types.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"qto": self.file.by_id(pset_id),
|
||||
"Name": props.active_pset_name,
|
||||
"Properties": properties,
|
||||
"name": props.active_pset_name,
|
||||
"properties": properties,
|
||||
},
|
||||
)
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
@@ -260,7 +260,7 @@ class AddPset(bpy.types.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(oprops.ifc_definition_id),
|
||||
"Name": pset_name,
|
||||
"name": pset_name,
|
||||
},
|
||||
)
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
@@ -281,7 +281,7 @@ class AddQto(bpy.types.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(oprops.ifc_definition_id),
|
||||
"Name": props.qto_name,
|
||||
"name": props.qto_name,
|
||||
},
|
||||
)
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
|
||||
@@ -99,13 +99,13 @@ class QuantifyObjects(bpy.types.Operator):
|
||||
"pset.add_qto",
|
||||
self.file,
|
||||
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
||||
Name=props.qto_name,
|
||||
name=props.qto_name,
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_qto",
|
||||
self.file,
|
||||
qto=qto,
|
||||
Properties={props.prop_name: result}
|
||||
properties={props.prop_name: result}
|
||||
)
|
||||
PsetData.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -66,6 +66,10 @@ classes = (
|
||||
operator.SelectTaskRelatedProducts,
|
||||
operator.VisualiseWorkScheduleDate,
|
||||
operator.VisualiseWorkScheduleDateRange,
|
||||
operator.RecalculateSchedule,
|
||||
operator.BlenderBIM_DatePicker,
|
||||
operator.BlenderBIM_DatePickerSetDate,
|
||||
operator.BlenderBIM_RedrawDatePicker,
|
||||
prop.WorkPlan,
|
||||
prop.BIMWorkPlanProperties,
|
||||
prop.Task,
|
||||
@@ -74,6 +78,7 @@ classes = (
|
||||
prop.WorkCalendar,
|
||||
prop.RecurrenceComponent,
|
||||
prop.BIMWorkCalendarProperties,
|
||||
prop.DatePickerProperties,
|
||||
ui.BIM_PT_work_plans,
|
||||
ui.BIM_PT_work_schedules,
|
||||
ui.BIM_PT_work_calendars,
|
||||
@@ -91,6 +96,7 @@ def register():
|
||||
bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties)
|
||||
bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties)
|
||||
bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties)
|
||||
bpy.types.Scene.DatePickerProperties = bpy.props.PointerProperty(type=prop.DatePickerProperties)
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
@@ -99,4 +105,5 @@ def unregister():
|
||||
del bpy.types.Scene.BIMWorkScheduleProperties
|
||||
del bpy.types.Scene.BIMTaskTreeProperties
|
||||
del bpy.types.Scene.BIMWorkCalendarProperties
|
||||
del bpy.types.Scene.DatePickerProperties
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import bpy
|
||||
import math
|
||||
import isodate
|
||||
import datetime
|
||||
@@ -5,8 +6,6 @@ from dateutil import parser
|
||||
from ifcopenshell.api.sequence.data import Data
|
||||
|
||||
|
||||
|
||||
|
||||
def derive_date(ifc_definition_id, attribute_name, date=None, is_earliest=False, is_latest=False):
|
||||
task = Data.tasks[ifc_definition_id]
|
||||
if task["TaskTime"]:
|
||||
@@ -56,3 +55,23 @@ def parse_duration(value):
|
||||
return isodate.parse_duration(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def canonicalise_time(time):
|
||||
if not time:
|
||||
return "-"
|
||||
return time.strftime("%d/%m/%y")
|
||||
|
||||
|
||||
def get_scene_prop(prop_path):
|
||||
prop = bpy.context.scene.get(prop_path.split(".")[0])
|
||||
for part in prop_path.split(".")[1:]:
|
||||
if part:
|
||||
prop = prop.get(part)
|
||||
return prop
|
||||
|
||||
|
||||
def set_scene_prop(prop_path, value):
|
||||
parent = get_scene_prop(prop_path[: prop_path.rfind(".")])
|
||||
prop = prop_path.split(".")[-1]
|
||||
parent[prop] = value
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import bpy
|
||||
import json
|
||||
import time
|
||||
import calendar
|
||||
import isodate
|
||||
import pystache
|
||||
import webbrowser
|
||||
@@ -13,7 +14,7 @@ import blenderbim.bim.helper
|
||||
import blenderbim.bim.module.sequence.helper as helper
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from dateutil import parser
|
||||
from dateutil import parser, relativedelta
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from ifcopenshell.api.sequence.data import Data
|
||||
@@ -660,6 +661,15 @@ class GenerateGanttChart(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.json = []
|
||||
self.sequence_type_map = {
|
||||
None: "FS",
|
||||
"START_START": "SS",
|
||||
"START_FINISH": "SF",
|
||||
"FINISH_START": "FS",
|
||||
"FINISH_FINISH": "FF",
|
||||
"USERDEFINED": "FS",
|
||||
"NOTDEFINED": "FS",
|
||||
}
|
||||
for task_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]:
|
||||
self.create_new_task_json(task_id)
|
||||
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
|
||||
@@ -670,23 +680,35 @@ class GenerateGanttChart(bpy.types.Operator):
|
||||
|
||||
def create_new_task_json(self, task_id):
|
||||
task = self.file.by_id(task_id)
|
||||
self.json.append(
|
||||
{
|
||||
"pID": task.id(),
|
||||
"pName": task.Name,
|
||||
"pStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
|
||||
"pEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
|
||||
"pPlanStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
|
||||
"pPlanEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
|
||||
"pClass": "ggroupblack",
|
||||
"pMile": 1 if task.IsMilestone else 0,
|
||||
"pComp": 0,
|
||||
"pGroup": 1,
|
||||
"pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
|
||||
"pOpen": 1,
|
||||
"pCost": 1,
|
||||
}
|
||||
data = {
|
||||
"pID": task.id(),
|
||||
"pName": task.Name,
|
||||
"pStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
|
||||
"pEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
|
||||
"pPlanStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
|
||||
"pPlanEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
|
||||
"pMile": 1 if task.IsMilestone else 0,
|
||||
"pComp": 0,
|
||||
"pGroup": 1 if task.IsNestedBy else 0,
|
||||
"pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
|
||||
"pOpen": 1,
|
||||
"pCost": 1,
|
||||
}
|
||||
if task.TaskTime and task.TaskTime.IsCritical:
|
||||
data["pClass"] = "gtaskred"
|
||||
elif data["pGroup"]:
|
||||
data["pClass"] = "ggroupblack"
|
||||
elif data["pMile"]:
|
||||
data["pClass"] = "gmilestone"
|
||||
else:
|
||||
data["pClass"] = "gtaskblue"
|
||||
data["pDepend"] = ",".join(
|
||||
[
|
||||
"{}{}".format(rel.RelatingProcess.id(), self.sequence_type_map[rel.SequenceType])
|
||||
for rel in task.IsSuccessorFrom or []
|
||||
]
|
||||
)
|
||||
self.json.append(data)
|
||||
for task_id in Data.tasks[task_id]["RelatedObjects"]:
|
||||
self.create_new_task_json(task_id)
|
||||
|
||||
@@ -1424,3 +1446,105 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator):
|
||||
"STARTED": round(self.start_frame + (((start - self.start) / self.duration) * self.total_frames)),
|
||||
"COMPLETED": round(self.start_frame + (((finish - self.start) / self.duration) * self.total_frames)),
|
||||
}
|
||||
|
||||
|
||||
class BlenderBIM_DatePicker(bpy.types.Operator):
|
||||
bl_label = "Date Picker"
|
||||
bl_idname = "bim.datepicker"
|
||||
display_date: bpy.props.StringProperty(name="Display Date")
|
||||
selected_date: bpy.props.StringProperty(name="Selected Date")
|
||||
target_prop: bpy.props.StringProperty(name="Target date prop to set")
|
||||
|
||||
def execute(self, context):
|
||||
helper.set_scene_prop(self.target_prop, self.selected_date)
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
self.selected_date = helper.get_scene_prop("DatePickerProperties.selected_date") or helper.canonicalise_time(
|
||||
datetime.now()
|
||||
)
|
||||
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
|
||||
current_month = (current_date.year, current_date.month)
|
||||
lines = calendar.monthcalendar(*current_month)
|
||||
month_title, week_titles = calendar.month(*current_month).splitlines()[:2]
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.prop(self, "selected_date")
|
||||
|
||||
split = layout.split()
|
||||
col = split.row()
|
||||
op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="")
|
||||
op.action = "previous"
|
||||
col = split.row()
|
||||
col.label(text=month_title.strip())
|
||||
col = split.row()
|
||||
col.alignment = "RIGHT"
|
||||
op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
|
||||
op.action = "next"
|
||||
|
||||
row = layout.row(align=True)
|
||||
for title in week_titles.split():
|
||||
col = row.column(align=True)
|
||||
col.alignment = "CENTER"
|
||||
col.label(text=title.strip())
|
||||
|
||||
for line in lines:
|
||||
row = layout.row(align=True)
|
||||
for i in line:
|
||||
col = row.column(align=True)
|
||||
if i == 0:
|
||||
col.label(text=" ")
|
||||
else:
|
||||
op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
|
||||
selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year)
|
||||
selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True)
|
||||
op.selected_date = helper.canonicalise_time(selected_date)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.display_date = helper.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now())
|
||||
context.scene.DatePickerProperties.display_date = self.display_date
|
||||
context.scene.DatePickerProperties.selected_date = self.display_date
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
|
||||
bl_label = "set date"
|
||||
bl_idname = "bim.datepicker_setdate"
|
||||
selected_date: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.DatePickerProperties.selected_date = self.selected_date
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderBIM_RedrawDatePicker(bpy.types.Operator):
|
||||
bl_label = "redraw datepicker window"
|
||||
bl_idname = "bim.redraw_datepicker"
|
||||
action: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
|
||||
|
||||
if self.action == "previous":
|
||||
date_to_set = current_date - relativedelta.relativedelta(months=1)
|
||||
elif self.action == "next":
|
||||
date_to_set = current_date + relativedelta.relativedelta(months=1)
|
||||
|
||||
context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RecalculateSchedule(bpy.types.Operator):
|
||||
bl_idname = "bim.recalculate_schedule"
|
||||
bl_label = "Recalculate Schedule"
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"sequence.recalculate_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule)
|
||||
)
|
||||
Data.load(self.file)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -104,6 +104,7 @@ def updateTaskTimeDateTime(self, context, startfinish):
|
||||
**{"task_time": task_time, "attributes": {startfinish_key: startfinish_datetime}},
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_task_properties(task=props.active_task_id)
|
||||
setattr(self, startfinish, canonicalise_time(startfinish_datetime))
|
||||
|
||||
|
||||
@@ -121,12 +122,13 @@ def updateTaskduration(self, context):
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task_time",
|
||||
self.file,
|
||||
**{"task_time":task_time, "attributes": {"ScheduleDuration": self.duration}},
|
||||
**{"task_time": task_time, "attributes": {"ScheduleDuration": self.duration}},
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
if props.active_task_id == self.ifc_definition_id:
|
||||
attribute = props.task_attributes.get("Duration")
|
||||
attribute.string_value = self.duration
|
||||
bpy.ops.bim.load_task_properties(task=props.active_task_id)
|
||||
|
||||
|
||||
def updateVisualisationStart(self, context):
|
||||
@@ -283,3 +285,8 @@ class BIMWorkCalendarProperties(PropertyGroup):
|
||||
)
|
||||
start_time: StringProperty(name="Start Time")
|
||||
end_time: StringProperty(name="End Time")
|
||||
|
||||
|
||||
class DatePickerProperties(PropertyGroup):
|
||||
display_date: StringProperty()
|
||||
selected_date: StringProperty()
|
||||
|
||||
@@ -2,6 +2,8 @@ import isodate
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.sequence.data import Data
|
||||
import blenderbim.bim.module.sequence.helper as helper
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BIM_PT_work_plans(Panel):
|
||||
@@ -112,6 +114,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.prop(self.props, "should_show_calendars", text="", icon="VIEW_ORTHO")
|
||||
row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO")
|
||||
row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id
|
||||
row.operator("bim.recalculate_schedule", text="", icon="FILE_REFRESH").work_schedule = work_schedule_id
|
||||
row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id
|
||||
row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL")
|
||||
elif self.props.active_work_schedule_id:
|
||||
@@ -133,8 +136,12 @@ class BIM_PT_work_schedules(Panel):
|
||||
|
||||
def draw_visualisation_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "visualisation_start", text="", icon="REW")
|
||||
row.prop(self.props, "visualisation_finish", text="", icon="FF")
|
||||
target_prop = "BIMWorkScheduleProperties.visualisation_start"
|
||||
op = row.operator("bim.datepicker", text=helper.get_scene_prop(target_prop), icon="REW")
|
||||
op.target_prop = target_prop
|
||||
target_prop = "BIMWorkScheduleProperties.visualisation_finish"
|
||||
op = row.operator("bim.datepicker", text=helper.get_scene_prop(target_prop), icon="FF")
|
||||
op.target_prop = target_prop
|
||||
op = row.operator("bim.visualise_work_schedule_date", text="", icon="RESTRICT_RENDER_OFF")
|
||||
op.work_schedule = self.props.active_work_schedule_id
|
||||
op = row.operator("bim.visualise_work_schedule_date_range", text="", icon="OUTLINER_OB_CAMERA")
|
||||
|
||||
@@ -38,7 +38,7 @@ class AssignContainer(bpy.types.Operator):
|
||||
|
||||
aggregate_collection = bpy.data.collections.get(related_element.name)
|
||||
|
||||
relating_structure_obj = IfcStore.id_map.get(relating_structure)
|
||||
relating_structure_obj = IfcStore.get_element(relating_structure)
|
||||
relating_collection = None
|
||||
if relating_structure_obj:
|
||||
relating_collection = bpy.data.collections.get(relating_structure_obj.name)
|
||||
|
||||
@@ -47,6 +47,7 @@ def draw_boundary_condition_editable_ui(layout, props):
|
||||
if attribute.is_optional:
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
|
||||
def draw_boundary_condition_read_only_ui(layout, boundary_condition_data):
|
||||
for key, value in boundary_condition_data.items():
|
||||
if key == "id" or key == "type" or value == None:
|
||||
@@ -59,7 +60,6 @@ def draw_boundary_condition_read_only_ui(layout, boundary_condition_data):
|
||||
row.label(text=str(value))
|
||||
|
||||
|
||||
|
||||
class BIM_PT_structural_boundary_conditions(Panel):
|
||||
bl_label = "IFC Structural Boundary Conditions"
|
||||
bl_idname = "BIM_PT_structural_boundary_conditions"
|
||||
@@ -231,7 +231,9 @@ class BIM_PT_structural_connection(Panel):
|
||||
row.prop(self.props, "ccs_z_angle", text="Z Angle")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_structural_connection_cs", text="Edit Connection CS", icon="GREASEPENCIL")
|
||||
row.operator(
|
||||
"bim.enable_editing_structural_connection_cs", text="Edit Connection CS", icon="GREASEPENCIL"
|
||||
)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO")
|
||||
@@ -417,7 +419,6 @@ class BIM_PT_structural_load_cases(Panel):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class BIM_UL_structural_activities(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
@@ -444,9 +445,7 @@ class BIM_PT_structural_loads(Panel):
|
||||
self.props = context.scene.BIMStructuralProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(
|
||||
text="{} Structural Loads Found".format(len(Data.structural_loads)), icon="GHOST_ENABLED"
|
||||
)
|
||||
row.label(text="{} Structural Loads Found".format(len(Data.structural_loads)), icon="GHOST_ENABLED")
|
||||
if self.props.is_editing_loads:
|
||||
row.prop(self.props, "structural_load_types", text="")
|
||||
row.operator("bim.add_structural_load", text="", icon="ADD").ifc_class = self.props.structural_load_types
|
||||
|
||||
@@ -11,9 +11,9 @@ def get_colour_settings(material):
|
||||
transparency = bsdf.inputs["Alpha"].default_value
|
||||
diffuse_colour = bsdf.inputs["Base Color"].default_value
|
||||
return {
|
||||
"SurfaceColour": material.diffuse_color,
|
||||
"Transparency": transparency,
|
||||
"DiffuseColour": diffuse_colour,
|
||||
"surface_colour": tuple(material.diffuse_color),
|
||||
"transparency": transparency,
|
||||
"diffuse_colour": tuple(diffuse_colour),
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class AddStyle(bpy.types.Operator):
|
||||
self.file = IfcStore.get_file()
|
||||
material = bpy.data.materials.get(self.material) if self.material else bpy.context.active_object.active_material
|
||||
settings = get_colour_settings(material)
|
||||
settings["Name"] = material.name
|
||||
settings["name"] = material.name
|
||||
settings["external_definition"] = None # TODO: Implement. See #1222
|
||||
style = ifcopenshell.api.run("style.add_style", self.file, **settings)
|
||||
material.BIMMaterialProperties.ifc_style_id = int(style.id())
|
||||
|
||||
@@ -5,6 +5,7 @@ import ifcopenshell.api
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.type.data import Data
|
||||
from ifcopenshell.api.geometry.data import Data as GeometryData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
|
||||
|
||||
class AssignType(bpy.types.Operator):
|
||||
@@ -31,6 +32,7 @@ class AssignType(bpy.types.Operator):
|
||||
)
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
GeometryData.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
MaterialData.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
representation_ids = GeometryData.products[oprops.ifc_definition_id]
|
||||
if not representation_ids:
|
||||
pass # TODO: clear geometry? Make void? Make none type?
|
||||
@@ -44,6 +46,7 @@ class AssignType(bpy.types.Operator):
|
||||
bpy.ops.bim.switch_representation(obj=related_object.name, ifc_definition_id=representation_id)
|
||||
|
||||
bpy.ops.bim.disable_editing_type(obj=related_object.name)
|
||||
MaterialData.load(self.file)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import blenderbim.bim.module.drawing
|
||||
from . import export_ifc
|
||||
from . import import_ifc
|
||||
from . import schema
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from mathutils import Vector, Matrix, Euler, geometry
|
||||
from math import radians, degrees, atan, tan, cos, sin
|
||||
@@ -26,6 +27,9 @@ class ExportIFC(bpy.types.Operator):
|
||||
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not IfcStore.get_file():
|
||||
self.report({"ERROR"}, "No IFC project is available for export - create or import a project first.")
|
||||
return {"FINISHED"}
|
||||
if bpy.context.scene.BIMProperties.ifc_file:
|
||||
self.filepath = bpy.context.scene.BIMProperties.ifc_file
|
||||
return self.execute(context)
|
||||
@@ -39,7 +43,7 @@ class ExportIFC(bpy.types.Operator):
|
||||
start = time.time()
|
||||
logger = logging.getLogger("ExportIFC")
|
||||
logging.basicConfig(
|
||||
filename=context.scene.BIMProperties.data_dir + "process.log", filemode="a", level=logging.DEBUG
|
||||
filename=os.path.join(context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG
|
||||
)
|
||||
extension = self.filepath.split(".")[-1]
|
||||
if extension == "ifczip":
|
||||
@@ -98,7 +102,7 @@ class ImportIFC(bpy.types.Operator, ImportHelper):
|
||||
start = time.time()
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
logging.basicConfig(
|
||||
filename=bpy.context.scene.BIMProperties.data_dir + "process.log", filemode="a", level=logging.DEBUG
|
||||
filename=os.path.join(bpy.context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG
|
||||
)
|
||||
|
||||
settings = import_ifc.IfcImportSettings.factory(context, self.filepath, logger)
|
||||
@@ -201,7 +205,7 @@ class FetchExternalMaterial(bpy.types.Operator):
|
||||
if location[-6:] != ".mpass":
|
||||
return {"FINISHED"}
|
||||
if not os.path.isabs(location):
|
||||
location = os.path.join(os.path.join(bpy.context.scene.BIMProperties.data_dir, location))
|
||||
location = os.path.join(bpy.context.scene.BIMProperties.data_dir, location)
|
||||
with open(location) as f:
|
||||
self.material_pass = json.load(f)
|
||||
if bpy.context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass:
|
||||
@@ -214,7 +218,7 @@ class FetchExternalMaterial(bpy.types.Operator):
|
||||
identification = bpy.context.active_object.active_material.BIMMaterialProperties.identification
|
||||
uri = self.material_pass[name]["uri"]
|
||||
if not os.path.isabs(uri):
|
||||
uri = os.path.join(os.path.join(bpy.context.scene.BIMProperties.data_dir, uri))
|
||||
uri = os.path.join(bpy.context.scene.BIMProperties.data_dir, uri)
|
||||
bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material"))
|
||||
for material in bpy.data.materials:
|
||||
if material.name == identification and material.library:
|
||||
|
||||
@@ -30,6 +30,18 @@ def getAttributeEnumValues(self, context):
|
||||
return [(e, e, "") for e in json.loads(self.enum_items)]
|
||||
|
||||
|
||||
def updateSchemaDir(self, context):
|
||||
import blenderbim.bim.schema
|
||||
|
||||
blenderbim.bim.schema.ifc.schema_dir = context.scene.BIMProperties.schema_dir
|
||||
|
||||
|
||||
def updateDataDir(self, context):
|
||||
import blenderbim.bim.schema
|
||||
|
||||
blenderbim.bim.schema.ifc.data_dir = context.scene.BIMProperties.data_dir
|
||||
|
||||
|
||||
def updateIfcFile(self, context):
|
||||
if context.scene.BIMProperties.ifc_file:
|
||||
IfcStore.file = None
|
||||
@@ -82,6 +94,7 @@ def getSubcontexts(self, context):
|
||||
"CoG",
|
||||
"Profile",
|
||||
"SurveyPoints",
|
||||
"Lighting",
|
||||
]
|
||||
for subcontext in subcontexts:
|
||||
subcontexts_enum.append((subcontext, subcontext, ""))
|
||||
@@ -150,8 +163,12 @@ class Attribute(PropertyGroup):
|
||||
|
||||
|
||||
class BIMProperties(PropertyGroup):
|
||||
schema_dir: StringProperty(default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory")
|
||||
data_dir: StringProperty(default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory")
|
||||
schema_dir: StringProperty(
|
||||
default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory", update=updateSchemaDir
|
||||
)
|
||||
data_dir: StringProperty(
|
||||
default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory", update=updateDataDir
|
||||
)
|
||||
ifc_file: StringProperty(name="IFC File", update=updateIfcFile)
|
||||
id_map: StringProperty(name="ID Map")
|
||||
guid_map: StringProperty(name="GUID Map")
|
||||
|
||||
@@ -10,8 +10,8 @@ cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
class IfcSchema:
|
||||
def __init__(self):
|
||||
self.schema_dir = Path(cwd).joinpath("schema") # TODO: make configurable
|
||||
self.data_dir = Path(cwd).joinpath("data") # TODO: make configurable
|
||||
self.schema_dir = Path(cwd).joinpath("schema")
|
||||
self.data_dir = Path(cwd).joinpath("data")
|
||||
# TODO: Make it less troublesome
|
||||
self.products = [
|
||||
"IfcElement",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
import numpy
|
||||
import importlib
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
@@ -14,6 +16,36 @@ def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings):
|
||||
if should_run_listeners:
|
||||
for listener in pre_listeners.get(".".join([ifc_key, usecase_path]), []):
|
||||
listener(usecase_path, ifc_file, **settings)
|
||||
|
||||
if None in registered_ifcs and ifc_key != registered_ifcs[None]:
|
||||
global_key = registered_ifcs[None]
|
||||
for listener in pre_listeners.get(".".join([global_key, usecase_path]), []):
|
||||
listener(usecase_path, ifc_file, **settings)
|
||||
|
||||
def serialise_entity_instance(entity):
|
||||
return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
|
||||
|
||||
vcs_settings = settings.copy()
|
||||
for key, value in settings.items():
|
||||
if isinstance(value, ifcopenshell.entity_instance):
|
||||
vcs_settings[key] = serialise_entity_instance(value)
|
||||
elif isinstance(value, numpy.ndarray):
|
||||
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
|
||||
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
|
||||
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
|
||||
if "add_representation" in usecase_path:
|
||||
pass
|
||||
# print(ifc_key, usecase_path, "{ ... settings too complex right now ... }")
|
||||
elif "owner." in usecase_path:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
# print(vcs_settings)
|
||||
# try:
|
||||
# print(ifc_key, usecase_path, json.dumps(vcs_settings))
|
||||
# except:
|
||||
# print(ifc_key, usecase_path, vcs_settings)
|
||||
|
||||
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
|
||||
module, usecase = usecase_path.split(".")
|
||||
usecase_class = getattr(getattr(getattr(ifcopenshell.api, module), usecase), "Usecase")
|
||||
@@ -26,13 +58,80 @@ def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings):
|
||||
if should_run_listeners:
|
||||
for listener in post_listeners.get(".".join([ifc_key, usecase_path]), []):
|
||||
listener(usecase_path, ifc_file, **settings)
|
||||
|
||||
if None in registered_ifcs and ifc_key != registered_ifcs[None]:
|
||||
global_key = registered_ifcs[None]
|
||||
for listener in post_listeners.get(".".join([global_key, usecase_path]), []):
|
||||
listener(usecase_path, ifc_file, **settings)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def add_pre_listener(usecase_path, ifc_file, callback):
|
||||
"""Add a pre listener
|
||||
There are 2 kind of listeners:
|
||||
when ifc file is defined the listener will only run for specified file
|
||||
when ifc file is None, the listener will run globally only based on usecase
|
||||
:param usecase_path: string, ifcopenshell api use case path
|
||||
:param ifc_file: ifc file object or None for global listener
|
||||
:param callback: callback function
|
||||
:return: ifc_key, uuid of listener, this is the prefix only, postfix is the usecase_path dot separated.
|
||||
"""
|
||||
ifc_key = registered_ifcs.setdefault(ifc_file, ifcopenshell.guid.new())
|
||||
pre_listeners.setdefault(".".join([ifc_key, usecase_path]), set()).add(callback)
|
||||
return ifc_key
|
||||
|
||||
|
||||
def add_post_listener(usecase_path, ifc_file, callback):
|
||||
"""Add a post listener
|
||||
There are 2 kind of listeners:
|
||||
when ifc file is defined the listener will only run for specified file
|
||||
when ifc file is None, the listener will run globally only based on usecase
|
||||
:param usecase_path: string, ifcopenshell api use case path
|
||||
:param ifc_file: ifc file object or None for global listener
|
||||
:param callback: callback function
|
||||
:return: ifc_key, uuid of listener, this is the prefix only, postfix is the usecase_path dot separated.
|
||||
"""
|
||||
ifc_key = registered_ifcs.setdefault(ifc_file, ifcopenshell.guid.new())
|
||||
post_listeners.setdefault(".".join([ifc_key, usecase_path]), set()).add(callback)
|
||||
return ifc_key
|
||||
|
||||
|
||||
def remove_pre_listener(callback, usecase_path=""):
|
||||
"""Remove a pre listener
|
||||
:param callback: callback function to remove
|
||||
:param usecase_path: string, optional, ifcopenshell api usecase path, may be prefixed with ifc_key, dot separated.
|
||||
:return:
|
||||
"""
|
||||
for listener_key, callbacks in pre_listeners.items():
|
||||
if not listener_key.endswith(usecase_path):
|
||||
continue
|
||||
to_remove = set()
|
||||
for fun in callbacks:
|
||||
if fun == callback:
|
||||
to_remove.add(callback)
|
||||
for callback in to_remove:
|
||||
pre_listeners[listener_key].remove(callback)
|
||||
|
||||
|
||||
def remove_post_listener(callback, usecase_path=""):
|
||||
"""Remove a post listener
|
||||
:param callback: callback function to remove
|
||||
:param usecase_path: string, optional ifcopenshell api usecase path, may be prefixed with ifc_key, dot separated.
|
||||
:return:
|
||||
"""
|
||||
for listener_key, callbacks in post_listeners.items():
|
||||
if not listener_key.endswith(usecase_path):
|
||||
continue
|
||||
to_remove = set()
|
||||
for fun in callbacks:
|
||||
if fun == callback:
|
||||
to_remove.add(callback)
|
||||
for callback in to_remove:
|
||||
post_listeners[listener_key].remove(callback)
|
||||
|
||||
|
||||
def remove_all_listeners():
|
||||
registered_ifcs.clear()
|
||||
pre_listeners.clear()
|
||||
post_listeners.clear()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
class Data:
|
||||
is_loaded = False
|
||||
boundaries = {}
|
||||
spaces = {}
|
||||
|
||||
@classmethod
|
||||
def purge(cls):
|
||||
cls.is_loaded = False
|
||||
cls.boundaries = {}
|
||||
cls.spaces = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, file):
|
||||
cls._file = file
|
||||
for boundary in cls._file.by_type("IfcRelSpaceBoundary"):
|
||||
data = boundary.get_info()
|
||||
data["RelatingSpace"] = data["RelatingSpace"].id() if data["RelatingSpace"] else None
|
||||
data["RelatedBuildingElement"] = (
|
||||
data["RelatedBuildingElement"].id() if data["RelatedBuildingElement"] else None
|
||||
)
|
||||
del data["ConnectionGeometry"]
|
||||
if cls._file.schema == "IFC2X3":
|
||||
pass
|
||||
else:
|
||||
if boundary.is_a("IfcRelSpaceBoundary1stLevel"):
|
||||
data["ParentBoundary"] = data["ParentBoundary"].id() if data["ParentBoundary"] else None
|
||||
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
|
||||
data["CorrespondingBoundary"] = (
|
||||
data["CorrespondingBoundary"].id() if data["CorrespondingBoundary"] else None
|
||||
)
|
||||
cls.boundaries[boundary.id()] = data
|
||||
cls.spaces.setdefault(data["RelatingSpace"], []).append(boundary.id())
|
||||
cls.is_loaded = True
|
||||
@@ -1,3 +1,6 @@
|
||||
import ifcopenshell.api
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
@@ -7,4 +10,13 @@ class Usecase:
|
||||
|
||||
def execute(self):
|
||||
# TODO: do a deep purge
|
||||
for inverse in self.file.get_inverse(self.settings["cost_item"]):
|
||||
if inverse.is_a("IfcRelNests"):
|
||||
if inverse.RelatingObject == self.settings["cost_item"]:
|
||||
for related_object in inverse.RelatedObjects:
|
||||
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=related_object)
|
||||
elif inverse.RelatedObjects == tuple(self.settings["cost_item"]):
|
||||
self.file.remove(inverse)
|
||||
elif inverse.is_a("IfcRelAssignsToControl"):
|
||||
self.file.remove(inverse)
|
||||
self.file.remove(self.settings["cost_item"])
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import ifcopenshell.util.unit
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
|
||||
Z_AXIS = Vector((0, 0, 1))
|
||||
X_AXIS = Vector((1, 0, 0))
|
||||
EPSILON = 1e-6
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
@@ -45,7 +49,26 @@ class Usecase:
|
||||
elif self.settings["context"].ContextType == "Plan":
|
||||
return self.create_plan_representation()
|
||||
return self.create_variable_representation()
|
||||
|
||||
def should_triangulate_face(self, face, threshold=EPSILON):
|
||||
vz = face.normal
|
||||
co = face.verts[0].co
|
||||
if vz.length < 0.5:
|
||||
return True
|
||||
if abs(vz.z) < 0.5:
|
||||
vx = vz.cross(Z_AXIS)
|
||||
else:
|
||||
vx = vz.cross(X_AXIS)
|
||||
vy = vx.cross(vz)
|
||||
tM = Matrix([
|
||||
[vx.x, vy.x, vz.x, co.x],
|
||||
[vx.y, vy.y, vz.y, co.y],
|
||||
[vx.z, vy.z, vz.z, co.z],
|
||||
[0, 0, 0, 1]
|
||||
]).inverted()
|
||||
|
||||
return any([abs((tM @ v.co).z) > threshold for v in face.verts])
|
||||
|
||||
def evaluate_geometry(self):
|
||||
for modifier in self.settings["blender_object"].modifiers:
|
||||
if modifier.type == "BOOLEAN":
|
||||
@@ -54,17 +77,11 @@ class Usecase:
|
||||
mesh = self.settings["blender_object"].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
|
||||
if not self.settings["should_force_triangulation"]:
|
||||
bmesh.ops.dissolve_limit(
|
||||
bm,
|
||||
angle_limit=0.00174533, # 1 degree
|
||||
use_dissolve_boundaries=False,
|
||||
verts=bm.verts[:],
|
||||
edges=bm.edges[:],
|
||||
delimit={"MATERIAL"},
|
||||
)
|
||||
if self.settings["should_force_triangulation"]:
|
||||
faces = bm.faces
|
||||
else:
|
||||
faces = [f for f in bm.faces if self.should_triangulate_face(f)]
|
||||
bmesh.ops.triangulate(bm, faces=faces)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
del bm
|
||||
@@ -99,6 +116,8 @@ class Usecase:
|
||||
return self.create_curve3d_representation()
|
||||
elif self.settings["context"].ContextIdentifier == "SurveyPoints":
|
||||
return self.create_geometric_curve_set_representation()
|
||||
elif self.settings["context"].ContextIdentifier == "Lighting":
|
||||
return self.create_lighting_representation()
|
||||
|
||||
def create_plan_representation(self):
|
||||
if self.settings["context"].ContextIdentifier == "Annotation":
|
||||
@@ -127,6 +146,28 @@ class Usecase:
|
||||
elif self.settings["context"].ContextIdentifier == "SurveyPoints":
|
||||
pass
|
||||
|
||||
def create_lighting_representation(self):
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
"LightSource",
|
||||
[self.create_light_source()],
|
||||
)
|
||||
|
||||
def create_light_source(self):
|
||||
if self.settings["geometry"].type == "POINT":
|
||||
return self.create_light_source_positional()
|
||||
|
||||
def create_light_source_positional(self):
|
||||
return self.file.create_entity(
|
||||
"IfcLightSourcePositional",
|
||||
**{
|
||||
"LightColour": self.file.createIfcColourRgb(None, *self.settings["geometry"].color),
|
||||
"Position": self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
|
||||
"Radius": self.convert_si_to_unit(self.settings["geometry"].shadow_soft_size),
|
||||
},
|
||||
)
|
||||
|
||||
def create_text_representation(self):
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
|
||||
@@ -8,7 +8,7 @@ class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"AxisCurve": None, # A Blender object
|
||||
"axis_curve": None, # A Blender object
|
||||
"grid_axis": None,
|
||||
}
|
||||
for key, value in settings.items():
|
||||
@@ -24,8 +24,8 @@ class Usecase:
|
||||
grid = [i for i in self.file.get_inverse(self.settings["grid_axis"]) if i.is_a("IfcGrid")][0]
|
||||
points = [
|
||||
Matrix(ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)).inverted()
|
||||
@ (self.settings["AxisCurve"].matrix_world @ v.co)
|
||||
for v in self.settings["AxisCurve"].data.vertices[0:2]
|
||||
@ (self.settings["axis_curve"].matrix_world @ v.co)
|
||||
for v in self.settings["axis_curve"].data.vertices[0:2]
|
||||
]
|
||||
self.settings["grid_axis"].AxisCurve = self.file.createIfcPolyline(
|
||||
[
|
||||
|
||||
@@ -2,20 +2,20 @@ class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"AxisTag": "A",
|
||||
"SameSense": True,
|
||||
"UVWAxes": "UAxes", # Choose which axes
|
||||
"Grid": None,
|
||||
"axis_tag": "A",
|
||||
"same_sense": True,
|
||||
"uvw_axes": "UAxes", # Choose which axes
|
||||
"grid": None,
|
||||
}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
element = self.file.create_entity("IfcGridAxis", **{
|
||||
"AxisTag": self.settings["AxisTag"],
|
||||
"SameSense": self.settings["SameSense"]
|
||||
"axis_tag": self.settings["axis_tag"],
|
||||
"SameSense": self.settings["same_sense"]
|
||||
})
|
||||
axes = list(getattr(self.settings["Grid"], self.settings["UVWAxes"]) or [])
|
||||
axes = list(getattr(self.settings["grid"], self.settings["uvw_axes"]) or [])
|
||||
axes.append(element)
|
||||
setattr(self.settings["Grid"], self.settings["UVWAxes"], axes)
|
||||
setattr(self.settings["grid"], self.settings["uvw_axes"], axes)
|
||||
return element
|
||||
|
||||
@@ -4,9 +4,9 @@ import ifcopenshell
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"Name": "Unnamed"}
|
||||
self.settings = {"name": "Unnamed"}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
return self.file.create_entity("IfcMaterial", **{"Name": self.settings["Name"] or "Unnamed"})
|
||||
return self.file.create_entity("IfcMaterial", **{"Name": self.settings["name"] or "Unnamed"})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
@@ -10,6 +11,9 @@ class Usecase:
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
material = ifcopenshell.util.element.get_material(self.settings["product"])
|
||||
if material:
|
||||
ifcopenshell.api.run("material.unassign_material", self.file, product=self.settings["product"])
|
||||
if self.settings["type"] == "IfcMaterial":
|
||||
self.assign_ifc_material()
|
||||
elif self.settings["type"] == "IfcMaterialConstituentSet":
|
||||
|
||||
@@ -9,6 +9,15 @@ class Usecase:
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
for association in self.settings["product"].HasAssociations:
|
||||
if association.is_a("IfcRelAssociatesMaterial"):
|
||||
self.file.remove(association)
|
||||
for rel in self.settings["product"].HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesMaterial"):
|
||||
if rel.RelatingMaterial.is_a("IfcMaterialLayerSetUsage") or rel.RelatingMaterial.is_a(
|
||||
"IfcMaterialProfileSetUsage"
|
||||
):
|
||||
self.file.remove(rel.RelatingMaterial)
|
||||
if len(rel.RelatedObjects) == 1:
|
||||
self.file.remove(rel)
|
||||
continue
|
||||
related_objects = set(rel.RelatedObjects)
|
||||
related_objects.remove(self.settings["product"])
|
||||
rel.RelatedObjects = list(related_objects)
|
||||
|
||||
@@ -2,14 +2,16 @@ class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"Identification": "APTR",
|
||||
"Name": "Aperture Science",
|
||||
"identification": "APTR",
|
||||
"name": "Aperture Science",
|
||||
}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.settings["Id"] = self.settings["Identification"]
|
||||
del self.settings["Identification"]
|
||||
return self.file.create_entity("IfcOrganization", **self.settings)
|
||||
data = {"Name": self.settings["name"]}
|
||||
if self.file.schema == "IFC2X3":
|
||||
data["Id"] = self.settings["identification"]
|
||||
else:
|
||||
data["Identification"] = self.settings["identification"]
|
||||
return self.file.create_entity("IfcOrganization", **data)
|
||||
|
||||
@@ -2,15 +2,17 @@ class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"Identification": "HSeldon",
|
||||
"FamilyName": "Seldon",
|
||||
"GivenName": "Hari",
|
||||
"identification": "HSeldon",
|
||||
"family_name": "Seldon",
|
||||
"given_name": "Hari",
|
||||
}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.settings["Id"] = self.settings["Identification"]
|
||||
del self.settings["Identification"]
|
||||
return self.file.create_entity("IfcPerson", **self.settings)
|
||||
data = {"FamilyName": self.settings["family_name"], "GivenName": self.settings["given_name"]}
|
||||
if self.file.schema == "IFC2X3":
|
||||
data["Id"] = self.settings["identification"]
|
||||
else:
|
||||
data["Identification"] = self.settings["identification"]
|
||||
return self.file.create_entity("IfcPerson", **data)
|
||||
|
||||
@@ -22,4 +22,5 @@ class Usecase:
|
||||
self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||
self.file.wrapped_data.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||
self.file.wrapped_data.header.file_name.authorization = "Nobody"
|
||||
self.file.wrapped_data.header.file_description.description = ('ViewDefinition[DesignTransferView]',)
|
||||
return self.file
|
||||
|
||||
@@ -4,7 +4,7 @@ import ifcopenshell
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"product": None, "Name": None}
|
||||
self.settings = {"product": None, "name": None}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
@@ -13,18 +13,18 @@ class Usecase:
|
||||
for rel in self.settings["product"].IsDefinedBy or []:
|
||||
if (
|
||||
rel.is_a("IfcRelDefinesByProperties")
|
||||
and rel.RelatingPropertyDefinition.Name == self.settings["Name"]
|
||||
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
|
||||
):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
pset = self.file.create_entity(
|
||||
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
|
||||
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
|
||||
)
|
||||
self.file.create_entity(
|
||||
"IfcRelDefinesByProperties",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
# TODO: owner history
|
||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
||||
"RelatedObjects": [self.settings["product"]],
|
||||
"RelatingPropertyDefinition": pset,
|
||||
}
|
||||
@@ -32,25 +32,25 @@ class Usecase:
|
||||
return pset
|
||||
elif self.settings["product"].is_a("IfcTypeObject"):
|
||||
for definition in self.settings["product"].HasPropertySets or []:
|
||||
if definition.Name == self.settings["Name"]:
|
||||
if definition.Name == self.settings["name"]:
|
||||
return definition
|
||||
|
||||
pset = self.file.create_entity(
|
||||
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
|
||||
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
|
||||
)
|
||||
has_property_sets = list(self.settings["product"].HasPropertySets or [])
|
||||
has_property_sets.append(pset)
|
||||
self.settings["product"].HasPropertySets = has_property_sets
|
||||
return pset
|
||||
elif self.settings["product"].is_a("IfcMaterialDefinition"):
|
||||
for definition in self.settings["product"].HasPropertySets or []:
|
||||
if definition.Name == self.settings["Name"]:
|
||||
for definition in self.settings["product"].HasProperties or []:
|
||||
if definition.Name == self.settings["name"]:
|
||||
return definition
|
||||
|
||||
return self.file.create_entity(
|
||||
"IfcMaterialProperties",
|
||||
**{
|
||||
"Name": self.settings["Name"],
|
||||
"Name": self.settings["name"],
|
||||
"Material": self.settings["product"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"product": None, "Name": None}
|
||||
self.settings = {"product": None, "name": None}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
@@ -13,18 +14,18 @@ class Usecase:
|
||||
for rel in self.settings["product"].IsDefinedBy or []:
|
||||
if (
|
||||
rel.is_a("IfcRelDefinesByProperties")
|
||||
and rel.RelatingPropertyDefinition.Name == self.settings["Name"]
|
||||
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
|
||||
):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
qto = self.file.create_entity(
|
||||
"IfcElementQuantity", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
|
||||
"IfcElementQuantity", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
|
||||
)
|
||||
self.file.create_entity(
|
||||
"IfcRelDefinesByProperties",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
# TODO: owner history
|
||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
||||
"RelatedObjects": [self.settings["product"]],
|
||||
"RelatingPropertyDefinition": qto,
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ class Data:
|
||||
@classmethod
|
||||
def add_pset(cls, pset, product_id):
|
||||
data = pset.get_info()
|
||||
del data["OwnerHistory"]
|
||||
del data["HasProperties"]
|
||||
if not pset.is_a("IfcMaterialProperties"):
|
||||
del data["OwnerHistory"]
|
||||
del data["HasProperties"]
|
||||
if hasattr(pset, "HasProperties"):
|
||||
props = pset.HasProperties or []
|
||||
elif hasattr(pset, "Properties"):
|
||||
|
||||
@@ -5,7 +5,7 @@ import ifcopenshell.util.pset
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"pset": None, "Name": None, "Properties": {}}
|
||||
self.settings = {"pset": None, "name": None, "properties": {}}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
@@ -17,8 +17,8 @@ class Usecase:
|
||||
self.extend_pset_with_new_properties(new_properties)
|
||||
|
||||
def update_pset_name(self):
|
||||
if self.settings["Name"]:
|
||||
self.settings["pset"].Name = self.settings["Name"]
|
||||
if self.settings["name"]:
|
||||
self.settings["pset"].Name = self.settings["name"]
|
||||
|
||||
def load_pset_template(self):
|
||||
# TODO: add IFC2X3 PsetQto template support
|
||||
@@ -30,19 +30,19 @@ class Usecase:
|
||||
self.update_existing_property(prop)
|
||||
|
||||
def update_existing_property(self, prop):
|
||||
if prop.Name not in self.settings["Properties"]:
|
||||
if prop.Name not in self.settings["properties"]:
|
||||
return
|
||||
value = self.settings["Properties"][prop.Name]
|
||||
value = self.settings["properties"][prop.Name]
|
||||
if value is None:
|
||||
prop.NominalValue = None
|
||||
else:
|
||||
primary_measure_type = self.get_primary_measure_type(prop.Name, previous_value=prop.NominalValue)
|
||||
prop.NominalValue = self.file.create_entity(primary_measure_type, value)
|
||||
del self.settings["Properties"][prop.Name]
|
||||
del self.settings["properties"][prop.Name]
|
||||
|
||||
def add_new_properties(self):
|
||||
properties = []
|
||||
for name, value in self.settings["Properties"].items():
|
||||
for name, value in self.settings["properties"].items():
|
||||
if value is None:
|
||||
continue
|
||||
primary_measure_type = self.get_primary_measure_type(name)
|
||||
|
||||
@@ -4,7 +4,7 @@ import ifcopenshell
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"qto": None, "Name": None, "Properties": {}}
|
||||
self.settings = {"qto": None, "name": None, "properties": {}}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
@@ -16,8 +16,8 @@ class Usecase:
|
||||
self.extend_qto_with_new_properties(new_properties)
|
||||
|
||||
def update_qto_name(self):
|
||||
if self.settings["Name"]:
|
||||
self.settings["qto"].Name = self.settings["Name"]
|
||||
if self.settings["name"]:
|
||||
self.settings["qto"].Name = self.settings["name"]
|
||||
|
||||
def load_qto_template(self):
|
||||
# TODO: add IFC2X3 PsetQto template support
|
||||
@@ -29,16 +29,16 @@ class Usecase:
|
||||
self.update_existing_property(prop)
|
||||
|
||||
def update_existing_property(self, prop):
|
||||
if prop.Name not in self.settings["Properties"]:
|
||||
if prop.Name not in self.settings["properties"]:
|
||||
return
|
||||
value = self.settings["Properties"][prop.Name]
|
||||
value = self.settings["properties"][prop.Name]
|
||||
if prop.is_a("IfcPhysicalSimpleQuantity"):
|
||||
prop[3] = float(value) if value else None
|
||||
del self.settings["Properties"][prop.Name]
|
||||
del self.settings["properties"][prop.Name]
|
||||
|
||||
def add_new_properties(self):
|
||||
properties = []
|
||||
for name, value in self.settings["Properties"].items():
|
||||
for name, value in self.settings["properties"].items():
|
||||
if value is None:
|
||||
continue
|
||||
property_type = self.get_canonical_property_type(name)
|
||||
|
||||
@@ -30,24 +30,48 @@ class Usecase:
|
||||
and self.settings["task_time"].ScheduleDuration
|
||||
and self.settings["task_time"].ScheduleStart
|
||||
):
|
||||
start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart)
|
||||
current_date = datetime.date(start.year, start.month, start.day)
|
||||
duration = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration).days
|
||||
self.calculate_finish()
|
||||
elif "ScheduleStart" in self.settings["attributes"].keys() and self.settings["task_time"].ScheduleDuration:
|
||||
self.calculate_finish()
|
||||
elif "ScheduleFinish" in self.settings["attributes"].keys() and self.settings["task_time"].ScheduleStart:
|
||||
self.calculate_duration()
|
||||
|
||||
task = [e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")]
|
||||
if not task:
|
||||
return
|
||||
else:
|
||||
task = task[0]
|
||||
def calculate_finish(self):
|
||||
start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart)
|
||||
current_date = datetime.date(start.year, start.month, start.day)
|
||||
duration = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration).days
|
||||
|
||||
calendar = ifcopenshell.util.sequence.derive_calendar(task)
|
||||
calendar = self.get_calendar()
|
||||
|
||||
while duration >= 0:
|
||||
if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not calendar:
|
||||
duration -= 1
|
||||
elif ifcopenshell.util.sequence.is_working_day(current_date, calendar):
|
||||
duration -= 1
|
||||
current_date += datetime.timedelta(days=1)
|
||||
while duration >= 0:
|
||||
if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not calendar:
|
||||
duration -= 1
|
||||
elif ifcopenshell.util.sequence.is_working_day(current_date, calendar):
|
||||
duration -= 1
|
||||
current_date += datetime.timedelta(days=1)
|
||||
|
||||
current_date -= datetime.timedelta(days=1)
|
||||
self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(current_date, "IfcDateTime")
|
||||
current_date -= datetime.timedelta(days=1)
|
||||
self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(current_date, "IfcDateTime")
|
||||
|
||||
def calculate_duration(self):
|
||||
start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart)
|
||||
finish = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleFinish)
|
||||
current_date = datetime.date(start.year, start.month, start.day)
|
||||
finish_date = datetime.date(finish.year, finish.month, finish.day)
|
||||
calendar = self.get_calendar()
|
||||
duration = datetime.timedelta()
|
||||
while current_date < finish_date:
|
||||
if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not calendar:
|
||||
duration += datetime.timedelta(days=1)
|
||||
elif ifcopenshell.util.sequence.is_working_day(current_date, calendar):
|
||||
duration += datetime.timedelta(days=1)
|
||||
current_date += datetime.timedelta(days=1)
|
||||
self.settings["task_time"].ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration")
|
||||
|
||||
def get_calendar(self):
|
||||
task = [e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")]
|
||||
if not task:
|
||||
return
|
||||
else:
|
||||
task = task[0]
|
||||
return ifcopenshell.util.sequence.derive_calendar(task)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import datetime
|
||||
import networkx as nx
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.date
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {"work_schedule": None}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
|
||||
def execute(self):
|
||||
# I learned everything about project dependency calcs from this YouTube playlist:
|
||||
# https://www.youtube.com/playlist?list=PLLRADeJk4TCK-X5vJY8focpFkau1MR7do
|
||||
import time
|
||||
|
||||
self.time = time.time()
|
||||
self.build_network_graph()
|
||||
print("{} :: {:.2f}".format("Build network", time.time() - self.time))
|
||||
print("TOTAL NODES AND EDGES", len(self.g.nodes), len(self.g.edges))
|
||||
self.time = time.time()
|
||||
self.calculate_all_paths_sorted_by_duration()
|
||||
print("{} :: {:.2f}".format("Calc all paths", time.time() - self.time))
|
||||
self.time = time.time()
|
||||
self.calculate_critical_path()
|
||||
print("{} :: {:.2f}".format("Calc critical", time.time() - self.time))
|
||||
self.time = time.time()
|
||||
self.calculate_forward_pass()
|
||||
print("{} :: {:.2f}".format("Forward", time.time() - self.time))
|
||||
self.time = time.time()
|
||||
self.calculate_backward_pass()
|
||||
print("{} :: {:.2f}".format("Backward", time.time() - self.time))
|
||||
self.time = time.time()
|
||||
self.update_task_times()
|
||||
print("{} :: {:.2f}".format("Update", time.time() - self.time))
|
||||
self.time = time.time()
|
||||
print("DONE!", self.critical_paths)
|
||||
|
||||
def build_network_graph(self):
|
||||
self.sequence_type_map = {
|
||||
None: "FS",
|
||||
"START_START": "SS",
|
||||
"START_FINISH": "SF",
|
||||
"FINISH_START": "FS",
|
||||
"FINISH_FINISH": "FF",
|
||||
"USERDEFINED": "FS",
|
||||
"NOTDEFINED": "FS",
|
||||
}
|
||||
self.g = nx.DiGraph()
|
||||
self.edges = []
|
||||
self.g.add_node("start", duration=0)
|
||||
self.g.add_node("finish", duration=0)
|
||||
for rel in self.settings["work_schedule"].Controls:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if not related_object.is_a("IfcTask"):
|
||||
continue
|
||||
self.add_node(related_object)
|
||||
self.g.add_edges_from(self.edges)
|
||||
|
||||
def add_node(self, task):
|
||||
if task.IsNestedBy:
|
||||
for rel in task.IsNestedBy:
|
||||
[self.add_node(o) for o in rel.RelatedObjects]
|
||||
return
|
||||
|
||||
if task.TaskTime and task.TaskTime.ScheduleDuration:
|
||||
duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration).days
|
||||
else:
|
||||
duration = 0
|
||||
self.g.add_node(task.id(), duration=duration)
|
||||
self.edges.extend(
|
||||
[
|
||||
(
|
||||
rel.RelatingProcess.id(),
|
||||
rel.RelatedProcess.id(),
|
||||
{
|
||||
"lag_time": 0
|
||||
if not rel.TimeLag or not rel.TimeLag.LagValue
|
||||
else ifcopenshell.util.date.ifc2datetime(rel.TimeLag.LagValue.wrappedValue).days,
|
||||
"type": self.sequence_type_map[rel.SequenceType],
|
||||
},
|
||||
)
|
||||
for rel in task.IsSuccessorFrom or []
|
||||
]
|
||||
)
|
||||
predecessor_types = [rel.SequenceType for rel in task.IsSuccessorFrom]
|
||||
successor_types = [rel.SequenceType for rel in task.IsPredecessorTo]
|
||||
# This is less correct, but less computation
|
||||
if not task.IsSuccessorFrom:
|
||||
self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"}))
|
||||
# This I think is more correct, but unlikely to be necessary in most
|
||||
# graphs, and simply adds more computation time
|
||||
# if not predecessor_types or (
|
||||
# "FINISH_START" not in predecessor_types and "START_START" not in predecessor_types
|
||||
# ):
|
||||
# self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"}))
|
||||
if not successor_types or ("FINISH_START" not in successor_types and "FINISH_FINISH" not in successor_types):
|
||||
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FS"}))
|
||||
|
||||
def calculate_all_paths_sorted_by_duration(self):
|
||||
self.paths = []
|
||||
total_paths = 0
|
||||
for path in nx.algorithms.simple_paths.all_simple_paths(self.g, "start", "finish"):
|
||||
total_duration = 0
|
||||
for i, node in enumerate(path):
|
||||
try:
|
||||
next_edge = self.g[node][path[i + 1]]
|
||||
prev_edge = self.g[path[i - 1]][node]
|
||||
except:
|
||||
continue
|
||||
if prev_edge["type"][1] == "S" and next_edge["type"][0] == "F":
|
||||
total_duration += self.g.nodes[node]["duration"]
|
||||
elif prev_edge["type"][1] == "F" and next_edge["type"][0] == "S":
|
||||
total_duration -= self.g.nodes[node]["duration"]
|
||||
total_duration += next_edge["lag_time"]
|
||||
self.paths.append((total_duration, path))
|
||||
total_paths += 1
|
||||
if total_paths % 2000 == 0:
|
||||
print(total_paths, total_duration)
|
||||
self.paths = list(reversed(sorted(self.paths, key=lambda x: x[0])))
|
||||
|
||||
def calculate_critical_path(self):
|
||||
self.critical_paths = [p for p in self.paths if p[0] == self.paths[0][0]]
|
||||
|
||||
def calculate_forward_pass(self):
|
||||
for path_data in self.paths:
|
||||
path = path_data[1]
|
||||
for i, node in enumerate(path):
|
||||
data = self.g.nodes[node]
|
||||
|
||||
if node == "start":
|
||||
data["early_start"] = 0
|
||||
else:
|
||||
prev_node = self.g.nodes[path[i - 1]]
|
||||
prev_edge = self.g[path[i - 1]][node]
|
||||
if prev_edge["type"] == "FS" and data.get("early_start") is None:
|
||||
data["early_start"] = prev_node["early_finish"] + prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "FF" and data.get("early_finish") is None:
|
||||
data["early_finish"] = prev_node["early_finish"] + prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "SS" and data.get("early_start") is None:
|
||||
data["early_start"] = prev_node["early_start"] + prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "SF" and data.get("early_finish") is None:
|
||||
data["early_finish"] = prev_node["early_start"] + prev_edge["lag_time"]
|
||||
|
||||
if data.get("early_finish") is None:
|
||||
data["early_finish"] = data["early_start"] + data["duration"]
|
||||
elif data.get("early_start") is None:
|
||||
data["early_start"] = data["early_finish"] - data["duration"]
|
||||
#print(data)
|
||||
|
||||
def calculate_backward_pass(self):
|
||||
critical_duration = self.critical_paths[0][0]
|
||||
for path_data in self.paths:
|
||||
path = list(reversed(path_data[1]))
|
||||
for i, node in enumerate(path):
|
||||
data = self.g.nodes[node]
|
||||
|
||||
if node == "finish":
|
||||
data["late_finish"] = critical_duration
|
||||
else:
|
||||
prev_node = self.g.nodes[path[i - 1]]
|
||||
prev_edge = self.g[node][path[i - 1]]
|
||||
if prev_edge["type"] == "FS" and data.get("late_finish") is None:
|
||||
data["late_finish"] = prev_node["late_start"] - prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "FF" and data.get("late_finish") is None:
|
||||
data["late_finish"] = prev_node["late_finish"] - prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "SS" and data.get("late_start") is None:
|
||||
data["late_start"] = prev_node["late_start"] - prev_edge["lag_time"]
|
||||
elif prev_edge["type"] == "SF" and data.get("late_start") is None:
|
||||
data["late_start"] = prev_node["late_finish"] - prev_edge["lag_time"]
|
||||
|
||||
if data.get("late_finish") is None:
|
||||
data["late_finish"] = data["late_start"] + data["duration"]
|
||||
elif data.get("late_start") is None:
|
||||
data["late_start"] = data["late_finish"] - data["duration"]
|
||||
|
||||
data["total_float"] = data["late_finish"] - data["early_finish"]
|
||||
#print("DATA", data)
|
||||
|
||||
def update_task_times(self):
|
||||
for ifc_definition_id in self.g.nodes:
|
||||
data = self.g.nodes[ifc_definition_id]
|
||||
if not data["duration"]:
|
||||
continue
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task_time",
|
||||
self.file,
|
||||
task_time=self.file.by_id(ifc_definition_id).TaskTime,
|
||||
attributes={
|
||||
"TotalFloat": ifcopenshell.util.date.datetime2ifc(
|
||||
datetime.timedelta(days=data["total_float"]), "IfcDuration"
|
||||
),
|
||||
"IsCritical": data["total_float"] == 0,
|
||||
},
|
||||
)
|
||||
@@ -25,4 +25,6 @@ class Usecase:
|
||||
ifcopenshell.api.run("sequence.remove_task", self.file, task=related_object)
|
||||
elif inverse.RelatedObjects == tuple(self.settings["task"]):
|
||||
self.file.remove(inverse)
|
||||
elif inverse.is_a("IfcRelAssignsToControl"):
|
||||
self.file.remove(inverse)
|
||||
self.file.remove(self.settings["task"])
|
||||
|
||||
@@ -2,15 +2,11 @@ class Usecase:
|
||||
def __init__(self, file, **settings):
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"Name": "Name",
|
||||
"SurfaceColour": [], # RGB
|
||||
"DiffuseColour": [], # RGB
|
||||
"Transparency": 0,
|
||||
"external_definition": {
|
||||
"Location": None,
|
||||
"Identification": None,
|
||||
"Name": "Name"
|
||||
},
|
||||
"name": "Name",
|
||||
"surface_colour": [], # RGB
|
||||
"diffuse_colour": [], # RGB
|
||||
"transparency": 0,
|
||||
"external_definition": {"location": None, "identification": None, "name": "Name"},
|
||||
}
|
||||
for key, value in settings.items():
|
||||
self.settings[key] = value
|
||||
@@ -20,22 +16,26 @@ class Usecase:
|
||||
if self.settings["external_definition"]:
|
||||
styles.append(self.create_externally_defined_surface_style())
|
||||
# Name is filled out because Revit treats this incorrectly as the material name
|
||||
return self.file.createIfcSurfaceStyle(self.settings["Name"], "BOTH", styles)
|
||||
return self.file.createIfcSurfaceStyle(self.settings["name"], "BOTH", styles)
|
||||
|
||||
def create_surface_style_rendering(self):
|
||||
return self.file.create_entity("IfcSurfaceStyleRendering", **{
|
||||
"SurfaceColour": self.create_colour_rgb(self.settings["SurfaceColour"]),
|
||||
"Transparency": (self.settings["Transparency"] - 1) * -1,
|
||||
"ReflectanceMethod": "NOTDEFINED",
|
||||
"DiffuseColour": self.create_colour_rgb(self.settings["DiffuseColour"])
|
||||
})
|
||||
return self.file.create_entity(
|
||||
"IfcSurfaceStyleRendering",
|
||||
**{
|
||||
"SurfaceColour": self.create_colour_rgb(self.settings["surface_colour"]),
|
||||
"Transparency": (self.settings["transparency"] - 1) * -1,
|
||||
"ReflectanceMethod": "NOTDEFINED",
|
||||
"DiffuseColour": self.create_colour_rgb(self.settings["diffuse_colour"]),
|
||||
}
|
||||
)
|
||||
|
||||
def create_externally_defined_surface_style(self):
|
||||
self.file.create_entity(
|
||||
"IfcExternallyDefinedSurfaceStyle", **{
|
||||
"Location": self.settings["Location"],
|
||||
"Identification": self.settings["Identification"],
|
||||
"Name": self.settings["Name"],
|
||||
"IfcExternallyDefinedSurfaceStyle",
|
||||
**{
|
||||
"Location": self.settings["location"],
|
||||
"Identification": self.settings["identification"],
|
||||
"Name": self.settings["name"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {
|
||||
"style": None,
|
||||
"SurfaceColour": [], # RGB
|
||||
"DiffuseColour": [], # RGB
|
||||
"Transparency": 0,
|
||||
"surface_colour": [], # RGB
|
||||
"diffuse_colour": [], # RGB
|
||||
"transparency": 0,
|
||||
"external_definition": {
|
||||
"Location": None,
|
||||
"Identification": None,
|
||||
"Name": "Name"
|
||||
"location": None,
|
||||
"identification": None,
|
||||
"name": "Name"
|
||||
},
|
||||
}
|
||||
for key, value in settings.items():
|
||||
@@ -20,20 +20,20 @@ class Usecase:
|
||||
for element in self.file.traverse(self.settings["style"]):
|
||||
if element.is_a("IfcSurfaceStyleShading"):
|
||||
if element.SurfaceColour:
|
||||
self.update_colour_rgb(element.SurfaceColour, self.settings["SurfaceColour"])
|
||||
self.update_colour_rgb(element.SurfaceColour, self.settings["surface_colour"])
|
||||
else:
|
||||
element.SurfaceColour = self.create_colour_rgb(self.settings["SurfaceColour"])
|
||||
element.Transparency = (self.settings["Transparency"] - 1) * -1
|
||||
element.SurfaceColour = self.create_colour_rgb(self.settings["surface_colour"])
|
||||
element.Transparency = (self.settings["transparency"] - 1) * -1
|
||||
if element.is_a("IfcSurfaceStyleRendering"):
|
||||
if element.DiffuseColour:
|
||||
self.update_colour_rgb(element.DiffuseColour, self.settings["DiffuseColour"])
|
||||
self.update_colour_rgb(element.DiffuseColour, self.settings["diffuse_colour"])
|
||||
else:
|
||||
element.DiffuseColour = self.create_colour_rgb(self.settings["DiffuseColour"])
|
||||
element.DiffuseColour = self.create_colour_rgb(self.settings["diffuse_colour"])
|
||||
# TODO: Move to separate usecase
|
||||
#if element.is_a("IfcExternallyDefinedSurfaceStyle"):
|
||||
# element.Location = self.settings["Location"]
|
||||
# element.Identification = self.settings["Identification"]
|
||||
# element.Name = self.settings["Name"]
|
||||
# element.Location = self.settings["location"]
|
||||
# element.Identification = self.settings["identification"]
|
||||
# element.Name = self.settings["name"]
|
||||
# has_external_definition = True
|
||||
#if not has_external_definition:
|
||||
# styles = list(self.settings["style"].Styles)
|
||||
@@ -43,18 +43,18 @@ class Usecase:
|
||||
|
||||
def create_surface_style_rendering(self):
|
||||
return self.file.create_entity("IfcSurfaceStyleRendering", **{
|
||||
"SurfaceColour": self.create_colour_rgb(self.settings["SurfaceColour"]),
|
||||
"Transparency": (self.settings["Transparency"] - 1) * -1,
|
||||
"SurfaceColour": self.create_colour_rgb(self.settings["surface_colour"]),
|
||||
"Transparency": (self.settings["transparency"] - 1) * -1,
|
||||
"ReflectanceMethod": "NOTDEFINED",
|
||||
"DiffuseColour": self.create_colour_rgb(self.settings["DiffuseColour"])
|
||||
"DiffuseColour": self.create_colour_rgb(self.settings["diffuse_colour"])
|
||||
})
|
||||
|
||||
def create_externally_defined_surface_style(self):
|
||||
self.file.create_entity(
|
||||
"IfcExternallyDefinedSurfaceStyle", **{
|
||||
"Location": self.settings["Location"],
|
||||
"Identification": self.settings["Identification"],
|
||||
"Name": self.settings["Name"],
|
||||
"Location": self.settings["location"],
|
||||
"Identification": self.settings["identification"],
|
||||
"Name": self.settings["name"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class Usecase:
|
||||
@@ -54,6 +55,7 @@ class Usecase:
|
||||
)
|
||||
|
||||
self.map_representations()
|
||||
self.map_material_usages()
|
||||
|
||||
def map_representations(self):
|
||||
if not self.settings["relating_type"].RepresentationMaps:
|
||||
@@ -79,3 +81,20 @@ class Usecase:
|
||||
self.file,
|
||||
**{"product": self.settings["related_object"], "representation": mapped_representation}
|
||||
)
|
||||
|
||||
def map_material_usages(self):
|
||||
type_material = ifcopenshell.util.element.get_material(self.settings["relating_type"])
|
||||
if type_material.is_a("IfcMaterialLayerSet"):
|
||||
ifcopenshell.api.run(
|
||||
"material.assign_material",
|
||||
self.file,
|
||||
product=self.settings["related_object"],
|
||||
type="IfcMaterialLayerSetUsage",
|
||||
)
|
||||
elif type_material.is_a("IfcMaterialProfileSet"):
|
||||
ifcopenshell.api.run(
|
||||
"material.assign_material",
|
||||
self.file,
|
||||
product=self.settings["related_object"],
|
||||
type="IfcMaterialProfileSetUsage",
|
||||
)
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ else:
|
||||
test_cases = []
|
||||
failed = []
|
||||
|
||||
# Create the ouput directory
|
||||
# Create the output directory
|
||||
cwd = os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())))
|
||||
os.chdir(cwd)
|
||||
if not os.path.exists("output"): os.mkdir("output")
|
||||
|
||||
Reference in New Issue
Block a user