2025-07-01
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
|
||||
from .utils import arrange_relax, global_loc, calc_collision_y, calc_node
|
||||
|
||||
|
||||
def step(step_num, iter_num, nodes, props, root_center, iter_func):
|
||||
iter_cnt = 0
|
||||
for i in range(iter_num):
|
||||
new_center = mathutils.Vector((0, 0))
|
||||
node_cnt = 0
|
||||
changed = False
|
||||
for node in nodes:
|
||||
if node.type == 'FRAME':
|
||||
continue
|
||||
if props.ArrangeOnlySelected and not node.select:
|
||||
continue
|
||||
t = i / iter_num
|
||||
if iter_func(node, t):
|
||||
changed = True
|
||||
new_center += global_loc(node)
|
||||
node_cnt += 1
|
||||
|
||||
if not changed and props.AdaptiveIters:
|
||||
break
|
||||
if not props.ArrangeOnlySelected:
|
||||
new_center /= node_cnt
|
||||
slide = root_center - new_center # Keep Center
|
||||
for node in nodes:
|
||||
if node.type == 'FRAME':
|
||||
continue
|
||||
node.location += slide
|
||||
iter_cnt += 1
|
||||
if iter_cnt > props.BackgroundIterations:
|
||||
iter_cnt = 0
|
||||
props.ArrangeState = str(i) + "/" + str(iter_num) + " " + str(step_num) + "/4"
|
||||
yield 1
|
||||
|
||||
|
||||
class NodeRelaxArrange(bpy.types.Operator):
|
||||
"""Arrange Nodes"""
|
||||
bl_idname = "node_relax.arrange"
|
||||
bl_label = "Arrange Nodes"
|
||||
|
||||
bl_options = {"UNDO", "REGISTER"}
|
||||
|
||||
_timer = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
if space.type == 'NODE_EDITOR' and space.node_tree is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def main_routine(self, context):
|
||||
yield 1
|
||||
nodes = self.tree.nodes
|
||||
props = context.scene.NodeRelax_props
|
||||
root_center = mathutils.Vector((0, 0)) # Original Center
|
||||
|
||||
if not props.ArrangeOnlySelected:
|
||||
node_cnt = 0
|
||||
for node in nodes:
|
||||
if node.type == 'FRAME':
|
||||
continue
|
||||
root_center += global_loc(node)
|
||||
node_cnt += 1
|
||||
root_center /= node_cnt
|
||||
|
||||
yield from step(1, props.Iterations_S1, nodes, props, root_center,
|
||||
lambda curr_node, e: arrange_relax(curr_node, 1, 1, props.Distance, False))
|
||||
|
||||
yield from step(2, props.Iterations_S2, nodes, props, root_center,
|
||||
lambda curr_node, e: arrange_relax(curr_node, 1, 1, props.Distance, True))
|
||||
|
||||
dist = mathutils.Vector((0, props.Distance))
|
||||
yield from step(3, props.Iterations_S3, nodes, props, root_center,
|
||||
lambda curr_node, e: calc_collision_y(curr_node, nodes, e, dist))
|
||||
|
||||
dist = mathutils.Vector((props.Distance, props.Distance))
|
||||
zero_vec = mathutils.Vector((0, 0))
|
||||
yield from step(4, props.Iterations_S4, nodes, props, root_center,
|
||||
lambda curr_node, e: calc_node(curr_node, nodes, min(1, e * 2), zero_vec, 0.2, 1, dist, True))
|
||||
|
||||
yield 0
|
||||
|
||||
def finish(self, context):
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
props = context.scene.NodeRelax_props
|
||||
props.ArrangeState = ""
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == 'TIMER':
|
||||
state = next(self.main_coroutine)
|
||||
if state == 0:
|
||||
self.finish(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
if event.type in {'ESC'}:
|
||||
self.finish(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.tree = context.space_data.edit_tree
|
||||
|
||||
wm = context.window_manager
|
||||
self.main_coroutine = self.main_routine(context)
|
||||
self._timer = wm.event_timer_add(0.01, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -0,0 +1,201 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
|
||||
from .utils import global_loc, calc_node
|
||||
import gpu
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
DRAW_COLOR = (1, 1, 1, 0.5)
|
||||
DRAW_RADIUS = 10
|
||||
|
||||
|
||||
def draw_callback(self):
|
||||
if self.drag_mode:
|
||||
if self.dragging_node:
|
||||
node = self.dragging_node
|
||||
loc = global_loc(node)
|
||||
|
||||
x1, y1 = loc[0] - DRAW_RADIUS, loc[1] + DRAW_RADIUS
|
||||
x2, y2 = loc[0] + node.dimensions[0] + DRAW_RADIUS, loc[1] - node.dimensions[1] - DRAW_RADIUS
|
||||
|
||||
shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
|
||||
|
||||
vertices = ((x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1))
|
||||
vertex_colors = (DRAW_COLOR, DRAW_COLOR, DRAW_COLOR, DRAW_COLOR, DRAW_COLOR)
|
||||
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
|
||||
|
||||
shader.bind()
|
||||
batch.draw(shader)
|
||||
else:
|
||||
draw_circle_2d(self.cursor_pos, DRAW_COLOR, self.radius)
|
||||
|
||||
|
||||
class NodeRelaxBrush(bpy.types.Operator):
|
||||
"""Relax Nodes"""
|
||||
bl_idname = "node_relax.brush"
|
||||
bl_label = "Relax Nodes"
|
||||
|
||||
bl_options = {"UNDO", "REGISTER"}
|
||||
|
||||
def __init__(self):
|
||||
self.radius = 100
|
||||
self.lmb = False
|
||||
self.delta = mathutils.Vector((0, 0))
|
||||
self.cursor_pos = mathutils.Vector((0, 0))
|
||||
self.cursor_prev_pos = mathutils.Vector((0, 0))
|
||||
self.slide_vec = mathutils.Vector((0, 0))
|
||||
self. drag_mode = False
|
||||
self.is_dragging = False
|
||||
self.dragging_node = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return space.type == 'NODE_EDITOR' and space.node_tree is not None
|
||||
|
||||
def update_cursor_pos(self, context, event):
|
||||
self.cursor_prev_pos = self.cursor_pos
|
||||
self.cursor_pos = mathutils.Vector(
|
||||
context.region.view2d.region_to_view(event.mouse_region_x, event.mouse_region_y))
|
||||
|
||||
def update_radius(self, context, original_radius):
|
||||
radiusM = context.region.view2d.region_to_view(original_radius, 0)
|
||||
radius0 = context.region.view2d.region_to_view(0, 0)
|
||||
self.radius = radiusM[0] - radius0[0]
|
||||
|
||||
def get_brush_influence(self, loc, size):
|
||||
self.delta.x = self.cursor_pos.x - min(max(self.cursor_pos.x, loc.x), loc.x + size.x)
|
||||
self.delta.y = self.cursor_pos.y - max(min(self.cursor_pos.y, loc.y), loc.y - size.y)
|
||||
|
||||
dist_sqr = self.delta.x * self.delta.x + self.delta.y * self.delta.y
|
||||
|
||||
return 1 - (dist_sqr / (self.radius * self.radius))
|
||||
|
||||
def main_operation(self, context):
|
||||
nodes = self.tree.nodes
|
||||
props = context.scene.NodeRelax_props
|
||||
|
||||
self.slide_vec = self.cursor_pos - self.cursor_prev_pos
|
||||
context.area.tag_redraw()
|
||||
|
||||
if self.drag_mode:
|
||||
if self.is_dragging:
|
||||
if self.dragging_node:
|
||||
self.dragging_node.location += self.slide_vec
|
||||
else:
|
||||
self.update_dragging_node(nodes)
|
||||
else:
|
||||
if self.lmb:
|
||||
dist = mathutils.Vector((props.Distance, props.Distance))
|
||||
for node in nodes:
|
||||
if node.type == 'FRAME':
|
||||
continue
|
||||
# Brush
|
||||
loc = global_loc(node)
|
||||
size = node.dimensions
|
||||
infl = self.get_brush_influence(loc, size)
|
||||
if infl <= 0:
|
||||
continue
|
||||
|
||||
# Calculate physics
|
||||
calc_node(node, nodes, infl, self.slide_vec * props.SlidePower, props.RelaxPower,
|
||||
props.CollisionPower, dist, False)
|
||||
|
||||
def update_dragging_node(self, nodes):
|
||||
self.dragging_node = None
|
||||
nearest = 0
|
||||
for node in nodes:
|
||||
if node.type == 'FRAME':
|
||||
continue
|
||||
loc = global_loc(node)
|
||||
pos = mathutils.Vector((loc.x, loc.y))
|
||||
pos.x += node.dimensions.x / 2
|
||||
pos.y -= node.dimensions.y / 2
|
||||
pos -= self.cursor_pos
|
||||
dist = pos.x * pos.x + pos.y * pos.y # Squared length
|
||||
if self.dragging_node is None or dist < nearest:
|
||||
self.dragging_node = node
|
||||
nearest = dist
|
||||
|
||||
def finish(self, context, props):
|
||||
st = bpy.types.SpaceNodeEditor
|
||||
st.draw_handler_remove(self.draw_handler, 'WINDOW')
|
||||
props.IsRunning = False
|
||||
|
||||
def modal(self, context, event):
|
||||
props = context.scene.NodeRelax_props
|
||||
|
||||
if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: # allow navigation shortcuts
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
if event.type == 'LEFT_SHIFT' or event.type == 'RIGHT_SHIFT': # drag individual node
|
||||
if event.value == 'PRESS':
|
||||
self.drag_mode = True
|
||||
if event.value == 'RELEASE':
|
||||
self.drag_mode = False
|
||||
self.is_dragging = False
|
||||
self.update_dragging_node(self.tree.nodes)
|
||||
context.area.tag_redraw()
|
||||
|
||||
if event.type == 'MOUSEMOVE':
|
||||
self.update_cursor_pos(context, event)
|
||||
self.update_radius(context, props.BrushSize)
|
||||
self.main_operation(context)
|
||||
|
||||
if event.type == 'WHEELUPMOUSE' or event.type == 'WHEELDOWNMOUSE':
|
||||
self.update_cursor_pos(context, event)
|
||||
self.update_radius(context, props.BrushSize)
|
||||
context.area.tag_redraw()
|
||||
|
||||
elif event.type == 'LEFTMOUSE':
|
||||
if event.value == 'PRESS':
|
||||
self.lmb = True
|
||||
if self.drag_mode:
|
||||
self.is_dragging = True
|
||||
else:
|
||||
self.update_cursor_pos(context, event)
|
||||
self.cursor_prev_pos = self.cursor_pos # No sliding
|
||||
self.main_operation(context)
|
||||
if event.value == 'RELEASE':
|
||||
self.lmb = False
|
||||
if self.drag_mode:
|
||||
self.is_dragging = False
|
||||
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.finish(context, props)
|
||||
context.area.tag_redraw()
|
||||
return {'FINISHED'}
|
||||
|
||||
if event.type == "LEFT_BRACKET":
|
||||
props.BrushSize -= 10
|
||||
props.BrushSize = max(props.BrushSize, 10)
|
||||
self.update_radius(context, props.BrushSize)
|
||||
context.area.tag_redraw()
|
||||
|
||||
elif event.type == "RIGHT_BRACKET":
|
||||
props.BrushSize += 10
|
||||
props.BrushSize = min(props.BrushSize, 1000)
|
||||
self.update_radius(context, props.BrushSize)
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.NodeRelax_props
|
||||
if props.IsRunning:
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.tree = context.space_data.edit_tree
|
||||
context.window_manager.modal_handler_add(self)
|
||||
st = bpy.types.SpaceNodeEditor
|
||||
self.draw_handler = st.draw_handler_add(draw_callback, (self,), 'WINDOW', 'POST_VIEW')
|
||||
|
||||
self.lmb = False
|
||||
props.IsRunning = True
|
||||
self.update_cursor_pos(context, event)
|
||||
self.update_radius(context, props.BrushSize)
|
||||
|
||||
context.area.tag_redraw()
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -0,0 +1,224 @@
|
||||
import mathutils
|
||||
|
||||
MOVE_UNIT = 1
|
||||
|
||||
|
||||
def global_loc(node):
|
||||
if node.parent:
|
||||
return global_loc(node.parent) + node.location
|
||||
else:
|
||||
return node.location
|
||||
|
||||
|
||||
def calc_node(node, nodes, influence, slide_vec, relax_power, collide_power, collide_dist, pull_non_siblings):
|
||||
if node.type == 'FRAME':
|
||||
return False
|
||||
|
||||
loc = global_loc(node)
|
||||
size = node.dimensions
|
||||
|
||||
offset = mathutils.Vector(slide_vec)
|
||||
|
||||
if relax_power > 0:
|
||||
# Relax
|
||||
tar_y = 0
|
||||
link_cnt = 0
|
||||
tar_x_in = loc.x
|
||||
has_input = False
|
||||
for socket in node.inputs: # Input links
|
||||
for link in socket.links:
|
||||
other = link.from_node
|
||||
if not pull_non_siblings and node.parent != other.parent:
|
||||
continue
|
||||
loc_other = global_loc(other)
|
||||
size_other = other.dimensions
|
||||
|
||||
x = loc_other.x + size_other.x + collide_dist.x
|
||||
if has_input > 0:
|
||||
tar_x_in = max(tar_x_in, x)
|
||||
else:
|
||||
tar_x_in = x
|
||||
has_input = True
|
||||
|
||||
tar_y += loc_other.y - size_other.y / 2
|
||||
link_cnt += 1
|
||||
|
||||
tar_x_out = loc.x
|
||||
has_output = False
|
||||
for socket in node.outputs: # Output links
|
||||
for link in socket.links:
|
||||
other = link.to_node
|
||||
if not pull_non_siblings and node.parent != other.parent:
|
||||
continue
|
||||
loc_other = global_loc(other)
|
||||
size_other = other.dimensions
|
||||
|
||||
x = loc_other.x - size.x - collide_dist.x
|
||||
if has_output > 0:
|
||||
tar_x_out = min(tar_x_out, x)
|
||||
else:
|
||||
tar_x_out = x
|
||||
has_output = True
|
||||
|
||||
tar_y += loc_other.y - size_other.y / 2
|
||||
link_cnt += 1
|
||||
|
||||
if link_cnt > 0:
|
||||
tar_x = tar_x_in * int(has_input) + tar_x_out * int(has_output)
|
||||
tar_x /= int(has_input) + int(has_output)
|
||||
tar_y /= link_cnt
|
||||
tar_y += size.y / 2
|
||||
offset.x += (tar_x - loc.x) * relax_power
|
||||
offset.y += (tar_y - loc.y) * relax_power
|
||||
|
||||
if collide_power > 0:
|
||||
# Collision
|
||||
for other in nodes:
|
||||
if other == node:
|
||||
continue
|
||||
if other.type == 'FRAME':
|
||||
continue
|
||||
collide(loc, global_loc(other), size, other.dimensions,
|
||||
offset, collide_power, collide_dist)
|
||||
|
||||
if abs(offset.x) > MOVE_UNIT or abs(offset.y) > MOVE_UNIT:
|
||||
node.location += offset * influence
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def socket_pos(socket, sockets, size):
|
||||
i = 0
|
||||
sockets_filtered = []
|
||||
for s in sockets:
|
||||
if len(s.links) > 0:
|
||||
sockets_filtered.append(s)
|
||||
|
||||
for s in sockets_filtered:
|
||||
if s == socket:
|
||||
return (i / len(sockets_filtered)) * size
|
||||
|
||||
i += 1
|
||||
return size / 2
|
||||
|
||||
|
||||
def calc_collision_y(node, nodes, collide_power, collide_dist):
|
||||
if node.type == 'FRAME':
|
||||
return False
|
||||
|
||||
loc = global_loc(node)
|
||||
size = node.dimensions
|
||||
|
||||
offset = mathutils.Vector((0, 0))
|
||||
|
||||
# Collision
|
||||
for other in nodes:
|
||||
if other == node:
|
||||
continue
|
||||
if other.type == 'FRAME':
|
||||
continue
|
||||
collide(loc, global_loc(other), size, other.dimensions,
|
||||
offset, 1, collide_dist, True)
|
||||
|
||||
if abs(offset.y) > MOVE_UNIT:
|
||||
node.location += offset * collide_power
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def arrange_relax(node, influence, relax_power, distance, clamped_pull):
|
||||
if node.type == 'FRAME':
|
||||
return False
|
||||
|
||||
loc = global_loc(node)
|
||||
size = node.dimensions
|
||||
|
||||
offset = mathutils.Vector((0, 0))
|
||||
|
||||
# Relax
|
||||
tar_y = 0
|
||||
tar_x_in = loc.x if clamped_pull else 0
|
||||
link_cnt = 0
|
||||
has_input = False
|
||||
for socket in node.inputs: # Input links
|
||||
for link in socket.links:
|
||||
other = link.from_node
|
||||
# if node.parent != other.parent: continue
|
||||
loc_other = global_loc(other)
|
||||
size_other = other.dimensions
|
||||
|
||||
x = loc_other.x + size_other.x + distance
|
||||
if clamped_pull:
|
||||
if has_input > 0:
|
||||
tar_x_in = max(tar_x_in, x)
|
||||
else:
|
||||
tar_x_in = x
|
||||
else:
|
||||
tar_x_in += x
|
||||
has_input = True
|
||||
|
||||
tar_y += loc_other.y + socket_pos(socket, node.inputs, size.y) - socket_pos(link.from_socket, other.outputs,
|
||||
size_other.y)
|
||||
link_cnt += 1
|
||||
|
||||
tar_x_out = loc.x if clamped_pull else 0
|
||||
has_output = False
|
||||
for socket in node.outputs: # Output links
|
||||
for link in socket.links:
|
||||
other = link.to_node
|
||||
# if node.parent != other.parent: continue
|
||||
loc_other = global_loc(other)
|
||||
size_other = other.dimensions
|
||||
|
||||
x = loc_other.x - size.x - distance
|
||||
if clamped_pull:
|
||||
if has_output > 0:
|
||||
tar_x_out = min(tar_x_out, x)
|
||||
else:
|
||||
tar_x_out = x
|
||||
else:
|
||||
tar_x_out += x
|
||||
has_output = True
|
||||
|
||||
tar_y += loc_other.y + socket_pos(socket, node.outputs, size.y) - socket_pos(link.to_socket, other.inputs,
|
||||
size_other.y)
|
||||
link_cnt += 1
|
||||
|
||||
if link_cnt > 0:
|
||||
if clamped_pull:
|
||||
tar_x = tar_x_in * int(has_input) + tar_x_out * int(has_output)
|
||||
tar_x /= int(has_input) + int(has_output)
|
||||
else:
|
||||
tar_x = (tar_x_in + tar_x_out) / link_cnt
|
||||
tar_y /= link_cnt
|
||||
offset.x += (tar_x - loc.x) * relax_power
|
||||
offset.y += (tar_y - loc.y) * relax_power
|
||||
|
||||
if abs(offset.x) > MOVE_UNIT or abs(offset.y) > MOVE_UNIT:
|
||||
node.location += offset * influence
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def collide(loc0, loc1, size0, size1, offset, power, dist, only_y=False):
|
||||
pos0 = loc0 + size0 / 2
|
||||
pos1 = loc1 + size1 / 2
|
||||
pos0.y -= size0.y
|
||||
pos1.y -= size1.y
|
||||
|
||||
size = (size0 + size1) / 2 + dist
|
||||
delta = pos1 - pos0
|
||||
inters = size - mathutils.Vector((abs(delta.x), abs(delta.y)))
|
||||
|
||||
if inters.x > 0 and inters.y > 0:
|
||||
if inters.y < inters.x or only_y:
|
||||
if delta.y > 0:
|
||||
inters.y *= -1
|
||||
offset.y += inters.y / 2 * power
|
||||
else:
|
||||
if delta.x > 0:
|
||||
inters.x *= -1
|
||||
offset.x += inters.x / 2 * power
|
||||
Reference in New Issue
Block a user