From edfcdbb86520b8fa97b3ab335eb5d97b6205d63b Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Thu, 3 Apr 2025 07:20:10 -0700 Subject: [PATCH] Playing with input panels for alignment --- .../bonsai/bim/module/alignment/__init__.py | 38 ++++- .../bonsai/bim/module/alignment/data.py | 80 ++++++++++ .../bonsai/bim/module/alignment/operator.py | 51 ++++++- .../bonsai/bim/module/alignment/prop.py | 64 ++++++++ src/bonsai/bonsai/bim/module/alignment/ui.py | 140 ++++++++++++++++++ src/bonsai/bonsai/core/alignment.py | 57 +++++++ src/bonsai/bonsai/core/tool.py | 4 + src/bonsai/bonsai/tool/__init__.py | 1 + src/bonsai/bonsai/tool/alignment.py | 120 +++++++++++++++ .../ifcopenshell/api/cogo/__init__.py | 31 ++++ .../ifcopenshell/api/cogo/add_survey_point.py | 44 ++++++ .../api/cogo/assign_survey_point.py | 38 +++++ .../api/cogo/edit_survey_point.py | 40 +++++ .../test/api/cogo/test_add_survey_point.py | 45 ++++++ .../test/api/cogo/test_assign_survey_point.py | 48 ++++++ .../test/api/cogo/test_edit_survey_point.py | 48 ++++++ 16 files changed, 844 insertions(+), 5 deletions(-) create mode 100644 src/bonsai/bonsai/bim/module/alignment/data.py create mode 100644 src/bonsai/bonsai/bim/module/alignment/prop.py create mode 100644 src/bonsai/bonsai/bim/module/alignment/ui.py create mode 100644 src/bonsai/bonsai/core/alignment.py create mode 100644 src/bonsai/bonsai/tool/alignment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cogo/__init__.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cogo/add_survey_point.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cogo/assign_survey_point.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cogo/edit_survey_point.py create mode 100644 src/ifcopenshell-python/test/api/cogo/test_add_survey_point.py create mode 100644 src/ifcopenshell-python/test/api/cogo/test_assign_survey_point.py create mode 100644 src/ifcopenshell-python/test/api/cogo/test_edit_survey_point.py 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)) +