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
parent 0c77fb81ce
commit 981b6cd65f
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
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)
+514 -44
View File
@@ -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