Skip to content

Commit d096f05

Browse files
committed
add lazy linking for graph panel
1 parent 7c7e91c commit d096f05

6 files changed

Lines changed: 421 additions & 1 deletion

File tree

material_maker/panels/graph_edit/graph_edit.gd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,20 @@ func _draw() -> void:
355355
MMGraphPortal.draw_links(self)
356356

357357
# Misc. useful functions
358+
359+
func get_closest_node_at_point(point: Vector2) -> GraphNode:
360+
var closest_dist : float = INF
361+
var closest_node : GraphNode
362+
for node in get_children():
363+
if node is GraphNode:
364+
var node_rect : Rect2 = node.get_rect()
365+
var dist : float = point.clamp(node_rect.position,
366+
node_rect.size + node_rect.position).distance_squared_to(point)
367+
if dist < closest_dist:
368+
closest_dist = dist
369+
closest_node = node
370+
return closest_node
371+
358372
func get_source(node, port) -> Dictionary:
359373
for c in get_connection_list():
360374
if c.to_node == node and c.to_port == port:

material_maker/panels/graph_edit/graph_edit.tscn

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
[gd_scene load_steps=8 format=3 uid="uid://dy1u50we7gtru"]
1+
[gd_scene load_steps=9 format=3 uid="uid://dy1u50we7gtru"]
22

33
[ext_resource type="Script" uid="uid://dkp4w3at1o6cm" path="res://material_maker/panels/graph_edit/graph_edit.gd" id="1"]
44
[ext_resource type="Texture2D" uid="uid://c0j4px4n72di5" path="res://material_maker/icons/icons.tres" id="2"]
55
[ext_resource type="Script" uid="uid://bne3k0g56crmy" path="res://material_maker/tools/undo_redo/undo_redo.gd" id="3"]
66
[ext_resource type="PackedScene" uid="uid://buj231c2gxm4o" path="res://material_maker/widgets/desc_button/desc_button.tscn" id="4"]
7+
[ext_resource type="PackedScene" uid="uid://bd3ummbwaq3i" path="res://material_maker/panels/graph_edit/lazy_link/lazy_link.tscn" id="5_u5byk"]
78

89
[sub_resource type="AtlasTexture" id="3"]
910
atlas = ExtResource("2")
@@ -88,6 +89,9 @@ layout_mode = 2
8889
[node name="UndoRedo" type="Node" parent="."]
8990
script = ExtResource("3")
9091

92+
[node name="LazyLink" parent="." instance=ExtResource("5_u5byk")]
93+
layout_mode = 1
94+
9195
[connection signal="connection_from_empty" from="." to="." method="request_popup" binds= [true]]
9296
[connection signal="connection_request" from="." to="." method="on_connect_node"]
9397
[connection signal="connection_to_empty" from="." to="." method="request_popup" binds= [false]]
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
extends Control
2+
3+
class_name LazyLink
4+
5+
enum LazyNode {
6+
FROM,
7+
TO,
8+
}
9+
10+
enum Port {
11+
INPUT,
12+
OUTPUT,
13+
}
14+
15+
var csf : float
16+
var popup_menu_item_height : int
17+
18+
const MAX_POPUP_HEIGHT = 375
19+
const SLOT_SVG = """
20+
<svg width="16" height="16">
21+
<circle cx="8" cy="8" r="6" fill="#FFF" stroke="#000" stroke-width="1"/>
22+
</svg>
23+
"""
24+
25+
var inactive_color : Color
26+
var inactive_link_color : Color
27+
var active_color : Color
28+
var active_link_color : Color
29+
var context_color : Color
30+
var context_link_color : Color
31+
32+
var source : MMGraphNodeMinimal
33+
var target : MMGraphNodeMinimal
34+
35+
var frame: StyleBoxFlat
36+
var linked_frame: StyleBoxFlat
37+
38+
var from_point : Vector2
39+
var has_context : bool = false
40+
var is_lazy_linking: bool = false
41+
var is_context_link: bool = false
42+
var is_context_linking : bool = false
43+
44+
45+
func _ready() -> void:
46+
csf = get_window().content_scale_factor
47+
popup_menu_item_height = (get_theme_constant("v_separation", "PopupMenu")
48+
+ get_theme_font_size("font"))
49+
50+
_setup_colors()
51+
frame = StyleBoxFlat.new()
52+
frame.shadow_size = 4
53+
frame.shadow_color = Color(0, 0, 0, 0.1)
54+
frame.draw_center = false
55+
frame.border_color = inactive_color
56+
frame.corner_detail = get_theme_constant("corner_detail", "MM_LazyLink")
57+
frame.set_border_width_all(get_theme_constant("border_width", "MM_LazyLink"))
58+
frame.set_corner_radius_all(get_theme_constant("corner_radius", "MM_LazyLink"))
59+
frame.set_expand_margin_all(get_theme_constant("expand_margin", "MM_LazyLink"))
60+
61+
linked_frame = frame.duplicate()
62+
linked_frame.border_color = active_color
63+
64+
func _setup_colors() -> void:
65+
inactive_color = get_theme_color("inactive_color", "MM_LazyLink")
66+
inactive_link_color = get_theme_color("inactive_link_color", "MM_LazyLink")
67+
active_color = get_theme_color("active_color", "MM_LazyLink")
68+
active_link_color = get_theme_color("active_link_color", "MM_LazyLink")
69+
context_color = get_theme_color("context_color", "MM_LazyLink")
70+
context_link_color = get_theme_color("context_link_color", "MM_LazyLink")
71+
72+
func _draw() -> void:
73+
if not is_context_linking and source:
74+
if target:
75+
if is_context_link:
76+
_set_colors(inactive_color, context_color,
77+
context_link_color, context_color)
78+
else:
79+
_set_colors(active_color, active_color,
80+
active_link_color, active_color)
81+
draw_style_box(linked_frame, source.get_rect())
82+
draw_style_box(linked_frame, target.get_rect())
83+
else:
84+
_set_colors(inactive_color, inactive_color,
85+
inactive_link_color, inactive_color)
86+
draw_style_box(frame, source.get_rect())
87+
88+
func _input(event: InputEvent) -> void:
89+
if event.is_action_pressed("ui_cancel"):
90+
end_link(true)
91+
elif event is InputEventMouseButton:
92+
if event.button_index == MOUSE_BUTTON_RIGHT:
93+
if event.pressed and event.alt_pressed:
94+
invalidate_link()
95+
if event.shift_pressed:
96+
is_context_link = true
97+
accept_event()
98+
is_lazy_linking = true
99+
source = get_parent().get_closest_node_at_point(
100+
get_local_mouse_position())
101+
show_node(LazyNode.TO)
102+
from_point = get_local_mouse_position()
103+
elif is_lazy_linking or is_context_link:
104+
end_link()
105+
elif event is InputEventMouseMotion:
106+
if event.button_mask & MOUSE_BUTTON_MASK_RIGHT != 0 and is_lazy_linking:
107+
accept_event()
108+
target = null
109+
var closest = get_parent().get_closest_node_at_point(
110+
get_local_mouse_position())
111+
if closest != source:
112+
target = closest
113+
set_link([from_point, get_local_mouse_position()])
114+
show_node(LazyNode.FROM)
115+
116+
func _set_colors(frame_color : Color, linked_frame_color : Color,
117+
link_color : Color, node_color : Color) -> void:
118+
frame.border_color = frame_color
119+
linked_frame.border_color = linked_frame_color
120+
$Link.gradient.colors[LazyNode.FROM] = link_color
121+
$NodeA.material.set_shader_parameter("node_color", node_color)
122+
123+
func set_link(pts : PackedVector2Array) -> void:
124+
$Link.points = pts
125+
126+
func end_link(is_cancel : bool = false) -> void:
127+
$NodeA.hide()
128+
$NodeB.hide()
129+
$Link.points = PackedVector2Array()
130+
if not is_cancel:
131+
if is_context_link:
132+
is_context_linking = true
133+
has_context = true
134+
create_context_connection()
135+
else:
136+
do_lazy_connection()
137+
if not is_context_linking:
138+
invalidate_link()
139+
queue_redraw()
140+
141+
func invalidate_link() -> void:
142+
source = null
143+
target = null
144+
is_context_link = false
145+
is_lazy_linking = false
146+
is_context_linking = false
147+
has_context = false
148+
149+
func show_node(node : LazyNode) -> void:
150+
var color_rect : ColorRect = $NodeA if node else $NodeB
151+
color_rect.show()
152+
color_rect.position = get_local_mouse_position() - color_rect.size * 0.5
153+
move_to_front()
154+
queue_redraw()
155+
156+
func port_attr(node : MMGraphNodeMinimal, port_idx : int,
157+
is_output : bool, key : String) -> String:
158+
if node:
159+
var defs : Array = (node.generator.get_output_defs()
160+
if is_output else node.generator.get_input_defs())
161+
if defs[port_idx].has(key):
162+
return defs[port_idx][key]
163+
return ""
164+
165+
func has_input_link(node : MMGraphNodeMinimal, port_idx : int) -> bool:
166+
var graph : GraphEdit = get_parent()
167+
if graph:
168+
for c : Dictionary in graph.connections:
169+
if c.to_port == port_idx and c.to_node == node.name:
170+
return true
171+
return false
172+
173+
func connect_port_type(types : Array, allow_any : bool = false) -> bool:
174+
var graph : GraphEdit = get_parent()
175+
if not graph:
176+
return false
177+
for out_port : int in source.get_output_port_count():
178+
for in_port : int in target.get_input_port_count():
179+
if (source.get_output_port_type(out_port) == target.get_input_port_type(in_port)
180+
and not has_input_link(target, in_port)):
181+
var link := (port_attr(source, out_port, Port.OUTPUT, "type") + "_" +
182+
port_attr(target, in_port, Port.INPUT, "type"))
183+
if allow_any or link in types:
184+
graph.on_connect_node(source.name, out_port, target.name, in_port)
185+
return true
186+
return false
187+
188+
func create_context_menu(is_output : bool, source_output : int = -1) -> void:
189+
var context_node : MMGraphNodeMinimal = source if is_output else target
190+
191+
# skip context menu if target node only has one input
192+
if not is_output and context_node.get_input_port_count() == 1:
193+
do_context_link(0, source_output)
194+
return
195+
196+
var popup := PopupMenu.new()
197+
popup.add_theme_constant_override(
198+
"item_%s_padding" % [ "end" if is_output else "start" ], 16)
199+
200+
if not is_output:
201+
popup.set_layout_direction(Window.LAYOUT_DIRECTION_RTL)
202+
203+
var context_port_count : int = (context_node.get_output_port_count()
204+
if is_output else context_node.get_input_port_count())
205+
206+
# determine port label
207+
for i in context_port_count:
208+
var port_name : String
209+
for attr : String in ["label", "shortdesc", "name"]:
210+
port_name = port_attr(context_node, i, is_output, attr)
211+
# skip positional label if there's nothing in it
212+
if attr == "label" and port_name.split(":")[0].is_valid_int():
213+
if port_name.split(":")[1].is_empty():
214+
continue
215+
if not port_name.is_empty():
216+
break
217+
port_name = " - "
218+
port_name = tr(port_name)
219+
220+
var context_port_color : Color = (context_node.get_output_port_color(i)
221+
if is_output else context_node.get_input_port_color(i))
222+
223+
var slot_icon := Image.new()
224+
slot_icon.load_svg_from_buffer(SLOT_SVG.replace("#FFF",
225+
"#" + context_port_color.to_html(false)).to_utf8_buffer())
226+
popup.add_icon_item(ImageTexture.create_from_image(slot_icon), port_name)
227+
228+
popup.content_scale_factor = csf
229+
popup.close_requested.connect(invalidate_link)
230+
popup.window_input.connect(popup_window_input)
231+
popup.popup_hide.connect(popup_hidden.bind(popup))
232+
popup.set_focused_item(0)
233+
234+
if source_output != -1:
235+
popup.id_pressed.connect(do_context_link.bind(source_output))
236+
else:
237+
popup.id_pressed.connect(do_context_link.bind(-1 if is_output else 0))
238+
239+
add_child(popup)
240+
popup.position = get_screen_transform() * (get_local_mouse_position() -
241+
Vector2(popup.get_contents_minimum_size().x - 16,
242+
popup_menu_item_height))
243+
244+
popup.size = popup.get_contents_minimum_size() * csf
245+
popup.max_size.y = MAX_POPUP_HEIGHT * int(csf)
246+
popup.show()
247+
248+
func popup_window_input(event : InputEvent) -> void:
249+
if event.is_action("ui_cancel"):
250+
invalidate_link()
251+
252+
func popup_hidden(popup : PopupMenu) -> void:
253+
if not has_context:
254+
invalidate_link()
255+
popup.queue_free()
256+
257+
func create_context_connection() -> void:
258+
var graph : GraphEdit = get_parent()
259+
if (source and target and graph
260+
and source.get_output_port_count()
261+
and target.get_input_port_count()):
262+
create_context_menu(source.get_output_port_count() != 1)
263+
264+
func do_context_link(to : int, from : int) -> void:
265+
if from != -1:
266+
if (source.get_output_port_type(from) == target.get_input_port_type(to)
267+
or source.get_output_port_type(from) == 42
268+
or target.get_input_port_type(to) == 42):
269+
get_parent().on_connect_node(source.name, from, target.name, to)
270+
invalidate_link()
271+
else:
272+
create_context_menu(Port.INPUT, to)
273+
274+
# handle lazy connection (alt + rmb)
275+
func do_lazy_connection() -> void:
276+
var graph : GraphEdit = get_parent()
277+
if (not (source and target and graph) or
278+
not (source.get_output_port_count()
279+
and target.get_input_port_count())):
280+
return
281+
282+
# connect by exact port name (short description, case-sensitive)
283+
for out_port : int in source.get_output_port_count():
284+
for in_port : int in target.get_input_port_count():
285+
if not has_input_link(target, in_port):
286+
if (port_attr(source, out_port, Port.OUTPUT, "shortdesc")
287+
== port_attr(target, in_port, Port.INPUT, "shortdesc")):
288+
graph.on_connect_node(source.name, out_port, target.name, in_port)
289+
return
290+
291+
# connect by exact port type (e.g. float, rgba)
292+
for out_port : int in source.get_output_port_count():
293+
for in_port : int in target.get_input_port_count():
294+
if (port_attr(source, out_port, Port.OUTPUT, "type")
295+
== port_attr(target, in_port, Port.INPUT, "type")
296+
and not has_input_link(target, in_port)):
297+
graph.on_connect_node(source.name, out_port, target.name, in_port)
298+
return
299+
300+
# connect color to color type first (i.e. from rgb/rgba)
301+
for type in [["rgba_rgba", "rgba_rgb", "rgb_rgba"], ["rgb_f"]]:
302+
if connect_port_type(type):
303+
return
304+
305+
# connect by compatible slot type
306+
if connect_port_type([], true):
307+
return
308+
309+
# allow "any" type (i.e. Switch/Reroute) to form connections
310+
for out_port : int in source.get_output_port_count():
311+
for in_port : int in target.get_input_port_count():
312+
if (source.get_output_port_type(out_port) == 42 or
313+
target.get_input_port_type(in_port) == 42
314+
and not has_input_link(target, in_port)):
315+
graph.on_connect_node(source.name, out_port, target.name, in_port)
316+
return
317+
318+
# force at least one connection(compatible slot type) even if all slots are used
319+
for in_port : int in target.get_input_port_count():
320+
if (source.get_output_port_type(0) == target.get_input_port_type(in_port)
321+
or source.get_output_port_type(0) == 42
322+
or target.get_input_port_type(in_port) == 42):
323+
graph.on_connect_node(source.name, 0, target.name, in_port)
324+
return
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://p7v3ka7aaxmu

0 commit comments

Comments
 (0)