mirror of
https://github.com/Relintai/material-maker.git
synced 2024-11-13 06:27:18 +01:00
5184f6375e
Fixed cut, copy and paste behavior when Material is selected (that node should not be deleted or copied, but connections should be pasted).
341 lines
9.5 KiB
GDScript
341 lines
9.5 KiB
GDScript
tool
|
|
extends GraphEdit
|
|
|
|
var save_path = null
|
|
var need_save = false
|
|
|
|
signal save_path_changed
|
|
signal graph_changed
|
|
|
|
func _ready():
|
|
$SaveViewport/ColorRect.material = $SaveViewport/ColorRect.material.duplicate(true)
|
|
OS.low_processor_usage_mode = true
|
|
center_view()
|
|
|
|
func _gui_input(event):
|
|
if event is InputEventKey and event.pressed:
|
|
if event.scancode == KEY_C:
|
|
center_view()
|
|
elif event.scancode == KEY_DELETE:
|
|
remove_selection()
|
|
|
|
# Misc. useful functions
|
|
|
|
func get_source(node, port):
|
|
for c in get_connection_list():
|
|
if c.to == node && c.to_port == port:
|
|
return { node=c.from, slot=c.from_port }
|
|
|
|
func offset_from_global_position(global_position):
|
|
return (scroll_offset + global_position - rect_global_position) / zoom
|
|
|
|
func add_node(node):
|
|
add_child(node)
|
|
node.connect("close_request", self, "remove_node", [ node ])
|
|
|
|
func connect_node(from, from_slot, to, to_slot):
|
|
var source_list = [ from ]
|
|
# Check if the new connection creates a cycle in the graph
|
|
while !source_list.empty():
|
|
var source = source_list.pop_front()
|
|
if source == to:
|
|
#print("cannot connect %s to %s (%s)" % [from, to, source])
|
|
return false
|
|
for c in get_connection_list():
|
|
if c.to == source and source_list.find(c.from) == -1:
|
|
source_list.append(c.from)
|
|
var disconnect = get_source(to, to_slot)
|
|
if disconnect != null:
|
|
.disconnect_node(disconnect.node, disconnect.slot, to, to_slot)
|
|
.connect_node(from, from_slot, to, to_slot)
|
|
send_changed_signal()
|
|
return true
|
|
|
|
func disconnect_node(from, from_slot, to, to_slot):
|
|
.disconnect_node(from, from_slot, to, to_slot)
|
|
send_changed_signal();
|
|
|
|
func remove_node(node):
|
|
var node_name = node.name
|
|
for c in get_connection_list():
|
|
if c.from == node_name or c.to == node_name:
|
|
disconnect_node(c.from, c.from_port, c.to, c.to_port)
|
|
send_changed_signal()
|
|
node.queue_free()
|
|
|
|
# Global operations on graph
|
|
|
|
func update_tab_title():
|
|
var title = "[unnamed]"
|
|
if save_path != null:
|
|
title = save_path.right(save_path.rfind("/")+1)
|
|
if need_save:
|
|
title += " *"
|
|
get_parent().set_tab_title(get_index(), title)
|
|
get_parent().update()
|
|
|
|
func set_need_save(ns):
|
|
if ns != need_save:
|
|
need_save = ns
|
|
if get_parent() is TabContainer:
|
|
update_tab_title()
|
|
|
|
func set_save_path(path):
|
|
if path != save_path:
|
|
save_path = path
|
|
if get_parent() is TabContainer:
|
|
update_tab_title()
|
|
else:
|
|
emit_signal("save_path_changed", self, path)
|
|
|
|
func clear_material():
|
|
clear_connections()
|
|
for c in get_children():
|
|
if c is GraphNode:
|
|
remove_child(c)
|
|
c.free()
|
|
send_changed_signal()
|
|
|
|
func new_material():
|
|
clear_material()
|
|
create_node({name="Material", type="material"})
|
|
set_save_path(null)
|
|
center_view()
|
|
|
|
func get_free_name(type):
|
|
var i = 0
|
|
while true:
|
|
var node_name = type+"_"+str(i)
|
|
if !has_node(node_name):
|
|
return node_name
|
|
i += 1
|
|
|
|
func create_nodes(data, position = null):
|
|
if data.has("type"):
|
|
var node_type = load("res://addons/procedural_material/nodes/"+data.type+".tscn")
|
|
if node_type != null:
|
|
var node = node_type.instance()
|
|
if data.has("name") && !has_node(data.name):
|
|
node.name = data.name
|
|
else:
|
|
node.name = get_free_name(data.type)
|
|
add_node(node)
|
|
node.deserialize(data)
|
|
if position != null:
|
|
node.offset += position
|
|
send_changed_signal()
|
|
return node
|
|
else:
|
|
if typeof(data.nodes) == TYPE_ARRAY and typeof(data.connections) == TYPE_ARRAY:
|
|
var names = {}
|
|
for c in data.nodes:
|
|
var node = create_nodes(c, position)
|
|
if node != null:
|
|
names[c.name] = node.name
|
|
node.selected = true
|
|
for c in data.connections:
|
|
connect_node(names[c.from], c.from_port, "Material" if c.to == "Material" else names[c.to], c.to_port)
|
|
return null
|
|
|
|
func load_file():
|
|
var dialog = FileDialog.new()
|
|
add_child(dialog)
|
|
dialog.rect_min_size = Vector2(500, 500)
|
|
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
|
dialog.mode = FileDialog.MODE_OPEN_FILE
|
|
dialog.add_filter("*.ptex;Procedural textures file")
|
|
dialog.connect("file_selected", self, "do_load_file")
|
|
dialog.popup_centered()
|
|
|
|
func do_load_file(filename):
|
|
var file = File.new()
|
|
if file.open(filename, File.READ) != OK:
|
|
return
|
|
var data = parse_json(file.get_as_text())
|
|
file.close()
|
|
clear_material()
|
|
for n in data.nodes:
|
|
var node = create_nodes(n)
|
|
for c in data.connections:
|
|
connect_node(c.from, c.from_port, c.to, c.to_port)
|
|
set_save_path(filename)
|
|
set_need_save(false)
|
|
center_view()
|
|
|
|
func save_file():
|
|
if save_path != null:
|
|
do_save_file(save_path)
|
|
else:
|
|
save_file_as()
|
|
|
|
func save_file_as():
|
|
var dialog = FileDialog.new()
|
|
add_child(dialog)
|
|
dialog.rect_min_size = Vector2(500, 500)
|
|
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
|
dialog.mode = FileDialog.MODE_SAVE_FILE
|
|
dialog.add_filter("*.ptex;Procedural textures file")
|
|
dialog.connect("file_selected", self, "do_save_file")
|
|
dialog.popup_centered()
|
|
|
|
func do_save_file(filename):
|
|
var data = { nodes = [] }
|
|
for c in get_children():
|
|
if c is GraphNode:
|
|
data.nodes.append(c.serialize())
|
|
data.connections = get_connection_list()
|
|
var file = File.new()
|
|
if file.open(filename, File.WRITE) == OK:
|
|
file.store_string(to_json(data))
|
|
file.close()
|
|
set_save_path(filename)
|
|
set_need_save(false)
|
|
|
|
func export_textures(size = null):
|
|
if save_path != null:
|
|
var prefix = save_path.left(save_path.rfind("."))
|
|
for c in get_children():
|
|
if c is GraphNode && c.has_method("export_textures"):
|
|
c.export_textures(prefix, size)
|
|
|
|
# Cut / copy / paste
|
|
|
|
func remove_selection():
|
|
for c in get_children():
|
|
if c is GraphNode and c.selected && c.name != "Material":
|
|
remove_node(c)
|
|
|
|
func serialize_selection():
|
|
var data = { nodes = [], connections = [] }
|
|
var nodes = []
|
|
for c in get_children():
|
|
if c is GraphNode and c.selected && c.name != "Material":
|
|
nodes.append(c)
|
|
if nodes.empty():
|
|
return null
|
|
var center = Vector2(0, 0)
|
|
for n in nodes:
|
|
center += n.offset+0.5*n.rect_size
|
|
center /= nodes.size()
|
|
for n in nodes:
|
|
var s = n.serialize()
|
|
var p = n.offset-center
|
|
s.node_position = { x=p.x, y=p.y }
|
|
data.nodes.append(s)
|
|
for c in get_connection_list():
|
|
var from = get_node(c.from)
|
|
var to = get_node(c.to)
|
|
if from != null and from.selected and to != null and to.selected:
|
|
data.connections.append(c)
|
|
return data
|
|
|
|
func can_copy():
|
|
for c in get_children():
|
|
if c is GraphNode and c.selected && c.name != "Material":
|
|
return true
|
|
return false
|
|
|
|
func cut():
|
|
copy()
|
|
remove_selection()
|
|
|
|
func copy():
|
|
OS.clipboard = to_json(serialize_selection())
|
|
|
|
func paste(pos = Vector2(0, 0)):
|
|
for c in get_children():
|
|
if c is GraphNode:
|
|
c.selected = false
|
|
var data = parse_json(OS.clipboard)
|
|
create_nodes(data, scroll_offset+0.5*rect_size)
|
|
|
|
# Center view
|
|
|
|
func center_view():
|
|
var center = Vector2(0, 0)
|
|
var node_count = 0
|
|
for c in get_children():
|
|
if c is GraphNode:
|
|
center += c.offset + 0.5*c.rect_size
|
|
node_count += 1
|
|
if node_count > 0:
|
|
center /= node_count
|
|
scroll_offset = center - 0.5*rect_size
|
|
|
|
# Delay after graph update
|
|
|
|
func send_changed_signal():
|
|
set_need_save(true)
|
|
$Timer.start()
|
|
|
|
func do_send_changed_signal():
|
|
emit_signal("graph_changed")
|
|
|
|
# Drag and drop
|
|
|
|
func can_drop_data(position, data):
|
|
return typeof(data) == TYPE_DICTIONARY and (data.has('type') or (data.has('nodes') and data.has('connections')))
|
|
|
|
func drop_data(position, data):
|
|
# The following mitigates the SpinBox problem (captures mouse while dragging)
|
|
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
create_nodes(data, offset_from_global_position(get_global_transform().xform(position)))
|
|
return true
|
|
|
|
# Save shader to image, create image texture
|
|
|
|
func setup_material(shader_material, textures, shader_code):
|
|
for k in textures.keys():
|
|
shader_material.set_shader_param(k+"_tex", textures[k])
|
|
shader_material.shader.code = shader_code
|
|
|
|
var render_queue = []
|
|
|
|
func render_shader_to_viewport(shader, textures, size, method, args):
|
|
render_queue.append( { shader=shader, textures=textures, size=size, method=method, args=args } )
|
|
if render_queue.size() == 1:
|
|
while !render_queue.empty():
|
|
var job = render_queue.front()
|
|
$SaveViewport.size = Vector2(job.size, job.size)
|
|
$SaveViewport/ColorRect.rect_position = Vector2(0, 0)
|
|
$SaveViewport/ColorRect.rect_size = Vector2(job.size, job.size)
|
|
var shader_material = $SaveViewport/ColorRect.material
|
|
shader_material.shader.code = job.shader
|
|
if job.textures != null:
|
|
for k in job.textures.keys():
|
|
shader_material.set_shader_param(k+"_tex", job.textures[k])
|
|
$SaveViewport.render_target_update_mode = Viewport.UPDATE_ONCE
|
|
$SaveViewport.update_worlds()
|
|
yield(get_tree(), "idle_frame")
|
|
yield(get_tree(), "idle_frame")
|
|
callv(job.method, job.args)
|
|
render_queue.pop_front()
|
|
|
|
func render_to_viewport(node, size, method, args):
|
|
render_shader_to_viewport(node.generate_shader(), node.get_textures(), size, method, args)
|
|
|
|
func export_texture(node, filename, size = 256):
|
|
if node == null:
|
|
return null
|
|
render_to_viewport(node, size, "do_export_texture", [ filename ])
|
|
|
|
func do_export_texture(filename):
|
|
var viewport_texture = $SaveViewport.get_texture()
|
|
var viewport_image = viewport_texture.get_data()
|
|
viewport_image.save_png(filename)
|
|
|
|
func precalculate_node(node, size, target_texture, object, method, args):
|
|
if node == null:
|
|
return null
|
|
render_to_viewport(node, size, "do_precalculate_texture", [ object, method, args ])
|
|
|
|
func precalculate_shader(shader, textures, size, target_texture, object, method, args):
|
|
render_shader_to_viewport(shader, textures, size, "do_precalculate_texture", [ target_texture, object, method, args ])
|
|
|
|
func do_precalculate_texture(target_texture, object, method, args):
|
|
var viewport_texture = $SaveViewport.get_texture()
|
|
target_texture.create_from_image(viewport_texture.get_data())
|
|
object.callv(method, args)
|
|
|