diff --git a/classes/UUID.gd b/classes/UUID.gd new file mode 100644 index 0000000..c03281a --- /dev/null +++ b/classes/UUID.gd @@ -0,0 +1,88 @@ +# MIT License +# +# Copyright (c) 2023 Xavier Sellier +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Note: The code might not be as pretty it could be, since it's written +# in a way that maximizes performance. Methods are inlined and loops are avoided. +class_name UUID +const BYTE_MASK: int = 0b11111111 + + +static func uuidbin() -> Array: + randomize() + # 16 random bytes with the bytes on index 6 and 8 modified + return [ + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK, + ((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + ] + +static func uuidbinrng(rng: RandomNumberGenerator) -> Array: + rng.randomize() + return [ + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK, + ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + ] + +static func v4() -> String: + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbin() + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +static func v4_rng(rng: RandomNumberGenerator) -> String: + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbinrng(rng) + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] diff --git a/classes/deck/deck.gd b/classes/deck/deck.gd new file mode 100644 index 0000000..f32ef42 --- /dev/null +++ b/classes/deck/deck.gd @@ -0,0 +1,57 @@ +class_name Deck + +var nodes: Dictionary + +enum Types{ + ERROR = -1, + BOOL, + NUMERIC, + STRING, + ARRAY, + DICTIONARY, +} + + +static var type_assoc: Dictionary = { + Types.ERROR: DeckType.DeckTypeError, + Types.BOOL: DeckType.DeckTypeBool, + Types.NUMERIC: DeckType.DeckTypeNumeric, + Types.STRING: DeckType.DeckTypeString, + Types.ARRAY: DeckType.DeckTypeArray, + Types.DICTIONARY: DeckType.DeckTypeDictionary, +} + + + +func add_node(node: GDScript, meta: Dictionary = {}) -> DeckNode: + # TODO: accept instances of DeckNode instead of instancing here? + var uuid := UUID.v4() + var node_inst: DeckNode = node.new() as DeckNode + nodes[uuid] = node_inst + node_inst._belonging_to = self + node_inst._id = uuid + + if !meta.is_empty(): + for k in meta: + node_inst.set_meta(k, meta[k]) + + return node_inst + + +func get_node(uuid: String) -> DeckNode: + return nodes.get(uuid) + + +func connect_nodes(from_node: DeckNode, to_node: DeckNode, from_port: int, to_port: int) -> bool: + # first, check that we can do the type conversion. + var type_a: Types = from_node.input_ports[from_port].type + var type_b: Types = to_node.output_ports[to_port].type + var err: DeckType = (type_assoc[type_b] as GDScript).from(type_assoc[type_a]) + if err is DeckType.DeckTypeError: + print(err.error_message) + return false + + # TODO: prevent duplicate connections + + from_node.add_outgoing_connection(from_port, to_node._id, to_port) + return true diff --git a/classes/deck/deck_node.gd b/classes/deck/deck_node.gd new file mode 100644 index 0000000..62f0ab6 --- /dev/null +++ b/classes/deck/deck_node.gd @@ -0,0 +1,41 @@ +class_name DeckNode + +var name: String +var input_ports: Array[Port] +var output_ports: Array[Port] +var outgoing_connections: Dictionary + +var _belonging_to: Deck +var _id: String + + +func add_input_port(type: Deck.Types, label: String, descriptor: String = "") -> void: + var port := Port.new(type, label, descriptor) + input_ports.append(port) + + +func add_output_port(type: Deck.Types, label: String, descriptor: String = "") -> void: + var port := Port.new(type, label, descriptor) + output_ports.append(port) + + +func send(from_port: int, data: DeckType, extra_data: Array = []) -> void: + if !(outgoing_connections.get(from_port)): + return + + for connection in outgoing_connections[from_port]: + connection = connection as Dictionary + # key is node uuid + # value is input port on destination node + for node in connection: + _belonging_to.get_node(node)._receive(connection[node], data, extra_data) + + +func _receive(to_port: int, data: DeckType, extra_data: Array = []) -> void: + pass + + +func add_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void: + var port_connections: Array = outgoing_connections.get(from_port, []) + port_connections.append({to_node: to_port}) + outgoing_connections[from_port] = port_connections diff --git a/classes/deck/nodes/button.gd b/classes/deck/nodes/button.gd new file mode 100644 index 0000000..8b6bd03 --- /dev/null +++ b/classes/deck/nodes/button.gd @@ -0,0 +1,11 @@ +extends DeckNode + + +func _init() -> void: + add_output_port( + Deck.Types.STRING, + "Press me", + "button" + ) + + name = "Button" diff --git a/classes/deck/nodes/print.gd b/classes/deck/nodes/print.gd new file mode 100644 index 0000000..eaf3aae --- /dev/null +++ b/classes/deck/nodes/print.gd @@ -0,0 +1,18 @@ +extends DeckNode + + +func _init() -> void: + add_input_port( + Deck.Types.STRING, + "Input", + "field" + ) + + name = "Print" + + +func _receive(_to_port: int, data: DeckType, extra_data: Array = []) -> void: + # we only have one port, so we can skip checking which port we received on + var data_to_print = data.get_value() + print(data_to_print) + print("extra data: ", extra_data) diff --git a/classes/deck/port.gd b/classes/deck/port.gd new file mode 100644 index 0000000..258ae10 --- /dev/null +++ b/classes/deck/port.gd @@ -0,0 +1,11 @@ +class_name Port + +var type: Deck.Types +var label: String +var descriptor: String + + +func _init(p_type: Deck.Types, p_label: String, p_descriptor: String = "") -> void: + type = p_type + label = p_label + descriptor = p_descriptor diff --git a/classes/types/deck_type.gd b/classes/types/deck_type.gd new file mode 100644 index 0000000..da7e437 --- /dev/null +++ b/classes/types/deck_type.gd @@ -0,0 +1,106 @@ +class_name DeckType + +var _value: Variant +var _success: bool = true + + +func is_valid() -> bool: + return _success + + +func get_value() -> Variant: + return _value + + +static func from(other: DeckType): + return null + + +class DeckTypeError extends DeckType: + var error_message: String + + + func _init(p_error_message: String = ""): + _value = self + _success = false + + error_message = p_error_message + + + static func from(other: DeckType) -> DeckTypeError: + return DeckTypeError.new() + + +class DeckTypeNumeric extends DeckType: + static func from(other: DeckType): + if other is DeckTypeNumeric: + return other + + if other is DeckTypeString: + if (other.get_value() as String).is_valid_float(): + var value: float = float(other.get_value() as String) + var inst := DeckTypeNumeric.new() + inst._value = value + return inst + else: + var err: DeckTypeError = DeckTypeError.from(other) + err.error_message = "Conversion from String to Numeric failed, check the number" + return err + + if other is DeckTypeBool: + var inst := DeckTypeNumeric.new() + inst._value = float(other.get_value() as bool) + return inst + + var err: DeckTypeError = DeckTypeError.from(other) + err.error_message = "Conversion to Numeric is only possible from String or Bool." + return err + + +class DeckTypeString extends DeckType: + static func from(other: DeckType): + if other is DeckTypeString: + return other + + var inst := DeckTypeString.new() + inst._value = var_to_str(other.get_value()) + + +class DeckTypeBool extends DeckType: + static func from(other: DeckType): + if other is DeckTypeBool: + return other + + if other is DeckTypeNumeric: + var inst := DeckTypeBool.new() + inst._value = bool(other.get_value()) + return inst + + if other is DeckTypeDictionary or other is DeckTypeArray: + var inst := DeckTypeBool.new() + inst._value = !other.get_value().is_empty() + return inst + + +class DeckTypeArray extends DeckType: + static func from(other: DeckType): + if other is DeckTypeString: + var inst := DeckTypeArray.new() + inst._value = str_to_var(other.get_value()) + return inst + + var err: DeckTypeError = DeckTypeError.from(other) + err.error_message = "conversions to Array is only possible from String" + return err + + +class DeckTypeDictionary extends DeckType: + static func from(other: DeckType): + if other is DeckTypeString: + var inst := DeckTypeDictionary.new() + inst._value = str_to_var(other.get_value()) + return inst + + var err: DeckTypeError = DeckTypeError.from(other) + err.error_message = "conversions to Dictionary is only possible from String" + return err diff --git a/port_drawer.gd b/port_drawer.gd new file mode 100644 index 0000000..7e07c20 --- /dev/null +++ b/port_drawer.gd @@ -0,0 +1,53 @@ +extends HBoxContainer + +@onready var left_slot: ColorRect = $LeftSlot +@onready var right_slot: ColorRect = $RightSlot + +var text: String + +signal button_pressed + + +func set_input_enabled(enabled: bool) -> void: + left_slot.visible = enabled + + +func set_output_enabled(enabled: bool) -> void: + right_slot.visible = enabled + + +func add_label(text: String) -> void: + var l := Label.new() + add_child(l) + l.text = text + move_child(l, 1) + l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + +func add_field() -> void: + var le := LineEdit.new() + add_child(le) + move_child(le, 1) + le.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + le.text_changed.connect( + func(new_text: String): + text = new_text + ) + + +func get_text() -> String: + return text + + +func add_button(text: String) -> void: + var b := Button.new() + b.text = text + add_child(b) + move_child(b, 1) + b.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + b.pressed.connect( + func(): + button_pressed.emit() + ) diff --git a/port_drawer.tscn b/port_drawer.tscn new file mode 100644 index 0000000..a7bb465 --- /dev/null +++ b/port_drawer.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=2 format=3 uid="uid://ddqtmahfxel26"] + +[ext_resource type="Script" path="res://port_drawer.gd" id="1_wot5w"] + +[node name="PortDrawer" type="HBoxContainer"] +script = ExtResource("1_wot5w") + +[node name="LeftSlot" type="ColorRect" parent="."] +visible = false +custom_minimum_size = Vector2(12, 12) +layout_mode = 2 + +[node name="RightSlot" type="ColorRect" parent="."] +visible = false +custom_minimum_size = Vector2(12, 12) +layout_mode = 2 diff --git a/project.godot b/project.godot index 34c5313..064e347 100644 --- a/project.godot +++ b/project.godot @@ -11,5 +11,7 @@ config_version=5 [application] config/name="Re-DotDeck" +config/tags=PackedStringArray("dot_deck") +run/main_scene="res://test.tscn" config/features=PackedStringArray("4.1", "Forward Plus") config/icon="res://icon.svg" diff --git a/test.gd b/test.gd new file mode 100644 index 0000000..e67e833 --- /dev/null +++ b/test.gd @@ -0,0 +1,32 @@ +extends Control + +var node_renderer_scene := preload("res://test_node_renderer.tscn") +@onready var nodes_container: HBoxContainer = $NodesContainer + +@onready var add_button_button: Button = $AddButtonButton +@onready var add_print_button: Button = $AddPrintButton +@onready var connect_them_button: Button = $ConnectThemButton + +var deck: Deck = Deck.new() +var button_node = preload("res://classes/deck/nodes/button.gd") +var print_node = preload("res://classes/deck/nodes/print.gd") + + +func _ready() -> void: + add_button_button.pressed.connect( + func(): + var node := deck.add_node(button_node) + var node_renderer = node_renderer_scene.instantiate() + node_renderer.node = node + nodes_container.add_child(node_renderer) + add_button_button.disabled = true + ) + + add_print_button.pressed.connect( + func(): + var node := deck.add_node(print_node) + var node_renderer = node_renderer_scene.instantiate() + node_renderer.node = node + nodes_container.add_child(node_renderer) + add_print_button.disabled = true + ) diff --git a/test.tscn b/test.tscn new file mode 100644 index 0000000..769c9dc --- /dev/null +++ b/test.tscn @@ -0,0 +1,46 @@ +[gd_scene load_steps=2 format=3 uid="uid://bhpd6rfiuimw5"] + +[ext_resource type="Script" path="res://test.gd" id="1_in4g7"] + +[node name="Test" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_in4g7") + +[node name="AddButtonButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 26.0 +offset_top = 576.0 +offset_right = 119.0 +offset_bottom = 607.0 +text = "Add Button" + +[node name="AddPrintButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 152.0 +offset_top = 576.0 +offset_right = 248.0 +offset_bottom = 607.0 +text = "Add Print" + +[node name="ConnectThemButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 283.0 +offset_top = 576.0 +offset_right = 379.0 +offset_bottom = 607.0 +text = "Connect them" + +[node name="NodesContainer" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 0.999646 +anchor_bottom = 0.245654 +offset_right = 0.40799 +offset_bottom = -0.184006 +theme_override_constants/separation = 20 +metadata/_edit_use_anchors_ = true diff --git a/test_node_renderer.gd b/test_node_renderer.gd new file mode 100644 index 0000000..33ca14d --- /dev/null +++ b/test_node_renderer.gd @@ -0,0 +1,38 @@ +extends PanelContainer + +@onready var name_label: Label = %NameLabel +@onready var elements_container: VBoxContainer = %ElementsContainer + +var node: DeckNode +const PortDrawer := preload("res://port_drawer.gd") +var port_drawer_scene := preload("res://port_drawer.tscn") + +# THIS IS SUPER JANK AND A HACK FOR DEMONSTRATION PURPOSES +# PLEASE DO NOT ACTUALLY DO ANYTHING THIS CLASS DOES +# IN THE REAL PROJECT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +func _ready() -> void: + name_label.text = node.name + for input_port in node.input_ports: + var port_drawer: PortDrawer = port_drawer_scene.instantiate() + elements_container.add_child(port_drawer) + port_drawer.set_input_enabled(true) + match input_port.descriptor: + "field": + port_drawer.add_field() +# "button": +# port_drawer.add_button(input_port.label) + _: + port_drawer.add_label(input_port.label) + + for i in node.output_ports.size(): + if elements_container.get_child_count() - 1 < i: + var pd: PortDrawer = port_drawer_scene.instantiate() + elements_container.add_child(pd) + var port_drawer: PortDrawer = elements_container.get_child(i) + port_drawer.set_output_enabled(true) + var output_port := node.output_ports[i] + if output_port.descriptor == "button": + port_drawer.add_button(output_port.label) + + diff --git a/test_node_renderer.tscn b/test_node_renderer.tscn new file mode 100644 index 0000000..d50f9da --- /dev/null +++ b/test_node_renderer.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=2 format=3 uid="uid://ch8s1d7vobhi4"] + +[ext_resource type="Script" path="res://test_node_renderer.gd" id="1_85wy1"] + +[node name="TestNodeRenderer" type="PanelContainer"] +custom_minimum_size = Vector2(300, 0) +anchors_preset = -1 +anchor_right = 0.26 +anchor_bottom = 0.34 +offset_right = 0.47998 +offset_bottom = -0.320007 +script = ExtResource("1_85wy1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ColorRect" type="ColorRect" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(4, 0) +layout_mode = 2 +color = Color(1, 1, 0.34902, 1) + +[node name="NameLabel" type="Label" parent="VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Name" + +[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ElementsContainer" type="VBoxContainer" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3