From e8cb9e7f3c79e806cb0ceaf4987262e415988195 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 17 Jan 2025 14:35:47 +0500 Subject: [PATCH] Date picker to support IfcDateTime See https://imgur.com/a/9a7JfMO --- src/bonsai/bonsai/bim/helper.py | 5 +- .../bonsai/bim/module/sequence/helper.py | 1 + .../bonsai/bim/module/sequence/operator.py | 74 ++++++++++++++----- src/bonsai/bonsai/bim/module/sequence/prop.py | 16 +++- src/bonsai/bonsai/tool/sequence.py | 50 +++++++++---- 5 files changed, 112 insertions(+), 34 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index b29e79f060..76ec3e70f5 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -98,9 +98,10 @@ def draw_attribute( if attribute.is_uri: op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER") op.data_path = attribute.path_from_id("string_value") - elif attribute.special_type == "DATE": + elif attribute.special_type in ("DATE", "DATETIME"): op = layout.operator("bim.datepicker", text="", icon="TIME") op.target_prop = attribute.path_from_id("string_value") + op.include_time = attribute.special_type == "DATETIME" if attribute.is_optional: layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") @@ -170,6 +171,8 @@ def import_attribute( new.is_uri = True elif attribute.type_of_attribute()._is("IfcDate"): new.special_type = "DATE" + elif attribute.type_of_attribute()._is("IfcDateTime"): + new.special_type = "DATETIME" elif data_type == "boolean": new.bool_value = False if new.is_null else bool(data[attribute.name()]) elif data_type == "integer": diff --git a/src/bonsai/bonsai/bim/module/sequence/helper.py b/src/bonsai/bonsai/bim/module/sequence/helper.py index eaf991dff8..983aeffee2 100644 --- a/src/bonsai/bonsai/bim/module/sequence/helper.py +++ b/src/bonsai/bonsai/bim/module/sequence/helper.py @@ -38,6 +38,7 @@ def parse_duration(value): def canonicalise_time(time: Union[datetime, None]) -> str: + """Actualy canonicalises datetime as just a date, time is not included.""" if not time: return "-" return time.strftime("%d/%m/%y") diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index a6729a444d..119fff263b 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -1178,27 +1178,45 @@ class Bonsai_DatePicker(bpy.types.Operator): bl_label = "Date Picker" bl_idname = "bim.datepicker" bl_options = {"REGISTER", "UNDO"} - display_date: bpy.props.StringProperty(name="Display Date") target_prop: bpy.props.StringProperty(name="Target date prop to set") + # TODO: base it on property type. + include_time: bpy.props.BoolProperty(name="Include Time", default=True) + + if TYPE_CHECKING: + target_prop: str + include_time: bool def execute(self, context): + selected_date = context.scene.DatePickerProperties.selected_date try: - value = parser.parse(context.scene.DatePickerProperties.selected_date, dayfirst=True, fuzzy=True) - self.set_scene_prop(self.target_prop, helper.canonicalise_time(value)) - except: - pass - return {"FINISHED"} + # Just to make sure the date is valid. + tool.Sequence.parse_isodate_datetime(selected_date, self.include_time) + self.set_scene_prop(self.target_prop, selected_date) + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, f"Provided date is invalid: '{selected_date}'. Exception: {str(e)}.") + return {"CANCELLED"} def draw(self, context): - current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True) - current_month = (current_date.year, current_date.month) + props = context.scene.DatePickerProperties + display_date = tool.Sequence.parse_isodate_datetime(props.display_date, False) + current_month = (display_date.year, display_date.month) lines = calendar.monthcalendar(*current_month) month_title, week_titles = calendar.month(*current_month).splitlines()[:2] layout = self.layout row = layout.row() - row.prop(context.scene.DatePickerProperties, "selected_date") + row.prop(props, "selected_date", text="Date") + # Time. + if self.include_time: + row = layout.row() + row.label(text="Time:") + row.prop(props, "selected_hour", text="H") + row.prop(props, "selected_min", text="M") + row.prop(props, "selected_sec", text="S") + + # Month. split = layout.split() col = split.row() op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="") @@ -1210,12 +1228,14 @@ class Bonsai_DatePicker(bpy.types.Operator): op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="") op.action = "next" + # Day of week. row = layout.row(align=True) for title in week_titles.split(): col = row.column(align=True) col.alignment = "CENTER" col.label(text=title.strip()) + # Days calendar. for line in lines: row = layout.row(align=True) for i in line: @@ -1223,15 +1243,31 @@ class Bonsai_DatePicker(bpy.types.Operator): if i == 0: col.label(text=" ") else: + selected_date = datetime(year=display_date.year, month=display_date.month, day=i) op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i)) - selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year) - selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True) - op.selected_date = helper.canonicalise_time(selected_date) + if self.include_time: + selected_date = selected_date.replace( + hour=props.selected_hour, minute=props.selected_min, second=props.selected_sec + ) + op.selected_date = tool.Sequence.isodate_datetime(selected_date, self.include_time) def invoke(self, context, event): - self.display_date = self.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now()) - context.scene.DatePickerProperties.display_date = self.display_date - context.scene.DatePickerProperties.selected_date = self.display_date + props = context.scene.DatePickerProperties + current_date_str = self.get_scene_prop(self.target_prop) + if current_date_str: + current_date = tool.Sequence.parse_isodate_datetime(current_date_str, self.include_time) + else: + current_date = datetime.now() + # Seconds of the moment when datepicker opened will probably only annoy users. + current_date = current_date.replace(second=0) + + if self.include_time: + props["selected_hour"] = current_date.hour + props["selected_min"] = current_date.minute + props["selected_sec"] = current_date.second + + props.display_date = tool.Sequence.isodate_datetime(current_date.replace(day=1), False) + props.selected_date = tool.Sequence.isodate_datetime(current_date, self.include_time) return context.window_manager.invoke_props_dialog(self) def get_scene_prop(self, prop_path: str) -> str: @@ -1261,15 +1297,15 @@ class Bonsai_RedrawDatePicker(bpy.types.Operator): action: bpy.props.StringProperty() def invoke(self, context, event): - current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True) + props = context.scene.DatePickerProperties + current_date = tool.Sequence.parse_isodate_datetime(props.display_date, False) if self.action == "previous": date_to_set = current_date - relativedelta.relativedelta(months=1) - elif self.action == "next": + else: # "next". date_to_set = current_date + relativedelta.relativedelta(months=1) - context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set) - + props.display_date = tool.Sequence.isodate_datetime(date_to_set, False) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index d99c651e4c..bdb1219218 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -520,9 +520,23 @@ class BIMWorkCalendarProperties(PropertyGroup): end_time: StringProperty(name="End Time") +def update_selected_date(self: "DatePickerProperties", context: bpy.types.Context) -> None: + # `include_time` is `True`, otherwise time props are not displayed in UI. + include_time = True + selected_date = tool.Sequence.parse_isodate_datetime(self.selected_date, include_time) + selected_date = selected_date.replace(hour=self.selected_hour, minute=self.selected_min, second=self.selected_sec) + self.selected_date = tool.Sequence.isodate_datetime(selected_date, include_time) + + class DatePickerProperties(PropertyGroup): - display_date: StringProperty(name="Display Date") + display_date: StringProperty( + name="Display Date", + description="Needed to keep track of what month is currently opened in date picker without affecting the currently selected date.", + ) selected_date: StringProperty(name="Selected Date") + selected_hour: IntProperty(min=0, max=23, update=update_selected_date) + selected_min: IntProperty(min=0, max=59, update=update_selected_date) + selected_sec: IntProperty(min=0, max=59, update=update_selected_date) class BIMDateTextProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 66fda638f1..0ffed5fc4c 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import re import bpy @@ -24,7 +25,9 @@ import base64 import pystache import mathutils import webbrowser +import isodate import ifcopenshell +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.sequence import ifcopenshell.util.date import ifcopenshell.util.element @@ -34,7 +37,11 @@ import bonsai.tool as tool import bonsai.bim.helper from dateutil import parser from datetime import datetime -from typing import Optional, Any, Union, Literal +from datetime import time as datetime_time +from typing import Optional, Any, Union, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + import bonsai.bim.prop class Sequence(bonsai.core.tool.Sequence): @@ -344,19 +351,23 @@ class Sequence(bonsai.core.tool.Sequence): def load_task_time_attributes(cls, task_time: ifcopenshell.entity_instance) -> None: import bonsai.bim.module.sequence.helper as helper - def callback(name, prop, data): + def callback( + name: str, prop: Union[bonsai.bim.prop.Attribute, None], data: dict[str, Any] + ) -> Union[bool, None]: if prop and prop.data_type == "string": - 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]: - for key, value in helper.parse_duration_as_blender_props(data[name]).items(): - duration_props[key] = value - return True + # TODO: Check actual attribute type instead of providing attribute names. + if name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"): + 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 data[name]: + for key, value in helper.parse_duration_as_blender_props(data[name]).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 @@ -1713,3 +1724,16 @@ class Sequence(bonsai.core.tool.Sequence): def get_animation_color_scheme(cls): if len(bpy.context.scene.BIMAnimationProperties.saved_color_schemes) > 0: return tool.Ifc.get().by_id(int(bpy.context.scene.BIMAnimationProperties.saved_color_schemes)) + + @classmethod + def parse_isodate_datetime(cls, datetime_str: str, include_time: bool) -> datetime: + if include_time: + return isodate.parse_datetime(datetime_str) + date = isodate.parse_date(datetime_str) + return datetime.combine(date, datetime_time()) + + @classmethod + def isodate_datetime(cls, datetime_: datetime, include_time: bool) -> str: + if include_time: + return isodate.datetime_isoformat(datetime_) + return isodate.date_isoformat(datetime_)