From 33660f81f6e71135bbabfe0960bdc07c6f8de2d4 Mon Sep 17 00:00:00 2001 From: djmonkeyuk Date: Tue, 7 Jul 2026 15:39:24 +0100 Subject: [PATCH 01/15] Bugfix on curve creation --- .../no_mans_sky_base_builder/utils/curve.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/utils/curve.py b/src/addons/no_mans_sky_base_builder/utils/curve.py index 97b5b130..d40c2d55 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve.py @@ -1,12 +1,14 @@ +import inspect +import math import os -import bpy import uuid -import math -from . import blend_utils, curve_utils, collection_utils, material, mirror_utils -from . import python as python_utils -from .. import builder, part -import inspect +import bpy + +from .. import builder, part +from . import (blend_utils, collection_utils, curve_utils, material, + mirror_utils) +from . import python as python_utils BUILDER = builder.Builder() @@ -83,7 +85,6 @@ def duplicate_along_curve( bpy_object, curve, number_of_duplicates=10, radius_mu object_id = curve["dup_ObjectID"] user_data = curve["dup_UserData"] - parent_col = curve["parent_col"] linked_curve_obj_col = collection_utils.get_collection(collection_utils.LINKED_CURVE_OBJ_COL) From 45356e11ddcd36552654fc50ebebc2ff6eace449 Mon Sep 17 00:00:00 2001 From: djmonkeyuk Date: Tue, 14 Jul 2026 11:32:19 +0100 Subject: [PATCH 02/15] Fixed incorrect scale of trophy parts --- release_notes.md | 6 ++++++ .../blender_manifest.toml | 2 +- .../exotic_decorations/SWARM_TROPHY_B.fbx | Bin 74220 -> 153808 bytes .../exotic_decorations/SWARM_TROPHY_G.fbx | Bin 77596 -> 153808 bytes .../exotic_decorations/SWARM_TROPHY_R.fbx | Bin 74124 -> 153808 bytes 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/release_notes.md b/release_notes.md index 4d1f0720..6f955683 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,9 @@ +release 6.5.1 +------------- + +* Fixed incorrect scale of Swarm Trophy parts. + + release 6.5.0 ------------- diff --git a/src/addons/no_mans_sky_base_builder/blender_manifest.toml b/src/addons/no_mans_sky_base_builder/blender_manifest.toml index 4f0133b3..132aa554 100644 --- a/src/addons/no_mans_sky_base_builder/blender_manifest.toml +++ b/src/addons/no_mans_sky_base_builder/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "no_mans_sky_base_builder" -version = "6.5.0" +version = "6.5.1" name = "No Man's Sky Base Builder" tagline = "A tool to assist with base building in No Mans Sky" maintainer = "DjMonkey <@djmonkeyuk>" diff --git a/src/addons/no_mans_sky_base_builder/models/exotic_decorations/SWARM_TROPHY_B.fbx b/src/addons/no_mans_sky_base_builder/models/exotic_decorations/SWARM_TROPHY_B.fbx index 2b1a1b351944e90d69ba0d05cc075b6a6b2a21f9..5e29c7b9e10dd14c03a51de4bd21aa7a7e4c6f5d 100644 GIT binary patch delta 65924 zcmce-d0bOTvoH)n1Y{&AA|gvbL{x;RsB9q_L`4TRA}TJB3?hSw5>`oo?{q?VL{yy3*|6BS z2warS#`PPv%y-`4ypgU?Y0&o6HqU}sK{i4t{`w}ApCR+iAUgl*LVr(Ke8{$t_>iCf zXPM-*i4T_5m4GKAN0v83g;jm{yqT;Y|kp!%U2KzV78Qh7m({VvzcwBr$ zDE0*C-Pn+LP?fl#fI#p4d$0%gqxbDRg5B!;%W?e4d2?Og0Xx;+7ht5NMZxQ=`G2d! z9Z(1GPfIhjpbnsyDD^rv1{2>^+e+c+c$#W_2YMVdEiQ5s=q!!D5R~on+!twB_k<8a z&VR?EDEH?f3^Z6xEc8*v=K0uc*aTni3g5VX;})9@>pfi7du(uX-n3~$>iqB4Q~Y!{ z{=YT44D4E?qdjf5SZx=t9cxh8qs=SU5L`nluSU(hp#9AmWT> zxZ-i4xYGfeC<}{9IP)zNefzfyOjKIuzO${HV}pW4KvYOHI%-F3$eFM`xDz>pFl|#X zO0~4=7E(SKx-Qs{c^<2&f=PbK4_g)(|23X_1g!cRQ4ajD^(c>#3_3v5*(wdqzJLW_ zp;2T{NNi+StY%tk`QXk(;}W7y{7aQ;kVg}No91{HX^0YHW5c52|HVpy8Z4iu1}{pC z-g4jvO(1-~47TAy6V8Cdaq~d^HEz?$()9uRh~K5DszJ0S;V%Pk{lAHK^HvwG;)2a{ zYG_yjLc^lMK5BgM{rZmqB!LZo8%Ei(aJ4}*h|sWkQoX#+0uMS1ebp|5u~owy<9!q9VR!CKoPi#YKTn0dja~mW=$*x!Gjc$$y`Ctg58>@OjqVY@>#ArQX_WcmC05B(BB-)o%Y0^Z5>$IEoF>boUL)DY4dGMk#?B0SNOsExwPfW_JF~vm+!_vtQRl_WvoI zd8kZ~U(=^Icz?IT`^@nOFr)vMF6YKr<5TZ#cW!I0R@AmTz(3G;pzXUp8|LI`Z1^tk z|4JS6<^N5cZ4tlxk`VV_J@8!}&Ghr8prICr4uhN;|NL9W-#P!UIxhInjx}<(CY*_; z44S!F%D=bm0Fj`NG@-v27keh;^q!D!1ZD_}{|E)m(0*e+zhJEs8En%yc>5f)X6-x> z3;t?|4$-W!|HaAwjO))IzKh&4#|GxYe`5txX6Tt zouL>TqF{+2fyQT^a|D`B{viOBqk#Ia`1DejR#(�jI0NYVHAW(Amoq@SXfauFY|!hu%Q^6 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 z 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 z z^NypmU_hZ7HGZ?;{l{di56rkH4RP`5eWbHo8xlm*YlmgxBLZA7rfo%!XlaCgaoTPL7|4n;$BJ)SQ7TRIebO! zKv`?Pc)u8B=_vBv4JlH`pPh0-?tp15x}mZHM90w+aASD}!}-|`fsWT~J39oxys_-} zPViG${aPzALhaY=X884BU{3Th)ltd3U4f)D#W2>M=dt`2r%A_lq|c84Lx-=ZbTsBa z@jjC;I6{JLx|Qv+eRd+EEDbmk+I=u_2RL|};bY>`g#xUB1!}N4^Dk9Spp8>PTsbbZ z0USn>lVhgetkO@Tl=m#^MjdYp9Sk2_eZdV3!ZCq$dk71#Z<^mMenpx1AIgEJyh(BQ zM}JJjP4S)LYC_|l0JiTZcRD~YO+RBRX<8UvYi8N4bB>UIAAU{jsJy5u2P^*h1_(93 zJocH&Yi=e^KX;q48gxUQ??sGVUo9n#-_G$%ExVOy&PZvN9>Ca^H;aR;HSbVv$XgtK z4sEe8OX5uWO%NVaN*UPBFUoXPchhEZ^3IY5&8($jt9(-aLJZ5GPj~US;QpdE1Zd

=5X}-fpF}^0{itJ}C@n;r+HkDq^`L>1 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 C2Uv~( delta 44646 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%0AZO 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 MNVzHGn|Txe3$OeVSO5S3 literal 77596 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 Date: Thu, 16 Jul 2026 21:35:48 +0100 Subject: [PATCH 03/15] Blender timer alternative for Linux. --- .../asset_browser/main.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/asset_browser/main.py b/src/addons/no_mans_sky_base_builder/asset_browser/main.py index eb5dfd81..6aa45e10 100644 --- a/src/addons/no_mans_sky_base_builder/asset_browser/main.py +++ b/src/addons/no_mans_sky_base_builder/asset_browser/main.py @@ -614,6 +614,7 @@ def sizeHint(self): qt_app = None qt_window = None qt_thread = None +IS_LINUX = sys.platform.startswith("linux") def qt_event_loop(): @@ -637,9 +638,20 @@ def load(): qt_app = QtWidgets.QApplication(sys.argv) else: qt_app = QtWidgets.QApplication.instance() - window = AssetBrowser() - window.show() - # Start Qt event loop in a separate thread to avoid blocking Blender + qt_window = AssetBrowser() + qt_window.show() + + if IS_LINUX: + # Qt objects were created by Blender's main thread. Qt requires their + # event loop to run on that same thread; calling exec_() from a Python + # worker happens to work on Windows but is rejected on Linux. + # Let Blender schedule short Qt event-processing slices instead. + if not bpy.app.timers.is_registered(qt_event_loop): + bpy.app.timers.register(qt_event_loop, first_interval=0.01) + return + + # Preserve the existing Windows behaviour, where the separate Qt event + # loop has historically coexisted with Blender without blocking its UI. qt_thread = threading.Thread(target=qt_thread_func, daemon=True) qt_thread.start() From d6f0bce1b1b9b2bbc662543dbc14326e83a5cd79 Mon Sep 17 00:00:00 2001 From: djmonkeyuk Date: Fri, 17 Jul 2026 16:32:34 +0100 Subject: [PATCH 04/15] Notes for 6.5.2 --- release_notes.md | 6 ++++++ src/addons/no_mans_sky_base_builder/blender_manifest.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/release_notes.md b/release_notes.md index 6f955683..c86c3684 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,9 @@ +release 6.5.2 +------------- + +* Fixed an issue where the Asset Browser would not load on Linux. + + release 6.5.1 ------------- diff --git a/src/addons/no_mans_sky_base_builder/blender_manifest.toml b/src/addons/no_mans_sky_base_builder/blender_manifest.toml index 132aa554..2cf2f0c8 100644 --- a/src/addons/no_mans_sky_base_builder/blender_manifest.toml +++ b/src/addons/no_mans_sky_base_builder/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "no_mans_sky_base_builder" -version = "6.5.1" +version = "6.5.2" name = "No Man's Sky Base Builder" tagline = "A tool to assist with base building in No Mans Sky" maintainer = "DjMonkey <@djmonkeyuk>" From 8d2b7a6e8d38624ca43be0d8a5949854b72ad424 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:53:15 +0530 Subject: [PATCH 05/15] Added SpaceBases and SpaceStationBases to Save Manager. Also fixed compatibility of Save Manager with other save editors. --- .../save_editor/save_editor_operators.py | 12 + .../save_editor/save_editor_presentation.py | 338 +++++++++++------- .../save_editor/save_editor_utils.py | 107 +++--- .../save_editor/save_file.py | 80 +++-- .../save_editor/save_manager.py | 102 +++--- 5 files changed, 392 insertions(+), 247 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py index f256836b..f40f9b4b 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py @@ -54,6 +54,8 @@ def execute(self, context): result = save_data.import_base_from_save_file(context) if result is not None: self.report({'INFO'}, result) + else: + self.report({'ERROR'}, "Error importing base") return {"FINISHED"} # Button to export base selected from list of bases in save editor section @@ -79,7 +81,16 @@ class PinBase(bpy.types.Operator): def execute(self, context): scene = context.scene save_data = scene.nms_save_data + base_tool = scene.nms_base_tool + save_data.pin_base() + pinned_base = save_data.get_pinned_base_data() + + if pinned_base is not None: + base_tool.deserialise_from_data(nms_data = pinned_base, start_new_file = False) + else: + self.report({'ERROR'}, "Base not found") + return {"FINISHED"} # Button to unpin base from top of editor @@ -122,6 +133,7 @@ class ExoprtPinnedBase(bpy.types.Operator): "Check export base name to update name along with objects.\n" "Base Name will be taken from 'Base Name' field in Base properties section" ) + def execute(self, context): scene = context.scene diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py index 1189744b..554f31ae 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py @@ -1,3 +1,10 @@ +"""The Save Manager sidebar panel, and the save manager UI itself. + +draw_save_manager() holds the whole UI. The "Save Manager" dropdown in the +viewport header (presentation/save_manager_menu.py) draws the same function +into its popover, so the sidebar and the dropdown cannot drift apart. +""" + import bpy from bpy.types import Panel @@ -7,6 +14,207 @@ ADDON_ID = __package__.rsplit(".", 1)[0] + +def get_save_data(context): + """The save editor's property group, or None if it is not registered yet.""" + return getattr(context.scene, "nms_save_data", None) + + +def get_save_folder_path(context): + """The configured save folder, or None if the preferences are unavailable. + + A draw can run before the addon's preferences exist - during registration, + or after a disable while the header is still on screen - so this never + assumes the lookup succeeds. + """ + addon = context.preferences.addons.get(ADDON_ID) + if addon is None: + return None + return addon.preferences.nms_save_folder_path + +def draw_pinned_base(container, save_data, base_props): + """Draw the pinned base UI section if a base is pinned.""" + if not save_data.pinned_base_check: + return + + pinned_base_type = save_data.get_base_type_string(save_data.pinned_base_type) + pinned_box = container + + pinned_content_row = pinned_box.row(align=False) + pinned_text_col = pinned_content_row.column(align=False) + pinned_buttons_col = pinned_content_row.column(align=True) + + pinned_text_col.label( + text=f"{base_props.string_base} ({pinned_base_type})", icon="PINNED" + ) + pinned_text_condensed_column = pinned_text_col.column(align=True) + pinned_text_condensed_column.label( + text=f"{save_data.pinned_save_slot_name}, (...{save_data.pinned_save_account[-3:]})" + ) + + pinned_buttons_col.scale_x = 0.7 + pinned_buttons_col.operator( + "object.nms_unpin_base", icon="UNPINNED", text=f"Unpin {pinned_base_type}" + ) + pinned_buttons_col.operator( + "object.import_pinned_base", icon="IMPORT", text="Import from Save" + ) + + pinned_export_button_row = pinned_box.row(align=False) + pinned_export_button_row.operator( + "object.export_pinned_base", icon="FILE_TICK", text="Export to Save" + ) + pinned_export_button_backup_row = pinned_export_button_row.row(align=True) + pinned_export_button_backup_row.scale_x = 0.7 + pinned_export_button_backup_row.operator( + "object.make_pinned_savefile_backup", + icon="COLLECTION_NEW", + text="Backup Save files", + ) + +def draw_base_picker(container, save_data): + + save_folder_path = get_save_folder_path(bpy.context) + + # Make a separate section to display elements related to selecting bases. + save_folder_box = container + sf_column = save_folder_box.column(align=True) + sf_enable_row = sf_column.row(align=True) + sf_enable_row.label(text="Select Save") + + # This row will contain a field where location of save folder is displayed. + if save_data.check_plugin_enabled and save_data.validate_save_folder( save_folder_path ): + # Button to choose path to save folder. + sf_enable_row.operator( + "object.nms_select_save_folder", text="", icon="FILE_FOLDER" + ) + + # A button to enable the save editor, this will also install additional + # dependencies required when pressed. + sf_enable_row.separator() + sf_enable_button_row = sf_enable_row.row(align=True) + sf_enable_button_row.scale_x = 0.7 if not save_data.check_plugin_enabled else 1.0 + sf_enable_icon = "TRIA_DOWN" if not save_data.check_plugin_enabled else "TRIA_UP" + sf_enable_text = "Enable" if not save_data.check_plugin_enabled else "" + sf_enable_button_row.prop( + save_data, "check_plugin_enabled", icon=sf_enable_icon, text=sf_enable_text + ) + + # Everything below is the Select Save section, which only applies once the + # save editor is switched on. + if not save_data.check_plugin_enabled: + return + + if not save_data.validate_save_folder(save_folder_path): + sf_column.separator() + select_folder_info_col = sf_column.column(align=True) + select_folder_info_col.scale_y = 0.8 + select_folder_info_col.label(text=" Save Folder should end with") + select_folder_info_col.label(text=r' path like : "...\HelloGames\NMS"') + sf_column.separator() + select_folder_button_col = sf_column.column(align=True) + select_folder_button_col.alert = True + select_folder_button_col.operator( + "object.nms_select_save_folder", + text="Select Save Folder", + icon="FILE_FOLDER", + ) + return + + sf_column.separator() + # Display list of accounts, will display steam ids for recognition. + sf_column.label(text="Account / Save Slot") + sf_column.prop(save_data, "nms_account_selected", icon="COMMUNITY") + # Display save slots present within an account. + sf_column.prop(save_data, "nms_save_slot", icon="LINENUMBERS_ON") + + # Section where bases can be selected to import/update/pin. This section + # will only be visible when the base list has been loaded. + if not save_data.is_base_data_loaded(): + return + + # Display backup buttons when a slot is selected. + base_type = save_data.get_base_type_string(save_data.nms_base_type) + backup_row = sf_column.row(align=True) + backup_row.operator( + "object.make_savefile_backup", icon="COLLECTION_NEW", text="Backup Saves" + ) + backup_row.operator( + "object.open_backup_folder", icon="FOLDER_REDIRECT", text="Open Backup Folder" + ) + + sf_column.separator() + sf_column.label(text=f"\U0001F4E6 Total Parts : {save_data.get_total_parts_count()}") + se_column = sf_column.column(align=True) + se_column.label(text="Base Type:") + # Radio buttons to select type of base. + base_type_row = se_column.row(align=True) + base_type_row.prop(save_data, "nms_base_type", expand=True, text="base type") + se_column.separator() + + # List of bases/corvettes. + se_column.label(text=f"{base_type} Selected:") + + # Show the base list in red when no base/corvette is selected. + base_index_row = se_column.row(align=True) + base_index_row.alert = not SaveManager.is_base_selected + + if not SaveManager.is_base_selected: + # List of bases for selection. + base_index_row.prop( + save_data, "nms_base_index", text="", icon="GEOMETRY_SET" + ) + return + + # List of bases for selection. + base_index_row.prop(save_data, "nms_base_index", text="") + + # Display buttons when a base is selected from the list. + if save_data.nms_base_type != BaseType.EXTERNAL_BASE: + # Pin a base without importing it to the scene, so that save data can + # be updated to the location marked by it. + base_index_pin_row = base_index_row.row(align=True) + base_index_pin_row.scale_x = 0.6 + base_index_pin_row.operator( + "object.nms_pin_base", icon="PINNED", text=" Pin for easy-access" + ) + + se_column.separator() + import_export_row = se_column.row(align=True) + # Import button, this button will import the base to the scene. + import_export_row.operator( + "object.nms_import_base_from_save", icon="IMPORT", text="Import from Save" + ) + + if save_data.nms_base_type != BaseType.EXTERNAL_BASE: + import_export_row.operator( + "object.nms_export_base_to_save", icon="EXPORT", text="Export to Save" + ) + + +def draw_save_manager(layout, context): + """The entire save manager UI, drawn into whatever layout is passed in. + + Shared by the sidebar panel below and the header dropdown in + presentation/save_manager_menu.py, so it takes a layout rather than + reading self.layout off a particular panel. + """ + save_data = get_save_data(context) + base_props = getattr(context.scene, "nms_base_tool", None) + save_folder_path = get_save_folder_path(context) + + if save_data is None or base_props is None or save_folder_path is None: + layout.label(text="Save Manager unavailable", icon="ERROR") + return + + # Display data related to a pinned base on top if there is any within a + # blend file. + if save_data.pinned_base_check: + draw_pinned_base(layout.box(),save_data,base_props) + + draw_base_picker(layout.box(), save_data) + + # Save Editor Panel --- class NMS_PT_save_editor_panel(Panel): bl_idname = "NMS_PT_save_editor_panel" @@ -21,132 +229,4 @@ def poll(self, context): return True def draw(self, context): - layout = self.layout - save_data = context.scene.nms_save_data - prefs = context.preferences.addons[ADDON_ID].preferences - save_folder_path = prefs.nms_save_folder_path - - icon_pcoll = icons.get_icons_pscroll() - pin_icon = icon_pcoll["pin"] - - #display data related to a pinned base on top if there is any withing a blend file - if save_data.pinned_base_check: - pinned_base_type = save_data.get_base_type_string(save_data.pinned_base_type) - pinned_box = layout.box() - pinned_col = pinned_box.column(align = True) - #make a row to display name of base, and buttons to import base data and unpin the base - pinned_main_row = pinned_col.row(align = True) - pinned_main_left_col = pinned_main_row.column(align = True) - #base type - pinned_main_left_col.label(text = f"Pinned {pinned_base_type}", icon_value = pin_icon.icon_id) - #name of base - pinned_main_left_col.label(text=f"Name : {save_data.pinned_base_name}") - #display save slot and last 3 digits of account number for easy recognition - pinned_main_left_col.label(text = f"{save_data.pinned_save_slot_name}, (...{save_data.pinned_save_account[-3:]})") - pinned_main_left_col.prop(save_data,"check_also_update_name") - - #column containing unpin, inport and backup button - pinned_main_right_col = pinned_main_row.column(align = True) - pinned_main_right_col.scale_x = 0.7 - pinned_main_right_col.operator("object.nms_unpin_base", icon="UNPINNED", text = f"Unpin {pinned_base_type}") - pinned_main_right_col.separator() - pinned_main_right_col.operator("object.import_pinned_base", icon="IMPORT", text = "Import from Save") - pinned_main_right_col.operator("object.make_pinned_savefile_backup", icon = "COLLECTION_NEW", text = "Backup Save files") - - # display a big update button as it is going to be used alot - #update_row.scale_y = 1.2 - pinned_col.separator() - pinned_col.operator("object.export_pinned_base", icon="FILE_TICK", text = "Export to Save") - - #make a seprate section to display elements related to selecting bases - save_folder_box = layout.box() - sf_column = save_folder_box.column(align=True) - sf_enable_row = sf_column.row(align = True) - sf_enable_row.label(text = "Select Save")#DISK_DRIVE - - #this row will contain a field where location of save folder is displayed - if save_data.check_plugin_enabled and save_data.validate_save_folder(save_folder_path): - #button to choose path to save folder - sf_enable_row.operator( "object.nms_select_save_folder", text="", icon='FILE_FOLDER') - - # A button to enable save editor, this will also install additional dependencies required when pressed - sf_enable_row.separator() - sf_enable_button_row = sf_enable_row.row(align = True) - sf_enable_button_row.scale_x = 0.7 if not save_data.check_plugin_enabled else 1.0 - sf_enable_icon = "TRIA_DOWN" if not save_data.check_plugin_enabled else "TRIA_UP" - sf_enable_text = "Enable" if not save_data.check_plugin_enabled else "" - sf_enable_button_row.prop(save_data,"check_plugin_enabled", icon = sf_enable_icon, text = sf_enable_text) - - # Select Save - if save_data.check_plugin_enabled: - if not save_data.validate_save_folder(save_folder_path): - sf_column.separator() - select_folder_info_col = sf_column.column(align = True) - select_folder_info_col.scale_y = 0.8 - select_folder_info_col.label(text = " Save Folder should end with") - select_folder_info_col.label(text = r' path like : "...\HelloGames\NMS"') - sf_column.separator() - select_folder_button_col = sf_column.column(align = True) - select_folder_button_col.alert = True - select_folder_button_col.operator( "object.nms_select_save_folder", text="Select Save Folder", icon='FILE_FOLDER') - else: - is_base_data_loaded = save_data.is_base_data_loaded() - sf_column.separator() - #display list of accounts, will display steam ids for recognition - sf_column.label(text = "Account / Save Slot") - sf_column.prop(save_data, "nms_account_selected", icon = "COMMUNITY")#USER - #display save slots present within an account - sf_column.prop(save_data, "nms_save_slot", icon = "LINENUMBERS_ON") #linenumbers_on SORTSIZE - - #Section where bases can be selected to import/update/pin - #this section will only be visible when base list has been loaded - if is_base_data_loaded: - #display backup buttons when a slot is selected - base_type = save_data.get_base_type_string(save_data.nms_base_type) - backup_row = sf_column.row(align = True) - backup_row.operator("object.make_savefile_backup", icon="COLLECTION_NEW", text = "Backup Saves") - backup_row.operator("object.open_backup_folder", icon="FOLDER_REDIRECT", text = "Open Backup Folder") - - sf_column.separator() - sf_column.label(text = f"📦 Total Parts : {save_data.get_total_parts_count()}") - se_column = sf_column.column(align = True) - se_column.label(text = "Base Type:")# icon = "MOD_BUILD" - #radio buttons to select type of base - base_type_row = se_column.row(align=True) - base_type_row.prop(save_data, "nms_base_type",expand=True, text = "base type") - se_column.separator() - - #list of bases/corvettes - se_column.label(text = f"{base_type} Selected:")# icon = "ASSET_MANAGER" - - #show base list in red when on base/corvette is selected - base_index_row = se_column.row(align = True) - if not SaveManager.is_base_selected: - base_index_row.alert = True - else: - base_index_row.alert = False - - if SaveManager.is_base_selected: - - #list of bases for selection - base_index_row.prop(save_data, "nms_base_index", text="") - - #display buttons when a base is selected from list - if save_data.nms_base_type != BaseType.EXTERNAL_BASE: - # show pin button and a base/corvette is selected - base_index_pin_row = base_index_row.row(align=True) - base_index_pin_row.scale_x = 0.6 - #Pin button, pin a base without imorting it to scene, so that save data can be updated to location marked by it - base_index_pin_row.operator("object.nms_pin_base", icon="PINNED", text = " Pin for easy-access") - - se_column.separator() - import_export_row = se_column.row(align=True) - #Import button, this button will import base to scene - import_export_row.operator("object.nms_import_base_from_save", icon="IMPORT", text = "Import from Save") - - if save_data.nms_base_type != BaseType.EXTERNAL_BASE: - import_export_row.operator("object.nms_export_base_to_save", icon="EXPORT", text = "Export to Save") - - else : - #list of bases for selection - base_index_row.prop(save_data, "nms_base_index", text="", icon = "GEOMETRY_SET") + draw_save_manager(self.layout, context) diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py index 4070afe7..2193dc78 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py @@ -20,6 +20,8 @@ class BaseType: BASE = "HomePlanetBase" FREIGHTER = "FreighterBase" EXTERNAL_BASE = "ExternalPlanetBase" + SPACESTATION_BASE = "PlayerSpaceStationBase" + SPACE_BASE = "PlayerSpaceBase" class BaseData: @@ -148,44 +150,45 @@ def get_save_slots_list(account): pattern = re.compile(r"save(\d+)\.hg") # store list of all hg save files hg_files_list = get_hg_files_in_folder(account) - + hg_files_set = set(hg_files_list) + # interate through each save file and record their pairs save_slots = [] for save_2 in hg_files_list: - - # save type is "Main" for normal save and "Season" for expeditiion - try: - save_type = SaveFile(save_2).search_property(SaveTranslation.active_context) - if save_type is None: - continue - except: - continue - - # validate name of save file + + # validate name of save file, save.hg has no number and is picked up as the pair of save2.hg match = pattern.fullmatch(save_2.name) if not match: continue - + #extract number from name of save file file_number = int(match.group(1)) if file_number %2 == 1: continue - + #name of linked save file save_1_name = "save.hg" if file_number == 2 else f"save{file_number - 1}.hg" - save_1 = Path(save_2.parent / save_1_name ) + save_1 = save_2.parent / save_1_name #check if linked save file exits in save folder or not - save_1_found = next((p for p in hg_files_list if p == save_1), None) - if not save_1_found: + if save_1 not in hg_files_set: continue - + + # save type is "Main" for normal save and "Season" for expeditiion + # both properties sit near the top of the save, so only the first block is usually decompressed + try: + properties = SaveFile(save_2).search_properties([SaveTranslation.active_context, SaveTranslation.save_name]) + except Exception: + continue + save_type = properties[SaveTranslation.active_context] + if save_type is None: + continue + #slot number is always half of second save file's number save_slot_number = file_number//2 #links to save files for this slot saves_links = [str(save_1), str(save_2)] - #extract save's name from save file data by partially loading it, and increasing efficiency - save_name = SaveFile(save_2).search_property(SaveTranslation.save_name) if save_type == "Main" else "Expedition" - + save_name = properties[SaveTranslation.save_name] if save_type == "Main" else "Expedition" + save_slot = { "slot": save_slot_number, "saves": saves_links, @@ -211,6 +214,9 @@ def extract_bases_list_from_save(save_slot): external = [] freighter = None + space = [] + space_station = [] + total_parts_count = 0 #record only what is necessary rather than entire data about base @@ -231,6 +237,10 @@ def extract_bases_list_from_save(save_slot): case BaseType.EXTERNAL_BASE: if base_data.parts_count > 0: external.append(base_data) + case BaseType.SPACE_BASE: + space.append(base_data) + case BaseType.SPACESTATION_BASE: + space_station.append(base_data) if base_data.base_type != BaseType.EXTERNAL_BASE: total_parts_count += base_data.parts_count @@ -244,6 +254,8 @@ def extract_bases_list_from_save(save_slot): "bases":bases, "external":external, "freighter": freighter, + "space":space + space_station, + "spacestation":space_station, "total_parts_count": total_parts_count, } @@ -271,7 +283,7 @@ def get_save_file(save_slot): # impart a base from save file def import_paticular_base_from_save(base_identifier, save_slot): save_file = get_save_file(save_slot) - data = save_file.load() + save_file.load() # fist see if base actially exists or not searched_base = save_file.search_base_with_identifier(base_identifier) @@ -283,31 +295,38 @@ def import_paticular_base_from_save(base_identifier, save_slot): return None #return bases after translating it to engish - return save_translation.translate_to_eng_data(searched_base) + searched_base = save_translation.translate_to_eng_data(searched_base) + + return searched_base #save a base to save file -def save_base_to_save_file(objects_data, base_identifier, save_slot, new_base_name = None): - save_file = get_save_file(save_slot) - data = save_file.load() - - # look for base in save file to see it it exist or not - in_base = save_file.search_base_with_identifier(base_identifier) - if in_base is None: - return - - # here update objects list with list provided - in_base[SaveTranslation.objects] = save_translation.translate_to_obf_data(objects_data) - - - # update name of base if provided - if new_base_name is not None: - ship_ownsership = save_file.get_ship_ownership_pointer() - in_base[SaveTranslation.base_name] = new_base_name - ship_ownsership[base_identifier["user_data"]][SaveTranslation.base_name] = new_base_name - - # save the file and make backup after update it - save_file.make_backup() - save_file.save() +def save_base_to_save_file(objects_data, base_identifier, save_slot, base_name = None): + from .save_file import SaveFile + for slot in save_slot: + save_file = SaveFile(slot) + save_file.load() + + # look for base in save file to see it it exist or not + in_base = save_file.search_base_with_identifier(base_identifier) + if in_base is None: + return + + # here update objects list with list provided + in_base[SaveTranslation.objects] = save_translation.translate_to_obf_data(objects_data) + + # update name of base if provided + if base_name is not None: + # update name in PersistentPlayerBases + in_base[SaveTranslation.base_name] = base_name + + # update name in ship_ownsership + userdata = base_identifier["user_data"] + ship_ownsership_element = save_file.get_ship_ownsership_element(userdata) + ship_ownsership_element[SaveTranslation.base_name] = base_name + + # save the file and make backup after update it + save_file.make_backup() + save_file.save() return "Base/Corvette saved sucessfully" # a folder within save_directory , where backups will be stored diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py index 9f987617..fab2adb2 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py @@ -1,4 +1,5 @@ import os +import re import json import struct import lz4.block @@ -11,6 +12,13 @@ MAGIC = 0xFEEDA1E5 CHUNK_SIZE = 0x80000 # 524288 bytes +# turn the raw bytes between a json string's quotes into text, resolving escapes like \" and \n +def decode_json_string(raw): + try: + return json.loads(b'"' + raw + b'"') + except ValueError: + return raw.decode("utf-8", errors="replace") + # this handles loading, saving and backup of a hg save file with provided path class SaveFile: @@ -61,7 +69,7 @@ def save(self, output_path=None): self.json_data, separators=(",", ":"), ensure_ascii=False - ).encode("utf-8") + ).encode("utf-8") + b"\x00" blocks = [] @@ -91,36 +99,40 @@ def make_backup(self, output_path = None): ) shutil.copy2(path, backup_file) + # search string properties in save file, decompressing one block at a time and stopping as soon as all are found + # returns {property: value}, value is None for properties that were not found + def search_properties(self, properties): + patterns = { + prop: re.compile(rb'"' + re.escape(prop.encode("utf-8")) + rb'"\s*:\s*"((?:[^"\\]|\\.)*)"') + for prop in properties + } + found = {prop: None for prop in properties} + buffer = bytearray() + with open(self.path, "rb") as f: + while patterns: + header = f.read(16) + if not header: + break + if len(header) < 16: + raise ValueError("Invalid HG block header") + magic, comp_size, decomp_size, _ = struct.unpack(" len(raw): - raise ValueError("Invalid HG block header") - magic, comp_size, decomp_size, _ = struct.unpack( "= userdata: + print(f"UserData = {userdata} , value is invalid") + return None + return ship_ownsership_pointer[userdata] # Return pointer to PlayerFreighterName in save data def get_freighter_name(self): @@ -227,6 +247,8 @@ def matches_base(self, base, identifier : BaseData): ) return base_tuple == identifier_tuple + + \ No newline at end of file diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py index 265a9f77..b47be303 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py @@ -1,6 +1,5 @@ import bpy -import json -from .. import builder +from .. import builder, builder_v2 from . import save_editor_dependencies from . import save_editor_utils from .save_editor_utils import BaseType, BaseData @@ -33,8 +32,9 @@ class SaveManager(bpy.types.PropertyGroup): # stores index of selected base in alternative tab when tabs are switched selected_base_type_tabs = { BaseType.CORVETTE : "Default", - BaseType.BASE: "Default", - BaseType.EXTERNAL_BASE: "Default" + BaseType.BASE : "Default", + BaseType.SPACE_BASE : "Default", + BaseType.EXTERNAL_BASE : "Default" } # A toggle button at top of Save Editor section @@ -81,8 +81,9 @@ class SaveManager(bpy.types.PropertyGroup): description="Type of the base, it can be a corvette and a normal planet base.", items = [ (BaseType.CORVETTE, "Corvette", "show list of corvettes and freighters",'AUTO',0), - (BaseType.BASE, "Base", "show list of bases",'HOME',1), - (BaseType.EXTERNAL_BASE, "", "show list of External",'INTERNET',2), + (BaseType.BASE, "Planet", "show list of bases",'HOME',1), + (BaseType.SPACE_BASE, "Space", "show list of space bases",'LIGHT_SUN',2), + (BaseType.EXTERNAL_BASE, "", "show list of External",'INTERNET',3), ], update = lambda self, context: self.on_base_type_selected(), options={'SKIP_SAVE'}, @@ -258,22 +259,28 @@ def get_bases_enum_list(self, extracted_base_data): enum_bases = [] base_type_key = self.get_base_type_key(self.nms_base_type) - + bases_list = extracted_base_data[base_type_key] + # convert list of bases into list for UI - for index, base_data in enumerate(extracted_base_data[base_type_key]): - + for base_data in bases_list: base_data: BaseData - list_entry = "" + base_name = base_data.base_name user_data = str(base_data.user_data) base_index = str(base_data.base_index) parts_count = str(base_data.parts_count) + + m_base_type = str(base_data.base_type) + + prefix = "" if self.nms_base_type == BaseType.CORVETTE: # if base is a corvette, add user_data in front of its name that represents its ingame slot number - list_entry = f"{user_data.rjust(2)}. {base_name} [ {parts_count} parts ]" - else : - list_entry = f"{base_name} [ {parts_count} parts ]" + prefix = f"{user_data.rjust(2)}. " + elif self.nms_base_type == BaseType.SPACE_BASE and m_base_type == BaseType.SPACESTATION_BASE: + prefix = f"(SpaceStation) " + + list_entry = prefix + f"{base_name} [ {parts_count} parts ]" # base index is stored here to make it easier to traverse save file, it is index of base within persistent player bases array enum_bases.append((base_index, list_entry, " base desc")) @@ -330,22 +337,16 @@ def import_base_from_save_file(self,context): def import_base(self, context, base_identifiers, save_links): #try to import json data from savefile and return error if data was not found - newly_imported_base = save_editor_utils.import_paticular_base_from_save(base_identifiers, save_links) - if newly_imported_base is None: + # translation already returns a fresh copy of the base, so it can be used directly + nms_base_json = save_editor_utils.import_paticular_base_from_save(base_identifiers, save_links) + if nms_base_json is None: return "Error Importing base, base data is None" - - try: - # convert imported base data to json - nms_import_data = json.dumps(newly_imported_base) - nms_base_json = json.loads(nms_import_data) - except: - return "Could not import base data, Incorrect json" # Import json into scene nms_tools = context.scene.nms_base_tool nms_tools.deserialise_from_data(nms_base_json) - BUILDER.deserialise_from_data(nms_base_json) - + #BUILDER.deserialise_from_data(nms_base_json) + builder_v2.deserialise_from_data(nms_base_json) #return a string for operators for status message return "Base/Corvette imported sucessfully" @@ -366,12 +367,21 @@ def export_base_to_save_file(self,context): return self.export_base(context, base_identifiers, current_slot_data["saves"]) # collect data from scene and export it to save file - def export_base(self,context, base_identifiers, save_links, new_base_name = None): + def export_base(self,context, base_identifiers, save_links): # convert scene to json representing base data nms_tools = context.scene.nms_base_tool serialised_base_objects_data = nms_tools.serialise(objects_only = True) + new_base_name = nms_tools.string_base + if not new_base_name or len(new_base_name) > 0: + new_base_name = None + # provide data to utils and return status string to calling function - return save_editor_utils.save_base_to_save_file(serialised_base_objects_data, base_identifiers, save_links, new_base_name) + result = save_editor_utils.save_base_to_save_file(serialised_base_objects_data, base_identifiers, save_links, base_name = new_base_name) + + # refresh UI + self.refresh_bases_list() + + return result # This functin collects data realated to base so that it can be identified in save file # since there is no unique property to identify a base, we prepare a fingerprint of that base with collection of properties @@ -425,7 +435,8 @@ def reset_base_list(self): SaveManager.selected_base_type_tabs = { BaseType.CORVETTE: "Default", BaseType.BASE: "Default", - BaseType.EXTERNAL_BASE: "Default" + BaseType.EXTERNAL_BASE: "Default", + BaseType.SPACE_BASE: "Default" } if SaveManager.enum_base_list is not None: @@ -515,23 +526,7 @@ def export_pinned_base(self, context): if base_identifiers is None: return "Pinned base identifiers are none" - # base name will not be udpated if it is None - new_base_name = None - - # check if export base name check box is checked - if self.check_also_update_name: - scene = context.scene - nms_tool = scene.nms_base_tool - string_base_name = nms_tool.string_base.strip() - # update of new base name after validating string base name - if string_base_name: - new_base_name = string_base_name - - result = self.export_base(context, base_identifiers, base_identifiers.save_slot, new_base_name) - - # upate base name if base name was sucessfully changed in save file - if result is not None and new_base_name is not None: - self.update_base_name(new_base_name) + result = self.export_base(context, base_identifiers, base_identifiers.save_slot) return result @@ -599,7 +594,24 @@ def get_base_type_key(self,base_type = None): return "bases" case BaseType.EXTERNAL_BASE: return "external" + case BaseType.SPACE_BASE: + return "space" + case BaseType.SPACESTATION_BASE: + return "space" return "bases" def get_total_parts_count(self): - return str(SaveManager.extracted_base_data["total_parts_count"]) \ No newline at end of file + return str(SaveManager.extracted_base_data["total_parts_count"]) + + # returns pinned base data extracted from save file + def get_pinned_base_data(self): + identifiers = self.get_pinned_base_identifiers() + save_slot = identifiers.save_slot + pinned_base = save_editor_utils.import_paticular_base_from_save(identifiers, save_slot) + return pinned_base + + # refresh list of bases/corvettes by importing fresh data form save file + def refresh_bases_list(self): + current_slot_data = self.get_current_slot_data() + SaveManager.extracted_base_data = save_editor_utils.extract_bases_list_from_save(current_slot_data["saves"]) + self.on_base_type_selected() \ No newline at end of file From d5c22ff097c022e25a826367cb4e5f7677762345 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:22:33 +0530 Subject: [PATCH 06/15] Switched to Swap in Place method for writing save files. Minor optimisations. --- .../save_editor/save_editor_operators.py | 19 +-- .../save_editor/save_editor_utils.py | 128 ++++++++++++++---- .../save_editor/save_file.py | 36 ++++- .../save_editor/save_manager.py | 43 +++--- 4 files changed, 163 insertions(+), 63 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py index f40f9b4b..edd3e1a0 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py @@ -67,9 +67,11 @@ class ExportBaseToSave(bpy.types.Operator): def execute(self, context): scene = context.scene save_data = scene.nms_save_data - result = save_data.export_base_to_save_file(context) - if result is not None: - self.report({'INFO'}, result) + success, message = save_data.export_base_to_save_file(context) + if not success: + self.report({'ERROR'}, message) + return {"CANCELLED"} + self.report({'INFO'}, message) return {"FINISHED"} # Button to pin base to top of the editor @@ -138,12 +140,11 @@ class ExoprtPinnedBase(bpy.types.Operator): def execute(self, context): scene = context.scene save_data = scene.nms_save_data - result = save_data.export_pinned_base(context) - if result is not None: - self.report({'INFO'}, result) - else: - self.report({'ERROR'}, "Update Failed, repinning the base may resolve this issue ") - print("result is none") + success, message = save_data.export_pinned_base(context) + if not success: + self.report({'ERROR'}, message) + return {"CANCELLED"} + self.report({'INFO'}, message) return {"FINISHED"} # a button to manually backup save files diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py index 2193dc78..4697225e 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py @@ -299,35 +299,109 @@ def import_paticular_base_from_save(base_identifier, save_slot): return searched_base +# error with its cause, for messages shown to the user +# file errors drop their full paths, the message already names the file +def describe_error(error): + if isinstance(error, OSError) and error.strerror: + return f"{type(error).__name__}: {error.strerror}" + return f"{type(error).__name__}: {error}" + #save a base to save file +# the newest save file is the one the game loads, so it must contain the base, the older one is updated too when it has it +# both files are prepared and backed up before anything is written, and the older file is written first so the newest stays newest +# returns (success, message) def save_base_to_save_file(objects_data, base_identifier, save_slot, base_name = None): - from .save_file import SaveFile - for slot in save_slot: - save_file = SaveFile(slot) - save_file.load() - - # look for base in save file to see it it exist or not - in_base = save_file.search_base_with_identifier(base_identifier) - if in_base is None: - return - - # here update objects list with list provided - in_base[SaveTranslation.objects] = save_translation.translate_to_obf_data(objects_data) - - # update name of base if provided - if base_name is not None: - # update name in PersistentPlayerBases - in_base[SaveTranslation.base_name] = base_name - - # update name in ship_ownsership - userdata = base_identifier["user_data"] - ship_ownsership_element = save_file.get_ship_ownsership_element(userdata) - ship_ownsership_element[SaveTranslation.base_name] = base_name - - # save the file and make backup after update it - save_file.make_backup() - save_file.save() - return "Base/Corvette saved sucessfully" + from .save_file import SaveFile, write_file_atomic + + newest = get_lastes_save_file_location(save_slot) + ordered_paths = [Path(p) for p in save_slot if Path(p) != newest] + [newest] + obf_objects = save_translation.translate_to_obf_data(objects_data) + + # load, update and compress each file in memory, nothing is written yet + prepared = [] + skipped = [] + for path in ordered_paths: + is_newest = path == newest + try: + save_file = SaveFile(path) + save_file.load() + + # look for base in save file to see it it exist or not + in_base = save_file.search_base_with_identifier(base_identifier) + if in_base is None: + if is_newest: + return False, ( + f"Export failed: base not found in {path.name}, the newest save file. " + "Nothing was written, repinning the base may resolve this issue" + ) + skipped.append(f"{path.name} (base not found)") + continue + + # here update objects list with list provided + in_base[SaveTranslation.objects] = obf_objects + + # update name of base if provided + if base_name is not None: + # update name in PersistentPlayerBases + in_base[SaveTranslation.base_name] = base_name + + # update name in ship_ownsership + userdata = base_identifier["user_data"] + ship_ownsership_element = save_file.get_ship_ownsership_element(userdata) + ship_ownsership_element[SaveTranslation.base_name] = base_name + + prepared.append((path, save_file.pack())) + except Exception as error: + if is_newest: + return False, f"Export failed preparing {path.name}: {describe_error(error)}. Nothing was written" + skipped.append(f"{path.name} ({describe_error(error)})") + + # back up every file before the first write + backups = {} + for path, _ in prepared: + try: + backups[path] = SaveFile(path).make_backup() + except Exception as error: + return False, f"Export failed backing up {path.name}: {describe_error(error)}. Nothing was written" + + # write, and put back files already written if a later one fails, so the slot is never left half exported + written = [] + for path, data in prepared: + try: + write_file_atomic(path, data) + except Exception as error: + message = f"Export failed writing {path.name}: {describe_error(error)}. " + return False, message + restore_from_backups(written, backups) + written.append(path) + + message = "Base/Corvette saved sucessfully to " + " and ".join(path.name for path, _ in reversed(prepared)) + if skipped: + message += ", skipped " + ", ".join(skipped) + return True, message + +# put written save files back from their backups, keeping their original modified times so the newest file stays newest +# returns a sentence describing what happened, for the export message +def restore_from_backups(written, backups): + from .save_file import write_file_atomic + + if not written: + return "Nothing was written" + + failed = [] + for path in written: + try: + backup = Path(backups[path]) + write_file_atomic(path, backup.read_bytes()) + shutil.copystat(backup, path) + except Exception as error: + failed.append(f"{path.name} ({describe_error(error)})") + + if failed: + return ( + "Restoring from backup also failed for " + ", ".join(failed) + + ", copy them back by hand from the blender_backup folder" + ) + return ", ".join(path.name for path in written) + " restored from backup" # a folder within save_directory , where backups will be stored def get_backups_folder(save_links): diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py index fab2adb2..813f4566 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py @@ -19,6 +19,25 @@ def decode_json_string(raw): except ValueError: return raw.decode("utf-8", errors="replace") +# write a file so that it is either fully replaced or left untouched, never half written +# data goes to a temporary file next to it first, which is then swapped in with a single rename +def write_file_atomic(path, data): + path = Path(path) + temp_path = path.with_name(path.name + ".tmp") + try: + with open(temp_path, "wb") as f: + f.write(data) + f.flush() + os.fsync(f.fileno()) + os.replace(temp_path, path) + except Exception: + # the original file is still intact, only the temporary file needs removing + try: + os.remove(temp_path) + except OSError: + pass + raise + # this handles loading, saving and backup of a hg save file with provided path class SaveFile: @@ -59,12 +78,11 @@ def load(self): self.json_data = json.loads(json_text) return self.json_data - # save data to save file - def save(self, output_path=None): + # compress loaded data into the bytes of a hg save file, without touching the disk + def pack(self): if self.json_data is None: raise ValueError("No JSON data loaded") - output_path = output_path or self.path json_bytes = json.dumps( self.json_data, separators=(",", ":"), @@ -78,11 +96,14 @@ def save(self, output_path=None): compressed = lz4.block.compress( chunk, store_size=False) header = struct.pack(" 0: new_base_name = None - # provide data to utils and return status string to calling function - result = save_editor_utils.save_base_to_save_file(serialised_base_objects_data, base_identifiers, save_links, base_name = new_base_name) - - # refresh UI - self.refresh_bases_list() - - return result + # provide data to utils and return status to calling function + success, message = save_editor_utils.save_base_to_save_file(serialised_base_objects_data, base_identifiers, save_links, base_name = new_base_name) + + # refresh UI, a pinned base can be exported while no save slot is selected + if success and self.get_current_slot_data() is not None: + self.refresh_bases_list() + + return success, message # This functin collects data realated to base so that it can be identified in save file # since there is no unique property to identify a base, we prepare a fingerprint of that base with collection of properties @@ -520,15 +524,14 @@ def import_pinned_base(self,context): self.import_base(context,base_identifiers, base_identifiers.save_slot) # called to save scene data to save file + # returns (success, message) def export_pinned_base(self, context): base_identifiers = self.get_pinned_base_identifiers() - + if base_identifiers is None: - return "Pinned base identifiers are none" - - result = self.export_base(context, base_identifiers, base_identifiers.save_slot) - - return result + return False, "Pinned base identifiers are none" + + return self.export_base(context, base_identifiers, base_identifiers.save_slot) # collect data of pinned base and return it def get_pinned_base_identifiers(self): From 6296312b7c2d0823e3dcfa1c54b7b1121369403d Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:30:30 +0530 Subject: [PATCH 07/15] Restructure builder into a package and move builder.py into a builder/ and split responsibility between different modules. Use one builder.BUILDER in entier folder --- .../no_mans_sky_base_builder/__init__.py | 2 +- .../no_mans_sky_base_builder/builder.py | 549 ------------------ .../builder/__init__.py | 4 + .../builder/builder.py | 283 +++++++++ .../builder/catalog.py | 139 +++++ .../builder/importer.py | 162 ++++++ .../builder/overrides.py | 76 +++ .../no_mans_sky_base_builder/builder/paths.py | 11 + src/addons/no_mans_sky_base_builder/part.py | 175 +++--- .../resources/mirror_corrections.json | 3 +- .../save_editor/save_manager.py | 7 +- .../tools/batch_tool.py | 2 +- .../tools/build_tool.py | 2 +- .../tools/properties.py | 2 +- .../no_mans_sky_base_builder/utils/curve.py | 2 +- 15 files changed, 773 insertions(+), 646 deletions(-) delete mode 100644 src/addons/no_mans_sky_base_builder/builder.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/__init__.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/builder.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/catalog.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/importer.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/overrides.py create mode 100644 src/addons/no_mans_sky_base_builder/builder/paths.py diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 1c997262..911bbc55 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -38,7 +38,7 @@ PRESET_PATH = os.path.join(USER_PATH, "presets") ASSET_BROWSER_PATH = os.path.join(FILE_PATH, "asset_browser") -BUILDER = builder.Builder() +BUILDER = builder.BUILDER GHOSTED_JSON = os.path.join(FILE_PATH, "resources", "ghosted.json") ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] diff --git a/src/addons/no_mans_sky_base_builder/builder.py b/src/addons/no_mans_sky_base_builder/builder.py deleted file mode 100644 index 64d4a8f2..00000000 --- a/src/addons/no_mans_sky_base_builder/builder.py +++ /dev/null @@ -1,549 +0,0 @@ -"""The builder contains all top level scene methods for managing NMS parts.""" - -# import cProfile -import importlib -import json -import math -import os -import time -from collections import defaultdict -from copy import copy - -import bpy -from mathutils import Matrix - -from . import part, preset -from .part_overrides import (bone, bone_replacer, line, locked, message, - power_control, turret, u_bytebeatline, u_pipeline, - u_portalline, u_powerline) -from .utils import blend_utils -from .utils import python as python_utils - - -class Builder(object): - - # Tool Level Paths --- - USER_PATH = os.path.join(os.path.expanduser("~"), "NoMansSkyBaseBuilder") - FILE_PATH = os.path.dirname(os.path.realpath(__file__)) - MODEL_PATH = os.path.join(FILE_PATH, "models") - FOSSIL_PARTS_PATH = os.path.join(FILE_PATH, "models", "fossil_parts") - NICE_JSON = os.path.join(FILE_PATH, "resources", "nice_names.json") - MODS_PATH = os.path.join(USER_PATH, "mods") - PRESET_PATH = os.path.join(USER_PATH, "presets") - - # Load in nice name information. - nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) - - override_classes = { - bone_replacer.BONE_REPLACER: [ - "FOS_HEAD", - "FOS_SKULL", - "FOS_LIMBS", - "FOS_TAIL", - "FOS_BODY", - ], - bone.BONE: [ - os.path.splitext(filename)[0] for filename in os.listdir(FOSSIL_PARTS_PATH) - ], - turret.TURRET: ["B_TUR_A", "B_TUR_B", "B_TUR_C", "B_TUR_D", "B_TUR_E"], - u_powerline.U_POWERLINE: ["U_POWERLINE"], - u_pipeline.U_PIPELINE: ["U_PIPELINE"], - u_portalline.U_PORTALLINE: ["U_PORTALLINE"], - u_bytebeatline.U_BYTEBEATLINE: ["U_BYTEBEATLINE"], - power_control.POWER_CONTROL: ["POWER_CONTROL"], - locked.LOCKED: [ - "BASE_FLAG", - "BRIDGECONNECTOR", - "AIRLCKCONNECTOR", - "FREIGHTER_CORE", - ], - message.MESSAGE: [ - "MESSAGEMODULE", - "BYTEBEAT", - "BYTEBEATSWITCH", - "HOLO_DISCO_0", - "FOS_BI", - "FOS_BIRD", - "FOS_BIRD_DIS", - "FOS_BI_DIS", - "FOS_BODY", - "FOS_BODY_DISP", - "FOS_BODY_MNT", - "FOS_GRUN", - "FOS_GRUN_DIS", - "FOS_LIMBS", - "FOS_LIMBS_DISP", - "FOS_LIMBS_MNT", - "FOS_QUAD", - "FOS_QUAD_DIS", - "FOS_SKULL", - "FOS_SKULL_DISP", - "FOS_SKULL_MNT", - "FOS_TAIL", - "FOS_TAIL_DISP", - "FOS_TAIL_MNT", - "FOS_WORM", - "FOS_WORM_DIS", - ], - } - - def __init__(self): - """Builder __init__.""" - - # Part Cache. - self.__part_cache = {} - self.__preset_cache = {} - - # Construct category and OBJ reference. - # Create default part pack. - self.available_packs = [("Parts", self.MODEL_PATH)] - - # Find any mods with model packs inside. - if os.path.exists(self.MODS_PATH): - mod_folders = os.listdir(self.MODS_PATH) - for mod_folder in mod_folders: - full_mod_path = os.path.join(self.MODS_PATH, mod_folder) - if "models" in os.listdir(full_mod_path): - full_model_path = os.path.join(self.MODS_PATH, mod_folder, "models") - self.available_packs.append((mod_folder, full_model_path)) - - # Find Parts and build a reference dictionary. - self.part_reference = {} - for pack_name, pack_folder in self.available_packs: - for category in self.get_categories(pack=pack_name): - parts = self.get_objs_from_category(category, pack=pack_name) - for part in parts: - # Get Unique ID. - unique_id = os.path.splitext(part)[0] - # Construct full path. - search_path = pack_folder or self.MODEL_PATH - part_path = os.path.join(search_path, category, part) - # Place part information into reference. - self.part_reference[unique_id] = { - "category": category, - "full_path": part_path, - "pack": pack_name, - } - - def clear_caches(self): - """Clear all the caches we use in this class.""" - self.__part_cache.clear() - - def add_to_part_cache(self, object_id, bpy_object): - """Add item to part cache.""" - self.__part_cache[object_id] = bpy_object.name - - def add_to_preset_cache(self, object_id, bpy_object): - """Add item to preset cache.""" - self.__part_cache[object_id] = bpy_object.name - - def find_object_by_id(self, object_id): - """Get the item from the part cache.""" - part_name = self.__part_cache.get(object_id, None) - # Return None if not found. - if not part_name: - return None - # If something is found, we need to check if it still exists. - if part_name in bpy.data.objects: - bpy_object = bpy.data.objects[part_name] - return self.get_builder_object_from_bpy_object(bpy_object) - # If all fails, return None. - return None - - @classmethod - def get_part_class(cls, object_id): - for class_ref, part_list in cls.override_classes.items(): - if object_id in part_list: - return class_ref - return part.Part - - def get_builder_object_from_bpy_object(self, bpy_object): - # Handle Presets. - if "PresetID" in bpy_object: - return preset.Preset.deserialise_from_object( - bpy_object=bpy_object, builder_object=self - ) - - # Handle Parts. - object_id = None - if "ObjectID" in bpy_object: - object_id = bpy_object.get("ObjectID") - elif "SnapID" in bpy_object: - object_id = bpy_object.get("SnapID") - - if not object_id: - return None - - use_class = self.get_part_class(object_id) - return use_class.deserialise_from_object( - bpy_object=bpy_object, builder_object=self - ) - - def find_preset_by_id(self, preset_id): - """Get the item from the part cache.""" - preset_name = self.__part_cache.get(preset_id, None) - # Return None if not found. - if not preset_name: - return None - # If something is found, we need to check if it still exists. - if preset_name in bpy.data.objects: - bpy_object = bpy.data.objects[preset_name] - return preset.Preset.deserialise_from_object( - bpy_object=bpy_object, builder_object=self - ) - # If all fails, return None. - return None - - def get_all_parts( - self, exclude_presets=False, skip_object_type=None, include_lines=False - ): - """Get all NMS parts in the scene. - - Args: - get_presets (bool): Choose to exclude parts generated via - preset. - """ - # Validate skip list - skip_object_type = skip_object_type or [] - - # Get all individual NMS parts. - flat_parts = [part for part in bpy.data.objects if "ObjectID" in part] - flat_parts = [ - part for part in flat_parts if part["ObjectID"] not in skip_object_type - ] - - # Include line conatrol points? - if include_lines: - flat_parts.extend( - [ - part - for part in bpy.data.objects - if "SnapID" in part and not "ObjectID" in part - ] - ) - - # If exclude presets is on, just return the top level objects. - if exclude_presets: - flat_parts = [ - part for part in flat_parts if part["belongs_to_preset"] == False - ] - flat_parts = sorted(flat_parts, key=Builder.by_order) - return flat_parts - - def get_all_presets(self): - """Get all Builder preset items in the scene.""" - return [part for part in bpy.data.objects if "PresetID" in part] - - def add_part(self, object_id, user_data=None, build_rigs=True): - """Add an item based on it's object ID.""" - use_class = self.get_part_class(object_id) - item = use_class( - object_id=object_id, - builder_object=self, - user_data=user_data, - build_rigs=build_rigs, - ) - return item - - def add_preset(self, preset_id): - """Add an item based on it's preset ID.""" - item = preset.Preset(preset_id=preset_id, builder_object=self) - return item - - def mirror_part(self, part_object): - object_id = part_object["ObjectID"] - new_object_id = part.Part.get_mirror_part_id(object_id) - # Flip mesh vertices across X - mirror_matrix = Matrix.Scale(-1, 4, (1, 0, 0)) - part_object.data.transform(mirror_matrix) - # Update ObjectID and name - part_object["ObjectID"] = new_object_id - part_object.name = new_object_id - # Update cache - if object_id in self.__part_cache: - del self.__part_cache[object_id] - self.__part_cache[new_object_id] = part_object.name - return part_object - - def flip_part(self, part_object): - object_id = part_object["ObjectID"] - new_object_id = part.Part.get_flip_part_id(object_id) - # Flip mesh vertices across Y - mirror_matrix = Matrix.Scale(-1, 4, (0, 1, 0)) - part_object.data.transform(mirror_matrix) - # Update ObjectID and name - part_object["ObjectID"] = new_object_id - part_object.name = new_object_id - # Update cache - if object_id in self.__part_cache: - del self.__part_cache[object_id] - self.__part_cache[new_object_id] = part_object.name - return part_object - - # Serialising --- - def serialise(self, get_presets=False, add_timestamp=False, as_prefab=False): - """Return NMS compatible dictionary. - - Args: - get_presets (bool): This will generate data for presets. And - exclude parts generated from presets. - Returns: - dict: Dictionary of base information. - """ - # Get all object part data. - object_list = [] - - for item in self.get_all_parts(exclude_presets=get_presets): - object_id = item["ObjectID"] - use_class = self.get_part_class(object_id) - item_obj = use_class.deserialise_from_object(item, builder_object=self) - object_list.append(item_obj.serialise()) - - # Create full dictionary. - key = "Prefab" if as_prefab else "Objects" - data = {key: object_list} - - if add_timestamp: - data["timestamp"] = int(time.time()) - - # Add Base Version - data["BaseVersion"] = 8 - - # Add preset information if specified. - if get_presets: - preset_list = [] - for _preset in self.get_all_presets(): - preset_obj = preset.Preset.deserialise_from_object( - _preset, builder_object=self - ) - preset_list.append(preset_obj.serialise()) - data["Presets"] = preset_list - - return data - - def deserialise_from_data(self, data): - """Given NMS data, reconstruct the base. - - We don't need to create a new class, we can act upon this one. - """ - - base_version = data.get("BaseVersion", 8) - - compensate_normal = True - if base_version < 5: - compensate_normal = False - - # Reconstruct objects. - for part_data in data.get("Objects", []): - object_id = part_data.get("ObjectID").replace("^", "") - use_class = self.get_part_class(object_id) - use_class.deserialise_from_data( - part_data, self, compensate_normal=compensate_normal - ) - - # Reconstruct presets. - for preset_data in data.get("Presets", []): - preset.Preset.deserialise_from_data( - preset_data, self, compensate_normal=compensate_normal - ) - - # Build Rigs. - self.build_rigs() - # Optimise control points. - self.optimise_control_points() - - @staticmethod - def by_order(bpy_object): - """Sorting method to get objects by the order attribute. - - Args: - bpy_object (bpy.ob): A blender object. - - Returns: - int: The order of which the item is/was built. - """ - return bpy_object.get("order", 0) - - # Category Methods --- - def get_categories(self, pack=None): - """Get the list of categories. - - Args: - pack (str): The model pack search under for categories. - Use this for mod support. Defaults to vanilla 'Parts'. - Returns: - list: List of folders underneath category path. - """ - # Validate Pack name. - pack = pack or "Parts" - # Get the associated model path. - search_path = self.get_model_path_from_pack(pack) - return os.listdir(search_path) - - def get_preset_categories(self): - """Get the list of preset categories. - - Returns: - list: List of folders underneath preset path. - """ - return [ - item - for item in os.listdir(preset.Preset.PRESET_PATH) - if os.path.isdir(os.path.join(preset.Preset.PRESET_PATH, item)) - ] - - def get_uncategorized_presets(self): - presets = [] - for preset_file in os.listdir(preset.Preset.PRESET_PATH): - if preset_file.endswith(".json") or preset_file.endswith(".nmsprefab"): - presets.append(os.path.splitext(preset_file)[0]) - return presets - - def get_presets_from_category(self, category): - """Get a list of presets underneath a category. - - Args: - category (str): The name of the category. - """ - presets = [] - for preset_file in os.listdir( - os.path.join(preset.Preset.PRESET_PATH, category) - ): - if preset_file.endswith(".json") or preset_file.endswith(".nmsprefab"): - presets.append(os.path.splitext(preset_file)[0]) - return presets - - def get_objs_from_category(self, category, pack=None): - """Get a list of parts belonging to a category. - - Args: - category (str): The name of the category. - pack (str): The model pack search under for categories. - Use this for mod support. Defaults to vanilla 'Parts'. - """ - # Validate Pack name. - pack = pack or "Parts" - # Get the associated model path. - search_path = self.get_model_path_from_pack(pack) - category_path = os.path.join(search_path, category) - all_objs = [part for part in os.listdir(category_path) if part.endswith(".fbx")] - file_names = sorted(all_objs) - return file_names - - def get_obj_path(self, part): - """Get the path to the OBJ file from a part.""" - part_dictionary = self.part_reference.get(part, {}) - return part_dictionary.get("full_path", None) - - def get_obj_parent_folder(self, part): - """Get the path to the OBJ file from a part.""" - path = self.get_obj_path(part) - folder = os.path.dirname(path).split(os.sep)[-1] - return folder - - def get_model_path_from_pack(self, pack_request): - """Given a pack name, return it's associated path. - - Args: - pack_request (str): The name of the pack - - Return: - str: The model path of the pack. - """ - for pack_name, pack_path in self.available_packs: - if pack_name == pack_request: - return pack_path - - def get_parts_from_category(self, category, pack=None): - """Get all the parts from a specific category. - - Args: - category (str): The category to search. - pack (str): The model pack name. Defaults to vanilla 'Parts'. - """ - # Validate pack name. - pack = pack or "Parts" - parts = [] - for item, value in self.part_reference.items(): - # Get pack and category values. - part_category = value["category"] - part_pack = value["pack"] - # Check both are valid. - pack_check = part_pack == pack - category_check = part_category == category - # Add to parts. - if pack_check and category_check: - parts.append(item) - return sorted(parts) - - def get_nice_name(self, part): - """Get a nice version of the part id.""" - part = os.path.basename(part) - nice_name = part.title().replace("_", " ") - return self.nice_name_dictionary.get(part, nice_name) - - def save_preset_to_file(self, preset_name): - # Get a file path. - all_presets = preset.Preset.get_presets() - if preset_name in preset.Preset.get_presets(): - file_path = all_presets[preset_name] - else: - file_path = os.path.join(self.PRESET_PATH, preset_name) - # Add .json if it's not specified. - if not file_path.endswith(".nmsprefab"): - file_path += ".nmsprefab" - # Save to file path - with open(file_path, "w") as stream: - json.dump(self.serialise(add_timestamp=False, as_prefab=True), stream, indent=4) - - def build_rigs(self): - """Get all items that require a rig and build them.""" - blend_utils.scene_refresh() - parts = self.get_all_parts(exclude_presets=True) - for part in parts: - builder_object = self.get_builder_object_from_bpy_object(part) - if hasattr(builder_object, "build_rig"): - builder_object.build_rig() - - def optimise_control_points(self): - """Find all control points that share the same location and combine them.""" - blend_utils.scene_refresh() - - # First build a dictionary of controls that match. - power_control_objects = [obj for obj in bpy.data.objects if "rig_item" in obj] - power_control_reference = defaultdict(list) - for power_control in power_control_objects: - # Create a key that will group the controls based on their location. - # I am rounding the decimal point to 4 as the accuracy means items - # in the same location have slightly different values. - key = ",".join( - [ - str(round(loc, 3)) - for loc in power_control.matrix_world.decompose()[0] - ] - ) - # Append the control to the key. - power_control_reference[key].append(power_control) - - # Swap any duplicate controls with the first instance. - for key, value in power_control_reference.items(): - unique_control = value[0] - other_controls = value[1:] - - if not other_controls: - continue - - for control in other_controls: - power_line = blend_utils.get_item_by_name(control["power_line"]) - power_line_obj = self.get_builder_object_from_bpy_object(power_line) - prev_start_control = bpy.data.objects[power_line_obj.start_control] - prev_end_control = bpy.data.objects[power_line_obj.end_control] - - # Assign new controls. - if control == prev_start_control: - power_line_obj.build_rig(unique_control, prev_end_control) - else: - power_line_obj.build_rig(prev_start_control, unique_control) - - # Hide away control. - blend_utils.remove_object(control.name) diff --git a/src/addons/no_mans_sky_base_builder/builder/__init__.py b/src/addons/no_mans_sky_base_builder/builder/__init__.py new file mode 100644 index 00000000..99930c0a --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/__init__.py @@ -0,0 +1,4 @@ +from .builder import Builder + +# one builder shared by the whole addon, so every tool sees the same part cache +BUILDER = Builder() diff --git a/src/addons/no_mans_sky_base_builder/builder/builder.py b/src/addons/no_mans_sky_base_builder/builder/builder.py new file mode 100644 index 00000000..14723fec --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/builder.py @@ -0,0 +1,283 @@ +"""The builder contains all top level scene methods for managing NMS parts.""" + +import json +import os +import time +from collections import defaultdict + +import bpy +from mathutils import Matrix + +from .. import part, preset +from ..utils import blend_utils +from . import importer, overrides, paths +from .catalog import Catalog + + +class Builder(Catalog): + + # Tool Level Paths --- + USER_PATH = paths.USER_PATH + FILE_PATH = paths.ADDON_PATH + MODEL_PATH = paths.MODEL_PATH + FOSSIL_PARTS_PATH = paths.FOSSIL_PARTS_PATH + NICE_JSON = paths.NICE_JSON + MODS_PATH = paths.MODS_PATH + PRESET_PATH = paths.PRESET_PATH + + override_classes = overrides.OVERRIDE_CLASSES + + def __init__(self): + """Builder __init__.""" + super(Builder, self).__init__() + # parts and presets share one cache, keyed by their id + self.__part_cache = {} + + # Cache --- + def clear_caches(self): + """Clear all the caches we use in this class.""" + self.__part_cache.clear() + + def add_to_part_cache(self, object_id, bpy_object): + """Add item to part cache.""" + self.__part_cache[object_id] = bpy_object.name + + def add_to_preset_cache(self, preset_id, bpy_object): + """Add item to preset cache.""" + self.__part_cache[preset_id] = bpy_object.name + + def _get_cached_bpy_object(self, item_id): + # cached names can go stale when the user deletes or renames objects + item_name = self.__part_cache.get(item_id, None) + if not item_name: + return None + return bpy.data.objects.get(item_name) + + def find_object_by_id(self, object_id): + """Get the item from the part cache.""" + bpy_object = self._get_cached_bpy_object(object_id) + if bpy_object is None: + return None + return self.get_builder_object_from_bpy_object(bpy_object) + + def find_preset_by_id(self, preset_id): + """Get the item from the part cache.""" + bpy_object = self._get_cached_bpy_object(preset_id) + if bpy_object is None: + return None + return preset.Preset.deserialise_from_object( + bpy_object=bpy_object, builder_object=self + ) + + # Lookups --- + @classmethod + def get_part_class(cls, object_id): + return overrides.get_part_class(object_id) + + def get_builder_object_from_bpy_object(self, bpy_object): + # Handle Presets. + if "PresetID" in bpy_object: + return preset.Preset.deserialise_from_object( + bpy_object=bpy_object, builder_object=self + ) + + # Handle Parts. + object_id = bpy_object.get("ObjectID") or bpy_object.get("SnapID") + if not object_id: + return None + + use_class = self.get_part_class(object_id) + return use_class.deserialise_from_object( + bpy_object=bpy_object, builder_object=self + ) + + def get_all_parts( + self, exclude_presets=False, skip_object_type=None, include_lines=False + ): + """Get all NMS parts in the scene. + + Args: + exclude_presets (bool): Choose to exclude parts generated via + preset. + skip_object_type (list): ObjectIDs to leave out. + include_lines (bool): Include line control points. + """ + skip_object_type = skip_object_type or [] + + flat_parts = [] + for item in bpy.context.scene.objects: + if "ObjectID" in item: + if item["ObjectID"] in skip_object_type: + continue + if exclude_presets and item.get("belongs_to_preset", False): + continue + flat_parts.append(item) + elif include_lines and "SnapID" in item: + flat_parts.append(item) + + return sorted(flat_parts, key=Builder.by_order) + + def get_all_presets(self): + """Get all Builder preset items in the scene.""" + return [item for item in bpy.context.scene.objects if "PresetID" in item] + + @staticmethod + def by_order(bpy_object): + """Sorting method to get objects by the order attribute. + + Args: + bpy_object (bpy.ob): A blender object. + + Returns: + int: The order of which the item is/was built. + """ + return bpy_object.get("order", 0) + + # Building --- + def add_part(self, object_id, user_data=None, build_rigs=True): + """Add an item based on it's object ID.""" + object_id = object_id.replace("^", "") + use_class = self.get_part_class(object_id) + + # captured before the part exists so it lands on the previous selection + active_object = bpy.context.active_object + item = use_class( + object_id=object_id, + builder_object=self, + user_data=user_data, + build_rigs=build_rigs, + ) + + if active_object is not None: + item.object.matrix_world = active_object.matrix_world.copy() + return item + + def add_preset(self, preset_id): + """Add an item based on it's preset ID.""" + return preset.Preset(preset_id=preset_id, builder_object=self) + + def mirror_part(self, part_object): + new_object_id = part.Part.get_mirror_part_id(part_object["ObjectID"]) + return self._swap_to_twin(part_object, new_object_id, (1, 0, 0)) + + def flip_part(self, part_object): + new_object_id = part.Part.get_flip_part_id(part_object["ObjectID"]) + return self._swap_to_twin(part_object, new_object_id, (0, 1, 0)) + + def _swap_to_twin(self, part_object, new_object_id, flip_axis): + # meshes are shared between parts, so never transform a shared one in place + if part_object.data.users > 1: + part_object.data = part_object.data.copy() + part_object.data.transform(Matrix.Scale(-1, 4, flip_axis)) + + # Update ObjectID, name and cache + self.__part_cache.pop(part_object["ObjectID"], None) + part_object["ObjectID"] = new_object_id + part_object.name = new_object_id + self.__part_cache[new_object_id] = part_object.name + return part_object + + def build_rigs(self): + """Get all items that require a rig and build them.""" + blend_utils.scene_refresh() + for item in self.get_all_parts(exclude_presets=True): + builder_object = self.get_builder_object_from_bpy_object(item) + if hasattr(builder_object, "build_rig"): + builder_object.build_rig() + + def optimise_control_points(self): + """Find all control points that share the same location and combine them.""" + blend_utils.scene_refresh() + + # Group controls by location, rounded as controls in the same place + # have slightly different values. + power_control_reference = defaultdict(list) + for power_control in bpy.data.objects: + if "rig_item" not in power_control: + continue + key = ",".join( + str(round(loc, 3)) for loc in power_control.matrix_world.decompose()[0] + ) + power_control_reference[key].append(power_control) + + # Swap any duplicate controls with the first instance. + for controls in power_control_reference.values(): + unique_control = controls[0] + for control in controls[1:]: + power_line = blend_utils.get_item_by_name(control["power_line"]) + power_line_obj = self.get_builder_object_from_bpy_object(power_line) + prev_start_control = bpy.data.objects[power_line_obj.start_control] + prev_end_control = bpy.data.objects[power_line_obj.end_control] + + if control == prev_start_control: + power_line_obj.build_rig(unique_control, prev_end_control) + else: + power_line_obj.build_rig(prev_start_control, unique_control) + + blend_utils.remove_object(control.name) + + # Serialising --- + def serialise(self, get_presets=False, add_timestamp=False, as_prefab=False): + """Return NMS compatible dictionary. + + Args: + get_presets (bool): This will generate data for presets. And + exclude parts generated from presets. + Returns: + dict: Dictionary of base information. + """ + object_list = [] + for item in self.get_all_parts(exclude_presets=get_presets): + use_class = self.get_part_class(item["ObjectID"]) + item_obj = use_class.deserialise_from_object(item, builder_object=self) + object_list.append(item_obj.serialise()) + + key = "Prefab" if as_prefab else "Objects" + data = {key: object_list} + + if add_timestamp: + data["timestamp"] = int(time.time()) + + data["BaseVersion"] = 8 + + if get_presets: + data["Presets"] = [ + preset.Preset.deserialise_from_object( + _preset, builder_object=self + ).serialise() + for _preset in self.get_all_presets() + ] + + return data + + def deserialise_from_data(self, data): + """Given NMS data, reconstruct the base.""" + if data is None: + return + + compensate_normal = data.get("BaseVersion", 8) >= 5 + + importer.import_objects( + self, data.get("Objects", []), compensate_normal=compensate_normal + ) + + for preset_data in data.get("Presets", []): + preset.Preset.deserialise_from_data( + preset_data, self, compensate_normal=compensate_normal + ) + + self.build_rigs() + self.optimise_control_points() + + def save_preset_to_file(self, preset_name): + all_presets = preset.Preset.get_presets() + if preset_name in all_presets: + file_path = all_presets[preset_name] + else: + file_path = os.path.join(self.PRESET_PATH, preset_name) + + if not file_path.endswith(".nmsprefab"): + file_path += ".nmsprefab" + + with open(file_path, "w") as stream: + json.dump(self.serialise(add_timestamp=False, as_prefab=True), stream, indent=4) diff --git a/src/addons/no_mans_sky_base_builder/builder/catalog.py b/src/addons/no_mans_sky_base_builder/builder/catalog.py new file mode 100644 index 00000000..3393507a --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/catalog.py @@ -0,0 +1,139 @@ +import os + +from .. import preset +from ..utils import python as python_utils +from . import paths + + +class Catalog(object): + """Lookup of the part models and presets available on disk.""" + + nice_name_dictionary = python_utils.load_dictionary(paths.NICE_JSON) + + def __init__(self): + # Create default part pack. + self.available_packs = [("Parts", paths.MODEL_PATH)] + + # Find any mods with model packs inside. + if os.path.exists(paths.MODS_PATH): + for mod_folder in os.listdir(paths.MODS_PATH): + full_mod_path = os.path.join(paths.MODS_PATH, mod_folder) + if "models" in os.listdir(full_mod_path): + full_model_path = os.path.join(full_mod_path, "models") + self.available_packs.append((mod_folder, full_model_path)) + + # Find Parts and build a reference dictionary. + self.part_reference = {} + for pack_name, pack_folder in self.available_packs: + search_path = pack_folder or paths.MODEL_PATH + for category in self.get_categories(pack=pack_name): + for part_file in self.get_objs_from_category(category, pack=pack_name): + unique_id = os.path.splitext(part_file)[0] + self.part_reference[unique_id] = { + "category": category, + "full_path": os.path.join(search_path, category, part_file), + "pack": pack_name, + } + + # Parts --- + def get_categories(self, pack=None): + """Get the list of categories. + + Args: + pack (str): The model pack search under for categories. + Use this for mod support. Defaults to vanilla 'Parts'. + Returns: + list: List of folders underneath category path. + """ + search_path = self.get_model_path_from_pack(pack or "Parts") + return os.listdir(search_path) + + def get_objs_from_category(self, category, pack=None): + """Get a list of parts belonging to a category. + + Args: + category (str): The name of the category. + pack (str): The model pack search under for categories. + Use this for mod support. Defaults to vanilla 'Parts'. + """ + search_path = self.get_model_path_from_pack(pack or "Parts") + category_path = os.path.join(search_path, category) + return sorted( + part_file + for part_file in os.listdir(category_path) + if part_file.endswith(".fbx") + ) + + def get_parts_from_category(self, category, pack=None): + """Get all the parts from a specific category. + + Args: + category (str): The category to search. + pack (str): The model pack name. Defaults to vanilla 'Parts'. + """ + pack = pack or "Parts" + return sorted( + item + for item, value in self.part_reference.items() + if value["pack"] == pack and value["category"] == category + ) + + def get_obj_path(self, part): + """Get the path to the OBJ file from a part.""" + return self.part_reference.get(part, {}).get("full_path", None) + + def get_obj_parent_folder(self, part): + """Get the path to the OBJ file from a part.""" + path = self.get_obj_path(part) + return os.path.dirname(path).split(os.sep)[-1] + + def get_model_path_from_pack(self, pack_request): + """Given a pack name, return it's associated path. + + Args: + pack_request (str): The name of the pack + + Return: + str: The model path of the pack. + """ + for pack_name, pack_path in self.available_packs: + if pack_name == pack_request: + return pack_path + + def get_nice_name(self, part): + """Get a nice version of the part id.""" + part = os.path.basename(part) + nice_name = part.title().replace("_", " ") + return self.nice_name_dictionary.get(part, nice_name) + + # Presets --- + def get_preset_categories(self): + """Get the list of preset categories. + + Returns: + list: List of folders underneath preset path. + """ + return [ + item + for item in os.listdir(preset.Preset.PRESET_PATH) + if os.path.isdir(os.path.join(preset.Preset.PRESET_PATH, item)) + ] + + def get_uncategorized_presets(self): + return self._list_presets(preset.Preset.PRESET_PATH) + + def get_presets_from_category(self, category): + """Get a list of presets underneath a category. + + Args: + category (str): The name of the category. + """ + return self._list_presets(os.path.join(preset.Preset.PRESET_PATH, category)) + + @staticmethod + def _list_presets(folder): + return [ + os.path.splitext(preset_file)[0] + for preset_file in os.listdir(folder) + if preset_file.endswith((".json", ".nmsprefab")) + ] diff --git a/src/addons/no_mans_sky_base_builder/builder/importer.py b/src/addons/no_mans_sky_base_builder/builder/importer.py new file mode 100644 index 00000000..58eba983 --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/importer.py @@ -0,0 +1,162 @@ +"""Fast bulk import of parts from NMS data. + +The import collection is excluded from the view layer while parts are created, +and parts with the same ObjectID and UserData share one mesh. +""" + +import time + +import bpy + +from ..part import Part +from ..utils import collection_utils, material +from . import overrides + +# name of collection to import objects to +IMPORT_COLLECTION_NAME = "Collection" + + +def import_objects(builder_object, objects_data, compensate_normal=True): + unique_objects = {} + unique_meshes = {} + + import_collection = collection_utils.get_collection(IMPORT_COLLECTION_NAME) + set_collection_excluded(import_collection.name, True) + + # a bad part must never leave the collection excluded + try: + for order, part_data in enumerate(objects_data): + raw_object_id = part_data.get(Part.PROP_OBJECT_ID, None) + if raw_object_id is None: + continue + + object_id = raw_object_id.replace("^", "") + user_data = part_data.get(Part.PROP_USER_DATA, 0) + + override_class = overrides.get_override_class(object_id) + if override_class is not None: + override_class.deserialise_from_data( + part_data, builder_object, compensate_normal=compensate_normal + ) + continue + + bpy_object = build_fbx_part( + builder_object, + object_id, + user_data, + import_collection, + unique_objects, + unique_meshes, + ) + if bpy_object is None: + continue + + restore_params(bpy_object, part_data, object_id) + bpy_object.matrix_world = deserialise_matrix_world(part_data) + bpy_object[Part.PROP_ORDER] = order + finally: + set_collection_excluded(import_collection.name, False) + bpy.context.view_layer.update() + + +def set_collection_excluded(collection_name, excluded): + layer_collection = bpy.context.view_layer.layer_collection.children.get( + collection_name + ) + if layer_collection: + layer_collection.exclude = excluded + + +# colour is a flat material on the mesh, so objects can only share a mesh +# when their ObjectID AND UserData match +def build_fbx_part( + builder_object, object_id, user_data, import_collection, unique_objects, unique_meshes +): + mesh_key = (object_id, user_data) + + # import object from disk when visiting that object_id for first time + if object_id not in unique_objects: + bpy_object = import_fbx_from_disk(builder_object, object_id) + if bpy_object is None: + return None + + collection_utils.move_object_into_collection(import_collection, bpy_object) + material.restore_material(bpy_object, user_data) + builder_object.add_to_part_cache(object_id, bpy_object) + + unique_objects[object_id] = bpy_object + unique_meshes[mesh_key] = bpy_object.data + return bpy_object + + bpy_object = unique_objects[object_id].copy() + import_collection.objects.link(bpy_object) + + if mesh_key in unique_meshes: + bpy_object.data = unique_meshes[mesh_key] + else: + bpy_object.data = bpy_object.data.copy() + material.restore_material(bpy_object, user_data) + unique_meshes[mesh_key] = bpy_object.data + + return bpy_object + + +def import_fbx_from_disk(builder_object, object_id): + fbx_path = builder_object.get_obj_path(object_id) + + objects_before = set(bpy.data.objects) + try: + if fbx_path is None: + bpy.ops.mesh.primitive_cube_add() + else: + bpy.ops.import_scene.fbx(filepath=fbx_path) + except RuntimeError: + pass + new_objects = [item for item in bpy.data.objects if item not in objects_before] + + if not new_objects: + # cube ops fail when the active collection is excluded + if fbx_path is not None: + return None + bpy_object = bpy.data.objects.new(object_id, bpy.data.meshes.new(object_id)) + else: + bpy_object = next( + (item for item in new_objects if item.type == "MESH"), new_objects[0] + ) + + bpy_object.name = object_id + if bpy_object.data is not None: + bpy_object.data.materials.clear() + + try: + bpy_object.select_set(False) + except RuntimeError: + pass + + return bpy_object + + +# copy params from part json to bpy_object +def restore_params(bpy_object, part_data, object_id): + user_data = part_data.get(Part.PROP_USER_DATA, "") + time_stamp = str(part_data.get(Part.PROP_TIMESTAMP, int(time.time()))) + message = part_data.get(Part.PROP_MESSAGE, None) + + bpy_object[Part.PROP_OBJECT_ID] = object_id + bpy_object[Part.PROP_SNAP_ID] = object_id + bpy_object[Part.PROP_USER_DATA] = str(user_data) + bpy_object[Part.PROP_TIMESTAMP] = time_stamp + bpy_object[Part.PROP_BELONGS_TO_PRESET] = False + + if message: + bpy_object[Part.PROP_MESSAGE] = message + + return bpy_object + + +# convert position, up and at to matrix world for blender object +def deserialise_matrix_world(part_data): + pos = part_data.get(Part.PROP_POSITION, [0.0, 0.0, 0.0]) + up = part_data.get(Part.PROP_UP, [0.0, 0.0, 0.0]) + at = part_data.get(Part.PROP_AT, [0.0, 0.0, 0.0]) + return Part.create_matrix_from_vectors(pos, up, at) diff --git a/src/addons/no_mans_sky_base_builder/builder/overrides.py b/src/addons/no_mans_sky_base_builder/builder/overrides.py new file mode 100644 index 00000000..1d11a537 --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/overrides.py @@ -0,0 +1,76 @@ +import os + +from ..part import Part +from ..part_overrides import (bone, bone_replacer, locked, message, + power_control, turret, u_bytebeatline, u_pipeline, + u_portalline, u_powerline) +from . import paths + +# parts that need their own class to be built, checked in order +OVERRIDE_CLASSES = { + bone_replacer.BONE_REPLACER: [ + "FOS_HEAD", + "FOS_SKULL", + "FOS_LIMBS", + "FOS_TAIL", + "FOS_BODY", + ], + bone.BONE: [ + os.path.splitext(filename)[0] + for filename in os.listdir(paths.FOSSIL_PARTS_PATH) + ], + turret.TURRET: ["B_TUR_A", "B_TUR_B", "B_TUR_C", "B_TUR_D", "B_TUR_E"], + u_powerline.U_POWERLINE: ["U_POWERLINE"], + u_pipeline.U_PIPELINE: ["U_PIPELINE"], + u_portalline.U_PORTALLINE: ["U_PORTALLINE"], + u_bytebeatline.U_BYTEBEATLINE: ["U_BYTEBEATLINE"], + power_control.POWER_CONTROL: ["POWER_CONTROL"], + locked.LOCKED: [ + "BASE_FLAG", + "BRIDGECONNECTOR", + "AIRLCKCONNECTOR", + "FREIGHTER_CORE", + ], + message.MESSAGE: [ + "MESSAGEMODULE", + "BYTEBEAT", + "BYTEBEATSWITCH", + "HOLO_DISCO_0", + "FOS_BI", + "FOS_BIRD", + "FOS_BIRD_DIS", + "FOS_BI_DIS", + "FOS_BODY", + "FOS_BODY_DISP", + "FOS_BODY_MNT", + "FOS_GRUN", + "FOS_GRUN_DIS", + "FOS_LIMBS", + "FOS_LIMBS_DISP", + "FOS_LIMBS_MNT", + "FOS_QUAD", + "FOS_QUAD_DIS", + "FOS_SKULL", + "FOS_SKULL_DISP", + "FOS_SKULL_MNT", + "FOS_TAIL", + "FOS_TAIL_DISP", + "FOS_TAIL_MNT", + "FOS_WORM", + "FOS_WORM_DIS", + ], +} + +# flattened {object_id: class}, the first class listing an id wins +_CLASS_BY_ID = {} +for _class_ref, _part_list in OVERRIDE_CLASSES.items(): + for _object_id in _part_list: + _CLASS_BY_ID.setdefault(_object_id, _class_ref) + + +def get_override_class(object_id): + return _CLASS_BY_ID.get(object_id) + + +def get_part_class(object_id): + return _CLASS_BY_ID.get(object_id, Part) diff --git a/src/addons/no_mans_sky_base_builder/builder/paths.py b/src/addons/no_mans_sky_base_builder/builder/paths.py new file mode 100644 index 00000000..0eea3026 --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/builder/paths.py @@ -0,0 +1,11 @@ +import os + +ADDON_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +USER_PATH = os.path.join(os.path.expanduser("~"), "NoMansSkyBaseBuilder") + +MODEL_PATH = os.path.join(ADDON_PATH, "models") +FOSSIL_PARTS_PATH = os.path.join(MODEL_PATH, "fossil_parts") +NICE_JSON = os.path.join(ADDON_PATH, "resources", "nice_names.json") + +MODS_PATH = os.path.join(USER_PATH, "mods") +PRESET_PATH = os.path.join(USER_PATH, "presets") diff --git a/src/addons/no_mans_sky_base_builder/part.py b/src/addons/no_mans_sky_base_builder/part.py index 0ff919a5..01190649 100644 --- a/src/addons/no_mans_sky_base_builder/part.py +++ b/src/addons/no_mans_sky_base_builder/part.py @@ -9,6 +9,8 @@ from .utils import blend_utils, material from .utils import python as python_utils +z_compensate = mathutils.Matrix.Rotation(math.radians(-90.0), 4, "X") + class Part(object): @@ -21,6 +23,20 @@ class Part(object): SNAP_MATRIX_DICTIONARY = python_utils.load_dictionary(SNAP_MATRIX_JSON) SNAP_PAIR_DICTIONARY = python_utils.load_dictionary(SNAP_PAIR_JSON) + # part property names + PROP_OBJECT_ID = "ObjectID" + PROP_SNAP_ID = "SnapID" + PROP_USER_DATA = "UserData" + PROP_TIMESTAMP = "Timestamp" + PROP_MESSAGE = "Message" + + PROP_POSITION = "Position" + PROP_UP = "Up" + PROP_AT = "At" + + PROP_BELONGS_TO_PRESET = "belongs_to_preset" + PROP_ORDER = "order" + SNAP_CACHE = {} def __init__( @@ -288,15 +304,25 @@ def retrieve_object_from_id(self, object_id): ] item = bpy.data.objects[new_objects[0]] item.name = object_id - # for convenience if saving obj/mtl files, delete any imported materials item.data.materials.clear() - item.select_set(False) + + # fails when the object's collection is excluded from the view layer + try: + item.select_set(False) + except RuntimeError: + pass + blend_utils.add_to_scene(item) return item - # Create cube. - bpy.ops.mesh.primitive_cube_add() - item = bpy.data.objects[bpy.context.object.name] + # Create cube, at data level if the active collection is excluded. + try: + bpy.ops.mesh.primitive_cube_add() + item = bpy.context.active_object + except RuntimeError: + mesh = bpy.data.meshes.new(object_id) + item = bpy.data.objects.new(object_id, mesh) + item.name = object_id blend_utils.add_to_scene(item) return item @@ -311,30 +337,18 @@ def serialise(self): # Get Matrix Data world_matrix = self.matrix_world # Bring the matrix from Blender Z-Up space into standard Y-up space. - z_compensate = mathutils.Matrix.Rotation(math.radians(-90.0), 4, "X") world_matrix_offset = z_compensate @ world_matrix # Retrieve Position, Up and At vectors. - pos = world_matrix_offset.decompose()[0] - up = [ - world_matrix_offset[0][1], - world_matrix_offset[1][1], - world_matrix_offset[2][1], - ] - at = mathutils.Vector( - ( - world_matrix_offset[0][2], - world_matrix_offset[1][2], - world_matrix_offset[2][2], - ) - ) - at = at.normalized() + pos = world_matrix_offset.translation + up = world_matrix_offset.col[1].to_3d() + at = world_matrix_offset.col[2].to_3d().normalized() return { - "ObjectID": self.object_id_format, - "Position": [pos[0], pos[1], pos[2]], - "Up": [up[0], up[1], up[2]], - "At": [at[0], at[1], at[2]], - "Timestamp": int(self.time_stamp), - "UserData": int(self.user_data), + Part.PROP_OBJECT_ID: self.object_id_format, + Part.PROP_POSITION: list(pos), + Part.PROP_UP: list(up), + Part.PROP_AT: list(at), + Part.PROP_TIMESTAMP: int(self.time_stamp), + Part.PROP_USER_DATA: int(self.user_data), } # Class Methods --- @@ -375,49 +389,60 @@ def deserialise_from_data( part.message = data.get("Message", "") return part + SUFFIX_COMPASS_ID_PAIRS = [ + ("_NE", "_NW"), + ("_NE1", "_NW1"), + ("_NE2", "_NW2"), + ("_NE3", "_NW3"), + ("_NETB", "_NWTB"), + ("_NETB1", "_NWTB1"), + ("_NETB2", "_NWTB2"), + ("_NETB3", "_NWTB3"), + ("_E", "_W"), + ("_N", "_S"), + ("_0", "_1"), + ] + + FLIP_COMPASS_IDS = [ + "N", + "E", + "W", + "S", + "NE", + "NE1", + "NE2", + "NE3", + "NW", + "NW1", + "NW2", + "NW3", + "NETB", + "NETB1", + "NETB2", + "NETB3", + "NWTB", + "NWTB1", + "NWTB2", + "NWTB3", + ] + # Static Methods --- @staticmethod def get_mirror_part_id(object_id): # Handle Compass - east_compass_ids = [ - "NE", - "NE1", - "NE2", - "NE3", - "NETB", - "NETB1", - "NETB2", - "NETB3", - ] - west_compass_ids = [ - "NW", - "NW1", - "NW2", - "NW3", - "NWTB", - "NWTB1", - "NWTB2", - "NWTB3", - ] - for idx, east_compass_id in enumerate(east_compass_ids): - if object_id.endswith(f"_{east_compass_id}"): - return ( - object_id[: -(len(east_compass_id) + 1)] - + "_" - + west_compass_ids[idx] - ) - for idx, west_compass_id in enumerate(west_compass_ids): - if object_id.endswith(f"_{west_compass_id}"): - return ( - object_id[: -(len(west_compass_id) + 1)] - + "_" - + east_compass_ids[idx] - ) + for east_str, west_str in Part.SUFFIX_COMPASS_ID_PAIRS: + if object_id.endswith(east_str): + return object_id[: -len(east_str)] + west_str + if object_id.endswith(west_str): + return object_id[: -len(west_str)] + east_str # Handle Winged - if object_id.endswith("_R"): - return object_id[:-2] - else: + if object_id.startswith("B_WNG"): + # default Aeron wing is B_WNG_R, so its mirror is B_WNG_R_R + if object_id in ("B_WNG_R", "B_WNG_R_R"): + return "B_WNG_R_R" if object_id == "B_WNG_R" else "B_WNG_R" + if object_id.endswith("_R"): + return object_id[:-2] return object_id + "_R" @staticmethod @@ -426,29 +451,7 @@ def get_flip_part_id(object_id): if "_Y_" in object_id: return object_id.replace("_Y_", "_") # Then check for unflipped. - compass_ids = [ - "N", - "E", - "W", - "S", - "NE", - "NE1", - "NE2", - "NE3", - "NW", - "NW1", - "NW2", - "NW3", - "NETB", - "NETB1", - "NETB2", - "NETB3", - "NWTB", - "NWTB1", - "NWTB2", - "NWTB3", - ] - for compass_id in compass_ids: + for compass_id in Part.FLIP_COMPASS_IDS: if object_id.endswith(f"_{compass_id}"): return object_id[: -(len(compass_id) + 1)] + "_Y_" + compass_id return None diff --git a/src/addons/no_mans_sky_base_builder/resources/mirror_corrections.json b/src/addons/no_mans_sky_base_builder/resources/mirror_corrections.json index 2288d87f..819593b0 100644 --- a/src/addons/no_mans_sky_base_builder/resources/mirror_corrections.json +++ b/src/addons/no_mans_sky_base_builder/resources/mirror_corrections.json @@ -14,8 +14,7 @@ "S_ROOF_IC" ], "mirror_z_180_identifiers": [ - "TRIFLOOR", - "B_WNG_R" + "TRIFLOOR" ], "position_offsets": { "STORAGEPANEL": [0.05444555, 0.0, 0.0] diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py index e56d0353..50b4f34c 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py @@ -1,10 +1,10 @@ import bpy -from .. import builder, builder_v2 +from .. import builder from . import save_editor_dependencies from . import save_editor_utils from .save_editor_utils import BaseType, BaseData -BUILDER = builder.Builder() +BUILDER = builder.BUILDER #save data to persist within blend files. class SaveManager(bpy.types.PropertyGroup): @@ -345,8 +345,7 @@ def import_base(self, context, base_identifiers, save_links): # Import json into scene nms_tools = context.scene.nms_base_tool nms_tools.deserialise_from_data(nms_base_json) - #BUILDER.deserialise_from_data(nms_base_json) - builder_v2.deserialise_from_data(nms_base_json) + BUILDER.deserialise_from_data(nms_base_json) #return a string for operators for status message return "Base/Corvette imported sucessfully" diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py index 11cad791..6000c798 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py @@ -14,7 +14,7 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.Builder() +BUILDER = builder.BUILDER class BatchTool(bpy.types.PropertyGroup): diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool.py b/src/addons/no_mans_sky_base_builder/tools/build_tool.py index e02d76cb..ffcd51e9 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool.py @@ -14,7 +14,7 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.Builder() +BUILDER = builder.BUILDER from mathutils import Vector diff --git a/src/addons/no_mans_sky_base_builder/tools/properties.py b/src/addons/no_mans_sky_base_builder/tools/properties.py index 6c0cb953..4568663c 100644 --- a/src/addons/no_mans_sky_base_builder/tools/properties.py +++ b/src/addons/no_mans_sky_base_builder/tools/properties.py @@ -14,7 +14,7 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.Builder() +BUILDER = builder.BUILDER class Properties(bpy.types.PropertyGroup): diff --git a/src/addons/no_mans_sky_base_builder/utils/curve.py b/src/addons/no_mans_sky_base_builder/utils/curve.py index d40c2d55..50f6e2bf 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve.py @@ -10,7 +10,7 @@ mirror_utils) from . import python as python_utils -BUILDER = builder.Builder() +BUILDER = builder.BUILDER FILE_PATH = os.path.dirname(os.path.realpath(__file__)) NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") From 7359fd05c49662335eb8411e17a5ff8ae380fb57 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:07:23 +0530 Subject: [PATCH 08/15] Added get_builder/set_builder and register_override/unregister_override so other addons can provide the builder or add part classes. --- .../no_mans_sky_base_builder/__init__.py | 98 +++++++++---------- .../asset_browser/main.py | 4 +- .../builder/__init__.py | 40 +++++++- .../builder/importer.py | 19 ++-- .../builder/overrides.py | 18 +++- .../save_editor/save_manager.py | 5 +- .../tools/batch_tool.py | 6 +- .../tools/build_tool.py | 18 ++-- .../tools/properties.py | 3 +- .../no_mans_sky_base_builder/utils/curve.py | 6 +- 10 files changed, 134 insertions(+), 83 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 911bbc55..555b18f8 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -16,7 +16,8 @@ from bpy.types import Panel, PropertyGroup from numpy import isin -from . import builder, icons, part, preset +from . import icons, part, preset +from .builder import get_builder from .part_overrides import line from .save_editor import save_editor_operators, save_editor_utils from .save_editor.save_editor_presentation import NMS_PT_save_editor_panel @@ -38,7 +39,6 @@ PRESET_PATH = os.path.join(USER_PATH, "presets") ASSET_BROWSER_PATH = os.path.join(FILE_PATH, "asset_browser") -BUILDER = builder.BUILDER GHOSTED_JSON = os.path.join(FILE_PATH, "resources", "ghosted.json") ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] @@ -87,7 +87,7 @@ def get_line_type_from_enum(context): class NMSSettings(PropertyGroup): # Build Array of base part types. (Vanilla Parts - Mods - Presets) enum_items = [] - for pack, _ in BUILDER.available_packs: + for pack, _ in get_builder().available_packs: enum_items.append((pack, pack, "View {0}...".format(pack))) enum_items.append(("PRESETS", "Prefabs", "View Prefabs...")) @@ -426,7 +426,7 @@ def serialise(self, get_presets=False, objects_only=False): "AutoPowerSetting": {"BaseAutoPowerSetting": self.auto_power_setting}, } # Capture Individual Objects - objects_data = BUILDER.serialise(get_presets=get_presets) + objects_data = get_builder().serialise(get_presets=get_presets) if objects_only: return objects_data["Objects"] @@ -454,7 +454,7 @@ def import_nms_data(self): # Start a new file self.deserialise_from_data(nms_import_data) - BUILDER.deserialise_from_data(nms_import_data) + get_builder().deserialise_from_data(nms_import_data) def export_nms_data(self, objects_only=False): """Generate data and place it into the user's clipboard. @@ -496,7 +496,7 @@ def load_nms_data(self, file_path): return # Build from Data self.deserialise_from_data(save_data) - BUILDER.deserialise_from_data(save_data) + get_builder().deserialise_from_data(save_data) def new_file(self): """Reset's the entire Blender scene to default. @@ -507,7 +507,7 @@ def new_file(self): * Removes all items with ObjectID, PresetID and NMS_LIGHT properties. * Resets the room visibility switch to default. """ - BUILDER.clear_caches() + get_builder().clear_caches() # Remove basic blender default items. blend_utils.remove_object("Cube") @@ -658,7 +658,7 @@ def apply_default_colour(self): # Figure out default index. object_id = obj["ObjectID"] if object_id: - parent_folder = BUILDER.get_obj_parent_folder(object_id) + parent_folder = get_builder().get_obj_parent_folder(object_id) if parent_folder: if parent_folder == "alloy_structures": index = 37 @@ -986,11 +986,11 @@ def draw_item( for part in all_parts: operator = part_row.operator( "object.list_build_operator", - text=BUILDER.get_nice_name(part), + text=get_builder().get_nice_name(part), ) operator.part_id = part operator.tooltip = ( - f"Name: {BUILDER.get_nice_name(part)}\nID: ({part})" + f"Name: {get_builder().get_nice_name(part)}\nID: ({part})" ) # Draw Presets @@ -1050,25 +1050,25 @@ def generate_ui_list_data(item_type="parts", pack=None): ui_list_data = [] # Presets if "presets" in item_type: - preset_categories = BUILDER.get_preset_categories() + preset_categories = get_builder().get_preset_categories() for category in preset_categories: - presets = BUILDER.get_presets_from_category(category) + presets = get_builder().get_presets_from_category(category) if presets: ui_list_data.append((category, "")) for _preset in sorted(presets): ui_list_data.append(("", _preset)) # Uncategorized. - presets = BUILDER.get_uncategorized_presets() + presets = get_builder().get_uncategorized_presets() if presets: ui_list_data.append(("Uncategorized Prefabs", "")) for _preset in sorted(presets): ui_list_data.append(("", _preset)) else: # Packs/Parts - for category in BUILDER.get_categories(pack=pack): + for category in get_builder().get_categories(pack=pack): ui_list_data.append((category, "")) - category_parts = BUILDER.get_parts_from_category(category, pack=pack) - category_parts = sorted(category_parts, key=BUILDER.get_nice_name) + category_parts = get_builder().get_parts_from_category(category, pack=pack) + category_parts = sorted(category_parts, key=get_builder().get_nice_name) new_parts = create_sublists(category_parts) for part in new_parts: joined_list = ",".join(part) @@ -1205,7 +1205,7 @@ class SaveAsPreset(bpy.types.Operator): def execute(self, context): # Save Preset. - BUILDER.save_preset_to_file(self.preset_name) + get_builder().save_preset_to_file(self.preset_name) # Refresh Preset List. scene = context.scene nms_tool = scene.nms_base_tool @@ -1376,9 +1376,9 @@ def execute(self, context): # Build item if self.part_id in preset.Preset.get_presets(): - new_item = BUILDER.add_preset(self.part_id) + new_item = get_builder().add_preset(self.part_id) else: - new_item = BUILDER.add_part(self.part_id) + new_item = get_builder().add_part(self.part_id) if hasattr(new_item, "build_rig"): new_item.build_rig() @@ -1387,7 +1387,7 @@ def execute(self, context): # If there was a previous selection, snap the new item to it. if selection: - builder_selection = BUILDER.get_builder_object_from_bpy_object(selection) + builder_selection = get_builder().get_builder_object_from_bpy_object(selection) if builder_selection: new_item.snap_to(builder_selection) return {"FINISHED"} @@ -1407,13 +1407,13 @@ def execute(self, context): nms_tool.new_file() preset.Preset( preset_id=self.part_id, - builder_object=BUILDER, + builder_object=get_builder(), create_control=False, apply_shader=False, build_rigs=True, ) - BUILDER.build_rigs() - BUILDER.optimise_control_points() + get_builder().build_rigs() + get_builder().optimise_control_points() return {"FINISHED"} def invoke(self, context, event): @@ -1498,13 +1498,13 @@ def execute(self, context): return {"CANCELLED"} # Create a new point at the cursor. - point = line.Line.create_point(BUILDER, name="ARBITRARY_POINT") + point = line.Line.create_point(get_builder(), name="ARBITRARY_POINT") point.location = context.scene.cursor.location # If another powerline was already selected, connect it if selection and "rig_item" in selection: line_object = selection.get("power_line", "U_POWERLINE").split(".")[0] - power_line = BUILDER.add_part(line_object, build_rigs=False) + power_line = get_builder().add_part(line_object, build_rigs=False) # Create controls. power_line.build_rig(start=selection, end=point) @@ -1523,7 +1523,7 @@ class Connect(bpy.types.Operator): def execute(self, context): # Validate selection. selected_objects = [ - BUILDER.get_builder_object_from_bpy_object(o) + get_builder().get_builder_object_from_bpy_object(o) for o in bpy.context.selected_objects ] selected_objects = [o for o in selected_objects if o.has_snap_point("POWER")] @@ -1538,7 +1538,7 @@ def execute(self, context): ShowMessageBox(message=message, title="Connect") return {"FINISHED"} - active_object = BUILDER.get_builder_object_from_bpy_object( + active_object = get_builder().get_builder_object_from_bpy_object( bpy.context.active_object ) if not active_object.has_snap_point("POWER"): @@ -1553,7 +1553,7 @@ def execute(self, context): continue # Build and perform connection. start_point, end_point = line.Line.generate_control_points( - active_object, selected_object, BUILDER + active_object, selected_object, get_builder() ) if not start_point or not end_point: # should have been tested by filtering selected_objects above @@ -1568,7 +1568,7 @@ def execute(self, context): # if "power_line" in start_point: # line_object_id = start_point["power_line"].split(".")[0] - power_line = BUILDER.add_part(line_object_id, build_rigs=False) + power_line = get_builder().add_part(line_object_id, build_rigs=False) # Create controls. power_line.build_rig(start=start_point, end=end_point) @@ -1601,7 +1601,7 @@ def execute(self, context): return {"FINISHED"} # Perform split. - power_line = BUILDER.get_builder_object_from_bpy_object(target) + power_line = get_builder().get_builder_object_from_bpy_object(target) power_line.divide() return {"FINISHED"} @@ -1631,7 +1631,7 @@ def execute(self, context): return {"FINISHED"} # Perform split. - power_line = BUILDER.get_builder_object_from_bpy_object(target) + power_line = get_builder().get_builder_object_from_bpy_object(target) power_line.split() return {"FINISHED"} @@ -1645,7 +1645,7 @@ class SelectConnected(bpy.types.Operator): def execute(self, context): selected_objects = [ - BUILDER.get_builder_object_from_bpy_object(o) + get_builder().get_builder_object_from_bpy_object(o) for o in bpy.context.selected_objects ] @@ -1665,10 +1665,10 @@ class SelectFloating(bpy.types.Operator): bl_options = {"UNDO", "REGISTER"} def execute(self, context): - for part in BUILDER.get_all_parts(include_lines=True): + for part in get_builder().get_all_parts(include_lines=True): if not "SnapID" in part: continue - part = BUILDER.get_builder_object_from_bpy_object(part) + part = get_builder().get_builder_object_from_bpy_object(part) if part.snap_id != "POWER_CONTROL": continue is_connected_to_object = False @@ -1699,10 +1699,10 @@ def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() # Build button. - button = BUILDER.add_part("U_SWITCHBUTTON") + button = get_builder().add_part("U_SWITCHBUTTON") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. @@ -1720,10 +1720,10 @@ class LogicWallSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("U_SWITCHWALL") + button = get_builder().add_part("U_SWITCHWALL") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1740,10 +1740,10 @@ class LogicProxSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("U_SWITCHPROX") + button = get_builder().add_part("U_SWITCHPROX") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1760,10 +1760,10 @@ class LogicInvSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("U_TRANSISTOR1") + button = get_builder().add_part("U_TRANSISTOR1") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1780,10 +1780,10 @@ class LogicAutoSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("U_TRANSISTOR2") + button = get_builder().add_part("U_TRANSISTOR2") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1800,10 +1800,10 @@ class LogicFloorSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("U_SWITCHPRESS") + button = get_builder().add_part("U_SWITCHPRESS") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1820,10 +1820,10 @@ class LogicBeatSwitch(bpy.types.Operator): def execute(self, context): # Get Selected item. selection = blend_utils.get_current_selection() - button = BUILDER.add_part("BYTEBEATSWITCH") + button = get_builder().add_part("BYTEBEATSWITCH") # Snap to selection. if selection: - selection = BUILDER.get_builder_object_from_bpy_object(selection) + selection = get_builder().get_builder_object_from_bpy_object(selection) button.snap_to(selection) # Select new item. button.select() @@ -1857,7 +1857,7 @@ def execute(self, context): names.append(pid) # Clear builder caches - BUILDER.clear_caches() + get_builder().clear_caches() self.report({"INFO"}, f"Prefabs split: {len(names)}, parts: {total}") return {"FINISHED"} diff --git a/src/addons/no_mans_sky_base_builder/asset_browser/main.py b/src/addons/no_mans_sky_base_builder/asset_browser/main.py index 6aa45e10..414d8289 100644 --- a/src/addons/no_mans_sky_base_builder/asset_browser/main.py +++ b/src/addons/no_mans_sky_base_builder/asset_browser/main.py @@ -557,7 +557,7 @@ def send_part_command_to_blender(self, item_id): import no_mans_sky_base_builder.preset as preset import no_mans_sky_base_builder.utils.blend_utils as blend_utils - BUILDER = no_mans_sky_base_builder.BUILDER + BUILDER = no_mans_sky_base_builder.builder.get_builder() selection = blend_utils.get_current_selection() @@ -590,7 +590,7 @@ def send_edit_preset_command_to_blender(self, item_id): import no_mans_sky_base_builder.preset as preset import no_mans_sky_base_builder.utils.blend_utils as blend_utils - BUILDER = no_mans_sky_base_builder.BUILDER + BUILDER = no_mans_sky_base_builder.builder.get_builder() nms_tool = bpy.context.scene.nms_base_tool if item_id in preset.Preset.get_presets(): diff --git a/src/addons/no_mans_sky_base_builder/builder/__init__.py b/src/addons/no_mans_sky_base_builder/builder/__init__.py index 99930c0a..bbbaec07 100644 --- a/src/addons/no_mans_sky_base_builder/builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/builder/__init__.py @@ -1,4 +1,40 @@ +"""Access to the builder shared by the whole addon. + +Other addons can swap the builder or add part classes from their register(): + + nms_builder.set_builder(MyBuilder()) # MyBuilder subclasses Builder + nms_builder.register_override(MyPart, ["CUBEROOM"]) + +and undo both from unregister() with set_builder(None) and unregister_override(). +""" + from .builder import Builder +from .overrides import register_override, unregister_override + +__all__ = [ + "Builder", + "get_builder", + "set_builder", + "register_override", + "unregister_override", +] + +_default_builder = None +_active_builder = None + + +def get_builder(): + global _default_builder, _active_builder + if _active_builder is None: + if _default_builder is None: + _default_builder = Builder() + _active_builder = _default_builder + return _active_builder + -# one builder shared by the whole addon, so every tool sees the same part cache -BUILDER = Builder() +def set_builder(builder_object): + # None restores the addon's own builder + global _active_builder + if builder_object is not None and not isinstance(builder_object, Builder): + raise TypeError("set_builder expects a Builder instance or None") + _active_builder = builder_object diff --git a/src/addons/no_mans_sky_base_builder/builder/importer.py b/src/addons/no_mans_sky_base_builder/builder/importer.py index 58eba983..675cf901 100644 --- a/src/addons/no_mans_sky_base_builder/builder/importer.py +++ b/src/addons/no_mans_sky_base_builder/builder/importer.py @@ -18,7 +18,7 @@ def import_objects(builder_object, objects_data, compensate_normal=True): unique_objects = {} - unique_meshes = {} + unique_materials = {} import_collection = collection_utils.get_collection(IMPORT_COLLECTION_NAME) set_collection_excluded(import_collection.name, True) @@ -46,7 +46,7 @@ def import_objects(builder_object, objects_data, compensate_normal=True): user_data, import_collection, unique_objects, - unique_meshes, + unique_materials, ) if bpy_object is None: continue @@ -70,9 +70,9 @@ def set_collection_excluded(collection_name, excluded): # colour is a flat material on the mesh, so objects can only share a mesh # when their ObjectID AND UserData match def build_fbx_part( - builder_object, object_id, user_data, import_collection, unique_objects, unique_meshes + builder_object, object_id, user_data, import_collection, unique_objects, unique_materials ): - mesh_key = (object_id, user_data) + material_key = (object_id, user_data) # import object from disk when visiting that object_id for first time if object_id not in unique_objects: @@ -85,18 +85,18 @@ def build_fbx_part( builder_object.add_to_part_cache(object_id, bpy_object) unique_objects[object_id] = bpy_object - unique_meshes[mesh_key] = bpy_object.data + unique_materials[material_key] = bpy_object.data return bpy_object bpy_object = unique_objects[object_id].copy() import_collection.objects.link(bpy_object) - if mesh_key in unique_meshes: - bpy_object.data = unique_meshes[mesh_key] + if material_key in unique_materials: + bpy_object.data = unique_materials[material_key] else: bpy_object.data = bpy_object.data.copy() material.restore_material(bpy_object, user_data) - unique_meshes[mesh_key] = bpy_object.data + unique_materials[material_key] = bpy_object.data return bpy_object @@ -148,8 +148,11 @@ def restore_params(bpy_object, part_data, object_id): bpy_object[Part.PROP_TIMESTAMP] = time_stamp bpy_object[Part.PROP_BELONGS_TO_PRESET] = False + # copies carry the source's message, so clear it when this part has none if message: bpy_object[Part.PROP_MESSAGE] = message + else: + bpy_object.pop(Part.PROP_MESSAGE, None) return bpy_object diff --git a/src/addons/no_mans_sky_base_builder/builder/overrides.py b/src/addons/no_mans_sky_base_builder/builder/overrides.py index 1d11a537..d8b11c99 100644 --- a/src/addons/no_mans_sky_base_builder/builder/overrides.py +++ b/src/addons/no_mans_sky_base_builder/builder/overrides.py @@ -68,9 +68,23 @@ _CLASS_BY_ID.setdefault(_object_id, _class_ref) +# classes added by other addons, these win over the built in table +_REGISTERED_BY_ID = {} + + +def register_override(class_ref, object_ids): + for object_id in object_ids: + _REGISTERED_BY_ID[object_id.replace("^", "")] = class_ref + + +def unregister_override(object_ids): + for object_id in object_ids: + _REGISTERED_BY_ID.pop(object_id.replace("^", ""), None) + + def get_override_class(object_id): - return _CLASS_BY_ID.get(object_id) + return _REGISTERED_BY_ID.get(object_id) or _CLASS_BY_ID.get(object_id) def get_part_class(object_id): - return _CLASS_BY_ID.get(object_id, Part) + return get_override_class(object_id) or Part diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py index 50b4f34c..7964060a 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_manager.py @@ -1,10 +1,9 @@ import bpy -from .. import builder +from ..builder import get_builder from . import save_editor_dependencies from . import save_editor_utils from .save_editor_utils import BaseType, BaseData -BUILDER = builder.BUILDER #save data to persist within blend files. class SaveManager(bpy.types.PropertyGroup): @@ -345,7 +344,7 @@ def import_base(self, context, base_identifiers, save_links): # Import json into scene nms_tools = context.scene.nms_base_tool nms_tools.deserialise_from_data(nms_base_json) - BUILDER.deserialise_from_data(nms_base_json) + get_builder().deserialise_from_data(nms_base_json) #return a string for operators for status message return "Base/Corvette imported sucessfully" diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py index 6000c798..a76f9440 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py @@ -4,7 +4,8 @@ import uuid from ..utils import blend_utils, curve, material from ..utils import python as python_utils -from .. import builder, part +from .. import part +from ..builder import get_builder from ..utils.mirror_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -14,7 +15,6 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.BUILDER class BatchTool(bpy.types.PropertyGroup): @@ -100,7 +100,7 @@ def batch_replace_with_target_object(self): else : # create a temp object if "ObjectID" is provided object_id = self.object_id - new_obj = BUILDER.add_part(object_id) + new_obj = get_builder().add_part(object_id) target_object = new_obj.object replaced_objects_list = [] diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool.py b/src/addons/no_mans_sky_base_builder/tools/build_tool.py index ffcd51e9..be430bb3 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool.py @@ -4,7 +4,8 @@ import uuid from ..utils import blend_utils, curve from ..utils import python as python_utils -from .. import builder, part +from .. import part +from ..builder import get_builder from ..utils.mirror_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -14,7 +15,6 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.BUILDER from mathutils import Vector @@ -108,7 +108,7 @@ def mirror(self, axis = None, center = None, change_orientation = False, auto_du # Build Item. mirror_part_exist = mirror_id in nice_name_dictionary.keys() if mirror_part_exist: - new_item = BUILDER.mirror_part(target) + new_item = get_builder().mirror_part(target) if not change_orientation: mirrored_matrix_world = mirror_utils.mirror_matrix_world_universal(object_id, new_item.matrix_world, axis,center) @@ -184,7 +184,7 @@ def flip(self): new_item = target if mirror_id in nice_name_dictionary.keys(): # Build Item. - new_item = BUILDER.flip_part(target) + new_item = get_builder().flip_part(target) new_items.append(new_item) if hasattr(new_item, "object"): @@ -291,19 +291,19 @@ def duplicate(self): object_id = target["ObjectID"] user_data = target["UserData"] # Build Item. - new_item = BUILDER.add_part(object_id, user_data=user_data) + new_item = get_builder().add_part(object_id, user_data=user_data) new_item.select() if "PresetID" in target: preset_id = target["PresetID"] # Build Item. - new_item = BUILDER.add_preset(preset_id) + new_item = get_builder().add_preset(preset_id) new_item.select() # Build Rig if need to. if hasattr(new_item, "build_rig"): new_item.build_rig() # Snap. - target = BUILDER.get_builder_object_from_bpy_object(target) + target = get_builder().get_builder_object_from_bpy_object(target) new_item.snap_to(target) def snap( @@ -343,8 +343,8 @@ def snap( return {"FINISHED"} # Perform Snap - source = BUILDER.get_builder_object_from_bpy_object(source) - target = BUILDER.get_builder_object_from_bpy_object(target) + source = get_builder().get_builder_object_from_bpy_object(source) + target = get_builder().get_builder_object_from_bpy_object(target) if source and target: source.snap_to( target, diff --git a/src/addons/no_mans_sky_base_builder/tools/properties.py b/src/addons/no_mans_sky_base_builder/tools/properties.py index 4568663c..932ef83d 100644 --- a/src/addons/no_mans_sky_base_builder/tools/properties.py +++ b/src/addons/no_mans_sky_base_builder/tools/properties.py @@ -4,7 +4,7 @@ import uuid from ..utils import blend_utils, curve from ..utils import python as python_utils -from .. import builder, part +from .. import part from ..utils.mirror_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -14,7 +14,6 @@ ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) GHOSTED_ITEMS = ghosted_reference["GHOSTED"] nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -BUILDER = builder.BUILDER class Properties(bpy.types.PropertyGroup): diff --git a/src/addons/no_mans_sky_base_builder/utils/curve.py b/src/addons/no_mans_sky_base_builder/utils/curve.py index 50f6e2bf..f1d37e8d 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve.py @@ -5,12 +5,12 @@ import bpy -from .. import builder, part +from .. import part +from ..builder import get_builder from . import (blend_utils, collection_utils, curve_utils, material, mirror_utils) from . import python as python_utils -BUILDER = builder.BUILDER FILE_PATH = os.path.dirname(os.path.realpath(__file__)) NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") @@ -95,7 +95,7 @@ def duplicate_along_curve( bpy_object, curve, number_of_duplicates=10, radius_mu caller = inspect.stack()[1] print("Called by:", caller.function) print("bulding new object") - new_item = BUILDER.add_part(object_id, user_data=user_data) + new_item = get_builder().add_part(object_id, user_data=user_data) new_obj = new_item.object else: new_obj = bpy_object.copy() From 2ca1a17ac80e64cb70d955adda7e935d138d7e1e Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:52:53 +0530 Subject: [PATCH 09/15] delete and add to scene methiods from blend_utils can work outside viewlayer now, Removed dependence of line.py on matrix_world so that those opeartions can happen with viewlayer disabled. This prevents triggering of Depsgraph Udpate after every change in scene. --- .../part_overrides/line.py | 76 +++++++++---------- .../utils/blend_utils.py | 25 +++--- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/part_overrides/line.py b/src/addons/no_mans_sky_base_builder/part_overrides/line.py index 8e68af8a..20a95e49 100644 --- a/src/addons/no_mans_sky_base_builder/part_overrides/line.py +++ b/src/addons/no_mans_sky_base_builder/part_overrides/line.py @@ -1,6 +1,5 @@ import math import os -from copy import copy import bpy import mathutils @@ -61,8 +60,16 @@ def build_rig(self, start=None, end=None): start (object): Pass a start control, if None a new one is made. end (object): Pass an end control, if None a new one is made. """ - # Start creating controls. - world_pos = copy(self.object.matrix_world) + # built from local transforms, matrix_world can be stale before a depsgraph update + world_pos = mathutils.Matrix.LocRotScale( + self.object.location, self.object.rotation_euler, self.object.scale + ) + if self.object.parent: + world_pos = ( + self.object.parent.matrix_world + @ self.object.matrix_parent_inverse + @ world_pos + ) world_loc = world_pos.decompose()[0] if not start: start = Line.create_point( @@ -97,67 +104,60 @@ def build_rig(self, start=None, end=None): end["power_line"] = self.object.name def split(self): - # Middle control. + prev_start_control = blend_utils.get_item_by_name(self.object["start_control"]) + prev_end_control = blend_utils.get_item_by_name(self.object["end_control"]) + + # Middle controls, placed between the existing controls. middle_control_a = self.create_point( self.builder, "_".join([self.name, "MID_A"]) ) middle_control_b = self.create_point( self.builder, "_".join([self.name, "MID_B"]) ) - # Snap the middle locations to the middle of power line. - world_pos = copy(self.matrix_world) - world_loc = world_pos.decompose()[0] - mid_a_pos = mathutils.Vector( - [world_pos[0][2] * 0.4, world_pos[1][2] * 0.4, world_pos[2][2] * 0.4] + middle_control_a.location = prev_start_control.location.lerp( + prev_end_control.location, 0.4 ) - middle_a_location = world_loc + mid_a_pos - middle_control_a.location = middle_a_location - - mid_b_pos = mathutils.Vector( - [world_pos[0][2] * 0.6, world_pos[1][2] * 0.6, world_pos[2][2] * 0.6] + middle_control_b.location = prev_start_control.location.lerp( + prev_end_control.location, 0.6 ) - middle_b_location = world_loc + mid_b_pos - middle_control_b.location = middle_b_location + # Create new powerline. new_powerline = self.builder.add_part(self.object_id, build_rigs=False) - # Create additional controls. - prev_start_control_name = self.object["start_control"] - prev_end_control_name = self.object["end_control"] - prev_start_control = blend_utils.get_item_by_name(prev_start_control_name) - prev_end_control = blend_utils.get_item_by_name(prev_end_control_name) # Remove constraints of the original power line. self.remove_constraints() # Assign new controls to the power lines. self.build_rig(prev_start_control, middle_control_a) new_powerline.build_rig(middle_control_b, prev_end_control) - # Select the middle controller. - blend_utils.select([middle_control_a, middle_control_b]) + + # fails when the controls are in an excluded collection + try: + blend_utils.select([middle_control_a, middle_control_b]) + except RuntimeError: + pass def divide(self): - # Middle control. + prev_start_control = blend_utils.get_item_by_name(self.object["start_control"]) + prev_end_control = blend_utils.get_item_by_name(self.object["end_control"]) + + # Middle control, placed between the existing controls. middle_control = self.create_point(self.builder, "_".join([self.name, "MID"])) - # Snap the middle location to the middle of power line. - world_pos = copy(self.matrix_world) - world_loc = world_pos.decompose()[0] - at_vec = mathutils.Vector( - [world_pos[0][2] / 2, world_pos[1][2] / 2, world_pos[2][2] / 2] + middle_control.location = prev_start_control.location.lerp( + prev_end_control.location, 0.5 ) - middle_location = world_loc + at_vec - middle_control.location = middle_location + # Create new powerline. new_powerline = self.builder.add_part(self.object_id, build_rigs=False) - # Create additional controls. - prev_start_control_name = self.object["start_control"] - prev_end_control_name = self.object["end_control"] - prev_start_control = blend_utils.get_item_by_name(prev_start_control_name) - prev_end_control = blend_utils.get_item_by_name(prev_end_control_name) # Remove constraints of the original power line. self.remove_constraints() # Assign new controls to the power lines. self.build_rig(prev_start_control, middle_control) new_powerline.build_rig(middle_control, prev_end_control) - # Select the middle controller. - blend_utils.select(middle_control) + + # fails when the control is in an excluded collection + try: + blend_utils.select(middle_control) + except RuntimeError: + pass # Class Methods --- @classmethod diff --git a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py index 5b0f379c..f4fac1a7 100644 --- a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py @@ -32,9 +32,14 @@ def add_to_scene(item, collection_name="Collection"): bpy.context.scene.collection.children.link(collection) # Add the item to the collection. - object_set = bpy.data.collections[collection_name].objects - if item.name not in object_set: - object_set.link(item) + collection = bpy.data.collections[collection_name] + if item.name not in collection.objects: + collection.objects.link(item) + + # operators like the fbx importer also link to the active collection + for other_collection in list(item.users_collection): + if other_collection != collection: + other_collection.objects.unlink(item) def get_item_by_name(item_name): @@ -154,16 +159,10 @@ def get_distance_between(matrix1, matrix2): def delete(bpy_object): """Remove the item and everything below it.""" - # Deselect all - bpy.ops.object.select_all(action="DESELECT") - - # Parent items to control. - for part in bpy_object.children: - part.hide_select = False - part.select_set(True) - - bpy_object.select_set(True) - bpy.ops.object.delete() + # removed at data level, selecting fails for objects outside the view layer + for part in list(bpy_object.children): + bpy.data.objects.remove(part, do_unlink=True) + bpy.data.objects.remove(bpy_object, do_unlink=True) def find_duplicates(decimals = 4): From 8e898dc130510512b3667c38549b38ffb5999703 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:37:22 +0530 Subject: [PATCH 10/15] Curve: Manipulating Weight of a point will change density of objeects arount that point. Many performance optimiztaions. --- .../no_mans_sky_base_builder/__init__.py | 139 +-- .../tools/build_tool.py | 11 +- .../tools/build_tool_operators.py | 22 +- .../tools/properties.py | 109 +-- .../utils/collection_utils.py | 32 - .../no_mans_sky_base_builder/utils/curve.py | 886 ++++++++++-------- .../utils/curve_utils.py | 460 +++++---- .../utils/mirror_utils.py | 10 + 8 files changed, 969 insertions(+), 700 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 555b18f8..9ca6d352 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -555,7 +555,8 @@ def new_file(self): preset_check = "PresetID" in bpy_object light_check = "NMS_LIGHT" in bpy_object rig_check = "rig_item" in bpy_object - if any([id_check, preset_check, light_check, rig_check]): + curve_check = curve.Curve.PROP_CURVE_ID in bpy_object + if any([id_check, preset_check, light_check, rig_check, curve_check]): blend_utils.remove_object(bpy_object.name) # Reset room vis @@ -1864,17 +1865,29 @@ def execute(self, context): -# Track curve objects -known_curves = set() +# Track curve objects by name, object references die with undo +known_curve_names = set() # To reset toggle button of save editor and initialize curve registry @persistent def reset_plugin_state(dummy): - + # collect all curves when a blend file is reopened - global known_curves - known_curves = set( obj for obj in bpy.data.objects if obj.type == 'CURVE' and obj.get("has_linked_objects", False) ) - curve.update_curves(known_curves) + global known_curve_names + known_curves = [ + obj for obj in bpy.context.scene.objects + if obj.type == 'CURVE' and obj.get("has_linked_objects", False) + ] + for curve_obj in known_curves: + curve.migrate_legacy_curve(curve_obj) + known_curve_names = set(obj.name for obj in known_curves) + + # followers are saved in place, so nothing is rebuilt on load, and the saved + # slider difference must never be applied to every curve in the file + for scene in bpy.data.scenes: + properties = scene.nms_properties + properties.prev_curve_number_of_objects = properties.active_curve_number_of_objects + properties.prev_curve_radius_multiplier = properties.active_curve_radius_multiplier # reset save editor's state to closed for scene in bpy.data.scenes: @@ -1899,66 +1912,70 @@ def active_object_watcher(scene, depsgraph): # Whenever a curve is modified, automatically update whatever child objects that are associated with that curve. @persistent def curve_udpate_handler(scene, depsgraph): - global known_curves - - # check each object in scene to detect if their parent curve has been deleted by user or not - # if not, we update object's base scale to keep track of transformation changes made by user - current_curves = set() - for obj in bpy.data.objects: - # if object type is a linked curve - if obj.type == 'CURVE' and obj.get("has_linked_objects", False): - current_curves.add(obj) - # if object type is a child of curve - elif "curve_parent" in obj: - parent_curve = bpy.data.objects.get(obj["curve_parent"]) - # remove object if it's parent curve has been deleted - if parent_curve is None: - bpy.data.objects.remove(obj, do_unlink=True) - # Update base scale to persiste changes to scale made by user when curve mode is switched - elif not parent_curve.get("parent_selected", True): - obj["base_scale"] = curve.calculate_base_scale(parent_curve, obj) - - # Detect dead curves, cuerves that have been deleted by user through blender - dead_curves = known_curves - current_curves - if dead_curves: - known_curves.difference_update(dead_curves) - - # identify new curves - new_curves_detected = [] - + global known_curve_names + Curve = curve.Curve + + # fired from inside a curve rebuild, the rebuild isn't finished yet + if curve.is_busy(): + return + # Collect curves that have recieved updates by user - updated_curves = set() + updated_curve_names = set() for update in depsgraph.updates: - if isinstance(update.id, bpy.types.Object): - # validate each object - orig_obj = bpy.data.objects.get(update.id.name) - if orig_obj and orig_obj.type == 'CURVE' and orig_obj.get("has_linked_objects", False): - updated_curves.add(orig_obj) - if orig_obj not in known_curves and orig_obj not in new_curves_detected: - # a completely new curve should not exist in know_curves set - new_curves_detected.append(orig_obj) - - # Handle duplication syncing - if new_curves_detected and known_curves: - for new_curve in new_curves_detected: - # if two curves have equal "unique_id", that means they have been duplicated using shift+d - # we need to duplicate objects in similar way on new curve too + if not isinstance(update.id, bpy.types.Object): + continue + # the object can already be gone by the time we look it up + orig_obj = bpy.data.objects.get(update.id.name) + if orig_obj is None: + continue + if orig_obj.type == 'CURVE' and Curve.PROP_CURVE_ID in orig_obj: + updated_curve_names.add(orig_obj.name) + elif Curve.PROP_CURVE_PARENT in orig_obj: + # keep scale changes the user makes to a follower while its curve isn't selected + parent_curve = scene.objects.get(orig_obj[Curve.PROP_CURVE_PARENT]) + if parent_curve is not None and not parent_curve.get(Curve.PROP_PARENT_SELECTED, True): + orig_obj[Curve.PROP_BASE_SCALE] = curve.calculate_base_scale(parent_curve, orig_obj) + + # a new curve sharing a CurveID with a known one was duplicated with shift+d + new_curve_names = updated_curve_names - known_curve_names + if new_curve_names and known_curve_names: + for new_curve_name in new_curve_names: try: - new_uuid = new_curve.get("unique_id") - # Look for curves that have same unique_id as new curve - # if a duplciate unique_id found, new curve must be duplicate of that curve - matching_curve = next((c for c in known_curves if c.get("unique_id") == new_uuid and c != new_curve), None) - if matching_curve is not None: - curve.sync_curves(new_curve, matching_curve) + new_curve = scene.objects.get(new_curve_name) + if new_curve is None: + continue + new_uuid = new_curve.get(Curve.PROP_CURVE_ID) + for curve_name in known_curve_names: + matching_curve = scene.objects.get(curve_name) + if (matching_curve is not None + and matching_curve.get(Curve.PROP_CURVE_ID) == new_uuid + and matching_curve.name != new_curve.name): + curve.sync_curves(new_curve, matching_curve) + break except ReferenceError as error: print("Reference error :", error) continue - - # Loop through all curves and update their children's transformations - curve.update_curves(known_curves) - - # Sync back down to the global tracking set - known_curves = current_curves + + # only rebuild curves that changed, the sliders apply themselves through their update callback + curves_to_update = [ + obj for obj in (scene.objects.get(name) for name in updated_curve_names) + if obj is not None + ] + if curves_to_update: + curve.update_curves(curves_to_update, children_by_curve=curve.get_curve_children_map()) + + # remove followers of curves the user deleted + dead_curve_names = set(name for name in known_curve_names if scene.objects.get(name) is None) + if dead_curve_names: + known_curve_names.difference_update(dead_curve_names) + orphans = [ + obj for obj in curve.get_follower_objects() + if obj.get(Curve.PROP_CURVE_PARENT) in dead_curve_names + ] + if orphans: + bpy.data.batch_remove(orphans) + + known_curve_names |= updated_curve_names class NMSAddonPreferences(bpy.types.AddonPreferences): diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool.py b/src/addons/no_mans_sky_base_builder/tools/build_tool.py index be430bb3..f75b6f77 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool.py @@ -228,7 +228,7 @@ def duplicate_along_curve(self, number_of_objects, radius_multiplier = 1.0): curve_object = new_curve_object else : - curve_object["unique_id"] = str(uuid.uuid4()) + curve_object[curve.Curve.PROP_CURVE_ID] = str(uuid.uuid4()) curve_object["parent_selected"] = True curve_object.show_in_front = True self.selected_curve_object_is_parent = True @@ -261,7 +261,10 @@ def delete(self): return for item in selected_objects: - blend_utils.delete(item) + if curve.Curve.PROP_CURVE_ID in item: + curve.delete_curve_and_children(item) + else: + blend_utils.delete(item) def duplicate(self): """Snaps one object to another based on selection.""" @@ -278,6 +281,10 @@ def duplicate(self): # Get Selected item. target = blend_utils.get_current_selection() + if curve.Curve.PROP_CURVE_ID in target: + blend_utils.select(curve.duplicate_curve(target)) + return + if "ObjectID" not in target and "PresetID" not in target: message = ( "This item can not be duplicated via the No Man's Sky tool. " diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py b/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py index ea05ea52..960a8ffa 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py @@ -278,15 +278,19 @@ class CurveBreakApart(bpy.types.Operator): bl_description = "Break apart objects from curve and make them independent" def execute(self, context): - active_object = bpy.context.active_object - try: - curve_obj, duplicates = curve.apply_curve_transforms_and_detach(active_object) - if duplicates is not None: - blend_utils.select(duplicates) - detached_count = len(duplicates) - self.report({'INFO'}, f"Created {detached_count} objects") - except TypeError as error_message: - self.report({'ERROR'}, str(error_message)) + unlinked_objects = [] + for obj in bpy.context.selected_objects: + if curve.Curve.PROP_CURVE_ID not in obj: + continue + try: + curve_obj, duplicates = curve.apply_curve_transforms_and_detach(obj) + unlinked_objects += duplicates + except TypeError as error_message: + self.report({'ERROR'}, str(error_message)) + + if unlinked_objects: + blend_utils.select(unlinked_objects) + self.report({'INFO'}, f"Created {len(unlinked_objects)} objects") return {"FINISHED"} diff --git a/src/addons/no_mans_sky_base_builder/tools/properties.py b/src/addons/no_mans_sky_base_builder/tools/properties.py index 932ef83d..f95a10fe 100644 --- a/src/addons/no_mans_sky_base_builder/tools/properties.py +++ b/src/addons/no_mans_sky_base_builder/tools/properties.py @@ -21,8 +21,8 @@ class Properties(bpy.types.PropertyGroup): active_curve_number_of_objects: bpy.props.IntProperty( name="Number of Objects", default = 10, - update = lambda self, context: self.on_number_of_objects_change(), - + update = lambda self, context: curve.update_selected_curves(), + min=1, # Absolute lowest value allowed max=1000, # Absolute highest value allowed soft_min=5, # Slider UI floor @@ -33,14 +33,18 @@ class Properties(bpy.types.PropertyGroup): active_curve_radius_multiplier: bpy.props.FloatProperty( name="Overall Radius", default = 1.0, - update = lambda self, context: self.on_curve_radius_multiplier_change(), - + update = lambda self, context: curve.update_selected_curves(), + min=0.0, # Absolute lowest value allowed max=100.0, # Absolute highest value allowed soft_min=0.01, # Slider UI floor soft_max=5.0 # Slider UI ceiling ) - + + # slider values at the last curve update, the difference is applied to selected curves + prev_curve_number_of_objects: bpy.props.IntProperty(default=10) + prev_curve_radius_multiplier: bpy.props.FloatProperty(default=1.0) + # For displaying name of target curve selected active_curve_name: bpy.props.StringProperty( name="active curve name", @@ -64,66 +68,62 @@ class Properties(bpy.types.PropertyGroup): ) active_object = None - - - def on_curve_radius_multiplier_change(self): - active = bpy.context.view_layer.objects.active - curve_obj = curve.get_curve_or_linked_curve(active) - - if curve_obj is None: - return - - original_object_id = curve_obj.get("dup_ObjectID",None) - if curve_obj and original_object_id: - - old_radius_miltiplier = curve_obj.get("radius_multiplier") - if old_radius_miltiplier and old_radius_miltiplier == self.active_curve_radius_multiplier: - return - - curve.update_curve_children(curve_obj, self.active_curve_radius_multiplier) - - def on_number_of_objects_change(self): - active = bpy.context.view_layer.objects.active - curve_obj = curve.get_curve_or_linked_curve(active) - - if curve_obj is None: - return - - original_object_id = curve_obj.get("dup_ObjectID",None) - if curve_obj and original_object_id: - - old_count = curve_obj.get("objects_count") - if old_count and old_count == self.active_curve_number_of_objects: - return - - curve.duplicate_along_curve( - None, - curve_obj, - self.active_curve_number_of_objects, - curve_obj.get("radius_multiplier",1.0) - ) - + def show_curve_edit_options(self,curve_obj): self.show_gap_edit_field = True self.active_curve_name = curve_obj.name - self.active_curve_number_of_objects = curve_obj.get("objects_count",10) - self.active_curve_radius_multiplier = curve_obj.get("radius_multiplier",1.0) - self.selected_curve_object_is_parent = curve_obj["parent_selected"] + + # switching curves must not read as a slider change + with curve.suspend_updates(): + self.prev_curve_number_of_objects = curve_obj.get("objects_count",10) + self.prev_curve_radius_multiplier = curve_obj.get("radius_multiplier",1.0) + self.active_curve_number_of_objects = self.prev_curve_number_of_objects + self.active_curve_radius_multiplier = self.prev_curve_radius_multiplier + + self.selected_curve_object_is_parent = curve_obj.get("parent_selected", True) def hide_curve_edit_options(self): self.show_gap_edit_field = False self.active_curve_name = "" - + def select_parent_curve(self): self.selected_curve_object_is_parent = True + selected_objects = list(bpy.context.selected_objects) active_object = bpy.context.active_object - curve.select_parent_curve(active_object) - + if active_object is not None and active_object not in selected_objects: + selected_objects.append(active_object) + + curves_to_select = [] + for obj in selected_objects: + if curve.Curve.PROP_CURVE_PARENT not in obj or curve.Curve.PROP_CURVE_ID in obj: + continue + parent_curve = curve.select_parent_curve(obj) + if parent_curve is not None and parent_curve not in curves_to_select: + curves_to_select.append(parent_curve) + + if curves_to_select: + blend_utils.select(curves_to_select) + def select_children_of_curve(self): self.selected_curve_object_is_parent = False - active_object = bpy.context.active_object - curve.select_children_of_curve(active_object) - + selected_objects = bpy.context.selected_objects + + if any(curve.Curve.PROP_CURVE_ID not in obj for obj in selected_objects): + ShowMessageBox( + message="All selected objects are not curves", + title="Selection Failed" + ) + return + + children_to_select = [] + for curve_obj in selected_objects: + children = curve.select_children_of_curve(curve_obj) + if children: + children_to_select.extend(children) + + if children_to_select: + blend_utils.select(children_to_select) + def active_curve_is_highlighted(self): selected_objects = bpy.context.selected_objects active_object = bpy.context.view_layer.objects.active @@ -144,6 +144,9 @@ def active_curve_is_highlighted(self): return False def set_active_obect(self, obj): + if obj is None: + return + curve_obj = curve.get_curve_or_linked_curve(obj) if curve_obj is not None: self.show_curve_edit_options(curve_obj) diff --git a/src/addons/no_mans_sky_base_builder/utils/collection_utils.py b/src/addons/no_mans_sky_base_builder/utils/collection_utils.py index 34c4d553..a851d3d8 100644 --- a/src/addons/no_mans_sky_base_builder/utils/collection_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/collection_utils.py @@ -7,38 +7,6 @@ LINKED_CURVE_OBJ_COL = "Linked Curve Objects" UNLINKED_CURVE_OBJ_COL = "Unlinked Curve Objects" -def move_curve_to_collection(curve_obj): - - global curve_prefix - global curve_suffix - - #parent collection of curve - master_col = get_parent_collection(curve_obj) - if str(master_col.name).endswith(curve_suffix): - master_col = get_parent_collection(master_col) - - if master_col is None: - if "master_col" in curve_obj and curve_obj["master_col"] is not None: - master_col = curve_obj["master_col"] - else : - master_col = bpy.context.scene.collection - - curve_obj["master_col"] = master_col - - #names for new collections to make - curve_collection_name = f"{curve_prefix}{curve_obj.name}{curve_suffix}" - curve_col = bpy.data.collections.get(curve_collection_name) - - # create a collection to house curve and also house a collection that comtains all children of that curve - if not curve_col: - curve_col = create_collection(curve_collection_name, "COLOR_02") - move_collection_into_collection(master_col, curve_col) - curve_obj["parent_col"] = curve_col - - move_object_into_collection(curve_col, curve_obj) - - return curve_col - # returns collection or creates a new collection def get_collection(collection_name): collection = bpy.data.collections.get(collection_name) diff --git a/src/addons/no_mans_sky_base_builder/utils/curve.py b/src/addons/no_mans_sky_base_builder/utils/curve.py index f1d37e8d..27055228 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve.py @@ -1,147 +1,316 @@ -import inspect +import contextlib +import functools import math -import os import uuid import bpy -from .. import part from ..builder import get_builder -from . import (blend_utils, collection_utils, curve_utils, material, - mirror_utils) -from . import python as python_utils +from ..part import Part +from . import collection_utils, curve_utils, material -FILE_PATH = os.path.dirname(os.path.realpath(__file__)) -NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") -nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) +class Curve: + # curve property names + PROP_HAS_LINKED_OBJECTS = "has_linked_objects" + PROP_RADIUS_MULTIPLIER = "radius_multiplier" + PROP_INITIAL_CURVE_SCALE = "initial_curve_scale" + PROP_OBJECTS_COUNT = "objects_count" + PROP_CURVE_ID = "CurveID" + PROP_PARENT_SELECTED = "parent_selected" + PROP_IS_INITIALISED = "is_initialised" + PROP_DENSITY_STEP = "density_step" + + # follower property names + PROP_CURVE_PARENT = "curve_parent" + PROP_BASE_SCALE = "base_scale" + PROP_CURVE_FACTOR = "curve_factor" + PROP_RADIUS = "radius" + + # the part duplicated along the curve + PROP_DUP_OBJECT_ID = f"dup_{Part.PROP_OBJECT_ID}" + PROP_DUP_USER_DATA = f"dup_{Part.PROP_USER_DATA}" + + # properties from older versions of the curve tool + LEGACY_PROPS = ("unique_id", "parent_col", "master_col") + + +# Rebuilding a curve can import fbx files, and every import fires the depsgraph handler +# mid rebuild. Entry points check this so a rebuild never starts another one. +_busy_depth = 0 + + +def is_busy(): + return _busy_depth > 0 + + +# hold off curve updates, e.g. while the panel sets both sliders +@contextlib.contextmanager +def suspend_updates(): + global _busy_depth + _busy_depth += 1 + try: + yield + finally: + _busy_depth -= 1 + + +def _marks_busy(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + with suspend_updates(): + return func(*args, **kwargs) + return wrapper + + +# weights start neutral, done before any spacing is measured from them +def ensure_initialised(curve_obj): + if curve_obj.get(Curve.PROP_IS_INITIALISED, False): + return + curve_utils.half_the_weight_points(curve_obj) + curve_obj[Curve.PROP_IS_INITIALISED] = True + # a step measured with the old weights would give the wrong count + curve_obj.pop(Curve.PROP_DENSITY_STEP, None) + + +# curves saved by older versions used unique_id and per curve collections +def migrate_legacy_curve(curve_obj): + if not curve_obj.get(Curve.PROP_HAS_LINKED_OBJECTS, False): + return + if Curve.PROP_CURVE_ID not in curve_obj: + curve_obj[Curve.PROP_CURVE_ID] = curve_obj.get("unique_id") or str(uuid.uuid4()) + for prop in Curve.LEGACY_PROPS: + curve_obj.pop(prop, None) + + +def get_follower_objects(): + # the whole scene, users move followers into their own collections + return bpy.context.scene.objects + + +def get_curve_children_map(objects=None): + """Group every follower by its curve in one pass. + + Returns: + dict: {curve name: [child objects]} + """ + objects = objects if objects is not None else get_follower_objects() + + children_by_curve = {} + for obj in objects: + parent_name = obj.get(Curve.PROP_CURVE_PARENT) + if parent_name is not None: + children_by_curve.setdefault(parent_name, []).append(obj) + return children_by_curve + + +@_marks_busy +def update_curves(updated_curves, children_by_curve=None): + if not updated_curves: + return + + properties = bpy.context.scene.nms_properties + + # sliders apply as relative changes, so every selected curve keeps its own values + count_delta = properties.active_curve_number_of_objects - properties.prev_curve_number_of_objects + radius_delta = properties.active_curve_radius_multiplier - properties.prev_curve_radius_multiplier -def update_curves(updated_curves): - scene = bpy.context.scene for curve_obj in updated_curves: - properties = scene.nms_properties - if "has_linked_objects" in curve_obj: - try: - if curve_obj.name == properties.active_curve_name: - new_radius_multiplier = properties.active_curve_radius_multiplier - elif "radius_multiplier" in curve_obj: - new_radius_multiplier = curve_obj["radius_multiplier"] - update_curve_children(curve_obj, new_radius_multiplier) - except ReferenceError: + if curve_obj is None or Curve.PROP_CURVE_ID not in curve_obj: + continue + + try: + ensure_initialised(curve_obj) + current_count = curve_obj.get(Curve.PROP_OBJECTS_COUNT, 0) + current_radius = curve_obj.get(Curve.PROP_RADIUS_MULTIPLIER, 1.0) + + new_number_of_objects = max(1, current_count + count_delta) + new_radius_multiplier = max(0.001, current_radius + radius_delta) + + total_density = curve_utils.get_total_curve_density(curve_obj, current_count) + + if count_delta != 0 or Curve.PROP_DENSITY_STEP not in curve_obj: + # count changed from the UI, remember the spacing it gives + curve_obj[Curve.PROP_DENSITY_STEP] = total_density / max(1, new_number_of_objects - 1) + else: + # weights changed, keep the spacing and work out the count from it + density_step = curve_obj[Curve.PROP_DENSITY_STEP] + if density_step > 0: + new_number_of_objects = max(1, int(round((total_density / density_step) + 1))) + if (bpy.context.active_object == curve_obj + and properties.active_curve_number_of_objects != new_number_of_objects): + # prev first, or the slider callback applies this again + properties.prev_curve_number_of_objects = new_number_of_objects + properties.active_curve_number_of_objects = new_number_of_objects + + children = ( + children_by_curve.get(curve_obj.name) + if children_by_curve is not None else None + ) + + if new_number_of_objects != current_count: + # this also reflows the followers, so the curve is finished here + duplicate_along_curve( + None, curve_obj, new_number_of_objects, new_radius_multiplier, + existing_objs=children, + ) + curve_obj[Curve.PROP_OBJECTS_COUNT] = new_number_of_objects continue + update_curve_children(curve_obj, new_radius_multiplier, children) + + except ReferenceError as error: + print(error) + continue + + properties.prev_curve_number_of_objects = properties.active_curve_number_of_objects + properties.prev_curve_radius_multiplier = properties.active_curve_radius_multiplier + + +# apply the curve sliders to every selected curve +def update_selected_curves(): + # a rebuild writing the sliders back must not apply them again + if is_busy(): + return + + properties = bpy.context.scene.nms_properties + if (properties.active_curve_number_of_objects == properties.prev_curve_number_of_objects + and properties.active_curve_radius_multiplier == properties.prev_curve_radius_multiplier): + return + + curves = [ + obj for obj in (getattr(bpy.context, "selected_objects", None) or []) + if Curve.PROP_CURVE_ID in obj + ] + active_object = bpy.context.view_layer.objects.active + if active_object is not None and Curve.PROP_CURVE_ID in active_object and active_object not in curves: + curves.append(active_object) + + if curves: + update_curves(curves, children_by_curve=get_curve_children_map()) + else: + properties.prev_curve_number_of_objects = properties.active_curve_number_of_objects + properties.prev_curve_radius_multiplier = properties.active_curve_radius_multiplier + + # update children on curve -def update_curve_children(curve_obj, new_radius_multier = None): +def update_curve_children(curve_obj, new_radius_multiplier=None, curve_children=None): """Refreshes transformations for all objects assigned to this curve.""" - if not curve_obj.get("has_linked_objects"): + if not curve_obj.get(Curve.PROP_HAS_LINKED_OBJECTS): return - - val_data, total_length = curve_utils.build_curve_eval_data(curve_obj, resolution=64) - - if new_radius_multier is not None: - curve_obj["radius_multiplier"] = new_radius_multier - - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == curve_obj.name: - curve_utils.update_obj_transformations(obj, curve_obj, val_data, total_length) - - -def duplicate_along_curve( bpy_object, curve, number_of_duplicates=10, radius_multiplier=1.0): - curve["has_linked_objects"] = True - curve["radius_multiplier"] = radius_multiplier - - if "initial_curve_scale" not in curve: - curve["initial_curve_scale"] = curve.scale.x - + + val_data, total_length = curve_utils.build_curve_eval_data(curve_obj, resolution=16) + + if new_radius_multiplier is not None: + curve_obj[Curve.PROP_RADIUS_MULTIPLIER] = new_radius_multiplier + + if curve_children is None: + children = get_all_curve_children(curve_obj, require_id=False) + else: + children = curve_children + + if not children: + return + + curve_utils.calculate_curve_factors(curve_obj, children) + curve_context = curve_utils.build_curve_context(curve_obj) + curve_name = curve_obj.name + for obj in children: + if obj.get(Curve.PROP_CURVE_PARENT) == curve_name: + curve_utils.update_obj_transformations( + obj, curve_obj, val_data, total_length, curve_context + ) + + +@_marks_busy +def duplicate_along_curve(bpy_object, curve, number_of_duplicates=10, radius_multiplier=1.0, existing_objs=None): + if curve.get(Curve.PROP_HAS_LINKED_OBJECTS, False): + curve_utils.normalise_curve_scale(curve) + + ensure_initialised(curve) + + curve[Curve.PROP_HAS_LINKED_OBJECTS] = True + curve[Curve.PROP_RADIUS_MULTIPLIER] = radius_multiplier + + if Curve.PROP_INITIAL_CURVE_SCALE not in curve: + curve[Curve.PROP_INITIAL_CURVE_SCALE] = curve.scale.x + if bpy_object is not None: - curve["dup_ObjectID"] = bpy_object["ObjectID"] - curve["dup_UserData"] = bpy_object["UserData"] - - # Calculate the fractional distance between bpy_objects. - gap_distance = 1.0 / (number_of_duplicates - 1) - - # Gather all bpy_objects currently following this curve - existing_objs = [obj for obj in bpy.context.scene.objects if obj.get("curve_parent") == curve.name] + curve[Curve.PROP_DUP_OBJECT_ID] = bpy_object[Part.PROP_OBJECT_ID] + curve[Curve.PROP_DUP_USER_DATA] = bpy_object[Part.PROP_USER_DATA] + + # callers that already grouped followers by curve can pass them in and skip the scan + if existing_objs is None: + existing_objs = get_all_curve_children(curve, require_id=False) or [] current_count = len(existing_objs) - - # here we check if number of objects needed on curve are more or less than previously duplicated objects. - # if number objects previously duplicated is more than what we need on curve, we remove extra objects + if number_of_duplicates < current_count: - remove_count = current_count - number_of_duplicates - removed = 0 - - # Loop backwards through the list to safely pop items without breaking index order - for i in range(len(existing_objs) - 1, -1, -1): - if removed >= remove_count: - break - - obj_to_remove = existing_objs[i] - existing_objs.pop(i) - bpy.data.objects.remove(obj_to_remove, do_unlink=True) - removed += 1 - - # Add additional obejcts if needed to reach desired number of objects + remove_objects_from_curve(current_count - number_of_duplicates, existing_objs) elif number_of_duplicates > current_count: - - object_id = curve["dup_ObjectID"] - user_data = curve["dup_UserData"] - - linked_curve_obj_col = collection_utils.get_collection(collection_utils.LINKED_CURVE_OBJ_COL) - - for _ in range(number_of_duplicates - current_count): - if len(existing_objs) == 0: - - if bpy_object is None: - caller = inspect.stack()[1] - print("Called by:", caller.function) - print("bulding new object") - new_item = get_builder().add_part(object_id, user_data=user_data) - new_obj = new_item.object - else: - new_obj = bpy_object.copy() - new_obj.data = bpy_object.data.copy() - for constraint in list(new_obj.constraints): - new_obj.constraints.remove(constraint) - - constraint = new_obj.constraints.new(type='FOLLOW_PATH') - constraint.target = curve - constraint.use_fixed_location = True - constraint.use_curve_follow = True - - new_obj["curve_parent"] = curve.name - new_obj["base_scale"] = 1.0 - new_obj.rotation_euler = (math.pi,0,0) - new_obj.location = (0,0,0) - new_obj.hide_select = True - - collection_utils.move_object_into_collection(linked_curve_obj_col, new_obj) - material.restore_material(new_obj, user_data) - - else : - new_obj = existing_objs[-1].copy() - new_obj["curve_parent"] = curve.name - linked_curve_obj_col.objects.link(new_obj) - - existing_objs.append(new_obj) + add_objects_to_curve(number_of_duplicates - current_count, curve, existing_objs, bpy_object) - # calcuate segment_length and total length of curve once - val_data, total_length = curve_utils.build_curve_eval_data(curve, resolution=64) - - # itreate over all objects and update their transformations - for i, obj in enumerate(existing_objs): - percentage_count = i * gap_distance - - constraint = next((c for c in obj.constraints if c.type == 'FOLLOW_PATH' and c.target == curve), None) - if constraint: - constraint.offset_factor = percentage_count - - obj["curve_factor"] = percentage_count - curve_utils.update_obj_transformations(obj, curve, val_data, total_length) - - curve["objects_count"] = len(existing_objs) - + update_curve_children(curve, radius_multiplier, existing_objs) + curve[Curve.PROP_OBJECTS_COUNT] = len(existing_objs) return existing_objs + +def remove_objects_from_curve(number_to_remove, existing_objs): + if number_to_remove <= 0: + return + + # trim the tail, keeping the order of what is left + keep_count = max(0, len(existing_objs) - number_to_remove) + doomed = existing_objs[keep_count:] + del existing_objs[keep_count:] + + # one batch, a remove() per object re-syncs the whole scene each time + if doomed: + bpy.data.batch_remove(doomed) + + +def add_objects_to_curve(number_to_add, curve, existing_objs, bpy_object=None): + linked_curve_obj_col = collection_utils.get_collection(collection_utils.LINKED_CURVE_OBJ_COL) + for _ in range(number_to_add): + if existing_objs: + new_obj = existing_objs[-1].copy() + new_obj[Curve.PROP_CURVE_PARENT] = curve.name + linked_curve_obj_col.objects.link(new_obj) + existing_objs.append(new_obj) + continue + + if bpy_object is not None: + new_obj = bpy_object.copy() + # colour lives on the mesh material, so recolouring the curve must not touch the source + new_obj.data = bpy_object.data.copy() + for constraint in list(new_obj.constraints): + new_obj.constraints.remove(constraint) + elif Curve.PROP_DUP_OBJECT_ID not in curve: + # group curves from other versions can only grow by copying a follower + return + else: + new_item = get_builder().add_part( + curve[Curve.PROP_DUP_OBJECT_ID], user_data=curve[Curve.PROP_DUP_USER_DATA] + ) + new_obj = new_item.object + + if Curve.PROP_DUP_USER_DATA in curve: + material.restore_material(new_obj, curve[Curve.PROP_DUP_USER_DATA]) + + constraint = new_obj.constraints.new(type='FOLLOW_PATH') + constraint.target = curve + constraint.use_fixed_location = True + constraint.use_curve_follow = True + + new_obj[Curve.PROP_CURVE_PARENT] = curve.name + new_obj[Curve.PROP_BASE_SCALE] = 1.0 + new_obj.rotation_euler = (math.pi, 0, 0) + new_obj.location = (0, 0, 0) + new_obj.hide_select = True + + collection_utils.move_object_into_collection(linked_curve_obj_col, new_obj) + existing_objs.append(new_obj) + + # check if given object is a supported curve or not def is_bezier_or_nurbs_path(curve): if not curve or curve.type != 'CURVE': @@ -151,332 +320,279 @@ def is_bezier_or_nurbs_path(curve): return True return False - + +@_marks_busy def apply_curve_transforms_and_detach(curve): - """ - Bakes the visual transforms created by the FOLLOW_PATH constraint into actual - loc/rot/scale data, then deletes the constraint so objects stay in place. - """ + """Bake the FOLLOW_PATH transforms into the followers and free them from the curve.""" if not is_bezier_or_nurbs_path(curve): raise TypeError("Please provide a valid curve object.") - - detached_count = 0 + duplicates = [] - - # Force Blender to evaluate the scene tree. - # If we don't do this, 'obj.matrix_world' might return stale data from before + + # matrix_world is stale until the scene is evaluated bpy.context.view_layer.update() - - + unlinked_curve_obj_col = collection_utils.get_collection(collection_utils.UNLINKED_CURVE_OBJ_COL) - - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == curve.name: - constraints_to_remove = [c for c in obj.constraints if c.type == 'FOLLOW_PATH' and c.target == curve] - - # Clean up custom properties - if "base_scale" in obj: - del obj["base_scale"] - - if "curve_factor" in obj: - del obj["curve_factor"] - - del obj["curve_parent"] - - if constraints_to_remove: - # Capture the exact 3D space matrix dictated by the constraint - baked_matrix = obj.matrix_world.copy() - - for c in constraints_to_remove: - obj.constraints.remove(c) - - # Re-apply the matrix so the object doesn't physically move when the constraint drops - obj.matrix_world = baked_matrix - detached_count += 1 - - obj.data = obj.data.copy() - obj.hide_select = False - obj.lock_location = (False, False, False) - duplicates.append(obj) - - collection_utils.move_object_into_collection(unlinked_curve_obj_col , obj) - - #blend_utils.select(duplicates) - - # Clean up custom curve properties - if "has_linked_objects" in curve: - del curve["has_linked_objects"] - - if "dup_ObjectID" in curve: - del curve["dup_ObjectID"] - del curve["dup_UserData"] - - if "radius_multiplier" in curve: - del curve["radius_multiplier"] - - if "objects_count" in curve: - del curve["objects_count"] - - if "unique_id" in curve: - del curve["unique_id"] - - if "parent_col" in curve: - parent_col = curve["parent_col"] - collection_utils.rename_to_unliked(parent_col) - del curve["parent_col"] - - if "master_col" in curve: - del curve["master_col"] - + + child_props_to_delete = ( + Curve.PROP_BASE_SCALE, + Curve.PROP_CURVE_FACTOR, + Curve.PROP_RADIUS, + Curve.PROP_CURVE_PARENT, + ) + + for obj in get_all_curve_children(curve, require_id=False): + for prop in child_props_to_delete: + obj.pop(prop, None) + + baked_matrix = obj.matrix_world.copy() + for constraint in [c for c in obj.constraints if c.type == 'FOLLOW_PATH' and c.target == curve]: + obj.constraints.remove(constraint) + # re-apply so the object doesn't move when the constraint drops + obj.matrix_world = baked_matrix + + # followers share a mesh, a detached part needs its own to be recoloured alone + obj.data = obj.data.copy() + obj.hide_select = False + obj.lock_location = (False, False, False) + duplicates.append(obj) + + collection_utils.move_object_into_collection(unlinked_curve_obj_col, obj) + + curve_props_to_delete = ( + Curve.PROP_CURVE_ID, + Curve.PROP_HAS_LINKED_OBJECTS, + Curve.PROP_DUP_OBJECT_ID, + Curve.PROP_DUP_USER_DATA, + Curve.PROP_RADIUS_MULTIPLIER, + Curve.PROP_OBJECTS_COUNT, + Curve.PROP_DENSITY_STEP, + ) + Curve.LEGACY_PROPS + + for prop in curve_props_to_delete: + curve.pop(prop, None) + return curve, duplicates -# make all objects linked ao a curve unselectable -def lock_all_objects(curve_obj, lock_location = True): - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == curve_obj.name: - obj.hide_select = True - #if lock_location: - #obj.lock_location = (lock_location, lock_location, lock_location) - + +# make all objects linked to a curve unselectable +def lock_all_objects(curve_obj): + for obj in get_all_curve_children(curve_obj, require_id=False): + obj.hide_select = True + + # make all objects linked to a curve selectable -def unlock_all_objects(curve_obj, lock_location = False): - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == curve_obj.name: - obj.hide_select = False - #obj.lock_location = (lock_location, lock_location, lock_location) - -# select parent curve of object -# make its children unselectable and make only curve selectable -def select_parent_curve(object): - parent_curve_name = object.get("curve_parent", None) +def unlock_all_objects(curve_obj): + for obj in get_all_curve_children(curve_obj, require_id=False): + obj.hide_select = False + + +# make the curve of a follower selectable and its followers not, returns the curve +def select_parent_curve(obj): + parent_curve_name = obj.get(Curve.PROP_CURVE_PARENT, None) if parent_curve_name is None: - return - + return None + parent_curve = bpy.data.objects.get(parent_curve_name) - if parent_curve is not None: + if parent_curve is not None and not parent_curve.get(Curve.PROP_PARENT_SELECTED, False): parent_curve.hide_select = False - parent_curve["parent_selected"] = True - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == parent_curve.name: - obj.hide_select = True - #obj.lock_location = (True, True, True) - #obj["base_scale"] = obj.scale.x/(parent_curve["radius_multiplier"]*obj["radius"]) - blend_utils.select(parent_curve) - -# select all children of curve present -# make all children linked to curve selectable and make curve unselectable + parent_curve[Curve.PROP_PARENT_SELECTED] = True + lock_all_objects(parent_curve) + return parent_curve + + +# make the followers of a curve selectable and the curve not, returns the followers def select_children_of_curve(curve): if not is_bezier_or_nurbs_path(curve): - return - - children = [] - for obj in bpy.context.scene.objects: - if obj.get("curve_parent") == curve.name: - obj.hide_select = False - children.append(obj) - + return None + + children = get_all_curve_children(curve, require_id=False) + for obj in children: + obj.hide_select = False + curve_utils.normalise_curve_scale(curve) - #curve["initial_curve_scale"] = 1 - curve.hide_select = True - curve["parent_selected"] = False - blend_utils.select(children) - -# return objects if object is a curve and has lihked objects -# or object is not curve, check if it is part of a curve, and returh curve linked to it + curve[Curve.PROP_PARENT_SELECTED] = False + return children + + +def get_all_curve_children(curve_obj, require_id=True): + if curve_obj is None: + return None + if require_id and Curve.PROP_CURVE_ID not in curve_obj: + return None + + return [ + obj for obj in get_follower_objects() + if obj.get(Curve.PROP_CURVE_PARENT) == curve_obj.name + ] + + +# the curve itself, or the curve an object follows def get_curve_or_linked_curve(obj): if obj is None: return None - - if is_bezier_or_nurbs_path(obj) and obj.get("has_linked_objects",False): + + if is_bezier_or_nurbs_path(obj) and obj.get(Curve.PROP_HAS_LINKED_OBJECTS, False): return obj - elif "curve_parent" in obj: - return bpy.data.objects.get(obj["curve_parent"]) + if Curve.PROP_CURVE_PARENT in obj: + return bpy.data.objects.get(obj[Curve.PROP_CURVE_PARENT]) return None + # delete selected curve and children linked to it def delete_curve_and_children(curve): if curve is None: raise TypeError("Selected object is None") - + if not is_bezier_or_nurbs_path(curve): raise TypeError("Object is not a curve") - - if not curve["has_linked_objects"]: - return TypeError("Object has no linked children") - - deleted_count = 0 - # Delete all linked objects first - for obj in list(bpy.data.objects): - if obj.get("curve_parent") == curve.name: - bpy.data.objects.remove(obj, do_unlink=True) - deleted_count += 1 - - # delete colelction containing children and curve - collection_utils.delete_collection(curve["parent_col"]) - - # try deleting the curve if it is outside collection + + if not curve.get(Curve.PROP_HAS_LINKED_OBJECTS, False): + raise TypeError("Object has no linked children") + + doomed = [ + obj for obj in bpy.data.objects + if obj.get(Curve.PROP_CURVE_PARENT) == curve.name + ] + if doomed: + bpy.data.batch_remove(doomed) + try: bpy.data.objects.remove(curve, do_unlink=True) - except Exception as e: + except ReferenceError: pass - return deleted_count + return len(doomed) -# replace curve objects on curve with source object provided + +# replace the objects on a curve with source_obj def replace_curve_object(curve_obj, source_obj): - new_curve_obj = curve_obj.copy() new_curve_obj.data = curve_obj.data.copy() - new_curve_obj["unique_id"] = str(uuid.uuid4()) - + new_curve_obj[Curve.PROP_CURVE_ID] = str(uuid.uuid4()) + for collection in curve_obj.users_collection: collection.objects.link(new_curve_obj) - - new_curve_obj["dup_ObjectID"] = source_obj["ObjectID"] - new_curve_obj["dup_UserData"] = source_obj["UserData"] - - sync_curves(new_curve_obj , curve_obj, duping_object_source= source_obj) - + + new_curve_obj[Curve.PROP_DUP_OBJECT_ID] = source_obj[Part.PROP_OBJECT_ID] + new_curve_obj[Curve.PROP_DUP_USER_DATA] = source_obj[Part.PROP_USER_DATA] + + sync_curves(new_curve_obj, curve_obj, duping_object_source=source_obj) + return new_curve_obj, curve_obj -# reset a curve to its default stage and delete all clildren on it + +# reset a curve to its default state and delete all children on it def reset_curve(curve): if curve is None: raise TypeError("curve is None") - - if not is_bezier_or_nurbs_path(curve) or "has_linked_objects" not in curve: + + if not is_bezier_or_nurbs_path(curve) or Curve.PROP_HAS_LINKED_OBJECTS not in curve: raise TypeError("object is not a valid curve") - - curve_obj, duplciates = apply_curve_transforms_and_detach(curve) - if duplciates is not None: - for obj in duplciates: - bpy.data.objects.remove(obj, do_unlink=True) + + curve_obj, duplicates = apply_curve_transforms_and_detach(curve) + if duplicates: + bpy.data.batch_remove(duplicates) return curve_obj -# mirror a curve and objects dupicated along it -def mirror_curve(curve_obj, axis = "Z", center = None, auto_duplicate = False): + +def _copy_curve(curve_obj): + new_curve_obj = curve_obj.copy() + new_curve_obj.data = curve_obj.data.copy() + new_curve_obj[Curve.PROP_CURVE_ID] = str(uuid.uuid4()) + parent_collection = collection_utils.get_parent_collection(curve_obj) or bpy.context.scene.collection + parent_collection.objects.link(new_curve_obj) + return new_curve_obj + + +def duplicate_curve(curve_obj): + if curve_obj is None: + return None + + new_curve_obj = _copy_curve(curve_obj) + sync_curves(new_curve_obj, curve_obj) + return new_curve_obj + + +# mirror a curve and the objects duplicated along it +def mirror_curve(curve_obj, axis="Z", center=None, auto_duplicate=False): if not is_bezier_or_nurbs_path(curve_obj): return None - - # Duplicate the object and its data block so they don't share identical vertices - if auto_duplicate: - new_curve_obj = curve_obj.copy() - new_curve_obj.data = curve_obj.data.copy() - new_curve_obj["unique_id"] = str(uuid.uuid4()) - if "parent_col" in curve_obj and curve_obj.get("parent_col") is not None: - collection = curve_obj["parent_col"] - collection.objects.link(new_curve_obj) - else: - bpy.context.collection.objects.link(new_curve_obj) - else: - new_curve_obj = curve_obj - - - #Apply the mathematical mirror to curve objects - curve_utils.mirror_curve_data(new_curve_obj, axis) - - # Mirror positin of curve according to center of reflection - location = new_curve_obj.location - if axis == "X": - new_curve_obj.location.x = mirror_utils.reflect_point_across(location.x, center.x) - new_curve_obj.rotation_euler.y *= -1 - new_curve_obj.rotation_euler.z *= -1 - elif axis == "Y": - new_curve_obj.location.y = mirror_utils.reflect_point_across(location.y, center.y) - new_curve_obj.rotation_euler.x *= -1 - new_curve_obj.rotation_euler.z *= -1 - elif axis == "Z": - new_curve_obj.location.z = mirror_utils.reflect_point_across(location.z, center.z) - new_curve_obj.rotation_euler.x *= -1 - new_curve_obj.rotation_euler.y *= -1 - - # update objectID of mirror part of that object exist - obj_id = new_curve_obj["dup_ObjectID"] - mirror_obj_id = part.Part.get_mirror_part_id(obj_id) - mirror_part_exist = mirror_obj_id in nice_name_dictionary.keys() - if mirror_part_exist: - new_curve_obj["dup_ObjectID"] = mirror_obj_id - - # syncing curves will make objects duplicating on them have identical transformations - sync_curves(new_curve_obj, curve_obj, True, axis, from_mirror= True) + + new_curve_obj = _copy_curve(curve_obj) if auto_duplicate else curve_obj + + curve_utils.mirror_curve(new_curve_obj, axis, center) + + # switch to the mirror part when one exists + object_id = new_curve_obj.get(Curve.PROP_DUP_OBJECT_ID) + mirror_obj_id = Part.get_mirror_part_id(object_id) if object_id else None + if mirror_obj_id in get_builder().nice_name_dictionary: + new_curve_obj[Curve.PROP_DUP_OBJECT_ID] = mirror_obj_id + + sync_curves(new_curve_obj, curve_obj, True, axis, from_mirror=True) return new_curve_obj -# this function takes two curves as argument -# iterage over objects of source curve and copy transformations of those objects to their alternatives in target curve -# this in a way creates eact copy of these curves no matter how much obects have been manupulated by user -# do_mirror: this argument dictates if child objects need to be mirrored -# asix: this dictates which direction objects need to be mirrored -# last two objects dictate which function is calling them, -def sync_curves(target_curve, source_curve, do_mirror = False, axis = None, from_mirror = False, duping_object_source = None): - - target_curve["unique_id"] = str(uuid.uuid4()) - - # all child objects of source curve - source_dupe_objects = [obj for obj in bpy.context.scene.objects if obj.get("curve_parent") == source_curve.name] - - radius_multiplier = target_curve["radius_multiplier"] - number_of_objects = target_curve["objects_count"] - +@_marks_busy +def sync_curves(target_curve, source_curve, do_mirror=False, axis=None, from_mirror=False, duping_object_source=None): + """Rebuild target_curve's followers to match source_curve's, optionally mirrored. + + Followers are matched by index, so per object edits made by the user carry over. + """ + target_curve[Curve.PROP_CURVE_ID] = str(uuid.uuid4()) + + source_dupe_objects = get_all_curve_children(source_curve, require_id=False) + + radius_multiplier = target_curve[Curve.PROP_RADIUS_MULTIPLIER] + number_of_objects = target_curve[Curve.PROP_OBJECTS_COUNT] + if duping_object_source is not None: - duping_obejct = duping_object_source - elif len(source_dupe_objects) > 0 and not from_mirror: - duping_obejct = source_dupe_objects[0] + duping_object = duping_object_source + elif source_dupe_objects and not from_mirror: + duping_object = source_dupe_objects[0] else: - duping_obejct = None - # all child objects of parent curve - target_dupe_obejcts = duplicate_along_curve(duping_obejct, target_curve, number_of_objects, radius_multiplier) - - if len(target_dupe_obejcts)>0: - target = target_dupe_obejcts[0] - material.restore_material(target, target["UserData"]) - - - # these curves are almost identical - # blender stores bpy.context.scene.objects in sorted order, so duplicated objects will alsmo match that order. - # objects on same index will have save transformations - for index,source in enumerate(source_dupe_objects): - if index >= len(target_dupe_obejcts): - break - - target = target_dupe_obejcts[index] + duping_object = None + + target_dupe_objects = duplicate_along_curve( + duping_object, target_curve, number_of_objects, radius_multiplier + ) + + if target_dupe_objects: + target = target_dupe_objects[0] + material.restore_material(target, target[Part.PROP_USER_DATA]) + + if Curve.PROP_DUP_USER_DATA in source_curve: + apply_color(target_curve, source_curve[Curve.PROP_DUP_USER_DATA]) + + for source, target in zip(source_dupe_objects, target_dupe_objects): target.rotation_euler = source.rotation_euler.copy() target.scale = source.scale.copy() target.location = source.location.copy() - target["base_scale"] = source["base_scale"] - + target[Curve.PROP_BASE_SCALE] = source.get(Curve.PROP_BASE_SCALE, 1.0) + if do_mirror: target.location.x = -target.location.x target.rotation_euler.y = -target.rotation_euler.y target.rotation_euler.z = -target.rotation_euler.z - - if axis is not None and axis == "Z": - target.rotation_euler.x += math.pi - target.rotation_euler.z += math.pi - - - - # Refresh evaluation data for the newly synced children - update_curve_children(target_curve, radius_multiplier) - -# scale of a child object of curve is mix of multiple products -# base scale is scale of object before getting influenced by curve's points + + +# scale of a follower before the curve's radius and scale are applied def calculate_base_scale(curve, obj): - curve_scale_multiplier = curve.scale.x/curve["initial_curve_scale"] - radius_multiplier = curve["radius_multiplier"] - point_radius = obj["radius"] - return obj.scale.x/( point_radius* radius_multiplier * curve_scale_multiplier ) + curve_scale_multiplier = curve.scale.x / curve.get(Curve.PROP_INITIAL_CURVE_SCALE, curve.scale.x) + radius_multiplier = curve.get(Curve.PROP_RADIUS_MULTIPLIER, 1.0) + point_radius = obj.get(Curve.PROP_RADIUS, 1.0) + return obj.scale.x / (point_radius * radius_multiplier * curve_scale_multiplier) + # change color of objects on curve def apply_color(curve_obj, user_data): - if curve_obj is None: return None - - if "has_linked_objects" in curve_obj and is_bezier_or_nurbs_path(curve_obj): - curve_obj["dup_UserData"] = user_data - for child_obj in bpy.context.scene.objects: - if "curve_parent" in child_obj and child_obj["curve_parent"] == curve_obj.name: - material.restore_material(child_obj, user_data) - break + + if Curve.PROP_HAS_LINKED_OBJECTS in curve_obj and is_bezier_or_nurbs_path(curve_obj): + curve_obj[Curve.PROP_DUP_USER_DATA] = user_data + # followers share one mesh, so painting the first colours them all + for child_obj in get_all_curve_children(curve_obj, require_id=False): + material.restore_material(child_obj, user_data) + break diff --git a/src/addons/no_mans_sky_base_builder/utils/curve_utils.py b/src/addons/no_mans_sky_base_builder/utils/curve_utils.py index 4bd60cbc..082f855d 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve_utils.py @@ -1,199 +1,239 @@ +import math + +from mathutils import Vector from mathutils.geometry import interpolate_bezier +from . import mirror_utils + +# Blender stores transforms as 32 bit floats, anything below this counts as unchanged +WRITE_EPSILON = 1e-6 + +# density integrals keyed by control point weights, so a changed weight never hits a stale entry +_density_cache = {} +_DENSITY_CACHE_LIMIT = 32 + + def build_curve_eval_data(curve_obj, resolution=16): - """ - Creates a high-resolution map of the curve. - Instead of jumping from Control Point to Control Point, we sample - the radius and tilt at dozens of micro-steps to perfectly match the curve's true shape. + """Sample radius and tilt along the curve. + + Returns: + tuple: ([(accumulated length, radius, tilt)], total length) """ spline = curve_obj.data.splines[0] points = spline.bezier_points if spline.bezier_points else spline.points count = len(points) - - # Return structure: list of tuples (accumulated_length, radius, tilt) + if count < 2: return [(0.0, points[0].radius if count else 1.0, points[0].tilt if count else 0.0)], 0.0 - eval_data = [] + eval_data = [(0.0, points[0].radius, points[0].tilt)] accumulated_length = 0.0 - - is_cyclic = spline.use_cyclic_u - segment_count = count if is_cyclic else count - 1 - - # Add starting point - eval_data.append((0.0, points[0].radius, points[0].tilt)) - - for i in range(segment_count): - p0 = points[i] - p1 = points[(i + 1) % count] - - if spline.type == 'BEZIER': - # Get high-res 3D points to capture the true geometric bend + segment_count = count if spline.use_cyclic_u else count - 1 + + if spline.type == 'BEZIER': + for i in range(segment_count): + p0 = points[i] + p1 = points[(i + 1) % count] segment_pts = interpolate_bezier( p0.co, p0.handle_right, p1.handle_left, p1.co, resolution + 1 ) - - # Step through each micro-segment + + rad0, tilt0 = p0.radius, p0.tilt + rad_delta = p1.radius - rad0 + tilt_delta = p1.tilt - tilt0 + for j in range(resolution): - # Calculate the tiny distance of this specific step - dist = (segment_pts[j+1] - segment_pts[j]).length - accumulated_length += dist - - # The parametric 't' (0.0 to 1.0) along the BEZIER segment + accumulated_length += (segment_pts[j + 1] - segment_pts[j]).length t = (j + 1) / resolution - - # Interpolate radius and tilt smoothly at this exact micro-step - rad = (1.0 - t) * p0.radius + t * p1.radius - tilt = (1.0 - t) * p0.tilt + t * p1.tilt - - eval_data.append((accumulated_length, rad, tilt)) - - else: - # Poly or NURBS fallback + eval_data.append( + (accumulated_length, rad0 + t * rad_delta, tilt0 + t * tilt_delta) + ) + else: + for i in range(segment_count): + p0 = points[i] + p1 = points[(i + 1) % count] v0 = p0.co.xyz if len(p0.co) == 4 else p0.co v1 = p1.co.xyz if len(p1.co) == 4 else p1.co - dist = (v0 - v1).length - accumulated_length += dist - - # For linear curves, just append the end of the segment + accumulated_length += (v1 - v0).length eval_data.append((accumulated_length, p1.radius, p1.tilt)) return eval_data, accumulated_length + def get_exact_radius_tilt(eval_data, total_length, factor): - """ - Searches the high-resolution map to find the exact radius and tilt - based on the physical arc-length factor (0.0 to 1.0). - """ - if not eval_data: + """Radius and tilt at a curve factor, smoothed with cubic hermite interpolation.""" + data_len = len(eval_data) + + if data_len == 0: return 1.0, 0.0 - if len(eval_data) == 1 or total_length == 0.0: + if data_len == 1 or total_length == 0.0: return eval_data[0][1], eval_data[0][2] - + target_length = factor * total_length - - # Clamp bounds if target_length <= 0.0: return eval_data[0][1], eval_data[0][2] if target_length >= total_length: return eval_data[-1][1], eval_data[-1][2] - - # Find the two micro-segments our target falls exactly between - for i in range(len(eval_data) - 1): - dist_a, rad_a, tilt_a = eval_data[i] - dist_b, rad_b, tilt_b = eval_data[i+1] - - if dist_a <= target_length <= dist_b: - segment_len = dist_b - dist_a - if segment_len == 0: - return rad_a, tilt_a - - # Perform a tiny, highly accurate linear interpolation - # between the two micro-points - t = (target_length - dist_a) / segment_len - - final_rad = (1.0 - t) * rad_a + t * rad_b - final_tilt = (1.0 - t) * tilt_a + t * tilt_b - return final_rad, final_tilt - - return eval_data[-1][1], eval_data[-1][2] - - -def update_obj_transformations(obj, curve_obj, eval_data, total_length): + + # binary search for the sample pair around target_length + left, right = 0, data_len - 1 + while left < right - 1: + mid = (left + right) // 2 + if eval_data[mid][0] <= target_length: + left = mid + else: + right = mid + + dist_a, rad_a, tilt_a = eval_data[left] + dist_b, rad_b, tilt_b = eval_data[right] + segment_len = dist_b - dist_a + if segment_len == 0: + return rad_a, tilt_a + + t = (target_length - dist_a) / segment_len + dist_prev, rad_prev, tilt_prev = eval_data[max(0, left - 1)] + dist_next, rad_next, tilt_next = eval_data[min(data_len - 1, right + 1)] + + def hermite_interp(y_prev, y_a, y_b, y_next): + dx_10 = dist_b - dist_prev + dx_31 = dist_next - dist_a + m1 = (y_b - y_prev) * (segment_len / dx_10) if dx_10 > 0 else 0.0 + m2 = (y_next - y_a) * (segment_len / dx_31) if dx_31 > 0 else 0.0 + + t2 = t * t + t3 = t2 * t + h00 = 2 * t3 - 3 * t2 + 1 + h10 = t3 - 2 * t2 + t + h01 = -2 * t3 + 3 * t2 + h11 = t3 - t2 + return h00 * y_a + h10 * m1 + h01 * y_b + h11 * m2 + + radius = hermite_interp(rad_prev, rad_a, rad_b, rad_next) + tilt = hermite_interp(tilt_prev, tilt_a, tilt_b, tilt_next) + return radius, tilt + + +def build_curve_context(curve_obj): + """Curve values shared by every child, read once per update. + + Returns: + tuple: (parent_selected, curve scale multiplier, radius multiplier) + """ + return ( + curve_obj.get("parent_selected", True), + curve_obj.scale.x / curve_obj.get("initial_curve_scale", 1.0), + curve_obj.get("radius_multiplier", 1.0), + ) + + +def update_obj_transformations(obj, curve_obj, eval_data, total_length, curve_context=None): factor = obj.get("curve_factor") - + update_object_factor(obj, curve_obj, factor) + if factor is None: return - - curve_scale_multiplier = curve_obj.scale.x/curve_obj["initial_curve_scale"] - radius_multiplier = curve_obj.get("radius_multiplier", 1.0) - - # Get accurate radius and tilt from our new high-res map + + if curve_context is None: + curve_context = build_curve_context(curve_obj) + parent_selected, curve_scale_multiplier, radius_multiplier = curve_context + + if not parent_selected: + return + radius, tilt = get_exact_radius_tilt(eval_data, total_length, factor) - - if curve_obj.get("parent_selected",True): - base_scale = obj["base_scale"] + if obj.get("radius") != radius: obj["radius"] = radius - - scale = radius * radius_multiplier * base_scale * curve_scale_multiplier - - # Clamp scale to a microscopic value above zero. - # This prevents the matrix inversion that snaps objects to the world origin. - if scale < 0.00001: - scale = 0.00001 - - obj.scale.x = scale - obj.scale.y = scale - obj.scale.z = scale - - -def mirror_curve_data(curve_obj, axis='X'): - """ - Mirrors a curve's points, handles, and tilt - along the specified local axis ('X', 'Y', or 'Z'). - """ + + scale = radius * radius_multiplier * obj.get("base_scale", 1.0) * curve_scale_multiplier + + # a zero scale breaks the matrix inversion and snaps objects to the origin + if scale < 0.00001: + scale = 0.00001 + + # writing a transform re-evaluates the object even when unchanged, so only write real changes + tolerance = WRITE_EPSILON * max(1.0, abs(scale)) + current_scale = obj.scale + if (abs(current_scale.x - scale) > tolerance + or abs(current_scale.y - scale) > tolerance + or abs(current_scale.z - scale) > tolerance): + obj.scale = (scale, scale, scale) + + +def update_object_factor(obj, curve_obj, factor, constraint=None): + if factor is None: + return + if constraint is None: + constraint = next( + (c for c in obj.constraints if c.type == 'FOLLOW_PATH' and c.target == curve_obj), + None, + ) + # setting offset_factor re-evaluates the constraint even when the value is the same + if constraint and abs(constraint.offset_factor - factor) > WRITE_EPSILON: + constraint.offset_factor = factor + if obj.get("curve_factor") != factor: + obj["curve_factor"] = factor + + +def mirror_curve(curve_obj, axis='X', center=Vector((0, 0, 0))): + """Mirror a curve's points, handles and tilt, then its location around center.""" if not curve_obj or curve_obj.type != 'CURVE': raise TypeError("Please provide a valid curve object.") - - # Normalize the axis input and map it to a vector index (X:0, Y:1, Z:2) + axis = axis.upper() if axis not in {'X', 'Y', 'Z'}: raise ValueError("Axis parameter must be 'X', 'Y', or 'Z'.") - + axis_idx = {'X': 0, 'Y': 1, 'Z': 2}[axis] - - # Operate on the curve's datablock - curve_data = curve_obj.data - - for spline in curve_data.splines: + + for spline in curve_obj.data.splines: if spline.type == 'BEZIER': - # Store original handle types and force them to FREE - stored_types = [] - for bp in spline.bezier_points: - stored_types.append({ - 'left': bp.handle_left_type, - 'right': bp.handle_right_type - }) + points = spline.bezier_points + # handles must be FREE while mirrored, then blender rebuilds them symmetrically + stored_types = [(bp.handle_left_type, bp.handle_right_type) for bp in points] + for bp in points: bp.handle_left_type = 'FREE' bp.handle_right_type = 'FREE' - - # Mirror the coordinates, handles, and tilt safely - for bp in spline.bezier_points: + + for bp in points: bp.co[axis_idx] *= -1.0 bp.handle_left[axis_idx] *= -1.0 bp.handle_right[axis_idx] *= -1.0 bp.tilt *= -1.0 - - # Restore the original handle types - # will allow Blender to rebuild them with perfect symmetry. - for bp, orig_type in zip(spline.bezier_points, stored_types): - bp.handle_left_type = orig_type['left'] - bp.handle_right_type = orig_type['right'] - - elif spline.type in {'NURBS', 'POLY'}: + + for bp, (left_type, right_type) in zip(points, stored_types): + bp.handle_left_type = left_type + bp.handle_right_type = right_type + else: for pt in spline.points: - # NURBS/POLY points are stored as 4D vectors (x, y, z, w) pt.co[axis_idx] *= -1.0 pt.tilt *= -1.0 - + + curve_obj.location = mirror_utils.reflect_point(curve_obj.location, center, axis) + if axis == "X": + curve_obj.rotation_euler.y *= -1 + curve_obj.rotation_euler.z *= -1 + elif axis == "Y": + curve_obj.rotation_euler.x *= -1 + curve_obj.rotation_euler.z *= -1 + elif axis == "Z": + curve_obj.rotation_euler.x *= -1 + curve_obj.rotation_euler.y *= -1 + def normalise_curve_scale(curve_obj): - """This prevents explosion of position transformations by resetting object scale to 1 without applying""" + """Bake object scale into the curve points, so children positions don't explode.""" if curve_obj.type != 'CURVE': print(f"'{curve_obj.name}' is not a curve object.") return - - # Optimization: Skip if already normalized - if curve_obj.scale[:] == (1.0, 1.0, 1.0): + + current_scale = curve_obj.scale[:] + if current_scale == (1.0, 1.0, 1.0): return - - # Get current scale - scale_x, scale_y, scale_z = curve_obj.scale - - # Scale all curve data points + + scale_x, scale_y, scale_z = current_scale + scale_vector = Vector((scale_x, scale_y, scale_z)) + for spline in curve_obj.data.splines: - - # Handle Bezier curves if spline.type == 'BEZIER': for point in spline.bezier_points: left_type = point.handle_left_type @@ -201,35 +241,139 @@ def normalise_curve_scale(curve_obj): point.handle_left_type = 'FREE' point.handle_right_type = 'FREE' - # Scale the main control point - point.co.x *= scale_x - point.co.y *= scale_y - point.co.z *= scale_z - - # Scale the left handle - point.handle_left.x *= scale_x - point.handle_left.y *= scale_y - point.handle_left.z *= scale_z - - # Scale the right handle - point.handle_right.x *= scale_x - point.handle_right.y *= scale_y - point.handle_right.z *= scale_z + point.co *= scale_vector + point.handle_left *= scale_vector + point.handle_right *= scale_vector point.handle_left_type = left_type point.handle_right_type = right_type - - # Handle NURBS and Poly curves else: + # NURBS points are 4D, the weight (w) stays untouched for point in spline.points: - # Note: NURBS points are 4D (x, y, z, w). - # Scaling x, y, and z is correct; the weight (w) remains untouched. point.co.x *= scale_x point.co.y *= scale_y point.co.z *= scale_z - - # Reset object scale to 1 + curve_obj.scale = (1, 1, 1) if "initial_curve_scale" in curve_obj and scale_x != 0: curve_obj["initial_curve_scale"] = curve_obj["initial_curve_scale"] / scale_x + + +def get_control_points(curve_obj): + if not curve_obj or curve_obj.type != 'CURVE': + return None + + spline = curve_obj.data.splines[0] + if spline.type == 'BEZIER': + return spline.bezier_points + if spline.type in {'POLY', 'NURBS'}: + return spline.points + return None + + +# Density --- +# each control point's softbody weight sets how tightly objects pack around it + +def smoothstep(t): + return t * t * (3.0 - 2.0 * t) + + +def exponential_scale(x, steepness=5.0): + return math.exp(steepness * (x - 0.5)) + + +def get_density_map(curve): + return [exponential_scale(point.weight_softbody) for point in get_control_points(curve)] + + +def half_the_weight_points(curve): + # 0.5 is the neutral weight, it gives even spacing + for point in get_control_points(curve): + point.weight_softbody = 0.5 + + +def get_density(density_map, factor): + if len(density_map) == 1: + return density_map[0] + position = factor * (len(density_map) - 1) + index = min(int(position), len(density_map) - 2) + t = smoothstep(position - index) + a = density_map[index] + b = density_map[index + 1] + return a + (b - a) * t + + +def factor_from_density(cumulative_density, sample_factors, target): + low = 0 + high = len(cumulative_density) - 1 + while low < high: + mid = (low + high) // 2 + if cumulative_density[mid] < target: + low = mid + 1 + else: + high = mid + + index = max(1, low) + d0 = cumulative_density[index - 1] + d1 = cumulative_density[index] + f0 = sample_factors[index - 1] + f1 = sample_factors[index] + + if d1 == d0: + return f0 + return f0 + (f1 - f0) * (target - d0) / (d1 - d0) + + +def build_density_samples(curve, sample_count): + """Sample the weight curve and integrate it, cached by weights and sample count. + + Returns: + tuple: (sample factors, cumulative density, total density), shared so read only + """ + density_map = tuple(get_density_map(curve)) + cache_key = (sample_count, density_map) + cached = _density_cache.get(cache_key) + if cached is not None: + return cached + + sample_factors = [i / (sample_count - 1) for i in range(sample_count)] + + cumulative_density = [0.0] + total_density = 0.0 + previous_density = get_density(density_map, sample_factors[0]) + for i in range(1, sample_count): + current_density = get_density(density_map, sample_factors[i]) + dx = sample_factors[i] - sample_factors[i - 1] + total_density += (previous_density + current_density) * 0.5 * dx + cumulative_density.append(total_density) + previous_density = current_density + + if len(_density_cache) >= _DENSITY_CACHE_LIMIT: + _density_cache.clear() + samples = (sample_factors, cumulative_density, total_density) + _density_cache[cache_key] = samples + return samples + + +def calculate_curve_factors(curve, existing_objs): + object_count = len(existing_objs) + if object_count == 0: + return + + if object_count == 1: + existing_objs[0]["curve_factor"] = 0.0 + return + + sample_factors, cumulative_density, total_density = build_density_samples( + curve, max(128, object_count * 16) + ) + for index, obj in enumerate(existing_objs): + target_density = index / (object_count - 1) * total_density + factor = factor_from_density(cumulative_density, sample_factors, target_density) + if obj.get("curve_factor") != factor: + obj["curve_factor"] = factor + + +def get_total_curve_density(curve, object_count=10): + return build_density_samples(curve, max(128, object_count * 16))[2] diff --git a/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py b/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py index 76c094cd..7aabe784 100644 --- a/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py @@ -62,6 +62,16 @@ def mirror_matrix_world(object_id, old_matrix_world, across_x=True): def reflect_point_across(source,origin): return (2 * origin) - source +# reflect a location across origin along one axis ("X", "Y" or "Z") +def reflect_point(source, origin, axis): + if origin is None: + return source + + x = reflect_point_across(source.x, origin.x) if axis == "X" else source.x + y = reflect_point_across(source.y, origin.y) if axis == "Y" else source.y + z = reflect_point_across(source.z, origin.z) if axis == "Z" else source.z + return Vector((x, y, z)) + # This function mirrors matrix world according to parameters passed # Axis has three possible string values : X, Y and Z # Center is a point across which mirroring will take place, it is a 3d Vector From 1794c9ddbecc2644d3b1510e8085a948b6c301c6 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:08:34 +0530 Subject: [PATCH 11/15] Added feature to group and ungroup objects non destructively. --- .../no_mans_sky_base_builder/__init__.py | 92 +++- .../builder/builder.py | 16 +- src/addons/no_mans_sky_base_builder/group.py | 420 ++++++++++++++++++ src/addons/no_mans_sky_base_builder/part.py | 10 +- .../tools/batch_tool.py | 31 +- .../tools/build_tool.py | 24 +- .../tools/build_tool_presentation.py | 30 +- .../utils/blend_utils.py | 172 ++++++- .../no_mans_sky_base_builder/utils/curve.py | 124 +++++- 9 files changed, 851 insertions(+), 68 deletions(-) create mode 100644 src/addons/no_mans_sky_base_builder/group.py diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 9ca6d352..e033a4ae 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -18,6 +18,7 @@ from . import icons, part, preset from .builder import get_builder +from .group import Group from .part_overrides import line from .save_editor import save_editor_operators, save_editor_utils from .save_editor.save_editor_presentation import NMS_PT_save_editor_panel @@ -556,7 +557,8 @@ def new_file(self): light_check = "NMS_LIGHT" in bpy_object rig_check = "rig_item" in bpy_object curve_check = curve.Curve.PROP_CURVE_ID in bpy_object - if any([id_check, preset_check, light_check, rig_check, curve_check]): + group_check = Group.PROP_GROUP_ID in bpy_object + if any([id_check, preset_check, light_check, rig_check, curve_check, group_check]): blend_utils.remove_object(bpy_object.name) # Reset room vis @@ -637,6 +639,8 @@ def apply_colour(self, colour_index=0, material=0): _material.assign_material(child_obj, int(colour_index), int(maeterial_index)) obj["dup_UserData"] = child_obj["UserData"] break + elif Group.PROP_GROUP_ID in obj: + Group.apply_colour(obj, int(colour_index), int(maeterial_index)) # for any other object else : _material.assign_material(obj, int(colour_index), int(maeterial_index)) @@ -656,8 +660,13 @@ def apply_default_colour(self): # Apply Colour Material. for obj in selected_objects: index = 0 + if Group.PROP_GROUP_ID in obj: + default_material = _material.assign_default_material(obj, index=index) + for slot_index in range(len(obj.data.materials)): + obj.data.materials[slot_index] = default_material + continue # Figure out default index. - object_id = obj["ObjectID"] + object_id = obj.get("ObjectID") if object_id: parent_folder = get_builder().get_obj_parent_folder(object_id) if parent_folder: @@ -1831,6 +1840,81 @@ def execute(self, context): return {"FINISHED"} +class GroupObjects(bpy.types.Operator): + """Merge the selected parts into one group""" + + bl_idname = "object.nms_group_objects" + bl_label = "Group Objects together" + bl_options = {"REGISTER", "UNDO"} + + origin_type: EnumProperty( + name="Select Origin", + description="Choose the origin of the group", + items=[ + ("median", "Median", "Use median of objects in viewport as origin of Group", "PIVOT_MEDIAN", 0), + ("active", "Active Object", "Use active object in viewport as origin of Group", "CON_PIVOT", 1), + ("cursor", "3D Cursor", "Use 3D cursor in viewport as origin of Group", "CURSOR", 2), + ], + default="active", + ) + + def invoke(self, context, event): + if len(context.selected_objects) < 2: + ShowMessageBox("Please select at least two objects", title="Group Objects", icon="ERROR") + return {"CANCELLED"} + return context.window_manager.invoke_props_dialog(self, title="Group Objects", confirm_text="Group") + + def execute(self, context): + selected_objects = context.selected_objects + if len(selected_objects) < 2: + return {"CANCELLED"} + + if self.origin_type == "active" and context.active_object: + target_matrix = context.active_object.matrix_world.copy() + elif self.origin_type == "cursor": + target_matrix = context.scene.cursor.matrix.copy() + else: + target_matrix = None + + if any(Group.PROP_GROUP_ID in obj for obj in selected_objects): + self.report({"ERROR"}, "A group can't be grouped again, ungroup it first") + return {"CANCELLED"} + + part_count = sum(1 for obj in selected_objects if "ObjectID" in obj) + merged_object = Group.group_objects(selected_objects, target_matrix) + if merged_object is None: + self.report({"ERROR"}, "Nothing to group, select at least one part") + return {"CANCELLED"} + + self.report({"INFO"}, f"{part_count} parts merged into {merged_object.name}") + return {"FINISHED"} + + def draw(self, context): + layout = self.layout + layout.label(text="Select Origin of Merged Object", icon="OUTLINER_OB_POINTCLOUD") + layout.row(align=True).prop(self, "origin_type", expand=True) + layout.label(text="This point will act as transformations pivot point") + + +class UngroupObjects(bpy.types.Operator): + """Split the selected groups back into parts""" + + bl_idname = "object.nms_ungroup_objects" + bl_label = "Ungroup Objects" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + restored_objects = [] + for item in list(context.selected_objects): + if Group.PROP_GROUP_ID in item: + restored_objects += Group.ungroup_objects(get_builder(), item) or [] + + if restored_objects: + self.report({"INFO"}, f"Ungrouped into {len(restored_objects)} parts") + blend_utils.select(restored_objects) + return {"FINISHED"} + + class SplitPreset(bpy.types.Operator): """Split the selected prefab into individual parts""" @@ -2051,7 +2135,9 @@ class NMSAddonPreferences(bpy.types.AddonPreferences): NMSAddonPreferences, SwitchWorkspace, - SplitPreset + SplitPreset, + GroupObjects, + UngroupObjects, ) classes = classes + save_editor_operators.classes + build_tool_operators.classes + batch_tool_operators.classes diff --git a/src/addons/no_mans_sky_base_builder/builder/builder.py b/src/addons/no_mans_sky_base_builder/builder/builder.py index 14723fec..8bdf33e4 100644 --- a/src/addons/no_mans_sky_base_builder/builder/builder.py +++ b/src/addons/no_mans_sky_base_builder/builder/builder.py @@ -8,7 +8,7 @@ import bpy from mathutils import Matrix -from .. import part, preset +from .. import group, part, preset from ..utils import blend_utils from . import importer, overrides, paths from .catalog import Catalog @@ -121,6 +121,10 @@ def get_all_presets(self): """Get all Builder preset items in the scene.""" return [item for item in bpy.context.scene.objects if "PresetID" in item] + def get_all_groups(self): + """Get all groups in the scene.""" + return [item for item in bpy.context.scene.objects if group.Group.PROP_GROUP_ID in item] + @staticmethod def by_order(bpy_object): """Sorting method to get objects by the order attribute. @@ -140,7 +144,7 @@ def add_part(self, object_id, user_data=None, build_rigs=True): use_class = self.get_part_class(object_id) # captured before the part exists so it lands on the previous selection - active_object = bpy.context.active_object + active_object = blend_utils.get_selected_active_object() item = use_class( object_id=object_id, builder_object=self, @@ -217,12 +221,13 @@ def optimise_control_points(self): blend_utils.remove_object(control.name) # Serialising --- - def serialise(self, get_presets=False, add_timestamp=False, as_prefab=False): + def serialise(self, get_presets=False, add_timestamp=False, as_prefab=False, include_groups=True): """Return NMS compatible dictionary. Args: get_presets (bool): This will generate data for presets. And exclude parts generated from presets. + include_groups (bool): Save the parts inside groups. Returns: dict: Dictionary of base information. """ @@ -232,6 +237,11 @@ def serialise(self, get_presets=False, add_timestamp=False, as_prefab=False): item_obj = use_class.deserialise_from_object(item, builder_object=self) object_list.append(item_obj.serialise()) + # NMS has no groups, a group is saved as the parts in it + if include_groups: + for group_obj in self.get_all_groups(): + object_list += group.Group.serialise(group_obj) or [] + key = "Prefab" if as_prefab else "Objects" data = {key: object_list} diff --git a/src/addons/no_mans_sky_base_builder/group.py b/src/addons/no_mans_sky_base_builder/group.py new file mode 100644 index 00000000..ae6d4a59 --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/group.py @@ -0,0 +1,420 @@ +"""Groups merge parts into one object and keep each part in a cache. + +The cache stores every part's ObjectID, UserData and matrix relative to the group, +so a group can be ungrouped, saved as its parts, or rebuilt somewhere else. +""" + +import json +import math +import os +import time +import uuid + +import bpy +import mathutils +from mathutils import Matrix, Vector + +from .part import Part +from .utils import blend_utils, material, mirror_utils +from .utils import python as python_utils + +FILE_PATH = os.path.dirname(os.path.realpath(__file__)) +nice_name_dictionary = python_utils.load_dictionary( + os.path.join(FILE_PATH, "resources", "nice_names.json") +) + + +class Group: + + # part properties kept in the cache + PROP_OBJECT_ID = Part.PROP_OBJECT_ID + PROP_USER_DATA = Part.PROP_USER_DATA + PROP_TIMESTAMP = Part.PROP_TIMESTAMP + PROP_MESSAGE = Part.PROP_MESSAGE + PROP_MATRIX_LOCAL = "matrix_local" + + # group properties + PROP_CHILD_CACHE = "child_cache" + PROP_GROUP_ID = "GroupID" + PROP_ORIGIN_OFFSET = "origin_offset" + PROP_ORIGIN_MATRIX = "origin_matrix" + PROP_PART_COUNT = "part_count" + PROP_IS_MIRROR = "is_mirror" + + MERGED_NAME = "Grouped_Objects" + + # Cache --- + @staticmethod + def cache_relative_matrices(parent_obj, object_list): + """Cache each part relative to parent_obj, without parenting anything. + + Returns: + str: JSON of {part name: part data} + """ + cache_data = {} + parent_matrix_inverted = parent_obj.matrix_world.inverted() + + for obj in object_list: + if obj == parent_obj or Group.PROP_OBJECT_ID not in obj: + continue + + matrix_local = parent_matrix_inverted @ obj.matrix_world + cache = { + Group.PROP_OBJECT_ID: obj[Group.PROP_OBJECT_ID], + Group.PROP_USER_DATA: obj.get(Group.PROP_USER_DATA, 0), + Group.PROP_TIMESTAMP: obj.get(Group.PROP_TIMESTAMP, int(time.time())), + Group.PROP_MATRIX_LOCAL: [list(row) for row in matrix_local], + } + if obj.get(Group.PROP_MESSAGE): + cache[Group.PROP_MESSAGE] = obj[Group.PROP_MESSAGE] + + cache_data[obj.name] = cache + + return json.dumps(cache_data) + + @staticmethod + def extract_cached_data(parent_obj): + """Returns (cached child data, origin matrix), or (None, None) when invalid.""" + if Group.PROP_CHILD_CACHE not in parent_obj: + return None, None + + try: + cache_child_data = json.loads(parent_obj[Group.PROP_CHILD_CACHE]) + except (TypeError, ValueError): + return None, None + origin_matrix = Group.str_to_matrix(parent_obj.get(Group.PROP_ORIGIN_MATRIX)) + return cache_child_data, origin_matrix + + # Grouping --- + @staticmethod + def group_objects(objects_to_group, target_matrix=None): + """Merge parts into one group object. + + Args: + objects_to_group (list): The objects to group, anything already a group stops it. + target_matrix (Matrix): Origin and axes of the group, the parts' median if None. + + Returns: + bpy.types.Object | None: The group object. + """ + if not objects_to_group: + return None + + objects_list = [] + for obj in objects_to_group: + if Group.PROP_GROUP_ID in obj: + return None + if Group.PROP_OBJECT_ID in obj: + objects_list.append(obj) + + if not objects_list: + return None + + if target_matrix is not None: + clean_matrix = target_matrix.copy() + else: + clean_matrix = Matrix.Identity(4) + total_location = Vector((0.0, 0.0, 0.0)) + for obj in objects_list: + total_location += obj.matrix_world.translation + clean_matrix.translation = total_location / len(objects_list) + + merged_object = blend_utils.merge_objects(objects_list, Group.MERGED_NAME) + if merged_object is None: + return None + + merged_object[Group.PROP_ORIGIN_MATRIX] = ( + Group.matrix_to_str(target_matrix) if target_matrix is not None else "" + ) + + prev_position = merged_object.matrix_world.translation.copy() + + # move the geometry the opposite way, so the parts don't move when the origin does + merged_object.data.transform(clean_matrix.inverted() @ merged_object.matrix_world) + merged_object.matrix_world = clean_matrix + merged_object.data.update() + + origin_difference = prev_position - merged_object.matrix_world.translation + merged_object[Group.PROP_ORIGIN_OFFSET] = ( + merged_object.matrix_world.to_3x3().inverted() @ origin_difference + ) + + merged_object[Group.PROP_CHILD_CACHE] = Group.cache_relative_matrices(merged_object, objects_list) + merged_object[Group.PROP_GROUP_ID] = str(uuid.uuid4()) + merged_object[Group.PROP_PART_COUNT] = len(objects_list) + merged_object[Group.PROP_IS_MIRROR] = False + + # only the merged parts, anything else that was selected stays + # one batch, a remove() per object re-syncs the whole scene each time + bpy.data.batch_remove(objects_list) + + return merged_object + + @staticmethod + def _get_child_user_data(parent_obj, cache_data): + # a UserData on the group itself comes from recolouring it, and wins over the parts + master_user_data = parent_obj.get(Group.PROP_USER_DATA) if parent_obj is not None else None + if master_user_data is not None: + return master_user_data + return cache_data.get(Group.PROP_USER_DATA, 0) + + @staticmethod + def _build_children(builder, cached_child_data, origin_matrix, parent_obj=None): + restored_objects = [] + for cache_data in cached_child_data.values(): + matrix_local_data = cache_data.get(Group.PROP_MATRIX_LOCAL) + if not matrix_local_data: + continue + + new_part = builder.add_part( + cache_data[Group.PROP_OBJECT_ID], + user_data=Group._get_child_user_data(parent_obj, cache_data), + ) + if new_part is None or not hasattr(new_part, "object"): + continue + + new_obj = new_part.object + new_obj[Group.PROP_TIMESTAMP] = str(cache_data.get(Group.PROP_TIMESTAMP, int(time.time()))) + if Group.PROP_MESSAGE in cache_data: + new_obj[Group.PROP_MESSAGE] = cache_data[Group.PROP_MESSAGE] + + new_obj.matrix_world = origin_matrix @ Matrix(matrix_local_data) + restored_objects.append(new_obj) + return restored_objects + + @staticmethod + def ungroup_objects(builder, parent_obj): + """Rebuild a group's parts in place and remove the group. + + Returns: + list | None: The restored parts. + """ + cached_child_data, _ = Group.extract_cached_data(parent_obj) + if not cached_child_data: + return None + + restored_objects = Group._build_children( + builder, cached_child_data, parent_obj.matrix_world.copy(), parent_obj + ) + bpy.data.objects.remove(parent_obj, do_unlink=True) + return restored_objects + + @staticmethod + def deserialise_to_group(builder, child_cache, origin_matrix=None): + """Build a group from a child cache string.""" + restored_objects = Group.deserialise_to_objects(builder, child_cache, origin_matrix) + if not restored_objects: + return None + return Group.group_objects(restored_objects, origin_matrix or Group.get_default_origin_matrix()) + + @staticmethod + def deserialise_to_objects(builder, child_cache, origin_matrix=None): + """Build the parts of a child cache string, without grouping them.""" + if origin_matrix is None: + origin_matrix = Group.get_default_origin_matrix() + + try: + cached_child_data = json.loads(child_cache) + except (TypeError, ValueError) as error: + print("Error reading group cache ", error) + return None + + return Group._build_children(builder, cached_child_data, origin_matrix) + + # Serialising --- + @staticmethod + def serialise(parent_obj): + """The group's parts as NMS object data. + + Returns: + list | None + """ + cached_child_data, _ = Group.extract_cached_data(parent_obj) + if not cached_child_data: + return None + + serialized_objects = [] + for cache_data in cached_child_data.values(): + matrix_world = Group.restore_matrix_world(parent_obj, cache_data) + if matrix_world is None: + continue + + pos, up, at = Group.extract_pos_up_at(matrix_world) + data = { + Part.PROP_TIMESTAMP: int(cache_data.get(Group.PROP_TIMESTAMP, int(time.time()))), + Part.PROP_OBJECT_ID: "^{0}".format(cache_data[Group.PROP_OBJECT_ID]), + Part.PROP_USER_DATA: int(Group._get_child_user_data(parent_obj, cache_data)), + Part.PROP_POSITION: list(pos), + Part.PROP_UP: list(up), + Part.PROP_AT: list(at), + } + if Group.PROP_MESSAGE in cache_data: + data[Part.PROP_MESSAGE] = cache_data[Group.PROP_MESSAGE] + + serialized_objects.append(data) + + return serialized_objects + + @staticmethod + def restore_matrix_world(parent_obj, child_cache_data): + matrix_local_data = child_cache_data.get(Group.PROP_MATRIX_LOCAL) + if not matrix_local_data: + return None + return parent_obj.matrix_world @ Matrix(matrix_local_data) + + @staticmethod + def extract_pos_up_at(matrix_world): + # bring the matrix from blender Z up space into NMS Y up space + world_matrix_offset = mathutils.Matrix.Rotation(math.radians(-90.0), 4, "X") @ matrix_world + pos = world_matrix_offset.translation + up = world_matrix_offset.col[1].to_3d() + at = world_matrix_offset.col[2].to_3d() + return pos, up, at + + # Colour --- + @staticmethod + def apply_colour(group_obj, colour_index, material_index): + """Recolour the whole group, it becomes the colour of every part in it.""" + new_material = material.assign_material(group_obj, colour_index, material_index) + # a merged group has one slot per colour it was made from + for index in range(len(group_obj.data.materials)): + group_obj.data.materials[index] = new_material + + # Lookups --- + @staticmethod + def get_all_groups(): + groups = [] + for obj in bpy.context.view_layer.objects: + try: + if obj is not None and Group.PROP_GROUP_ID in obj: + groups.append(obj) + except ReferenceError: + continue + return groups + + @staticmethod + def find_mirror_group(target, groups_list=None): + """The first group sharing target's GroupID from the other side of the mirror.""" + existing_groups = Group.get_all_groups() if groups_list is None else groups_list + is_target_mirror = target.get(Group.PROP_IS_MIRROR, False) + + for obj in existing_groups: + try: + if obj is None or Group.PROP_GROUP_ID not in obj or obj.name == target.name: + continue + if obj[Group.PROP_GROUP_ID] != target[Group.PROP_GROUP_ID]: + continue + if obj.get(Group.PROP_IS_MIRROR, False) != is_target_mirror: + return obj + except ReferenceError: + continue + return None + + # Matrices --- + @staticmethod + def matrix_to_str(matrix): + return json.dumps([list(row) for row in matrix]) + + @staticmethod + def str_to_matrix(json_string): + if not json_string: + return None + try: + return Matrix(json.loads(json_string)) + except (TypeError, ValueError): + print("error decoding json string : ", json_string) + return None + + @staticmethod + def extract_origin_matrix(group_obj): + return Group.str_to_matrix(group_obj.get(Group.PROP_ORIGIN_MATRIX)) + + @staticmethod + def get_default_origin_matrix(): + return Matrix.Identity(4) + + # Mirroring --- + @staticmethod + def mirror_cache_data(child_cache, origin_matrix, axis, center): + """Mirror a group's cached parts without building any of them. + + Each part is mirrored the way the mirror tool mirrors a part, and swapped + for its mirror twin when there is one. + + Returns: + tuple: (new child cache json, new origin matrix) or (None, None). + """ + try: + cached_child_data = json.loads(child_cache) + except (TypeError, ValueError) as error: + print("Error mirroring group cache: ", error) + return None, None + + if origin_matrix is None: + origin_matrix = Group.get_default_origin_matrix() + + new_origin = mirror_utils.mirror_matrix_world_universal(None, origin_matrix, axis, center) + new_origin_inverted = new_origin.inverted() + + new_child_cache = {} + for child_name, cache_data in cached_child_data.items(): + matrix_local_data = cache_data.get(Group.PROP_MATRIX_LOCAL) + if not matrix_local_data: + continue + + new_cache_data = dict(cache_data) + object_id = cache_data[Group.PROP_OBJECT_ID] + mirror_part_id = Part.get_mirror_part_id(object_id) + + # corrections are keyed on the id before the swap, same as the mirror tool + matrix_world = mirror_utils.mirror_matrix_world_universal( + object_id, origin_matrix @ Matrix(matrix_local_data), axis, center + ) + + if mirror_part_id in nice_name_dictionary: + new_cache_data[Group.PROP_OBJECT_ID] = mirror_part_id + new_cache_data[Group.PROP_MATRIX_LOCAL] = [ + list(row) for row in new_origin_inverted @ matrix_world + ] + new_child_cache[child_name] = new_cache_data + + return json.dumps(new_child_cache), new_origin + + @staticmethod + def mirror_group(builder, group_obj, axis, center, auto_duplicate=False, existing_groups=None): + """Mirror a group, reusing an existing mirror twin's mesh when there is one. + + Returns: + bpy.types.Object | None: The mirrored group. + """ + is_target_mirror = group_obj.get(Group.PROP_IS_MIRROR, False) + group_id = group_obj[Group.PROP_GROUP_ID] + new_matrix_world = mirror_utils.mirror_matrix_world_universal( + None, group_obj.matrix_world.copy(), axis, center + ) + + found_match = Group.find_mirror_group(group_obj, existing_groups) + if found_match is not None: + # a copy of the twin is the mirror already, no parts need building + mirrored_group = blend_utils.duplicate_part(found_match) + mirrored_group.matrix_world = new_matrix_world + else: + new_child_cache, new_origin = Group.mirror_cache_data( + group_obj[Group.PROP_CHILD_CACHE], group_obj.matrix_world.copy(), axis, center + ) + if new_child_cache is None: + return None + mirrored_group = Group.deserialise_to_group(builder, new_child_cache, new_origin) + if mirrored_group is None: + return None + mirrored_group[Group.PROP_GROUP_ID] = group_id + mirrored_group[Group.PROP_IS_MIRROR] = not is_target_mirror + if Group.PROP_USER_DATA in group_obj: + mirrored_group[Group.PROP_USER_DATA] = group_obj[Group.PROP_USER_DATA] + + if not auto_duplicate: + group_name = group_obj.name + bpy.data.objects.remove(group_obj, do_unlink=True) + mirrored_group.name = group_name + + return mirrored_group diff --git a/src/addons/no_mans_sky_base_builder/part.py b/src/addons/no_mans_sky_base_builder/part.py index 01190649..a5844ca8 100644 --- a/src/addons/no_mans_sky_base_builder/part.py +++ b/src/addons/no_mans_sky_base_builder/part.py @@ -315,11 +315,15 @@ def retrieve_object_from_id(self, object_id): blend_utils.add_to_scene(item) return item - # Create cube, at data level if the active collection is excluded. + # Create cube, at data level if the active collection is excluded + # or the context has no active object (timers, other restricted contexts). + item = None try: bpy.ops.mesh.primitive_cube_add() - item = bpy.context.active_object - except RuntimeError: + item = bpy.context.view_layer.objects.active + except (RuntimeError, AttributeError): + pass + if item is None: mesh = bpy.data.meshes.new(object_id) item = bpy.data.objects.new(object_id, mesh) diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py index a76f9440..bbd9acf5 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py @@ -6,6 +6,7 @@ from ..utils import python as python_utils from .. import part from ..builder import get_builder +from ..group import Group from ..utils.mirror_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -92,7 +93,7 @@ def batch_replace_with_target_object(self): if self.nms_batch_replace_type == "target": target_object = self.target_object - if "ObjectID" not in target_object: + if "ObjectID" not in target_object and Group.PROP_GROUP_ID not in target_object: title="Batch Replace Objects" message="Target Object is Invalid" ShowMessageBox(message=message, title=title ) @@ -116,7 +117,7 @@ def batch_replace_with_target_object(self): if source_object is None: continue - if "ObjectID" in source_object: + if "ObjectID" in source_object or Group.PROP_GROUP_ID in source_object: # This create a linked duplicate replaced_object = target_object.copy() if replaced_object.data: @@ -204,25 +205,13 @@ def select_same_objects(self): ) return 0 - #Gather unique keys from selected obejcts - keys_to_find = [] - for obj in selected_objects: - if "ObjectID" not in obj: - continue - - obj_id = obj["ObjectID"] - if obj_id not in keys_to_find: - keys_to_find.append(obj_id) - - #Iterate through all objects to collect matches with keys_to_find - found_matches = [] - for obj in bpy.data.objects: - if "ObjectID" not in obj: - continue - - obj_id = obj["ObjectID"] - if obj_id in keys_to_find: - found_matches.append(obj) + # parts match by ObjectID, groups by GroupID + def get_key(obj): + return obj.get("ObjectID") or obj.get(Group.PROP_GROUP_ID) + + keys_to_find = set(filter(None, (get_key(obj) for obj in selected_objects))) + + found_matches = [obj for obj in bpy.data.objects if get_key(obj) in keys_to_find] if len(found_matches) > 0: blend_utils.select(found_matches) diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool.py b/src/addons/no_mans_sky_base_builder/tools/build_tool.py index f75b6f77..ccc53766 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool.py @@ -6,6 +6,7 @@ from ..utils import python as python_utils from .. import part from ..builder import get_builder +from ..group import Group from ..utils.mirror_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -94,7 +95,8 @@ def mirror(self, axis = None, center = None, change_orientation = False, auto_du # Get Selected item. new_items = [] - for target in selected_objects: + existing_groups = None + for target in list(selected_objects): # Part if "ObjectID" in target : object_id = target["ObjectID"] @@ -131,8 +133,20 @@ def mirror(self, axis = None, center = None, change_orientation = False, auto_du new_curve_obj = curve.mirror_curve(target, axis, center, should_auto_duplicate) if new_curve_obj is not None: new_items.append(new_curve_obj) - - blend_utils.select(new_items) + + # mirror if object is a group, change orientation doesn't apply to groups + elif Group.PROP_GROUP_ID in target and not change_orientation: + if existing_groups is None: + existing_groups = Group.get_all_groups() + mirrored_group = Group.mirror_group( + get_builder(), target, axis, center, auto_duplicate, existing_groups + ) + if mirrored_group is not None: + existing_groups.append(mirrored_group) + new_items.append(mirrored_group) + + if new_items: + blend_utils.select(new_items) return {"FINISHED"} # called my Perform Mirror button in advanced mirroring options @@ -285,6 +299,10 @@ def duplicate(self): blend_utils.select(curve.duplicate_curve(target)) return + if Group.PROP_GROUP_ID in target: + blend_utils.select(blend_utils.duplicate_part(target)) + return + if "ObjectID" not in target and "PresetID" not in target: message = ( "This item can not be duplicated via the No Man's Sky tool. " diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool_presentation.py b/src/addons/no_mans_sky_base_builder/tools/build_tool_presentation.py index cf79d440..f0d6ff71 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool_presentation.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool_presentation.py @@ -28,25 +28,34 @@ def draw(self, context): build_tools_col = layout.column(align = True) tools_row = build_tools_col.row(align = True) tools_column = tools_row.column(align = True) - tools_column.scale_x = 1.3333 + tools_column.scale_x = 1.4333 snap_column = tools_row.column(align = True) # Create Part Count Box. - part_box = tools_column.box() + part_box = snap_column.box() splitter = part_box.split(factor=0.7) splitter.label(text="Part Count:" )# , icon = "GEOMETRY_NODES" - part_count = len([obj for obj in bpy.data.objects if "ObjectID" in obj]) + # a group counts as the parts inside it + part_count = sum( + 1 if "ObjectID" in obj else obj.get("part_count", 0) + for obj in bpy.data.objects + if "ObjectID" in obj or "GroupID" in obj + ) splitter.label(text="{}".format(part_count)) tools_box = tools_column.box() - tools_col = tools_box.column() - + tools_col = tools_box.column(align = False) tools_col.label(text="Common Tools") - tools_col.operator("object.nms_duplicate", icon="DUPLICATE") - tools_col.operator("object.nms_delete", icon="TRASH") + del_dup_col = tools_col.column(align = True) + del_dup_col.operator("object.nms_duplicate", icon="DUPLICATE") + del_dup_col.operator("object.nms_delete", icon="TRASH") tools_col.label(text="Curve") tools_col.operator("object.nms_duplicate_along_curve", icon="MOD_DASH") - + tools_col.label(text="Grouping") + group_row = tools_col.row(align=True) + group_row.operator("object.nms_group_objects", text="Group", icon="OUTLINER_OB_POINTCLOUD") + group_row.operator("object.nms_ungroup_objects", text="Ungroup", icon="OUTLINER_DATA_POINTCLOUD") + # Create Snapping box. snap_box = snap_column.box() @@ -79,8 +88,9 @@ def draw(self, context): orientation_box = snap_column.box() mirror_col = orientation_box.column(align=True) mirror_col.label(text="Orientation") # icon = "ORIENTATION_GIMBAL" - mirror_col.operator("object.nms_mirror", icon="ARROW_LEFTRIGHT") - mirror_col.operator("object.nms_flip", icon="DECORATE_OVERRIDE") + mirror_col_row = mirror_col.row(align = True) + mirror_col_row.operator("object.nms_mirror", icon="ARROW_LEFTRIGHT") + mirror_col_row.operator("object.nms_flip", icon="DECORATE_OVERRIDE") #mirror_col.operator("object.nms_turn", icon="GESTURE_ROTATE") diff --git a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py index f4fac1a7..f21df34f 100644 --- a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py @@ -3,6 +3,7 @@ import math import addon_utils +import bmesh import bpy from ..utils import blend_utils @@ -127,6 +128,24 @@ def select(selection, add=False): set_active_item(selection[-1]) +def get_selected_active_object(): + """The active object, only when it is also selected. + + Read from the view layer, context.active_object is missing in timers and + other restricted contexts. + """ + view_layer = getattr(bpy.context, "view_layer", None) + if view_layer is None: + return None + try: + active_object = view_layer.objects.active + if active_object is None or not active_object.select_get(view_layer=view_layer): + return None + return active_object + except (ReferenceError, RuntimeError): + return None + + def get_current_selection(): """Get the current selected item. @@ -220,7 +239,156 @@ def duplicate_part(target): Return duplicated object """ new_item = target.copy() - new_item.data = target.data.copy() + if new_item.data: + new_item.data = target.data.copy() for collection in target.users_collection: collection.objects.link(new_item) - return target \ No newline at end of file + return new_item + + +def deselect_all(): + """Clear the selection without going through bpy.ops.""" + view_layer = bpy.context.view_layer + for item in view_layer.objects: + if item.select_get(view_layer=view_layer): + item.select_set(False, view_layer=view_layer) + + +def select_only(item): + deselect_all() + view_layer = bpy.context.view_layer + if item.name in view_layer.objects: + item.select_set(True) + view_layer.objects.active = item + + +def needs_operator_join(objects): + # vertex groups, shape keys and object linked materials are dropped by a bmesh merge + for obj in objects: + if obj.vertex_groups or obj.data.shape_keys: + return True + for slot in obj.material_slots: + if slot.link != 'DATA': + return True + return False + + +# above this many vertices the join operator is faster than bmesh +BMESH_MERGE_VERT_LIMIT = 110000 + + +def merge_objects_with_bmesh(objects, object_name): + """Join meshes directly into the first object's space, without bpy.ops.""" + base = objects[0] + base_inverse = base.matrix_world.inverted() + + # slots are pooled by material across every object, in first seen order + materials = [] + material_indices = {} + + bm = bmesh.new() + for obj in objects: + slot_map = [] + for slot in obj.material_slots: + material = slot.material + if material is None: + slot_map.append(0) + continue + index = material_indices.get(material.name) + if index is None: + index = len(materials) + material_indices[material.name] = index + materials.append(material) + slot_map.append(index) + + needs_remap = slot_map != list(range(len(slot_map))) + + if obj is base and not needs_remap: + bm.from_mesh(obj.data) + continue + + # Mesh.transform also carries custom split normals, a vertex loop would not + mesh_copy = obj.data.copy() + if obj is not base: + mesh_copy.transform(base_inverse @ obj.matrix_world) + + if needs_remap: + last_slot = len(slot_map) - 1 + indices = [0] * len(mesh_copy.polygons) + mesh_copy.polygons.foreach_get("material_index", indices) + mesh_copy.polygons.foreach_set( + "material_index", + [slot_map[i if i <= last_slot else last_slot] for i in indices], + ) + + bm.from_mesh(mesh_copy) + bpy.data.meshes.remove(mesh_copy) + + mesh = bpy.data.meshes.new(object_name) + bm.to_mesh(mesh) + bm.free() + + for material in materials: + mesh.materials.append(material) + + merged = bpy.data.objects.new(object_name, mesh) + for collection in base.users_collection: + collection.objects.link(merged) + merged.matrix_world = base.matrix_world.copy() + return merged + + +def merge_objects_with_operator(objects, object_name): + view_layer = bpy.context.view_layer + bpy.ops.object.select_all(action='DESELECT') + for obj in objects: + obj.select_set(True) + view_layer.objects.active = objects[0] + + # duplicate leaves its copy of the active object active, which is what gets joined into + meshes_before = set(bpy.data.meshes) + bpy.ops.object.duplicate(linked=False) + bpy.ops.object.join() + + merged = view_layer.objects.active + merged.name = object_name + + # join keeps only the active mesh, the other duplicated meshes are left unused + orphans = [ + mesh for mesh in bpy.data.meshes + if mesh.users == 0 and mesh not in meshes_before + ] + if orphans: + bpy.data.batch_remove(orphans) + + for key in list(merged.keys()): + del merged[key] + + return merged + + +def merge_objects(objects, object_name): + """Merge mesh objects into a new object, leaving the originals untouched. + + Returns: + bpy.types.Object | None + """ + objects = [obj for obj in objects if obj and obj.type == 'MESH'] + if not objects: + print("No objects to merge") + return None + + try: + total_verts = sum(len(obj.data.vertices) for obj in objects) + if needs_operator_join(objects) or total_verts > BMESH_MERGE_VERT_LIMIT: + merged = merge_objects_with_operator(objects, object_name) + else: + merged = merge_objects_with_bmesh(objects, object_name) + + merged.data.update() + select_only(merged) + return merged + + except Exception as error: + print("Error Occured while grouping objects : ", str(error)) + return None \ No newline at end of file diff --git a/src/addons/no_mans_sky_base_builder/utils/curve.py b/src/addons/no_mans_sky_base_builder/utils/curve.py index 27055228..2af3048b 100644 --- a/src/addons/no_mans_sky_base_builder/utils/curve.py +++ b/src/addons/no_mans_sky_base_builder/utils/curve.py @@ -6,8 +6,9 @@ import bpy from ..builder import get_builder +from ..group import Group from ..part import Part -from . import collection_utils, curve_utils, material +from . import collection_utils, curve_utils, material, userdata class Curve: @@ -31,6 +32,13 @@ class Curve: PROP_DUP_OBJECT_ID = f"dup_{Part.PROP_OBJECT_ID}" PROP_DUP_USER_DATA = f"dup_{Part.PROP_USER_DATA}" + # the group duplicated along the curve + PROP_DUP_IS_GROUP = "is_group" + PROP_DUP_GROUP_ID = f"dup_{Group.PROP_GROUP_ID}" + PROP_GROUP_CHILD_CACHE = f"dup_{Group.PROP_CHILD_CACHE}" + PROP_ORIGIN_MATRIX = f"dupe_{Group.PROP_ORIGIN_MATRIX}" + GROUP_PROPS = (PROP_DUP_IS_GROUP, PROP_DUP_GROUP_ID, PROP_GROUP_CHILD_CACHE, PROP_ORIGIN_MATRIX) + # properties from older versions of the curve tool LEGACY_PROPS = ("unique_id", "parent_col", "master_col") @@ -236,8 +244,7 @@ def duplicate_along_curve(bpy_object, curve, number_of_duplicates=10, radius_mul curve[Curve.PROP_INITIAL_CURVE_SCALE] = curve.scale.x if bpy_object is not None: - curve[Curve.PROP_DUP_OBJECT_ID] = bpy_object[Part.PROP_OBJECT_ID] - curve[Curve.PROP_DUP_USER_DATA] = bpy_object[Part.PROP_USER_DATA] + store_duplicate_source(curve, bpy_object) # callers that already grouped followers by curve can pass them in and skip the scan if existing_objs is None: @@ -254,6 +261,50 @@ def duplicate_along_curve(bpy_object, curve, number_of_duplicates=10, radius_mul return existing_objs +# remember what the curve duplicates, a part or a group +def store_duplicate_source(curve, source_obj): + if Group.PROP_GROUP_ID in source_obj: + curve[Curve.PROP_DUP_IS_GROUP] = True + curve[Curve.PROP_DUP_GROUP_ID] = source_obj[Group.PROP_GROUP_ID] + curve[Curve.PROP_GROUP_CHILD_CACHE] = source_obj[Group.PROP_CHILD_CACHE] + curve[Curve.PROP_ORIGIN_MATRIX] = source_obj.get(Group.PROP_ORIGIN_MATRIX, "") + curve.pop(Curve.PROP_DUP_OBJECT_ID, None) + if Part.PROP_USER_DATA in source_obj: + curve[Curve.PROP_DUP_USER_DATA] = source_obj[Part.PROP_USER_DATA] + else: + curve.pop(Curve.PROP_DUP_USER_DATA, None) + else: + for prop in Curve.GROUP_PROPS: + curve.pop(prop, None) + curve[Curve.PROP_DUP_IS_GROUP] = False + curve[Curve.PROP_DUP_OBJECT_ID] = source_obj[Part.PROP_OBJECT_ID] + curve[Curve.PROP_DUP_USER_DATA] = source_obj[Part.PROP_USER_DATA] + + +def is_group_curve(curve): + return bool(curve.get(Curve.PROP_DUP_IS_GROUP, False)) and Curve.PROP_GROUP_CHILD_CACHE in curve + + +# colour one follower, a group has one material slot per colour so every slot is painted +def paint_follower(obj, user_data): + if Group.PROP_GROUP_ID not in obj: + material.restore_material(obj, user_data) + return + try: + user_data = int(user_data) + except (TypeError, ValueError): + return + Group.apply_colour(obj, userdata.get_colour(user_data), userdata.get_material(user_data)) + + +def build_group_follower(curve): + origin_matrix = Group.str_to_matrix(curve.get(Curve.PROP_ORIGIN_MATRIX)) or Group.get_default_origin_matrix() + new_obj = Group.deserialise_to_group(get_builder(), curve[Curve.PROP_GROUP_CHILD_CACHE], origin_matrix) + if new_obj is not None: + new_obj[Group.PROP_GROUP_ID] = curve.get(Curve.PROP_DUP_GROUP_ID, new_obj[Group.PROP_GROUP_ID]) + return new_obj + + def remove_objects_from_curve(number_to_remove, existing_objs): if number_to_remove <= 0: return @@ -284,17 +335,22 @@ def add_objects_to_curve(number_to_add, curve, existing_objs, bpy_object=None): new_obj.data = bpy_object.data.copy() for constraint in list(new_obj.constraints): new_obj.constraints.remove(constraint) - elif Curve.PROP_DUP_OBJECT_ID not in curve: - # group curves from other versions can only grow by copying a follower - return - else: + elif is_group_curve(curve): + new_obj = build_group_follower(curve) + if new_obj is None: + return + elif Curve.PROP_DUP_OBJECT_ID in curve: new_item = get_builder().add_part( - curve[Curve.PROP_DUP_OBJECT_ID], user_data=curve[Curve.PROP_DUP_USER_DATA] + curve[Curve.PROP_DUP_OBJECT_ID], user_data=curve.get(Curve.PROP_DUP_USER_DATA, 0) ) new_obj = new_item.object + else: + return if Curve.PROP_DUP_USER_DATA in curve: - material.restore_material(new_obj, curve[Curve.PROP_DUP_USER_DATA]) + paint_follower(new_obj, curve[Curve.PROP_DUP_USER_DATA]) + if Group.PROP_GROUP_ID in new_obj: + new_obj[Part.PROP_USER_DATA] = curve[Curve.PROP_DUP_USER_DATA] constraint = new_obj.constraints.new(type='FOLLOW_PATH') constraint.target = curve @@ -367,7 +423,7 @@ def apply_curve_transforms_and_detach(curve): Curve.PROP_RADIUS_MULTIPLIER, Curve.PROP_OBJECTS_COUNT, Curve.PROP_DENSITY_STEP, - ) + Curve.LEGACY_PROPS + ) + Curve.GROUP_PROPS + Curve.LEGACY_PROPS for prop in curve_props_to_delete: curve.pop(prop, None) @@ -475,8 +531,7 @@ def replace_curve_object(curve_obj, source_obj): for collection in curve_obj.users_collection: collection.objects.link(new_curve_obj) - new_curve_obj[Curve.PROP_DUP_OBJECT_ID] = source_obj[Part.PROP_OBJECT_ID] - new_curve_obj[Curve.PROP_DUP_USER_DATA] = source_obj[Part.PROP_USER_DATA] + store_duplicate_source(new_curve_obj, source_obj) sync_curves(new_curve_obj, curve_obj, duping_object_source=source_obj) @@ -524,11 +579,23 @@ def mirror_curve(curve_obj, axis="Z", center=None, auto_duplicate=False): curve_utils.mirror_curve(new_curve_obj, axis, center) - # switch to the mirror part when one exists - object_id = new_curve_obj.get(Curve.PROP_DUP_OBJECT_ID) - mirror_obj_id = Part.get_mirror_part_id(object_id) if object_id else None - if mirror_obj_id in get_builder().nice_name_dictionary: - new_curve_obj[Curve.PROP_DUP_OBJECT_ID] = mirror_obj_id + if is_group_curve(new_curve_obj): + # mirror the group's cache, followers are rebuilt from it + new_child_cache, new_origin_matrix = Group.mirror_cache_data( + new_curve_obj[Curve.PROP_GROUP_CHILD_CACHE], + Group.str_to_matrix(new_curve_obj.get(Curve.PROP_ORIGIN_MATRIX)), + axis, + center, + ) + if new_child_cache is not None: + new_curve_obj[Curve.PROP_GROUP_CHILD_CACHE] = new_child_cache + new_curve_obj[Curve.PROP_ORIGIN_MATRIX] = Group.matrix_to_str(new_origin_matrix) + else: + # switch to the mirror part when one exists + object_id = new_curve_obj.get(Curve.PROP_DUP_OBJECT_ID) + mirror_obj_id = Part.get_mirror_part_id(object_id) if object_id else None + if mirror_obj_id in get_builder().nice_name_dictionary: + new_curve_obj[Curve.PROP_DUP_OBJECT_ID] = mirror_obj_id sync_curves(new_curve_obj, curve_obj, True, axis, from_mirror=True) return new_curve_obj @@ -558,9 +625,10 @@ def sync_curves(target_curve, source_curve, do_mirror=False, axis=None, from_mir duping_object, target_curve, number_of_objects, radius_multiplier ) - if target_dupe_objects: + target_is_group = is_group_curve(target_curve) + if target_dupe_objects and not target_is_group: target = target_dupe_objects[0] - material.restore_material(target, target[Part.PROP_USER_DATA]) + material.restore_material(target, target.get(Part.PROP_USER_DATA, 0)) if Curve.PROP_DUP_USER_DATA in source_curve: apply_color(target_curve, source_curve[Curve.PROP_DUP_USER_DATA]) @@ -576,6 +644,10 @@ def sync_curves(target_curve, source_curve, do_mirror=False, axis=None, from_mir target.rotation_euler.y = -target.rotation_euler.y target.rotation_euler.z = -target.rotation_euler.z + if axis == "Z" and target_is_group: + target.rotation_euler.x += math.pi + target.rotation_euler.z += math.pi + # scale of a follower before the curve's radius and scale are applied def calculate_base_scale(curve, obj): @@ -592,7 +664,13 @@ def apply_color(curve_obj, user_data): if Curve.PROP_HAS_LINKED_OBJECTS in curve_obj and is_bezier_or_nurbs_path(curve_obj): curve_obj[Curve.PROP_DUP_USER_DATA] = user_data - # followers share one mesh, so painting the first colours them all - for child_obj in get_all_curve_children(curve_obj, require_id=False): - material.restore_material(child_obj, user_data) - break + children = get_all_curve_children(curve_obj, require_id=False) + if not children: + return None + + # followers share one mesh, so painting the first colours them all, + # but each one saves its own UserData + paint_follower(children[0], user_data) + painted_user_data = children[0].get(Part.PROP_USER_DATA, str(user_data)) + for child_obj in children: + child_obj[Part.PROP_USER_DATA] = painted_user_data From 7e9becf51e248db65061cdb023081c426f82362c Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:40:02 +0530 Subject: [PATCH 12/15] Improvements to batch tools including new features. --- .../no_mans_sky_base_builder/__init__.py | 8 +- .../save_editor/save_editor_utils.py | 11 +- .../save_editor/save_file.py | 3 +- .../tools/batch_tool.py | 232 ++++++++---------- .../tools/batch_tool_operators.py | 68 ++++- .../tools/batch_tool_presentation.py | 39 ++- .../tools/build_tool.py | 2 +- .../tools/build_tool_operators.py | 2 +- .../tools/properties.py | 2 +- .../tools/properties_presentation.py | 2 +- .../utils/blend_utils.py | 13 + .../utils/mirror_utils.py | 6 - 12 files changed, 194 insertions(+), 194 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index e033a4ae..5652fa9b 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -19,6 +19,7 @@ from . import icons, part, preset from .builder import get_builder from .group import Group +from .utils.blend_utils import ShowMessageBox from .part_overrides import line from .save_editor import save_editor_operators, save_editor_utils from .save_editor.save_editor_presentation import NMS_PT_save_editor_panel @@ -50,13 +51,6 @@ # Setting Support Methods --- -def ShowMessageBox(message="", title="Message Box", icon="INFO"): - def draw(self, context): - self.layout.label(text=message) - - bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - - def part_switch(self, context): """Toggle method for switching between parts and presets.""" scene = context.scene diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py index 4697225e..04697d5a 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py @@ -11,6 +11,7 @@ from . import save_translation from .save_translation import SaveTranslation +from ..utils.blend_utils import ShowMessageBox system = platform.system() ADDON_ID = __package__.rsplit(".", 1)[0] @@ -57,14 +58,8 @@ def __init__(self, index = None ,base = None, ship_ownership = None): self.galactic_address = str(base[SaveTranslation.galactic_address]) self.base_type = in_base_type self.parts_count = parts_count - - -# called to display messages/notificatoin -def ShowMessageBox(message="", title="Message Box", icon="INFO"): - def draw(self, context): - self.layout.label(text=message) - bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - + + # return save folder path for different OS versions def get_default_save_folder(): if system == "Windows": diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py index 813f4566..4c03b783 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_file.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_file.py @@ -7,7 +7,8 @@ import shutil from .save_translation import SaveTranslation -from .save_editor_utils import BaseData, BaseType, ShowMessageBox +from .save_editor_utils import BaseData, BaseType +from ..utils.blend_utils import ShowMessageBox MAGIC = 0xFEEDA1E5 CHUNK_SIZE = 0x80000 # 524288 bytes diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py index bbd9acf5..be2c2ae3 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool.py @@ -1,24 +1,13 @@ -from ..utils import mirror_utils import bpy -import os -import uuid -from ..utils import blend_utils, curve, material -from ..utils import python as python_utils -from .. import part + from ..builder import get_builder from ..group import Group -from ..utils.mirror_utils import ShowMessageBox - -FILE_PATH = os.path.dirname(os.path.realpath(__file__)) -NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") +from ..utils import blend_utils, curve, material +from ..utils.blend_utils import ShowMessageBox -GHOSTED_JSON = os.path.join(FILE_PATH,"..", "resources", "ghosted.json") -ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON) -GHOSTED_ITEMS = ghosted_reference["GHOSTED"] -nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) class BatchTool(bpy.types.PropertyGroup): - + nms_batch_replace_type: bpy.props.EnumProperty( name="Swap With", description="Replace all selected objects with another object of choosing", @@ -29,21 +18,21 @@ class BatchTool(bpy.types.PropertyGroup): options={'SKIP_SAVE'}, default = "target" ) - + object_id: bpy.props.StringProperty( name="ObjectID", description="Enter ObjectID of object you want selections to be replaced with", default="", maxlen=1024, ) - + target_object: bpy.props.PointerProperty( name="Target Object", type=bpy.types.Object, options={'SKIP_SAVE'}, - description = "This object's origin will be take in to account for center of reflection" + description = "Object the selection is replaced with" ) - + color_picker: bpy.props.PointerProperty( name="Colour Picker", type=bpy.types.Object, @@ -51,171 +40,142 @@ class BatchTool(bpy.types.PropertyGroup): description = "Pick an object to use are reference for colouring", update = lambda self, context: self.on_color_picked() ) - - - + def on_color_picked(self): target_object = self.color_picker if target_object is None: return - + if "UserData" in target_object: target_userdata = target_object["UserData"] selected_objects = bpy.context.selected_objects for obj in selected_objects: material.restore_material(obj, target_userdata) self.color_picker = None - + # Replace --- def batch_replace_with_target_object(self): """Replace all selected objects with duplicates of the target object.""" - selected_objects = list(bpy.context.selected_objects) - bpy.ops.object.select_all(action='DESELECT') + blend_utils.deselect_all() + title = "Batch Replace Objects" if not selected_objects: - title="Batch Replace Objects" - message="Make sure you have an item selected." - ShowMessageBox( message= message, title=title ) + ShowMessageBox(message="Make sure you have an item selected.", title=title) return 0 - if self.nms_batch_replace_type == "target" and not self.target_object: - title="Batch Replace Objects" - message="Choose valid a target object to replace the selection with." - ShowMessageBox( message= message, title=title ) - return 0 - elif self.nms_batch_replace_type == "object_id" and self.object_id not in nice_name_dictionary: - title="Batch Replace Objects" - message="Enter a valid ObjectID to replace the selection with." - ShowMessageBox( message= message, title=title ) - return 0 - - if self.nms_batch_replace_type == "target": target_object = self.target_object + if target_object is None: + ShowMessageBox(message="Choose valid a target object to replace the selection with.", title=title) + return 0 if "ObjectID" not in target_object and Group.PROP_GROUP_ID not in target_object: - title="Batch Replace Objects" - message="Target Object is Invalid" - ShowMessageBox(message=message, title=title ) + ShowMessageBox(message="Target Object is Invalid", title=title) return 0 - else : - # create a temp object if "ObjectID" is provided - object_id = self.object_id - new_obj = get_builder().add_part(object_id) - target_object = new_obj.object - + replaced_objects = self.batch_replace(target_object, selected_objects) + else: + if self.object_id not in get_builder().nice_name_dictionary: + ShowMessageBox(message="Enter a valid ObjectID to replace the selection with.", title=title) + return 0 + replaced_objects = self.batch_replace_with_object_id(self.object_id, selected_objects) + + if not replaced_objects: + return 0 + + try: + blend_utils.select(replaced_objects) + except ReferenceError as error: + print(error) + + return len(replaced_objects) + + def batch_replace_with_object_id(self, object_id, objects_to_replace): + # a temporary part to copy from, removed once the replace is done + target_object = get_builder().add_part(object_id).object + replaced_objects = self.batch_replace(target_object, objects_to_replace) + bpy.data.objects.remove(target_object, do_unlink=True) + return replaced_objects + + def batch_replace(self, target_object, objects_to_replace): replaced_objects_list = [] objects_to_delete = [] - + # Get the current active collection to link the new objects to current_collection = bpy.context.collection - for source_object in selected_objects: - if source_object == target_object: + for source_object in objects_to_replace: + if source_object is None or source_object == target_object: continue - - if source_object is None: - continue - + if "ObjectID" in source_object or Group.PROP_GROUP_ID in source_object: - # This create a linked duplicate replaced_object = target_object.copy() + # colour lives on the mesh material, so every replacement needs its own mesh if replaced_object.data: replaced_object.data = replaced_object.data.copy() - # Copy transforms replaced_object.matrix_world = source_object.matrix_world.copy() - # Link the new object to the scene current_collection.objects.link(replaced_object) - + replaced_objects_list.append(replaced_object) objects_to_delete.append(source_object) - elif "has_linked_objects" in source_object and source_object.get("has_linked_objects", False): + elif source_object.get("has_linked_objects", False): new_curve, old_curve = curve.replace_curve_object(source_object, target_object) replaced_objects_list.append(new_curve) objects_to_delete.append(old_curve) - - # Delete old objects - for obj in objects_to_delete: - bpy.data.objects.remove(obj, do_unlink=True) - - # Select the new objects - try: - if len(replaced_objects_list) > 0: - blend_utils.select(replaced_objects_list) - except ReferenceError as error: - print(error) - - # delete temp object - if self.nms_batch_replace_type == "object_id": - bpy.data.objects.remove(target_object, do_unlink=True) - - return len(replaced_objects_list) - - - def select_same_colored_objects(self): - """Selects objects with same UserData, for easy experimentation with colors""" - + + # one batch, a remove() per object re-syncs the whole scene each time + if objects_to_delete: + bpy.data.batch_remove(objects_to_delete) + + return replaced_objects_list + + # Select --- + def select_matching(self, get_key, title): + """Select every object whose key matches one of the selected objects' keys. + + Returns: + int: How many objects were found beyond the ones already selected. + """ selected_objects = bpy.context.selected_objects - if not selected_objects: - ShowMessageBox( - message="Make sure you have an item selected.", title="Find Objects with same Color" - ) + ShowMessageBox(message="Make sure you have an item selected.", title=title) return 0 - - #Gather unique keys from selected obejcts - keys_to_find = [] - for obj in selected_objects: - if "ObjectID" not in obj: - continue - - obj_id = obj["ObjectID"] - user_data = obj["UserData"] - if (obj_id,user_data) not in keys_to_find: - keys_to_find.append((obj_id,user_data)) - - #Iterate through all objects to collect matches with keys_to_find - found_matches = [] - for obj in bpy.data.objects: - if "ObjectID" not in obj: - continue - - obj_id = obj["ObjectID"] - user_data = obj["UserData"] - - new_key = (obj_id, user_data) - if new_key in keys_to_find: - found_matches.append(obj) - - if len(found_matches) > 0: + + keys_to_find = set(filter(lambda key: key is not None, (get_key(obj) for obj in selected_objects))) + found_matches = [obj for obj in bpy.context.view_layer.objects if get_key(obj) in keys_to_find] + + if found_matches: blend_utils.select(found_matches) - + return len(found_matches) - len(keys_to_find) - - + + def select_same_colored_objects(self): + """Selects parts with the same ObjectID and UserData, for easy experimentation with colors""" + def get_key(obj): + if "ObjectID" not in obj: + return None + return (obj["ObjectID"], str(obj.get("UserData", 0))) + + return self.select_matching(get_key, "Find Objects with same Color") + + def select_all_same_colored_objects(self): + """Selects parts with the same UserData, whatever part they are""" + def get_key(obj): + if "ObjectID" not in obj: + return None + return str(obj.get("UserData", 0)) + + return self.select_matching(get_key, "Find Objects with same Color") + def select_same_objects(self): - """Selects objects with same ObjectIDs""" - - selected_objects = bpy.context.selected_objects - - if not selected_objects: - ShowMessageBox( - message="Make sure you have an item selected.", title="Select same Objects" - ) - return 0 - - # parts match by ObjectID, groups by GroupID + """Selects parts with the same ObjectID and groups with the same GroupID""" def get_key(obj): return obj.get("ObjectID") or obj.get(Group.PROP_GROUP_ID) - keys_to_find = set(filter(None, (get_key(obj) for obj in selected_objects))) + return self.select_matching(get_key, "Select same Objects") - found_matches = [obj for obj in bpy.data.objects if get_key(obj) in keys_to_find] - - if len(found_matches) > 0: + def select_all_groups(self): + found_matches = [obj for obj in bpy.context.view_layer.objects if Group.PROP_GROUP_ID in obj] + if found_matches: blend_utils.select(found_matches) - - return len(found_matches) - len(keys_to_find) - - \ No newline at end of file + return len(found_matches) diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool_operators.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool_operators.py index 2c0851dc..81b620c1 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool_operators.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool_operators.py @@ -23,7 +23,20 @@ def execute(self, context): found_matches = batch_tool.select_same_colored_objects() self.report({'INFO'}, f"found {found_matches} matches") return {"FINISHED"} - + + +class SelectAllSameColoredObjects(bpy.types.Operator): + """Select every part with the same colour, whatever part it is.""" + + bl_idname = "object.nms_select_all_same_colors" + bl_label = "Select all same colors" + bl_options = {"UNDO", "REGISTER"} + + def execute(self, context): + found_matches = context.scene.nms_batch_tool.select_all_same_colored_objects() + self.report({'INFO'}, f"found {found_matches} matches") + return {"FINISHED"} + class SelectSameObjects(bpy.types.Operator): """Select objects with similar colors for easy experimentation.""" @@ -48,13 +61,39 @@ class BatchReplaceTarget(bpy.types.Operator): bl_label = "Batch Replace" bl_options = {"UNDO", "REGISTER"} + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog( + self, title="Batch Replace", confirm_text="Replace" + ) + def execute(self, context): - scene = context.scene - batch_tool = scene.nms_batch_tool - + batch_tool = context.scene.nms_batch_tool replaced_count = batch_tool.batch_replace_with_target_object() self.report({'INFO'}, f"replaced {replaced_count} objects") + + # the target is usually removed by the replace, clearing it while the + # operator is still running makes blender show a warning popup + def clear_picker(): + batch_tool.target_object = None + return None + + bpy.app.timers.register(clear_picker, first_interval=0.0) return {"FINISHED"} + + def draw(self, context): + batch_tool = context.scene.nms_batch_tool + + column = self.layout.column(align=True) + column.label(text="Replace with a", icon="GROUP_VERTEX") + column.row(align=True).prop(batch_tool, "nms_batch_replace_type", expand=True) + column.separator() + if batch_tool.nms_batch_replace_type == "target": + column.label(text="Replace with a target object") + column.prop(batch_tool, "target_object", text="") + else: + column.label(text="Replace with ObjectId") + column.prop(batch_tool, "object_id", text="") + column.separator() class BatchSelectRandom(bpy.types.Operator): @@ -239,13 +278,26 @@ def apply_pattern(self, context): if enabled: obj.select_set(False) - - - + +class SelectAllGroups(bpy.types.Operator): + """Select every group in the scene.""" + + bl_idname = "object.nms_select_all_groups" + bl_label = "Select Groups" + bl_options = {"UNDO", "REGISTER"} + + def execute(self, context): + group_count = context.scene.nms_batch_tool.select_all_groups() + self.report({'INFO'}, f"found {group_count} groups") + return {"FINISHED"} + + classes = ( SelectSameColoredObjects, + SelectAllSameColoredObjects, SelectSameObjects, BatchReplaceTarget, BatchSelectRandom, - BatchSelectPattern + BatchSelectPattern, + SelectAllGroups, ) \ No newline at end of file diff --git a/src/addons/no_mans_sky_base_builder/tools/batch_tool_presentation.py b/src/addons/no_mans_sky_base_builder/tools/batch_tool_presentation.py index 903f2d57..cb6ae156 100644 --- a/src/addons/no_mans_sky_base_builder/tools/batch_tool_presentation.py +++ b/src/addons/no_mans_sky_base_builder/tools/batch_tool_presentation.py @@ -20,35 +20,26 @@ def poll(self, context): def draw(self, context): layout = self.layout - scene = context.scene - batch_tool = scene.nms_batch_tool - - top_row = layout.row(align = True) + batch_column = layout.column(align = True) + top_row = batch_column.row(align = True) select_box = top_row.box() - select_box.label(text = "Batch Select") select_col = select_box.column(align = True) select_col.label(text = "Select objects with") - select_col.operator("object.nms_select_same_colors", text = "Same Colour", icon = "MOD_SOFT") + select_color_row = select_col.row(align = True) + select_color_row.operator("object.nms_select_same_colors", text = "Same Colour", icon = "MOD_SOFT") + # same colour on any part, not only the same part + select_color_row.operator("object.nms_select_all_same_colors", text = "", icon = "TEXTURE") select_col.operator("object.nms_select_same_objects", text = "Same ObjectID", icon = "CON_SIZELIKE") - - more_options_box = top_row.box() - more_options_box.label(text = "Selection Tools",) + select_col.operator("object.nms_select_all_groups", text = "Select all Groups", icon = "OUTLINER_OB_GROUP_INSTANCE") + + more_options_box = top_row.box() more_options_column = more_options_box.column(align = True) + more_options_column.label(text = "Selection Tools") more_options_column.operator("object.nms_select_duplicates", text = "Find Duplicates", icon = "BRUSH_DATA") more_options_column.operator("object.nms_batch_select_random", text = "Select Random", icon = "MOD_NOISE") more_options_column.operator("object.nms_batch_select_pattern", text = "Select Pattern", icon = "MOD_OCEAN") - - batch_replace_box = layout.box() - batch_replace_box.label(text = "Batch Replace", icon = "GROUP_VERTEX") - batch_replace_col = batch_replace_box.column(align = True) - #batch_replace_col.label(text = "Replace with :") - batch_replace_col.row(align = True).prop(batch_tool,"nms_batch_replace_type", expand = True) - batch_replace_col.separator() - if batch_tool.nms_batch_replace_type == "target": - batch_replace_col.label(text = "Replace with a target object") - batch_replace_col.prop(batch_tool,"target_object", text = "") - else : - batch_replace_col.label(text = "Replace with ObjectId") - batch_replace_col.prop(batch_tool,"object_id", text = "") - batch_replace_col.separator() - batch_replace_col.operator("object.nms_batch_replace_target", text = "Batch Replace", icon = "CON_FOLLOWPATH") \ No newline at end of file + + # the replace options are in the operator's dialog + batch_replace_box = batch_column.box().column(align = True) + batch_replace_box.label(text = "Batch Replace") + batch_replace_box.operator("object.nms_batch_replace_target", text = "Batch Replace", icon = "GROUP_VERTEX") \ No newline at end of file diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool.py b/src/addons/no_mans_sky_base_builder/tools/build_tool.py index ccc53766..09b3c650 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool.py @@ -7,7 +7,7 @@ from .. import part from ..builder import get_builder from ..group import Group -from ..utils.mirror_utils import ShowMessageBox +from ..utils.blend_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") diff --git a/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py b/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py index 960a8ffa..1d23bbea 100644 --- a/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py +++ b/src/addons/no_mans_sky_base_builder/tools/build_tool_operators.py @@ -1,6 +1,6 @@ import bpy from ..utils import blend_utils, curve -from ..utils.mirror_utils import ShowMessageBox +from ..utils.blend_utils import ShowMessageBox from mathutils import Vector diff --git a/src/addons/no_mans_sky_base_builder/tools/properties.py b/src/addons/no_mans_sky_base_builder/tools/properties.py index f95a10fe..b6872ce8 100644 --- a/src/addons/no_mans_sky_base_builder/tools/properties.py +++ b/src/addons/no_mans_sky_base_builder/tools/properties.py @@ -5,7 +5,7 @@ from ..utils import blend_utils, curve from ..utils import python as python_utils from .. import part -from ..utils.mirror_utils import ShowMessageBox +from ..utils.blend_utils import ShowMessageBox FILE_PATH = os.path.dirname(os.path.realpath(__file__)) NICE_JSON = os.path.join(FILE_PATH,"..","resources","nice_names.json") diff --git a/src/addons/no_mans_sky_base_builder/tools/properties_presentation.py b/src/addons/no_mans_sky_base_builder/tools/properties_presentation.py index fec2f9c6..8dd50794 100644 --- a/src/addons/no_mans_sky_base_builder/tools/properties_presentation.py +++ b/src/addons/no_mans_sky_base_builder/tools/properties_presentation.py @@ -30,7 +30,7 @@ def draw(self, context): properties_box = layout.box() properties_column = properties_box.column(align=True) - properties_column.label(text = "Base Properties", icon_value = home_icon.icon_id) + properties_column.label(text = "Base Properties", icon = "HOME") base_prop_split = properties_column.split(factor = 0.3) base_label_col = base_prop_split.column(align = True) base_label_col.label(text = "Base Name :") diff --git a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py index f21df34f..86e45dab 100644 --- a/src/addons/no_mans_sky_base_builder/utils/blend_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/blend_utils.py @@ -8,6 +8,19 @@ from ..utils import blend_utils +def ShowMessageBox(message="", title="Message Box", icon="INFO"): + """Show a message in a popup, or print it when there is no window.""" + # popups crash blender when there is no window, e.g. in background mode + if bpy.app.background or bpy.context.window_manager is None or not bpy.context.window_manager.windows: + print(f"{title}: {message}") + return + + def draw(self, context): + self.layout.label(text=message) + + bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) + + def load_plugin(plugin_name): """Load a blender plugin.""" is_enabled, _ = addon_utils.check(plugin_name) diff --git a/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py b/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py index 7aabe784..de60bae3 100644 --- a/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py +++ b/src/addons/no_mans_sky_base_builder/utils/mirror_utils.py @@ -25,12 +25,6 @@ def load_mirror_corrections(): } -def ShowMessageBox(message="", title="Message Box", icon="INFO"): - def draw(self, context): - self.layout.label(text=message) - bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - - #This function mirrors matrix world across x axis def mirror_matrix_world(object_id, old_matrix_world, across_x=True): From a3a88cd546262bfbf54555394a453305d8be47f6 Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:17:50 +0530 Subject: [PATCH 13/15] Simplified workspace can now be reversed, It also removes undesireable buttons from toolbar ( Buttons on left side of viewport ) . --- .../no_mans_sky_base_builder/__init__.py | 33 +- .../utils/workspace.py | 406 +++++++++++++++--- 2 files changed, 378 insertions(+), 61 deletions(-) diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 5652fa9b..02caeafe 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -293,6 +293,8 @@ class NMSSettings(PropertyGroup): is_workspace_cleaned: BoolProperty( name="Is Workspace Cleaned", description="Check if workspace has been cleaned by user", default=False ) + # what simplifying changed, so restoring can put it back + workspace_state: StringProperty(name="Workspace State", default="") room_vis_switch: IntProperty(name="room_vis_switch", default=0) @@ -769,7 +771,9 @@ def draw(self, context): workspace_column = workspace_box.column(align = True) workspace_column.label(text = "Workspace") workspace_column.operator("object.nms_launch_asset_browser", text = "Launch Asset Browser", icon = "DESKTOP") - if not nms_tool.is_workspace_cleaned: + if nms_tool.is_workspace_cleaned: + workspace_column.operator("object.nms_cleanup_workspace", text = "Restore Blender Workspace", icon = "LOOP_BACK") + else: workspace_column.operator("object.nms_cleanup_workspace", text = "Simplify Blender Workspace", icon = "WORKSPACE") @@ -1189,15 +1193,19 @@ def execute(self, context): class SwitchWorkspace(bpy.types.Operator): - """Switch to a simpler workspace""" + """Switch to a simpler workspace, or back to how blender was before""" bl_idname = "object.nms_cleanup_workspace" bl_label = "Switch workspace" def execute(self, context): - scene = context.scene - nms_tool = scene.nms_base_tool - nms_tool.is_workspace_cleaned = True - workspace.cleanup_workspace(context) + nms_tool = context.scene.nms_base_tool + if nms_tool.is_workspace_cleaned: + workspace.restore_workspace(context, nms_tool.workspace_state) + nms_tool.workspace_state = "" + nms_tool.is_workspace_cleaned = False + else: + nms_tool.workspace_state = workspace.cleanup_workspace(context) or "" + nms_tool.is_workspace_cleaned = True return {"FINISHED"} class SaveAsPreset(bpy.types.Operator): @@ -1972,6 +1980,12 @@ def reset_plugin_state(dummy): save_data = scene.nms_save_data save_data.check_plugin_enabled = False + # the toolbar trim isn't saved in the file, so apply it again for a simplified workspace + if bpy.context.scene.nms_base_tool.is_workspace_cleaned: + workspace.hide_viewport_tools() + else: + workspace.restore_viewport_tools() + last_active = None # keep track of active object to display or hide additional options related to that object @persistent @@ -2211,8 +2225,11 @@ def unregister(): if curve_udpate_handler in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(curve_udpate_handler) - - + + # the toolbar is patched while the workspace is simplified + workspace.restore_viewport_tools() + + if __name__ == "__main__": diff --git a/src/addons/no_mans_sky_base_builder/utils/workspace.py b/src/addons/no_mans_sky_base_builder/utils/workspace.py index c51cae9a..1e753aa6 100644 --- a/src/addons/no_mans_sky_base_builder/utils/workspace.py +++ b/src/addons/no_mans_sky_base_builder/utils/workspace.py @@ -1,78 +1,378 @@ +import json +import os +import uuid import bpy +from ..builder import paths + +SIMPLIFIED_WORKSPACE_NAME = "No Man's Sky Base & Corvette Builder" + +# deleted workspaces are backed up here, blender can't bring a deleted workspace back +BACKUP_PATH = os.path.join(paths.USER_PATH, "workspace_backups") + # Workspace Cleanup -def cleanup_workspace(context) -> None: - - new_workspace_name = "No Man's Sky Base & Corvette Builder" - +# simplify blender to one workspace for base building +# returns json of everything changed, used by restore_workspace +def cleanup_workspace(context): layout_ws = bpy.data.workspaces.get("Layout") if layout_ws is None: - return - - # Snapshot list before iterating because collection changes during deletion + return None + + win = context.window + state = { + "layout_name": layout_ws.name, + "active_workspace": win.workspace.name if win.workspace else layout_ws.name, + "deleted_workspaces": [], + "backup_file": None, + "statusbar": {screen.name: screen.show_statusbar for screen in bpy.data.screens}, + "properties": snapshot_properties_areas(), + "timeline_closed": False, + } + + # snapshot list before iterating because collection changes during deletion other_workspaces = [ws for ws in bpy.data.workspaces if ws != layout_ws] + if other_workspaces: + state["deleted_workspaces"] = [ws.name for ws in other_workspaces] + state["backup_file"] = backup_workspaces(other_workspaces) + for ws in other_workspaces: with bpy.context.temp_override(workspace=ws): bpy.ops.workspace.delete() - # Switch to Layout and rename it - bpy.context.window.workspace = layout_ws - layout_ws.name = new_workspace_name - - # remove timeline area from bttom of layout workspace - remove_timeline_area() + # switch to layout and rename it + win.workspace = layout_ws + layout_ws.name = SIMPLIFIED_WORKSPACE_NAME + + # remove timeline area from bottom of layout workspace + state["timeline_closed"] = remove_timeline_area(win) # remove extra tabs from properties panel simplify_object_properties() - + # hide status bar for screen in bpy.data.screens: screen.show_statusbar = False - - -def remove_timeline_area(): - win = bpy.context.window + + # trim the viewport toolbar down to what base building uses + hide_viewport_tools(context) + + return json.dumps(state) + + +# undo cleanup_workspace using the state it returned +def restore_workspace(context, state_json): + try: + state = json.loads(state_json) if state_json else {} + except ValueError: + state = {} + + restore_viewport_tools() + + win = context.window + layout_ws = bpy.data.workspaces.get(SIMPLIFIED_WORKSPACE_NAME) or win.workspace + if layout_ws is not None: + layout_ws.name = state.get("layout_name", "Layout") + + restore_backed_up_workspaces(win, state) + + if layout_ws is not None: + win.workspace = layout_ws + + if state.get("timeline_closed", True): + add_timeline_area(win) + + restore_properties_areas(state.get("properties", [])) + + saved_statusbar = state.get("statusbar", {}) + for screen in bpy.data.screens: + screen.show_statusbar = saved_statusbar.get(screen.name, True) + + # go back to the workspace that was open before simplifying + active_ws = bpy.data.workspaces.get(state.get("active_workspace", "")) + if active_ws is not None: + win.workspace = active_ws + + +# Workspace Backup +# save workspaces to a blend file, returns its path +def backup_workspaces(workspaces): + os.makedirs(BACKUP_PATH, exist_ok=True) + backup_file = os.path.join(BACKUP_PATH, "{}.blend".format(uuid.uuid4())) + if bpy.app.version >= (5, 1, 0): + bpy.data.libraries.write(backup_file, set(workspaces), compress=True) + else: + # libraries.write crashes blender 4.5 when given workspaces or screens, + # a copy of the whole file holds them too + bpy.ops.wm.save_as_mainfile(filepath=backup_file, copy=True, compress=True) + return backup_file + + +# add backed up workspaces back as normal workspace tabs, then delete the backup +def restore_backed_up_workspaces(win, state): + backup_file = state.get("backup_file") + if not backup_file or not os.path.isfile(backup_file): + return + + for name in state.get("deleted_workspaces", []): + if name in bpy.data.workspaces: + continue + with bpy.context.temp_override(window=win): + bpy.ops.workspace.append_activate(idname=name, filepath=backup_file) + + try: + os.remove(backup_file) + except OSError: + pass + + +# Areas +# close every timeline area, returns True if one was closed +def remove_timeline_area(win): screen = win.screen - timeline_area_panel_filter = ( area for area in screen.areas if area.type == "DOPESHEET_EDITOR" and area.spaces.active.mode == "TIMELINE") - timeline_area_iterator = next(timeline_area_panel_filter,None) - if timeline_area_iterator: - with bpy.context.temp_override(window=win, screen=screen, area=timeline_area_iterator): + # snapshot first, closing an area changes screen.areas + timeline_areas = [ + area for area in screen.areas + if area.type == "DOPESHEET_EDITOR" and area.spaces.active.mode == "TIMELINE" + ] + for area in timeline_areas: + with bpy.context.temp_override(window=win, screen=screen, area=area): bpy.ops.screen.area_close() - - -def simplify_object_properties(): - # Loop through all open windows to find the active screen reliably + return bool(timeline_areas) + + +# split the biggest 3d viewport and turn its lower part into a timeline +def add_timeline_area(win): + screen = win.screen + if any(area.type == "DOPESHEET_EDITOR" and area.spaces.active.mode == "TIMELINE" for area in screen.areas): + return + + view3d_area = max( + (area for area in screen.areas if area.type == "VIEW_3D"), + key=lambda area: area.width * area.height, + default=None, + ) + if view3d_area is None: + return + + areas_before = set(screen.areas) + with bpy.context.temp_override(window=win, screen=screen, area=view3d_area): + bpy.ops.screen.area_split(direction="HORIZONTAL", factor=0.15) + + new_areas = [area for area in screen.areas if area not in areas_before] + if not new_areas: + return + + # the split leaves two areas, the lower one becomes the timeline + split_areas = new_areas + [view3d_area] + timeline_area = min(split_areas, key=lambda area: area.y) + for area in split_areas: + area.type = "VIEW_3D" + timeline_area.type = "DOPESHEET_EDITOR" + timeline_area.spaces.active.mode = "TIMELINE" + + +# yields (screen name, index, area) for every properties area in every window +def get_properties_areas(): for window in bpy.context.window_manager.windows: - - # Check if the window actually has a screen right now if not window.screen: continue - - for area in window.screen.areas: - if area.type != 'PROPERTIES': - continue - - space = area.spaces.active - if not space: - continue - - # Loop through all attributes in the space properties - for attr in dir(space): - if attr.startswith("show_properties_"): - try: - setattr(space, attr, False) - except AttributeError: - pass - - # Turn the Object tab back on and set context - # there can be an exception when no object is selected, at that time, there is no objects porperty in tabs + properties_areas = [area for area in window.screen.areas if area.type == "PROPERTIES"] + for index, area in enumerate(properties_areas): + yield window.screen.name, index, area + + +# record the visible tabs and active tab of every properties area +def snapshot_properties_areas(): + snapshot = [] + for screen_name, index, area in get_properties_areas(): + space = area.spaces.active + if not space: + continue + snapshot.append({ + "screen": screen_name, + "index": index, + "context": space.context, + "tabs": {attr: getattr(space, attr) for attr in dir(space) if attr.startswith("show_properties_")}, + }) + return snapshot + + +def restore_properties_areas(snapshot): + saved = {(entry["screen"], entry["index"]): entry for entry in snapshot} + for screen_name, index, area in get_properties_areas(): + space = area.spaces.active + entry = saved.get((screen_name, index)) + if not space: + continue + + tabs = entry["tabs"] if entry else {} + for attr in dir(space): + if attr.startswith("show_properties_"): + try: + setattr(space, attr, tabs.get(attr, True)) + except (AttributeError, TypeError): + pass + + if entry: try: - space.show_properties_object = True - space.context = 'OBJECT' + space.context = entry["context"] except TypeError: pass - - # Refresh the UI - area.tag_redraw() - + area.tag_redraw() + + +# hide every properties tab except object +def simplify_object_properties(): + for _screen_name, _index, area in get_properties_areas(): + space = area.spaces.active + if not space: + continue + + for attr in dir(space): + if attr.startswith("show_properties_"): + try: + setattr(space, attr, False) + except AttributeError: + pass + + # there is no object tab when no object is selected + try: + space.show_properties_object = True + space.context = 'OBJECT' + except TypeError: + pass + + area.tag_redraw() + + +# Toolbar Cleanup +# tools removed from the 3d viewport toolbar, none of them are used for base building +HIDDEN_VIEW3D_TOOLS = { + "builtin.annotate", + "builtin.annotate_line", + "builtin.annotate_polygon", + "builtin.annotate_eraser", + "builtin.measure", + "builtin.primitive_cube_add", + "builtin.primitive_cone_add", + "builtin.primitive_cylinder_add", + "builtin.primitive_uv_sphere_add", + "builtin.primitive_ico_sphere_add", + # the breakdowner button, in object mode since blender 5.2 + "builtin.breakdowner", + "builtin.push", + "builtin.relax", +} + +# blender's own VIEW3D_PT_tools_active.tools_from_context, kept to restore the toolbar +ORIGINAL_TOOLS_FROM_CONTEXT = None + + +# drop hidden tools from toolbar entries +# an entry is None (separator), a ToolDef, a tuple of ToolDefs or a callable +def strip_tool_items(items): + result = [] + for item in items: + if item is None: + # only keep a separator that follows something kept + if result and result[-1] is not None: + result.append(None) + continue + + # ToolDef is a namedtuple, so it is checked before tuples + if hasattr(item, "idname"): + if item.idname not in HIDDEN_VIEW3D_TOOLS: + result.append(item) + continue + + if isinstance(item, (tuple, list)): + kept = strip_tool_items(item) + if kept: + result.append(type(item)(kept)) + continue + + # callables generate their tools at draw time + result.append(item) + + # a trailing separator draws a gap at the bottom of the toolbar + while result and result[-1] is None: + result.pop() + + return result + + +def get_view3d_tool_panel(): + from bl_ui.space_toolsystem_toolbar import VIEW3D_PT_tools_active + + return VIEW3D_PT_tools_active + + +def redraw_toolbars(): + window_manager = bpy.data.window_managers[0] if bpy.data.window_managers else None + if window_manager is None: + return + for window in window_manager.windows: + for area in window.screen.areas: + if area.type == "VIEW_3D": + area.tag_redraw() + + +# switch any window using a hidden tool back to the select tool +def reset_hidden_active_tools(context=None): + window_manager = bpy.data.window_managers[0] if bpy.data.window_managers else None + if window_manager is None: + return + + mode = getattr(context or bpy.context, "mode", None) or "OBJECT" + + # windows come from the window manager, context.workspace isn't reliable right after the switch + for window in window_manager.windows: + workspace = window.workspace + if workspace is None: + continue + + active = workspace.tools.from_space_view3d_mode(mode, create=False) + if active is None or active.idname not in HIDDEN_VIEW3D_TOOLS: + continue + + area = next((a for a in window.screen.areas if a.type == "VIEW_3D"), None) + if area is None: + continue + + with bpy.context.temp_override(window=window, screen=window.screen, area=area): + bpy.ops.wm.tool_set_by_id(name="builtin.select_box", space_type="VIEW_3D") + + +# remove hidden tools from the toolbar +# wraps tools_from_context, the hook blender uses to filter its tools, +# so registering and unregistering tools keeps working normally +def hide_viewport_tools(context=None): + global ORIGINAL_TOOLS_FROM_CONTEXT + + panel = get_view3d_tool_panel() + + if ORIGINAL_TOOLS_FROM_CONTEXT is None: + ORIGINAL_TOOLS_FROM_CONTEXT = panel.tools_from_context.__func__ + original = ORIGINAL_TOOLS_FROM_CONTEXT + + def tools_from_context(cls, context, mode=None): + yield from strip_tool_items(original(cls, context, mode)) + + panel.tools_from_context = classmethod(tools_from_context) + + reset_hidden_active_tools(context) + redraw_toolbars() + + +# put the stock blender toolbar back +def restore_viewport_tools(): + global ORIGINAL_TOOLS_FROM_CONTEXT + + if ORIGINAL_TOOLS_FROM_CONTEXT is None: + return + + get_view3d_tool_panel().tools_from_context = classmethod(ORIGINAL_TOOLS_FROM_CONTEXT) + ORIGINAL_TOOLS_FROM_CONTEXT = None + + redraw_toolbars() From b584c662117f2f3c6ee50d0e19bfda5c525e9e6e Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:14:51 +0530 Subject: [PATCH 14/15] Major optimistaion to duplicate objects. Duplicating objects with Shift+D will not create unique copies of meshes for each of them. Added a checkbox in addon preferences to disable it. Moved Addon Preferences into a seperate module. --- .../no_mans_sky_base_builder/__init__.py | 34 ++++--- .../addon_preferences.py | 94 +++++++++++++++++++ .../save_editor/save_editor_operators.py | 17 ++-- .../save_editor/save_editor_presentation.py | 20 +--- .../save_editor/save_editor_utils.py | 9 +- .../utils/material.py | 42 +++++++++ 6 files changed, 169 insertions(+), 47 deletions(-) create mode 100644 src/addons/no_mans_sky_base_builder/addon_preferences.py diff --git a/src/addons/no_mans_sky_base_builder/__init__.py b/src/addons/no_mans_sky_base_builder/__init__.py index 02caeafe..7e73f277 100644 --- a/src/addons/no_mans_sky_base_builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/__init__.py @@ -19,6 +19,8 @@ from . import icons, part, preset from .builder import get_builder from .group import Group +from . import addon_preferences +from .addon_preferences import NMSAddonPreferences from .utils.blend_utils import ShowMessageBox from .part_overrides import line from .save_editor import save_editor_operators, save_editor_utils @@ -47,7 +49,6 @@ NICE_JSON = os.path.join(FILE_PATH, "resources", "nice_names.json") nice_name_dictionary = python_utils.load_dictionary(NICE_JSON) -ADDON_ID = __package__ # Setting Support Methods --- @@ -641,6 +642,9 @@ def apply_colour(self, colour_index=0, material=0): else : _material.assign_material(obj, int(colour_index), int(maeterial_index)) + # recolouring gives parts their own mesh, parts now the same colour share one again + _material.optimise_materials() + # Refresh the viewport. bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) @@ -674,6 +678,9 @@ def apply_default_colour(self): index = 23 _material.assign_default_material(obj, index=index) + # recolouring gives parts their own mesh, parts now the same colour share one again + _material.optimise_materials() + # Refresh the viewport. bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) @@ -1980,6 +1987,11 @@ def reset_plugin_state(dummy): save_data = scene.nms_save_data save_data.check_plugin_enabled = False + # identical parts share one mesh, which keeps big bases light in memory and on disk + relinked, removed = _material.optimise_materials() + if relinked: + print(f"NMS: {relinked} parts now share meshes, {removed} duplicate meshes removed") + # the toolbar trim isn't saved in the file, so apply it again for a simplified workspace if bpy.context.scene.nms_base_tool.is_workspace_cleaned: workspace.hide_viewport_tools() @@ -2070,17 +2082,6 @@ def curve_udpate_handler(scene, depsgraph): known_curve_names |= updated_curve_names -class NMSAddonPreferences(bpy.types.AddonPreferences): - bl_idname = ADDON_ID - - nms_save_folder_path: StringProperty( - name="Save Directory", - description="Folder where save files are stored", - subtype='DIR_PATH', - default = str(save_editor_utils.get_default_save_folder()) - ) - - preview_collections = {} # Plugin Registration --- @@ -2198,11 +2199,14 @@ def register(): if curve_udpate_handler not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(curve_udpate_handler) - - - + + addon_preferences.apply_duplicate_mesh_setting() + def unregister(): + # put blender's Duplicate Data > Mesh back the way it was + addon_preferences.restore_duplicate_mesh_setting() + for pcoll in preview_collections.values(): bpy.utils.previews.remove(pcoll) preview_collections.clear() diff --git a/src/addons/no_mans_sky_base_builder/addon_preferences.py b/src/addons/no_mans_sky_base_builder/addon_preferences.py new file mode 100644 index 00000000..db6afa2b --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/addon_preferences.py @@ -0,0 +1,94 @@ +import bpy +from bpy.props import BoolProperty, StringProperty + +from .save_editor import save_editor_utils + +ADDON_ID = __package__ + + +class NMSAddonPreferences(bpy.types.AddonPreferences): + bl_idname = ADDON_ID + + nms_save_folder_path: StringProperty( + name="Save Directory", + description="Folder where save files are stored", + subtype='DIR_PATH', + default = str(save_editor_utils.get_default_save_folder()) + ) + + share_meshes_on_duplicate: BoolProperty( + name="Share part meshes on duplicate", + description=( + "Turn off blender's Duplicate Data > Mesh, so Shift+D copies share the " + "original part's mesh, like Alt+D. Keeps big bases light" + ), + default=True, + update=lambda self, context: apply_duplicate_mesh_setting(self), + ) + + # blender's own Duplicate Data > Mesh value, put back when sharing is turned off + original_duplicate_mesh: BoolProperty(default=True) + has_original_duplicate_mesh: BoolProperty(default=False) + + def draw(self, context): + layout = self.layout + layout.prop(self, "nms_save_folder_path") + layout.prop(self, "share_meshes_on_duplicate") + + +def get_addon_preferences(): + addon = bpy.context.preferences.addons.get(ADDON_ID) + return addon.preferences if addon else None + + +# the NMS save folder, None while the addon's preferences aren't available +def get_save_folder_path(): + prefs = get_addon_preferences() + return prefs.nms_save_folder_path if prefs else None + + +# store the NMS save folder and write the user preferences to disk +# returns True when the folder was stored +def set_save_folder_path(path, save=True): + prefs = get_addon_preferences() + if prefs is None: + return False + + prefs.nms_save_folder_path = str(path) + if save: + save_user_preferences() + return True + + +def save_user_preferences(): + # the operator needs a window, there is none in background mode + try: + bpy.ops.wm.save_userpref() + except RuntimeError as error: + print("Could not save preferences:", error) + + +# turn blender's Duplicate Data > Mesh off while sharing is on, remembering its value +def apply_duplicate_mesh_setting(prefs=None): + prefs = prefs or get_addon_preferences() + if prefs is None: + return + + if not prefs.share_meshes_on_duplicate: + restore_duplicate_mesh_setting(prefs) + return + + edit_preferences = bpy.context.preferences.edit + if not prefs.has_original_duplicate_mesh: + prefs.original_duplicate_mesh = edit_preferences.use_duplicate_mesh + prefs.has_original_duplicate_mesh = True + edit_preferences.use_duplicate_mesh = False + + +# put blender's Duplicate Data > Mesh back the way it was +def restore_duplicate_mesh_setting(prefs=None): + prefs = prefs or get_addon_preferences() + if prefs is None or not prefs.has_original_duplicate_mesh: + return + bpy.context.preferences.edit.use_duplicate_mesh = prefs.original_duplicate_mesh + prefs.has_original_duplicate_mesh = False diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py index edd3e1a0..6a85e907 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_operators.py @@ -1,7 +1,7 @@ import bpy from . import save_editor_utils -ADDON_ID = __package__.rsplit(".", 1)[0] +from .. import addon_preferences # Operator for button to select save folder directory class SelectSaveFolder(bpy.types.Operator): @@ -15,30 +15,25 @@ class SelectSaveFolder(bpy.types.Operator): ) def execute(self, context): - prefs = context.scene print("folder is :", self.directory) if save_editor_utils.validate_save_folder(str(self.directory)): - - prefs = context.preferences.addons[ADDON_ID].preferences - prefs.nms_save_folder_path = self.directory - bpy.ops.wm.save_userpref() - + + addon_preferences.set_save_folder_path(self.directory) + scene = context.scene save_data = scene.nms_save_data save_data.on_check_plugin_enabled(context) save_data.nms_account_selected = "Default" self.report({'INFO'}, f"Selected folder is valid NMS save folder") - print("Selected folder is valid:", prefs.nms_save_folder_path) + print("Selected folder is valid:", addon_preferences.get_save_folder_path()) else : self.report({'ERROR'}, f"Selected folder is not a NMS save folder. Please select the correct folder.") print("Selected folder is invalid:", self.directory) return {'FINISHED'} def invoke(self, context, event): - prefs = context.preferences.addons[ADDON_ID].preferences - save_root_folder = prefs.nms_save_folder_path - self.directory = save_root_folder + self.directory = addon_preferences.get_save_folder_path() or "" context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py index 554f31ae..26647e66 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_presentation.py @@ -8,30 +8,16 @@ import bpy from bpy.types import Panel -from .. import icons +from .. import addon_preferences, icons from .save_editor_utils import BaseType from .save_manager import SaveManager -ADDON_ID = __package__.rsplit(".", 1)[0] - def get_save_data(context): """The save editor's property group, or None if it is not registered yet.""" return getattr(context.scene, "nms_save_data", None) -def get_save_folder_path(context): - """The configured save folder, or None if the preferences are unavailable. - - A draw can run before the addon's preferences exist - during registration, - or after a disable while the header is still on screen - so this never - assumes the lookup succeeds. - """ - addon = context.preferences.addons.get(ADDON_ID) - if addon is None: - return None - return addon.preferences.nms_save_folder_path - def draw_pinned_base(container, save_data, base_props): """Draw the pinned base UI section if a base is pinned.""" if not save_data.pinned_base_check: @@ -74,7 +60,7 @@ def draw_pinned_base(container, save_data, base_props): def draw_base_picker(container, save_data): - save_folder_path = get_save_folder_path(bpy.context) + save_folder_path = addon_preferences.get_save_folder_path() # Make a separate section to display elements related to selecting bases. save_folder_box = container @@ -201,7 +187,7 @@ def draw_save_manager(layout, context): """ save_data = get_save_data(context) base_props = getattr(context.scene, "nms_base_tool", None) - save_folder_path = get_save_folder_path(context) + save_folder_path = addon_preferences.get_save_folder_path() if save_data is None or base_props is None or save_folder_path is None: layout.label(text="Save Manager unavailable", icon="ERROR") diff --git a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py index 04697d5a..792f3c26 100644 --- a/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py +++ b/src/addons/no_mans_sky_base_builder/save_editor/save_editor_utils.py @@ -11,10 +11,10 @@ from . import save_translation from .save_translation import SaveTranslation +from .. import addon_preferences from ..utils.blend_utils import ShowMessageBox system = platform.system() -ADDON_ID = __package__.rsplit(".", 1)[0] class BaseType: CORVETTE = "PlayerShipBase" @@ -114,9 +114,10 @@ def get_hg_files_in_folder(folder): #Returns all account folders def get_accounts_list(context): - prefs = context.preferences.addons[ADDON_ID].preferences - save_folder_path = prefs.nms_save_folder_path - + save_folder_path = addon_preferences.get_save_folder_path() + if not save_folder_path or not os.path.isdir(save_folder_path): + return [] + steam_names = get_steam_names() accounts_list = [] root_dir = Path(save_folder_path) diff --git a/src/addons/no_mans_sky_base_builder/utils/material.py b/src/addons/no_mans_sky_base_builder/utils/material.py index 17b97065..c6d9bfcb 100644 --- a/src/addons/no_mans_sky_base_builder/utils/material.py +++ b/src/addons/no_mans_sky_base_builder/utils/material.py @@ -128,6 +128,11 @@ def set_material(item, material): # Don't bother if we can't even apply material to object. if not hasattr(item.data, "materials"): return + + # colour lives on the mesh, so a shared mesh would recolour every part using it + if item.data.users > 1 and "curve_parent" not in item: + item.data = item.data.copy() + # Assign Material if not item.data.materials: # Add the material to the object @@ -286,3 +291,40 @@ def assign_material(item, colour_index=0, material_index=0): set_material(item, material) return material + +# parts with the same ObjectID and UserData share one mesh +# override classes and curve followers are left alone, they manage their own meshes +# returns (parts relinked, meshes removed) +def optimise_materials(): + from ..builder import overrides + + shared_meshes = {} + replaced_meshes = set() + relinked = 0 + + for obj in bpy.data.objects: + if obj.type != "MESH" or "ObjectID" not in obj or "curve_parent" in obj: + continue + if overrides.get_override_class(obj["ObjectID"]) is not None: + continue + + mesh = obj.data + # a shape key or an open edit session would be lost by swapping the mesh + if mesh.shape_keys or mesh.is_editmode: + continue + + # the first part's mesh is reused, nothing is copied + key = (obj["ObjectID"], str(obj.get("UserData", 0))) + shared_mesh = shared_meshes.setdefault(key, mesh) + if shared_mesh != mesh: + obj.data = shared_mesh + replaced_meshes.add(mesh) + relinked += 1 + + # one batch, a remove() per mesh re-syncs the whole file each time + unused_meshes = [mesh for mesh in replaced_meshes if mesh.users == 0] + if unused_meshes: + bpy.data.batch_remove(unused_meshes) + + return relinked, len(unused_meshes) + From 7aa5e283fd8287e94989c205ce2e9407a14c0a2a Mon Sep 17 00:00:00 2001 From: Kuma the Wizard <277236726+kuma-the-wizard@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:04:19 +0530 Subject: [PATCH 15/15] Material logic can now be replaced by other addons. Moved material creation and assignment into a MaterialProvider class in utils/material_provider.py, other addons can subclass it and activate it with set_material_provider() from their register() set_material_provider(None) restores the default . --- .../builder/__init__.py | 12 +- .../utils/material.py | 181 ++------------ .../utils/material_provider.py | 236 ++++++++++++++++++ 3 files changed, 268 insertions(+), 161 deletions(-) create mode 100644 src/addons/no_mans_sky_base_builder/utils/material_provider.py diff --git a/src/addons/no_mans_sky_base_builder/builder/__init__.py b/src/addons/no_mans_sky_base_builder/builder/__init__.py index bbbaec07..8c7b2c9b 100644 --- a/src/addons/no_mans_sky_base_builder/builder/__init__.py +++ b/src/addons/no_mans_sky_base_builder/builder/__init__.py @@ -1,13 +1,18 @@ """Access to the builder shared by the whole addon. -Other addons can swap the builder or add part classes from their register(): +Other addons can swap the builder, add part classes or supply their own +materials from their register(): nms_builder.set_builder(MyBuilder()) # MyBuilder subclasses Builder nms_builder.register_override(MyPart, ["CUBEROOM"]) + nms_builder.set_material_provider(MyMaterials()) # MyMaterials subclasses MaterialProvider -and undo both from unregister() with set_builder(None) and unregister_override(). +and undo them from unregister() with set_builder(None), unregister_override() +and set_material_provider(None). """ +from ..utils.material_provider import (MaterialProvider, get_material_provider, + set_material_provider) from .builder import Builder from .overrides import register_override, unregister_override @@ -17,6 +22,9 @@ "set_builder", "register_override", "unregister_override", + "MaterialProvider", + "get_material_provider", + "set_material_provider", ] _default_builder = None diff --git a/src/addons/no_mans_sky_base_builder/utils/material.py b/src/addons/no_mans_sky_base_builder/utils/material.py index c6d9bfcb..c726c0f2 100644 --- a/src/addons/no_mans_sky_base_builder/utils/material.py +++ b/src/addons/no_mans_sky_base_builder/utils/material.py @@ -96,200 +96,63 @@ def get_colours_from_palette(palette): return data -def validate_material(colour_name, colour_value): - """Creates or returns a material based on its name. +def darken_color(color, factor=0.6): + """Return a darker version of an RGB color.""" + return [c * factor for c in color] - Args: - colour_name (str): The name of the material. - colour_value (list): RGBA values representing the colour. - Returns: - bpy.Material: The Blender material. - """ - # Retrieve material if it already exists. - colour_material = bpy.data.materials.get(colour_name, None) - # Create material. - if not colour_material: - colour_material = bpy.data.materials.new(name=colour_name) - colour_material.diffuse_color = colour_value - return colour_material +def _provider(): + # imported here because material_provider reads the palette data from this module + from .material_provider import get_material_provider + return get_material_provider() -def set_material(item, material): - """Set the material on an item. - Args: - item (bpy.Object): The Blender object to assign the material to. - material (bpy.Material): The material to assign. +# the addon calls these, they forward to whichever provider is active - Returns: - bpy.Material: The Blender material. - """ - # Don't bother if we can't even apply material to object. - if not hasattr(item.data, "materials"): - return - # colour lives on the mesh, so a shared mesh would recolour every part using it - if item.data.users > 1 and "curve_parent" not in item: - item.data = item.data.copy() +def validate_material(colour_name, colour_value): + return _provider().validate_material(colour_name, colour_value) - # Assign Material - if not item.data.materials: - # Add the material to the object - item.data.materials.append(material) - else: - # If a material already exists, swap it. - item.data.materials[0] = material - return material +def set_material(item, material): + return _provider().set_material(item, material) -def assign_power_material(item): - """Assign light blue material to object. - Args: - item (bpy.Object): The Blender object to assign the material to. - """ - material = validate_material("powerline_material", [0.0, 0.5, 1.0, 0.5]) - set_material(item, material) +def assign_power_material(item): + return _provider().assign_power_material(item) def assign_portal_material(item): - """Assign teal material to object. - - Args: - item (bpy.Object): The Blender object to assign the material to. - """ - material = validate_material("portalline_material", [0.0, 1.0, 1.0, 0.5]) - set_material(item, material) + return _provider().assign_portal_material(item) def assign_pipe_material(item): - """Assign grey material to object. - - Args: - item (bpy.Object): The Blender object to assign the material to. - """ - material = validate_material("pipeline_material", [0.3, 0.3, 0.3, 0.9]) - set_material(item, material) + return _provider().assign_pipe_material(item) def assign_bytebeat_material(item): - """Assign light purple material to object. - - Args: - item (bpy.Object): The Blender object to assign the material to. - """ - material = validate_material("bytebeat_material", [0.8, 0.0, 0.8, 0.5]) - set_material(item, material) + return _provider().assign_bytebeat_material(item) def assign_preset_material(item): - """Assign gold material to object. - - Args: - item (bpy.Object): The Blender object to assign the material to. - """ - # Material name. - material_name = "preset_material" - # Add transparent tag to material name. - item_name = item.get("ObjectID", "") - if item_name in GHOSTED_ITEMS: - material_name += "_transparent" - - material = validate_material(material_name, [0.8, 0.300186, 0.178301, 1.0]) - set_material(item, material) + return _provider().assign_preset_material(item) def assign_default_material(item, index=0): - """Given a blender object. Assign the default material, - - Args: - item (bpy_types.Object): A Blender object. - - Returns: - bpy_types.Material: The material that is applied. - """ - # Apply Custom Variable. - item["UserData"] = str(index) - # Get colour values. - colour_values = [0.8, 0.8, 0.8, 1.0] - # Get or create the material. - material = validate_material("default_material", colour_values) - set_material(item, material) - return material + return _provider().assign_default_material(item, index=index) def get_colour_from_palette_data(colour_index, material_index): - """Get colour data from palette data. - - Args: - index (int): The colour index. - """ - return BAKED_INDEX_COLOURS.get(colour_index, [0.8, 0.8, 0.8, 1.0]) - - -def darken_color(color, factor=0.6): - """Return a darker version of an RGB color.""" - return [c * factor for c in color] + return _provider().get_colour_from_palette_data(colour_index, material_index) def restore_material(item, user_data_value): - # Validate material+color index - try: - user_data_value = int(user_data_value) - except ValueError: - return - col = userdata.get_colour(user_data_value) - mat = userdata.get_material(user_data_value) - assign_material(item, col, mat) + return _provider().restore_material(item, user_data_value) def assign_material(item, colour_index=0, material_index=0): - """Given a blender object. assign a material and UserData index. - - Args: - item (bpy_types.Object): A Blender object. - colour_index (int): The colour index determined by No Man's Sky. - material (str): The material type. - - Returns: - bpy_types.Material: The material that is applied. - """ - # Some Defaults - alpha_value = 1.0 - - # Apply Custom Variable. - reference_value = int(item.get("UserData", 0)) - new_userdata_value = userdata.update_colour_material( - reference_value, colour_index=colour_index, material_index=material_index - ) - item["UserData"] = str(new_userdata_value) - - # Create Material - colour_name = "{0}_material".format(new_userdata_value) - # Add transparent tag to material name. - item_name = item.get("ObjectID", "") - if item_name in GHOSTED_ITEMS: - colour_name += "_transparent" - - # Get colour values. - primary = get_colour_from_palette_data(colour_index, material_index) - if isinstance(primary, str): - primary = eval(primary) - if len(primary) < 4: - primary.append(alpha_value) - - # Get or create the material. - material = validate_material(colour_name, primary) - - # Add Metadata to object. - nice_name = get_nice_name_from_indicies(colour_index, material_index) - if nice_name and ":" in nice_name: - item["readonly:Material"] = nice_name.split(":")[0].strip() - item["readonly:Colour"] = nice_name.split(":")[1].strip() - - set_material(item, material) - return material + return _provider().assign_material(item, colour_index, material_index) # parts with the same ObjectID and UserData share one mesh diff --git a/src/addons/no_mans_sky_base_builder/utils/material_provider.py b/src/addons/no_mans_sky_base_builder/utils/material_provider.py new file mode 100644 index 00000000..62c64051 --- /dev/null +++ b/src/addons/no_mans_sky_base_builder/utils/material_provider.py @@ -0,0 +1,236 @@ +"""The material logic other addons can replace. + +Other addons can supply their own material logic from their register(): + + nms_builder.set_material_provider(MyMaterials()) # MyMaterials subclasses MaterialProvider + +and undo it from unregister() with set_material_provider(None). + +The rest of the addon never calls this module directly, it goes through the +functions in material.py which forward to the active provider. +""" + +import bpy + +from . import userdata +from .material import (BAKED_INDEX_COLOURS, GHOSTED_ITEMS, + get_nice_name_from_indicies) + +__all__ = [ + "MaterialProvider", + "get_material_provider", + "set_material_provider", +] + + +class MaterialProvider: + """Creates and assigns the materials used by the addon. + + Override only what you need, the other methods go through self so + replacing validate_material() alone changes every material the addon makes. + Keep "transparent" in the name of ghosted materials, the ghost toggle + looks for it. + """ + + def validate_material(self, colour_name, colour_value): + """Creates or returns a material based on its name. + + Args: + colour_name (str): The name of the material. + colour_value (list): RGBA values representing the colour. + + Returns: + bpy.Material: The Blender material. + """ + # Retrieve material if it already exists. + colour_material = bpy.data.materials.get(colour_name, None) + # Create material. + if not colour_material: + colour_material = bpy.data.materials.new(name=colour_name) + colour_material.diffuse_color = colour_value + return colour_material + + def set_material(self, item, material): + """Set the material on an item. + + Args: + item (bpy.Object): The Blender object to assign the material to. + material (bpy.Material): The material to assign. + + Returns: + bpy.Material: The Blender material. + """ + # Don't bother if we can't even apply material to object. + if not hasattr(item.data, "materials"): + return + + # colour lives on the mesh, so a shared mesh would recolour every part using it + if item.data.users > 1 and "curve_parent" not in item: + item.data = item.data.copy() + + # Assign Material + if not item.data.materials: + # Add the material to the object + item.data.materials.append(material) + else: + # If a material already exists, swap it. + item.data.materials[0] = material + return material + + def assign_power_material(self, item): + """Assign light blue material to object. + + Args: + item (bpy.Object): The Blender object to assign the material to. + """ + material = self.validate_material("powerline_material", [0.0, 0.5, 1.0, 0.5]) + self.set_material(item, material) + + def assign_portal_material(self, item): + """Assign teal material to object. + + Args: + item (bpy.Object): The Blender object to assign the material to. + """ + material = self.validate_material("portalline_material", [0.0, 1.0, 1.0, 0.5]) + self.set_material(item, material) + + def assign_pipe_material(self, item): + """Assign grey material to object. + + Args: + item (bpy.Object): The Blender object to assign the material to. + """ + material = self.validate_material("pipeline_material", [0.3, 0.3, 0.3, 0.9]) + self.set_material(item, material) + + def assign_bytebeat_material(self, item): + """Assign light purple material to object. + + Args: + item (bpy.Object): The Blender object to assign the material to. + """ + material = self.validate_material("bytebeat_material", [0.8, 0.0, 0.8, 0.5]) + self.set_material(item, material) + + def assign_preset_material(self, item): + """Assign gold material to object. + + Args: + item (bpy.Object): The Blender object to assign the material to. + """ + # Material name. + material_name = "preset_material" + # Add transparent tag to material name. + item_name = item.get("ObjectID", "") + if item_name in GHOSTED_ITEMS: + material_name += "_transparent" + + material = self.validate_material(material_name, [0.8, 0.300186, 0.178301, 1.0]) + self.set_material(item, material) + + def assign_default_material(self, item, index=0): + """Given a blender object. Assign the default material, + + Args: + item (bpy_types.Object): A Blender object. + + Returns: + bpy_types.Material: The material that is applied. + """ + # Apply Custom Variable. + item["UserData"] = str(index) + # Get colour values. + colour_values = [0.8, 0.8, 0.8, 1.0] + # Get or create the material. + material = self.validate_material("default_material", colour_values) + self.set_material(item, material) + return material + + def get_colour_from_palette_data(self, colour_index, material_index): + """Get colour data from palette data. + + Args: + index (int): The colour index. + """ + return BAKED_INDEX_COLOURS.get(colour_index, [0.8, 0.8, 0.8, 1.0]) + + def restore_material(self, item, user_data_value): + # Validate material+color index + try: + user_data_value = int(user_data_value) + except ValueError: + return + col = userdata.get_colour(user_data_value) + mat = userdata.get_material(user_data_value) + self.assign_material(item, col, mat) + + def assign_material(self, item, colour_index=0, material_index=0): + """Given a blender object. assign a material and UserData index. + + Args: + item (bpy_types.Object): A Blender object. + colour_index (int): The colour index determined by No Man's Sky. + material (str): The material type. + + Returns: + bpy_types.Material: The material that is applied. + """ + # Some Defaults + alpha_value = 1.0 + + # Apply Custom Variable. + reference_value = int(item.get("UserData", 0)) + new_userdata_value = userdata.update_colour_material( + reference_value, colour_index=colour_index, material_index=material_index + ) + item["UserData"] = str(new_userdata_value) + + # Create Material + colour_name = "{0}_material".format(new_userdata_value) + # Add transparent tag to material name. + item_name = item.get("ObjectID", "") + if item_name in GHOSTED_ITEMS: + colour_name += "_transparent" + + # Get colour values. + primary = self.get_colour_from_palette_data(colour_index, material_index) + if isinstance(primary, str): + primary = eval(primary) + # copy so the baked palette entry isn't extended in place + primary = list(primary) + if len(primary) < 4: + primary.append(alpha_value) + + # Get or create the material. + material = self.validate_material(colour_name, primary) + + # Add Metadata to object. + nice_name = get_nice_name_from_indicies(colour_index, material_index) + if nice_name and ":" in nice_name: + item["readonly:Material"] = nice_name.split(":")[0].strip() + item["readonly:Colour"] = nice_name.split(":")[1].strip() + + self.set_material(item, material) + return material + + +_default_provider = None +_active_provider = None + + +def get_material_provider(): + global _default_provider, _active_provider + if _active_provider is None: + if _default_provider is None: + _default_provider = MaterialProvider() + _active_provider = _default_provider + return _active_provider + + +def set_material_provider(provider): + # None restores the addon's own materials + global _active_provider + if provider is not None and not isinstance(provider, MaterialProvider): + raise TypeError("set_material_provider expects a MaterialProvider instance or None") + _active_provider = provider