mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 20:45:59 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ba79dc105 | |||
| 52c686b656 | |||
| 64d98f1295 | |||
| ceb2c4075c | |||
| 166e7d52ec | |||
| 644c2652ea | |||
| 63b201fd95 | |||
| d70616056c | |||
| a65e376886 | |||
| a985c70f0c | |||
| 0d09e02e7e | |||
| 23e3551f27 | |||
| 885bca0865 | |||
| 0f874dad51 | |||
| 107873cf6e | |||
| 4a8f5af7ba | |||
| 83084aeb7b | |||
| 51640ccc28 | |||
| aefbc134ea | |||
| 29e1d70334 | |||
| 9118e529e6 | |||
| 8ee0a1611d | |||
| 2af4388f40 | |||
| 93a2d18ea6 | |||
| fc49859ea4 | |||
| c0ac9cb380 | |||
| d5cc6c3c8e | |||
| 9b22b5000f | |||
| 3b69a4cc78 | |||
| adf54efcc5 | |||
| f6962eb58e | |||
| 11601d6532 | |||
| be2131d62c | |||
| 3ffbf46ac2 | |||
| 3943107eec | |||
| 94e3f75341 | |||
| 8bc8b950e7 | |||
| 05a95ac666 | |||
| f016c803e4 | |||
| bac0dd37fa | |||
| 75a37cf737 | |||
| bb7e9c4610 |
@@ -22,26 +22,34 @@ from . import ui, operator, prop
|
||||
classes = (
|
||||
operator.ColourByRelatedBuildingElement,
|
||||
operator.DisableEditingBoundary,
|
||||
operator.DisableEditingBoundaryGeometry,
|
||||
operator.EditBoundaryAttributes,
|
||||
operator.EditBoundaryGeometry,
|
||||
operator.EnableEditingBoundary,
|
||||
operator.EnableEditingBoundaryGeometry,
|
||||
operator.HideBoundaries,
|
||||
operator.LoadBoundary,
|
||||
operator.LoadProjectSpaceBoundaries,
|
||||
operator.LoadSpaceBoundaries,
|
||||
operator.LoadBoundary,
|
||||
operator.SelectRelatedElementBoundaries,
|
||||
operator.SelectProjectBoundaries,
|
||||
operator.SelectRelatedElementBoundaries,
|
||||
operator.SelectRelatedElementTypeBoundaries,
|
||||
operator.SelectSpaceBoundaries,
|
||||
operator.ShowBoundaries,
|
||||
operator.UpdateBoundaryGeometry,
|
||||
ui.BIM_PT_Boundary,
|
||||
ui.BIM_PT_SceneBoundaries,
|
||||
ui.BIM_PT_SpaceBoundaries,
|
||||
prop.BIMBoundaryProperties,
|
||||
prop.BIMObjectBoundaryProperties,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
|
||||
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
|
||||
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMBoundaryProperties
|
||||
del bpy.types.Object.bim_boundary_properties
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
class BoundaryDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
selected_tris = []
|
||||
unselected_vertices = []
|
||||
unselected_edges = []
|
||||
unselected_tris = []
|
||||
|
||||
for boundary in context.scene.BIMBoundaryProperties.boundaries:
|
||||
obj = boundary.obj
|
||||
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
|
||||
continue
|
||||
|
||||
if obj.mode == "EDIT":
|
||||
continue # A profile decorator or something else is used here.
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
obj.data.calc_loop_triangles()
|
||||
|
||||
if obj.select_get():
|
||||
offset = len(selected_vertices)
|
||||
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
else:
|
||||
offset = len(unselected_vertices)
|
||||
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
@@ -24,8 +24,10 @@ import ifcopenshell.api
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
|
||||
|
||||
|
||||
def get_boundaries_collection(blender_space):
|
||||
@@ -38,6 +40,20 @@ def get_boundaries_collection(blender_space):
|
||||
return boundaries_collection
|
||||
|
||||
|
||||
def disable_editing_boundary_geometry(context):
|
||||
ProfileDecorator.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
old_mesh = obj.data
|
||||
loader = Loader()
|
||||
obj.data = loader.create_mesh(element)
|
||||
tool.Geometry.delete_data(old_mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self):
|
||||
self.ifc_file = None
|
||||
@@ -376,3 +392,132 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary_geometry"
|
||||
bl_label = "Enable Editing Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
|
||||
surface = element.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
tool.Model.import_surface(surface, obj)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context))
|
||||
if not bpy.app.background:
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_boundary_geometry"
|
||||
bl_label = "Edit Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
ProfileDecorator.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
|
||||
surface = tool.Model.export_surface(obj)
|
||||
|
||||
if not surface:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
ProfileDecorator.install(
|
||||
context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context)
|
||||
)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return
|
||||
|
||||
old_surface = element.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
for inverse in tool.Ifc.get().get_inverse(old_surface):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_surface, surface)
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface)
|
||||
|
||||
old_mesh = obj.data
|
||||
loader = Loader()
|
||||
obj.data = loader.create_mesh(element)
|
||||
tool.Geometry.delete_data(old_mesh)
|
||||
|
||||
|
||||
class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_boundary_geometry"
|
||||
bl_label = "Disable Editing Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
return disable_editing_boundary_geometry(context)
|
||||
|
||||
|
||||
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.show_boundaries"
|
||||
bl_label = "Show Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMBoundaryProperties
|
||||
loader = Loader()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not getattr(element, "BoundedBy", None):
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
for rel in element.BoundedBy or []:
|
||||
boundary_obj = loader.load_boundary(rel, context.active_object)
|
||||
new = props.boundaries.add()
|
||||
new.obj = boundary_obj
|
||||
BoundaryDecorator.install(bpy.context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_boundaries"
|
||||
bl_label = "Hide Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
to_delete = set()
|
||||
spaces = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
spaces.add(element)
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
spaces.add(element.RelatingSpace)
|
||||
|
||||
for element in spaces:
|
||||
for boundary in element.BoundedBy or []:
|
||||
boundary_obj = tool.Ifc.get_object(boundary)
|
||||
if boundary_obj:
|
||||
to_delete.add(boundary_obj)
|
||||
for boundary_obj in to_delete:
|
||||
tool.Ifc.unlink(obj=boundary_obj)
|
||||
bpy.data.objects.remove(boundary_obj)
|
||||
context.scene.BIMBoundaryProperties.boundaries.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from blenderbim.bim.prop import ObjProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
@@ -52,9 +53,13 @@ def element_filter(self, object):
|
||||
return False
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
class BIMObjectBoundaryProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
||||
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
||||
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
|
||||
@@ -46,7 +46,7 @@ class CadTool(WorkSpaceTool):
|
||||
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
def draw_settings(context, layout, workspace_tool):
|
||||
obj = context.active_object
|
||||
if not obj or not obj.data:
|
||||
return
|
||||
@@ -54,14 +54,19 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
if element.is_a("IfcProfileDef"):
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
row.operator("bim.edit_boundary_geometry", text="Save Profile")
|
||||
row.operator("bim.disable_editing_boundary_geometry", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -255,28 +260,30 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
if tool.Ifc.get_entity(bpy.context.active_object):
|
||||
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
|
||||
if element.is_a("IfcProfileDef"):
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
elif element.is_a("IfcProfileDef"):
|
||||
bpy.ops.bim.edit_boundary_geometry()
|
||||
else:
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_railing_path()
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_railing_path()
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
|
||||
else:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
|
||||
@@ -32,6 +32,7 @@ def refresh():
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
DecoratorData.layerset_cache = {}
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -155,6 +156,7 @@ class DecoratorData:
|
||||
# stores 1 type of data per object
|
||||
data = {}
|
||||
cut_cache = {}
|
||||
layerset_cache = {}
|
||||
|
||||
# used by Ifc Annotations with ObjectType = "BATTING"
|
||||
@classmethod
|
||||
@@ -226,9 +228,11 @@ class DecoratorData:
|
||||
|
||||
props = obj.BIMTextProperties
|
||||
# getting font size
|
||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
# use `regular` as default
|
||||
if classes:
|
||||
|
||||
# get font size
|
||||
if classes := pset_data.get("Classes", None):
|
||||
classes_split = classes.split()
|
||||
# prioritize smaller font sizes just like in svg
|
||||
font_size_type = next(
|
||||
@@ -238,6 +242,9 @@ class DecoratorData:
|
||||
font_size_type = "regular"
|
||||
font_size = FONT_SIZES[font_size_type]
|
||||
|
||||
# get symbol
|
||||
symbol = pset_data.get("Symbol", None)
|
||||
|
||||
# other attributes
|
||||
props_literals = props.literals
|
||||
props_literals_n = len(props.literals)
|
||||
@@ -255,7 +262,7 @@ class DecoratorData:
|
||||
|
||||
literals_data.append(literal_data)
|
||||
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size}
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol}
|
||||
cls.data[obj.name] = text_data
|
||||
return text_data
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ class BaseDecorator:
|
||||
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
color = context.scene.DocProperties.decorations_colour
|
||||
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
|
||||
|
||||
fmt = GPUVertFormat()
|
||||
fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT")
|
||||
@@ -341,7 +341,7 @@ class BaseDecorator:
|
||||
|
||||
dpi = context.preferences.system.dpi
|
||||
|
||||
color = context.scene.DocProperties.decorations_colour
|
||||
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
|
||||
|
||||
ang = -Vector((1, 0)).angle_signed(text_dir)
|
||||
cos = math.cos(ang)
|
||||
@@ -437,12 +437,16 @@ class BaseDecorator:
|
||||
props = obj.BIMTextProperties
|
||||
text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj)
|
||||
literals_data = text_data["Literals"]
|
||||
symbol = text_data["Symbol"]
|
||||
text_scale = 1.0
|
||||
|
||||
# draw asterisk symbol to visually indicate that there are multiple literals
|
||||
if len(literals_data) > 1:
|
||||
# draw asterisk symbol to indicate that there is some symbol that's not shown in viewport
|
||||
if symbol:
|
||||
verts = [text_world_position]
|
||||
idxs = [(0, 0)]
|
||||
self.draw_lines(context, obj, verts, idxs)
|
||||
# NOTE: for now we assume that scale is uniform
|
||||
text_scale = obj.scale.x
|
||||
|
||||
line_i = 0
|
||||
for literal_data in literals_data:
|
||||
@@ -457,7 +461,7 @@ class BaseDecorator:
|
||||
gap=0,
|
||||
center=False,
|
||||
vcenter=False,
|
||||
font_size_mm=text_data["FontSize"],
|
||||
font_size_mm=text_data["FontSize"] * text_scale,
|
||||
line_no=line_i,
|
||||
box_alignment=box_alignment,
|
||||
)
|
||||
@@ -1910,8 +1914,8 @@ class CutDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def __call__(self, context):
|
||||
self.model_props = context.scene.BIMModelProperties
|
||||
selected_elements_color = self.model_props.decorator_color_selected
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
@@ -1993,6 +1997,8 @@ class CutDecorator:
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts:
|
||||
return verts, edges
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
@@ -2004,14 +2010,35 @@ class CutDecorator:
|
||||
return verts, edges
|
||||
|
||||
def slice_layersets(self, context, obj, cut_verts, cut_edges):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
imat = obj.matrix_world.inverted()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if "LAYER" not in (tool.Model.get_usage_type(element) or ""):
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts is not None:
|
||||
return verts, edges
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
layers = self.get_layer_data(element)
|
||||
|
||||
if not layers:
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
minx = min([co[0] for co in obj.bound_box])
|
||||
maxx = max([co[0] for co in obj.bound_box])
|
||||
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
|
||||
@@ -2106,6 +2133,7 @@ class CutDecorator:
|
||||
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
|
||||
offset += len(linestring)
|
||||
|
||||
DecoratorData.layerset_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def bisect_mesh(self, obj, bm, camera):
|
||||
@@ -2140,6 +2168,10 @@ class CutDecorator:
|
||||
usage = ifcopenshell.util.element.get_material(element)
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
layer_set = usage.ForLayerSet
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return # No use slicing if there's only one layer
|
||||
|
||||
total_thickness = layer_set.TotalThickness
|
||||
half_thickness = total_thickness / 2
|
||||
min_layers = []
|
||||
@@ -2191,22 +2223,30 @@ class CutDecorator:
|
||||
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
|
||||
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
|
||||
for rel in wall.ConnectedTo:
|
||||
# How do you join to a non layered element? Not sure.
|
||||
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
|
||||
continue
|
||||
if rel.RelatingConnectionType == "ATPATH":
|
||||
connections["ATPATH"].append(
|
||||
self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
else:
|
||||
connections[rel.RelatingConnectionType] = self.get_connection_metadata(
|
||||
obj, rel.RelatedElement, centerline, min_edge, max_edge
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections[rel.RelatingConnectionType] = metadata
|
||||
for rel in wall.ConnectedFrom:
|
||||
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
|
||||
continue
|
||||
# We only consider ATPATH since in this situation, we have the
|
||||
# priority. The non-priority wall never has any layers that need to
|
||||
# "turn a corner".
|
||||
if rel.RelatedConnectionType == "ATPATH":
|
||||
connections["ATPATH"].append(
|
||||
self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
|
||||
for connection in connections["ATPATH"]:
|
||||
if connection["angle"] > 0:
|
||||
@@ -2216,9 +2256,10 @@ class CutDecorator:
|
||||
return connections
|
||||
|
||||
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
|
||||
imat = obj.matrix_world.inverted()
|
||||
rel_obj = tool.Ifc.get_object(rel_element)
|
||||
layers = self.get_layer_data(rel_element)
|
||||
if not layers:
|
||||
return
|
||||
minx = min([co[0] for co in rel_obj.bound_box])
|
||||
maxx = max([co[0] for co in rel_obj.bound_box])
|
||||
rel_centerline = [
|
||||
@@ -2227,7 +2268,11 @@ class CutDecorator:
|
||||
]
|
||||
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
|
||||
rel_centerline = [v.to_2d() for v in rel_centerline]
|
||||
intersection, _ = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
if intersection:
|
||||
intersection, _ = intersection
|
||||
else:
|
||||
return
|
||||
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
|
||||
if closest_centerline_point == rel_centerline[1]:
|
||||
rel_centerline = [rel_centerline[1], rel_centerline[0]]
|
||||
|
||||
@@ -829,6 +829,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
ifc = tool.Ifc.get()
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
||||
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
||||
continue
|
||||
|
||||
classes = self.get_svg_classes(element)
|
||||
classes.append("cut")
|
||||
|
||||
@@ -865,7 +869,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if is_closed_polygon:
|
||||
el.getparent().remove(el)
|
||||
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
try:
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
||||
merged_polygons = polygons
|
||||
|
||||
if type(merged_polygons) == shapely.MultiPolygon:
|
||||
merged_polygons = merged_polygons.geoms
|
||||
|
||||
@@ -312,9 +312,6 @@ class DocProperties(PropertyGroup):
|
||||
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
|
||||
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
|
||||
decorations_colour: FloatVectorProperty(
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
|
||||
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
|
||||
titleblocks_dir: StringProperty(
|
||||
|
||||
@@ -673,7 +673,6 @@ class SvgWriter:
|
||||
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
|
||||
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
|
||||
|
||||
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
|
||||
classes = self.get_attribute_classes(text_obj)
|
||||
classes_str = " ".join(classes)
|
||||
|
||||
@@ -687,7 +686,13 @@ class SvgWriter:
|
||||
# if there is a symbol with template text fields
|
||||
# then we just populate it's fields with the data from text literals
|
||||
if template_text_fields:
|
||||
symbol_xml.attrib["transform"] = f"translate({', '.join(map(str, text_position_svg))})"
|
||||
# NOTE: for now we assume that scale is uniform
|
||||
symbol_transform = (
|
||||
f"translate({', '.join(map(str, text_position_svg))})"
|
||||
f" rotate({angle})"
|
||||
f" scale({text_obj.scale.x})"
|
||||
)
|
||||
symbol_xml.attrib["transform"] = symbol_transform
|
||||
symbol_xml.attrib.pop("id")
|
||||
# note: zip makes sure that we iterate over the shortest list
|
||||
for field, text_literal in zip(template_text_fields, text_literals):
|
||||
@@ -699,6 +704,7 @@ class SvgWriter:
|
||||
if not symbol_svg or not template_text_fields:
|
||||
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
|
||||
|
||||
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
|
||||
for text_literal in text_literals:
|
||||
# after pretty indentation some redundant spaces can occur in svg tags
|
||||
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
|
||||
@@ -732,7 +738,6 @@ class SvgWriter:
|
||||
add_text_tag(True)
|
||||
add_text_tag(False)
|
||||
|
||||
|
||||
def draw_break_annotations(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -38,7 +38,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
face = None
|
||||
@@ -62,7 +62,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -90,7 +90,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -292,7 +292,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -389,7 +389,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
faces = bm.faces
|
||||
|
||||
@@ -720,6 +720,11 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
|
||||
if context.active_object:
|
||||
context.active_object.select_set(True)
|
||||
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element.is_a("IfcRelSpaceBoundary"):
|
||||
return bpy.ops.bim.enable_editing_boundary_geometry()
|
||||
|
||||
for obj in objs:
|
||||
if not obj:
|
||||
continue
|
||||
@@ -834,6 +839,11 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
if not tool.Ifc.get():
|
||||
return {"FINISHED"}
|
||||
|
||||
if context.active_object:
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element.is_a("IfcRelSpaceBoundary"):
|
||||
return bpy.ops.bim.edit_boundary_geometry()
|
||||
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
|
||||
self.edited_objs = []
|
||||
|
||||
@@ -88,7 +88,7 @@ class CommitChanges(bpy.types.Operator):
|
||||
repo
|
||||
and repo.head.is_detached
|
||||
and (
|
||||
not tool.is_valid_ref_format(props.new_branch_name)
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
|
||||
or props.new_branch_name in [branch.name for branch in repo.branches]
|
||||
)
|
||||
):
|
||||
|
||||
@@ -96,6 +96,7 @@ classes = (
|
||||
slab.ResetVertex,
|
||||
slab.SetArcIndex,
|
||||
space.GenerateSpace,
|
||||
space.GenerateSpacesFromWalls,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
|
||||
@@ -59,6 +59,7 @@ class AuthoringData:
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail()
|
||||
cls.data["is_voidable_element"] = cls.is_voidable_element()
|
||||
cls.data["has_visible_openings"] = cls.has_visible_openings()
|
||||
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
|
||||
cls.data["active_class"] = cls.active_class()
|
||||
cls.data["active_material_usage"] = cls.active_material_usage()
|
||||
cls.data["active_representation_type"] = cls.active_representation_type()
|
||||
@@ -69,7 +70,10 @@ class AuthoringData:
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialStructureElementType")
|
||||
else:
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names.extend([d.name() for d in declarations])
|
||||
|
||||
@@ -161,6 +165,17 @@ class AuthoringData:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def has_visible_boundaries(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
if element.is_a("IfcRelSpaceBoundary"):
|
||||
return True
|
||||
for boundary in getattr(element, "BoundedBy", []):
|
||||
if tool.Ifc.get_object(boundary):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def active_class(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -177,7 +192,7 @@ class AuthoringData:
|
||||
def active_representation_type(cls):
|
||||
if bpy.context.active_object:
|
||||
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
|
||||
if representation:
|
||||
if representation and representation.is_a("IfcShapeRepresentation"):
|
||||
return representation.RepresentationType
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -82,14 +82,14 @@ class ProfileDecorator:
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
faces_color = transparent_color(self.model_props.decorator_color_special)
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
self.model_props = context.scene.BIMModelProperties
|
||||
selected_elements_color = self.model_props.decorator_color_selected
|
||||
unselected_elements_color = self.model_props.decorator_color_unselected
|
||||
special_elements_color = self.model_props.decorator_color_special
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
obj = context.active_object
|
||||
|
||||
@@ -214,8 +214,8 @@ class ProfileDecorator:
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
|
||||
self.draw_batch("POINTS", special_vertices, self.model_props.decorator_color_special)
|
||||
self.draw_batch("POINTS", selected_vertices, self.model_props.decorator_color_selected)
|
||||
self.draw_batch("POINTS", special_vertices, special_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
# Draw arcs
|
||||
arc_centroids = []
|
||||
|
||||
@@ -657,7 +657,7 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
if not element or not getattr(element, "HasOpenings", None):
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
@@ -682,7 +682,6 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
to_delete = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -694,7 +693,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
if opening_obj:
|
||||
to_delete.add(opening_obj)
|
||||
for opening_obj in to_delete:
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
tool.Ifc.unlink(obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
tool.Model.clear_scene_openings()
|
||||
return {"FINISHED"}
|
||||
@@ -793,10 +792,10 @@ class DecorationsHandler:
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
self.model_props = context.scene.BIMModelProperties
|
||||
selected_elements_color = self.model_props.decorator_color_selected
|
||||
unselected_elements_color = self.model_props.decorator_color_unselected
|
||||
special_elements_color = self.model_props.decorator_color_special
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
|
||||
@@ -54,7 +54,7 @@ class DumbProfileGenerator:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.depth = props.extrusion_depth * self.unit_scale
|
||||
self.depth = props.extrusion_depth
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||
|
||||
@@ -141,33 +141,6 @@ class BIMModelProperties(PropertyGroup):
|
||||
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
|
||||
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
decorator_color_selected: bpy.props.FloatVectorProperty(
|
||||
name="Selected Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(0.545, 0.863, 0, 1), # green
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of selected verts/edges (used in profile editing mode)",
|
||||
)
|
||||
decorator_color_unselected: bpy.props.FloatVectorProperty(
|
||||
name="Not Selected Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(1, 1, 1, 1), # green
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of not selected verts/edges (used in profile editing mode)",
|
||||
)
|
||||
decorator_color_special: bpy.props.FloatVectorProperty(
|
||||
name="Special Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(0.157, 0.565, 1, 1), # blue
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
|
||||
)
|
||||
|
||||
|
||||
class BIMArrayProperties(PropertyGroup):
|
||||
@@ -499,12 +472,12 @@ class BIMRailingProperties(PropertyGroup):
|
||||
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
|
||||
)
|
||||
cap_types = (
|
||||
("none", "none", ""),
|
||||
("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""),
|
||||
("TO_END_POST", "TO_END_POST", ""),
|
||||
("TO_FLOOR", "TO_FLOOR", ""),
|
||||
("TO_WALL", "TO_WALL", ""),
|
||||
("180", "180", ""),
|
||||
("to_wall", "to_wall", ""),
|
||||
("to_floor", "to_floor", ""),
|
||||
("to_end_post", "to_end_post", ""),
|
||||
("to_end_post_and_floor", "to_end_post_and_floor", ""),
|
||||
("NONE", "NONE", ""),
|
||||
)
|
||||
|
||||
railing_added_previously: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@@ -47,6 +47,10 @@ def float_is_zero(f):
|
||||
|
||||
|
||||
def bm_mesh_clean_up(bm):
|
||||
# leave only object's footprint
|
||||
min_z = min([v.co.z for v in bm.verts])
|
||||
bmesh.ops.delete(bm, geom=[v for v in bm.verts if not float_is_zero(v.co.z - min_z)], context="VERTS")
|
||||
|
||||
# remove internal edges and faces
|
||||
# adding missing faces so we could rely on `e.is_boundary` later
|
||||
bmesh.ops.contextual_create(bm, geom=bm.edges[:])
|
||||
|
||||
@@ -26,12 +26,20 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.type
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
from shapely import Polygon
|
||||
|
||||
|
||||
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_space"
|
||||
bl_label = "Generate Space"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
@@ -166,3 +174,157 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_spaces_from_walls"
|
||||
bl_label = "Generate Spaces From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Generate spaces from selected walls. The active object must be a wall."
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
return context.selected_objects and element.is_a("IfcWall")
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active objct must be a wall and
|
||||
# have to be selected walls
|
||||
props = context.scene.BIMModelProperties
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
if not active_obj:
|
||||
self.report({'ERROR'}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = None
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if not element.is_a("IfcWall"):
|
||||
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
|
||||
return
|
||||
|
||||
collection = active_obj.users_collection[0]
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
self.report({'ERROR'}, "No collection found. Please insert one.")
|
||||
return
|
||||
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
|
||||
return
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({'ERROR'}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
x, y, z = active_obj.matrix_world.translation.xyz
|
||||
mat = active_obj.matrix_world
|
||||
h = active_obj.dimensions.z
|
||||
selected_objects = bpy.context.selected_objects
|
||||
|
||||
boundary_elements = self.get_boundary_elements(selected_objects)
|
||||
|
||||
polys = self.get_polygons(boundary_elements)
|
||||
|
||||
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
|
||||
|
||||
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style = 2, join_style = 2)
|
||||
|
||||
i=0
|
||||
for linear_ring in union.interiors:
|
||||
poly = Polygon(linear_ring)
|
||||
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style = 2, join_style = 2)
|
||||
|
||||
bm = self.get_bmesh_from_polygon(poly, mat, h)
|
||||
|
||||
name = "Space" + str(i)
|
||||
mesh = bpy.data.meshes.new(name = name)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
obj.matrix_world = mat
|
||||
collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
i+=1
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_boundary_elements(self, selected_objects):
|
||||
boundary_elements = []
|
||||
for obj in selected_objects:
|
||||
subelement = tool.Ifc.get_entity(obj)
|
||||
if subelement.is_a("IfcWall"):
|
||||
boundary_elements.append(subelement)
|
||||
return boundary_elements
|
||||
|
||||
def get_polygons(self, boundary_elements):
|
||||
polys = []
|
||||
for boundary_element in boundary_elements:
|
||||
obj = tool.Ifc.get_object(boundary_element)
|
||||
if not obj:
|
||||
continue
|
||||
points = []
|
||||
base = self.get_obj_base_points(obj)
|
||||
for index in ["low_left", "low_right", "high_right", "high_left"]:
|
||||
point = base[index]
|
||||
points.append(point)
|
||||
|
||||
polys.append(Polygon(points))
|
||||
return polys
|
||||
|
||||
def get_obj_base_points(self, obj):
|
||||
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
|
||||
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
|
||||
return {
|
||||
"low_left": (x_values[0], y_values[0]),
|
||||
"high_left": (x_values[3], y_values[3]),
|
||||
"low_right": (x_values[4], y_values[4]),
|
||||
"high_right": (x_values[7], y_values[7]),
|
||||
}
|
||||
|
||||
def get_converted_tolerance(self, tolerance):
|
||||
model = tool.Ifc.get()
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
||||
prefix=getattr(project_unit, "Prefix", None)
|
||||
|
||||
converted_tolerance = ifcopenshell.util.unit.convert(
|
||||
value = tolerance,
|
||||
from_prefix = None,
|
||||
from_unit = "METRE",
|
||||
to_prefix = prefix,
|
||||
to_unit = project_unit.Name,
|
||||
)
|
||||
return tolerance
|
||||
|
||||
def get_bmesh_from_polygon(self, poly, mat, h):
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
mat_invert = mat.inverted()
|
||||
|
||||
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.triangle_fill(bm, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces = bm.faces)
|
||||
|
||||
return bm
|
||||
|
||||
@@ -135,6 +135,10 @@ class BIM_PT_authoring(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.generate_space")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.generate_spaces_from_walls")
|
||||
|
||||
|
||||
class BIM_PT_array(bpy.types.Panel):
|
||||
|
||||
@@ -436,8 +436,8 @@ class DumbWallGenerator:
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.width = self.layers["thickness"]
|
||||
self.height = props.extrusion_depth * self.unit_scale
|
||||
self.length = props.length * self.unit_scale
|
||||
self.height = props.extrusion_depth
|
||||
self.length = props.length
|
||||
self.rotation = 0.0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
|
||||
|
||||
@@ -57,6 +57,7 @@ class BimTool(WorkSpaceTool):
|
||||
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
|
||||
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
|
||||
("bim.hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
|
||||
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
|
||||
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
|
||||
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
|
||||
@@ -274,11 +275,9 @@ class BimToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
|
||||
|
||||
cls.layout.row(align=True).label(text="Mode")
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Show / edit openings")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_D")
|
||||
row.operator("bim.hotkey", text="Decomposition").hotkey = "A_D"
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
|
||||
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
|
||||
add_layout_hotkey_operator(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
@@ -581,6 +580,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.props.y = self.y
|
||||
self.props.z = self.z
|
||||
|
||||
def hotkey_A_B(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if AuthoringData.data["has_visible_boundaries"]:
|
||||
bpy.ops.bim.hide_boundaries()
|
||||
else:
|
||||
bpy.ops.bim.show_boundaries()
|
||||
|
||||
def hotkey_A_D(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
|
||||
@@ -136,6 +136,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = context.scene.BIMProfileProperties
|
||||
profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
obj = tool.Model.import_profile(profile)
|
||||
tool.Ifc.link(profile, obj)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -49,9 +49,12 @@ classes = (
|
||||
operator.DisableEditingResourceCostValue,
|
||||
operator.CalculateResourceWork,
|
||||
operator.ImportResources,
|
||||
operator.EditProductivityData,
|
||||
prop.Resource,
|
||||
prop.BIMResourceProperties,
|
||||
prop.BIMResourceTreeProperties,
|
||||
prop.ISODuration,
|
||||
prop.BIMResourceProductivity,
|
||||
ui.BIM_PT_resources,
|
||||
ui.BIM_UL_resources,
|
||||
)
|
||||
@@ -60,8 +63,10 @@ classes = (
|
||||
def register():
|
||||
bpy.types.Scene.BIMResourceProperties = bpy.props.PointerProperty(type=prop.BIMResourceProperties)
|
||||
bpy.types.Scene.BIMResourceTreeProperties = bpy.props.PointerProperty(type=prop.BIMResourceTreeProperties)
|
||||
bpy.types.Scene.BIMResourceProductivity = bpy.props.PointerProperty(type=prop.BIMResourceProductivity)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMResourceProperties
|
||||
del bpy.types.Scene.BIMResourceTreeProperties
|
||||
del bpy.types.Scene.BIMResourceProductivity
|
||||
|
||||
@@ -52,9 +52,25 @@ class ResourceData:
|
||||
if resource.BaseQuantity:
|
||||
base_quantity = resource.BaseQuantity.get_info()
|
||||
del base_quantity["Unit"]
|
||||
results[resource.id()] = {"type": resource.is_a(), "BaseQuantity": base_quantity}
|
||||
results[resource.id()] = {
|
||||
"type": resource.is_a(),
|
||||
"BaseQuantity": base_quantity,
|
||||
}
|
||||
if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
|
||||
results[resource.id()]["Productivity"] = {}
|
||||
productivity = cls.get_productivity(resource)
|
||||
if productivity:
|
||||
results[resource.id()]["Productivity"] = {
|
||||
"QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(productivity),
|
||||
"TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity),
|
||||
"QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity),
|
||||
}
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_productivity(cls, resource):
|
||||
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False)
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
results = []
|
||||
|
||||
@@ -346,3 +346,13 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
|
||||
def _execute(self, context):
|
||||
core.import_resources(tool.Resource, file_path=self.filepath)
|
||||
|
||||
|
||||
class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_productivity_data"
|
||||
bl_description = "Apply"
|
||||
bl_label = "Edit Productivity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.edit_productivity_pset(tool.Ifc, tool.Resource)
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.resource
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.pset.data
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -71,6 +73,18 @@ def get_quantity_types(self, context):
|
||||
|
||||
def update_active_resource_index(self, context):
|
||||
blenderbim.bim.module.pset.data.refresh()
|
||||
if self.should_show_productivity:
|
||||
tool.Resource.load_productivity_data()
|
||||
|
||||
|
||||
class ISODuration(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
years: IntProperty(name="Years", default=0)
|
||||
months: IntProperty(name="Months", default=0)
|
||||
days: IntProperty(name="Days", default=0)
|
||||
hours: IntProperty(name="Hours", default=0)
|
||||
minutes: IntProperty(name="Minutes", default=0)
|
||||
seconds: IntProperty(name="Seconds", default=0)
|
||||
|
||||
|
||||
class Resource(PropertyGroup):
|
||||
@@ -113,3 +127,11 @@ class BIMResourceProperties(PropertyGroup):
|
||||
quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types")
|
||||
is_editing_quantity: BoolProperty(name="Is Editing Quantity")
|
||||
quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute)
|
||||
should_show_productivity: BoolProperty(name="Edit Productivity", update=update_active_resource_index)
|
||||
|
||||
|
||||
class BIMResourceProductivity(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
quantity_consumed: CollectionProperty(name="Duration", type=ISODuration)
|
||||
quantity_produced: FloatProperty(name="Quantity Produced")
|
||||
quantity_produced_name: StringProperty(name="Quantity Produced Name")
|
||||
@@ -65,7 +65,11 @@ class BIM_PT_resources(Panel):
|
||||
self.props,
|
||||
"active_resource_index",
|
||||
)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.prop(self.props, "should_show_productivity", icon="RECOVER_LAST")
|
||||
if self.props.should_show_productivity:
|
||||
self.draw_productivity_ui(context)
|
||||
if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES":
|
||||
self.draw_editable_resource_attributes_ui()
|
||||
elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY":
|
||||
@@ -75,6 +79,45 @@ class BIM_PT_resources(Panel):
|
||||
elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE":
|
||||
self.draw_editable_resource_time_attributes_ui()
|
||||
|
||||
def draw_productivity_ui(self, context):
|
||||
total_resources = len(self.tprops.resources)
|
||||
if not total_resources or self.props.active_resource_index >= total_resources:
|
||||
return
|
||||
|
||||
ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id
|
||||
resource = ResourceData.data["resources"][ifc_definition_id]
|
||||
|
||||
if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Resource type cannot have productivity data", icon="ERROR")
|
||||
return
|
||||
|
||||
self.productivity_props = context.scene.BIMResourceProductivity
|
||||
|
||||
if resource["Productivity"]:
|
||||
produtivitiy_rate_message = "Current Rate: {}/{}".format(
|
||||
resource["Productivity"]["QuantityProduced"], resource["Productivity"]["TimeConsumed"]
|
||||
)
|
||||
row = self.layout.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "LEFT"
|
||||
produtivitiy_rate_message = "No productivity data found"
|
||||
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.prop(self.productivity_props, "quantity_produced", text="Quantity Produced")
|
||||
row.prop(self.productivity_props, "quantity_produced_name", text="Quantity Name")
|
||||
row = self.layout.row()
|
||||
row.alignment = "RIGHT"
|
||||
self.draw_duration_property(self.productivity_props.quantity_consumed, row)
|
||||
row = self.layout.row()
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK")
|
||||
|
||||
def draw_resource_operators(self):
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.add_resource", text="Add SubContract", icon="TEXT")
|
||||
@@ -111,8 +154,9 @@ class BIM_PT_resources(Panel):
|
||||
|
||||
if not self.props.active_resource_id:
|
||||
if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
|
||||
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
|
||||
op.resource = ifc_definition_id
|
||||
if resource["Productivity"]:
|
||||
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
|
||||
op.resource = ifc_definition_id
|
||||
row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES")
|
||||
op.resource = ifc_definition_id
|
||||
@@ -204,10 +248,20 @@ class BIM_PT_resources(Panel):
|
||||
op.parent = parent_id
|
||||
op.cost_value = cost_value_id
|
||||
|
||||
def draw_duration_property(self, duration_props, layout):
|
||||
for duration_prop in duration_props:
|
||||
if duration_prop.name == "BaseQuantityConsumed":
|
||||
layout.label(text=duration_prop.name)
|
||||
layout.prop(duration_prop, "years", text="Y")
|
||||
layout.prop(duration_prop, "months", text="M")
|
||||
layout.prop(duration_prop, "days", text="D")
|
||||
layout.prop(duration_prop, "hours", text="H")
|
||||
layout.prop(duration_prop, "minutes", text="Min")
|
||||
layout.prop(duration_prop, "seconds", text="S")
|
||||
|
||||
|
||||
class BIM_UL_resources(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
resource = ResourceData.data["resources"][item.ifc_definition_id]
|
||||
icon_map = {
|
||||
"IfcSubContractResource": "TEXT",
|
||||
"IfcCrewResource": "COMMUNITY",
|
||||
@@ -217,6 +271,7 @@ class BIM_UL_resources(UIList):
|
||||
"IfcConstructionProductResource": "PACKAGE",
|
||||
}
|
||||
if item:
|
||||
resource = ResourceData.data["resources"][item.ifc_definition_id]
|
||||
props = context.scene.BIMResourceProperties
|
||||
row = layout.row(align=True)
|
||||
for i in range(0, item.level_index):
|
||||
|
||||
@@ -97,8 +97,11 @@ classes = (
|
||||
operator.RemoveWorkPlan,
|
||||
operator.RemoveWorkSchedule,
|
||||
operator.RemoveWorkTime,
|
||||
operator.ReorderTask,
|
||||
operator.SelectTaskRelatedProducts,
|
||||
operator.SelectTaskRelatedInputs,
|
||||
operator.SelectWorkScheduleProducts,
|
||||
operator.SelectUnassignedWorkScheduleProducts,
|
||||
operator.SetTaskSortColumn,
|
||||
operator.SetupDefaultTaskColumns,
|
||||
operator.UnassignLagTime,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.doc import get_predefined_type_doc
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -34,6 +35,7 @@ class SequenceData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"predefined_types": cls.get_work_schedule_types(),
|
||||
"has_work_plans": cls.has_work_plans(),
|
||||
"has_work_schedules": cls.has_work_schedules(),
|
||||
"has_work_calendars": cls.has_work_calendars(),
|
||||
@@ -221,8 +223,27 @@ class SequenceData:
|
||||
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl:
|
||||
if rel.RelatingControl.is_a("IfcWorkCalendar"):
|
||||
data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id())
|
||||
data["NestingIndex"] = None
|
||||
for rel in task.Nests or []:
|
||||
data["NestingIndex"] = rel.RelatedObjects.index(task)
|
||||
cls.data["tasks"][task.id()] = data
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_types(cls):
|
||||
results = []
|
||||
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
|
||||
version = tool.Ifc.get_schema()
|
||||
for attribute in declaration.attributes():
|
||||
if attribute.name() == "PredefinedType":
|
||||
results.extend(
|
||||
[
|
||||
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
|
||||
for e in attribute.type_of_attribute().declared_type().enumeration_items()
|
||||
]
|
||||
)
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
class WorkPlansData:
|
||||
data = {}
|
||||
|
||||
@@ -67,9 +67,11 @@ def parse_duration_as_blender_props(dt, simplify=True):
|
||||
|
||||
|
||||
def simplify_duration(durations_attributes, duration_type, prop_name):
|
||||
for item in durations_attributes:
|
||||
if item.name == prop_name:
|
||||
duration_props = item
|
||||
duration_props = None
|
||||
for collection in durations_attributes:
|
||||
if collection.name == prop_name:
|
||||
duration_props = collection
|
||||
break
|
||||
if duration_props and not duration_type or duration_type == "ELAPSEDTIME":
|
||||
duration_string = "P{}Y{}M{}DT{}H{}M{}S".format(
|
||||
duration_props.years if duration_props.years else 0,
|
||||
@@ -114,11 +116,19 @@ def simplify_duration(durations_attributes, duration_type, prop_name):
|
||||
hours, seconds = divmod(seconds_left, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
|
||||
return "P{}Y{}M{}DT{}H{}M{}S".format(
|
||||
int(years),
|
||||
int(months),
|
||||
int(days),
|
||||
int(hours),
|
||||
int(minutes),
|
||||
int(seconds),
|
||||
)
|
||||
duration_string = "P"
|
||||
if years > 0:
|
||||
duration_string += "{}Y".format(int(years))
|
||||
if months > 0:
|
||||
duration_string += "{}M".format(int(months))
|
||||
if total_days > 0:
|
||||
duration_string += "{}D".format(int(total_days))
|
||||
if hours > 0 or minutes > 0 or seconds > 0:
|
||||
duration_string += "T"
|
||||
if hours > 0:
|
||||
duration_string += "{}H".format(int(hours))
|
||||
if minutes > 0:
|
||||
duration_string += "{}M".format(int(minutes))
|
||||
if seconds > 0:
|
||||
duration_string += "{}S".format(int(seconds))
|
||||
return duration_string
|
||||
|
||||
@@ -275,13 +275,13 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
|
||||
class EnableEditingTask(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_task"
|
||||
bl_label = "Enable Editing Task"
|
||||
bl_idname = "bim.enable_editing_task_attributes"
|
||||
bl_label = "Enable Editing Task Attributes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
task: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.enable_editing_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
core.enable_editing_task_attributes(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1295,7 +1295,7 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
|
||||
class LoadProductTasks(bpy.types.Operator):
|
||||
bl_idname = "bim.load_product_tasks"
|
||||
bl_idname = "bim.load_product_related_tasks"
|
||||
bl_label = "Load Product Tasks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -1310,7 +1310,7 @@ class LoadProductTasks(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.load_product_tasks(
|
||||
core.load_product_related_tasks(
|
||||
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -1327,3 +1327,46 @@ class HighlightTask(bpy.types.Operator):
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectWorkScheduleProducts(bpy.types.Operator):
|
||||
bl_idname = "bim.select_work_schedule_products"
|
||||
bl_label = "Select Work Schedule Products"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
r = core.select_work_schedule_products(
|
||||
tool.Sequence, tool.Spatial, work_schedule=tool.Ifc.get().by_id(self.work_schedule)
|
||||
)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectUnassignedWorkScheduleProducts(bpy.types.Operator):
|
||||
bl_idname = "bim.select_unassigned_work_schedule_products"
|
||||
bl_label = "Select Unassigned Work Schedule Products"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
r = core.select_unassigned_work_schedule_products(tool.Ifc, tool.Sequence, tool.Spatial)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ReorderTask(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.reorder_task_nesting"
|
||||
bl_label = "Reorder Nesting"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
new_index: bpy.props.IntProperty()
|
||||
task: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
r = core.reorder_task_nesting(
|
||||
tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task), new_index=self.new_index
|
||||
)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
|
||||
@@ -20,6 +20,8 @@ import bpy
|
||||
import isodate
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.attribute
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.sequence as core
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.sequence.data import SequenceData
|
||||
import blenderbim.bim.module.pset.data
|
||||
@@ -37,7 +39,6 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
taskcolumns_enum = []
|
||||
tasktimecolumns_enum = []
|
||||
|
||||
@@ -108,7 +109,7 @@ def updateTaskName(self, context):
|
||||
props = context.scene.BIMWorkScheduleProperties
|
||||
if not props.is_task_update_enabled or self.name == "Unnamed":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task",
|
||||
self.file,
|
||||
@@ -124,7 +125,7 @@ def updateTaskIdentification(self, context):
|
||||
props = context.scene.BIMWorkScheduleProperties
|
||||
if not props.is_task_update_enabled or self.identification == "XXX":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task",
|
||||
self.file,
|
||||
@@ -160,7 +161,7 @@ def updateTaskTimeDateTime(self, context, startfinish):
|
||||
if startfinish_value == "-":
|
||||
return
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
try:
|
||||
startfinish_datetime = parser.isoparse(startfinish_value)
|
||||
@@ -208,7 +209,7 @@ def updateTaskDuration(self, context):
|
||||
self.duration = "-"
|
||||
return
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
task = self.file.by_id(self.ifc_definition_id)
|
||||
if task.TaskTime:
|
||||
task_time = task.TaskTime
|
||||
@@ -228,6 +229,12 @@ def updateTaskDuration(self, context):
|
||||
bpy.ops.bim.load_task_properties()
|
||||
|
||||
|
||||
def get_schedule_predefined_types(self, context):
|
||||
if not SequenceData.is_loaded:
|
||||
SequenceData.load()
|
||||
return SequenceData.data["predefined_types"]
|
||||
|
||||
|
||||
def update_visualisation_start(self, context):
|
||||
update_visualisation_start_finish(self, context, "visualisation_start")
|
||||
|
||||
@@ -278,6 +285,14 @@ def update_color_progress(self, context):
|
||||
color[2] = color_progress.b
|
||||
|
||||
|
||||
def update_sort_reversed(self, context):
|
||||
if context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
|
||||
core.load_task_tree(
|
||||
tool.Sequence,
|
||||
work_schedule=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_work_schedule_id),
|
||||
)
|
||||
|
||||
|
||||
class Task(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=updateTaskName)
|
||||
identification: StringProperty(name="Identification", update=updateTaskIdentification)
|
||||
@@ -335,6 +350,9 @@ class ISODuration(PropertyGroup):
|
||||
|
||||
|
||||
class BIMWorkScheduleProperties(PropertyGroup):
|
||||
work_schedule_predefined_types: EnumProperty(
|
||||
items=get_schedule_predefined_types, name="Predefined Type", default=None
|
||||
)
|
||||
durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration)
|
||||
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
|
||||
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
|
||||
@@ -346,12 +364,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_id: IntProperty(name="Active Task Id")
|
||||
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
|
||||
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False)
|
||||
should_show_bar_visual_option: BoolProperty(name="Should Show Settings UI", default=False)
|
||||
should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False)
|
||||
should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False)
|
||||
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
|
||||
columns: CollectionProperty(name="Columns", type=Attribute)
|
||||
active_column_index: IntProperty(name="Active Column Index")
|
||||
sort_column: StringProperty(name="Sort Column")
|
||||
is_sort_reversed: BoolProperty(name="Is Sort Reversed")
|
||||
is_sort_reversed: BoolProperty(name="Is Sort Reversed", update=update_sort_reversed)
|
||||
column_types: EnumProperty(
|
||||
items=[
|
||||
("IfcTask", "IfcTask", ""),
|
||||
@@ -404,6 +423,8 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
||||
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
|
||||
active_product_input_task_index: IntProperty(name="Active Product Input Task Index")
|
||||
enable_reorder: BoolProperty(name="Enable Reorder", default=False)
|
||||
show_task_operators: BoolProperty(name="Show Task Options", default=True)
|
||||
|
||||
|
||||
class BIMTaskTreeProperties(PropertyGroup):
|
||||
@@ -501,3 +522,4 @@ class BIMAnimationProperties(PropertyGroup):
|
||||
description="color picker",
|
||||
update=update_color_progress,
|
||||
)
|
||||
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
|
||||
|
||||
@@ -117,15 +117,19 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.tprops = context.scene.BIMTaskTreeProperties
|
||||
self.animation_props = context.scene.BIMAnimationProperties
|
||||
|
||||
row = self.layout.row()
|
||||
if SequenceData.data["has_work_schedules"]:
|
||||
row.label(
|
||||
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
|
||||
icon="TEXT",
|
||||
)
|
||||
else:
|
||||
row.label(text="No Work Schedules found.", icon="TEXT")
|
||||
row.operator("bim.add_work_schedule", text="", icon="ADD")
|
||||
if not self.props.active_work_schedule_id:
|
||||
row = self.layout.row()
|
||||
if SequenceData.data["has_work_schedules"]:
|
||||
row.label(
|
||||
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
|
||||
icon="TEXT",
|
||||
)
|
||||
else:
|
||||
row.label(text="No Work Schedules found.", icon="TEXT")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.prop(self.props, "work_schedule_predefined_types")
|
||||
row.operator("bim.add_work_schedule", text="Add new", icon="ADD")
|
||||
|
||||
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
|
||||
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
|
||||
@@ -133,17 +137,43 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
|
||||
|
||||
if self.props.active_work_schedule_id == work_schedule_id:
|
||||
if self.props.editing_type == "WORK_SCHEDULE":
|
||||
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
|
||||
elif self.props.editing_type == "TASKS":
|
||||
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
|
||||
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")
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Schedule tools")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.operator(
|
||||
"bim.generate_gantt_chart", text="Generate Gantt", icon="NLA"
|
||||
).work_schedule = work_schedule_id
|
||||
row1.operator(
|
||||
"bim.recalculate_schedule", text="Re-calculate Schedule", icon="FILE_REFRESH"
|
||||
).work_schedule = work_schedule_id
|
||||
row2 = col.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
row2.operator(
|
||||
"bim.select_work_schedule_products", text="Select Assigned", icon="RESTRICT_SELECT_OFF"
|
||||
).work_schedule = work_schedule_id
|
||||
row2.operator(
|
||||
"bim.select_unassigned_work_schedule_products", text="Select Unassigned", icon="RESTRICT_SELECT_OFF"
|
||||
).work_schedule = work_schedule_id
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Settings")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
|
||||
row2 = col.row(align=True)
|
||||
row2.prop(self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO")
|
||||
row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO")
|
||||
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
|
||||
else:
|
||||
row.operator(
|
||||
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
|
||||
@@ -161,6 +191,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.draw_column_ui()
|
||||
if self.props.should_show_visualisation_ui:
|
||||
self.draw_visualisation_ui()
|
||||
if self.props.should_show_snapshot_ui:
|
||||
self.draw_snapshot_ui()
|
||||
self.draw_editable_task_ui(work_schedule_id)
|
||||
|
||||
def draw_task_operators(self):
|
||||
@@ -178,18 +210,28 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.operator("bim.edit_task", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_task", text="", icon="CANCEL")
|
||||
else:
|
||||
row.prop(self.props, "should_show_bar_visual_option", text="", icon="NLA_PUSHDOWN")
|
||||
row.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
|
||||
row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
||||
row.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ifc_definition_id
|
||||
row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = ifc_definition_id
|
||||
row.operator("bim.add_task", text="", icon="ADD").task = ifc_definition_id
|
||||
row.operator("bim.duplicate_task", text="", icon="DUPLICATE").task = ifc_definition_id
|
||||
row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id
|
||||
row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL")
|
||||
if self.props.show_task_operators:
|
||||
row2 = self.layout.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
|
||||
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
|
||||
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
||||
row2.operator(
|
||||
"bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO"
|
||||
).task = ifc_definition_id
|
||||
row2.operator(
|
||||
"bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL"
|
||||
).task = ifc_definition_id
|
||||
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
|
||||
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
|
||||
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
|
||||
|
||||
def draw_column_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.setup_default_task_columns", text="Show Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row = self.layout.row()
|
||||
row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row.alignment = "RIGHT"
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "column_types", text="")
|
||||
column_type = self.props.column_types
|
||||
@@ -226,7 +268,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
op.work_schedule = self.props.active_work_schedule_id
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Animation Options")
|
||||
row.label(text="Speed Settings")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "speed_types", text="")
|
||||
if self.props.speed_types == "FRAME_SPEED":
|
||||
@@ -237,21 +279,50 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.prop(self.props, "speed_real_duration", text="")
|
||||
elif self.props.speed_types == "MULTIPLIER_SPEED":
|
||||
row.prop(self.props, "speed_multiplier", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Display Settings")
|
||||
row = self.layout.row(align=True)
|
||||
if not self.animation_props.is_editing:
|
||||
op = row.operator(
|
||||
"bim.enable_editing_task_animation_colors", text="Customize Animation Colors", icon="SEQUENCE_COLOR_04"
|
||||
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
|
||||
)
|
||||
else:
|
||||
op = row.operator(
|
||||
"bim.disable_editing_task_animation_colors", text="Hide Animation Colors", icon="SEQUENCE_COLOR_01"
|
||||
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
|
||||
)
|
||||
|
||||
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
|
||||
if self.animation_props.should_show_task_bar_options:
|
||||
row = self.layout.row()
|
||||
row.label(text="Task Bar Options", icon="NLA_PUSHDOWN")
|
||||
row.alignment = "LEFT"
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN")
|
||||
row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN")
|
||||
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
# Column1
|
||||
col = grid.column()
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(self.animation_props, "color_progress")
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(self.animation_props, "color_full")
|
||||
|
||||
if self.animation_props.is_editing:
|
||||
self.draw_visualisation_settings_ui()
|
||||
|
||||
row = self.layout.row()
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA")
|
||||
op.work_schedule = self.props.active_work_schedule_id
|
||||
|
||||
def draw_snapshot_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Create Construction Snapshot:")
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW")
|
||||
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
|
||||
op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF")
|
||||
op.work_schedule = self.props.active_work_schedule_id
|
||||
|
||||
@@ -286,6 +357,13 @@ class BIM_PT_work_schedules(Panel):
|
||||
draw_attributes(self.props.work_schedule_attributes, self.layout)
|
||||
|
||||
def draw_editable_task_ui(self, work_schedule_id):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_summary_task", text="Add Summary Task", icon="ADD").work_schedule = work_schedule_id
|
||||
row.operator("bim.expand_all_tasks", text="Expand All")
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
self.draw_task_operators()
|
||||
self.layout.template_list(
|
||||
"BIM_UL_tasks",
|
||||
@@ -295,9 +373,6 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.props,
|
||||
"active_task_index",
|
||||
)
|
||||
row = self.layout.row()
|
||||
row.operator("bim.expand_all_tasks", text="Expand All")
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
||||
self.draw_editable_task_attributes_ui()
|
||||
elif self.props.active_task_id and self.props.editing_task_type == "CALENDAR":
|
||||
@@ -430,7 +505,7 @@ class BIM_PT_task_icom(Panel):
|
||||
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
|
||||
op.related_object = input_id
|
||||
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
|
||||
op.task = task.ifc_definition_id
|
||||
|
||||
row2 = col.row()
|
||||
@@ -519,7 +594,7 @@ class BIM_UL_task_resources(UIList):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
row.prop(item, "schedule_usage", emboss=False, text="")
|
||||
row.label(text=str(item.schedule_usage))
|
||||
|
||||
|
||||
class BIM_UL_animation_colors(UIList):
|
||||
@@ -583,7 +658,7 @@ class BIM_UL_tasks(UIList):
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
if self.props.should_show_bar_visual_option:
|
||||
if self.props.should_show_task_bar_selection:
|
||||
row.prop(
|
||||
item,
|
||||
"has_bar_visual",
|
||||
@@ -591,6 +666,8 @@ class BIM_UL_tasks(UIList):
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
if self.props.enable_reorder:
|
||||
self.draw_order_operator(row, item.ifc_definition_id)
|
||||
if self.props.active_task_id:
|
||||
if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id:
|
||||
if item.is_predecessor:
|
||||
@@ -605,6 +682,18 @@ class BIM_UL_tasks(UIList):
|
||||
op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False)
|
||||
op.task = item.ifc_definition_id
|
||||
|
||||
def draw_order_operator(self, row, ifc_definition_id):
|
||||
task = SequenceData.data["tasks"][ifc_definition_id]
|
||||
if task["NestingIndex"] is not None:
|
||||
if task["NestingIndex"] == 0:
|
||||
op = row.operator("bim.reorder_task_nesting", icon="TRIA_DOWN", text="")
|
||||
op.task = ifc_definition_id
|
||||
op.new_index = task["NestingIndex"] + 1
|
||||
elif task["NestingIndex"] > 0:
|
||||
op = row.operator("bim.reorder_task_nesting", icon="TRIA_UP", text="")
|
||||
op.task = ifc_definition_id
|
||||
op.new_index = task["NestingIndex"] - 1
|
||||
|
||||
def draw_hierarchy(self, row, item):
|
||||
for i in range(0, item.level_index):
|
||||
row.label(text="", icon="BLANK1")
|
||||
@@ -825,7 +914,7 @@ class BIM_PT_4D_Tools(Panel):
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMWorkScheduleProperties
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
|
||||
row.operator("bim.load_product_related_tasks", text="Load Tasks", icon="FILE_REFRESH")
|
||||
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
col1 = grid.column()
|
||||
|
||||
@@ -120,6 +120,36 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
|
||||
)
|
||||
lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True)
|
||||
decorations_colour: bpy.props.FloatVectorProperty(
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
decorator_color_selected: bpy.props.FloatVectorProperty(
|
||||
name="Selected Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(0.545, 0.863, 0, 1), # green
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of selected verts/edges (used in profile editing mode)",
|
||||
)
|
||||
decorator_color_unselected: bpy.props.FloatVectorProperty(
|
||||
name="Not Selected Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(1, 1, 1, 1), # white
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of not selected verts/edges (used in profile editing mode)",
|
||||
)
|
||||
decorator_color_special: bpy.props.FloatVectorProperty(
|
||||
name="Special Elements Color",
|
||||
subtype="COLOR",
|
||||
default=(0.157, 0.565, 1, 1), # blue
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -172,13 +202,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
row.prop(context.scene.BIMModelProperties, "occurrence_name_function")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.DocProperties, "decorations_colour")
|
||||
row.prop(self, "decorations_colour")
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.BIMModelProperties, "decorator_color_selected")
|
||||
row.prop(self, "decorator_color_selected")
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.BIMModelProperties, "decorator_color_unselected")
|
||||
row.prop(self, "decorator_color_unselected")
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.BIMModelProperties, "decorator_color_special")
|
||||
row.prop(self, "decorator_color_special")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.BIMProperties, "schema_dir")
|
||||
|
||||
@@ -170,3 +170,15 @@ def unassign_resource(ifc, spatial, resource=None, products=None):
|
||||
products = spatial.get_selected_products()
|
||||
for product in products:
|
||||
ifc.run("resource.unassign_resource", relating_resource=resource, related_object=product)
|
||||
|
||||
|
||||
def edit_productivity_pset(ifc, resource_tool):
|
||||
resource = resource_tool.get_highlighted_resource()
|
||||
if resource is None:
|
||||
return
|
||||
productivity = resource_tool.get_productivity(resource)
|
||||
if productivity:
|
||||
pset = ifc.get().by_id(productivity["id"])
|
||||
else:
|
||||
pset = ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
|
||||
ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes())
|
||||
|
||||
@@ -74,47 +74,47 @@ def enable_editing_work_schedule(sequence, work_schedule=None):
|
||||
|
||||
def enable_editing_work_schedule_tasks(sequence, work_schedule=None):
|
||||
sequence.enable_editing_work_schedule_tasks(work_schedule)
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def create_task_tree(sequence, work_schedule):
|
||||
sequence.create_task_tree(work_schedule)
|
||||
def load_task_tree(sequence, work_schedule):
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def expand_task(sequence, task=None):
|
||||
sequence.expand_task(task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def expand_all_tasks(sequence):
|
||||
sequence.expand_all_tasks()
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def contract_task(sequence, task=None):
|
||||
sequence.contract_task(task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def contract_all_tasks(sequence):
|
||||
sequence.contract_all_tasks()
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def remove_task(ifc, sequence, task=None):
|
||||
ifc.run("sequence.remove_task", task=task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
sequence.disable_selecting_deleted_task()
|
||||
|
||||
@@ -129,21 +129,24 @@ def disable_editing_work_schedule(sequence):
|
||||
|
||||
def add_summary_task(ifc, sequence, work_schedule=None):
|
||||
ifc.run("sequence.add_task", work_schedule=work_schedule)
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def add_task(ifc, sequence, parent_task=None):
|
||||
ifc.run("sequence.add_task", parent_task=parent_task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def enable_editing_task(sequence, task=None):
|
||||
sequence.load_task_attributes(task)
|
||||
sequence.enable_editing_task(task)
|
||||
|
||||
def enable_editing_task_attributes(sequence, task=None):
|
||||
sequence.load_task_attributes(task)
|
||||
sequence.enable_editing_task_attributes(task)
|
||||
|
||||
|
||||
def edit_task(ifc, sequence, task=None):
|
||||
attributes = sequence.get_task_attributes()
|
||||
@@ -165,7 +168,7 @@ def copy_task_attribute(ifc, sequence, attribute_name=None):
|
||||
def duplicate_task(ifc, sequence, task=None):
|
||||
ifc.run("sequence.duplicate_task", task=task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
@@ -430,6 +433,20 @@ def select_task_inputs(sequence, spatial, task=None):
|
||||
spatial.select_products(products=sequence.get_task_inputs(task))
|
||||
|
||||
|
||||
def select_work_schedule_products(sequence, spatial, work_schedule=None):
|
||||
products = sequence.get_work_schedule_products(work_schedule)
|
||||
spatial.select_products(products)
|
||||
|
||||
|
||||
def select_unassigned_work_schedule_products(ifc, sequence, spatial):
|
||||
spatial.deselect_all()
|
||||
products = ifc.get().by_type("IfcElement")
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
schedule_products = sequence.get_work_schedule_products(work_schedule)
|
||||
selection = [product for product in products if product not in schedule_products]
|
||||
spatial.select_products(selection)
|
||||
|
||||
|
||||
def recalculate_schedule(ifc, work_schedule=None):
|
||||
ifc.run("sequence.recalculate_schedule", work_schedule=work_schedule)
|
||||
|
||||
@@ -450,7 +467,7 @@ def calculate_task_duration(ifc, sequence, task=None):
|
||||
ifc.run("sequence.calculate_task_duration", task=task)
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
if work_schedule:
|
||||
sequence.create_task_tree(work_schedule)
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
@@ -459,6 +476,8 @@ def highlight_task(sequence, task=None):
|
||||
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
|
||||
if is_work_schedule_active:
|
||||
sequence.highlight_task(task)
|
||||
else:
|
||||
return "Work schedule is not active"
|
||||
|
||||
|
||||
def highlight_product_related_task(sequence, spatial, product_type=None):
|
||||
@@ -522,5 +541,18 @@ def generate_gantt_chart(sequence, work_schedule):
|
||||
json = sequence.create_tasks_json(work_schedule)
|
||||
sequence.generate_gantt_browser_chart(json)
|
||||
|
||||
def load_product_tasks(sequence, product=None):
|
||||
sequence.load_product_tasks(product)
|
||||
|
||||
def load_product_related_tasks(sequence, product=None):
|
||||
sequence.load_product_related_tasks(product)
|
||||
|
||||
|
||||
def reorder_task_nesting(ifc, sequence, task, new_index):
|
||||
is_sorting_enabled = sequence.is_sorting_enabled()
|
||||
is_sort_reversed = sequence.is_sort_reversed()
|
||||
if is_sorting_enabled or is_sort_reversed:
|
||||
return "Remove manual sorting"
|
||||
else:
|
||||
ifc.run("nest.reorder_nesting", item= task, new_index= new_index)
|
||||
work_schedule= sequence.get_active_work_schedule()
|
||||
sequence.load_task_tree(work_schedule)
|
||||
sequence.load_task_properties()
|
||||
@@ -562,32 +562,37 @@ class Qto:
|
||||
|
||||
@interface
|
||||
class Resource:
|
||||
def load_resources(cls): pass
|
||||
def load_resource_properties(cls): pass
|
||||
def clear_productivity_data(cls, props): pass
|
||||
def contract_resource(cls, resource): pass
|
||||
def disable_editing_resource_cost_value(cls): pass
|
||||
def disable_editing_resource_quantity(cls): pass
|
||||
def disable_editing_resource(cls): pass
|
||||
def disable_resource_editing_ui(cls): pass
|
||||
def load_resource_attributes(cls, resource): pass
|
||||
def enable_editing_resource(cls, resource): pass
|
||||
def get_resource_attributes(cls): pass
|
||||
def enable_editing_resource_time(cls, resource): pass
|
||||
def get_resource_time(cls, resource): pass
|
||||
def load_resource_time_attributes(cls, resource_time): pass
|
||||
def get_resource_time_attributes(cls): pass
|
||||
def enable_editing_resource_costs(cls, resource): pass
|
||||
def disable_editing_resource_cost_value(cls): pass
|
||||
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
|
||||
def load_cost_value_attributes(cls, cost_value): pass
|
||||
def edit_productivity_pset(cls, resource, attributes): pass
|
||||
def enable_editing_cost_value_attributes(cls, cost_value): pass
|
||||
def get_resource_cost_value_formula(cls): pass
|
||||
def get_resource_cost_value_attributes(cls): pass
|
||||
def enable_editing_resource_base_quantity(cls, resource): pass
|
||||
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
|
||||
def enable_editing_resource_costs(cls, resource): pass
|
||||
def enable_editing_resource_quantity(cls, resource_quantity): pass
|
||||
def disable_editing_resource_quantity(cls): pass
|
||||
def get_resource_quantity_attributes(cls): pass
|
||||
def enable_editing_resource_time(cls, resource): pass
|
||||
def enable_editing_resource(cls, resource): pass
|
||||
def expand_resource(cls, resource): pass
|
||||
def contract_resource(cls, resource): pass
|
||||
def get_highlighted_resource(cls): pass
|
||||
def get_productivity_attributes(cls): pass
|
||||
def get_productivity(cls, resource, should_inherit): pass
|
||||
def get_resource_attributes(cls): pass
|
||||
def get_resource_cost_value_attributes(cls): pass
|
||||
def get_resource_cost_value_formula(cls): pass
|
||||
def get_resource_quantity_attributes(cls): pass
|
||||
def get_resource_time_attributes(cls): pass
|
||||
def get_resource_time(cls, resource): pass
|
||||
def import_resources(cls, file_path): pass
|
||||
|
||||
def load_cost_value_attributes(cls, cost_value): pass
|
||||
def load_productivity_data(cls): pass
|
||||
def load_resource_attributes(cls, resource): pass
|
||||
def load_resource_properties(cls): pass
|
||||
def load_resource_time_attributes(cls, resource_time): pass
|
||||
def load_resources(cls): pass
|
||||
|
||||
@interface
|
||||
class Root:
|
||||
@@ -619,11 +624,24 @@ class Search:
|
||||
@interface
|
||||
class Sequence:
|
||||
def add_task_column(cls, column_type, name, data_type): pass
|
||||
def add_text_animation_handler(cls, settings): pass
|
||||
def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass
|
||||
def animate_creation(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_destruction(cls, obj, start_frame, color, animation_type): pass
|
||||
def animate_input(cls, obj, start_frame, product_frame, animation_type): pass
|
||||
def animate_movement_from(cls, obj, start_frame, color, animation_type): pass
|
||||
def animate_movement_to(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_objects(cls, settings, frames, clear_previous, animation_type): pass
|
||||
def animate_operation(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_output(cls, obj, start_frame, product_frame): pass
|
||||
def clear_object_animation(cls, obj): pass
|
||||
def clear_objects_animation(cls, include_blender_objects): pass
|
||||
def contract_all_tasks(cls): pass
|
||||
def contract_task(cls, task): pass
|
||||
def create_bars(cls, tasks): pass
|
||||
def create_bars(cls, tasks):pass
|
||||
def create_task_tree(cls, work_schedule): pass
|
||||
def create_new_task_json(cls, task, json, type_map=None): pass
|
||||
def load_task_tree(cls, work_schedule): pass
|
||||
def create_tasks_json(cls, work_schedule=None): pass
|
||||
def disable_editing_rel_sequence(cls): pass
|
||||
def disable_editing_task_animation_colors(cls): pass
|
||||
def disable_editing_task_time(cls): pass
|
||||
@@ -640,7 +658,7 @@ class Sequence:
|
||||
def enable_editing_task_calendar(cls, task): pass
|
||||
def enable_editing_task_sequence(cls, task): pass
|
||||
def enable_editing_task_time(cls, task): pass
|
||||
def enable_editing_task(cls, task): pass
|
||||
def enable_editing_task_attributes(cls, task): pass
|
||||
def enable_editing_work_calendar_times(cls, work_calendar): pass
|
||||
def enable_editing_work_calendar(cls, work_calendar): pass
|
||||
def enable_editing_work_plan_schedules(cls, work_plan): pass
|
||||
@@ -651,11 +669,13 @@ class Sequence:
|
||||
def expand_all_tasks(cls): pass
|
||||
def expand_task(cls, task): pass
|
||||
def find_related_input_tasks(cls, product): pass
|
||||
def find_related_output_tasks(cls, column): pass
|
||||
def find_related_output_tasks(cls, product): pass
|
||||
def generate_gantt_browser_chart(cls, task_json): pass
|
||||
def get_active_task(cls): pass
|
||||
def get_active_work_schedule(cls): pass
|
||||
def get_animation_bar_tasks(cls): pass
|
||||
def get_animation_product_frames(cls, work_schedule, settings): pass
|
||||
def get_animation_settings(cls): pass
|
||||
def get_checked_tasks(cls): pass
|
||||
def get_direct_nested_tasks(cls, task):pass
|
||||
def get_direct_task_outputs(cls, task): pass
|
||||
@@ -665,7 +685,6 @@ class Sequence:
|
||||
def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass
|
||||
def get_recurrence_pattern_times(cls): pass
|
||||
def get_rel_sequence_attributes(cls): pass
|
||||
|
||||
def get_selected_resource(cls): pass
|
||||
def get_start_date(cls): pass
|
||||
def get_task_attribute_value(cls, attribute_name): pass
|
||||
@@ -678,12 +697,15 @@ class Sequence:
|
||||
def get_work_calendar_attributes(cls): pass
|
||||
def get_work_plan_attributes(cls): pass
|
||||
def get_work_schedule_attributes(cls): pass
|
||||
def get_work_schedule_products(cls, work_schedule): pass
|
||||
def get_work_schedule(cls, task): pass
|
||||
def get_work_time_attributes(cls): pass
|
||||
def guess_date_range(cls, work_schedule): pass
|
||||
def has_task_assignments(cls, product, cost_schedule=None): pass
|
||||
def highlight_task(cls, task): pass
|
||||
def is_work_schedule_active(cls, work_schedule): pass
|
||||
def load_lag_time_attributes(cls, lag_time): pass
|
||||
def load_product_related_tasks(cls, product): pass
|
||||
def load_rel_sequence_attributes(cls, rel_sequence): pass
|
||||
def load_resources(cls): pass
|
||||
def load_task_animation_colors(cls): pass
|
||||
@@ -707,6 +729,7 @@ class Sequence:
|
||||
def show_snapshot(cls, product_states): pass
|
||||
def update_visualisation_date(cls, start_date, finish_date): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Spatial:
|
||||
def can_contain(cls, structure_obj, element_obj): pass
|
||||
|
||||
@@ -1347,15 +1347,16 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
"""returns a set of elements that are included in the drawing"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
|
||||
include = pset.get("Include", None)
|
||||
if include:
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include))
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
|
||||
else:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
|
||||
elements = {e for e in elements if e.is_a() != "IfcSpace"}
|
||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
|
||||
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
|
||||
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
|
||||
elements.update(annotations)
|
||||
|
||||
@@ -1370,7 +1371,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
include = pset.get("Include", None)
|
||||
elements = set(ifc_file.by_type("IfcSpace"))
|
||||
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")])
|
||||
if include:
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
|
||||
exclude = pset.get("Exclude", None)
|
||||
@@ -1449,14 +1450,13 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
|
||||
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
|
||||
drawing = tool.Ifc.get_entity(camera)
|
||||
all_elements = set(tool.Ifc.get().by_type("IfcElement")) - set(tool.Ifc.get().by_type("IfcOpeningElement"))
|
||||
filtered_elements = cls.get_drawing_elements(drawing)
|
||||
hidden_elements = list(all_elements - filtered_elements)
|
||||
hidden_objs = [tool.Ifc.get_object(e) for e in hidden_elements]
|
||||
|
||||
# Running operators is much more efficient in this scenario than looping through each element
|
||||
bpy.ops.object.hide_view_clear()
|
||||
|
||||
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
|
||||
hidden_objs = [o for o in bpy.context.visible_objects if tool.Ifc.get_entity(o) not in filtered_elements]
|
||||
|
||||
for hidden_obj in hidden_objs:
|
||||
if bpy.context.view_layer.objects.get(hidden_obj.name):
|
||||
hidden_obj.hide_set(True)
|
||||
@@ -1492,6 +1492,36 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def get_elements_in_camera_view(cls, camera, objs):
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
x = camera.data.ortho_scale / 2
|
||||
y = (camera.data.BIMCameraProperties.raster_y / camera.data.BIMCameraProperties.raster_x) * x
|
||||
else:
|
||||
y = camera.data.ortho_scale / 2
|
||||
x = (camera.data.BIMCameraProperties.raster_x / camera.data.BIMCameraProperties.raster_y) * y
|
||||
|
||||
camera_inverse_matrix = camera.matrix_world.inverted()
|
||||
return set([
|
||||
tool.Ifc.get_entity(o)
|
||||
for o in objs
|
||||
if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
|
||||
and tool.Ifc.get_entity(o)
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end):
|
||||
local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box]
|
||||
for v in local_bbox:
|
||||
if v.z < -clip_start and v.z > -clip_end and abs(v.x) < x and abs(v.y) < y:
|
||||
return True
|
||||
if any([v.z > -clip_start for v in local_bbox]) and any([v.z < -clip_end for v in local_bbox]):
|
||||
return True
|
||||
elif any([v.x < -x for v in local_bbox]) and any([v.x > x for v in local_bbox]):
|
||||
return True
|
||||
elif any([v.y < -y for v in local_bbox]) and any([v.y > y for v in local_bbox]):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_intersecting_camera(cls, obj, camera):
|
||||
# Based on separating axis theorem
|
||||
|
||||
@@ -84,6 +84,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
bpy.data.collections.remove(obj.users_collection[0])
|
||||
if getattr(element, "FillsVoids", None):
|
||||
bpy.ops.bim.remove_filling(filling=element.id())
|
||||
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
if element.HasFillings:
|
||||
for rel in element.HasFillings:
|
||||
@@ -97,7 +98,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id())
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
try:
|
||||
obj.name
|
||||
bpy.data.objects.remove(obj)
|
||||
@@ -316,7 +317,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
|
||||
@classmethod
|
||||
def is_edited(cls, obj):
|
||||
return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs
|
||||
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
|
||||
|
||||
@classmethod
|
||||
def is_mapped_representation(cls, representation):
|
||||
|
||||
@@ -285,9 +285,6 @@ class IfcGit:
|
||||
for obj in blender_collection.objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
bpy.data.collections.remove(blender_collection)
|
||||
for collection in bpy.data.collections:
|
||||
if not collection.users:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
@classmethod
|
||||
def is_valid_branch_name(cls, new_branch_name):
|
||||
|
||||
@@ -115,6 +115,56 @@ class Model(blenderbim.core.tool.Model):
|
||||
cls.bm.free()
|
||||
return profile
|
||||
|
||||
@classmethod
|
||||
def export_surface(cls, obj):
|
||||
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
||||
|
||||
edge1 = p2 - p1
|
||||
edge2 = p3 - p1
|
||||
normal = edge1.cross(edge2)
|
||||
z_axis = normal.normalized()
|
||||
x_axis = p2 - p1
|
||||
x_axis.normalize()
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
|
||||
position = Matrix()
|
||||
position.col[0][:3] = x_axis
|
||||
position.col[1][:3] = y_axis
|
||||
position.col[2][:3] = z_axis
|
||||
position.col[3][:3] = p1
|
||||
|
||||
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
helper = Helper(tool.Ifc.get())
|
||||
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
|
||||
|
||||
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
||||
return
|
||||
|
||||
cls.bm = bmesh.new()
|
||||
cls.bm.from_mesh(obj.data)
|
||||
cls.bm.verts.ensure_lookup_table()
|
||||
cls.bm.edges.ensure_lookup_table()
|
||||
|
||||
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / cls.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
))
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
cls.points = cls.export_points(position, indices["points"])
|
||||
|
||||
surface.OuterBoundary = cls.export_curve(position, indices["profile"])
|
||||
results = []
|
||||
for inner_curve in indices["inner_curves"]:
|
||||
results.append(cls.export_curve(position, inner_curve))
|
||||
surface.InnerBoundaries = results
|
||||
|
||||
cls.bm.free()
|
||||
return surface
|
||||
|
||||
@classmethod
|
||||
def generate_occurrence_name(cls, element_type, ifc_class):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
@@ -218,6 +268,44 @@ class Model(blenderbim.core.tool.Model):
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def import_surface(cls, surface, obj=None):
|
||||
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
cls.vertices = []
|
||||
cls.edges = []
|
||||
cls.arcs = []
|
||||
cls.circles = []
|
||||
|
||||
if surface.is_a("IfcCurveBoundedPlane"):
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist())
|
||||
position[0][3] *= cls.unit_scale
|
||||
position[1][3] *= cls.unit_scale
|
||||
position[2][3] *= cls.unit_scale
|
||||
|
||||
cls.import_curve(obj, position, surface.OuterBoundary)
|
||||
for inner_boundary in surface.InnerBoundaries:
|
||||
cls.import_curve(obj, position, inner_boundary)
|
||||
|
||||
mesh = bpy.data.meshes.new("Surface")
|
||||
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||
mesh.BIMMeshProperties.subshape_type = "PROFILE"
|
||||
|
||||
if obj is None:
|
||||
obj = bpy.data.objects.new("Surface", mesh)
|
||||
else:
|
||||
obj.data = mesh
|
||||
|
||||
for arc in cls.arcs:
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
group.add(arc, 1, "REPLACE")
|
||||
|
||||
for circle in cls.circles:
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
group.add(circle, 1, "REPLACE")
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def import_curve(cls, obj, position, curve):
|
||||
offset = len(cls.vertices)
|
||||
@@ -232,6 +320,10 @@ class Model(blenderbim.core.tool.Model):
|
||||
cls.vertices.append(global_point)
|
||||
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))])
|
||||
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
|
||||
elif curve.is_a("IfcCompositeCurve"):
|
||||
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
|
||||
for segment in curve.Segments:
|
||||
cls.import_curve(obj, position, segment.ParentCurve)
|
||||
elif curve.is_a("IfcIndexedPolyCurve"):
|
||||
is_arc = False
|
||||
is_closed = False
|
||||
|
||||
@@ -133,8 +133,8 @@ class Project(blenderbim.core.tool.Project):
|
||||
def set_default_modeling_dimensions(cls):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
props.extrusion_depth = 3 / unit_scale
|
||||
props.length = 1 / unit_scale
|
||||
props.extrusion_depth = 3
|
||||
props.length = 1
|
||||
props.rl1 = 0
|
||||
props.rl2 = 1 / unit_scale
|
||||
props.x = 0.5 / unit_scale
|
||||
|
||||
@@ -30,6 +30,8 @@ from datetime import datetime
|
||||
from dateutil import parser
|
||||
import ifcopenshell.util.date as ifcdateutils
|
||||
import ifcopenshell.util.cost
|
||||
import ifcopenshell.util.resource
|
||||
import blenderbim.bim.schema
|
||||
|
||||
|
||||
class Resource(blenderbim.core.tool.Resource):
|
||||
@@ -58,6 +60,7 @@ class Resource(blenderbim.core.tool.Resource):
|
||||
if not resource.HasContext:
|
||||
continue
|
||||
create_new_resource_li(resource, 0)
|
||||
cls.load_productivity_data()
|
||||
props.is_editing = True
|
||||
|
||||
@classmethod
|
||||
@@ -299,8 +302,83 @@ class Resource(blenderbim.core.tool.Resource):
|
||||
|
||||
@classmethod
|
||||
def get_highlighted_resource(cls):
|
||||
return tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMResourceTreeProperties.resources[
|
||||
bpy.context.scene.BIMResourceProperties.active_resource_index
|
||||
resources = len(bpy.context.scene.BIMResourceTreeProperties.resources)
|
||||
if resources and resources > bpy.context.scene.BIMResourceProperties.active_resource_index:
|
||||
return tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMResourceTreeProperties.resources[
|
||||
bpy.context.scene.BIMResourceProperties.active_resource_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def clear_productivity_data(cls, props):
|
||||
for duration_prop in props.quantity_consumed or []:
|
||||
if duration_prop.name == "BaseQuantityConsumed":
|
||||
duration_prop.years = 0
|
||||
duration_prop.months = 0
|
||||
duration_prop.days = 0
|
||||
duration_prop.hours = 0
|
||||
duration_prop.minutes = 0
|
||||
duration_prop.seconds = 0
|
||||
props.quantity_produced = 0
|
||||
props.quantity_produced_name = ""
|
||||
|
||||
@classmethod
|
||||
def load_productivity_data(cls):
|
||||
duration_props = None
|
||||
for collection_prop in bpy.context.scene.BIMResourceProductivity.quantity_consumed:
|
||||
duration_props = collection_prop if collection_prop.name == "BaseQuantityConsumed" else None
|
||||
break
|
||||
if not duration_props:
|
||||
duration_props = bpy.context.scene.BIMResourceProductivity.quantity_consumed.add()
|
||||
duration_props.name = "BaseQuantityConsumed"
|
||||
cls.clear_productivity_data(bpy.context.scene.BIMResourceProductivity)
|
||||
current_resource = tool.Resource.get_highlighted_resource()
|
||||
if current_resource:
|
||||
productivity = cls.get_productivity(current_resource)
|
||||
print("Resource:", current_resource, "productivity", productivity)
|
||||
if productivity:
|
||||
bpy.context.scene.BIMResourceProductivity.quantity_produced = (
|
||||
ifcopenshell.util.resource.get_quantity_produced(productivity)
|
||||
)
|
||||
bpy.context.scene.BIMResourceProductivity.quantity_produced_name = (
|
||||
ifcopenshell.util.resource.get_quantity_produced_name(productivity)
|
||||
)
|
||||
time_consumed = ifcopenshell.util.resource.get_unit_consumed(productivity)
|
||||
if time_consumed:
|
||||
durations_attributes = helper.parse_duration_as_blender_props(time_consumed)
|
||||
duration_props.years = durations_attributes["years"]
|
||||
duration_props.months = durations_attributes["months"]
|
||||
duration_props.days = durations_attributes["days"]
|
||||
duration_props.hours = durations_attributes["hours"]
|
||||
duration_props.minutes = durations_attributes["minutes"]
|
||||
duration_props.seconds = durations_attributes["seconds"]
|
||||
|
||||
@classmethod
|
||||
def get_productivity_attributes(cls):
|
||||
props = bpy.context.scene.BIMResourceProductivity
|
||||
productivity = {}
|
||||
if props.quantity_consumed:
|
||||
productivity["BaseQuantityConsumed"] = helper.simplify_duration(
|
||||
props.quantity_consumed, "ELAPSEDTIME", "BaseQuantityConsumed"
|
||||
)
|
||||
productivity["BaseQuantityProducedValue"] = props.quantity_produced
|
||||
productivity["BaseQuantityProducedName"] = props.quantity_produced_name
|
||||
return productivity
|
||||
|
||||
@classmethod
|
||||
def get_productivity(cls, resource, should_inherit=False):
|
||||
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=should_inherit)
|
||||
|
||||
@classmethod
|
||||
def edit_productivity_pset(cls, resource, attributes):
|
||||
productivity = cls.get_productivity(resource)
|
||||
if productivity:
|
||||
pset = tool.Ifc.get().by_id(productivity["id"])
|
||||
else:
|
||||
pset = tool.Ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
|
||||
tool.Ifc.run(
|
||||
"pset.edit_pset",
|
||||
pset=pset,
|
||||
properties=attributes,
|
||||
)
|
||||
@@ -27,6 +27,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import json
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.bim.module.sequence.helper as helper
|
||||
@@ -132,7 +133,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
props.editing_type = "TASKS"
|
||||
|
||||
@classmethod
|
||||
def create_task_tree(cls, work_schedule):
|
||||
def load_task_tree(cls, work_schedule):
|
||||
bpy.context.scene.BIMTaskTreeProperties.tasks.clear()
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
cls.contracted_tasks = json.loads(props.contracted_tasks)
|
||||
@@ -143,10 +144,30 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def get_sorted_tasks_ids(cls, tasks):
|
||||
cls.sort_keys = {task.id(): cls.get_sort_key(task) for task in tasks}
|
||||
related_object_ids = sorted(cls.sort_keys, key=cls.natural_sort_key)
|
||||
def get_sort_key(task):
|
||||
# Sorting only applies to actual tasks, not the WBS
|
||||
# for rel in task.IsNestedBy:
|
||||
# for object in rel.RelatedObjects:
|
||||
# if object.is_a("IfcTask"):
|
||||
# return "0000000000" + (task.Identification or "")
|
||||
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
|
||||
if column_type == "IfcTask":
|
||||
return task.get_info(task)[name] or ""
|
||||
elif column_type == "IfcTaskTime" and task.TaskTime:
|
||||
return task.TaskTime.get_info(task)[name]
|
||||
return task.Identification or ""
|
||||
|
||||
def natural_sort_key(i, _nsre=re.compile("([0-9]+)")):
|
||||
s = sort_keys[i]
|
||||
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
|
||||
|
||||
if bpy.context.scene.BIMWorkScheduleProperties.sort_column:
|
||||
sort_keys = {task.id(): get_sort_key(task) for task in tasks}
|
||||
related_object_ids = sorted(sort_keys, key=natural_sort_key)
|
||||
else:
|
||||
related_object_ids = [task.id() for task in tasks]
|
||||
if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed:
|
||||
return related_object_ids.reverse()
|
||||
related_object_ids.reverse()
|
||||
return related_object_ids
|
||||
|
||||
@classmethod
|
||||
@@ -162,27 +183,6 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
for related_object_id in cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_nested_tasks(task)):
|
||||
cls.create_new_task_li(related_object_id, level_index + 1)
|
||||
|
||||
@classmethod
|
||||
def natural_sort_key(cls, i, _nsre=re.compile("([0-9]+)")):
|
||||
s = cls.sort_keys[i]
|
||||
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
|
||||
|
||||
@classmethod
|
||||
def get_sort_key(cls, task):
|
||||
# Sorting only applies to actual tasks, not the WBS
|
||||
for rel in task.IsNestedBy:
|
||||
for object in rel.RelatedObjects:
|
||||
if object.is_a("IfcTask"):
|
||||
return "0000000000" + (task.Identification or "")
|
||||
if not bpy.context.scene.BIMWorkScheduleProperties.sort_column:
|
||||
return task.Identification or ""
|
||||
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
|
||||
if column_type == "IfcTask":
|
||||
return task.Name or ""
|
||||
elif column_type == "IfcTaskTime" and task.TaskTime:
|
||||
return task.TaskTime.Name or ""
|
||||
return task.Identification or ""
|
||||
|
||||
@classmethod
|
||||
def load_task_properties(cls, task=None):
|
||||
def canonicalise_time(time):
|
||||
@@ -330,7 +330,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
blenderbim.bim.helper.import_attributes2(task, props.task_attributes)
|
||||
|
||||
@classmethod
|
||||
def enable_editing_task(cls, task):
|
||||
def enable_editing_task_attributes(cls, task):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
props.active_task_id = task.id()
|
||||
props.editing_task_type = "ATTRIBUTES"
|
||||
@@ -409,16 +409,15 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
def load_task_resources(cls, resources):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
props.task_resources.clear()
|
||||
if resources:
|
||||
for resource in resources:
|
||||
new = props.task_resources.add()
|
||||
new.ifc_definition_id = resource.id()
|
||||
new.name = resource.Name or "Unnamed"
|
||||
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
|
||||
for resource in resources or []:
|
||||
new = props.task_resources.add()
|
||||
new.ifc_definition_id = resource.id()
|
||||
new.name = resource.Name or "Unnamed"
|
||||
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
|
||||
|
||||
@classmethod
|
||||
def load_resources(cls):
|
||||
bpy.ops.bim.load_resources() # remove and refactor
|
||||
blenderbim.core.resource.load_resources(tool.Resource)
|
||||
|
||||
@classmethod
|
||||
def get_task_inputs(cls, task):
|
||||
@@ -430,10 +429,22 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
|
||||
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
def are_entities_same_class(cls, entities):
|
||||
if not entities:
|
||||
return False
|
||||
if len(entities) == 1:
|
||||
return True
|
||||
first = entities[0]
|
||||
for entity in entities:
|
||||
if entity.is_a() != first.is_a():
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_task_resources(cls, task):
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
|
||||
return ifcopenshell.util.sequence.get_task_resource(task, is_deep)
|
||||
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
def load_task_inputs(cls, inputs):
|
||||
@@ -734,6 +745,8 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
def remove_task_column(cls, name):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
props.columns.remove(props.columns.find(name))
|
||||
if props.sort_column == name:
|
||||
props.sort_column = ""
|
||||
|
||||
@classmethod
|
||||
def set_task_sort_column(cls, column):
|
||||
@@ -781,7 +794,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks)
|
||||
expand_ancestors(parent_task)
|
||||
work_schedule = cls.get_active_work_schedule()
|
||||
cls.create_task_tree(work_schedule)
|
||||
cls.load_task_tree(work_schedule)
|
||||
cls.load_task_properties()
|
||||
|
||||
task_props = bpy.context.scene.BIMTaskTreeProperties
|
||||
@@ -1512,9 +1525,8 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
|
||||
|
||||
@classmethod
|
||||
def load_product_tasks(cls, product):
|
||||
def load_product_related_tasks(cls, product):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
props.is_task_update_enabled = False
|
||||
props.product_input_tasks.clear()
|
||||
props.product_output_tasks.clear()
|
||||
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
|
||||
@@ -1526,3 +1538,27 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
new = props.product_output_tasks.add()
|
||||
new.name = task.Name or "Unnamed"
|
||||
new.ifc_definition_id = task.id()
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_products(cls, work_schedule):
|
||||
products = []
|
||||
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
||||
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
|
||||
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
|
||||
return products
|
||||
|
||||
@classmethod
|
||||
def has_task_assignments(cls, product, work_schedule=None):
|
||||
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
|
||||
if work_schedule:
|
||||
task_inputs = [task for task in task_inputs or [] if cls.get_work_schedule(task) == work_schedule]
|
||||
task_ouputs = [task for task in task_ouputs or [] if cls.get_work_schedule(task) == work_schedule]
|
||||
return bool(task_inputs or task_ouputs)
|
||||
|
||||
@classmethod
|
||||
def is_sorting_enabled(cls):
|
||||
return bpy.context.scene.BIMWorkScheduleProperties.sort_column
|
||||
|
||||
@classmethod
|
||||
def is_sort_reversed(cls):
|
||||
return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed
|
||||
|
||||
@@ -241,6 +241,22 @@ Scenario: Remove Resource Quantity
|
||||
When I press "bim.remove_resource_quantity(resource={labor_resource})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Add Productivity data
|
||||
Given an empty IFC project
|
||||
When I press "bim.load_resources"
|
||||
When I press "bim.add_resource(ifc_class="IfcCrewResource")"
|
||||
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
|
||||
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
|
||||
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
|
||||
And I set "scene.BIMResourceProperties.active_resource_index" to "1"
|
||||
And I set "scene.BIMResourceProperties.should_show_productivity" to "True"
|
||||
And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00"
|
||||
And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea"
|
||||
And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5"
|
||||
When I press "bim.edit_productivity_data()"
|
||||
And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()"
|
||||
|
||||
|
||||
Scenario: Calculate Resource Work
|
||||
Given an empty IFC project
|
||||
And I press "bim.add_work_schedule"
|
||||
@@ -248,7 +264,7 @@ Scenario: Calculate Resource Work
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -261,12 +277,23 @@ Scenario: Calculate Resource Work
|
||||
And I press "bim.assign_class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.qto_name" to "Qto_WallBaseQuantities"
|
||||
When I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
And the variable "qto" is "{ifc}.by_type('IfcElementQuantity')[-1].id()"
|
||||
And I press "bim.enable_pset_editing(obj='IfcWall/Cube', obj_type='Object', pset_id={qto})"
|
||||
#And I set "active_object.PsetProperties.properties[2].metadata.float_value" to "3.00"
|
||||
# TO DO: complete
|
||||
Then nothing happens
|
||||
When I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
|
||||
And I set "active_object.PsetProperties.properties[5].metadata.float_value" to "50.00"
|
||||
And I press "bim.edit_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
When I press "bim.load_resources"
|
||||
When I press "bim.add_resource(ifc_class="IfcCrewResource")"
|
||||
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
|
||||
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
|
||||
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
|
||||
And I set "scene.BIMResourceProperties.active_resource_index" to "1"
|
||||
And I set "scene.BIMResourceProperties.should_show_productivity" to "True"
|
||||
And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00"
|
||||
And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea"
|
||||
And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5"
|
||||
When I press "bim.edit_productivity_data()"
|
||||
And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()"
|
||||
When I press "bim.calculate_resource_work(resource={labor_resource})"
|
||||
Then nothing happens
|
||||
|
||||
|
||||
Scenario: Assign Resource
|
||||
|
||||
@@ -225,7 +225,7 @@ Scenario: Enable editing task
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
When I press "bim.enable_editing_task(task={task})"
|
||||
When I press "bim.enable_editing_task_attributes(task={task})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Copy task attribute
|
||||
@@ -236,7 +236,7 @@ Scenario: Copy task attribute
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes[2].string_value" to "Foo"
|
||||
When I set "scene.BIMTaskTreeProperties.tasks[1].is_selected" to "True"
|
||||
And I press "bim.copy_task_attribute(name='Description')"
|
||||
@@ -253,7 +253,7 @@ Scenario: Unassign task Successor
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
And I press "bim.assign_successor(task={nested_task_two})"
|
||||
When I press "bim.unassign_successor(task={nested_task_two})"
|
||||
Then nothing happens
|
||||
@@ -269,7 +269,7 @@ Scenario: Edit time Lag
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
When I press "bim.assign_successor(task={nested_task_two})"
|
||||
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
|
||||
When I press "bim.assign_lag_time(sequence={rel_sequence})"
|
||||
@@ -290,7 +290,7 @@ Scenario: Unassign time Lag
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
When I press "bim.assign_successor(task={nested_task_two})"
|
||||
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
|
||||
When I press "bim.assign_lag_time(sequence={rel_sequence})"
|
||||
@@ -309,7 +309,7 @@ Scenario: Edit Sequence Relationship
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
When I press "bim.assign_successor(task={nested_task_two})"
|
||||
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
|
||||
And I press "bim.enable_editing_sequence_attributes(sequence={rel_sequence})"
|
||||
@@ -340,7 +340,7 @@ Scenario: Animate the construction of a wall
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -376,7 +376,7 @@ Scenario: Animate the demolition of a wall
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "DEMOLITION"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -415,7 +415,7 @@ Scenario: Animate the operation of a wall
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "OPERATION"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -448,7 +448,7 @@ Scenario: Animate the movement of a wall
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "MOVE"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -539,7 +539,7 @@ Scenario: Generate Gantt Chart
|
||||
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
|
||||
And I press "bim.add_summary_task(work_schedule={work_schedule})"
|
||||
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
|
||||
And I press "bim.enable_editing_task(task={task})"
|
||||
And I press "bim.enable_editing_task_attributes(task={task})"
|
||||
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
|
||||
And I press "bim.edit_task"
|
||||
And I press "bim.enable_editing_task_time(task={task})"
|
||||
@@ -788,7 +788,7 @@ Scenario: Duplicate Task
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
And I press "bim.assign_successor(task={nested_task_two})"
|
||||
When I press "bim.duplicate_task(task={task})"
|
||||
Then nothing happens
|
||||
@@ -804,7 +804,7 @@ Scenario: Duplicate Task and edit sequence Relationship
|
||||
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
|
||||
When I press "bim.add_task(task={task})"
|
||||
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
|
||||
And I press "bim.enable_editing_task(task={nested_task_one})"
|
||||
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
|
||||
And I press "bim.assign_successor(task={nested_task_two})"
|
||||
And I press "bim.duplicate_task(task={task})"
|
||||
When I press "bim.enable_editing_task_sequence(task={nested_task_one})"
|
||||
|
||||
@@ -751,6 +751,14 @@ def saving_sample_test_files(and_open_in_blender=None):
|
||||
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
|
||||
|
||||
|
||||
@given(parsers.parse("I load test blend file"))
|
||||
@when(parsers.parse("I load test blend file"))
|
||||
@then(parsers.parse("I load test blend file"))
|
||||
def opening_sample_test_files_in_blender():
|
||||
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file.blend"
|
||||
bpy.ops.wm.open_mainfile(filepath=filepath, display_file_selector=False)
|
||||
|
||||
|
||||
# TODO: merge to single fixture with `saving_sample_test_files`; add "and wait"
|
||||
@given(parsers.parse("I save sample test files and open in blender"))
|
||||
@when(parsers.parse("I save sample test files and open in blender"))
|
||||
|
||||
@@ -127,8 +127,8 @@ class TestSetDefaultModelingDimensions(NewFile):
|
||||
ifcopenshell.api.run("unit.assign_unit", ifc)
|
||||
subject.set_default_modeling_dimensions()
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
assert props.extrusion_depth == 3000
|
||||
assert props.length == 1000
|
||||
assert props.extrusion_depth == 3
|
||||
assert props.length == 1
|
||||
assert props.rl1 == 0
|
||||
assert props.rl2 == 1000
|
||||
assert props.x == 500
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
from itertools import chain
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
import collections
|
||||
import mathutils
|
||||
from pprint import pprint
|
||||
from math import pi, cos, sin, tan
|
||||
from math import pi, cos, sin, tan, radians
|
||||
|
||||
|
||||
def mm(x):
|
||||
@@ -75,7 +75,8 @@ class Usecase:
|
||||
railing_radius = self.settings["railing_diameter"] / 2
|
||||
support_spacing = self.settings["support_spacing"]
|
||||
clear_width = self.settings["clear_width"]
|
||||
height = self.settings["height"]
|
||||
# for calculations purposes we use height without railing radius
|
||||
height = self.settings["height"] - railing_radius
|
||||
cap_type = self.settings["terminal_type"]
|
||||
ifc_context = self.settings["context"]
|
||||
railing_coords = self.settings["railing_path"]
|
||||
@@ -106,8 +107,11 @@ class Usecase:
|
||||
solid = builder.create_swept_disk_solid(polyline, support_radius)
|
||||
|
||||
support_disk_circle = builder.circle(radius=support_disk_radius)
|
||||
|
||||
angle = V(0, 1).angle_signed(ortho_dir.xy)
|
||||
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_by_y_kwargs(), angle)
|
||||
support_disk = builder.extrude(
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **builder.extrude_by_y_kwargs()
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
|
||||
)
|
||||
return [solid, support_disk]
|
||||
|
||||
@@ -127,7 +131,7 @@ class Usecase:
|
||||
)[0]
|
||||
|
||||
midpointco = center + ((fillet_v1co.lerp(fillet_v2co, 0.5) - center).normalized() * radius)
|
||||
return fillet_v1co, midpointco, fillet_v2co
|
||||
return [fillet_v1co, midpointco, fillet_v2co]
|
||||
|
||||
def add_arcs_on_turnings_points(base_points):
|
||||
"""add 3 point fillet arcs on turning points of the railing path"""
|
||||
@@ -204,12 +208,54 @@ class Usecase:
|
||||
# TODO: implement more cap types
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
|
||||
start = railing_coords_for_cap[-1]
|
||||
start_point = railing_coords_for_cap[-1]
|
||||
cap_dir = (railing_coords_for_cap[-1] - railing_coords_for_cap[-2]).xy.to_3d().normalized()
|
||||
arc_point = start + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start + terminal_radius * 2 * z_down]
|
||||
ortho_dir = (cap_dir.yx * V(1, -1)).to_3d().normalized()
|
||||
if start:
|
||||
ortho_dir = -ortho_dir
|
||||
|
||||
arc_middle_point_cos = sin(radians(45))
|
||||
|
||||
if cap_type in ('180', 'TO_END_POST'):
|
||||
arc_point = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + terminal_radius * 2 * z_down]
|
||||
|
||||
if cap_type == 'TO_END_POST':
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point.z -= terminal_radius * 2
|
||||
cap_coords.append(end_point)
|
||||
|
||||
elif cap_type == 'TO_WALL':
|
||||
arc_point = start_point + cap_dir * clear_width * arc_middle_point_cos + ortho_dir * clear_width * (1-arc_middle_point_cos)
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + ortho_dir * clear_width + cap_dir * clear_width]
|
||||
|
||||
elif cap_type == 'TO_FLOOR':
|
||||
arc_point = start_point + cap_dir * terminal_radius * arc_middle_point_cos + z_down * terminal_radius * (1-arc_middle_point_cos)
|
||||
arc_points.append(arc_point)
|
||||
arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
cap_coords = [
|
||||
arc_point,
|
||||
arc_end,
|
||||
arc_end+z_down*(height-terminal_radius),
|
||||
]
|
||||
|
||||
elif cap_type == 'TO_END_POST_AND_FLOOR':
|
||||
first_arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
first_arc_coords = get_fillet_points(
|
||||
start_point, start_point + cap_dir * terminal_radius,
|
||||
first_arc_end, terminal_radius)
|
||||
arc_points.append(first_arc_coords[1])
|
||||
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point.z -= height
|
||||
second_arc_coords = get_fillet_points(
|
||||
first_arc_end, first_arc_end + z_down * terminal_radius, end_point, terminal_radius
|
||||
)
|
||||
arc_points.append(second_arc_coords[1])
|
||||
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
|
||||
|
||||
railing_coords = railing_coords_for_cap + cap_coords
|
||||
|
||||
if start:
|
||||
@@ -219,8 +265,9 @@ class Usecase:
|
||||
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
|
||||
railing_coords = add_arcs_on_turnings_points(railing_coords)
|
||||
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
if cap_type != 'NONE':
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
|
||||
railing_path = builder.polyline(
|
||||
railing_coords, closed=False, arc_points=[railing_coords.index(p) for p in arc_points]
|
||||
|
||||
@@ -21,7 +21,12 @@ import ifcopenshell.api
|
||||
|
||||
class Usecase:
|
||||
def __init__(
|
||||
self, file, parent_resource=None, ifc_class="IfcCrewResource", name=None, predefined_type="NOTDEFINED"
|
||||
self,
|
||||
file,
|
||||
parent_resource=None,
|
||||
ifc_class="IfcCrewResource",
|
||||
name=None,
|
||||
predefined_type="NOTDEFINED",
|
||||
):
|
||||
"""Add a new construction resource
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import math
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.resource
|
||||
|
||||
|
||||
class Usecase:
|
||||
@@ -59,28 +60,10 @@ class Usecase:
|
||||
self.settings = {"resource": resource}
|
||||
|
||||
def execute(self):
|
||||
self.productivity = ifcopenshell.util.element.get_psets(
|
||||
amount_worked = ifcopenshell.util.resource.get_resource_required_work(
|
||||
self.settings["resource"]
|
||||
).get("EPset_Productivity", None)
|
||||
if not self.productivity:
|
||||
# Proposal for Schema - If instance doesn't have any productivity, use the parent's productivity - if any.
|
||||
if not self.settings["resource"].Nests:
|
||||
return
|
||||
else:
|
||||
parent_resource = self.settings["resource"].Nests[0].RelatingObject
|
||||
self.productivity = ifcopenshell.util.element.get_psets(
|
||||
parent_resource
|
||||
).get("EPset_Productivity", None)
|
||||
if not self.productivity:
|
||||
return
|
||||
|
||||
unit_consumed = self.get_unit_consumed()
|
||||
self.unit_produced_name = self.productivity.get(
|
||||
"BaseQuantityProducedName", None
|
||||
)
|
||||
unit_produced = self.productivity.get("BaseQuantityProducedValue", None)
|
||||
total_produced = self.get_total_produced()
|
||||
if not unit_consumed or not unit_produced or not total_produced:
|
||||
if not amount_worked:
|
||||
return
|
||||
if not self.settings["resource"].Usage:
|
||||
ifcopenshell.api.run(
|
||||
@@ -88,37 +71,4 @@ class Usecase:
|
||||
self.file,
|
||||
resource=self.settings["resource"],
|
||||
)
|
||||
if "T" in self.productivity.get("BaseQuantityConsumed", None):
|
||||
seconds = (unit_consumed.days * 24 * 60 * 60) + unit_consumed.seconds
|
||||
amount_worked = total_produced / unit_produced * seconds
|
||||
self.settings[
|
||||
"resource"
|
||||
].Usage.ScheduleWork = f"PT{amount_worked / 60 / 60}H"
|
||||
else:
|
||||
days = unit_consumed.days + (unit_consumed.seconds / (24 * 60 * 60))
|
||||
amount_worked = total_produced / unit_produced * days
|
||||
self.settings["resource"].Usage.ScheduleWork = f"P{amount_worked}D"
|
||||
|
||||
def get_unit_consumed(self):
|
||||
duration = self.productivity.get("BaseQuantityConsumed", None)
|
||||
if not duration:
|
||||
return
|
||||
return ifcopenshell.util.date.ifc2datetime(duration)
|
||||
|
||||
def get_total_produced(self):
|
||||
total = 0
|
||||
for rel in self.settings["resource"].HasAssignments or []:
|
||||
if not rel.is_a("IfcRelAssignsToProcess"):
|
||||
continue
|
||||
for rel2 in rel.RelatingProcess.HasAssignments or []:
|
||||
if not rel2.is_a("IfcRelAssignsToProduct"):
|
||||
continue
|
||||
if self.unit_produced_name == "Count":
|
||||
total += 1
|
||||
else:
|
||||
psets = ifcopenshell.util.element.get_psets(rel2.RelatingProduct)
|
||||
for pset in psets.values():
|
||||
for name, value in pset.items():
|
||||
if name == self.unit_produced_name:
|
||||
total += float(value)
|
||||
return total
|
||||
self.settings["resource"].Usage.ScheduleWork = amount_worked
|
||||
|
||||
@@ -51,7 +51,10 @@ class Usecase:
|
||||
physical_quantity=time, attributes={"TimeValue": 8.0})
|
||||
"""
|
||||
self.file = file
|
||||
self.settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}}
|
||||
self.settings = {
|
||||
"physical_quantity": physical_quantity,
|
||||
"attributes": attributes or {},
|
||||
}
|
||||
|
||||
def execute(self):
|
||||
for name, value in self.settings["attributes"].items():
|
||||
|
||||
@@ -49,8 +49,9 @@ class Usecase:
|
||||
self.settings = {"task": task}
|
||||
|
||||
def execute(self):
|
||||
result = ifcopenshell.util.element.copy(self.file, self.settings["task"])
|
||||
result = ifcopenshell.util.element.copy_deep(self.file, self.settings["task"])
|
||||
self.copy_indirect_attributes(self.settings["task"], result)
|
||||
self.copy_sequence_relationship([self.settings["task"]], [result])
|
||||
return result
|
||||
|
||||
def copy_sequence_relationship(self, original_tasks, duplicated_tasks):
|
||||
|
||||
@@ -9,7 +9,7 @@ from codegen import indent
|
||||
|
||||
def reverse_compile(s):
|
||||
return re.sub(
|
||||
"\s*\-\s*EXPRESS_ONE_BASED_INDEXING",
|
||||
r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING",
|
||||
"",
|
||||
re.sub(
|
||||
", )?+.(, INDETERMINATE)\\"[::-1],
|
||||
|
||||
@@ -216,6 +216,11 @@ def get_bounding_box_center(bbox):
|
||||
|
||||
def serialize_shape(shape):
|
||||
shapes = BRepTools.BRepTools_ShapeSet()
|
||||
|
||||
# @todo provide method to get ifcopenshell's built-in occt version to
|
||||
# see whether this is necessary
|
||||
shapes.SetFormatNb(2)
|
||||
|
||||
shapes.Add(shape)
|
||||
return shapes.WriteToString()
|
||||
|
||||
|
||||
@@ -781,6 +781,18 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]):
|
||||
):
|
||||
to_delete.add(subelement)
|
||||
subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:])
|
||||
# See #3052. IfcOpenShell is extremely slow in removing elements if
|
||||
# the element has an inverse, and that inverse references that
|
||||
# element in a big list. The most common example is an
|
||||
# IfcPolygonalFaceSet with a Faces attribute of tens of thousands
|
||||
# of IfcIndexedPolygonalFace. In this situation, removing a
|
||||
# IfcIndexedPolygonalFace will take very, very long. If we are
|
||||
# going to delete an element (i.e. added to the to_delete set), we
|
||||
# clear any large lists (10 is an arbitrary threshold) to prevent
|
||||
# this issue.
|
||||
for i, attribute in enumerate(subelement):
|
||||
if isinstance(attribute, tuple) and len(attribute) > 10:
|
||||
subelement[i] = []
|
||||
# We delete elements from subgraph in reverse order to allow batching to work
|
||||
for subelement in filter(lambda e: e in to_delete, subgraph[::-1]):
|
||||
ifc_file.remove(subelement)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.date
|
||||
|
||||
|
||||
def get_productivity(resource, should_inherit=True):
|
||||
productivity = ifcopenshell.util.element.get_psets(
|
||||
resource
|
||||
).get("EPset_Productivity", None)
|
||||
if should_inherit and not productivity:
|
||||
# Proposal for Schema - If instance doesn't have any productivity, inherit it's parent's productivity
|
||||
if not resource.Nests:
|
||||
return None
|
||||
else:
|
||||
parent_resource = resource.Nests[0].RelatingObject
|
||||
productivity = ifcopenshell.util.element.get_psets(
|
||||
parent_resource
|
||||
).get("EPset_Productivity", None)
|
||||
return productivity
|
||||
|
||||
def get_unit_consumed(productivity):
|
||||
duration = productivity.get("BaseQuantityConsumed", None)
|
||||
if not duration:
|
||||
return
|
||||
return ifcopenshell.util.date.ifc2datetime(duration)
|
||||
|
||||
def get_quantity_produced(productivity):
|
||||
if not productivity:
|
||||
return 0
|
||||
return productivity.get("BaseQuantityProducedValue", 0)
|
||||
|
||||
def get_quantity_produced_name(productivity):
|
||||
if not productivity:
|
||||
return ""
|
||||
return productivity.get("BaseQuantityProducedName", "")
|
||||
|
||||
def get_total_quantity_produced(resource, quantity_name_in_process):
|
||||
def get_product_quantity(product, quantity_name):
|
||||
psets = ifcopenshell.util.element.get_psets(product)
|
||||
for pset in psets.values():
|
||||
for name, value in pset.items():
|
||||
if name == quantity_name:
|
||||
return float(value)
|
||||
|
||||
total = 0
|
||||
products = get_parametric_resource_products(resource)
|
||||
if quantity_name_in_process == "Count":
|
||||
total = len(products)
|
||||
else:
|
||||
for product in products:
|
||||
total += get_product_quantity(product, quantity_name_in_process)
|
||||
return total
|
||||
|
||||
def get_parametric_resource_products(resource):
|
||||
products = []
|
||||
for rel in resource.HasAssignments or []:
|
||||
if not rel.is_a("IfcRelAssignsToProcess"):
|
||||
continue
|
||||
for rel2 in rel.RelatingProcess.HasAssignments or []:
|
||||
if not rel2.is_a("IfcRelAssignsToProduct"):
|
||||
continue
|
||||
products.append(rel2.RelatingProduct)
|
||||
return products
|
||||
|
||||
def get_resource_required_work(resource):
|
||||
productivity = get_productivity(resource)
|
||||
if productivity:
|
||||
quantity_produced = get_quantity_produced(productivity)
|
||||
time_consumed = get_unit_consumed(productivity)
|
||||
quantity_name_in_process = get_quantity_produced_name(productivity)
|
||||
total_quantity_to_produce = get_total_quantity_produced(resource, quantity_name_in_process)
|
||||
if not time_consumed or not quantity_produced or not total_quantity_to_produce:
|
||||
return
|
||||
iso_string = ""
|
||||
if "T" in productivity.get("BaseQuantityConsumed", ""):
|
||||
seconds = (time_consumed.days * 24 * 60 * 60) + time_consumed.seconds
|
||||
productivity_ratio = seconds / quantity_produced
|
||||
required_work = total_quantity_to_produce * productivity_ratio
|
||||
iso_string = f"PT{required_work / 60 / 60}H"
|
||||
else:
|
||||
days = time_consumed.days + (time_consumed.seconds / (24 * 60 * 60))
|
||||
productivity_ratio = days / quantity_produced
|
||||
required_work = total_quantity_to_produce * productivity_ratio
|
||||
iso_string = f"P{required_work}D"
|
||||
return iso_string
|
||||
@@ -568,3 +568,18 @@ class ShapeBuilder:
|
||||
"position_z_axis": Vector((0, -1, 0)),
|
||||
"extrusion_vector": Vector((0, 0, -1)),
|
||||
}
|
||||
|
||||
def rotate_extrusion_kwargs_by_z(self, kwargs, angle, counter_clockwise=False):
|
||||
"""shortcut to rotate extrusion kwargs by z axis
|
||||
|
||||
`kwargs` expected to have `position_x_axis` and `position_z_axis` keys
|
||||
|
||||
`angle` is a rotation value in radians
|
||||
|
||||
by default rotation is clockwise, to make it counter clockwise use `counter_clockwise` flag
|
||||
"""
|
||||
rot = Matrix.Rotation(-angle, 3, "Z")
|
||||
kwargs = kwargs.copy() # prevent mutation of original kwargs
|
||||
kwargs["position_x_axis"].rotate(rot)
|
||||
kwargs["position_z_axis"].rotate(rot)
|
||||
return kwargs
|
||||
|
||||
@@ -2600,7 +2600,7 @@ std::pair<IfcUtil::IfcBaseClass*, double> IfcFile::getUnit(const std::string& un
|
||||
);
|
||||
|
||||
IfcUtil::IfcBaseClass* unc = *mu->data().getArgument(
|
||||
mu->declaration().as_entity()->attribute_index("ValueComponent")
|
||||
mu->declaration().as_entity()->attribute_index("UnitComponent")
|
||||
);
|
||||
|
||||
return_value.second *= static_cast<double>(*vlc->data().getArgument(0));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Authlib==0.15.4
|
||||
email-validator==1.1.3
|
||||
Flask==2.0.1
|
||||
Flask==2.3.2
|
||||
Flask-Login==0.5.0
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-WTF==0.15.1
|
||||
|
||||
Reference in New Issue
Block a user