fix: Major streamlining of plot workflow.
Various small adjustments with a big total impact: - Toggling 3D preview no longer propagates a DataChanged, which prevents chronic `bl_select()` invocation that greatly slows down switching between images. - Viewer now uses a replot() context manager to hide any active plots whenever no plots were generated, so that turning off plots / previewing a node (chain) without plots properly turns off the image, instead of letting the image hang around. - `self.managed_objs` is now properly invalidated, instead of trying to set the `name` attribute of in-memory objects, so that persistance keeps up with `sim_node_name` changes, so that all the ex. 'Viz' nodes don't all try to hog a single 'Viz' image name. - A pre-save handler was added, which ensures all images are packed into the .blend, to ensure that the images will pop up on the next file load. - A fake user is now assigned to all new images, to nail down the idea that `ManagedBLImage` is the owner. - `name` setter of `ManagedBLImage` was unreasonably bugged (it's actually incredible that it worked) - it has been fixed, as well as other changes applied to the class as a whole (including @classmethod on the UI-area/space getters and minor None-sanitizing).main
parent
fc0d7afa4d
commit
c63dda2224
|
@ -1,3 +1,5 @@
|
||||||
|
"""Declares `ManagedBLImage`."""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import time
|
import time
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
@ -21,11 +23,16 @@ SPACE_TYPE = 'IMAGE_EDITOR'
|
||||||
# - Managed BL Image
|
# - Managed BL Image
|
||||||
####################
|
####################
|
||||||
class ManagedBLImage(base.ManagedObj):
|
class ManagedBLImage(base.ManagedObj):
|
||||||
|
"""Represents a Blender Image datablock, encapsulating various useful interactions with it.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: The name of the image.
|
||||||
|
"""
|
||||||
|
|
||||||
managed_obj_type = ct.ManagedObjType.ManagedBLImage
|
managed_obj_type = ct.ManagedObjType.ManagedBLImage
|
||||||
_bl_image_name: str
|
_bl_image_name: str
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
## TODO: Check that blender doesn't have any other images by the same name.
|
|
||||||
self._bl_image_name = name
|
self._bl_image_name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -34,25 +41,32 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
|
|
||||||
@name.setter
|
@name.setter
|
||||||
def name(self, value: str):
|
def name(self, value: str):
|
||||||
# Image Doesn't Exist
|
log.info(
|
||||||
if not (bl_image := bpy.data.images.get(self._bl_image_name)):
|
'Setting ManagedBLImage from "%s" to "%s"',
|
||||||
# ...AND Desired Image Name is Not Taken
|
self.name,
|
||||||
if not bpy.data.objects.get(value):
|
value,
|
||||||
self._bl_image_name = value
|
)
|
||||||
return
|
current_bl_image = bpy.data.images.get(self._bl_image_name)
|
||||||
|
wanted_bl_image = bpy.data.images.get(value)
|
||||||
|
|
||||||
# ...AND Desired Image Name is Taken
|
# Yoink Image Name
|
||||||
msg = f'Desired name {value} for BL image is taken'
|
if current_bl_image is None and wanted_bl_image is None:
|
||||||
|
self._bl_image_name = value
|
||||||
|
|
||||||
|
# Alter Image Name
|
||||||
|
elif current_bl_image is not None and wanted_bl_image is None:
|
||||||
|
self._bl_image_name = value
|
||||||
|
current_bl_image.name = value
|
||||||
|
|
||||||
|
# Overlapping Image Name
|
||||||
|
elif wanted_bl_image is not None:
|
||||||
|
msg = f'ManagedBLImage "{self._bl_image_name}" could not change its name to "{value}", since it already exists.'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Object DOES Exist
|
|
||||||
bl_image.name = value
|
|
||||||
self._bl_image_name = bl_image.name
|
|
||||||
## - When name exists, Blender adds .### to prevent overlap.
|
|
||||||
## - `set_name` is allowed to change the name; nodes account for this.
|
|
||||||
|
|
||||||
def free(self):
|
def free(self):
|
||||||
if bl_image := bpy.data.images.get(self.name):
|
bl_image = bpy.data.images.get(self.name)
|
||||||
|
if bl_image is not None:
|
||||||
|
log.debug('Freeing ManagedBLImage "%s"', self.name)
|
||||||
bpy.data.images.remove(bl_image)
|
bpy.data.images.remove(bl_image)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -72,7 +86,8 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
channels = 4 if color_model == 'RGBA' else 3
|
channels = 4 if color_model == 'RGBA' else 3
|
||||||
|
|
||||||
# Remove Image (if mismatch)
|
# Remove Image (if mismatch)
|
||||||
if (bl_image := bpy.data.images.get(self.name)) and (
|
bl_image = bpy.data.images.get(self.name)
|
||||||
|
if bl_image is not None and (
|
||||||
bl_image.size[0] != width_px
|
bl_image.size[0] != width_px
|
||||||
or bl_image.size[1] != height_px
|
or bl_image.size[1] != height_px
|
||||||
or bl_image.channels != channels
|
or bl_image.channels != channels
|
||||||
|
@ -81,7 +96,8 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
self.free()
|
self.free()
|
||||||
|
|
||||||
# Create Image w/Geometry (if none exists)
|
# Create Image w/Geometry (if none exists)
|
||||||
if not (bl_image := bpy.data.images.get(self.name)):
|
bl_image = bpy.data.images.get(self.name)
|
||||||
|
if bl_image is None:
|
||||||
bl_image = bpy.data.images.new(
|
bl_image = bpy.data.images.new(
|
||||||
self.name,
|
self.name,
|
||||||
width=width_px,
|
width=width_px,
|
||||||
|
@ -89,44 +105,62 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
float_buffer=dtype == 'float32',
|
float_buffer=dtype == 'float32',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Enable Fake User
|
||||||
|
bl_image.use_fake_user = True
|
||||||
|
|
||||||
return bl_image
|
return bl_image
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Editor UX Manipulation
|
# - Editor UX Manipulation
|
||||||
####################
|
####################
|
||||||
@property
|
@classmethod
|
||||||
def preview_area(self) -> bpy.types.Area:
|
def preview_area(cls) -> bpy.types.Area | None:
|
||||||
"""Returns the visible preview area in the Blender UI.
|
"""Deduces a Blender UI area that can be used for image preview.
|
||||||
If none are valid, return None.
|
|
||||||
|
Returns:
|
||||||
|
A Blender UI area, if an appropriate one is visible; else `None`,
|
||||||
"""
|
"""
|
||||||
valid_areas = [
|
valid_areas = [
|
||||||
area for area in bpy.context.screen.areas if area.type == AREA_TYPE
|
area for area in bpy.context.screen.areas if area.type == AREA_TYPE
|
||||||
]
|
]
|
||||||
if valid_areas:
|
if valid_areas:
|
||||||
return valid_areas[0]
|
return valid_areas[0]
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@classmethod
|
||||||
def preview_space(self) -> bpy.types.SpaceProperties:
|
def preview_space(cls) -> bpy.types.SpaceProperties | None:
|
||||||
"""Returns the visible preview space in the visible preview area of
|
"""Deduces a Blender UI space, within `self.preview_area`, that can be used for image preview.
|
||||||
the Blender UI
|
|
||||||
|
Returns:
|
||||||
|
A Blender UI space within `self.preview_area`, if it isn't None; else, `None`.
|
||||||
"""
|
"""
|
||||||
if preview_area := self.preview_area:
|
preview_area = cls.preview_area()
|
||||||
return next(
|
if preview_area is not None:
|
||||||
|
valid_spaces = [
|
||||||
space for space in preview_area.spaces if space.type == SPACE_TYPE
|
space for space in preview_area.spaces if space.type == SPACE_TYPE
|
||||||
)
|
]
|
||||||
|
if valid_spaces:
|
||||||
|
return valid_spaces[0]
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Methods
|
# - Methods
|
||||||
####################
|
####################
|
||||||
def bl_select(self) -> None:
|
def bl_select(self) -> None:
|
||||||
"""Synchronizes the managed object to the preview, by manipulating
|
"""Selects the image by loading it into an on-screen UI area/space.
|
||||||
relevant editors.
|
|
||||||
"""
|
|
||||||
if bl_image := bpy.data.images.get(self.name):
|
|
||||||
self.preview_space.image = bl_image
|
|
||||||
|
|
||||||
def hide_preview(self) -> None:
|
Notes:
|
||||||
self.preview_space.image = None
|
The image must already be available, else nothing will happen.
|
||||||
|
"""
|
||||||
|
bl_image = bpy.data.images.get(self.name)
|
||||||
|
if bl_image is not None:
|
||||||
|
self.preview_space().image = bl_image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def hide_preview(cls) -> None:
|
||||||
|
"""Deselects the image by loading `None` into the on-screen UI area/space."""
|
||||||
|
cls.preview_space().image = None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Image Geometry
|
# - Image Geometry
|
||||||
|
@ -138,7 +172,7 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
dpi: int | None = None,
|
dpi: int | None = None,
|
||||||
):
|
):
|
||||||
# Compute Image Geometry
|
# Compute Image Geometry
|
||||||
if preview_area := self.preview_area:
|
if preview_area := self.preview_area():
|
||||||
# Retrieve DPI from Blender Preferences
|
# Retrieve DPI from Blender Preferences
|
||||||
_dpi = bpy.context.preferences.system.dpi
|
_dpi = bpy.context.preferences.system.dpi
|
||||||
|
|
||||||
|
@ -188,12 +222,10 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
image_data = func_image_data(4)
|
image_data = func_image_data(4)
|
||||||
width_px = image_data.shape[1]
|
width_px = image_data.shape[1]
|
||||||
height_px = image_data.shape[0]
|
height_px = image_data.shape[0]
|
||||||
# log.debug('Computed Image Data (%f)', time.perf_counter() - time_start)
|
|
||||||
|
|
||||||
bl_image = self.bl_image(width_px, height_px, 'RGBA', 'float32')
|
bl_image = self.bl_image(width_px, height_px, 'RGBA', 'float32')
|
||||||
bl_image.pixels.foreach_set(np.float32(image_data).ravel())
|
bl_image.pixels.foreach_set(np.float32(image_data).ravel())
|
||||||
bl_image.update()
|
bl_image.update()
|
||||||
# log.debug('Set BL Image (%f)', time.perf_counter() - time_start)
|
|
||||||
|
|
||||||
if bl_select:
|
if bl_select:
|
||||||
self.bl_select()
|
self.bl_select()
|
||||||
|
@ -259,4 +291,15 @@ class ManagedBLImage(base.ManagedObj):
|
||||||
if bl_select:
|
if bl_select:
|
||||||
self.bl_select()
|
self.bl_select()
|
||||||
times.append(time.perf_counter() - times[0])
|
times.append(time.perf_counter() - times[0])
|
||||||
#log.critical('Timing of MPL Plot: %s', str(times))
|
# log.critical('Timing of MPL Plot: %s', str(times))
|
||||||
|
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def pack_managed_images(_):
|
||||||
|
for image in bpy.data.images:
|
||||||
|
if image.is_dirty:
|
||||||
|
image.pack()
|
||||||
|
## TODO: Only pack images declared by a ManagedBLImage
|
||||||
|
|
||||||
|
|
||||||
|
bpy.app.handlers.save_pre.append(pack_managed_images)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import bpy
|
||||||
from blender_maxwell.utils import logger
|
from blender_maxwell.utils import logger
|
||||||
|
|
||||||
from . import contracts as ct
|
from . import contracts as ct
|
||||||
|
from .managed_objs.managed_bl_image import ManagedBLImage
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
@ -209,6 +210,22 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
for bl_socket in [*node.inputs, *node.outputs]:
|
for bl_socket in [*node.inputs, *node.outputs]:
|
||||||
bl_socket.locked = False
|
bl_socket.locked = False
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def replot(self) -> None:
|
||||||
|
self.is_currently_replotting = True
|
||||||
|
self.something_plotted = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.is_currently_replotting = False
|
||||||
|
if not self.something_plotted:
|
||||||
|
ManagedBLImage.hide_preview()
|
||||||
|
|
||||||
|
def report_show_plot(self, node: bpy.types.Node) -> None:
|
||||||
|
if hasattr(self, 'is_currently_replotting') and self.is_currently_replotting:
|
||||||
|
self.something_plotted = True
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def repreview_all(self) -> None:
|
def repreview_all(self) -> None:
|
||||||
all_nodes_with_preview_active = {
|
all_nodes_with_preview_active = {
|
||||||
|
@ -220,15 +237,12 @@ class MaxwellSimTree(bpy.types.NodeTree):
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
|
self.is_currently_repreviewing = False
|
||||||
for dangling_previewed_node in [
|
for dangling_previewed_node in [
|
||||||
node
|
node
|
||||||
for node_instance_id, node in all_nodes_with_preview_active.items()
|
for node_instance_id, node in all_nodes_with_preview_active.items()
|
||||||
if node_instance_id not in self.newly_previewed_nodes
|
if node_instance_id not in self.newly_previewed_nodes
|
||||||
]:
|
]:
|
||||||
# log.debug(
|
|
||||||
# 'Removing Dangling Preview of Node "{%s}"',
|
|
||||||
# str(dangling_previewed_node),
|
|
||||||
# )
|
|
||||||
dangling_previewed_node.preview_active = False
|
dangling_previewed_node.preview_active = False
|
||||||
|
|
||||||
def report_show_preview(self, node: bpy.types.Node) -> None:
|
def report_show_preview(self, node: bpy.types.Node) -> None:
|
||||||
|
|
|
@ -226,16 +226,16 @@ class OperateMathNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
def draw_label(self):
|
def draw_label(self):
|
||||||
labels = {
|
labels = {
|
||||||
'ADD': lambda: 'Filter: L + R',
|
'ADD': lambda: 'L + R',
|
||||||
'SUB': lambda: 'Filter: L - R',
|
'SUB': lambda: 'L - R',
|
||||||
'MUL': lambda: 'Filter: L · R',
|
'MUL': lambda: 'L · R',
|
||||||
'DIV': lambda: 'Filter: L / R',
|
'DIV': lambda: 'L / R',
|
||||||
'POW': lambda: 'Filter: L^R',
|
'POW': lambda: 'L^R',
|
||||||
'ATAN2': lambda: 'Filter: atan2(L,R)',
|
'ATAN2': lambda: 'atan2(L,R)',
|
||||||
}
|
}
|
||||||
|
|
||||||
if (label := labels.get(self.operation)) is not None:
|
if (label := labels.get(self.operation)) is not None:
|
||||||
return label()
|
return 'Operate: ' + label()
|
||||||
|
|
||||||
return self.bl_label
|
return self.bl_label
|
||||||
|
|
||||||
|
|
|
@ -186,6 +186,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
node_type = ct.NodeType.Viz
|
node_type = ct.NodeType.Viz
|
||||||
bl_label = 'Viz'
|
bl_label = 'Viz'
|
||||||
|
use_sim_node_name = True
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Sockets
|
# - Sockets
|
||||||
|
@ -285,7 +286,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='viz_mode',
|
prop_name='viz_mode',
|
||||||
## run_on_init: Implicitly triggered.
|
run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_viz_mode_changed(self):
|
def on_viz_mode_changed(self):
|
||||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||||
|
|
|
@ -245,9 +245,9 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='sim_node_name',
|
prop_name='sim_node_name',
|
||||||
props={'sim_node_name', 'managed_objs', 'managed_obj_types'},
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def _on_sim_node_name_changed(self, props: dict):
|
def _on_sim_node_name_changed(self):
|
||||||
log.info(
|
log.info(
|
||||||
'Changed Sim Node Name of a "%s" to "%s" (self=%s)',
|
'Changed Sim Node Name of a "%s" to "%s" (self=%s)',
|
||||||
self.bl_idname,
|
self.bl_idname,
|
||||||
|
@ -256,8 +256,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set Name of Managed Objects
|
# Set Name of Managed Objects
|
||||||
for mobj in props['managed_objs'].values():
|
self.managed_objs = bl_cache.Signal.InvalidateCache
|
||||||
mobj.name = props['sim_node_name']
|
|
||||||
|
|
||||||
@events.on_value_changed(prop_name='active_socket_set')
|
@events.on_value_changed(prop_name='active_socket_set')
|
||||||
def _on_socket_set_changed(self):
|
def _on_socket_set_changed(self):
|
||||||
|
@ -291,16 +290,27 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
## TODO: Account for FlowKind
|
## TODO: Account for FlowKind
|
||||||
bl_socket.value = socket_value
|
bl_socket.value = socket_value
|
||||||
|
|
||||||
|
@events.on_show_plot(stop_propagation=False)
|
||||||
|
def _on_show_plot(self):
|
||||||
|
node_tree = self.id_data
|
||||||
|
if len(self.event_methods_by_event[ct.FlowEvent.ShowPlot]) > 1:
|
||||||
|
## TODO: Is this check good enough?
|
||||||
|
## TODO: Set icon/indicator/something to make it clear which node is being previewed.
|
||||||
|
node_tree.report_show_plot(self)
|
||||||
|
|
||||||
@events.on_show_preview()
|
@events.on_show_preview()
|
||||||
def _on_show_preview(self):
|
def _on_show_preview(self):
|
||||||
node_tree = self.id_data
|
node_tree = self.id_data
|
||||||
node_tree.report_show_preview(self)
|
node_tree.report_show_preview(self)
|
||||||
|
|
||||||
# Set Preview to Active
|
# Set Preview to Active
|
||||||
## Implicitly triggers any @on_value_changed for preview_active.
|
## Implicitly triggers any @on_value_changed for preview_active.
|
||||||
if not self.preview_active:
|
if not self.preview_active:
|
||||||
self.preview_active = True
|
self.preview_active = True
|
||||||
|
|
||||||
@events.on_value_changed(prop_name='preview_active', props={'preview_active'})
|
@events.on_value_changed(
|
||||||
|
prop_name='preview_active', props={'preview_active'}, stop_propagation=True
|
||||||
|
)
|
||||||
def _on_preview_changed(self, props):
|
def _on_preview_changed(self, props):
|
||||||
if not props['preview_active']:
|
if not props['preview_active']:
|
||||||
for mobj in self.managed_objs.values():
|
for mobj in self.managed_objs.values():
|
||||||
|
|
|
@ -137,6 +137,10 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
props={'auto_plot'},
|
props={'auto_plot'},
|
||||||
)
|
)
|
||||||
def on_changed_plot_preview(self, props):
|
def on_changed_plot_preview(self, props):
|
||||||
|
node_tree = self.id_data
|
||||||
|
|
||||||
|
# Unset Plot if Nothing Plotted
|
||||||
|
with node_tree.replot():
|
||||||
if props['auto_plot']:
|
if props['auto_plot']:
|
||||||
self.trigger_event(ct.FlowEvent.ShowPlot)
|
self.trigger_event(ct.FlowEvent.ShowPlot)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue