diff --git a/src/bonsai/bonsai/bim/module/alignment/__init__.py b/src/bonsai/bonsai/bim/module/alignment/__init__.py
index 3c49e0b4fe..4b1d1f8f96 100644
--- a/src/bonsai/bonsai/bim/module/alignment/__init__.py
+++ b/src/bonsai/bonsai/bim/module/alignment/__init__.py
@@ -16,21 +16,51 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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
-# from . import ui, prop, operator
-from . import operator
-
-classes = (operator.ImportAlignmentCSV,)
+# 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.ImportAlignmentCSV,
+ operator.BuildAlignment,
+ operator.SurveyPoint,
+ prop.BIMAlignmentBuilderProperties,
+ ui.BIM_PT_alignment,
+)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
+
+# 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():
+ # Properties are usually stored on bpy.types.Scene when they are something
+ # that affects everything in the project, or bpy.types.Object when they
+ # affect a single BIM element.
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
+ bpy.types.Scene.BIMAlignmentBuilderProperties = bpy.props.PointerProperty(type=prop.BIMAlignmentBuilderProperties)
+# 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.BIMAlignmentBuilderProperties
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
diff --git a/src/bonsai/bonsai/bim/module/alignment/data.py b/src/bonsai/bonsai/bim/module/alignment/data.py
new file mode 100644
index 0000000000..1651b68477
--- /dev/null
+++ b/src/bonsai/bonsai/bim/module/alignment/data.py
@@ -0,0 +1,80 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai 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.
+#
+# Bonsai 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 Bonsai. If not, see .
+
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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 bonsai.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!
+ AlignmentBuilderData.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 AlignmentBuilderData:
+ # 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
+ x = 0.0
+ y = 0.0
+
+ # 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(),
+ "x" : cls.x(),
+ "y" : cls.y()
+ }
+ 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 x(cls):
+ return cls.x
+
+ @classmethod
+ def y(cls):
+ return cls.y
diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py
index c11f8ca718..f7d63c9cf4 100644
--- a/src/bonsai/bonsai/bim/module/alignment/operator.py
+++ b/src/bonsai/bonsai/bim/module/alignment/operator.py
@@ -18,6 +18,17 @@
# pyright: reportUnnecessaryTypeIgnoreComment=error
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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 os
import ifcopenshell.api.alignment
@@ -28,7 +39,7 @@ import json
import time
import calendar
import isodate
-import bonsai.core.sequence as core
+import bonsai.core.alignment as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
@@ -112,3 +123,41 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
+
+
+
+# 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 BuildAlignment(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.build_alignment"
+
+ # In the interface, this button will have the text "Build Alignment".
+ bl_label = "Build Alignment"
+
+ # 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 = "Builds a dummy alignment"
+
+ # 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.build_alignment(tool.Alignment)
+
+class SurveyPoint(bpy.types.Operator,tool.Ifc.Operator):
+ bl_idname = "bim.add_survey_point"
+ bl_label = "Add Survey Point"
+ bl_options = {"REGISTER","UNDO"}
+ bl_description = "Adds a survey point"
+ def _execute(self,context):
+ core.add_survey_point(tool.Alignment,x=bpy.context.scene.BIMAlignmentBuilderProperties.x,y=bpy.context.scene.BIMAlignmentBuilderProperties.y)
\ No newline at end of file
diff --git a/src/bonsai/bonsai/bim/module/alignment/prop.py b/src/bonsai/bonsai/bim/module/alignment/prop.py
new file mode 100644
index 0000000000..58042091db
--- /dev/null
+++ b/src/bonsai/bonsai/bim/module/alignment/prop.py
@@ -0,0 +1,64 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai 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.
+#
+# Bonsai 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 Bonsai. If not, see .
+
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai code. Please feel free to reach
+# out if you have any questions or need further guidance. Happy hacking!
+
+# ############################################################################ #
+
+# Every module has a prop.py file to define Blender properties. Any time you
+# want an interface widget like an input field, dropdown, checkbox, or number
+# slider, you need a Blender property to store that widget's data. If you want
+# to store data that will affect the Blender interface, you also need a
+# property. Properties are stored in the .blend file, so when your user closes
+# their Blender session, and reopens it, things are how they left it.
+
+import bpy
+from bpy.types import PropertyGroup
+
+# Properties have many different data types. We won't use all of them in this
+# demo module, but this is a list for your reference.
+from bpy.props import (
+ PointerProperty,
+ StringProperty,
+ EnumProperty,
+ BoolProperty,
+ IntProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ CollectionProperty,
+)
+
+
+# All properties must belong in a property group. Usually, you'd have a group
+# named after your module.
+class BIMAlignmentBuilderProperties(PropertyGroup):
+ # This first property is a string. This means that in the interface, it will
+ # represent a text input field. We can give it a name and a default value.
+ # The name will be the label shown next to the input field in the interface.
+ name: StringProperty(name="Name", default="New Project Name")
+ # Not all properties need to be shown using their equivalent input widget.
+ # In this case, we can store a message string, but we will never show it as
+ # an input text field in the ui.py.
+ message: StringProperty(name="Message")
+ show_hints: BoolProperty(name="Show Hints", default=False)
+ #webui_message: StringProperty(name="Web UI Message", default="Hello, Web UI!")
+ x: FloatProperty(name="x")
+ y: FloatProperty(name="y")
diff --git a/src/bonsai/bonsai/bim/module/alignment/ui.py b/src/bonsai/bonsai/bim/module/alignment/ui.py
new file mode 100644
index 0000000000..817c2ce0d6
--- /dev/null
+++ b/src/bonsai/bonsai/bim/module/alignment/ui.py
@@ -0,0 +1,140 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai 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.
+#
+# Bonsai 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 Bonsai. If not, see .
+
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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 bonsai.bim.module.alignment.data import AlignmentBuilderData
+
+
+# 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_alignment(bpy.types.Panel):
+ # Every panel has a title.
+ bl_label = "Alignment 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_alignment"
+
+ # This tells the panel to appear in the properties section (in the bottom
+ # right by default) of the Blender interface.
+ bl_space_type = "VIEW_3D" #"PROPERTIES"
+ bl_region_type = "UI" #"WINDOW"
+ bl_category = "Survey"
+
+ # 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 AlignmentBuilderData.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.
+ AlignmentBuilderData.load()
+
+ # Interface panels often show properties. For convenience, define where
+ # the properties are stored for the module.
+ self.props = context.scene.BIMAlignmentBuilderProperties
+
+ # 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 AlignmentBuilderData.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.build_alignment")
+
+ # 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="Build Dummy Alignment")
+ # 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=AlignmentData.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")
+
+ row = self.layout.row()
+ row.label(text="Survey Point")
+ row.prop(self.props,"x")
+ row.prop(self.props,"y")
+ row = self.layout.row()
+ row.operator("bim.add_survey_point")
+
+ #if self.props.show_hints:
+ # row = self.layout.row()
+ # row.label(text="Name cannot be blank!")
diff --git a/src/bonsai/bonsai/core/alignment.py b/src/bonsai/bonsai/core/alignment.py
new file mode 100644
index 0000000000..f999dd12a7
--- /dev/null
+++ b/src/bonsai/bonsai/core/alignment.py
@@ -0,0 +1,57 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai 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.
+#
+# Bonsai 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 Bonsai. If not, see .
+
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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 build_alignment(alignment):
+ # 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.
+ alignment.build()
+
+
+def add_survey_point(alignment,x,y):
+ alignment.add_survey_point(x,y)
\ No newline at end of file
diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py
index 6135be093c..031b2141fa 100644
--- a/src/bonsai/bonsai/core/tool.py
+++ b/src/bonsai/bonsai/core/tool.py
@@ -74,6 +74,10 @@ class Aggregate:
def get_container(cls, element): pass
def get_relating_object(cls, related_element): pass
+@interface
+class Alignment:
+ def build_alignment(cls): pass
+ def add_survey_point(cls,x,y): pass
@interface
class Bcf:
diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py
index 6e3fc32944..52e543c900 100644
--- a/src/bonsai/bonsai/tool/__init__.py
+++ b/src/bonsai/bonsai/tool/__init__.py
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see .
from bonsai.tool.aggregate import Aggregate
+from bonsai.tool.alignment import Alignment
from bonsai.tool.bcf import Bcf
from bonsai.tool.blender import Blender
from bonsai.tool.boundary import Boundary
diff --git a/src/bonsai/bonsai/tool/alignment.py b/src/bonsai/bonsai/tool/alignment.py
new file mode 100644
index 0000000000..ff79ac0612
--- /dev/null
+++ b/src/bonsai/bonsai/tool/alignment.py
@@ -0,0 +1,120 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai 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.
+#
+# Bonsai 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 Bonsai. If not, see .
+
+# ############################################################################ #
+
+# Hey there! Welcome to the Bonsai 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 ifcopenshell.settings
+import bpy
+import bonsai.core.tool
+import bonsai.tool as tool
+import ifcopenshell.api
+import ifcopenshell.api.alignment
+import ifcopenshell
+import ifcopenshell.api.cogo
+
+# There is always one class in each tool file, which implements the interface
+# defined by `core/tool.py`.
+class Alignment(bonsai.core.tool.Alignment):
+ @classmethod
+ def add_survey_point(cls,x,y):
+ model = tool.Ifc.get()
+ point = model.createIfcCartesianPoint((x,y))
+ annotation = ifcopenshell.api.cogo.add_survey_point(model,point)
+
+ m = tool.Loader.create_point_cloud_mesh(annotation.Representation.Representations[0])
+ tool.Ifc.link(annotation.Representation.Representations[0],m)
+
+ # create a new Blender object
+ annotation_obj = bpy.data.objects.new(tool.Loader.get_name(annotation), m)
+
+ # link the blender object to with the IFC element
+ tool.Geometry.link(annotation, annotation_obj)
+
+ # assign the object to the blender collections
+ tool.Collector.assign(annotation_obj, should_clean_users_collection=False)
+
+
+ @classmethod
+ def build(cls):
+ coordinates = [(0.0,0.0),(100.0,0.0),(1000.,200.)]
+ radii = [(100.)]
+
+ vpoints = [(0.0,0.0),(100.0,0.0),(200.0,150.0)]
+ lengths = [(50.)]
+
+ model = tool.Ifc.get()
+
+ # create an IfcAlignment with Name="Dummy"
+ alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(model,"Dummy",coordinates,radii,vpoints,lengths)
+
+ ifcopenshell.api.alignment.create_geometric_representation(model, alignment)
+ ifcopenshell.api.alignment.add_stationing_to_alignment(model, alignment=alignment, start_station=0.0)
+
+ # IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
+ sites = model.by_type("IfcSite")
+ for site in sites:
+ ifcopenshell.api.spatial.reference_structure(model, products=[alignment], relating_structure=site)
+
+ # process the generated IfcReferent for the alignment
+ for rel in alignment.IsNestedBy:
+ for referent in rel.RelatedObjects:
+ if referent.is_a("IfcReferent"):
+ referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
+ tool.Geometry.link(referent, referent_obj)
+ tool.Collector.assign(referent_obj, should_clean_users_collection=False)
+
+ # an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
+ # get all the alignment curves
+ curves = []
+ for rel in alignment.IsDecomposedBy:
+ for agg in rel.RelatedObjects:
+ if agg.is_a("IfcAlignment"):
+ curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
+
+ # if there aren't any curves from aggregation, then there is only a single vertical or no vertical
+ if len(curves) == 0:
+ curves.append(ifcopenshell.api.alignment.get_curve(alignment))
+
+ settings = ifcopenshell.geom.settings()
+ for curve in curves:
+ shape = ifcopenshell.geom.create_shape(settings, curve)
+
+ # create a new Blender mesh
+ mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
+ mesh = bpy.data.meshes.new(mesh_name)
+ m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
+
+ # create a new Blender object
+ alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
+
+ # link the blender object to with the alignment element
+ tool.Geometry.link(alignment, alignment_obj)
+
+ # assign the object to the blender collections
+ tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cogo/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/cogo/__init__.py
new file mode 100644
index 0000000000..0bc2329504
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/cogo/__init__.py
@@ -0,0 +1,31 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+"""
+Coordinate Geometry (cogo) functions primarily for survey points and control monument for layout, parcels, etc.
+"""
+
+from .add_survey_point import add_survey_point
+from .assign_survey_point import assign_survey_point
+from .edit_survey_point import edit_survey_point
+
+__all__ = [
+ "add_survey_point",
+ "assign_survey_point",
+ "edit_survey_point",
+]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cogo/add_survey_point.py b/src/ifcopenshell-python/ifcopenshell/api/cogo/add_survey_point.py
new file mode 100644
index 0000000000..2b3c17381a
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/cogo/add_survey_point.py
@@ -0,0 +1,44 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import ifcopenshell
+from ifcopenshell import entity_instance
+import typing
+
+def add_survey_point(file: ifcopenshell.file, survey_point: entity_instance) -> entity_instance:
+ """
+ Adds a single survey point to the model based on IFC Concept Template 4.1.7.1.2.5.
+ Survey points are located relative to IfcRepresentationContext.WorldCoordinateSystem
+
+ :param survey_point: The survey point
+ :return: an IfcAnnotation entity
+
+ Example:
+
+ .. code:: python
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
+ """
+ context = ifcopenshell.util.representation.get_context(file,"Model","Annotation","MODEL_VIEW")
+ shape_representation = file.createIfcShapeRepresentation(ContextOfItems=context,RepresentationIdentifier='Annotation',RepresentationType='Point',Items=[survey_point])
+ representation = file.createIfcProductDefinitionShape(Representations=[shape_representation])
+ annotation = file.createIfcAnnotation(ifcopenshell.guid.new(),ObjectPlacement=context.WorldCoordinateSystem,Representation=representation,PredefinedType="SURVEY")
+ site = file.by_type("IfcSite")[0]
+ ifcopenshell.api.spatial.assign_container(file,relating_structure=site,products=[annotation])
+
+ return annotation
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cogo/assign_survey_point.py b/src/ifcopenshell-python/ifcopenshell/api/cogo/assign_survey_point.py
new file mode 100644
index 0000000000..0208a93fb4
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/cogo/assign_survey_point.py
@@ -0,0 +1,38 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import ifcopenshell
+from ifcopenshell import entity_instance
+import typing
+
+def assign_survey_point(annotation: entity_instance, survey_point: entity_instance):
+ """
+ Assigns a coordinate point to a survey point annotation
+
+ :param annotaton: The survey point annotation
+ :param survey_point: The survey point
+ :return: None
+
+ Example:
+
+ .. code:: python
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
+ ifcopenshell.api.cogo.assign_surve_point(annotation,file.createIfcCartesianPoint(4000.0,3500.0,100.0))
+ """
+ annotation.Representation.Representations[0].Items = [survey_point]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cogo/edit_survey_point.py b/src/ifcopenshell-python/ifcopenshell/api/cogo/edit_survey_point.py
new file mode 100644
index 0000000000..46aaa1eff3
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/cogo/edit_survey_point.py
@@ -0,0 +1,40 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import ifcopenshell
+from ifcopenshell import entity_instance
+import typing
+
+def edit_survey_point(annotation: entity_instance, x:float,y:float,z:float=0.0):
+ """
+ Edits the location of a previously defined survey point
+
+ :param survey_point: The survey point
+ :return: None
+
+ Example:
+
+ .. code:: python
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
+ ifcopenshell.api.cogo.edit_surve_point(annotation,3500.0,2000.0)
+ """
+ if annotation.Representation.Representations[0].Items[0].Dim == 2:
+ annotation.Representation.Representations[0].Items[0].Coordinates = ((x,y))
+ else:
+ annotation.Representation.Representations[0].Items[0].Coordinates = ((x,y,z))
diff --git a/src/ifcopenshell-python/test/api/cogo/test_add_survey_point.py b/src/ifcopenshell-python/test/api/cogo/test_add_survey_point.py
new file mode 100644
index 0000000000..f142f4318f
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/cogo/test_add_survey_point.py
@@ -0,0 +1,45 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import ifcopenshell.api.alignment
+import ifcopenshell.api.context
+import ifcopenshell.api.cogo
+
+
+def test_add_survey_point():
+ file = ifcopenshell.file(schema="IFC4X3_ADD2")
+ project = file.createIfcProject(Name="Test")
+ site = file.createIfcSite(GlobalId=ifcopenshell.guid.new(),Name="MySite")
+ ifcopenshell.api.aggregate.assign_object(file,relating_object=project,products=[site])
+ geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model")
+ axis_model_representation_subcontext = ifcopenshell.api.context.add_context(
+ file,
+ context_type="Model",
+ context_identifier="Annotation",
+ target_view="MODEL_VIEW",
+ parent=geometric_representation_context,
+ )
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint((50.0,10.0)))
+ assert annotation
+ assert annotation.PredefinedType == "SURVEY"
+ assert annotation.Representation.Representations[0].RepresentationIdentifier == "Annotation"
+ assert annotation.Representation.Representations[0].RepresentationType == "Point"
+ assert annotation.Representation.Representations[0].Items[0].Coordinates == pytest.approx((50.0,10.0))
+
diff --git a/src/ifcopenshell-python/test/api/cogo/test_assign_survey_point.py b/src/ifcopenshell-python/test/api/cogo/test_assign_survey_point.py
new file mode 100644
index 0000000000..83e7fd5fac
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/cogo/test_assign_survey_point.py
@@ -0,0 +1,48 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import ifcopenshell.api.alignment
+import ifcopenshell.api.context
+import ifcopenshell.api.cogo
+
+
+def test_assign_survey_point():
+ file = ifcopenshell.file(schema="IFC4X3_ADD2")
+ project = file.createIfcProject(Name="Test")
+ site = file.createIfcSite(GlobalId=ifcopenshell.guid.new(),Name="MySite")
+ ifcopenshell.api.aggregate.assign_object(file,relating_object=project,products=[site])
+ geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model")
+ axis_model_representation_subcontext = ifcopenshell.api.context.add_context(
+ file,
+ context_type="Model",
+ context_identifier="Annotation",
+ target_view="MODEL_VIEW",
+ parent=geometric_representation_context,
+ )
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint((50.0,10.0)))
+ assert annotation
+ assert annotation.PredefinedType == "SURVEY"
+ assert annotation.Representation.Representations[0].RepresentationIdentifier == "Annotation"
+ assert annotation.Representation.Representations[0].RepresentationType == "Point"
+ assert annotation.Representation.Representations[0].Items[0].Coordinates == pytest.approx((50.0,10.0))
+
+ ifcopenshell.api.cogo.assign_survey_point(annotation,file.createIfcCartesianPoint((20.0,30.0,40.0)))
+ assert annotation.Representation.Representations[0].Items[0].Coordinates == pytest.approx((20.0,30.0,40.0))
+
diff --git a/src/ifcopenshell-python/test/api/cogo/test_edit_survey_point.py b/src/ifcopenshell-python/test/api/cogo/test_edit_survey_point.py
new file mode 100644
index 0000000000..59426acae7
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/cogo/test_edit_survey_point.py
@@ -0,0 +1,48 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2025 Thomas Krijnen
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import ifcopenshell.api.alignment
+import ifcopenshell.api.context
+import ifcopenshell.api.cogo
+
+
+def test_edit_survey_point():
+ file = ifcopenshell.file(schema="IFC4X3_ADD2")
+ project = file.createIfcProject(Name="Test")
+ site = file.createIfcSite(GlobalId=ifcopenshell.guid.new(),Name="MySite")
+ ifcopenshell.api.aggregate.assign_object(file,relating_object=project,products=[site])
+ geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model")
+ axis_model_representation_subcontext = ifcopenshell.api.context.add_context(
+ file,
+ context_type="Model",
+ context_identifier="Annotation",
+ target_view="MODEL_VIEW",
+ parent=geometric_representation_context,
+ )
+
+ annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint((50.0,10.0)))
+ assert annotation
+ assert annotation.PredefinedType == "SURVEY"
+ assert annotation.Representation.Representations[0].RepresentationIdentifier == "Annotation"
+ assert annotation.Representation.Representations[0].RepresentationType == "Point"
+ assert annotation.Representation.Representations[0].Items[0].Coordinates == pytest.approx((50.0,10.0))
+
+ ifcopenshell.api.cogo.edit_survey_point(annotation,20.0,30.0)
+ assert annotation.Representation.Representations[0].Items[0].Coordinates == pytest.approx((20.0,30.0))
+