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
[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.

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ class SocketType(BlenderTypeEnum):
Complex3DVector = enum.auto()
# Blender
BlenderMaterial = enum.auto()
BlenderObject = 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 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 <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)
_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: <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)):
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 pydantic as pyd
from .. import base
from ... import contracts as ct
from .. import base
####################

View File

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

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.
- 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>
"""

View File

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