From 221d5378e4ad817e64cc942b8a7b7433d819aa52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Mon, 1 Apr 2024 16:48:56 +0200 Subject: [PATCH] feat: ManagedObj Semantics --- ; | 339 +++++++++++++++++ TODO.md | 33 +- pyproject.toml | 2 +- src/blender_maxwell/__init__.py | 6 +- src/blender_maxwell/assets/__init__.py | 14 + src/blender_maxwell/assets/assets.blend | 3 + .../assets/blender_assets.cats.txt | 10 + .../assets/blender_assets.cats.txt~ | 10 + .../assets/geonodes/primitives/box.blend | 3 + .../assets/geonodes/primitives/ring.blend | 3 + .../assets/geonodes/primitives/sphere.blend | 3 + .../assets/geonodes/template.blend | 3 + src/blender_maxwell/assets/import_geonodes.py | 345 ++++++++++++++++++ .../{blends => assets}/starter.blend | 0 src/blender_maxwell/blends/__init__.py | 0 src/blender_maxwell/blends/bl_append.py | 0 src/blender_maxwell/info.py | 31 +- .../contracts/socket_colors.py | 1 + .../contracts/socket_from_bl_direct.py | 17 +- .../contracts/socket_shapes.py | 1 + .../contracts/socket_types.py | 1 + .../managed_objs/managed_bl_object.py | 210 ++++++----- .../node_trees/maxwell_sim_nodes/node_tree.py | 8 +- .../nodes/monitors/eh_field_monitor.py | 100 +++-- .../maxwell_sim_nodes/nodes/outputs/viewer.py | 23 +- .../nodes/structures/geonodes_structure.py | 34 +- .../maxwell_sim_nodes/sockets/__init__.py | 1 + .../sockets/blender/__init__.py | 7 +- .../sockets/blender/geonodes.py | 4 +- .../sockets/blender/material.py | 55 +++ .../sockets/blender/object.py | 4 +- src/blender_maxwell/registration.py | 4 +- src/blender_maxwell/services/tdcloud.py | 1 + src/blender_maxwell/utils/logger.py | 28 +- 34 files changed, 1063 insertions(+), 241 deletions(-) create mode 100644 ; create mode 100644 src/blender_maxwell/assets/__init__.py create mode 100644 src/blender_maxwell/assets/assets.blend create mode 100644 src/blender_maxwell/assets/blender_assets.cats.txt create mode 100644 src/blender_maxwell/assets/blender_assets.cats.txt~ create mode 100644 src/blender_maxwell/assets/geonodes/primitives/box.blend create mode 100644 src/blender_maxwell/assets/geonodes/primitives/ring.blend create mode 100644 src/blender_maxwell/assets/geonodes/primitives/sphere.blend create mode 100644 src/blender_maxwell/assets/geonodes/template.blend create mode 100644 src/blender_maxwell/assets/import_geonodes.py rename src/blender_maxwell/{blends => assets}/starter.blend (100%) delete mode 100644 src/blender_maxwell/blends/__init__.py delete mode 100644 src/blender_maxwell/blends/bl_append.py create mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py diff --git a/; b/; new file mode 100644 index 0000000..cd6e3c4 --- /dev/null +++ b/; @@ -0,0 +1,339 @@ +"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell.""" + +import enum +import typing as typ +from pathlib import Path + +import bpy +import typing_extensions as typx + +from .. import info +from ..utils import logger + +log = logger.get(__name__) + +BLOperatorStatus: typ.TypeAlias = set[ + typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] +] + + +#################### +# - GeoNodes Specification +#################### +class GeoNodes(enum.StrEnum): + """Defines available GeoNodes groups vendored as part of Blender Maxwell. + + The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself. + """ + + PrimitiveBox = 'box' + PrimitiveRing = 'ring' + PrimitiveSphere = 'sphere' + + +# GeoNodes Path Mapping +GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives' +GN_PARENT_PATHS: dict[GeoNodes, Path] = { + GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH, + GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH, + GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH, +} + + +#################### +# - Import GeoNodes (Link/Append) +#################### +ImportMethod: typ.TypeAlias = typx.Literal['append', 'link'] + + +def import_geonodes( + geonodes: GeoNodes, + import_method: ImportMethod, + force_import: bool = False, +) -> bpy.types.GeometryNodeGroup: + """Given a pre-defined GeoNodes group packaged with Blender Maxwell. + + The procedure is as follows: + + - Link it to the current .blend file. + - Retrieve the node group and return it. + """ + if geonodes in bpy.data.node_groups and not force_import: + log.info( + 'Found Existing GeoNodes Tree (name=%s)', + geonodes + ) + return bpy.data.node_groups[geonodes] + + filename = geonodes + filepath = str( + GN_PARENT_PATHS[geonodes] / (geonodes + '.blend') / 'NodeTree' / geonodes + ) + directory = filepath.removesuffix(geonodes) + log.info( + '% GeoNodes Tree (filename=%s, directory=%s, filepath=%s)', + "Linking" if import_method == 'link' else "Appending" + filename, + directory, + filepath, + ) + bpy.ops.wm.append( + filepath=filepath, + directory=directory, + filename=filename, + check_existing=False, + set_fake=True, + link=import_method == 'link', + ) + + return bpy.data.node_groups[geonodes] + + +#################### +# - GeoNodes Asset Shelf +#################### +# class GeoNodesAssetShelf(bpy.types.AssetShelf): +# bl_space_type = 'NODE_EDITOR' +# bl_idname = 'blender_maxwell.asset_shelf__geonodes' +# bl_options = {'NO_ASSET_DRAG'} +# +# @classmethod +# def poll(cls, context): +# return ( +# (space := context.get('space_data')) +# and (node_tree := space.get('node_tree')) +# and (node_tree.bl_idname == 'MaxwellSimTreeType') +# ) +# +# @classmethod +# def asset_poll(cls, asset: bpy.types.AssetRepresentation): +# return asset.id_type == 'NODETREE' + + +#################### +# - GeoNodes Asset Shelf Panel for MaxwellSimTree +#################### +class NodeAssetPanel(bpy.types.Panel): + bl_idname = 'blender_maxwell.panel__node_asset_panel' + bl_label = 'Node GeoNodes Asset Panel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Assets' + + # @classmethod + # def poll(cls, context): + # return ( + # (space := context.get('space_data')) is not None + # and (node_tree := space.get('node_tree')) is not None + # and (node_tree.bl_idname == 'MaxwellSimTreeType') + # ) + + def draw(self, context): + layout = self.layout + workspace = context.workspace + wm = context.window_manager + + # list_id must be unique otherwise behaviour gets weird when the template_asset_view is shown twice + # (drag operator stops working in AssetPanelDrag, clickable area of all Assets in AssetPanelNoDrag gets + # reduced to below the Asset name and clickable area of Current File Assets in AssetPanelDrag gets + # reduced as if it didn't have a drag operator) + _activate_op_props, _drag_op_props = layout.template_asset_view( + 'geo_nodes_asset_shelf', + workspace, + 'asset_library_reference', + wm, + 'active_asset_list', + wm, + 'active_asset_index', + drag_operator=AppendGeoNodes.bl_idname, + ) + + +#################### +# - Append GeoNodes Operator +#################### +def get_view_location(region, coords, ui_scale): + x, y = region.view2d.region_to_view(*coords) + return x / ui_scale, y / ui_scale + + +class AppendGeoNodes(bpy.types.Operator): + """Operator allowing the user to append a vendored GeoNodes tree for use in a simulation.""" + + bl_idname = 'blender_maxwell.blends__import_geo_nodes' + bl_label = 'Import GeoNode Tree' + bl_description = 'Append a geometry node tree from the Blender Maxwell plugin, either via linking or appending' + bl_options = frozenset({'REGISTER'}) + + #################### + # - Properties + #################### + _asset: bpy.types.AssetRepresentation | None = None + _start_drag_x: bpy.props.IntProperty() + _start_drag_y: bpy.props.IntProperty() + + #################### + # - UI + #################### + def draw(self, _: bpy.types.Context) -> None: + """Draws the UI of the operator.""" + layout = self.layout + col = layout.column() + col.prop(self, 'geonodes_to_append', expand=True) + + #################### + # - Execution + #################### + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + """Defines when the operator can be run. + + Returns: + Whether the operator can be run. + """ + return context.asset is not None + + def invoke(self, context, event): + self._start_drag_x = event.mouse_x + self._start_drag_y = event.mouse_y + return self.execute(context) + + def execute(self, context: bpy.types.Context) -> BLOperatorStatus: + """Initializes the while-dragging modal handler, which executes custom logic when the mouse button is released. + + Runs in response to drag_handler of a `UILayout.template_asset_view`. + """ + asset: bpy.types.AssetRepresentation = context.asset + log.info('Dragging Asset: %s', asset.name) + + # Store Asset for Modal & Drag Start + self._asset = context.asset + + # Register Modal Operator & Tag Area for Redraw + context.window_manager.modal_handler_add(self) + context.area.tag_redraw() + + # Set Modal Cursor + context.window.cursor_modal_set('CROSS') + + # Return Status of Running Modal + return {'RUNNING_MODAL'} + + def modal( + self, context: bpy.types.Context, event: bpy.types.Event + ) -> BLOperatorStatus: + """When LMB is released, creates a GeoNodes Structure node. + + Runs in response to events in the node editor while dragging an asset from the side panel. + """ + if (asset := self._asset) is None: + return {'PASS_THROUGH'} + + if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + log.info('Released Dragged Asset: %s', asset.name) + area = context.area + editor_region = next( + region for region in area.regions.values() if region.type == 'WINDOW' + ) + + # Check if Mouse Coordinates are: + ## - INSIDE of Node Editor + ## - INSIDE of Node Editor's WINDOW Region + if ( + (event.mouse_x >= area.x and event.mouse_x < area.x + area.width) + and (event.mouse_y >= area.y and event.mouse_y < area.y + area.height) + ) and ( + ( + event.mouse_x >= editor_region.x + and event.mouse_x < editor_region.x + editor_region.width + ) + and ( + event.mouse_y >= editor_region.y + and event.mouse_y < editor_region.y + editor_region.height + ) + ): + log.info( + 'Asset "%s" Released in Main Window of Node Editor', asset.name + ) + space = context.space_data + node_tree = space.node_tree + + ui_scale = context.preferences.system.ui_scale + node_location = get_view_location( + editor_region, + [ + event.mouse_x - editor_region.x, + event.mouse_y - editor_region.y, + ], + ui_scale, + ) + + # Create GeoNodes Structure Node + #space.cursor_location_from_region(*node_location) + log.info( + 'Creating GeoNodes Structure Node at (%d, %d)', + *tuple(space.cursor_location), + ) + bpy.ops.node.select_all(action='DESELECT') + structure_node = node_tree.nodes.new('GeoNodesStructureNodeType') + structure_node.select = True + structure_node.location.x = node_location[0] + structure_node.location.y = node_location[1] + context.area.tag_redraw() + print(structure_node.location) + + # Import the GeoNodes Structure + geonodes = import_geonodes(asset.name, 'append') + + # Create the GeoNodes Node + + # Create a GeoNodes Structure w/Designated GeoNodes Group @ Mouse Position + context.window.cursor_modal_restore() + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + +#################### +# - Blender Registration +#################### +# def initialize_asset_libraries(_: bpy.types.Scene): +# bpy.app.handlers.load_post.append(initialize_asset_libraries) +## TODO: Move to top-level registration. + +asset_libraries = bpy.context.preferences.filepaths.asset_libraries +if ( + asset_library_idx := asset_libraries.find('Blender Maxwell') +) != -1 and asset_libraries['Blender Maxwell'].path != str(info.PATH_ASSETS): + bpy.ops.preferences.asset_library_remove(asset_library_idx) + +if 'Blender Maxwell' not in asset_libraries: + bpy.ops.preferences.asset_library_add() + asset_library = asset_libraries[-1] ## Since the operator adds to the end + asset_library.name = 'Blender Maxwell' + asset_library.path = str(info.PATH_ASSETS) + +bpy.types.WindowManager.active_asset_list = bpy.props.CollectionProperty( + type=bpy.types.AssetHandle +) +bpy.types.WindowManager.active_asset_index = bpy.props.IntProperty() +## TODO: Do something differently + +BL_REGISTER = [ + # GeoNodesAssetShelf, + NodeAssetPanel, + AppendGeoNodes, +] + +BL_KEYMAP_ITEM_DEFS = [ + # { + # '_': [ + # AppendGeoNodes.bl_idname, + # 'LEFTMOUSE', + # 'CLICK_DRAG', + # ], + # 'ctrl': False, + # 'shift': False, + # 'alt': False, + # } +] diff --git a/TODO.md b/TODO.md index d4d3efe..81d201f 100644 --- a/TODO.md +++ b/TODO.md @@ -33,6 +33,7 @@ ## Outputs [x] Viewer - [ ] **BIG ONE**: Remove image preview when disabling plots. +- [ ] Declare Preview unit system on the viewer node. - [ ] Either enforce singleton, or find a way to have several viewers at the same time. - [ ] A setting that live-previews just a value. - [ ] Pop-up multiline string print as alternative to console print. @@ -174,13 +175,24 @@ [ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :)) [ ] Tests / Wood Pile -[ ] Primitives / Plane -[ ] Primitives / Box -[ ] Primitives / Sphere -[ ] Primitives / Cylinder -[ ] Primitives / Ring -[ ] Primitives / Capsule -[ ] Primitives / Cone +[ ] Structures / Primitives / Plane +[x] Structures / Primitives / Box +[x] Structures / Primitives / Sphere +[ ] Structures / Primitives / Cylinder +[x] Structures / Primitives / Ring +[ ] Structures / Primitives / Capsule +[ ] Structures / Primitives / Cone + +[ ] Structures / Arrays / Square +[ ] Structures / Arrays / Square-Hole +[ ] Structures / Arrays / Cyl +[ ] Structures / Arrays / Cyl-Hole +[x] Structures / Arrays / Box +[x] Structures / Arrays / Sphere +[ ] Structures / Arrays / Cylinder +[-] Structures / Arrays / Ring +[ ] Structures / Arrays / Capsule +[ ] Structures / Arrays / Cone [ ] Array / Square Array **NOTE: Ring and cylinder** [ ] Array / Hex Array **NOTE: Ring and cylinder** @@ -337,12 +349,15 @@ # Architecture ## CRITICAL -With these things in place +With these things in place, we're in tip top shape: [ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially +[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything. + [ ] Finalize Viewer node unit systems. + [ ] Introduce a simplified (maybe caching) method of translating sympy-enabled values, ex. 'Center', into values for external use (ex. in a Tidy3D object or in a Blender preview) based on + [ ] Abstract the actual unit system dict-like data structure out from the UnitSystem socket. [ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing. - I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs. [ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under. -[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything. ## Registration and Contracts [x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description. diff --git a/pyproject.toml b/pyproject.toml index 3b3ca7f..f6aca59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "networkx==3.2.*", "rich==12.5.*", "rtree==1.2.*", - # Pin Blender 4.1.0-Compatible Versions ## The dependency resolver will report if anything is wonky. "urllib3==1.26.8", @@ -100,6 +99,7 @@ ignore = [ "Q001", # Conflicts w/Formatter "Q002", # Conflicts w/Formatter "Q003", # Conflicts w/Formatter + "D206", # Conflicts w/Formatter "B008", # FastAPI uses this for Depends(), Security(), etc. . "E701", # class foo(Parent): pass or if simple: return are perfectly elegant "ERA001", # 'Commented-out code' seems to be just about anything to ruff diff --git a/src/blender_maxwell/__init__.py b/src/blender_maxwell/__init__.py index d117997..7bbca28 100644 --- a/src/blender_maxwell/__init__.py +++ b/src/blender_maxwell/__init__.py @@ -46,9 +46,10 @@ BL_REGISTER__BEFORE_DEPS = [ def BL_REGISTER__AFTER_DEPS(path_deps: Path): log.info('Loading After-Deps BL_REGISTER') with pydeps.importable_addon_deps(path_deps): - from . import node_trees, operators + from . import assets, node_trees, operators return [ *operators.BL_REGISTER, + *assets.BL_REGISTER, *node_trees.BL_REGISTER, ] @@ -62,9 +63,10 @@ BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS = [ def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path): log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS') with pydeps.importable_addon_deps(path_deps): - from . import operators + from . import assets, operators return [ *operators.BL_KEYMAP_ITEM_DEFS, + *assets.BL_KEYMAP_ITEM_DEFS, ] diff --git a/src/blender_maxwell/assets/__init__.py b/src/blender_maxwell/assets/__init__.py new file mode 100644 index 0000000..a6e325a --- /dev/null +++ b/src/blender_maxwell/assets/__init__.py @@ -0,0 +1,14 @@ +from . import import_geonodes + +BL_REGISTER = [ + *import_geonodes.BL_REGISTER, +] + +BL_KEYMAP_ITEM_DEFS = [ + *import_geonodes.BL_KEYMAP_ITEM_DEFS, +] + +__all__ = [ + 'BL_REGISTER', + 'BL_KEYMAP_ITEM_DEFS', +] diff --git a/src/blender_maxwell/assets/assets.blend b/src/blender_maxwell/assets/assets.blend new file mode 100644 index 0000000..b293485 --- /dev/null +++ b/src/blender_maxwell/assets/assets.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faec3c9b23a081a7829eca1bfc8feef6f1458d808bc6b275a9d35ba7d33d97a5 +size 737120 diff --git a/src/blender_maxwell/assets/blender_assets.cats.txt b/src/blender_maxwell/assets/blender_assets.cats.txt new file mode 100644 index 0000000..efd41a1 --- /dev/null +++ b/src/blender_maxwell/assets/blender_assets.cats.txt @@ -0,0 +1,10 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +783a7efe-c424-42b1-9771-42c862515891:Structures:Structures +ccb80eec-7e20-453d-89fb-0486b7abf7d4:Structures/Primitives:Structures-Primitives diff --git a/src/blender_maxwell/assets/blender_assets.cats.txt~ b/src/blender_maxwell/assets/blender_assets.cats.txt~ new file mode 100644 index 0000000..efd41a1 --- /dev/null +++ b/src/blender_maxwell/assets/blender_assets.cats.txt~ @@ -0,0 +1,10 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +783a7efe-c424-42b1-9771-42c862515891:Structures:Structures +ccb80eec-7e20-453d-89fb-0486b7abf7d4:Structures/Primitives:Structures-Primitives diff --git a/src/blender_maxwell/assets/geonodes/primitives/box.blend b/src/blender_maxwell/assets/geonodes/primitives/box.blend new file mode 100644 index 0000000..2158231 --- /dev/null +++ b/src/blender_maxwell/assets/geonodes/primitives/box.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cd4c9f42c5d9e97db8f3b755d4a028ab73339b6682026b833a13413e4335a99 +size 851789 diff --git a/src/blender_maxwell/assets/geonodes/primitives/ring.blend b/src/blender_maxwell/assets/geonodes/primitives/ring.blend new file mode 100644 index 0000000..f7873ef --- /dev/null +++ b/src/blender_maxwell/assets/geonodes/primitives/ring.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418a8fac57f9a5bcc34811f15b3226faad1ea64b8dde50cc4aa07eb15c0b012f +size 892163 diff --git a/src/blender_maxwell/assets/geonodes/primitives/sphere.blend b/src/blender_maxwell/assets/geonodes/primitives/sphere.blend new file mode 100644 index 0000000..41de196 --- /dev/null +++ b/src/blender_maxwell/assets/geonodes/primitives/sphere.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9621bca9d715216b78310d2154440e1c875c1e5239377e5e8e8b47fb7be4f61e +size 853330 diff --git a/src/blender_maxwell/assets/geonodes/template.blend b/src/blender_maxwell/assets/geonodes/template.blend new file mode 100644 index 0000000..e2b9526 --- /dev/null +++ b/src/blender_maxwell/assets/geonodes/template.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b52579ca903bd6df83d8704f1972740823e8ebedabe55de2095e2d3a64db0b40 +size 730512 diff --git a/src/blender_maxwell/assets/import_geonodes.py b/src/blender_maxwell/assets/import_geonodes.py new file mode 100644 index 0000000..1405360 --- /dev/null +++ b/src/blender_maxwell/assets/import_geonodes.py @@ -0,0 +1,345 @@ +"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell.""" + +import enum +import typing as typ +from pathlib import Path + +import bpy +import typing_extensions as typx + +from .. import info +from ..utils import logger + +log = logger.get(__name__) + +BLOperatorStatus: typ.TypeAlias = set[ + typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] +] + + +#################### +# - GeoNodes Specification +#################### +class GeoNodes(enum.StrEnum): + """Defines available GeoNodes groups vendored as part of Blender Maxwell. + + The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself. + """ + + PrimitiveBox = 'box' + PrimitiveRing = 'ring' + PrimitiveSphere = 'sphere' + + +# GeoNodes Path Mapping +GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives' +GN_PARENT_PATHS: dict[GeoNodes, Path] = { + GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH, + GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH, + GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH, +} + + +#################### +# - Import GeoNodes (Link/Append) +#################### +ImportMethod: typ.TypeAlias = typx.Literal['append', 'link'] + + +def import_geonodes( + geonodes: GeoNodes, + import_method: ImportMethod, + force_import: bool = False, +) -> bpy.types.GeometryNodeGroup: + """Given a pre-defined GeoNodes group packaged with Blender Maxwell. + + The procedure is as follows: + + - Link it to the current .blend file. + - Retrieve the node group and return it. + """ + if geonodes in bpy.data.node_groups and not force_import: + log.info( + 'Found Existing GeoNodes Tree (name=%s)', + geonodes + ) + return bpy.data.node_groups[geonodes] + + filename = geonodes + filepath = str( + GN_PARENT_PATHS[geonodes] / (geonodes + '.blend') / 'NodeTree' / geonodes + ) + directory = filepath.removesuffix(geonodes) + log.info( + '%s GeoNodes (filename=%s, directory=%s, filepath=%s)', + "Linking" if import_method == 'link' else "Appending", + filename, + directory, + filepath, + ) + bpy.ops.wm.append( + filepath=filepath, + directory=directory, + filename=filename, + check_existing=False, + set_fake=True, + link=import_method == 'link', + ) + + return bpy.data.node_groups[geonodes] + + +#################### +# - GeoNodes Asset Shelf +#################### +# class GeoNodesAssetShelf(bpy.types.AssetShelf): +# bl_space_type = 'NODE_EDITOR' +# bl_idname = 'blender_maxwell.asset_shelf__geonodes' +# bl_options = {'NO_ASSET_DRAG'} +# +# @classmethod +# def poll(cls, context): +# return ( +# (space := context.get('space_data')) +# and (node_tree := space.get('node_tree')) +# and (node_tree.bl_idname == 'MaxwellSimTreeType') +# ) +# +# @classmethod +# def asset_poll(cls, asset: bpy.types.AssetRepresentation): +# return asset.id_type == 'NODETREE' + + +#################### +# - GeoNodes Asset Shelf Panel for MaxwellSimTree +#################### +class NodeAssetPanel(bpy.types.Panel): + bl_idname = 'blender_maxwell.panel__node_asset_panel' + bl_label = 'Node GeoNodes Asset Panel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Assets' + + # @classmethod + # def poll(cls, context): + # return ( + # (space := context.get('space_data')) is not None + # and (node_tree := space.get('node_tree')) is not None + # and (node_tree.bl_idname == 'MaxwellSimTreeType') + # ) + + def draw(self, context): + layout = self.layout + workspace = context.workspace + wm = context.window_manager + + # list_id must be unique otherwise behaviour gets weird when the template_asset_view is shown twice + # (drag operator stops working in AssetPanelDrag, clickable area of all Assets in AssetPanelNoDrag gets + # reduced to below the Asset name and clickable area of Current File Assets in AssetPanelDrag gets + # reduced as if it didn't have a drag operator) + _activate_op_props, _drag_op_props = layout.template_asset_view( + 'geo_nodes_asset_shelf', + workspace, + 'asset_library_reference', + wm, + 'active_asset_list', + wm, + 'active_asset_index', + drag_operator=AppendGeoNodes.bl_idname, + ) + + +#################### +# - Append GeoNodes Operator +#################### +def get_view_location(region, coords, ui_scale): + x, y = region.view2d.region_to_view(*coords) + return x / ui_scale, y / ui_scale + + +class AppendGeoNodes(bpy.types.Operator): + """Operator allowing the user to append a vendored GeoNodes tree for use in a simulation.""" + + bl_idname = 'blender_maxwell.blends__import_geo_nodes' + bl_label = 'Import GeoNode Tree' + bl_description = 'Append a geometry node tree from the Blender Maxwell plugin, either via linking or appending' + bl_options = frozenset({'REGISTER'}) + + #################### + # - Properties + #################### + _asset: bpy.types.AssetRepresentation | None = None + _start_drag_x: bpy.props.IntProperty() + _start_drag_y: bpy.props.IntProperty() + + #################### + # - UI + #################### + def draw(self, _: bpy.types.Context) -> None: + """Draws the UI of the operator.""" + layout = self.layout + col = layout.column() + col.prop(self, 'geonodes_to_append', expand=True) + + #################### + # - Execution + #################### + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + """Defines when the operator can be run. + + Returns: + Whether the operator can be run. + """ + return context.asset is not None + + def invoke(self, context, event): + self._start_drag_x = event.mouse_x + self._start_drag_y = event.mouse_y + return self.execute(context) + + def execute(self, context: bpy.types.Context) -> BLOperatorStatus: + """Initializes the while-dragging modal handler, which executes custom logic when the mouse button is released. + + Runs in response to drag_handler of a `UILayout.template_asset_view`. + """ + asset: bpy.types.AssetRepresentation = context.asset + log.debug('Dragging Asset: %s', asset.name) + + # Store Asset for Modal & Drag Start + self._asset = context.asset + + # Register Modal Operator & Tag Area for Redraw + context.window_manager.modal_handler_add(self) + context.area.tag_redraw() + + # Set Modal Cursor + context.window.cursor_modal_set('CROSS') + + # Return Status of Running Modal + return {'RUNNING_MODAL'} + + def modal( + self, context: bpy.types.Context, event: bpy.types.Event + ) -> BLOperatorStatus: + """When LMB is released, creates a GeoNodes Structure node. + + Runs in response to events in the node editor while dragging an asset from the side panel. + """ + if (asset := self._asset) is None: + return {'PASS_THROUGH'} + + if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + log.debug('Released Dragged Asset: %s', asset.name) + area = context.area + editor_region = next( + region for region in area.regions.values() if region.type == 'WINDOW' + ) + + # Check if Mouse Coordinates are: + ## - INSIDE of Node Editor + ## - INSIDE of Node Editor's WINDOW Region + if ( + (event.mouse_x >= area.x and event.mouse_x < area.x + area.width) + and (event.mouse_y >= area.y and event.mouse_y < area.y + area.height) + ) and ( + ( + event.mouse_x >= editor_region.x + and event.mouse_x < editor_region.x + editor_region.width + ) + and ( + event.mouse_y >= editor_region.y + and event.mouse_y < editor_region.y + editor_region.height + ) + ): + log.info( + 'Asset "%s" Released in Main Window of Node Editor', asset.name + ) + space = context.space_data + node_tree = space.node_tree + + # Computing GeoNodes View Location + ## 1. node_tree.cursor_location gives clicked loc, not released. + ## 2. event.mouse_region_* has inverted x wrt. event.mouse_*. + ## - View2D.region_to_view expects the event.mouse_* order. + ## - Is it a bug? Who knows! + ## 3. We compute it manually, to avoid the jank. + node_location = get_view_location( + editor_region, + [ + event.mouse_x - editor_region.x, + event.mouse_y - editor_region.y, + ], + context.preferences.system.ui_scale, + ) + + # Create GeoNodes Structure Node + ## 1. Deselect other nodes + ## 2. Select the new one + ## 3. Move it into place + ## 4. Redraw (so we see the new node right away) + log.info( + 'Creating GeoNodes Structure Node at (%d, %d)', + *tuple(node_location), + ) + bpy.ops.node.select_all(action='DESELECT') + node = node_tree.nodes.new('GeoNodesStructureNodeType') + node.select = True + node.location.x = node_location[0] + node.location.y = node_location[1] + context.area.tag_redraw() + + # Import & Attach the GeoNodes Tree to the Node + geonodes = import_geonodes(asset.name, 'append') + node.inputs['GeoNodes'].value = geonodes + + # Restore the Pre-Modal Mouse Cursor Shape + context.window.cursor_modal_restore() + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + +#################### +# - Blender Registration +#################### +# def initialize_asset_libraries(_: bpy.types.Scene): +# bpy.app.handlers.load_post.append(initialize_asset_libraries) +## TODO: Move to top-level registration. + +asset_libraries = bpy.context.preferences.filepaths.asset_libraries +if ( + asset_library_idx := asset_libraries.find('Blender Maxwell') +) != -1 and asset_libraries['Blender Maxwell'].path != str(info.PATH_ASSETS): + bpy.ops.preferences.asset_library_remove(asset_library_idx) + +if 'Blender Maxwell' not in asset_libraries: + bpy.ops.preferences.asset_library_add() + asset_library = asset_libraries[-1] ## Since the operator adds to the end + asset_library.name = 'Blender Maxwell' + asset_library.path = str(info.PATH_ASSETS) + +bpy.types.WindowManager.active_asset_list = bpy.props.CollectionProperty( + type=bpy.types.AssetHandle +) +bpy.types.WindowManager.active_asset_index = bpy.props.IntProperty() +## TODO: Do something differently + +BL_REGISTER = [ + # GeoNodesAssetShelf, + NodeAssetPanel, + AppendGeoNodes, +] + +BL_KEYMAP_ITEM_DEFS = [ + # { + # '_': [ + # AppendGeoNodes.bl_idname, + # 'LEFTMOUSE', + # 'CLICK_DRAG', + # ], + # 'ctrl': False, + # 'shift': False, + # 'alt': False, + # } +] diff --git a/src/blender_maxwell/blends/starter.blend b/src/blender_maxwell/assets/starter.blend similarity index 100% rename from src/blender_maxwell/blends/starter.blend rename to src/blender_maxwell/assets/starter.blend diff --git a/src/blender_maxwell/blends/__init__.py b/src/blender_maxwell/blends/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/blender_maxwell/blends/bl_append.py b/src/blender_maxwell/blends/bl_append.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/blender_maxwell/info.py b/src/blender_maxwell/info.py index 5a28239..de66fbb 100644 --- a/src/blender_maxwell/info.py +++ b/src/blender_maxwell/info.py @@ -3,38 +3,47 @@ from pathlib import Path import bpy +PATH_ADDON_ROOT = Path(__file__).resolve().parent + #################### # - Addon Info #################### -PATH_ADDON_ROOT = Path(__file__).resolve().parent - -# Addon Information -## bl_info is filled with PROJ_SPEC when packing the .zip. with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f: PROJ_SPEC = tomllib.load(f) + ## bl_info is filled with PROJ_SPEC when packing the .zip. ADDON_NAME = PROJ_SPEC['project']['name'] ADDON_VERSION = PROJ_SPEC['project']['version'] -# PyDeps Path Info -## requirements.lock is written when packing the .zip. -## By default, the addon pydeps are kept in the addon dir. +#################### +# - Asset Info +#################### +PATH_ASSETS = PATH_ADDON_ROOT / 'assets' + +#################### +# - PyDeps Info +#################### PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock' DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies' +## requirements.lock is written when packing the .zip. +## By default, the addon pydeps are kept in the addon dir. -# Logging Info +#################### +# - Logging Info +#################### +DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log' +DEFAULT_LOG_PATH.touch(exist_ok=True) ## By default, the addon file log writes to the addon dir. ## The initial .log_level contents are written when packing the .zip. ## Subsequent changes are managed by nodeps.utils.simple_logger.py. -DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log' -DEFAULT_LOG_PATH.touch(exist_ok=True) PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level' with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f: BOOTSTRAP_LOG_LEVEL = int(f.read().strip()) + #################### -# - Addon Getters +# - Addon Prefs Info #################### def addon_prefs() -> bpy.types.AddonPreferences | None: if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None: diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py index 8f198d9..1ef38ae 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py @@ -42,6 +42,7 @@ SOCKET_COLORS = { ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach # Blender + ST.BlenderMaterial: (0.8, 0.6, 1.0, 1.0), # Lighter Purple ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py index 239f4a6..0fc86ba 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_from_bl_direct.py @@ -1,11 +1,17 @@ from .socket_types import SocketType as ST BL_SOCKET_DIRECT_TYPE_MAP = { - ('NodeSocketString', 1): ST.String, - ('NodeSocketBool', 1): ST.Bool, + # Blender ('NodeSocketCollection', 1): ST.BlenderCollection, ('NodeSocketImage', 1): ST.BlenderImage, ('NodeSocketObject', 1): ST.BlenderObject, + ('NodeSocketMaterial', 1): ST.BlenderMaterial, + + # Basic + ('NodeSocketString', 1): ST.String, + ('NodeSocketBool', 1): ST.Bool, + + # Float ('NodeSocketFloat', 1): ST.RealNumber, # ("NodeSocketFloatAngle", 1): ST.PhysicalAngle, # ("NodeSocketFloatDistance", 1): ST.PhysicalLength, @@ -13,12 +19,17 @@ BL_SOCKET_DIRECT_TYPE_MAP = { ('NodeSocketFloatPercentage', 1): ST.RealNumber, # ("NodeSocketFloatTime", 1): ST.PhysicalTime, # ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime, + + # Int ('NodeSocketInt', 1): ST.IntegerNumber, ('NodeSocketIntFactor', 1): ST.IntegerNumber, ('NodeSocketIntPercentage', 1): ST.IntegerNumber, ('NodeSocketIntUnsigned', 1): ST.IntegerNumber, - ('NodeSocketRotation', 2): ST.PhysicalRot2D, + + # Array-Like ('NodeSocketColor', 3): ST.Color, + ('NodeSocketRotation', 2): ST.PhysicalRot2D, + ('NodeSocketVector', 2): ST.Real2DVector, ('NodeSocketVector', 3): ST.Real3DVector, # ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py index 09b2f76..15f39e1 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py @@ -38,6 +38,7 @@ SOCKET_SHAPES = { ST.PhysicalPol: 'CIRCLE', ST.PhysicalFreq: 'CIRCLE', # Blender + ST.BlenderMaterial: 'DIAMOND', ST.BlenderObject: 'DIAMOND', ST.BlenderCollection: 'DIAMOND', ST.BlenderImage: 'DIAMOND', diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py index ce894c6..909585d 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py @@ -34,6 +34,7 @@ class SocketType(BlenderTypeEnum): Complex3DVector = enum.auto() # Blender + BlenderMaterial = enum.auto() BlenderObject = enum.auto() BlenderCollection = enum.auto() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py index 72dd4e3..64de2a7 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py @@ -1,18 +1,15 @@ -import typing as typ -import typing_extensions as typx -import functools import contextlib -import io -import numpy as np -import pydantic as pyd -import matplotlib.axis as mpl_ax - -import bpy import bmesh +import bpy +import numpy as np +import typing_extensions as typx +from ....utils import logger from .. import contracts as ct +log = logger.get(__name__) + ModifierType = typx.Literal['NODES', 'ARRAY'] MODIFIER_NAMES = { 'NODES': 'BLMaxwell_GeoNodes', @@ -22,6 +19,9 @@ MANAGED_COLLECTION_NAME = 'BLMaxwell' PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible' +#################### +# - BLCollection +#################### def bl_collection( collection_name: str, view_layer_exclude: bool ) -> bpy.types.Collection: @@ -44,63 +44,89 @@ def bl_collection( return collection +#################### +# - BLObject +#################### class ManagedBLObject(ct.schemas.ManagedObj): managed_obj_type = ct.ManagedObjType.ManagedBLObject - _bl_object_name: str + _bl_object_name: str | None = None - def __init__(self, name: str): - self._bl_object_name = name - - # Object Name + #################### + # - BL Object Name + #################### @property def name(self): return self._bl_object_name @name.setter - def set_name(self, value: str) -> None: - # Object Doesn't Exist - if not (bl_object := bpy.data.objects.get(self._bl_object_name)): - # ...AND Desired Object Name is Not Taken - if not bpy.data.objects.get(value): - self._bl_object_name = value - return + def name(self, value: str) -> None: + log.info( + 'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value + ) - # ...AND Desired Object Name is Taken + if not bpy.data.objects.get(value): + log.info( + 'Desired BLObject Name "%s" Not Taken', + value, + ) + + if self._bl_object_name is None: + log.info( + 'Set New BLObject Name to "%s"', + value, + ) + elif bl_object := bpy.data.objects.get(self._bl_object_name): + log.info( + 'Changed BLObject Name to "%s"', + value, + ) + bl_object.name = value else: - msg = f'Desired name {value} for BL object is taken' - raise ValueError(msg) + msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted' + raise RuntimeError(msg) - # Object DOES Exist - bl_object.name = value - self._bl_object_name = bl_object.name - ## - When name exists, Blender adds .### to prevent overlap. - ## - `set_name` is allowed to change the name; nodes account for this. + # Set Internal Name + self._bl_object_name = value + else: + log.info( + 'Desired BLObject Name "%s" is Taken. Using Blender Rename', + value, + ) - # Object Datablock Name - @property - def bl_mesh_name(self): - return self.name + # Set Name Anyway, but Respect Blender's Renaming + ## When a name already exists, Blender adds .### to prevent overlap. + ## `set_name` is allowed to change the name; nodes account for this. + bl_object.name = value + self._bl_object_name = bl_object.name - @property - def bl_volume_name(self): - return self.name + log.info( + 'Changed BLObject Name to "%s"', + bl_object.name, + ) - # Deallocation + #################### + # - Allocation + #################### + def __init__(self, name: str): + self.name = name + + #################### + # - Deallocation + #################### def free(self): - if not (bl_object := bpy.data.objects.get(self.name)): - return ## Nothing to do + if (bl_object := bpy.data.objects.get(self.name)) is None: + return # Delete the Underlying Datablock ## This automatically deletes the object too - if bl_object.type == 'MESH': - bpy.data.meshes.remove(bl_object.data) - elif bl_object.type == 'EMPTY': + log.info('Removing "%s" BLObject', bl_object.type) + if bl_object.type in {'MESH', 'EMPTY'}: bpy.data.meshes.remove(bl_object.data) elif bl_object.type == 'VOLUME': bpy.data.volumes.remove(bl_object.data) else: - msg = f'Type of to-delete `bl_object`, {bl_object.type}, is not valid' - raise ValueError(msg) + msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"' + raise RuntimeError(msg) #################### # - Actions @@ -133,9 +159,16 @@ class ManagedBLObject(ct.schemas.ManagedObj): ) ).objects ): + log.info('Moving "%s" to Preview Collection', bl_object.name) preview_collection.objects.link(bl_object) + # Display Parameters if kind == 'EMPTY' and empty_display_type is not None: + log.info( + 'Setting Empty Display Type "%s" for "%s"', + empty_display_type, + bl_object.name, + ) bl_object.empty_display_type = empty_display_type def hide_preview( @@ -155,21 +188,22 @@ class ManagedBLObject(ct.schemas.ManagedObj): ) ).objects ): + log.info('Removing "%s" from Preview Collection', bl_object.name) preview_collection.objects.unlink(bl_object) def bl_select(self) -> None: """Selects the managed Blender object globally, causing it to be ex. outlined in the 3D viewport. """ - if not (bl_object := bpy.data.objects.get(self.name)): - msg = 'Managed BLObject does not exist' - raise ValueError(msg) + if (bl_object := bpy.data.objects.get(self.name)) is not None: + bpy.ops.object.select_all(action='DESELECT') + bl_object.select_set(True) - bpy.ops.object.select_all(action='DESELECT') - bl_object.select_set(True) + msg = 'Managed BLObject does not exist' + raise ValueError(msg) #################### - # - Managed Object Management + # - BLObject Management #################### def bl_object( self, @@ -177,30 +211,41 @@ class ManagedBLObject(ct.schemas.ManagedObj): ): """Returns the managed blender object. - If the requested object data type is different, then delete the old + If the requested object data kind is different, then delete the old object and recreate. """ # Remove Object (if mismatch) - if ( - bl_object := bpy.data.objects.get(self.name) - ) and bl_object.type != kind: + if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind: + log.info( + 'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)', + bl_object.name, + bl_object.type, + kind, + ) self.free() # Create Object w/Appropriate Data Block if not (bl_object := bpy.data.objects.get(self.name)): + log.info( + 'Creating "%s" with kind "%s"', + bl_object.name, + kind, + ) if kind == 'MESH': - bl_data = bpy.data.meshes.new(self.bl_mesh_name) + bl_data = bpy.data.meshes.new(self.name) elif kind == 'EMPTY': bl_data = None elif kind == 'VOLUME': raise NotImplementedError else: - msg = ( - f'Requested `bl_object` type {bl_object.type} is not valid' - ) + msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"' raise ValueError(msg) bl_object = bpy.data.objects.new(self.name, bl_data) + log.debug( + 'Linking "%s" to Base Collection', + bl_object.name, + ) bl_collection( MANAGED_COLLECTION_NAME, view_layer_exclude=True ).objects.link(bl_object) @@ -211,17 +256,16 @@ class ManagedBLObject(ct.schemas.ManagedObj): # - Mesh Data Properties #################### @property - def raw_mesh(self) -> bpy.types.Mesh: - """Returns the object's raw mesh data. + def mesh_data(self) -> bpy.types.Mesh: + """Directly loads the Blender mesh data. - Raises an error if the object has no mesh data. + Raises: + ValueError: If the object has no mesh data. """ - if ( - bl_object := bpy.data.objects.get(self.name) - ) and bl_object.type == 'MESH': + if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': return bl_object.data - msg = f'Requested MESH data from `bl_object` of type {bl_object.type}' + msg = f'Requested mesh data from {self.name} of type {bl_object.type}' raise ValueError(msg) @contextlib.contextmanager @@ -230,9 +274,7 @@ class ManagedBLObject(ct.schemas.ManagedObj): evaluate: bool = True, triangulate: bool = False, ) -> bpy.types.Mesh: - if ( - bl_object := bpy.data.objects.get(self.name) - ) and bl_object.type == 'MESH': + if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': bmesh_mesh = None try: bmesh_mesh = bmesh.new() @@ -254,7 +296,7 @@ class ManagedBLObject(ct.schemas.ManagedObj): bmesh_mesh.free() else: - msg = f'Requested BMesh from `bl_object` of type {bl_object.type}' + msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"' raise ValueError(msg) @property @@ -262,27 +304,31 @@ class ManagedBLObject(ct.schemas.ManagedObj): ## TODO: Cached # Ensure Updated Geometry + log.debug('Updating View Layer') bpy.context.view_layer.update() - ## TODO: Must we? # Compute Evaluted + Triangulated Mesh + log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name) _mesh = bpy.data.meshes.new(name='TemporaryMesh') with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh: bmesh_mesh.to_mesh(_mesh) # Optimized Vertex Copy ## See + log.debug('Copying Vertices from "%s"', self.name) verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64) _mesh.vertices.foreach_get('co', verts) verts.shape = (-1, 3) # Optimized Triangle Copy ## To understand, read it, **carefully**. + log.debug('Copying Faces from "%s"', self.name) faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64) _mesh.polygons.foreach_get('vertices', faces) faces.shape = (-1, 3) # Remove Temporary Mesh + log.debug('Removing Temporary Mesh') bpy.data.meshes.remove(_mesh) return { @@ -291,7 +337,7 @@ class ManagedBLObject(ct.schemas.ManagedObj): } #################### - # - Modifier Methods + # - Modifiers #################### def bl_modifier( self, @@ -299,10 +345,10 @@ class ManagedBLObject(ct.schemas.ManagedObj): ): """Creates a new modifier for the current `bl_object`. - For all Blender modifier type names, see: + - Modifier Type Names: """ if not (bl_object := bpy.data.objects.get(self.name)): - msg = "Can't add modifier to BL object that doesn't exist" + msg = f'Tried to add modifier to "{self.name}", but it has no bl_object' raise ValueError(msg) # (Create and) Return Modifier @@ -376,8 +422,7 @@ class ManagedBLObject(ct.schemas.ManagedObj): # Quickly Determine if IDPropertyArray is Equal if ( hasattr(bl_modifier[interface_identifier], 'to_list') - and tuple(bl_modifier[interface_identifier].to_list()) - == value + and tuple(bl_modifier[interface_identifier].to_list()) == value ): continue @@ -395,18 +440,3 @@ class ManagedBLObject(ct.schemas.ManagedObj): # Update DepGraph (if anything changed) if modifier_altered: bl_object.data.update() - - # @property - # def volume(self) -> bpy.types.Volume: - # """Returns the object's volume data. - # - # Raises an error if the object has no volume data. - # """ - # if ( - # (bl_object := bpy.data.objects.get(self.bl_object_name)) - # and bl_object.type == "VOLUME" - # ): - # return bl_object.data - # - # msg = f"Requested VOLUME data from `bl_object` of type {bl_object.type}" - # raise ValueError(msg) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py index c2c5d36..d19d743 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -125,9 +125,7 @@ class MaxwellSimTree(bpy.types.NodeTree): 'to_add': [], } for link_ptr in delta_links['removed']: - from_socket = self._node_link_cache.link_ptrs_from_sockets[ - link_ptr - ] + from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr] to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr] # Update Socket Caches @@ -136,9 +134,7 @@ class MaxwellSimTree(bpy.types.NodeTree): # Trigger Report Chain on Socket that Just Lost a Link ## Aka. Forward-Refresh Caches Relying on Linkage - if not ( - consent_removal := to_socket.sync_link_removed(from_socket) - ): + if not (consent_removal := to_socket.sync_link_removed(from_socket)): # Did Not Consent to Removal: Queue Add Link link_alterations['to_add'].append((from_socket, to_socket)) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py index 543b0de..2b481eb 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py @@ -1,24 +1,22 @@ -import typing as typ -import functools - import bpy -import tidy3d as td import sympy as sp import sympy.physics.units as spu -import numpy as np -import scipy as sc +import tidy3d as td -from .....utils import analyze_geonodes +from .....utils import analyze_geonodes, logger from .....utils import extra_sympy_units as spux from ... import contracts as ct -from ... import sockets -from ... import managed_objs +from ... import managed_objs, sockets from .. import base +log = logger.get(__name__) + GEONODES_MONITOR_BOX = 'monitor_box' class EHFieldMonitorNode(base.MaxwellSimNode): + """Node providing for the monitoring of electromagnetic fields within a given planar region or volume.""" + node_type = ct.NodeType.EHFieldMonitor bl_label = 'E/H Field Monitor' use_sim_node_name = True @@ -41,9 +39,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode): }, 'Time Domain': { 'Rec Start': sockets.PhysicalTimeSocketDef(), - 'Rec Stop': sockets.PhysicalTimeSocketDef( - default_value=200 * spux.fs - ), + 'Rec Stop': sockets.PhysicalTimeSocketDef(default_value=200 * spux.fs), 'Samples/Time': sockets.IntegerNumberSocketDef( default_value=100, ), @@ -60,19 +56,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode): ) } - #################### - # - Properties - #################### - - #################### - # - UI - #################### - def draw_props(self, context, layout): - pass - - def draw_info(self, context, col): - pass - #################### # - Output Sockets #################### @@ -91,7 +74,8 @@ class EHFieldMonitorNode(base.MaxwellSimNode): ) def compute_monitor( self, input_sockets: dict, props: dict - ) -> td.FieldTimeMonitor: + ) -> td.FieldMonitor | td.FieldTimeMonitor: + """Computes the value of the 'Monitor' output socket, which the user can select as being either a `td.FieldMonitor` or `td.FieldTimeMonitor`.""" _center = input_sockets['Center'] _size = input_sockets['Size'] _samples_space = input_sockets['Samples/Space'] @@ -103,33 +87,44 @@ class EHFieldMonitorNode(base.MaxwellSimNode): if props['active_socket_set'] == 'Freq Domain': freqs = input_sockets['Freqs'] + log.info( + 'Computing FieldMonitor (name=%s) with center=%s, size=%s', + props['sim_node_name'], + center, + size, + ) return td.FieldMonitor( center=center, size=size, name=props['sim_node_name'], interval_space=samples_space, freqs=[ - float(spu.convert_to(freq, spu.hertz) / spu.hertz) - for freq in freqs + float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs ], ) - else: ## Time Domain - _rec_start = input_sockets['Rec Start'] - _rec_stop = input_sockets['Rec Stop'] - samples_time = input_sockets['Samples/Time'] + ## Time Domain + _rec_start = input_sockets['Rec Start'] + _rec_stop = input_sockets['Rec Stop'] + samples_time = input_sockets['Samples/Time'] - rec_start = spu.convert_to(_rec_start, spu.second) / spu.second - rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second + rec_start = spu.convert_to(_rec_start, spu.second) / spu.second + rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second - return td.FieldTimeMonitor( - center=center, - size=size, - name=props['sim_node_name'], - start=rec_start, - stop=rec_stop, - interval=samples_time, - interval_space=samples_space, - ) + log.info( + 'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s', + props['sim_node_name'], + center, + size, + ) + return td.FieldTimeMonitor( + center=center, + size=size, + name=props['sim_node_name'], + start=rec_start, + stop=rec_stop, + interval=samples_time, + interval_space=samples_space, + ) #################### # - Preview - Changes to Input Sockets @@ -144,32 +139,22 @@ class EHFieldMonitorNode(base.MaxwellSimNode): input_sockets: dict, managed_objs: dict[str, ct.schemas.ManagedObj], ): + """Alters the managed 3D preview objects whenever the center or size input sockets are changed.""" _center = input_sockets['Center'] - center = tuple( - [float(el) for el in spu.convert_to(_center, spu.um) / spu.um] - ) + center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) _size = input_sockets['Size'] - size = tuple( - [float(el) for el in spu.convert_to(_size, spu.um) / spu.um] - ) - ## TODO: Preview unit system?? Presume um for now + size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um]) # Retrieve Hard-Coded GeoNodes and Analyze Input geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX] - geonodes_interface = analyze_geonodes.interface( - geo_nodes, direc='INPUT' - ) + geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') # Sync Modifier Inputs managed_objs['monitor_box'].sync_geonodes_modifier( geonodes_node_group=geo_nodes, geonodes_identifier_to_value={ geonodes_interface['Size'].identifier: size, - ## TODO: Use 'bl_socket_map.value_to_bl`! - ## - This accounts for auto-conversion, unit systems, etc. . - ## - We could keep it in the node base class... - ## - ...But it needs aligning with Blender, too. Hmm. }, ) @@ -186,6 +171,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode): self, managed_objs: dict[str, ct.schemas.ManagedObj], ): + """Requests that the managed object be previewed in response to a user request to show the preview.""" managed_objs['monitor_box'].show_preview('MESH') self.on_value_changed__center_size() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 8c3aacc..4f976c6 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -1,17 +1,14 @@ -import functools -import typing as typ -import json -from pathlib import Path - import bpy import sympy as sp -import pydantic as pyd -import tidy3d as td +from .....utils import logger from ... import contracts as ct from ... import sockets -from .. import base from ...managed_objs import managed_bl_object +from .. import base + +log = logger.get(__name__) +console = logger.OUTPUT_CONSOLE class ConsoleViewOperator(bpy.types.Operator): @@ -67,9 +64,7 @@ class ViewerNode(base.MaxwellSimNode): name='Auto 3D Preview', description="Whether to auto-preview anything 3D, that's plugged into the viewer node", default=False, - update=lambda self, context: self.sync_prop( - 'auto_3d_preview', context - ), + update=lambda self, context: self.sync_prop('auto_3d_preview', context), ) #################### @@ -111,9 +106,9 @@ class ViewerNode(base.MaxwellSimNode): return if isinstance(data, sp.Basic): - sp.pprint(data, use_unicode=True) - - print(str(data)) + console.print(sp.pretty(data, use_unicode=True)) + else: + console.print(data) #################### # - Updates diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py index 3197bea..a54fcda 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py @@ -1,39 +1,31 @@ import typing as typ import tidy3d as td -import numpy as np -import sympy as sp -import sympy.physics.units as spu - -import bpy -from bpy_types import bpy_types -import bmesh from .....utils import analyze_geonodes -from ... import bl_socket_map +from ... import bl_socket_map, managed_objs, sockets from ... import contracts as ct -from ... import sockets from .. import base -from ... import managed_objs class GeoNodesStructureNode(base.MaxwellSimNode): node_type = ct.NodeType.GeoNodesStructure bl_label = 'GeoNodes Structure' + use_sim_node_name = True #################### # - Sockets #################### - input_sockets = { + input_sockets: typ.ClassVar = { 'Unit System': sockets.PhysicalUnitSystemSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(), 'GeoNodes': sockets.BlenderGeoNodesSocketDef(), } - output_sockets = { + output_sockets: typ.ClassVar = { 'Structure': sockets.MaxwellStructureSocketDef(), } - managed_obj_defs = { + managed_obj_defs: typ.ClassVar = { 'geometry': ct.schemas.ManagedObjDef( mk=lambda name: managed_objs.ManagedBLObject(name), name_prefix='', @@ -92,9 +84,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode): # Analyze GeoNodes ## Extract Valid Inputs (via GeoNodes Tree "Interface") - geonodes_interface = analyze_geonodes.interface( - geo_nodes, direc='INPUT' - ) + geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') # Set Loose Input Sockets ## Retrieve the appropriate SocketDef for the Blender Interface Socket @@ -138,9 +128,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode): # Analyze GeoNodes Interface (input direction) ## This retrieves NodeTreeSocketInterface elements - geonodes_interface = analyze_geonodes.interface( - geo_nodes, direc='INPUT' - ) + geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') ## TODO: Check that Loose Sockets matches the Interface ## - If the user deletes an interface socket, bad things will happen. @@ -156,9 +144,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode): loose_input_sockets[socket_name], unit_system, ) - for socket_name, bl_interface_socket in ( - geonodes_interface.items() - ) + for socket_name, bl_interface_socket in (geonodes_interface.items()) }, ) @@ -185,6 +171,4 @@ class GeoNodesStructureNode(base.MaxwellSimNode): BL_REGISTER = [ GeoNodesStructureNode, ] -BL_NODES = { - ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES) -} +BL_NODES = {ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)} diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py index e1c74b4..98237f7 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py @@ -41,6 +41,7 @@ PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef from . import blender +BlenderMaterialSocketDef = blender.BlenderMaterialSocketDef BlenderObjectSocketDef = blender.BlenderObjectSocketDef BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef BlenderImageSocketDef = blender.BlenderImageSocketDef diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py index e18eff0..d96a0f0 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py @@ -1,6 +1,7 @@ +from . import collection, material from . import object as object_socket -from . import collection +BlenderMaterialSocketDef = material.BlenderMaterialSocketDef BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef @@ -8,13 +9,13 @@ from . import image BlenderImageSocketDef = image.BlenderImageSocketDef -from . import geonodes -from . import text +from . import geonodes, text BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef BlenderTextSocketDef = text.BlenderTextSocketDef BL_REGISTER = [ + *material.BL_REGISTER, *object_socket.BL_REGISTER, *collection.BL_REGISTER, *text.BL_REGISTER, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py index 8f88bd4..9c3b01d 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py @@ -1,10 +1,8 @@ -import typing as typ - import bpy import pydantic as pyd -from .. import base from ... import contracts as ct +from .. import base #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py new file mode 100644 index 0000000..8473683 --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py @@ -0,0 +1,55 @@ +import bpy +import pydantic as pyd + +from ... import contracts as ct +from .. import base + + +class BlenderMaterialBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.BlenderMaterial + bl_label = 'Blender Material' + + #################### + # - Properties + #################### + raw_value: bpy.props.PointerProperty( + name='Blender Material', + description='Represents a Blender material', + type=bpy.types.Material, + update=(lambda self, context: self.sync_prop('raw_value', context)), + ) + + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, 'raw_value', text='') + + #################### + # - Default Value + #################### + @property + def value(self) -> bpy.types.Material | None: + return self.raw_value + + @value.setter + def value(self, value: bpy.types.Material) -> None: + self.raw_value = value + + +#################### +# - Socket Configuration +#################### +class BlenderMaterialSocketDef(pyd.BaseModel): + socket_type: ct.SocketType = ct.SocketType.BlenderMaterial + + def init(self, bl_socket: BlenderMaterialBLSocket) -> None: + pass + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaterialBLSocket, +] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py index 178e9ea..556331c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py @@ -1,10 +1,8 @@ -import typing as typ - import bpy import pydantic as pyd -from .. import base from ... import contracts as ct +from .. import base #################### diff --git a/src/blender_maxwell/registration.py b/src/blender_maxwell/registration.py index 81b6a3d..c32c50f 100644 --- a/src/blender_maxwell/registration.py +++ b/src/blender_maxwell/registration.py @@ -139,8 +139,6 @@ def delay_registration( DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb -def run_delayed_registration( - delayed_reg_key: DelayedRegKey, path_deps: Path -) -> None: +def run_delayed_registration(delayed_reg_key: DelayedRegKey, path_deps: Path) -> None: register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key) register_cb(path_deps) diff --git a/src/blender_maxwell/services/tdcloud.py b/src/blender_maxwell/services/tdcloud.py index 6a04a06..f67c60a 100644 --- a/src/blender_maxwell/services/tdcloud.py +++ b/src/blender_maxwell/services/tdcloud.py @@ -1,4 +1,5 @@ """Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library. + - SimulationTask: - Tidy3D Stub: """ diff --git a/src/blender_maxwell/utils/logger.py b/src/blender_maxwell/utils/logger.py index efd6bef..380cc11 100644 --- a/src/blender_maxwell/utils/logger.py +++ b/src/blender_maxwell/utils/logger.py @@ -3,6 +3,7 @@ from pathlib import Path import rich.console import rich.logging +import rich.traceback from .. import info from ..nodeps.utils import simple_logger @@ -15,6 +16,16 @@ from ..nodeps.utils.simple_logger import ( sync_loggers, # noqa: F401 ) +OUTPUT_CONSOLE = rich.console.Console( + color_system='truecolor', + ## TODO: color_system should be 'auto'; bl_run.py hijinks are interfering +) +ERROR_CONSOLE = rich.console.Console( + color_system='truecolor', stderr=True + ## TODO: color_system should be 'auto'; bl_run.py hijinks are interfering +) +rich.traceback.install(show_locals=True, console=ERROR_CONSOLE) + #################### # - Logging Handlers @@ -26,19 +37,14 @@ def console_handler(level: LogLevel) -> rich.logging.RichHandler: ) rich_handler = rich.logging.RichHandler( level=level, - console=rich.console.Console( - color_system='truecolor', stderr=True - ), ## TODO: Should be 'auto'; bl_run.py hijinks are interfering - # console=rich.console.Console(stderr=True), + console=ERROR_CONSOLE, rich_tracebacks=True, ) rich_handler.setFormatter(rich_formatter) return rich_handler -def file_handler( - path_log_file: Path, level: LogLevel -) -> rich.logging.RichHandler: +def file_handler(path_log_file: Path, level: LogLevel) -> rich.logging.RichHandler: return simple_logger.file_handler(path_log_file, level) @@ -60,7 +66,7 @@ def get(module_name): #################### # - Logger Sync #################### -#def upgrade_simple_loggers(): -# """Upgrades simple loggers to rich-enabled loggers.""" -# for logger in simple_loggers(): -# setup_logger(console_handler, file_handler, logger) +# def upgrade_simple_loggers(): +# """Upgrades simple loggers to rich-enabled loggers.""" +# for logger in simple_loggers(): +# setup_logger(console_handler, file_handler, logger)