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: var scancode_with_modifiers = event.get_scancode_with_modifiers() if scancode_with_modifiers == KEY_C: center_view() elif scancode_with_modifiers == 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(): if !get_parent().has_method("set_tab_title"): return var title = "[unnamed]" if save_path != null: title = save_path.right(save_path.rfind("/")+1) if need_save: title += " *" if get_parent().has_method("set_tab_title"): get_parent().set_tab_title(get_index(), title) func set_need_save(ns): if ns != need_save: need_save = ns update_tab_title() func set_save_path(path): if path != save_path: save_path = path update_tab_title() 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 == null: return 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)