mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 12:36:00 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9422f804fb | |||
| 415a7746be | |||
| ee903208dc | |||
| f89e834fea | |||
| e8b4335741 | |||
| 83010f0542 | |||
| 04e5f5036c | |||
| 5d86d38a14 | |||
| a82db1f622 | |||
| fb618d536b | |||
| 23f96779fa | |||
| 7b67854654 | |||
| 2e7f941465 | |||
| 1c1706fde3 | |||
| ca77a895e0 | |||
| 0f447f964e | |||
| 2c31fc6b25 | |||
| 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 |
@@ -34,7 +34,8 @@ bl_info = {
|
||||
if sys.modules.get("bpy", None):
|
||||
# Process *.pth in /libs/site/packages to setup globally importable modules
|
||||
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
|
||||
site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
|
||||
import blenderbim.bim
|
||||
|
||||
|
||||
@@ -1,67 +1,53 @@
|
||||
<link href="jsgantt.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="../../module/sequence/gantt/main.css" rel="stylesheet" type="text/css"/>
|
||||
<script src="jsgantt.js" type="text/javascript"></script>
|
||||
<style>
|
||||
.gtaskcellwkend,
|
||||
.gtaskcellcurrent,
|
||||
.gminorheadingwkend {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
.gitemhighlight td {
|
||||
background-color: #ffdaaa;
|
||||
}
|
||||
|
||||
.gtaskblue {
|
||||
background: #4281A4;
|
||||
}
|
||||
|
||||
.gtaskred {
|
||||
background: #C1666B;
|
||||
}
|
||||
|
||||
.gtaskgreen {
|
||||
background: #48A9A6;
|
||||
}
|
||||
|
||||
.gtaskyellow {
|
||||
background: #D4B483;
|
||||
}
|
||||
|
||||
.gmainleft {
|
||||
overflow: visible;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
<a href="#print" id="print">Print Mode</a>
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
<script type="text/javascript">
|
||||
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
|
||||
g.setOptions({
|
||||
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vQuarterColWidth: 36,
|
||||
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
|
||||
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
|
||||
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
|
||||
vLang: 'en',
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: false, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
|
||||
vUseToolTip: false, // Disable tooltips.
|
||||
vTotalHeight: 900,
|
||||
});
|
||||
document.getElementById('print').addEventListener('click', function() {
|
||||
g.setTotalHeight("");
|
||||
g.Draw();
|
||||
});
|
||||
var json_data = `
|
||||
{{{json_data}}}
|
||||
`;
|
||||
JSGantt.parseJSONString(json_data, g);
|
||||
g.Draw();
|
||||
<script type="text/javascript" src="../../module/sequence/gantt/index.js"></script>
|
||||
<script>
|
||||
var json_data = `{{{json_data}}}`;
|
||||
</script>
|
||||
|
||||
<div class="no-print">
|
||||
<h4>Choose a language:
|
||||
<select id="lang" class="sweet_button" onchange="set_language(event)">
|
||||
<option value='cn'>Chinese (cn)</option>
|
||||
<option value='cs'>Czech (cs)</option>
|
||||
<option value='nl'>Dutch (Standard)</option>
|
||||
<option value='en' selected>English (en)</option>
|
||||
<option value='fr'>French (fr)</option>
|
||||
<option value='de'>German (de)</option>
|
||||
<option value='hu'>Hungarian (hu)</option>
|
||||
<option value='id'>Indonesian (id)</option>
|
||||
<option value='it'>Italian (it)</option>
|
||||
<option value='ja'>Japanese (ja)</option>
|
||||
<option value='pt'>Portuguese (pt)</option>
|
||||
<option value='ru'>Russian (ru)</option>
|
||||
<option value='es'>Spanish (es)</option>
|
||||
<option value='sv'>Swedish (sv)</option>
|
||||
<option value='tr'>Turkish (tr)</option>
|
||||
</select>
|
||||
<h4>
|
||||
<div id="print_options">
|
||||
<select class="sweet_button" id="print_page_size">
|
||||
<option value="210,297">A4 Portrait</option>
|
||||
<option value="297,210">A4 Landscape</option>
|
||||
<option value="297,420">A3 Portrait</option>
|
||||
<option value="420,297">A3 Landscape</option>
|
||||
<option value="420,594">A2 Portrait</option>
|
||||
<option value="594,420">A2 Landscape</option>
|
||||
<option value="594,841">A1 Portrait</option>
|
||||
<option value="841,594">A1 Landscape</option>
|
||||
<option value="841,1189">A0 Portrait</option>
|
||||
<option value="1189,841">A0 Landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div id="schedule-data"></div>
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
|
||||
<script>
|
||||
var name = `{{{name}}}`;
|
||||
setupPage(name);
|
||||
create_gantt_chart(json_data)
|
||||
</script>
|
||||
@@ -178,6 +178,11 @@ class IfcImporter:
|
||||
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings.set(self.settings.STRICT_TOLERANCE, True)
|
||||
self.settings_curve = ifcopenshell.geom.settings()
|
||||
self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True)
|
||||
self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True)
|
||||
self.settings_native = ifcopenshell.geom.settings()
|
||||
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
||||
self.settings_2d = ifcopenshell.geom.settings()
|
||||
@@ -683,28 +688,31 @@ class IfcImporter:
|
||||
|
||||
def create_generic_elements(self, elements):
|
||||
# Based on my experience in viewing BIM models, representations are prioritised as follows:
|
||||
# 1. 3D Body, 2. 2D Plans, 3. Point clouds, 4. No representation
|
||||
# If an element has a representation that doesn't follow 1, 2, or 3, it will not show by default.
|
||||
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
|
||||
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
|
||||
# The user can load them later if they want to view them.
|
||||
products = self.create_products(elements)
|
||||
elements -= products
|
||||
products = self.create_curve_products(elements)
|
||||
products = self.create_products(elements, settings=self.settings_curve)
|
||||
elements -= products
|
||||
products = self.create_products(elements, settings=self.settings_2d)
|
||||
elements -= products
|
||||
products = self.create_pointclouds(elements)
|
||||
elements -= products
|
||||
for element in elements:
|
||||
self.create_product(element)
|
||||
|
||||
def create_products(self, products):
|
||||
def create_products(self, products, settings=None):
|
||||
if settings is None:
|
||||
settings = self.settings
|
||||
|
||||
results = set()
|
||||
if not products:
|
||||
return results
|
||||
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
self.settings, self.file, multiprocessing.cpu_count(), include=products
|
||||
)
|
||||
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
|
||||
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
@@ -771,10 +779,10 @@ class IfcImporter:
|
||||
self.structural_collection.children.link(self.structural_connection_collection)
|
||||
self.project["blender"].children.link(self.structural_collection)
|
||||
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralCurveMember"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralCurveConnection"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceMember"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceConnection"))
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d)
|
||||
self.create_structural_point_connections()
|
||||
|
||||
def create_structural_point_connections(self):
|
||||
@@ -862,40 +870,6 @@ class IfcImporter:
|
||||
self.link_element(product, obj)
|
||||
return product
|
||||
|
||||
def create_curve_products(self, products):
|
||||
results = set()
|
||||
if not products:
|
||||
return results
|
||||
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
self.settings_2d, self.file, multiprocessing.cpu_count(), include=products
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
checkpoint = time.time()
|
||||
total = 0
|
||||
while True:
|
||||
total += 1
|
||||
if total % 250 == 0:
|
||||
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
||||
checkpoint = time.time()
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.id)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
if not iterator.next():
|
||||
break
|
||||
print("Done creating geometry")
|
||||
return results
|
||||
|
||||
def create_product(self, element, shape=None, mesh=None):
|
||||
if element is None:
|
||||
return
|
||||
|
||||
@@ -20,28 +20,37 @@ import bpy
|
||||
from . import ui, operator, prop
|
||||
|
||||
classes = (
|
||||
operator.AddBoundary,
|
||||
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)
|
||||
@@ -20,12 +20,17 @@ import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import logging
|
||||
import numpy as np
|
||||
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 math import pi, inf
|
||||
from mathutils import Vector
|
||||
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 +43,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 +395,237 @@ 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(obj)
|
||||
for rel in element.BoundedBy or []:
|
||||
boundary_obj = loader.load_boundary(rel, obj)
|
||||
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"}
|
||||
|
||||
|
||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boundary"
|
||||
bl_label = "Add Boundary"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
|
||||
objs = context.selected_objects
|
||||
if len(objs) == 2:
|
||||
# The user may select two objects, a space and its related building element
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = obj
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
elif len(objs) == 1:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
element = tool.Ifc.get_entity(objs[0])
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = objs[0]
|
||||
|
||||
target = bpy.context.scene.cursor.location
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
|
||||
if not (subelement.is_a("IfcWall") or subelement.is_a("IfcSlab")):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(subelement)
|
||||
if obj:
|
||||
raycast = obj.closest_point_on_mesh(obj.matrix_world.inverted() @ target, distance=0.1)
|
||||
if raycast[0]:
|
||||
related_building_element = subelement
|
||||
related_building_element_obj = obj
|
||||
break
|
||||
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
target_distance = inf
|
||||
target_face = None
|
||||
for face in bm.faces:
|
||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = related_building_element_obj.closest_point_on_mesh(related_building_element_obj.matrix_world.inverted() @ centroid, distance=1)
|
||||
if raycast[0]:
|
||||
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
if distance < target_distance:
|
||||
target_face = face
|
||||
target_distance = distance
|
||||
|
||||
if not target_face:
|
||||
return
|
||||
|
||||
bm2 = bmesh.new()
|
||||
bm2.faces.new([bm2.verts.new(v.co.copy()) for v in target_face.verts])
|
||||
mesh = bpy.data.meshes.new("Boundary")
|
||||
bm2.to_mesh(mesh)
|
||||
|
||||
bm2.free()
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new("Boundary", mesh)
|
||||
obj.matrix_world = relating_space_obj.matrix_world.copy()
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
surface = tool.Model.export_surface(obj)
|
||||
connection_geometry = tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class="IfcRelSpaceBoundary")
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary.RelatedBuildingElement = related_building_element
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
bpy.ops.bim.show_boundaries()
|
||||
obj = tool.Ifc.get_object(boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# 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/>.
|
||||
|
||||
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
|
||||
import svgwrite
|
||||
|
||||
from odf.opendocument import load
|
||||
@@ -23,6 +24,8 @@ from odf.table import Table, TableRow, TableColumn, TableCell
|
||||
from odf.text import P
|
||||
from odf.style import Style
|
||||
|
||||
FONT_SIZE = 4.13
|
||||
|
||||
|
||||
class Scheduler:
|
||||
def schedule(self, infile, outfile):
|
||||
@@ -37,32 +40,87 @@ class Scheduler:
|
||||
styles = {}
|
||||
for style in doc.getElementsByType(Style):
|
||||
name = style.getAttribute("name")
|
||||
# looking for table-column-properties or table-row-properties
|
||||
if not style.firstChild:
|
||||
continue
|
||||
styles[name] = {key[1]: value for key, value in style.firstChild.attributes.items()}
|
||||
if style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
|
||||
style_children = [style.firstChild]
|
||||
else:
|
||||
# for style:table-cell-properties we need to collect also text and paragraph properties
|
||||
style_children = style.childNodes
|
||||
|
||||
styles[name] = {}
|
||||
for child in style_children:
|
||||
child_params = {key[1]: value for key, value in child.attributes.items()}
|
||||
styles[name].update(child_params)
|
||||
|
||||
table = doc.getElementsByType(Table)[0]
|
||||
|
||||
# collect columns width
|
||||
column_widths = []
|
||||
for col in table.getElementsByType(TableColumn):
|
||||
style_name = col.getAttribute("stylename")
|
||||
repeat = col.getAttribute("numbercolumnsrepeated")
|
||||
repeat = int(repeat) if repeat else 1
|
||||
for i in range(0, repeat):
|
||||
for i in range(repeat):
|
||||
if not style_name or "column-width" not in styles[style_name]:
|
||||
column_widths.append(50)
|
||||
column_width = 50
|
||||
else:
|
||||
column_widths.append(self.convert_to_mm(styles[style_name]["column-width"]))
|
||||
column_width = self.convert_to_mm(styles[style_name]["column-width"])
|
||||
column_widths.append(column_width)
|
||||
|
||||
# collect rows height
|
||||
row_heights = []
|
||||
for col in table.getElementsByType(TableRow):
|
||||
style_name = col.getAttribute("stylename")
|
||||
repeat = col.getAttribute("numberrowsrepeated")
|
||||
repeat = int(repeat) if repeat else 1
|
||||
for i in range(repeat):
|
||||
if not style_name or "row-height" not in styles[style_name]:
|
||||
row_height = 6
|
||||
else:
|
||||
row_height = self.convert_to_mm(styles[style_name]["row-height"])
|
||||
row_heights.append(row_height)
|
||||
|
||||
# draw table
|
||||
y = self.margin
|
||||
for tri, tr in enumerate(table.getElementsByType(TableRow)):
|
||||
x = self.margin
|
||||
height = 6
|
||||
height = row_heights[tri]
|
||||
tdi = 0
|
||||
|
||||
for td in tr.getElementsByType(TableCell):
|
||||
column_span = td.getAttribute("numbercolumnsspanned")
|
||||
column_span = int(column_span) if column_span else 1
|
||||
|
||||
repeat = td.getAttribute("numbercolumnsrepeated")
|
||||
repeat = int(repeat) if repeat else 1
|
||||
|
||||
# figuring text alignment
|
||||
style_name = td.getAttribute("stylename")
|
||||
style = styles[style_name] if style_name else {}
|
||||
if style_name and "vertical-align" in style and style["vertical-align"] != "automatic":
|
||||
vertical_align = style["vertical-align"]
|
||||
else:
|
||||
vertical_align = "bottom"
|
||||
|
||||
if style_name and "text-align" in style and style["text-align"] != "automatic":
|
||||
horizontal_align = style["text-align"]
|
||||
horizontal_align = "middle" if horizontal_align == "center" else horizontal_align
|
||||
else:
|
||||
horizontal_align = "left"
|
||||
|
||||
if vertical_align == "middle" and horizontal_align == "middle":
|
||||
box_alignment = "center"
|
||||
else:
|
||||
box_alignment = f"{vertical_align}-{horizontal_align}"
|
||||
|
||||
# for future use
|
||||
wrap_text = style.get("wrap-option", None) == "wrap"
|
||||
|
||||
# drawing cells and text
|
||||
for i in range(0, repeat):
|
||||
width = column_widths[tdi]
|
||||
width = sum(column_widths[tdi : tdi + int(column_span)])
|
||||
self.svg.add(
|
||||
self.svg.rect(
|
||||
insert=(x, y),
|
||||
@@ -72,9 +130,26 @@ class Scheduler:
|
||||
)
|
||||
value = td.getElementsByType(P)
|
||||
if value:
|
||||
self.add_text(value[0], x + self.padding, y + self.padding)
|
||||
# figuring text position based on alignment
|
||||
text_position = [0, 0]
|
||||
if box_alignment.endswith("left"):
|
||||
text_position[0] = x + self.padding
|
||||
elif box_alignment.endswith("middle") or box_alignment == "center":
|
||||
text_position[0] = x + width / 2
|
||||
elif box_alignment.endswith("right"):
|
||||
text_position[0] = x + width - self.padding
|
||||
|
||||
if box_alignment.startswith("top"):
|
||||
text_position[1] = y + self.padding
|
||||
elif box_alignment.startswith("middle") or box_alignment == "center":
|
||||
text_position[1] = y + height / 2
|
||||
elif box_alignment.startswith("bottom"):
|
||||
text_position[1] = y + height - self.padding
|
||||
|
||||
self.add_text(value[0], *text_position, box_alignment=box_alignment)
|
||||
x += width
|
||||
tdi += 1
|
||||
tdi += column_span
|
||||
|
||||
y += height
|
||||
total_width = sum(column_widths) + (self.margin * 2)
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
@@ -82,20 +157,16 @@ class Scheduler:
|
||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def add_text(self, text, x, y):
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
str(text).upper(),
|
||||
insert=tuple((x, y)),
|
||||
**{
|
||||
"font-size": 4.13,
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "start",
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "hanging",
|
||||
}
|
||||
)
|
||||
)
|
||||
def add_text(self, text, x, y, box_alignment="bottom-left"):
|
||||
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
text_params = {
|
||||
"font-size": FONT_SIZE,
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "start",
|
||||
}
|
||||
text_params.update(box_alignment_params)
|
||||
text_tag = self.svg.text(str(text).upper(), insert=tuple((x, y)), **text_params)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
# XSL is what defines the units of measurements in ODF
|
||||
|
||||
@@ -234,7 +234,7 @@ class SvgWriter:
|
||||
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
|
||||
rl *= unit_scale
|
||||
rl = "{:.3f}m".format(rl)
|
||||
text_style = self.get_box_alignment_parameters("bottom-left")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
|
||||
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
|
||||
if tag:
|
||||
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
|
||||
@@ -263,7 +263,7 @@ class SvgWriter:
|
||||
"UP",
|
||||
insert=tuple(text_position),
|
||||
class_="STAIR",
|
||||
**self.get_box_alignment_parameters("center"),
|
||||
**SvgWriter.get_box_alignment_parameters("center"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -287,7 +287,7 @@ class SvgWriter:
|
||||
)
|
||||
line["stroke-dasharray"] = "12.5, 3, 3, 3"
|
||||
axis_tag = tool.Ifc.get_entity(obj).Name
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
axis_tag,
|
||||
@@ -557,7 +557,7 @@ class SvgWriter:
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = symbol_position_svg
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
reference_id,
|
||||
@@ -590,7 +590,7 @@ class SvgWriter:
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = symbol_position_svg
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
|
||||
@@ -617,7 +617,8 @@ class SvgWriter:
|
||||
return (reference_id, sheet_id)
|
||||
return ("-", "-")
|
||||
|
||||
def get_box_alignment_parameters(self, box_alignment):
|
||||
@staticmethod
|
||||
def get_box_alignment_parameters(box_alignment):
|
||||
"""Convenience method to get svg parameters for text alignment
|
||||
in a readable way.
|
||||
|
||||
@@ -673,7 +674,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 +687,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 +705,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
|
||||
@@ -715,7 +722,7 @@ class SvgWriter:
|
||||
text_tag = self.svg.text(
|
||||
"",
|
||||
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
|
||||
**self.get_box_alignment_parameters(text_literal.BoxAlignment),
|
||||
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
@@ -732,7 +739,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
|
||||
@@ -790,7 +796,7 @@ class SvgWriter:
|
||||
rl = "{:.3f}m".format(rl)
|
||||
|
||||
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
|
||||
text_style = self.get_box_alignment_parameters(box_alignment)
|
||||
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
"RL +{}".format(rl),
|
||||
@@ -912,7 +918,7 @@ class SvgWriter:
|
||||
text_offset = (text_position - center_position).xy.normalized() * 5
|
||||
text_position += text_offset
|
||||
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
angle_text = abs(round(math.degrees(angle), 3))
|
||||
if is_reflex:
|
||||
angle_text = 360 - angle_text
|
||||
@@ -973,7 +979,7 @@ class SvgWriter:
|
||||
)
|
||||
text_position += text_offset
|
||||
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
radius = (points[-1].co - points[-2].co).length
|
||||
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
|
||||
tag = element.Description or f"R{radius}"
|
||||
@@ -1046,7 +1052,7 @@ class SvgWriter:
|
||||
)
|
||||
text_position += text_offset
|
||||
|
||||
text_style = self.get_box_alignment_parameters("center")
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
|
||||
|
||||
def draw_diameter_annotations(self, obj):
|
||||
@@ -1146,7 +1152,7 @@ class SvgWriter:
|
||||
text_format(text),
|
||||
insert=text_position,
|
||||
class_="DIMENSION",
|
||||
**(text_kwargs | self.get_box_alignment_parameters(box_alignment)),
|
||||
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
|
||||
)
|
||||
|
||||
if not show_description_only:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -448,18 +448,30 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.roof_added_previously:
|
||||
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
|
||||
|
||||
# rejecting original roof shape to be safe
|
||||
# taking into account only it's bounding box dimensions
|
||||
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
|
||||
min_x, min_y = -5, -5
|
||||
max_x, max_y = 5, 5
|
||||
min_z = 0
|
||||
else:
|
||||
bbox = tool.Blender.get_object_bounding_box(obj)
|
||||
min_x = bbox["min_x"]
|
||||
min_y = bbox["min_y"]
|
||||
max_x = bbox["max_x"]
|
||||
max_y = bbox["max_y"]
|
||||
min_z = bbox["min_z"]
|
||||
|
||||
roof_data = props.get_general_kwargs()
|
||||
path_data = get_path_data(obj)
|
||||
if not path_data:
|
||||
path_data = {
|
||||
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
|
||||
"verts": [
|
||||
Vector([-5.0, -5.0, 0.0]) / si_conversion,
|
||||
Vector([-5.0, 5.0, 0.0]) / si_conversion,
|
||||
Vector([5.0, 5.0, 0.0]) / si_conversion,
|
||||
Vector([5.0, -5.0, 0.0]) / si_conversion,
|
||||
],
|
||||
}
|
||||
path_data = {
|
||||
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
|
||||
"verts": [
|
||||
Vector([min_x, min_y, min_z]) / si_conversion,
|
||||
Vector([min_x, max_y, min_z]) / si_conversion,
|
||||
Vector([max_x, max_y, min_z]) / si_conversion,
|
||||
Vector([max_x, min_y, min_z]) / si_conversion,
|
||||
],
|
||||
}
|
||||
roof_data["path_data"] = path_data
|
||||
|
||||
update_bbim_roof_pset(element, roof_data)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -132,9 +132,9 @@ class BIM_PT_authoring(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
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.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)
|
||||
|
||||
@@ -44,6 +44,7 @@ class BimTool(WorkSpaceTool):
|
||||
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
|
||||
# ("bim.sync_modeling", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
|
||||
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
|
||||
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
@@ -57,6 +58,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")]}),
|
||||
@@ -260,6 +262,11 @@ class BimToolUI:
|
||||
else:
|
||||
row.operator("bim.add_potential_opening", text="Add Void")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_B")
|
||||
row.operator("bim.add_boundary", text="Add Boundary")
|
||||
|
||||
if AuthoringData.data["is_voidable_element"]:
|
||||
if AuthoringData.data["has_visible_openings"]:
|
||||
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
|
||||
@@ -274,11 +281,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):
|
||||
@@ -571,6 +576,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.extend_profile(join_type="V")
|
||||
|
||||
def hotkey_S_B(self):
|
||||
bpy.ops.bim.add_boundary()
|
||||
|
||||
def hotkey_S_O(self):
|
||||
if len(bpy.context.selected_objects) == 2:
|
||||
bpy.ops.bim.add_opening()
|
||||
@@ -581,6 +589,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
|
||||
@@ -44,13 +46,12 @@ def purge():
|
||||
|
||||
def updateResourceName(self, context):
|
||||
props = context.scene.BIMResourceProperties
|
||||
if not props.is_resource_update_enabled or self.name == "Unnamed":
|
||||
if not props.is_resource_update_enabled:
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
tool.Ifc.run(
|
||||
"resource.edit_resource",
|
||||
self.file,
|
||||
**{"resource": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
|
||||
resource=tool.Ifc.get().by_id(self.ifc_definition_id),
|
||||
attributes={"Name": self.name},
|
||||
)
|
||||
if props.active_resource_id == self.ifc_definition_id:
|
||||
attribute = props.resource_attributes.get("Name")
|
||||
@@ -71,12 +72,42 @@ 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()
|
||||
|
||||
|
||||
def updateResourceUsage(self, context):
|
||||
props = context.scene.BIMResourceProperties
|
||||
if not props.is_resource_update_enabled:
|
||||
return
|
||||
|
||||
if self.schedule_usage == "":
|
||||
return
|
||||
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
if not resource.Usage:
|
||||
tool.Ifc.run(
|
||||
"resource.add_resource_time",
|
||||
resource=resource,
|
||||
)
|
||||
resource.Usage.ScheduleUsage = self.schedule_usage
|
||||
blenderbim.bim.module.pset.data.refresh()
|
||||
tool.Resource.load_resource_properties()
|
||||
|
||||
|
||||
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):
|
||||
name: StringProperty(name="Name", update=updateResourceName)
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
schedule_usage: FloatProperty(name="Schedule Usage")
|
||||
schedule_usage: FloatProperty(name="Schedule Usage", update=updateResourceUsage)
|
||||
has_children: BoolProperty(name="Has Children")
|
||||
is_expanded: BoolProperty(name="Is Expanded")
|
||||
level_index: IntProperty(name="Level Index")
|
||||
@@ -113,3 +144,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,
|
||||
@@ -114,6 +117,7 @@ classes = (
|
||||
operator.DisableEditingTaskAnimationColors,
|
||||
operator.LoadProductTasks,
|
||||
operator.HighlightTask,
|
||||
operator.CreateBaseline,
|
||||
prop.WorkPlan,
|
||||
prop.BIMWorkPlanProperties,
|
||||
prop.Task,
|
||||
|
||||
@@ -19,12 +19,15 @@
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.doc import get_predefined_type_doc
|
||||
import ifcopenshell.util.date as dateutil
|
||||
|
||||
|
||||
def refresh():
|
||||
SequenceData.is_loaded = False
|
||||
WorkPlansData.is_loaded = False
|
||||
TaskICOMData.is_loaded = False
|
||||
WorkScheduleData.is_loaded = False
|
||||
|
||||
|
||||
class SequenceData:
|
||||
@@ -34,6 +37,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 +225,66 @@ 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 WorkScheduleData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"can_have_baselines": cls.can_have_baselines(),
|
||||
"active_work_schedule_baselines": cls.active_work_schedule_baselines(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def can_have_baselines(cls):
|
||||
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
|
||||
return False
|
||||
return (
|
||||
tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).PredefinedType
|
||||
== "PLANNED"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def active_work_schedule_baselines(cls):
|
||||
results = []
|
||||
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
|
||||
return []
|
||||
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).Declares:
|
||||
for work_schedule in rel.RelatedObjects:
|
||||
if work_schedule.PredefinedType == "BASELINE":
|
||||
results.append(
|
||||
{
|
||||
"id": work_schedule.id(),
|
||||
"name": work_schedule.Name or "Unnamed",
|
||||
"date": str(dateutil.ifc2datetime(work_schedule.CreationDate)),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
class WorkPlansData:
|
||||
data = {}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
let g;
|
||||
|
||||
function create_gantt_chart(json_data) {
|
||||
g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'week');
|
||||
g.setOptions({
|
||||
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vQuarterColWidth: 36,
|
||||
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
|
||||
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
|
||||
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
|
||||
vLang: "en",
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: false, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
|
||||
vUseToolTip: false, // Disable tooltips.
|
||||
vTotalHeight: 900,
|
||||
});
|
||||
JSGantt.parseJSONString(json_data, g);
|
||||
g.Draw();
|
||||
}
|
||||
function set_language(e){
|
||||
lang = e && e.target ? e.target.value : "en";
|
||||
g.setOptions({
|
||||
vLang: lang,
|
||||
});
|
||||
g.Draw();
|
||||
}
|
||||
|
||||
function setupPage(name){
|
||||
let print_button = document.createElement("button");
|
||||
print_button.id = "print-schedule";
|
||||
print_button.innerHTML = "Print";
|
||||
print_button.addEventListener("click", function(){
|
||||
g.setTotalHeight("");
|
||||
g.Draw();
|
||||
var values = document.getElementById("print_page_size").value.split(",")
|
||||
let css =
|
||||
"@media print {\n @page {\n size: " + values[0] + "mm " + values[1] + "mm;\n }\n /* set gantt container to the same width as the page */\n .gantt {\n width: " + values[0] + "mm;\n }\n };";
|
||||
g.printChart(values[0], values[1], css);
|
||||
g.setTotalHeight(900);
|
||||
g.Draw();
|
||||
});
|
||||
document.getElementById("print_options").appendChild(print_button);
|
||||
|
||||
// add a nice layout to show the schedule name
|
||||
let schedule_name = document.createElement("h1");
|
||||
// create string to say "Schedule: <name>"
|
||||
let schedule_name_string = document.createTextNode("Schedule: " + name);
|
||||
// add the string to the h1 element
|
||||
schedule_name.appendChild(schedule_name_string);
|
||||
document.getElementById("schedule-data").appendChild(schedule_name);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
.gtaskcellwkend,
|
||||
.gtaskcellcurrent,
|
||||
.gminorheadingwkend {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
.gitemhighlight td {
|
||||
background-color: #ffdaaa;
|
||||
}
|
||||
|
||||
.gtaskblue {
|
||||
background: #4281A4;
|
||||
}
|
||||
|
||||
.gtaskred {
|
||||
background: #C1666B;
|
||||
}
|
||||
|
||||
.gtaskgreen {
|
||||
background: #48A9A6;
|
||||
}
|
||||
|
||||
.gtaskyellow {
|
||||
background: #D4B483;
|
||||
}
|
||||
|
||||
.gmainleft {
|
||||
overflow: visible;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.gname.ggroupitem {
|
||||
background-color: #4c92ec;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gtaskcellwkend,
|
||||
.gtaskcellcurrent,
|
||||
.gminorheadingwkend {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
.gitemhighlight td {
|
||||
background-color: #ffdaaa;
|
||||
}
|
||||
|
||||
.gtaskblue {
|
||||
background: #4281A4;
|
||||
}
|
||||
|
||||
.gtaskred {
|
||||
background: #C1666B;
|
||||
}
|
||||
|
||||
.gtaskgreen {
|
||||
background: #48A9A6;
|
||||
}
|
||||
|
||||
.gtaskyellow {
|
||||
background: #D4B483;
|
||||
}
|
||||
|
||||
.gmainleft {
|
||||
resize: horizontal;
|
||||
height:100%;
|
||||
overflow-x: visible;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.gmainright {
|
||||
height:100%;
|
||||
overflow: auto;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.gcaption {
|
||||
/* github style header 3 */
|
||||
font-size: 0.75rem;
|
||||
font-weight: 200;
|
||||
line-height: 1.25;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.gchartgrid {
|
||||
background-color: transparent;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.gchartcontainer {
|
||||
border: 1px solid #e1e1e1;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button, .sweet_button {
|
||||
border: none;
|
||||
background-color: #ffffff;
|
||||
color: black;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
/* add shadow */
|
||||
box-shadow: 0 0 0.7rem rgba(95, 95, 95, 0.4);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -168,15 +168,14 @@ class EnableEditingWorkSchedule(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingWorkScheduleTasks(bpy.types.Operator):
|
||||
class EnableEditingWorkScheduleTasks(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_work_schedule_tasks"
|
||||
bl_label = "Enable Editing Work Schedule Tasks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
core.enable_editing_work_schedule_tasks(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadTaskProperties(bpy.types.Operator):
|
||||
@@ -275,13 +274,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 +1294,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 +1309,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 +1326,66 @@ 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)
|
||||
|
||||
|
||||
class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.create_baseline"
|
||||
bl_label = "Create Schedule Baseline"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.create_baseline(
|
||||
tool.Ifc, tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule), name=self.name
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "name", text="Baseline Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
@@ -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,9 @@ 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)
|
||||
should_show_schedule_baseline_ui: BoolProperty(name="Baselines", default=False)
|
||||
|
||||
|
||||
class BIMTaskTreeProperties(PropertyGroup):
|
||||
@@ -501,3 +523,4 @@ class BIMAnimationProperties(PropertyGroup):
|
||||
description="color picker",
|
||||
update=update_color_progress,
|
||||
)
|
||||
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
|
||||
|
||||
@@ -21,7 +21,7 @@ import blenderbim.bim.helper
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import draw_attributes
|
||||
from blenderbim.bim.module.sequence.data import WorkPlansData, SequenceData, TaskICOMData
|
||||
from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData
|
||||
|
||||
|
||||
class BIM_PT_work_plans(Panel):
|
||||
@@ -113,55 +113,101 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw(self, context):
|
||||
if not SequenceData.is_loaded:
|
||||
SequenceData.load()
|
||||
if not WorkScheduleData.is_loaded:
|
||||
WorkScheduleData.load()
|
||||
self.props = context.scene.BIMWorkScheduleProperties
|
||||
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)
|
||||
|
||||
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 not work_schedule["PredefinedType"] == "BASELINE":
|
||||
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":
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
|
||||
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")
|
||||
else:
|
||||
row.operator(
|
||||
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
|
||||
).work_schedule = work_schedule_id
|
||||
row.operator(
|
||||
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
|
||||
).work_schedule = work_schedule_id
|
||||
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
|
||||
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
|
||||
if WorkScheduleData.data["can_have_baselines"]:
|
||||
row3 = col.row()
|
||||
row3.alignment = "RIGHT"
|
||||
row3.prop(self.props, "should_show_schedule_baseline_ui", icon="RESTRICT_INSTANCED_OFF")
|
||||
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"
|
||||
).work_schedule = work_schedule_id
|
||||
row.operator(
|
||||
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
|
||||
).work_schedule = work_schedule_id
|
||||
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
|
||||
|
||||
if self.props.active_work_schedule_id == work_schedule_id:
|
||||
if self.props.editing_type == "WORK_SCHEDULE":
|
||||
self.draw_editable_work_schedule_ui()
|
||||
elif self.props.editing_type == "TASKS":
|
||||
if self.props.should_show_column_ui:
|
||||
if self.props.active_work_schedule_id == work_schedule_id:
|
||||
if self.props.editing_type == "WORK_SCHEDULE":
|
||||
self.draw_editable_work_schedule_ui()
|
||||
elif self.props.editing_type == "TASKS":
|
||||
self.draw_baseline_ui(work_schedule_id)
|
||||
self.draw_column_ui()
|
||||
if self.props.should_show_visualisation_ui:
|
||||
self.draw_visualisation_ui()
|
||||
self.draw_editable_task_ui(work_schedule_id)
|
||||
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)
|
||||
else:
|
||||
self.draw_readonly_work_schedule_ui(work_schedule_id)
|
||||
|
||||
def draw_task_operators(self):
|
||||
row = self.layout.row(align=True)
|
||||
@@ -178,18 +224,30 @@ 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")
|
||||
if not self.props.should_show_column_ui:
|
||||
return
|
||||
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 +284,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 +295,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 +373,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 +389,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":
|
||||
@@ -381,6 +472,48 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_editable_task_time_attributes_ui(self):
|
||||
blenderbim.bim.helper.draw_attributes(self.props.task_time_attributes, self.layout)
|
||||
|
||||
def draw_baseline_ui(self, work_schedule_id):
|
||||
if not self.props.should_show_schedule_baseline_ui:
|
||||
return
|
||||
row3 = self.layout.row()
|
||||
row3.alignment = "RIGHT"
|
||||
row3.operator("bim.create_baseline", text="Add Baseline", icon="ADD").work_schedule = work_schedule_id
|
||||
if WorkScheduleData.data["active_work_schedule_baselines"]:
|
||||
for baseline in WorkScheduleData.data["active_work_schedule_baselines"]:
|
||||
baseline_row = self.layout.row()
|
||||
baseline_row.alignment = "RIGHT"
|
||||
baseline_row.label(
|
||||
text="{} @ {}".format(baseline["name"], baseline["date"]), icon="RESTRICT_INSTANCED_OFF"
|
||||
)
|
||||
baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"]
|
||||
baseline_row.operator(
|
||||
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
|
||||
).work_schedule = baseline["id"]
|
||||
|
||||
def draw_readonly_work_schedule_ui(self, work_schedule_id):
|
||||
if self.props.active_work_schedule_id == work_schedule_id:
|
||||
row = self.layout.row()
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.disable_editing_work_schedule", text="Disable editing", 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="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)
|
||||
if self.props.editing_type == "TASKS":
|
||||
self.draw_column_ui()
|
||||
self.layout.template_list(
|
||||
"BIM_UL_tasks",
|
||||
"",
|
||||
self.tprops,
|
||||
"tasks",
|
||||
self.props,
|
||||
"active_task_index",
|
||||
)
|
||||
|
||||
class BIM_PT_task_icom(Panel):
|
||||
bl_label = "IFC Task ICOM"
|
||||
@@ -430,7 +563,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 +652,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 +716,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 +724,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 +740,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 +972,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,22 +129,26 @@ 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()
|
||||
ifc.run("sequence.edit_task", task=task, attributes=attributes)
|
||||
@@ -165,7 +169,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()
|
||||
|
||||
|
||||
@@ -245,7 +249,10 @@ def unassign_input_products(ifc, sequence, spatial, task=None, products=None):
|
||||
def assign_resource(ifc, sequence, task=None):
|
||||
resource = sequence.get_selected_resource()
|
||||
sub_resource = ifc.run(
|
||||
"resource.add_resource", parent_resource=resource, ifc_class=resource.is_a(), name=resource.Name
|
||||
"resource.add_resource",
|
||||
parent_resource=resource,
|
||||
ifc_class=resource.is_a(),
|
||||
name="{}/{}".format(resource.Name or "Unnamed", task.Name or ""),
|
||||
)
|
||||
ifc.run("sequence.assign_process", relating_process=task, related_object=sub_resource)
|
||||
resources = sequence.get_task_resources(task)
|
||||
@@ -430,6 +437,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 +471,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 +480,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 +545,22 @@ 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()
|
||||
|
||||
|
||||
def create_baseline(ifc, sequence, work_schedule, name):
|
||||
ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,6 +71,9 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
return
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
if element.is_a("IfcRelSpaceBoundary"):
|
||||
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
|
||||
return bpy.data.objects.remove(obj)
|
||||
if obj.users_collection and obj.users_collection[0].name == obj.name:
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not parent:
|
||||
@@ -84,6 +87,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 +101,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 +320,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,82 @@ 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)
|
||||
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
|
||||
@@ -1485,7 +1498,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
"pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
|
||||
"pOpen": 1,
|
||||
"pCost": 1,
|
||||
"ifcduration": task_time.ScheduleDuration if task_time else "",
|
||||
"ifcduration": str(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleDuration)) if (task_time and task_time.ScheduleDuration) else "",
|
||||
}
|
||||
|
||||
if task_time and task_time.IsCritical:
|
||||
@@ -1506,15 +1519,15 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def generate_gantt_browser_chart(cls, task_json):
|
||||
active_work_schedule = cls.get_active_work_schedule()
|
||||
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
|
||||
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
|
||||
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json)}))
|
||||
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json), "name":active_work_schedule.Name or "Unnamed"}))
|
||||
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 +1539,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
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 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
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, boundary=None):
|
||||
"""Removes a space boundary
|
||||
|
||||
The relating space or related building element is untouched. Only the
|
||||
boundary and its connection geometry is removed.
|
||||
|
||||
:param boundary: The IfcRelSpaceBoundary you want to remove.
|
||||
:type boundary: ifcopenshell.entity_instance.entity_instance
|
||||
:return: None
|
||||
:rtype: None
|
||||
|
||||
Example:
|
||||
|
||||
# A boring boundary with no geometry. Note that this boundary is
|
||||
# invalid and does not relate to any space or building element.
|
||||
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
|
||||
|
||||
# Let's remove it!
|
||||
ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2)
|
||||
"""
|
||||
self.file = file
|
||||
self.settings = {"boundary": boundary}
|
||||
|
||||
def execute(self):
|
||||
geometry = self.settings["boundary"].ConnectionGeometry
|
||||
if geometry:
|
||||
self.settings["boundary"].ConnectionGeometry = None
|
||||
ifcopenshell.util.element.remove_deep2(self.file, geometry)
|
||||
self.file.remove(self.settings["boundary"])
|
||||
@@ -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]
|
||||
|
||||
@@ -37,7 +37,7 @@ class Usecase:
|
||||
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
|
||||
axis_tag="1", uvw_axes="VAxes", grid=grid)
|
||||
|
||||
# Let's create a third so we can remvoe it later
|
||||
# Let's create a third so we can remove it later
|
||||
axis_2 = ifcopenshell.api.run("grid.create_grid_axis", model,
|
||||
axis_tag="2", uvw_axes="VAxes", grid=grid)
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -105,7 +105,7 @@ class Usecase:
|
||||
else:
|
||||
ifcopenshell.api.run("type.unassign_type", self.file, related_object=self.settings["product"])
|
||||
elif inverse.is_a("IfcRelSpaceBoundary"):
|
||||
self.file.remove(inverse)
|
||||
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=inverse)
|
||||
elif inverse.is_a("IfcRelFillsElement"):
|
||||
self.file.remove(inverse)
|
||||
elif inverse.is_a("IfcRelVoidsElement"):
|
||||
|
||||
@@ -165,7 +165,9 @@ class Usecase:
|
||||
"IfcRelAssignsToControl",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
||||
"OwnerHistory": ifcopenshell.api.run(
|
||||
"owner.create_owner_history", self.file
|
||||
),
|
||||
"RelatedObjects": [task],
|
||||
"RelatingControl": self.settings["work_schedule"],
|
||||
}
|
||||
@@ -178,5 +180,9 @@ class Usecase:
|
||||
relating_object=self.settings["parent_task"],
|
||||
)
|
||||
if self.settings["parent_task"].Identification:
|
||||
task.Identification = self.settings["parent_task"].Identification + "." + str(len(rel.RelatedObjects))
|
||||
task.Identification = (
|
||||
self.settings["parent_task"].Identification
|
||||
+ "."
|
||||
+ str(len(rel.RelatedObjects))
|
||||
)
|
||||
return task
|
||||
|
||||
@@ -22,7 +22,14 @@ from datetime import datetime
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, name="Unnamed", predefined_type="NOTDEFINED", start_time=None, work_plan=None):
|
||||
def __init__(
|
||||
self,
|
||||
file,
|
||||
name="Unnamed",
|
||||
predefined_type="NOTDEFINED",
|
||||
start_time=None,
|
||||
work_plan=None,
|
||||
):
|
||||
"""Add a new work schedule
|
||||
|
||||
A work schedule is a group of tasks, where the tasks are typically
|
||||
|
||||
@@ -20,7 +20,9 @@ import ifcopenshell.util.date
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, rel_sequence=None, lag_value=None, duration_type="WORKTIME"):
|
||||
def __init__(
|
||||
self, file, rel_sequence=None, lag_value=None, duration_type="WORKTIME"
|
||||
):
|
||||
"""Assign a lag time to a sequence relationship between tasks
|
||||
|
||||
A task sequence (e.g. finish to start) may optionally have a lag time
|
||||
|
||||
@@ -93,7 +93,6 @@ class Usecase:
|
||||
"related_object": related_object,
|
||||
}
|
||||
|
||||
|
||||
def execute(self):
|
||||
if self.settings["related_object"].HasAssignments:
|
||||
for assignment in self.settings["related_object"].HasAssignments:
|
||||
|
||||
@@ -21,7 +21,13 @@ import ifcopenshell.api
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, relating_process=None, related_process=None, sequence_type="FINISH_START"):
|
||||
def __init__(
|
||||
self,
|
||||
file,
|
||||
relating_process=None,
|
||||
related_process=None,
|
||||
sequence_type="FINISH_START",
|
||||
):
|
||||
"""Assign a sequential relationship between tasks
|
||||
|
||||
Tasks in construction sequencing typically have sequence relationships
|
||||
@@ -118,7 +124,9 @@ class Usecase:
|
||||
"IfcRelSequence",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
||||
"OwnerHistory": ifcopenshell.api.run(
|
||||
"owner.create_owner_history", self.file
|
||||
),
|
||||
"RelatingProcess": self.settings["relating_process"],
|
||||
"RelatedProcess": self.settings["related_process"],
|
||||
"SequenceType": self.settings["sequence_type"],
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021-2022 Dion Moult <dion@thinkmoult.com>, Yassine Oualid <yassine@sigmadimensions.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
|
||||
import ifcopenshell.util.system
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, work_schedule=None, name=None):
|
||||
"""Creates a baseline for your Work Schedule
|
||||
|
||||
Using a IfcWorkSchdule having PredefinedType=PLANNED,
|
||||
We can create a baseline for our work schedule. This IfcWorkSchedule will have PredefinedType=BASELINE
|
||||
and the IfcWorkSchedule.CreationDate indicating the date of the baseline creation, and IfcWorkSchedule.Name indicating the name of the baseline.
|
||||
|
||||
The following relationships are also baselined:
|
||||
|
||||
* Same Tasks & attributes
|
||||
* Same Task Relationships
|
||||
* Same Construction Resources
|
||||
* Same Resource Relationships
|
||||
|
||||
:param work_schedule: The planned work_schedule to baseline
|
||||
:type work_schedule: ifcopenshell.entity_instance.entity_instance
|
||||
:return: The baseline work_schedule
|
||||
:rtype: ifcopenshell.entity_instance.entity_instance
|
||||
|
||||
Example:
|
||||
.. code:: python
|
||||
|
||||
# We have a Work Schedule
|
||||
planned_work_schedule = WorkSchedule(name="Design new feature",predefinedType="PLANNED", deadline="2023-03-01")
|
||||
|
||||
# And now we have a baseline for our Work Schedule
|
||||
baseline_work_schedule = ifcopenshell.api.run("sequence.create_baseline",file, work_schedule= planned_work_schedule, name="Baseline 1")
|
||||
"""
|
||||
self.file = file
|
||||
self.settings = {"work_schedule": work_schedule, "name": name}
|
||||
|
||||
def execute(self):
|
||||
result = self.create_baseline_work_schedule(self.settings["work_schedule"])
|
||||
return result
|
||||
|
||||
def create_baseline_work_schedule(self, work_schedule):
|
||||
# create work schedule
|
||||
if not work_schedule.PredefinedType == "PLANNED":
|
||||
return
|
||||
baseline_work_schedule = ifcopenshell.api.run(
|
||||
"sequence.add_work_schedule",
|
||||
self.file,
|
||||
name=work_schedule.Name,
|
||||
predefined_type="BASELINE",
|
||||
)
|
||||
baseline_work_schedule.Name = self.settings["name"]
|
||||
self.create_baseline_reference(work_schedule, baseline_work_schedule)
|
||||
self.task_tracker = {"current": [], "duplicate": []}
|
||||
self.copy_tasks_with_reference(
|
||||
work_schedule, baseline_work_schedule
|
||||
) # TODO: Fix task nesting order when duplicating children
|
||||
self.copy_sequence_relationship(
|
||||
self.task_tracker["current"], self.task_tracker["duplicate"]
|
||||
)
|
||||
|
||||
def create_baseline_reference(self, relating_object, related_object):
|
||||
referenced_by = None
|
||||
if relating_object.Declares:
|
||||
referenced_by = relating_object.Declares[0]
|
||||
if referenced_by:
|
||||
related_objects = list(referenced_by.RelatedObjects)
|
||||
related_objects.append(related_object)
|
||||
referenced_by.RelatedObjects = related_objects
|
||||
ifcopenshell.api.run(
|
||||
"owner.update_owner_history", self.file, **{"element": referenced_by}
|
||||
)
|
||||
else:
|
||||
referenced_by = self.file.create_entity(
|
||||
"IfcRelDefinesByObject",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.run(
|
||||
"owner.create_owner_history", self.file
|
||||
),
|
||||
"RelatedObjects": [related_object],
|
||||
"RelatingObject": relating_object,
|
||||
}
|
||||
)
|
||||
return referenced_by
|
||||
|
||||
def copy_tasks_with_reference(self, work_schedule, baseline_work_schedule):
|
||||
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
||||
new_task = self.copy_task(task)
|
||||
self.file.create_entity(
|
||||
"IfcRelAssignsToControl",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.run(
|
||||
"owner.create_owner_history", self.file
|
||||
),
|
||||
"RelatedObjects": [new_task],
|
||||
"RelatingControl": baseline_work_schedule,
|
||||
}
|
||||
)
|
||||
|
||||
def copy_task(self, task):
|
||||
new_task = ifcopenshell.util.element.copy_deep(self.file, task)
|
||||
self.task_tracker["current"].append(task)
|
||||
self.task_tracker["duplicate"].append(new_task)
|
||||
self.create_baseline_reference(task, new_task)
|
||||
self.copy_indirect_attributes(task, new_task)
|
||||
return new_task
|
||||
|
||||
def copy_indirect_attributes(self, from_element, to_element):
|
||||
for inverse in self.file.get_inverse(from_element):
|
||||
if inverse.is_a("IfcRelDefinesByProperties"):
|
||||
inverse = ifcopenshell.util.element.copy(self.file, inverse)
|
||||
inverse.RelatedObjects = [to_element]
|
||||
pset = ifcopenshell.util.element.copy_deep(
|
||||
self.file, inverse.RelatingPropertyDefinition
|
||||
)
|
||||
inverse.RelatingPropertyDefinition = pset
|
||||
elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element:
|
||||
nested_tasks = [e for e in inverse.RelatedObjects]
|
||||
if nested_tasks:
|
||||
new_tasks = []
|
||||
for t in nested_tasks:
|
||||
new_task = self.copy_task(t)
|
||||
new_tasks.append(new_task)
|
||||
inverse = ifcopenshell.util.element.copy(self.file, inverse)
|
||||
inverse.RelatingObject = to_element
|
||||
inverse.RelatedObjects = new_tasks
|
||||
for t in new_tasks:
|
||||
ifcopenshell.api.run(
|
||||
"nest.unassign_object", self.file, related_object=t
|
||||
)
|
||||
rel = ifcopenshell.api.run(
|
||||
"nest.assign_object",
|
||||
self.file,
|
||||
related_object=t,
|
||||
relating_object=to_element,
|
||||
)
|
||||
elif inverse.is_a("IfcRelSequence") and (
|
||||
inverse.RelatingProcess == from_element
|
||||
or inverse.RelatedProcess == from_element
|
||||
):
|
||||
continue
|
||||
elif inverse.is_a(
|
||||
"IfcRelAssignsToControl"
|
||||
) and inverse.RelatingControl.is_a("IfcWorkSchedule"):
|
||||
continue
|
||||
elif inverse.is_a("IfcRelDefinesByObject"):
|
||||
continue
|
||||
else:
|
||||
for i, value in enumerate(inverse):
|
||||
if value == from_element:
|
||||
new_inverse = ifcopenshell.util.element.copy(self.file, inverse)
|
||||
new_inverse[i] = to_element
|
||||
elif isinstance(value, (tuple, list)) and from_element in value:
|
||||
new_value = list(value)
|
||||
new_value.append(to_element)
|
||||
inverse[i] = new_value
|
||||
|
||||
def copy_sequence_relationship(self, original_tasks, duplicated_tasks):
|
||||
for i, original_task in enumerate(original_tasks):
|
||||
for inverse in self.file.get_inverse(original_task):
|
||||
if inverse.is_a("IfcRelSequence") and (
|
||||
inverse.RelatingProcess == original_task
|
||||
or inverse.RelatedProcess == original_task
|
||||
):
|
||||
original_task_index = original_tasks.index(original_task)
|
||||
duplicated_task = duplicated_tasks[original_task_index]
|
||||
relating_process, related_process = None, None
|
||||
if inverse.RelatingProcess == original_task:
|
||||
relating_process = duplicated_task
|
||||
else:
|
||||
related_process = duplicated_task
|
||||
if inverse.RelatedProcess in original_tasks:
|
||||
related_process_index = original_tasks.index(
|
||||
inverse.RelatedProcess
|
||||
)
|
||||
related_process = duplicated_tasks[related_process_index]
|
||||
if inverse.RelatingProcess in original_tasks:
|
||||
relating_process_index = original_tasks.index(
|
||||
inverse.RelatingProcess
|
||||
)
|
||||
relating_process = duplicated_tasks[relating_process_index]
|
||||
if relating_process and related_process:
|
||||
rel = ifcopenshell.api.run(
|
||||
"sequence.assign_sequence",
|
||||
self.file,
|
||||
relating_process=relating_process,
|
||||
related_process=related_process,
|
||||
)
|
||||
if inverse.TimeLag:
|
||||
ifcopenshell.api.run(
|
||||
"sequence.assign_lag_time",
|
||||
self.file,
|
||||
rel_sequence=rel,
|
||||
lag_value= ifcopenshell.util.date.ifc2datetime(inverse.TimeLag.LagValue.wrappedValue) if inverse.TimeLag.LagValue else None,
|
||||
duration_type=inverse.TimeLag.DurationType,
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -54,7 +54,10 @@ class Usecase:
|
||||
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
|
||||
"""
|
||||
self.file = file
|
||||
self.settings = {"recurrence_pattern": recurrence_pattern, "attributes": attributes or {}}
|
||||
self.settings = {
|
||||
"recurrence_pattern": recurrence_pattern,
|
||||
"attributes": attributes or {},
|
||||
}
|
||||
|
||||
def execute(self):
|
||||
for name, value in self.settings["attributes"].items():
|
||||
|
||||
@@ -110,6 +110,16 @@ class Usecase:
|
||||
related_objects = list(inverse.RelatedObjects)
|
||||
related_objects.remove(self.settings["task"])
|
||||
inverse.RelatedObjects = related_objects
|
||||
elif inverse.is_a("IfcRelAssignsToObject"):
|
||||
if (
|
||||
inverse.RelatingObject == self.settings["task"]
|
||||
or len(inverse.RelatedObjects) == 1
|
||||
):
|
||||
self.file.remove(inverse)
|
||||
else:
|
||||
related_objects = list(inverse.RelatedObjects)
|
||||
related_objects.remove(self.settings["task"])
|
||||
inverse.RelatedObjects = related_objects
|
||||
elif inverse.is_a("IfcRelAssignsToProcess"):
|
||||
self.file.remove(inverse)
|
||||
|
||||
|
||||
@@ -55,10 +55,32 @@ class Usecase:
|
||||
definition=self.settings["work_schedule"],
|
||||
relating_context=self.file.by_type("IfcContext")[0],
|
||||
)
|
||||
for rel in self.settings["work_schedule"].Controls:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcTask"):
|
||||
if self.settings["work_schedule"].Declares:
|
||||
for rel in self.settings["work_schedule"].Declares:
|
||||
for work_schedule in rel.RelatedObjects:
|
||||
ifcopenshell.api.run(
|
||||
"sequence.remove_work_schedule",
|
||||
self.file,
|
||||
work_schedule=work_schedule,
|
||||
)
|
||||
for inverse in self.file.get_inverse(self.settings["work_schedule"]):
|
||||
if inverse.is_a("IfcRelDefinesByObject"):
|
||||
if (
|
||||
inverse.RelatingObject == self.settings["work_schedule"]
|
||||
or len(inverse.RelatedObjects) == 1
|
||||
):
|
||||
self.file.remove(inverse)
|
||||
else:
|
||||
related_objects = list(inverse.RelatedObjects)
|
||||
related_objects.remove(self.settings["work_schedule"])
|
||||
inverse.RelatedObjects = related_objects
|
||||
elif inverse.is_a("IfcRelAssignsToControl"):
|
||||
[
|
||||
ifcopenshell.api.run(
|
||||
"sequence.remove_task", self.file, task=related_object
|
||||
)
|
||||
for related_object in inverse.RelatedObjects
|
||||
if related_object.is_a("IfcTask")
|
||||
]
|
||||
|
||||
self.file.remove(self.settings["work_schedule"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 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 test.bootstrap
|
||||
import ifcopenshell.api
|
||||
|
||||
|
||||
class TestRemoveBoundary(test.bootstrap.IFC4):
|
||||
def test_run(self):
|
||||
boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary")
|
||||
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=boundary)
|
||||
assert not self.file.by_type("IfcRelSpaceBoundary")
|
||||
|
||||
def test_removing_connection_geometry(self):
|
||||
geometry = self.file.createIfcConnectionSurfaceGeometry()
|
||||
boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary")
|
||||
boundary.ConnectionGeometry = geometry
|
||||
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=boundary)
|
||||
assert not self.file.by_type("IfcRelSpaceBoundary")
|
||||
assert not self.file.by_type("IfcConnectionSurfaceGeometry")
|
||||
@@ -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