From 981b6cd65fdfeade00d8fb818bc8ac6bdf41b7f6 Mon Sep 17 00:00:00 2001 From: Sigma Dimensions <79010126+myoualid@users.noreply.github.com> Date: Wed, 4 Jan 2023 02:08:52 +0100 Subject: [PATCH] Various 4D improvements: - refactor work schedule visualisation operator - Fix text handler - Improve durations UI for task times (proposal to replicate for all durations) - Simplify durations ( P32H as P4D or P1D8H) - Implement years/months in various datetime calculations - Feature to customise animation colors per construction task type - Feature to add Visual task bars - Feature to customise Visual task bar material colors - Move helper functions from blenderbim.bim.module.sequence to ifcopenshell.util.sequence --- src/blenderbim/blenderbim/bim/helper.py | 17 +- .../bim/module/sequence/__init__.py | 15 +- .../blenderbim/bim/module/sequence/helper.py | 164 +++-- .../bim/module/sequence/operator.py | 276 +-------- .../blenderbim/bim/module/sequence/prop.py | 81 ++- .../blenderbim/bim/module/sequence/ui.py | 107 +++- src/blenderbim/blenderbim/core/sequence.py | 23 + src/blenderbim/blenderbim/tool/sequence.py | 558 ++++++++++++++++-- .../ifcopenshell/util/sequence.py | 143 ++++- 9 files changed, 972 insertions(+), 412 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index f1705e2ec5..ccbd176f9f 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -43,16 +43,25 @@ def draw_attribute(attribute, layout, copy_operator=None): return if value_name == "enum_value": prop_with_search(layout, attribute, "enum_value", text=attribute.name) + elif attribute.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]: + propis = bpy.context.scene.BIMWorkScheduleProperties + for item in propis.durations_attributes: + if item.name == attribute.name: + duration_props = item + layout.label(text=attribute.name) + layout.prop(duration_props, "years", text="Y") + layout.prop(duration_props, "months", text="M") + layout.prop(duration_props, "days", text="D") + layout.prop(duration_props, "hours", text="H") + layout.prop(duration_props, "minutes", text="Min") + layout.prop(duration_props, "seconds", text="S") + break else: layout.prop( attribute, value_name, text=attribute.name, ) - if "ScheduleDuration" in attribute.name: - layout.prop(bpy.context.scene.BIMDuration, "duration_days", text="D") - layout.prop(bpy.context.scene.BIMDuration, "duration_hours", text="H") - layout.prop(bpy.context.scene.BIMDuration, "duration_minutes", text="M") if attribute.is_optional: layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index dd24137729..603a48243f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -26,6 +26,7 @@ classes = ( operator.AddSummaryTask, operator.AddTask, operator.AddTaskColumn, + operator.AddTaskBars, operator.AddTimePeriod, operator.AddWorkCalendar, operator.AddWorkPlan, @@ -108,19 +109,23 @@ classes = ( operator.UnassignWorkSchedule, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, + operator.LoadTaskAnimationColors, + operator.DisableEditingTaskAnimationColors, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, prop.TaskResource, prop.TaskProduct, + prop.BIMDuration, prop.BIMWorkScheduleProperties, prop.BIMTaskTreeProperties, + prop.BIMTaskTypeColor, + prop.BIMAnimationProperties, prop.WorkCalendar, prop.RecurrenceComponent, prop.BIMWorkCalendarProperties, prop.DatePickerProperties, prop.BIMDateTextProperties, - prop.BIMDuration, ui.BIM_PT_work_plans, ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, @@ -130,7 +135,9 @@ classes = ( ui.BIM_UL_task_resources, ui.BIM_UL_task_outputs, ui.BIM_UL_tasks, - ui.BIM_PT_SequenceToolKit, + ui.BIM_PT_Task_Tools, + ui.BIM_PT_Task_Bar_Creator, + ui.BIM_UL_animation_colors, ) @@ -151,8 +158,8 @@ def register(): bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) + bpy.types.Scene.BIMAnimationProperties = bpy.props.PointerProperty(type=prop.BIMAnimationProperties) bpy.types.Scene.DatePickerProperties = bpy.props.PointerProperty(type=prop.DatePickerProperties) - bpy.types.Scene.BIMDuration = bpy.props.PointerProperty(type=prop.BIMDuration) bpy.types.TextCurve.BIMDateTextProperties = bpy.props.PointerProperty(type=prop.BIMDateTextProperties) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) @@ -164,7 +171,7 @@ def unregister(): del bpy.types.Scene.BIMTaskTreeProperties del bpy.types.Scene.BIMWorkCalendarProperties del bpy.types.Scene.DatePickerProperties - del bpy.types.Scene.BIMDuration + del bpy.types.Scene.BIMAnimationProperties del bpy.types.TextCurve.BIMDateTextProperties bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/helper.py b/src/blenderbim/blenderbim/bim/module/sequence/helper.py index 659f10f2d5..06742c7c1b 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/helper.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/helper.py @@ -19,26 +19,7 @@ import isodate from dateutil import parser import ifcopenshell.util.date as ifcdateutils - - -def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False): - if task.TaskTime: - current_date = ( - ifcdateutils.ifc2datetime(getattr(task.TaskTime, attribute_name)) - if getattr(task.TaskTime, attribute_name) - else "" - ) - if current_date: - return current_date - for subtask in get_all_nested_tasks(task): - current_date = derive_date(subtask, attribute_name, date=date, is_earliest=is_earliest, is_latest=is_latest) - if is_earliest: - if current_date and (date is None or current_date < date): - date = current_date - if is_latest: - if current_date and (date is None or current_date > date): - date = current_date - return date +from datetime import timedelta def parse_datetime(value): @@ -64,88 +45,77 @@ def canonicalise_time(time): return time.strftime("%d/%m/%y") -def get_nested_tasks(task): - return [ - related_object - for rel in task.IsNestedBy - for related_object in rel.RelatedObjects - if related_object.is_a("IfcTask") - ] +def parse_duration_as_blender_props(dt): + seconds = dt.seconds + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + days = dt.days + months = int(getattr(dt, "months", 0)) + years = int(getattr(dt, "years", 0)) + return { + "years": years, + "months": months, + "days": days if days else 0, + "hours": hours if hours else 0, + "minutes": minutes if minutes else 0, + "seconds": seconds if seconds else 0, + } -def get_parent_task(task): - return task.Nests[0].RelatingObject if task.Nests and task.Nests[0].RelatingObject.is_a("IfcTask") else None +def simplify_duration(durations_attributes, duration_type, prop_name): + for item in durations_attributes: + if item.name == prop_name: + duration_props = item + print(duration_props, "duration_props") + 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, + duration_props.months if duration_props.months else 0, + duration_props.days if duration_props.days else 0, + duration_props.hours if duration_props.hours else 0, + duration_props.minutes if duration_props.minutes else 0, + duration_props.seconds if duration_props.seconds else 0, + ) + duration_object = ifcdateutils.ifc2datetime(duration_string) + elif duration_props and duration_type == "WORKTIME": + years = (duration_props.years * 365 * 24 * 60 * 60) if duration_props.years else 0 + months = (duration_props.months * 30 * 24 * 60 * 60) if duration_props.months else 0 + days = (duration_props.days * 24 * 60 * 60) if duration_props.days else 0 + days_subtotal = (years + months + days) / (24 * 60 * 60) + hours = (duration_props.hours * 60 * 60) if duration_props.hours else 0 + minutes = (duration_props.minutes * 60) if duration_props.minutes else 0 + seconds = duration_props.seconds if duration_props.seconds else 0 + total_seconds = hours + minutes + seconds -def get_task_work_schedule(task): - parent_task = get_parent_task(task) - if parent_task: - get_task_work_schedule(parent_task) - else: - schedules = [ - rel.RelatingControl - for rel in task.HasAssignments - if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule") - ] - print(f"Returning {schedules}") - return schedules + # TODO: implement actual calendar worktime + calendar_seconds_per_day = 8 * 60 * 60 + extra_days, seconds_left = divmod(total_seconds, calendar_seconds_per_day) + total_days = days_subtotal + extra_days + duration_object = timedelta(days=total_days, seconds=seconds_left) + if duration_object: + total_days = int(duration_object.days) + seconds_left = int(duration_object.seconds) + years, days = divmod(total_days, 365) + if hasattr(duration_object, "years"): + years += duration_object.years + months, days = divmod(days, 30) + if hasattr(duration_object, "months"): + months += duration_object.months -def get_all_nested_tasks(task): - for nested_task in get_nested_tasks(task): - yield nested_task - yield from get_all_nested_tasks(nested_task) + if months >= 12: + extra_years, months = divmod(months, 12) + years += extra_years + hours, seconds = divmod(seconds_left, 3600) + minutes, seconds = divmod(seconds, 60) -def get_work_schedule_tasks(work_schedule): - tasks = [] - for root_task in get_root_tasks(work_schedule): - nested_tasks = get_all_nested_tasks(root_task) - tasks.extend(nested_tasks) - return tasks - - -def get_root_tasks(work_schedule): - return [obj for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")] - - -def get_root_tasks_ids(work_schedule): - return [obj.id() for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")] - - -def guess_date_range(work_schedule): - earliest = None - latest = None - root_tasks = get_root_tasks(work_schedule) - tasks_with_assignements = [] - for task in root_tasks: - if has_task_outputs(task): - tasks_with_assignements.append(task) - for sub_task in get_all_nested_tasks(task): - if has_task_outputs(sub_task): - tasks_with_assignements.append(sub_task) - - for task in tasks_with_assignements: - derived_start = derive_date(task, "ScheduleStart", is_earliest=True) - derived_finish = derive_date(task, "ScheduleFinish", is_latest=True) - if derived_start and (not earliest or derived_start < earliest): - earliest = derived_start - if derived_finish and (not latest or derived_finish > latest): - latest = derived_finish - return earliest, latest - - -def get_direct_task_outputs(task): - return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")] - - -def get_task_outputs(task, is_deep=False): - if not is_deep: - return get_direct_task_outputs(task) - else: - nested_tasks = get_all_nested_tasks(task) - return [output for nested_task in nested_tasks for output in get_direct_task_outputs(nested_task)] - - -def has_task_outputs(task): - return len(get_task_outputs(task)) > 0 + return "P{}Y{}M{}DT{}H{}M{}S".format( + int(years), + int(months), + int(days), + int(hours), + int(minutes), + int(seconds), + ) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 6560c57f71..b0de7a29bc 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -35,19 +35,6 @@ from ifcopenshell.api.sequence.data import Data from ifcopenshell.api.resource.data import Data as ResourceData -def animate_text(scene, context): - data = bpy.data.curves.get("Timeline") - if not data or not bpy.data.objects.get("Timeline"): - self.remove_text_animation_handler() - scene.frame_current - props = data.BIMDateTextProperties - start = parser.parse(props.start, dayfirst=True, fuzzy=True) - finish = parser.parse(props.finish, dayfirst=True, fuzzy=True) - duration = finish - start - frame_date = (((scene.frame_current - props.start_frame) / props.total_frames) * duration) + start - data.body = frame_date.date().isoformat() - - class AddWorkPlan(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_work_plan" bl_label = "Add Work Plan" @@ -1104,239 +1091,9 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): work_schedule: bpy.props.IntProperty() def execute(self, context): - self.file = IfcStore.get_file() - self.props = context.scene.BIMWorkScheduleProperties - self.start = parser.parse(self.props.visualisation_start, dayfirst=True, fuzzy=True) - self.finish = parser.parse(self.props.visualisation_finish, dayfirst=True, fuzzy=True) - self.duration = self.finish - self.start - self.start_frame = 1 - self.total_frames = self.calculate_total_frames(context) - self.preprocess_tasks() - - for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: - continue - self.earliest_frame = None - product_frames = self.product_frames.get(obj.BIMObjectProperties.ifc_definition_id, []) - for product_frame in product_frames: - if product_frame["relationship"] == "input": - self.animate_input(obj, product_frame) - elif product_frame["relationship"] == "output": - self.animate_output(obj, product_frame) - self.add_text_animation_handler() - - area = next(area for area in context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].shading.color_type = "OBJECT" - context.scene.frame_start = self.start_frame - context.scene.frame_end = int(self.start_frame + self.total_frames) - # with open("/home/dion/animation.json", "w") as json_file: - # guid_frames = {} - # for k, v in self.product_frames.items(): - # guid_frames[self.file.by_id(k).GlobalId] = v - # json.dump(guid_frames, json_file) + core.visualise_work_schedule_date_range(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule)) return {"FINISHED"} - def add_text_animation_handler(self): - data = bpy.data.curves.get("Timeline") - if not data: - data = bpy.data.curves.new(type="FONT", name="Timeline") - obj = bpy.data.objects.get("Timeline") - if not obj: - obj = bpy.data.objects.new(name="Timeline", object_data=data) - bpy.context.scene.collection.objects.link(obj) - obj.data.BIMDateTextProperties.start_frame = self.start_frame - obj.data.BIMDateTextProperties.total_frames = int(self.total_frames) - obj.data.BIMDateTextProperties.start = self.props.visualisation_start - obj.data.BIMDateTextProperties.finish = self.props.visualisation_finish - bpy.app.handlers.frame_change_post.append(animate_text) - - def remove_text_animation_handler(self): - bpy.app.handlers.frame_change_post.remove(animate_text) - - def animate_input(self, obj, product_frame): - if product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]: - self.animate_movement_from(obj, product_frame) - elif product_frame["type"] in ["DEMOLITION", "DISMANTLE", "DISPOSAL", "REMOVAL"]: - self.animate_destruction(obj, product_frame) - else: - self.animate_consumption(obj, product_frame) - - def animate_output(self, obj, product_frame): - if product_frame["type"] in ["CONSTRUCTION", "INSTALLATION", "NOTDEFINED"]: - self.animate_creation(obj, product_frame) - elif product_frame["type"] in ["ATTENDANCE", "MAINTENANCE", "OPERATION", "RENOVATION"]: - self.animate_operation(obj, product_frame) - elif product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]: - self.animate_movement_to(obj, product_frame) - else: - self.animate_operation(obj, product_frame) - - def animate_creation(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.hide_viewport = True - obj.hide_render = True - obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) - obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.hide_viewport = False - obj.hide_render = False - obj.color = (0.0, 1.0, 0.0, 1) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - - def animate_destruction(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.color = (1.0, 1.0, 1.0, 1) - obj.hide_viewport = False - obj.hide_render = False - obj.keyframe_insert(data_path="color", frame=self.start_frame) - obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) - obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) - obj.color = (1.0, 0.0, 0.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.hide_viewport = True - obj.hide_render = True - obj.color = (0.0, 0.0, 0.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) - - def animate_operation(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) - obj.color = (0.0, 0.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - - def animate_movement_to(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.hide_viewport = True - obj.hide_render = True - obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) - obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.hide_viewport = False - obj.hide_render = False - obj.color = (1.0, 1.0, 0.0, 1) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - - def animate_movement_from(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.hide_viewport = False - obj.hide_render = False - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) - obj.color = (1.0, 0.5, 0.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.hide_viewport = True - obj.hide_render = True - obj.color = (0.0, 0.0, 0.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) - - def animate_consumption(self, obj, product_frame): - if self.earliest_frame is None or product_frame["STARTED"] < self.earliest_frame: - obj.color = (1.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=self.start_frame) - self.earliest_frame = product_frame["STARTED"] - obj.hide_viewport = False - obj.hide_render = False - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) - obj.color = (0.0, 1.0, 1.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) - obj.hide_viewport = True - obj.hide_render = True - obj.color = (0.0, 0.0, 0.0, 1) - obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) - obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) - - def calculate_total_frames(self, context): - if self.props.speed_types == "FRAME_SPEED": - return self.calculate_using_frames( - self.start, - self.finish, - self.props.speed_animation_frames, - isodate.parse_duration(self.props.speed_real_duration), - ) - elif self.props.speed_types == "DURATION_SPEED": - return self.calculate_using_duration( - self.start, - self.finish, - context.scene.render.fps, - isodate.parse_duration(self.props.speed_animation_duration), - isodate.parse_duration(self.props.speed_real_duration), - ) - elif self.props.speed_types == "MULTIPLIER_SPEED": - return self.calculate_using_multiplier( - self.start, - self.finish, - context.scene.render.fps, - self.props.speed_multiplier, - ) - - def calculate_using_multiplier(self, start, finish, fps, multiplier): - animation_time = (finish - start) / multiplier - return animation_time.total_seconds() * fps - - def calculate_using_duration(self, start, finish, fps, animation_duration, real_duration): - return self.calculate_using_multiplier(start, finish, fps, real_duration / animation_duration) - - def calculate_using_frames(self, start, finish, animation_frames, real_duration): - return ((finish - start) / real_duration) * animation_frames - - def preprocess_tasks(self): - self.product_frames = {} - for rel in self.file.by_id(self.work_schedule).Controls or []: - for related_object in rel.RelatedObjects: - if related_object.is_a("IfcTask"): - self.preprocess_task(related_object) - - def preprocess_task(self, task): - for rel in task.IsNestedBy or []: - for related_object in rel.RelatedObjects: - self.preprocess_task(related_object) - start = helper.derive_date(task, "ScheduleStart", is_earliest=True) - finish = helper.derive_date(task, "ScheduleFinish", is_latest=True) - if not start or not finish: - return - if not Data.is_loaded: - Data.load(self.file) # TO DO: REFACTOR OPERATOR - for output_id in Data.tasks[task.id()]["Outputs"]: - self.add_product_frame(output_id, task, start, finish, "output") - for input_id in Data.tasks[task.id()]["Inputs"]: - self.add_product_frame(input_id, task, start, finish, "input") - - def add_product_frame(self, product_id, task, start, finish, relationship): - self.product_frames.setdefault(product_id, []).append( - { - "type": task.PredefinedType, - "relationship": relationship, - "STARTED": round(self.start_frame + (((start - self.start) / self.duration) * self.total_frames)), - "COMPLETED": round(self.start_frame + (((finish - self.start) / self.duration) * self.total_frames)), - } - ) - class BlenderBIM_DatePicker(bpy.types.Operator): bl_label = "Date Picker" @@ -1567,3 +1324,34 @@ class ContractAllTasks(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.contract_all_tasks(tool.Sequence) + + +class AddTaskBars(bpy.types.Operator): + bl_idname = "bim.add_task_bars" + bl_label = "Show Task Bars" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Shows the Status of each task" + + def execute(self, context): + core.add_task_bars(tool.Sequence) + return {"FINISHED"} + + +class LoadTaskAnimationColors(bpy.types.Operator): + bl_idname = "bim.enable_editing_task_animation_colors" + bl_label = "Load Animation Colors" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + core.enable_editing_task_animation_colors(tool.Sequence) + return {"FINISHED"} + + +class DisableEditingTaskAnimationColors(bpy.types.Operator): + bl_idname = "bim.disable_editing_task_animation_colors" + bl_label = "Disable Editing Colors" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + core.disable_editing_task_animation_colors(tool.Sequence) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 7d7f993fcb..d44d3386f3 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -249,6 +249,28 @@ def update_visualisation_start_finish(self, context, startfinish): setattr(self, startfinish, canonical_value) +def update_color_full(self, context): + material = bpy.data.materials.get("color_full") + if material: + color_full = bpy.context.scene.BIMAnimationProperties.color_full + inputs = material.node_tree.nodes["Principled BSDF"].inputs + color = inputs["Base Color"].default_value + color[0] = color_full.r + color[1] = color_full.g + color[2] = color_full.b + + +def update_color_progress(self, context): + material = bpy.data.materials.get("color_progress") + if material: + color_progress = bpy.context.scene.BIMAnimationProperties.color_progress + inputs = material.node_tree.nodes["Principled BSDF"].inputs + color = inputs["Base Color"].default_value + color[0] = color_progress.r + color[1] = color_progress.g + color[2] = color_progress.b + + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) @@ -256,6 +278,7 @@ class Task(PropertyGroup): has_children: BoolProperty(name="Has Children") is_selected: BoolProperty(name="Is Selected") is_expanded: BoolProperty(name="Is Expanded") + has_bar_visual: BoolProperty(name="Bar Visualization", default=False) level_index: IntProperty(name="Level Index") duration: StringProperty(name="Duration", update=updateTaskDuration) start: StringProperty(name="Start", update=updateTaskTimeStart) @@ -294,7 +317,18 @@ class BIMWorkPlanProperties(PropertyGroup): work_schedules: EnumProperty(items=getWorkSchedules, name="Work Schedules") +class BIMDuration(PropertyGroup): + name: StringProperty(name="Attribute") + years: IntProperty(name="Years") + months: IntProperty(name="Months") + days: IntProperty(name="Days ") + hours: IntProperty(name="Hours") + minutes: IntProperty(name="Minutes") + seconds: IntProperty(name="Seconds") + + class BIMWorkScheduleProperties(PropertyGroup): + durations_attributes: CollectionProperty(name="Durations Attributes", type=BIMDuration) work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars") work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) editing_type: StringProperty(name="Editing Type") @@ -305,6 +339,7 @@ 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_column_ui: BoolProperty(name="Should Show Column UI", default=False) columns: CollectionProperty(name="Columns", type=Attribute) active_column_index: IntProperty(name="Active Column Index") @@ -358,13 +393,6 @@ class BIMWorkScheduleProperties(PropertyGroup): is_nested_task_outputs: BoolProperty( name="Is Nested Task Outputs", default=False, update=update_active_task_outputs ) - active_nested_task_output_index: IntProperty(name="Active Nested Tasks Output Index") - - -class BIMDuration(PropertyGroup): - duration_days: IntProperty(name="Days ") - duration_hours: IntProperty(name="Hours") - duration_minutes: IntProperty(name="Minutes") class BIMTaskTreeProperties(PropertyGroup): @@ -423,3 +451,42 @@ class BIMDateTextProperties(PropertyGroup): total_frames: IntProperty(name="Total Frames") start: StringProperty(name="Start") finish: StringProperty(name="Finish") + + +class BIMTaskTypeColor(PropertyGroup): + name: StringProperty(name="Name") + animation_type: StringProperty(name="Type") + color: FloatVectorProperty( + name="Color", + subtype="COLOR", + default=(1, 0, 0), + min=0.0, + max=1.0, + # update=update_task_animation_color, + ) + + +class BIMAnimationProperties(PropertyGroup): + is_editing: BoolProperty(name="Is Loaded", default=False) + active_color_component_outputs_index: IntProperty(name="Active Color Component Index") + active_color_component_inputs_index: IntProperty(name="Active Color Component Index") + task_colors_components_inputs: CollectionProperty(name="Groups", type=BIMTaskTypeColor) + task_colors_components_outputs: CollectionProperty(name="Groups", type=BIMTaskTypeColor) + color_full: FloatVectorProperty( + name="Full Bar", + subtype="COLOR", + default=(1.0, 0.0, 0.0), + min=0.0, + max=1.0, + description="color picker", + update=update_color_full, + ) + color_progress: FloatVectorProperty( + name="Progress Bar", + subtype="COLOR", + default=(0.0, 1.0, 0.0), + min=0.0, + max=1.0, + description="color picker", + update=update_color_progress, + ) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 9836f4d660..cd68492df0 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -123,6 +123,7 @@ class BIM_PT_work_schedules(Panel): SequenceData.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"]: @@ -185,6 +186,7 @@ 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 @@ -220,6 +222,8 @@ class BIM_PT_work_schedules(Panel): self.layout.template_list("BIM_UL_task_columns", "", self.props, "columns", self.props, "active_column_index") def draw_visualisation_ui(self): + row = self.layout.row(align=True) + row.label(text="Start Date/ Date Range:") row = self.layout.row(align=True) op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") op.target_prop = "BIMWorkScheduleProperties.visualisation_start" @@ -227,11 +231,9 @@ class BIM_PT_work_schedules(Panel): op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") op.work_schedule = self.props.active_work_schedule_id - op = row.operator("bim.visualise_work_schedule_date", text="", icon="RESTRICT_RENDER_OFF") - op.work_schedule = self.props.active_work_schedule_id - op = row.operator("bim.visualise_work_schedule_date_range", text="", icon="OUTLINER_OB_CAMERA") - op.work_schedule = self.props.active_work_schedule_id + row = self.layout.row(align=True) + row.label(text="Animation Options") row = self.layout.row(align=True) row.prop(self.props, "speed_types", text="") if self.props.speed_types == "FRAME_SPEED": @@ -242,6 +244,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="") + if not self.animation_props.is_editing: + op = row.operator( + "bim.enable_editing_task_animation_colors", text="Customize Animation Colors", icon="SEQUENCE_COLOR_04" + ) + else: + op = row.operator( + "bim.disable_editing_task_animation_colors", text="Hide Animation Colors", icon="SEQUENCE_COLOR_01" + ) + + if self.animation_props.is_editing: + self.draw_visualisation_settings_ui() + + row = self.layout.row() + 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 + op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF") + op.work_schedule = self.props.active_work_schedule_id + + def draw_visualisation_settings_ui(self): + grid = self.layout.grid_flow(columns=2, even_columns=True) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_inputs", + self.animation_props, + "active_color_component_inputs_index", + ) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_outputs", + self.animation_props, + "active_color_component_outputs_index", + ) def draw_editable_work_schedule_ui(self): draw_attributes(self.props.work_schedule_attributes, self.layout) @@ -375,6 +421,7 @@ class BIM_PT_task_icom(Panel): row2 = col.row(align=True) total_task_inputs = len(self.props.task_inputs) row2.label(text="Inputs ({})".format(total_task_inputs)) + if context.selected_objects: op = row2.operator("bim.assign_process", icon="ADD", text="") op.task = task.ifc_definition_id @@ -431,6 +478,7 @@ class BIM_PT_task_icom(Panel): row2 = col.row(align=True) total_task_outputs = len(self.props.task_outputs) row2.label(text="Outputs ({})".format(total_task_outputs)) + if context.selected_objects: op = row2.operator("bim.assign_product", icon="ADD", text="") op.task = task.ifc_definition_id @@ -450,6 +498,7 @@ class BIM_PT_task_icom(Panel): "BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index" ) + class BIM_UL_task_columns(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): props = context.scene.BIMWorkScheduleProperties @@ -477,6 +526,14 @@ class BIM_UL_task_resources(UIList): row.prop(item, "schedule_usage", emboss=False, text="") +class BIM_UL_animation_colors(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row() + row.prop(item, "color", text="") + row.label(text=item.name) + + class BIM_UL_task_outputs(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: @@ -508,7 +565,14 @@ class BIM_UL_tasks(UIList): text="", emboss=False, ) - + if self.props.should_show_bar_visual_option: + row.prop( + item, + "has_bar_visual", + icon="COLLECTION_COLOR_04" if item.has_bar_visual else "OUTLINER_COLLECTION", + text="", + emboss=False, + ) 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: @@ -733,9 +797,34 @@ class BIM_PT_work_calendars(Panel): draw_attributes(self.props.work_calendar_attributes, self.layout) -class BIM_PT_SequenceToolKit(Panel): - bl_label = "Sequence Toolkit" - bl_idname = "BIM_PT_Sequence_toolkit" +class BIM_PT_Task_Tools(Panel): + bl_label = "Task Bar Creator" + bl_idname = "BIM_PT_Task_Bar_Creator" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Sequence Toolkit" + + def draw(self, context): + self.animation_props = context.scene.BIMAnimationProperties + row = self.layout.row() + row.operator("bim.add_task_bars", text="Add Bar Visual", icon="NLA_PUSHDOWN") + + grid = self.layout.grid_flow(columns=2, even_columns=True) + # Column1 + col = grid.column() + row1 = col.row(align=True) + row1.label(text="Bar Colors", icon="NLA_PUSHDOWN") + + row2 = col.row(align=True) + row2.prop(self.animation_props, "color_progress") + + row3 = col.row(align=True) + row3.prop(self.animation_props, "color_full") + + +class BIM_PT_Task_Bar_Creator(Panel): + bl_label = "Task Tools" + bl_idname = "BIM_PT_Task_Tools" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Sequence Toolkit" @@ -743,7 +832,7 @@ class BIM_PT_SequenceToolKit(Panel): def draw(self, context): row = self.layout.row() row.operator( - "bim.highlight_product_related_task", text="Go to Input-related Task ", icon="STYLUS_PRESSURE" + "bim.highlight_product_related_task", text="Find Input-related Task ", icon="STYLUS_PRESSURE" ).product_type = "Input" row = self.layout.row() row.operator( diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index bb00ec9f7c..02a8e71385 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -478,3 +478,26 @@ def guess_date_range(sequence, work_schedule=None): def setup_default_task_columns(sequence): sequence.setup_default_task_columns() + + +def add_task_bars(sequence): + tasks = sequence.get_animation_bar_tasks() + if tasks: + sequence.create_bars(tasks) + + +def enable_editing_task_animation_colors(sequence): + sequence.load_task_animation_colors() + sequence.enable_editing_task_animation_colors() + + +def disable_editing_task_animation_colors(sequence): + sequence.disable_editing_task_animation_colors() + + +def visualise_work_schedule_date_range(sequence, work_schedule=None): + settings = sequence.get_animation_settings() + product_frames = sequence.get_animation_product_frames(work_schedule, settings) + sequence.load_task_animation_colors() + sequence.animate_objects(settings, product_frames) + sequence.add_text_animation_handler(settings) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 11ed7d5fd2..7151b979f5 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -27,9 +27,9 @@ import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.helper import blenderbim.bim.module.sequence.helper as helper - from dateutil import parser from datetime import datetime +import mathutils class Sequence(blenderbim.core.tool.Sequence): @@ -131,16 +131,18 @@ class Sequence(blenderbim.core.tool.Sequence): props = bpy.context.scene.BIMWorkScheduleProperties cls.contracted_tasks = json.loads(props.contracted_tasks) - related_objects_ids = helper.get_root_tasks_ids(work_schedule) - if not related_objects_ids: - return - cls.sort_keys = {i: cls.get_sort_key(tool.Ifc.get().by_id(i)) for i in related_objects_ids} - related_objects_ids = sorted(cls.sort_keys, key=cls.natural_sort_key) - if props.is_sort_reversed: - related_objects_ids.reverse() + related_objects_ids = cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_root_tasks(work_schedule)) for related_object_id in related_objects_ids: cls.create_new_task_li(related_object_id, 0) + @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) + if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed: + return related_object_ids.reverse() + return related_object_ids + @classmethod def create_new_task_li(cls, related_object_id, level_index): task = tool.Ifc.get().by_id(related_object_id) @@ -151,11 +153,7 @@ class Sequence(blenderbim.core.tool.Sequence): if task.IsNestedBy: new.has_children = True if new.is_expanded: - cls.sort_keys = {subtask.id(): cls.get_sort_key(subtask) for subtask in helper.get_nested_tasks(task)} - related_object_ids = sorted(cls.sort_keys, key=cls.natural_sort_key) - if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed: - related_object_ids.reverse() - for related_object_id in related_object_ids: + 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 @@ -226,8 +224,8 @@ class Sequence(blenderbim.core.tool.Sequence): else "-" ) else: - derived_start = helper.derive_date(task, "ScheduleStart", is_earliest=True) - derived_finish = helper.derive_date(task, "ScheduleFinish", is_latest=True) + derived_start = ifcopenshell.util.sequence.derive_date(task, "ScheduleStart", is_earliest=True) + derived_finish = ifcopenshell.util.sequence.derive_date(task, "ScheduleFinish", is_latest=True) item.derived_start = canonicalise_time(derived_start) if derived_start else "" item.derived_finish = canonicalise_time(derived_finish) if derived_finish else "" if derived_start and derived_finish and calendar: @@ -347,22 +345,25 @@ class Sequence(blenderbim.core.tool.Sequence): def load_task_time_attributes(cls, task_time): def callback(name, prop, data): if prop.data_type == "string": - if name == "ScheduleDuration" and data[name] and isinstance(data[name], str): + duration_props = bpy.context.scene.BIMWorkScheduleProperties.durations_attributes.add() + duration_props.name = name + if prop.is_null: + for key in duration_props.keys(): + if key != "name": + setattr(duration_props, key, 0) + return True + if name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"] and data[name]: time_object = ifcopenshell.util.date.ifc2datetime(data[name]) - minutes = int(round(time_object.seconds / 60 % 60)) - hours = (time_object.seconds - minutes * 60) / 60 / 60 - bpy.context.scene.BIMDuration.duration_days = int(time_object.days) - bpy.context.scene.BIMDuration.duration_hours = round(hours) - bpy.context.scene.BIMDuration.duration_minutes = ( - minutes # should consider years, months and seconds. - ) - return True - if isinstance(data[name], datetime): - prop.string_value = "" if prop.is_null else data[name].isoformat() + for key, value in helper.parse_duration_as_blender_props(time_object).items(): + duration_props[key] = value return True + if isinstance(data[name], datetime): + prop.string_value = "" if prop.is_null else data[name].isoformat() + return True props = bpy.context.scene.BIMWorkScheduleProperties props.task_time_attributes.clear() + props.durations_attributes.clear() blenderbim.bim.helper.import_attributes2(task_time, props.task_time_attributes, callback) @classmethod @@ -380,26 +381,29 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def get_task_time_attributes(cls): def callback(attributes, prop): + if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime": if prop.is_null: attributes[prop.name] = None return True attributes[prop.name] = helper.parse_datetime(prop.string_value) return True - elif prop.name == "ScheduleDuration": + elif prop.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]: + props = bpy.context.scene.BIMWorkScheduleProperties if prop.is_null: + attributes[prop.name] = None + for value in props.durations_attributes.values(): + value = 0 + return True + else: + duration_type = getattr(attributes, "DurationType", None) + time_split_iso_duration = helper.simplify_duration( + props.durations_attributes, duration_type, prop.name + ) + attributes[prop.name] = time_split_iso_duration + for value in props.durations_attributes.values(): + value = 0 return True - # TODO make this parse PT32 as P4D - attributes[prop.name] = helper.parse_duration(prop.string_value) - dprops = bpy.context.scene.BIMDuration - duration_days = dprops.duration_days if dprops.duration_days else 0 - duration_hours = dprops.duration_hours if dprops.duration_hours else 0 - duration_minutes = dprops.duration_minutes if dprops.duration_minutes else 0 - dprops.duration_days = 0 - dprops.duration_hours = 0 - dprops.duration_minutes = 0 - attributes[prop.name] = helper.parse_duration(f"P{duration_days}DT{duration_hours}H{duration_minutes}M") - return True props = bpy.context.scene.BIMWorkScheduleProperties return blenderbim.bim.helper.export_attributes(props.task_time_attributes, callback) @@ -455,16 +459,16 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def get_direct_nested_tasks(cls, task): - return helper.get_nested_tasks(task) + return ifcopenshell.util.sequence.get_nested_tasks(task) @classmethod def get_direct_task_outputs(cls, task): - return helper.get_direct_task_outputs(task) + return ifcopenshell.util.sequence.get_direct_task_outputs(task) @classmethod def get_task_outputs(cls, task): is_deep = bpy.context.scene.BIMWorkScheduleProperties.is_nested_task_outputs - return helper.get_task_outputs(task, is_deep) + return ifcopenshell.util.sequence.get_task_outputs(task, is_deep) @classmethod def get_task_resources(cls, task): @@ -786,7 +790,7 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def guess_date_range(cls, work_schedule): - return helper.guess_date_range(work_schedule) + return ifcopenshell.util.sequence.guess_date_range(work_schedule) @classmethod def update_visualisation_date(cls, start_date, finish_date): @@ -795,8 +799,474 @@ class Sequence(blenderbim.core.tool.Sequence): return "-" return time.strftime("%d/%m/%y") - print(start_date, finish_date) - props = bpy.context.scene.BIMWorkScheduleProperties props.visualisation_start = canonicalise_time(start_date) props.visualisation_finish = canonicalise_time(finish_date) + + @classmethod + def get_animation_bar_tasks(cls): + return [ + tool.Ifc.get().by_id(item.ifc_definition_id) + for item in bpy.context.scene.BIMTaskTreeProperties.tasks + if item.has_bar_visual + ] or [] + + @classmethod + def create_bars(cls, tasks): + def set_material(name, r, g, b): + material = bpy.data.materials.new(name) + material.use_nodes = True + material.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (r, g, b, 1.0) + return material + + def get_animation_materials(): + if "color_progress" in bpy.data.materials: + material_progress = bpy.data.materials["color_progress"] + else: + material_progress = set_material("color_progress", 0.0, 1.0, 0.0) + if "color_full" in bpy.data.materials: + material_full = bpy.data.materials["color_full"] + else: + material_full = set_material("color_full", 1.0, 0.0, 0.0) + return material_progress, material_full + + vertical_spacing = 0.5 + size_ratio = 1 / 20 + collection = bpy.data.collections.new("Bar Visual") + bpy.context.scene.collection.children.link(collection) + + lead_bar_thickness = 0.2 + size = 1.0 + + def create_task_bar_data(tasks, vertical_spacing): + props = bpy.context.scene.BIMWorkScheduleProperties + viz_start = ( + parser.parse(props.visualisation_start, dayfirst=True, fuzzy=True) + if props.visualisation_start + else None + ) + viz_finish = parser.parse(props.visualisation_finish, dayfirst=True, fuzzy=True) + + start_frame = bpy.context.scene.frame_start + end_frame = bpy.context.scene.frame_end + total_frames = end_frame - start_frame + task_data = [] + + material_progress, material_full = get_animation_materials() + for task in tasks: + task_start_date = ifcopenshell.util.sequence.derive_date(task, "ScheduleStart", is_earliest=True) + finish_date = ifcopenshell.util.sequence.derive_date(task, "ScheduleFinish", is_latest=True) + if task_start_date and finish_date: + duration = viz_finish - viz_start + task_start_frame = round(start_frame + (((task_start_date - viz_start) / duration) * total_frames)) + task_finish_frame = round(start_frame + (((finish_date - viz_start) / duration) * total_frames)) + data = { + "Name": task.Name if task.Name else "Unnamed", + "StartDate": task_start_date, + "FinishDate": finish_date, + "Start_frame": task_start_frame, + "Finish_frame": task_finish_frame, + "bar": None, + } + bar = add_bar( + size=1, material=material_progress, vertical_spacing=vertical_spacing, task=data, animate=True + ) + color_progress = bpy.context.scene.BIMAnimationProperties.color_progress + bar.color = (color_progress.r, color_progress.g, color_progress.b, 1.0) + + bar2 = add_bar( + size=1, material=material_full, vertical_spacing=vertical_spacing, task=data, animate=False + ) + color_full = bpy.context.scene.BIMAnimationProperties.color_full + bar2.color = (color_full.r, color_full.g, color_full.b, 1.0) + + bar_size = (data["Finish_frame"] - data["Start_frame"]) * size_ratio + bar2.scale = (lead_bar_thickness, bar_size, 1) + + shift_object(bar2, x=-((size + lead_bar_thickness) / 2)) + + start_text = add_text( + data["StartDate"].strftime("%d/%m/%y"), 0, "RIGHT", vertical_spacing, collection + ) + + add_text(data["Name"], 0, "LEFT", vertical_spacing, collection) + + finish_text = add_text( + data["FinishDate"].strftime("%d/%m/%y"), bar_size, "LEFT", vertical_spacing, collection + ) + + shift_object(start_text, y=-(size + lead_bar_thickness)) + + shift_object(finish_text, y=-(size + lead_bar_thickness)) + + vertical_spacing += 5 + collection.objects.link(bar) + collection.objects.link(bar2) + + return + + def animate_bar(bar, task): + scale = (1, size_ratio, 1) + bar.scale = scale + bar.keyframe_insert(data_path="scale", frame=task["Start_frame"]) + scale2 = (1, (task["Finish_frame"] - task["Start_frame"]) * size_ratio, 1) + bar.scale = scale2 + bar.keyframe_insert(data_path="scale", frame=task["Finish_frame"]) + + def place_bar(bar, vertical_spacing): + for vertex in bar.data.vertices: + vertex.co[1] += 0.5 + bpy.ops.transform.rotate(value=1.5708, orient_axis="Z") + bpy.ops.transform.translate(value=(0, -vertical_spacing, 0), orient_type="GLOBAL") + + def shift_object(obj, x=0.0, y=0.0, z=0.0): + vec = mathutils.Vector((x, y, z)) + inv = obj.matrix_world.copy() + inv.invert() + vec_rot = vec @ inv + obj.location = obj.location + vec_rot + + def add_text(text, x_position, align, vertical_spacing, collection=None): + bpy.ops.object.text_add() + bpy.context.object.data.align_x = align + bpy.context.object.data.align_y = "CENTER" + bpy.ops.transform.translate(value=(x_position, -(vertical_spacing - 1), 0), orient_type="GLOBAL") + bpy.context.object.data.body = text + if collection: + collection.objects.link(bpy.context.object) + return bpy.context.object + + def add_bar(size, material, vertical_spacing, task=None, animate=False): + bpy.ops.mesh.primitive_plane_add(size=size) + bpy.context.object.data.materials.append(material) + place_bar(bpy.context.object, vertical_spacing) + if task and animate: + animate_bar(bpy.context.object, task) + return bpy.context.object + + if tasks: + data = create_task_bar_data(tasks, vertical_spacing) + + @classmethod + def enable_editing_task_animation_colors(cls): + bpy.context.scene.BIMAnimationProperties.is_editing = True + + @classmethod + def load_task_animation_colors(cls): + props = bpy.context.scene.BIMAnimationProperties + if not props.task_colors_components_inputs: + if tool.Ifc.schema(): + # for attribute in tool.Ifc.schema().declaration_by_name("IfcTask").all_attributes(): + # if attribute.name() != "PredefinedType": + # continue + # task_types = ifcopenshell.util.attribute.get_enum_items(attribute) + # return [(e, e, "") for e in enum_items] + groups = { + "CREATION": { + "PredefinedType": ["CONSTRUCTION", "INSTALLATION"], + "Color": (0.0, 1.0, 0.0), + }, + "DESTRUCTION": { + "PredefinedType": ["DEMOLITION", "DISMANTLE", "DISPOSAL", "REMOVAL"], + "Color": (1.0, 0.0, 0.0), + }, + "MOVEMENT_FROM": { + "PredefinedType": ["LOGISTIC", "MOVE"], + "Color": (1.0, 0.5, 0.0), + }, + "MOVEMENT_TO": { + "PredefinedType": ["LOGISTIC", "MOVE"], + "Color": (1.0, 1.0, 0.0), + }, + "OPERATION": { + "PredefinedType": ["ATTENDANCE", "MAINTENANCE", "OPERATION", "RENOVATION"], + "Color": (0.0, 0.0, 1.0), + }, + "USERDEFINED": { + "PredefinedType": ["USERDEFINED", "NOTDEFINED"], + "Color": (0.2, 0.2, 0.2), + }, + } + for group, data in groups.items(): + for predefined_type in data["PredefinedType"]: + if group in ["CREATION", "OPERATION", "MOVEMENT_TO"]: + predefined_type_item = props.task_colors_components_outputs.add() + elif group in ["MOVEMENT_FROM", "DESTRUCTION"]: + predefined_type_item = props.task_colors_components_inputs.add() + else: + predefined_type_item = props.task_colors_components_outputs.add() + predefined_type_item.name = predefined_type + predefined_type_item.color = data["Color"] + + @classmethod + def disable_editing_task_animation_colors(cls): + bpy.context.scene.BIMAnimationProperties.is_editing = False + + @classmethod + def get_animation_settings(cls): + def calculate_total_frames(fps): + if props.speed_types == "FRAME_SPEED": + return calculate_using_frames( + start, + finish, + props.speed_animation_frames, + isodate.parse_duration(props.speed_real_duration), + ) + elif props.speed_types == "DURATION_SPEED": + return calculate_using_duration( + start, + finish, + fps, + isodate.parse_duration(props.speed_animation_duration), + isodate.parse_duration(props.speed_real_duration), + ) + elif props.speed_types == "MULTIPLIER_SPEED": + return calculate_using_multiplier( + start, + finish, + fps, + props.speed_multiplier, + ) + + def calculate_using_multiplier(start, finish, fps, multiplier): + animation_time = (finish - start) / multiplier + return animation_time.seconds_left() * fps + + def calculate_using_duration(start, finish, fps, animation_duration, real_duration): + return calculate_using_multiplier(start, finish, fps, real_duration / animation_duration) + + def calculate_using_frames(start, finish, animation_frames, real_duration): + return ((finish - start) / real_duration) * animation_frames + + props = bpy.context.scene.BIMWorkScheduleProperties + start = parser.parse(props.visualisation_start, dayfirst=True, fuzzy=True) + finish = parser.parse(props.visualisation_finish, dayfirst=True, fuzzy=True) + duration = finish - start + start_frame = 1 + total_frames = calculate_total_frames(bpy.context.scene.render.fps) + return { + "start": start, + "finish": finish, + "duration": duration, + "start_frame": start_frame, + "total_frames": total_frames, + } + + @classmethod + def get_animation_product_frames(cls, work_schedule, settings): + def preprocess_task(task): + for subtask in ifcopenshell.util.sequence.get_nested_tasks(task): + preprocess_task(subtask) + start = ifcopenshell.util.sequence.derive_date(task, "ScheduleStart", is_earliest=True) + finish = ifcopenshell.util.sequence.derive_date(task, "ScheduleFinish", is_latest=True) + if not start or not finish: + return + for output in ifcopenshell.util.sequence.get_task_outputs(task): + add_product_frame(output.id(), task.PredefinedType, start, finish, "output") + for input in cls.get_task_inputs(task): + add_product_frame(input.id(), task.PredefinedType, start, finish, "input") + + def add_product_frame(product_id, type, product_start, product_finish, relationship): + product_frames.setdefault(product_id, []).append( + { + "type": type, + "relationship": relationship, + "STARTED": round( + settings["start_frame"] + + (((product_start - settings["start"]) / settings["duration"]) * settings["total_frames"]) + ), + "COMPLETED": round( + settings["start_frame"] + + (((product_finish - settings["start"]) / settings["duration"]) * settings["total_frames"]) + ), + } + ) + + product_frames = {} + for root_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): + preprocess_task(root_task) + return product_frames + + @classmethod + def animate_objects(cls, settings, frames): + for obj in bpy.data.objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + if obj.animation_data: + obj.animation_data_clear() + cls.earliest_frame = None + product_frames = frames.get(obj.BIMObjectProperties.ifc_definition_id, []) + for product_frame in product_frames: + if product_frame["relationship"] == "input": + cls.animate_input(obj, settings["start_frame"], product_frame) + elif product_frame["relationship"] == "output": + cls.animate_output(obj, settings["start_frame"], product_frame) + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].shading.color_type = "OBJECT" + bpy.context.scene.frame_start = settings["start_frame"] + bpy.context.scene.frame_end = int(settings["start_frame"] + settings["total_frames"]) + + @classmethod + def animate_input(cls, obj, start_frame, product_frame): + props = bpy.context.scene.BIMAnimationProperties + color = props.task_colors_components_inputs[product_frame["type"]].color + if product_frame["type"] in ["LOGISTIC", "MOVE"]: + cls.animate_movement_from(obj, start_frame, product_frame, color) + elif product_frame["type"] in ["DEMOLITION", "DISMANTLE", "DISPOSAL", "REMOVAL"]: + cls.animate_destruction(obj, start_frame, product_frame, color) + else: + cls.animate_consumption(obj, start_frame, product_frame, color) + + @classmethod + def animate_output(cls, obj, start_frame, product_frame): + props = bpy.context.scene.BIMAnimationProperties + color = props.task_colors_components_outputs[product_frame["type"]].color + if product_frame["type"] in ["CONSTRUCTION", "INSTALLATION", "NOTDEFINED"]: + cls.animate_creation(obj, start_frame, product_frame, color) + elif product_frame["type"] in ["ATTENDANCE", "MAINTENANCE", "OPERATION", "RENOVATION"]: + cls.animate_operation(obj, start_frame, product_frame, color) + elif product_frame["type"] in ["LOGISTIC", "MOVE"]: + cls.animate_movement_to(obj, product_frame, color) + else: + cls.animate_operation(obj, start_frame, product_frame, color) + + @classmethod + def animate_destruction(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.color = (1.0, 1.0, 1.0, 1) + obj.hide_viewport = False + obj.hide_render = False + obj.keyframe_insert(data_path="color", frame=start_frame) + obj.keyframe_insert(data_path="hide_viewport", frame=start_frame) + obj.keyframe_insert(data_path="hide_render", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.hide_viewport = True + obj.hide_render = True + obj.color = (0.0, 0.0, 0.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) + + @classmethod + def animate_movement_from(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.hide_viewport = False + obj.hide_render = False + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.hide_viewport = True + obj.hide_render = True + obj.color = (0.0, 0.0, 0.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) + + @classmethod + def animate_consumption(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.hide_viewport = False + obj.hide_render = False + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.hide_viewport = True + obj.hide_render = True + obj.color = (0.0, 0.0, 0.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) + + @classmethod + def animate_creation(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.hide_viewport = True + obj.hide_render = True + obj.keyframe_insert(data_path="hide_viewport", frame=start_frame) + obj.keyframe_insert(data_path="hide_render", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.hide_viewport = False + obj.hide_render = False + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + + @classmethod + def animate_operation(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + + @classmethod + def animate_movement_to(cls, obj, start_frame, product_frame, color): + if cls.earliest_frame is None or product_frame["STARTED"] < cls.earliest_frame: + obj.hide_viewport = True + obj.hide_render = True + obj.keyframe_insert(data_path="hide_viewport", frame=start_frame) + obj.keyframe_insert(data_path="hide_render", frame=start_frame) + cls.earliest_frame = product_frame["STARTED"] + obj.hide_viewport = False + obj.hide_render = False + obj.color = (color.r, color.g, color.b, 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) + + @classmethod + def add_text_animation_handler(cls, settings): + def remove_handler(function): + bpy.app.handlers.frame_change_post.remove(function) + + def append_handler(function): + bpy.app.handlers.frame_change_post.append(function) + + def animate_text_handler(scene): + data = bpy.data.curves.get("Timeline") + if not data or not bpy.data.objects.get("Timeline"): + remove_handler(animate_text_handler) + data.body = get_frame_date(scene, data.BIMDateTextProperties) + + def get_frame_date(scene, props): + start = parser.parse(props.start, dayfirst=True, fuzzy=True) + finish = parser.parse(props.finish, dayfirst=True, fuzzy=True) + duration = finish - start + frame_date = (((scene.frame_current - props.start_frame) / props.total_frames) * duration) + start + return frame_date.date().isoformat() + + data = bpy.data.curves.get("Timeline") + if not data: + data = bpy.data.curves.new(type="FONT", name="Timeline") + obj = bpy.data.objects.get("Timeline") + if not obj: + obj = bpy.data.objects.new(name="Timeline", object_data=data) + bpy.context.scene.collection.objects.link(obj) + + obj.data.BIMDateTextProperties.start_frame = settings["start_frame"] + obj.data.BIMDateTextProperties.total_frames = int(settings["total_frames"]) + obj.data.BIMDateTextProperties.start = bpy.context.scene.BIMWorkScheduleProperties.visualisation_start + obj.data.BIMDateTextProperties.finish = bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish + append_handler(animate_text_handler) diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 96bc2e51f7..7b67ae468a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -21,6 +21,32 @@ import ifcopenshell.util.date from functools import lru_cache +def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False): + if task.TaskTime: + current_date = ( + ifcopenshell.util.date.ifc2datetime(getattr(task.TaskTime, attribute_name)) + if getattr(task.TaskTime, attribute_name) + else "" + ) + if current_date: + return current_date + for subtask in get_all_nested_tasks(task): + current_date = derive_date( + subtask, + attribute_name, + date=date, + is_earliest=is_earliest, + is_latest=is_latest, + ) + if is_earliest: + if current_date and (date is None or current_date < date): + date = current_date + if is_latest: + if current_date and (date is None or current_date > date): + date = current_date + return date + + def derive_calendar(task): calendar = [ rel.RelatingControl @@ -38,7 +64,7 @@ def count_working_days(start, finish, calendar): result = 0 current_date = datetime.date(start.year, start.month, start.day) finish_date = datetime.date(finish.year, finish.month, finish.day) - while current_date < finish_date: + while current_date <= finish_date: if ( calendar and calendar.WorkingTimes @@ -58,7 +84,11 @@ def get_start_or_finish_date( # Typically a milestone will have zero duration, so the start == finish return start # We minus 1 because the start day itself is counted as a day - duration = datetime.timedelta(days=duration.days - 1) + months = int(getattr(duration, "months", 0)) + years = int(getattr(duration, "years", 0)) + total_duration = duration.days + months * 30 + years * 12 * 30 + duration = datetime.timedelta(days=total_duration - 1) + if date_type == "START": duration = -duration result = offset_date(start, duration, duration_type, calendar) @@ -69,7 +99,10 @@ def get_start_or_finish_date( def offset_date(start, duration, duration_type, calendar): current_date = start - abs_duration = abs(duration.days) + months = getattr(duration, "months", 0) + years = getattr(duration, "years", 0) + + abs_duration = abs((duration.days + months * 30 + years * 12 * 30)) date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1) while abs_duration > 0: if duration_type == "ELAPSEDTIME" or not is_calendar_applicable( @@ -199,3 +232,107 @@ def is_work_time_applicable_to_day(work_time, day): and math.floor(day.day / 7) + 1 == recurrence.Position ) return False # TODO + + +def get_task_work_schedule(task): + parent_task = get_parent_task(task) + if parent_task: + get_task_work_schedule(parent_task) + else: + schedules = [ + rel.RelatingControl + for rel in task.HasAssignments + if rel.is_a("IfcRelAssignsToControl") + and rel.RelatingControl.is_a("IfcWorkSchedule") + ] + return schedules + + +def get_nested_tasks(task): + return [object for rel in task.IsNestedBy for object in rel.RelatedObjects] + + +def get_parent_task(task): + return ( + task.Nests[0].RelatingObject + if task.Nests and task.Nests[0].RelatingObject.is_a("IfcTask") + else None + ) + + +def get_all_nested_tasks(task): + for nested_task in get_nested_tasks(task): + yield nested_task + yield from get_all_nested_tasks(nested_task) + + +def get_work_schedule_tasks(work_schedule): + tasks = [] + for root_task in get_root_tasks(work_schedule): + nested_tasks = get_all_nested_tasks(root_task) + tasks.extend(nested_tasks) + return tasks + + +def get_root_tasks(work_schedule): + return [ + obj + for rel in work_schedule.Controls + for obj in rel.RelatedObjects + if obj.is_a("IfcTask") + ] + + +def get_root_tasks_ids(work_schedule): + return [ + obj.id() + for rel in work_schedule.Controls + for obj in rel.RelatedObjects + if obj.is_a("IfcTask") + ] + + +def guess_date_range(work_schedule): + earliest = None + latest = None + root_tasks = get_root_tasks(work_schedule) + tasks_with_assignements = [] + for task in root_tasks: + if has_task_outputs(task): + tasks_with_assignements.append(task) + for sub_task in get_all_nested_tasks(task): + if has_task_outputs(sub_task): + tasks_with_assignements.append(sub_task) + + for task in tasks_with_assignements: + derived_start = derive_date(task, "ScheduleStart", is_earliest=True) + derived_finish = derive_date(task, "ScheduleFinish", is_latest=True) + if derived_start and (not earliest or derived_start < earliest): + earliest = derived_start + if derived_finish and (not latest or derived_finish > latest): + latest = derived_finish + return earliest, latest + + +def get_direct_task_outputs(task): + return [ + rel.RelatingProduct + for rel in task.HasAssignments + if rel.is_a("IfcRelAssignsToProduct") + ] + + +def get_task_outputs(task, is_deep=False): + if not is_deep: + return get_direct_task_outputs(task) + else: + nested_tasks = get_all_nested_tasks(task) + return [ + output + for nested_task in nested_tasks + for output in get_direct_task_outputs(nested_task) + ] + + +def has_task_outputs(task): + return len(get_task_outputs(task)) > 0