2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -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