#1153 You can now add / remove / open schedules, implemented as IFC documents

This commit is contained in:
Dion Moult
2022-04-28 14:16:39 +10:00
parent 9ea6499b5a
commit 3f28ae18c0
12 changed files with 305 additions and 61 deletions
@@ -39,6 +39,7 @@ classes = (
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableEditingDrawings,
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
operator.DisableEditingTextProduct,
@@ -49,7 +50,9 @@ classes = (
operator.EnableEditingTextProduct,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadSchedules,
operator.LoadSheets,
operator.OpenSchedule,
operator.OpenSheet,
operator.OpenView,
operator.RemoveDrawing,
@@ -61,7 +64,6 @@ classes = (
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SelectDocIfcFile,
operator.SelectScheduleFile,
prop.Variable,
prop.Drawing,
prop.Schedule,
@@ -24,6 +24,7 @@ import blenderbim.tool as tool
def refresh():
TextData.is_loaded = False
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
@@ -94,3 +95,17 @@ class DrawingsData:
)
return results
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
class SchedulesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"total_schedules": cls.total_schedules()}
cls.is_loaded = True
@classmethod
def total_schedules(cls):
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"])
@@ -913,55 +913,53 @@ class RemoveSheet(bpy.types.Operator, Operator):
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
class AddSchedule(bpy.types.Operator):
class AddSchedule(bpy.types.Operator, Operator):
bl_idname = "bim.add_schedule"
bl_label = "Add Schedule"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
new = context.scene.DocProperties.schedules.add()
new.name = "SCHEDULE {}".format(len(context.scene.DocProperties.schedules))
return {"FINISHED"}
class RemoveSchedule(bpy.types.Operator):
bl_idname = "bim.remove_schedule"
bl_label = "Remove Schedule"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.DocProperties
props.schedules.remove(self.index)
return {"FINISHED"}
class SelectScheduleFile(bpy.types.Operator):
bl_idname = "bim.select_schedule_file"
bl_label = "Select Documentation IFC File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ods", options={"HIDDEN"})
index: bpy.props.IntProperty()
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context):
props = context.scene.DocProperties
props.active_schedule.file = self.filepath
return {"FINISHED"}
def _execute(self, context):
filepath = self.filepath
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
core.add_schedule(
tool.Ifc,
tool.Drawing,
uri=filepath,
)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class RemoveSchedule(bpy.types.Operator, Operator):
bl_idname = "bim.remove_schedule"
bl_label = "Remove Schedule"
bl_options = {"REGISTER", "UNDO"}
schedule: bpy.props.IntProperty()
def _execute(self, context):
core.remove_schedule(tool.Ifc, tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
class OpenSchedule(bpy.types.Operator, Operator):
bl_idname = "bim.open_schedule"
bl_label = "Open Schedule"
bl_options = {"REGISTER", "UNDO"}
schedule: bpy.props.IntProperty()
def _execute(self, context):
core.open_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
class BuildSchedule(bpy.types.Operator):
bl_idname = "bim.build_schedule"
bl_label = "Build Schedule"
@classmethod
def poll(cls, context):
return context.scene.DocProperties.active_schedule.file
def execute(self, context):
props = context.scene.DocProperties
schedule = props.active_schedule
@@ -1269,6 +1267,24 @@ class DisableEditingSheets(bpy.types.Operator, Operator):
core.disable_editing_sheets(tool.Drawing)
class LoadSchedules(bpy.types.Operator, Operator):
bl_idname = "bim.load_schedules"
bl_label = "Load Schedules"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.load_schedules(tool.Drawing)
class DisableEditingSchedules(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_schedules"
bl_label = "Disable Editing Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.disable_editing_schedules(tool.Drawing)
class LoadDrawings(bpy.types.Operator, Operator):
bl_idname = "bim.load_drawings"
bl_label = "Load Drawings"
@@ -153,6 +153,11 @@ def update_drawing_name(self, context):
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
def update_schedule_name(self, context):
schedule = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_schedule_name(tool.Ifc, tool.Drawing, schedule=schedule, name=self.name)
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
@@ -231,8 +236,9 @@ class Drawing(PropertyGroup):
class Schedule(PropertyGroup):
name: StringProperty(name="Name")
file: StringProperty(name="File")
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_schedule_name)
identification: StringProperty(name="Identification")
class Sheet(PropertyGroup):
@@ -305,6 +311,7 @@ class DocProperties(PropertyGroup):
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
should_extract: BoolProperty(name="Should Extract", default=True)
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
target_view: EnumProperty(
items=[
("PLAN_VIEW", "Plan", ""),
@@ -335,10 +342,6 @@ class DocProperties(PropertyGroup):
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@property
def active_schedule(self):
return self.schedules[self.active_schedule_index]
class BIMCameraProperties(PropertyGroup):
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
@@ -20,7 +20,7 @@ import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
from blenderbim.bim.module.drawing.data import TextData, SheetsData, DrawingsData
from blenderbim.bim.module.drawing.data import TextData, SheetsData, SchedulesData, DrawingsData
class BIM_PT_camera(Panel):
@@ -196,22 +196,31 @@ class BIM_PT_schedules(Panel):
bl_category = "BIM Documentation"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.scene.DocProperties
if not SchedulesData.is_loaded:
SchedulesData.load()
row = layout.row(align=True)
row.operator("bim.add_schedule")
self.props = context.scene.DocProperties
if props.schedules:
row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="")
row.operator("bim.remove_schedule", icon="X", text="").index = props.active_schedule_index
if not self.props.is_editing_schedules:
row = self.layout.row(align=True)
row.label(text=f"{SchedulesData.data['total_schedules']} Schedules Found", icon="LONGDISPLAY")
row.operator("bim.load_schedules", text="", icon="IMPORT")
return
layout.template_list("BIM_UL_generic", "", props, "schedules", props, "active_schedule_index")
row = self.layout.row(align=True)
row.operator("bim.add_schedule", icon="ADD")
row.operator("bim.disable_editing_schedules", text="", icon="CANCEL")
row = layout.row()
row.prop(props.active_schedule, "file")
row.operator("bim.select_schedule_file", icon="FILE_FOLDER", text="")
if self.props.schedules:
if self.props.active_schedule_index < len(self.props.schedules):
active_schedule = self.props.schedules[self.props.active_schedule_index]
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.open_schedule", icon="URL", text="").schedule = active_schedule.ifc_definition_id
row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="")
row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
self.layout.template_list("BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index")
class BIM_PT_sheets(Panel):
+5 -2
View File
@@ -106,8 +106,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="SVG to DXF Command",
description="E.g. [['inkscape', svg, '-o', eps], ['pstoedit', '-dt', '-f', 'dxf:-polyaslines -mm', eps, dxf, '-psarg', '-dNOSAFER']]",
)
svg_command: StringProperty(name="SVG Command", description="E.g. [['firefox-bin', path]]")
pdf_command: StringProperty(name="PDF Command", description="E.g. [['firefox-bin', path]]")
svg_command: StringProperty(name="SVG Command", description="E.g. [['firefox', path]]")
pdf_command: StringProperty(name="PDF Command", description="E.g. [['firefox', path]]")
spreadsheet_command: StringProperty(name="Spreadsheet Command", description="E.g. [['libreoffice', path]]")
openlca_port: IntProperty(name="OpenLCA IPC Port", default=8080)
should_hide_empty_props: BoolProperty(name="Should Hide Empty Properties", default=True)
should_play_chaching_sound: BoolProperty(
@@ -141,6 +142,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(self, "pdf_command")
row = layout.row()
row.prop(self, "spreadsheet_command")
row = layout.row()
row.prop(self, "openlca_port")
row = layout.row()
row.prop(self, "should_hide_empty_props")
+36
View File
@@ -89,6 +89,42 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def load_schedules(drawing):
drawing.import_schedules()
drawing.enable_editing_schedules()
def disable_editing_schedules(drawing):
drawing.disable_editing_schedules()
def add_schedule(ifc, drawing, uri=None):
schedule = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=schedule)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
else:
attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
drawing.import_schedules()
def remove_schedule(ifc, drawing, schedule=None):
ifc.run("document.remove_information", information=schedule)
drawing.import_schedules()
def open_schedule(drawing, schedule=None):
drawing.open_spreadsheet(drawing.get_schedule_location(schedule))
def update_schedule_name(ifc, drawing, schedule=None, name=None):
if drawing.get_name(schedule) != name:
ifc.run("document.edit_information", information=schedule, attributes={"Name": name})
def load_drawings(drawing):
drawing.import_drawings()
drawing.enable_editing_drawings()
+3
View File
@@ -161,11 +161,13 @@ class Drawing:
def delete_drawing_elements(cls, elements): pass
def delete_object(cls, obj): pass
def disable_editing_drawings(cls): pass
def disable_editing_schedules(cls): pass
def disable_editing_sheets(cls): pass
def disable_editing_text(cls, obj): pass
def disable_editing_text_product(cls, obj): pass
def enable_editing(cls, obj): pass
def enable_editing_drawings(cls): pass
def enable_editing_schedules(cls): pass
def enable_editing_sheets(cls): pass
def enable_editing_text(cls, obj): pass
def enable_editing_text_product(cls, obj): pass
@@ -187,6 +189,7 @@ class Drawing:
def get_text_literal(cls, obj): pass
def get_text_product(cls, element): pass
def import_drawings(cls): pass
def import_schedules(cls): pass
def import_sheets(cls): pass
def import_text_attributes(cls, obj): pass
def import_text_product(cls, obj): pass
+33
View File
@@ -98,6 +98,10 @@ class Drawing(blenderbim.core.tool.Drawing):
def disable_editing_drawings(cls):
bpy.context.scene.DocProperties.is_editing_drawings = False
@classmethod
def disable_editing_schedules(cls):
bpy.context.scene.DocProperties.is_editing_schedules = False
@classmethod
def disable_editing_sheets(cls):
bpy.context.scene.DocProperties.is_editing_sheets = False
@@ -122,6 +126,10 @@ class Drawing(blenderbim.core.tool.Drawing):
def enable_editing_drawings(cls):
bpy.context.scene.DocProperties.is_editing_drawings = True
@classmethod
def enable_editing_schedules(cls):
bpy.context.scene.DocProperties.is_editing_schedules = True
@classmethod
def enable_editing_sheets(cls):
bpy.context.scene.DocProperties.is_editing_sheets = True
@@ -197,6 +205,12 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_name(cls, element):
return element.Name
@classmethod
def get_schedule_location(cls, schedule):
if tool.Ifc.get_schema() == "IFC2X3":
return schedule.DocumentReferences[0].Location
return schedule.Location
@classmethod
def get_sheet_filename(cls, document):
if hasattr(document, "Identification"):
@@ -273,6 +287,19 @@ class Drawing(blenderbim.core.tool.Drawing):
new.name = drawing.Name or "Unnamed"
new.target_view = cls.get_drawing_target_view(drawing)
@classmethod
def import_schedules(cls):
bpy.context.scene.DocProperties.schedules.clear()
schedules = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"]
for schedule in schedules:
new = bpy.context.scene.DocProperties.schedules.add()
new.ifc_definition_id = schedule.id()
new.name = schedule.Name or "Unnamed"
if tool.Ifc.get_schema() == "IFC2X3":
new.identification = schedule.DocumentId
else:
new.identification = schedule.Identification
@classmethod
def import_sheets(cls):
props = bpy.context.scene.DocProperties
@@ -333,6 +360,12 @@ class Drawing(blenderbim.core.tool.Drawing):
else:
webbrowser.open("file://" + path)
@classmethod
def open_spreadsheet(cls, uri):
cls.open_with_user_command(
bpy.context.preferences.addons["blenderbim"].preferences.spreadsheet_command, uri
)
@classmethod
def open_svg(cls, filename):
cls.open_with_user_command(
+65
View File
@@ -126,6 +126,71 @@ class TestRemoveSheet:
subject.remove_sheet(ifc, drawing, sheet="sheet")
class TestLoadSchedules:
def test_run(self, drawing):
drawing.import_schedules().should_be_called()
drawing.enable_editing_schedules().should_be_called()
subject.load_schedules(drawing)
class TestDisableEditingSchedules:
def test_run(self, drawing):
drawing.disable_editing_schedules().should_be_called()
subject.disable_editing_schedules(drawing)
class TestAddSchedule:
def test_run(self, ifc, drawing):
ifc.run("document.add_information").should_be_called().will_return("schedule")
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC4")
ifc.run(
"document.edit_information",
information="schedule",
attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": "uri"},
).should_be_called()
drawing.import_schedules().should_be_called()
subject.add_schedule(ifc, drawing, uri="uri")
def test_using_a_document_id_in_ifc2x3(self, ifc, drawing):
ifc.run("document.add_information").should_be_called().will_return("schedule")
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC2X3")
ifc.run(
"document.edit_information",
information="schedule",
attributes={"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"},
).should_be_called()
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
drawing.import_schedules().should_be_called()
subject.add_schedule(ifc, drawing, uri="uri")
class TestRemoveSchedule:
def test_run(self, ifc, drawing):
ifc.run("document.remove_information", information="schedule").should_be_called()
drawing.import_schedules().should_be_called()
subject.remove_schedule(ifc, drawing, schedule="schedule")
class TestOpenSchedule:
def test_run(self, drawing):
drawing.get_schedule_location("schedule").should_be_called().will_return("uri")
drawing.open_spreadsheet("uri").should_be_called()
subject.open_schedule(drawing, schedule="schedule")
class TestUpdateScheduleName:
def test_do_not_update_if_name_unchanged(self, ifc, drawing):
drawing.get_name("schedule").should_be_called().will_return("name")
subject.update_drawing_name(ifc, drawing, schedule="schedule", name="name")
def test_run(self, ifc, drawing):
drawing.get_name("schedule").should_be_called().will_return("oldname")
ifc.run("document.edit_information", information="schedule", attributes={"Name": "name"}).should_be_called()
subject.update_drawing_name(ifc, drawing, schedule="schedule", name="name")
class TestLoadDrawings:
def test_run(self, drawing):
drawing.import_drawings().should_be_called()
+5 -5
View File
@@ -60,8 +60,8 @@ class TestAddRepresentation:
profile_set_usage="profile_set_usage",
).should_be_called().will_return("representation")
# Styles are relevant for meshes with faces only
geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(True)
# Styles are relevant for body representations only (as a simplification)
geometry.is_body_representation("representation").should_be_called().will_return(True)
# Add styles
geometry.get_object_materials_without_styles("obj").should_be_called().will_return(["material"])
@@ -102,7 +102,7 @@ class TestAddRepresentation:
== "representation"
)
def test_not_handling_styles_if_representation_has_no_faces(self, ifc, geometry, style, surveyor):
def test_not_handling_styles_if_not_a_body_representation(self, ifc, geometry, style, surveyor):
TestEditObjectPlacement.predict(self, ifc, geometry, surveyor)
# Add representation
@@ -126,8 +126,8 @@ class TestAddRepresentation:
profile_set_usage="profile_set_usage",
).should_be_called().will_return("representation")
# Styles are relevant for meshes with faces only
geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(False)
# Styles are relevant for body representations only (as a simplification)
geometry.is_body_representation("representation").should_be_called().will_return(False)
# Assign representation to product
ifc.run("geometry.assign_representation", product="element", representation="representation").should_be_called()
+59
View File
@@ -87,6 +87,13 @@ class TestDisableEditingDrawings(NewFile):
assert bpy.context.scene.DocProperties.is_editing_drawings == False
class TestDisableEditingSchedules(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_schedules = True
subject.disable_editing_schedules()
assert bpy.context.scene.DocProperties.is_editing_schedules == False
class TestDisableEditingSheets(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_sheets = True
@@ -125,6 +132,13 @@ class TestEnableEditingDrawings(NewFile):
assert bpy.context.scene.DocProperties.is_editing_drawings == True
class TestEnableEditingSchedules(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_schedules = False
subject.enable_editing_schedules()
assert bpy.context.scene.DocProperties.is_editing_schedules == True
class TestEnableEditingSheets(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_sheets = False
@@ -263,6 +277,22 @@ class TestGetName(NewFile):
assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar"
class TestGetScheduleLocation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
schedule = ifcopenshell.api.run("document.add_information", ifc)
schedule.Location = "uri"
reference = ifcopenshell.api.run("document.add_reference", ifc, information=schedule)
subject.get_schedule_location(schedule) == "uri"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
reference = ifc.createIfcDocumentReference(Location="uri")
schedule = ifc.createIfcDocumentInformation(DocumentReferences=[reference])
subject.get_sheet_filename(schedule) == "uri"
class TestGetSheetFilename(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -408,6 +438,30 @@ class TestImportDrawings(NewFile):
assert props.drawings[0].target_view == "PLAN_VIEW"
class TestImportSchedules(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_schedules()
props = bpy.context.scene.DocProperties
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_schedules()
props = bpy.context.scene.DocProperties
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
class TestImportSheets(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -475,6 +529,11 @@ class TestImportTextProduct(NewFile):
assert label_obj.BIMTextProperties.relating_product is None
class TestOpenSchedule(NewFile):
def open_spreadsheet(self):
pass
class TestOpenSvg(NewFile):
def test_nothing(self):
pass