Date picker to support IfcDateTime

See https://imgur.com/a/9a7JfMO
This commit is contained in:
Andrej730
2025-01-17 14:35:47 +05:00
parent fa98a7e8f7
commit e8cb9e7f3c
5 changed files with 112 additions and 34 deletions
+4 -1
View File
@@ -98,9 +98,10 @@ def draw_attribute(
if attribute.is_uri: if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER") op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value") 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 = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value") op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if attribute.is_optional: if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") 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 new.is_uri = True
elif attribute.type_of_attribute()._is("IfcDate"): elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE" new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
new.special_type = "DATETIME"
elif data_type == "boolean": elif data_type == "boolean":
new.bool_value = False if new.is_null else bool(data[attribute.name()]) new.bool_value = False if new.is_null else bool(data[attribute.name()])
elif data_type == "integer": elif data_type == "integer":
@@ -38,6 +38,7 @@ def parse_duration(value):
def canonicalise_time(time: Union[datetime, None]) -> str: def canonicalise_time(time: Union[datetime, None]) -> str:
"""Actualy canonicalises datetime as just a date, time is not included."""
if not time: if not time:
return "-" return "-"
return time.strftime("%d/%m/%y") return time.strftime("%d/%m/%y")
@@ -1178,27 +1178,45 @@ class Bonsai_DatePicker(bpy.types.Operator):
bl_label = "Date Picker" bl_label = "Date Picker"
bl_idname = "bim.datepicker" bl_idname = "bim.datepicker"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
display_date: bpy.props.StringProperty(name="Display Date")
target_prop: bpy.props.StringProperty(name="Target date prop to set") 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): def execute(self, context):
selected_date = context.scene.DatePickerProperties.selected_date
try: try:
value = parser.parse(context.scene.DatePickerProperties.selected_date, dayfirst=True, fuzzy=True) # Just to make sure the date is valid.
self.set_scene_prop(self.target_prop, helper.canonicalise_time(value)) tool.Sequence.parse_isodate_datetime(selected_date, self.include_time)
except: self.set_scene_prop(self.target_prop, selected_date)
pass return {"FINISHED"}
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): def draw(self, context):
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True) props = context.scene.DatePickerProperties
current_month = (current_date.year, current_date.month) display_date = tool.Sequence.parse_isodate_datetime(props.display_date, False)
current_month = (display_date.year, display_date.month)
lines = calendar.monthcalendar(*current_month) lines = calendar.monthcalendar(*current_month)
month_title, week_titles = calendar.month(*current_month).splitlines()[:2] month_title, week_titles = calendar.month(*current_month).splitlines()[:2]
layout = self.layout layout = self.layout
row = layout.row() 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() split = layout.split()
col = split.row() col = split.row()
op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="") 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 = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
op.action = "next" op.action = "next"
# Day of week.
row = layout.row(align=True) row = layout.row(align=True)
for title in week_titles.split(): for title in week_titles.split():
col = row.column(align=True) col = row.column(align=True)
col.alignment = "CENTER" col.alignment = "CENTER"
col.label(text=title.strip()) col.label(text=title.strip())
# Days calendar.
for line in lines: for line in lines:
row = layout.row(align=True) row = layout.row(align=True)
for i in line: for i in line:
@@ -1223,15 +1243,31 @@ class Bonsai_DatePicker(bpy.types.Operator):
if i == 0: if i == 0:
col.label(text=" ") col.label(text=" ")
else: else:
selected_date = datetime(year=display_date.year, month=display_date.month, day=i)
op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i)) op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year) if self.include_time:
selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True) selected_date = selected_date.replace(
op.selected_date = helper.canonicalise_time(selected_date) 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): def invoke(self, context, event):
self.display_date = self.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now()) props = context.scene.DatePickerProperties
context.scene.DatePickerProperties.display_date = self.display_date current_date_str = self.get_scene_prop(self.target_prop)
context.scene.DatePickerProperties.selected_date = self.display_date 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) return context.window_manager.invoke_props_dialog(self)
def get_scene_prop(self, prop_path: str) -> str: def get_scene_prop(self, prop_path: str) -> str:
@@ -1261,15 +1297,15 @@ class Bonsai_RedrawDatePicker(bpy.types.Operator):
action: bpy.props.StringProperty() action: bpy.props.StringProperty()
def invoke(self, context, event): 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": if self.action == "previous":
date_to_set = current_date - relativedelta.relativedelta(months=1) date_to_set = current_date - relativedelta.relativedelta(months=1)
elif self.action == "next": else: # "next".
date_to_set = current_date + relativedelta.relativedelta(months=1) 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"} return {"FINISHED"}
+15 -1
View File
@@ -520,9 +520,23 @@ class BIMWorkCalendarProperties(PropertyGroup):
end_time: StringProperty(name="End Time") 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): 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_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): class BIMDateTextProperties(PropertyGroup):
+37 -13
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os import os
import re import re
import bpy import bpy
@@ -24,7 +25,9 @@ import base64
import pystache import pystache
import mathutils import mathutils
import webbrowser import webbrowser
import isodate
import ifcopenshell import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.sequence import ifcopenshell.util.sequence
import ifcopenshell.util.date import ifcopenshell.util.date
import ifcopenshell.util.element import ifcopenshell.util.element
@@ -34,7 +37,11 @@ import bonsai.tool as tool
import bonsai.bim.helper import bonsai.bim.helper
from dateutil import parser from dateutil import parser
from datetime import datetime 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): 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: def load_task_time_attributes(cls, task_time: ifcopenshell.entity_instance) -> None:
import bonsai.bim.module.sequence.helper as helper 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": if prop and prop.data_type == "string":
duration_props = bpy.context.scene.BIMWorkScheduleProperties.durations_attributes.add() # TODO: Check actual attribute type instead of providing attribute names.
duration_props.name = name if name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
if prop.is_null: duration_props = bpy.context.scene.BIMWorkScheduleProperties.durations_attributes.add()
for key in duration_props.keys(): duration_props.name = name
if key != "name": if prop.is_null:
setattr(duration_props, key, 0) for key in duration_props.keys():
return True if key != "name":
if name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"] and data[name]: setattr(duration_props, key, 0)
for key, value in helper.parse_duration_as_blender_props(data[name]).items(): return True
duration_props[key] = value if data[name]:
return True for key, value in helper.parse_duration_as_blender_props(data[name]).items():
duration_props[key] = value
return True
if isinstance(data[name], datetime): if isinstance(data[name], datetime):
prop.string_value = "" if prop.is_null else data[name].isoformat() prop.string_value = "" if prop.is_null else data[name].isoformat()
return True return True
@@ -1713,3 +1724,16 @@ class Sequence(bonsai.core.tool.Sequence):
def get_animation_color_scheme(cls): def get_animation_color_scheme(cls):
if len(bpy.context.scene.BIMAnimationProperties.saved_color_schemes) > 0: if len(bpy.context.scene.BIMAnimationProperties.saved_color_schemes) > 0:
return tool.Ifc.get().by_id(int(bpy.context.scene.BIMAnimationProperties.saved_color_schemes)) 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_)