feat: ManagedObj Semantics

main
Sofus Albert Høgsbro Rose 2024-04-01 16:48:56 +02:00
parent a4e764ba21
commit 221d5378e4
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
34 changed files with 1063 additions and 241 deletions

339
; 100644
View File

@ -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,
# }
]

33
TODO.md
View File

@ -33,6 +33,7 @@
## Outputs ## Outputs
[x] Viewer [x] Viewer
- [ ] **BIG ONE**: Remove image preview when disabling plots. - [ ] **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. - [ ] Either enforce singleton, or find a way to have several viewers at the same time.
- [ ] A setting that live-previews just a value. - [ ] A setting that live-previews just a value.
- [ ] Pop-up multiline string print as alternative to console print. - [ ] 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 / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
[ ] Tests / Wood Pile [ ] Tests / Wood Pile
[ ] Primitives / Plane [ ] Structures / Primitives / Plane
[ ] Primitives / Box [x] Structures / Primitives / Box
[ ] Primitives / Sphere [x] Structures / Primitives / Sphere
[ ] Primitives / Cylinder [ ] Structures / Primitives / Cylinder
[ ] Primitives / Ring [x] Structures / Primitives / Ring
[ ] Primitives / Capsule [ ] Structures / Primitives / Capsule
[ ] Primitives / Cone [ ] 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 / Square Array **NOTE: Ring and cylinder**
[ ] Array / Hex Array **NOTE: Ring and cylinder** [ ] Array / Hex Array **NOTE: Ring and cylinder**
@ -337,12 +349,15 @@
# Architecture # Architecture
## CRITICAL ## 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 [ ] 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. [ ] 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. - 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. [ ] 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 ## Registration and Contracts
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description. [x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.

View File

@ -14,7 +14,6 @@ dependencies = [
"networkx==3.2.*", "networkx==3.2.*",
"rich==12.5.*", "rich==12.5.*",
"rtree==1.2.*", "rtree==1.2.*",
# Pin Blender 4.1.0-Compatible Versions # Pin Blender 4.1.0-Compatible Versions
## The dependency resolver will report if anything is wonky. ## The dependency resolver will report if anything is wonky.
"urllib3==1.26.8", "urllib3==1.26.8",
@ -100,6 +99,7 @@ ignore = [
"Q001", # Conflicts w/Formatter "Q001", # Conflicts w/Formatter
"Q002", # Conflicts w/Formatter "Q002", # Conflicts w/Formatter
"Q003", # Conflicts w/Formatter "Q003", # Conflicts w/Formatter
"D206", # Conflicts w/Formatter
"B008", # FastAPI uses this for Depends(), Security(), etc. . "B008", # FastAPI uses this for Depends(), Security(), etc. .
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant "E701", # class foo(Parent): pass or if simple: return are perfectly elegant
"ERA001", # 'Commented-out code' seems to be just about anything to ruff "ERA001", # 'Commented-out code' seems to be just about anything to ruff

View File

@ -46,9 +46,10 @@ BL_REGISTER__BEFORE_DEPS = [
def BL_REGISTER__AFTER_DEPS(path_deps: Path): def BL_REGISTER__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_REGISTER') log.info('Loading After-Deps BL_REGISTER')
with pydeps.importable_addon_deps(path_deps): with pydeps.importable_addon_deps(path_deps):
from . import node_trees, operators from . import assets, node_trees, operators
return [ return [
*operators.BL_REGISTER, *operators.BL_REGISTER,
*assets.BL_REGISTER,
*node_trees.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): def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS') log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
with pydeps.importable_addon_deps(path_deps): with pydeps.importable_addon_deps(path_deps):
from . import operators from . import assets, operators
return [ return [
*operators.BL_KEYMAP_ITEM_DEFS, *operators.BL_KEYMAP_ITEM_DEFS,
*assets.BL_KEYMAP_ITEM_DEFS,
] ]

View File

@ -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',
]

BIN
src/blender_maxwell/assets/assets.blend (Stored with Git LFS) 100644

Binary file not shown.

View File

@ -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

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/blender_maxwell/assets/geonodes/template.blend (Stored with Git LFS) 100644

Binary file not shown.

View File

@ -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,
# }
]

View File

@ -3,38 +3,47 @@ from pathlib import Path
import bpy import bpy
PATH_ADDON_ROOT = Path(__file__).resolve().parent
#################### ####################
# - Addon Info # - 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: with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
PROJ_SPEC = tomllib.load(f) PROJ_SPEC = tomllib.load(f)
## bl_info is filled with PROJ_SPEC when packing the .zip.
ADDON_NAME = PROJ_SPEC['project']['name'] ADDON_NAME = PROJ_SPEC['project']['name']
ADDON_VERSION = PROJ_SPEC['project']['version'] ADDON_VERSION = PROJ_SPEC['project']['version']
# PyDeps Path Info ####################
## requirements.lock is written when packing the .zip. # - Asset Info
## By default, the addon pydeps are kept in the addon dir. ####################
PATH_ASSETS = PATH_ADDON_ROOT / 'assets'
####################
# - PyDeps Info
####################
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock' PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies' 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. ## By default, the addon file log writes to the addon dir.
## The initial .log_level contents are written when packing the .zip. ## The initial .log_level contents are written when packing the .zip.
## Subsequent changes are managed by nodeps.utils.simple_logger.py. ## 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' PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f: with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
BOOTSTRAP_LOG_LEVEL = int(f.read().strip()) BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
#################### ####################
# - Addon Getters # - Addon Prefs Info
#################### ####################
def addon_prefs() -> bpy.types.AddonPreferences | None: def addon_prefs() -> bpy.types.AddonPreferences | None:
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None: if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:

View File

@ -42,6 +42,7 @@ SOCKET_COLORS = {
ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange
ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach
# Blender # 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.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.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple
ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple

View File

@ -1,11 +1,17 @@
from .socket_types import SocketType as ST from .socket_types import SocketType as ST
BL_SOCKET_DIRECT_TYPE_MAP = { BL_SOCKET_DIRECT_TYPE_MAP = {
('NodeSocketString', 1): ST.String, # Blender
('NodeSocketBool', 1): ST.Bool,
('NodeSocketCollection', 1): ST.BlenderCollection, ('NodeSocketCollection', 1): ST.BlenderCollection,
('NodeSocketImage', 1): ST.BlenderImage, ('NodeSocketImage', 1): ST.BlenderImage,
('NodeSocketObject', 1): ST.BlenderObject, ('NodeSocketObject', 1): ST.BlenderObject,
('NodeSocketMaterial', 1): ST.BlenderMaterial,
# Basic
('NodeSocketString', 1): ST.String,
('NodeSocketBool', 1): ST.Bool,
# Float
('NodeSocketFloat', 1): ST.RealNumber, ('NodeSocketFloat', 1): ST.RealNumber,
# ("NodeSocketFloatAngle", 1): ST.PhysicalAngle, # ("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
# ("NodeSocketFloatDistance", 1): ST.PhysicalLength, # ("NodeSocketFloatDistance", 1): ST.PhysicalLength,
@ -13,12 +19,17 @@ BL_SOCKET_DIRECT_TYPE_MAP = {
('NodeSocketFloatPercentage', 1): ST.RealNumber, ('NodeSocketFloatPercentage', 1): ST.RealNumber,
# ("NodeSocketFloatTime", 1): ST.PhysicalTime, # ("NodeSocketFloatTime", 1): ST.PhysicalTime,
# ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime, # ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
# Int
('NodeSocketInt', 1): ST.IntegerNumber, ('NodeSocketInt', 1): ST.IntegerNumber,
('NodeSocketIntFactor', 1): ST.IntegerNumber, ('NodeSocketIntFactor', 1): ST.IntegerNumber,
('NodeSocketIntPercentage', 1): ST.IntegerNumber, ('NodeSocketIntPercentage', 1): ST.IntegerNumber,
('NodeSocketIntUnsigned', 1): ST.IntegerNumber, ('NodeSocketIntUnsigned', 1): ST.IntegerNumber,
('NodeSocketRotation', 2): ST.PhysicalRot2D,
# Array-Like
('NodeSocketColor', 3): ST.Color, ('NodeSocketColor', 3): ST.Color,
('NodeSocketRotation', 2): ST.PhysicalRot2D,
('NodeSocketVector', 2): ST.Real2DVector, ('NodeSocketVector', 2): ST.Real2DVector,
('NodeSocketVector', 3): ST.Real3DVector, ('NodeSocketVector', 3): ST.Real3DVector,
# ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D, # ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,

View File

@ -38,6 +38,7 @@ SOCKET_SHAPES = {
ST.PhysicalPol: 'CIRCLE', ST.PhysicalPol: 'CIRCLE',
ST.PhysicalFreq: 'CIRCLE', ST.PhysicalFreq: 'CIRCLE',
# Blender # Blender
ST.BlenderMaterial: 'DIAMOND',
ST.BlenderObject: 'DIAMOND', ST.BlenderObject: 'DIAMOND',
ST.BlenderCollection: 'DIAMOND', ST.BlenderCollection: 'DIAMOND',
ST.BlenderImage: 'DIAMOND', ST.BlenderImage: 'DIAMOND',

View File

@ -34,6 +34,7 @@ class SocketType(BlenderTypeEnum):
Complex3DVector = enum.auto() Complex3DVector = enum.auto()
# Blender # Blender
BlenderMaterial = enum.auto()
BlenderObject = enum.auto() BlenderObject = enum.auto()
BlenderCollection = enum.auto() BlenderCollection = enum.auto()

View File

@ -1,18 +1,15 @@
import typing as typ
import typing_extensions as typx
import functools
import contextlib import contextlib
import io
import numpy as np
import pydantic as pyd
import matplotlib.axis as mpl_ax
import bpy
import bmesh import bmesh
import bpy
import numpy as np
import typing_extensions as typx
from ....utils import logger
from .. import contracts as ct from .. import contracts as ct
log = logger.get(__name__)
ModifierType = typx.Literal['NODES', 'ARRAY'] ModifierType = typx.Literal['NODES', 'ARRAY']
MODIFIER_NAMES = { MODIFIER_NAMES = {
'NODES': 'BLMaxwell_GeoNodes', 'NODES': 'BLMaxwell_GeoNodes',
@ -22,6 +19,9 @@ MANAGED_COLLECTION_NAME = 'BLMaxwell'
PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible' PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
####################
# - BLCollection
####################
def bl_collection( def bl_collection(
collection_name: str, view_layer_exclude: bool collection_name: str, view_layer_exclude: bool
) -> bpy.types.Collection: ) -> bpy.types.Collection:
@ -44,63 +44,89 @@ def bl_collection(
return collection return collection
####################
# - BLObject
####################
class ManagedBLObject(ct.schemas.ManagedObj): class ManagedBLObject(ct.schemas.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLObject 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 # - BL Object Name
####################
# Object Name
@property @property
def name(self): def name(self):
return self._bl_object_name return self._bl_object_name
@name.setter @name.setter
def set_name(self, value: str) -> None: def name(self, value: str) -> None:
# Object Doesn't Exist log.info(
if not (bl_object := bpy.data.objects.get(self._bl_object_name)): 'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value
# ...AND Desired Object Name is Not Taken )
if not bpy.data.objects.get(value):
self._bl_object_name = value
return
# ...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: else:
msg = f'Desired name {value} for BL object is taken' msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted'
raise ValueError(msg) raise RuntimeError(msg)
# Object DOES Exist # Set Internal Name
bl_object.name = value self._bl_object_name = value
self._bl_object_name = bl_object.name else:
## - When name exists, Blender adds .### to prevent overlap. log.info(
## - `set_name` is allowed to change the name; nodes account for this. 'Desired BLObject Name "%s" is Taken. Using Blender Rename',
value,
)
# Object Datablock Name # Set Name Anyway, but Respect Blender's Renaming
@property ## When a name already exists, Blender adds .### to prevent overlap.
def bl_mesh_name(self): ## `set_name` is allowed to change the name; nodes account for this.
return self.name bl_object.name = value
self._bl_object_name = bl_object.name
@property log.info(
def bl_volume_name(self): 'Changed BLObject Name to "%s"',
return self.name bl_object.name,
)
# Deallocation ####################
# - Allocation
####################
def __init__(self, name: str):
self.name = name
####################
# - Deallocation
####################
def free(self): def free(self):
if not (bl_object := bpy.data.objects.get(self.name)): if (bl_object := bpy.data.objects.get(self.name)) is None:
return ## Nothing to do return
# Delete the Underlying Datablock # Delete the Underlying Datablock
## This automatically deletes the object too ## This automatically deletes the object too
if bl_object.type == 'MESH': log.info('Removing "%s" BLObject', bl_object.type)
bpy.data.meshes.remove(bl_object.data) if bl_object.type in {'MESH', 'EMPTY'}:
elif bl_object.type == 'EMPTY':
bpy.data.meshes.remove(bl_object.data) bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == 'VOLUME': elif bl_object.type == 'VOLUME':
bpy.data.volumes.remove(bl_object.data) bpy.data.volumes.remove(bl_object.data)
else: else:
msg = f'Type of to-delete `bl_object`, {bl_object.type}, is not valid' msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"'
raise ValueError(msg) raise RuntimeError(msg)
#################### ####################
# - Actions # - Actions
@ -133,9 +159,16 @@ class ManagedBLObject(ct.schemas.ManagedObj):
) )
).objects ).objects
): ):
log.info('Moving "%s" to Preview Collection', bl_object.name)
preview_collection.objects.link(bl_object) preview_collection.objects.link(bl_object)
# Display Parameters
if kind == 'EMPTY' and empty_display_type is not None: 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 bl_object.empty_display_type = empty_display_type
def hide_preview( def hide_preview(
@ -155,21 +188,22 @@ class ManagedBLObject(ct.schemas.ManagedObj):
) )
).objects ).objects
): ):
log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection.objects.unlink(bl_object) preview_collection.objects.unlink(bl_object)
def bl_select(self) -> None: def bl_select(self) -> None:
"""Selects the managed Blender object globally, causing it to be ex. """Selects the managed Blender object globally, causing it to be ex.
outlined in the 3D viewport. outlined in the 3D viewport.
""" """
if not (bl_object := bpy.data.objects.get(self.name)): if (bl_object := bpy.data.objects.get(self.name)) is not None:
msg = 'Managed BLObject does not exist' bpy.ops.object.select_all(action='DESELECT')
raise ValueError(msg) bl_object.select_set(True)
bpy.ops.object.select_all(action='DESELECT') msg = 'Managed BLObject does not exist'
bl_object.select_set(True) raise ValueError(msg)
#################### ####################
# - Managed Object Management # - BLObject Management
#################### ####################
def bl_object( def bl_object(
self, self,
@ -177,30 +211,41 @@ class ManagedBLObject(ct.schemas.ManagedObj):
): ):
"""Returns the managed blender object. """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. object and recreate.
""" """
# Remove Object (if mismatch) # Remove Object (if mismatch)
if ( if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind:
bl_object := bpy.data.objects.get(self.name) log.info(
) and bl_object.type != kind: 'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)',
bl_object.name,
bl_object.type,
kind,
)
self.free() self.free()
# Create Object w/Appropriate Data Block # Create Object w/Appropriate Data Block
if not (bl_object := bpy.data.objects.get(self.name)): if not (bl_object := bpy.data.objects.get(self.name)):
log.info(
'Creating "%s" with kind "%s"',
bl_object.name,
kind,
)
if kind == 'MESH': if kind == 'MESH':
bl_data = bpy.data.meshes.new(self.bl_mesh_name) bl_data = bpy.data.meshes.new(self.name)
elif kind == 'EMPTY': elif kind == 'EMPTY':
bl_data = None bl_data = None
elif kind == 'VOLUME': elif kind == 'VOLUME':
raise NotImplementedError raise NotImplementedError
else: else:
msg = ( msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"'
f'Requested `bl_object` type {bl_object.type} is not valid'
)
raise ValueError(msg) raise ValueError(msg)
bl_object = bpy.data.objects.new(self.name, bl_data) bl_object = bpy.data.objects.new(self.name, bl_data)
log.debug(
'Linking "%s" to Base Collection',
bl_object.name,
)
bl_collection( bl_collection(
MANAGED_COLLECTION_NAME, view_layer_exclude=True MANAGED_COLLECTION_NAME, view_layer_exclude=True
).objects.link(bl_object) ).objects.link(bl_object)
@ -211,17 +256,16 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# - Mesh Data Properties # - Mesh Data Properties
#################### ####################
@property @property
def raw_mesh(self) -> bpy.types.Mesh: def mesh_data(self) -> bpy.types.Mesh:
"""Returns the object's raw mesh data. """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 ( if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH':
bl_object := bpy.data.objects.get(self.name)
) and bl_object.type == 'MESH':
return bl_object.data 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) raise ValueError(msg)
@contextlib.contextmanager @contextlib.contextmanager
@ -230,9 +274,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
evaluate: bool = True, evaluate: bool = True,
triangulate: bool = False, triangulate: bool = False,
) -> bpy.types.Mesh: ) -> bpy.types.Mesh:
if ( if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH':
bl_object := bpy.data.objects.get(self.name)
) and bl_object.type == 'MESH':
bmesh_mesh = None bmesh_mesh = None
try: try:
bmesh_mesh = bmesh.new() bmesh_mesh = bmesh.new()
@ -254,7 +296,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
bmesh_mesh.free() bmesh_mesh.free()
else: 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) raise ValueError(msg)
@property @property
@ -262,27 +304,31 @@ class ManagedBLObject(ct.schemas.ManagedObj):
## TODO: Cached ## TODO: Cached
# Ensure Updated Geometry # Ensure Updated Geometry
log.debug('Updating View Layer')
bpy.context.view_layer.update() bpy.context.view_layer.update()
## TODO: Must we?
# Compute Evaluted + Triangulated Mesh # Compute Evaluted + Triangulated Mesh
log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name)
_mesh = bpy.data.meshes.new(name='TemporaryMesh') _mesh = bpy.data.meshes.new(name='TemporaryMesh')
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh: with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
bmesh_mesh.to_mesh(_mesh) bmesh_mesh.to_mesh(_mesh)
# Optimized Vertex Copy # Optimized Vertex Copy
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html> ## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
log.debug('Copying Vertices from "%s"', self.name)
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64) verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
_mesh.vertices.foreach_get('co', verts) _mesh.vertices.foreach_get('co', verts)
verts.shape = (-1, 3) verts.shape = (-1, 3)
# Optimized Triangle Copy # Optimized Triangle Copy
## To understand, read it, **carefully**. ## To understand, read it, **carefully**.
log.debug('Copying Faces from "%s"', self.name)
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64) faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
_mesh.polygons.foreach_get('vertices', faces) _mesh.polygons.foreach_get('vertices', faces)
faces.shape = (-1, 3) faces.shape = (-1, 3)
# Remove Temporary Mesh # Remove Temporary Mesh
log.debug('Removing Temporary Mesh')
bpy.data.meshes.remove(_mesh) bpy.data.meshes.remove(_mesh)
return { return {
@ -291,7 +337,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
} }
#################### ####################
# - Modifier Methods # - Modifiers
#################### ####################
def bl_modifier( def bl_modifier(
self, self,
@ -299,10 +345,10 @@ class ManagedBLObject(ct.schemas.ManagedObj):
): ):
"""Creates a new modifier for the current `bl_object`. """Creates a new modifier for the current `bl_object`.
For all Blender modifier type names, see: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items> - Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
""" """
if not (bl_object := bpy.data.objects.get(self.name)): 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) raise ValueError(msg)
# (Create and) Return Modifier # (Create and) Return Modifier
@ -376,8 +422,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Quickly Determine if IDPropertyArray is Equal # Quickly Determine if IDPropertyArray is Equal
if ( if (
hasattr(bl_modifier[interface_identifier], 'to_list') hasattr(bl_modifier[interface_identifier], 'to_list')
and tuple(bl_modifier[interface_identifier].to_list()) and tuple(bl_modifier[interface_identifier].to_list()) == value
== value
): ):
continue continue
@ -395,18 +440,3 @@ class ManagedBLObject(ct.schemas.ManagedObj):
# Update DepGraph (if anything changed) # Update DepGraph (if anything changed)
if modifier_altered: if modifier_altered:
bl_object.data.update() 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)

View File

@ -125,9 +125,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
'to_add': [], 'to_add': [],
} }
for link_ptr in delta_links['removed']: for link_ptr in delta_links['removed']:
from_socket = self._node_link_cache.link_ptrs_from_sockets[ from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr]
link_ptr
]
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr] to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
# Update Socket Caches # Update Socket Caches
@ -136,9 +134,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
# Trigger Report Chain on Socket that Just Lost a Link # Trigger Report Chain on Socket that Just Lost a Link
## Aka. Forward-Refresh Caches Relying on Linkage ## Aka. Forward-Refresh Caches Relying on Linkage
if not ( if not (consent_removal := to_socket.sync_link_removed(from_socket)):
consent_removal := to_socket.sync_link_removed(from_socket)
):
# Did Not Consent to Removal: Queue Add Link # Did Not Consent to Removal: Queue Add Link
link_alterations['to_add'].append((from_socket, to_socket)) link_alterations['to_add'].append((from_socket, to_socket))

View File

@ -1,24 +1,22 @@
import typing as typ
import functools
import bpy import bpy
import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import numpy as np import tidy3d as td
import scipy as sc
from .....utils import analyze_geonodes from .....utils import analyze_geonodes, logger
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import managed_objs, sockets
from ... import managed_objs
from .. import base from .. import base
log = logger.get(__name__)
GEONODES_MONITOR_BOX = 'monitor_box' GEONODES_MONITOR_BOX = 'monitor_box'
class EHFieldMonitorNode(base.MaxwellSimNode): class EHFieldMonitorNode(base.MaxwellSimNode):
"""Node providing for the monitoring of electromagnetic fields within a given planar region or volume."""
node_type = ct.NodeType.EHFieldMonitor node_type = ct.NodeType.EHFieldMonitor
bl_label = 'E/H Field Monitor' bl_label = 'E/H Field Monitor'
use_sim_node_name = True use_sim_node_name = True
@ -41,9 +39,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
}, },
'Time Domain': { 'Time Domain': {
'Rec Start': sockets.PhysicalTimeSocketDef(), 'Rec Start': sockets.PhysicalTimeSocketDef(),
'Rec Stop': sockets.PhysicalTimeSocketDef( 'Rec Stop': sockets.PhysicalTimeSocketDef(default_value=200 * spux.fs),
default_value=200 * spux.fs
),
'Samples/Time': sockets.IntegerNumberSocketDef( 'Samples/Time': sockets.IntegerNumberSocketDef(
default_value=100, 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 # - Output Sockets
#################### ####################
@ -91,7 +74,8 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
) )
def compute_monitor( def compute_monitor(
self, input_sockets: dict, props: dict 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'] _center = input_sockets['Center']
_size = input_sockets['Size'] _size = input_sockets['Size']
_samples_space = input_sockets['Samples/Space'] _samples_space = input_sockets['Samples/Space']
@ -103,33 +87,44 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs'] freqs = input_sockets['Freqs']
log.info(
'Computing FieldMonitor (name=%s) with center=%s, size=%s',
props['sim_node_name'],
center,
size,
)
return td.FieldMonitor( return td.FieldMonitor(
center=center, center=center,
size=size, size=size,
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=samples_space, interval_space=samples_space,
freqs=[ freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz) float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
for freq in freqs
], ],
) )
else: ## Time Domain ## Time Domain
_rec_start = input_sockets['Rec Start'] _rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop'] _rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time'] samples_time = input_sockets['Samples/Time']
rec_start = spu.convert_to(_rec_start, 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 rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
return td.FieldTimeMonitor( log.info(
center=center, 'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s',
size=size, props['sim_node_name'],
name=props['sim_node_name'], center,
start=rec_start, size,
stop=rec_stop, )
interval=samples_time, return td.FieldTimeMonitor(
interval_space=samples_space, 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 # - Preview - Changes to Input Sockets
@ -144,32 +139,22 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
input_sockets: dict, input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], 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 = input_sockets['Center']
center = tuple( center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um])
[float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
)
_size = input_sockets['Size'] _size = input_sockets['Size']
size = tuple( size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um])
[float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
)
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input # Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX] geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
geonodes_interface = analyze_geonodes.interface( geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
geo_nodes, direc='INPUT'
)
# Sync Modifier Inputs # Sync Modifier Inputs
managed_objs['monitor_box'].sync_geonodes_modifier( managed_objs['monitor_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes, geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={ geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size, 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, self,
managed_objs: dict[str, ct.schemas.ManagedObj], 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') managed_objs['monitor_box'].show_preview('MESH')
self.on_value_changed__center_size() self.on_value_changed__center_size()

View File

@ -1,17 +1,14 @@
import functools
import typing as typ
import json
from pathlib import Path
import bpy import bpy
import sympy as sp import sympy as sp
import pydantic as pyd
import tidy3d as td
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base
from ...managed_objs import managed_bl_object from ...managed_objs import managed_bl_object
from .. import base
log = logger.get(__name__)
console = logger.OUTPUT_CONSOLE
class ConsoleViewOperator(bpy.types.Operator): class ConsoleViewOperator(bpy.types.Operator):
@ -67,9 +64,7 @@ class ViewerNode(base.MaxwellSimNode):
name='Auto 3D Preview', name='Auto 3D Preview',
description="Whether to auto-preview anything 3D, that's plugged into the viewer node", description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
default=False, default=False,
update=lambda self, context: self.sync_prop( update=lambda self, context: self.sync_prop('auto_3d_preview', context),
'auto_3d_preview', context
),
) )
#################### ####################
@ -111,9 +106,9 @@ class ViewerNode(base.MaxwellSimNode):
return return
if isinstance(data, sp.Basic): if isinstance(data, sp.Basic):
sp.pprint(data, use_unicode=True) console.print(sp.pretty(data, use_unicode=True))
else:
print(str(data)) console.print(data)
#################### ####################
# - Updates # - Updates

View File

@ -1,39 +1,31 @@
import typing as typ import typing as typ
import tidy3d as td 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 .....utils import analyze_geonodes
from ... import bl_socket_map from ... import bl_socket_map, managed_objs, sockets
from ... import contracts as ct from ... import contracts as ct
from ... import sockets
from .. import base from .. import base
from ... import managed_objs
class GeoNodesStructureNode(base.MaxwellSimNode): class GeoNodesStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.GeoNodesStructure node_type = ct.NodeType.GeoNodesStructure
bl_label = 'GeoNodes Structure' bl_label = 'GeoNodes Structure'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Unit System': sockets.PhysicalUnitSystemSocketDef(), 'Unit System': sockets.PhysicalUnitSystemSocketDef(),
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'GeoNodes': sockets.BlenderGeoNodesSocketDef(), 'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'geometry': ct.schemas.ManagedObjDef( 'geometry': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='', name_prefix='',
@ -92,9 +84,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
# Analyze GeoNodes # Analyze GeoNodes
## Extract Valid Inputs (via GeoNodes Tree "Interface") ## Extract Valid Inputs (via GeoNodes Tree "Interface")
geonodes_interface = analyze_geonodes.interface( geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
geo_nodes, direc='INPUT'
)
# Set Loose Input Sockets # Set Loose Input Sockets
## Retrieve the appropriate SocketDef for the Blender Interface Socket ## Retrieve the appropriate SocketDef for the Blender Interface Socket
@ -138,9 +128,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
# Analyze GeoNodes Interface (input direction) # Analyze GeoNodes Interface (input direction)
## This retrieves NodeTreeSocketInterface elements ## This retrieves NodeTreeSocketInterface elements
geonodes_interface = analyze_geonodes.interface( geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
geo_nodes, direc='INPUT'
)
## TODO: Check that Loose Sockets matches the Interface ## TODO: Check that Loose Sockets matches the Interface
## - If the user deletes an interface socket, bad things will happen. ## - If the user deletes an interface socket, bad things will happen.
@ -156,9 +144,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
loose_input_sockets[socket_name], loose_input_sockets[socket_name],
unit_system, unit_system,
) )
for socket_name, bl_interface_socket in ( for socket_name, bl_interface_socket in (geonodes_interface.items())
geonodes_interface.items()
)
}, },
) )
@ -185,6 +171,4 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
GeoNodesStructureNode, GeoNodesStructureNode,
] ]
BL_NODES = { BL_NODES = {ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)}
ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)
}

View File

@ -41,6 +41,7 @@ PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef
from . import blender from . import blender
BlenderMaterialSocketDef = blender.BlenderMaterialSocketDef
BlenderObjectSocketDef = blender.BlenderObjectSocketDef BlenderObjectSocketDef = blender.BlenderObjectSocketDef
BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef
BlenderImageSocketDef = blender.BlenderImageSocketDef BlenderImageSocketDef = blender.BlenderImageSocketDef

View File

@ -1,6 +1,7 @@
from . import collection, material
from . import object as object_socket from . import object as object_socket
from . import collection
BlenderMaterialSocketDef = material.BlenderMaterialSocketDef
BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef
BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef
@ -8,13 +9,13 @@ from . import image
BlenderImageSocketDef = image.BlenderImageSocketDef BlenderImageSocketDef = image.BlenderImageSocketDef
from . import geonodes from . import geonodes, text
from . import text
BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef
BlenderTextSocketDef = text.BlenderTextSocketDef BlenderTextSocketDef = text.BlenderTextSocketDef
BL_REGISTER = [ BL_REGISTER = [
*material.BL_REGISTER,
*object_socket.BL_REGISTER, *object_socket.BL_REGISTER,
*collection.BL_REGISTER, *collection.BL_REGISTER,
*text.BL_REGISTER, *text.BL_REGISTER,

View File

@ -1,10 +1,8 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from .. import base
from ... import contracts as ct from ... import contracts as ct
from .. import base
#################### ####################

View File

@ -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,
]

View File

@ -1,10 +1,8 @@
import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
from .. import base
from ... import contracts as ct from ... import contracts as ct
from .. import base
#################### ####################

View File

@ -139,8 +139,6 @@ def delay_registration(
DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb
def run_delayed_registration( def run_delayed_registration(delayed_reg_key: DelayedRegKey, path_deps: Path) -> None:
delayed_reg_key: DelayedRegKey, path_deps: Path
) -> None:
register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key) register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key)
register_cb(path_deps) register_cb(path_deps)

View File

@ -1,4 +1,5 @@
"""Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library. """Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library.
- SimulationTask: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_core.py> - SimulationTask: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_core.py>
- Tidy3D Stub: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/api/tidy3d_stub.py> - Tidy3D Stub: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/api/tidy3d_stub.py>
""" """

View File

@ -3,6 +3,7 @@ from pathlib import Path
import rich.console import rich.console
import rich.logging import rich.logging
import rich.traceback
from .. import info from .. import info
from ..nodeps.utils import simple_logger from ..nodeps.utils import simple_logger
@ -15,6 +16,16 @@ from ..nodeps.utils.simple_logger import (
sync_loggers, # noqa: F401 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 # - Logging Handlers
@ -26,19 +37,14 @@ def console_handler(level: LogLevel) -> rich.logging.RichHandler:
) )
rich_handler = rich.logging.RichHandler( rich_handler = rich.logging.RichHandler(
level=level, level=level,
console=rich.console.Console( console=ERROR_CONSOLE,
color_system='truecolor', stderr=True
), ## TODO: Should be 'auto'; bl_run.py hijinks are interfering
# console=rich.console.Console(stderr=True),
rich_tracebacks=True, rich_tracebacks=True,
) )
rich_handler.setFormatter(rich_formatter) rich_handler.setFormatter(rich_formatter)
return rich_handler return rich_handler
def file_handler( def file_handler(path_log_file: Path, level: LogLevel) -> rich.logging.RichHandler:
path_log_file: Path, level: LogLevel
) -> rich.logging.RichHandler:
return simple_logger.file_handler(path_log_file, level) return simple_logger.file_handler(path_log_file, level)
@ -60,7 +66,7 @@ def get(module_name):
#################### ####################
# - Logger Sync # - Logger Sync
#################### ####################
#def upgrade_simple_loggers(): # def upgrade_simple_loggers():
# """Upgrades simple loggers to rich-enabled loggers.""" # """Upgrades simple loggers to rich-enabled loggers."""
# for logger in simple_loggers(): # for logger in simple_loggers():
# setup_logger(console_handler, file_handler, logger) # setup_logger(console_handler, file_handler, logger)