Skip to content

Commit 564e374

Browse files
committed
add lazy linking for graph panel
1 parent 3b18267 commit 564e374

File tree

7 files changed

+439
-4
lines changed

7 files changed

+439
-4
lines changed

export_presets.cfg

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

material_maker/panels/graph_edit/graph_edit.gd

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func _ready() -> void:
5353
add_valid_connection_type(t, 42)
5454
add_valid_connection_type(42, t)
5555

56+
5657
func _exit_tree():
5758
remove_crash_recovery_file()
5859

@@ -221,6 +222,7 @@ func _gui_input(event) -> void:
221222
if rect.has_point(get_global_mouse_position()):
222223
mm_globals.set_tip_text("Space/#RMB: Nodes menu, Arrow keys: Pan, Mouse wheel: Zoom", 3)
223224

225+
224226
func get_padded_node_rect(graph_node:GraphNode) -> Rect2:
225227
var rect : Rect2 = graph_node.get_global_rect()
226228
var padding := 8 * graph_node.get_global_transform().get_scale().x
@@ -230,6 +232,20 @@ func get_padded_node_rect(graph_node:GraphNode) -> Rect2:
230232

231233

232234
# Misc. useful functions
235+
236+
func get_closest_node_at_point(point: Vector2) -> GraphNode:
237+
var closest_dist : float = INF
238+
var closest_node : GraphNode
239+
for node in get_children():
240+
if node is GraphNode:
241+
var node_rect : Rect2 = node.get_rect()
242+
var dist : float = point.clamp(node_rect.position,
243+
node_rect.size + node_rect.position).distance_squared_to(point)
244+
if dist < closest_dist:
245+
closest_dist = dist
246+
closest_node = node
247+
return closest_node
248+
233249
func get_source(node, port) -> Dictionary:
234250
for c in get_connection_list():
235251
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://c673bqg0uj8li" 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")
@@ -86,6 +87,9 @@ layout_mode = 2
8687
[node name="UndoRedo" type="Node" parent="."]
8788
script = ExtResource("3")
8889

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