mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-10-03 21:38:12 +00:00
New demo module to teach developers how to hack on the BlenderBIM Add-on.
This commit is contained in:
@@ -68,6 +68,8 @@ modules = {
|
|||||||
"covetool": None,
|
"covetool": None,
|
||||||
"augin": None,
|
"augin": None,
|
||||||
"debug": None,
|
"debug": None,
|
||||||
|
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||||
|
# "demo": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has a __init__.py file to load all of its classes. Every
|
||||||
|
# operation, property, and interface needs to be registered with the Blender
|
||||||
|
# system when the add-on loads. This is where it happens.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from . import ui, prop, operator
|
||||||
|
|
||||||
|
# You'll need to provide a list of every one of your classes here. If you forget
|
||||||
|
# to specify your class, it won't load and you won't be able to use that
|
||||||
|
# operator, property, or interface.
|
||||||
|
classes = (
|
||||||
|
operator.DemonstrateHelloWorld,
|
||||||
|
operator.DemonstrateRenameProject,
|
||||||
|
prop.BIMDemoProperties,
|
||||||
|
ui.BIM_PT_demo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# When the add-on loads, this register function is called. This allows you to
|
||||||
|
# perform additional tasks during startup. If you need to store custom
|
||||||
|
# properties, this is where you tell Blender where they are going to be stored.
|
||||||
|
# You might see more advanced registrations happening in other modules.
|
||||||
|
def register():
|
||||||
|
bpy.types.Scene.BIMDemoProperties = bpy.props.PointerProperty(type=prop.BIMDemoProperties)
|
||||||
|
|
||||||
|
|
||||||
|
# When someone disables the add-on, we need to unload everything we loaded. This
|
||||||
|
# does the reverse of the register function.
|
||||||
|
def unregister():
|
||||||
|
del bpy.types.Scene.BIMDemoProperties
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has a data.py file to load data for its interface panels to
|
||||||
|
# display. When a panel needs to show information that doesn't come from a
|
||||||
|
# Blender property, it gets that data from one of these classes. This separation
|
||||||
|
# of "fetching data" and "displaying data" is a common concept that makes UI
|
||||||
|
# code much simpler, more efficient, and less error-prone to refresh data.
|
||||||
|
|
||||||
|
import blenderbim.tool as tool
|
||||||
|
|
||||||
|
|
||||||
|
# All data must have a refresh function. The refresh function simply sets all
|
||||||
|
# data class to be not yet loaded. So the next time the interface is drawn to
|
||||||
|
# the user, it will force the data to be reloaded.
|
||||||
|
def refresh():
|
||||||
|
# When you define your own data classes, just add to this list!
|
||||||
|
DemoData.is_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
# This is a sample data class. It correlates to a single interface panel. Panels
|
||||||
|
# should not share data classes. This makes it easy to write your interface
|
||||||
|
# without having your code mixed in with other parts of the interface. As a
|
||||||
|
# convention, the class is named the same name as the panel.
|
||||||
|
class DemoData:
|
||||||
|
# All data classes must have two variables. One to store all the data it has
|
||||||
|
# loaded and another to store the load state.
|
||||||
|
data = {}
|
||||||
|
is_loaded = False
|
||||||
|
|
||||||
|
# Every data class must have a load function. This lets us load data in a
|
||||||
|
# predictable manner.
|
||||||
|
@classmethod
|
||||||
|
def load(cls):
|
||||||
|
# The load function always has two responsibilities: populate the data,
|
||||||
|
# and set is_loaded to true.
|
||||||
|
cls.data = {
|
||||||
|
"has_project": cls.has_project(),
|
||||||
|
"project_name": cls.project_name(),
|
||||||
|
}
|
||||||
|
cls.is_loaded = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_project(cls):
|
||||||
|
# Here, we check whether or not there is an active IFC project in our
|
||||||
|
# Blender session.
|
||||||
|
return bool(tool.Ifc.get())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def project_name(cls):
|
||||||
|
# Imagine how messy our UI code would be if all of this was mixed in
|
||||||
|
# with our layout code. Here, it's isolated and testable, and the UX
|
||||||
|
# becomes easier to maintain and change.
|
||||||
|
ifc = tool.Ifc.get()
|
||||||
|
if ifc:
|
||||||
|
return ifc.by_type("IfcProject")[0].Name or "Unnamed"
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has an operator.py file to define all of the buttons that a user
|
||||||
|
# can click on from the Blender interface. Blender calls these buttons
|
||||||
|
# "Operators", since they correlate to a single user operation.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import blenderbim.tool as tool
|
||||||
|
import blenderbim.core.demo as core
|
||||||
|
import blenderbim.bim.handler
|
||||||
|
|
||||||
|
|
||||||
|
# Each button correlates to a class like the one below. In this case, we're
|
||||||
|
# creating a new button that will execute a hello world feature.
|
||||||
|
class DemonstrateHelloWorld(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
# Every operator has a unique ID. If you enable Python tooltips and hover
|
||||||
|
# over any button in the interface, you will see each button will run a
|
||||||
|
# function that uses this ID. For example, hovering over this button will
|
||||||
|
# show that the code it executes is "bpy.ops.bim.demonstrate_hello_world()"
|
||||||
|
bl_idname = "bim.demonstrate_hello_world"
|
||||||
|
|
||||||
|
# In the interface, this button will have the text "Print Hello World".
|
||||||
|
bl_label = "Print Hello World"
|
||||||
|
|
||||||
|
# This code means that the user can undo or redo after pressing the button.
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
# When hovering over the button, this helpful description will be shown.
|
||||||
|
bl_description = "Prints the text 'Hello World'"
|
||||||
|
|
||||||
|
# When the button is pressed, this _execute() function will run.
|
||||||
|
def _execute(self, context):
|
||||||
|
# Every operator should do one thing only: execute a core function. In
|
||||||
|
# order to execute a core function, the operator's responsibility is to
|
||||||
|
# pass in all of the inputs the core needs to do its job.
|
||||||
|
|
||||||
|
# A core function simply tells tools what to do, so a core function will
|
||||||
|
# always need at least one tool as an input.
|
||||||
|
core.demonstrate_hello_world(tool.Demo)
|
||||||
|
|
||||||
|
|
||||||
|
# This class, just like the one above, will correlate to another button in the
|
||||||
|
# interface. It has a different ID and a different label.
|
||||||
|
class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
bl_idname = "bim.demonstrate_rename_project"
|
||||||
|
bl_label = "Rename IFC Project"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
bl_description = "Renames your IFC project name and prints the name"
|
||||||
|
|
||||||
|
def _execute(self, context):
|
||||||
|
# This core function requires three inputs: two tools, and a name
|
||||||
|
# string. In this case, the name is taken from some custom Blender
|
||||||
|
# properties. Generally, the inputs to the core function will come from
|
||||||
|
# properties (such as an input field) or data from the scene (like the
|
||||||
|
# actively selected object).
|
||||||
|
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from blenderbim.bim.prop import StrProperty, Attribute
|
||||||
|
from bpy.types import PropertyGroup
|
||||||
|
from bpy.props import (
|
||||||
|
PointerProperty,
|
||||||
|
StringProperty,
|
||||||
|
EnumProperty,
|
||||||
|
BoolProperty,
|
||||||
|
IntProperty,
|
||||||
|
FloatProperty,
|
||||||
|
FloatVectorProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BIMDemoProperties(PropertyGroup):
|
||||||
|
name: StringProperty(name="Name", default="New Project Name")
|
||||||
|
message: StringProperty(name="Message")
|
||||||
|
show_hints: BoolProperty(name="Show Hints", default=False)
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has a ui.py file to define its interface. Interfaces describe how
|
||||||
|
# panels, buttons, labels, and input fields are laid out.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from blenderbim.bim.module.demo.data import DemoData
|
||||||
|
|
||||||
|
# Every panel in the interface correlates to one of these classes. If you enable
|
||||||
|
# the "Developer Extras" option in Blender, then you can actually right click on
|
||||||
|
# any panel in Blender, and click "Edit Source". This will bring you to a ui.py
|
||||||
|
# file like this one where you can see this code. Pretty neat!
|
||||||
|
class BIM_PT_demo(bpy.types.Panel):
|
||||||
|
# Every panel has a title.
|
||||||
|
bl_label = "BlenderBIM Demo"
|
||||||
|
|
||||||
|
# Every panel must have an ID. It must be unique. For example, you may want
|
||||||
|
# to later reference the panel (such as if you want to create a nested
|
||||||
|
# subpanel).
|
||||||
|
bl_idname = "BIM_PT_demo"
|
||||||
|
|
||||||
|
# This tells the panel to appear in the properties section (in the bottom
|
||||||
|
# right by default) of the Blender interface.
|
||||||
|
bl_space_type = "PROPERTIES"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
|
||||||
|
# This tells the panel to appear in the "scene" tab of the properties panel.
|
||||||
|
bl_context = "scene"
|
||||||
|
|
||||||
|
# Every panel has a draw function. This draws all the layout of the panel,
|
||||||
|
# including all of its labels, buttons, and so on. Note that if the panel is
|
||||||
|
# hidden, this draw function will not be called.
|
||||||
|
def draw(self, context):
|
||||||
|
# Before drawing any content, the panel must load any dynamic variables
|
||||||
|
# that it will show. Most panels have dynamic content, which display
|
||||||
|
# changing data from your BIM model. You must load that data first. The
|
||||||
|
# two lines are always the same, "if not loaded, then load".
|
||||||
|
if not DemoData.is_loaded:
|
||||||
|
# Each panel should have its own data class that it loads data from.
|
||||||
|
# Note that we only load data if it hasn't already been loaded.
|
||||||
|
# This is because interface panels are drawn continuously. This
|
||||||
|
# draw() function will be called every time you scroll or move your
|
||||||
|
# mouse over it. Loading data is slow, so we only refresh data when
|
||||||
|
# we have to.
|
||||||
|
DemoData.load()
|
||||||
|
|
||||||
|
# Interface panels often show properties. For convenience, define where
|
||||||
|
# the properties are stored for the module.
|
||||||
|
self.props = context.scene.BIMDemoProperties
|
||||||
|
|
||||||
|
# This defines a new "row" in our layout. When a new row is defined, the
|
||||||
|
# things on that row, like buttons, labels, and input fields, show on a
|
||||||
|
# new line.
|
||||||
|
row = self.layout.row()
|
||||||
|
# This is the simplest interface element - a label placed in our row.
|
||||||
|
# If you enable the "Icon Viewer" add-on, you can view a list of
|
||||||
|
# Blender's built-in icons to choose from in the Blender text editor's
|
||||||
|
# side panel.
|
||||||
|
row.label(text="This is a demo panel", icon="INFO")
|
||||||
|
|
||||||
|
# Our interface can contain simple logic. For example, if we don't have
|
||||||
|
# an IFC project, show an error message and don't draw anything else.
|
||||||
|
# The ui.py should always have very simple logic. Details like how to
|
||||||
|
# determine whether we have a project is delegated to the data loader.
|
||||||
|
if not DemoData.data["has_project"]:
|
||||||
|
row = self.layout.row()
|
||||||
|
row.label(text="Load or create an IFC project first", icon="ERROR")
|
||||||
|
return
|
||||||
|
|
||||||
|
row = self.layout.row()
|
||||||
|
# When you want to show a button in the interface, you have to specify
|
||||||
|
# an operator. Operators are defined in operator.py. You reference an
|
||||||
|
# operator using its unique ID.
|
||||||
|
row.operator("bim.demonstrate_hello_world")
|
||||||
|
|
||||||
|
# Blender properties can be used to affect what your panel shows. Notice
|
||||||
|
# how the logic is very simple. Only use simple boolean predicates and
|
||||||
|
# loops in your layout. No complex logic should ever been seen in the
|
||||||
|
# interface code.
|
||||||
|
if self.props.message:
|
||||||
|
row = self.layout.row()
|
||||||
|
row.label(text=self.props.message)
|
||||||
|
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.label(text="Project Name")
|
||||||
|
# Sometimes you need data that doesn't come from Blender properties,
|
||||||
|
# like from your IFC model. In this case, just get some data from your
|
||||||
|
# data loader. For those familiar with how templating languages work,
|
||||||
|
# this is exactly the same.
|
||||||
|
row.label(text=DemoData.data["project_name"])
|
||||||
|
|
||||||
|
row = self.layout.row()
|
||||||
|
# We've seen how to show text labels and buttons using operators. What
|
||||||
|
# about text input fields, number sliders, dropdowns, checkboxes, and
|
||||||
|
# scrollable lists? The way Blender works is that you simply show a
|
||||||
|
# Blender property in your interface. The data type of that property
|
||||||
|
# determines which UI widget is shown. A text property will show a text
|
||||||
|
# field. A number property will show a number slider. An enum property
|
||||||
|
# will show a drop down. And so on. In this case, our name property is a
|
||||||
|
# text data type, so expect to see a text input field show up here.
|
||||||
|
row.prop(self.props, "name")
|
||||||
|
row = self.layout.row()
|
||||||
|
# Here's another button, referencing another operator.
|
||||||
|
row.operator("bim.demonstrate_rename_project")
|
||||||
|
|
||||||
|
if self.props.show_hints:
|
||||||
|
row = self.layout.row()
|
||||||
|
row.label(text="Name cannot be blank!")
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has a core.py file to define all of its core functions. A core
|
||||||
|
# function describes what happens when the user wants to do something like
|
||||||
|
# pressing a button.
|
||||||
|
|
||||||
|
# Think of a core function as a short poem of pseudocode that describes what
|
||||||
|
# happens in different usecases. A core should be no more than 50 lines of code,
|
||||||
|
# even in the most complex of features. A core simply delegates tasks to tools
|
||||||
|
# in a sequence that describes the flow of logic in a feature - in other words,
|
||||||
|
# it tells tools to do different things. You will notice that the core doesn't
|
||||||
|
# have any code that deals with Blender or IFC directly - these are all little
|
||||||
|
# details that are hidden away in tools. The core is not interested in these
|
||||||
|
# details, the core is only concerned with the big picture.
|
||||||
|
|
||||||
|
# Imagine, no matter how complex a software can be, every feature can be
|
||||||
|
# described in regular sentences in under 50 lines. That is the purpose of the
|
||||||
|
# core.
|
||||||
|
|
||||||
|
# Here's the simplest possible core function. It does one thing only. Remember:
|
||||||
|
# core functions delegate tasks to tools, so all core functions need at least
|
||||||
|
# one tool.
|
||||||
|
def demonstrate_hello_world(demo):
|
||||||
|
# We're telling the demo tool to set a message. We aren't interested how the
|
||||||
|
# tool works, that's a detail. We aren't interested in the interface, like
|
||||||
|
# where the message is shown. You can name these functions whatever you feel
|
||||||
|
# best describes what's going on, like if you had to describe the feature to
|
||||||
|
# someone else.
|
||||||
|
demo.set_message("Hello, World!")
|
||||||
|
|
||||||
|
|
||||||
|
# Here's a slightly more complex core function. It uses two tools. By default,
|
||||||
|
# you'll want to use your module's tool (in this case, "Demo") for all tasks,
|
||||||
|
# except for changing IFC data, where you'll use the "Ifc" tool. At a glance,
|
||||||
|
# simply by seeing what tools are used, this gives you an idea about what
|
||||||
|
# aspects (or dependencies) a core function cares about. This function also has
|
||||||
|
# an non-tool input called "name". Non-tool inputs should be keyword arguments
|
||||||
|
# with possible default values.
|
||||||
|
def demonstrate_rename_project(ifc, demo, name=None):
|
||||||
|
# As you can see, core functions read almost like pseudocode. A great way to
|
||||||
|
# code a new feature is to write out what it does in English first, then
|
||||||
|
# change them into tool functions. You can choose any function you want, and
|
||||||
|
# you can see a list of every single tool function in # `core/tool.py`.
|
||||||
|
if name:
|
||||||
|
project = demo.get_project()
|
||||||
|
ifc.run("attribute.edit_attributes", product=project, attributes={"Name": name})
|
||||||
|
demo.clear_name_field()
|
||||||
|
demo.hide_user_hints()
|
||||||
|
else:
|
||||||
|
demo.show_user_hints()
|
||||||
@@ -30,6 +30,38 @@ def interface(cls):
|
|||||||
return type(cls.__name__, (Interface, cls), attrs)
|
return type(cls.__name__, (Interface, cls), attrs)
|
||||||
|
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# This portion of the code is singled out as it is part of the Demo module, used
|
||||||
|
# to teach people how the code is structured in the BlenderBIM Add-on.
|
||||||
|
|
||||||
|
# This is an example "interface" (as in, a software interface, not a user
|
||||||
|
# interface) for the Demo module. Think of it as a list of all of the functions
|
||||||
|
# your module can perform. In this file, you will come across every single
|
||||||
|
# capability of the entire add-on. By breaking it down into little functions, we
|
||||||
|
# can test each one separately, and at a glance know if there are opportunities
|
||||||
|
# for code reuse or refactoring. At this point, we're only interested in a list
|
||||||
|
# of functions, not the details of how they're implemented. This is because the
|
||||||
|
# purpose of the core is to just give a high level overview of the application's
|
||||||
|
# capabilities.
|
||||||
|
|
||||||
|
@interface
|
||||||
|
class Demo:
|
||||||
|
def clear_name_field(cls): pass
|
||||||
|
def get_project(cls): pass
|
||||||
|
def hide_user_hints(cls): pass
|
||||||
|
def set_message(cls, message): pass
|
||||||
|
def show_user_hints(cls): pass
|
||||||
|
|
||||||
|
# The rest of the code in this file is not part of the Demo tutorial.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
@interface
|
@interface
|
||||||
class Aggregate:
|
class Aggregate:
|
||||||
def can_aggregate(cls, relating_object, related_object): pass
|
def can_aggregate(cls, relating_object, related_object): pass
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from blenderbim.tool.brick import Brick
|
|||||||
from blenderbim.tool.collector import Collector
|
from blenderbim.tool.collector import Collector
|
||||||
from blenderbim.tool.context import Context
|
from blenderbim.tool.context import Context
|
||||||
from blenderbim.tool.debug import Debug
|
from blenderbim.tool.debug import Debug
|
||||||
|
from blenderbim.tool.demo import Demo
|
||||||
from blenderbim.tool.drawing import Drawing
|
from blenderbim.tool.drawing import Drawing
|
||||||
from blenderbim.tool.geometry import Geometry
|
from blenderbim.tool.geometry import Geometry
|
||||||
from blenderbim.tool.ifc import Ifc
|
from blenderbim.tool.ifc import Ifc
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Every module has a tool file which implements all the functions that the core
|
||||||
|
# needs. Whereas the core is simply high level code, the tool file has the
|
||||||
|
# concrete implementations, dealing with exactly how things interact with
|
||||||
|
# Blender's property systems, IFC's data structures, the filesystem, geometry
|
||||||
|
# processing, and more.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import blenderbim.core.tool
|
||||||
|
import blenderbim.tool as tool
|
||||||
|
|
||||||
|
|
||||||
|
# There is always one class in each tool file, which implements the interface
|
||||||
|
# defined by `core/tool.py`.
|
||||||
|
class Demo(blenderbim.core.tool.Demo):
|
||||||
|
@classmethod
|
||||||
|
def clear_name_field(cls):
|
||||||
|
# In this concrete implementation, we see that "clear name field"
|
||||||
|
# actually translates to "set this Blender string property to empty
|
||||||
|
# string". In this case, it's pretty simple - but even simple scenarios
|
||||||
|
# like these are important to implement in the tool, as it makes the
|
||||||
|
# pseudocode easier to read in the core, and makes it easier to test
|
||||||
|
# implementations separately from control flow. It also makes it easy to
|
||||||
|
# refactor and share functions, where every tool function is captured by
|
||||||
|
# a function name that describes its intention.
|
||||||
|
bpy.context.scene.BIMDemoProperties.name = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_project(cls):
|
||||||
|
return tool.Ifc.get().by_type("IfcProject")[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def hide_user_hints(cls):
|
||||||
|
bpy.context.scene.BIMDemoProperties.show_hints = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_message(cls, message):
|
||||||
|
bpy.context.scene.BIMDemoProperties.message = message
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def show_user_hints(cls):
|
||||||
|
bpy.context.scene.BIMDemoProperties.show_hints = True
|
||||||
@@ -65,3 +65,9 @@ class Ifc(blenderbim.core.tool.Ifc):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def unlink(cls, element=None, obj=None):
|
def unlink(cls, element=None, obj=None):
|
||||||
IfcStore.unlink_element(element, obj)
|
IfcStore.unlink_element(element, obj)
|
||||||
|
|
||||||
|
class Operator:
|
||||||
|
def execute(self, context):
|
||||||
|
IfcStore.execute_ifc_operator(self, context)
|
||||||
|
blenderbim.bim.handler.refresh_ui_data()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ markers =
|
|||||||
brick
|
brick
|
||||||
context
|
context
|
||||||
debug
|
debug
|
||||||
|
demo
|
||||||
drawing
|
drawing
|
||||||
geometry
|
geometry
|
||||||
library
|
library
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# This allows us to write full integration tests that test how all systems work
|
||||||
|
# as a whole. Whilst other tests focus on portions of the software, these tests
|
||||||
|
# simulate what happens when a user opens Blender, presses buttons, and does
|
||||||
|
# things.
|
||||||
|
|
||||||
|
# These tests read like english. You can see all the possible sentences defined
|
||||||
|
# in test_feature.py. Most of the time, there is already a sentence defined for
|
||||||
|
# what you want to test.
|
||||||
|
|
||||||
|
@demo
|
||||||
|
Feature: Demo
|
||||||
|
|
||||||
|
# Every operator has at least one scenario associated with it to test it.
|
||||||
|
Scenario: Demonstrate hello world
|
||||||
|
Given an empty IFC project
|
||||||
|
When I press "bim.demonstrate_hello_world"
|
||||||
|
# Blender doesn't have a way of testing that things are visible in the
|
||||||
|
# interface and layout. We can check properties, and whats in the 3D
|
||||||
|
# scenegraph, but not layout. There is no "DOM" like in web applications.
|
||||||
|
# Too bad, we can't check the results, but we still write the test, that way
|
||||||
|
# we can still check for errors like crashes or Python errors, like a "smoke
|
||||||
|
# test".
|
||||||
|
Then nothing happens
|
||||||
|
|
||||||
|
# This operator has two scenarios because there are two possibilities of a user
|
||||||
|
# interacting with it.
|
||||||
|
Scenario: Demonstrate rename project - with a name provided
|
||||||
|
Given an empty IFC project
|
||||||
|
When I set "scene.BIMDemoProperties.name" to "Foobar"
|
||||||
|
And I press "bim.demonstrate_rename_project"
|
||||||
|
Then the object "IfcProject/Foobar" is an "IfcProject"
|
||||||
|
|
||||||
|
# This is the other possible scenario for the rename project operator.
|
||||||
|
Scenario: Demonstrate rename project - with no name
|
||||||
|
Given an empty IFC project
|
||||||
|
When I set "scene.BIMDemoProperties.name" to ""
|
||||||
|
And I press "bim.demonstrate_rename_project"
|
||||||
|
Then the object "IfcProject/My Project" is an "IfcProject"
|
||||||
@@ -209,6 +209,19 @@ def i_set_prop_to_value(prop, value):
|
|||||||
exec(f"bpy.context.{prop} = {value}")
|
exec(f"bpy.context.{prop} = {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@given(parsers.parse('I set "{prop}" to ""'))
|
||||||
|
@when(parsers.parse('I set "{prop}" to ""'))
|
||||||
|
def i_set_prop_to_value(prop):
|
||||||
|
try:
|
||||||
|
eval(f"bpy.context.{prop}")
|
||||||
|
except:
|
||||||
|
assert False, "Property does not exist"
|
||||||
|
try:
|
||||||
|
exec(f'bpy.context.{prop} = r""')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@when(parsers.parse('I am on frame "{number}"'))
|
@when(parsers.parse('I am on frame "{number}"'))
|
||||||
def i_am_on_frame_number(number):
|
def i_am_on_frame_number(number):
|
||||||
bpy.context.scene.frame_set(int(number))
|
bpy.context.scene.frame_set(int(number))
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ def debug():
|
|||||||
prophet.verify()
|
prophet.verify()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def demo():
|
||||||
|
prophet = Prophecy(blenderbim.core.tool.Demo)
|
||||||
|
yield prophet
|
||||||
|
prophet.verify()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def drawing():
|
def drawing():
|
||||||
prophet = Prophecy(blenderbim.core.tool.Drawing)
|
prophet = Prophecy(blenderbim.core.tool.Drawing)
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Testing the core might seem strange if you haven't written this type of
|
||||||
|
# abstract test before. You essentially want to test that things are called in
|
||||||
|
# the right sequence. This seems almost like writing the code twice, like in
|
||||||
|
# double entry bookkeeping in accounting. However, it guards against typos,
|
||||||
|
# helps document intention of different logical flows, and ensures that all
|
||||||
|
# permutations of logic flows meet sanity checks.
|
||||||
|
|
||||||
|
# We always call the module we're testing the "test subject". This makes our
|
||||||
|
# tests simple to read: just look for where the "subject" is called!
|
||||||
|
import blenderbim.core.demo as subject
|
||||||
|
# These are like mocks, stubs, or spy objects. They don't do anything, but they
|
||||||
|
# let us check our test expectations.
|
||||||
|
from test.core.bootstrap import ifc, demo
|
||||||
|
|
||||||
|
|
||||||
|
# Let's test the hello world function.
|
||||||
|
class TestDemonstrateHelloWorld:
|
||||||
|
# This function has only one logical flow, there is no benefit to describing
|
||||||
|
# it further, so we have a test_run function. We need to specify all the
|
||||||
|
# mock objects we need in the signature.
|
||||||
|
def test_run(self, demo):
|
||||||
|
# We set an expectation that in this default sequence of events, the
|
||||||
|
# demo tool should have the set_message called with the "Hello, World!"
|
||||||
|
# string as its argument. Notice how our test also reads like English.
|
||||||
|
demo.set_message("Hello, World!").should_be_called()
|
||||||
|
# After we've finished specifying our test expectations, let's run the
|
||||||
|
# test subject!
|
||||||
|
subject.demonstrate_hello_world(demo)
|
||||||
|
|
||||||
|
|
||||||
|
# Another test, but this time with two logical flows.
|
||||||
|
class TestDemonstrateRenameProject:
|
||||||
|
# The default logical flow is where we rename the project successfully.
|
||||||
|
def test_renaming_the_project(self, ifc, demo):
|
||||||
|
# This time, we describe an expectation that the demo tool should have
|
||||||
|
# its get_project() function called with no attributes. When it is
|
||||||
|
# called, we expect it to return "project" as a string.
|
||||||
|
|
||||||
|
# This might sound strange. How is get_project implemented? Does it
|
||||||
|
# actually return a string? We don't know, and we don't care. That's a
|
||||||
|
# detail that our core isn't interested in. All our core is interested
|
||||||
|
# in is that we get back a project - we're arbitrarily using a string to
|
||||||
|
# represent it.
|
||||||
|
demo.get_project().should_be_called().will_return("project")
|
||||||
|
# Here, again, we're not interested in the details. However, we are
|
||||||
|
# interested in checking that the project we previously retrieved is
|
||||||
|
# passed verbatim into the Ifc tool.
|
||||||
|
ifc.run("attribute.edit_attributes", product="project", attributes={"Name": "name"}).should_be_called()
|
||||||
|
demo.clear_name_field().should_be_called()
|
||||||
|
demo.hide_user_hints().should_be_called()
|
||||||
|
# Let's test our test subject! Notice that just like the arbitrary
|
||||||
|
# string "project", we've specified an arbitrary input string of "name".
|
||||||
|
subject.demonstrate_rename_project(ifc, demo, name="name")
|
||||||
|
|
||||||
|
# One alternative flow is when no name is provided. What happens then?
|
||||||
|
def test_showing_a_hint_if_no_name_provided(self, ifc, demo):
|
||||||
|
demo.show_user_hints().should_be_called()
|
||||||
|
subject.demonstrate_rename_project(ifc, demo, name=None)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of BlenderBIM Add-on.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# BlenderBIM Add-on 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||||
|
# out if you have any questions or need further guidance. Happy hacking!
|
||||||
|
|
||||||
|
# ############################################################################ #
|
||||||
|
|
||||||
|
# Because our tools have well defined, isolated functions, it means we can test
|
||||||
|
# them very easily in isolation. Tests are fun, fast, and easy to setup!
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import ifcopenshell
|
||||||
|
import blenderbim.core.tool
|
||||||
|
import blenderbim.tool as tool
|
||||||
|
from test.bim.bootstrap import NewFile
|
||||||
|
from blenderbim.tool.demo import Demo as subject
|
||||||
|
|
||||||
|
|
||||||
|
# Our first test is that our tool implements all the abstract methods defined by
|
||||||
|
# the `core/tool.py` interface. Anytime the core wants something that the tools
|
||||||
|
# don't provide, this test will catch it. This type of test comes for free in
|
||||||
|
# other languages, but not Python, so we test it explicitly.
|
||||||
|
class TestImplementsTool(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
assert isinstance(subject(), blenderbim.core.tool.Demo)
|
||||||
|
|
||||||
|
|
||||||
|
# These are fairly boring tests. What it does demonstrate is that no matter how
|
||||||
|
# complex a BIM application can get, it can always be reduced down to small,
|
||||||
|
# easily tested functions. Note that these functions actually are concrete
|
||||||
|
# implementations, so that means that you need to use Blender headlessly to run
|
||||||
|
# these tests.
|
||||||
|
class TestClearNameField(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
bpy.context.scene.BIMDemoProperties.name = "name"
|
||||||
|
subject.clear_name_field()
|
||||||
|
assert bpy.context.scene.BIMDemoProperties.name == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetProject(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
# Sometimes, there is a bit of preparation work to setup a scenario that
|
||||||
|
# can be tested. In this case, we need to set an active IFC dataset with
|
||||||
|
# a project. This is normal.
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
project = ifc.createIfcProject()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
assert subject.get_project() == project
|
||||||
|
|
||||||
|
|
||||||
|
class TestHideUserHints(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
bpy.context.scene.BIMDemoProperties.show_hints = True
|
||||||
|
subject.hide_user_hints()
|
||||||
|
assert bpy.context.scene.BIMDemoProperties.show_hints == False
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetMessage(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
bpy.context.scene.BIMDemoProperties.message = ""
|
||||||
|
subject.set_message("message")
|
||||||
|
assert bpy.context.scene.BIMDemoProperties.message == "message"
|
||||||
|
|
||||||
|
|
||||||
|
class TestShowUserHints(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
bpy.context.scene.BIMDemoProperties.show_hints = False
|
||||||
|
subject.show_user_hints()
|
||||||
|
assert bpy.context.scene.BIMDemoProperties.show_hints == True
|
||||||
Reference in New Issue
Block a user