From b76b05fc7194275bbddc63df4e307e3dd8f65d62 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Jul 2025 19:25:36 +0500 Subject: [PATCH] sequence.copy_work_schedule #6901 Similar to e50215ddc but for IfcWorkSchedules Button location - https://files.catbox.moe/jfd1ol.png --- .../bonsai/bim/module/sequence/__init__.py | 1 + .../bonsai/bim/module/sequence/operator.py | 14 +++++ src/bonsai/bonsai/bim/module/sequence/ui.py | 1 + src/bonsai/bonsai/core/sequence.py | 4 ++ src/bonsai/bonsai/tool/sequence.py | 7 +++ .../api/cost/copy_cost_schedule.py | 1 + .../ifcopenshell/api/sequence/__init__.py | 2 + .../api/sequence/copy_work_schedule.py | 51 +++++++++++++++++++ .../api/sequence/duplicate_task.py | 9 ++-- .../test/api/cost/test_copy_cost_schedule.py | 1 + .../api/sequence/test_copy_work_schedule.py | 47 +++++++++++++++++ 11 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/copy_work_schedule.py create mode 100644 src/ifcopenshell-python/test/api/sequence/test_copy_work_schedule.py diff --git a/src/bonsai/bonsai/bim/module/sequence/__init__.py b/src/bonsai/bonsai/bim/module/sequence/__init__.py index 13e23e1f2f..15337c4227 100644 --- a/src/bonsai/bonsai/bim/module/sequence/__init__.py +++ b/src/bonsai/bonsai/bim/module/sequence/__init__.py @@ -46,6 +46,7 @@ classes = ( operator.ContractTask, operator.CopyTask, operator.CopyTaskAttribute, + operator.CopyWorkSchedule, operator.CreateBaseline, operator.DisableEditingSequence, operator.DisableEditingTask, diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 05eec37138..d85c955180 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -277,6 +277,20 @@ class RemoveWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): core.remove_work_schedule(tool.Ifc, work_schedule=tool.Ifc.get().by_id(self.work_schedule)) +class CopyWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.copy_work_schedule" + bl_label = "Copy Work Schedule" + bl_description = "Create a duplicate of the provided work schedule." + bl_options = {"REGISTER", "UNDO"} + work_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + work_schedule: int + + def _execute(self, context): + core.copy_work_schedule(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule)) + + class EnableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.enable_editing_work_schedule" bl_label = "Enable Editing Work Schedule" diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index 2f2c5597aa..876ccc5ea2 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -246,6 +246,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL").work_schedule = ( work_schedule_id ) + row.operator("bim.copy_work_schedule", text="", icon="DUPLICATE").work_schedule = work_schedule_id row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: if self.props.editing_type == "WORK_SCHEDULE": diff --git a/src/bonsai/bonsai/core/sequence.py b/src/bonsai/bonsai/core/sequence.py index 9581f2595e..63cf984078 100644 --- a/src/bonsai/bonsai/core/sequence.py +++ b/src/bonsai/bonsai/core/sequence.py @@ -71,6 +71,10 @@ def remove_work_schedule(ifc: type[tool.Ifc], work_schedule: ifcopenshell.entity ifc.run("sequence.remove_work_schedule", work_schedule=work_schedule) +def copy_work_schedule(sequence: type[tool.Sequence], work_schedule: ifcopenshell.entity_instance) -> None: + sequence.copy_work_schedule(work_schedule) + + def assign_work_schedule( ifc: type[tool.Ifc], work_plan: ifcopenshell.entity_instance, work_schedule: ifcopenshell.entity_instance ) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index dc4a843fcd..9e9551836e 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -22,6 +22,7 @@ import re import bpy import json import base64 +import ifcopenshell.api.sequence import pystache import mathutils import webbrowser @@ -1846,3 +1847,9 @@ class Sequence(bonsai.core.tool.Sequence): if not element or not element.is_a("IfcProduct"): continue obj.hide_set(element not in visible_elements) + + @classmethod + def copy_work_schedule(cls, work_schedule: ifcopenshell.entity_instance) -> None: + ifc_file = tool.Ifc.get() + new_schedule = ifcopenshell.api.sequence.copy_work_schedule(ifc_file, work_schedule) + new_schedule.Name = (new_schedule.Name or "Unnamed") + " Copy" diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py index bd22d3833a..b23c1d5694 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py @@ -36,6 +36,7 @@ def copy_cost_schedule( schedule = ifcopenshell.api.cost.add_cost_schedule(model) new_schedule = ifcopenshell.api.cost.copy_cost_schedule(schedule) """ + # Shared code logic with copy_work_schedule. new_schedule = ifcopenshell.util.element.copy(file, cost_schedule) for rel in cost_schedule.Controls: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/__init__.py index 585d4cfa3c..b0bf2344b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/__init__.py @@ -38,6 +38,7 @@ from .assign_sequence import assign_sequence from .assign_work_plan import assign_work_plan from .calculate_task_duration import calculate_task_duration from .cascade_schedule import cascade_schedule +from .copy_work_schedule import copy_work_schedule from .create_baseline import create_baseline from .duplicate_task import duplicate_task from .edit_lag_time import edit_lag_time @@ -84,6 +85,7 @@ __all__ = [ "assign_work_plan", "calculate_task_duration", "cascade_schedule", + "copy_work_schedule", "create_baseline", "duplicate_task", "edit_lag_time", diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/copy_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/copy_work_schedule.py new file mode 100644 index 0000000000..30b1123ac7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/copy_work_schedule.py @@ -0,0 +1,51 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell.api.control +import ifcopenshell.api.sequence +import ifcopenshell.util.element + + +def copy_work_schedule( + file: ifcopenshell.file, + work_schedule: ifcopenshell.entity_instance, +) -> ifcopenshell.entity_instance: + """Copy a work schedule. + + :param work_schedule: IfcWorkSchedule to copy. + :return: The duplicated IfcWorkSchedule entity. + + Example: + + .. code:: python + + work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction") + schedule = ifcopenshell.api.sequence.add_work_schedule(model, + name="Construction Schedule A", work_plan=work_plan) + new_schedule = ifcopenshell.api.sequence.copy_work_schedule(model, schedule) + """ + # Shared code logic with copy_cost_schedule. + new_schedule = ifcopenshell.util.element.copy(file, work_schedule) + + for rel in work_schedule.Controls: + for task in rel.RelatedObjects: + duplicated_tasks = ifcopenshell.api.sequence.duplicate_task(file, task)[1] + # All other nested items are not connected to the work schedule explicitly. + duplicated_task = duplicated_tasks[0] + ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_task) + return new_schedule diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index 5c9c1a4c65..236ecd89c9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -27,6 +27,7 @@ import ifcopenshell.util.sequence from typing import Union, Any +# TODO: inconsistent name with other copy_xxx api methods. def duplicate_task( file: ifcopenshell.file, task: ifcopenshell.entity_instance ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: @@ -58,7 +59,7 @@ def duplicate_task( class Usecase: file: ifcopenshell.file - settings: dict[str, Any] + settings: dict[str, list[ifcopenshell.entity_instance]] def execute(self): self.tracker = {"current": [], "duplicate": []} @@ -66,14 +67,16 @@ class Usecase: self.copy_sequence_relationship(self.tracker["current"], self.tracker["duplicate"]) return self.tracker["current"], self.tracker["duplicate"] - def duplicate_task(self, task): + def duplicate_task(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: new_task = ifcopenshell.util.element.copy_deep(self.file, task) self.tracker["current"].append(task) self.tracker["duplicate"].append(new_task) self.copy_indirect_attributes(task, new_task) return new_task - def copy_indirect_attributes(self, from_element, to_element): + def copy_indirect_attributes( + self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance + ) -> None: for inverse in self.file.get_inverse(from_element): if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) diff --git a/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py b/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py index 489d0e03b5..119c24387e 100644 --- a/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py +++ b/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py @@ -23,6 +23,7 @@ import test.bootstrap class TestCopyCostSchedule(test.bootstrap.IFC4): def test_run(self): + # Shared code logic with test_copy_work_schedule. schedule = ifcopenshell.api.cost.add_cost_schedule(self.file, name="Foo") item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule) ifcopenshell.api.cost.add_cost_item(self.file, cost_item=item) # Subitem. diff --git a/src/ifcopenshell-python/test/api/sequence/test_copy_work_schedule.py b/src/ifcopenshell-python/test/api/sequence/test_copy_work_schedule.py new file mode 100644 index 0000000000..7bb686ceb6 --- /dev/null +++ b/src/ifcopenshell-python/test/api/sequence/test_copy_work_schedule.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell.api.sequence +import ifcopenshell.util.sequence +import test.bootstrap + + +class TestCopyWorkSchedule(test.bootstrap.IFC4): + def test_run(self): + self.file.create_entity("IfcProject") + # Shared code logic with test_copy_cost_schedule. + work_plan = ifcopenshell.api.sequence.add_work_plan(self.file) + schedule = ifcopenshell.api.sequence.add_work_schedule(self.file, work_plan=work_plan) + task = ifcopenshell.api.sequence.add_task(self.file, work_schedule=schedule) + ifcopenshell.api.sequence.add_task(self.file, parent_task=task) # Subtask. + old_cost_items = set(self.file.by_type("IfcTask")) + + new_schedule = ifcopenshell.api.sequence.copy_work_schedule(self.file, schedule) + + # We don't check how well IfcTasks are copied, + # it should be tested separately in test_duplicate_task. + assert isinstance(new_schedule, ifcopenshell.entity_instance) + assert new_schedule != schedule + assert len(ifcopenshell.util.sequence.get_root_tasks(new_schedule)) == 1 + new_tasks = set(ifcopenshell.util.sequence.get_work_schedule_tasks(new_schedule)) + assert len(new_tasks) == 2 + assert len(new_tasks.intersection(old_cost_items)) == 0 + + +class TestCopyWorkScheduleIFC4X3(test.bootstrap.IFC4X3, TestCopyWorkSchedule): + pass