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
This commit is contained in:
Sigma Dimensions
2023-01-04 02:08:52 +01:00
committed by Dion Moult
parent 83e483555f
commit af1949678a
9 changed files with 972 additions and 412 deletions
+13 -4
View File
@@ -43,16 +43,25 @@ def draw_attribute(attribute, layout, copy_operator=None):
return return
if value_name == "enum_value": if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name) 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: else:
layout.prop( layout.prop(
attribute, attribute,
value_name, value_name,
text=attribute.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: if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
@@ -26,6 +26,7 @@ classes = (
operator.AddSummaryTask, operator.AddSummaryTask,
operator.AddTask, operator.AddTask,
operator.AddTaskColumn, operator.AddTaskColumn,
operator.AddTaskBars,
operator.AddTimePeriod, operator.AddTimePeriod,
operator.AddWorkCalendar, operator.AddWorkCalendar,
operator.AddWorkPlan, operator.AddWorkPlan,
@@ -108,19 +109,23 @@ classes = (
operator.UnassignWorkSchedule, operator.UnassignWorkSchedule,
operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDate,
operator.VisualiseWorkScheduleDateRange, operator.VisualiseWorkScheduleDateRange,
operator.LoadTaskAnimationColors,
operator.DisableEditingTaskAnimationColors,
prop.WorkPlan, prop.WorkPlan,
prop.BIMWorkPlanProperties, prop.BIMWorkPlanProperties,
prop.Task, prop.Task,
prop.TaskResource, prop.TaskResource,
prop.TaskProduct, prop.TaskProduct,
prop.BIMDuration,
prop.BIMWorkScheduleProperties, prop.BIMWorkScheduleProperties,
prop.BIMTaskTreeProperties, prop.BIMTaskTreeProperties,
prop.BIMTaskTypeColor,
prop.BIMAnimationProperties,
prop.WorkCalendar, prop.WorkCalendar,
prop.RecurrenceComponent, prop.RecurrenceComponent,
prop.BIMWorkCalendarProperties, prop.BIMWorkCalendarProperties,
prop.DatePickerProperties, prop.DatePickerProperties,
prop.BIMDateTextProperties, prop.BIMDateTextProperties,
prop.BIMDuration,
ui.BIM_PT_work_plans, ui.BIM_PT_work_plans,
ui.BIM_PT_work_schedules, ui.BIM_PT_work_schedules,
ui.BIM_PT_work_calendars, ui.BIM_PT_work_calendars,
@@ -130,7 +135,9 @@ classes = (
ui.BIM_UL_task_resources, ui.BIM_UL_task_resources,
ui.BIM_UL_task_outputs, ui.BIM_UL_task_outputs,
ui.BIM_UL_tasks, 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.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties)
bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties) bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties)
bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) 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.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.TextCurve.BIMDateTextProperties = bpy.props.PointerProperty(type=prop.BIMDateTextProperties)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) 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.BIMTaskTreeProperties
del bpy.types.Scene.BIMWorkCalendarProperties del bpy.types.Scene.BIMWorkCalendarProperties
del bpy.types.Scene.DatePickerProperties del bpy.types.Scene.DatePickerProperties
del bpy.types.Scene.BIMDuration del bpy.types.Scene.BIMAnimationProperties
del bpy.types.TextCurve.BIMDateTextProperties del bpy.types.TextCurve.BIMDateTextProperties
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -19,26 +19,7 @@
import isodate import isodate
from dateutil import parser from dateutil import parser
import ifcopenshell.util.date as ifcdateutils import ifcopenshell.util.date as ifcdateutils
from datetime import timedelta
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
def parse_datetime(value): def parse_datetime(value):
@@ -64,88 +45,77 @@ def canonicalise_time(time):
return time.strftime("%d/%m/%y") return time.strftime("%d/%m/%y")
def get_nested_tasks(task): def parse_duration_as_blender_props(dt):
return [ seconds = dt.seconds
related_object hours, seconds = divmod(seconds, 3600)
for rel in task.IsNestedBy minutes, seconds = divmod(seconds, 60)
for related_object in rel.RelatedObjects days = dt.days
if related_object.is_a("IfcTask") 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): def simplify_duration(durations_attributes, duration_type, prop_name):
return task.Nests[0].RelatingObject if task.Nests and task.Nests[0].RelatingObject.is_a("IfcTask") else None 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): # TODO: implement actual calendar worktime
parent_task = get_parent_task(task) calendar_seconds_per_day = 8 * 60 * 60
if parent_task: extra_days, seconds_left = divmod(total_seconds, calendar_seconds_per_day)
get_task_work_schedule(parent_task) total_days = days_subtotal + extra_days
else: duration_object = timedelta(days=total_days, seconds=seconds_left)
schedules = [ if duration_object:
rel.RelatingControl total_days = int(duration_object.days)
for rel in task.HasAssignments seconds_left = int(duration_object.seconds)
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule") years, days = divmod(total_days, 365)
] if hasattr(duration_object, "years"):
print(f"Returning {schedules}") years += duration_object.years
return schedules
months, days = divmod(days, 30)
if hasattr(duration_object, "months"):
months += duration_object.months
def get_all_nested_tasks(task): if months >= 12:
for nested_task in get_nested_tasks(task): extra_years, months = divmod(months, 12)
yield nested_task years += extra_years
yield from get_all_nested_tasks(nested_task)
hours, seconds = divmod(seconds_left, 3600)
minutes, seconds = divmod(seconds, 60)
def get_work_schedule_tasks(work_schedule): return "P{}Y{}M{}DT{}H{}M{}S".format(
tasks = [] int(years),
for root_task in get_root_tasks(work_schedule): int(months),
nested_tasks = get_all_nested_tasks(root_task) int(days),
tasks.extend(nested_tasks) int(hours),
return tasks int(minutes),
int(seconds),
)
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
@@ -35,19 +35,6 @@ from ifcopenshell.api.sequence.data import Data
from ifcopenshell.api.resource.data import Data as ResourceData 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): class AddWorkPlan(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_work_plan" bl_idname = "bim.add_work_plan"
bl_label = "Add Work Plan" bl_label = "Add Work Plan"
@@ -1104,239 +1091,9 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator):
work_schedule: bpy.props.IntProperty() work_schedule: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() core.visualise_work_schedule_date_range(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
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)
return {"FINISHED"} 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): class BlenderBIM_DatePicker(bpy.types.Operator):
bl_label = "Date Picker" bl_label = "Date Picker"
@@ -1567,3 +1324,34 @@ class ContractAllTasks(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
core.contract_all_tasks(tool.Sequence) 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"}
@@ -249,6 +249,28 @@ def update_visualisation_start_finish(self, context, startfinish):
setattr(self, startfinish, canonical_value) 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): class Task(PropertyGroup):
name: StringProperty(name="Name", update=updateTaskName) name: StringProperty(name="Name", update=updateTaskName)
identification: StringProperty(name="Identification", update=updateTaskIdentification) identification: StringProperty(name="Identification", update=updateTaskIdentification)
@@ -256,6 +278,7 @@ class Task(PropertyGroup):
has_children: BoolProperty(name="Has Children") has_children: BoolProperty(name="Has Children")
is_selected: BoolProperty(name="Is Selected") is_selected: BoolProperty(name="Is Selected")
is_expanded: BoolProperty(name="Is Expanded") is_expanded: BoolProperty(name="Is Expanded")
has_bar_visual: BoolProperty(name="Bar Visualization", default=False)
level_index: IntProperty(name="Level Index") level_index: IntProperty(name="Level Index")
duration: StringProperty(name="Duration", update=updateTaskDuration) duration: StringProperty(name="Duration", update=updateTaskDuration)
start: StringProperty(name="Start", update=updateTaskTimeStart) start: StringProperty(name="Start", update=updateTaskTimeStart)
@@ -294,7 +317,18 @@ class BIMWorkPlanProperties(PropertyGroup):
work_schedules: EnumProperty(items=getWorkSchedules, name="Work Schedules") 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): class BIMWorkScheduleProperties(PropertyGroup):
durations_attributes: CollectionProperty(name="Durations Attributes", type=BIMDuration)
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars") work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
editing_type: StringProperty(name="Editing Type") editing_type: StringProperty(name="Editing Type")
@@ -305,6 +339,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_id: IntProperty(name="Active Task Id") active_task_id: IntProperty(name="Active Task Id")
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) 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) should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
columns: CollectionProperty(name="Columns", type=Attribute) columns: CollectionProperty(name="Columns", type=Attribute)
active_column_index: IntProperty(name="Active Column Index") active_column_index: IntProperty(name="Active Column Index")
@@ -358,13 +393,6 @@ class BIMWorkScheduleProperties(PropertyGroup):
is_nested_task_outputs: BoolProperty( is_nested_task_outputs: BoolProperty(
name="Is Nested Task Outputs", default=False, update=update_active_task_outputs 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): class BIMTaskTreeProperties(PropertyGroup):
@@ -423,3 +451,42 @@ class BIMDateTextProperties(PropertyGroup):
total_frames: IntProperty(name="Total Frames") total_frames: IntProperty(name="Total Frames")
start: StringProperty(name="Start") start: StringProperty(name="Start")
finish: StringProperty(name="Finish") 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,
)
@@ -123,6 +123,7 @@ class BIM_PT_work_schedules(Panel):
SequenceData.load() SequenceData.load()
self.props = context.scene.BIMWorkScheduleProperties self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties self.tprops = context.scene.BIMTaskTreeProperties
self.animation_props = context.scene.BIMAnimationProperties
row = self.layout.row() row = self.layout.row()
if SequenceData.data["has_work_schedules"]: 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.edit_task", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_task", text="", icon="CANCEL") row.operator("bim.disable_editing_task", text="", icon="CANCEL")
else: 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_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_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_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") self.layout.template_list("BIM_UL_task_columns", "", self.props, "columns", self.props, "active_column_index")
def draw_visualisation_ui(self): def draw_visualisation_ui(self):
row = self.layout.row(align=True)
row.label(text="Start Date/ Date Range:")
row = self.layout.row(align=True) row = self.layout.row(align=True)
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start" op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
@@ -227,11 +231,9 @@ class BIM_PT_work_schedules(Panel):
op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" op.target_prop = "BIMWorkScheduleProperties.visualisation_finish"
op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH")
op.work_schedule = self.props.active_work_schedule_id 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 = self.layout.row(align=True)
row.prop(self.props, "speed_types", text="") row.prop(self.props, "speed_types", text="")
if self.props.speed_types == "FRAME_SPEED": 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="") row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "MULTIPLIER_SPEED": elif self.props.speed_types == "MULTIPLIER_SPEED":
row.prop(self.props, "speed_multiplier", text="") 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): def draw_editable_work_schedule_ui(self):
draw_attributes(self.props.work_schedule_attributes, self.layout) draw_attributes(self.props.work_schedule_attributes, self.layout)
@@ -375,6 +421,7 @@ class BIM_PT_task_icom(Panel):
row2 = col.row(align=True) row2 = col.row(align=True)
total_task_inputs = len(self.props.task_inputs) total_task_inputs = len(self.props.task_inputs)
row2.label(text="Inputs ({})".format(total_task_inputs)) row2.label(text="Inputs ({})".format(total_task_inputs))
if context.selected_objects: if context.selected_objects:
op = row2.operator("bim.assign_process", icon="ADD", text="") op = row2.operator("bim.assign_process", icon="ADD", text="")
op.task = task.ifc_definition_id op.task = task.ifc_definition_id
@@ -431,6 +478,7 @@ class BIM_PT_task_icom(Panel):
row2 = col.row(align=True) row2 = col.row(align=True)
total_task_outputs = len(self.props.task_outputs) total_task_outputs = len(self.props.task_outputs)
row2.label(text="Outputs ({})".format(total_task_outputs)) row2.label(text="Outputs ({})".format(total_task_outputs))
if context.selected_objects: if context.selected_objects:
op = row2.operator("bim.assign_product", icon="ADD", text="") op = row2.operator("bim.assign_product", icon="ADD", text="")
op.task = task.ifc_definition_id 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" "BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
) )
class BIM_UL_task_columns(UIList): class BIM_UL_task_columns(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMWorkScheduleProperties props = context.scene.BIMWorkScheduleProperties
@@ -477,6 +526,14 @@ class BIM_UL_task_resources(UIList):
row.prop(item, "schedule_usage", emboss=False, text="") 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): class BIM_UL_task_outputs(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item: if item:
@@ -508,7 +565,14 @@ class BIM_UL_tasks(UIList):
text="", text="",
emboss=False, 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.active_task_id:
if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id: if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id:
if item.is_predecessor: if item.is_predecessor:
@@ -733,9 +797,34 @@ class BIM_PT_work_calendars(Panel):
draw_attributes(self.props.work_calendar_attributes, self.layout) draw_attributes(self.props.work_calendar_attributes, self.layout)
class BIM_PT_SequenceToolKit(Panel): class BIM_PT_Task_Tools(Panel):
bl_label = "Sequence Toolkit" bl_label = "Task Bar Creator"
bl_idname = "BIM_PT_Sequence_toolkit" 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_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Sequence Toolkit" bl_category = "Sequence Toolkit"
@@ -743,7 +832,7 @@ class BIM_PT_SequenceToolKit(Panel):
def draw(self, context): def draw(self, context):
row = self.layout.row() row = self.layout.row()
row.operator( 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" ).product_type = "Input"
row = self.layout.row() row = self.layout.row()
row.operator( row.operator(
@@ -478,3 +478,26 @@ def guess_date_range(sequence, work_schedule=None):
def setup_default_task_columns(sequence): def setup_default_task_columns(sequence):
sequence.setup_default_task_columns() 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)
+514 -44
View File
@@ -27,9 +27,9 @@ import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim.helper import blenderbim.bim.helper
import blenderbim.bim.module.sequence.helper as helper import blenderbim.bim.module.sequence.helper as helper
from dateutil import parser from dateutil import parser
from datetime import datetime from datetime import datetime
import mathutils
class Sequence(blenderbim.core.tool.Sequence): class Sequence(blenderbim.core.tool.Sequence):
@@ -131,16 +131,18 @@ class Sequence(blenderbim.core.tool.Sequence):
props = bpy.context.scene.BIMWorkScheduleProperties props = bpy.context.scene.BIMWorkScheduleProperties
cls.contracted_tasks = json.loads(props.contracted_tasks) cls.contracted_tasks = json.loads(props.contracted_tasks)
related_objects_ids = helper.get_root_tasks_ids(work_schedule) related_objects_ids = cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_root_tasks(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()
for related_object_id in related_objects_ids: for related_object_id in related_objects_ids:
cls.create_new_task_li(related_object_id, 0) 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 @classmethod
def create_new_task_li(cls, related_object_id, level_index): def create_new_task_li(cls, related_object_id, level_index):
task = tool.Ifc.get().by_id(related_object_id) task = tool.Ifc.get().by_id(related_object_id)
@@ -151,11 +153,7 @@ class Sequence(blenderbim.core.tool.Sequence):
if task.IsNestedBy: if task.IsNestedBy:
new.has_children = True new.has_children = True
if new.is_expanded: if new.is_expanded:
cls.sort_keys = {subtask.id(): cls.get_sort_key(subtask) for subtask in helper.get_nested_tasks(task)} for related_object_id in cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.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:
cls.create_new_task_li(related_object_id, level_index + 1) cls.create_new_task_li(related_object_id, level_index + 1)
@classmethod @classmethod
@@ -226,8 +224,8 @@ class Sequence(blenderbim.core.tool.Sequence):
else "-" else "-"
) )
else: else:
derived_start = helper.derive_date(task, "ScheduleStart", is_earliest=True) derived_start = ifcopenshell.util.sequence.derive_date(task, "ScheduleStart", is_earliest=True)
derived_finish = helper.derive_date(task, "ScheduleFinish", is_latest=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_start = canonicalise_time(derived_start) if derived_start else ""
item.derived_finish = canonicalise_time(derived_finish) if derived_finish else "" item.derived_finish = canonicalise_time(derived_finish) if derived_finish else ""
if derived_start and derived_finish and calendar: 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 load_task_time_attributes(cls, task_time):
def callback(name, prop, data): def callback(name, prop, data):
if prop.data_type == "string": 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]) time_object = ifcopenshell.util.date.ifc2datetime(data[name])
minutes = int(round(time_object.seconds / 60 % 60)) for key, value in helper.parse_duration_as_blender_props(time_object).items():
hours = (time_object.seconds - minutes * 60) / 60 / 60 duration_props[key] = value
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()
return True 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 = bpy.context.scene.BIMWorkScheduleProperties
props.task_time_attributes.clear() props.task_time_attributes.clear()
props.durations_attributes.clear()
blenderbim.bim.helper.import_attributes2(task_time, props.task_time_attributes, callback) blenderbim.bim.helper.import_attributes2(task_time, props.task_time_attributes, callback)
@classmethod @classmethod
@@ -380,26 +381,29 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod @classmethod
def get_task_time_attributes(cls): def get_task_time_attributes(cls):
def callback(attributes, prop): def callback(attributes, prop):
if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime": if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime":
if prop.is_null: if prop.is_null:
attributes[prop.name] = None attributes[prop.name] = None
return True return True
attributes[prop.name] = helper.parse_datetime(prop.string_value) attributes[prop.name] = helper.parse_datetime(prop.string_value)
return True return True
elif prop.name == "ScheduleDuration": elif prop.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]:
props = bpy.context.scene.BIMWorkScheduleProperties
if prop.is_null: 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 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 props = bpy.context.scene.BIMWorkScheduleProperties
return blenderbim.bim.helper.export_attributes(props.task_time_attributes, callback) return blenderbim.bim.helper.export_attributes(props.task_time_attributes, callback)
@@ -455,16 +459,16 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod @classmethod
def get_direct_nested_tasks(cls, task): def get_direct_nested_tasks(cls, task):
return helper.get_nested_tasks(task) return ifcopenshell.util.sequence.get_nested_tasks(task)
@classmethod @classmethod
def get_direct_task_outputs(cls, task): def get_direct_task_outputs(cls, task):
return helper.get_direct_task_outputs(task) return ifcopenshell.util.sequence.get_direct_task_outputs(task)
@classmethod @classmethod
def get_task_outputs(cls, task): def get_task_outputs(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.is_nested_task_outputs 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 @classmethod
def get_task_resources(cls, task): def get_task_resources(cls, task):
@@ -786,7 +790,7 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod @classmethod
def guess_date_range(cls, work_schedule): def guess_date_range(cls, work_schedule):
return helper.guess_date_range(work_schedule) return ifcopenshell.util.sequence.guess_date_range(work_schedule)
@classmethod @classmethod
def update_visualisation_date(cls, start_date, finish_date): def update_visualisation_date(cls, start_date, finish_date):
@@ -795,8 +799,474 @@ class Sequence(blenderbim.core.tool.Sequence):
return "-" return "-"
return time.strftime("%d/%m/%y") return time.strftime("%d/%m/%y")
print(start_date, finish_date)
props = bpy.context.scene.BIMWorkScheduleProperties props = bpy.context.scene.BIMWorkScheduleProperties
props.visualisation_start = canonicalise_time(start_date) props.visualisation_start = canonicalise_time(start_date)
props.visualisation_finish = canonicalise_time(finish_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)
@@ -21,6 +21,32 @@ import ifcopenshell.util.date
from functools import lru_cache 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): def derive_calendar(task):
calendar = [ calendar = [
rel.RelatingControl rel.RelatingControl
@@ -38,7 +64,7 @@ def count_working_days(start, finish, calendar):
result = 0 result = 0
current_date = datetime.date(start.year, start.month, start.day) current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day) finish_date = datetime.date(finish.year, finish.month, finish.day)
while current_date < finish_date: while current_date <= finish_date:
if ( if (
calendar calendar
and calendar.WorkingTimes and calendar.WorkingTimes
@@ -58,7 +84,11 @@ def get_start_or_finish_date(
# Typically a milestone will have zero duration, so the start == finish # Typically a milestone will have zero duration, so the start == finish
return start return start
# We minus 1 because the start day itself is counted as a day # 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": if date_type == "START":
duration = -duration duration = -duration
result = offset_date(start, duration, duration_type, calendar) 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): def offset_date(start, duration, duration_type, calendar):
current_date = start 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) date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1)
while abs_duration > 0: while abs_duration > 0:
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable( 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 and math.floor(day.day / 7) + 1 == recurrence.Position
) )
return False # TODO 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