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
|
## Outputs
|
||||||
[x] Viewer
|
[x] Viewer
|
||||||
- [ ] **BIG ONE**: Remove image preview when disabling plots.
|
- [ ] **BIG ONE**: Remove image preview when disabling plots.
|
||||||
|
- [ ] Declare Preview unit system on the viewer node.
|
||||||
- [ ] Either enforce singleton, or find a way to have several viewers at the same time.
|
- [ ] Either enforce singleton, or find a way to have several viewers at the same time.
|
||||||
- [ ] A setting that live-previews just a value.
|
- [ ] A setting that live-previews just a value.
|
||||||
- [ ] Pop-up multiline string print as alternative to console print.
|
- [ ] Pop-up multiline string print as alternative to console print.
|
||||||
|
@ -174,13 +175,24 @@
|
||||||
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
|
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
|
||||||
[ ] Tests / Wood Pile
|
[ ] Tests / Wood Pile
|
||||||
|
|
||||||
[ ] Primitives / Plane
|
[ ] Structures / Primitives / Plane
|
||||||
[ ] Primitives / Box
|
[x] Structures / Primitives / Box
|
||||||
[ ] Primitives / Sphere
|
[x] Structures / Primitives / Sphere
|
||||||
[ ] Primitives / Cylinder
|
[ ] Structures / Primitives / Cylinder
|
||||||
[ ] Primitives / Ring
|
[x] Structures / Primitives / Ring
|
||||||
[ ] Primitives / Capsule
|
[ ] Structures / Primitives / Capsule
|
||||||
[ ] Primitives / Cone
|
[ ] Structures / Primitives / Cone
|
||||||
|
|
||||||
|
[ ] Structures / Arrays / Square
|
||||||
|
[ ] Structures / Arrays / Square-Hole
|
||||||
|
[ ] Structures / Arrays / Cyl
|
||||||
|
[ ] Structures / Arrays / Cyl-Hole
|
||||||
|
[x] Structures / Arrays / Box
|
||||||
|
[x] Structures / Arrays / Sphere
|
||||||
|
[ ] Structures / Arrays / Cylinder
|
||||||
|
[-] Structures / Arrays / Ring
|
||||||
|
[ ] Structures / Arrays / Capsule
|
||||||
|
[ ] Structures / Arrays / Cone
|
||||||
|
|
||||||
[ ] Array / Square Array **NOTE: Ring and cylinder**
|
[ ] Array / Square Array **NOTE: Ring and cylinder**
|
||||||
[ ] Array / Hex Array **NOTE: Ring and cylinder**
|
[ ] Array / Hex Array **NOTE: Ring and cylinder**
|
||||||
|
@ -337,12 +349,15 @@
|
||||||
|
|
||||||
# Architecture
|
# Architecture
|
||||||
## CRITICAL
|
## CRITICAL
|
||||||
With these things in place
|
With these things in place, we're in tip top shape:
|
||||||
[ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially
|
[ ] Linkability / Appendability of library GeoNodes groups, including being able to semantically ask for a particular GeoNodes tree without 'magic strings' that are entirely end-user-file dependent, is completely critical. especially
|
||||||
|
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
|
||||||
|
[ ] Finalize Viewer node unit systems.
|
||||||
|
[ ] Introduce a simplified (maybe caching) method of translating sympy-enabled values, ex. 'Center', into values for external use (ex. in a Tidy3D object or in a Blender preview) based on
|
||||||
|
[ ] Abstract the actual unit system dict-like data structure out from the UnitSystem socket.
|
||||||
[ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing.
|
[ ] Ship the addon with libraries of GeoNodes groups (with NO dependency on the addon), which are linked (internal use) or appended (end-user-selected structures) when needed for previewing.
|
||||||
- I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs.
|
- I don't know that library overrides are the correct approach when it comes to structures used by the end-user. It's extremely easy to make a change to a library structure (or have one made for us by a Blender update!) that completely wrecks all end-user simulations that use it, or override it. By appending, the structure becomes 'part of' the user's simulation, which also makes it quite a bit easier for the user to alter (even drastically) for their own needs.
|
||||||
[ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under.
|
[ ] License header UI for MaxwellSimTrees, to clarify the AGPL-compatible potentially user-selected license that trees must be distributed under.
|
||||||
[ ] Simplify the boilerplate needed to add a particular 3D preview driven by the input sockets of a particular GeoNodes group. It's currently hard for all the wrong reasons, and greatly halts our velocity in developing useful 3D previews of any/everything.
|
|
||||||
|
|
||||||
## Registration and Contracts
|
## Registration and Contracts
|
||||||
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.
|
[x] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description.
|
||||||
|
|
|
@ -14,7 +14,6 @@ dependencies = [
|
||||||
"networkx==3.2.*",
|
"networkx==3.2.*",
|
||||||
"rich==12.5.*",
|
"rich==12.5.*",
|
||||||
"rtree==1.2.*",
|
"rtree==1.2.*",
|
||||||
|
|
||||||
# Pin Blender 4.1.0-Compatible Versions
|
# Pin Blender 4.1.0-Compatible Versions
|
||||||
## The dependency resolver will report if anything is wonky.
|
## The dependency resolver will report if anything is wonky.
|
||||||
"urllib3==1.26.8",
|
"urllib3==1.26.8",
|
||||||
|
@ -100,6 +99,7 @@ ignore = [
|
||||||
"Q001", # Conflicts w/Formatter
|
"Q001", # Conflicts w/Formatter
|
||||||
"Q002", # Conflicts w/Formatter
|
"Q002", # Conflicts w/Formatter
|
||||||
"Q003", # Conflicts w/Formatter
|
"Q003", # Conflicts w/Formatter
|
||||||
|
"D206", # Conflicts w/Formatter
|
||||||
"B008", # FastAPI uses this for Depends(), Security(), etc. .
|
"B008", # FastAPI uses this for Depends(), Security(), etc. .
|
||||||
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant
|
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant
|
||||||
"ERA001", # 'Commented-out code' seems to be just about anything to ruff
|
"ERA001", # 'Commented-out code' seems to be just about anything to ruff
|
||||||
|
|
|
@ -46,9 +46,10 @@ BL_REGISTER__BEFORE_DEPS = [
|
||||||
def BL_REGISTER__AFTER_DEPS(path_deps: Path):
|
def BL_REGISTER__AFTER_DEPS(path_deps: Path):
|
||||||
log.info('Loading After-Deps BL_REGISTER')
|
log.info('Loading After-Deps BL_REGISTER')
|
||||||
with pydeps.importable_addon_deps(path_deps):
|
with pydeps.importable_addon_deps(path_deps):
|
||||||
from . import node_trees, operators
|
from . import assets, node_trees, operators
|
||||||
return [
|
return [
|
||||||
*operators.BL_REGISTER,
|
*operators.BL_REGISTER,
|
||||||
|
*assets.BL_REGISTER,
|
||||||
*node_trees.BL_REGISTER,
|
*node_trees.BL_REGISTER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -62,9 +63,10 @@ BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS = [
|
||||||
def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
|
def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
|
||||||
log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
|
log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
|
||||||
with pydeps.importable_addon_deps(path_deps):
|
with pydeps.importable_addon_deps(path_deps):
|
||||||
from . import operators
|
from . import assets, operators
|
||||||
return [
|
return [
|
||||||
*operators.BL_KEYMAP_ITEM_DEFS,
|
*operators.BL_KEYMAP_ITEM_DEFS,
|
||||||
|
*assets.BL_KEYMAP_ITEM_DEFS,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
import bpy
|
||||||
|
|
||||||
|
PATH_ADDON_ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Addon Info
|
# - Addon Info
|
||||||
####################
|
####################
|
||||||
PATH_ADDON_ROOT = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
# Addon Information
|
|
||||||
## bl_info is filled with PROJ_SPEC when packing the .zip.
|
|
||||||
with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
|
with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
|
||||||
PROJ_SPEC = tomllib.load(f)
|
PROJ_SPEC = tomllib.load(f)
|
||||||
|
## bl_info is filled with PROJ_SPEC when packing the .zip.
|
||||||
|
|
||||||
ADDON_NAME = PROJ_SPEC['project']['name']
|
ADDON_NAME = PROJ_SPEC['project']['name']
|
||||||
ADDON_VERSION = PROJ_SPEC['project']['version']
|
ADDON_VERSION = PROJ_SPEC['project']['version']
|
||||||
|
|
||||||
# PyDeps Path Info
|
####################
|
||||||
## requirements.lock is written when packing the .zip.
|
# - Asset Info
|
||||||
## By default, the addon pydeps are kept in the addon dir.
|
####################
|
||||||
|
PATH_ASSETS = PATH_ADDON_ROOT / 'assets'
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - PyDeps Info
|
||||||
|
####################
|
||||||
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
|
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
|
||||||
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
|
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
|
||||||
|
## requirements.lock is written when packing the .zip.
|
||||||
|
## By default, the addon pydeps are kept in the addon dir.
|
||||||
|
|
||||||
# Logging Info
|
####################
|
||||||
|
# - Logging Info
|
||||||
|
####################
|
||||||
|
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
|
||||||
|
DEFAULT_LOG_PATH.touch(exist_ok=True)
|
||||||
## By default, the addon file log writes to the addon dir.
|
## By default, the addon file log writes to the addon dir.
|
||||||
## The initial .log_level contents are written when packing the .zip.
|
## The initial .log_level contents are written when packing the .zip.
|
||||||
## Subsequent changes are managed by nodeps.utils.simple_logger.py.
|
## Subsequent changes are managed by nodeps.utils.simple_logger.py.
|
||||||
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
|
|
||||||
DEFAULT_LOG_PATH.touch(exist_ok=True)
|
|
||||||
|
|
||||||
PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
|
PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
|
||||||
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
|
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
|
||||||
BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
|
BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Addon Getters
|
# - Addon Prefs Info
|
||||||
####################
|
####################
|
||||||
def addon_prefs() -> bpy.types.AddonPreferences | None:
|
def addon_prefs() -> bpy.types.AddonPreferences | None:
|
||||||
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:
|
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:
|
||||||
|
|
|
@ -42,6 +42,7 @@ SOCKET_COLORS = {
|
||||||
ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange
|
ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange
|
||||||
ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach
|
ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach
|
||||||
# Blender
|
# Blender
|
||||||
|
ST.BlenderMaterial: (0.8, 0.6, 1.0, 1.0), # Lighter Purple
|
||||||
ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple
|
ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple
|
||||||
ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple
|
ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple
|
||||||
ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple
|
ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
from .socket_types import SocketType as ST
|
from .socket_types import SocketType as ST
|
||||||
|
|
||||||
BL_SOCKET_DIRECT_TYPE_MAP = {
|
BL_SOCKET_DIRECT_TYPE_MAP = {
|
||||||
('NodeSocketString', 1): ST.String,
|
# Blender
|
||||||
('NodeSocketBool', 1): ST.Bool,
|
|
||||||
('NodeSocketCollection', 1): ST.BlenderCollection,
|
('NodeSocketCollection', 1): ST.BlenderCollection,
|
||||||
('NodeSocketImage', 1): ST.BlenderImage,
|
('NodeSocketImage', 1): ST.BlenderImage,
|
||||||
('NodeSocketObject', 1): ST.BlenderObject,
|
('NodeSocketObject', 1): ST.BlenderObject,
|
||||||
|
('NodeSocketMaterial', 1): ST.BlenderMaterial,
|
||||||
|
|
||||||
|
# Basic
|
||||||
|
('NodeSocketString', 1): ST.String,
|
||||||
|
('NodeSocketBool', 1): ST.Bool,
|
||||||
|
|
||||||
|
# Float
|
||||||
('NodeSocketFloat', 1): ST.RealNumber,
|
('NodeSocketFloat', 1): ST.RealNumber,
|
||||||
# ("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
|
# ("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
|
||||||
# ("NodeSocketFloatDistance", 1): ST.PhysicalLength,
|
# ("NodeSocketFloatDistance", 1): ST.PhysicalLength,
|
||||||
|
@ -13,12 +19,17 @@ BL_SOCKET_DIRECT_TYPE_MAP = {
|
||||||
('NodeSocketFloatPercentage', 1): ST.RealNumber,
|
('NodeSocketFloatPercentage', 1): ST.RealNumber,
|
||||||
# ("NodeSocketFloatTime", 1): ST.PhysicalTime,
|
# ("NodeSocketFloatTime", 1): ST.PhysicalTime,
|
||||||
# ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
|
# ("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
|
||||||
|
|
||||||
|
# Int
|
||||||
('NodeSocketInt', 1): ST.IntegerNumber,
|
('NodeSocketInt', 1): ST.IntegerNumber,
|
||||||
('NodeSocketIntFactor', 1): ST.IntegerNumber,
|
('NodeSocketIntFactor', 1): ST.IntegerNumber,
|
||||||
('NodeSocketIntPercentage', 1): ST.IntegerNumber,
|
('NodeSocketIntPercentage', 1): ST.IntegerNumber,
|
||||||
('NodeSocketIntUnsigned', 1): ST.IntegerNumber,
|
('NodeSocketIntUnsigned', 1): ST.IntegerNumber,
|
||||||
('NodeSocketRotation', 2): ST.PhysicalRot2D,
|
|
||||||
|
# Array-Like
|
||||||
('NodeSocketColor', 3): ST.Color,
|
('NodeSocketColor', 3): ST.Color,
|
||||||
|
('NodeSocketRotation', 2): ST.PhysicalRot2D,
|
||||||
|
|
||||||
('NodeSocketVector', 2): ST.Real2DVector,
|
('NodeSocketVector', 2): ST.Real2DVector,
|
||||||
('NodeSocketVector', 3): ST.Real3DVector,
|
('NodeSocketVector', 3): ST.Real3DVector,
|
||||||
# ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,
|
# ("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,
|
||||||
|
|
|
@ -38,6 +38,7 @@ SOCKET_SHAPES = {
|
||||||
ST.PhysicalPol: 'CIRCLE',
|
ST.PhysicalPol: 'CIRCLE',
|
||||||
ST.PhysicalFreq: 'CIRCLE',
|
ST.PhysicalFreq: 'CIRCLE',
|
||||||
# Blender
|
# Blender
|
||||||
|
ST.BlenderMaterial: 'DIAMOND',
|
||||||
ST.BlenderObject: 'DIAMOND',
|
ST.BlenderObject: 'DIAMOND',
|
||||||
ST.BlenderCollection: 'DIAMOND',
|
ST.BlenderCollection: 'DIAMOND',
|
||||||
ST.BlenderImage: 'DIAMOND',
|
ST.BlenderImage: 'DIAMOND',
|
||||||
|
|
|
@ -34,6 +34,7 @@ class SocketType(BlenderTypeEnum):
|
||||||
Complex3DVector = enum.auto()
|
Complex3DVector = enum.auto()
|
||||||
|
|
||||||
# Blender
|
# Blender
|
||||||
|
BlenderMaterial = enum.auto()
|
||||||
BlenderObject = enum.auto()
|
BlenderObject = enum.auto()
|
||||||
BlenderCollection = enum.auto()
|
BlenderCollection = enum.auto()
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import typing as typ
|
|
||||||
import typing_extensions as typx
|
|
||||||
import functools
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pydantic as pyd
|
|
||||||
import matplotlib.axis as mpl_ax
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
import bmesh
|
import bmesh
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
import typing_extensions as typx
|
||||||
|
|
||||||
|
from ....utils import logger
|
||||||
from .. import contracts as ct
|
from .. import contracts as ct
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
ModifierType = typx.Literal['NODES', 'ARRAY']
|
ModifierType = typx.Literal['NODES', 'ARRAY']
|
||||||
MODIFIER_NAMES = {
|
MODIFIER_NAMES = {
|
||||||
'NODES': 'BLMaxwell_GeoNodes',
|
'NODES': 'BLMaxwell_GeoNodes',
|
||||||
|
@ -22,6 +19,9 @@ MANAGED_COLLECTION_NAME = 'BLMaxwell'
|
||||||
PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
|
PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - BLCollection
|
||||||
|
####################
|
||||||
def bl_collection(
|
def bl_collection(
|
||||||
collection_name: str, view_layer_exclude: bool
|
collection_name: str, view_layer_exclude: bool
|
||||||
) -> bpy.types.Collection:
|
) -> bpy.types.Collection:
|
||||||
|
@ -44,63 +44,89 @@ def bl_collection(
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - BLObject
|
||||||
|
####################
|
||||||
class ManagedBLObject(ct.schemas.ManagedObj):
|
class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
managed_obj_type = ct.ManagedObjType.ManagedBLObject
|
managed_obj_type = ct.ManagedObjType.ManagedBLObject
|
||||||
_bl_object_name: str
|
_bl_object_name: str | None = None
|
||||||
|
|
||||||
def __init__(self, name: str):
|
####################
|
||||||
self._bl_object_name = name
|
# - BL Object Name
|
||||||
|
####################
|
||||||
# Object Name
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self._bl_object_name
|
return self._bl_object_name
|
||||||
|
|
||||||
@name.setter
|
@name.setter
|
||||||
def set_name(self, value: str) -> None:
|
def name(self, value: str) -> None:
|
||||||
# Object Doesn't Exist
|
log.info(
|
||||||
if not (bl_object := bpy.data.objects.get(self._bl_object_name)):
|
'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value
|
||||||
# ...AND Desired Object Name is Not Taken
|
)
|
||||||
|
|
||||||
if not bpy.data.objects.get(value):
|
if not bpy.data.objects.get(value):
|
||||||
self._bl_object_name = value
|
log.info(
|
||||||
return
|
'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:
|
else:
|
||||||
msg = f'Desired name {value} for BL object is taken'
|
msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted'
|
||||||
raise ValueError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
# Object DOES Exist
|
# Set Internal Name
|
||||||
|
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
|
bl_object.name = value
|
||||||
self._bl_object_name = bl_object.name
|
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
|
log.info(
|
||||||
@property
|
'Changed BLObject Name to "%s"',
|
||||||
def bl_mesh_name(self):
|
bl_object.name,
|
||||||
return self.name
|
)
|
||||||
|
|
||||||
@property
|
####################
|
||||||
def bl_volume_name(self):
|
# - Allocation
|
||||||
return self.name
|
####################
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
# Deallocation
|
####################
|
||||||
|
# - Deallocation
|
||||||
|
####################
|
||||||
def free(self):
|
def free(self):
|
||||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
if (bl_object := bpy.data.objects.get(self.name)) is None:
|
||||||
return ## Nothing to do
|
return
|
||||||
|
|
||||||
# Delete the Underlying Datablock
|
# Delete the Underlying Datablock
|
||||||
## This automatically deletes the object too
|
## This automatically deletes the object too
|
||||||
if bl_object.type == 'MESH':
|
log.info('Removing "%s" BLObject', bl_object.type)
|
||||||
bpy.data.meshes.remove(bl_object.data)
|
if bl_object.type in {'MESH', 'EMPTY'}:
|
||||||
elif bl_object.type == 'EMPTY':
|
|
||||||
bpy.data.meshes.remove(bl_object.data)
|
bpy.data.meshes.remove(bl_object.data)
|
||||||
elif bl_object.type == 'VOLUME':
|
elif bl_object.type == 'VOLUME':
|
||||||
bpy.data.volumes.remove(bl_object.data)
|
bpy.data.volumes.remove(bl_object.data)
|
||||||
else:
|
else:
|
||||||
msg = f'Type of to-delete `bl_object`, {bl_object.type}, is not valid'
|
msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"'
|
||||||
raise ValueError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Actions
|
# - Actions
|
||||||
|
@ -133,9 +159,16 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
)
|
)
|
||||||
).objects
|
).objects
|
||||||
):
|
):
|
||||||
|
log.info('Moving "%s" to Preview Collection', bl_object.name)
|
||||||
preview_collection.objects.link(bl_object)
|
preview_collection.objects.link(bl_object)
|
||||||
|
|
||||||
|
# Display Parameters
|
||||||
if kind == 'EMPTY' and empty_display_type is not None:
|
if kind == 'EMPTY' and empty_display_type is not None:
|
||||||
|
log.info(
|
||||||
|
'Setting Empty Display Type "%s" for "%s"',
|
||||||
|
empty_display_type,
|
||||||
|
bl_object.name,
|
||||||
|
)
|
||||||
bl_object.empty_display_type = empty_display_type
|
bl_object.empty_display_type = empty_display_type
|
||||||
|
|
||||||
def hide_preview(
|
def hide_preview(
|
||||||
|
@ -155,21 +188,22 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
)
|
)
|
||||||
).objects
|
).objects
|
||||||
):
|
):
|
||||||
|
log.info('Removing "%s" from Preview Collection', bl_object.name)
|
||||||
preview_collection.objects.unlink(bl_object)
|
preview_collection.objects.unlink(bl_object)
|
||||||
|
|
||||||
def bl_select(self) -> None:
|
def bl_select(self) -> None:
|
||||||
"""Selects the managed Blender object globally, causing it to be ex.
|
"""Selects the managed Blender object globally, causing it to be ex.
|
||||||
outlined in the 3D viewport.
|
outlined in the 3D viewport.
|
||||||
"""
|
"""
|
||||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
if (bl_object := bpy.data.objects.get(self.name)) is not None:
|
||||||
msg = 'Managed BLObject does not exist'
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
bl_object.select_set(True)
|
bl_object.select_set(True)
|
||||||
|
|
||||||
|
msg = 'Managed BLObject does not exist'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Managed Object Management
|
# - BLObject Management
|
||||||
####################
|
####################
|
||||||
def bl_object(
|
def bl_object(
|
||||||
self,
|
self,
|
||||||
|
@ -177,30 +211,41 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
):
|
):
|
||||||
"""Returns the managed blender object.
|
"""Returns the managed blender object.
|
||||||
|
|
||||||
If the requested object data type is different, then delete the old
|
If the requested object data kind is different, then delete the old
|
||||||
object and recreate.
|
object and recreate.
|
||||||
"""
|
"""
|
||||||
# Remove Object (if mismatch)
|
# Remove Object (if mismatch)
|
||||||
if (
|
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind:
|
||||||
bl_object := bpy.data.objects.get(self.name)
|
log.info(
|
||||||
) and bl_object.type != kind:
|
'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)',
|
||||||
|
bl_object.name,
|
||||||
|
bl_object.type,
|
||||||
|
kind,
|
||||||
|
)
|
||||||
self.free()
|
self.free()
|
||||||
|
|
||||||
# Create Object w/Appropriate Data Block
|
# Create Object w/Appropriate Data Block
|
||||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||||
|
log.info(
|
||||||
|
'Creating "%s" with kind "%s"',
|
||||||
|
bl_object.name,
|
||||||
|
kind,
|
||||||
|
)
|
||||||
if kind == 'MESH':
|
if kind == 'MESH':
|
||||||
bl_data = bpy.data.meshes.new(self.bl_mesh_name)
|
bl_data = bpy.data.meshes.new(self.name)
|
||||||
elif kind == 'EMPTY':
|
elif kind == 'EMPTY':
|
||||||
bl_data = None
|
bl_data = None
|
||||||
elif kind == 'VOLUME':
|
elif kind == 'VOLUME':
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
else:
|
else:
|
||||||
msg = (
|
msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"'
|
||||||
f'Requested `bl_object` type {bl_object.type} is not valid'
|
|
||||||
)
|
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
bl_object = bpy.data.objects.new(self.name, bl_data)
|
bl_object = bpy.data.objects.new(self.name, bl_data)
|
||||||
|
log.debug(
|
||||||
|
'Linking "%s" to Base Collection',
|
||||||
|
bl_object.name,
|
||||||
|
)
|
||||||
bl_collection(
|
bl_collection(
|
||||||
MANAGED_COLLECTION_NAME, view_layer_exclude=True
|
MANAGED_COLLECTION_NAME, view_layer_exclude=True
|
||||||
).objects.link(bl_object)
|
).objects.link(bl_object)
|
||||||
|
@ -211,17 +256,16 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
# - Mesh Data Properties
|
# - Mesh Data Properties
|
||||||
####################
|
####################
|
||||||
@property
|
@property
|
||||||
def raw_mesh(self) -> bpy.types.Mesh:
|
def mesh_data(self) -> bpy.types.Mesh:
|
||||||
"""Returns the object's raw mesh data.
|
"""Directly loads the Blender mesh data.
|
||||||
|
|
||||||
Raises an error if the object has no mesh data.
|
Raises:
|
||||||
|
ValueError: If the object has no mesh data.
|
||||||
"""
|
"""
|
||||||
if (
|
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH':
|
||||||
bl_object := bpy.data.objects.get(self.name)
|
|
||||||
) and bl_object.type == 'MESH':
|
|
||||||
return bl_object.data
|
return bl_object.data
|
||||||
|
|
||||||
msg = f'Requested MESH data from `bl_object` of type {bl_object.type}'
|
msg = f'Requested mesh data from {self.name} of type {bl_object.type}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -230,9 +274,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
evaluate: bool = True,
|
evaluate: bool = True,
|
||||||
triangulate: bool = False,
|
triangulate: bool = False,
|
||||||
) -> bpy.types.Mesh:
|
) -> bpy.types.Mesh:
|
||||||
if (
|
if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH':
|
||||||
bl_object := bpy.data.objects.get(self.name)
|
|
||||||
) and bl_object.type == 'MESH':
|
|
||||||
bmesh_mesh = None
|
bmesh_mesh = None
|
||||||
try:
|
try:
|
||||||
bmesh_mesh = bmesh.new()
|
bmesh_mesh = bmesh.new()
|
||||||
|
@ -254,7 +296,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
bmesh_mesh.free()
|
bmesh_mesh.free()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = f'Requested BMesh from `bl_object` of type {bl_object.type}'
|
msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -262,27 +304,31 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
## TODO: Cached
|
## TODO: Cached
|
||||||
|
|
||||||
# Ensure Updated Geometry
|
# Ensure Updated Geometry
|
||||||
|
log.debug('Updating View Layer')
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
## TODO: Must we?
|
|
||||||
|
|
||||||
# Compute Evaluted + Triangulated Mesh
|
# Compute Evaluted + Triangulated Mesh
|
||||||
|
log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name)
|
||||||
_mesh = bpy.data.meshes.new(name='TemporaryMesh')
|
_mesh = bpy.data.meshes.new(name='TemporaryMesh')
|
||||||
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
|
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
|
||||||
bmesh_mesh.to_mesh(_mesh)
|
bmesh_mesh.to_mesh(_mesh)
|
||||||
|
|
||||||
# Optimized Vertex Copy
|
# Optimized Vertex Copy
|
||||||
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
|
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
|
||||||
|
log.debug('Copying Vertices from "%s"', self.name)
|
||||||
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
|
verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64)
|
||||||
_mesh.vertices.foreach_get('co', verts)
|
_mesh.vertices.foreach_get('co', verts)
|
||||||
verts.shape = (-1, 3)
|
verts.shape = (-1, 3)
|
||||||
|
|
||||||
# Optimized Triangle Copy
|
# Optimized Triangle Copy
|
||||||
## To understand, read it, **carefully**.
|
## To understand, read it, **carefully**.
|
||||||
|
log.debug('Copying Faces from "%s"', self.name)
|
||||||
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
|
faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64)
|
||||||
_mesh.polygons.foreach_get('vertices', faces)
|
_mesh.polygons.foreach_get('vertices', faces)
|
||||||
faces.shape = (-1, 3)
|
faces.shape = (-1, 3)
|
||||||
|
|
||||||
# Remove Temporary Mesh
|
# Remove Temporary Mesh
|
||||||
|
log.debug('Removing Temporary Mesh')
|
||||||
bpy.data.meshes.remove(_mesh)
|
bpy.data.meshes.remove(_mesh)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -291,7 +337,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Modifier Methods
|
# - Modifiers
|
||||||
####################
|
####################
|
||||||
def bl_modifier(
|
def bl_modifier(
|
||||||
self,
|
self,
|
||||||
|
@ -299,10 +345,10 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
):
|
):
|
||||||
"""Creates a new modifier for the current `bl_object`.
|
"""Creates a new modifier for the current `bl_object`.
|
||||||
|
|
||||||
For all Blender modifier type names, see: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
|
- Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
|
||||||
"""
|
"""
|
||||||
if not (bl_object := bpy.data.objects.get(self.name)):
|
if not (bl_object := bpy.data.objects.get(self.name)):
|
||||||
msg = "Can't add modifier to BL object that doesn't exist"
|
msg = f'Tried to add modifier to "{self.name}", but it has no bl_object'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# (Create and) Return Modifier
|
# (Create and) Return Modifier
|
||||||
|
@ -376,8 +422,7 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
# Quickly Determine if IDPropertyArray is Equal
|
# Quickly Determine if IDPropertyArray is Equal
|
||||||
if (
|
if (
|
||||||
hasattr(bl_modifier[interface_identifier], 'to_list')
|
hasattr(bl_modifier[interface_identifier], 'to_list')
|
||||||
and tuple(bl_modifier[interface_identifier].to_list())
|
and tuple(bl_modifier[interface_identifier].to_list()) == value
|
||||||
== value
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -395,18 +440,3 @@ class ManagedBLObject(ct.schemas.ManagedObj):
|
||||||
# Update DepGraph (if anything changed)
|
# Update DepGraph (if anything changed)
|
||||||
if modifier_altered:
|
if modifier_altered:
|
||||||
bl_object.data.update()
|
bl_object.data.update()
|
||||||
|
|
||||||
# @property
|
|
||||||
# def volume(self) -> bpy.types.Volume:
|
|
||||||
# """Returns the object's volume data.
|
|
||||||
#
|
|
||||||
# Raises an error if the object has no volume data.
|
|
||||||
# """
|
|
||||||
# if (
|
|
||||||
# (bl_object := bpy.data.objects.get(self.bl_object_name))
|
|
||||||
# and bl_object.type == "VOLUME"
|
|
||||||
# ):
|
|
||||||
# return bl_object.data
|
|
||||||
#
|
|
||||||
# msg = f"Requested VOLUME data from `bl_object` of type {bl_object.type}"
|
|
||||||
# raise ValueError(msg)
|
|
||||||
|
|
|
@ -125,9 +125,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
'to_add': [],
|
'to_add': [],
|
||||||
}
|
}
|
||||||
for link_ptr in delta_links['removed']:
|
for link_ptr in delta_links['removed']:
|
||||||
from_socket = self._node_link_cache.link_ptrs_from_sockets[
|
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr]
|
||||||
link_ptr
|
|
||||||
]
|
|
||||||
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
|
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
|
||||||
|
|
||||||
# Update Socket Caches
|
# Update Socket Caches
|
||||||
|
@ -136,9 +134,7 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
|
|
||||||
# Trigger Report Chain on Socket that Just Lost a Link
|
# Trigger Report Chain on Socket that Just Lost a Link
|
||||||
## Aka. Forward-Refresh Caches Relying on Linkage
|
## Aka. Forward-Refresh Caches Relying on Linkage
|
||||||
if not (
|
if not (consent_removal := to_socket.sync_link_removed(from_socket)):
|
||||||
consent_removal := to_socket.sync_link_removed(from_socket)
|
|
||||||
):
|
|
||||||
# Did Not Consent to Removal: Queue Add Link
|
# Did Not Consent to Removal: Queue Add Link
|
||||||
link_alterations['to_add'].append((from_socket, to_socket))
|
link_alterations['to_add'].append((from_socket, to_socket))
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import typing as typ
|
|
||||||
import functools
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import tidy3d as td
|
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
import numpy as np
|
import tidy3d as td
|
||||||
import scipy as sc
|
|
||||||
|
|
||||||
from .....utils import analyze_geonodes
|
from .....utils import analyze_geonodes, logger
|
||||||
from .....utils import extra_sympy_units as spux
|
from .....utils import extra_sympy_units as spux
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from ... import sockets
|
from ... import managed_objs, sockets
|
||||||
from ... import managed_objs
|
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
GEONODES_MONITOR_BOX = 'monitor_box'
|
GEONODES_MONITOR_BOX = 'monitor_box'
|
||||||
|
|
||||||
|
|
||||||
class EHFieldMonitorNode(base.MaxwellSimNode):
|
class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
|
"""Node providing for the monitoring of electromagnetic fields within a given planar region or volume."""
|
||||||
|
|
||||||
node_type = ct.NodeType.EHFieldMonitor
|
node_type = ct.NodeType.EHFieldMonitor
|
||||||
bl_label = 'E/H Field Monitor'
|
bl_label = 'E/H Field Monitor'
|
||||||
use_sim_node_name = True
|
use_sim_node_name = True
|
||||||
|
@ -41,9 +39,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
},
|
},
|
||||||
'Time Domain': {
|
'Time Domain': {
|
||||||
'Rec Start': sockets.PhysicalTimeSocketDef(),
|
'Rec Start': sockets.PhysicalTimeSocketDef(),
|
||||||
'Rec Stop': sockets.PhysicalTimeSocketDef(
|
'Rec Stop': sockets.PhysicalTimeSocketDef(default_value=200 * spux.fs),
|
||||||
default_value=200 * spux.fs
|
|
||||||
),
|
|
||||||
'Samples/Time': sockets.IntegerNumberSocketDef(
|
'Samples/Time': sockets.IntegerNumberSocketDef(
|
||||||
default_value=100,
|
default_value=100,
|
||||||
),
|
),
|
||||||
|
@ -60,19 +56,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
|
||||||
# - Properties
|
|
||||||
####################
|
|
||||||
|
|
||||||
####################
|
|
||||||
# - UI
|
|
||||||
####################
|
|
||||||
def draw_props(self, context, layout):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def draw_info(self, context, col):
|
|
||||||
pass
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Output Sockets
|
# - Output Sockets
|
||||||
####################
|
####################
|
||||||
|
@ -91,7 +74,8 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
def compute_monitor(
|
def compute_monitor(
|
||||||
self, input_sockets: dict, props: dict
|
self, input_sockets: dict, props: dict
|
||||||
) -> td.FieldTimeMonitor:
|
) -> td.FieldMonitor | td.FieldTimeMonitor:
|
||||||
|
"""Computes the value of the 'Monitor' output socket, which the user can select as being either a `td.FieldMonitor` or `td.FieldTimeMonitor`."""
|
||||||
_center = input_sockets['Center']
|
_center = input_sockets['Center']
|
||||||
_size = input_sockets['Size']
|
_size = input_sockets['Size']
|
||||||
_samples_space = input_sockets['Samples/Space']
|
_samples_space = input_sockets['Samples/Space']
|
||||||
|
@ -103,17 +87,22 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
if props['active_socket_set'] == 'Freq Domain':
|
if props['active_socket_set'] == 'Freq Domain':
|
||||||
freqs = input_sockets['Freqs']
|
freqs = input_sockets['Freqs']
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Computing FieldMonitor (name=%s) with center=%s, size=%s',
|
||||||
|
props['sim_node_name'],
|
||||||
|
center,
|
||||||
|
size,
|
||||||
|
)
|
||||||
return td.FieldMonitor(
|
return td.FieldMonitor(
|
||||||
center=center,
|
center=center,
|
||||||
size=size,
|
size=size,
|
||||||
name=props['sim_node_name'],
|
name=props['sim_node_name'],
|
||||||
interval_space=samples_space,
|
interval_space=samples_space,
|
||||||
freqs=[
|
freqs=[
|
||||||
float(spu.convert_to(freq, spu.hertz) / spu.hertz)
|
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
|
||||||
for freq in freqs
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else: ## Time Domain
|
## Time Domain
|
||||||
_rec_start = input_sockets['Rec Start']
|
_rec_start = input_sockets['Rec Start']
|
||||||
_rec_stop = input_sockets['Rec Stop']
|
_rec_stop = input_sockets['Rec Stop']
|
||||||
samples_time = input_sockets['Samples/Time']
|
samples_time = input_sockets['Samples/Time']
|
||||||
|
@ -121,6 +110,12 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
|
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
|
||||||
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
|
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s',
|
||||||
|
props['sim_node_name'],
|
||||||
|
center,
|
||||||
|
size,
|
||||||
|
)
|
||||||
return td.FieldTimeMonitor(
|
return td.FieldTimeMonitor(
|
||||||
center=center,
|
center=center,
|
||||||
size=size,
|
size=size,
|
||||||
|
@ -144,32 +139,22 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
input_sockets: dict,
|
input_sockets: dict,
|
||||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||||
):
|
):
|
||||||
|
"""Alters the managed 3D preview objects whenever the center or size input sockets are changed."""
|
||||||
_center = input_sockets['Center']
|
_center = input_sockets['Center']
|
||||||
center = tuple(
|
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um])
|
||||||
[float(el) for el in spu.convert_to(_center, spu.um) / spu.um]
|
|
||||||
)
|
|
||||||
|
|
||||||
_size = input_sockets['Size']
|
_size = input_sockets['Size']
|
||||||
size = tuple(
|
size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um])
|
||||||
[float(el) for el in spu.convert_to(_size, spu.um) / spu.um]
|
|
||||||
)
|
|
||||||
## TODO: Preview unit system?? Presume um for now
|
|
||||||
|
|
||||||
# Retrieve Hard-Coded GeoNodes and Analyze Input
|
# Retrieve Hard-Coded GeoNodes and Analyze Input
|
||||||
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
|
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
|
||||||
geonodes_interface = analyze_geonodes.interface(
|
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
|
||||||
geo_nodes, direc='INPUT'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sync Modifier Inputs
|
# Sync Modifier Inputs
|
||||||
managed_objs['monitor_box'].sync_geonodes_modifier(
|
managed_objs['monitor_box'].sync_geonodes_modifier(
|
||||||
geonodes_node_group=geo_nodes,
|
geonodes_node_group=geo_nodes,
|
||||||
geonodes_identifier_to_value={
|
geonodes_identifier_to_value={
|
||||||
geonodes_interface['Size'].identifier: size,
|
geonodes_interface['Size'].identifier: size,
|
||||||
## TODO: Use 'bl_socket_map.value_to_bl`!
|
|
||||||
## - This accounts for auto-conversion, unit systems, etc. .
|
|
||||||
## - We could keep it in the node base class...
|
|
||||||
## - ...But it needs aligning with Blender, too. Hmm.
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -186,6 +171,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
self,
|
self,
|
||||||
managed_objs: dict[str, ct.schemas.ManagedObj],
|
managed_objs: dict[str, ct.schemas.ManagedObj],
|
||||||
):
|
):
|
||||||
|
"""Requests that the managed object be previewed in response to a user request to show the preview."""
|
||||||
managed_objs['monitor_box'].show_preview('MESH')
|
managed_objs['monitor_box'].show_preview('MESH')
|
||||||
self.on_value_changed__center_size()
|
self.on_value_changed__center_size()
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import functools
|
|
||||||
import typing as typ
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
import pydantic as pyd
|
|
||||||
import tidy3d as td
|
|
||||||
|
|
||||||
|
from .....utils import logger
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from ... import sockets
|
from ... import sockets
|
||||||
from .. import base
|
|
||||||
from ...managed_objs import managed_bl_object
|
from ...managed_objs import managed_bl_object
|
||||||
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
console = logger.OUTPUT_CONSOLE
|
||||||
|
|
||||||
|
|
||||||
class ConsoleViewOperator(bpy.types.Operator):
|
class ConsoleViewOperator(bpy.types.Operator):
|
||||||
|
@ -67,9 +64,7 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
name='Auto 3D Preview',
|
name='Auto 3D Preview',
|
||||||
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
|
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
|
||||||
default=False,
|
default=False,
|
||||||
update=lambda self, context: self.sync_prop(
|
update=lambda self, context: self.sync_prop('auto_3d_preview', context),
|
||||||
'auto_3d_preview', context
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -111,9 +106,9 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(data, sp.Basic):
|
if isinstance(data, sp.Basic):
|
||||||
sp.pprint(data, use_unicode=True)
|
console.print(sp.pretty(data, use_unicode=True))
|
||||||
|
else:
|
||||||
print(str(data))
|
console.print(data)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Updates
|
# - Updates
|
||||||
|
|
|
@ -1,39 +1,31 @@
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import tidy3d as td
|
import tidy3d as td
|
||||||
import numpy as np
|
|
||||||
import sympy as sp
|
|
||||||
import sympy.physics.units as spu
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy_types import bpy_types
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
from .....utils import analyze_geonodes
|
from .....utils import analyze_geonodes
|
||||||
from ... import bl_socket_map
|
from ... import bl_socket_map, managed_objs, sockets
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from ... import sockets
|
|
||||||
from .. import base
|
from .. import base
|
||||||
from ... import managed_objs
|
|
||||||
|
|
||||||
|
|
||||||
class GeoNodesStructureNode(base.MaxwellSimNode):
|
class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
node_type = ct.NodeType.GeoNodesStructure
|
node_type = ct.NodeType.GeoNodesStructure
|
||||||
bl_label = 'GeoNodes Structure'
|
bl_label = 'GeoNodes Structure'
|
||||||
|
use_sim_node_name = True
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Sockets
|
# - Sockets
|
||||||
####################
|
####################
|
||||||
input_sockets = {
|
input_sockets: typ.ClassVar = {
|
||||||
'Unit System': sockets.PhysicalUnitSystemSocketDef(),
|
'Unit System': sockets.PhysicalUnitSystemSocketDef(),
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
|
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
|
||||||
}
|
}
|
||||||
output_sockets = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Structure': sockets.MaxwellStructureSocketDef(),
|
'Structure': sockets.MaxwellStructureSocketDef(),
|
||||||
}
|
}
|
||||||
|
|
||||||
managed_obj_defs = {
|
managed_obj_defs: typ.ClassVar = {
|
||||||
'geometry': ct.schemas.ManagedObjDef(
|
'geometry': ct.schemas.ManagedObjDef(
|
||||||
mk=lambda name: managed_objs.ManagedBLObject(name),
|
mk=lambda name: managed_objs.ManagedBLObject(name),
|
||||||
name_prefix='',
|
name_prefix='',
|
||||||
|
@ -92,9 +84,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
# Analyze GeoNodes
|
# Analyze GeoNodes
|
||||||
## Extract Valid Inputs (via GeoNodes Tree "Interface")
|
## Extract Valid Inputs (via GeoNodes Tree "Interface")
|
||||||
geonodes_interface = analyze_geonodes.interface(
|
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
|
||||||
geo_nodes, direc='INPUT'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set Loose Input Sockets
|
# Set Loose Input Sockets
|
||||||
## Retrieve the appropriate SocketDef for the Blender Interface Socket
|
## Retrieve the appropriate SocketDef for the Blender Interface Socket
|
||||||
|
@ -138,9 +128,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
# Analyze GeoNodes Interface (input direction)
|
# Analyze GeoNodes Interface (input direction)
|
||||||
## This retrieves NodeTreeSocketInterface elements
|
## This retrieves NodeTreeSocketInterface elements
|
||||||
geonodes_interface = analyze_geonodes.interface(
|
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
|
||||||
geo_nodes, direc='INPUT'
|
|
||||||
)
|
|
||||||
|
|
||||||
## TODO: Check that Loose Sockets matches the Interface
|
## TODO: Check that Loose Sockets matches the Interface
|
||||||
## - If the user deletes an interface socket, bad things will happen.
|
## - If the user deletes an interface socket, bad things will happen.
|
||||||
|
@ -156,9 +144,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
loose_input_sockets[socket_name],
|
loose_input_sockets[socket_name],
|
||||||
unit_system,
|
unit_system,
|
||||||
)
|
)
|
||||||
for socket_name, bl_interface_socket in (
|
for socket_name, bl_interface_socket in (geonodes_interface.items())
|
||||||
geonodes_interface.items()
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -185,6 +171,4 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
GeoNodesStructureNode,
|
GeoNodesStructureNode,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)}
|
||||||
ct.NodeType.GeoNodesStructure: (ct.NodeCategory.MAXWELLSIM_STRUCTURES)
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef
|
||||||
|
|
||||||
from . import blender
|
from . import blender
|
||||||
|
|
||||||
|
BlenderMaterialSocketDef = blender.BlenderMaterialSocketDef
|
||||||
BlenderObjectSocketDef = blender.BlenderObjectSocketDef
|
BlenderObjectSocketDef = blender.BlenderObjectSocketDef
|
||||||
BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef
|
BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef
|
||||||
BlenderImageSocketDef = blender.BlenderImageSocketDef
|
BlenderImageSocketDef = blender.BlenderImageSocketDef
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
from . import collection, material
|
||||||
from . import object as object_socket
|
from . import object as object_socket
|
||||||
from . import collection
|
|
||||||
|
|
||||||
|
BlenderMaterialSocketDef = material.BlenderMaterialSocketDef
|
||||||
BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef
|
BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef
|
||||||
BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef
|
BlenderCollectionSocketDef = collection.BlenderCollectionSocketDef
|
||||||
|
|
||||||
|
@ -8,13 +9,13 @@ from . import image
|
||||||
|
|
||||||
BlenderImageSocketDef = image.BlenderImageSocketDef
|
BlenderImageSocketDef = image.BlenderImageSocketDef
|
||||||
|
|
||||||
from . import geonodes
|
from . import geonodes, text
|
||||||
from . import text
|
|
||||||
|
|
||||||
BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef
|
BlenderGeoNodesSocketDef = geonodes.BlenderGeoNodesSocketDef
|
||||||
BlenderTextSocketDef = text.BlenderTextSocketDef
|
BlenderTextSocketDef = text.BlenderTextSocketDef
|
||||||
|
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
|
*material.BL_REGISTER,
|
||||||
*object_socket.BL_REGISTER,
|
*object_socket.BL_REGISTER,
|
||||||
*collection.BL_REGISTER,
|
*collection.BL_REGISTER,
|
||||||
*text.BL_REGISTER,
|
*text.BL_REGISTER,
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import typing as typ
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
|
|
||||||
from .. import base
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
|
from .. import base
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -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 bpy
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
|
|
||||||
from .. import base
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
|
from .. import base
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -139,8 +139,6 @@ def delay_registration(
|
||||||
DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb
|
DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb
|
||||||
|
|
||||||
|
|
||||||
def run_delayed_registration(
|
def run_delayed_registration(delayed_reg_key: DelayedRegKey, path_deps: Path) -> None:
|
||||||
delayed_reg_key: DelayedRegKey, path_deps: Path
|
|
||||||
) -> None:
|
|
||||||
register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key)
|
register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key)
|
||||||
register_cb(path_deps)
|
register_cb(path_deps)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library.
|
"""Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library.
|
||||||
|
|
||||||
- SimulationTask: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_core.py>
|
- SimulationTask: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_core.py>
|
||||||
- Tidy3D Stub: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/api/tidy3d_stub.py>
|
- Tidy3D Stub: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/api/tidy3d_stub.py>
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import rich.console
|
import rich.console
|
||||||
import rich.logging
|
import rich.logging
|
||||||
|
import rich.traceback
|
||||||
|
|
||||||
from .. import info
|
from .. import info
|
||||||
from ..nodeps.utils import simple_logger
|
from ..nodeps.utils import simple_logger
|
||||||
|
@ -15,6 +16,16 @@ from ..nodeps.utils.simple_logger import (
|
||||||
sync_loggers, # noqa: F401
|
sync_loggers, # noqa: F401
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OUTPUT_CONSOLE = rich.console.Console(
|
||||||
|
color_system='truecolor',
|
||||||
|
## TODO: color_system should be 'auto'; bl_run.py hijinks are interfering
|
||||||
|
)
|
||||||
|
ERROR_CONSOLE = rich.console.Console(
|
||||||
|
color_system='truecolor', stderr=True
|
||||||
|
## TODO: color_system should be 'auto'; bl_run.py hijinks are interfering
|
||||||
|
)
|
||||||
|
rich.traceback.install(show_locals=True, console=ERROR_CONSOLE)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Logging Handlers
|
# - Logging Handlers
|
||||||
|
@ -26,19 +37,14 @@ def console_handler(level: LogLevel) -> rich.logging.RichHandler:
|
||||||
)
|
)
|
||||||
rich_handler = rich.logging.RichHandler(
|
rich_handler = rich.logging.RichHandler(
|
||||||
level=level,
|
level=level,
|
||||||
console=rich.console.Console(
|
console=ERROR_CONSOLE,
|
||||||
color_system='truecolor', stderr=True
|
|
||||||
), ## TODO: Should be 'auto'; bl_run.py hijinks are interfering
|
|
||||||
# console=rich.console.Console(stderr=True),
|
|
||||||
rich_tracebacks=True,
|
rich_tracebacks=True,
|
||||||
)
|
)
|
||||||
rich_handler.setFormatter(rich_formatter)
|
rich_handler.setFormatter(rich_formatter)
|
||||||
return rich_handler
|
return rich_handler
|
||||||
|
|
||||||
|
|
||||||
def file_handler(
|
def file_handler(path_log_file: Path, level: LogLevel) -> rich.logging.RichHandler:
|
||||||
path_log_file: Path, level: LogLevel
|
|
||||||
) -> rich.logging.RichHandler:
|
|
||||||
return simple_logger.file_handler(path_log_file, level)
|
return simple_logger.file_handler(path_log_file, level)
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +66,7 @@ def get(module_name):
|
||||||
####################
|
####################
|
||||||
# - Logger Sync
|
# - Logger Sync
|
||||||
####################
|
####################
|
||||||
#def upgrade_simple_loggers():
|
# def upgrade_simple_loggers():
|
||||||
# """Upgrades simple loggers to rich-enabled loggers."""
|
# """Upgrades simple loggers to rich-enabled loggers."""
|
||||||
# for logger in simple_loggers():
|
# for logger in simple_loggers():
|
||||||
# setup_logger(console_handler, file_handler, logger)
|
# setup_logger(console_handler, file_handler, logger)
|
||||||
|
|
Loading…
Reference in New Issue