├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── __init__.py ├── bl ├── __init__.py ├── handlers.py ├── menus.py ├── operators │ ├── __init__.py │ ├── check_geom.py │ ├── choose_namelist_id.py │ ├── clean_ma_slots.py │ ├── copy_params.py │ ├── gis.py │ ├── load_bf_settings.py │ ├── mesh_tools.py │ ├── run_external.py │ ├── scene_export.py │ ├── scene_import.py │ ├── show_fds_code.py │ ├── show_fds_geometry.py │ ├── show_ui.py │ └── update_addon.py ├── panels.py ├── preferences.py ├── ui │ ├── __init__.py │ ├── bf_ui │ │ ├── __init__.py │ │ ├── properties.py │ │ ├── topbar.py │ │ └── view3d.py │ └── simplify_ui.py └── ui_lists.py ├── config.py ├── dev ├── doxygen │ ├── .gitignore │ ├── doxyfile │ └── update-doxygen.sh └── make_release.py ├── docs ├── CNAME ├── _config.yml └── index.md ├── lang ├── MN_SURF.py ├── ON_DEVC.py ├── ON_GEOM │ ├── ON_GEOM.py │ ├── __init__.py │ ├── bingeom.py │ ├── geom_to_ob.py │ └── ob_to_geom.py ├── ON_HOLE.py ├── ON_INIT.py ├── ON_MESH │ ├── ON_MESH.py │ ├── __init__.py │ ├── align_meshes.py │ ├── calc_meshes.py │ └── split_mesh.py ├── ON_MOVE │ ├── ON_MOVE.py │ ├── __init__.py │ └── t34.py ├── ON_MULT │ ├── ON_MULT.py │ ├── __init__.py │ └── multiply.py ├── ON_OBST.py ├── ON_PROF.py ├── ON_SLCF.py ├── ON_VENT.py ├── ON_ZONE.py ├── ON_other.py ├── OP_PB │ ├── OP_PB.py │ ├── __init__.py │ ├── ob_to_pbs.py │ └── pbs_to_ob.py ├── OP_SURF_ID.py ├── OP_XB │ ├── OP_XB.py │ ├── __init__.py │ ├── calc_pixels.py │ ├── calc_voxels.py │ ├── ob_to_xbs.py │ └── xbs_to_ob.py ├── OP_XYZ │ ├── OP_XYZ.py │ ├── __init__.py │ ├── ob_to_xyzs.py │ └── xyzs_to_ob.py ├── SN_CATF.py ├── SN_DUMP │ ├── SN_DUMP.py │ ├── __init__.py │ └── sc_to_ge1.py ├── SN_HEAD.py ├── SN_MISC.py ├── SN_MOVE.py ├── SN_MULT.py ├── SN_PRES.py ├── SN_RADI.py ├── SN_REAC.py ├── SN_TIME.py ├── SN_config.py ├── __init__.py ├── bf_collection.py ├── bf_material.py ├── bf_object.py └── bf_scene │ ├── __init__.py │ ├── bf_scene.py │ ├── export_helper.py │ └── import_helper.py ├── startup.blend ├── types ├── __init__.py ├── bf_exception.py ├── bf_namelist.py ├── bf_param.py └── fds_list.py └── utils ├── __init__.py ├── binpacking.py ├── geometry.py ├── gis.py ├── io.py ├── text.py ├── ui.py └── updater.py /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | .trash 4 | 5 | # Blender 6 | *.blend1 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Jupyter Notebook 17 | .ipynb_checkpoints 18 | 19 | # pyenv 20 | .python-version 21 | 22 | # Environments 23 | .env 24 | .venv 25 | env/ 26 | venv/ 27 | ENV/ 28 | env.bak/ 29 | venv.bak/ 30 | 31 | # Spyder project settings 32 | .spyderproject 33 | .spyproject 34 | 35 | # Visual Studio Code 36 | .vscode 37 | 38 | # html Doxygen 39 | doxyfile/html 40 | 41 | # verification settings 42 | verification/verification.sh 43 | *.log 44 | 45 | # FDS calculations 46 | *.s3d 47 | *.s3d.sz 48 | *_cpu.csv 49 | *_git.txt 50 | *_hrr.csv 51 | *_steps.csv 52 | *_devc.csv 53 | *.binfo 54 | *.end 55 | *.out 56 | *.sinfo 57 | *.smv 58 | *.ge 59 | *.ge2 60 | *.bf 61 | *.bf.bnd 62 | *.sf 63 | *.sf.bnd 64 | *.prt5 65 | *.prt5.bnd 66 | 67 | *.zip 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlenderFDS repository 2 | 3 | ![](https://github.com/firetools/blenderfds/wiki/p/web/logo.png) 4 | 5 | This is the main repository of *BlenderFDS*, the open user interface for 6 | the [NIST Fire Dynamics Simulator (FDS)](https://pages.nist.gov/fds-smv/). 7 | 8 | | [Read the
wiki doc](https://github.com/firetools/blenderfds/wiki) | [Ask a question on the
discussion group](https://groups.google.com/g/blenderfds) | [Submit an
issue](https://github.com/firetools/blenderfds/issues) | 9 | | :---: | :---: | :---: | 10 | 11 | *** 12 | 13 | The development of *BlenderFDS* was funded by a grant from 14 | the Italian Ministry of Foreign Affairs and International Cooperation. 15 | 16 | ![MAECI](https://github.com/firetools/blenderfds/wiki/p/web/logo-maeci.jpeg) 17 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Op calc area of selected bc 4 | HRR Ramp 5 | Auto mesh cutter (https://fireworkbench.com/fds_prepro_mesh) 6 | 7 | check SN_config also when exporting, something could be wrong! 8 | 9 | check mat_slots preview bug corrected, and no need for use_node=False 10 | 11 | Investigate use of bf_dialog: 12 | WM_OT_bf_dialog(Operator): # FIXME move to ui? # FIXME Use WM_OT_bf_dialog? 13 | 14 | collection instance? 15 | 16 | Manage HVAC 17 | Split and coarser MESHes 18 | 19 | release notes 20 | 21 | test cell_size 22 | test geolocation 23 | 24 | user doc 25 | 26 | progress_begin(min, max) 27 | 28 | progress_update(value) 29 | 30 | progress_end() 31 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | # BlenderFDS, an open tool for the NIST Fire Dynamics Simulator 4 | # Copyright (C) 2013 Emanuele Gissi, http://www.blenderfds.org 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | bl_info = { 20 | "name": "BlenderFDS", 21 | "author": "Emanuele Gissi", 22 | "description": "BlenderFDS, an open graphical editor for the NIST Fire Dynamics Simulator", 23 | "blender": (3, 2, 0), 24 | "version": (6, 0, 0), 25 | "location": "File > Export > FDS Case (.fds)", 26 | "warning": "", 27 | "category": "Import-Export", 28 | "doc_url": "http://www.blenderfds.org/", 29 | "tracker_url": "https://github.com/firetools/blenderfds/issues", 30 | "support": "COMMUNITY", 31 | } 32 | 33 | import logging 34 | 35 | # Reading class definitions, 36 | # bl should be imported before lang, 37 | # because it imports ui_lists 38 | from . import bl, lang 39 | 40 | logging.basicConfig(level=logging.INFO) # INFO or DEBUG 41 | log = logging.getLogger(__name__) 42 | 43 | 44 | # Automatic registering/deregistering 45 | # of Blender entities 46 | def register(): 47 | log.info("Register BlenderFDS...") 48 | bl.register() 49 | lang.register() 50 | 51 | 52 | def unregister(): 53 | log.info("Unregister BlenderFDS...") 54 | lang.unregister() 55 | bl.unregister() 56 | -------------------------------------------------------------------------------- /bl/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, Blender related extensions. 5 | """ 6 | 7 | from . import handlers, menus, operators, panels, preferences, ui_lists, ui 8 | 9 | ms_to_register = ( 10 | handlers, 11 | menus, 12 | operators, 13 | panels, 14 | preferences, 15 | ui_lists, 16 | ui, 17 | ) 18 | 19 | 20 | def register(): 21 | for m in ms_to_register: 22 | m.register() 23 | 24 | 25 | def unregister(): 26 | for m in ms_to_register: 27 | m.unregister() 28 | -------------------------------------------------------------------------------- /bl/menus.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, import/export menus. 5 | """ 6 | 7 | import bpy 8 | 9 | 10 | def menu_func_import_FDS(self, context): 11 | self.layout.operator("import_to_scene.fds", text="NIST FDS Case (.fds)") 12 | 13 | 14 | def menu_func_export_to_fds(self, context): 15 | self.layout.operator("export_scene.fds", text="NIST FDS Case (.fds)") 16 | 17 | 18 | def register(): 19 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import_FDS) 20 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export_to_fds) 21 | 22 | 23 | def unregister(): 24 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_to_fds) 25 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_FDS) 26 | -------------------------------------------------------------------------------- /bl/operators/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, Blender Operators. 5 | """ 6 | 7 | from . import ( 8 | check_geom, 9 | choose_namelist_id, 10 | copy_params, 11 | gis, 12 | load_bf_settings, 13 | mesh_tools, 14 | scene_export, 15 | scene_import, 16 | show_fds_code, 17 | show_fds_geometry, 18 | show_ui, 19 | run_external, 20 | clean_ma_slots, 21 | update_addon, 22 | ) 23 | 24 | ms_to_register = ( 25 | scene_import, 26 | scene_export, 27 | check_geom, 28 | choose_namelist_id, 29 | copy_params, 30 | gis, 31 | load_bf_settings, 32 | mesh_tools, 33 | show_fds_code, 34 | show_fds_geometry, 35 | show_ui, 36 | run_external, 37 | clean_ma_slots, 38 | update_addon, 39 | ) 40 | 41 | 42 | def register(): 43 | for m in ms_to_register: 44 | m.register() 45 | 46 | 47 | def unregister(): 48 | for m in reversed(ms_to_register): 49 | m.unregister() 50 | -------------------------------------------------------------------------------- /bl/operators/check_geom.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to check GEOM sanity and intersections. 5 | """ 6 | 7 | import logging 8 | from bpy.types import Operator 9 | from ...types import BFException 10 | from ... import lang 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class OBJECT_OT_bf_check_intersections(Operator): 16 | """! 17 | Check self-intersections or intersections with other selected objects. 18 | """ 19 | 20 | bl_label = "Check Intersections" 21 | bl_idname = "object.bf_geom_check_intersections" 22 | bl_description = ( 23 | "Check self-intersections or intersections with other selected objects" 24 | ) 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return context.object 29 | 30 | def execute(self, context): 31 | w = context.window_manager.windows[0] 32 | w.cursor_modal_set("WAIT") 33 | ob = context.object 34 | obs = context.selected_objects 35 | if obs: 36 | obs.remove(ob) 37 | try: 38 | lang.ON_GEOM.check_intersections( 39 | context, ob, obs, protect=ob.data.bf_geom_protect 40 | ) 41 | except BFException as err: 42 | self.report({"ERROR"}, f"Check intersections: {err}") 43 | return {"CANCELLED"} 44 | else: 45 | self.report({"INFO"}, "No intersection detected") 46 | return {"FINISHED"} 47 | finally: 48 | w.cursor_modal_restore() 49 | 50 | 51 | class OBJECT_OT_bf_check_sanity(Operator): 52 | """! 53 | Check if closed orientable manifold, with no degenerate geometry. 54 | """ 55 | 56 | bl_label = "Check Sanity" 57 | bl_idname = "object.bf_geom_check_sanity" 58 | bl_description = "Check if closed orientable manifold, with no degenerate geometry" 59 | 60 | @classmethod 61 | def poll(cls, context): 62 | return context.object 63 | 64 | def execute(self, context): 65 | w = context.window_manager.windows[0] 66 | w.cursor_modal_set("WAIT") 67 | ob = context.object 68 | try: 69 | lang.ON_GEOM.check_geom_sanity( 70 | context, 71 | ob, 72 | protect=ob.data.bf_geom_protect, 73 | is_open=ob.data.bf_geom_is_terrain, 74 | ) 75 | except BFException as err: 76 | self.report({"ERROR"}, f"Check sanity: {err}") 77 | return {"CANCELLED"} 78 | else: 79 | self.report({"INFO"}, "Geometry sanity ok") 80 | return {"FINISHED"} 81 | finally: 82 | w.cursor_modal_restore() 83 | 84 | 85 | bl_classes = [OBJECT_OT_bf_check_intersections, OBJECT_OT_bf_check_sanity] 86 | 87 | 88 | def register(): 89 | from bpy.utils import register_class 90 | 91 | for c in bl_classes: 92 | register_class(c) 93 | 94 | 95 | def unregister(): 96 | from bpy.utils import unregister_class 97 | 98 | for c in reversed(bl_classes): 99 | unregister_class(c) 100 | -------------------------------------------------------------------------------- /bl/operators/choose_namelist_id.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to choose IDs for MATL_ID, PROP_ID in free text. 5 | """ 6 | 7 | import logging, csv 8 | from bpy.types import Operator 9 | from bpy.props import EnumProperty 10 | from ...types import FDSList 11 | from ... import config 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | # Helper function 16 | 17 | 18 | def get_referenced_ids(context, fds_label="SURF"): 19 | """! 20 | Get fds_label IDs referenced in Free Text. 21 | """ 22 | fds_list = FDSList() 23 | sc = context.scene 24 | 25 | # Get namelists from Free Text 26 | if sc.bf_config_text: 27 | fds_list.from_fds(f90_namelists=sc.bf_config_text.as_string()) 28 | 29 | # Prepare list of IDs 30 | items = list() 31 | for fds_namelist in fds_list.get_fds_namelists(fds_label=fds_label): 32 | fds_param = fds_namelist.get_fds_param(fds_label="ID") 33 | if fds_param: 34 | hid = fds_param.get_value() 35 | items.append((hid, hid, "")) # (identifier, name, description) 36 | items.sort(key=lambda k: k[0]) 37 | 38 | return items 39 | 40 | 41 | # MATL_ID 42 | 43 | 44 | def _get_matl_items(self, context): 45 | return get_referenced_ids(context, fds_label="MATL") 46 | 47 | 48 | class MATERIAL_OT_bf_choose_matl_id(Operator): 49 | bl_label = "Choose MATL_ID" 50 | bl_idname = "material.bf_choose_matl_id" 51 | bl_description = "Choose MATL_ID from MATLs available in Free Text" 52 | bl_property = "bf_matl_id" 53 | 54 | bf_matl_id: EnumProperty( 55 | name="MATL_ID", 56 | description="MATL_ID parameter", 57 | items=_get_matl_items, # Updating function 58 | ) 59 | 60 | @classmethod 61 | def poll(cls, context): 62 | return context.object and context.object.active_material 63 | 64 | def execute(self, context): 65 | if self.bf_matl_id: 66 | context.object.active_material.bf_matl_id = self.bf_matl_id 67 | self.report({"INFO"}, "MATL_ID set") 68 | return {"FINISHED"} 69 | else: 70 | self.report({"WARNING"}, "MATL_ID not set") 71 | return {"CANCELLED"} 72 | 73 | def invoke(self, context, event): 74 | context.window_manager.invoke_search_popup(self) 75 | return {"FINISHED"} 76 | 77 | 78 | def _get_devc_prop_items(self, context): 79 | return get_referenced_ids(context, fds_label="PROP") 80 | 81 | 82 | # PROP_ID 83 | 84 | 85 | class OBJECT_OT_bf_choose_devc_prop_id(Operator): 86 | bl_label = "Choose PROP_ID" 87 | bl_idname = "object.bf_choose_devc_prop_id" 88 | bl_description = "Choose PROP_ID from PROP namelists available in Free Text" 89 | bl_property = "bf_devc_prop_id" 90 | 91 | bf_devc_prop_id: EnumProperty( 92 | name="PROP_ID", 93 | description="PROP_ID parameter", 94 | items=_get_devc_prop_items, # Updating function 95 | ) 96 | 97 | @classmethod 98 | def poll(cls, context): 99 | return context.object 100 | 101 | def execute(self, context): 102 | if self.bf_devc_prop_id: 103 | context.object.bf_devc_prop_id = self.bf_devc_prop_id 104 | self.report({"INFO"}, "PROP_ID set") 105 | return {"FINISHED"} 106 | else: 107 | self.report({"WARNING"}, "PROP_ID not set") 108 | return {"CANCELLED"} 109 | 110 | def invoke(self, context, event): 111 | context.window_manager.invoke_search_popup(self) 112 | return {"FINISHED"} 113 | 114 | 115 | # CTRL_ID 116 | 117 | 118 | def _get_devc_ctrl_items(self, context): 119 | return get_referenced_ids(context, fds_label="CTRL") 120 | 121 | 122 | class OBJECT_OT_bf_choose_devc_ctrl_id(Operator): 123 | bl_label = "Choose CTRL_ID" 124 | bl_idname = "object.bf_choose_devc_ctrl_id" 125 | bl_description = "Choose CTRL_ID from CTRL namelists available in Free Text" 126 | bl_property = "bf_devc_ctrl_id" 127 | 128 | bf_devc_ctrl_id: EnumProperty( 129 | name="CTRL_ID", 130 | description="CTRL_ID parameter", 131 | items=_get_devc_ctrl_items, # Updating function 132 | ) 133 | 134 | @classmethod 135 | def poll(cls, context): 136 | return context.object 137 | 138 | def execute(self, context): 139 | if self.bf_devc_ctrl_id: 140 | context.object.bf_devc_ctrl_id = self.bf_devc_ctrl_id 141 | self.report({"INFO"}, "CTRL_ID set") 142 | return {"FINISHED"} 143 | else: 144 | self.report({"WARNING"}, "CTRL_ID not set") 145 | return {"CANCELLED"} 146 | 147 | def invoke(self, context, event): 148 | context.window_manager.invoke_search_popup(self) 149 | return {"FINISHED"} 150 | 151 | 152 | # DEVC QUANTITY from config.FDS_QUANTITIES 153 | 154 | 155 | def _get_devc_quantity_items(self, context): 156 | return ( 157 | (q[0], q[0], q[0]) 158 | for q in csv.reader(config.FDS_QUANTITIES.splitlines()) 159 | if "D" in q[2] 160 | ) 161 | 162 | 163 | class OBJECT_OT_bf_choose_devc_quantity(Operator): 164 | bl_label = "Choose QUANTITY for DEVC" 165 | bl_idname = "object.bf_choose_devc_quantity" 166 | bl_description = "Choose QUANTITY parameter for DEVC namelist" 167 | bl_property = "bf_quantity" 168 | 169 | bf_quantity: EnumProperty( 170 | name="QUANTITY", 171 | description="QUANTITY parameter for DEVC namelist", 172 | items=_get_devc_quantity_items, 173 | ) 174 | 175 | @classmethod 176 | def poll(cls, context): 177 | return context.object 178 | 179 | def execute(self, context): 180 | if self.bf_quantity: 181 | context.object.bf_quantity = self.bf_quantity 182 | self.report({"INFO"}, "QUANTITY set") 183 | return {"FINISHED"} 184 | else: 185 | self.report({"WARNING"}, "QUANTITY not set") 186 | return {"CANCELLED"} 187 | 188 | def invoke(self, context, event): 189 | context.window_manager.invoke_search_popup(self) 190 | return {"FINISHED"} 191 | 192 | 193 | # Register/unregister 194 | 195 | bl_classes = [ 196 | MATERIAL_OT_bf_choose_matl_id, 197 | OBJECT_OT_bf_choose_devc_prop_id, 198 | OBJECT_OT_bf_choose_devc_ctrl_id, 199 | OBJECT_OT_bf_choose_devc_quantity, 200 | ] 201 | 202 | 203 | def register(): 204 | from bpy.utils import register_class 205 | 206 | for c in bl_classes: 207 | register_class(c) 208 | 209 | 210 | def unregister(): 211 | from bpy.utils import unregister_class 212 | 213 | for c in reversed(bl_classes): 214 | unregister_class(c) 215 | -------------------------------------------------------------------------------- /bl/operators/clean_ma_slots.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators for the MESH namelist. 5 | """ 6 | 7 | 8 | import logging, bpy 9 | from bpy.types import Operator 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class OBJECT_OT_bf_clean_ma_slots(Operator): 15 | """! 16 | Clean Material slots of Object, keep only active Object. 17 | """ 18 | 19 | bl_label = "Clean Material Slots" 20 | bl_idname = "object.bf_clean_ma_slots" 21 | bl_description = "Clean Material slots from unused or empty instances" 22 | bl_options = {"REGISTER", "UNDO"} 23 | 24 | @classmethod 25 | def poll(cls, context): 26 | ob = context.object 27 | return ob and len(ob.material_slots) > 1 28 | 29 | def execute(self, context): 30 | ob = context.object 31 | orig_active_ma_index = ob.active_material_index 32 | for i in reversed(range(len(ob.material_slots))): 33 | ms = ob.material_slots[i] 34 | ob.active_material_index = i 35 | if ms.material and i == orig_active_ma_index: 36 | continue 37 | bpy.ops.object.material_slot_remove() 38 | ob.active_material_index = 0 39 | self.report({"INFO"}, "Material slots cleaned") 40 | return {"FINISHED"} 41 | 42 | 43 | bl_classes = [ 44 | OBJECT_OT_bf_clean_ma_slots, 45 | ] 46 | 47 | 48 | def register(): 49 | from bpy.utils import register_class 50 | 51 | for c in bl_classes: 52 | register_class(c) 53 | 54 | 55 | def unregister(): 56 | from bpy.utils import unregister_class 57 | 58 | for c in reversed(bl_classes): 59 | unregister_class(c) 60 | -------------------------------------------------------------------------------- /bl/operators/copy_params.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to copy FDS parameter values between entities. 5 | """ 6 | 7 | import logging, bpy 8 | from bpy.types import Operator, Scene 9 | from bpy.props import StringProperty 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def _bf_props_copy(context, source_element, dest_elements): 15 | """! 16 | Copy all FDS parameters from source_element to dest_elements. 17 | """ 18 | # Get bf_namelists 19 | if isinstance(source_element, Scene): 20 | bf_namelists = source_element.bf_namelists 21 | else: 22 | bf_namelists = tuple((source_element.bf_namelist,)) 23 | for dest_element in dest_elements: 24 | dest_element.bf_namelist_cls = source_element.bf_namelist_cls 25 | # Copy them 26 | for bf_namelist in bf_namelists: 27 | for dest_element in dest_elements: 28 | bf_namelist.copy_to(context=context, dest_element=dest_element) 29 | 30 | 31 | class SCENE_OT_bf_copy_props_to_sc(Operator): 32 | """! 33 | Copy current FDS case parameters to another Scene. 34 | """ 35 | 36 | bl_label = "Copy To" 37 | bl_idname = "scene.bf_props_to_sc" 38 | bl_description = "Copy case parameters to another Scene" 39 | bl_options = {"REGISTER", "UNDO"} 40 | 41 | bf_dest_element: StringProperty(name="Destination") 42 | 43 | def draw(self, context): 44 | self.layout.prop_search(self, "bf_dest_element", bpy.data, "scenes") 45 | 46 | def invoke(self, context, event): 47 | wm = context.window_manager 48 | return wm.invoke_props_dialog(self) 49 | 50 | def execute(self, context): 51 | # Get source and dest element 52 | source_element = context.scene 53 | dest_element = bpy.data.scenes.get(self.bf_dest_element, None) 54 | if source_element == dest_element: 55 | self.report({"WARNING"}, "Destination same as source") 56 | return {"CANCELLED"} 57 | if not dest_element: 58 | self.report({"ERROR"}, "No destination") 59 | return {"CANCELLED"} 60 | if not source_element: 61 | self.report({"ERROR"}, "No source") 62 | return {"CANCELLED"} 63 | 64 | # Copy 65 | _bf_props_copy(context, source_element, (dest_element,)) 66 | self.report({"INFO"}, f"Parameters copied") 67 | return {"FINISHED"} 68 | 69 | 70 | class OBJECT_OT_bf_copy_FDS_properties_to_sel_obs(Operator): 71 | """! 72 | Copy current FDS parameters to selected Objects. 73 | """ 74 | 75 | bl_label = "Copy To" 76 | bl_idname = "object.bf_props_to_sel_obs" 77 | bl_description = "Copy FDS parameters to selected Objects" 78 | bl_options = {"REGISTER", "UNDO"} 79 | 80 | @classmethod 81 | def poll(cls, context): 82 | return context.object 83 | 84 | def invoke(self, context, event): 85 | # Ask for confirmation 86 | wm = context.window_manager 87 | return wm.invoke_confirm(self, event) 88 | 89 | def execute(self, context): 90 | bpy.ops.object.mode_set(mode="OBJECT") 91 | 92 | # Get source and destination objects 93 | source_element = context.object 94 | dest_elements = set( 95 | ob 96 | for ob in context.selected_objects 97 | if ob.type == "MESH" and ob != source_element 98 | ) 99 | if not dest_elements: 100 | self.report({"ERROR"}, "No destination, select Objects") 101 | return {"CANCELLED"} 102 | if not source_element: 103 | self.report({"ERROR"}, "No source") 104 | return {"CANCELLED"} 105 | 106 | # Copy 107 | _bf_props_copy(context, source_element, dest_elements) 108 | self.report( 109 | {"INFO"}, 110 | f"Parameters copied to {len(dest_elements)} selected Object(s)", 111 | ) 112 | return {"FINISHED"} 113 | 114 | 115 | class MATERIAL_OT_bf_copy_props_to_ma(Operator): 116 | """! 117 | Copy current FDS parameters to another Material. 118 | """ 119 | 120 | bl_label = "Copy To" 121 | bl_idname = "material.bf_props_to_ma" 122 | bl_description = "Copy parameters to another Material" 123 | bl_options = {"REGISTER", "UNDO"} 124 | 125 | bf_dest_element: StringProperty(name="Destination") 126 | 127 | @classmethod 128 | def poll(cls, context): 129 | ob = context.object 130 | return ob and ob.active_material 131 | 132 | def draw(self, context): 133 | self.layout.prop_search(self, "bf_dest_element", bpy.data, "materials") 134 | 135 | def invoke(self, context, event): 136 | wm = context.window_manager 137 | return wm.invoke_props_dialog(self) 138 | 139 | def execute(self, context): 140 | # Get source and dest element 141 | source_element = context.object.active_material 142 | dest_element = bpy.data.materials.get(self.bf_dest_element, None) 143 | if source_element == dest_element: 144 | self.report({"WARNING"}, "Destination same as source") 145 | return {"CANCELLED"} 146 | if not dest_element: 147 | self.report({"ERROR"}, "No destination") 148 | return {"CANCELLED"} 149 | if not source_element: 150 | self.report({"ERROR"}, "No source") 151 | return {"CANCELLED"} 152 | 153 | # Copy 154 | _bf_props_copy(context, source_element, (dest_element,)) 155 | self.report({"INFO"}, "Parameters copied") 156 | return {"FINISHED"} 157 | 158 | 159 | bl_classes = [ 160 | SCENE_OT_bf_copy_props_to_sc, 161 | OBJECT_OT_bf_copy_FDS_properties_to_sel_obs, 162 | MATERIAL_OT_bf_copy_props_to_ma, 163 | ] 164 | 165 | 166 | def register(): 167 | from bpy.utils import register_class 168 | 169 | for c in bl_classes: 170 | register_class(c) 171 | 172 | 173 | def unregister(): 174 | from bpy.utils import unregister_class 175 | 176 | for c in reversed(bl_classes): 177 | unregister_class(c) 178 | -------------------------------------------------------------------------------- /bl/operators/gis.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators for geographic operations. 5 | """ 6 | 7 | import logging, bpy 8 | from bpy.types import Operator 9 | from bpy.props import BoolProperty, FloatProperty 10 | from ...config import LATLON_P 11 | from ... import utils 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class _bf_set_geoloc: 17 | """! 18 | Set geographic location (WGS84). 19 | """ 20 | 21 | bl_label = "Set Geolocation" 22 | # bl_idname = "scene.bf_set_geoloc" 23 | bl_description = "Set geographic location (WGS84)" 24 | bl_options = {"REGISTER", "UNDO"} 25 | 26 | show: BoolProperty(name="Show Geolocation", default=False) 27 | 28 | bf_lon: FloatProperty( 29 | name="Longitude", 30 | description="Longitude (WGS84, EPSG:4326) in decimal degrees", 31 | min=-180, 32 | max=+180, 33 | precision=LATLON_P, 34 | ) 35 | 36 | bf_lat: FloatProperty( 37 | name="Latitude", 38 | description="Latitude (WGS84, EPSG:4326) in decimal degrees", 39 | min=-80, 40 | max=+84, 41 | precision=LATLON_P, 42 | ) 43 | 44 | def draw(self, context): 45 | """! 46 | Draw function for the operator. 47 | @param context: the Blender context. 48 | """ 49 | col = self.layout.column(align=True) 50 | col.label(text="Error on horizontal geoposition < 1 m") 51 | col.prop(self, "bf_lon", text="Longitude") 52 | col.prop(self, "bf_lat", text="Latitude") 53 | 54 | def _get_loc(self, context): 55 | """! 56 | Placeholder method to get the XYZ location 57 | @param context: the Blender context. 58 | @return xyz location 59 | """ 60 | return 0.0, 0.0, 0.0 61 | 62 | def _set_loc(self, context, xyz): 63 | """! 64 | Placeholder method to set the XYZ location 65 | @param context: the Blender context. 66 | @param xyz location 67 | """ 68 | pass 69 | 70 | def execute(self, context): 71 | # Get loc, use only unmodified z 72 | _, _, z = self._get_loc(context) 73 | sc = context.scene 74 | utm = utils.gis.LonLat( 75 | lon=self.bf_lon, 76 | lat=self.bf_lat, 77 | ).to_UTM(force_zn=sc.bf_origin_utm_zn, force_ne=sc.bf_origin_utm_ne) 78 | # Compute loc relative to scene origin 79 | scale_length = sc.unit_settings.scale_length 80 | x = (utm.easting - sc.bf_origin_utm_easting) / scale_length 81 | y = (utm.northing - sc.bf_origin_utm_northing) / scale_length 82 | self._set_loc(context, (x, y, z)) 83 | self.report({"INFO"}, "Geolocation set") 84 | return {"FINISHED"} 85 | 86 | def invoke(self, context, event): 87 | sc = context.scene 88 | # Check origin geolocation 89 | if not sc.bf_misc_export or not sc.bf_origin_export: 90 | self.report({"ERROR"}, "Undefined location, set origin geolocation in MISC") 91 | return {"FINISHED"} 92 | # Get loc, convert to set default 93 | x, y, _ = self._get_loc(context) 94 | scale_length = sc.unit_settings.scale_length 95 | utm = utils.gis.UTM( 96 | zn=sc.bf_origin_utm_zn, 97 | ne=sc.bf_origin_utm_ne, 98 | easting=sc.bf_origin_utm_easting + x * scale_length, 99 | northing=sc.bf_origin_utm_northing + y * scale_length, 100 | ) 101 | lonlat = utm.to_LonLat() 102 | # Show 103 | if self.show: 104 | url = lonlat.to_url() 105 | bpy.ops.wm.url_open(url=url) 106 | self.report({"INFO"}, "Geolocation shown") 107 | return {"FINISHED"} 108 | # Set defaults 109 | self.bf_lon = lonlat.lon 110 | self.bf_lat = lonlat.lat 111 | # Call dialog 112 | wm = context.window_manager 113 | return wm.invoke_props_dialog(self) 114 | 115 | 116 | class SCENE_OT_bf_set_cursor_geoloc(Operator, _bf_set_geoloc): 117 | """! 118 | Set geographic location (WGS84). 119 | """ 120 | 121 | bl_idname = "scene.bf_set_cursor_geoloc" 122 | 123 | @classmethod 124 | def poll(cls, context): 125 | return context.scene.cursor 126 | 127 | def _get_loc(self, context): 128 | return context.scene.cursor.location 129 | 130 | def _set_loc(self, context, xyz): 131 | context.scene.cursor.location = xyz 132 | 133 | 134 | class SCENE_OT_bf_set_ob_geoloc(Operator, _bf_set_geoloc): 135 | """! 136 | Set geographic location (WGS84). 137 | """ 138 | 139 | bl_idname = "scene.bf_set_ob_geoloc" 140 | 141 | @classmethod 142 | def poll(cls, context): 143 | return context.object 144 | 145 | def _get_loc(self, context): 146 | return context.object.location 147 | 148 | def _set_loc(self, context, xyz): 149 | context.object.location = xyz 150 | 151 | 152 | bl_classes = [ 153 | SCENE_OT_bf_set_cursor_geoloc, 154 | SCENE_OT_bf_set_ob_geoloc, 155 | ] 156 | 157 | 158 | def register(): 159 | from bpy.utils import register_class 160 | 161 | for c in bl_classes: 162 | register_class(c) 163 | 164 | 165 | def unregister(): 166 | from bpy.utils import unregister_class 167 | 168 | for c in reversed(bl_classes): 169 | unregister_class(c) 170 | -------------------------------------------------------------------------------- /bl/operators/load_bf_settings.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to load default settings. 5 | """ 6 | 7 | import os, sys, bpy, logging 8 | from bpy.types import Operator 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class WM_OT_bf_load_blenderfds_settings(Operator): 14 | """! 15 | Load default BlenderFDS settings, deleting current data. 16 | """ 17 | 18 | bl_label = "Load Default BlenderFDS Settings" 19 | bl_idname = "wm.bf_load_blenderfds_settings" 20 | bl_description = "Load default BlenderFDS settings, deleting current data!" 21 | 22 | def invoke(self, context, event): 23 | # Ask for confirmation 24 | wm = context.window_manager 25 | return wm.invoke_confirm(self, event) 26 | 27 | def execute(self, context): 28 | # Set default startup.blend 29 | filepath = os.path.join( 30 | os.path.dirname(sys.modules[__package__].__file__), "../../startup.blend" 31 | ) 32 | bpy.ops.wm.open_mainfile(filepath=filepath, load_ui=True, use_scripts=True) 33 | bpy.ops.wm.save_homefile() 34 | # Load default commands 35 | bpy.ops.wm.bf_restore_default_commands() 36 | # Save user preferences 37 | bpy.ops.wm.save_userpref() 38 | # Open new file (unlink startup) 39 | bpy.ops.wm.read_homefile() 40 | # Report 41 | self.report({"INFO"}, "Default settings loaded") 42 | return {"FINISHED"} 43 | 44 | 45 | bl_classes = [ 46 | WM_OT_bf_load_blenderfds_settings, 47 | ] 48 | 49 | 50 | def register(): 51 | from bpy.utils import register_class 52 | 53 | for c in bl_classes: 54 | register_class(c) 55 | 56 | 57 | def unregister(): 58 | from bpy.utils import unregister_class 59 | 60 | for c in reversed(bl_classes): 61 | unregister_class(c) 62 | -------------------------------------------------------------------------------- /bl/operators/scene_export.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to export an FDS case. 5 | """ 6 | 7 | import bpy 8 | from bpy.types import Operator 9 | from bpy.props import StringProperty 10 | from bpy_extras.io_utils import ExportHelper 11 | from ... import utils 12 | 13 | 14 | class ExportSceneToFDS(Operator, ExportHelper): 15 | """! 16 | Export current Blender Scene to an FDS case file. 17 | """ 18 | 19 | bl_idname = "export_scene.fds" 20 | bl_label = "Export to FDS" 21 | bl_description = "Export current Blender Scene to an FDS case file" 22 | bl_options = {"UNDO"} 23 | 24 | filename_ext = ".fds" 25 | filter_glob: StringProperty(default="*.fds", options={"HIDDEN"}) 26 | 27 | @classmethod 28 | def poll(cls, context): 29 | return context.scene 30 | 31 | def invoke(self, context, event): 32 | # Check if saved 33 | if not bpy.data.is_saved: 34 | self.report({"ERROR"}, "Save the Blender file first!") 35 | return {"CANCELLED"} 36 | 37 | # Set best filepath as default, empty path is /home/user 38 | sc = context.scene 39 | self.filepath = utils.io.append_filename( 40 | path=sc.bf_config_directory, name=sc.name, extension=".fds" 41 | ) 42 | return super().invoke(context, event) 43 | 44 | def execute(self, context): 45 | w = context.window_manager.windows[0] 46 | w.cursor_modal_set("WAIT") 47 | 48 | # If filepath was relative, keep it relative 49 | sc = context.scene 50 | if not utils.io.is_abs(sc.bf_config_directory): 51 | self.filepath = bpy.path.relpath(self.filepath) 52 | sc.bf_config_directory, sc.name = utils.io.extract_path_name(self.filepath) 53 | 54 | # Export 55 | try: 56 | sc.to_fds(context=context, full=True, save=True) 57 | except Exception as err: 58 | w.cursor_modal_restore() 59 | self.report({"ERROR"}, str(err)) 60 | return {"CANCELLED"} 61 | 62 | # Close 63 | w.cursor_modal_restore() 64 | self.report({"INFO"}, "FDS case exported") 65 | return {"FINISHED"} 66 | 67 | 68 | # Register/unregister 69 | 70 | bl_classes = (ExportSceneToFDS,) 71 | 72 | 73 | def register(): 74 | from bpy.utils import register_class 75 | 76 | for c in bl_classes: 77 | register_class(c) 78 | 79 | 80 | def unregister(): 81 | from bpy.utils import unregister_class 82 | 83 | for c in reversed(bl_classes): 84 | unregister_class(c) 85 | -------------------------------------------------------------------------------- /bl/operators/scene_import.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operator to import an FDS case. 5 | """ 6 | 7 | import bpy 8 | from bpy.types import Operator 9 | from bpy.props import StringProperty 10 | from bpy_extras.io_utils import ImportHelper 11 | from ... import utils 12 | from ...types import BFException 13 | 14 | 15 | def _import_fds_case_to_scene(context, filepath, to_new_scene=True): 16 | if to_new_scene: 17 | sc = bpy.data.scenes.new("New case") 18 | else: 19 | sc = context.scene 20 | fds_namelist_qty = sc.from_fds( 21 | context, 22 | filepath=filepath, 23 | ) 24 | return fds_namelist_qty # Number of imported namelists 25 | 26 | 27 | class ImportFDSToScene(Operator, ImportHelper): 28 | """! 29 | Import an FDS case file to a new Blender Scene. 30 | """ 31 | 32 | bl_idname = "import_to_scene.fds" 33 | bl_label = "Import from FDS" 34 | bl_description = "Import an FDS case file to a new Blender Scene" 35 | bl_options = {"UNDO"} 36 | 37 | filename_ext = ".fds" 38 | filter_glob: StringProperty(default="*.fds", options={"HIDDEN"}) 39 | 40 | @classmethod 41 | def poll(cls, context): 42 | return context.scene 43 | 44 | def execute(self, context): 45 | w = context.window_manager.windows[0] 46 | w.cursor_modal_set("WAIT") 47 | 48 | # Import 49 | try: 50 | _import_fds_case_to_scene(context, self.filepath, to_new_scene=True) 51 | except BFException as err: 52 | w.cursor_modal_restore() 53 | self.report({"ERROR"}, str(err)) 54 | return {"CANCELLED"} 55 | 56 | # Close 57 | utils.ui.view_all(context=context) 58 | w.cursor_modal_restore() 59 | self.report({"INFO"}, "FDS case imported") 60 | return {"FINISHED"} 61 | 62 | 63 | class ImportFDSToCurrentScene(Operator, ImportHelper): 64 | """! 65 | Import FDS case file to current Blender Scene. 66 | """ 67 | 68 | bl_idname = "import_to_current_scene.fds" 69 | bl_label = "Import Snippet" 70 | bl_description = "Import FDS code snippet to current Blender Scene" 71 | bl_options = {"UNDO"} 72 | 73 | filename_ext = ".fds" 74 | filter_glob: StringProperty(default="*.fds", options={"HIDDEN"}) 75 | 76 | @classmethod 77 | def poll(cls, context): 78 | return context.scene 79 | 80 | def execute(self, context): 81 | w = context.window_manager.windows[0] 82 | w.cursor_modal_set("WAIT") 83 | 84 | # Import 85 | try: 86 | fds_namelist_qty = _import_fds_case_to_scene( 87 | context, self.filepath, to_new_scene=False 88 | ) 89 | except BFException as err: 90 | w.cursor_modal_restore() 91 | self.report({"ERROR"}, str(err)) 92 | return {"CANCELLED"} 93 | 94 | # Close 95 | utils.ui.view_all(context=context) 96 | w.cursor_modal_restore() 97 | self.report( 98 | {"INFO"}, 99 | fds_namelist_qty == 1 100 | and "1 namelist imported" 101 | or f"{fds_namelist_qty} namelists imported", 102 | ) 103 | return {"FINISHED"} 104 | 105 | 106 | bl_classes = [ 107 | ImportFDSToScene, 108 | ImportFDSToCurrentScene, 109 | ] 110 | 111 | 112 | def register(): 113 | from bpy.utils import register_class 114 | 115 | for c in bl_classes: 116 | register_class(c) 117 | 118 | 119 | def unregister(): 120 | from bpy.utils import unregister_class 121 | 122 | for c in reversed(bl_classes): 123 | unregister_class(c) 124 | -------------------------------------------------------------------------------- /bl/operators/show_fds_code.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to show generated FDS code. 5 | """ 6 | 7 | import logging 8 | from bpy.types import Operator 9 | from bpy.props import StringProperty, EnumProperty 10 | from ...types import BFException 11 | from ... import utils 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class WM_OT_bf_dialog(Operator): 17 | """! 18 | BlenderFDS Dialog. 19 | """ 20 | 21 | bl_label = "BlenderFDS" 22 | bl_idname = "wm.bf_dialog" 23 | bl_description = "BlenderFDS Dialog" 24 | 25 | type: EnumProperty( 26 | name="Type", 27 | items=(("INFO", "Information", "Information"), ("ERROR", "Error", "Error")), 28 | description="Dialog type", 29 | default="INFO", 30 | ) 31 | 32 | msg: StringProperty( 33 | name="Message", description="Dialog message", default="No message" 34 | ) 35 | 36 | description: StringProperty(name="Description", description="Dialog description") 37 | 38 | def execute(self, context): 39 | return {"FINISHED"} 40 | 41 | def invoke(self, context, event): 42 | wm = context.window_manager 43 | return wm.invoke_props_dialog(self) 44 | 45 | def draw(self, context): 46 | layout = self.layout 47 | col = layout.column() 48 | col.label(text=self.msg, icon=self.type) 49 | if self.description: 50 | col.separator() 51 | descriptions = self.description.splitlines() 52 | for description in descriptions: 53 | row = col.row() 54 | row.label(text=description) 55 | 56 | 57 | class _show_fds_code: 58 | """! 59 | Helper for showing fds code operators 60 | """ 61 | 62 | def draw(self, context): 63 | if self.lines: 64 | lines = self.lines.split("\n") 65 | else: 66 | lines = ("No FDS code is exported",) 67 | if len(lines) > 20: 68 | lines = lines[:15] + [" ···"] + lines[-4:] 69 | layout = self.layout 70 | for line in lines: 71 | layout.label(text=line) 72 | 73 | def execute(self, context): 74 | self.report({"INFO"}, "FDS code shown") 75 | return {"FINISHED"} 76 | 77 | def _get_lines(self, context): 78 | """! 79 | Placeholder method to get text lines. 80 | """ 81 | return str() 82 | 83 | def invoke(self, context, event): 84 | w = context.window_manager.windows[0] 85 | w.cursor_modal_set("WAIT") 86 | try: 87 | self.lines = self._get_lines(context) # get FDS code 88 | except BFException as err: 89 | self.report({"ERROR"}, str(err)) 90 | return {"CANCELLED"} 91 | else: 92 | wm = context.window_manager 93 | return wm.invoke_props_dialog(self, width=600) 94 | finally: 95 | w.cursor_modal_restore() 96 | 97 | 98 | class OBJECT_OT_bf_show_fds_code(_show_fds_code, Operator): 99 | """! 100 | Show FDS code exported from current Object. 101 | """ 102 | 103 | bl_label = "Show FDS Code" 104 | bl_idname = "object.bf_show_fds_code" 105 | bl_description = "Show FDS code exported from current Object" 106 | 107 | @classmethod 108 | def poll(cls, context): 109 | return context.object 110 | 111 | def _get_lines(self, context): 112 | return context.object.to_fds_list(context).to_string() 113 | 114 | 115 | class COLLECTION_OT_bf_show_fds_code(_show_fds_code, Operator): 116 | """! 117 | Show FDS code exported from current Object. 118 | """ 119 | 120 | bl_label = "Show FDS Code" 121 | bl_idname = "collection.bf_show_fds_code" 122 | bl_description = "Show FDS code exported from current Collection" 123 | 124 | @classmethod 125 | def poll(cls, context): 126 | return context.collection 127 | 128 | def _get_lines(self, context): 129 | return context.collection.to_fds_list(context).to_string() 130 | 131 | 132 | class MATERIAL_OT_bf_show_fds_code(_show_fds_code, Operator): 133 | """! 134 | Show FDS code exported from current Material. 135 | """ 136 | 137 | bl_label = "Show FDS Code" 138 | bl_idname = "material.bf_show_fds_code" 139 | bl_description = "Show FDS code exported from current Material" 140 | 141 | @classmethod 142 | def poll(cls, context): 143 | return context.object and context.object.active_material 144 | 145 | def _get_lines(self, context): 146 | return context.object.active_material.to_fds_list(context).to_string() 147 | 148 | 149 | class SCENE_OT_bf_show_fds_code(_show_fds_code, Operator): 150 | """! 151 | Show FDS code exported from Scene. 152 | """ 153 | 154 | bl_label = "Show FDS Code" 155 | bl_idname = "scene.bf_show_fds_code" 156 | bl_description = "Show FDS code exported from Scene" 157 | 158 | @classmethod 159 | def poll(cls, context): 160 | return context.scene 161 | 162 | def _get_lines(self, context): 163 | return context.scene.to_fds_list(context).to_string() 164 | 165 | 166 | bl_classes = [ 167 | WM_OT_bf_dialog, 168 | OBJECT_OT_bf_show_fds_code, 169 | COLLECTION_OT_bf_show_fds_code, 170 | MATERIAL_OT_bf_show_fds_code, 171 | SCENE_OT_bf_show_fds_code, 172 | ] 173 | 174 | 175 | def register(): 176 | from bpy.utils import register_class 177 | 178 | for c in bl_classes: 179 | register_class(c) 180 | 181 | 182 | def unregister(): 183 | from bpy.utils import unregister_class 184 | 185 | for c in reversed(bl_classes): 186 | unregister_class(c) 187 | -------------------------------------------------------------------------------- /bl/operators/show_fds_geometry.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to show generated FDS geometry. 5 | """ 6 | 7 | 8 | import logging 9 | from bpy.types import Operator 10 | from bpy.props import BoolProperty 11 | from ...types import BFException 12 | from ... import utils, lang 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class OBJECT_OT_bf_show_fds_geometry(Operator): 18 | """! 19 | Show geometry of Object as exported to FDS. 20 | """ 21 | 22 | bl_label = "Show FDS Geometry" 23 | bl_idname = "object.bf_show_fds_geometry" 24 | bl_description = "Show/Hide geometry as exported to FDS" 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | ob = context.object # object or active_object? always object 29 | return ob and not ob.bf_is_tmp and not ob.bf_has_tmp 30 | 31 | def execute(self, context): 32 | # Init 33 | w = context.window_manager.windows[0] 34 | w.cursor_modal_set("WAIT") 35 | sc = context.scene 36 | ob = context.object 37 | 38 | # Export and import back as temporary geometry 39 | try: 40 | f90_namelists = ob.to_fds_list(context).to_string() 41 | sc.from_fds( 42 | context=context, 43 | f90_namelists=f90_namelists, 44 | set_tmp=True, 45 | ) 46 | except BFException as err: 47 | utils.geometry.rm_tmp_objects() 48 | self.report({"ERROR"}, str(err)) 49 | return {"CANCELLED"} 50 | else: 51 | utils.geometry.set_has_tmp(context=context, ob=ob) 52 | self.report({"INFO"}, "FDS geometry shown") 53 | return {"FINISHED"} 54 | finally: 55 | w.cursor_modal_restore() 56 | 57 | 58 | class SCENE_OT_bf_hide_tmp_geometry(Operator): 59 | """! 60 | Hide all temporary geometry. 61 | """ 62 | 63 | bl_label = "Hide FDS Geometry" 64 | bl_idname = "scene.bf_hide_fds_geometry" 65 | bl_description = "Hide all generated temporary geometry" 66 | 67 | def execute(self, context): 68 | w = context.window_manager.windows[0] 69 | w.cursor_modal_set("WAIT") 70 | 71 | ob = context.object 72 | utils.geometry.rm_tmp_objects() 73 | try: 74 | context.view_layer.objects.active = ob 75 | except ReferenceError: 76 | try: 77 | context.view_layer.objects.active = context.selected_objects[0] 78 | except IndexError: 79 | pass 80 | 81 | w.cursor_modal_restore() 82 | self.report({"INFO"}, "Temporary geometry hidden") 83 | return {"FINISHED"} 84 | 85 | 86 | bl_classes = [ 87 | OBJECT_OT_bf_show_fds_geometry, 88 | SCENE_OT_bf_hide_tmp_geometry, 89 | ] 90 | 91 | 92 | def register(): 93 | from bpy.utils import register_class 94 | 95 | for c in bl_classes: 96 | register_class(c) 97 | 98 | 99 | def unregister(): 100 | from bpy.utils import unregister_class 101 | 102 | for c in reversed(bl_classes): 103 | unregister_class(c) 104 | -------------------------------------------------------------------------------- /bl/operators/show_ui.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to show parts of Blender UI. 5 | """ 6 | 7 | import bpy 8 | from bpy.types import Operator 9 | from ... import utils 10 | 11 | 12 | class SCENE_OT_bf_show_material_panel(Operator): 13 | """! 14 | Show the Material Panel. 15 | """ 16 | 17 | bl_label = "Show the Material Panel" 18 | bl_idname = "scene.bf_show_material_panel" 19 | bl_description = "Show the Material Panel of the property editor" 20 | 21 | def execute(self, context): 22 | utils.ui.show_property_panel(context, space_context="MATERIAL") 23 | self.report({"INFO"}, f"See Blender Material Panel") 24 | return {"FINISHED"} 25 | 26 | 27 | class SCENE_OT_bf_show_text(Operator): 28 | """! 29 | Show free text in the editor. 30 | """ 31 | 32 | bl_label = "Show Free Text" 33 | bl_idname = "scene.bf_show_text" 34 | bl_description = "Show free text in the editor" 35 | 36 | def execute(self, context): 37 | context.scene.bf_config_text = utils.ui.show_bl_text( 38 | context=context, 39 | bl_text=context.scene.bf_config_text, 40 | name="Text", 41 | ) 42 | self.report( 43 | {"INFO"}, f"See Blender text editor: <{context.scene.bf_config_text.name}>" 44 | ) 45 | return {"FINISHED"} 46 | 47 | 48 | bl_classes = [ 49 | SCENE_OT_bf_show_text, 50 | SCENE_OT_bf_show_material_panel, 51 | ] 52 | 53 | 54 | def register(): 55 | from bpy.utils import register_class 56 | 57 | for c in bl_classes: 58 | register_class(c) 59 | 60 | 61 | def unregister(): 62 | from bpy.utils import unregister_class 63 | 64 | for c in reversed(bl_classes): 65 | unregister_class(c) 66 | -------------------------------------------------------------------------------- /bl/operators/update_addon.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, operators to update the addon. 5 | """ 6 | 7 | import logging 8 | from bpy.types import Operator 9 | from bpy.props import EnumProperty 10 | from ...utils import updater 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class WM_OT_bf_update_tags(Operator): 16 | bl_label = "Update Tags" 17 | bl_idname = "wm.bf_update_tags" 18 | bl_description = "Update tags" 19 | 20 | def execute(self, context): 21 | try: 22 | context.window_manager["tags"] = updater.get_tags() 23 | except Exception as err: 24 | self.report({"ERROR"}, str(err)) 25 | return {"CANCELLED"} 26 | else: 27 | self.report({"INFO"}, "Tags updated") 28 | return {"FINISHED"} 29 | 30 | 31 | def _get_target_items(self, context): 32 | tags = context.window_manager.get("tags", ()) 33 | return ((t["name"], t["name"], f"Install {t['name']} version") for t in tags) 34 | 35 | 36 | class WM_OT_bf_update_addon(Operator): 37 | bl_label = "Upgrade Addon" 38 | bl_idname = "wm.bf_update_addon" 39 | bl_description = "Upgrade, downgrade, or restore the addon from its repository" 40 | 41 | bf_target: EnumProperty( 42 | name="Version", 43 | description="Select the addon version to install", 44 | items=_get_target_items, 45 | ) 46 | 47 | def draw(self, context): 48 | row = self.layout.row(align=True) 49 | row.prop(self, "bf_target") 50 | row.operator("wm.bf_update_tags", text="", icon="FILE_REFRESH") 51 | 52 | def invoke(self, context, event): 53 | wm = context.window_manager 54 | return wm.invoke_props_dialog(self) 55 | 56 | def execute(self, context): 57 | 58 | # Get zipball_url 59 | tags = context.window_manager.get("tags", list()) 60 | url_file = None 61 | for t in tags: 62 | if t["name"] == self.bf_target: 63 | url_file = t["zipball_url"] 64 | break 65 | if not url_file: 66 | self.report({"ERROR"}, "Update cancelled, no zip file selected.") 67 | return {"CANCELLED"} 68 | 69 | # Install 70 | try: 71 | updater.install_addon(url_file=url_file) 72 | except Exception as err: 73 | self.report({"ERROR"}, f"Install failed: {err}") 74 | return {"CANCELLED"} 75 | self.report({"WARNING"}, f"Install completed, restart Blender!") 76 | context.window_manager["bf_restart_required"] = True 77 | return {"FINISHED"} 78 | 79 | 80 | bl_classes = [ 81 | WM_OT_bf_update_tags, 82 | WM_OT_bf_update_addon, 83 | ] 84 | 85 | 86 | def register(): 87 | from bpy.utils import register_class 88 | 89 | for c in bl_classes: 90 | register_class(c) 91 | 92 | 93 | def unregister(): 94 | from bpy.utils import unregister_class 95 | 96 | for c in reversed(bl_classes): 97 | unregister_class(c) 98 | -------------------------------------------------------------------------------- /bl/preferences.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, preferences panel. 5 | """ 6 | 7 | import bpy 8 | import logging 9 | 10 | from bpy.types import AddonPreferences 11 | from bpy.props import BoolProperty, StringProperty 12 | from . import ui 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # Get preference value like this: 17 | # prefs = context.preferences.addons[__package__.split(".")[0]].preferences 18 | # prefs.bf_pref_simplify_ui 19 | 20 | 21 | def update_bf_pref_simplify_ui(prefs, context): 22 | ui.toggle_simple_ui(prefs, context) 23 | 24 | 25 | class BFPreferences(AddonPreferences): 26 | """! 27 | BlenderFDS, preferences panel 28 | """ 29 | 30 | bl_idname = __package__.split(".")[0] 31 | 32 | bf_pref_simplify_ui: BoolProperty( 33 | name="Simplify Blender UI", 34 | description="Simplify Blender user interface", 35 | default=True, 36 | update=update_bf_pref_simplify_ui, 37 | ) 38 | 39 | bf_pref_fds_command: StringProperty( 40 | name="Run FDS", 41 | description="\n".join( 42 | ( 43 | "Run FDS command:", 44 | " <{n}> is replaced by the number of MPI processes,", 45 | " <{t}> by the number of threads,", 46 | " <{f}> by the fds case filepath (eg. /example/case.fds).", 47 | " <{i}> by the fds case inputfile (eg. case.fds).", 48 | " <{p}> by the fds case path (eg. /example/).", 49 | ) 50 | ), 51 | default="", 52 | ) 53 | 54 | bf_pref_smv_command: StringProperty( 55 | name="Open Smokeview", 56 | description="\n".join( 57 | ( 58 | "Open Smokeview command:", 59 | " <{f}> is replaced by the smv filepath (eg. /example/case.smv),", 60 | " <{p}> by the fds case path (eg. /example/).", 61 | ) 62 | ), 63 | default="", 64 | ) 65 | 66 | bf_pref_term_command: StringProperty( 67 | name="Open Terminal", 68 | description="\n".join( 69 | ( 70 | "Open terminal command:", 71 | " <{c}> is replaced by FDS or Smokeview command.", 72 | ) 73 | ), 74 | default="", 75 | ) 76 | 77 | def draw(self, context): 78 | """! 79 | Draw UI elements into the panel UI layout. 80 | @param context: the Blender context. 81 | @return Blender layout. 82 | """ 83 | paths = context.preferences.filepaths 84 | layout = self.layout 85 | 86 | row = layout.row() 87 | row.operator("wm.bf_load_blenderfds_settings") 88 | if context.window_manager.get("bf_restart_required"): 89 | alert_row = row.row() 90 | alert_row.alert = True 91 | alert_row.operator("wm.quit_blender", text="Restart Blender", icon="ERROR") 92 | else: 93 | row.operator("wm.bf_update_addon") 94 | 95 | col = layout.column() 96 | col.prop(self, "bf_pref_simplify_ui") 97 | col.prop(paths, "use_load_ui", text="Load UI setup when loading .blend files") 98 | col.prop( 99 | paths, 100 | "use_relative_paths", 101 | text="Default to relative paths in the file selector", 102 | ) 103 | 104 | box = layout.box() 105 | box.label(text="External Commands") 106 | 107 | row = box.row(align=True) 108 | row.prop(self, "bf_pref_fds_command", text="FDS") 109 | row.operator( 110 | "wm.bf_restore_default_commands", text="", icon="LOOP_BACK" 111 | ).bf_command = "FDS" 112 | 113 | row = box.row(align=True) 114 | row.prop(self, "bf_pref_smv_command", text="Smokeview") 115 | row.operator( 116 | "wm.bf_restore_default_commands", text="", icon="LOOP_BACK" 117 | ).bf_command = "Smokeview" 118 | 119 | row = box.row(align=True) 120 | row.prop(self, "bf_pref_term_command", text="Terminal") 121 | row.operator( 122 | "wm.bf_restore_default_commands", text="", icon="LOOP_BACK" 123 | ).bf_command = "Terminal" 124 | 125 | return layout 126 | 127 | 128 | def register(): 129 | log.info("Register preferences...") 130 | bpy.utils.register_class(BFPreferences) 131 | 132 | 133 | def unregister(): 134 | log.info("Unregister preferences...") 135 | bpy.utils.unregister_class(BFPreferences) 136 | -------------------------------------------------------------------------------- /bl/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, ui simplification. 5 | """ 6 | 7 | from . import simplify_ui 8 | from .simplify_ui import toggle_simple_ui 9 | 10 | ms_to_register = (simplify_ui,) 11 | 12 | 13 | def register(): 14 | for m in ms_to_register: 15 | m.register() 16 | 17 | 18 | def unregister(): 19 | for m in reversed(ms_to_register): 20 | m.unregister() 21 | -------------------------------------------------------------------------------- /bl/ui/bf_ui/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, classes for simplified ui. 5 | """ 6 | 7 | from . import properties, topbar, view3d 8 | 9 | # Register/Unregister 10 | 11 | ms_to_register = ( 12 | properties, 13 | topbar, 14 | view3d, 15 | ) 16 | 17 | 18 | def register(): 19 | for m in ms_to_register: 20 | m.register() 21 | 22 | 23 | def unregister(): 24 | for m in ms_to_register: 25 | m.unregister() 26 | -------------------------------------------------------------------------------- /bl/ui/bf_ui/properties.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from bpy.types import Panel, Scene, Object, Collection 4 | from bpy.utils import register_class, unregister_class 5 | 6 | 7 | class PROPERTIES_PT_navigation_bar(Panel): 8 | bl_space_type = "PROPERTIES" 9 | bl_region_type = "NAVIGATION_BAR" 10 | bl_label = "Navigation Bar" 11 | bl_options = {"HIDE_HEADER"} 12 | 13 | def draw(self, context): 14 | space = context.space_data 15 | pin_id = space.pin_id 16 | 17 | layout = self.layout 18 | layout.scale_x = 1.4 19 | layout.scale_y = 1.4 20 | 21 | # Check space.context for strange situations 22 | if space.context not in ( 23 | "OBJECT", 24 | "DATA", 25 | "MATERIAL", 26 | "SCENE", 27 | "COLLECTION", 28 | "MODIFIER", 29 | ): 30 | space.context = "SCENE" 31 | 32 | # Set UI 33 | if not pin_id: 34 | sc = context.scene 35 | ob = context.active_object 36 | co = context.collection 37 | 38 | col = layout.column(align=True) 39 | col.prop_enum(space, "context", "SCENE", text="", icon="SCENE_DATA") 40 | 41 | if co != sc.collection: 42 | col.prop_enum( 43 | space, "context", "COLLECTION", text="", icon="OUTLINER_COLLECTION" 44 | ) 45 | if ob: 46 | col = layout.column(align=True) 47 | col.prop_enum(space, "context", "OBJECT", text="", icon="OBJECT_DATA") 48 | if ob.type == "MESH": 49 | col.prop_enum( 50 | space, "context", "MODIFIER", text="", icon="MODIFIER_DATA" 51 | ) 52 | if ob.type == "MESH" or ob.type == "EMPTY": 53 | col.prop_enum(space, "context", "DATA", text="", icon="MESH_DATA") 54 | if ob.type == "MESH": 55 | col.prop_enum( 56 | space, "context", "MATERIAL", text="", icon="MATERIAL_DATA" 57 | ) 58 | 59 | else: 60 | if isinstance(pin_id, Scene): 61 | sc = pin_id 62 | col = layout.column(align=True) 63 | col.prop_enum(space, "context", "SCENE", text="", icon="SCENE_DATA") 64 | elif isinstance(pin_id, Collection): 65 | co = pin_id 66 | col = layout.column(align=True) 67 | col.prop_enum( 68 | space, "context", "COLLECTION", text="", icon="OUTLINER_COLLECTION" 69 | ) 70 | elif isinstance(pin_id, Object): 71 | ob = pin_id 72 | col = layout.column(align=True) 73 | col.prop_enum(space, "context", "OBJECT", text="", icon="OBJECT_DATA") 74 | if ob.type == "MESH": 75 | col.prop_enum( 76 | space, "context", "MODIFIER", text="", icon="MODIFIER_DATA" 77 | ) 78 | if ob.type == "MESH" or ob.type == "EMPTY": 79 | col.prop_enum(space, "context", "DATA", text="", icon="MESH_DATA") 80 | if ob.type == "MESH": 81 | col.prop_enum( 82 | space, "context", "MATERIAL", text="", icon="MATERIAL_DATA" 83 | ) 84 | 85 | 86 | # Register/Unregister 87 | 88 | bl_classes = [ 89 | PROPERTIES_PT_navigation_bar, 90 | ] 91 | 92 | 93 | def register(): 94 | from bpy.utils import register_class 95 | 96 | for c in bl_classes: 97 | register_class(c) 98 | 99 | 100 | def unregister(): 101 | from bpy.utils import unregister_class 102 | 103 | for c in reversed(bl_classes): 104 | unregister_class(c) 105 | -------------------------------------------------------------------------------- /bl/ui/bf_ui/topbar.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from bpy.types import Menu 4 | from bpy.utils import register_class, unregister_class 5 | 6 | 7 | class TOPBAR_MT_editor_menus(Menu): 8 | bl_idname = "TOPBAR_MT_editor_menus" 9 | bl_label = "" 10 | 11 | def draw(self, context): 12 | layout = self.layout 13 | 14 | # Allow calling this menu directly (this might not be a header area). 15 | if getattr(context.area, "show_menus", False): 16 | layout.menu("TOPBAR_MT_blender", text="", icon="BLENDER") 17 | else: 18 | layout.menu("TOPBAR_MT_blender", text="Blender") 19 | 20 | layout.menu("TOPBAR_MT_file") 21 | layout.menu("TOPBAR_MT_edit") 22 | 23 | layout.menu("TOPBAR_MT_window") 24 | layout.menu("TOPBAR_MT_help") 25 | 26 | 27 | # Register/Unregister 28 | 29 | bl_classes = [ 30 | TOPBAR_MT_editor_menus, 31 | ] 32 | 33 | 34 | def register(): 35 | from bpy.utils import register_class 36 | 37 | for c in bl_classes: 38 | register_class(c) 39 | 40 | 41 | def unregister(): 42 | from bpy.utils import unregister_class 43 | 44 | for c in reversed(bl_classes): 45 | unregister_class(c) 46 | -------------------------------------------------------------------------------- /bl/ui/bf_ui/view3d.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import bpy 4 | from bpy.types import Menu 5 | from bpy.app.translations import contexts as i18n_contexts 6 | from bpy.utils import register_class, unregister_class 7 | 8 | 9 | class VIEW3D_MT_view(Menu): 10 | bl_idname = "VIEW3D_MT_view" 11 | bl_label = "View" 12 | 13 | def draw(self, context): 14 | layout = self.layout 15 | view = context.space_data 16 | 17 | layout.prop(view, "show_region_toolbar") 18 | layout.prop(view, "show_region_ui") 19 | layout.prop(view, "show_region_tool_header") 20 | layout.prop(view, "show_region_hud") 21 | 22 | layout.separator() 23 | 24 | layout.operator( 25 | "view3d.view_selected", text="Frame Selected" 26 | ).use_all_regions = False 27 | if view.region_quadviews: 28 | layout.operator( 29 | "view3d.view_selected", text="Frame Selected (Quad View)" 30 | ).use_all_regions = True 31 | 32 | layout.operator("view3d.view_all").center = False 33 | layout.operator("view3d.view_persportho", text="Perspective/Orthographic") 34 | layout.menu("VIEW3D_MT_view_local") 35 | 36 | layout.separator() 37 | layout.menu("VIEW3D_MT_view_viewpoint") 38 | layout.menu("VIEW3D_MT_view_navigation") 39 | layout.menu("VIEW3D_MT_view_align") 40 | 41 | layout.separator() 42 | 43 | layout.menu("INFO_MT_area") 44 | 45 | 46 | class VIEW3D_MT_add(Menu): 47 | bl_label = "Add" 48 | bl_translation_context = i18n_contexts.operator_default 49 | 50 | def draw(self, context): 51 | layout = self.layout 52 | layout.operator_context = "EXEC_REGION_WIN" 53 | layout.menu("VIEW3D_MT_mesh_add", icon="OUTLINER_OB_MESH") 54 | layout.menu("VIEW3D_MT_curve_add", icon="OUTLINER_OB_CURVE") 55 | layout.menu("VIEW3D_MT_surface_add", icon="OUTLINER_OB_SURFACE") 56 | layout.operator_menu_enum( 57 | "object.gpencil_add", 58 | "type", 59 | text="Grease Pencil", 60 | icon="OUTLINER_OB_GREASEPENCIL", 61 | ) 62 | 63 | layout.separator() 64 | layout.operator_menu_enum( 65 | "object.empty_add", "type", text="Empty", icon="OUTLINER_OB_EMPTY" 66 | ) 67 | layout.menu("VIEW3D_MT_image_add", text="Image", icon="OUTLINER_OB_IMAGE") 68 | 69 | # FIXME Manage Collection instances 70 | # layout.separator() 71 | # has_collections = bool(bpy.data.collections) 72 | # col = layout.column() 73 | # col.enabled = has_collections 74 | 75 | # if not has_collections or len(bpy.data.collections) > 10: 76 | # col.operator_context = "INVOKE_REGION_WIN" 77 | # col.operator( 78 | # "object.collection_instance_add", 79 | # text="Collection Instance..." 80 | # if has_collections 81 | # else "No Collections to Instance", 82 | # icon="OUTLINER_OB_GROUP_INSTANCE", 83 | # ) 84 | # else: 85 | # col.operator_menu_enum( 86 | # "object.collection_instance_add", 87 | # "collection", 88 | # text="Collection Instance", 89 | # icon="OUTLINER_OB_GROUP_INSTANCE", 90 | # ) 91 | 92 | 93 | class VIEW3D_MT_object(Menu): 94 | bl_context = "objectmode" 95 | bl_label = "Object" 96 | 97 | def draw(self, _context): 98 | layout = self.layout 99 | 100 | layout.menu("VIEW3D_MT_transform_object") 101 | layout.operator_menu_enum( 102 | "object.origin_set", text="Set Origin", property="type" 103 | ) 104 | layout.menu("VIEW3D_MT_mirror") 105 | layout.menu("VIEW3D_MT_object_clear") 106 | layout.menu("VIEW3D_MT_object_apply") 107 | layout.menu("VIEW3D_MT_snap") 108 | 109 | layout.separator() 110 | 111 | layout.operator("object.duplicate_move") 112 | layout.operator("object.duplicate_move_linked") 113 | layout.operator("object.join") 114 | 115 | layout.separator() 116 | 117 | layout.operator("view3d.copybuffer", text="Copy Objects", icon="COPYDOWN") 118 | layout.operator("view3d.pastebuffer", text="Paste Objects", icon="PASTEDOWN") 119 | 120 | layout.separator() 121 | 122 | layout.menu("VIEW3D_MT_object_asset") 123 | # layout.menu("VIEW3D_MT_object_parent") 124 | layout.menu("VIEW3D_MT_object_collection") 125 | layout.menu("VIEW3D_MT_object_relations") 126 | # layout.menu("VIEW3D_MT_object_constraints") 127 | # layout.menu("VIEW3D_MT_object_track") 128 | layout.menu("VIEW3D_MT_make_links") 129 | 130 | layout.separator() 131 | 132 | layout.operator("object.shade_smooth") 133 | layout.operator("object.shade_flat") 134 | 135 | layout.separator() 136 | 137 | # layout.menu("VIEW3D_MT_object_animation") 138 | # layout.menu("VIEW3D_MT_object_rigid_body") 139 | 140 | # layout.separator() 141 | 142 | # layout.menu("VIEW3D_MT_object_quick_effects") 143 | 144 | # layout.separator() 145 | 146 | layout.menu("VIEW3D_MT_object_convert") 147 | 148 | layout.separator() 149 | 150 | layout.menu("VIEW3D_MT_object_showhide") 151 | layout.menu("VIEW3D_MT_object_cleanup") 152 | 153 | layout.separator() 154 | 155 | layout.operator_context = "EXEC_REGION_WIN" 156 | layout.operator("object.delete", text="Delete").use_global = False 157 | layout.operator("object.delete", text="Delete Global").use_global = True 158 | 159 | 160 | # Register/Unregister 161 | 162 | bl_classes = [VIEW3D_MT_view, VIEW3D_MT_add, VIEW3D_MT_object] 163 | 164 | 165 | def register(): 166 | from bpy.utils import register_class 167 | 168 | for c in bl_classes: 169 | register_class(c) 170 | 171 | 172 | def unregister(): 173 | from bpy.utils import unregister_class 174 | 175 | for c in reversed(bl_classes): 176 | unregister_class(c) 177 | -------------------------------------------------------------------------------- /bl/ui/simplify_ui.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, simplify Blender ui classes. 5 | """ 6 | 7 | import logging, bpy 8 | from bpy.utils import register_class, unregister_class 9 | from . import bf_ui 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | ## name of original Blender ui class to be checked for existance 14 | bl_check_cls_name = "WORKSPACE_PT_addons" 15 | 16 | ## names of original Blender ui classes to be unregistered 17 | bl_cls_names = list() 18 | 19 | # from: 3.0/scripts/startup/bl_ui/properties_workspace.py 20 | bl_cls_names.extend( 21 | ( 22 | # "WORKSPACE_PT_main", 23 | "WORKSPACE_PT_addons", 24 | "WORKSPACE_PT_custom_props", 25 | ) 26 | ) 27 | 28 | # from: 3.0/scripts/startup/bl_ui/properties_scene.py 29 | bl_cls_names.extend( 30 | ( 31 | # "SCENE_UL_keying_set_paths", 32 | "SCENE_PT_scene", 33 | # "SCENE_PT_unit", # for BlenderBIM addon compat 34 | "SCENE_PT_physics", 35 | "SCENE_PT_keying_sets", 36 | "SCENE_PT_keying_set_paths", 37 | "SCENE_PT_keyframing_settings", 38 | "SCENE_PT_audio", 39 | "SCENE_PT_rigid_body_world", 40 | "SCENE_PT_rigid_body_world_settings", 41 | "SCENE_PT_rigid_body_cache", 42 | "SCENE_PT_rigid_body_field_weights", 43 | "SCENE_PT_custom_props", 44 | ) 45 | ) 46 | 47 | # from: 3.0/scripts/startup/bl_ui/properties_collection.py 48 | bl_cls_names.extend( 49 | ( 50 | # "COLLECTION_PT_collection_flags", 51 | # "COLLECTION_PT_instancing", 52 | "COLLECTION_PT_lineart_collection", 53 | ) 54 | ) 55 | 56 | # from: 3.0/scripts/startup/bl_ui/properties_object.py 57 | bl_cls_names.extend( 58 | ( 59 | # "OBJECT_PT_context_object", 60 | # "OBJECT_PT_transform", # for BlenderBIM addon compat 61 | # "OBJECT_PT_delta_transform", 62 | # "OBJECT_PT_relations", 63 | # "COLLECTION_MT_context_menu", 64 | # "OBJECT_PT_collections", 65 | "OBJECT_PT_instancing", 66 | "OBJECT_PT_instancing_size", 67 | "OBJECT_PT_motion_paths", 68 | "OBJECT_PT_motion_paths_display", 69 | # "OBJECT_PT_display", 70 | # "OBJECT_PT_visibility", 71 | "OBJECT_PT_lineart", 72 | "OBJECT_PT_custom_props", 73 | ) 74 | ) 75 | 76 | # from: 3.0/scripts/startup/bl_ui/properties_data_mesh.py 77 | bl_cls_names.extend( 78 | ( 79 | # "MESH_MT_vertex_group_context_menu", 80 | # "MESH_MT_shape_key_context_menu", 81 | # "MESH_UL_vgroups", 82 | # "MESH_UL_fmaps", 83 | # "MESH_UL_shape_keys", 84 | # "MESH_UL_uvmaps", 85 | # "MESH_UL_vcols", 86 | # "MESH_UL_attributes", 87 | # "DATA_PT_context_mesh", 88 | "DATA_PT_vertex_groups", 89 | "DATA_PT_shape_keys", 90 | "DATA_PT_uv_texture", 91 | "DATA_PT_vertex_colors", 92 | "DATA_PT_sculpt_vertex_colors", 93 | "DATA_PT_face_maps", 94 | "DATA_PT_mesh_attributes", 95 | "DATA_PT_normals", 96 | "DATA_PT_texture_space", 97 | # "DATA_PT_remesh", 98 | "DATA_PT_customdata", 99 | "DATA_PT_custom_props_mesh", 100 | ) 101 | ) 102 | 103 | # from: 3.0/scripts/startup/bl_ui/properties_material.py 104 | bl_cls_names.extend( 105 | ( 106 | # "MATERIAL_MT_context_menu", 107 | # "MATERIAL_UL_matslots", 108 | # "MATERIAL_PT_preview", 109 | # "EEVEE_MATERIAL_PT_context_material", 110 | "EEVEE_MATERIAL_PT_surface", 111 | "EEVEE_MATERIAL_PT_volume", 112 | "EEVEE_MATERIAL_PT_settings", 113 | "MATERIAL_PT_lineart", 114 | "MATERIAL_PT_viewport", 115 | "EEVEE_MATERIAL_PT_viewport_settings", 116 | "MATERIAL_PT_custom_props", 117 | ) 118 | ) 119 | 120 | # Replaced classes 121 | # from: 3.0/scripts/startup/bl_ui/space_properties.py 122 | bl_cls_names.extend( 123 | ( 124 | "PROPERTIES_PT_navigation_bar", 125 | "TOPBAR_MT_editor_menus", 126 | "VIEW3D_MT_view", 127 | "VIEW3D_MT_add", 128 | "VIEW3D_MT_object", 129 | ) 130 | ) 131 | 132 | ## references to original Blender ui classes to unregister 133 | bl_cls_refs = list() 134 | 135 | 136 | def _load_bl_cls_refs(): 137 | log.debug(f"Load original Blender ui classes...") 138 | for n in bl_cls_names: 139 | module_name = f"bpy.types.{n}" 140 | # Check existance 141 | try: 142 | cls = eval(module_name) 143 | log.debug(f"Load: Found original ui class <{module_name}>...") 144 | except: 145 | log.debug(f"Load: Unknown original ui class <{module_name}>...") 146 | else: 147 | bl_cls_refs.append(cls) 148 | 149 | 150 | def _set_simple_ui(): 151 | log.debug("Set simple ui...") 152 | 153 | # Check already simple 154 | if not hasattr(bpy.types, bl_check_cls_name): 155 | log.debug("Already simple, do not touch.") 156 | return 157 | 158 | # Unregister original 159 | for cls in bl_cls_refs: 160 | try: 161 | unregister_class(cls) 162 | except: 163 | log.debug(f"Unregister: unknown original class <{cls}>") 164 | 165 | # Register new ui 166 | bf_ui.register() 167 | 168 | 169 | def _set_normal_ui(): 170 | log.debug("Set normal ui...") 171 | 172 | # Check already normal 173 | if hasattr(bpy.types, bl_check_cls_name): 174 | return 175 | 176 | # Unregister new ui 177 | bf_ui.unregister() 178 | 179 | # Register original 180 | for cls in bl_cls_refs: 181 | try: 182 | register_class(cls) 183 | except: 184 | log.debug(f"Register: unknown original class <{cls}>") 185 | 186 | 187 | def toggle_simple_ui(prefs=None, context=None, force_normal=False): 188 | log.info("Toggle simple ui...") 189 | 190 | # First start, load original references 191 | if not bl_cls_refs: 192 | _load_bl_cls_refs() 193 | 194 | if force_normal: 195 | _set_normal_ui() 196 | return 197 | 198 | if context is None: 199 | context = bpy.context 200 | if prefs is None: 201 | prefs = context.preferences.addons[__package__.split(".")[0]].preferences 202 | if prefs.bf_pref_simplify_ui: 203 | _set_simple_ui() 204 | else: 205 | _set_normal_ui() 206 | 207 | 208 | # Register/Unregister 209 | 210 | 211 | def register(): 212 | toggle_simple_ui() # at start, check 213 | 214 | 215 | def unregister(): 216 | toggle_simple_ui(force_normal=True) # at end, restore 217 | -------------------------------------------------------------------------------- /bl/ui_lists.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, Blender UI lists. 5 | """ 6 | 7 | from bpy.types import PropertyGroup, UIList 8 | from bpy.props import BoolProperty, StringProperty 9 | from bpy.utils import register_class, unregister_class 10 | import logging 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | # Prepare for "Other" parameters 15 | # PropertyGroup and UIList 16 | # The PG properties should always be: bf_export, name 17 | 18 | 19 | class WM_PG_bf_other(PropertyGroup): 20 | """! 21 | Blender PropertyGroup for items of 'other' FDS parameters. 22 | """ 23 | 24 | bf_export: BoolProperty(name="Export", default=True) 25 | name: StringProperty(name="Name") 26 | 27 | 28 | class WM_UL_bf_other_items(UIList): 29 | """! 30 | Blender UIList for items of 'other' FDS parameters. 31 | """ 32 | 33 | def draw_item(self, context, layout, data, item, icon, active_data): 34 | row = layout.row() 35 | row.active = item.bf_export 36 | row = row.split(factor=0.08) 37 | row.prop(item, "bf_export", text="") 38 | row.prop(item, "name", text="", emboss=False, icon_value=icon) 39 | 40 | 41 | bl_classes = [ 42 | WM_PG_bf_other, 43 | WM_UL_bf_other_items, 44 | ] 45 | 46 | 47 | def register(): 48 | log.info("Register ui_lists...") 49 | for c in bl_classes: 50 | register_class(c) 51 | 52 | 53 | def unregister(): 54 | log.info("Unregister ui_lists...") 55 | for c in reversed(bl_classes): 56 | unregister_class(c) 57 | -------------------------------------------------------------------------------- /dev/doxygen/.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | *.log 3 | -------------------------------------------------------------------------------- /dev/doxygen/update-doxygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: bash /path/to/update-doxygen.sh 3 | 4 | # System variables 5 | DOXYGEN_PATHFILE="/opt/doxygen/doxygen" 6 | 7 | # Generation of new doxygen 8 | echo "Doxygen generation ..." 9 | DOXYGEN_PATHFILE doxyfile > doxygen.log 2>&1 10 | 11 | # Moving to branch gh-pages 12 | echo "Pulling gh-pages branch ..." 13 | git checkout gh-pages 14 | git pull 15 | 16 | # Replacing current doxygen 17 | echo "Deploying doxygen ..." 18 | rm -rf ../docs/html 19 | mv html ../docs/ 20 | 21 | # Commit 22 | echo "Commit doxygen ..." 23 | git add ../docs/html 24 | COMMITMESSAGE="Doxygen update $(date)" 25 | git commit -m "$COMMITMESSAGE" 26 | git push 27 | 28 | # Restoring master 29 | git checkout master 30 | -------------------------------------------------------------------------------- /dev/make_release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # BlenderFDS, an open tool for the NIST Fire Dynamics Simulator 4 | # Copyright (C) 2013 Emanuele Gissi, http://www.blenderfds.org 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """! 20 | Automatic creation of blenderfds release zip. 21 | """ 22 | 23 | import shutil, tempfile 24 | 25 | # Config 26 | ignore_patterns = ( 27 | "dev", 28 | "docs", 29 | "__pycache__", 30 | "*.pyc", 31 | "*.blend1", 32 | "TODO.md", 33 | ".git", 34 | ".gitignore", 35 | ".vscode", 36 | ) 37 | output_filename = "blenderfds" 38 | 39 | # Make 40 | with tempfile.TemporaryDirectory() as tmpdirname: 41 | shutil.copytree( 42 | "..", 43 | tmpdirname + "/blenderfds", 44 | symlinks=False, 45 | ignore=shutil.ignore_patterns(*ignore_patterns), 46 | ) 47 | shutil.make_archive(output_filename, "zip", tmpdirname) 48 | 49 | print(f"Done: {output_filename}.zip") 50 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | blenderfds.org -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | 3 | title: BlenderFDS 4 | description: The open user interface for the
NIST Fire Dynamics Simulator (FDS) 5 | logo: https://github.com/firetools/blenderfds/wiki/p/web/logo.png 6 | show_downloads: false 7 | # google_analytics: [Your Google Analytics tracking ID] 8 | 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 |
9 | 10 |

11 | 12 | ## BlenderFDS, full control over the FDS input file 13 | 14 | BlenderFDS helps you build complex FDS models *faster*, 15 | because it eases geometric data entry with powerful 3D editing tools. 16 | But you remain in full control over all the input parameters, 17 | as BlenderFDS does not hide the FDS complexity and flexibility. 18 | 19 | You can import CAD models and existing FDS input files, 20 | and quickly adapt them to your needs. 21 | 22 | BlenderFDS is developed in Python 23 | as a free and open source [Blender](https://www.blender.org/) addon, 24 | and it is fully scriptable. 25 | 26 | ## [Install BlenderFDS](https://github.com/firetools/blenderfds/wiki/Install) on the supported platforms. 27 | 28 | [![os](https://github.com/firetools/blenderfds/wiki/p/web/os.png)](https://github.com/firetools/blenderfds/wiki/Install) 29 | 30 | ![blenderfds](https://github.com/firetools/blenderfds/wiki/p/screencast/bf.gif) 31 | 32 | ## What is FDS? 33 | 34 | [NIST Fire Dynamics Simulator (FDS)](https://pages.nist.gov/fds-smv/) 35 | is a large-eddy simulation (LES) code for low-speed flows, 36 | with an emphasis on smoke and heat transport from fires. 37 | 38 | ## What is Blender? 39 | 40 | [Blender](https://www.blender.org/) is the free and open source 3D creation suite. 41 | It supports the entirety of the 3D pipeline from modelling to visualisation, 42 | and BlenderFDS makes use of its 3D modeling tools. 43 | 44 | ## A family of open tools for FDS 45 | 46 | Another companion tool is [qgis2fds](https://github.com/firetools/qgis2fds/wiki), 47 | an open [QGIS](https://www.qgis.org) plugin 48 | that exports terrains and landuse for wildfire simulation and atmospheric dispersion of fire pollutants. 49 | The resulting FDS file can be imported to BlenderFDS for further customization. 50 | 51 | ![qgis2fds](https://github.com/firetools/blenderfds/wiki/p/screencast/qgis2fds.gif) 52 | 53 | ## Funded by the WUIFI-21 project 54 | 55 | The development of BlenderFDS was funded by a grant from 56 | the [Italian Ministry of Foreign Affairs and International Cooperation](https://www.esteri.it/). 57 | 58 | ![MAECI](https://github.com/firetools/blenderfds/wiki/p/web/logo-maeci.jpeg) 59 | 60 | ## License 61 | 62 | BlenderFDS is free software under the terms of 63 | the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.en.html). 64 | It is distributed in the hope that it will be useful, 65 | but without any warranty. 66 | 67 | If you prefer buying professional support, take a look at 68 | [PyroSim](https://www.thunderheadeng.com/pyrosim) 69 | -------------------------------------------------------------------------------- /lang/MN_SURF.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistMa 5 | from .bf_material import ( 6 | MP_namelist_cls, 7 | MP_ID, 8 | MP_FYI, 9 | MP_DEFAULT, 10 | MP_RGB, 11 | MP_COLOR, 12 | MP_TRANSPARENCY, 13 | MP_other, 14 | ) 15 | from .. import config 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | class MN_SURF(BFNamelistMa): 21 | label = "SURF" 22 | description = "Boundary condition" 23 | enum_id = 2000 24 | fds_label = "SURF" 25 | bpy_export = "bf_surf_export" 26 | bpy_export_default = True 27 | bf_params = ( 28 | MP_namelist_cls, 29 | MP_ID, 30 | MP_FYI, 31 | MP_DEFAULT, 32 | MP_RGB, 33 | MP_COLOR, 34 | MP_TRANSPARENCY, 35 | MP_other, 36 | ) 37 | bf_import_order = 40 38 | 39 | def get_exported(self, context): # was in MP_namelist_cls 40 | if self.element.name in config.DEFAULT_MAS: 41 | return False 42 | return super().get_exported(context) 43 | -------------------------------------------------------------------------------- /lang/ON_DEVC.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Object 5 | from bpy.props import FloatProperty, BoolProperty, StringProperty 6 | from ..types import BFParam, BFNamelistOb 7 | from .bf_object import OP_namelist_cls, OP_ID, OP_FYI, OP_ID_suffix, OP_other 8 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 9 | from .OP_XYZ import OP_XYZ 10 | from .OP_SURF_ID import OP_SURF_ID 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class OP_DEVC_QUANTITY(BFParam): 16 | label = "QUANTITY" 17 | description = "Output quantity" 18 | fds_label = "QUANTITY" 19 | bpy_type = Object 20 | bpy_prop = StringProperty 21 | bpy_idname = "bf_quantity" 22 | 23 | def draw_operators(self, context, layout): 24 | layout.operator("object.bf_choose_devc_quantity", icon="VIEWZOOM", text="") 25 | 26 | 27 | class OP_DEVC_PROP_ID(BFParam): 28 | label = "PROP_ID" 29 | description = "Reference to a PROP namelist" 30 | fds_label = "PROP_ID" 31 | bpy_type = Object 32 | bpy_prop = StringProperty 33 | bpy_idname = "bf_devc_prop_id" 34 | 35 | def draw_operators(self, context, layout): 36 | layout.operator("object.bf_choose_devc_prop_id", icon="VIEWZOOM", text="") 37 | 38 | 39 | class ON_DEVC(BFNamelistOb): 40 | label = "DEVC" 41 | description = "Device" 42 | collection = "Output" 43 | enum_id = 1011 44 | fds_label = "DEVC" 45 | bf_params = ( 46 | OP_namelist_cls, 47 | OP_ID, 48 | OP_FYI, 49 | OP_DEVC_QUANTITY, 50 | OP_DEVC_PROP_ID, 51 | OP_SURF_ID, 52 | OP_XB, 53 | OP_XB_voxel_size, 54 | OP_XB_center_voxels, 55 | OP_XYZ, 56 | OP_ID_suffix, 57 | OP_other, 58 | ) 59 | bf_other = {"appearance": "WIRE"} 60 | -------------------------------------------------------------------------------- /lang/ON_GEOM/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .ON_GEOM import ON_GEOM 4 | from .geom_to_ob import geom_to_ob 5 | from .ob_to_geom import ob_to_geom, check_intersections, check_geom_sanity 6 | -------------------------------------------------------------------------------- /lang/ON_GEOM/bingeom.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, FDS bingeom files input/output routines. 5 | """ 6 | 7 | import struct, logging 8 | import numpy as np 9 | from ... import utils 10 | from ...types import BFException 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | # Read/write fds bingeom files 15 | 16 | # The FDS bingeom file is written from Fortran90 like this: 17 | # WRITE(731) INTEGER_ONE 18 | # WRITE(731) N_VERTS,N_FACES,N_SURF_ID,N_VOLUS 19 | # WRITE(731) VERTS(1:3*N_VERTS) 20 | # WRITE(731) FACES(1:3*N_FACES) 21 | # WRITE(731) SURFS(1:N_FACES) 22 | # WRITE(731) VOLUS(1:4*N_VOLUS) 23 | 24 | 25 | def _read_record(f, req_dtype, req_dlen): 26 | """! 27 | Read a record from an open binary unformatted sequential Fortran90 file. 28 | @param f: open Python file object in 'rb' mode. 29 | @param req_dtype: requested type of data in 'int32' or 'float64'. 30 | @param req_dlen: requested length of the record. 31 | @return np.array of read data. 32 | """ 33 | # The start tag is an int32 number (4 bytes) declaring the length of the record in bytes 34 | tag = struct.unpack("i", f.read(4))[0] 35 | # Calc the length of the record in numbers, using the byte size of the requested dtype 36 | dlen = int(tag / np.dtype(req_dtype).itemsize) 37 | if dlen != req_dlen: 38 | raise IOError( 39 | f"Different requested and declared record length: {req_dlen}, {dlen}" 40 | ) 41 | # Read the record 42 | data = np.fromfile(f, dtype=req_dtype, count=req_dlen) 43 | # The end tag should be equal to the start tag 44 | end_tag = struct.unpack("i", f.read(4))[0] # end tag, last 4 bytes, int32 45 | if tag != end_tag: # check tags 46 | raise IOError(f"Different start and end record tags: {tag}, {end_tag}") 47 | # print(f"Read: record tag: {tag} dlen: {len(data)}\ndata: {data}") # TODO log debug 48 | return data 49 | 50 | 51 | def read_bingeom_file(filepath): 52 | """! 53 | Read FDS bingeom file 54 | @param filepath: filepath to be read from 55 | @return n_surf_id as integer, and fds_verts, fds_faces, fds_surfs, fds_volus as np.arrays in FDS flat format 56 | """ 57 | try: 58 | with open(filepath, "rb") as f: 59 | geom_type = _read_record(f, req_dtype="int32", req_dlen=1)[0] 60 | if geom_type not in (1, 2): 61 | msg = f"Unknown GEOM type <{geom_type}> in bingeom file <{filepath}>" 62 | raise AssertionError(msg) 63 | n_verts, n_faces, n_surf_id, n_volus = _read_record( 64 | f, req_dtype="int32", req_dlen=4 65 | ) 66 | fds_verts = _read_record(f, req_dtype="float64", req_dlen=3 * n_verts) 67 | fds_faces = _read_record(f, req_dtype="int32", req_dlen=3 * n_faces) 68 | fds_surfs = _read_record(f, req_dtype="int32", req_dlen=n_faces) 69 | fds_volus = _read_record(f, req_dtype="int32", req_dlen=4 * n_volus) 70 | except Exception as err: 71 | raise BFException(None, f"Error reading bingeom file: <{filepath}>\n{err}") 72 | return n_surf_id, fds_verts, fds_faces, fds_surfs, fds_volus, geom_type 73 | 74 | 75 | def _write_record(f, data): 76 | """! 77 | Write a record to a binary unformatted sequential Fortran90 file. 78 | @param f: open Python file object in 'wb' mode. 79 | @param data: np.array() of data. 80 | """ 81 | # Calc start and end record tag 82 | tag = len(data) * data.dtype.itemsize 83 | # print(f"Write: record tag: {tag} dlen: {len(data)}\ndata: {data}") # TODO log debug 84 | # Write start tag, data, and end tag 85 | f.write(struct.pack("i", tag)) 86 | data.tofile(f) 87 | f.write(struct.pack("i", tag)) 88 | 89 | 90 | def write_bingeom_file( 91 | geom_type, 92 | n_surf_id, 93 | fds_verts, 94 | fds_faces, 95 | fds_surfs, 96 | fds_volus, 97 | filepath, 98 | force_dir=False, 99 | ): 100 | """! 101 | Write FDS bingeom file. 102 | @param geom_type: GEOM type (eg. 1 is manifold, 2 is terrain) 103 | @param n_surf_id: number of referred boundary conditions 104 | @param fds_verts: vertices coordinates in FDS flat format, eg. (x0, y0, z0, x1, y1, ...) 105 | @param fds_faces: faces connectivity in FDS flat format, eg. (i0, j0, k0, i1, ...) 106 | @param fds_surfs: boundary condition indexes in FDS flat format, eg. (b0, b1, ...) 107 | @param fds_volus: volumes connectivity in FDS flat format, eg. (i0, j0, k0, w0, i1, ...) 108 | @param filepath: destination filepath 109 | """ 110 | 111 | try: 112 | if force_dir: 113 | utils.io.make_dir(filepath) 114 | with open(filepath, "wb") as f: 115 | _write_record(f, np.array((geom_type,), dtype="int32")) # 1 or 2 if terrain 116 | _write_record( 117 | f, 118 | np.array( 119 | ( 120 | len(fds_verts) // 3, 121 | len(fds_faces) // 3, 122 | n_surf_id, 123 | len(fds_volus) // 4, 124 | ), 125 | dtype="int32", 126 | ), 127 | ) 128 | _write_record(f, np.array(fds_verts, dtype="float64")) 129 | _write_record(f, np.array(fds_faces, dtype="int32")) 130 | _write_record(f, np.array(fds_surfs, dtype="int32")) 131 | _write_record(f, np.array(fds_volus, dtype="int32")) 132 | except Exception as err: 133 | raise BFException(None, f"Error writing bingeom file: <{filepath}>\n{err}") 134 | -------------------------------------------------------------------------------- /lang/ON_HOLE.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistOb 5 | from .bf_object import ( 6 | OP_namelist_cls, 7 | OP_ID, 8 | OP_FYI, 9 | OP_other, 10 | OP_RGB_override, 11 | OP_COLOR_override, 12 | OP_TRANSPARENCY_override, 13 | ) 14 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 15 | from .ON_MULT import OP_other_MULT_ID 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | class ON_HOLE(BFNamelistOb): 21 | label = "HOLE" 22 | description = "Obstruction cutout" 23 | collection = "Obstacles" 24 | enum_id = 1009 25 | fds_label = "HOLE" 26 | bf_params = ( 27 | OP_namelist_cls, 28 | OP_ID, 29 | OP_FYI, 30 | OP_XB, 31 | OP_XB_voxel_size, 32 | OP_XB_center_voxels, 33 | OP_other_MULT_ID, 34 | OP_RGB_override, 35 | OP_COLOR_override, 36 | OP_TRANSPARENCY_override, 37 | OP_other, 38 | ) 39 | bf_other = {"appearance": "WIRE"} 40 | -------------------------------------------------------------------------------- /lang/ON_INIT.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistOb 5 | from .bf_object import OP_namelist_cls, OP_ID, OP_FYI, OP_ID_suffix, OP_other 6 | from .OP_XB import OP_XB 7 | from .OP_XYZ import OP_XYZ 8 | from .ON_MULT import OP_other_MULT_ID 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class ON_INIT(BFNamelistOb): 14 | label = "INIT" 15 | description = "Initial condition" 16 | collection = "Domain" 17 | enum_id = 1015 18 | fds_label = "INIT" 19 | bf_params = ( 20 | OP_namelist_cls, 21 | OP_ID, 22 | OP_FYI, 23 | OP_XB, 24 | OP_XYZ, 25 | OP_ID_suffix, 26 | OP_other_MULT_ID, 27 | OP_other, 28 | ) 29 | bf_other = {"appearance": "WIRE"} 30 | -------------------------------------------------------------------------------- /lang/ON_MESH/ON_MESH.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | MESH namelist definition 5 | """ 6 | 7 | import logging 8 | from bpy.types import Object 9 | from bpy.props import IntVectorProperty 10 | from ...config import LP 11 | from ...types import BFParam, BFNamelistOb, FDSParam, FDSMulti, FDSList, BFException 12 | from ... import utils 13 | from ..bf_object import ( 14 | OP_namelist_cls, 15 | OP_ID, 16 | OP_FYI, 17 | OP_other, 18 | OP_RGB_override, 19 | OP_COLOR_override, 20 | ) 21 | from ..OP_XB import OP_XB_BBOX 22 | from ..ON_MULT import OP_other_MULT_ID 23 | from .calc_meshes import get_mesh_geometry 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | def update_bf_mesh_nsplits(ob, context): 29 | utils.geometry.rm_geometric_cache(ob=ob) 30 | if ob.bf_has_tmp: 31 | utils.geometry.rm_tmp_objects() 32 | 33 | class OP_MESH_IJK(BFParam): 34 | label = "IJK" 35 | description = "Cell numbers along axis" 36 | fds_label = "IJK" 37 | bpy_type = Object 38 | bpy_idname = "bf_mesh_ijk" 39 | bpy_prop = IntVectorProperty 40 | bpy_default = (10, 10, 10) 41 | bpy_other = {"size": 3, "min": 1} 42 | 43 | def draw(self, context, layout): 44 | ob = self.element 45 | try: 46 | _, _, _, nmesh, nsplit, nmult, ncell_tot, ncell, cs, aspect, has_good_ijk = get_mesh_geometry(context=context, ob=ob) 47 | except BFException: 48 | col = layout.column(align=True) 49 | col.label(text="No MESH info while in Edit Mode.") 50 | else: 51 | col = layout.column(align=True) 52 | col.label(text=f"MESH Qty: {nmesh} | Splits: {nsplit} | Multiples: {nmult}") 53 | col.label(text=f"Cell Qty: {ncell_tot} (~{ncell} each)") 54 | col.label(text=f"Size: {cs[0]:.{LP}f}·{cs[1]:.{LP}f}·{cs[2]:.{LP}f}m | Aspect: {aspect:.1f} | Poisson: {has_good_ijk}") 55 | 56 | col = layout.column() 57 | row = col.row(align=True) 58 | row.prop(ob, "bf_mesh_ijk", text="IJK, Splits") 59 | sub = row.row(align=True) 60 | sub.active = ob.bf_mesh_nsplits_export 61 | sub.prop(ob, "bf_mesh_nsplits", text="") 62 | 63 | class OP_MESH_nsplits(BFParam): 64 | label = "Split IJK" 65 | description = "Evenly split IJK along each axis generating an array\nof MESH namelists, conserving the cell size" 66 | bpy_type = Object 67 | bpy_idname = "bf_mesh_nsplits" 68 | bpy_prop = IntVectorProperty 69 | bpy_default = (1, 1, 1) 70 | bpy_other = {"size": 3, "min": 1, "update": update_bf_mesh_nsplits} 71 | bpy_export = "bf_mesh_nsplits_export" 72 | bpy_export_default = False 73 | 74 | def draw(self, context, layout): 75 | layout.prop(self.element, "bf_mesh_nsplits_export", text="Split") 76 | 77 | class OP_MESH_XB_BBOX(OP_XB_BBOX): 78 | # This class implements OP_XB_BBOX 79 | # adding MESH split, IJK calculations, and MPI processes 80 | 81 | def to_fds_list(self, context) -> FDSList: 82 | ob = self.element 83 | hids, ijks, xbs, _, _, _, _, ncell, cs, aspect, has_good_ijk = get_mesh_geometry(context=context, ob=ob) 84 | msg = f" | Size: {cs[0]:.{LP}f}·{cs[1]:.{LP}f}·{cs[2]:.{LP}f}m | Aspect: {aspect:.1f} | Poisson: {has_good_ijk}" 85 | match len(xbs): 86 | case 0: 87 | return FDSList() # FIXME raise exception? 88 | case 1: 89 | msg = f"Cell Qty: {ncell}{msg}" 90 | return FDSParam(fds_label="XB", value=xbs[0], precision=LP, msgs=(msg,)) 91 | case _: 92 | iterable = ( 93 | (FDSParam(fds_label="ID", value=hid) for hid in hids), 94 | (FDSParam(fds_label="IJK", value=ijk, msgs=(f"Cell Qty: {ijk[0]*ijk[1]*ijk[2]}{msg}",)) for ijk in ijks), 95 | (FDSParam(fds_label="XB", value=xb, precision=LP) for xb in xbs), 96 | ) 97 | return FDSMulti(iterable=iterable) 98 | 99 | class OP_MESH_MPI_PROCESS(BFParam): # only import 100 | label = "MPI_PROCESS" 101 | description = "MPI process number" 102 | fds_label = "MPI_PROCESS" # only import 103 | bpy_type = Object 104 | 105 | def draw(self, context, layout): 106 | pass 107 | 108 | def set_value(self, context, value=None): 109 | # Set the number of MPI processes from the FDS file 110 | sc = context.scene 111 | sc.bf_config_mpi_processes_export = True 112 | if value + 1 > sc.bf_config_mpi_processes: 113 | sc.bf_config_mpi_processes = value + 1 114 | 115 | class ON_MESH(BFNamelistOb): 116 | label = "MESH" 117 | description = "Domain of simulation" 118 | collection = "Domain" 119 | enum_id = 1014 120 | fds_label = "MESH" 121 | bf_params = ( 122 | OP_namelist_cls, 123 | OP_ID, 124 | OP_FYI, 125 | OP_MESH_MPI_PROCESS, 126 | OP_MESH_IJK, 127 | OP_MESH_nsplits, 128 | OP_MESH_XB_BBOX, 129 | OP_other_MULT_ID, 130 | OP_RGB_override, 131 | OP_COLOR_override, 132 | OP_other, 133 | ) 134 | bf_other = {"appearance": "BBOX"} 135 | bf_import_order = 30 136 | 137 | def draw_operators(self, context, layout): 138 | col = layout.column(align=True) 139 | col.operator("object.bf_set_suggested_mesh_cell_size") 140 | col.operator("object.bf_set_mesh_cell_size") 141 | col.operator("object.bf_align_to_mesh") 142 | -------------------------------------------------------------------------------- /lang/ON_MESH/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .ON_MESH import ON_MESH 4 | from .align_meshes import align_meshes 5 | from .calc_meshes import get_cell_sizes, get_ijk_from_desired_cs 6 | -------------------------------------------------------------------------------- /lang/ON_MESH/calc_meshes.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | Calc all MESH parameters. 5 | """ 6 | 7 | from math import ceil 8 | from ... import utils 9 | from .split_mesh import split_mesh 10 | from ..ON_MULT import multiply_xbs 11 | 12 | 13 | def get_factor(n): 14 | """!Generator for prime factors of n (from http://dhananjaynene.com/).""" 15 | yield 1 16 | i = 2 17 | limit = n ** 0.5 18 | while i <= limit: 19 | if n % i == 0: 20 | yield i 21 | n = n / i 22 | limit = n ** 0.5 23 | else: 24 | i += 1 25 | if n > 1: 26 | yield int(n) 27 | 28 | 29 | def get_n_for_poisson(n): 30 | """!Get a good number for poisson solver at least bigger than n.""" 31 | good = set((1, 2, 3, 5)) 32 | while True: 33 | if [i for i in get_factor(n) if i not in good]: 34 | n += 1 35 | else: 36 | break 37 | return n 38 | 39 | 40 | def get_poisson_ijk(ijk): 41 | """!Get an IJK respecting the Poisson constraint, close to the current one.""" 42 | return ijk[0], get_n_for_poisson(ijk[1]), get_n_for_poisson(ijk[2]) 43 | 44 | 45 | def get_ijk_from_desired_cs(context, ob, desired_cs, poisson): # Used by: mesh_tools.py 46 | """!Calc MESH IJK from cell sizes.""" 47 | xb = utils.geometry.get_bbox_xb(context=context, ob=ob, world=True) 48 | ijk = ( 49 | round((xb[1] - xb[0]) / desired_cs[0]) or 1, 50 | round((xb[3] - xb[2]) / desired_cs[1]) or 1, 51 | round((xb[5] - xb[4]) / desired_cs[2]) or 1, 52 | ) 53 | if poisson: 54 | return get_poisson_ijk(ijk) 55 | else: 56 | return ijk 57 | 58 | 59 | def get_cell_sizes(context, ob): # used by: mesh_tools.py 60 | """!Get cell sizes.""" 61 | xb = utils.geometry.get_bbox_xb(context=context, ob=ob, world=True) 62 | ijk = ob.bf_mesh_ijk 63 | return ( 64 | (xb[1] - xb[0]) / ijk[0], 65 | (xb[3] - xb[2]) / ijk[1], 66 | (xb[5] - xb[4]) / ijk[2], 67 | ) 68 | 69 | 70 | def get_mesh_geometry(context, ob): 71 | """!Get geometry and info on generated MESH instances.""" 72 | # Split 73 | hids, ijks, xbs, ncell, cs, nsplit = split_mesh( 74 | hid=ob.name, 75 | ijk=ob.bf_mesh_ijk, 76 | export=ob.bf_mesh_nsplits_export, 77 | nsplits=ob.bf_mesh_nsplits, 78 | xb=utils.geometry.get_bbox_xb(context=context, ob=ob, world=True), 79 | ) 80 | 81 | # Multiply 82 | hids, xbs, _, nmult = multiply_xbs( 83 | context=context, ob=ob, hids=hids, xbs=xbs, msgs=list() 84 | ) 85 | ijks *= nmult 86 | 87 | # Prepare header 88 | ijk = ob.bf_mesh_ijk 89 | ncell_tot = ijk[0] * ijk[1] * ijk[2] * nmult 90 | has_good_ijk = tuple(ijk) == get_poisson_ijk(ijk) and "Yes" or "No" 91 | aspect = get_cell_aspect(cs) 92 | nmesh = nmult * nsplit 93 | return ( 94 | hids, 95 | ijks, 96 | xbs, 97 | nmesh, 98 | nsplit, 99 | nmult, 100 | ncell_tot, 101 | ncell, 102 | cs, 103 | aspect, 104 | has_good_ijk, 105 | ) 106 | 107 | 108 | def get_cell_aspect(cell_sizes): 109 | """!Get max cell aspect ratio.""" 110 | scs = sorted(cell_sizes) 111 | try: 112 | return max( 113 | scs[2] / scs[0], 114 | scs[2] / scs[1], 115 | scs[1] / scs[0], 116 | ) 117 | except ZeroDivisionError: 118 | return 999.0 119 | -------------------------------------------------------------------------------- /lang/ON_MESH/split_mesh.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | Split MESH parameters according to nsplits. 5 | """ 6 | 7 | 8 | from ...types import BFException 9 | 10 | # FIXME split when there is a coarser mesh? 11 | 12 | 13 | def split_cells(ncell, nsplit): 14 | """! 15 | Split ncell cells in nsplit parts, conserving the total number ncell of cells 16 | """ 17 | if ncell < nsplit * 3: 18 | return list((ncell,)) 19 | base = ncell // nsplit 20 | remainder = ncell % nsplit 21 | ncells = list((base,)) * nsplit 22 | for i in range(0, remainder): 23 | ncells[i] += 1 24 | return ncells 25 | 26 | 27 | def split_mesh(hid, ijk, export, nsplits, xb): 28 | """! 29 | Split ijk cells along the axis in nsplits, calc new hids, ijks and xbs 30 | """ 31 | # Init 32 | ijks, xbs = list(), list() 33 | if not export: 34 | nsplits = 1, 1, 1 35 | # Split cells along axis 36 | icells = split_cells(ijk[0], nsplits[0]) 37 | jcells = split_cells(ijk[1], nsplits[1]) 38 | kcells = split_cells(ijk[2], nsplits[2]) 39 | ncell = icells[0] * jcells[0] * kcells[0] 40 | expected_nsplits = nsplits[0] * nsplits[1] * nsplits[2] 41 | # Prepare new mesh ijks and origins (in cell number) 42 | corigins = list() 43 | corigin_i, corigin_j, corigin_k = 0, 0, 0 44 | for i in icells: 45 | for j in jcells: 46 | for k in kcells: 47 | ijks.append((i, j, k)) 48 | corigins.append((corigin_i, corigin_j, corigin_k)) 49 | corigin_k += k 50 | corigin_k = 0 51 | corigin_j += j 52 | corigin_j = 0 53 | corigin_i += i 54 | # Calc cell sizes along axis 55 | cs = ( 56 | (xb[1] - xb[0]) / ijk[0], 57 | (xb[3] - xb[2]) / ijk[1], 58 | (xb[5] - xb[4]) / ijk[2], 59 | ) 60 | # Prepare xbs 61 | # corigin (0,0,0) is at coordinates xb[0], xb[2], xb [4] 62 | for i, corigin in enumerate(corigins): 63 | xbs.append( 64 | ( 65 | xb[0] + corigin[0] * cs[0], 66 | xb[0] + (corigin[0] + ijks[i][0]) * cs[0], 67 | xb[2] + corigin[1] * cs[1], 68 | xb[2] + (corigin[1] + ijks[i][1]) * cs[1], 69 | xb[4] + corigin[2] * cs[2], 70 | xb[4] + (corigin[2] + ijks[i][2]) * cs[2], 71 | ) 72 | ) 73 | # Prepare hids 74 | nsplit = len(xbs) 75 | if nsplit > 1: 76 | hids = tuple(f"{hid}_s{i}" for i in range(len(xbs))) 77 | else: 78 | hids = tuple((hid,)) 79 | if nsplit != expected_nsplits: 80 | raise BFException(sender=None, msg="Too much splitting, not enough cells.") 81 | return hids, ijks, xbs, ncell, cs, nsplit 82 | -------------------------------------------------------------------------------- /lang/ON_MOVE/ON_MOVE.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Object 5 | from bpy.props import FloatProperty, IntProperty, FloatVectorProperty 6 | from ...config import T34_P 7 | from ...types import BFNamelist, BFParam, BFNotImported, FDSList, FDSNamelist, FDSParam 8 | from ... import utils 9 | from .t34 import calc_t34, calc_bl_matrix 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class OP_MOVE_ID(BFParam): 15 | label = "ID" 16 | description = "Geometric transformation name" 17 | fds_label = "ID" 18 | bpy_type = Object 19 | 20 | def get_value(self, context): 21 | return f"{self.element.name}_move" 22 | 23 | def set_value(self, context, value=None): 24 | pass 25 | 26 | 27 | class OP_MOVE_T34(BFParam): 28 | label = "T34" 29 | description = "Geometric transformation 3x4 matrix" 30 | fds_label = "T34" 31 | bpy_type = Object 32 | bpy_other = {"precision": T34_P} 33 | 34 | def get_value(self, context): 35 | return calc_t34(m=self.element.matrix_world) 36 | 37 | def set_value(self, context, value=None): 38 | utils.geometry.transform_ob( 39 | ob=self.element, 40 | m=calc_bl_matrix(t34=value), 41 | force_othogonal=False, 42 | ) 43 | 44 | 45 | class ON_MOVE(BFNamelist): # not in namelist menu 46 | label = "MOVE" 47 | description = "Geometric Transformation" 48 | bpy_type = Object 49 | enum_id = None 50 | fds_label = "MOVE" 51 | bf_params = ( 52 | OP_MOVE_ID, 53 | OP_MOVE_T34, 54 | ) 55 | 56 | def from_fds_list(self, context, fds_list, fds_label=None): 57 | # Read fds_params 58 | ps = { # label: default value 59 | "T34": None, 60 | "X0": 0.0, 61 | "Y0": 0.0, 62 | "Z0": 0.0, 63 | "AXIS": (0.0, 0.0, 1.0), 64 | "ROTATION_ANGLE": 0.0, 65 | "SCALE": None, 66 | "SCALEX": 1.0, 67 | "SCALEY": 1.0, 68 | "SCALEZ": 1.0, 69 | "DX": 0.0, 70 | "DY": 0.0, 71 | "DZ": 0.0, 72 | } 73 | for fds_label in ps: 74 | fds_param = fds_list.get_fds_param(fds_label=fds_label, remove=True) 75 | if fds_param: 76 | ps[fds_label] = fds_param.get_value() # assign value 77 | m = calc_bl_matrix( 78 | t34=ps["T34"], 79 | dx=ps["DX"], 80 | dy=ps["DY"], 81 | dz=ps["DZ"], 82 | scale=ps["SCALE"], 83 | scalex=ps["SCALEX"], 84 | scaley=ps["SCALEY"], 85 | scalez=ps["SCALEZ"], 86 | x0=ps["X0"], 87 | y0=ps["Y0"], 88 | z0=ps["Z0"], 89 | rotation_angle=ps["ROTATION_ANGLE"], 90 | axis=ps["AXIS"], 91 | ) 92 | utils.geometry.transform_ob(ob=self.element, m=m, force_othogonal=False) 93 | 94 | 95 | # Called by other namelists 96 | # that support a MOVE_ID 97 | # and the relative MOVE namelist 98 | # (See also: ON_MULT) 99 | 100 | 101 | class OP_other_MOVE_ID(BFParam): 102 | label = "MOVE_ID" 103 | description = "Reference to geometric transformation" 104 | fds_label = "MOVE_ID" 105 | bpy_type = Object 106 | 107 | def get_value(self, context): 108 | return f"{self.element.name}_move" 109 | 110 | def set_value(self, context, value=None): 111 | # Get required MOVE parameters from dict created by SN_MOVE 112 | try: 113 | f90_params = context.scene["bf_move_coll"][value] 114 | except KeyError as err: 115 | raise BFNotImported(self, f"Missing MOVE ID='{value}'") 116 | ON_MOVE(element=self.element).from_fds_list( 117 | context=context, 118 | fds_list=FDSList(f90_params=f90_params), 119 | ) 120 | 121 | def get_active(self, context): # set in namelists that use it (eg. ON_GEOM) 122 | return False 123 | 124 | def get_exported(self, context): 125 | return self.get_active(context) 126 | 127 | def to_fds_list(self, context) -> FDSList: 128 | if self.get_exported(context): 129 | return FDSList( 130 | iterable=( 131 | super().to_fds_list(context), 132 | ON_MOVE(element=self.element).to_fds_list(context), 133 | ) 134 | ) 135 | else: 136 | return FDSList() 137 | 138 | def draw(self, context, layout): # only label 139 | row = layout.split(factor=0.4) 140 | active = self.get_active(context) 141 | row.active = active 142 | row.alignment = "RIGHT" 143 | row.label(text=self.label) 144 | row.alignment = "EXPAND" 145 | row.label(icon=active and "LINKED" or "UNLINKED") 146 | -------------------------------------------------------------------------------- /lang/ON_MOVE/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .ON_MOVE import ON_MOVE, OP_other_MOVE_ID 4 | -------------------------------------------------------------------------------- /lang/ON_MOVE/t34.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from math import radians 5 | from mathutils import Matrix, Vector 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | # As a reminder: 11 | # context.object.matrix_world = 12 | # Matrix(((1.0, 0.0, 0.0, 1.0), 13 | # (0.0, 1.0, 0.0, 2.0), 14 | # (0.0, 0.0, 1.0, 3.0), 15 | # (0.0, 0.0, 0.0, 1.0))) 16 | # context.object.matrix_world[1][3] = 2. 17 | # t34 = 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 2.0, 3.0 18 | 19 | # m = mathutils.Matrix.Rotation(0.7, 4, "Z") 20 | # bpy.data.objects["Cube"].matrix_world @= m # apply rotation 21 | # bpy.data.objects["Cube"].matrix_world @= m.copy().invert() # apply inverse rotation 22 | 23 | 24 | def calc_t34(m) -> tuple: 25 | """! 26 | Transform the Blender transformation matrix into the FDS MOVE T34 notation. 27 | """ 28 | return tuple(m[i][j] for j in range(4) for i in range(3)) 29 | 30 | 31 | def calc_bl_matrix( 32 | t34=None, 33 | dx=0.0, 34 | dy=0.0, 35 | dz=0.0, 36 | scale=None, 37 | scalex=1.0, 38 | scaley=1.0, 39 | scalez=1.0, 40 | x0=0.0, 41 | y0=0.0, 42 | z0=0.0, 43 | rotation_angle=0.0, 44 | axis=(0.0, 0.0, 1.0), 45 | ) -> Matrix: 46 | """! 47 | Transform the FDS MOVE notation into a Blender 4x4 transformation matrix. 48 | """ 49 | if t34: 50 | m = list(tuple(t34[j * 3 + i] for j in range(4)) for i in range(3)) 51 | m.append((0.0, 0.0, 0.0, 1.0)) # add last row 52 | m = Matrix(m) 53 | else: 54 | if scale: 55 | scalex = scaley = scalez = scale 56 | m = ( 57 | Matrix().Translation((dx, dy, dz)) # last applied 58 | @ Matrix().Scale(scalex, 4, (1, 0, 0)) 59 | @ Matrix().Scale(scaley, 4, (0, 1, 0)) 60 | @ Matrix().Scale(scalez, 4, (0, 0, 1)) 61 | @ Matrix().Translation((x0, y0, z0)) 62 | @ Matrix().Rotation(radians(rotation_angle), 4, Vector(axis)) 63 | @ Matrix().Translation((-x0, -y0, -z0)) # first applied 64 | ) 65 | return m 66 | -------------------------------------------------------------------------------- /lang/ON_MULT/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .ON_MULT import ON_MULT, OP_other_MULT_ID 4 | from .multiply import multiply_xbs 5 | -------------------------------------------------------------------------------- /lang/ON_MULT/multiply.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from ... import utils 4 | from ..OP_XB.xbs_to_ob import xbs_to_ob 5 | 6 | 7 | def multiply_xbs(context, ob, hids, xbs, msgs): 8 | """! 9 | Return lists of xb and their hid as multiplied by FDS MULT from ob. 10 | """ 11 | # Calc, fast track 12 | if not ob.bf_mult_export: 13 | nmult = 1 14 | return hids, xbs, msgs, nmult 15 | 16 | # Init the rest 17 | dxb = ob.bf_mult_dxb 18 | d = (ob.bf_mult_dx, ob.bf_mult_dy, ob.bf_mult_dz) 19 | d0 = (ob.bf_mult_dx0, ob.bf_mult_dy0, ob.bf_mult_dz0) 20 | lower = ( 21 | ob.bf_mult_i_lower, 22 | ob.bf_mult_j_lower, 23 | ob.bf_mult_k_lower, 24 | ob.bf_mult_n_lower, 25 | ) 26 | lower_skip = ( 27 | ob.bf_mult_i_lower_skip, 28 | ob.bf_mult_j_lower_skip, 29 | ob.bf_mult_k_lower_skip, 30 | ob.bf_mult_n_lower_skip, 31 | ) 32 | upper = ( 33 | ob.bf_mult_i_upper, 34 | ob.bf_mult_j_upper, 35 | ob.bf_mult_k_upper, 36 | ob.bf_mult_n_upper, 37 | ) 38 | upper_skip = ( 39 | ob.bf_mult_i_upper_skip, 40 | ob.bf_mult_j_upper_skip, 41 | ob.bf_mult_k_upper_skip, 42 | ob.bf_mult_n_upper_skip, 43 | ) 44 | 45 | # Calc, slow track 46 | multi_xbs, multi_hids = list(), list() 47 | for xb, hid in zip(xbs, hids): 48 | new_xbs, new_hids = multiply_xb( 49 | xb, 50 | hid, 51 | dxb, 52 | d, # dx,dy,dz 53 | d0, # dx0,dy0,dz0, 54 | lower, # i,j,k,n 55 | lower_skip, # i,j,k,n 56 | upper, # i,j,k,n 57 | upper_skip, 58 | ) 59 | multi_xbs.extend(new_xbs) 60 | multi_hids.extend(new_hids) 61 | nmult = len(new_xbs) 62 | 63 | # Msgs 64 | if nmult > 1: 65 | msgs.append(f"Multiples: {nmult}") 66 | 67 | # Generate multiples 68 | return multi_hids, multi_xbs, msgs, nmult 69 | 70 | 71 | def multiply_xb( 72 | xb, 73 | hid, 74 | dxb, 75 | d, # dx,dy,dz 76 | d0, # dx0,dy0,dz0, 77 | lower, # i,j,k,n 78 | lower_skip, # i,j,k,n 79 | upper, # i,j,k,n 80 | upper_skip, # i,j,k,n 81 | ): 82 | """! 83 | Return lists of xb and their hid as multiplied by FDS MULT. 84 | """ 85 | xbs, hids = list(), list() 86 | dx, dy, dz = d 87 | dx0, dy0, dz0 = d0 88 | i_lower, j_lower, k_lower, n_lower = lower 89 | i_lower_skip, j_lower_skip, k_lower_skip, n_lower_skip = lower_skip 90 | i_upper, j_upper, k_upper, n_upper = upper 91 | i_upper_skip, j_upper_skip, k_upper_skip, n_upper_skip = upper_skip 92 | has_skip = False 93 | if any(dxb): 94 | # N, DXB: OBST MULT example 95 | if n_lower_skip >= n_lower or n_upper_skip <= n_upper: 96 | has_skip = True 97 | for n in range(n_lower, n_upper + 1): 98 | if has_skip and n_lower_skip <= n <= n_upper_skip: 99 | continue 100 | xbs.append( 101 | ( 102 | xb[0] + dx0 + n * dxb[0], 103 | xb[1] + dx0 + n * dxb[1], 104 | xb[2] + dy0 + n * dxb[2], 105 | xb[3] + dy0 + n * dxb[3], 106 | xb[4] + dz0 + n * dxb[4], 107 | xb[5] + dz0 + n * dxb[5], 108 | ) 109 | ) 110 | hids.append(f"{hid}_{n}") 111 | else: 112 | # I,J,K: MESH refining example 113 | if ( 114 | i_lower_skip >= i_lower 115 | or j_lower_skip >= j_lower 116 | or k_lower_skip >= k_lower 117 | or i_upper_skip <= i_upper 118 | or j_upper_skip <= j_upper 119 | or k_upper_skip <= k_upper 120 | ): 121 | has_skip = True 122 | i_lower_skip = max(i_lower, i_lower_skip) 123 | for i in range(i_lower, i_upper + 1): 124 | for j in range(j_lower, j_upper + 1): 125 | for k in range(k_lower, k_upper + 1): 126 | if has_skip and ( 127 | i_lower_skip <= i <= i_upper_skip 128 | and j_lower_skip <= j <= j_upper_skip 129 | and k_lower_skip <= k <= k_upper_skip 130 | ): 131 | continue 132 | xbs.append( 133 | ( 134 | xb[0] + dx0 + i * dx, 135 | xb[1] + dx0 + i * dx, 136 | xb[2] + dy0 + j * dy, 137 | xb[3] + dy0 + j * dy, 138 | xb[4] + dz0 + k * dz, 139 | xb[5] + dz0 + k * dz, 140 | ) 141 | ) 142 | hids.append(f"{hid}_i{i}_j{j}_k{k}") 143 | return xbs, hids 144 | -------------------------------------------------------------------------------- /lang/ON_OBST.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistOb 5 | from .bf_object import ( 6 | OP_namelist_cls, 7 | OP_ID, 8 | OP_FYI, 9 | OP_ID_suffix, 10 | OP_other, 11 | OP_RGB_override, 12 | OP_COLOR_override, 13 | OP_TRANSPARENCY_override, 14 | ) 15 | from .OP_SURF_ID import OP_SURF_ID 16 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 17 | from .ON_MULT import OP_other_MULT_ID 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class ON_OBST(BFNamelistOb): 23 | label = "OBST" 24 | description = "Obstruction" 25 | collection = "Obstacles" 26 | enum_id = 1000 27 | fds_label = "OBST" 28 | bf_params = ( 29 | OP_namelist_cls, 30 | OP_ID, 31 | OP_FYI, 32 | OP_SURF_ID, 33 | OP_XB, 34 | OP_XB_voxel_size, 35 | OP_XB_center_voxels, 36 | OP_ID_suffix, 37 | OP_other_MULT_ID, 38 | OP_RGB_override, 39 | OP_COLOR_override, 40 | OP_TRANSPARENCY_override, 41 | OP_other, 42 | ) 43 | bf_import_order = 100 44 | -------------------------------------------------------------------------------- /lang/ON_PROF.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistOb 5 | from .bf_object import OP_namelist_cls, OP_ID, OP_FYI, OP_ID_suffix, OP_other 6 | from .ON_DEVC import OP_DEVC_QUANTITY 7 | from .OP_XYZ import OP_XYZ 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class ON_PROF(BFNamelistOb): 13 | label = "PROF" 14 | description = "Wall profile output" 15 | collection = "Output" 16 | enum_id = 1013 17 | fds_label = "PROF" 18 | bf_params = ( 19 | OP_namelist_cls, 20 | OP_ID, 21 | OP_FYI, 22 | OP_DEVC_QUANTITY, 23 | OP_XYZ, 24 | OP_ID_suffix, 25 | OP_other, 26 | ) 27 | bf_other = {"appearance": "WIRE"} 28 | -------------------------------------------------------------------------------- /lang/ON_SLCF.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, re 4 | from bpy.types import Object 5 | from bpy.props import BoolProperty 6 | from ..types import BFParam, BFNamelistOb, BFException 7 | from .bf_object import OP_namelist_cls, OP_ID, OP_FYI, OP_ID_suffix, OP_other 8 | from .ON_DEVC import OP_DEVC_QUANTITY 9 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 10 | from .OP_PB import OP_PB, OP_PBX, OP_PBY, OP_PBZ 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class OP_SLCF_VECTOR(BFParam): 16 | label = "VECTOR" 17 | description = "Create animated vectors" 18 | fds_label = "VECTOR" 19 | fds_default = False 20 | bpy_type = Object 21 | bpy_prop = BoolProperty 22 | bpy_idname = "bf_slcf_vector" 23 | 24 | 25 | class OP_SLCF_CELL_CENTERED(BFParam): 26 | label = "CELL_CENTERED" 27 | description = "Output the actual cell-centered data with no averaging" 28 | fds_label = "CELL_CENTERED" 29 | fds_default = False 30 | bpy_type = Object 31 | bpy_prop = BoolProperty 32 | bpy_idname = "bf_slcf_cell_centered" 33 | 34 | 35 | class ON_SLCF(BFNamelistOb): 36 | label = "SLCF" 37 | description = "Slice file" 38 | collection = "Output" 39 | enum_id = 1012 40 | fds_label = "SLCF" 41 | bf_params = ( 42 | OP_namelist_cls, 43 | OP_ID, 44 | OP_FYI, 45 | OP_DEVC_QUANTITY, 46 | OP_SLCF_VECTOR, 47 | OP_SLCF_CELL_CENTERED, 48 | OP_XB, 49 | OP_XB_voxel_size, 50 | OP_XB_center_voxels, 51 | OP_PB, 52 | OP_PBX, 53 | OP_PBY, 54 | OP_PBZ, 55 | OP_ID_suffix, 56 | OP_other, 57 | ) 58 | bf_other = {"appearance": "WIRE"} 59 | -------------------------------------------------------------------------------- /lang/ON_VENT.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, bpy 4 | from bpy.types import Object 5 | from bpy.props import PointerProperty 6 | from ..types import BFParam, BFNamelistOb, BFException 7 | from .bf_object import ( 8 | OP_namelist_cls, 9 | OP_ID, 10 | OP_FYI, 11 | OP_ID_suffix, 12 | OP_other, 13 | OP_RGB_override, 14 | OP_COLOR_override, 15 | OP_TRANSPARENCY_override, 16 | ) 17 | from .OP_SURF_ID import OP_SURF_ID 18 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 19 | from .OP_XYZ import OP_XYZ 20 | from .OP_PB import OP_PB, OP_PBX, OP_PBY, OP_PBZ 21 | from .ON_MULT import OP_other_MULT_ID 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | class OP_VENT_OBST_ID(BFParam): 27 | label = "OBST_ID" 28 | description = "Specify OBST on which projecting the condition" 29 | fds_label = "OBST_ID" 30 | bpy_type = Object 31 | bpy_prop = PointerProperty 32 | bpy_idname = "bf_vent_obst_id" 33 | bpy_other = {"type": Object} 34 | 35 | def get_value(self, context): 36 | if self.element.bf_vent_obst_id: 37 | return self.element.bf_vent_obst_id.name 38 | 39 | def set_value(self, context, value): 40 | if value: 41 | ob = bpy.data.objects.get(value) 42 | if ob: 43 | self.element.bf_vent_obst_id = ob 44 | else: 45 | raise BFException(self, f"Object <{value}> not found") 46 | else: 47 | self.element.bf_vent_obst_id = None 48 | 49 | 50 | class ON_VENT(BFNamelistOb): 51 | label = "VENT" 52 | description = "Boundary condition patch" 53 | collection = "Obstacles" 54 | enum_id = 1010 55 | fds_label = "VENT" 56 | bf_params = ( 57 | OP_namelist_cls, 58 | OP_ID, 59 | OP_FYI, 60 | OP_SURF_ID, 61 | OP_VENT_OBST_ID, 62 | OP_XB, 63 | OP_XB_voxel_size, 64 | OP_XB_center_voxels, 65 | OP_XYZ, 66 | OP_PB, 67 | OP_PBX, 68 | OP_PBY, 69 | OP_PBZ, 70 | OP_ID_suffix, 71 | OP_other_MULT_ID, 72 | OP_RGB_override, 73 | OP_COLOR_override, 74 | OP_TRANSPARENCY_override, 75 | OP_other, 76 | ) 77 | bf_import_order = 300 78 | -------------------------------------------------------------------------------- /lang/ON_ZONE.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistOb 5 | from .bf_object import OP_namelist_cls, OP_ID, OP_FYI, OP_other 6 | from .OP_XYZ import OP_XYZ 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class ON_ZONE(BFNamelistOb): 12 | label = "ZONE" 13 | description = "Pressure zone" 14 | collection = "Domain" 15 | enum_id = 1016 16 | fds_label = "ZONE" 17 | bf_params = OP_namelist_cls, OP_ID, OP_FYI, OP_XYZ, OP_other 18 | bf_other = {"appearance": "WIRE"} 19 | -------------------------------------------------------------------------------- /lang/ON_other.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, re 4 | from bpy.types import Object 5 | from bpy.props import StringProperty 6 | from ..types import BFParam, BFNamelistOb, BFException 7 | from .bf_object import ( 8 | OP_namelist_cls, 9 | OP_ID, 10 | OP_FYI, 11 | OP_ID_suffix, 12 | OP_other, 13 | OP_RGB_override, 14 | OP_COLOR_override, 15 | OP_TRANSPARENCY_override, 16 | ) 17 | from .OP_SURF_ID import OP_SURF_ID 18 | from .OP_XB import OP_XB, OP_XB_voxel_size, OP_XB_center_voxels 19 | from .OP_XYZ import OP_XYZ 20 | from .OP_PB import OP_PB, OP_PBX, OP_PBY, OP_PBZ 21 | from .ON_MULT import OP_other_MULT_ID 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | class OP_other_namelist(BFParam): 27 | label = "Label" 28 | description = "Other namelist label, eg " 29 | bpy_type = Object 30 | bpy_prop = StringProperty 31 | bpy_idname = "bf_other_namelist" 32 | bpy_default = "ABCD" 33 | bpy_other = {"maxlen": 4} 34 | 35 | def check(self, context): 36 | if not re.match("^[A-Z0-9_]{4}$", self.element.bf_other_namelist): 37 | raise BFException( 38 | self, 39 | f"Malformed other namelist label <{self.element.bf_other_namelist}>", 40 | ) 41 | 42 | 43 | class ON_other(BFNamelistOb): 44 | label = "Other" 45 | description = "Other namelist" 46 | enum_id = 1007 47 | bf_params = ( 48 | OP_namelist_cls, 49 | OP_other_namelist, 50 | OP_ID, 51 | OP_FYI, 52 | OP_SURF_ID, 53 | OP_XB, 54 | OP_XB_voxel_size, 55 | OP_XB_center_voxels, 56 | OP_XYZ, 57 | OP_PB, 58 | OP_PBX, 59 | OP_PBY, 60 | OP_PBZ, 61 | OP_ID_suffix, 62 | OP_other_MULT_ID, 63 | OP_RGB_override, 64 | OP_COLOR_override, 65 | OP_TRANSPARENCY_override, 66 | OP_other, 67 | ) 68 | 69 | @property 70 | def fds_label(self): 71 | return self.element.bf_other_namelist 72 | -------------------------------------------------------------------------------- /lang/OP_PB/OP_PB.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Object 5 | from bpy.props import EnumProperty, BoolProperty 6 | from ...config import LP 7 | from ...types import BFParam, FDSParam, FDSList, FDSMulti 8 | from ... import utils 9 | from .ob_to_pbs import ob_to_pbs 10 | from .pbs_to_ob import pbs_to_ob 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def update_bf_pb(ob, context): 16 | # Rm tmp geometry and cache 17 | utils.geometry.rm_geometric_cache(ob=ob) 18 | if ob.bf_has_tmp: 19 | utils.geometry.rm_tmp_objects() 20 | # Prevent double multiparam 21 | if ob.bf_pb == "PLANES" and ob.bf_pb_export: 22 | if ob.bf_xb in ("VOXELS", "FACES", "PIXELS", "EDGES"): 23 | ob["bf_xb_export"] = False # prevent multiple update 24 | if ob.bf_xyz == "VERTICES": 25 | ob["bf_xyz_export"] = False 26 | return 27 | 28 | 29 | class OP_PB(BFParam): 30 | label = "PBX, PBY, PBZ" 31 | description = "Export as planes" 32 | fds_label = None # used for importing 33 | bpy_type = Object 34 | bpy_idname = "bf_pb" 35 | bpy_prop = EnumProperty 36 | bpy_other = { 37 | "update": update_bf_pb, 38 | "items": ( 39 | ("PLANES", "Planes", "Planes, one for each face of this object", 100), 40 | ), 41 | } 42 | bpy_export = "bf_pb_export" 43 | bpy_export_default = False 44 | 45 | def to_fds_list(self, context) -> FDSList: 46 | if not self.get_exported(context): 47 | return FDSList() 48 | ob = self.element 49 | hids, pbs, msgs = ob_to_pbs(context, ob, bf_pb=ob.bf_pb) 50 | match len(pbs): 51 | case 0: 52 | return FDSList() 53 | case 1: 54 | return FDSParam(fds_label=pbs[0][0], value=pbs[0][1], precision=LP) 55 | case _: 56 | iterable=( 57 | (FDSParam(fds_label="ID", value=hid) for hid in hids), 58 | (FDSParam(fds_label=axis, value=pb, precision=LP) for axis, pb in pbs), 59 | ) 60 | return FDSMulti(iterable=iterable, msgs=msgs) 61 | 62 | def set_value(self, context, value=None): 63 | bf_pb = pbs_to_ob( 64 | context=context, 65 | ob=self.element, 66 | pbs=((self.fds_label, value),), 67 | set_origin=True, 68 | add=True, 69 | ) 70 | self.element.bf_pb = bf_pb 71 | self.element.bf_pb_export = True 72 | 73 | 74 | class OP_PBX(OP_PB): # only for importing 75 | fds_label = "PBX" 76 | bpy_prop = None # already defined 77 | 78 | def draw(self, context, layout): 79 | pass 80 | 81 | def get_exported(self, context): 82 | return False 83 | 84 | 85 | class OP_PBY(OP_PBX): # only for importing 86 | fds_label = "PBY" 87 | 88 | 89 | class OP_PBZ(OP_PBX): # only for importing 90 | fds_label = "PBZ" 91 | -------------------------------------------------------------------------------- /lang/OP_PB/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .OP_PB import OP_PB, OP_PBX, OP_PBY, OP_PBZ 4 | from .ob_to_pbs import ob_to_pbs 5 | from .pbs_to_ob import pbs_to_ob 6 | -------------------------------------------------------------------------------- /lang/OP_PB/ob_to_pbs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, translate Blender object geometry to FDS PB notation. 5 | """ 6 | 7 | import logging 8 | from ... import config 9 | from ...types import BFException 10 | from ..OP_XB.ob_to_xbs import _ob_to_xbs_faces 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | EFD = config.FLAT_DIFFERENCE 15 | 16 | def _ob_to_pbs_planes(context, ob, world) -> tuple((list, list)): 17 | """! 18 | Transform Object faces to pbs notation. 19 | @param context: the Blender context. 20 | @param ob: the Blender object. 21 | @param world: True to return the object in world coordinates. 22 | @return the pbs notation and messages. 23 | """ 24 | # pbs is: ("PBX", 3.5), ("PBX", 4.), ("PBY", .5) ... 25 | pbs = list() 26 | xbs, _ = _ob_to_xbs_faces(context, ob, world) 27 | 28 | # For each face build a plane... 29 | for xb in xbs: 30 | if abs(xb[1] - xb[0]) < EFD: 31 | pbs.append(("PBX", xb[0])) 32 | elif abs(xb[3] - xb[2]) < EFD: 33 | pbs.append(("PBY", xb[2])) 34 | elif abs(xb[5] - xb[4]) < EFD: 35 | pbs.append(("PBZ", xb[4])) 36 | else: 37 | raise ValueError( 38 | "BFDS: Building planes impossible, problem in ob_to_xbs_faces." 39 | ) 40 | pbs.sort() 41 | if not pbs: 42 | raise BFException(ob, "PB*: No exported planes!") 43 | msgs = list((f"PB* Planes: {len(pbs)}",)) 44 | return pbs, msgs 45 | 46 | 47 | def ob_to_pbs(context, ob, bf_pb, world=True) -> tuple((list, list)): 48 | """! 49 | Transform Object geometry according to bf_pb to pbs notation. 50 | @param context: the Blender context. 51 | @param ob: the Blender object. 52 | @param bf_pb: string in (PLANES,) 53 | @param world: True to return the object in world coordinates. 54 | @return the pbs notation and messages. 55 | """ 56 | # Calc pbs and msgs 57 | # pbs is: ("PBX", 3.5), ("PBX", 4.), ("PBY", .5) ... 58 | pbs, msgs = tuple(), tuple() 59 | if ob.bf_pb_export: 60 | pbs, msgs = _ob_to_pbs_planes(context, ob, world=world) 61 | 62 | # Calc hids 63 | n = ob.name 64 | match ob.bf_id_suffix: 65 | case "IDI": 66 | hids = (f"{n}_{i}" for i, _ in enumerate(pbs)) 67 | case _: 68 | hids = ( 69 | {"PBX": f"{n}_x{pb:+.3f}", "PBY": f"{n}_y{pb:+.3f}", "PBZ": f"{n}_z{pb:+.3f}"}[axis] 70 | for axis, pb in pbs 71 | ) 72 | 73 | return tuple(hids), tuple(pbs), tuple(msgs) 74 | -------------------------------------------------------------------------------- /lang/OP_PB/pbs_to_ob.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, translate geometry from FDS PB notation to a Blender mesh. 5 | """ 6 | 7 | import logging 8 | from ..OP_XB.xbs_to_ob import xbs_to_ob 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def pbs_to_ob(context, ob, pbs, add=False, set_origin=False) -> str(): 14 | """! 15 | Import pbs planes (("PBX",x3,), ("PBX",x7,), ("PBY",y9,), ...) into existing Blender Object. 16 | @param context: the Blender context. 17 | @param ob: the Blender Object. 18 | @param pbs: the pbs planes. 19 | @param add: add to existing Mesh. 20 | @param set_origin: set reasonable origin. 21 | @return "PLANES" 22 | """ 23 | if not pbs: 24 | return "PLANES" 25 | # Convert the geometry 26 | xbs, sl = list(), context.scene.unit_settings.scale_length 27 | for pb in pbs: 28 | match pb[0]: 29 | case "PBX": 30 | xbs.append((pb[1], pb[1], -sl, +sl, -sl, +sl)) # PBX is 0 31 | case "PBY": 32 | xbs.append((-sl, +sl, pb[1], pb[1], -sl, +sl)) # PBY is 1 33 | case "PBZ": 34 | xbs.append((-sl, +sl, -sl, +sl, pb[1], pb[1])) # PBZ is 2 35 | case _: 36 | raise AssertionError(f"Unrecognized PB* <{pb}>") 37 | # Inject geometry 38 | xbs_to_ob(context=context, ob=ob, xbs=xbs, bf_xb="FACES", add=add, set_origin=set_origin) 39 | return "PLANES" 40 | -------------------------------------------------------------------------------- /lang/OP_SURF_ID.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, bpy 4 | from bpy.types import Object 5 | from ..types import BFParam, BFException 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class OP_SURF_ID(BFParam): 11 | label = "SURF_ID" 12 | description = "Reference to SURF" 13 | fds_label = "SURF_ID" 14 | bpy_type = Object 15 | bpy_idname = "active_material" 16 | bpy_export = "bf_surf_id_export" 17 | bpy_export_default = False 18 | 19 | def get_value(self, context): 20 | if self.element.active_material: 21 | return self.element.active_material.name 22 | 23 | def set_value(self, context, value): 24 | ob = self.element 25 | if value is None: 26 | ob.active_material = None 27 | else: 28 | try: # TODO use utils 29 | ma = bpy.data.materials.get(value) 30 | except IndexError: 31 | raise BFException(self, f"Blender Material <{value}> does not exists") 32 | else: 33 | ob.active_material = ma 34 | ob.bf_surf_id_export = True 35 | 36 | def get_exported(self, context): 37 | ob = self.element 38 | return ob.bf_surf_id_export and bool(ob.active_material) 39 | 40 | def check(self, context): 41 | ob = self.element 42 | if ( 43 | ob.bf_surf_id_export 44 | and ob.active_material 45 | and not ob.active_material.bf_surf_export 46 | ): 47 | raise BFException( 48 | self, f"Referenced SURF <{ob.active_material.name}> not exported" 49 | ) 50 | 51 | def draw_operators(self, context, layout): 52 | layout.operator("object.bf_clean_ma_slots", icon="BRUSH_DATA", text="") 53 | -------------------------------------------------------------------------------- /lang/OP_XB/OP_XB.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Object 5 | from bpy.props import EnumProperty, BoolProperty, FloatProperty 6 | from ...config import LP 7 | from ...types import BFParam, FDSParam, FDSList, FDSMulti 8 | from ... import utils 9 | from .ob_to_xbs import ob_to_xbs 10 | from .xbs_to_ob import xbs_to_ob 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def update_bf_xb(ob, context): 16 | # Rm tmp geometry and cache 17 | utils.geometry.rm_geometric_cache(ob=ob) 18 | if ob.bf_has_tmp: 19 | utils.geometry.rm_tmp_objects() 20 | # Prevent double multiparam 21 | if ob.bf_xb in ("VOXELS", "FACES", "PIXELS", "EDGES") and ob.bf_xb_export: 22 | if ob.bf_xyz == "VERTICES": 23 | ob["bf_xyz_export"] = False # prevent multiple update 24 | if ob.bf_pb == "PLANES": 25 | ob["bf_pb_export"] = False 26 | return 27 | 28 | 29 | class OP_XB_voxel_size(BFParam): 30 | label = "Voxel/Pixel Size" 31 | description = "Voxel/pixel size for the current Object" 32 | bpy_type = Object 33 | bpy_idname = "bf_xb_voxel_size" 34 | bpy_prop = FloatProperty 35 | bpy_default = 0.1 36 | bpy_other = { 37 | "step": 1.0, 38 | "precision": LP, 39 | "min": 0.001, 40 | "max": 20.0, 41 | "unit": "LENGTH", 42 | "update": update_bf_xb, 43 | } 44 | bpy_export = "bf_xb_custom_voxel" 45 | bpy_export_default = False 46 | 47 | def get_active(self, context): 48 | ob = self.element 49 | return ob.bf_xb_export and ob.bf_xb in ("VOXELS", "PIXELS") 50 | 51 | 52 | class OP_XB_center_voxels(BFParam): 53 | label = "Center Voxels/Pixels" 54 | description = "Center voxels/pixels to Object bounding box" 55 | bpy_type = Object 56 | bpy_idname = "bf_xb_center_voxels" 57 | bpy_prop = BoolProperty 58 | bpy_default = False 59 | bpy_other = {"update": update_bf_xb} 60 | 61 | def get_active(self, context): 62 | ob = self.element 63 | return ob.bf_xb_export and ob.bf_xb in ("VOXELS", "PIXELS") 64 | 65 | 66 | class OP_XB(BFParam): 67 | label = "XB" 68 | description = "Export as volumes/faces/edges" 69 | fds_label = "XB" 70 | bpy_type = Object 71 | bpy_idname = "bf_xb" 72 | bpy_prop = EnumProperty 73 | bpy_other = { 74 | "update": update_bf_xb, 75 | "items": ( 76 | ("BBOX", "Bounding Box", "Export object bounding box", 100), 77 | ("VOXELS", "Voxels", "Export voxels from voxelized solid Object", 200), 78 | ("FACES", "Faces", "Export faces, one for each face", 300), 79 | ("PIXELS", "Pixels", "Export pixels from pixelized flat Object", 400), 80 | ("EDGES", "Edges", "Export segments, one for each edge", 500), 81 | ), 82 | } 83 | bpy_export = "bf_xb_export" 84 | bpy_export_default = False 85 | 86 | def to_fds_list(self, context) -> FDSList: 87 | if not self.get_exported(context): 88 | return FDSList() 89 | ob = self.element 90 | hids, xbs, msgs = ob_to_xbs(context=context, ob=ob, bf_xb=ob.bf_xb) 91 | match len(xbs): 92 | case 0: 93 | return FDSList() 94 | case 1: 95 | return FDSParam(fds_label="XB", value=xbs[0], precision=LP) 96 | case _: 97 | iterable=( 98 | (FDSParam(fds_label="ID", value=hid) for hid in hids), 99 | (FDSParam(fds_label="XB", value=xb, precision=LP) for xb in xbs), 100 | ) 101 | return FDSMulti(iterable=iterable, msgs=msgs) 102 | 103 | def set_value(self, context, value=None): 104 | ob = self.element 105 | bf_xb = xbs_to_ob( 106 | context=context, 107 | ob=ob, 108 | xbs=(value,), 109 | set_origin=True, 110 | add=True 111 | ) 112 | ob.bf_xb = bf_xb 113 | ob.bf_xb_export = True 114 | 115 | 116 | class OP_XB_BBOX(OP_XB): # should always work, even without bf_xb_export and bf_xb 117 | label = "XB" 118 | description = "Export as object bounding box (BBOX)" 119 | fds_label = "XB" 120 | bpy_type = Object 121 | bpy_idname = None 122 | bpy_export = None 123 | 124 | def to_fds_list(self, context) -> FDSList: 125 | ob = self.element 126 | xb = utils.geometry.get_bbox_xb(context=context, ob=ob, world=True) 127 | return FDSParam(fds_label="XB", value=xb, precision=LP) 128 | 129 | def draw(self, context, layout): # draw label only 130 | row = layout.split(factor=0.4) 131 | row.alignment = "RIGHT" 132 | row.label(text=f"XB") 133 | row.alignment = "EXPAND" 134 | row.label(text=f"Bounding Box") 135 | -------------------------------------------------------------------------------- /lang/OP_XB/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .OP_XB import ( 4 | OP_XB, 5 | OP_XB_voxel_size, 6 | OP_XB_center_voxels, 7 | OP_XB_BBOX, 8 | ) 9 | from .ob_to_xbs import ob_to_xbs 10 | from .xbs_to_ob import xbs_to_ob 11 | -------------------------------------------------------------------------------- /lang/OP_XB/calc_pixels.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, pixelization algorithms. 5 | """ 6 | 7 | import bpy, logging 8 | from ...types import BFException 9 | from ... import utils 10 | from .calc_voxels import _get_voxel_size, get_voxels 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def get_pixels(context, ob): 16 | """! 17 | Get pixels from flat object in xbs format. 18 | @param context: the Blender context. 19 | @param ob: the Blender object. 20 | @return the xbs and the voxel size. 21 | """ 22 | log.debug(f"Get pixels in Object <{ob.name}>...") 23 | # Check object and init 24 | if ob.type not in {"MESH", "CURVE", "SURFACE", "FONT", "META"}: 25 | raise BFException(ob, "Object can not be converted to mesh.") 26 | if not ob.data.vertices: 27 | raise BFException(ob, "Empty object, no available geometry") 28 | voxel_size = _get_voxel_size(context, ob) 29 | # Get evaluated ob (eg. modifiers applied) and its Mesh 30 | dg = context.evaluated_depsgraph_get() 31 | ob_eval = ob.evaluated_get(dg) # no need to clean up, it is tmp 32 | me_eval = bpy.data.meshes.new_from_object(ob_eval) # static 33 | # Create a new Object, in world coo 34 | ob_copy = bpy.data.objects.new(f"{ob.name}_voxels_tmp", me_eval) 35 | ob_copy.data.transform(ob.matrix_world) 36 | context.collection.objects.link(ob_copy) 37 | # Set data for ob_copy 38 | ob_copy.bf_xb_custom_voxel = ob.bf_xb_custom_voxel 39 | ob_copy.bf_xb_center_voxels = ob.bf_xb_center_voxels 40 | ob_copy.bf_xb_voxel_size = ob.bf_xb_voxel_size 41 | # Get flat axis of evaluated ob 42 | flat_axis = _get_flat_axis(ob_copy, voxel_size) 43 | # Check how flat it is 44 | if ob_copy.dimensions[flat_axis] > voxel_size / 2.0: 45 | bpy.data.meshes.remove(ob_copy.data, do_unlink=True) 46 | raise BFException(ob, "Object is not flat enough.") 47 | # Get origin for flat xbs 48 | xb = utils.geometry.get_bbox_xb(context, ob_copy, world=True) 49 | flat_origin = ( 50 | (xb[1] + xb[0]) / 2.0, 51 | (xb[3] + xb[2]) / 2.0, 52 | (xb[5] + xb[4]) / 2.0, 53 | ) 54 | # Add solidify modifier 55 | _add_solidify_mod(context, ob_copy, voxel_size) 56 | # Voxelize (already corrected for unit_settings) 57 | try: 58 | xbs, voxel_size = get_voxels(context=context, ob=ob_copy) 59 | except BFException as err: 60 | raise BFException(ob, f"No pixel created: {err}") 61 | finally: 62 | bpy.data.meshes.remove(ob_copy.data, do_unlink=True) # clean up 63 | # Flatten the solidified object xbs 64 | choice = (_x_flatten_xbs, _y_flatten_xbs, _z_flatten_xbs)[flat_axis] 65 | xbs = choice(xbs, flat_origin) 66 | return xbs, voxel_size 67 | 68 | 69 | def _add_solidify_mod(context, ob, voxel_size): 70 | """! 71 | Add new solidify modifier. 72 | @param context: the Blender context. 73 | @param ob: the Blender object. 74 | @param voxel_size: the voxel size of the object. 75 | @return the modifier. 76 | """ 77 | mo = ob.modifiers.new("solidify_tmp", "SOLIDIFY") 78 | mo.thickness = voxel_size * 3 79 | mo.offset = 0.0 # centered 80 | return mo 81 | 82 | 83 | def _get_flat_axis(ob, voxel_size): 84 | """! 85 | Get object flat axis. 86 | @param ob: the Blender object. 87 | @param voxel_size: the voxel size of the object. 88 | @return the object flat axis. 89 | """ 90 | dimensions = ob.dimensions 91 | choices = [ 92 | (dimensions[0], 0), # object faces are normal to x axis 93 | (dimensions[1], 1), # ... to y axis 94 | (dimensions[2], 2), # ... to z axis 95 | ] 96 | choices.sort(key=lambda k: k[0]) # sort by dimension 97 | return choices[0][1] 98 | 99 | 100 | def _x_flatten_xbs(xbs, flat_origin): 101 | """! 102 | Flatten voxels to obtain pixels (normal to x axis) at flat_origin height. 103 | @param xbs: voxelized xbs. 104 | @param flat_origin: local origin of flat object. 105 | @return xbs. 106 | """ 107 | return [[flat_origin[0], flat_origin[0], xb[2], xb[3], xb[4], xb[5]] for xb in xbs] 108 | 109 | 110 | def _y_flatten_xbs(xbs, flat_origin): 111 | """! 112 | Flatten voxels to obtain pixels (normal to y axis) at flat_origin height. 113 | @param xbs: voxelized xbs. 114 | @param flat_origin: local origin of flat object. 115 | @return xbs. 116 | """ 117 | return [[xb[0], xb[1], flat_origin[1], flat_origin[1], xb[4], xb[5]] for xb in xbs] 118 | 119 | 120 | def _z_flatten_xbs(xbs, flat_origin): 121 | """! 122 | Flatten voxels to obtain pixels (normal to z axis) at flat_origin height. 123 | @param xbs: voxelized xbs. 124 | @param flat_origin: local origin of flat object. 125 | @return xbs. 126 | """ 127 | return [[xb[0], xb[1], xb[2], xb[3], flat_origin[2], flat_origin[2]] for xb in xbs] 128 | -------------------------------------------------------------------------------- /lang/OP_XB/xbs_to_ob.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, translate geometry from FDS XB notation to a Blender mesh. 5 | """ 6 | 7 | import bmesh, logging 8 | from mathutils import Matrix, Vector 9 | from ... import config 10 | from ...types import BFException 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | EFD = config.FLAT_DIFFERENCE 15 | 16 | 17 | def _xbs_edges_to_bm(bm, xbs, scale_length): 18 | for xb in xbs: 19 | x0, x1, y0, y1, z0, z1 = (c / scale_length for c in xb) 20 | v0 = bm.verts.new((x0, y0, z0)) 21 | v1 = bm.verts.new((x1, y1, z1)) 22 | bm.edges.new((v0, v1)) 23 | 24 | def _xbs_faces_to_bm(bm, xbs, scale_length): 25 | for xb in xbs: 26 | x0, x1, y0, y1, z0, z1 = (c / scale_length for c in xb) 27 | if abs(x1 - x0) <= EFD: # i face 28 | v0 = bm.verts.new((x0, y0, z0)) 29 | v1 = bm.verts.new((x0, y1, z0)) 30 | v2 = bm.verts.new((x0, y1, z1)) 31 | v3 = bm.verts.new((x0, y0, z1)) 32 | elif abs(y1 - y0) <= EFD: # j face 33 | v0 = bm.verts.new((x0, y0, z0)) 34 | v1 = bm.verts.new((x1, y0, z0)) 35 | v2 = bm.verts.new((x1, y0, z1)) 36 | v3 = bm.verts.new((x0, y0, z1)) 37 | elif abs(z1 - z0) <= EFD: # k face 38 | v0 = bm.verts.new((x0, y0, z0)) 39 | v1 = bm.verts.new((x0, y1, z0)) 40 | v2 = bm.verts.new((x1, y1, z0)) 41 | v3 = bm.verts.new((x1, y0, z0)) 42 | else: 43 | raise AssertionError(f"Unrecognized face <{xb}> in XB") 44 | bm.faces.new((v0, v1, v2, v3)) 45 | 46 | def _xbs_bbox_to_bm(bm, xbs, scale_length): 47 | for xb in xbs: 48 | x0, x1, y0, y1, z0, z1 = (c / scale_length for c in xb) 49 | v000 = bm.verts.new((x0, y0, z0)) 50 | v100 = bm.verts.new((x1, y0, z0)) 51 | v110 = bm.verts.new((x1, y1, z0)) 52 | v010 = bm.verts.new((x0, y1, z0)) 53 | v001 = bm.verts.new((x0, y0, z1)) 54 | v101 = bm.verts.new((x1, y0, z1)) 55 | v111 = bm.verts.new((x1, y1, z1)) 56 | v011 = bm.verts.new((x0, y1, z1)) 57 | bm.faces.new((v000, v001, v011, v010)) # -x 0 58 | bm.faces.new((v111, v101, v100, v110)) # +x 1 59 | bm.faces.new((v000, v100, v101, v001)) # -y 2 60 | bm.faces.new((v111, v110, v010, v011)) # +y 3 61 | bm.faces.new((v000, v010, v110, v100)) # -z 4 bottom 62 | bm.faces.new((v111, v011, v001, v101)) # +z 5 top 63 | 64 | def _is_faces(xbs, scale_length): 65 | """! 66 | Check if xbs are only faces. 67 | """ 68 | for xb in xbs: 69 | x0, x1, y0, y1, z0, z1 = (c / scale_length for c in xb) 70 | if abs(x1 - x0) > EFD and abs(y1 - y0) > EFD and abs(z1 - z0) > EFD: 71 | return False 72 | return True 73 | 74 | _xbs_to_bm = { 75 | "BBOX": _xbs_bbox_to_bm, 76 | "VOXELS": _xbs_bbox_to_bm, 77 | "PIXELS": _xbs_bbox_to_bm, 78 | "EDGES": _xbs_edges_to_bm, 79 | "FACES": _xbs_faces_to_bm, 80 | } 81 | 82 | def xbs_to_ob(context, ob, xbs, bf_xb=None, add=False, set_origin=False): 83 | """! 84 | Set xbs geometry ((x0,x1,y0,y1,z0,z1,), ...) to Blender Object. 85 | @param context: the Blender context. 86 | @param ob: the Blender object. 87 | @param xbs: the xbs values. 88 | @param bf_xb: the xb parameter between BBOX, VOXELS, FACES, PIXELS, EDGES. 89 | @param add: add to existing Mesh. 90 | @param set_origin: set reasonable origin. 91 | @return the new xb parameter between BBOX, VOXELS, FACES, PIXELS, EDGES. 92 | """ 93 | if not xbs: 94 | return "BBOX" 95 | # Generate the new bmesh 96 | bm = bmesh.new() 97 | if add: # get existing data in world coo 98 | bm.from_mesh(ob.data) 99 | bm.transform(ob.matrix_world) 100 | scale_length = context.scene.unit_settings.scale_length 101 | if not bf_xb: 102 | if _is_faces(xbs=xbs, scale_length=scale_length): 103 | bf_xb = "FACES" 104 | else: 105 | bf_xb = "BBOX" 106 | # Inject geometry 107 | _xbs_to_bm[bf_xb](bm=bm, xbs=xbs, scale_length=scale_length) 108 | # Transform to local coo 109 | if set_origin: 110 | origin = Vector((xbs[0][0],xbs[0][2],xbs[0][4])) 111 | ma = Matrix.Translation(-origin) 112 | ob.matrix_world = Matrix.Translation(+origin) 113 | else: 114 | ma = ob.matrix_world.inverted() 115 | bm.transform(ma) 116 | bm.to_mesh(ob.data) 117 | bm.free() 118 | return bf_xb 119 | 120 | def set_materials(ob) -> None: 121 | """! 122 | Set Blender Materials from Material Slots to Object Mesh faces. 123 | @param ob: the Blender object. 124 | """ 125 | me = ob.data 126 | match len(me.materials): 127 | case 0: # no SURF_ID 128 | return 129 | case 1: # SURF_ID = 'A' 130 | for face in me.polygons: 131 | face.material_index = 0 132 | case 3: # SURF_IDS = 'A', 'B', 'C' (top, sides, bottom) 133 | if len(me.polygons) > 6: 134 | raise AssertionError(f"Too many polygons in Object {ob.name}: {len(me.polygons)}") 135 | for iface, face in enumerate(me.polygons): 136 | if iface % 6 == 5: 137 | face.material_index = 0 # top 138 | elif iface % 6 == 4: 139 | face.material_index = 2 # bottom 140 | else: 141 | face.material_index = 1 # sides 142 | case 6: # SURF_ID6 = ... (x0, x1, y0, y1, z0, z1) 143 | if len(me.polygons) > 6: 144 | raise AssertionError(f"Too many polygons in Object {ob.name}: {len(me.polygons)}") 145 | for iface, face in enumerate(me.polygons): 146 | face.material_index = iface % 6 147 | case _: 148 | raise BFException(ob, f"Wrong number of Material Slots") 149 | -------------------------------------------------------------------------------- /lang/OP_XYZ/OP_XYZ.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Object 5 | from bpy.props import EnumProperty, BoolProperty, FloatProperty 6 | from ...config import LP 7 | from ...types import BFParam, FDSParam, FDSList, FDSMulti 8 | from ... import utils 9 | from .ob_to_xyzs import ob_to_xyzs 10 | from .xyzs_to_ob import xyzs_to_ob 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def update_bf_xyz(ob, context): 16 | # Rm tmp geometry and cache 17 | utils.geometry.rm_geometric_cache(ob=ob) 18 | if ob.bf_has_tmp: 19 | utils.geometry.rm_tmp_objects() 20 | # Prevent double multiparam 21 | if ob.bf_xyz == "VERTICES" and ob.bf_xyz_export: 22 | if ob.bf_xb in ("VOXELS", "FACES", "PIXELS", "EDGES"): 23 | ob["bf_xb_export"] = False # prevent multiple update 24 | if ob.bf_pb == "PLANES": 25 | ob["bf_pb_export"] = False 26 | return 27 | 28 | 29 | class OP_XYZ(BFParam): 30 | label = "XYZ" 31 | description = "Export as points" 32 | fds_label = "XYZ" 33 | bpy_type = Object 34 | bpy_idname = "bf_xyz" 35 | bpy_prop = EnumProperty 36 | bpy_other = { 37 | "update": update_bf_xyz, 38 | "items": ( 39 | ("CENTER", "Center", "Point, center point of this object", 100), 40 | ("VERTICES", "Vertices", "Points, one for each vertex of this object", 200), 41 | ), 42 | } 43 | bpy_export = "bf_xyz_export" 44 | bpy_export_default = False 45 | 46 | def to_fds_list(self, context) -> FDSList: 47 | if not self.get_exported(context): 48 | return FDSList() 49 | ob = self.element 50 | hids, xyzs, msgs = ob_to_xyzs(context=context, ob=ob, bf_xyz=ob.bf_xyz) 51 | match len(xyzs): 52 | case 0: 53 | return FDSList() 54 | case 1: 55 | return FDSParam(fds_label="XYZ", value=xyzs[0], precision=LP) 56 | case _: 57 | iterable=( 58 | (FDSParam(fds_label="ID", value=hid) for hid in hids), 59 | (FDSParam(fds_label="XYZ", value=xyz, precision=LP) for xyz in xyzs), 60 | ) 61 | 62 | return FDSMulti(iterable=iterable, msgs=msgs) 63 | 64 | def set_value(self, context, value=None): 65 | bf_xyz = xyzs_to_ob( 66 | context=context, 67 | ob=self.element, 68 | xyzs=(value,), 69 | set_origin=True, 70 | add=True, 71 | ) 72 | self.element.bf_xyz = bf_xyz 73 | self.element.bf_xyz_export = True 74 | 75 | 76 | class OP_XYZ_center(OP_XYZ): 77 | description = "Export as points (center)" 78 | bpy_prop = None # do not redefine 79 | bf_xyz_idxs = (0,) # CENTER, VERTICES 80 | -------------------------------------------------------------------------------- /lang/OP_XYZ/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .OP_XYZ import OP_XYZ 4 | from .ob_to_xyzs import ob_to_xyzs 5 | from .xyzs_to_ob import xyzs_to_ob 6 | -------------------------------------------------------------------------------- /lang/OP_XYZ/ob_to_xyzs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, translate Blender object geometry to FDS XYZ notation. 5 | """ 6 | 7 | import logging 8 | from ...types import BFException 9 | from ... import utils 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def _ob_to_xyzs_vertices(context, ob, world) -> tuple((list, list)): 15 | """! 16 | Transform Object vertices to xyzs notation. 17 | @param context: the Blender context. 18 | @param ob: the Blender object. 19 | @param world: True to return the object in world coordinates. 20 | @return the xyzs notation and any error message: ((x0,y0,z0,), ...), 'Msg'. 21 | """ 22 | bm = utils.geometry.get_object_bmesh(context, ob, world=world) 23 | bm.verts.ensure_lookup_table() 24 | scale_length = context.scene.unit_settings.scale_length 25 | xyzs = list(tuple(c * scale_length for c in v.co) for v in bm.verts) 26 | bm.free() 27 | xyzs.sort() 28 | if not xyzs: 29 | raise BFException(ob, "XYZ: No exported vertices!") 30 | msgs = list((f"XYZ Vertices: {len(xyzs)}",)) 31 | return xyzs, msgs 32 | 33 | 34 | def _ob_to_xyzs_center(context, ob, world) -> tuple((list, list)): 35 | """! 36 | Transform Object center to xyzs notation. 37 | @param context: the Blender context. 38 | @param ob: the Blender object. 39 | @param world: True to return the object in world coordinates. 40 | @return the xyzs notation and any error message: ((x0,y0,z0,), ...), 'Msg'. 41 | """ 42 | scale_length = context.scene.unit_settings.scale_length 43 | if world: 44 | xyzs = list((tuple(l * scale_length for l in ob.location),)) 45 | else: 46 | xyzs = list(((0.0, 0.0, 0.0),)) 47 | msgs = list() 48 | return xyzs, msgs 49 | 50 | 51 | _choice_to_xyzs = {"CENTER": _ob_to_xyzs_center, "VERTICES": _ob_to_xyzs_vertices} 52 | 53 | 54 | def ob_to_xyzs(context, ob, bf_xyz, world=True) -> tuple((list, list, list)): 55 | """! 56 | Transform Object geometry according to bf_xyz to xyzs notation. 57 | @param context: the Blender context. 58 | @param ob: the Blender object. 59 | @param bf_xyz: string in (CENTER, VERTICES). 60 | @param world: True to return the object in world coordinates. 61 | @return the xyzs notation and any error message: ((x0,y0,z0,), ...), 'Msg'. 62 | """ 63 | # Calc xyzs and msgs 64 | xyzs, msgs = tuple(), tuple() 65 | if ob.bf_xyz_export: 66 | xyzs, msgs = _choice_to_xyzs[bf_xyz](context, ob, world) 67 | 68 | # Calc hids 69 | n = ob.name 70 | match ob.bf_id_suffix: 71 | case "IDI": 72 | hids = (f"{n}_{i}" for i, _ in enumerate(xyzs)) 73 | case "IDX": 74 | hids = (f"{n}_x{xyz[0]:+.3f}" for xyz in xyzs) 75 | case "IDY": 76 | hids = (f"{n}_y{xyz[1]:+.3f}" for xyz in xyzs) 77 | case "IDZ": 78 | hids = (f"{n}_z{xyz[2]:+.3f}" for xyz in xyzs) 79 | case "IDXY": 80 | hids = (f"{n}_x{xyz[0]:+.3f}_y{xyz[1]:+.3f}" for xyz in xyzs) 81 | case "IDXZ": 82 | hids = (f"{n}_x{xyz[0]:+.3f}_z{xyz[2]:+.3f}" for xyz in xyzs) 83 | case "IDYZ": 84 | hids = (f"{n}_y{xyz[1]:+.3f}_z{xyz[2]:+.3f}" for xyz in xyzs) 85 | case "IDXYZ": 86 | hids = (f"{n}_x{xyz[0]:+.3f}_y{xyz[1]:+.3f}_z{xyz[2]:+.3f}" for xyz in xyzs) 87 | case _: 88 | raise AssertionError(f"Unknown suffix <{ob.bf_id_suffix}>") 89 | 90 | return tuple(hids), tuple(xyzs), tuple(msgs) 91 | -------------------------------------------------------------------------------- /lang/OP_XYZ/xyzs_to_ob.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, translate geometry from FDS XYZ notation to a Blender mesh. 5 | """ 6 | 7 | import bmesh, logging 8 | from mathutils import Matrix, Vector 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def _xyzs_to_bm(bm, xyzs, scale_length): 14 | for xyz in xyzs: 15 | bm.verts.new(c / scale_length for c in xyz) 16 | 17 | 18 | def xyzs_to_ob(context, ob, xyzs, add=False, set_origin=False) -> str(): 19 | """! 20 | Import xyzs vertices ((x0,y0,z0,), ...) into existing Blender Object. 21 | @param context: the Blender context. 22 | @param ob: the Blender object. 23 | @param xyzs: the xyzs vertices. 24 | @param add: add to existing Mesh. 25 | @param set_origin: set reasonable origin. 26 | @return: the new bf_xyz in CENTER or VERTICES 27 | """ 28 | if not xyzs: 29 | return "VERTICES" 30 | # Generate the new bmesh 31 | bm = bmesh.new() 32 | if add: # get existing data in world coo 33 | bm.from_mesh(ob.data) 34 | bm.transform(ob.matrix_world) 35 | scale_length = context.scene.unit_settings.scale_length 36 | # Inject geometry 37 | if len(xyzs) > 1: 38 | _xyzs_to_bm(bm, xyzs, scale_length) 39 | # Transform to local coo 40 | if set_origin: 41 | origin = Vector(xyzs[0]) 42 | ma = Matrix.Translation(-origin) 43 | ob.matrix_world = Matrix.Translation(+origin) 44 | else: 45 | ma = ob.matrix_world.inverted() 46 | # same ob.matrix_world 47 | bm.transform(ma) 48 | bm.to_mesh(ob.data) 49 | bm.free() 50 | if len(xyzs) == 1: 51 | return "CENTER" 52 | else: 53 | return "VERTICES" 54 | -------------------------------------------------------------------------------- /lang/SN_CATF.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, os 4 | from bpy.types import Scene 5 | from .. import utils 6 | from ..types import BFParam, BFNamelistSc, BFNotImported 7 | 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class SP_CATF_files(BFParam): 13 | label = "Concatenated File Paths" 14 | description = "Concatenated file paths" 15 | fds_label = "OTHER_FILES" 16 | bpy_type = Scene 17 | 18 | def set_value(self, context, value=None): 19 | if not value: 20 | return 21 | if isinstance(value, str): 22 | value = (value,) 23 | for v in value: 24 | filepath = utils.io.transform_rfds_to_abs(context=context, filepath_rfds=v) 25 | context.scene.from_fds(context=context, filepath=filepath) 26 | 27 | 28 | class SN_CATF(BFNamelistSc): # for importing only 29 | label = "CATF" 30 | description = "Concatenated file paths" 31 | fds_label = "CATF" 32 | bf_params = (SP_CATF_files,) 33 | bf_import_order = 50 34 | 35 | def get_exported(self, context): 36 | return False 37 | -------------------------------------------------------------------------------- /lang/SN_DUMP/SN_DUMP.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, io 4 | from bpy.types import Scene 5 | from bpy.props import BoolProperty, FloatProperty, IntProperty 6 | from ...config import TIME_P 7 | from ... import utils 8 | from ...types import BFParam, BFParamOther, BFParamFYI, BFNamelistSc, FDSParam, FDSList 9 | from ...bl.ui_lists import WM_PG_bf_other, WM_UL_bf_other_items 10 | from .sc_to_ge1 import scene_to_ge1 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class SP_DUMP_FYI(BFParamFYI): 16 | bpy_type = Scene 17 | bpy_idname = "bf_dump_fyi" 18 | 19 | 20 | class SP_DUMP_render_file(BFParam): 21 | label = "Export Geometric Description File" 22 | description = "Export geometric description file GE1" 23 | fds_label = "RENDER_FILE" 24 | bpy_type = Scene 25 | bpy_idname = "bf_dump_render_file" 26 | bpy_prop = BoolProperty 27 | fds_default = False 28 | 29 | def to_fds_list(self, context) -> FDSList: 30 | if not self.get_exported(context): 31 | return FDSList() 32 | filepath = utils.io.transform_rbl_to_abs( 33 | context=context, 34 | filepath_rbl=context.scene.bf_config_directory, 35 | name=self.element.name, 36 | extension=".ge1", 37 | ) 38 | ge1_text = scene_to_ge1(context, self) 39 | utils.io.write_txt_file(filepath, ge1_text) 40 | return FDSParam(fds_label="RENDER_FILE", value=f"{self.element.name}.ge1") 41 | 42 | def set_value(self, context, value=None): 43 | self.element.bf_dump_render_file = bool(value) 44 | 45 | 46 | class SP_DUMP_STATUS_FILES(BFParam): 47 | label = "STATUS_FILES" 48 | description = "Export status file (*.notready), deleted when the simulation is completed successfully" 49 | fds_label = "STATUS_FILES" 50 | fds_default = False 51 | bpy_type = Scene 52 | bpy_prop = BoolProperty 53 | bpy_idname = "bf_dump_status_files" 54 | 55 | 56 | class SP_DUMP_NFRAMES(BFParam): 57 | label = "NFRAMES" 58 | description = "Number of output dumps per calculation" 59 | fds_label = "NFRAMES" 60 | fds_default = 1000 61 | bpy_type = Scene 62 | bpy_idname = "bf_dump_nframes" 63 | bpy_prop = IntProperty 64 | bpy_other = {"min": 1} 65 | bpy_export = "bf_dump_nframes_export" 66 | bpy_export_default = False 67 | 68 | def get_exported(self, context): 69 | return ( 70 | super().get_exported(context) 71 | and not self.element.bf_dump_frames_freq_export 72 | ) 73 | 74 | 75 | class SP_DUMP_frames_freq(BFParam): 76 | label = "Dump Output Frequency [s]" 77 | description = "Dump output frequency in seconds" 78 | bpy_type = Scene 79 | bpy_idname = "bf_dump_frames_freq" 80 | bpy_prop = FloatProperty 81 | bpy_other = {"min": 0.01, "precision": TIME_P} 82 | bpy_default = 1.0 83 | bpy_export = "bf_dump_frames_freq_export" 84 | bpy_export_default = False 85 | 86 | def to_fds_list(self, context) -> FDSList: 87 | if not self.get_exported(context): 88 | return FDSList() 89 | nframes = int( 90 | (self.element.bf_time_t_end - self.element.bf_time_t_begin) 91 | // self.element.bf_dump_frames_freq 92 | ) 93 | if nframes < 1: 94 | nframes = 1 95 | return FDSParam(fds_label="NFRAMES", value=nframes) 96 | 97 | 98 | class SP_DUMP_DT_RESTART(BFParam): 99 | label = "DT_RESTART [s]" 100 | description = "Time interval between restart files are saved" 101 | fds_label = "DT_RESTART" 102 | fds_default = 600.0 103 | bpy_type = Scene 104 | bpy_idname = "bf_dump_dt_restart" 105 | bpy_prop = FloatProperty 106 | bpy_other = {"min": 1.0, "precision": TIME_P} 107 | bpy_export = "bf_dump_dt_restart_export" 108 | bpy_export_default = False 109 | 110 | 111 | class SP_DUMP_other(BFParamOther): 112 | bpy_type = Scene 113 | bpy_idname = "bf_dump_other" 114 | bpy_pg = WM_PG_bf_other 115 | bpy_ul = WM_UL_bf_other_items 116 | 117 | 118 | class SN_DUMP(BFNamelistSc): 119 | label = "DUMP" 120 | description = "Output parameters" 121 | enum_id = 3005 122 | fds_label = "DUMP" 123 | bpy_export = "bf_dump_export" 124 | bpy_export_default = False 125 | bf_params = ( 126 | SP_DUMP_FYI, 127 | SP_DUMP_render_file, 128 | SP_DUMP_STATUS_FILES, 129 | SP_DUMP_NFRAMES, 130 | SP_DUMP_frames_freq, 131 | SP_DUMP_DT_RESTART, 132 | SP_DUMP_other, 133 | ) 134 | -------------------------------------------------------------------------------- /lang/SN_DUMP/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .SN_DUMP import SN_DUMP 4 | -------------------------------------------------------------------------------- /lang/SN_DUMP/sc_to_ge1.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, export geometry to ge1 cad file format. 5 | """ 6 | 7 | import bpy, bmesh 8 | from ... import utils 9 | from ...config import LP 10 | 11 | # GE1 file format: 12 | 13 | # [APPEARANCE] < immutable title 14 | # 3 < number of appearances (from Blender materials) 15 | # INERT < appearance name 16 | # 0 200 200 50 0.0 0.0 0.5 < index, red, green, blue, twidth, theight, alpha, tx0, ty0, tz0 (t is texture) 17 | # < texture file, blank if None 18 | # Burner 19 | # 1 255 0 0 0.0 0.0 0.5 20 | # 21 | # Dummy Hole < automatically inserted to render HOLEs 22 | # 2 150 150 150 0.0 0.0 0.5 23 | # 24 | # [FACES] < immutable title 25 | # 2 < number of *quad* faces (from OBST and SURF objects triamgulated bm.faces) 26 | # 6.0 3.9 0.5 6.0 1.9 0.5 6.0 1.9 1.9 6.0 3.9 1.9 0 < x0, y0, z0, x1, y1, z1, ..., ref to appearance index 27 | # 6.0 3.9 0.5 6.0 1.9 0.5 6.0 1.9 1.9 6.0 3.9 1.9 0 28 | # EOF 29 | 30 | 31 | def _get_appearance(name, i, rgb): 32 | return f"{name}\n{i} {rgb[0]} {rgb[1]} {rgb[2]} 0. 0. {rgb[3]:.3f} 0. 0. 0.\n\n" 33 | 34 | 35 | def scene_to_ge1(context, scene): 36 | """! 37 | Export scene geometry in FDS GE1 notation. 38 | @param context: the blender context. 39 | @param scene: the Blender scene. 40 | @return FDS GE1 notation. 41 | """ 42 | # Cursor 43 | w = context.window_manager.windows[0] 44 | w.cursor_modal_set("WAIT") 45 | # Get GE1 appearances from materials 46 | appearances, ma_to_appearance = list(), dict() 47 | for i, ma in enumerate(bpy.data.materials): 48 | appearances.append( 49 | _get_appearance( 50 | name=ma.name, 51 | i=i, 52 | rgb=( 53 | int(ma.diffuse_color[0] * 255), 54 | int(ma.diffuse_color[1] * 255), 55 | int(ma.diffuse_color[2] * 255), 56 | ma.diffuse_color[3], 57 | ), 58 | ) 59 | ) 60 | ma_to_appearance[ma.name] = i 61 | # Append dummy material for HOLEs 62 | i += 1 63 | appearances.append( 64 | _get_appearance(name="Dummy Hole", i=i, rgb=(150, 150, 150, 0.5)) 65 | ) 66 | ma_to_appearance[ma.name] = i 67 | # Select GE1 objects 68 | allowed_nls = ("ON_OBST", "ON_GEOM", "ON_VENT", "ON_HOLE") 69 | obs = ( 70 | ob 71 | for ob in context.scene.objects 72 | if ob.type == "MESH" 73 | and not ob.hide_render # show only exported obs 74 | and not ob.bf_is_tmp # do not show tmp obs 75 | and ob.bf_namelist_cls in allowed_nls # show only allowed namelists 76 | and (ob.active_material and ob.active_material.name != "OPEN") # no OPEN 77 | ) 78 | # Get GE1 faces from selected objects 79 | gefaces = list() 80 | for ob in obs: 81 | # Get the bmesh from the Object and triangulate 82 | bm = utils.geometry.get_object_bmesh(context=context, ob=ob, world=True) 83 | bmesh.ops.triangulate(bm, faces=bm.faces) 84 | # Get ob material_slots 85 | material_slots = ob.material_slots 86 | # Get default_material_name 87 | if ob.bf_namelist_cls == "ON_HOLE": 88 | default_material_name = "Dummy Hole" 89 | elif ob.bf_namelist_cls == "ON_GEOM": 90 | default_material_name = None 91 | elif ob.active_material: 92 | default_material_name = ob.active_material.name 93 | else: 94 | default_material_name = "INERT" 95 | scale_length = context.scene.unit_settings.scale_length 96 | for f in bm.faces: 97 | # Grab ordered vertices coordinates 98 | coos = [co for v in f.verts for co in v.co] 99 | coos.extend((coos[-3], coos[-2], coos[-1])) # tri to quad 100 | items = [f"{coo * scale_length:.{LP}f}" for coo in coos] 101 | # Get appearance_index 102 | if default_material_name: 103 | material_name = default_material_name 104 | else: 105 | material_name = material_slots[f.material_index].material.name 106 | appearance_index = str(ma_to_appearance.get(material_name, 0)) + "\n" 107 | items.append(appearance_index) 108 | # Append GE1 face 109 | gefaces.append(" ".join(items)) 110 | 111 | # Prepare GE1 file and return 112 | ge1_file_a = f"[APPEARANCE]\n{len(appearances)}\n{''.join(appearances)}" 113 | ge1_file_f = f"[FACES]\n{len(gefaces)}\n{''.join(gefaces)}" 114 | # Close 115 | w.cursor_modal_restore() 116 | return "".join((ge1_file_a, ge1_file_f)) 117 | -------------------------------------------------------------------------------- /lang/SN_HEAD.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Scene 5 | from ..types import BFParam, BFParamFYI, BFNamelistSc, FDSList, BFException 6 | from .. import utils 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class SP_HEAD_CHID(BFParam): 12 | label = "CHID" 13 | description = "Case identificator, also used as case filename" 14 | fds_label = "CHID" 15 | bpy_type = Scene 16 | bpy_idname = "name" 17 | 18 | def check(self, context): 19 | name = self.element.name 20 | if "." in name: 21 | raise BFException(self, f"No periods allowed") 22 | 23 | def copy_to(self, context, dest_element): 24 | pass 25 | 26 | 27 | class SP_HEAD_TITLE(BFParamFYI): 28 | label = "TITLE" 29 | description = "Case description" 30 | fds_label = "TITLE" 31 | bpy_type = Scene 32 | bpy_idname = "bf_head_title" 33 | bpy_other = {"maxlen": 64} 34 | 35 | 36 | class SN_HEAD(BFNamelistSc): 37 | label = "HEAD" 38 | description = "Case header" 39 | enum_id = 3001 40 | fds_label = "HEAD" 41 | bpy_export = "bf_head_export" 42 | bpy_export_default = True 43 | bf_params = SP_HEAD_CHID, SP_HEAD_TITLE 44 | bf_import_order = 0 # first imported 45 | 46 | 47 | class SN_TAIL(BFNamelistSc): # importing only, prevent free_text 48 | label = "TAIL" 49 | description = "Case closing" 50 | enum_id = 3010 # to avoid TAIL import into free_text 51 | fds_label = "TAIL" 52 | bf_import_order = 1e5 # last imported 53 | 54 | def get_exported(self, context): 55 | return False 56 | 57 | def from_fds_list(self, context, fds_list): 58 | pass 59 | -------------------------------------------------------------------------------- /lang/SN_MOVE.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistSc, BFNotImported 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | # This namelist is not displayed in the bf_namelist_cls menu, 9 | # and has no panel. 10 | # Used for importing only, it translates 11 | # FDS MOVE transformations to a Scene dict 12 | # It is used in conjuction with ON_MOVE 13 | 14 | 15 | class SN_MOVE(BFNamelistSc): 16 | label = "MOVE" 17 | description = "Geometric transformations" 18 | enum_id = False # no bf_namelist_cls menu 19 | fds_label = "MOVE" 20 | bf_import_order = 10 21 | 22 | def get_exported(self, context): # No automatic export 23 | return False 24 | 25 | def from_fds_list(self, context, fds_list): 26 | # Get ID 27 | fds_param = fds_list.get_fds_param(fds_label="ID", remove=True) 28 | if not fds_param: 29 | raise BFNotImported(self, f"Missing ID in: {fds_list}") 30 | 31 | # Prepare Scene dict 32 | if "bf_move_coll" not in context.scene: 33 | context.scene["bf_move_coll"] = dict() 34 | bf_move_coll = context.scene["bf_move_coll"] 35 | 36 | # Set Scene dict by ID, eg. {"ob_move": "A=3 B=4 C=5"} 37 | f90_params = " ".join((item.to_string() for item in fds_list)) 38 | bf_move_coll[fds_param.get_value()] = f90_params 39 | -------------------------------------------------------------------------------- /lang/SN_MULT.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from ..types import BFNamelistSc, BFNotImported 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | # This namelist is not displayed in the bf_namelist_cls menu, 9 | # and has no panel. 10 | # Used for importing only, it translates 11 | # FDS MULT transformations to a Scene dict 12 | # It is used in conjuction with ON_MULT 13 | 14 | 15 | class SN_MULT(BFNamelistSc): 16 | label = "MULT" 17 | description = "Multiplier transformations" 18 | enum_id = False # No bf_namelist_cls menu 19 | fds_label = "MULT" 20 | bf_import_order = 20 21 | 22 | def get_exported(self, context): # No automatic export 23 | return False 24 | 25 | def from_fds_list(self, context, fds_list): 26 | # Get ID 27 | fds_param = fds_list.get_fds_param(fds_label="ID", remove=True) 28 | if not fds_param: 29 | raise BFNotImported(self, f"Missing ID in: {fds_list}") 30 | 31 | # Prepare Scene dict 32 | if "bf_mult_coll" not in context.scene: 33 | context.scene["bf_mult_coll"] = dict() 34 | bf_mult_coll = context.scene["bf_mult_coll"] 35 | 36 | # Set Scene dict by ID, eg. {"ob_mult": "A=3 B=4 C=5"} 37 | f90_params = " ".join((item.to_string() for item in fds_list)) 38 | bf_mult_coll[fds_param.get_value()] = f90_params 39 | -------------------------------------------------------------------------------- /lang/SN_PRES.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Scene 5 | from bpy.props import BoolProperty, FloatProperty, IntProperty 6 | from ..config import VELOCITY_TOLERANCE_P 7 | from ..types import ( 8 | BFParam, 9 | BFParamOther, 10 | BFParamFYI, 11 | BFNamelistSc, 12 | ) 13 | from ..bl.ui_lists import ( 14 | WM_PG_bf_other, 15 | WM_UL_bf_other_items, 16 | ) 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | class SP_PRES_FYI(BFParamFYI): 22 | bpy_type = Scene 23 | bpy_idname = "bf_pres_fyi" 24 | 25 | 26 | class SP_PRES_BAROCLINIC(BFParam): 27 | label = "BAROCLINIC" 28 | description = "Consider baroclinic torque" 29 | fds_label = "BAROCLINIC" 30 | fds_default = True 31 | bpy_type = Scene 32 | bpy_prop = BoolProperty 33 | bpy_idname = "bf_pres_baroclinic" 34 | 35 | 36 | class SP_PRES_VELOCITY_TOLERANCE(BFParam): 37 | label = "VELOCITY_TOLERANCE" 38 | description = "Maximum allowable normal velocity component\non the solid boundary or the largest error at a mesh interface" 39 | fds_label = "VELOCITY_TOLERANCE" 40 | bpy_type = Scene 41 | bpy_idname = "bf_pres_velocity_tolerance" 42 | bpy_prop = FloatProperty 43 | bpy_export = "bf_pres_velocity_tolerance_export" 44 | bpy_export_default = False 45 | bpy_other = {"precision": VELOCITY_TOLERANCE_P, "min": 0.0, "max": 1.0} 46 | 47 | 48 | class SP_PRES_MAX_PRESSURE_ITERATIONS(BFParam): 49 | label = "MAX_PRESSURE_ITERATIONS" 50 | description = "Maximum number of pressure iterations for each half of the time step" 51 | fds_label = "MAX_PRESSURE_ITERATIONS" 52 | fds_default = 10 53 | bpy_type = Scene 54 | bpy_idname = "bf_pres_max_pressure_iterations" 55 | bpy_prop = IntProperty 56 | bpy_other = {"min": 1} 57 | bpy_export = "bf_pres_max_pressure_iterations_export" 58 | bpy_export_default = False 59 | 60 | 61 | class SP_PRES_other(BFParamOther): 62 | bpy_type = Scene 63 | bpy_idname = "bf_pres_other" 64 | bpy_pg = WM_PG_bf_other 65 | bpy_ul = WM_UL_bf_other_items 66 | 67 | 68 | class SN_PRES(BFNamelistSc): 69 | label = "PRES" 70 | description = "Pressure Solver" 71 | enum_id = 3007 72 | fds_label = "PRES" 73 | bpy_export = "bf_pres_export" 74 | bpy_export_default = False 75 | bf_params = ( 76 | SP_PRES_FYI, 77 | SP_PRES_BAROCLINIC, 78 | SP_PRES_VELOCITY_TOLERANCE, 79 | SP_PRES_MAX_PRESSURE_ITERATIONS, 80 | SP_PRES_other, 81 | ) 82 | -------------------------------------------------------------------------------- /lang/SN_RADI.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Scene 5 | from bpy.props import BoolProperty, IntProperty 6 | from ..types import ( 7 | BFParam, 8 | BFParamOther, 9 | BFParamFYI, 10 | BFNamelistSc, 11 | ) 12 | from ..bl.ui_lists import ( 13 | WM_PG_bf_other, 14 | WM_UL_bf_other_items, 15 | ) 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | class SP_RADI_FYI(BFParamFYI): 21 | bpy_type = Scene 22 | bpy_idname = "bf_radi_fyi" 23 | 24 | 25 | class SP_RADI_RADIATION(BFParam): 26 | label = "RADIATION" 27 | description = "Turn on/off the radiation solver" 28 | fds_label = "RADIATION" 29 | fds_default = True 30 | bpy_type = Scene 31 | bpy_prop = BoolProperty 32 | bpy_idname = "bf_radi_radiation" 33 | 34 | 35 | class SP_RADI_NUMBER_RADIATION_ANGLES(BFParam): 36 | label = "NUMBER_RADIATION_ANGLES" 37 | description = "Number of angles for spatial resolution of radiation solver" 38 | fds_label = "NUMBER_RADIATION_ANGLES" 39 | fds_default = 100 40 | bpy_type = Scene 41 | bpy_idname = "bf_radi_number_radiation_angles" 42 | bpy_prop = IntProperty 43 | bpy_other = {"min": 1} 44 | bpy_export = "bf_radi_number_radiation_angles_export" 45 | bpy_export_default = False 46 | 47 | 48 | class SP_RADI_TIME_STEP_INCREMENT(BFParam): 49 | label = "TIME_STEP_INCREMENT" 50 | description = "Frequency of calls to the radiation solver in time steps" 51 | fds_label = "TIME_STEP_INCREMENT" 52 | fds_default = 3 53 | bpy_type = Scene 54 | bpy_idname = "bf_radi_time_step_increment" 55 | bpy_prop = IntProperty 56 | bpy_other = {"min": 1} 57 | bpy_export = "bf_radi_time_step_increment_export" 58 | bpy_export_default = False 59 | 60 | 61 | class SP_RADI_ANGLE_INCREMENT(BFParam): 62 | label = "ANGLE_INCREMENT" 63 | description = "Increment over which the angles are updated" 64 | fds_label = "ANGLE_INCREMENT" 65 | fds_default = 5 66 | bpy_type = Scene 67 | bpy_idname = "bf_radi_angle_increment" 68 | bpy_prop = IntProperty 69 | bpy_other = {"min": 1} 70 | bpy_export = "bf_radi_angle_increment_export" 71 | bpy_export_default = False 72 | 73 | 74 | class SP_RADI_RADIATION_ITERATIONS(BFParam): 75 | label = "RADIATION_ITERATIONS" 76 | description = "Number of times the radiative intensity is updated in a time step" 77 | fds_label = "RADIATION_ITERATIONS" 78 | fds_default = 1 79 | bpy_type = Scene 80 | bpy_idname = "bf_radi_radiation_iterations" 81 | bpy_prop = IntProperty 82 | bpy_other = {"min": 1} 83 | bpy_export = "bf_radi_radiation_iterations_export" 84 | bpy_export_default = False 85 | 86 | 87 | class SP_RADI_other(BFParamOther): 88 | bpy_type = Scene 89 | bpy_idname = "bf_radi_other" 90 | bpy_pg = WM_PG_bf_other 91 | bpy_ul = WM_UL_bf_other_items 92 | 93 | 94 | class SN_RADI(BFNamelistSc): 95 | label = "RADI" 96 | description = "Radiation parameters" 97 | enum_id = 3006 98 | fds_label = "RADI" 99 | bpy_export = "bf_radi_export" 100 | bpy_export_default = False 101 | bf_params = ( 102 | SP_RADI_FYI, 103 | SP_RADI_RADIATION, 104 | SP_RADI_NUMBER_RADIATION_ANGLES, 105 | SP_RADI_TIME_STEP_INCREMENT, 106 | SP_RADI_ANGLE_INCREMENT, 107 | SP_RADI_RADIATION_ITERATIONS, 108 | SP_RADI_other, 109 | ) 110 | -------------------------------------------------------------------------------- /lang/SN_REAC.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Scene 5 | from bpy.props import BoolProperty, FloatProperty, StringProperty 6 | from ..config import HOC_P, RADIATIVE_FRACTION_P, YIELD_P 7 | from ..types import BFParam, BFParamOther, BFParamFYI, BFNamelistSc 8 | from ..bl.ui_lists import ( 9 | WM_PG_bf_other, 10 | WM_UL_bf_other_items, 11 | ) 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class SP_REAC_FYI(BFParamFYI): 17 | bpy_type = Scene 18 | bpy_idname = "bf_reac_fyi" 19 | 20 | 21 | class SP_REAC_FUEL(BFParam): 22 | label = "FUEL" 23 | description = "Identificator of fuel species" 24 | fds_label = "FUEL" 25 | bpy_type = Scene 26 | bpy_prop = StringProperty 27 | bpy_idname = "bf_reac_fuel" 28 | 29 | 30 | class SP_REAC_FORMULA(BFParam): 31 | label = "FORMULA" 32 | description = "Chemical formula of fuel species, it can only contain C, H, O, or N" 33 | fds_label = "FORMULA" 34 | bpy_type = Scene 35 | bpy_prop = StringProperty 36 | bpy_idname = "bf_reac_formula" 37 | bpy_export = "bf_reac_formula_export" 38 | bpy_export_default = False 39 | 40 | 41 | class SP_REAC_CO_YIELD(BFParam): 42 | label = "CO_YIELD [kg/kg]" 43 | description = "Fraction of fuel mass converted into carbon monoxide" 44 | fds_label = "CO_YIELD" 45 | fds_default = 0.0 46 | bpy_type = Scene 47 | bpy_prop = FloatProperty 48 | bpy_idname = "bf_reac_co_yield" 49 | bpy_other = {"step": 1.0, "precision": YIELD_P, "min": 0.0, "max": 1.0} 50 | bpy_export = "bf_reac_co_yield_export" 51 | bpy_export_default = False 52 | 53 | 54 | class SP_REAC_SOOT_YIELD(SP_REAC_CO_YIELD): 55 | label = "SOOT_YIELD [kg/kg]" 56 | description = "Fraction of fuel mass converted into smoke particulate" 57 | fds_label = "SOOT_YIELD" 58 | bpy_type = Scene 59 | bpy_idname = "bf_reac_soot_yield" 60 | bpy_export = "bf_reac_soot_yield_export" 61 | bpy_export_default = False 62 | 63 | 64 | class SP_REAC_HEAT_OF_COMBUSTION(BFParam): 65 | label = "HEAT_OF_COMBUSTION [kJ/kg]" 66 | description = "Fuel heat of combustion" 67 | fds_label = "HEAT_OF_COMBUSTION" 68 | fds_default = 0.0 69 | bpy_type = Scene 70 | bpy_idname = "bf_reac_heat_of_combustion" 71 | bpy_prop = FloatProperty 72 | bpy_other = {"precision": HOC_P, "min": 0.0} 73 | bpy_export = "bf_reac_heat_of_combustion_export" 74 | bpy_export_default = False 75 | 76 | 77 | class SP_REAC_IDEAL(BFParam): 78 | label = "IDEAL" 79 | description = "Set ideal heat of combustion" 80 | fds_label = "IDEAL" 81 | fds_default = False 82 | bpy_type = Scene 83 | bpy_prop = BoolProperty 84 | bpy_idname = "bf_reac_ideal" 85 | 86 | 87 | class SP_REAC_RADIATIVE_FRACTION(BFParam): 88 | label = "RADIATIVE_FRACTION" 89 | description = ( 90 | "Fraction of the total combustion energy that is released " 91 | "in the form of thermal radiation" 92 | ) 93 | fds_label = "RADIATIVE_FRACTION" 94 | fds_default = 0.35 95 | bpy_type = Scene 96 | bpy_idname = "bf_reac_radiative_fraction" 97 | bpy_prop = FloatProperty 98 | bpy_other = {"precision": RADIATIVE_FRACTION_P, "min": 0.0, "max": 1.0} 99 | bpy_export = "bf_reac_radiative_fraction_export" 100 | bpy_export_default = False 101 | 102 | 103 | class SP_REAC_other(BFParamOther): 104 | bpy_type = Scene 105 | bpy_idname = "bf_reac_other" 106 | bpy_pg = WM_PG_bf_other 107 | bpy_ul = WM_UL_bf_other_items 108 | 109 | 110 | class SN_REAC(BFNamelistSc): 111 | label = "REAC" 112 | description = "Reaction" 113 | enum_id = 3004 114 | fds_label = "REAC" 115 | bpy_export = "bf_reac_export" 116 | bpy_export_default = False 117 | bf_params = ( 118 | SP_REAC_FYI, 119 | SP_REAC_FUEL, 120 | SP_REAC_FORMULA, 121 | SP_REAC_CO_YIELD, 122 | SP_REAC_SOOT_YIELD, 123 | SP_REAC_HEAT_OF_COMBUSTION, 124 | SP_REAC_IDEAL, 125 | SP_REAC_RADIATIVE_FRACTION, # moved from RADI 126 | SP_REAC_other, 127 | ) 128 | -------------------------------------------------------------------------------- /lang/SN_TIME.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Scene 5 | from bpy.props import BoolProperty, FloatProperty 6 | from ..config import TIME_P 7 | from ..types import BFParam, BFParamOther, BFNamelistSc, FDSParam, FDSList 8 | from ..bl.ui_lists import WM_PG_bf_other, WM_UL_bf_other_items 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class SP_TIME_setup_only(BFParam): 14 | label = "Smokeview Geometry Setup" 15 | description = "Set Smokeview to setup only geometry" 16 | bpy_type = Scene 17 | bpy_idname = "bf_time_setup_only" 18 | bpy_prop = BoolProperty 19 | bpy_default = False 20 | 21 | 22 | class SP_TIME_T_BEGIN(BFParam): 23 | label = "T_BEGIN [s]" 24 | description = "Simulation starting time" 25 | fds_label = "T_BEGIN" 26 | fds_default = 0.0 27 | bpy_type = Scene 28 | bpy_idname = "bf_time_t_begin" 29 | bpy_prop = FloatProperty 30 | bpy_other = { 31 | "step": 100.0, 32 | "precision": TIME_P, 33 | } # "unit": "TIME", not working 34 | 35 | def get_active(self, context): 36 | return not self.element.bf_time_setup_only 37 | 38 | 39 | class SP_TIME_T_END(BFParam): 40 | label = "T_END [s]" 41 | description = "Simulation ending time" 42 | fds_label = "T_END" 43 | bpy_type = Scene 44 | bpy_idname = "bf_time_t_end" 45 | bpy_prop = FloatProperty 46 | bpy_default = 1.0 47 | bpy_other = { 48 | "step": 100.0, 49 | "precision": TIME_P, 50 | } # "unit": "TIME", not working 51 | 52 | def to_fds_list(self, context) -> FDSList: 53 | if self.element.bf_time_setup_only: 54 | return FDSParam( 55 | fds_label="T_END", 56 | value=0.0, 57 | msg="Smokeview setup only", 58 | precision=TIME_P, 59 | ) 60 | return super().to_fds_list(context) 61 | 62 | def get_active(self, context): 63 | return not self.element.bf_time_setup_only 64 | 65 | def get_exported(self, context): 66 | return True 67 | 68 | 69 | class SP_TIME_other(BFParamOther): 70 | bpy_type = Scene 71 | bpy_idname = "bf_time_other" 72 | bpy_pg = WM_PG_bf_other 73 | bpy_ul = WM_UL_bf_other_items 74 | 75 | 76 | class SN_TIME(BFNamelistSc): 77 | label = "TIME" 78 | description = "Simulation time settings" 79 | enum_id = 3002 80 | fds_label = "TIME" 81 | bpy_export = "bf_time_export" 82 | bpy_export_default = True 83 | bf_params = SP_TIME_T_BEGIN, SP_TIME_T_END, SP_TIME_setup_only, SP_TIME_other 84 | -------------------------------------------------------------------------------- /lang/SN_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, os, bpy 4 | from bpy.types import Scene, Material 5 | from bpy.props import ( 6 | BoolProperty, 7 | FloatProperty, 8 | StringProperty, 9 | PointerProperty, 10 | EnumProperty, 11 | IntProperty, 12 | ) 13 | from ..config import LP, DEFAULT_MAS 14 | from ..types import BFParam, BFNamelistSc, BFException 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class SP_config_case_name(BFParam): 20 | label = "Case Filename" 21 | description = "Filename for exported FDS case,\nalso used as HEAD CHID" 22 | bpy_type = Scene 23 | bpy_idname = "name" 24 | 25 | def copy_to(self, context, dest_element): 26 | pass 27 | 28 | def draw_operators(self, context, layout): 29 | layout.operator("scene.bf_props_to_sc", icon="COPYDOWN", text="") 30 | 31 | 32 | class SP_config_directory(BFParam): 33 | label = "Case Directory" 34 | description = "Default destination directory for the exported FDS case" 35 | bpy_type = Scene 36 | bpy_idname = "bf_config_directory" 37 | bpy_prop = StringProperty 38 | # no bpy_default, user choice when saving 39 | bpy_other = {"subtype": "DIR_PATH", "maxlen": 1024} 40 | 41 | def check(self, context): 42 | value = self.element.bf_config_directory 43 | if not os.path.exists(bpy.path.abspath(value or "//.")): 44 | raise BFException(self, f"Case directory <{value}> not available") 45 | 46 | def copy_to(self, context, dest_element): 47 | pass 48 | 49 | 50 | class SP_config_text(BFParam): 51 | label = "Free Text" 52 | description = "Internal free text, included verbatim" 53 | bpy_type = Scene 54 | bpy_idname = "bf_config_text" 55 | bpy_prop = PointerProperty 56 | bpy_other = {"type": bpy.types.Text} 57 | 58 | def draw_operators(self, context, layout): 59 | layout.operator("scene.bf_show_text", text="", icon="GREASEPENCIL") 60 | 61 | 62 | class SP_config_text_position(BFParam): 63 | label = "Free Text Position" 64 | description = "Set Free Text position in the exported file" 65 | bpy_type = Scene 66 | bpy_idname = "bf_config_text_position" 67 | bpy_prop = EnumProperty 68 | bpy_default = "BEGIN" 69 | bpy_other = { 70 | "items": ( 71 | ( 72 | "BEGIN", 73 | "At Beginning", 74 | "Insert at the beginning of the exported file", 75 | 100, 76 | ), 77 | ( 78 | "END", 79 | "At End", 80 | "Insert at the end of the exported file", 81 | 500, 82 | ), 83 | ), 84 | } 85 | 86 | 87 | class SP_config_default_voxel_size(BFParam): 88 | label = "Default Voxel/Pixel Size" 89 | description = "Default voxel/pixel resolution" 90 | bpy_type = Scene 91 | bpy_idname = "bf_default_voxel_size" 92 | bpy_prop = FloatProperty 93 | bpy_default = 0.1 94 | bpy_other = { 95 | "step": 1.0, 96 | "precision": LP, 97 | "min": 0.001, 98 | "max": 20.0, 99 | "unit": "LENGTH", 100 | } 101 | 102 | 103 | class SP_config_default_SURF(BFParam): 104 | label = "Default SURF" 105 | description = ( 106 | "Specify the particular SURF to be applied as the default boundary condition" 107 | ) 108 | bpy_type = Scene 109 | bpy_prop = PointerProperty 110 | bpy_idname = "bf_default_surf" 111 | bpy_other = {"type": Material} 112 | 113 | def check(self, context): 114 | if ( 115 | self.element.bf_default_surf 116 | and self.element.bf_default_surf.name in DEFAULT_MAS 117 | ): 118 | raise BFException( 119 | self, 120 | f"Cannot set predefined boundary condition <{self.element.name}> as default SURF", 121 | ) 122 | 123 | 124 | class SP_config_mpi_processes(BFParam): 125 | label = "MPI Processes" 126 | description = ( 127 | "Number of MPI processes automatically allocated to the MESH instances." 128 | ) 129 | bpy_type = Scene 130 | bpy_idname = "bf_config_mpi_processes" 131 | bpy_prop = IntProperty 132 | bpy_default = 1 133 | bpy_other = {"min": 1} 134 | bpy_export = "bf_config_mpi_processes_export" 135 | bpy_export_default = True 136 | 137 | 138 | class SP_config_openmp_threads(BFParam): 139 | label = "OpenMP Threads" 140 | description = "Number of OpenMP threads assigned to each process." 141 | bpy_type = Scene 142 | bpy_idname = "bf_config_openmp_threads" 143 | bpy_prop = IntProperty 144 | bpy_default = 1 145 | bpy_other = {"min": 1} 146 | bpy_export = "bf_config_openmp_threads_export" 147 | bpy_export_default = False 148 | 149 | 150 | class SN_config(BFNamelistSc): 151 | label = "FDS Case Config" 152 | bf_params = ( 153 | SP_config_case_name, 154 | SP_config_directory, 155 | SP_config_text, 156 | SP_config_text_position, 157 | SP_config_default_voxel_size, 158 | SP_config_default_SURF, 159 | SP_config_mpi_processes, 160 | SP_config_openmp_threads, 161 | ) 162 | 163 | def draw(self, context, layout): 164 | row = layout.column(align=True) 165 | row.operator("scene.bf_show_fds_code", icon="HIDE_OFF") 166 | return super().draw(context, layout) 167 | -------------------------------------------------------------------------------- /lang/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | # Import all py file to have them loaded 4 | # the order of scene namelists is also the exporting order 5 | 6 | from . import ( 7 | bf_scene, 8 | bf_material, 9 | bf_collection, 10 | bf_object, 11 | SN_MOVE, 12 | SN_config, 13 | SN_HEAD, 14 | SN_TIME, 15 | SN_MISC, 16 | SN_PRES, 17 | SN_RADI, 18 | SN_REAC, 19 | SN_CATF, 20 | SN_DUMP, 21 | SN_MOVE, 22 | SN_MULT, 23 | MN_SURF, 24 | OP_XB, 25 | OP_XYZ, 26 | OP_PB, 27 | OP_SURF_ID, 28 | ON_DEVC, 29 | ON_GEOM, 30 | ON_HOLE, 31 | ON_INIT, 32 | ON_MESH, 33 | ON_MOVE, 34 | ON_MULT, 35 | ON_OBST, 36 | ON_other, 37 | ON_PROF, 38 | ON_SLCF, 39 | ON_VENT, 40 | ON_ZONE, 41 | ) 42 | 43 | 44 | def register(): 45 | import logging 46 | from ..types import BFParam, BFNamelist 47 | 48 | log = logging.getLogger(__name__) 49 | log.info("Register lang...") 50 | 51 | # Update namelist_cls items (after importing all namelists) 52 | bf_object.update_OP_namelist_cls_items() 53 | bf_material.update_MP_namelist_cls_items() 54 | 55 | # Register Blender entities extensions 56 | bf_object.BFObject.register() 57 | bf_material.BFMaterial.register() 58 | bf_scene.BFScene.register() 59 | bf_collection.BFCollection.register() 60 | 61 | # Register all lang classes, as recorded in BFParam and BFNamelist 62 | for bf_param in BFParam.subclasses: 63 | bf_param.register() 64 | for bf_namelist in BFNamelist.subclasses: 65 | bf_namelist.register() 66 | 67 | 68 | def unregister(): 69 | import logging 70 | from ..types import BFParam, BFNamelist 71 | 72 | log = logging.getLogger(__name__) 73 | log.info("Unregister lang...") 74 | 75 | # Unregister Blender entities extensions 76 | bf_object.BFObject.unregister() 77 | bf_material.BFMaterial.unregister() 78 | bf_scene.BFScene.unregister() 79 | bf_collection.BFCollection.unregister() 80 | 81 | # Unregister all lang classes, as recorded in BFParam and BFNamelist 82 | for bf_namelist in BFNamelist.subclasses: 83 | bf_namelist.unregister() 84 | for bf_param in BFParam.subclasses: 85 | bf_param.unregister() 86 | -------------------------------------------------------------------------------- /lang/bf_collection.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | 5 | from bpy.types import Collection 6 | from ..types import FDSList 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class BFCollection: 12 | """! 13 | Extension of Blender Collection. 14 | """ 15 | 16 | def get_layer_collection(self, context, _layer_collection=None): 17 | """! 18 | Return related layer_collection in current context. 19 | @param context: the Blender context. 20 | @param _layer_collection: internal use for recursivity. 21 | @return layer_collection related to self in current context. 22 | """ 23 | if not _layer_collection: 24 | _layer_collection = context.view_layer.layer_collection 25 | found = None 26 | if _layer_collection.name == self.name: 27 | return _layer_collection 28 | for c in _layer_collection.children: 29 | found = self.get_layer_collection(context, _layer_collection=c) 30 | if found: 31 | return found 32 | 33 | def to_fds_list(self, context, full=False) -> FDSList: 34 | """! 35 | Return the FDSList instance from self, never None. 36 | """ 37 | layer_collection = self.get_layer_collection(context) 38 | if self.hide_render or layer_collection.exclude: 39 | return FDSList() # exclude self from exporting 40 | header = f"\n-- Blender Collection: <{self.name}>" 41 | if full: 42 | # MESH are centrally managed and exported by bf_scene 43 | obs = list(ob for ob in self.objects if ob.bf_namelist_cls != "ON_MESH") 44 | else: 45 | obs = list(self.objects) 46 | obs.sort(key=lambda k: k.name) # alphabetic by name 47 | iterable = (ob.to_fds_list(context=context) for ob in obs) 48 | fds_list = FDSList(header=header, iterable=iterable) 49 | fds_list.extend( 50 | child.to_fds_list(context=context, full=full) for child in self.children 51 | ) 52 | return fds_list 53 | 54 | @classmethod 55 | def register(cls): 56 | """! 57 | Register related Blender properties. 58 | @param cls: class to be registered. 59 | """ 60 | Collection.to_fds_list = cls.to_fds_list 61 | Collection.get_layer_collection = cls.get_layer_collection 62 | 63 | @classmethod 64 | def unregister(cls): 65 | """! 66 | Unregister related Blender properties. 67 | @param cls: class to be unregistered. 68 | """ 69 | del Collection.get_layer_collection 70 | del Collection.to_fds_list 71 | -------------------------------------------------------------------------------- /lang/bf_material.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | from bpy.types import Material 5 | from bpy.props import EnumProperty 6 | from ..types import ( 7 | BFParam, 8 | BFParamOther, 9 | BFParamFYI, 10 | BFNamelist, 11 | BFException, 12 | BFNotImported, 13 | FDSList, 14 | ) 15 | from ..bl.ui_lists import WM_PG_bf_other, WM_UL_bf_other_items 16 | from .. import config 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | class BFMaterial: 22 | """! 23 | Extension of Blender Material. 24 | """ 25 | 26 | @property 27 | def bf_namelist(self): 28 | """! 29 | Return related bf_namelist, instance of BFNamelist. 30 | """ 31 | return BFNamelist.get_subclass(cls_name=self.bf_namelist_cls)(element=self) 32 | 33 | def to_fds_list(self, context) -> FDSList: 34 | """! 35 | Return the FDSList instance from self, never None. 36 | """ 37 | return self.bf_namelist.to_fds_list(context) 38 | 39 | @classmethod 40 | def register(cls): 41 | """! 42 | Register related Blender properties. 43 | @param cls: class to be registered. 44 | """ 45 | Material.bf_namelist = cls.bf_namelist 46 | Material.to_fds_list = cls.to_fds_list 47 | 48 | @classmethod 49 | def unregister(cls): 50 | """! 51 | Unregister related Blender properties. 52 | @param cls: class to be unregistered. 53 | """ 54 | del Material.to_fds_list 55 | del Material.bf_namelist 56 | 57 | 58 | # Before updating the items, the lang classes should be imported 59 | # Only after updating the items, the MP_namelist_cls can be registered 60 | def update_MP_namelist_cls_items(): 61 | items = [ 62 | (cls.__name__, cls.label, cls.description, cls.enum_id) 63 | for cls in BFNamelist.subclasses 64 | if cls.bpy_type == Material and cls.enum_id 65 | ] 66 | items.sort(key=lambda k: k[1]) 67 | MP_namelist_cls.bpy_other["items"] = items 68 | # log.debug(f"Updated MP_namelist_cls items (before registration): {items}") 69 | 70 | 71 | def update_MP_namelist_cls(self, context): 72 | self.bf_namelist.set_appearance(context) 73 | 74 | 75 | class MP_namelist_cls(BFParam): 76 | label = "Namelist" 77 | description = "Identification of FDS namelist" 78 | bpy_type = Material 79 | bpy_idname = "bf_namelist_cls" 80 | bpy_prop = EnumProperty 81 | bpy_other = { 82 | "items": (("MN_SURF", "SURF", "Generic boundary condition", 2000),), 83 | "update": update_MP_namelist_cls, 84 | } 85 | bpy_default = "MN_SURF" 86 | 87 | def draw_operators(self, context, layout): 88 | layout.operator("material.bf_props_to_ma", icon="COPYDOWN", text="") 89 | 90 | 91 | class MP_ID(BFParam): 92 | label = "ID" 93 | description = "Material identification name" 94 | fds_label = "ID" 95 | bpy_type = Material 96 | bpy_prop = None # to avoid creation 97 | bpy_idname = "name" 98 | 99 | def copy_to(self, context, dest_element): 100 | pass 101 | 102 | def draw(self, context, layout): 103 | row = layout.row(align=True) 104 | row.prop(self.element, "name", text="ID") 105 | row.prop(self.element, "use_fake_user", text="") # force export 106 | 107 | 108 | class MP_FYI(BFParamFYI): 109 | bpy_type = Material 110 | bpy_idname = "bf_fyi" 111 | 112 | 113 | class MP_RGB(BFParam): 114 | label = "RGB, TRANSPARENCY" 115 | description = "Set color and transparency of the boundary condition" 116 | fds_label = "RGB" 117 | bpy_type = Material 118 | bpy_prop = None # Do not register 119 | bpy_idname = "diffuse_color" 120 | 121 | def get_value(self, context): 122 | c = getattr(self.element, self.bpy_idname) 123 | return (int(c[0] * 255), int(c[1] * 255), int(c[2] * 255)) 124 | 125 | def set_value(self, context, value): 126 | c = getattr(self.element, self.bpy_idname) 127 | c[0], c[1], c[2] = value[0] / 255.0, value[1] / 255.0, value[2] / 255.0 128 | 129 | 130 | class MP_COLOR(BFParam): # only import 131 | label = "COLOR" 132 | description = "Color" 133 | fds_label = "COLOR" 134 | bpy_type = Material 135 | bpy_prop = None # Do not register 136 | bpy_idname = "diffuse_color" 137 | 138 | def get_exported(self, context): 139 | return False 140 | 141 | def set_value(self, context, value): 142 | c = getattr(self.element, self.bpy_idname) 143 | rgb = config.FDS_COLORS.get(value) 144 | if not rgb: 145 | raise BFException(self, f"Unknown color <{value}>") 146 | c[0], c[1], c[2] = rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0 147 | 148 | def draw(self, context, layout): # see MP_RGB 149 | pass 150 | 151 | 152 | class MP_TRANSPARENCY(BFParam): # no draw 153 | label = "TRANSPARENCY" 154 | description = "Transparency" 155 | fds_label = "TRANSPARENCY" 156 | fds_default = 1.0 157 | bpy_type = Material 158 | bpy_prop = None # Do not register 159 | bpy_idname = "diffuse_color" 160 | 161 | def get_value(self, context): 162 | c = getattr(self.element, self.bpy_idname) 163 | return c[3] 164 | 165 | def set_value(self, context, value): 166 | c = getattr(self.element, self.bpy_idname) 167 | c[3] = value 168 | 169 | def draw(self, context, layout): # see MP_RGB 170 | pass 171 | 172 | 173 | # class MP_MATL_ID(BFParam): 174 | # label = "MATL_ID" 175 | # description = "Reference to a MATL (Material) line for self properties" 176 | # fds_label = "MATL_ID" 177 | # bpy_type = Material 178 | # bpy_prop = StringProperty 179 | # bpy_idname = "bf_matl_id" 180 | 181 | # def draw_operators(self, context, layout): 182 | # layout.operator("material.bf_choose_matl_id", icon="VIEWZOOM", text="") 183 | 184 | # def set_value(self, context, value): 185 | # if isinstance(value, str): 186 | # return super().set_value(context, value) 187 | # else: 188 | # raise BFNotImported(self, "Material list not handled") 189 | 190 | # When this Material is default, export DEFAULT=T 191 | class MP_DEFAULT(BFParam): # no label 192 | label = "DEFAULT" 193 | description = "Set default SURF" 194 | fds_label = "DEFAULT" 195 | fds_default = False 196 | bpy_type = Material 197 | 198 | def get_value(self, context): 199 | return context.scene.bf_default_surf == self.element 200 | 201 | def set_value(self, context, value=None): 202 | context.scene.bf_default_surf = self.element 203 | 204 | 205 | class MP_other(BFParamOther): 206 | bpy_type = Material 207 | bpy_idname = "bf_other" 208 | bpy_pg = WM_PG_bf_other 209 | bpy_ul = WM_UL_bf_other_items 210 | -------------------------------------------------------------------------------- /lang/bf_scene/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .bf_scene import BFScene -------------------------------------------------------------------------------- /lang/bf_scene/bf_scene.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging, bpy, os 4 | from bpy.types import Scene 5 | from bpy.props import IntVectorProperty 6 | from ...config import MAXLEN 7 | from ...types import BFNamelist, FDSList, BFParam 8 | from ... import utils 9 | from . import export_helper, import_helper 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class BFScene: 15 | """! 16 | Extension of Blender Scene. 17 | """ 18 | 19 | @property 20 | def bf_namelists(self): 21 | """! 22 | Return related bf_namelist, instance of BFNamelist. 23 | """ 24 | return (n(element=self) for n in BFNamelist.subclasses if n.bpy_type == Scene) 25 | 26 | def to_fds_list(self, context, full=False) -> FDSList: 27 | """! 28 | Return the FDSList instance from self, never None. 29 | """ 30 | # Set mysef as the right Scene instance in the context 31 | # It is needed, because context.scene is needed elsewhere 32 | bpy.context.window.scene = self # set context.scene 33 | return export_helper.sc_to_fds_list(context=context, sc=self, full=full) 34 | 35 | def to_fds(self, context, full=False, save=False): 36 | """! 37 | Return the FDS formatted string. 38 | @param context: the Blender context. 39 | @param full: if True, return full FDS case. 40 | @param save: if True, save to disk. 41 | @return FDS formatted string (eg. "&OBST ID='Test' /"), or None. 42 | """ 43 | log.info(f"Export from Scene {self.name}...") 44 | text = self.to_fds_list(context=context, full=full).to_string() 45 | if save: 46 | filepath = utils.io.transform_rbl_to_abs( 47 | context=context, 48 | filepath_rbl=self.bf_config_directory, 49 | name=self.name, 50 | extension=".fds", 51 | ) 52 | log.info(f"Save to {filepath}...") 53 | utils.io.write_txt_file(filepath, text) 54 | log.info("Done!") 55 | return text 56 | 57 | def from_fds( 58 | self, 59 | context, 60 | filepath=None, 61 | f90_namelists=None, 62 | fds_list=None, 63 | set_tmp=False, 64 | ): 65 | """! 66 | Set self.bf_namelists from FDSList, on error raise BFException. 67 | @param context: the Blender context. 68 | @param filepath: filepath of FDS case to be imported. 69 | @param f90_namelists: FDS formatted string of namelists, eg. "&OBST ID='Test' /\n&TAIL /". 70 | @param fds_list: FDSList of FDSNamelists. 71 | @param set_tmp: set temporary Objects. 72 | """ 73 | log.info(f"Import to Scene {self.name}...") 74 | 75 | # Set mysef as the right Scene instance in the context 76 | # this is used by context.scene calls elsewhere 77 | # Also for visibility 78 | context.window.scene = self 79 | 80 | # Load fds case from filepath 81 | if filepath: 82 | log.info(f"Load from {filepath}...") 83 | filepath = utils.io.transform_rbl_to_abs( 84 | context=context, 85 | filepath_rbl=filepath, 86 | ) 87 | f90_namelists = utils.io.read_txt_file(filepath=filepath) 88 | # and set imported fds case dir, because others rely on it 89 | # it is restored later 90 | bf_config_directory = self.bf_config_directory 91 | self.bf_config_directory = os.path.dirname(filepath) 92 | 93 | # Load fds case from f90_namelists 94 | if f90_namelists: 95 | fds_list = FDSList(f90_namelists=f90_namelists) 96 | 97 | # Load fds_case from fds_list (protect from None) 98 | if not fds_list: 99 | log.info("Prepare FDSList...") 100 | fds_list = FDSList() 101 | 102 | fds_namelist_qty = len(fds_list) 103 | 104 | # Prepare free text for unmanaged namelists, no rewind 105 | # if not existing, create 106 | log.info("Prepare free text...") 107 | self.bf_config_text = utils.ui.show_bl_text( 108 | context=context, 109 | bl_text=self.bf_config_text, 110 | name="New Free Text", 111 | ) 112 | 113 | # Import 114 | texts = list() 115 | filename = bpy.path.basename(filepath or "") 116 | import_helper.sc_from_fds_list( 117 | context, 118 | sc=self, 119 | fds_list=fds_list, 120 | set_tmp=set_tmp, 121 | texts=texts, 122 | filename=filename, 123 | ) 124 | 125 | # Finally, write free text 126 | log.info("Write free text...") 127 | header = None 128 | if filepath: 129 | header = f"-- From: <{filename}>" 130 | utils.ui.write_bl_text( 131 | context, bl_text=self.bf_config_text, header=header, texts=texts 132 | ) 133 | 134 | # Restore fds case dir, to avoid overwriting imported case 135 | if filepath: 136 | self.bf_config_directory = bf_config_directory 137 | 138 | log.info("Done!") 139 | return fds_namelist_qty # feedback 140 | 141 | @classmethod 142 | def register(cls): 143 | """! 144 | Register related Blender properties. 145 | @param cls: class to be registered. 146 | """ 147 | Scene.bf_namelists = cls.bf_namelists 148 | Scene.to_fds_list = cls.to_fds_list 149 | Scene.to_fds = cls.to_fds 150 | Scene.from_fds = cls.from_fds 151 | 152 | @classmethod 153 | def unregister(cls): 154 | """! 155 | Unregister related Blender properties. 156 | @param cls: class to be unregistered. 157 | """ 158 | del Scene.bf_file_version 159 | del Scene.from_fds 160 | del Scene.to_fds 161 | del Scene.to_fds_list 162 | del Scene.bf_namelists 163 | 164 | 165 | # Automatically filled by an handler 166 | class OP_file_version(BFParam): 167 | label = "BlenderFDS File Version" 168 | description = "BlenderFDS File Version" 169 | bpy_type = Scene 170 | bpy_idname = "bf_file_version" 171 | bpy_prop = IntVectorProperty 172 | bpy_default = (0, 0, 0) 173 | bpy_other = {"size": 3} 174 | -------------------------------------------------------------------------------- /startup.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firetools/blenderfds/a0542f2b20d79dc8ec56da138a07da74b3abfb61/startup.blend -------------------------------------------------------------------------------- /types/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, types. 5 | """ 6 | 7 | from .bf_exception import BFException, BFNotImported 8 | from .bf_namelist import BFNamelist, BFNamelistMa, BFNamelistOb, BFNamelistSc 9 | from .bf_param import BFParam, BFParamFYI, BFParamOther 10 | from .fds_list import FDSList, FDSMulti, FDSNamelist, FDSParam 11 | 12 | # Nothing to register here 13 | -------------------------------------------------------------------------------- /types/bf_exception.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, general utilities and exception types. 5 | """ 6 | 7 | 8 | class BFException(Exception): 9 | """! 10 | Exception raised by BlenderFDS methods in case of an error. 11 | """ 12 | 13 | def __init__(self, sender=None, msg=None): 14 | """! 15 | Class constructor. 16 | @param sender: the object that generates the exception 17 | @param msg: exception message 18 | """ 19 | ## The object that generates the exception 20 | self.sender = sender 21 | ## Exception message 22 | self.msg = msg or "Unknown error" 23 | 24 | def __str__(self): 25 | sender = self.sender 26 | if sender: 27 | element = getattr(sender, "element", None) 28 | if element: 29 | name = f"{element.name}: {sender.fds_label or sender.label or sender.__class__.__name__}" 30 | else: 31 | name = getattr(sender, "name", None) or sender.__class__.__name__ 32 | return f"ERROR: {name}: {self.msg}" 33 | else: 34 | return self.msg 35 | 36 | def __repr__(self) -> str: 37 | return f"{self.__class__.__name__}(sender={self.sender}, msg={self.msg})" 38 | 39 | 40 | class BFNotImported(BFException): 41 | """! 42 | Exception raised by BlenderFDS methods when some FDS importing fails. 43 | """ 44 | 45 | pass 46 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from . import geometry, io, gis, ui, binpacking, text, updater 4 | 5 | # Nothing to register here 6 | -------------------------------------------------------------------------------- /utils/binpacking.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | Simple binpacking algorithm. 5 | """ 6 | 7 | 8 | def _argmin(l): 9 | """Get the index of min item in an iterable.""" 10 | l = tuple(l) 11 | return min(range(len(l)), key=l.__getitem__) 12 | 13 | 14 | def binpack(nbin, item_weigths): 15 | """First-fit binpacking algorithm with fixed number of bins. 16 | 17 | @param nbin: fixed number of bins 18 | @param item_weigths: list of items and their weigths: ((w0, item0), (w1, item1), ...) 19 | @return: list of bins: [[wb0, [item0, item1, ...]], [wb1, [item5, ...]], ...] 20 | """ 21 | # Reverse sort items by weigth 22 | item_weigths = list(item_weigths) 23 | item_weigths.sort(key=lambda k: k[0], reverse=True) 24 | 25 | # Init bins 26 | bins = list(list((0, list())) for i in range(nbin)) 27 | 28 | # Pack 29 | for i, (weigth, item) in enumerate(item_weigths): 30 | # Get the index of the lighter bin 31 | j = _argmin(tuple(bin[0] for bin in bins)) 32 | # Add the weigth and assign the item to the bin 33 | bins[j][0] += weigth 34 | bins[j][1].append(item) 35 | 36 | return bins 37 | 38 | 39 | def test(): 40 | item_weigths = ((10, "A"), (12, "B"), (11, "C"), (4, "D"), (3, "E"), (2, "F")) 41 | 42 | bins = binpack(nbin=1, item_weigths=item_weigths) 43 | print(len(bins), ":", bins) 44 | assert bins == [[42, ["B", "C", "A", "D", "E", "F"]]] 45 | 46 | bins = binpack(nbin=2, item_weigths=item_weigths) 47 | print(len(bins), ":", bins) 48 | assert bins == [[21, ["B", "D", "E", "F"]], [21, ["C", "A"]]] 49 | 50 | bins = binpack(nbin=3, item_weigths=item_weigths) 51 | print(len(bins), ":", bins) 52 | assert bins == [[14, ["B", "F"]], [14, ["C", "E"]], [14, ["A", "D"]]] 53 | 54 | bins = binpack(nbin=4, item_weigths=item_weigths) 55 | print(len(bins), ":", bins) 56 | assert bins == [[12, ["B"]], [11, ["C"]], [10, ["A"]], [9, ["D", "E", "F"]]] 57 | 58 | bins = binpack(nbin=5, item_weigths=item_weigths) 59 | print(len(bins), ":", bins) 60 | assert bins == [[12, ["B"]], [11, ["C"]], [10, ["A"]], [4, ["D"]], [5, ["E", "F"]]] 61 | 62 | bins = binpack(nbin=6, item_weigths=item_weigths) 63 | print(len(bins), ":", bins) 64 | assert bins == [ 65 | [12, ["B"]], 66 | [11, ["C"]], 67 | [10, ["A"]], 68 | [4, ["D"]], 69 | [3, ["E"]], 70 | [2, ["F"]], 71 | ] 72 | 73 | bins = binpack(nbin=7, item_weigths=item_weigths) 74 | print(len(bins), ":", bins) 75 | assert bins == [ 76 | [12, ["B"]], 77 | [11, ["C"]], 78 | [10, ["A"]], 79 | [4, ["D"]], 80 | [3, ["E"]], 81 | [2, ["F"]], 82 | [0, []], 83 | ] 84 | 85 | 86 | if __name__ == "__main__": 87 | test() 88 | -------------------------------------------------------------------------------- /utils/text.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, text management utilities. 5 | """ 6 | 7 | from ..config import MAXLEN, INDENT 8 | 9 | 10 | def append_word(lines, word, separator=" ", force_break=False) -> list: 11 | """! 12 | Append word to the last of lines, generate newline and ident if needed. 13 | """ 14 | if not force_break and len(lines[-1]) + len(separator) + len(word) <= MAXLEN: 15 | # append to current line 16 | lines[-1] += separator + word 17 | else: 18 | # append to new line w indent, wo separator 19 | lines.append(" " * INDENT + word) 20 | return lines -------------------------------------------------------------------------------- /utils/ui.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """! 4 | BlenderFDS, Blender user interface utilities. 5 | """ 6 | 7 | import bpy 8 | 9 | 10 | def get_screen_area(context, area_type="PROPERTIES"): 11 | """! 12 | Get existing ui area or create one 13 | """ 14 | selected_area = None 15 | for w in context.window_manager.windows: 16 | for area in w.screen.areas: 17 | if area.type == area_type: 18 | selected_area = area 19 | break 20 | if not selected_area: 21 | # Call user prefs window 22 | bpy.ops.screen.userpref_show("INVOKE_DEFAULT") 23 | # Change area type 24 | area = context.window_manager.windows[-1].screen.areas[0] 25 | area.type = area_type 26 | selected_area = area 27 | return selected_area 28 | 29 | 30 | def show_bl_text(context, bl_text=None, name=None): 31 | """! 32 | Show bl_text in Blender Text Editor. 33 | """ 34 | # If not given, create text 35 | if not bl_text: 36 | bl_text = bpy.data.texts.new(name or str()) 37 | # Rewind to the first line 38 | bl_text.current_line_index = 0 39 | # Search existing ui area or create one 40 | selected_area = get_screen_area(context, area_type="TEXT_EDITOR") 41 | # Set highlighting 42 | space = selected_area.spaces[0] 43 | space.text = bl_text 44 | space.show_line_numbers = True 45 | space.show_line_highlight = True 46 | space.show_word_wrap = True 47 | space.show_margin = True 48 | space.margin_column = 130 49 | space.show_syntax_highlight = True 50 | return bl_text 51 | 52 | 53 | def write_bl_text(context, bl_text, header=None, texts=()): 54 | """! 55 | Write text in bl_text from Blender Text Editor. 56 | """ 57 | if not texts: 58 | return 59 | # Prepare body 60 | body = bl_text.as_string().strip() 61 | if body: 62 | body += "\n" 63 | if header: 64 | body += "\n" 65 | if header: 66 | body += f"{header}\n" 67 | body += "\n".join(texts) 68 | bl_text.from_string(body) 69 | 70 | 71 | def show_property_panel(context, space_context="MATERIAL"): 72 | """! 73 | Show Material Panel. 74 | """ 75 | selected_area = get_screen_area(context, area_type="PROPERTIES") 76 | selected_area.spaces[0].context = space_context 77 | 78 | 79 | def view_all(context): 80 | """! 81 | Run view3d.view_all operator. 82 | """ 83 | for area in context.screen.areas: 84 | if area.type == "VIEW_3D": 85 | for region in area.regions: 86 | if region.type == "WINDOW": 87 | override = { 88 | "area": area, 89 | "region": region, 90 | "edit_object": context.edit_object, 91 | } 92 | with bpy.context.temp_override(**override): 93 | bpy.ops.view3d.view_all() 94 | for space in area.spaces: 95 | if space.type == "VIEW_3D": 96 | space.clip_end = 1e6 97 | --------------------------------------------------------------------------------