mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
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:
@@ -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="")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user