feat: ManagedObj Semantics
parent
a4e764ba21
commit
221d5378e4
|
@ -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
33
TODO.md
|
@ -33,6 +33,7 @@
|
|||
## Outputs
|
||||
[x] Viewer
|
||||
- [ ] **BIG ONE**: Remove image preview when disabling plots.
|
||||
- [ ] Declare Preview unit system on the viewer node.
|
||||
- [ ] Either enforce singleton, or find a way to have several viewers at the same time.
|
||||
- [ ] A setting that live-previews just a value.
|
||||
- [ ] Pop-up multiline string print as alternative to console print.
|
||||
|
@ -174,13 +175,24 @@
|
|||
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
|
||||
[ ] Tests / Wood Pile
|
||||
|
||||
[ ] Primitives / Plane
|
||||
[ ] Primitives / Box
|
||||
[ ] Primitives / Sphere
|
||||
[ ] Primitives / Cylinder
|
||||
[ ] Primitives / Ring
|
||||
[ ] Primitives / Capsule
|
||||
[ ] Primitives / Cone
|
||||
[ ] Structures / Primitives / Plane
|
||||
[x] Structures / Primitives / Box
|
||||
[x] Structures / Primitives / Sphere
|
||||
[ ] Structures / Primitives / Cylinder
|
||||
[x] Structures / Primitives / Ring
|
||||
[ ] Structures / Primitives / Capsule
|
||||
[ ] Structures / Primitives / Cone
|
||||
|
||||
[ ] Structures / Arrays / Square
|
||||
[ ] Structures / Arrays / Square-Hole
|
||||
[ ] Structures / Arrays / Cyl
|
||||
[ ] Structures / Arrays / Cyl-Hole
|
||||
[x] Structures / Arrays / Box
|
||||
[x] Structures / Arrays / Sphere
|
||||
[ ] Structures / Arrays / Cylinder
|
||||
[-] Structures / Arrays / Ring
|
||||
[ ] Structures / Arrays / Capsule
|
||||
[ ] Structures / Arrays / Cone
|
||||
|
||||
[ ] Array / Square Array **NOTE: Ring and cylinder**
|
||||
[ ] Array / Hex Array **NOTE: Ring and cylinder**
|
||||
|
@ -337,12 +349,15 @@
|
|||
|
||||
# Architecture
|
||||
## CRITICAL
|
||||
With these things in place
|
||||
With these things in place, we're in tip top shape:
|
||||
[ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially
|
||||
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
|
||||
[ ] Finalize Viewer node unit systems.
|
||||
[ ] Introduce a simplified (maybe caching) method of translating sympy-enabled values, ex. 'Center', into values for external use (ex. in a Tidy3D object or in a Blender preview) based on
|
||||
[ ] Abstract the actual unit system dict-like data structure out from the UnitSystem socket.
|
||||
[ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing.
|
||||
- I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs.
|
||||
[ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under.
|
||||
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
|
||||
|
||||
## Registration and Contracts
|
||||
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
Binary file not shown.
|
@ -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
|
|
@ -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.
Binary file not shown.
|
@ -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,
|
||||
# }
|
||||
]
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -34,6 +34,7 @@ class SocketType(BlenderTypeEnum):
|
|||
Complex3DVector = enum.auto()
|
||||
|
||||
# Blender
|
||||
BlenderMaterial = enum.auto()
|
||||
BlenderObject = enum.auto()
|
||||
BlenderCollection = enum.auto()
|
||||
|
||||
|
|
|
@ -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
|
||||
def name(self, value: str) -> None:
|
||||
log.info(
|
||||
'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value
|
||||
)
|
||||
|
||||
if not bpy.data.objects.get(value):
|
||||
self._bl_object_name = value
|
||||
return
|
||||
log.info(
|
||||
'Desired BLObject Name "%s" Not Taken',
|
||||
value,
|
||||
)
|
||||
|
||||
# ...AND Desired Object Name is Taken
|
||||
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
|
||||
# Set Internal Name
|
||||
self._bl_object_name = value
|
||||
else:
|
||||
log.info(
|
||||
'Desired BLObject Name "%s" is Taken. Using Blender Rename',
|
||||
value,
|
||||
)
|
||||
|
||||
# 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
|
||||
## - When name exists, Blender adds .### to prevent overlap.
|
||||
## - `set_name` is allowed to change the name; nodes account for this.
|
||||
|
||||
# Object Datablock Name
|
||||
@property
|
||||
def bl_mesh_name(self):
|
||||
return self.name
|
||||
log.info(
|
||||
'Changed BLObject Name to "%s"',
|
||||
bl_object.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def bl_volume_name(self):
|
||||
return self.name
|
||||
####################
|
||||
# - Allocation
|
||||
####################
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
# Deallocation
|
||||
####################
|
||||
# - 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)
|
||||
|
||||
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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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,17 +87,22 @@ 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
|
||||
## Time Domain
|
||||
_rec_start = input_sockets['Rec Start']
|
||||
_rec_stop = input_sockets['Rec Stop']
|
||||
samples_time = input_sockets['Samples/Time']
|
||||
|
@ -121,6 +110,12 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
|||
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
|
||||
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
|
||||
|
||||
log.info(
|
||||
'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s',
|
||||
props['sim_node_name'],
|
||||
center,
|
||||
size,
|
||||
)
|
||||
return td.FieldTimeMonitor(
|
||||
center=center,
|
||||
size=size,
|
||||
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -41,6 +41,7 @@ PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef
|
|||
|
||||
from . import blender
|
||||
|
||||
BlenderMaterialSocketDef = blender.BlenderMaterialSocketDef
|
||||
BlenderObjectSocketDef = blender.BlenderObjectSocketDef
|
||||
BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef
|
||||
BlenderImageSocketDef = blender.BlenderImageSocketDef
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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,
|
||||
]
|
|
@ -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
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
"""
|
||||
|
|
|
@ -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():
|
||||
# def upgrade_simple_loggers():
|
||||
# """Upgrades simple loggers to rich-enabled loggers."""
|
||||
# for logger in simple_loggers():
|
||||
# setup_logger(console_handler, file_handler, logger)
|
||||
|
|
Loading…
Reference in New Issue