From 4d9785e3ce7d8917d024b88633889b8e89bb4a11 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 19 Jul 2026 22:01:38 +0300 Subject: [PATCH] Bonsai: allow 4D animation at parent task granularity For large schedules the reporter of #2381 wants to visualise the construction sequence at a higher level, without the detailed order of subtasks. Add an Aggregate Collapsed Tasks option to the animation settings. When enabled, any task collapsed in the task tree animates as a single unit: products assigned to its nested tasks all use the collapsed task's derived date range. The task tree's expand and collapse state therefore selects the granularity per branch, and the default (everything expanded, option off) keeps existing behaviour. Each product keeps the PredefinedType of its own task, so demolition subtasks still animate as demolition within the aggregated window. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/sequence/prop.py | 8 ++++++++ src/bonsai/bonsai/bim/module/sequence/ui.py | 3 +++ src/bonsai/bonsai/tool/sequence.py | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index 97f274c421..1d6cc521bd 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -542,6 +542,12 @@ class BIMWorkScheduleProperties(PropertyGroup): should_auto_select: BoolProperty( name="Auto Select", description="Auto select elements when task is selected", default=False ) + should_aggregate_contracted_tasks: BoolProperty( + name="Aggregate Collapsed Tasks", + description="Animate tasks collapsed in the task tree as single units, " + "using the collapsed task's derived date range for all products of its nested tasks", + default=False, + ) if TYPE_CHECKING: work_schedule_predefined_types: str @@ -602,6 +608,8 @@ class BIMWorkScheduleProperties(PropertyGroup): show_task_operators: bool should_show_schedule_baseline_ui: bool filter_by_active_schedule: bool + should_auto_select: bool + should_aggregate_contracted_tasks: bool class BIMTaskTreeProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index e393c425c2..c184964790 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -585,6 +585,9 @@ class BIM_PT_animation_tools(Panel): row.label(text="Display Settings") row = self.layout.row() row.alignment = "RIGHT" + row.prop(self.props, "should_aggregate_contracted_tasks", icon="COLLAPSEMENU") + row = self.layout.row() + row.alignment = "RIGHT" row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bars", icon="NLA_PUSHDOWN") if self.animation_props.should_show_task_bar_options: row = self.layout.row() diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 3da5290311..6f83975e4b 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -1335,6 +1335,9 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def get_animation_product_frames(cls, work_schedule: ifcopenshell.entity_instance, settings: dict[str, Any]): def preprocess_task(task): + if task.id() in contracted_tasks and ifcopenshell.util.sequence.get_nested_tasks(task): + preprocess_aggregated_task(task) + return for subtask in ifcopenshell.util.sequence.get_nested_tasks(task): preprocess_task(subtask) start = ifcopenshell.util.sequence.derive_date(task, "ScheduleStart", is_earliest=True) @@ -1346,6 +1349,19 @@ class Sequence(bonsai.core.tool.Sequence): for input in cls.get_task_inputs(task): add_product_frame(input.id(), task.PredefinedType, start, finish, "input") + def preprocess_aggregated_task(task): + # A collapsed task animates as a single unit: all products of its + # nested tasks use the collapsed task's derived date range. + 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 subtask in [task, *ifcopenshell.util.sequence.get_all_nested_tasks(task)]: + for output in ifcopenshell.util.sequence.get_task_outputs(subtask): + add_product_frame(output.id(), subtask.PredefinedType, start, finish, "output") + for input in ifcopenshell.util.sequence.get_task_inputs(subtask): + add_product_frame(input.id(), subtask.PredefinedType, start, finish, "input") + def add_product_frame(product_id, type, product_start, product_finish, relationship): product_frames.setdefault(product_id, []).append( { @@ -1363,6 +1379,10 @@ class Sequence(bonsai.core.tool.Sequence): ) product_frames = {} + contracted_tasks: set[int] = set() + props = cls.get_work_schedule_props() + if props.should_aggregate_contracted_tasks: + contracted_tasks = set(json.loads(props.contracted_tasks)) for root_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): preprocess_task(root_task) return product_frames