From 19e67613db999aac31420a70418b4047146815e5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 09:27:31 +1000 Subject: [PATCH 1/6] New recipe to clean up IFCs that store quantities in properties. --- .../ifcopenshell/api/pset/add_pset.py | 4 +- .../ifcopenshell/api/pset/add_qto.py | 1 + .../recipes/ConvertPropertiesToQuantities.py | 59 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index 47e2fd31a4..73c9c3cf19 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -22,6 +22,7 @@ class Usecase: "RelatingPropertyDefinition": pset, } ) + return pset elif self.settings["product"].is_a("IfcTypeObject"): pset = self.file.create_entity( "IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]} @@ -29,8 +30,9 @@ class Usecase: has_property_sets = list(self.settings["product"].HasPropertySets or []) has_property_sets.append(pset) self.settings["product"].HasPropertySets = has_property_sets + return pset elif self.settings["product"].is_a("IfcMaterialDefinition"): - pset = self.file.create_entity( + return self.file.create_entity( "IfcMaterialProperties", **{ "Name": self.settings["Name"], diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py index d176e011c9..53b8246722 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py @@ -22,3 +22,4 @@ class Usecase: "RelatingPropertyDefinition": qto, } ) + return qto diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py b/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py new file mode 100644 index 0000000000..af228c2035 --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py @@ -0,0 +1,59 @@ +import ifcopenshell +import ifcopenshell.util.pset +import ifcopenshell.util.element + + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + self.qto_template_cache = {} + self.psetqto = ifcopenshell.util.pset.get_template("IFC4") + + self.source_property_name = self.args[0] + self.destination_quantity_name = self.args[1] + + for product in self.file.by_type("IfcTypeProduct"): + self.process_product(product, product.HasPropertySets or []) + + for product in self.file.by_type("IfcProduct"): + self.process_product( + product, + [r.RelatingPropertyDefinition for r in product.IsDefinedBy if r.is_a("IfcRelDefinesByProperties")], + ) + + def process_product(self, product, definitions): + value = None + has_quantity = False + qtos = {} + for definition in definitions or []: + if definition.is_a("IfcPropertySet"): + for prop in definition.HasProperties: + if prop.is_a("IfcPropertySingleValue") and prop.Name == self.source_property_name: + value = prop.NominalValue.wrappedValue if prop.NominalValue else None + elif definition.is_a("IfcElementQuantity"): + qtos[definition.Name] = definition + for quantity in definition.Quantities: + if quantity.is_a("IfcPhysicalSimpleQuantity") and quantity.Name == self.destination_quantity_name: + has_quantity = True + + if value and not has_quantity: + qto_name = self.get_qto_name(product.is_a()) + qto = qtos.get(qto_name, ifcopenshell.api.run("pset.add_qto", self.file, product=product, Name=qto_name)) + ifcopenshell.api.run( + "pset.edit_qto", self.file, qto=qto, Properties={self.destination_quantity_name: value} + ) + + def get_qto_name(self, ifc_class): + for template in self.get_qto_templates(ifc_class): + if self.destination_quantity_name in [t.Name for t in template.HasPropertyTemplates]: + return template.Name + + def get_qto_templates(self, ifc_class): + if ifc_class not in self.qto_template_cache: + self.qto_template_cache[ifc_class] = self.psetqto.get_applicable(ifc_class, qto_only=True) + return self.qto_template_cache[ifc_class] From 3224c4fc77b0a012edd88a6ecb78db6b9a8fabd4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 10:39:17 +1000 Subject: [PATCH 2/6] New IfcPatch recipe to convert units of an IFC file. See #1247. --- .../ifcpatch/recipes/ConvertLengthUnit.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py new file mode 100644 index 0000000000..328d6112d7 --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py @@ -0,0 +1,40 @@ +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.pset +import ifcopenshell.util.element + + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + unit = {"is_metric": "METERS" in self.args[0], "raw": self.args[0]} + self.file_patched = ifcopenshell.api.run("project.create_file", version=self.file.schema) + project = ifcopenshell.api.run("root.create_entity", self.file_patched, ifc_class="IfcProject") + unit_assignment = ifcopenshell.api.run("unit.assign_unit", self.file_patched, **{"length": unit}) + + # Is there a better way? + for element in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + element.Precision = 1E-8 + + # If we don't add openings first, they don't get converted + for element in self.file.by_type("IfcOpeningElement"): + self.file_patched.add(element) + + for element in self.file: + self.file_patched.add(element) + + new_length = [u for u in unit_assignment.Units if u.UnitType == "LENGTHUNIT"][0] + old_length = [ + u for u in self.file_patched.by_type("IfcProject")[1].UnitsInContext.Units if u.UnitType == "LENGTHUNIT" + ][0] + + for inverse in self.file_patched.get_inverse(old_length): + ifcopenshell.util.element.replace_attribute(inverse, old_length, new_length) + + self.file_patched.remove(old_length) + self.file_patched.remove(project) From 02bfbf01448b48d5c33c33268679ec835521d3c0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 11:34:50 +1000 Subject: [PATCH 3/6] You can now show calendars in the work schedule task tree --- src/blenderbim/blenderbim/bim/module/sequence/operator.py | 6 ++++++ src/blenderbim/blenderbim/bim/module/sequence/prop.py | 2 ++ src/blenderbim/blenderbim/bim/module/sequence/ui.py | 8 ++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 5244df45b2..73d649c05d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -309,6 +309,10 @@ class LoadTaskProperties(bpy.types.Operator): item.start = "-" item.finish = "-" item.duration = "-" + if task["HasAssignmentsWorkCalendar"]: + item.calendar = Data.work_calendars[task["HasAssignmentsWorkCalendar"][0]]["Name"] or "Unnamed" + else: + item.calendar = "" self.props.is_task_update_enabled = True return {"FINISHED"} @@ -1065,6 +1069,7 @@ class EditTaskCalendar(bpy.types.Operator): }, ) Data.load(IfcStore.get_file()) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} @@ -1085,6 +1090,7 @@ class RemoveTaskCalendar(bpy.types.Operator): }, ) Data.load(IfcStore.get_file()) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index ad51aff00d..4a5a750939 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -159,6 +159,7 @@ class Task(PropertyGroup): derived_start: StringProperty(name="Derived Start") derived_finish: StringProperty(name="Derived Finish") derived_duration: StringProperty(name="Derived Duration") + calendar: StringProperty(name="Calendar") is_predecessor: BoolProperty(name="Is Predecessor") is_successor: BoolProperty(name="Is Successor") @@ -189,6 +190,7 @@ class BIMWorkScheduleProperties(PropertyGroup): task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) should_show_times: BoolProperty(name="Should Show Times", default=False) + should_show_calendars: BoolProperty(name="Should Show Calendars", default=False) active_task_time_id: IntProperty(name="Active Task Id") task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index ea5aab4a13..0a60b91168 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -109,6 +109,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") elif self.props.editing_type == "TASKS": row.prop(self.props, "should_show_times", text="", icon="TIME") + row.prop(self.props, "should_show_calendars", text="", icon="VIEW_ORTHO") row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO") row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id @@ -336,6 +337,9 @@ class BIM_UL_tasks(UIList): else: row.prop(item, "duration", emboss=False, text="") + if props.should_show_calendars: + row.label(text=item.calendar) + if context.active_object: oprops = context.active_object.BIMObjectProperties row = layout.row(align=True) @@ -349,10 +353,6 @@ class BIM_UL_tasks(UIList): if props.active_task_id == item.ifc_definition_id: if props.editing_task_type == "TASKTIME": row.operator("bim.edit_task_time", text="", icon="CHECKMARK") - elif props.editing_task_type == "CALENDAR": - row.operator("bim.disable_editing_task", text="", icon="CHECKMARK") - elif props.editing_task_type == "SEQUENCE": - row.operator("bim.disable_editing_task", text="", icon="CHECKMARK") elif props.editing_task_type == "ATTRIBUTES": row.operator("bim.edit_task", text="", icon="CHECKMARK") row.operator("bim.disable_editing_task", text="", icon="CANCEL") From b1ed85addaffb6d9f93895734b560484eb525ca0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 18:31:05 +1000 Subject: [PATCH 4/6] Fix incorrect P6 duration import. Now things finally make sense. --- src/ifcp6/ifcp6/p62ifc.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ifcp6/ifcp6/p62ifc.py b/src/ifcp6/ifcp6/p62ifc.py index c128b2e1f6..a5b057f734 100644 --- a/src/ifcp6/ifcp6/p62ifc.py +++ b/src/ifcp6/ifcp6/p62ifc.py @@ -1,5 +1,4 @@ import datetime -from datetime import timedelta import ifcopenshell import ifcopenshell.api import ifcopenshell.util.date @@ -94,6 +93,7 @@ class P62Ifc: self.calendars[calendar_id] = { "Name": calendar.find("pr:Name", self.ns).text, "Type": calendar.find("pr:Type", self.ns).text, + "HoursPerDay": calendar.find("pr:HoursPerDay", self.ns).text, "StandardWorkWeek": standard_work_week, "HolidayOrExceptions": exceptions, } @@ -122,7 +122,7 @@ class P62Ifc: "Identification": activity.find("pr:Id", self.ns).text, "StartDate": datetime.datetime.fromisoformat(activity.find("pr:StartDate", self.ns).text), "FinishDate": datetime.datetime.fromisoformat(activity.find("pr:FinishDate", self.ns).text), - "PlannedDuration": datetime.timedelta(hours=float(activity.find("pr:PlannedDuration", self.ns).text)), + "PlannedDuration": activity.find("pr:PlannedDuration", self.ns).text, "Status": activity.find("pr:Status", self.ns).text, "CalendarObjectId": activity.find("pr:CalendarObjectId", self.ns).text, "ifc": None, @@ -238,10 +238,9 @@ class P62Ifc: "sequence.edit_recurrence_pattern", self.file, recurrence_pattern=recurrence, - attributes={"DayComponent": month_data["FullDay"], "MonthComponent": [month], "Occurrences": 1}, + attributes={"DayComponent": month_data["FullDay"], "MonthComponent": [month]}, ) - def process_work_time_exceptions(self, year, month, month_data, calendar): for day in month_data["WorkTime"]: if day["ifc"]: @@ -280,7 +279,7 @@ class P62Ifc: "sequence.edit_recurrence_pattern", self.file, recurrence_pattern=recurrence, - attributes={"DayComponent": day_component, "MonthComponent": [month], "Occurrences": 1}, + attributes={"DayComponent": day_component, "MonthComponent": [month]}, ) for work_time in day["WorkTimes"]: ifcopenshell.api.run( @@ -291,7 +290,6 @@ class P62Ifc: end_time=work_time["Finish"], ) - def create_tasks(self, work_schedule): for wbs in self.wbs.values(): self.create_task_from_wbs(wbs, work_schedule) @@ -336,6 +334,7 @@ class P62Ifc: }, ) task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=activity["ifc"]) + calendar = self.calendars[activity["CalendarObjectId"]] ifcopenshell.api.run( "sequence.edit_task_time", self.file, @@ -344,18 +343,23 @@ class P62Ifc: "ScheduleStart": activity["StartDate"], "ScheduleFinish": activity["FinishDate"], "DurationType": "WORKTIME" if activity["PlannedDuration"] else None, - "ScheduleDuration": activity["PlannedDuration"] if activity["PlannedDuration"] else None, + "ScheduleDuration": datetime.timedelta( + days=float(activity["PlannedDuration"]) / float(calendar["HoursPerDay"]) + ) + or None + if activity["PlannedDuration"] + else None, + }, + ) + # Seem crashy + ifcopenshell.api.run( + "control.assign_control", + self.file, + **{ + "relating_control": calendar["ifc"], + "related_object": activity["ifc"], }, ) - if activity["CalendarObjectId"]: - ifcopenshell.api.run( - "control.assign_control", - self.file, - **{ - "relating_control": self.calendars[activity["CalendarObjectId"]]["ifc"], - "related_object": activity["ifc"], - }, - ) def create_rel_sequences(self): self.sequence_type_map = { From 1b4b2a526dead97e8b68293f3f1b6eb18fe825fd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 18:34:25 +1000 Subject: [PATCH 5/6] Derived durations now calculate working days based on the calendar --- .../blenderbim/bim/module/sequence/helper.py | 77 +++++++++++++++++++ .../bim/module/sequence/operator.py | 6 +- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/helper.py b/src/blenderbim/blenderbim/bim/module/sequence/helper.py index 8046e09174..1fd685fd57 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/helper.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/helper.py @@ -1,8 +1,84 @@ +import math import isodate +import datetime from dateutil import parser from ifcopenshell.api.sequence.data import Data +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: + if is_working_day(current_date, calendar): + result += 1 + current_date += datetime.timedelta(days=1) + return result + + +def is_working_day(day, calendar): + is_working_day = False + for work_time_id in calendar["WorkingTimes"] or []: + if is_work_time_applicable_to_day(Data.work_times[work_time_id], day): + is_working_day = True + break + if not is_working_day: + return is_working_day + for work_time_id in calendar["ExceptionTimes"] or []: + if is_work_time_applicable_to_day(Data.work_times[work_time_id], day): + is_working_day = False + break + return is_working_day + + +def is_work_time_applicable_to_day(work_time, day): + if work_time["Start"] and work_time["Start"] > day: + return False + + if work_time["Finish"] and work_time["Finish"] < day: + return False + + if not work_time["RecurrencePattern"]: + return True + + recurrence = Data.recurrence_patterns[work_time["RecurrencePattern"]] + + if recurrence["RecurrenceType"] == "DAILY": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return True + if not work_time["Start"]: + return False + return False # TODO + elif recurrence["RecurrenceType"] == "WEEKLY": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return (day.weekday() + 1) in recurrence["WeekdayComponent"] + if not work_time["Start"]: + return False + return False # TODO + elif recurrence["RecurrenceType"] == "MONTHLY_BY_DAY_OF_MONTH": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return day.day in recurrence["DayComponent"] + return False # TODO + elif recurrence["RecurrenceType"] == "MONTHLY_BY_POSITION": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return (day.weekday() + 1) in recurrence["WeekdayComponent"] and math.floor(day.day / 7) + 1 == recurrence[ + "Position" + ] + return False # TODO + elif recurrence["RecurrenceType"] == "YEARLY_BY_DAY_OF_MONTH": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return day.month in recurrence["MonthComponent"] and day.day in recurrence["DayComponent"] + return False # TODO + elif recurrence["RecurrenceType"] == "YEARLY_BY_POSITION": + if not recurrence["Interval"] and not recurrence["Occurrences"]: + return ( + day.month in recurrence["MonthComponent"] + and (day.weekday() + 1) in recurrence["WeekdayComponent"] + and math.floor(day.day / 7) + 1 == recurrence["Position"] + ) + return False # TODO + + def derive_date(ifc_definition_id, attribute_name, date=None, is_earliest=False, is_latest=False): task = Data.tasks[ifc_definition_id] if task["TaskTime"]: @@ -31,6 +107,7 @@ def parse_datetime(value): except: return None + def parse_duration(value): try: return isodate.parse_duration(value) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 73d649c05d..4d4c5314fb 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -303,9 +303,9 @@ class LoadTaskProperties(bpy.types.Operator): derived_finish = helper.derive_date(item.ifc_definition_id, "ScheduleFinish", is_latest=True) item.derived_start = self.canonicalise_time(derived_start) if derived_start else "" item.derived_finish = self.canonicalise_time(derived_finish) if derived_finish else "" - if derived_start and derived_finish: - derived_duration = ifcopenshell.util.date.timedelta2duration(derived_finish - derived_start) - item.derived_duration = isodate.duration_isoformat(derived_duration) + if derived_start and derived_finish and calendar: + derived_duration = helper.count_working_days(derived_start, derived_finish, calendar) + item.derived_duration = f"P{derived_duration}D" item.start = "-" item.finish = "-" item.duration = "-" From d98ac8ce8938c72c7a1590803a4f1187fb649336 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 May 2021 18:40:51 +1000 Subject: [PATCH 6/6] Implement inherited calendars in the sequencing task tree --- .../blenderbim/bim/module/sequence/helper.py | 29 +++++++++++++++++-- .../bim/module/sequence/operator.py | 23 +++++++++++---- .../blenderbim/bim/module/sequence/prop.py | 3 +- .../blenderbim/bim/module/sequence/ui.py | 5 +++- .../ifcopenshell/api/sequence/data.py | 1 + 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/helper.py b/src/blenderbim/blenderbim/bim/module/sequence/helper.py index 1fd685fd57..0bb980d09a 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/helper.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/helper.py @@ -86,9 +86,32 @@ def derive_date(ifc_definition_id, attribute_name, date=None, is_earliest=False, if current_date: return current_date for subtask in task["RelatedObjects"]: - current_date = derive_date( - subtask, attribute_name, date=date, is_earliest=is_earliest, is_latest=is_latest - ) + 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(ifc_definition_id): + task = Data.tasks[ifc_definition_id] + if task["HasAssignmentsWorkCalendar"]: + return Data.work_calendars[task["HasAssignmentsWorkCalendar"][0]] + if task["Nests"]: + return derive_calendar(task["Nests"][0]) + + +def derive_duration(ifc_definition_id, attribute_name): + task = Data.tasks[ifc_definition_id] + if task["TaskTime"]: + current_date = Data.task_times[task["TaskTime"]][attribute_name] + if current_date: + return current_date + for subtask in task["RelatedObjects"]: + 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 diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 4d4c5314fb..d8b550d5e8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -291,6 +291,14 @@ class LoadTaskProperties(bpy.types.Operator): item.is_successor = self.props.active_task_id in [ Data.sequences[r]["RelatingProcess"] for r in task["IsSuccessorFrom"] ] + + calendar = helper.derive_calendar(item.ifc_definition_id) + if task["HasAssignmentsWorkCalendar"]: + item.calendar = calendar["Name"] or "Unnamed" + else: + item.calendar = "" + item.derived_calendar = calendar["Name"] or "Unnamed" if calendar else "" + if task["TaskTime"]: task_time = Data.task_times[task["TaskTime"]] item.start = self.canonicalise_time(task_time["ScheduleStart"]) @@ -309,10 +317,7 @@ class LoadTaskProperties(bpy.types.Operator): item.start = "-" item.finish = "-" item.duration = "-" - if task["HasAssignmentsWorkCalendar"]: - item.calendar = Data.work_calendars[task["HasAssignmentsWorkCalendar"][0]]["Name"] or "Unnamed" - else: - item.calendar = "" + self.props.is_task_update_enabled = True return {"FINISHED"} @@ -1069,7 +1074,10 @@ class EditTaskCalendar(bpy.types.Operator): }, ) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_task_properties(task=self.task) + if Data.tasks[self.task]["RelatedObjects"]: + bpy.ops.bim.load_task_properties() + else: + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} @@ -1090,7 +1098,10 @@ class RemoveTaskCalendar(bpy.types.Operator): }, ) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_task_properties(task=self.task) + if Data.tasks[self.task]["RelatedObjects"]: + bpy.ops.bim.load_task_properties() + else: + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 4a5a750939..5671b4b212 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -156,10 +156,11 @@ class Task(PropertyGroup): duration: StringProperty(name="Duration") start: StringProperty(name="Start", update=updateTaskTimeStart) finish: StringProperty(name="Finish", update=updateTaskTimeFinish) + calendar: StringProperty(name="Calendar") derived_start: StringProperty(name="Derived Start") derived_finish: StringProperty(name="Derived Finish") derived_duration: StringProperty(name="Derived Duration") - calendar: StringProperty(name="Calendar") + derived_calendar: StringProperty(name="Derived Calendar") is_predecessor: BoolProperty(name="Is Predecessor") is_successor: BoolProperty(name="Is Successor") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 0a60b91168..6cf9a948de 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -338,7 +338,10 @@ class BIM_UL_tasks(UIList): row.prop(item, "duration", emboss=False, text="") if props.should_show_calendars: - row.label(text=item.calendar) + if item.derived_calendar: + row.label(text=item.derived_calendar + "*") + else: + row.label(text=item.calendar or "-") if context.active_object: oprops = context.active_object.BIMObjectProperties diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index a59b519e94..2a3c32c2fb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -133,6 +133,7 @@ class Data: data["TaskTime"] = data["TaskTime"].id() for rel in task.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] + data["Nests"] = [r.RelatingObject.id() for r in task.Nests or []] [ data["RelatingProducts"].append(r.RelatingProduct.id()) for r in task.HasAssignments