Compare commits

...

35 Commits

Author SHA1 Message Date
Dion Moult 8338cc72db New styles on sequencing gantt chart to show dependency arrows, critical path, task groups, and milestones 2021-06-02 17:31:55 +10:00
Dion Moult 67b636f5fb New API usecase to recalculate schedule times, including:
* Critical path
* Total float
* Early start
* Early finish
* Late start
* Late finish
2021-06-02 17:30:05 +10:00
Jesusbill 3a754c8ecc fix of the fix n add_pset 2021-06-01 16:33:47 +02:00
Dion Moult d64d7cf27a Dumb slabs now roundtrip the Blender solidify modifier 2021-05-31 17:43:42 +10:00
Dion Moult 6ff4c95ac7 Dumb slabs now auto update thickness based on layer thickness and type changes. 2021-05-31 15:27:22 +10:00
Dion Moult 0b6af5f848 Experimental usecase serialisation function, in preparation for client-server systems and version control. 2021-05-31 14:18:24 +10:00
Dion Moult 3ded4dff5f Parametric engine is now registered on load, so you don't need to manually activate it 2021-05-31 14:17:33 +10:00
Dion Moult 452631b997 Updating either a duration, start, or finish date for a task now auto-updates the other attribute 2021-05-31 12:42:02 +10:00
Dion Moult c041dd13ad Fix bug where users saw a scary error message when exporting with no IFC data 2021-05-31 09:19:04 +10:00
Jesusbill 994ccb94f4 fix bug in adding psets for materials 2021-05-30 13:32:59 +02:00
Dion Moult c7ba643ef7 Fix material unassignment greedy purging bug, and auto assign usages of typed layer / profile sets. 2021-05-30 21:03:50 +10:00
Dion Moult 0b6f3113a7 Changing types of a dumb wall now auto updates their wall thicknesses 2021-05-30 21:03:50 +10:00
s-leger 730efcae49 Bugfix Set changed size during iteration (#1506)
Fix for remove  RuntimeError: Set changed size during iteration
Add docstring
2021-05-29 22:03:00 +10:00
Dion Moult 62b297757e Dumb slabs now are marked as parametric and inherit thickness from the slab type 2021-05-29 14:59:02 +10:00
s-leger 0533a00631 Allow global listeners (#1500)
* Allow global listeners

Add "global" listeners when ifc_file is None, listeners are run whatever the file.
This way allow to register listeners before a file exists, and "global" listeners.

Add listener returns the ifc_key
Remove listeners allow optional filtering by usecase_path / ifc_key.usecase_path.

* Update __init__.py

Fix was only removing the first listener found.
2021-05-29 09:06:38 +10:00
Dion Moult 7fe2e93dea Fix #1491. Bug where drawings couldn't have a slash in their names. 2021-05-28 17:26:00 +10:00
Dion Moult acc9bd736f Fix #1048. Switching drawings now works when in local view. 2021-05-28 16:09:42 +10:00
Dion Moult 478612b6ae Fix #976. Bug where project data dir was not very customisable without running into issues. 2021-05-28 15:58:19 +10:00
Dion Moult a24d683613 Fix #1396. New walls are selected when created. 2021-05-28 14:50:36 +10:00
Dion Moult 0b5d7cdbf3 Fix #1455. Bump IfcOpenShell. 2021-05-28 14:36:48 +10:00
Dion Moult b8e20ec1a5 Reimplement raster underlays when generating drawings. See #1153. 2021-05-28 14:33:46 +10:00
Dion Moult 1cd243f13c Fix #1462. Standardise IfcOpenShell API to use Pythonic argument names. 2021-05-28 10:59:39 +10:00
s-leger 0a2adbb632 Implement remove pre / pos / all listeners (#1498)
Remove listeners, passing callback method.
Will not handle file pre_load cleanup / post_load setup here to prevent any blender's dependancy.
2021-05-28 10:07:56 +10:00
Dion Moult 8882c99455 Implement editing IFC header metadata. See #747. 2021-05-28 09:50:55 +10:00
Dion Moult 7890ded698 Fix #1429. MVDs now reflect the full DesignTransferView by default. 2021-05-28 09:49:57 +10:00
bosonprojets 7d2d9b2bcf purging of cost and task inverse relationships in ifcopenshell.api, and amendement to cost item values editing UX in BlenderBIM 2021-05-28 00:24:01 +01:00
s-leger 75ae501ff9 Evaluate coplanar vertex in faces (#1489)
The idea is to only triangulate non co-planar faces when required.

Timings for evaluation with matrix based algo.
IfcRoof/Roof found 3927 of 116178 polygons: 0.366638 sec
IfcWall/Wall.012 found 0 of 6 polygons: 0.000046 sec

So should be acceptable for common use cases
But with single precision in blender we may still be over occ precision with a 1e-6 epsilon.
2021-05-28 08:57:22 +10:00
luz paz 880fbe9533 Fix misc. source comment typos
Found via `codespell`
2021-05-27 09:40:06 +02:00
Dion Moult c2684f4fbf New split wall tool 2021-05-27 16:33:34 +10:00
Dion Moult afdc1e6679 New feature to export point lights (IfcLightSourcePositional) into IFC 2021-05-27 13:55:44 +10:00
Dion Moult 8935a17844 Really barebones UI to list space boundaries. 2021-05-27 13:54:18 +10:00
Dion Moult 93c59e81c6 Minor code review for BCF XML 3.0 implementation with TestPrab 2021-05-26 18:00:21 +10:00
Dion Moult d423274e62 New wall flip tool with hotkey. Wall aligner now allows flipped walls. 2021-05-26 17:20:34 +10:00
Dion Moult 7958dd837b New wall tool with hotkeys for rapid wall editing 2021-05-26 15:45:45 +10:00
Long e42a638220 implement DatePicker, visualisation time demo (#1488) 2021-05-26 14:59:20 +10:00
78 changed files with 2025 additions and 635 deletions
+2 -2
View File
@@ -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}")
+2 -2
View File
@@ -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"])
+1 -1
View File
@@ -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
View File
@@ -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"])
+1 -1
View File
@@ -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
View File
@@ -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)
+9 -9
View 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
+20
View File
@@ -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):
@@ -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"}
+8 -4
View File
@@ -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:
+19 -2
View File
@@ -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")
+2 -2
View File
@@ -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
View File
@@ -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")