From e1def9427fe5d77ca47ec6dbe608ec291acdfc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Fri, 11 Jul 2025 19:40:26 +0200 Subject: [PATCH 01/40] Update README.md: tutorial for visualization --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 74ae966..4753575 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,21 @@ To access this functionality, you will have to load an ontology and then click o **If nothing happens when enabling real time information, it either means that your ontology is missing the linking data or that there are no messages in the RabbitMQ exchange. You can explore a dump file with a text editor to check if linking data was correctly loaded.** +## Access Digital Twin Visualization + +### Preparation + +- Create or Open another Godot project containing your visualization. **Warning: Godot software version must match DTInsight's**. +- Create a scene called `main` that will host your visualization. +- Configure a fucntion `_on_message(message)` that will receive raw real-time data from DTInsight. +- Export the project as a `.pck` + +### Use + +If the OML description is properly configured (by having the `DTDFVocab:HasVisualization true` property), simply pick the exported `.pck` file and you will be able to open the visualization on DTInsight. + +Screenshot 2025-07-11 133809 + ## Access Digital Twin script If you have your Digital Twin's scripts on your pc and an ontology providing a relative path to those files, you will be able to access those script files from the visualizer. From acd9b8708fecb6a2e8a02f7c1798a885fb5dafa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Fri, 11 Jul 2025 19:41:55 +0200 Subject: [PATCH 02/40] Update README.md: typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4753575..250a549 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ To access this functionality, you will have to load an ontology and then click o ### Preparation - Create or Open another Godot project containing your visualization. **Warning: Godot software version must match DTInsight's**. -- Create a scene called `main` that will host your visualization. -- Configure a fucntion `_on_message(message)` that will receive raw real-time data from DTInsight. +- Create a scene called `main.tscn` that will host your visualization. +- Configure a function `_on_message(message)` that will receive raw real-time data from DTInsight. - Export the project as a `.pck` ### Use From 88af42ca6450888e210292df50f0c73033799b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Mon, 14 Jul 2025 19:46:41 -0400 Subject: [PATCH 03/40] Fixed issues and added descriptions to the constellation --- DTContainer/DT&PTContainer.gd | 2 + Fuseki/Data/FusekiData.gd | 2 +- GenericDisplay/GenericDisplay.gd | 10 +++- GenericDisplay/generic_display.tscn | 93 +++++++++++++++++++++++++++-- RabbitMQ/Data/RabbitData.gd | 24 +++++--- 5 files changed, 115 insertions(+), 16 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 4c8043f..5e21623 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -221,6 +221,8 @@ func update_node_with(visual_container : HBoxContainer, fuseki_node_data : Dicti new_node.set_python_script_location(fuseki_node_data[key][PythonConfig.LOCATION_KEY][0]) if ("HasVisualization" in fuseki_node_data[key].keys() and fuseki_node_data[key]["HasVisualization"][0] == "true"): new_node.set_visualization() + if ("desc" in fuseki_node_data[key].keys() and fuseki_node_data[key]["desc"][0] != ""): + new_node.set_description(fuseki_node_data[key]["desc"][0]) visual_container.add_child(new_node) var displayed_element = NamedNode.new() displayed_element.name = key diff --git a/Fuseki/Data/FusekiData.gd b/Fuseki/Data/FusekiData.gd index 4ddd53e..9d908cb 100644 --- a/Fuseki/Data/FusekiData.gd +++ b/Fuseki/Data/FusekiData.gd @@ -134,7 +134,7 @@ static func parse_fuseki_value(value) -> String: #Transform the result agregator in an array of GenericLinkedNodes or #in a dictionary depending on the data type static func format_result(result_aggregator, is_link = false): - return parse_link_result(result_aggregator) if (is_link) else parse_element_result(result_aggregator) + return parse_link_result(result_aggregator) if (is_link) else parse_element_result(result_aggregator) #Transform the result agregator in an Array of GenericLinkedNodes static func parse_link_result(result_agregator) -> Array[GenericLinkedNodes]: diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 92102c7..cf7f898 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -76,6 +76,9 @@ func _on_file_selected(pck_path: String) -> void: if not last_loaded_pck_path.is_empty(): $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = false +func _on_description_button_pressed() -> void: + $PopupDescription.popup() + func _on_viz_pop_up_button_pressed(): var pop_up_panel = $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton/PopupPanel var pop_up_panel_3d_root = %popup_root @@ -103,7 +106,7 @@ func _on_viz_pop_up_button_pressed(): #Informations ------------------------------------------------------------------ func set_text(text : String) -> void: - var node : Label = get_node("GenericDisplay/PresentationBox/GenericElementName") + var node : Label = $GenericDisplay/PresentationBox/GenericElementName node.text = text func set_python_script_location(path : String) -> void: @@ -126,6 +129,11 @@ func set_visualization(): if last_loaded_pck_path.is_empty(): $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = true +func set_description(description): + $GenericDisplay/PresentationBox/DescriptionButton.show() + $PopupDescription.title = $GenericDisplay/PresentationBox/GenericElementName.text + " description" + $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description + func set_info(new_data : Array[String], is_bool = false) -> void: data = to_float_array(new_data, is_bool) var node : Label = get_node("GenericDisplay/RealTimeContainer/GenericElementAttributes") diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index a447b4e..a50dc57 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=7 format=3 uid="uid://tvcjlrt40n0q"] +[gd_scene load_steps=10 format=3 uid="uid://tvcjlrt40n0q"] [ext_resource type="Script" uid="uid://ost4ju4q0ybe" path="res://GenericDisplay/GenericDisplay.gd" id="1_5lixw"] [ext_resource type="Script" uid="uid://jeh5r2brp2mt" path="res://GenericDisplay/PopUpChart/PopUpChartControl.gd" id="2_5nhr6"] @@ -16,6 +16,42 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5nhr6"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.1, 0.1, 0.1, 0.6) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 +corner_detail = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0c6qw"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0, 0, 0, 0.6) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 +corner_detail = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ycnc1"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.225, 0.225, 0.225, 0.6) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 +corner_detail = 5 + [sub_resource type="ViewportTexture" id="ViewportTexture_ipcor"] viewport_path = NodePath("GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton/PopupPanel/TextureRect/SubViewport") @@ -51,6 +87,17 @@ text = "Default" horizontal_alignment = 1 vertical_alignment = 3 +[node name="DescriptionButton" type="Button" parent="GenericDisplay/PresentationBox"] +visible = false +layout_mode = 2 +tooltip_text = "Open Description" +theme_override_font_sizes/font_size = 10 +theme_override_styles/normal = SubResource("StyleBoxFlat_5nhr6") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0c6qw") +theme_override_styles/hover = SubResource("StyleBoxFlat_ycnc1") +text = " i " +icon_alignment = 1 + [node name="ScriptButton" type="Button" parent="GenericDisplay/PresentationBox"] visible = false layout_mode = 2 @@ -126,8 +173,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 516.0 -offset_bottom = 516.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("2_5nhr6") @@ -149,6 +196,41 @@ alignment = 1 [node name="Chart" parent="PopupChart/ChartControl/VBoxContainer/ChartContainer" instance=ExtResource("3_0c6qw")] layout_mode = 2 +[node name="PopupDescription" type="PopupPanel" parent="."] +disable_3d = true +oversampling_override = 1.0 +title = "Description" +initial_position = 4 +size = Vector2i(400, 200) +unresizable = false +borderless = false +always_on_top = true + +[node name="DescriptionControl" type="Control" parent="PopupDescription"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = 396.0 +offset_bottom = 196.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="DescriptionContainer" type="VBoxContainer" parent="PopupDescription/DescriptionControl"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Description" type="RichTextLabel" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +layout_mode = 2 +size_flags_vertical = 3 +text = "No description" + [node name="PopupScript" type="PopupPanel" parent="."] disable_3d = true oversampling_override = 1.0 @@ -166,8 +248,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 516.0 -offset_bottom = 516.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_ipcor") @@ -188,6 +270,7 @@ text = "Select the DT script folder in the settings first" [connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] [connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] [connection signal="file_selected" from="GenericDisplay/FilePicker" to="." method="_on_file_selected"] +[connection signal="pressed" from="GenericDisplay/PresentationBox/DescriptionButton" to="." method="_on_description_button_pressed"] [connection signal="pressed" from="GenericDisplay/PresentationBox/ScriptButton" to="." method="_on_script_button_pressed"] [connection signal="pressed" from="GenericDisplay/RealTimeContainer/PopUpButton" to="." method="_on_pop_up_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPickerButton" to="." method="_on_viz_picker_button_pressed"] diff --git a/RabbitMQ/Data/RabbitData.gd b/RabbitMQ/Data/RabbitData.gd index f128248..6c2bfc3 100644 --- a/RabbitMQ/Data/RabbitData.gd +++ b/RabbitMQ/Data/RabbitData.gd @@ -28,15 +28,21 @@ func set_fuseki_data_manager(fuseki : FusekiData) -> void: fuseki_data = fuseki func update_rabbit_data() -> void: - exchange_name = fuseki_data.rabbit_exchange["incubator_exchange"]["exchange_name_str"][0] - routing_key = fuseki_data.rabbit_route["incubator_routing_key"]["routing_key_str"][0] - source = fuseki_data.rabbit_source["incubator_source"]["source_str"][0] - for ml in fuseki_data.rabbit_message_listener.keys(): - var new_ml = MessageListner.new() - new_ml.viz_container = fuseki_data.rabbit_message_listener[ml]["MLTarget"][0] - new_ml.data = Fifo.new() - message_listeners[fuseki_data.rabbit_message_listener[ml]["MLSource"][0]] = new_ml - rabbit_mq_controller.get_rabbit_parameters(exchange_name, [routing_key] as Array[String]) + # Check if the necessary data exists + if "rabbit_exchange" in fuseki_data and "incubator_exchange" in fuseki_data.rabbit_exchange and "exchange_name_str" in fuseki_data.rabbit_exchange["incubator_exchange"]: + exchange_name = fuseki_data.rabbit_exchange["incubator_exchange"]["exchange_name_str"][0] + + if "rabbit_route" in fuseki_data and "incubator_routing_key" in fuseki_data.rabbit_route and "routing_key_str" in fuseki_data.rabbit_route["incubator_routing_key"]: + routing_key = fuseki_data.rabbit_route["incubator_routing_key"]["routing_key_str"][0] + + if "rabbit_source" in fuseki_data and "incubator_source" in fuseki_data.rabbit_source and "source_str" in fuseki_data.rabbit_source["incubator_source"]: + source = fuseki_data.rabbit_source["incubator_source"]["source_str"][0] + + # Only proceed if all necessary data exists + if exchange_name != "" and routing_key != "" and source != "": + rabbit_mq_controller.get_rabbit_parameters(exchange_name, [routing_key] as Array[String]) + else: + print("Missing required data. Skipping operation.") func _on_updated_rabbit(rabbit_data : String) -> void: # Emit signal for sending data to other nodes From b4d05b976fe55e76f8440c1209096f5d23ffe384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 16 Jul 2025 16:27:29 -0400 Subject: [PATCH 04/40] Made DT constellation more compact --- DTContainer/DT&PTContainer.gd | 4 +- DTContainer/DT&PTContainer.tscn | 75 ++++++++++++++++++++++---------- GenericDisplay/GenericDisplay.gd | 4 +- MainScene/MainScene.tscn | 5 +-- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 5e21623..b080acb 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -6,8 +6,8 @@ class_name DT_PT #Reference each visual data container @onready var service_container = $DTContainer/ServicesPanel/ServicesOrganizer/ServicesContainer @onready var enabler_container = $DTContainer/EnablersPanel/EnablerOrganizer/EnablersContainer -@onready var model_container = $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsContainer -@onready var data_container = $DTContainer/ModelsDataPanel/ModelsDataContainer/DataContainer +@onready var model_container = $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/ModelsContainer +@onready var data_container = $DTContainer/ModelsDataPanel/ModelsDataContainer/DataPanelContainer/DataContainer @onready var operator_container = $"PTContainer/Operator&EnvContainer/OperatorPanel/OperatorOrganizer/OperatorContainer" @onready var machine_container = $PTContainer/DataTravelContainer/MachinePanel/MachineOrganizer/MachineContainer @onready var data_transmitted_container = $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/DataTransmittedOrganizer/DataTransmittedContainer diff --git a/DTContainer/DT&PTContainer.tscn b/DTContainer/DT&PTContainer.tscn index 479fe9b..2f93664 100644 --- a/DTContainer/DT&PTContainer.tscn +++ b/DTContainer/DT&PTContainer.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=10 format=3 uid="uid://nqgolpvgp7yg"] +[gd_scene load_steps=12 format=3 uid="uid://nqgolpvgp7yg"] [ext_resource type="Script" uid="uid://cijvy55ehil" path="res://DTContainer/DT&PTContainer.gd" id="1_juoha"] @@ -74,10 +74,33 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb3t8"] +content_margin_left = 0.0 +content_margin_top = 0.0 +content_margin_right = 0.0 +content_margin_bottom = 0.0 +bg_color = Color(0.10196078, 0.10196078, 0.10196078, 0) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +corner_detail = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panlg"] +content_margin_left = 0.0 +content_margin_top = 0.0 +content_margin_right = 0.0 +content_margin_bottom = 0.0 +bg_color = Color(0.10196078, 0.10196078, 0.10196078, 0) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +corner_detail = 5 + [node name="DT&PTContainer" type="HBoxContainer"] -offset_top = 732.0 -offset_right = 240.0 -offset_bottom = 732.0 +offset_right = 1260.0 +offset_bottom = 480.0 theme_override_constants/separation = 50 script = ExtResource("1_juoha") @@ -93,7 +116,6 @@ theme_override_constants/separation = 0 [node name="OperatorPanel" type="PanelContainer" parent="PTContainer/Operator&EnvContainer"] z_index = -1 layout_mode = 2 -size_flags_vertical = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_sp6md") @@ -117,13 +139,11 @@ size_flags_vertical = 3 [node name="ExperimentContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer"] layout_mode = 2 -size_flags_vertical = 3 theme_override_constants/separation = 0 [node name="EnvPanel" type="PanelContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer"] z_index = -1 layout_mode = 2 -size_flags_horizontal = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_ts1y0") @@ -140,14 +160,13 @@ uppercase = true layout_mode = 2 [node name="EnvContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/EnvOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 223) layout_mode = 2 size_flags_horizontal = 6 [node name="SystemPanel" type="PanelContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer"] z_index = -1 layout_mode = 2 -size_flags_horizontal = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_5tggx") @@ -165,7 +184,7 @@ uppercase = true layout_mode = 2 [node name="SystemContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/SystemOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 223) layout_mode = 2 size_flags_horizontal = 6 @@ -176,7 +195,6 @@ theme_override_constants/separation = 0 [node name="MachinePanel" type="PanelContainer" parent="PTContainer/DataTravelContainer"] z_index = -1 layout_mode = 2 -size_flags_vertical = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_qb31t") @@ -200,7 +218,6 @@ size_flags_horizontal = 6 [node name="DataOutPanel" type="PanelContainer" parent="PTContainer/DataTravelContainer"] z_index = -1 layout_mode = 2 -size_flags_vertical = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_rl1kt") @@ -218,6 +235,7 @@ custom_minimum_size = Vector2(200, 200) layout_mode = 2 [node name="SensorsOrganizer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] +custom_minimum_size = Vector2(0, 100) layout_mode = 2 size_flags_vertical = 3 @@ -236,6 +254,7 @@ text = "Data transmitted" uppercase = true [node name="DataTransmittedOrganizer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] +custom_minimum_size = Vector2(0, 92) layout_mode = 2 size_flags_vertical = 3 @@ -267,7 +286,7 @@ uppercase = true layout_mode = 2 [node name="ServicesContainer" type="HBoxContainer" parent="DTContainer/ServicesPanel/ServicesOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 @@ -290,7 +309,7 @@ uppercase = true layout_mode = 2 [node name="EnablersContainer" type="HBoxContainer" parent="DTContainer/EnablersPanel/EnablerOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 @@ -301,7 +320,15 @@ layout_mode = 2 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_ox2pm") -[node name="ModelsLabel" type="Label" parent="DTContainer/ModelsDataPanel"] +[node name="ModelsDataContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel"] +layout_mode = 2 +theme_override_constants/separation = 50 + +[node name="ModelsPanelContainer" type="PanelContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_qb3t8") + +[node name="ModelsLabel" type="Label" parent="DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer"] z_index = 1 layout_mode = 2 size_flags_horizontal = 0 @@ -311,17 +338,17 @@ text = "Models " uppercase = true -[node name="ModelsDataContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel"] -layout_mode = 2 -theme_override_constants/separation = 50 - -[node name="ModelsContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] -custom_minimum_size = Vector2(200, 200) +[node name="ModelsContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer"] +custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 -[node name="DataLabel" type="Label" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] +[node name="DataPanelContainer" type="PanelContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_panlg") + +[node name="DataLabel" type="Label" parent="DTContainer/ModelsDataPanel/ModelsDataContainer/DataPanelContainer"] z_index = 1 layout_mode = 2 size_flags_horizontal = 0 @@ -330,7 +357,7 @@ theme_override_colors/font_color = Color(0, 0, 0, 1) text = "Data" uppercase = true -[node name="DataContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] -custom_minimum_size = Vector2(200, 200) +[node name="DataContainer" type="HBoxContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer/DataPanelContainer"] +custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index cf7f898..2b145d3 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -166,8 +166,8 @@ func format_float(f: float, sig_digits: int = 4) -> String: var abs_f = abs(f) var exponent = floor(log(abs_f) / log(10)) - var scale = pow(10, sig_digits - 1 - exponent) - var rounded = round(f * scale) / scale + var _scale = pow(10, sig_digits - 1 - exponent) + var rounded = round(f * _scale) / _scale var result = String.num(rounded, 10) diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 99f8b63..509bbd5 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -25,10 +25,7 @@ script = ExtResource("1_r7thu") [node name="DTContainer" parent="." instance=ExtResource("1_lklxr")] z_as_relative = false -offset_left = -9.0 -offset_top = -2.0 -offset_right = 1344.0 -offset_bottom = 726.0 +offset_bottom = 503.0 [node name="Camera2D" type="Camera2D" parent="."] position = Vector2(575, 326) From 4ed55489c4f80cedcf7240be318b019ea6ec4889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 16 Jul 2025 16:48:10 -0400 Subject: [PATCH 05/40] Fixed container size and outer link distance --- Config/StyleConfig.gd | 2 +- DTContainer/DT&PTContainer.tscn | 18 +++++++++--------- MainScene/MainScene.tscn | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Config/StyleConfig.gd b/Config/StyleConfig.gd index 9d50f94..576a73c 100644 --- a/Config/StyleConfig.gd +++ b/Config/StyleConfig.gd @@ -18,7 +18,7 @@ class DTElement: class Link: const WIDTH : int = 5 - const MEAN_OUTER_LINK_DISTANCE : int = 30 * WIDTH + const MEAN_OUTER_LINK_DISTANCE : int = 15 * WIDTH const DIMMED_COLOR : Color = Color.GRAY const HIGHLIGHT_COLOR : Color = Color.DIM_GRAY diff --git a/DTContainer/DT&PTContainer.tscn b/DTContainer/DT&PTContainer.tscn index 2f93664..d816e0a 100644 --- a/DTContainer/DT&PTContainer.tscn +++ b/DTContainer/DT&PTContainer.tscn @@ -132,7 +132,7 @@ uppercase = true layout_mode = 2 [node name="OperatorContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer/OperatorPanel/OperatorOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 175) layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 @@ -160,7 +160,7 @@ uppercase = true layout_mode = 2 [node name="EnvContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/EnvOrganizer"] -custom_minimum_size = Vector2(200, 223) +custom_minimum_size = Vector2(200, 250) layout_mode = 2 size_flags_horizontal = 6 @@ -184,7 +184,7 @@ uppercase = true layout_mode = 2 [node name="SystemContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/SystemOrganizer"] -custom_minimum_size = Vector2(200, 223) +custom_minimum_size = Vector2(200, 250) layout_mode = 2 size_flags_horizontal = 6 @@ -211,7 +211,7 @@ uppercase = true layout_mode = 2 [node name="MachineContainer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/MachinePanel/MachineOrganizer"] -custom_minimum_size = Vector2(200, 200) +custom_minimum_size = Vector2(200, 175) layout_mode = 2 size_flags_horizontal = 6 @@ -221,7 +221,11 @@ layout_mode = 2 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_rl1kt") -[node name="SensorsLabel" type="Label" parent="PTContainer/DataTravelContainer/DataOutPanel"] +[node name="DataOutContainer" type="VBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel"] +custom_minimum_size = Vector2(200, 200) +layout_mode = 2 + +[node name="SensorsLabel" type="Label" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] z_index = 1 layout_mode = 2 size_flags_horizontal = 0 @@ -230,10 +234,6 @@ theme_override_colors/font_color = Color(0, 0, 0, 1) text = "Sensors" uppercase = true -[node name="DataOutContainer" type="VBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel"] -custom_minimum_size = Vector2(200, 200) -layout_mode = 2 - [node name="SensorsOrganizer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] custom_minimum_size = Vector2(0, 100) layout_mode = 2 diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 509bbd5..2371b6c 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -25,7 +25,7 @@ script = ExtResource("1_r7thu") [node name="DTContainer" parent="." instance=ExtResource("1_lklxr")] z_as_relative = false -offset_bottom = 503.0 +offset_bottom = 505.0 [node name="Camera2D" type="Camera2D" parent="."] position = Vector2(575, 326) @@ -100,6 +100,7 @@ icon_alignment = 1 script = ExtResource("6_tgjmw") [node name="HelpPopup" type="PopupPanel" parent="ControlLayer/HelpButton"] +oversampling_override = 1.0 title = "Help" position = Vector2i(18, 129) size = Vector2i(305, 455) From 1ad4b797c40c7be9689a065af7c5d5e83b31062f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 24 Jul 2025 22:25:18 -0400 Subject: [PATCH 06/40] Added IsPlanned look on nodes, as well as default border --- Config/LegendsConfig.gd | 10 ++++++++-- Config/StyleConfig.gd | 5 +++-- DTContainer/DT&PTContainer.gd | 2 ++ GenericDisplay/GenericDisplay.gd | 5 +++++ GenericDisplay/generic_display.tscn | 13 +++++++++---- Legends/Legends.gd | 5 +++-- Legends/Legends.tscn | 20 ++++++++++++-------- 7 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Config/LegendsConfig.gd b/Config/LegendsConfig.gd index a61b9e5..5232774 100644 --- a/Config/LegendsConfig.gd +++ b/Config/LegendsConfig.gd @@ -1,11 +1,17 @@ extends Node const LEGENDS : Dictionary = { - "Realtime relationship" : RT_LEGENDS + "Realtime Relationships" : RT_LEGENDS, + "Component Evolution" : BG_LEGENDS } const RT_LEGENDS : Dictionary = { "Slower than realtime" : StyleConfig.RTBorder.SLOWER_THAN_RT_COLOR, "Realtime" : StyleConfig.RTBorder.RT_COLOR, - "Faster than realtime" : StyleConfig.RTBorder.FASTER_THAN_RT_COLOR + "Faster than realtime" : StyleConfig.RTBorder.FASTER_THAN_RT_COLOR, +} + +const BG_LEGENDS : Dictionary = { + "Implemented": StyleConfig.DTElement.DIMMED_COLOR, + "Planned": StyleConfig.DTElement.PLANNED_COLOR } diff --git a/Config/StyleConfig.gd b/Config/StyleConfig.gd index 576a73c..100f092 100644 --- a/Config/StyleConfig.gd +++ b/Config/StyleConfig.gd @@ -13,6 +13,7 @@ class DTElement: const DIMMED_COLOR : Color = Color.GRAY const HIGHLIGHT_COLOR : Color = Color.DIM_GRAY const TEXT_HIGHLIGHT_COLOR : Color = Color.WHITE_SMOKE + const PLANNED_COLOR : Color = Color(Color.WHITE, 0.6) #Visual link style class Link: @@ -29,5 +30,5 @@ class Legends: const COME_IN_TEXT : String = "Show legend" const RETRACT_BUTTON_COLOR : Color = Color.DARK_ORANGE - const PANEL_COLOR : Color = Color.GRAY - const CATEGORY_ANNONCE_COLOR : Color = Color.DIM_GRAY + const PANEL_COLOR : Color = Color.DARK_GRAY + const CATEGORY_ANNONCE_COLOR : Color = Color(0.3, 0.3, 0.3) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index b080acb..ddcff51 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -221,6 +221,8 @@ func update_node_with(visual_container : HBoxContainer, fuseki_node_data : Dicti new_node.set_python_script_location(fuseki_node_data[key][PythonConfig.LOCATION_KEY][0]) if ("HasVisualization" in fuseki_node_data[key].keys() and fuseki_node_data[key]["HasVisualization"][0] == "true"): new_node.set_visualization() + if ("IsPlanned" in fuseki_node_data[key].keys() and fuseki_node_data[key]["IsPlanned"][0] == "true"): + new_node.set_is_planned() if ("desc" in fuseki_node_data[key].keys() and fuseki_node_data[key]["desc"][0] != ""): new_node.set_description(fuseki_node_data[key]["desc"][0]) visual_container.add_child(new_node) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 2b145d3..5a6d571 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -129,11 +129,16 @@ func set_visualization(): if last_loaded_pck_path.is_empty(): $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = true +# Enables a button showing DTComponent's description func set_description(description): $GenericDisplay/PresentationBox/DescriptionButton.show() $PopupDescription.title = $GenericDisplay/PresentationBox/GenericElementName.text + " description" $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description +# Changes the style of the node when the DTComponent is planned +func set_is_planned(): + self_modulate = StyleConfig.DTElement.PLANNED_COLOR + func set_info(new_data : Array[String], is_bool = false) -> void: data = to_float_array(new_data, is_bool) var node : Label = get_node("GenericDisplay/RealTimeContainer/GenericElementAttributes") diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index a50dc57..f868ec4 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -11,6 +11,11 @@ content_margin_top = 10.0 content_margin_right = 20.0 content_margin_bottom = 10.0 bg_color = Color(0.843137, 0.843137, 0.843137, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.270222, 0.270222, 0.270222, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 @@ -173,8 +178,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_right = 516.0 +offset_bottom = 516.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("2_5nhr6") @@ -248,8 +253,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_right = 516.0 +offset_bottom = 516.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_ipcor") diff --git a/Legends/Legends.gd b/Legends/Legends.gd index f2fe66f..b4a3c2b 100644 --- a/Legends/Legends.gd +++ b/Legends/Legends.gd @@ -18,9 +18,10 @@ func set_retract_button_color(color : Color): retract_button.add_theme_stylebox_override("normal", buttonStylebox) func set_panel_color(color : Color): - var panelStylebox : StyleBoxFlat = legends_panel.get_theme_stylebox("normal").duplicate() + var panelStylebox : StyleBoxFlat = legends_panel.get_theme_stylebox("panel").duplicate() panelStylebox.bg_color = color - legends_panel.add_theme_stylebox_override("normal", panelStylebox) + panelStylebox.border_color + legends_panel.add_theme_stylebox_override("panel", panelStylebox) func build_legends(legends : Dictionary): for categoryKey : String in legends.keys(): diff --git a/Legends/Legends.tscn b/Legends/Legends.tscn index d839787..b5c7e93 100644 --- a/Legends/Legends.tscn +++ b/Legends/Legends.tscn @@ -10,13 +10,17 @@ content_margin_bottom = 5.0 corner_radius_top_left = 10 corner_radius_top_right = 10 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8gdv1"] -content_margin_left = 5.0 -content_margin_top = 5.0 -content_margin_right = 5.0 -content_margin_bottom = 5.0 -corner_radius_bottom_right = 10 -corner_radius_bottom_left = 10 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6lkht"] +content_margin_left = 3.0 +content_margin_top = 3.0 +content_margin_right = 3.0 +content_margin_bottom = 3.0 +bg_color = Color(0.1, 0.1, 0.1, 0.6) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +corner_detail = 5 [node name="Legends" type="VBoxContainer"] anchors_preset = 3 @@ -42,7 +46,7 @@ text = "VVV" [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 2 -theme_override_styles/panel = SubResource("StyleBoxFlat_8gdv1") +theme_override_styles/panel = SubResource("StyleBoxFlat_6lkht") [node name="LegendsContainer" type="VBoxContainer" parent="PanelContainer"] layout_mode = 2 From e08ad17fe7ed360fbedc74a3d3ba8dbcc5be77a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 7 Aug 2025 18:04:26 -0400 Subject: [PATCH 07/40] Fixed container size --- DTContainer/DT&PTContainer.tscn | 1 + 1 file changed, 1 insertion(+) diff --git a/DTContainer/DT&PTContainer.tscn b/DTContainer/DT&PTContainer.tscn index d816e0a..85aafb8 100644 --- a/DTContainer/DT&PTContainer.tscn +++ b/DTContainer/DT&PTContainer.tscn @@ -167,6 +167,7 @@ size_flags_horizontal = 6 [node name="SystemPanel" type="PanelContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer"] z_index = -1 layout_mode = 2 +size_flags_horizontal = 3 mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_5tggx") From e835fca38ae0ed5f458498fedd0b9bfbe5d56387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 13 Aug 2025 17:53:59 -0400 Subject: [PATCH 08/40] Removed audio player --- Audio/drop_001.ogg | Bin 5859 -> 0 bytes Audio/drop_001.ogg.import | 19 ------------------- MainScene/MainScene.tscn | 2 -- MainScene/MainSceneController.gd | 5 ----- 4 files changed, 26 deletions(-) delete mode 100644 Audio/drop_001.ogg delete mode 100644 Audio/drop_001.ogg.import diff --git a/Audio/drop_001.ogg b/Audio/drop_001.ogg deleted file mode 100644 index 091750a730dafb33f7aefd781af78ea9633b08d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5859 zcmeG=c~q0f_7fIK5RoOUA}2wR1cQ%2uwcQ4AVR_>As|FiS%L%*WL2t`h=5_OhCneO zg+LI(QcywBs#FD3WEa8uS`Y=QR7I^~U4Qc>Xnp>AUf(%A@4q)^GP!f_ow+l2nYr_M zZ`EkK?%cUD5dkd|5|hHXu`5Gkw-Zsy@?{Ze&i*KXh7*%La2>qT93@N#042a9VDc-c=#jnl+w{%z z7X>OqodT*GJ_E}{nh-udQy(@5jN&ij zkCw)(TGW)Msajvg?f0<0S5@G(`d&?C{_6XL6aBP>mL~=o9^_&F(8jY1g7zfz+E16x zHyGJIaBwZO1qNXnG&C|jpIBWS52uoE3kP^`o51xDmfno*ZZ_%}F!g>y^3k>(uyOHa zdHTVU6tp3kGm@M%lDs#tIAi0fy@97PHjZWl&1D4ZXU^9bd&lOGD%&~^1 zO$@~o_F~2K%z6b7hNuLG5ttAKmGtWH$VP5SYeZe^_U>b(?qg~)7U(fb$Z9}2S5*D~ zYItA~=dUX@;<5#xLS1&n8Fj^(x-d<<;z=qp4POS(rc@@WBc9@#KR% z&u<=!-x~ojI{;L&QAeE76{roSYWC`kdHekQmMTe0LKRQi>7MYun@`;mYBD+ zvE=j3yG8Bh(%v8JZ^IE;KbAzbVS*s^dsyki9t?u})P1PXZo#Aaok!Aed-E5BVDZ)X z0^`K5)eFkWmx)!zdHL%YwfomI4a_BpX|kQ;07%!6dGY&3=E`y}t`cNhTqPX8X5CFf zn)-{qt7n=nYB;Nrp%;^Ep%>@1OqC@x+Pg(bT5hmcNQ6uVQ6i5hXjEL-L8k=!v*T&J0<+mYud*C-ow@zjRjV6ta>>V4) z+oG5GsbhVJ8~}qRT^5tVkX&+C>3%O7MsDEukrRjQI%IU^kg5AIQ_n$C&l8IGQ_6t0 zix0)qS0`vdCuvBRA!1^mf5%Or z^he~B6Y?kQ@|V#Crh$UxDMj0|M74*@?$wG_KZ%@(?4!lmNBP;){Cq-I5jRUDZ4*ad zZJas%m-R#BxW=2p3M0oQ-t?o$X(t;w!)Q82@_Z`aqvHTnXrhbW#|8kP{h*q=>>Tl7 zS`IU9hnbdsEXRKzF_1b;@f@N+#TEi!0Kna<>g!*sYlR%N&vJ@D(=QoyY+8MjS4*W(4fKqsCS`^JQa=H z6=CEdg+2s4v8Wzx%$h&{9g@PzPO0dpP0|gDRPy>{CSx`#5jSH0m zZ<4=X{zR6KEz0{NSyYNAmF4;OCH-%P|7qYaH2_^4r4E0(vD&vHVCkPPjt2k_r2=?B`zrOrnkwD20I(B;1;YHoO~63` z9w)F&1pomj#rV&s`OmSR2t@G+02~kp=%(xqzXt2w*~@F#a)I}EDB*(KvuVe zVy3G?poEzL4P`LC1KT}F@wsbPB;krL$C2EJ)*j-(Bq+;-0zu8P>6B_r-&446LbjNz z0IwIQc4b8Et0EXA_4N^#831Pr+HczxO{Rzm*)V^SUX3GlKV9pC_4M@%`sPkR(R7xW zAgY$0UI_*D$*w4v+(n2!pedeMcnZVQI7m-4m4(b8l{PMfoI?~($T^}cju2JXO5sgH zl`#${1?lk-oD|+J3J8;5&`2SN$A#lgH`26?J9K?^U@ zhv`i%HWbNmgaR_mlf4*MiXqNi5(fE73z(}RP8}v3iIAmA$fW&JngH{UBaj6JsB&#* zM|)Xmv1IDj0qx_6xysC;y6J-IlrnRew?+!82|31B#yx;c_8^Ki)6@O=y%I)zIP( z^m{WDse$QvHua`Qbn+MI86MLDXbiNv6QBHxsZhXR=+JWQ31)>w?Cb^_pNy7NU1vv= zb^Gj9DyoF+Y+~yKG!VqWt*|#O#7l(i9LrYyl4v}%S`@^E+Avxm4sIoi#At)K<_`U+ zJQqUAMuZyGU`w1#aBm)Fht{_&ugn!7xcZhGCPJolC)O82qI9dyIsMRj@-(|Z>%t3T zUrWdw1$|*zSYB->8#)e#lbdHbEKKNC22@8oKsD2MGB@zk4X(uK8S`X@X@hS_=z5)q zharB&tkO?_ZiY)oUSd^f8Nmsrm(XSgq3;P5=>q0eI07L+ab}Y1@(~0i6K2$YAV`El zrfe))mLkKaD3YmaLEjZJ;kXPaL!LWBg{VRqG6DlWS+O*XoFEiHKFK@*3WD>`6QCeC zjeGX|j&cQidr8ADCp@|D>Gjc5? z#RWyQ?qa4|$O>{|F4@(7l11y%heviA0E(K{+Gs3w-##^9M|C@>08|&MExqVMrLM$R zwwLeluroih3Vuf5U(?9qci_ksh6IahMP7$;<{oglf7I>FDc(xOP5m7~Dl{}rBQ3y) zio)30-Oe=sc{Y7XX}K@!J{8IVz!;3f(gwaTzV-df3w123mtCPH8@B9vreJCXC&1z`u7)~WO#BagXG=r9lwrKf2C?^SlQ%g+T@U7@LHHGLyG6;Ins`w zZ!{VD=QQM<)!9CF<>=T%_4xVovEh{?cZOlzaL{%eE2XOQ0tUH8Z(517i!Un!RWEi?puxp8Rm$6 z&DjGL&P|<$6siAyX^U>Ne`L?L?g;FF>bj=wKb+abJ)WgoOlkAB3?#KpJ=$d=)v3#l zaPjfk)3Wl+y3Kkg->z5DUKp;069A~hSt zku2r@NY+xZ>blD8w15_iG=ElByf_#&ISdn>3GX?%aO<)$=@;W%?S; zhi;BU;6TBCgP_0pFQ`*+@J{YkPh=#lA`n|0rQk^1(veZtviXK|m| zeD?H8rt&%Sug{OIF+P{k@F3^>Ki>Y_GyB~(=p}3}Yy_!i4A@y%g|!O&buAljUUE-< zQSy9E##JJ5ut3^w?W6|Ig};6lx#VFPD{uVdAK;uBX!kIi8ix))P9=4E{5sx#O-&(O zW_S~(7Ri~Qj51T>*k(sAx`Auu2e8&}XANYJ@LGr;LBnrDP^*N==xQyj~2^AIeAy< zmefO)NrxqZ4R^nPaAvw~b8BSbp^UX)RMd1=^5n|!!@-7gGZW8mjQtd3_9SWd?vmrR zEvGk*EuBs?UvkV_v_$u}S8vwbdQUWX|{dv zAUZmFc)=32nIixC0JhGGfQ}1IES Date: Thu, 11 Sep 2025 14:30:58 -0400 Subject: [PATCH 09/40] Removed outdated code --- MainScene/MainSceneController.gd | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index 2f536b5..44df942 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -44,21 +44,3 @@ func _check_for_data(): func _update_fuseki_data(): dt_container.feed_fuseki_data(fuseki_data) - -func load_yaml_from_dump(): - var http = HTTPRequest.new() - add_child(http) - http.request_completed.connect(self._on_HTTPRequest_yaml_completed) - # TODO: Fix this hardcoded URL - var error = http.request("https://oakeslab-polymtl.github.io/DTDF/data_dump.yaml") - #var error = http.request("http://localhost:1313/DTDF/data_dump.yaml") - if error != OK: - push_error("An error occurred in the HTTP request for YAML data.") - -func _on_HTTPRequest_yaml_completed(result, response_code, headers, body): - if response_code == 200: - var yaml_content = body.get_string_from_utf8() - fuseki_dumper.load_from_dump(fuseki_data, yaml_content) - print("YAML data loaded successfully from HTTP request") - else: - push_error("Failed to load YAML data. Response code: " + str(response_code)) From 6d8abb318e69cf1f3a0274e4a22d95a0a549d279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Tue, 16 Sep 2025 21:30:04 +0200 Subject: [PATCH 10/40] Add YouTube overview section to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 250a549..992e0fd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ The purpose of this project is to provide users with a graphical and interactive image +## YouTube Overview + +[![DTInsight video](https://img.youtube.com/vi/CD0pdK-eGXY/0.jpg)](https://www.youtube.com/watch?v=CD0pdK-eGXY) + # About this project This tool was built with the aim of being cross compatible with any digital twin ontology built with the Ontology Modeling Lanuage (OML) vocabulary available at "https://github.com/oakeslabmtl/DTDF". To build such ontologies we recommend tools such as openCAESAR Rosetta available at "https://github.com/opencaesar/oml-rosetta". From c2e9dff5c41528d528e1a21fb69ad180e153bc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 17 Sep 2025 20:28:47 -0400 Subject: [PATCH 11/40] Migration to Godot 4.5 --- DigitalTwinVisualisationProject.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DigitalTwinVisualisationProject.csproj b/DigitalTwinVisualisationProject.csproj index f2696a7..bded035 100644 --- a/DigitalTwinVisualisationProject.csproj +++ b/DigitalTwinVisualisationProject.csproj @@ -1,4 +1,4 @@ - + net8.0 true From dad6529cb5598501744a7a9f935ed45bb793055a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 17 Sep 2025 20:30:20 -0400 Subject: [PATCH 12/40] New functionality: change node background and border color for contextual DT views The legend is now dynamically integrated to the new view panel --- Config/LegendsConfig.gd | 6 +-- Config/StyleConfig.gd | 26 ++++++--- DTContainer/DT&PTContainer.gd | 62 ++++++++++++++++------ GenericDisplay/GenericDisplay.gd | 58 ++++++++++---------- Legends/Legends.gd | 29 +++++++--- Legends/Legends.tscn | 49 ++--------------- MainScene/MainScene.tscn | 91 ++++++++++++++++++++++++++++++-- 7 files changed, 210 insertions(+), 111 deletions(-) diff --git a/Config/LegendsConfig.gd b/Config/LegendsConfig.gd index 5232774..1fbc87a 100644 --- a/Config/LegendsConfig.gd +++ b/Config/LegendsConfig.gd @@ -6,9 +6,9 @@ const LEGENDS : Dictionary = { } const RT_LEGENDS : Dictionary = { - "Slower than realtime" : StyleConfig.RTBorder.SLOWER_THAN_RT_COLOR, - "Realtime" : StyleConfig.RTBorder.RT_COLOR, - "Faster than realtime" : StyleConfig.RTBorder.FASTER_THAN_RT_COLOR, + "Slower than realtime" : StyleConfig.Timescale.SLOWER_THAN_REALTIME, + "Realtime" : StyleConfig.Timescale.REALTIME, + "Faster than realtime" : StyleConfig.Timescale.FASTER_THAN_REALTIME, } const BG_LEGENDS : Dictionary = { diff --git a/Config/StyleConfig.gd b/Config/StyleConfig.gd index 100f092..4864ece 100644 --- a/Config/StyleConfig.gd +++ b/Config/StyleConfig.gd @@ -1,15 +1,29 @@ extends Node -#Realtime relationship style -class RTBorder: - const BORDER_WIDTH : int = 5 +# Deployment relationship style +class Deployment: + const ON_DEVICE : Color = Color(0.2, 0.6, 0.2) + const EDGE : Color = Color(0.2, 0.4, 0.8) + const FOG : Color = Color.LIGHT_STEEL_BLUE + const CLOUD : Color = Color.AQUA + +# Implementation relationship style +class Implementation: + const PLANNED : Color = Color.YELLOW + const ACTIVE : Color = Color.ORANGE + const IMPLEMENTED : Color = Color.GREEN - const SLOWER_THAN_RT_COLOR : Color = Color.BLUE_VIOLET - const RT_COLOR : Color = Color.AQUAMARINE - const FASTER_THAN_RT_COLOR : Color = Color.GOLD +# Timescale relationship style +class Timescale: + const SLOWER_THAN_REALTIME : Color = Color.BLUE_VIOLET + const REALTIME : Color = Color.AQUAMARINE + const FASTER_THAN_REALTIME : Color = Color.GOLD #DT/RT displayed element style class DTElement: + const BORDER_WIDTH : int = 5 + const BORDER_COLOR : Color = Color("454545") + const DIMMED_COLOR : Color = Color.GRAY const HIGHLIGHT_COLOR : Color = Color.DIM_GRAY const TEXT_HIGHLIGHT_COLOR : Color = Color.WHITE_SMOKE diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index ddcff51..c031e3a 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -26,6 +26,10 @@ var highlighted_element = null var all_highlighted_element = [] var highlighted_with_click = false +# Border and background views +var bg_view = 0 +var border_view = 0 + #initialization func _ready(): GenericDisplaySignals.generic_display_over.connect(_on_element_over) @@ -42,23 +46,44 @@ class NamedNode: var displayed_node_list: Array[NamedNode] #Attributes -const border_attribute : String = "hasTimeScale" +const timescale_key : String = "hasTimeScale" +const implementation_key : String = "hasImplementation" const type_attribute : String = "type" #Operator/machine -const opperator_type : String = "Insight" +const operator_type : String = "Insight" #Set the starting start of a node when loaded func set_starting_node_style(namedNode : NamedNode, attributes : Dictionary): - namedNode.node.set_dimmed_style() - if (border_attribute in attributes.keys()): - match attributes[border_attribute]: + # Structural style + if (PythonConfig.LOCATION_KEY in attributes.keys()): + namedNode.node.set_python_script_location(attributes[PythonConfig.LOCATION_KEY][0]) + if ("HasVisualization" in attributes.keys() and attributes["HasVisualization"][0] == "true"): + namedNode.node.set_visualization() + if ("desc" in attributes.keys() and attributes["desc"][0] != ""): + namedNode.node.set_description(attributes["desc"][0]) + # Visual style + namedNode.node.set_default_style() + if (implementation_key in attributes.keys()): + var is_border = border_view == 2 + var is_bg = bg_view == 2 + match attributes[implementation_key]: + ["planned"]: + namedNode.node.set_node_style(StyleConfig.Implementation.PLANNED, is_bg, is_border) + ["active"]: + namedNode.node.set_node_style(StyleConfig.Implementation.ACTIVE, is_bg, is_border) + ["implemented"]: + namedNode.node.set_node_style(StyleConfig.Implementation.IMPLEMENTED, is_bg, is_border) + if (timescale_key in attributes.keys()): + var is_border = border_view == 3 + var is_bg = bg_view == 3 + match attributes[timescale_key]: ["slower_trt"]: - namedNode.node.set_slower_style() + namedNode.node.set_node_style(StyleConfig.Timescale.SLOWER_THAN_REALTIME, is_bg, is_border) ["rt"]: - namedNode.node.set_rt_style() + namedNode.node.set_node_style(StyleConfig.Timescale.REALTIME, is_bg, is_border) ["faster_trt"]: - namedNode.node.set_faster_style() + namedNode.node.set_node_style(StyleConfig.Timescale.FASTER_THAN_REALTIME, is_bg, is_border) #Set highlighted element on signal func _on_element_over(element_name, click: bool): @@ -204,7 +229,7 @@ func update_provided_things(operator_container : HBoxContainer, machine_containe var machine_data : Dictionary = {} for entry_name in provided_data.keys(): if (type_attribute in provided_data[entry_name].keys()): - if (opperator_type in provided_data[entry_name][type_attribute]): + if (operator_type in provided_data[entry_name][type_attribute]): operator_data[entry_name] = provided_data[entry_name] else: machine_data[entry_name] = provided_data[entry_name] @@ -217,14 +242,6 @@ func update_node_with(visual_container : HBoxContainer, fuseki_node_data : Dicti for key in fuseki_node_data.keys(): var new_node = GenericDisplay.instantiate() new_node.set_text(key) - if (PythonConfig.LOCATION_KEY in fuseki_node_data[key].keys()): - new_node.set_python_script_location(fuseki_node_data[key][PythonConfig.LOCATION_KEY][0]) - if ("HasVisualization" in fuseki_node_data[key].keys() and fuseki_node_data[key]["HasVisualization"][0] == "true"): - new_node.set_visualization() - if ("IsPlanned" in fuseki_node_data[key].keys() and fuseki_node_data[key]["IsPlanned"][0] == "true"): - new_node.set_is_planned() - if ("desc" in fuseki_node_data[key].keys() and fuseki_node_data[key]["desc"][0] != ""): - new_node.set_description(fuseki_node_data[key]["desc"][0]) visual_container.add_child(new_node) var displayed_element = NamedNode.new() displayed_element.name = key @@ -343,6 +360,8 @@ func feed_fuseki_data(feed): #Update all displayed information on data update from a signal from FusekiCallerButton func on_fuseki_data_updated(): + if fuseki_data == null: + return displayed_node_list.clear() update_node_with(service_container, fuseki_data.service) update_node_with(enabler_container, fuseki_data.enabler) @@ -386,3 +405,12 @@ func _on_rabbit_data_updated(container_name: String, data : Array[String]): var last_data = data[data.size() - 1] var is_bool : bool = last_data == "true" or last_data == "false" container.set_info(data, is_bool) + + +func _on_node_border_option_item_selected(index: int) -> void: + border_view = index + on_fuseki_data_updated() + +func _on_node_bg_option_item_selected(index: int) -> void: + bg_view = index + on_fuseki_data_updated() diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 5a6d571..32c0f7c 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -18,6 +18,8 @@ var script_file_path : String = "" var absolute_path : String = "" var data : Array = [] var highlightable : bool = true +var bg_color : Color = StyleConfig.DTElement.DIMMED_COLOR +var border_color : Color = StyleConfig.DTElement.BORDER_COLOR var last_loaded_pck_path: String = "" @@ -32,7 +34,7 @@ func _on_display_highlight(highlighted_elements_names : Array): if (element.text in highlighted_elements_names): set_highlight_style() else: - set_dimmed_style() + set_default_style() func _on_mouse_entered(): GenericDisplaySignals.generic_display_over.emit(element.text, false) @@ -135,10 +137,6 @@ func set_description(description): $PopupDescription.title = $GenericDisplay/PresentationBox/GenericElementName.text + " description" $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description -# Changes the style of the node when the DTComponent is planned -func set_is_planned(): - self_modulate = StyleConfig.DTElement.PLANNED_COLOR - func set_info(new_data : Array[String], is_bool = false) -> void: data = to_float_array(new_data, is_bool) var node : Label = get_node("GenericDisplay/RealTimeContainer/GenericElementAttributes") @@ -149,7 +147,6 @@ func set_info(new_data : Array[String], is_bool = false) -> void: info = "on" else: info = "off" - else: info = format_float(last_data, 4) node.text = "Real time info : \n" + info @@ -181,7 +178,6 @@ func format_float(f: float, sig_digits: int = 4) -> String: return result - func update_chart(last_data) -> void: if pop_up_chart.visible == false : return @@ -190,36 +186,38 @@ func update_chart(last_data) -> void: else : chart.add_value(last_data) -#Background style -------------------------------------------------------------- -func set_dimmed_style(): - set_bg_color(StyleConfig.DTElement.DIMMED_COLOR) - set_text_color(Color.BLACK) +# Style -------------------------------------------------------------- +## Highlights +func set_default_style(): + change_bg_color(bg_color) + change_border_color(border_color) + change_text_color(Color.BLACK) func set_highlight_style(): - set_bg_color(StyleConfig.DTElement.HIGHLIGHT_COLOR) - set_text_color(StyleConfig.DTElement.TEXT_HIGHLIGHT_COLOR) + change_bg_color(StyleConfig.DTElement.HIGHLIGHT_COLOR) + change_text_color(StyleConfig.DTElement.TEXT_HIGHLIGHT_COLOR) -func set_bg_color(color : Color): +## Changing styles +func change_bg_color(color : Color): var styleBox : StyleBoxFlat = get_theme_stylebox("panel").duplicate() styleBox.bg_color = color add_theme_stylebox_override("panel", styleBox) -func set_text_color(color: Color): - element.set("theme_override_colors/font_color", color) - realtime_element.set("theme_override_colors/font_color", color) - -#Border style ------------------------------------------------------------------ -func set_slower_style(): - set_border_color(StyleConfig.RTBorder.SLOWER_THAN_RT_COLOR) - -func set_rt_style(): - set_border_color(StyleConfig.RTBorder.RT_COLOR) - -func set_faster_style(): - set_border_color(StyleConfig.RTBorder.FASTER_THAN_RT_COLOR) - -func set_border_color(color : Color): +func change_border_color(color : Color): var styleBox : StyleBoxFlat = get_theme_stylebox("panel").duplicate() styleBox.border_color = color - styleBox.set_border_width_all(StyleConfig.RTBorder.BORDER_WIDTH) + styleBox.set_border_width_all(StyleConfig.DTElement.BORDER_WIDTH) add_theme_stylebox_override("panel", styleBox) + +func change_text_color(color: Color): + element.set("theme_override_colors/font_color", color) + realtime_element.set("theme_override_colors/font_color", color) + +## Style definitions +func set_node_style(_color: Color, is_bg: bool, is_border: bool): + if is_bg: + bg_color = _color + change_bg_color(bg_color) + if is_border: + border_color = _color + change_border_color(border_color) diff --git a/Legends/Legends.gd b/Legends/Legends.gd index b4a3c2b..250371f 100644 --- a/Legends/Legends.gd +++ b/Legends/Legends.gd @@ -7,15 +7,7 @@ extends Container const LegendElement = preload("res://Legends/LegendElement.tscn") func _ready() -> void: - set_retract_button_color(StyleConfig.Legends.RETRACT_BUTTON_COLOR) set_panel_color(StyleConfig.Legends.PANEL_COLOR) - build_legends(LegendsConfig.LEGENDS) - retract_button.text = StyleConfig.Legends.GO_AWAY_TEXT - -func set_retract_button_color(color : Color): - var buttonStylebox : StyleBoxFlat = retract_button.get_theme_stylebox("normal").duplicate() - buttonStylebox.bg_color = color - retract_button.add_theme_stylebox_override("normal", buttonStylebox) func set_panel_color(color : Color): var panelStylebox : StyleBoxFlat = legends_panel.get_theme_stylebox("panel").duplicate() @@ -56,3 +48,24 @@ func hide_legends(): retract_button.text = StyleConfig.Legends.COME_IN_TEXT legends_hidden = !legends_hidden position += Vector2(0, translate_distance) + + +func _on_node_style_option_item_selected(index: int) -> void: + for child in legends_container.get_children(): + child.queue_free() + match index: + 1: + var deployment = StyleConfig.Deployment.new() + var deployment_map = deployment.get_script().get_script_constant_map() + for key in deployment_map: + instanciate_legend_element(key, deployment_map[key]) + 2: + var implementation = StyleConfig.Implementation.new() + var implementation_map = implementation.get_script().get_script_constant_map() + for key in implementation_map: + instanciate_legend_element(key, implementation_map[key]) + 3: + var timescale = StyleConfig.Timescale.new() + var timescale_map = timescale.get_script().get_script_constant_map() + for key in timescale_map: + instanciate_legend_element(key, timescale_map[key]) diff --git a/Legends/Legends.tscn b/Legends/Legends.tscn index b5c7e93..ef44b22 100644 --- a/Legends/Legends.tscn +++ b/Legends/Legends.tscn @@ -1,54 +1,15 @@ -[gd_scene load_steps=4 format=3 uid="uid://cwd5j8qr66eh3"] +[gd_scene load_steps=2 format=3 uid="uid://cf7nrorpbpbjb"] -[ext_resource type="Script" uid="uid://boaf20u185t3g" path="res://Legends/Legends.gd" id="1_rrdvj"] - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2h335"] -content_margin_left = 10.0 -content_margin_top = 5.0 -content_margin_right = 10.0 -content_margin_bottom = 5.0 -corner_radius_top_left = 10 -corner_radius_top_right = 10 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6lkht"] -content_margin_left = 3.0 -content_margin_top = 3.0 -content_margin_right = 3.0 -content_margin_bottom = 3.0 -bg_color = Color(0.1, 0.1, 0.1, 0.6) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -corner_detail = 5 +[ext_resource type="Script" uid="uid://boaf20u185t3g" path="res://Legends/Legends.gd" id="1_6lkht"] [node name="Legends" type="VBoxContainer"] -anchors_preset = 3 -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = -71.0 -offset_top = -63.0 -offset_right = -20.0 -offset_bottom = -20.0 -grow_horizontal = 0 -grow_vertical = 0 +offset_right = 6.0 +offset_bottom = 6.0 theme_override_constants/separation = 0 -script = ExtResource("1_rrdvj") - -[node name="RetractButton" type="Button" parent="."] -layout_mode = 2 -size_flags_vertical = 4 -theme_override_colors/font_color = Color(0, 0, 0, 1) -theme_override_styles/normal = SubResource("StyleBoxFlat_2h335") -text = "VVV" +script = ExtResource("1_6lkht") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 2 -theme_override_styles/panel = SubResource("StyleBoxFlat_6lkht") [node name="LegendsContainer" type="VBoxContainer" parent="PanelContainer"] layout_mode = 2 - -[connection signal="pressed" from="RetractButton" to="." method="_on_retract_button_pressed"] diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index ad6e103..6bf53f2 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -5,7 +5,6 @@ [ext_resource type="Script" uid="uid://chsxi3pvlcut1" path="res://Camera/CameraControl.gd" id="3_5mxu4"] [ext_resource type="Script" uid="uid://cwl13x3trg1o3" path="res://ControlPanel/ControlPanelSuperviser.gd" id="4_xqnle"] [ext_resource type="PackedScene" uid="uid://bx42q4hl46td1" path="res://Fuseki/Data/FusekiData.tscn" id="5_4e51a"] -[ext_resource type="PackedScene" uid="uid://cwd5j8qr66eh3" path="res://Legends/Legends.tscn" id="5_8cnmr"] [ext_resource type="Theme" uid="uid://xukk6lvgcpaa" path="res://main_theme.tres" id="5_77ryg"] [ext_resource type="Script" uid="uid://wfoj63h06hpx" path="res://MainScene/help_button.gd" id="6_tgjmw"] [ext_resource type="Script" uid="uid://b0mon1bk14hp6" path="res://MainScene/settings_button.gd" id="7_gs64f"] @@ -13,6 +12,7 @@ [ext_resource type="PackedScene" uid="uid://cowug1mbtod0e" path="res://Fuseki/Caller/FusekiCallerBtton.tscn" id="8_ped26"] [ext_resource type="Script" uid="uid://cfkgtkfhl06v5" path="res://RabbitMQ/Control/RabbitButton.gd" id="9_qda1k"] [ext_resource type="Texture2D" uid="uid://doyxcp824t4vw" path="res://gear.png" id="12_77ryg"] +[ext_resource type="PackedScene" uid="uid://cf7nrorpbpbjb" path="res://Legends/Legends.tscn" id="12_80vrr"] [ext_resource type="Script" uid="uid://0b3cap3tphwi" path="res://MainScene/CICDServer.gd" id="15_gmiik"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ffs3u"] @@ -83,8 +83,6 @@ disabled = true text = "Record data form RabbitMQ :" script = ExtResource("9_qda1k") -[node name="Legends" parent="ControlLayer" instance=ExtResource("5_8cnmr")] - [node name="HelpButton" type="Button" parent="ControlLayer"] anchors_preset = 2 anchor_top = 1.0 @@ -179,6 +177,89 @@ anchor_right = 0.5 anchor_bottom = 0.5 IS_IN_GAME_MENU = false +[node name="ViewPanel" type="PanelContainer" parent="ControlLayer"] +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -373.0 +offset_top = -125.0 +offset_right = -18.0 +offset_bottom = -18.0 +grow_horizontal = 0 +grow_vertical = 0 + +[node name="MarginContainer" type="MarginContainer" parent="ControlLayer/ViewPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer"] +layout_mode = 2 + +[node name="ViewLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "dt component views" +horizontal_alignment = 1 +uppercase = true + +[node name="HSeparator" type="HSeparator" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 + +[node name="NodeBGLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +text = "Background Color" + +[node name="NodeBGOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +selected = 0 +allow_reselect = true +item_count = 4 +popup/item_0/text = "None" +popup/item_0/id = 0 +popup/item_1/text = "Deployment" +popup/item_1/id = 1 +popup/item_2/text = "Implementation" +popup/item_2/id = 2 +popup/item_3/text = "Timescale" +popup/item_3/id = 3 + +[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer" instance=ExtResource("12_80vrr")] +layout_mode = 2 + +[node name="VBoxContainer2" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 + +[node name="NodeBorderLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] +layout_mode = 2 +text = "Border Color" + +[node name="NodeBorderOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] +layout_mode = 2 +selected = 0 +allow_reselect = true +item_count = 4 +popup/item_0/text = "None" +popup/item_0/id = 0 +popup/item_1/text = "Deployment" +popup/item_1/id = 1 +popup/item_2/text = "Implementation" +popup/item_2/id = 2 +popup/item_3/text = "Timescale" +popup/item_3/id = 3 + +[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2" instance=ExtResource("12_80vrr")] +layout_mode = 2 + [node name="FusekiData" parent="." instance=ExtResource("5_4e51a")] [node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container", "to_hide")] @@ -190,3 +271,7 @@ to_hide = [NodePath("../ControlLayer")] [connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] [connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] [connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/SettingsButton" method="_on_settings_settings_menu_closed"] +[connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="DTContainer" method="_on_node_bg_option_item_selected"] +[connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Legends" method="_on_node_style_option_item_selected"] +[connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="DTContainer" method="_on_node_border_option_item_selected"] +[connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Legends" method="_on_node_style_option_item_selected"] From 3c9f3f8da2700f555f1679a97c88b13afda51381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 18 Sep 2025 17:03:29 -0400 Subject: [PATCH 13/40] Don't hide legend on the web --- MainScene/MainSceneController.gd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index 44df942..8eb3a5e 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -12,7 +12,9 @@ func _ready(): if OS.has_feature("web"): print("Detected running on the web") - $ControlLayer.hide() + #$ControlLayer.hide() + $ControlLayer/ControlPanel.hide() + $ControlLayer/SettingsButton.hide() # Start checking for data after a short delay get_tree().create_timer(1.0).timeout.connect(_check_for_data) else: From 7913ce00f892b373e7b939df797e5cf5c488530e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 13:22:37 -0400 Subject: [PATCH 14/40] Simplified code --- Fuseki/Data/FusekiData.gd | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Fuseki/Data/FusekiData.gd b/Fuseki/Data/FusekiData.gd index 9d908cb..58719ee 100644 --- a/Fuseki/Data/FusekiData.gd +++ b/Fuseki/Data/FusekiData.gd @@ -122,8 +122,9 @@ static func parse_fuseki_json(json, is_link = false): new_json_value.json_value = parse_fuseki_value(result[head]["value"]) tuple_aggregator.append(new_json_value) result_aggregator.append(tuple_aggregator) - var formated_result = format_result(result_aggregator, is_link) - return formated_result + #Transform the result agregator in an array of GenericLinkedNodes or + #in a dictionary depending on the data type + return parse_link_result(result_aggregator) if (is_link) else parse_element_result(result_aggregator) #Remove oml link data from values static func parse_fuseki_value(value) -> String: @@ -131,11 +132,6 @@ static func parse_fuseki_value(value) -> String: return value.split("#")[1] return value -#Transform the result agregator in an array of GenericLinkedNodes or -#in a dictionary depending on the data type -static func format_result(result_aggregator, is_link = false): - return parse_link_result(result_aggregator) if (is_link) else parse_element_result(result_aggregator) - #Transform the result agregator in an Array of GenericLinkedNodes static func parse_link_result(result_agregator) -> Array[GenericLinkedNodes]: var formated_result : Array[GenericLinkedNodes] = [] From 9317757b42534034f6ea3a2c1ac8e2a145d2fae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 15:00:48 -0400 Subject: [PATCH 15/40] Better comments --- MainScene/MainSceneController.gd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index 8eb3a5e..cbd4196 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -8,18 +8,22 @@ extends Node func _ready(): fuseki_caller.set_fuseki_data_manager(fuseki_data) fuseki_dumper.set_fuseki_data_manager(fuseki_data) + dt_container.set_fuseki_data(fuseki_data) FusekiSignals.fuseki_data_updated.connect(_update_fuseki_data) if OS.has_feature("web"): print("Detected running on the web") - #$ControlLayer.hide() + # Hide UI $ControlLayer/ControlPanel.hide() $ControlLayer/SettingsButton.hide() + # Start checking for data after a short delay get_tree().create_timer(1.0).timeout.connect(_check_for_data) else: + # Use RabbitMQ if not on the web RabbitMq.set_fuseki_data_manager(fuseki_data) +# Checks for YAML data from the web page it is embedded in func _check_for_data(): print("🔍 Checking for YAML data...") @@ -41,7 +45,6 @@ func _check_for_data(): fuseki_dumper.load_from_dump(fuseki_data, result) else: print("⏳ No data yet, will try again...") - # Try again in 0.5 seconds get_tree().create_timer(0.5).timeout.connect(_check_for_data) func _update_fuseki_data(): From 013ba768b11164bf777f9b61612c53aeea432a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 15:01:00 -0400 Subject: [PATCH 16/40] Editable description prototype --- GenericDisplay/generic_display.tscn | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index f868ec4..35870a3 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -218,8 +218,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 396.0 -offset_bottom = 196.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 @@ -231,10 +231,11 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="Description" type="RichTextLabel" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +[node name="Description" type="TextEdit" parent="PopupDescription/DescriptionControl/DescriptionContainer"] layout_mode = 2 size_flags_vertical = 3 text = "No description" +wrap_mode = 1 [node name="PopupScript" type="PopupPanel" parent="."] disable_3d = true From 489ac6e4b831b74adfe5ee3cac1fa7c8c1406a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 15:07:33 -0400 Subject: [PATCH 17/40] Lock camera when editing description --- GenericDisplay/GenericDisplay.gd | 6 ++++++ GenericDisplay/generic_display.tscn | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 32c0f7c..bcf51a1 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -79,7 +79,13 @@ func _on_file_selected(pck_path: String) -> void: $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = false func _on_description_button_pressed() -> void: + CameraSignals.disable_camera_movement.emit() + CameraSignals.disable_camera_zoom.emit() $PopupDescription.popup() + +func _on_popup_description_popup_hide() -> void: + CameraSignals.enable_camera_movement.emit() + CameraSignals.enable_camera_zoom.emit() func _on_viz_pop_up_button_pressed(): var pop_up_panel = $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton/PopupPanel diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index 35870a3..f393cec 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -218,8 +218,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_right = 396.0 +offset_bottom = 196.0 grow_horizontal = 2 grow_vertical = 2 @@ -281,3 +281,4 @@ text = "Select the DT script folder in the settings first" [connection signal="pressed" from="GenericDisplay/RealTimeContainer/PopUpButton" to="." method="_on_pop_up_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPickerButton" to="." method="_on_viz_picker_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton" to="." method="_on_viz_pop_up_button_pressed"] +[connection signal="popup_hide" from="PopupDescription" to="." method="_on_popup_description_popup_hide"] From eaecef70e1cd9dbbc0181140de1f365944856d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 15:57:35 -0400 Subject: [PATCH 18/40] Adding nodes to containers --- DTContainer/DT&PTContainer.gd | 62 ++++++++ DTContainer/DT&PTContainer.tscn | 64 ++++++++- DTContainer/add_component_button.tscn | 8 ++ MainScene/MainScene.tscn | 197 ++++++++++++++------------ 4 files changed, 240 insertions(+), 91 deletions(-) create mode 100644 DTContainer/add_component_button.tscn diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index c031e3a..956d269 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -35,6 +35,9 @@ func _ready(): GenericDisplaySignals.generic_display_over.connect(_on_element_over) RabbitSignals.updated_data.connect(_on_rabbit_data_updated) +func set_fuseki_data(_fuseki_data: FusekiData) -> void: + fuseki_data = _fuseki_data + #Node and element manipulation functions ----------------------------------------------------------- #Displays node referenced by its name @@ -414,3 +417,62 @@ func _on_node_border_option_item_selected(index: int) -> void: func _on_node_bg_option_item_selected(index: int) -> void: bg_view = index on_fuseki_data_updated() + +## Adding nodes to containers + +var new_node_index = 1 +var new_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": ["New description here"] } + +func _on_add_component_button_pressed_env() -> void: + fuseki_data.env["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_system() -> void: + fuseki_data.sys_component["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_operator() -> void: + var new_fuseki_node_data_insight = new_fuseki_node_data + new_fuseki_node_data_insight["type"].append("Insight") + fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_insight + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_machine() -> void: + var new_fuseki_node_data_action = new_fuseki_node_data + new_fuseki_node_data_action["type"].append("Action") + fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_action + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_sensors() -> void: + fuseki_data.sensing_component["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_data_trans() -> void: + fuseki_data.data_transmitted["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_services() -> void: + fuseki_data.service["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_enablers() -> void: + fuseki_data.enabler["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_models() -> void: + fuseki_data.model["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() + +func _on_add_component_button_pressed_data() -> void: + fuseki_data.data["new_node_" + str(new_node_index)] = new_fuseki_node_data + new_node_index += 1 + on_fuseki_data_updated() diff --git a/DTContainer/DT&PTContainer.tscn b/DTContainer/DT&PTContainer.tscn index 85aafb8..c323342 100644 --- a/DTContainer/DT&PTContainer.tscn +++ b/DTContainer/DT&PTContainer.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=12 format=3 uid="uid://nqgolpvgp7yg"] +[gd_scene load_steps=14 format=3 uid="uid://nqgolpvgp7yg"] [ext_resource type="Script" uid="uid://cijvy55ehil" path="res://DTContainer/DT&PTContainer.gd" id="1_juoha"] +[ext_resource type="PackedScene" uid="uid://ru63hln676hc" path="res://DTContainer/add_component_button.tscn" id="2_panlg"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sp6md"] content_margin_left = 20.0 @@ -41,6 +42,8 @@ content_margin_bottom = 20.0 bg_color = Color(0.933333, 0.733333, 1, 1) corner_radius_bottom_right = 20 +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_panlg"] + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xj583"] content_margin_left = 20.0 content_margin_top = 20.0 @@ -100,8 +103,8 @@ corner_detail = 5 [node name="DT&PTContainer" type="HBoxContainer"] offset_right = 1260.0 -offset_bottom = 480.0 -theme_override_constants/separation = 50 +offset_bottom = 505.0 +theme_override_constants/separation = 0 script = ExtResource("1_juoha") [node name="PTContainer" type="HBoxContainer" parent="."] @@ -137,6 +140,9 @@ layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 +[node name="AddComponentButton" parent="PTContainer/Operator&EnvContainer/OperatorPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="ExperimentContainer" type="HBoxContainer" parent="PTContainer/Operator&EnvContainer"] layout_mode = 2 theme_override_constants/separation = 0 @@ -164,6 +170,9 @@ custom_minimum_size = Vector2(200, 250) layout_mode = 2 size_flags_horizontal = 6 +[node name="AddComponentButton" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="SystemPanel" type="PanelContainer" parent="PTContainer/Operator&EnvContainer/ExperimentContainer"] z_index = -1 layout_mode = 2 @@ -189,6 +198,9 @@ custom_minimum_size = Vector2(200, 250) layout_mode = 2 size_flags_horizontal = 6 +[node name="AddComponentButton" parent="PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="DataTravelContainer" type="VBoxContainer" parent="PTContainer"] layout_mode = 2 theme_override_constants/separation = 0 @@ -216,6 +228,9 @@ custom_minimum_size = Vector2(200, 175) layout_mode = 2 size_flags_horizontal = 6 +[node name="AddComponentButton" parent="PTContainer/DataTravelContainer/MachinePanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="DataOutPanel" type="PanelContainer" parent="PTContainer/DataTravelContainer"] z_index = -1 layout_mode = 2 @@ -245,7 +260,10 @@ layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 -[node name="DataTransmittedLabel" type="Label" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] +layout_mode = 2 + +[node name="DataTransmittedLabel" type="Label" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer"] z_index = 1 layout_mode = 2 size_flags_horizontal = 0 @@ -254,6 +272,13 @@ theme_override_colors/font_color = Color(0, 0, 0, 1) text = "Data transmitted" uppercase = true +[node name="HFill" type="Control" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="AddComponentButton" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="DataTransmittedOrganizer" type="HBoxContainer" parent="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer"] custom_minimum_size = Vector2(0, 92) layout_mode = 2 @@ -264,6 +289,14 @@ layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 +[node name="AddComponentButton" parent="PTContainer/DataTravelContainer/DataOutPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + +[node name="VSeparator" type="VSeparator" parent="."] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +theme_override_styles/separator = SubResource("StyleBoxEmpty_panlg") + [node name="DTContainer" type="VBoxContainer" parent="."] layout_mode = 2 alignment = 1 @@ -291,6 +324,9 @@ custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 +[node name="AddComponentButton" parent="DTContainer/ServicesPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="EnablersPanel" type="PanelContainer" parent="DTContainer"] z_index = -1 layout_mode = 2 @@ -315,6 +351,9 @@ layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 +[node name="AddComponentButton" parent="DTContainer/EnablersPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="ModelsDataPanel" type="PanelContainer" parent="DTContainer"] z_index = -1 layout_mode = 2 @@ -345,6 +384,9 @@ layout_mode = 2 size_flags_horizontal = 6 size_flags_vertical = 3 +[node name="AddComponentButton" parent="DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer" instance=ExtResource("2_panlg")] +layout_mode = 2 + [node name="DataPanelContainer" type="PanelContainer" parent="DTContainer/ModelsDataPanel/ModelsDataContainer"] layout_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_panlg") @@ -362,3 +404,17 @@ uppercase = true custom_minimum_size = Vector2(200, 125) layout_mode = 2 size_flags_horizontal = 6 + +[node name="AddComponentButton" parent="DTContainer/ModelsDataPanel" instance=ExtResource("2_panlg")] +layout_mode = 2 + +[connection signal="pressed" from="PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_operator"] +[connection signal="pressed" from="PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_env"] +[connection signal="pressed" from="PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_system"] +[connection signal="pressed" from="PTContainer/DataTravelContainer/MachinePanel/AddComponentButton" to="." method="_on_add_component_button_pressed_machine"] +[connection signal="pressed" from="PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton" to="." method="_on_add_component_button_pressed_data_trans"] +[connection signal="pressed" from="PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_sensors"] +[connection signal="pressed" from="DTContainer/ServicesPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_services"] +[connection signal="pressed" from="DTContainer/EnablersPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_enablers"] +[connection signal="pressed" from="DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton" to="." method="_on_add_component_button_pressed_models"] +[connection signal="pressed" from="DTContainer/ModelsDataPanel/AddComponentButton" to="." method="_on_add_component_button_pressed_data"] diff --git a/DTContainer/add_component_button.tscn b/DTContainer/add_component_button.tscn new file mode 100644 index 0000000..e81702c --- /dev/null +++ b/DTContainer/add_component_button.tscn @@ -0,0 +1,8 @@ +[gd_scene format=3 uid="uid://ru63hln676hc"] + +[node name="AddComponentButton" type="Button"] +custom_minimum_size = Vector2(22, 22) +size_flags_horizontal = 8 +size_flags_vertical = 0 +theme_override_font_sizes/font_size = 10 +text = "+" diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 6bf53f2..ff6d6bc 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -25,7 +25,6 @@ script = ExtResource("1_r7thu") [node name="DTContainer" parent="." instance=ExtResource("1_lklxr")] z_as_relative = false -offset_bottom = 505.0 [node name="Camera2D" type="Camera2D" parent="."] position = Vector2(575, 326) @@ -79,10 +78,117 @@ theme = ExtResource("5_77ryg") [node name="RabbitButton" type="CheckButton" parent="ControlLayer/ControlPanel/MarginContainer/ControlContainer"] unique_name_in_owner = true layout_mode = 2 +tooltip_text = "Enable listening of messages from RabbitMQ" disabled = true text = "Record data form RabbitMQ :" script = ExtResource("9_qda1k") +[node name="VSeparator4" type="VSeparator" parent="ControlLayer/ControlPanel/MarginContainer/ControlContainer"] +layout_mode = 2 +theme = ExtResource("5_77ryg") + +[node name="ViewPanel" type="PanelContainer" parent="ControlLayer"] +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -340.0 +offset_top = -114.0 +offset_right = -12.0 +offset_bottom = -7.0 +grow_horizontal = 0 +grow_vertical = 0 +size_flags_horizontal = 0 +size_flags_vertical = 0 + +[node name="MarginContainer" type="MarginContainer" parent="ControlLayer/ViewPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer"] +layout_mode = 2 + +[node name="ViewLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "dt component views" +horizontal_alignment = 1 +uppercase = true + +[node name="HSeparator" type="HSeparator" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 + +[node name="NodeBGLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +text = "Background Color" + +[node name="NodeBGOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +selected = 0 +allow_reselect = true +item_count = 4 +popup/item_0/text = "None" +popup/item_0/id = 0 +popup/item_1/text = "Deployment" +popup/item_1/id = 1 +popup/item_2/text = "Implementation" +popup/item_2/id = 2 +popup/item_3/text = "Timescale" +popup/item_3/id = 3 + +[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer" instance=ExtResource("12_80vrr")] +layout_mode = 2 + +[node name="VBoxContainer2" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 + +[node name="NodeBorderLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] +layout_mode = 2 +text = "Border Color" + +[node name="NodeBorderOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] +layout_mode = 2 +selected = 0 +allow_reselect = true +item_count = 4 +popup/item_0/text = "None" +popup/item_0/id = 0 +popup/item_1/text = "Deployment" +popup/item_1/id = 1 +popup/item_2/text = "Implementation" +popup/item_2/id = 2 +popup/item_3/text = "Timescale" +popup/item_3/id = 3 + +[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2" instance=ExtResource("12_80vrr")] +layout_mode = 2 + +[node name="PanelContainer" type="PanelContainer" parent="ControlLayer"] +anchors_preset = 2 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 50.0 +offset_top = -41.0 +offset_right = 202.0 +offset_bottom = -10.0 +grow_vertical = 0 +size_flags_horizontal = 0 +size_flags_vertical = 0 + +[node name="EditButton" type="CheckButton" parent="ControlLayer/PanelContainer"] +layout_mode = 2 +tooltip_text = "Enables the visual editing of the DT constellation" +text = "Visual editing" + [node name="HelpButton" type="Button" parent="ControlLayer"] anchors_preset = 2 anchor_top = 1.0 @@ -177,89 +283,6 @@ anchor_right = 0.5 anchor_bottom = 0.5 IS_IN_GAME_MENU = false -[node name="ViewPanel" type="PanelContainer" parent="ControlLayer"] -anchors_preset = 3 -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = -373.0 -offset_top = -125.0 -offset_right = -18.0 -offset_bottom = -18.0 -grow_horizontal = 0 -grow_vertical = 0 - -[node name="MarginContainer" type="MarginContainer" parent="ControlLayer/ViewPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer"] -layout_mode = 2 - -[node name="ViewLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "dt component views" -horizontal_alignment = 1 -uppercase = true - -[node name="HSeparator" type="HSeparator" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] -layout_mode = 2 - -[node name="HBoxContainer" type="HBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer"] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] -layout_mode = 2 - -[node name="NodeBGLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] -layout_mode = 2 -text = "Background Color" - -[node name="NodeBGOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"] -layout_mode = 2 -selected = 0 -allow_reselect = true -item_count = 4 -popup/item_0/text = "None" -popup/item_0/id = 0 -popup/item_1/text = "Deployment" -popup/item_1/id = 1 -popup/item_2/text = "Implementation" -popup/item_2/id = 2 -popup/item_3/text = "Timescale" -popup/item_3/id = 3 - -[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer" instance=ExtResource("12_80vrr")] -layout_mode = 2 - -[node name="VBoxContainer2" type="VBoxContainer" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer"] -layout_mode = 2 - -[node name="NodeBorderLabel" type="Label" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] -layout_mode = 2 -text = "Border Color" - -[node name="NodeBorderOption" type="OptionButton" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"] -layout_mode = 2 -selected = 0 -allow_reselect = true -item_count = 4 -popup/item_0/text = "None" -popup/item_0/id = 0 -popup/item_1/text = "Deployment" -popup/item_1/id = 1 -popup/item_2/text = "Implementation" -popup/item_2/id = 2 -popup/item_3/text = "Timescale" -popup/item_3/id = 3 - -[node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2" instance=ExtResource("12_80vrr")] -layout_mode = 2 - [node name="FusekiData" parent="." instance=ExtResource("5_4e51a")] [node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container", "to_hide")] @@ -268,10 +291,10 @@ container = NodePath("../DTContainer") to_hide = [NodePath("../ControlLayer")] [connection signal="toggled" from="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" to="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" method="_on_toggled"] -[connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] -[connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] -[connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/SettingsButton" method="_on_settings_settings_menu_closed"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="DTContainer" method="_on_node_bg_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Legends" method="_on_node_style_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="DTContainer" method="_on_node_border_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Legends" method="_on_node_style_option_item_selected"] +[connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] +[connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] +[connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/SettingsButton" method="_on_settings_settings_menu_closed"] From 76f1388b1c1b8b573a4440f4b78b040b924b3ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 16:26:39 -0400 Subject: [PATCH 19/40] Editable component name --- GenericDisplay/GenericDisplay.gd | 10 ++++++-- GenericDisplay/generic_display.tscn | 36 +++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index bcf51a1..2e6fd8c 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -137,11 +137,17 @@ func set_visualization(): if last_loaded_pck_path.is_empty(): $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = true -# Enables a button showing DTComponent's description +# Enables a button showing DTComponent information func set_description(description): $GenericDisplay/PresentationBox/DescriptionButton.show() - $PopupDescription.title = $GenericDisplay/PresentationBox/GenericElementName.text + " description" + $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text = $GenericDisplay/PresentationBox/GenericElementName.text $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description + $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.grab_focus() + +# Editable component name +func _on_component_name_edit_text_changed() -> void: + $GenericDisplay/PresentationBox/GenericElementName.text = $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text + # TODO: Keep link information when name changes func set_info(new_data : Array[String], is_bool = false) -> void: data = to_float_array(new_data, is_bool) diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index f393cec..ec08bec 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -204,9 +204,9 @@ layout_mode = 2 [node name="PopupDescription" type="PopupPanel" parent="."] disable_3d = true oversampling_override = 1.0 -title = "Description" +title = "Component Information" initial_position = 4 -size = Vector2i(400, 200) +size = Vector2i(400, 225) unresizable = false borderless = false always_on_top = true @@ -218,8 +218,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 396.0 -offset_bottom = 196.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 @@ -231,10 +231,31 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +[node name="ComponentNameContainer" type="HBoxContainer" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +custom_minimum_size = Vector2(0, 35) +layout_mode = 2 + +[node name="ComponentNameLabel" type="Label" parent="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer"] +layout_mode = 2 +text = "Name:" + +[node name="ComponentNameEdit" type="TextEdit" parent="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +focus_next = NodePath("../../Description") +placeholder_text = "Placeholder component name" + +[node name="HSeparator" type="HSeparator" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +layout_mode = 2 + +[node name="DescriptionLabel" type="Label" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +layout_mode = 2 +text = "Description:" + [node name="Description" type="TextEdit" parent="PopupDescription/DescriptionControl/DescriptionContainer"] layout_mode = 2 size_flags_vertical = 3 -text = "No description" +placeholder_text = "No description" wrap_mode = 1 [node name="PopupScript" type="PopupPanel" parent="."] @@ -254,8 +275,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 516.0 -offset_bottom = 516.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_ipcor") @@ -282,3 +303,4 @@ text = "Select the DT script folder in the settings first" [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPickerButton" to="." method="_on_viz_picker_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton" to="." method="_on_viz_pop_up_button_pressed"] [connection signal="popup_hide" from="PopupDescription" to="." method="_on_popup_description_popup_hide"] +[connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit" to="." method="_on_component_name_edit_text_changed"] From 6178a5731d452f4ceb3602902a5b2812333b3113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 16:29:50 -0400 Subject: [PATCH 20/40] Fixed layout bug --- GenericDisplay/generic_display.tscn | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index ec08bec..99f5626 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -95,6 +95,8 @@ vertical_alignment = 3 [node name="DescriptionButton" type="Button" parent="GenericDisplay/PresentationBox"] visible = false layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 tooltip_text = "Open Description" theme_override_font_sizes/font_size = 10 theme_override_styles/normal = SubResource("StyleBoxFlat_5nhr6") @@ -275,8 +277,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_right = 516.0 +offset_bottom = 516.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_ipcor") From d79b618a0d29295967eca56d1a4a1bcad8b84c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 16:39:23 -0400 Subject: [PATCH 21/40] Description as a component tooltip --- GenericDisplay/GenericDisplay.gd | 5 +++++ GenericDisplay/generic_display.tscn | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 2e6fd8c..f0942a0 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -139,6 +139,7 @@ func set_visualization(): # Enables a button showing DTComponent information func set_description(description): + tooltip_text = description $GenericDisplay/PresentationBox/DescriptionButton.show() $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text = $GenericDisplay/PresentationBox/GenericElementName.text $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description @@ -149,6 +150,10 @@ func _on_component_name_edit_text_changed() -> void: $GenericDisplay/PresentationBox/GenericElementName.text = $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text # TODO: Keep link information when name changes +# Editable component description +func _on_description_text_changed() -> void: + tooltip_text = $PopupDescription/DescriptionControl/DescriptionContainer/Description.text + func set_info(new_data : Array[String], is_bool = false) -> void: data = to_float_array(new_data, is_bool) var node : Label = get_node("GenericDisplay/RealTimeContainer/GenericElementAttributes") diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index 99f5626..9e7583c 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -220,8 +220,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_right = 396.0 +offset_bottom = 221.0 grow_horizontal = 2 grow_vertical = 2 @@ -306,3 +306,4 @@ text = "Select the DT script folder in the settings first" [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton" to="." method="_on_viz_pop_up_button_pressed"] [connection signal="popup_hide" from="PopupDescription" to="." method="_on_popup_description_popup_hide"] [connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit" to="." method="_on_component_name_edit_text_changed"] +[connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/Description" to="." method="_on_description_text_changed"] From 6fafa934d794c9fa7a5b717ac11ac51c9393af41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 25 Sep 2025 17:10:27 -0400 Subject: [PATCH 22/40] Fixed dictionary issue --- DTContainer/DT&PTContainer.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 956d269..278598e 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -434,14 +434,14 @@ func _on_add_component_button_pressed_system() -> void: on_fuseki_data_updated() func _on_add_component_button_pressed_operator() -> void: - var new_fuseki_node_data_insight = new_fuseki_node_data + var new_fuseki_node_data_insight = new_fuseki_node_data.duplicate_deep() new_fuseki_node_data_insight["type"].append("Insight") fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_insight new_node_index += 1 on_fuseki_data_updated() func _on_add_component_button_pressed_machine() -> void: - var new_fuseki_node_data_action = new_fuseki_node_data + var new_fuseki_node_data_action = new_fuseki_node_data.duplicate_deep() new_fuseki_node_data_action["type"].append("Action") fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_action new_node_index += 1 From a182ac9044a1329bbdc8c032dfd2756d5bcd3c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Fri, 26 Sep 2025 01:29:56 -0400 Subject: [PATCH 23/40] Speed improvements & toggle visual editing button working --- DTContainer/DT&PTContainer.gd | 59 +++++++++++++++++++++++++++++------ MainScene/MainScene.tscn | 6 ++-- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 278598e..679b922 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -426,53 +426,92 @@ var new_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component func _on_add_component_button_pressed_env() -> void: fuseki_data.env["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(env_container, fuseki_data.env) func _on_add_component_button_pressed_system() -> void: fuseki_data.sys_component["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(sys_container, fuseki_data.sys_component) func _on_add_component_button_pressed_operator() -> void: var new_fuseki_node_data_insight = new_fuseki_node_data.duplicate_deep() new_fuseki_node_data_insight["type"].append("Insight") fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_insight new_node_index += 1 - on_fuseki_data_updated() + update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) func _on_add_component_button_pressed_machine() -> void: var new_fuseki_node_data_action = new_fuseki_node_data.duplicate_deep() new_fuseki_node_data_action["type"].append("Action") fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_action new_node_index += 1 - on_fuseki_data_updated() + update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) func _on_add_component_button_pressed_sensors() -> void: fuseki_data.sensing_component["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(sensor_container, fuseki_data.sensing_component) func _on_add_component_button_pressed_data_trans() -> void: fuseki_data.data_transmitted["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(data_transmitted_container, fuseki_data.data_transmitted) func _on_add_component_button_pressed_services() -> void: fuseki_data.service["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(service_container, fuseki_data.service) func _on_add_component_button_pressed_enablers() -> void: fuseki_data.enabler["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(enabler_container, fuseki_data.enabler) func _on_add_component_button_pressed_models() -> void: fuseki_data.model["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(model_container, fuseki_data.model) func _on_add_component_button_pressed_data() -> void: fuseki_data.data["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - on_fuseki_data_updated() + update_node_with(data_container, fuseki_data.data) + +## Visual editing + +func _on_visual_editing_button_toggled(toggled_on: bool) -> void: + if toggled_on: + enable_visual_editing() + else: + disable_visual_editing() + +func enable_visual_editing(): + $DTContainer/EnablersPanel/AddComponentButton.show() + $DTContainer/ServicesPanel/AddComponentButton.show() + $DTContainer/ModelsDataPanel/AddComponentButton.show() + $PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton.show() + $PTContainer/DataTravelContainer/MachinePanel/AddComponentButton.show() + $"PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton".show() + $"PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton".show() + $"PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton".show() + $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton.show() + $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton.show() + +func disable_visual_editing(): + $DTContainer/EnablersPanel/AddComponentButton.hide() + $DTContainer/ServicesPanel/AddComponentButton.hide() + $DTContainer/ModelsDataPanel/AddComponentButton.hide() + $PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton.hide() + $PTContainer/DataTravelContainer/MachinePanel/AddComponentButton.hide() + $"PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton".hide() + $"PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton".hide() + $"PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton".hide() + $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton.hide() + $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton.hide() + + +func _on_visual_editing_button_ready(source: Node) -> void: + if source.button_pressed: + enable_visual_editing() + else: + disable_visual_editing() diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index ff6d6bc..ce16a5a 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -172,7 +172,7 @@ popup/item_3/id = 3 [node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2" instance=ExtResource("12_80vrr")] layout_mode = 2 -[node name="PanelContainer" type="PanelContainer" parent="ControlLayer"] +[node name="VisualEditingPanelContainer" type="PanelContainer" parent="ControlLayer"] anchors_preset = 2 anchor_top = 1.0 anchor_bottom = 1.0 @@ -184,7 +184,7 @@ grow_vertical = 0 size_flags_horizontal = 0 size_flags_vertical = 0 -[node name="EditButton" type="CheckButton" parent="ControlLayer/PanelContainer"] +[node name="VisualEditingButton" type="CheckButton" parent="ControlLayer/VisualEditingPanelContainer"] layout_mode = 2 tooltip_text = "Enables the visual editing of the DT constellation" text = "Visual editing" @@ -295,6 +295,8 @@ to_hide = [NodePath("../ControlLayer")] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Legends" method="_on_node_style_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="DTContainer" method="_on_node_border_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Legends" method="_on_node_style_option_item_selected"] +[connection signal="ready" from="ControlLayer/VisualEditingPanelContainer/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_ready" flags=18] +[connection signal="toggled" from="ControlLayer/VisualEditingPanelContainer/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_toggled"] [connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] [connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] [connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/SettingsButton" method="_on_settings_settings_menu_closed"] From 8e687787cef9eed7df05aeb9e4863e356daea94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Tue, 30 Sep 2025 02:39:31 -0400 Subject: [PATCH 24/40] Enabled node removal. Node editing improvements. FusekiData updated by visual editing. --- Config/StyleConfig.gd | 4 - DTContainer/DT&PTContainer.gd | 164 +++++++++++++++++++++--- GenericDisplay/GenericDisplay.gd | 41 +++++- GenericDisplay/GenericDisplaySignals.gd | 1 + GenericDisplay/generic_display.tscn | 24 +++- Legends/Legends.gd | 14 -- MainScene/MainScene.tscn | 3 +- project.godot | 4 + 8 files changed, 212 insertions(+), 43 deletions(-) diff --git a/Config/StyleConfig.gd b/Config/StyleConfig.gd index 4864ece..1068035 100644 --- a/Config/StyleConfig.gd +++ b/Config/StyleConfig.gd @@ -40,9 +40,5 @@ class Link: #Legends style class Legends: - const GO_AWAY_TEXT : String = "Hide legend" - const COME_IN_TEXT : String = "Show legend" - - const RETRACT_BUTTON_COLOR : Color = Color.DARK_ORANGE const PANEL_COLOR : Color = Color.DARK_GRAY const CATEGORY_ANNONCE_COLOR : Color = Color(0.3, 0.3, 0.3) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 679b922..e20c29b 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -33,7 +33,14 @@ var border_view = 0 #initialization func _ready(): GenericDisplaySignals.generic_display_over.connect(_on_element_over) + GenericDisplaySignals.generic_display_edit.connect(edit_node) RabbitSignals.updated_data.connect(_on_rabbit_data_updated) + + var buttons = get_tree().get_nodes_in_group("main_buttons") + for btn in buttons: + if btn.name == "VisualEditingButton": + btn.toggled.connect(_on_visual_editing_toggled) + _on_visual_editing_toggled(btn.button_pressed) func set_fuseki_data(_fuseki_data: FusekiData) -> void: fuseki_data = _fuseki_data @@ -389,7 +396,7 @@ func _process(_delta): static func free_all_child(node : Node): for child in node.get_children(): if (child.get_class() == "PanelContainer"): - child.free() + child.queue_free() #Draw all links func _draw(): @@ -421,65 +428,73 @@ func _on_node_bg_option_item_selected(index: int) -> void: ## Adding nodes to containers var new_node_index = 1 -var new_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": ["New description here"] } +var default_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": ["New description here"] } func _on_add_component_button_pressed_env() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.env["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(env_container, fuseki_data.env) func _on_add_component_button_pressed_system() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.sys_component["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(sys_container, fuseki_data.sys_component) func _on_add_component_button_pressed_operator() -> void: - var new_fuseki_node_data_insight = new_fuseki_node_data.duplicate_deep() - new_fuseki_node_data_insight["type"].append("Insight") - fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_insight + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() + new_fuseki_node_data["type"].append("Insight") + fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) func _on_add_component_button_pressed_machine() -> void: - var new_fuseki_node_data_action = new_fuseki_node_data.duplicate_deep() - new_fuseki_node_data_action["type"].append("Action") - fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data_action + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() + new_fuseki_node_data["type"].append("Action") + fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) func _on_add_component_button_pressed_sensors() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.sensing_component["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(sensor_container, fuseki_data.sensing_component) func _on_add_component_button_pressed_data_trans() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.data_transmitted["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(data_transmitted_container, fuseki_data.data_transmitted) func _on_add_component_button_pressed_services() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.service["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(service_container, fuseki_data.service) func _on_add_component_button_pressed_enablers() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.enabler["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(enabler_container, fuseki_data.enabler) func _on_add_component_button_pressed_models() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.model["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(model_container, fuseki_data.model) func _on_add_component_button_pressed_data() -> void: + var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() fuseki_data.data["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 update_node_with(data_container, fuseki_data.data) ## Visual editing -func _on_visual_editing_button_toggled(toggled_on: bool) -> void: +func _on_visual_editing_toggled(toggled_on: bool) -> void: if toggled_on: enable_visual_editing() else: @@ -509,9 +524,128 @@ func disable_visual_editing(): $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton.hide() $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton.hide() - -func _on_visual_editing_button_ready(source: Node) -> void: - if source.button_pressed: - enable_visual_editing() - else: - disable_visual_editing() +func rename_dict_key(dict: Dictionary, old_key, new_key) -> void: + if old_key == new_key or not dict.has(old_key): + return # nothing to do + var keys = dict.keys() + var values = dict.values() + for i in range(keys.size()): + if keys[i] == old_key: + keys[i] = new_key + break + dict.clear() + for i in range(keys.size()): + dict[keys[i]] = values[i] + +# Edit the FusekiData of a specific node +func edit_node(node_name: String, parent_container: Node, delete: bool, new_node_name: String, new_node_description: String): + match parent_container: + service_container: + if delete: + fuseki_data.service.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.service, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.service[node_name]["desc"] = [new_node_description] + update_node_with(service_container, fuseki_data.service) + + enabler_container: + if delete: + fuseki_data.enabler.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.enabler, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.enabler[node_name]["desc"] = [new_node_description] + update_node_with(enabler_container, fuseki_data.enabler) + + model_container: + if delete: + fuseki_data.model.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.model, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.model[node_name]["desc"] = [new_node_description] + update_node_with(model_container, fuseki_data.model) + + data_container: + if delete: + fuseki_data.data.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.data, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.data[node_name]["desc"] = [new_node_description] + update_node_with(data_container, fuseki_data.data) + + operator_container: + if delete: + fuseki_data.provided_thing.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.provided_thing, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] + update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) + + machine_container: + if delete: + fuseki_data.provided_thing.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.provided_thing, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] + update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) + + data_transmitted_container: + if delete: + fuseki_data.data_transmitted.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.data_transmitted, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.data_transmitted[node_name]["desc"] = [new_node_description] + update_node_with(data_transmitted_container, fuseki_data.data_transmitted) + + sensor_container: + if delete: + fuseki_data.sensing_component.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.sensing_component, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.sensing_component[node_name]["desc"] = [new_node_description] + update_node_with(sensor_container, fuseki_data.sensing_component) + + env_container: + if delete: + fuseki_data.env.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.env, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.env[node_name]["desc"] = [new_node_description] + update_node_with(env_container, fuseki_data.env) + + sys_container: + if delete: + fuseki_data.sys_component.erase(node_name) + else: + if node_name != new_node_name: + rename_dict_key(fuseki_data.sys_component, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + fuseki_data.sys_component[node_name]["desc"] = [new_node_description] + update_node_with(sys_container, fuseki_data.sys_component) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index f0942a0..1dab9c5 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -5,6 +5,7 @@ class_name GenericDisplay @onready var element : Label = $GenericDisplay/PresentationBox/GenericElementName @onready var realtime_element : Label = $GenericDisplay/RealTimeContainer/GenericElementAttributes @onready var script_button = $GenericDisplay/PresentationBox/ScriptButton +@onready var edit_button = $GenericDisplay/PresentationBox/EditButton @onready var real_time_container = $GenericDisplay/RealTimeContainer @onready var attributes : Label = $GenericDisplay/RealTimeContainer/GenericElementAttributes @onready var visualization_container = $GenericDisplay/VisualizationContainer @@ -21,6 +22,9 @@ var highlightable : bool = true var bg_color : Color = StyleConfig.DTElement.DIMMED_COLOR var border_color : Color = StyleConfig.DTElement.BORDER_COLOR +var node_name_before_edit: String +var node_desc_before_edit: String + var last_loaded_pck_path: String = "" func _ready(): @@ -28,6 +32,12 @@ func _ready(): ChartSignals.hide.connect(_on_hide_chart_pop_up_signal) ScriptSignals.hide.connect(_on_hide_script_pop_up_signal) ScriptSignals.scripts_folder_selected.connect(_on_script_folder_updated) + + var buttons = get_tree().get_nodes_in_group("main_buttons") + for btn in buttons: + if btn.name == "VisualEditingButton": + btn.toggled.connect(_on_visual_editing_toggled) + _on_visual_editing_toggled(btn.button_pressed) #Signal handling --------------------------------------------------------------- func _on_display_highlight(highlighted_elements_names : Array): @@ -67,6 +77,12 @@ func _on_script_folder_updated(dir : String) -> void: script_software_directory = dir set_python_script() +func _on_visual_editing_toggled(enabled: bool): + if enabled: + edit_button.show() + else: + edit_button.hide() + #Trigger the file dialog when the button is pressed func _on_viz_picker_button_pressed() -> void: var file_picker = $GenericDisplay/FilePicker @@ -78,14 +94,25 @@ func _on_file_selected(pck_path: String) -> void: if not last_loaded_pck_path.is_empty(): $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton.disabled = false -func _on_description_button_pressed() -> void: +func _on_edit_button_pressed() -> void: + $PopupDescription.popup() + +func _on_popup_description_about_to_popup() -> void: CameraSignals.disable_camera_movement.emit() CameraSignals.disable_camera_zoom.emit() - $PopupDescription.popup() + # Save current variables + node_name_before_edit = $GenericDisplay/PresentationBox/GenericElementName.text func _on_popup_description_popup_hide() -> void: CameraSignals.enable_camera_movement.emit() CameraSignals.enable_camera_zoom.emit() + + # Emit signal to edit the node's FusekiData + var node_name = $GenericDisplay/PresentationBox/GenericElementName.text + var node_desc = $PopupDescription/DescriptionControl/DescriptionContainer/Description.text + var parent_container = get_parent() + # Edit the descirption + GenericDisplaySignals.generic_display_edit.emit(node_name_before_edit, parent_container, false, node_name, node_desc) func _on_viz_pop_up_button_pressed(): var pop_up_panel = $GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton/PopupPanel @@ -140,7 +167,6 @@ func set_visualization(): # Enables a button showing DTComponent information func set_description(description): tooltip_text = description - $GenericDisplay/PresentationBox/DescriptionButton.show() $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text = $GenericDisplay/PresentationBox/GenericElementName.text $PopupDescription/DescriptionControl/DescriptionContainer/Description.text = description $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.grab_focus() @@ -148,7 +174,6 @@ func set_description(description): # Editable component name func _on_component_name_edit_text_changed() -> void: $GenericDisplay/PresentationBox/GenericElementName.text = $PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit.text - # TODO: Keep link information when name changes # Editable component description func _on_description_text_changed() -> void: @@ -238,3 +263,11 @@ func set_node_style(_color: Color, is_bg: bool, is_border: bool): if is_border: border_color = _color change_border_color(border_color) + +func _on_delete_button_pressed() -> void: + var node_name = $GenericDisplay/PresentationBox/GenericElementName.text + var parent_container = get_parent() + GenericDisplaySignals.generic_display_edit.emit(node_name, parent_container, true, "", "") + CameraSignals.enable_camera_movement.emit() + CameraSignals.enable_camera_zoom.emit() + queue_free() diff --git a/GenericDisplay/GenericDisplaySignals.gd b/GenericDisplay/GenericDisplaySignals.gd index 1744f30..639d9ce 100644 --- a/GenericDisplay/GenericDisplaySignals.gd +++ b/GenericDisplay/GenericDisplaySignals.gd @@ -2,3 +2,4 @@ extends Node signal generic_display_over signal generic_display_highlight +signal generic_display_edit diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index 9e7583c..d023fa2 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -92,18 +92,18 @@ text = "Default" horizontal_alignment = 1 vertical_alignment = 3 -[node name="DescriptionButton" type="Button" parent="GenericDisplay/PresentationBox"] -visible = false +[node name="EditButton" type="Button" parent="GenericDisplay/PresentationBox"] layout_mode = 2 size_flags_horizontal = 0 size_flags_vertical = 0 -tooltip_text = "Open Description" +tooltip_text = "Edit" theme_override_font_sizes/font_size = 10 theme_override_styles/normal = SubResource("StyleBoxFlat_5nhr6") theme_override_styles/pressed = SubResource("StyleBoxFlat_0c6qw") theme_override_styles/hover = SubResource("StyleBoxFlat_ycnc1") text = " i " icon_alignment = 1 +expand_icon = true [node name="ScriptButton" type="Button" parent="GenericDisplay/PresentationBox"] visible = false @@ -246,6 +246,7 @@ layout_mode = 2 size_flags_horizontal = 3 focus_next = NodePath("../../Description") placeholder_text = "Placeholder component name" +context_menu_enabled = false [node name="HSeparator" type="HSeparator" parent="PopupDescription/DescriptionControl/DescriptionContainer"] layout_mode = 2 @@ -258,8 +259,21 @@ text = "Description:" layout_mode = 2 size_flags_vertical = 3 placeholder_text = "No description" +context_menu_enabled = false wrap_mode = 1 +[node name="HSeparator2" type="HSeparator" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +layout_mode = 2 + +[node name="DeleteButton" type="Button" parent="PopupDescription/DescriptionControl/DescriptionContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.87, 0.25230002, 0.25230002, 1) +theme_override_colors/font_focus_color = Color(0.87058824, 0.2509804, 0.2509804, 1) +theme_override_colors/font_pressed_color = Color(0.87058824, 0.2509804, 0.2509804, 1) +theme_override_colors/font_hover_color = Color(0.87058824, 0.2509804, 0.2509804, 1) +theme_override_colors/font_hover_pressed_color = Color(0.87058824, 0.2509804, 0.2509804, 1) +text = "Delete" + [node name="PopupScript" type="PopupPanel" parent="."] disable_3d = true oversampling_override = 1.0 @@ -299,11 +313,13 @@ text = "Select the DT script folder in the settings first" [connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] [connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] [connection signal="file_selected" from="GenericDisplay/FilePicker" to="." method="_on_file_selected"] -[connection signal="pressed" from="GenericDisplay/PresentationBox/DescriptionButton" to="." method="_on_description_button_pressed"] +[connection signal="pressed" from="GenericDisplay/PresentationBox/EditButton" to="." method="_on_edit_button_pressed"] [connection signal="pressed" from="GenericDisplay/PresentationBox/ScriptButton" to="." method="_on_script_button_pressed"] [connection signal="pressed" from="GenericDisplay/RealTimeContainer/PopUpButton" to="." method="_on_pop_up_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPickerButton" to="." method="_on_viz_picker_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton" to="." method="_on_viz_pop_up_button_pressed"] +[connection signal="about_to_popup" from="PopupDescription" to="." method="_on_popup_description_about_to_popup"] [connection signal="popup_hide" from="PopupDescription" to="." method="_on_popup_description_popup_hide"] [connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit" to="." method="_on_component_name_edit_text_changed"] [connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/Description" to="." method="_on_description_text_changed"] +[connection signal="pressed" from="PopupDescription/DescriptionControl/DescriptionContainer/DeleteButton" to="." method="_on_delete_button_pressed"] diff --git a/Legends/Legends.gd b/Legends/Legends.gd index 250371f..33d7a83 100644 --- a/Legends/Legends.gd +++ b/Legends/Legends.gd @@ -1,6 +1,5 @@ extends Container -@onready var retract_button = $RetractButton @onready var legends_panel = $PanelContainer @onready var legends_container = $PanelContainer/LegendsContainer @@ -36,19 +35,6 @@ func instanciate_legend_element(text : String, color : Color): #Hiding function --------------------------------------------------------------------------------- var legends_hidden : bool = false -func _on_retract_button_pressed() -> void: - hide_legends() - -func hide_legends(): - var translate_distance = legends_panel.size.y + 20 - if (legends_hidden): - translate_distance = -translate_distance - retract_button.text = StyleConfig.Legends.GO_AWAY_TEXT - else: - retract_button.text = StyleConfig.Legends.COME_IN_TEXT - legends_hidden = !legends_hidden - position += Vector2(0, translate_distance) - func _on_node_style_option_item_selected(index: int) -> void: for child in legends_container.get_children(): diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index ce16a5a..9e97571 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -184,7 +184,7 @@ grow_vertical = 0 size_flags_horizontal = 0 size_flags_vertical = 0 -[node name="VisualEditingButton" type="CheckButton" parent="ControlLayer/VisualEditingPanelContainer"] +[node name="VisualEditingButton" type="CheckButton" parent="ControlLayer/VisualEditingPanelContainer" groups=["main_buttons"]] layout_mode = 2 tooltip_text = "Enables the visual editing of the DT constellation" text = "Visual editing" @@ -295,7 +295,6 @@ to_hide = [NodePath("../ControlLayer")] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Legends" method="_on_node_style_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="DTContainer" method="_on_node_border_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Legends" method="_on_node_style_option_item_selected"] -[connection signal="ready" from="ControlLayer/VisualEditingPanelContainer/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_ready" flags=18] [connection signal="toggled" from="ControlLayer/VisualEditingPanelContainer/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_toggled"] [connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] [connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] diff --git a/project.godot b/project.godot index f2b955f..97e8b74 100644 --- a/project.godot +++ b/project.godot @@ -46,6 +46,10 @@ project/assembly_name="DigitalTwinVisualisationProject" enabled=PackedStringArray("res://addons/easy_charts/plugin.cfg", "res://addons/modular-settings-menu/plugin.cfg") +[global_group] + +main_buttons="" + [input] cam_zoom_in={ From abed6ea367d5fa28a04ed328217e57fe3afe8211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Tue, 30 Sep 2025 02:54:31 -0400 Subject: [PATCH 25/40] Right click a node to enable editing --- GenericDisplay/GenericDisplay.gd | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 1dab9c5..3151cc7 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -53,11 +53,15 @@ func _on_mouse_exited(): GenericDisplaySignals.generic_display_over.emit("", false) func _gui_input(event: InputEvent) -> void: - if event.button_mask == MOUSE_BUTTON_LEFT and highlightable: - highlightable = false - GenericDisplaySignals.generic_display_over.emit(element.text, true) - await get_tree().create_timer(0.2).timeout - highlightable = true + if event is InputEventMouseButton: + if event.button_mask == MOUSE_BUTTON_LEFT and highlightable: + highlightable = false + GenericDisplaySignals.generic_display_over.emit(element.text, true) + await get_tree().create_timer(0.2).timeout + highlightable = true + if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: + if $GenericDisplay/PresentationBox/EditButton.visible: + $PopupDescription.popup() func _on_pop_up_button_pressed() -> void: chart.feed_historic(data) From 83c9be31af3720db79f8d3bf28e480518d8d8bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 2 Oct 2025 01:43:57 -0400 Subject: [PATCH 26/40] Speed improvements, partial component removal working, bug fixes --- DTContainer/DT&PTContainer.gd | 204 ++++++++++++++++++------------- GenericDisplay/GenericDisplay.gd | 5 +- 2 files changed, 123 insertions(+), 86 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index e20c29b..1fe7da2 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -18,6 +18,14 @@ class_name DT_PT #Access to fuseki data var fuseki_data : FusekiData = null +var links_as_dict_enabler_to_service : Dictionary = {} +var links_as_dict_model_to_enabler : Dictionary = {} +var links_as_dict_service_to_provided_thing : Dictionary = {} +var links_as_dict_sensor_to_data_transmitted : Dictionary = {} +var links_as_dict_data_to_enabler : Dictionary = {} +var links_as_dict_data_transmitted_to_data : Dictionary = {} + + #Load the generic display scene const GenericDisplay = preload("res://GenericDisplay/generic_display.tscn") @@ -47,13 +55,9 @@ func set_fuseki_data(_fuseki_data: FusekiData) -> void: #Node and element manipulation functions ----------------------------------------------------------- -#Displays node referenced by its name -class NamedNode: - var name : String - var node : GenericDisplay - #Array of displayes nodes -var displayed_node_list: Array[NamedNode] +var displayed_node_list: Array[Node] +var link_dict: Dictionary #Attributes const timescale_key : String = "hasTimeScale" @@ -64,36 +68,38 @@ const type_attribute : String = "type" const operator_type : String = "Insight" #Set the starting start of a node when loaded -func set_starting_node_style(namedNode : NamedNode, attributes : Dictionary): +func set_starting_node_style(generic_node : GenericDisplay, attributes : Dictionary): # Structural style if (PythonConfig.LOCATION_KEY in attributes.keys()): - namedNode.node.set_python_script_location(attributes[PythonConfig.LOCATION_KEY][0]) + generic_node.set_python_script_location(attributes[PythonConfig.LOCATION_KEY][0]) if ("HasVisualization" in attributes.keys() and attributes["HasVisualization"][0] == "true"): - namedNode.node.set_visualization() + generic_node.set_visualization() if ("desc" in attributes.keys() and attributes["desc"][0] != ""): - namedNode.node.set_description(attributes["desc"][0]) + generic_node.set_description(attributes["desc"][0]) # Visual style - namedNode.node.set_default_style() + generic_node.set_default_style() + generic_node.set_node_style(StyleConfig.DTElement.DIMMED_COLOR, true, false) + generic_node.set_node_style(StyleConfig.DTElement.BORDER_COLOR, false, true) if (implementation_key in attributes.keys()): var is_border = border_view == 2 var is_bg = bg_view == 2 match attributes[implementation_key]: ["planned"]: - namedNode.node.set_node_style(StyleConfig.Implementation.PLANNED, is_bg, is_border) + generic_node.set_node_style(StyleConfig.Implementation.PLANNED, is_bg, is_border) ["active"]: - namedNode.node.set_node_style(StyleConfig.Implementation.ACTIVE, is_bg, is_border) + generic_node.set_node_style(StyleConfig.Implementation.ACTIVE, is_bg, is_border) ["implemented"]: - namedNode.node.set_node_style(StyleConfig.Implementation.IMPLEMENTED, is_bg, is_border) - if (timescale_key in attributes.keys()): + generic_node.set_node_style(StyleConfig.Implementation.IMPLEMENTED, is_bg, is_border) + elif (timescale_key in attributes.keys()): var is_border = border_view == 3 var is_bg = bg_view == 3 match attributes[timescale_key]: ["slower_trt"]: - namedNode.node.set_node_style(StyleConfig.Timescale.SLOWER_THAN_REALTIME, is_bg, is_border) + generic_node.set_node_style(StyleConfig.Timescale.SLOWER_THAN_REALTIME, is_bg, is_border) ["rt"]: - namedNode.node.set_node_style(StyleConfig.Timescale.REALTIME, is_bg, is_border) + generic_node.set_node_style(StyleConfig.Timescale.REALTIME, is_bg, is_border) ["faster_trt"]: - namedNode.node.set_node_style(StyleConfig.Timescale.FASTER_THAN_REALTIME, is_bg, is_border) + generic_node.set_node_style(StyleConfig.Timescale.FASTER_THAN_REALTIME, is_bg, is_border) #Set highlighted element on signal func _on_element_over(element_name, click: bool): @@ -129,12 +135,10 @@ func _on_element_over(element_name, click: bool): #Return a node by its name in the displayes_node_list func get_node_by_name(node_name : String) -> GenericDisplay: for displayed_node in displayed_node_list: - if (displayed_node.name == node_name && displayed_node.node != null): - return displayed_node.node - var displayed_node_list_string = "" - for displayed_node in displayed_node_list: - displayed_node_list_string += displayed_node.name - print(node_name + " not found in " + displayed_node_list_string) + if (displayed_node.name == node_name && displayed_node != null): + return displayed_node + #print(node_name + " not found in :") + #print(displayed_node_list) return null #Get a list of elements connected to inputed element @@ -155,14 +159,14 @@ static func get_middle_x(node) -> int: return node_position.x + node_size.x / 2 - StyleConfig.Link.WIDTH / 2 #Return middle bottom position of a node -func get_bottom_side(node) -> Vector2: +func get_bottom_side(node: Node) -> Vector2: var node_position = node.global_position var node_size = node.size var corrected_position = Vector2(DT_PT.get_middle_x(node), node_position.y + node_size.y) return corrected_position #Return middle top position of a node -func get_top_side(node) -> Vector2: +func get_top_side(node: Node) -> Vector2: var node_position = node.global_position var corrected_position = Vector2(DT_PT.get_middle_x(node), node_position.y) return corrected_position @@ -248,22 +252,25 @@ func update_provided_things(operator_container : HBoxContainer, machine_containe #Update a node with Fuseki element data by creating a generic display node func update_node_with(visual_container : HBoxContainer, fuseki_node_data : Dictionary): - DT_PT.free_all_child(visual_container) + #DT_PT.free_all_child(visual_container) for key in fuseki_node_data.keys(): - var new_node = GenericDisplay.instantiate() - new_node.set_text(key) - visual_container.add_child(new_node) - var displayed_element = NamedNode.new() - displayed_element.name = key - displayed_element.node = new_node - displayed_node_list.append(displayed_element) - set_starting_node_style(displayed_element, fuseki_node_data[key]) - -#Transform fuseki link data (a list a link) to a dictionary -func to_link_dictionary(fuseki_link_data) -> Dictionary: - var links : Dictionary = {} #desination as key -> list + var dt_component = get_node_by_name(key) + if dt_component == null: + var new_node : GenericDisplay = GenericDisplay.instantiate() + new_node.name = key + new_node.set_text(key) + visual_container.add_child(new_node) + displayed_node_list.append(new_node) + set_starting_node_style(new_node, fuseki_node_data[key]) + else: + set_starting_node_style(dt_component, fuseki_node_data[key]) + +#Transform fuseki link data to a dictionary (source node -> destination node) +func to_link_dictionary(links, fuseki_link_data): for link in fuseki_link_data: var source_node = get_node_by_name(link.source) + if source_node == null: + return {} if(not links.has(source_node)): links[source_node] = [] links[source_node].append(get_node_by_name(link.destination)) @@ -278,7 +285,7 @@ func get_appropriate_link_color(is_in_critical_path : bool): return StyleConfig.Link.HIGHLIGHT_COLOR if is_in_critical_path else StyleConfig.Link.DIMMED_COLOR #Draw from element node on y axis to linking lane on x axis, with an arrow if destination of link -func draw_element_to_lane(node, drawable_y_position : int, color : Color, destination : bool = false) -> int: +func draw_element_to_lane(node, drawable_y_position : int, color : Color, destination : bool = false): var is_pointing_up : bool = node.global_position.y < drawable_y_position var drawing_position_element : Vector2 = get_bottom_side(node) if (is_pointing_up) else get_top_side(node) var adjusted_x : int = get_drawable_x_position(drawing_position_element.x) @@ -307,22 +314,22 @@ func draw_link_lane(x_drawn : Array[int], x_highlight : Array[int], drawable_y_p draw_line_differed(Vector2(most_left_highlight_x_position, drawable_y_position), Vector2(most_right_highlight_x_position, drawable_y_position), StyleConfig.Link.HIGHLIGHT_COLOR, StyleConfig.Link.WIDTH, true) #Get not already drawed y -func get_drawable_y_height(key, array_nodes: Array) -> int: - var potential_y_position = (key.global_position.y + key.size.y + array_nodes[0].global_position.y) / 2 +func get_drawable_y_height(key_node: Node, array_nodes: Array) -> int: + var potential_y_position = (key_node.global_position.y + key_node.size.y + array_nodes[0].global_position.y) / 2 return get_viable_position(potential_y_position, already_drawn_y, 1) #Get not already drawed x func get_drawable_x_position(potential_x : int) -> int: return get_viable_position(potential_x, already_drawn_x, 1) -func get_drawable_y_position_for_container_side(side : ContainerSide, key : Object, links : Array) -> int: +func get_drawable_y_position_for_container_side(side : ContainerSide, key_node : Node, links : Array) -> int: match side : ContainerSide.ANY : - return get_drawable_y_height(key, links) + return get_drawable_y_height(key_node, links) ContainerSide.TOP : - return get_viable_position(get_top_side(key).y - StyleConfig.Link.MEAN_OUTER_LINK_DISTANCE, already_drawn_y, 1) + return get_viable_position(get_top_side(key_node).y - StyleConfig.Link.MEAN_OUTER_LINK_DISTANCE, already_drawn_y, 1) ContainerSide.BOTTOM : - return get_viable_position(get_bottom_side(key).y + StyleConfig.Link.MEAN_OUTER_LINK_DISTANCE, already_drawn_y, 1) + return get_viable_position(get_bottom_side(key_node).y + StyleConfig.Link.MEAN_OUTER_LINK_DISTANCE, already_drawn_y, 1) return 0 func get_viable_position(potential : int, concerned_list : Array[int], iteration : int) -> int: @@ -336,22 +343,23 @@ func get_viable_position(potential : int, concerned_list : Array[int], iteration return potential #With Fuseki link data draw those links -func update_link_with(fuseki_link_data, force_side_source : ContainerSide = ContainerSide.ANY): - if(fuseki_link_data == null): - return - var links_as_dict : Dictionary = to_link_dictionary(fuseki_link_data) - for key in links_as_dict.keys(): - var drawable_y_position : int = get_drawable_y_position_for_container_side(force_side_source, key, links_as_dict[key]) +func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSide = ContainerSide.ANY): + for key_node in links_as_dict.keys(): + if key_node == null or links_as_dict[key_node][0] == null: + continue + var drawable_y_position : int = get_drawable_y_position_for_container_side(force_side_source, key_node, links_as_dict[key_node]) var x_drawn_list : Array[int] = [] var x_highlight_list : Array[int] = [] - var source_in_critical_path : bool = in_critical_path(key, links_as_dict[key]) + var source_in_critical_path : bool = in_critical_path(key_node, links_as_dict[key_node]) var source_color : Color = get_appropriate_link_color(source_in_critical_path) - var source_x = draw_element_to_lane(key, drawable_y_position, source_color) + var source_x = draw_element_to_lane(key_node, drawable_y_position, source_color) x_drawn_list.append(source_x) if(source_in_critical_path): x_highlight_list.append(source_x) - for association_element in links_as_dict[key]: - var destination_in_critical_path : bool = in_critical_path(key, [association_element]) + for association_element in links_as_dict[key_node]: + if association_element == null or links_as_dict[key_node][0] == null: + continue + var destination_in_critical_path : bool = in_critical_path(key_node, [association_element]) var arrow_color : Color = get_appropriate_link_color(destination_in_critical_path) var drawn_x : int = draw_element_to_lane(association_element, drawable_y_position, arrow_color, true) x_drawn_list.append(drawn_x) @@ -364,6 +372,9 @@ func update_link_with(fuseki_link_data, force_side_source : ContainerSide = Cont #Feed fuseki data func feed_fuseki_data(feed): fuseki_data = feed + for displayed_node in displayed_node_list: + displayed_node.get_parent().remove_child(displayed_node) + displayed_node_list.clear() on_fuseki_data_updated() await get_tree().create_timer(0.2).timeout CameraSignals.zoom_to_fit.emit(get_rect()) @@ -372,7 +383,6 @@ func feed_fuseki_data(feed): func on_fuseki_data_updated(): if fuseki_data == null: return - displayed_node_list.clear() update_node_with(service_container, fuseki_data.service) update_node_with(enabler_container, fuseki_data.enabler) update_node_with(model_container, fuseki_data.model) @@ -382,6 +392,22 @@ func on_fuseki_data_updated(): update_node_with(env_container, fuseki_data.env) update_node_with(sys_container, fuseki_data.sys_component) update_node_with(data_container, fuseki_data.data) + + update_link_dicts() + +func update_link_dicts(): + links_as_dict_enabler_to_service.clear() + links_as_dict_model_to_enabler.clear() + links_as_dict_service_to_provided_thing.clear() + links_as_dict_sensor_to_data_transmitted.clear() + links_as_dict_data_to_enabler.clear() + links_as_dict_data_transmitted_to_data.clear() + to_link_dictionary(links_as_dict_enabler_to_service, fuseki_data.enabler_to_service) + to_link_dictionary(links_as_dict_model_to_enabler, fuseki_data.model_to_enabler) + to_link_dictionary(links_as_dict_service_to_provided_thing, fuseki_data.service_to_provided_thing) + to_link_dictionary(links_as_dict_sensor_to_data_transmitted, fuseki_data.sensor_to_data_transmitted) + to_link_dictionary(links_as_dict_data_to_enabler, fuseki_data.data_to_enabler) + to_link_dictionary(links_as_dict_data_transmitted_to_data, fuseki_data.data_transmitted_to_data) func _process(_delta): already_drawn_x.clear() @@ -400,13 +426,13 @@ static func free_all_child(node : Node): #Draw all links func _draw(): - if (not fuseki_data == null): - update_link_with(fuseki_data.enabler_to_service) - update_link_with(fuseki_data.model_to_enabler) - update_link_with(fuseki_data.service_to_provided_thing, ContainerSide.TOP) - update_link_with(fuseki_data.sensor_to_data_transmitted) - update_link_with(fuseki_data.data_to_enabler) - update_link_with(fuseki_data.data_transmitted_to_data, ContainerSide.BOTTOM) + if (fuseki_data): + update_link_with(links_as_dict_enabler_to_service) + update_link_with(links_as_dict_model_to_enabler) + update_link_with(links_as_dict_service_to_provided_thing, ContainerSide.TOP) + update_link_with(links_as_dict_sensor_to_data_transmitted) + update_link_with(links_as_dict_data_to_enabler) + update_link_with(links_as_dict_data_transmitted_to_data, ContainerSide.BOTTOM) draw_all_differed() # Rabbit MQ data integration --------------------------------------------------- @@ -537,15 +563,24 @@ func rename_dict_key(dict: Dictionary, old_key, new_key) -> void: for i in range(keys.size()): dict[keys[i]] = values[i] +func delete_component(node_name, fuseki_dict, parent_container: Node): + fuseki_dict.erase(node_name) + get_node_by_name(node_name).queue_free() + displayed_node_list.erase(get_node_by_name(node_name)) + +func rename_component(fuseki_dict, node_name, new_node_name): + rename_dict_key(fuseki_dict, node_name, new_node_name) + get_node_by_name(node_name).name = new_node_name + # Edit the FusekiData of a specific node func edit_node(node_name: String, parent_container: Node, delete: bool, new_node_name: String, new_node_description: String): match parent_container: service_container: if delete: - fuseki_data.service.erase(node_name) + delete_component(node_name, fuseki_data.service, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.service, node_name, new_node_name) + rename_component(fuseki_data.service, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.service[node_name]["desc"] = [new_node_description] @@ -553,10 +588,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node enabler_container: if delete: - fuseki_data.enabler.erase(node_name) + delete_component(node_name, fuseki_data.enabler, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.enabler, node_name, new_node_name) + rename_component(fuseki_data.enabler, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.enabler[node_name]["desc"] = [new_node_description] @@ -564,10 +599,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node model_container: if delete: - fuseki_data.model.erase(node_name) + delete_component(node_name, fuseki_data.model, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.model, node_name, new_node_name) + rename_component(fuseki_data.model, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.model[node_name]["desc"] = [new_node_description] @@ -575,10 +610,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node data_container: if delete: - fuseki_data.data.erase(node_name) + delete_component(node_name, fuseki_data.data, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.data, node_name, new_node_name) + rename_component(fuseki_data.data, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.data[node_name]["desc"] = [new_node_description] @@ -586,10 +621,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node operator_container: if delete: - fuseki_data.provided_thing.erase(node_name) + delete_component(node_name, fuseki_data.provided_thing, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.provided_thing, node_name, new_node_name) + rename_component(fuseki_data.provided_thing, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] @@ -597,10 +632,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node machine_container: if delete: - fuseki_data.provided_thing.erase(node_name) + delete_component(node_name, fuseki_data.provided_thing, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.provided_thing, node_name, new_node_name) + rename_component(fuseki_data.provided_thing, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] @@ -608,10 +643,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node data_transmitted_container: if delete: - fuseki_data.data_transmitted.erase(node_name) + delete_component(node_name, fuseki_data.data_transmitted, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.data_transmitted, node_name, new_node_name) + rename_component(fuseki_data.data_transmitted, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.data_transmitted[node_name]["desc"] = [new_node_description] @@ -619,10 +654,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node sensor_container: if delete: - fuseki_data.sensing_component.erase(node_name) + delete_component(node_name, fuseki_data.sensing_component, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.sensing_component, node_name, new_node_name) + rename_component(fuseki_data.sensing_component, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.sensing_component[node_name]["desc"] = [new_node_description] @@ -630,10 +665,10 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node env_container: if delete: - fuseki_data.env.erase(node_name) + delete_component(node_name, fuseki_data.env, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.env, node_name, new_node_name) + rename_component(fuseki_data.env, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.env[node_name]["desc"] = [new_node_description] @@ -641,11 +676,12 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node sys_container: if delete: - fuseki_data.sys_component.erase(node_name) + delete_component(node_name, fuseki_data.sys_component, parent_container) else: if node_name != new_node_name: - rename_dict_key(fuseki_data.sys_component, node_name, new_node_name) + rename_component(fuseki_data.sys_component, node_name, new_node_name) node_name = new_node_name if new_node_description: fuseki_data.sys_component[node_name]["desc"] = [new_node_description] update_node_with(sys_container, fuseki_data.sys_component) + update_link_dicts() diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 3151cc7..5057281 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -235,6 +235,8 @@ func update_chart(last_data) -> void: # Style -------------------------------------------------------------- ## Highlights func set_default_style(): + set_node_style(bg_color, true, false) + set_node_style(border_color, false, true) change_bg_color(bg_color) change_border_color(border_color) change_text_color(Color.BLACK) @@ -261,7 +263,7 @@ func change_text_color(color: Color): ## Style definitions func set_node_style(_color: Color, is_bg: bool, is_border: bool): - if is_bg: + if is_bg: bg_color = _color change_bg_color(bg_color) if is_border: @@ -274,4 +276,3 @@ func _on_delete_button_pressed() -> void: GenericDisplaySignals.generic_display_edit.emit(node_name, parent_container, true, "", "") CameraSignals.enable_camera_movement.emit() CameraSignals.enable_camera_zoom.emit() - queue_free() From a889f6fa6c71442357da1a673f9c91511b98f427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 2 Oct 2025 01:50:33 -0400 Subject: [PATCH 27/40] Fixed empty description --- DTContainer/DT&PTContainer.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 1fe7da2..cac0a2c 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -76,6 +76,8 @@ func set_starting_node_style(generic_node : GenericDisplay, attributes : Diction generic_node.set_visualization() if ("desc" in attributes.keys() and attributes["desc"][0] != ""): generic_node.set_description(attributes["desc"][0]) + else: + generic_node.set_description("") # Visual style generic_node.set_default_style() generic_node.set_node_style(StyleConfig.DTElement.DIMMED_COLOR, true, false) From 14ba6d5be33dede8da44a483982421e651522e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 2 Oct 2025 01:54:38 -0400 Subject: [PATCH 28/40] Set default description to empty --- DTContainer/DT&PTContainer.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index cac0a2c..4723ffc 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -456,7 +456,7 @@ func _on_node_bg_option_item_selected(index: int) -> void: ## Adding nodes to containers var new_node_index = 1 -var default_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": ["New description here"] } +var default_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": [""] } func _on_add_component_button_pressed_env() -> void: var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() From d4cf444157cd14e98db685f253030026a92d6856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 2 Oct 2025 02:04:12 -0400 Subject: [PATCH 29/40] Upgraded to 4.5 --- incubator.pck | Bin 2932872 -> 2932792 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/incubator.pck b/incubator.pck index b29e952c829cb3b6b1a651ccb16835ed0e1ef8b9..e29fb00c03ce204cf393de78c8d3446e556930f7 100644 GIT binary patch delta 3573 zcmX}rcQhN^`vCC7h!wSJ)k;vSu~!u}YVX-nwX39>M6L1$p`?^3r5by`rAD=?LXFVY zDp8|+?OEidMo}~2m%iWc`Q39q=Q;N|_dd_Pe>_QX?}1IR?}6zMAZHvD3k71KbXX`o z7RrEyGGd`1ER+cgWyV5TuuxVklno07W1;L==mjj40}JK+0Oc}F$IO}oE}7|HFbb-) z52Ob>V1WWh1{h-7rWeoHwYXsTu$GczQOU#MhD5yIvrT44;G4$H`;b821;2aA1DQH@ zQO4v&vCMOf`XOfl7{fshdnQ2 z#R8w0%Y}qE(z$ef$ING_FT%3i2Rb6ORNS_{-q+9w+pl~s0kT8(fl)ce&97`Sa=20l z3ss|HBv%VwiD~=m*#?;PXTVajASxuek1G=s{L{PP+ajYEKhL7kxUAzq5 zdyYq&lslMwy-ys8B;s!&OfAN0vgmgv^$X_Mm-@M=pJQ=;mkk3uA5%PFI+rDmiLKO~ zpfZcrO?NT2RMRsMX4ij*?U7xo(jsM0D8o9D8#{&r zB(N8#!Yg(|)tR;AzW9psvBWY*x(u;&71|FWg@}CLAobn-D zpXiy``K5$Y^cfhlgIe2u9i$@m{u41x@o#mXQo!H{U&Xqoubih4Y|r>m1(YjuD(}27 zm}G*?+{jzi7#@^62K>OCjx+ zP0DC1%n$j>%lXw<<<1^omw{{dVM6ihz8jsZm3hI8+&~iIgSg(yQZIwX;aH`Au2Cao ze!h4oBHJ}HIjvecdpF&mCnrzDaM0W9FA_G~I9OasGDl)S6};GnaVt#eF)FWeB+MWG zl%10i>S?iCG*V*AR$>!2VeD?cIFOq6PIFt|IsoK8NLEhqq1(D+9hKiMofZcjB%KIG zo{XxCL?6P7^P)d?M5ic@(IhiqwmHxs|u;Vb^ZH>k-3 zVVx4A-gw|HVR$-%!GSlAs_My5?GX)iM4sA4D1GZrgdcZ9a;W3@Tx@-RBwx~*c-A$S zrURvjysk#=rw1anW*?-bBCj|#pOAK}a+t2oc?{pj>2kh3xN5slWKBTZ>2cbR*mPJ; zep%f!k3IX2C}rlzWBG+6 zcyjqsPDFy+LyyCAA5(x`U&WgZ!-dMUpb% zX87QWLG$-QvN4dvg68i%RAn%buco zPa(gtzn{mv^~-wW*X%>Mw{~V)+3dW55B6}QcpiJ|&o|Af-KX0!-&5r+$R>k=$;*rJ zW@2_BRZn29^jdqU)5+e?U5BK2x7EkQk9%k9z7o`>ir43nBWCM)3%Yw2Qt>BnW4bxv0X=;yymk&V8{r&A*NAcH-0gszmtkK;b#-(5Lq6y}5~=Wj_>8Z_38EjJx~V=2 zhJQ$7y4xJw*VXjkq1&_Jmm`!4!*5x)l08qlo89_n1i^VtkuR4?*S)#ts#S**{ZA}r z)YQ5$(Etxs!q1Sx8Cz+hnENgL(~Ti&r51Xp4T_xT|XBeY+0OgxG#kTUP1lk#Z3szD~w;SL29xZsmJ6gv@7`tzzuU&VRQi`={>} z^cC&LMyKX`N^^V41n#g>YtD7f;aq>AiE&hP@=^T8!LDN zoxLfUKYMt6M9vdzlAe8C17MRM(-6Xa0BNu8p+7IllWf}tc3mJpKN<6#@M=J&N#7RJ zX~2HC;?W}7QXqhev5opb-SJ=4Tm+?7dQYyRE?lW0Qi*b-Y1lBy+n#&`A?L$_+iYYf zZLQ^J>&^`C4c6o*wiFX%;NfKf=?~QxZ9)P6zwmiETw4COPyri*dpvJcr?@0}MK8L< z@5%gUXt?9{QDKIY^MXP2bLSWpnIC0eQDR2S{7-YEGw2inIsiq$HW`PxdtZC$1`|$f zgwJRiEFIIgTvGeAkzaBuI3Q)IZYr61xN%ei`*{AOg2;;3&gD`T*sTeuUP5_!HOV|o zqq3a!r7d(Q9dC&4)&0J_C!ZoET1Ukx0^Gij_lVWUpV(@Z{4$;vh88zAH|ioVt`tcG%HSFT8$@xRzJ$&thGqMs6g*p!LO0EV-t}Y&ZP1ms}d|MdP(Iw-M3Q<{2j-vOdKxn31z@D z4HpS`dg(^GL_3f)jSP(}jU3Gtf*lB^R0HH7#MQ&T0`d17VETA+1B|!v0}Mt7+`-Ee zV3xojd?W#e0(#;9`4jXBu1zo|Iv|AL{Sn3lREE^U{+%aLy8n)qoV>WiXVH-~&F(1w zqMCaBKUroPxGxH+5EAaMgwJV#X#&&n!!0mv;1Hg@6{dO33n=~P93YY5-vDSg-2fjflGR@i4CP#=$Nhuxrs zzINKfz3{w5n5Bq4%YTc0^G;Uq$;y9wC?)^=oAt_*KUraXClMyV{J(O=_yQtK4`P?Y z_3sV#|9xq^XgzyPYElf8_-@cM{O2{ieg}*n4{3lgvM9h2L8#zRfE^y%L2Imp_v?U3 F{2%=Kus{F+ delta 3667 zcmYMvcRbbq7YFcr?JKgmwrhr~YsqpOK zhXq6LO$cvo3d*6vM#?-HeVA^^6nr+j@&)Br^pati6>a3_4>49U(L#H?e*9X~UbJ(5 zu~bOb_saKFOYl7CA!rViqE2lUH;Q?|X_lRQGdV1dZEBi8Am{_UxMbwFHSX=+73g!d zbM>kq*MCMA0KC#lm6ywKN12(oBDp^-G&G(=SjUs$>yzPK2PH4m)|o=lj~lz@%U`Tn zGwwiGtfv=*C8yuueHtV^Z~J%=6|&p{6u0J`ZkuN}?eEy+AE1I>KT#q#e{*fxf8o&Y zeUS8i7SoxUHHR6|CnXOxhYvIhSPN_HyBO}foZFvQ2lnR)81gd=;;H!8dCZsjnPr4= z(>{IsQS-?*0fx+2pr0u#k@wL+JqXW6)dr_}| zL5@$j&;vYDQDx#CC%fT4^Qi2k zg*Nh$j0mH{Gj6(dL{Btd0;>(klJj|7QBfowA94fL`$EBdR9vXVlN-cv7;S6YeCAA< zOj)Vib2+(E>CzXnE*YEgyHY!wf7O031GLcJvO_MFr#-T0+#D`anc<2KeQf5awvc|q zeoaJln!<^pJEJ4cqgs4D^l!*YSmo4AwL{aKfA#H%MyI6Gz8H@StUOYN=*iU92Mi@D z3C|42l8Bn~T{VGGLV-BaV`AmrPWwHlAEbMUE8&zMJwr>V4A;&X8HWv1%8!m2LBOHQ zbqSSN&@sb7N+62BnVec39A6V%nNO0CR%5x5LUY@f#yq$mu$P!iTZ!fQ+J#p*m+`U- zmE(02JtrG)c~_f4YXy42NSl{@YsNV)HD>WfDtuj6=VU%s+e7PWS8d8`s(BlJzn2u) z6klpz^{q;f#8otvRMfJz6UG4%1&tSX+v3vK=w)6SOK^+>i)PA?I2BZ{9vVaQ&U3bk z;%is+j@?`Hp!izW0VOkYWo5A^jhV-X-|m-thsX)OKI@kAHA_%k)N*@g=hlN^&?6Jc z`_2yC%aqCG3RR(`uIEZjowsVrN@}Ccq`5Q1&OlGcpLcPf%V;^|k#x5OteleL#Dx&3#HFk{gj70m^df9A`*bw~Tp z6$q>K%n|hZU+jmAU2<+mGU=uTtZ;A66co4)@#HVPfZ#tKnE-JQKP`D-W(d%tB#nuA%6Y)zS<{@h6Y+3OMQL2izGO&+(6)EX}*F5^iN z*Tknd)Dzj4rhtHVWfoBLpNA)#nD~~9*S%kMRYm2E3Iwi3dm^t0Bp;rVuZ}j{bh8$v z$g9p2=sBvh1bsl8$N6kq<}mYxsM}X+@U~11E$WdYndY}B`rcOQna|62kcFMEaNEIe zQ|PU(g&!WBY?4oiB;pC9{*TQnXZlXlg$rmkU^2O`5x7{*Tiw8$^zIb}tNJMH;pncC zZ}pXi0Qy87D`%0mTP!E!yfKvAR5Dd3KlH5u^>}X&X?N}FsPddS_WvtXH7T!2B9W+W#SMFsheXFs5vp>5rxF}}8 z*R9~#76RC^iq;;v(vM7bDGl9M0>9siY;C3l`AwJChffa;qtlCo``lCJm|h_h`U{~U z;q2sr6LN;svVf*m6rmprY+sJ}(5-X8nDHWDwOGoWX$zBXpEaQ?uDsG*+Z?P&<(Hx^ zmdN2#>KZTZxD4ur3R`z?eZI#q`g-+2Xfj)Y3oyl!pX5qf=o6E*9=kqOk~_bo-3uEE znYW)Dv)2yc@o=}#zfSt(_0PI8hM+wi_azu}h@K6P>2=&^zR@+e5;&0CL-ay$^aS-` z73(+l4AgJCZb0x%`qx~_qaxTsOqF7_uB%4yz`F;2g{jWIQhUddPgV@dpg=cc!jxu< z0Bc|;KNCPUUNb%F8#yV{2DTeu0h2KT!y4m*H~B5=kFd(5H1Tb}=5$0reg1trSEYO3 zr<+ShCge$Vq)L&Zc6MLCr@7DV<6y^+lfaGNe`qCSDU)Iwx+(nL2cGxCYg=a$Vtyis zj83u1t?DoT{`J-%F4guq65jplF=_Y<5TI0~9oWylN<5^TDBL_zTfcc{H(tM`S+8^w zk|^-`?q7^|sX&{xzforAZ9YsQ>S|K)?ZS6G?Sqn?9ig@7H%w0IL=1)df>;9H4ILhd zyJx|>qy#VL=?7i@ng1*ICNcr>;mOX>`g`{+P59PDT^Evmk!aF^({UQT3jw;de`!W` zSyQ%sWqNZ>rnt#KsHhydw<5Fql}W5Cl_xQISJQG9g#{0y5+6l8fU z+r;S<7fNj!|Fg1A!hoWB(umQB(@4<3Xe4Q*XryUmh`V@M z@@?R*6zHK_u-W39GwtAK6(8u-5oilj+(eNkdvK(VC`hO`xci=K8s|klQv*Fq-hBaT z!gTP%5i`dWMRnE@YWS}_#VLG^pzg***Bh!jmDd8~53&UzD=4W*V;T7%IlGzs+`oa6 z&E&fCsQbq--O`0j`fm{JZjqHMzy*R0y% zM(W{cB=p~wIMvS}$U@)M@8X7F6Y$iAE~mWvWHvkaxK4X|)cfg?dvWz+VWZK~ZeW;P zSYMbK;)}i+%`$a*M;h-dS)A(}t~QWv4!3%GrYddqo>L6Z`Bg}bW>Ji#7}tP$OgguW zt=%PgbLq5%lh4c)ViX5*oxBYIX5TBRISWU3c%!nJNwG|dKri`_v@Rf!khjTSo8`mG zXZe)jEME<;SxLCb(J^n46gP2Hp!6SHcC>T~w2U@_;~~5U)@j1j7B6zdPC|FyJgY)FS}r zM1UJ!<^5mi6X0?8mODByuu0zz5~-|0lfxX>aAYMdjuAf9me@ z@3piJ@g)I%mJYm#o2!FcfZcHNM0gg3_!F9ncxMtc*#|A9- zXF87IPXRfUoU2xltaz$y9d?e8$YTDx%SnU!?~!Liyio2zpzSK$*>`Yb@FFqz9lRY3 zPQ_`who1*4<09VE4!6N|(u$}B5t-Utr^X;k2XWgZxByO>0B3}p^+5#%hM+-)xDgWV M!xA{Y7P!>E0LB*Qs{jB1 From 1e1936dfca4c9cd88459ebbce7068c4b81bbe94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Thu, 2 Oct 2025 02:14:02 -0400 Subject: [PATCH 30/40] Improved link handling --- DTContainer/DT&PTContainer.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 4723ffc..5372e75 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -686,4 +686,5 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node if new_node_description: fuseki_data.sys_component[node_name]["desc"] = [new_node_description] update_node_with(sys_container, fuseki_data.sys_component) + fuseki_data.build_relations() update_link_dicts() From b48b6293cfeb10f94e6542010f331ac63c6ae4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Fri, 3 Oct 2025 10:13:14 -0400 Subject: [PATCH 31/40] Fixed removing nodes breaking other links --- DTContainer/DT&PTContainer.gd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 5372e75..afa11aa 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -347,7 +347,11 @@ func get_viable_position(potential : int, concerned_list : Array[int], iteration #With Fuseki link data draw those links func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSide = ContainerSide.ANY): for key_node in links_as_dict.keys(): - if key_node == null or links_as_dict[key_node][0] == null: + if key_node == null: + continue + # Remove null values from the array + links_as_dict[key_node] = links_as_dict[key_node].filter(func(e): return e != null) + if links_as_dict[key_node].is_empty(): continue var drawable_y_position : int = get_drawable_y_position_for_container_side(force_side_source, key_node, links_as_dict[key_node]) var x_drawn_list : Array[int] = [] @@ -359,8 +363,6 @@ func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSi if(source_in_critical_path): x_highlight_list.append(source_x) for association_element in links_as_dict[key_node]: - if association_element == null or links_as_dict[key_node][0] == null: - continue var destination_in_critical_path : bool = in_critical_path(key_node, [association_element]) var arrow_color : Color = get_appropriate_link_color(destination_in_critical_path) var drawn_x : int = draw_element_to_lane(association_element, drawable_y_position, arrow_color, true) From 1c39fb0cb32118f525de431794c4417ed0002773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Mon, 27 Oct 2025 13:24:33 -0400 Subject: [PATCH 32/40] Better DT component tooltip visibility --- GenericDisplay/generic_display.tscn | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index d023fa2..c4653b8 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -1,10 +1,24 @@ -[gd_scene load_steps=10 format=3 uid="uid://tvcjlrt40n0q"] +[gd_scene load_steps=12 format=3 uid="uid://tvcjlrt40n0q"] [ext_resource type="Script" uid="uid://ost4ju4q0ybe" path="res://GenericDisplay/GenericDisplay.gd" id="1_5lixw"] [ext_resource type="Script" uid="uid://jeh5r2brp2mt" path="res://GenericDisplay/PopUpChart/PopUpChartControl.gd" id="2_5nhr6"] [ext_resource type="PackedScene" uid="uid://dlwq4kmdb3bhs" path="res://addons/easy_charts/control_charts/chart.tscn" id="3_0c6qw"] [ext_resource type="Script" uid="uid://qe3n3obtygdn" path="res://GenericDisplay/PopUpScript/PopUpScriptControl.gd" id="3_ipcor"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6yx0x"] +content_margin_left = 10.0 +content_margin_top = 5.0 +content_margin_right = 10.0 +content_margin_bottom = 5.0 +bg_color = Color(0, 0, 0, 0.7490196) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="Theme" id="Theme_5nhr6"] +TooltipPanel/styles/panel = SubResource("StyleBoxFlat_6yx0x") + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2niug"] content_margin_left = 20.0 content_margin_top = 10.0 @@ -61,10 +75,11 @@ corner_detail = 5 viewport_path = NodePath("GenericDisplay/VisualizationContainer/HBoxContainer/VizPopUpButton/PopupPanel/TextureRect/SubViewport") [node name="GenericDisplayContainer" type="PanelContainer"] -offset_right = 97.0 +offset_right = 123.0 offset_bottom = 43.0 size_flags_vertical = 4 mouse_default_cursor_shape = 2 +theme = SubResource("Theme_5nhr6") theme_override_styles/panel = SubResource("StyleBoxFlat_2niug") script = ExtResource("1_5lixw") @@ -180,8 +195,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 516.0 -offset_bottom = 516.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("2_5nhr6") @@ -220,8 +235,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 396.0 -offset_bottom = 221.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 @@ -291,8 +306,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 4.0 offset_top = 4.0 -offset_right = 516.0 -offset_bottom = 516.0 +offset_right = -4.0 +offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_ipcor") From e6cebdec9151402920e02ffa9fccc9b55b29b853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Mon, 27 Oct 2025 13:25:20 -0400 Subject: [PATCH 33/40] Small optimization --- DTContainer/DT&PTContainer.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index afa11aa..4796c9e 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -689,4 +689,3 @@ func edit_node(node_name: String, parent_container: Node, delete: bool, new_node fuseki_data.sys_component[node_name]["desc"] = [new_node_description] update_node_with(sys_container, fuseki_data.sys_component) fuseki_data.build_relations() - update_link_dicts() From bcc32c30635be967a04ae210d2e15a347a6f86c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Mon, 16 Feb 2026 16:46:58 -0500 Subject: [PATCH 34/40] Visual improvements and fixes (#6) * Drag and drop for creating arrows * Create links between nodes Arrows from containers "System Env" and "System" to containers "Models" and "Data" are still not correctly drawn * Correcting the highlighting of the nodes that have links created by the user * correcting a dragging problem caused by small or/and involuntary movements * Handle the removal of the links when nodes are removed manually * Add the manual deletion of links with a check button * Change the way of selecting links to delete Previously, it was only possible to select links via their horizontal lines. Now it is possible to select them via their vertical lines too. * Better selection of links to remove Highlight the selected link in red when deletion mode is active. When a node is selected, only links related to that node can be deleted. * Links can only be added if visual editing is checked * Fixed typo * add a button to call a Python script * Added dumping of missing characteristic * Add a popup window to select a Python script to execute when "Call a script" is pressed It still needs some graphics improvements * Change Edit button of nodes from "i" to "Edit" * Change size of edit button for a better fit in the space --------- Co-authored-by: paul-moreau <112432533+paul-moreau@users.noreply.github.com> --- Config/FusekiConfig.gd | 1 + DTContainer/DT&PTContainer.gd | 309 +++++++++++++++++++++++- Fuseki/Caller/FusekiQuery.gd | 9 + Fuseki/Data/FusekiData.gd | 2 + Fuseki/Dump/FusekiDumper.gd | 2 +- GenericDisplay/GenericDisplay.gd | 23 +- GenericDisplay/GenericDisplaySignals.gd | 4 + GenericDisplay/generic_display.tscn | 25 +- MainScene/MainScene.tscn | 162 +++++++------ MainScene/python_runner.gd | 7 + MainScene/python_runner.gd.uid | 1 + ScriptRunnerPanel.tscn | 61 +++++ script_runner_panel.gd | 48 ++++ script_runner_panel.gd.uid | 1 + script_test.py | 2 + 15 files changed, 559 insertions(+), 98 deletions(-) create mode 100644 MainScene/python_runner.gd create mode 100644 MainScene/python_runner.gd.uid create mode 100644 ScriptRunnerPanel.tscn create mode 100644 script_runner_panel.gd create mode 100644 script_runner_panel.gd.uid create mode 100644 script_test.py diff --git a/Config/FusekiConfig.gd b/Config/FusekiConfig.gd index e716b97..61d162b 100644 --- a/Config/FusekiConfig.gd +++ b/Config/FusekiConfig.gd @@ -28,6 +28,7 @@ class JsonHead: const C8 = "c8" const C9 = "c9" const C12 = "c12" + const C13 = "c13" const C14 = "c14" const C15 = "c15" const C16 = "c16" diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 4796c9e..6baa435 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -34,6 +34,10 @@ var highlighted_element = null var all_highlighted_element = [] var highlighted_with_click = false +var deletion_mode = false +var hovered_link = null +var visual_editing_mode = false + # Border and background views var bg_view = 0 var border_view = 0 @@ -43,18 +47,121 @@ func _ready(): GenericDisplaySignals.generic_display_over.connect(_on_element_over) GenericDisplaySignals.generic_display_edit.connect(edit_node) RabbitSignals.updated_data.connect(_on_rabbit_data_updated) + + GenericDisplaySignals.start_drag.connect(_on_start_drag) + GenericDisplaySignals.dragging.connect(_on_dragging) + GenericDisplaySignals.stop_drag.connect(_on_stop_drag) var buttons = get_tree().get_nodes_in_group("main_buttons") for btn in buttons: if btn.name == "VisualEditingButton": btn.toggled.connect(_on_visual_editing_toggled) _on_visual_editing_toggled(btn.button_pressed) + elif btn.name == "LinkDeletionButton": + btn.toggled.connect(_on_link_deletion_toggled) + +func _input(event): + if not deletion_mode: + return + if event is InputEventMouseMotion: + var mouse_pos = get_global_mouse_position() + var link = get_link_under_mouse(mouse_pos) + if not link.is_empty(): + hovered_link = link + if link["type"] == "fuseki": + Input.set_default_cursor_shape(Input.CURSOR_FORBIDDEN) + else: + Input.set_default_cursor_shape(Input.CURSOR_POINTING_HAND) + else: + hovered_link = null + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + elif event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: + if hovered_link != null: + if hovered_link["type"] == "fuseki": + print("Warning: Deleting a Fuseki link (from data source)") + delete_link(hovered_link) + hovered_link = null + Input.set_default_cursor_shape(Input.CURSOR_ARROW) func set_fuseki_data(_fuseki_data: FusekiData) -> void: fuseki_data = _fuseki_data #Node and element manipulation functions ----------------------------------------------------------- +# Drag and drop functions +var dragging_from : GenericDisplay = null +var current_hover_target : GenericDisplay = null +var drag_start_pos : Vector2 +var is_dragging := false +const MIN_DRAG_DISTANCE := 5.0 # pixels, arbitrarily chosen + +func get_node_under_mouse(pos : Vector2) -> GenericDisplay: + for node in displayed_node_list: + if node.get_global_rect().has_point(pos): + return node + return null + +func get_link_under_mouse(mouse_pos: Vector2) -> Dictionary: + const CLICK_TOLERANCE = 2.0 # tolerance pixels for clicking on a link + var links_to_check = drawn_links + if highlighted_element != null: + links_to_check = drawn_links.filter(func(l): return l["source"] == highlighted_element or l["destination"] == highlighted_element) + for link in links_to_check: + var min_x = min(link["source_x"], link["dest_x"]) + var max_x = max(link["source_x"], link["dest_x"]) + var on_horizontal = abs(mouse_pos.y - link["lane_y"]) < CLICK_TOLERANCE and mouse_pos.x >= min_x - CLICK_TOLERANCE and mouse_pos.x <= max_x + CLICK_TOLERANCE + if on_horizontal: + return link + var src_node = link["source"] + var src_y_start = get_bottom_side(src_node).y if src_node.global_position.y < link["lane_y"] else get_top_side(src_node).y + var min_y_src = min(src_y_start, link["lane_y"]) + var max_y_src = max(src_y_start, link["lane_y"]) + var on_vertical_src = abs(mouse_pos.x - link["source_x"]) < CLICK_TOLERANCE and mouse_pos.y >= min_y_src - CLICK_TOLERANCE and mouse_pos.y <= max_y_src + CLICK_TOLERANCE + if on_vertical_src: + return link + var dest_node = link["destination"] + var dest_y_end = get_bottom_side(dest_node).y if dest_node.global_position.y < link["lane_y"] else get_top_side(dest_node).y + var min_y_dest = min(link["lane_y"], dest_y_end) + var max_y_dest = max(link["lane_y"], dest_y_end) + var on_vertical_dest = abs(mouse_pos.x - link["dest_x"]) < CLICK_TOLERANCE and mouse_pos.y >= min_y_dest - CLICK_TOLERANCE and mouse_pos.y <= max_y_dest + CLICK_TOLERANCE + if on_vertical_dest: + return link + return {} + +func _on_start_drag(node): + if not visual_editing_mode: + return + dragging_from = node + current_hover_target = null + is_dragging = false + drag_start_pos = get_global_mouse_position() + +func _on_dragging(node, mouse_pos): + if dragging_from == null: + return + if not is_dragging: + if drag_start_pos.distance_to(mouse_pos) > MIN_DRAG_DISTANCE: + is_dragging = true + if is_dragging: + var hovered = get_node_under_mouse(mouse_pos) + if hovered != dragging_from: + current_hover_target = hovered + +func _on_stop_drag(node): + if dragging_from == null: + return + if not is_dragging: + dragging_from = null + current_hover_target = null + return + if current_hover_target != null and current_hover_target != dragging_from: + _add_user_link(dragging_from, current_hover_target) + # reset + dragging_from = null + current_hover_target = null + is_dragging = false + #Array of displayes nodes var displayed_node_list: Array[Node] var link_dict: Dictionary @@ -146,11 +253,29 @@ func get_node_by_name(node_name : String) -> GenericDisplay: #Get a list of elements connected to inputed element func get_all_connected_to(element_name : String) -> Array[String]: var all_connected : Array[String]= [element_name] - var all_links = fuseki_data.enabler_to_service + fuseki_data.model_to_enabler + fuseki_data.service_to_provided_thing + fuseki_data.sensor_to_data_transmitted + fuseki_data.data_transmitted_to_data + fuseki_data.data_to_enabler + var all_links = [] + all_links += fuseki_data.enabler_to_service + fuseki_data.model_to_enabler + fuseki_data.service_to_provided_thing + fuseki_data.sensor_to_data_transmitted + fuseki_data.data_transmitted_to_data + fuseki_data.data_to_enabler + for src in user_links.keys(): + for dst in user_links[src]: + all_links.append({"source": src.name, "destination": dst.name}) + for src in user_links_top2top.keys(): + for dst in user_links_top2top[src]: + all_links.append({"source": src.name, "destination": dst.name}) + for src in user_links_bot2bot.keys(): + for dst in user_links_bot2bot[src]: + all_links.append({"source": src.name, "destination": dst.name}) for link in all_links: - if link.source == element_name: + var source_name := "" + var dest_name := "" + if typeof(link) == TYPE_DICTIONARY: + source_name = link["source"] + dest_name = link["destination"] + else : + source_name = link.source + dest_name = link.destination + if source_name == element_name: all_connected.append(link.destination) - if link.destination == element_name: + if dest_name == element_name: all_connected.append(link.source) return all_connected @@ -192,6 +317,43 @@ var highlighted_lines : Array[Dictionary] = [] var dimmed_triangles : Array[Dictionary] = [] var highlighted_triangles : Array[Dictionary] = [] +var drawn_links : Array[Dictionary] = [] + +# 3 dictionary with (source node -> [destination node, ...]) +# Differencing the bot and top containers for formatting the arrows correctly +var user_links : Dictionary = {} +var user_links_bot2bot : Dictionary = {} +var user_links_top2top : Dictionary = {} + +# 2 list for top and bottom containers, for formatting the arrows correctly +var top_container := [ + "ServicesContainer", + "MachineContainer", + "OperatorContainer" +] +var bot_container := [ + "DataTransmittedContainer", + "ModelsContainer", + "DataContainer", + "SystemContainer", + "EnvContainer" +] + +# Add link a user-drawn link +func _add_user_link(src: GenericDisplay, dest: GenericDisplay): + if src.get_parent().name in bot_container and dest.get_parent().name in bot_container: + if not user_links_bot2bot.has(src): + user_links_bot2bot[src] = [] + user_links_bot2bot[src].append(dest) + elif src.get_parent().name in top_container and dest.get_parent().name in top_container: + if not user_links_top2top.has(src): + user_links_top2top[src] = [] + user_links_top2top[src].append(dest) + else: + if not user_links.has(src): + user_links[src] = [] + user_links[src].append(dest) + #Add lines to call list func draw_line_differed(start: Vector2, end: Vector2, color: Color, width : int, antialiased: bool): if (color == StyleConfig.Link.DIMMED_COLOR): @@ -239,6 +401,16 @@ func draw_all_differed(): for triangle in triangles_draw_list: draw_triangle(triangle["aimed_at"], triangle["vertical_shift"], triangle["color"], triangle["is_pointing_up"]) +func register_drawn_link(source_node: GenericDisplay, dest_node: GenericDisplay, lane_y: int, source_x: int, dest_x: int, link_dict_type: String): + drawn_links.append({ + "source": source_node, + "destination": dest_node, + "lane_y": lane_y, + "source_x": source_x, + "dest_x": dest_x, + "type": link_dict_type # "normal", "bot2bot", "top2top", "fuseki" + }) + #Update operator container and machine container, differenciate on attibute func update_provided_things(operator_container : HBoxContainer, machine_container : HBoxContainer, provided_data : Dictionary): var operator_data : Dictionary = {} @@ -252,7 +424,7 @@ func update_provided_things(operator_container : HBoxContainer, machine_containe update_node_with(operator_container, operator_data) update_node_with(machine_container, machine_data) -#Update a node with Fuseki element data by creating a generic display node +# Update a node with Fuseki element data by creating a generic display node func update_node_with(visual_container : HBoxContainer, fuseki_node_data : Dictionary): #DT_PT.free_all_child(visual_container) for key in fuseki_node_data.keys(): @@ -345,7 +517,7 @@ func get_viable_position(potential : int, concerned_list : Array[int], iteration return potential #With Fuseki link data draw those links -func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSide = ContainerSide.ANY): +func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSide = ContainerSide.ANY, link_type: String = "fuseki"): for key_node in links_as_dict.keys(): if key_node == null: continue @@ -369,8 +541,69 @@ func update_link_with(links_as_dict: Dictionary, force_side_source : ContainerSi x_drawn_list.append(drawn_x) if(destination_in_critical_path): x_highlight_list.append(drawn_x) + register_drawn_link(key_node, association_element, drawable_y_position, source_x, drawn_x, link_type) draw_link_lane(x_drawn_list, x_highlight_list, drawable_y_position) +func delete_link(link_info: Dictionary): + if link_info.is_empty(): + return + + var source = link_info["source"] + var dest = link_info["destination"] + var link_type = link_info["type"] + + # Delete a Fuseki link + if link_type == "fuseki": + delete_fuseki_link(source, dest) + return + + # Delete a user link + var target_dict = null + match link_type: + "normal": + target_dict = user_links + "bot2bot": + target_dict = user_links_bot2bot + "top2top": + target_dict = user_links_top2top + + if target_dict != null and target_dict.has(source): + target_dict[source].erase(dest) + if target_dict[source].is_empty(): + target_dict.erase(source) + print("User link deleted: ", source.name, " -> ", dest.name) + +func delete_fuseki_link(source: GenericDisplay, dest: GenericDisplay): + if fuseki_data == null: + return + + var source_name = source.name + var dest_name = dest.name + + var link_arrays = [ + fuseki_data.enabler_to_service, + fuseki_data.model_to_enabler, + fuseki_data.service_to_provided_thing, + fuseki_data.sensor_to_data_transmitted, + fuseki_data.data_transmitted_to_data, + fuseki_data.data_to_enabler + ] + + var link_found = false + for link_array in link_arrays: + for i in range(link_array.size() - 1, -1, -1): + var link = link_array[i] + if link.source == source_name and link.destination == dest_name: + link_array.remove_at(i) + link_found = true + print("Fuseki link deleted: ", source_name, " -> ", dest_name) + break + if link_found: + break + + if link_found: + update_link_dicts() + #Process and display ------------------------------------------------------------------------------- #Feed fuseki data @@ -420,6 +653,7 @@ func _process(_delta): highlighted_lines.clear() dimmed_triangles.clear() highlighted_triangles.clear() + drawn_links.clear() queue_redraw() #Free all childs of a node @@ -431,14 +665,34 @@ static func free_all_child(node : Node): #Draw all links func _draw(): if (fuseki_data): - update_link_with(links_as_dict_enabler_to_service) - update_link_with(links_as_dict_model_to_enabler) - update_link_with(links_as_dict_service_to_provided_thing, ContainerSide.TOP) - update_link_with(links_as_dict_sensor_to_data_transmitted) - update_link_with(links_as_dict_data_to_enabler) - update_link_with(links_as_dict_data_transmitted_to_data, ContainerSide.BOTTOM) + update_link_with(links_as_dict_enabler_to_service, ContainerSide.ANY, "fuseki") + update_link_with(links_as_dict_model_to_enabler, ContainerSide.ANY, "fuseki") + update_link_with(links_as_dict_service_to_provided_thing, ContainerSide.TOP, "fuseki") + update_link_with(links_as_dict_sensor_to_data_transmitted, ContainerSide.ANY, "fuseki") + update_link_with(links_as_dict_data_to_enabler, ContainerSide.ANY, "fuseki") + update_link_with(links_as_dict_data_transmitted_to_data, ContainerSide.BOTTOM, "fuseki") + update_link_with(user_links, ContainerSide.ANY, "normal") + update_link_with(user_links_bot2bot, ContainerSide.BOTTOM, "bot2bot") + update_link_with(user_links_top2top, ContainerSide.TOP, "top2top") draw_all_differed() + if deletion_mode and hovered_link != null and not hovered_link.is_empty(): + var col = Color.RED + var width = StyleConfig.Link.WIDTH + 2 + var ly = hovered_link["lane_y"] + var sx = hovered_link["source_x"] + var dx = hovered_link["dest_x"] + var src_node = hovered_link["source"] + var dest_node = hovered_link["destination"] + draw_line(Vector2(sx, ly), Vector2(dx, ly), col, width, true) + var src_y_edge = get_bottom_side(src_node).y if src_node.global_position.y < ly else get_top_side(src_node).y + draw_line(Vector2(sx, src_y_edge), Vector2(sx, ly), col, width, true) + var dest_y_edge = get_bottom_side(dest_node).y if dest_node.global_position.y < ly else get_top_side(dest_node).y + draw_line(Vector2(dx, ly), Vector2(dx, dest_y_edge), col, width, true) + var is_pointing_up = dest_node.global_position.y < ly + var vertical_shift = StyleConfig.Link.WIDTH if is_pointing_up else - StyleConfig.Link.WIDTH + draw_triangle(Vector2(dx, dest_y_edge), vertical_shift, col, is_pointing_up) + # Rabbit MQ data integration --------------------------------------------------- func _on_rabbit_data_updated(container_name: String, data : Array[String]): var container : GenericDisplay = get_node_by_name(container_name) @@ -525,11 +779,19 @@ func _on_add_component_button_pressed_data() -> void: ## Visual editing func _on_visual_editing_toggled(toggled_on: bool) -> void: + visual_editing_mode = toggled_on if toggled_on: enable_visual_editing() else: disable_visual_editing() +func _on_link_deletion_toggled(toggled_on: bool) -> void: + deletion_mode = toggled_on + if not toggled_on: + hovered_link = null + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + print("Link deletion mode: ", "ON" if deletion_mode else "OFF") + func enable_visual_editing(): $DTContainer/EnablersPanel/AddComponentButton.show() $DTContainer/ServicesPanel/AddComponentButton.show() @@ -567,7 +829,32 @@ func rename_dict_key(dict: Dictionary, old_key, new_key) -> void: for i in range(keys.size()): dict[keys[i]] = values[i] +func remove_all_user_links_for(node: GenericDisplay) -> void: + if user_links.has(node): + user_links.erase(node) + for src in user_links.keys(): + user_links[src].erase(node) + if user_links[src].is_empty(): + user_links.erase(src) + + if user_links_bot2bot.has(node): + user_links_bot2bot.erase(node) + for src in user_links_bot2bot.keys(): + user_links_bot2bot[src].erase(node) + if user_links_bot2bot[src].is_empty(): + user_links_bot2bot.erase(src) + + if user_links_top2top.has(node): + user_links_top2top.erase(node) + for src in user_links_top2top.keys(): + user_links_top2top[src].erase(node) + if user_links_top2top[src].is_empty(): + user_links_top2top.erase(src) + + func delete_component(node_name, fuseki_dict, parent_container: Node): + var node = get_node_by_name(node_name) + remove_all_user_links_for(node) fuseki_dict.erase(node_name) get_node_by_name(node_name).queue_free() displayed_node_list.erase(get_node_by_name(node_name)) diff --git a/Fuseki/Caller/FusekiQuery.gd b/Fuseki/Caller/FusekiQuery.gd index ba1535f..9178c0e 100644 --- a/Fuseki/Caller/FusekiQuery.gd +++ b/Fuseki/Caller/FusekiQuery.gd @@ -184,6 +184,15 @@ const QUERIES = { ?c12 a DTDFvocab:Constellation . OPTIONAL {?c12 ?attribute ?value} }", +"c13": "PREFIX DTDFvocab: + PREFIX rdfs: + PREFIX base: + + SELECT * + WHERE { + ?c13 a DTDFvocab:EvolutionStage . + OPTIONAL {?c13 ?attribute ?value} + }", "c14": "PREFIX DTDFvocab: PREFIX rdfs: PREFIX base: diff --git a/Fuseki/Data/FusekiData.gd b/Fuseki/Data/FusekiData.gd index 58719ee..dd8e423 100644 --- a/Fuseki/Data/FusekiData.gd +++ b/Fuseki/Data/FusekiData.gd @@ -30,6 +30,7 @@ var c7 : Dictionary var c8 : Dictionary var c9 : Dictionary var c12 : Dictionary +var c13 : Dictionary var c14 : Dictionary var c15 : Dictionary var c16 : Dictionary @@ -92,6 +93,7 @@ func input_data_from_fuseki_JSON(json): FusekiConfig.JsonHead.C8: "c8", FusekiConfig.JsonHead.C9: "c9", FusekiConfig.JsonHead.C12: "c12", + FusekiConfig.JsonHead.C13: "c13", FusekiConfig.JsonHead.C14: "c14", FusekiConfig.JsonHead.C15: "c15", FusekiConfig.JsonHead.C16: "c16", diff --git a/Fuseki/Dump/FusekiDumper.gd b/Fuseki/Dump/FusekiDumper.gd index 4674cde..f34d6e3 100644 --- a/Fuseki/Dump/FusekiDumper.gd +++ b/Fuseki/Dump/FusekiDumper.gd @@ -36,7 +36,7 @@ const CHARACTERISTICS_CONFIG = [ {id = "C10", name = "DT models and data", fields = ["model", "data"], desc_only = false, is_composite = true}, {id = "C11", name = "Tooling and enablers", field = "enabler", desc_only = false}, {id = "C12", name = "DT constellation", field = "c12", desc_only = true}, - {id = "C13", name = "Twinning process and DT evolution", field = "c15", desc_only = true}, + {id = "C13", name = "Twinning process and DT evolution", field = "c13", desc_only = true}, {id = "C14", name = "Fidelity and validity considerations", field = "c14", desc_only = true}, {id = "C15", name = "DT technical connection", field = "c15", desc_only = true}, {id = "C16", name = "DT hosting/deployment", field = "c16", desc_only = true}, diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 5057281..2e6c246 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -5,7 +5,7 @@ class_name GenericDisplay @onready var element : Label = $GenericDisplay/PresentationBox/GenericElementName @onready var realtime_element : Label = $GenericDisplay/RealTimeContainer/GenericElementAttributes @onready var script_button = $GenericDisplay/PresentationBox/ScriptButton -@onready var edit_button = $GenericDisplay/PresentationBox/EditButton +@onready var edit_button = $GenericDisplay/EditButton @onready var real_time_container = $GenericDisplay/RealTimeContainer @onready var attributes : Label = $GenericDisplay/RealTimeContainer/GenericElementAttributes @onready var visualization_container = $GenericDisplay/VisualizationContainer @@ -51,7 +51,10 @@ func _on_mouse_entered(): func _on_mouse_exited(): GenericDisplaySignals.generic_display_over.emit("", false) - + +var is_dragging = false +var drag_start_position : Vector2 + func _gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: if event.button_mask == MOUSE_BUTTON_LEFT and highlightable: @@ -60,8 +63,22 @@ func _gui_input(event: InputEvent) -> void: await get_tree().create_timer(0.2).timeout highlightable = true if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: - if $GenericDisplay/PresentationBox/EditButton.visible: + if $GenericDisplay/EditButton.visible: $PopupDescription.popup() + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed: + # Drag start + is_dragging = true + drag_start_position = get_global_mouse_position() + GenericDisplaySignals.start_drag.emit(self) + else: + # End drag + if is_dragging: + is_dragging = false + GenericDisplaySignals.stop_drag.emit(self) + if event is InputEventMouseMotion and is_dragging: + GenericDisplaySignals.dragging.emit(self, get_global_mouse_position()) + func _on_pop_up_button_pressed() -> void: chart.feed_historic(data) diff --git a/GenericDisplay/GenericDisplaySignals.gd b/GenericDisplay/GenericDisplaySignals.gd index 639d9ce..efea55b 100644 --- a/GenericDisplay/GenericDisplaySignals.gd +++ b/GenericDisplay/GenericDisplaySignals.gd @@ -3,3 +3,7 @@ extends Node signal generic_display_over signal generic_display_highlight signal generic_display_edit + +signal start_drag(node) +signal dragging(node, mouse_pos) +signal stop_drag(node) diff --git a/GenericDisplay/generic_display.tscn b/GenericDisplay/generic_display.tscn index c4653b8..8f45d74 100644 --- a/GenericDisplay/generic_display.tscn +++ b/GenericDisplay/generic_display.tscn @@ -107,24 +107,25 @@ text = "Default" horizontal_alignment = 1 vertical_alignment = 3 -[node name="EditButton" type="Button" parent="GenericDisplay/PresentationBox"] +[node name="ScriptButton" type="Button" parent="GenericDisplay/PresentationBox"] +visible = false +layout_mode = 2 +text = "Script" + +[node name="EditButton" type="Button" parent="GenericDisplay"] +custom_minimum_size = Vector2(60, 25) layout_mode = 2 -size_flags_horizontal = 0 +size_flags_horizontal = 4 size_flags_vertical = 0 -tooltip_text = "Edit" -theme_override_font_sizes/font_size = 10 +tooltip_text = "Edit component" +theme_override_font_sizes/font_size = 14 theme_override_styles/normal = SubResource("StyleBoxFlat_5nhr6") theme_override_styles/pressed = SubResource("StyleBoxFlat_0c6qw") theme_override_styles/hover = SubResource("StyleBoxFlat_ycnc1") -text = " i " +text = "Edit" icon_alignment = 1 expand_icon = true -[node name="ScriptButton" type="Button" parent="GenericDisplay/PresentationBox"] -visible = false -layout_mode = 2 -text = "Script" - [node name="RealTimeContainer" type="VBoxContainer" parent="GenericDisplay"] visible = false layout_mode = 2 @@ -328,7 +329,7 @@ text = "Select the DT script folder in the settings first" [connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] [connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] [connection signal="file_selected" from="GenericDisplay/FilePicker" to="." method="_on_file_selected"] -[connection signal="pressed" from="GenericDisplay/PresentationBox/EditButton" to="." method="_on_edit_button_pressed"] +[connection signal="pressed" from="GenericDisplay/EditButton" to="." method="_on_edit_button_pressed"] [connection signal="pressed" from="GenericDisplay/PresentationBox/ScriptButton" to="." method="_on_script_button_pressed"] [connection signal="pressed" from="GenericDisplay/RealTimeContainer/PopUpButton" to="." method="_on_pop_up_button_pressed"] [connection signal="pressed" from="GenericDisplay/VisualizationContainer/HBoxContainer/VizPickerButton" to="." method="_on_viz_picker_button_pressed"] @@ -337,4 +338,4 @@ text = "Select the DT script folder in the settings first" [connection signal="popup_hide" from="PopupDescription" to="." method="_on_popup_description_popup_hide"] [connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/ComponentNameContainer/ComponentNameEdit" to="." method="_on_component_name_edit_text_changed"] [connection signal="text_changed" from="PopupDescription/DescriptionControl/DescriptionContainer/Description" to="." method="_on_description_text_changed"] -[connection signal="pressed" from="PopupDescription/DescriptionControl/DescriptionContainer/DeleteButton" to="." method="_on_delete_button_pressed"] +[connection signal="pressed" from="PopupDescription/DescriptionControl/DescriptionContainer/DeleteButton" to="." method="_on_delete_button_pressed"] \ No newline at end of file diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 9e97571..40fae38 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3 uid="uid://cxj8tr6re2b4t"] +[gd_scene load_steps=19 format=3 uid="uid://cxj8tr6re2b4t"] [ext_resource type="PackedScene" uid="uid://nqgolpvgp7yg" path="res://DTContainer/DT&PTContainer.tscn" id="1_lklxr"] [ext_resource type="Script" uid="uid://dfun6prbhxg7c" path="res://MainScene/MainSceneController.gd" id="1_r7thu"] @@ -11,8 +11,10 @@ [ext_resource type="PackedScene" uid="uid://px6a2dg8cawb" path="res://addons/modular-settings-menu/scenes/settings.tscn" id="8_ixws2"] [ext_resource type="PackedScene" uid="uid://cowug1mbtod0e" path="res://Fuseki/Caller/FusekiCallerBtton.tscn" id="8_ped26"] [ext_resource type="Script" uid="uid://cfkgtkfhl06v5" path="res://RabbitMQ/Control/RabbitButton.gd" id="9_qda1k"] +[ext_resource type="PackedScene" uid="uid://3rf0dti147xm" path="res://ScriptRunnerPanel.tscn" id="10_xqnle"] [ext_resource type="Texture2D" uid="uid://doyxcp824t4vw" path="res://gear.png" id="12_77ryg"] [ext_resource type="PackedScene" uid="uid://cf7nrorpbpbjb" path="res://Legends/Legends.tscn" id="12_80vrr"] +[ext_resource type="Script" uid="uid://cdswvlml33tfq" path="res://MainScene/python_runner.gd" id="13_gmiik"] [ext_resource type="Script" uid="uid://0b3cap3tphwi" path="res://MainScene/CICDServer.gd" id="15_gmiik"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ffs3u"] @@ -20,6 +22,8 @@ border_color = Color(0.259294, 0.259294, 0.259294, 1) corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_80vrr"] + [node name="MainScene" type="Node"] script = ExtResource("1_r7thu") @@ -80,7 +84,7 @@ unique_name_in_owner = true layout_mode = 2 tooltip_text = "Enable listening of messages from RabbitMQ" disabled = true -text = "Record data form RabbitMQ :" +text = "Record data from RabbitMQ :" script = ExtResource("9_qda1k") [node name="VSeparator4" type="VSeparator" parent="ControlLayer/ControlPanel/MarginContainer/ControlContainer"] @@ -172,74 +176,53 @@ popup/item_3/id = 3 [node name="Legends" parent="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2" instance=ExtResource("12_80vrr")] layout_mode = 2 -[node name="VisualEditingPanelContainer" type="PanelContainer" parent="ControlLayer"] -anchors_preset = 2 -anchor_top = 1.0 +[node name="SettingsBackground" type="ColorRect" parent="ControlLayer"] +visible = false +anchors_preset = 15 +anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 50.0 -offset_top = -41.0 -offset_right = 202.0 -offset_bottom = -10.0 -grow_vertical = 0 -size_flags_horizontal = 0 -size_flags_vertical = 0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.639216) -[node name="VisualEditingButton" type="CheckButton" parent="ControlLayer/VisualEditingPanelContainer" groups=["main_buttons"]] -layout_mode = 2 -tooltip_text = "Enables the visual editing of the DT constellation" -text = "Visual editing" +[node name="Settings" parent="ControlLayer/SettingsBackground" instance=ExtResource("8_ixws2")] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +IS_IN_GAME_MENU = false -[node name="HelpButton" type="Button" parent="ControlLayer"] +[node name="ButtonsContainer" type="PanelContainer" parent="ControlLayer"] anchors_preset = 2 anchor_top = 1.0 anchor_bottom = 1.0 -offset_left = 10.0 -offset_top = -41.0 -offset_right = 41.0 -offset_bottom = -10.0 +offset_top = -72.0 +offset_right = 178.0 grow_vertical = 0 -tooltip_text = "Get help" -text = "?" -icon_alignment = 1 -script = ExtResource("6_tgjmw") - -[node name="HelpPopup" type="PopupPanel" parent="ControlLayer/HelpButton"] -oversampling_override = 1.0 -title = "Help" -position = Vector2i(18, 129) -size = Vector2i(305, 455) -unresizable = false -borderless = false +theme_override_styles/panel = SubResource("StyleBoxFlat_80vrr") -[node name="RichTextLabel" type="RichTextLabel" parent="ControlLayer/HelpButton/HelpPopup"] -offset_left = 4.0 -offset_top = 4.0 -offset_right = 301.0 -offset_bottom = 451.0 -text = "DTInsight allows you to visualize the architecture of your Digital Twin. It does by gathering its ontological structure through a Fuseki server, and displaying the incoming RabbitMQ data. - -To get started, either: -- Connect to Fuseki by clicking on the \"Call Fuseki\" button. -- Load an existing dump by picking a dump file and selecting \"Load\" +[node name="VBoxContainer" type="VBoxContainer" parent="ControlLayer/ButtonsContainer"] +layout_mode = 2 -Once the graph is generated, hover on the nodes (clickable) to highlight connections." +[node name="ButtonGrid" type="GridContainer" parent="ControlLayer/ButtonsContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/h_separation = 10 +theme_override_constants/v_separation = 10 +columns = 2 -[node name="SettingsButton" type="Button" parent="ControlLayer"] -anchors_preset = 2 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 10.0 -offset_top = -81.0 -offset_right = 41.0 -offset_bottom = -50.0 -grow_vertical = 0 +[node name="SettingsButton" type="Button" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid"] +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 tooltip_text = "Settings" icon = ExtResource("12_77ryg") icon_alignment = 1 expand_icon = true script = ExtResource("7_gs64f") -[node name="SettingsPopup" type="PopupPanel" parent="ControlLayer/SettingsButton"] +[node name="SettingsPopup" type="PopupPanel" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton"] auto_translate_mode = 1 mode = 2 title = "Settings" @@ -248,23 +231,66 @@ size = Vector2i(490, 560) borderless = false always_on_top = true -[node name="TabContainer" type="TabContainer" parent="ControlLayer/SettingsButton/SettingsPopup"] +[node name="TabContainer" type="TabContainer" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton/SettingsPopup"] offset_left = 4.0 offset_top = 4.0 offset_right = 486.0 offset_bottom = 556.0 current_tab = 0 -[node name="General" type="VBoxContainer" parent="ControlLayer/SettingsButton/SettingsPopup/TabContainer"] +[node name="General" type="VBoxContainer" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton/SettingsPopup/TabContainer"] layout_mode = 2 metadata/_tab_index = 0 -[node name="RabbitMQ" type="VBoxContainer" parent="ControlLayer/SettingsButton/SettingsPopup/TabContainer"] +[node name="RabbitMQ" type="VBoxContainer" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton/SettingsPopup/TabContainer"] visible = false layout_mode = 2 metadata/_tab_index = 1 -[node name="SettingsBackground" type="ColorRect" parent="ControlLayer"] +[node name="VisualEditingButton" type="CheckButton" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid" groups=["main_buttons"]] +layout_mode = 2 +tooltip_text = "Enables the visual editing of the DT constellation" +text = "Visual editing" + +[node name="HelpButton" type="Button" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid"] +layout_mode = 2 +tooltip_text = "Get help" +text = "?" +icon_alignment = 1 +script = ExtResource("6_tgjmw") + +[node name="HelpPopup" type="PopupPanel" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/HelpButton"] +oversampling_override = 1.0 +title = "Help" +position = Vector2i(18, 129) +size = Vector2i(305, 455) +unresizable = false +borderless = false + +[node name="RichTextLabel" type="RichTextLabel" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/HelpButton/HelpPopup"] +offset_left = 4.0 +offset_top = 4.0 +offset_right = 301.0 +offset_bottom = 451.0 +text = "DTInsight allows you to visualize the architecture of your Digital Twin. It does by gathering its ontological structure through a Fuseki server, and displaying the incoming RabbitMQ data. + +To get started, either: +- Connect to Fuseki by clicking on the \"Call Fuseki\" button. +- Load an existing dump by picking a dump file and selecting \"Load\" + +Once the graph is generated, hover on the nodes (clickable) to highlight connections." + +[node name="LinkDeletionButton" type="CheckButton" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid" groups=["main_buttons"]] +layout_mode = 2 +text = "Link Deletion" + +[node name="PythonButton" type="Button" parent="ControlLayer/ButtonsContainer/VBoxContainer"] +layout_mode = 2 +text = "Call a script" +script = ExtResource("13_gmiik") +metadata/_custom_type_script = "uid://clu02vc02js06" + +[node name="ScriptRunnerBackground" type="ColorRect" parent="ControlLayer"] visible = false anchors_preset = 15 anchor_right = 1.0 @@ -273,29 +299,23 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0, 0, 0, 0.639216) -[node name="Settings" parent="ControlLayer/SettingsBackground" instance=ExtResource("8_ixws2")] +[node name="ScriptRunnerPanel" parent="ControlLayer/ScriptRunnerBackground" instance=ExtResource("10_xqnle")] unique_name_in_owner = true layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -IS_IN_GAME_MENU = false [node name="FusekiData" parent="." instance=ExtResource("5_4e51a")] -[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container", "to_hide")] +[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container")] script = ExtResource("15_gmiik") container = NodePath("../DTContainer") -to_hide = [NodePath("../ControlLayer")] [connection signal="toggled" from="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" to="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" method="_on_toggled"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="DTContainer" method="_on_node_bg_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Legends" method="_on_node_style_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="DTContainer" method="_on_node_border_option_item_selected"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/NodeBorderOption" to="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Legends" method="_on_node_style_option_item_selected"] -[connection signal="toggled" from="ControlLayer/VisualEditingPanelContainer/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_toggled"] -[connection signal="pressed" from="ControlLayer/HelpButton" to="ControlLayer/HelpButton" method="_on_help_button_pressed"] -[connection signal="pressed" from="ControlLayer/SettingsButton" to="ControlLayer/SettingsButton" method="_on_pressed"] -[connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/SettingsButton" method="_on_settings_settings_menu_closed"] +[connection signal="settings_menu_closed" from="ControlLayer/SettingsBackground/Settings" to="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton" method="_on_settings_settings_menu_closed"] +[connection signal="pressed" from="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton" to="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton" method="_on_pressed"] +[connection signal="toggled" from="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/VisualEditingButton" to="DTContainer" method="_on_visual_editing_button_toggled"] +[connection signal="pressed" from="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/HelpButton" to="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/HelpButton" method="_on_help_button_pressed"] +[connection signal="pressed" from="ControlLayer/ButtonsContainer/VBoxContainer/PythonButton" to="ControlLayer/ButtonsContainer/VBoxContainer/PythonButton" method="_on_pressed"] diff --git a/MainScene/python_runner.gd b/MainScene/python_runner.gd new file mode 100644 index 0000000..5f726cb --- /dev/null +++ b/MainScene/python_runner.gd @@ -0,0 +1,7 @@ +extends Button + +@onready var script_runner_panel: Control = $"../../../ScriptRunnerBackground" + +func _on_pressed(): + script_runner_panel.visible = true + script_runner_panel.move_to_front() diff --git a/MainScene/python_runner.gd.uid b/MainScene/python_runner.gd.uid new file mode 100644 index 0000000..35cb428 --- /dev/null +++ b/MainScene/python_runner.gd.uid @@ -0,0 +1 @@ +uid://cdswvlml33tfq diff --git a/ScriptRunnerPanel.tscn b/ScriptRunnerPanel.tscn new file mode 100644 index 0000000..1bf782f --- /dev/null +++ b/ScriptRunnerPanel.tscn @@ -0,0 +1,61 @@ +[gd_scene load_steps=2 format=3 uid="uid://3rf0dti147xm"] + +[ext_resource type="Script" uid="uid://d1h3p24nh6ahy" path="res://script_runner_panel.gd" id="1_gnab7"] + +[node name="ScriptRunnerPanel" type="Control"] +layout_mode = 3 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_gnab7") + +[node name="FileDialog" type="FileDialog" parent="."] +title = "Open a File" +file_mode = 0 +access = 2 +filters = PackedStringArray("*.py ; Python scripts") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 151.0 +offset_bottom = 101.0 +theme_override_constants/separation = 20 + +[node name="TitleLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +text = "Run a Python script" + +[node name="InputContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="PathLineEdit" type="LineEdit" parent="VBoxContainer/InputContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +editable = false + +[node name="PickButton" type="Button" parent="VBoxContainer/InputContainer"] +layout_mode = 2 +text = "Pick file" + +[node name="ActionButtons" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="RunButton" type="Button" parent="VBoxContainer/ActionButtons"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Apply" + +[node name="BackButton" type="Button" parent="VBoxContainer/ActionButtons"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Back" + +[connection signal="file_selected" from="FileDialog" to="." method="_on_file_dialog_file_selected"] +[connection signal="pressed" from="VBoxContainer/InputContainer/PickButton" to="." method="_on_pick_button_pressed"] +[connection signal="pressed" from="VBoxContainer/ActionButtons/RunButton" to="." method="_on_run_button_pressed"] +[connection signal="pressed" from="VBoxContainer/ActionButtons/BackButton" to="." method="_on_back_button_pressed"] diff --git a/script_runner_panel.gd b/script_runner_panel.gd new file mode 100644 index 0000000..c1457b5 --- /dev/null +++ b/script_runner_panel.gd @@ -0,0 +1,48 @@ +extends Control + +# On récupère les références vers nos nœuds pour pouvoir les modifier +@onready var path_line_edit = $VBoxContainer/InputContainer/PathLineEdit +@onready var file_dialog = $FileDialog + +# Appelé quand on clique sur "Choisir fichier" +func _on_pick_button_pressed(): + # Ouvre la fenêtre de sélection de fichier + file_dialog.popup_centered() + +# Appelé quand l'utilisateur a choisi un fichier dans le FileDialog +func _on_file_dialog_file_selected(path): + # On affiche le chemin dans le champ texte + path_line_edit.text = path + +# Appelé quand on clique sur "Run" (anciennement Apply) +func _on_run_button_pressed(): + var script_path = path_line_edit.text + + if script_path == "": + printerr("Erreur : Aucun fichier sélectionné.") + return + + if not FileAccess.file_exists(script_path): + printerr("Erreur : Fichier Python introuvable à : ", script_path) + return + + # Ton code d'exécution Python + var command = "python" + var args = [script_path] + var output = [] + print("Lancement du script : ", script_path) + + # Exécution (bloquante avec true, attention si le script est long) + var exit_code = OS.execute(command, args, output, true) + + if exit_code == 0: + print("Script exécuté avec succès : ", output) + # Optionnel : Fermer le popup après succès + _on_back_button_pressed() + else: + printerr("Erreur Python (Code %s) : " % exit_code, output) + +# Appelé quand on clique sur "Back" +func _on_back_button_pressed(): + # On cache simplement ce panneau + get_parent().visible = false diff --git a/script_runner_panel.gd.uid b/script_runner_panel.gd.uid new file mode 100644 index 0000000..75a331e --- /dev/null +++ b/script_runner_panel.gd.uid @@ -0,0 +1 @@ +uid://d1h3p24nh6ahy diff --git a/script_test.py b/script_test.py new file mode 100644 index 0000000..3f38b48 --- /dev/null +++ b/script_test.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print("Hello world, you've pushed the DTInsight Button") \ No newline at end of file From e1ebf07ff65883100f2b1a5e77eb03d747fcc3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 18 Feb 2026 11:35:24 -0500 Subject: [PATCH 35/40] Fixed CICD issues --- MainScene/MainScene.tscn | 3 ++- MainScene/MainSceneController.gd | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 40fae38..70a54c0 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -305,9 +305,10 @@ layout_mode = 1 [node name="FusekiData" parent="." instance=ExtResource("5_4e51a")] -[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container")] +[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container", "to_hide")] script = ExtResource("15_gmiik") container = NodePath("../DTContainer") +to_hide = [NodePath("../ControlLayer")] [connection signal="toggled" from="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" to="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" method="_on_toggled"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="DTContainer" method="_on_node_bg_option_item_selected"] diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index cbd4196..931235b 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -15,7 +15,7 @@ func _ready(): print("Detected running on the web") # Hide UI $ControlLayer/ControlPanel.hide() - $ControlLayer/SettingsButton.hide() + $ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton.hide() # Start checking for data after a short delay get_tree().create_timer(1.0).timeout.connect(_check_for_data) From 307f859e0a02aef062e0a7ec3a4a95431ffab325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 25 Feb 2026 23:17:10 -0500 Subject: [PATCH 36/40] Added build info --- MainScene/MainScene.tscn | 43 +++++++++++++++++++++++++++++++- MainScene/MainSceneController.gd | 1 - project.godot | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 70a54c0..7d6a3af 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=19 format=3 uid="uid://cxj8tr6re2b4t"] +[gd_scene load_steps=20 format=3 uid="uid://cxj8tr6re2b4t"] [ext_resource type="PackedScene" uid="uid://nqgolpvgp7yg" path="res://DTContainer/DT&PTContainer.tscn" id="1_lklxr"] [ext_resource type="Script" uid="uid://dfun6prbhxg7c" path="res://MainScene/MainSceneController.gd" id="1_r7thu"] @@ -17,6 +17,16 @@ [ext_resource type="Script" uid="uid://cdswvlml33tfq" path="res://MainScene/python_runner.gd" id="13_gmiik"] [ext_resource type="Script" uid="uid://0b3cap3tphwi" path="res://MainScene/CICDServer.gd" id="15_gmiik"] +[sub_resource type="GDScript" id="GDScript_xqnle"] +resource_name = "VersionLabel" +script/source = "extends Label + +func _ready() -> void: + # Get the version string from Project Settings + var version = ProjectSettings.get_setting(\"application/config/version\") + text = \"v\" + version +" + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ffs3u"] border_color = Color(0.259294, 0.259294, 0.259294, 1) corner_radius_bottom_right = 20 @@ -49,6 +59,36 @@ color = Color(0.933333, 0.913725, 0.933333, 1) [node name="ControlLayer" type="CanvasLayer" parent="."] +[node name="InformationContainer" type="HBoxContainer" parent="ControlLayer"] +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -152.0 +offset_top = -19.0 +offset_right = 152.0 +offset_bottom = -1.0 +grow_horizontal = 2 +grow_vertical = 0 + +[node name="LinkButton" type="LinkButton" parent="ControlLayer/InformationContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_focus_color = Color(0.11858126, 0.11858139, 0.11858175, 1) +theme_override_colors/font_pressed_color = Color(0.288518, 0.28851798, 0.28851798, 1) +theme_override_colors/font_hover_color = Color(0.18000469, 0.18000486, 0.18000537, 1) +theme_override_font_sizes/font_size = 13 +text = "https://github.com/oakeslabmtl/DTInsight" +uri = "https://github.com/oakeslabmtl/DTInsight" + +[node name="VersionLabel" type="Label" parent="ControlLayer/InformationContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.038397167, 0.038397167, 0.03839716, 1) +theme_override_font_sizes/font_size = 13 +text = "vX.X" +script = SubResource("GDScript_xqnle") + [node name="ControlPanel" type="PanelContainer" parent="ControlLayer"] anchors_preset = 5 anchor_left = 0.5 @@ -224,6 +264,7 @@ script = ExtResource("7_gs64f") [node name="SettingsPopup" type="PopupPanel" parent="ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton"] auto_translate_mode = 1 +oversampling_override = 1.0 mode = 2 title = "Settings" initial_position = 1 diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index 931235b..dd096d7 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -15,7 +15,6 @@ func _ready(): print("Detected running on the web") # Hide UI $ControlLayer/ControlPanel.hide() - $ControlLayer/ButtonsContainer/VBoxContainer/ButtonGrid/SettingsButton.hide() # Start checking for data after a short delay get_tree().create_timer(1.0).timeout.connect(_check_for_data) diff --git a/project.godot b/project.godot index 97e8b74..e9c77ad 100644 --- a/project.godot +++ b/project.godot @@ -11,6 +11,7 @@ config_version=5 [application] config/name="DTInsight" +config/version="1.2.0" run/main_scene="res://MainScene/MainScene.tscn" config/features=PackedStringArray("4.5", "C#", "Forward Plus") run/low_processor_mode=true From 7b34ceda1708b8faff68a3251de19f4fa5da456c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 25 Feb 2026 23:35:51 -0500 Subject: [PATCH 37/40] Update README.md --- README.md | 80 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 992e0fd..295cd6e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# DTInsight +# DTInsight: A Tool for Explicit, Interactive, and Continuous Digital Twin Reporting + +[![DOI](https://img.shields.io/badge/DOI-10.1109%2Fmodels--c68889.2025.00030-blue)](https://doi.org/10.1109/models-c68889.2025.00030) +[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/) The purpose of this project is to provide users with a graphical and interactive representation of a Digital Twin. @@ -8,23 +11,23 @@ The purpose of this project is to provide users with a graphical and interactive [![DTInsight video](https://img.youtube.com/vi/CD0pdK-eGXY/0.jpg)](https://www.youtube.com/watch?v=CD0pdK-eGXY) -# About this project +## About this project -This tool was built with the aim of being cross compatible with any digital twin ontology built with the Ontology Modeling Lanuage (OML) vocabulary available at "https://github.com/oakeslabmtl/DTDF". To build such ontologies we recommend tools such as openCAESAR Rosetta available at "https://github.com/opencaesar/oml-rosetta". +This tool was built with the aim of being cross-compatible with any digital twin ontology built with the Ontology Modeling Language (OML) vocabulary available at [DTDF](https://github.com/oakeslabmtl/DTDF). To build such ontologies we recommend tools such as openCAESAR Rosetta available at [oml-rosetta](https://github.com/opencaesar/oml-rosetta). -To benefit from all of the real time communication features you will need a related digital twin simulator publishing its data with RabbitMQ, similar to this project "https://github.com/INTO-CPS-Association/example_digital-twin_incubator", as well as an ontology providing the necessary data to connect it to the visualization. +To benefit from all of the real-time communication features you will need a related digital twin simulator publishing its data with RabbitMQ, similar to this project [example_digital-twin_incubator](https://github.com/INTO-CPS-Association/example_digital-twin_incubator), as well as an ontology providing the necessary data to connect it to the visualization. -# How to use +## How to use To demonstrate how to use this tool we will take the example of the incubator project available with the links provided in the "About this project" section. -## Settings +### Settings Click on the gear on the bottom left button to access the settings panel. -## Load an ontology +### Load an ontology -### Load from Fuseki +#### Load from Fuseki This method will be used while developing new ontologies and allow users to transfer a digital twin's ontology in the visualizer. @@ -36,23 +39,23 @@ With the Fuseki server ready your Digital Twin will be available in one click in image -### Load from a dump +#### Load from a dump If a dump from an existing Digital Twin was created you will be able to access your visualization without going through the process of setting up a Fuseki server. You will only have to select the correct file from the file picker in the visualizer and click on the "Load" button. image -#### Create a dump +##### Create a dump -To create a dump, you will first have to load an ontology in the visualizer, then you will be able to put a dump file location in the correct input box and click on the "Dump" button. ** This dump can be read to check on the informations currently loaded in the visualizer ** +To create a dump, you will first have to load an ontology in the visualizer, then you will be able to put a dump file location in the correct input box and click on the "Dump" button. **This dump can be read to check on the informations currently loaded in the visualizer.** -## Navigate the Digital Twin +### Navigate the Digital Twin Once a Digital Twin is loaded into the visualizer, you will have a top-down view of its architecture and of its functional components. -**Camera controls :** +**Camera controls:** -*QWERTY keyboard :* \ +*QWERTY keyboard:* \ W - Move up \ S - Move down \ A - Move left \ @@ -60,7 +63,7 @@ D - Move Right \ Q - Zoom out \ E - Zoom in -*AZERTY keyboard :* \ +*AZERTY keyboard:* \ Z - Move up \ S - Move down \ Q - Move left \ @@ -70,40 +73,38 @@ E - Zoom in *You can also move the camera by clicking and dragging it* -**Highlighting relationships :** +**Highlighting relationships:** By hovering the mouse pointer on top of a component in the visualization, you will be able to highlight the selected component as well as its connected components. This feature allows you to visualize interconnections within your Digital Twin and understand how the data flow into it. image -## Real-Time Data +### Real-Time Data If you have a Digital Twin publishing some data on RabbitMQ exchanges and an ontology providing the necessary linking data you will be able to access real time information on your Digital Twin components in the visualization. image -To access this functionality, you will have to load an ontology and then click on the switch "Record data form RabbitMQ". Once this switch is activated, real time data will be displayed in the DIgital Twin's component as well as a button. This button allows you to access a real time graph recording the last hundred messages coming from RabbitMQ. (At this time this graph can display numerical or boolean values. - -image +To access this functionality, you will have to load an ontology and then click on the switch "Record data form RabbitMQ". Once this switch is activated, real time data will be displayed in the DIgital Twin's component as well as a button. This button allows you to access a real time graph recording the last hundred messages coming from RabbitMQ. (At this time this graph can display numerical or boolean values.) **If nothing happens when enabling real time information, it either means that your ontology is missing the linking data or that there are no messages in the RabbitMQ exchange. You can explore a dump file with a text editor to check if linking data was correctly loaded.** -## Access Digital Twin Visualization +### Access Digital Twin Visualization -### Preparation +#### Preparation - Create or Open another Godot project containing your visualization. **Warning: Godot software version must match DTInsight's**. - Create a scene called `main.tscn` that will host your visualization. - Configure a function `_on_message(message)` that will receive raw real-time data from DTInsight. - Export the project as a `.pck` -### Use +#### Use If the OML description is properly configured (by having the `DTDFVocab:HasVisualization true` property), simply pick the exported `.pck` file and you will be able to open the visualization on DTInsight. Screenshot 2025-07-11 133809 -## Access Digital Twin script +### Access Digital Twin script If you have your Digital Twin's scripts on your pc and an ontology providing a relative path to those files, you will be able to access those script files from the visualizer. @@ -117,13 +118,30 @@ Once loaded if a script is associated with a Digital Twin component, a new butto image -# Credits +## Citation + +If you use DTInsight in your research, please cite our paper: + +```bibtex +@INPROCEEDINGS{fiter2025dtinsight, + author={Fiter, Kérian and Malassigné-Onfroy, Louis and Oakes, Bentley}, + booktitle={2025 ACM/IEEE 28th International Conference on Model Driven Engineering Languages and Systems Companion (MODELS-C)}, + title={DTInsight: A Tool for Explicit, Interactive, and Continuous Digital Twin Reporting}, + year={2025}, + volume={}, + number={}, + pages={139-143}, + keywords={Visualization;Three-dimensional displays;Systematics;Data visualization;Ontologies;Real-time systems;Software;Digital twins;Stakeholders;Monitoring;digital twins;software visualization;software documentation;decision-making;ontologies;OML;monitoring}, + doi={10.1109/MODELS-C68889.2025.00030} +} +``` + +## Credits -@Ryskann / Louis Malassigné-Onfroy \ -@Kérian Fiter \ -Under the supervision of [Prof. Bentley Oakes](https://bentleyjoakes.github.io/) +- **Kérian Fiter** (@Kérian Fiter) +- **Louis Malassigné-Onfroy** (@Ryskann) +- Under the supervision of **[Prof. Bentley Oakes](https://bentleyjoakes.github.io/)** -# License +## License -This project is licensed under the -[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). +This project is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). From 40aae45c6e74d18c08328e8335f1924d5d0e4134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 25 Feb 2026 23:38:14 -0500 Subject: [PATCH 38/40] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 295cd6e..6320c66 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ If you use DTInsight in your research, please cite our paper: ## Credits -- **Kérian Fiter** (@Kérian Fiter) +- **[Kérian Fiter](https://kerianfiter.github.io/)** (@KerianFiter) - **Louis Malassigné-Onfroy** (@Ryskann) - Under the supervision of **[Prof. Bentley Oakes](https://bentleyjoakes.github.io/)** From e331b3fbfc97077ac6ac250a629cbfa68c64bcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 25 Feb 2026 23:39:50 -0500 Subject: [PATCH 39/40] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6320c66..f345ef8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # DTInsight: A Tool for Explicit, Interactive, and Continuous Digital Twin Reporting [![DOI](https://img.shields.io/badge/DOI-10.1109%2Fmodels--c68889.2025.00030-blue)](https://doi.org/10.1109/models-c68889.2025.00030) +[![arXiv](https://img.shields.io/badge/arXiv-2508.18431-b31b1b.svg)](https://arxiv.org/pdf/2508.18431) [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/) The purpose of this project is to provide users with a graphical and interactive representation of a Digital Twin. @@ -120,7 +121,7 @@ Once loaded if a script is associated with a Digital Twin component, a new butto ## Citation -If you use DTInsight in your research, please cite our paper: +If you use DTInsight in your research, please cite our paper. An open-access preprint is available on [arXiv](https://arxiv.org/pdf/2508.18431). ```bibtex @INPROCEEDINGS{fiter2025dtinsight, From 00ea3a5a96355dd48a28014f8467b6e3dba93636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9rian=20Fiter?= Date: Wed, 18 Mar 2026 23:08:00 -0400 Subject: [PATCH 40/40] Quality improvements (#7) * Fixes and comments * Imroved Fuseki handling * Simplified logic * Compressed code * Improved zoom, better comments * Fixed issues * Update README.md * Revise Fuseki server startup and loading tasks Updated instructions for starting the Fuseki server and loading ontology data in the README. --- Camera/CameraControl.gd | 18 +- Config/CameraConfig.gd | 4 +- Config/StyleConfig.gd | 19 ++ DTContainer/DT&PTContainer.gd | 335 +++++++++----------------- Fuseki/Caller/FusekiCallerButton.gd | 27 ++- Fuseki/Caller/FusekiQuery.gd | 356 ++++++++-------------------- Fuseki/Data/FusekiData.gd | 332 ++++++++++++-------------- Fuseki/Data/FusekiParser.gd | 68 ++++++ Fuseki/Data/FusekiParser.gd.uid | 1 + Fuseki/Data/FusekiSignals.gd | 7 + Fuseki/Dump/FusekiDumper.gd | 350 ++++++++++++++++----------- GenericDisplay/GenericDisplay.gd | 2 - Legends/Legends.gd | 15 +- MainScene/CICDServer.gd | 158 +++++++----- MainScene/MainScene.tscn | 3 +- MainScene/MainSceneController.gd | 53 +++-- MainScene/help_button.gd | 1 + MainScene/python_runner.gd | 6 +- MainScene/settings_button.gd | 13 +- README.md | 211 ++++++++++++----- script_runner_panel.gd | 35 +-- 21 files changed, 1036 insertions(+), 978 deletions(-) create mode 100644 Fuseki/Data/FusekiParser.gd create mode 100644 Fuseki/Data/FusekiParser.gd.uid diff --git a/Camera/CameraControl.gd b/Camera/CameraControl.gd index 51f105e..8602e7a 100644 --- a/Camera/CameraControl.gd +++ b/Camera/CameraControl.gd @@ -5,8 +5,10 @@ class_name CameraControl var movement_enabled : bool = true var zoom_enabled : bool = true var currently_zooming : bool = false +var target_zoom : Vector2 = Vector2(1.0, 1.0) func _ready() -> void: + target_zoom = zoom CameraSignals.disable_camera_movement.connect(_disable_camera_movement) CameraSignals.enable_camera_movement.connect(_enable_camera_movement) CameraSignals.enable_camera_zoom.connect(_enable_camera_zoom) @@ -18,8 +20,11 @@ func _process(delta : float): var inputmovementVector : Vector2 = handle_movement_input() moveCamera(inputmovementVector, delta) if zoom_enabled: - var inputZoomVector : Vector2 = handle_zoom_input(false) - zoom_camera(inputZoomVector) + var keyZoomVector : Vector2 = handle_zoom_input(false) + if keyZoomVector != Vector2.ZERO: + set_target_zoom(keyZoomVector * delta) + + zoom = zoom.lerp(target_zoom, 15.0 * delta) #Movement functions ----------------------------------------------------------- var current_velocity : Vector2 = Vector2(0, 0) @@ -71,7 +76,8 @@ func _unhandled_input(event: InputEvent) -> void: handle_mouse_movement(event) if zoom_enabled: var inputZoomVector : Vector2 = handle_zoom_input(true) - zoom_camera(inputZoomVector) + if inputZoomVector != Vector2.ZERO: + set_target_zoom(inputZoomVector) func handle_mouse_movement(event : InputEventMouseMotion) -> void: translate(-event.relative / zoom) @@ -91,8 +97,8 @@ func handle_zoom_input(using_mouse : bool): input_vector += Vector2(-CameraConfig.Zoom.ZOOM_SCROLL_SPEED, -CameraConfig.Zoom.ZOOM_SCROLL_SPEED) return input_vector -func zoom_camera(input_vector: Vector2): - zoom = clamp(lerp(zoom, zoom + input_vector, 0.01), Vector2(CameraConfig.Zoom.MAX_ZOOM_OUT_SCALE, CameraConfig.Zoom.MAX_ZOOM_OUT_SCALE), Vector2(CameraConfig.Zoom.MAX_ZOOM_IN_SCALE, CameraConfig.Zoom.MAX_ZOOM_IN_SCALE)) +func set_target_zoom(input_vector: Vector2): + target_zoom = clamp(target_zoom + input_vector, Vector2(CameraConfig.Zoom.MAX_ZOOM_OUT_SCALE, CameraConfig.Zoom.MAX_ZOOM_OUT_SCALE), Vector2(CameraConfig.Zoom.MAX_ZOOM_IN_SCALE, CameraConfig.Zoom.MAX_ZOOM_IN_SCALE)) #Signal handling --------------------------------------------------------------- func _disable_camera_movement() -> void: @@ -114,7 +120,7 @@ func _zoom_to_fit(target_rect: Rect2): # Use the smaller scale to ensure the whole target fits var zoom_factor = min(scale_x, scale_y) - zoom = Vector2(zoom_factor, zoom_factor) + target_zoom = Vector2(zoom_factor, zoom_factor) # Center the camera on the target position = target_rect.position + target_rect.size * 0.5 diff --git a/Config/CameraConfig.gd b/Config/CameraConfig.gd index 399d620..d2da6bb 100644 --- a/Config/CameraConfig.gd +++ b/Config/CameraConfig.gd @@ -10,5 +10,5 @@ class Movement: class Zoom: const MAX_ZOOM_IN_SCALE : float = 2 const MAX_ZOOM_OUT_SCALE : float = 0.3 - const ZOOM_KEY_SPEED : float = 1 - const ZOOM_SCROLL_SPEED : float = 5 + const ZOOM_KEY_SPEED : float = 1.5 + const ZOOM_SCROLL_SPEED : float = 0.2 diff --git a/Config/StyleConfig.gd b/Config/StyleConfig.gd index 1068035..137400d 100644 --- a/Config/StyleConfig.gd +++ b/Config/StyleConfig.gd @@ -6,18 +6,37 @@ class Deployment: const EDGE : Color = Color(0.2, 0.4, 0.8) const FOG : Color = Color.LIGHT_STEEL_BLUE const CLOUD : Color = Color.AQUA + + const MAP : Dictionary = { + "ON_DEVICE": ON_DEVICE, + "EDGE": EDGE, + "FOG": FOG, + "CLOUD": CLOUD + } # Implementation relationship style class Implementation: const PLANNED : Color = Color.YELLOW const ACTIVE : Color = Color.ORANGE const IMPLEMENTED : Color = Color.GREEN + + const MAP : Dictionary = { + "PLANNED": PLANNED, + "ACTIVE": ACTIVE, + "IMPLEMENTED": IMPLEMENTED + } # Timescale relationship style class Timescale: const SLOWER_THAN_REALTIME : Color = Color.BLUE_VIOLET const REALTIME : Color = Color.AQUAMARINE const FASTER_THAN_REALTIME : Color = Color.GOLD + + const MAP : Dictionary = { + "SLOWER_THAN_REALTIME": SLOWER_THAN_REALTIME, + "REALTIME": REALTIME, + "FASTER_THAN_REALTIME": FASTER_THAN_REALTIME + } #DT/RT displayed element style class DTElement: diff --git a/DTContainer/DT&PTContainer.gd b/DTContainer/DT&PTContainer.gd index 6baa435..1ca09df 100644 --- a/DTContainer/DT&PTContainer.gd +++ b/DTContainer/DT&PTContainer.gd @@ -102,31 +102,36 @@ func get_node_under_mouse(pos : Vector2) -> GenericDisplay: return node return null +func _is_point_on_segment(pos: Vector2, start: Vector2, end: Vector2, tolerance: float) -> bool: + var min_x = min(start.x, end.x) - tolerance + var max_x = max(start.x, end.x) + tolerance + var min_y = min(start.y, end.y) - tolerance + var max_y = max(start.y, end.y) + tolerance + return pos.x >= min_x and pos.x <= max_x and pos.y >= min_y and pos.y <= max_y + func get_link_under_mouse(mouse_pos: Vector2) -> Dictionary: - const CLICK_TOLERANCE = 2.0 # tolerance pixels for clicking on a link + const CLICK_TOLERANCE = 2.0 # tolerance pixels for clicking on a link var links_to_check = drawn_links if highlighted_element != null: links_to_check = drawn_links.filter(func(l): return l["source"] == highlighted_element or l["destination"] == highlighted_element) + for link in links_to_check: - var min_x = min(link["source_x"], link["dest_x"]) - var max_x = max(link["source_x"], link["dest_x"]) - var on_horizontal = abs(mouse_pos.y - link["lane_y"]) < CLICK_TOLERANCE and mouse_pos.x >= min_x - CLICK_TOLERANCE and mouse_pos.x <= max_x + CLICK_TOLERANCE - if on_horizontal: + # Horizontal lane + if _is_point_on_segment(mouse_pos, Vector2(link["source_x"], link["lane_y"]), Vector2(link["dest_x"], link["lane_y"]), CLICK_TOLERANCE): return link + + # Vertical source branch var src_node = link["source"] var src_y_start = get_bottom_side(src_node).y if src_node.global_position.y < link["lane_y"] else get_top_side(src_node).y - var min_y_src = min(src_y_start, link["lane_y"]) - var max_y_src = max(src_y_start, link["lane_y"]) - var on_vertical_src = abs(mouse_pos.x - link["source_x"]) < CLICK_TOLERANCE and mouse_pos.y >= min_y_src - CLICK_TOLERANCE and mouse_pos.y <= max_y_src + CLICK_TOLERANCE - if on_vertical_src: + if _is_point_on_segment(mouse_pos, Vector2(link["source_x"], src_y_start), Vector2(link["source_x"], link["lane_y"]), CLICK_TOLERANCE): return link + + # Vertical dest branch var dest_node = link["destination"] var dest_y_end = get_bottom_side(dest_node).y if dest_node.global_position.y < link["lane_y"] else get_top_side(dest_node).y - var min_y_dest = min(link["lane_y"], dest_y_end) - var max_y_dest = max(link["lane_y"], dest_y_end) - var on_vertical_dest = abs(mouse_pos.x - link["dest_x"]) < CLICK_TOLERANCE and mouse_pos.y >= min_y_dest - CLICK_TOLERANCE and mouse_pos.y <= max_y_dest + CLICK_TOLERANCE - if on_vertical_dest: + if _is_point_on_segment(mouse_pos, Vector2(link["dest_x"], link["lane_y"]), Vector2(link["dest_x"], dest_y_end), CLICK_TOLERANCE): return link + return {} func _on_start_drag(node): @@ -714,67 +719,29 @@ func _on_node_bg_option_item_selected(index: int) -> void: var new_node_index = 1 var default_fuseki_node_data : Dictionary = { "type": ["NamedIndividual", "Component", "ContainedElement", "Thing", "DescribedThing", "ImplementationThing", "ConnectionComponent", "TimeScaleThing", "ConceptInstance"], "desc": [""] } -func _on_add_component_button_pressed_env() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.env["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(env_container, fuseki_data.env) - -func _on_add_component_button_pressed_system() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.sys_component["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(sys_container, fuseki_data.sys_component) - -func _on_add_component_button_pressed_operator() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - new_fuseki_node_data["type"].append("Insight") - fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) - -func _on_add_component_button_pressed_machine() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - new_fuseki_node_data["type"].append("Action") - fuseki_data.provided_thing["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) - -func _on_add_component_button_pressed_sensors() -> void: +func _add_component(target_dict: Dictionary, container, extra_types: Array = []) -> void: var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.sensing_component["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(sensor_container, fuseki_data.sensing_component) - -func _on_add_component_button_pressed_data_trans() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.data_transmitted["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(data_transmitted_container, fuseki_data.data_transmitted) - -func _on_add_component_button_pressed_services() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.service["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(service_container, fuseki_data.service) - -func _on_add_component_button_pressed_enablers() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.enabler["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(enabler_container, fuseki_data.enabler) - -func _on_add_component_button_pressed_models() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.model["new_node_" + str(new_node_index)] = new_fuseki_node_data - new_node_index += 1 - update_node_with(model_container, fuseki_data.model) - -func _on_add_component_button_pressed_data() -> void: - var new_fuseki_node_data = default_fuseki_node_data.duplicate_deep() - fuseki_data.data["new_node_" + str(new_node_index)] = new_fuseki_node_data + for type in extra_types: + new_fuseki_node_data["type"].append(type) + + target_dict["new_node_" + str(new_node_index)] = new_fuseki_node_data new_node_index += 1 - update_node_with(data_container, fuseki_data.data) + + if container is Callable: + container.call() + else: + update_node_with(container, target_dict) + +func _on_add_component_button_pressed_env(): _add_component(fuseki_data.env, env_container) +func _on_add_component_button_pressed_system(): _add_component(fuseki_data.sys_component, sys_container) +func _on_add_component_button_pressed_operator(): _add_component(fuseki_data.provided_thing, func(): update_provided_things(operator_container, machine_container, fuseki_data.provided_thing), ["Insight"]) +func _on_add_component_button_pressed_machine(): _add_component(fuseki_data.provided_thing, func(): update_provided_things(operator_container, machine_container, fuseki_data.provided_thing), ["Action"]) +func _on_add_component_button_pressed_sensors(): _add_component(fuseki_data.sensing_component, sensor_container) +func _on_add_component_button_pressed_data_trans(): _add_component(fuseki_data.data_transmitted, data_transmitted_container) +func _on_add_component_button_pressed_services(): _add_component(fuseki_data.service, service_container) +func _on_add_component_button_pressed_enablers(): _add_component(fuseki_data.enabler, enabler_container) +func _on_add_component_button_pressed_models(): _add_component(fuseki_data.model, model_container) +func _on_add_component_button_pressed_data(): _add_component(fuseki_data.data, data_container) ## Visual editing @@ -792,29 +759,24 @@ func _on_link_deletion_toggled(toggled_on: bool) -> void: Input.set_default_cursor_shape(Input.CURSOR_ARROW) print("Link deletion mode: ", "ON" if deletion_mode else "OFF") -func enable_visual_editing(): - $DTContainer/EnablersPanel/AddComponentButton.show() - $DTContainer/ServicesPanel/AddComponentButton.show() - $DTContainer/ModelsDataPanel/AddComponentButton.show() - $PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton.show() - $PTContainer/DataTravelContainer/MachinePanel/AddComponentButton.show() - $"PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton".show() - $"PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton".show() - $"PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton".show() - $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton.show() - $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton.show() - -func disable_visual_editing(): - $DTContainer/EnablersPanel/AddComponentButton.hide() - $DTContainer/ServicesPanel/AddComponentButton.hide() - $DTContainer/ModelsDataPanel/AddComponentButton.hide() - $PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton.hide() - $PTContainer/DataTravelContainer/MachinePanel/AddComponentButton.hide() - $"PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton".hide() - $"PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton".hide() - $"PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton".hide() - $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton.hide() - $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton.hide() +func _set_visual_editing_buttons_visible(is_visible: bool): + var buttons = [ + $DTContainer/EnablersPanel/AddComponentButton, + $DTContainer/ServicesPanel/AddComponentButton, + $DTContainer/ModelsDataPanel/AddComponentButton, + $PTContainer/DataTravelContainer/DataOutPanel/AddComponentButton, + $PTContainer/DataTravelContainer/MachinePanel/AddComponentButton, + $"PTContainer/Operator&EnvContainer/OperatorPanel/AddComponentButton", + $"PTContainer/Operator&EnvContainer/ExperimentContainer/EnvPanel/AddComponentButton", + $"PTContainer/Operator&EnvContainer/ExperimentContainer/SystemPanel/AddComponentButton", + $DTContainer/ModelsDataPanel/ModelsDataContainer/ModelsPanelContainer/AddComponentButton, + $PTContainer/DataTravelContainer/DataOutPanel/DataOutContainer/HBoxContainer/AddComponentButton + ] + for btn in buttons: + if btn: btn.visible = is_visible + +func enable_visual_editing(): _set_visual_editing_buttons_visible(true) +func disable_visual_editing(): _set_visual_editing_buttons_visible(false) func rename_dict_key(dict: Dictionary, old_key, new_key) -> void: if old_key == new_key or not dict.has(old_key): @@ -829,27 +791,18 @@ func rename_dict_key(dict: Dictionary, old_key, new_key) -> void: for i in range(keys.size()): dict[keys[i]] = values[i] -func remove_all_user_links_for(node: GenericDisplay) -> void: - if user_links.has(node): - user_links.erase(node) - for src in user_links.keys(): - user_links[src].erase(node) - if user_links[src].is_empty(): - user_links.erase(src) +func _remove_node_from_link_dict(node: GenericDisplay, link_dict: Dictionary) -> void: + if link_dict.has(node): + link_dict.erase(node) + for src in link_dict.keys(): + link_dict[src].erase(node) + if link_dict[src].is_empty(): + link_dict.erase(src) - if user_links_bot2bot.has(node): - user_links_bot2bot.erase(node) - for src in user_links_bot2bot.keys(): - user_links_bot2bot[src].erase(node) - if user_links_bot2bot[src].is_empty(): - user_links_bot2bot.erase(src) - - if user_links_top2top.has(node): - user_links_top2top.erase(node) - for src in user_links_top2top.keys(): - user_links_top2top[src].erase(node) - if user_links_top2top[src].is_empty(): - user_links_top2top.erase(src) +func remove_all_user_links_for(node: GenericDisplay) -> void: + _remove_node_from_link_dict(node, user_links) + _remove_node_from_link_dict(node, user_links_bot2bot) + _remove_node_from_link_dict(node, user_links_top2top) func delete_component(node_name, fuseki_dict, parent_container: Node): @@ -865,114 +818,46 @@ func rename_component(fuseki_dict, node_name, new_node_name): # Edit the FusekiData of a specific node func edit_node(node_name: String, parent_container: Node, delete: bool, new_node_name: String, new_node_description: String): - match parent_container: - service_container: - if delete: - delete_component(node_name, fuseki_data.service, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.service, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.service[node_name]["desc"] = [new_node_description] - update_node_with(service_container, fuseki_data.service) - - enabler_container: - if delete: - delete_component(node_name, fuseki_data.enabler, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.enabler, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.enabler[node_name]["desc"] = [new_node_description] - update_node_with(enabler_container, fuseki_data.enabler) - - model_container: - if delete: - delete_component(node_name, fuseki_data.model, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.model, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.model[node_name]["desc"] = [new_node_description] - update_node_with(model_container, fuseki_data.model) - - data_container: - if delete: - delete_component(node_name, fuseki_data.data, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.data, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.data[node_name]["desc"] = [new_node_description] - update_node_with(data_container, fuseki_data.data) - - operator_container: - if delete: - delete_component(node_name, fuseki_data.provided_thing, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.provided_thing, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] - update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) - - machine_container: - if delete: - delete_component(node_name, fuseki_data.provided_thing, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.provided_thing, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.provided_thing[node_name]["desc"] = [new_node_description] - update_provided_things(operator_container, machine_container, fuseki_data.provided_thing) - - data_transmitted_container: - if delete: - delete_component(node_name, fuseki_data.data_transmitted, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.data_transmitted, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.data_transmitted[node_name]["desc"] = [new_node_description] - update_node_with(data_transmitted_container, fuseki_data.data_transmitted) - - sensor_container: - if delete: - delete_component(node_name, fuseki_data.sensing_component, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.sensing_component, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.sensing_component[node_name]["desc"] = [new_node_description] - update_node_with(sensor_container, fuseki_data.sensing_component) - - env_container: - if delete: - delete_component(node_name, fuseki_data.env, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.env, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.env[node_name]["desc"] = [new_node_description] - update_node_with(env_container, fuseki_data.env) - - sys_container: - if delete: - delete_component(node_name, fuseki_data.sys_component, parent_container) - else: - if node_name != new_node_name: - rename_component(fuseki_data.sys_component, node_name, new_node_name) - node_name = new_node_name - if new_node_description: - fuseki_data.sys_component[node_name]["desc"] = [new_node_description] - update_node_with(sys_container, fuseki_data.sys_component) + var target_dict = null + var update_func = null + + if parent_container == service_container: + target_dict = fuseki_data.service + update_func = func(): update_node_with(service_container, target_dict) + elif parent_container == enabler_container: + target_dict = fuseki_data.enabler + update_func = func(): update_node_with(enabler_container, target_dict) + elif parent_container == model_container: + target_dict = fuseki_data.model + update_func = func(): update_node_with(model_container, target_dict) + elif parent_container == data_container: + target_dict = fuseki_data.data + update_func = func(): update_node_with(data_container, target_dict) + elif parent_container == data_transmitted_container: + target_dict = fuseki_data.data_transmitted + update_func = func(): update_node_with(data_transmitted_container, target_dict) + elif parent_container == sensor_container: + target_dict = fuseki_data.sensing_component + update_func = func(): update_node_with(sensor_container, target_dict) + elif parent_container == env_container: + target_dict = fuseki_data.env + update_func = func(): update_node_with(env_container, target_dict) + elif parent_container == sys_container: + target_dict = fuseki_data.sys_component + update_func = func(): update_node_with(sys_container, target_dict) + elif parent_container == operator_container or parent_container == machine_container: + target_dict = fuseki_data.provided_thing + update_func = func(): update_provided_things(operator_container, machine_container, target_dict) + + if target_dict != null and update_func != null: + if delete: + delete_component(node_name, target_dict, parent_container) + else: + if node_name != new_node_name: + rename_component(target_dict, node_name, new_node_name) + node_name = new_node_name + if new_node_description: + target_dict[node_name]["desc"] = [new_node_description] + update_func.call() + fuseki_data.build_relations() diff --git a/Fuseki/Caller/FusekiCallerButton.gd b/Fuseki/Caller/FusekiCallerButton.gd index 62078d9..cd874c5 100644 --- a/Fuseki/Caller/FusekiCallerButton.gd +++ b/Fuseki/Caller/FusekiCallerButton.gd @@ -1,3 +1,10 @@ +## Button that triggers a full Fuseki SPARQL query cycle. +## On press it: +## 1. Clears existing data via FusekiSignals. +## 2. Iterates over every query defined in FusekiQuery.QUERIES sequentially, +## awaiting each HTTP response before firing the next. +## 3. Emits FusekiSignals.fuseki_data_updated once all responses are processed. + extends Button @onready var sparql_request = $SparqlFusekiQueries @@ -5,24 +12,40 @@ extends Button var fuseki_data_manager: FusekiData + +## Injects the shared [FusekiData] manager used to store parsed query results. func set_fuseki_data_manager(manager: FusekiData) -> void: fuseki_data_manager = manager + func _on_pressed() -> void: + # Signal all listeners to clear stale data before the new query cycle. FusekiSignals.fuseki_data_clear.emit() + + # Disable the button for the duration of the query cycle to prevent + # overlapping requests. disabled = true + # Fire each SPARQL query in sequence; await ensures one completes before + # the next begins so FusekiData is populated in a deterministic order. for query_name in fuseki_query_manager.QUERIES.keys(): - var query = fuseki_query_manager.QUERIES[query_name] + var query: String = fuseki_query_manager.QUERIES[query_name] _send_query(query) await sparql_request.request_completed FusekiSignals.fuseki_data_updated.emit() disabled = false + +## Sends a single URL-encoded SPARQL query to the configured Fuseki endpoint. func _send_query(query: String) -> void: - sparql_request.request(FusekiConfig.URL + FusekiConfig.DATASET + FusekiConfig.ENDPOINT + query.uri_encode()) + sparql_request.request( + FusekiConfig.URL + FusekiConfig.DATASET + FusekiConfig.ENDPOINT + query.uri_encode() + ) + +## Callback connected to the HTTPRequest node's `request_completed` signal. +## Parses the JSON body and forwards the result to the FusekiData manager. func _on_fuseki_completion(_result, _response_code, _headers, body) -> void: var json = JSON.parse_string(body.get_string_from_utf8()) fuseki_data_manager.input_data_from_fuseki_JSON(json) diff --git a/Fuseki/Caller/FusekiQuery.gd b/Fuseki/Caller/FusekiQuery.gd index 9178c0e..f015eb0 100644 --- a/Fuseki/Caller/FusekiQuery.gd +++ b/Fuseki/Caller/FusekiQuery.gd @@ -1,259 +1,107 @@ -extends Node - -#Sparql queries intended to interact with the Fuseki Server - -const QUERIES = { -#Dt/PT objects ------------------------------------------------------------------------------------- -"services": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?service a DTDFvocab:Service . - OPTIONAL {?service ?attribute ?value} - }", -"enablers": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?enabler a DTDFvocab:Enabler . - OPTIONAL {?enabler ?attribute ?value} - }", - "models": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?model a DTDFvocab:Model . - OPTIONAL {?model ?attribute ?value} - }", -"provided_things": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?provided a DTDFvocab:ProvidedThing . - OPTIONAL {?provided ?attribute ?value} - }", - "data_transmitted": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?dataT a DTDFvocab:DataTransmitted . - OPTIONAL {?dataT ?attribute ?value} - }", -"data": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?data a DTDFvocab:Data . - OPTIONAL {?data ?attribute ?value} - }", - "sensors": "PREFIX DTDFvocab: - PREFIX rdfs: - - SELECT * - WHERE { - ?sensor a DTDFvocab:SensingComponent . - OPTIONAL {?sensor ?attribute ?value} - }", -"env": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT ?env ?attribute ?value - WHERE { - ?sus a DTDFvocab:SystemUnderStudy . - ?sus DTDFvocab:hasEnvironment ?envS . - ?env base:isContainedIn ?envS . - OPTIONAL {?env ?attribute ?value} - }", -"sys_component": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT ?sysComponent ?attribute ?value - WHERE { - ?sus a DTDFvocab:SystemUnderStudy . - ?sus DTDFvocab:hasSystem ?sys . - ?sysComponent base:isContainedIn ?sys . - OPTIONAL {?sysComponent ?attribute ?value} - }", -#Rabbit objects ------------------------------------------------------------------------------------ -"rabbit_exchange": "PREFIX rabbit: - - SELECT ?exc ?attribute ?value - WHERE { - ?exc a rabbit:ExchangeName . - ?exc ?attribute ?value - }", -"rabbit_routing_key": "PREFIX rabbit: - - SELECT ?route ?attribute ?value - WHERE { - ?route a rabbit:RoutingKey . - ?route ?attribute ?value - }", -"rabbit_source": "PREFIX rabbit: - - SELECT ?source ?attribute ?value - WHERE { - ?source a rabbit:Source . - ?source ?attribute ?value - }", -"rabbit_message_listener": "PREFIX rabbit: - - SELECT ?ml ?attribute ?value - WHERE { - ?ml a rabbit:MessageListener . - ?ml ?attribute ?value - }", -#Characteristics ------------------------------------------------------------------------------------ -"c1": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: +## Catalogue of SPARQL queries used to retrieve Digital Twin Description Framework +## (DTDF) data from the Fuseki triple store. +## +## All queries follow the same three-column SELECT pattern: +## ? ?attribute ?value +## which maps directly to the FusekiData parsing pipeline. - SELECT * - WHERE { - ?c1 a DTDFvocab:SystemUnderStudy . - OPTIONAL {?c1 ?attribute ?value} - }", -"c2": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c2 a DTDFvocab:ActingComponent . - OPTIONAL {?c2 ?attribute ?value} - }", -"c4": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c4 a DTDFvocab:DataTransmitted . - OPTIONAL {?c4 ?attribute ?value} - }", -"c5": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c5 a DTDFvocab:VirtualToPhysical . - OPTIONAL {?c5 ?attribute ?value} - }", -"c7": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c7 a DTDFvocab:TimeScale . - OPTIONAL {?c7 ?attribute ?value} - }", -"c8": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c8 a DTDFvocab:Multiplicity . - OPTIONAL {?c8 ?attribute ?value} - }", -"c9": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c9 a DTDFvocab:LifeCycleStage . - OPTIONAL {?c9 ?attribute ?value} - }", -"c12": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c12 a DTDFvocab:Constellation . - OPTIONAL {?c12 ?attribute ?value} - }", -"c13": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c13 a DTDFvocab:EvolutionStage . - OPTIONAL {?c13 ?attribute ?value} - }", -"c14": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c14 a DTDFvocab:FidelityConsideration . - OPTIONAL {?c14 ?attribute ?value} - }", -"c15": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c15 a DTDFvocab:TechnicalConnection . - OPTIONAL {?c15 ?attribute ?value} - }", -"c16": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c16 a DTDFvocab:Deployment . - OPTIONAL {?c16 ?attribute ?value} - }", -"c18": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: - - SELECT * - WHERE { - ?c18 a DTDFvocab:HoriIntegration . - OPTIONAL {?c18 ?attribute ?value} - }", -"c19": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: +extends Node - SELECT * - WHERE { - ?c19 a DTDFvocab:DataOwnershipPrivacy . - OPTIONAL {?c19 ?attribute ?value} - }", -"c20": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: +var QUERIES: Dictionary = {} - SELECT * - WHERE { - ?c20 a DTDFvocab:Standardization . - OPTIONAL {?c20 ?attribute ?value} - }", -"c21": "PREFIX DTDFvocab: - PREFIX rdfs: - PREFIX base: +const PREFIXES = { + "DTDFvocab": "", + "rdfs": "", + "base": "", + "rabbit": "" +} - SELECT * - WHERE { - ?c21 a DTDFvocab:SecuritySafety . - OPTIONAL {?c21 ?attribute ?value} - }", +const TEMPLATES = { + "standard": """PREFIX DTDFvocab: {DTDFvocab} +PREFIX rdfs: {rdfs} +PREFIX base: {base} + +SELECT * WHERE {{ + ?{alias} a {rdf_type} . + OPTIONAL {{?{alias} ?attribute ?value}} +}}""", + + "env": """PREFIX DTDFvocab: {DTDFvocab} +PREFIX rdfs: {rdfs} +PREFIX base: {base} + +SELECT ?env ?attribute ?value WHERE {{ + ?sus a DTDFvocab:SystemUnderStudy . + ?sus DTDFvocab:hasEnvironment ?envS . + ?env base:isContainedIn ?envS . + OPTIONAL {{?env ?attribute ?value}} +}}""", + + "sys_component": """PREFIX DTDFvocab: {DTDFvocab} +PREFIX rdfs: {rdfs} +PREFIX base: {base} + +SELECT ?sysComponent ?attribute ?value WHERE {{ + ?sus a DTDFvocab:SystemUnderStudy . + ?sus DTDFvocab:hasSystem ?sys . + ?sysComponent base:isContainedIn ?sys . + OPTIONAL {{?sysComponent ?attribute ?value}} +}}""", + + "rabbit": """PREFIX rabbit: {rabbit} + +SELECT ?{alias} ?attribute ?value WHERE {{ + ?{alias} a {rdf_type} . + ?{alias} ?attribute ?value +}}""" } + +# ── Metadata for Query Generation ────────────────────────────────────────────── +const QUERY_METADATA = [ + { id = "services", alias = "service", type = "DTDFvocab:Service" }, + { id = "enablers", alias = "enabler", type = "DTDFvocab:Enabler" }, + { id = "models", alias = "model", type = "DTDFvocab:Model" }, + { id = "provided_things", alias = "provided", type = "DTDFvocab:ProvidedThing" }, + { id = "data_transmitted", alias = "dataT", type = "DTDFvocab:DataTransmitted" }, + { id = "data", alias = "data", type = "DTDFvocab:Data" }, + { id = "sensors", alias = "sensor", type = "DTDFvocab:SensingComponent" }, + + # Explicit template queries + { id = "env", template = "env" }, + { id = "sys_component", template = "sys_component" }, + + # RabbitMQ + { id = "rabbit_exchange", alias = "exc", type = "rabbit:ExchangeName", template = "rabbit" }, + { id = "rabbit_routing_key", alias = "route", type = "rabbit:RoutingKey", template = "rabbit" }, + { id = "rabbit_source", alias = "source", type = "rabbit:Source", template = "rabbit" }, + { id = "rabbit_message_listener", alias = "ml", type = "rabbit:MessageListener", template = "rabbit" }, + + # Characteristics + { id = "characteristic_system_under_study", alias = "c1", type = "DTDFvocab:SystemUnderStudy" }, + { id = "characteristic_acting_component", alias = "c2", type = "DTDFvocab:ActingComponent" }, + { id = "characteristic_data_transmitted", alias = "c4", type = "DTDFvocab:DataTransmitted" }, + { id = "characteristic_virtual_to_physical", alias = "c5", type = "DTDFvocab:VirtualToPhysical" }, + { id = "characteristic_time_scale", alias = "c7", type = "DTDFvocab:TimeScale" }, + { id = "characteristic_multiplicity", alias = "c8", type = "DTDFvocab:Multiplicity" }, + { id = "characteristic_life_cycle_stage", alias = "c9", type = "DTDFvocab:LifeCycleStage" }, + { id = "characteristic_constellation", alias = "c12", type = "DTDFvocab:Constellation" }, + { id = "characteristic_evolution_stage", alias = "c13", type = "DTDFvocab:EvolutionStage" }, + { id = "characteristic_fidelity_consideration", alias = "c14", type = "DTDFvocab:FidelityConsideration" }, + { id = "characteristic_technical_connection", alias = "c15", type = "DTDFvocab:TechnicalConnection" }, + { id = "characteristic_deployment", alias = "c16", type = "DTDFvocab:Deployment" }, + { id = "characteristic_horizontal_integration", alias = "c18", type = "DTDFvocab:HoriIntegration" }, + { id = "characteristic_data_ownership", alias = "c19", type = "DTDFvocab:DataOwnershipPrivacy" }, + { id = "characteristic_standardization", alias = "c20", type = "DTDFvocab:Standardization" }, + { id = "characteristic_security_safety", alias = "c21", type = "DTDFvocab:SecuritySafety" } +] + +func _init() -> void: + for meta in QUERY_METADATA: + QUERIES[meta.id] = _build_query(meta) + +func _build_query(meta: Dictionary) -> String: + var template_name = meta.get("template", "standard") + var format_data = PREFIXES.duplicate() + format_data["alias"] = meta.get("alias", "") + format_data["rdf_type"] = meta.get("type", "") + return TEMPLATES[template_name].format(format_data) diff --git a/Fuseki/Data/FusekiData.gd b/Fuseki/Data/FusekiData.gd index dd8e423..21f2fba 100644 --- a/Fuseki/Data/FusekiData.gd +++ b/Fuseki/Data/FusekiData.gd @@ -1,97 +1,125 @@ +## Central data store for all information retrieved from the Fuseki triple store. +## +## Responsibilities: +## - Receive raw JSON responses from Fuseki queries (via input_data_from_fuseki_JSON). +## - Parse and store them into typed GDScript dictionaries and arrays. +## - Derive directed relationship arrays (e.g. model → enabler) after each update. +## - Provide a clean slate via empty() before each new query cycle. +## +## Data layout per entity dictionary: +## { "": { "": ["", ...], ... }, ... } + extends Node class_name FusekiData -#Represent a link between two elements by their names + +## Represents a directed link between two entities, identified by name. class GenericLinkedNodes: var source: String var destination: String -#Internal data stucture + +## Internal intermediate structure used during JSON parsing. +## Holds one column value from a SPARQL result row. class JsonValue: var json_name: String - var json_value : String - -#Store parsed values from Fuseki -var service : Dictionary -var enabler : Dictionary -var model : Dictionary -var provided_thing : Dictionary -var data_transmitted : Dictionary -var sensing_component : Dictionary -var env : Dictionary -var sys_component : Dictionary -var data : Dictionary -var c1 : Dictionary -var c2 : Dictionary -var c4 : Dictionary -var c5 : Dictionary -var c7 : Dictionary -var c8 : Dictionary -var c9 : Dictionary -var c12 : Dictionary -var c13 : Dictionary -var c14 : Dictionary -var c15 : Dictionary -var c16 : Dictionary -var c18 : Dictionary -var c19 : Dictionary -var c20 : Dictionary -var c21 : Dictionary - -var service_to_provided_thing : Array[GenericLinkedNodes] -var enabler_to_service : Array[GenericLinkedNodes] -var model_to_enabler : Array[GenericLinkedNodes] -var sensor_to_data_transmitted : Array[GenericLinkedNodes] -var data_to_enabler : Array[GenericLinkedNodes] -var data_transmitted_to_data : Array[GenericLinkedNodes] - -var rabbit_exchange : Dictionary -var rabbit_route : Dictionary -var rabbit_source : Dictionary -var rabbit_message_listener : Dictionary - -func _ready(): + var json_value: String + + +# ── DTDF entity dictionaries ────────────────────────────────────────────────── +var service: Dictionary +var enabler: Dictionary +var model: Dictionary +var provided_thing: Dictionary +var data_transmitted: Dictionary +var sensing_component: Dictionary +var env: Dictionary +var sys_component: Dictionary +var data: Dictionary + +# ── DT characteristic dictionaries (numbered per the DTDF taxonomy) ─────────── +var c1: Dictionary # System under study +var c2: Dictionary # Physical acting components +var c4: Dictionary # Physical-to-virtual interaction +var c5: Dictionary # Virtual-to-physical interaction +var c7: Dictionary # Twinning time-scale +var c8: Dictionary # Multiplicities +var c9: Dictionary # Life-cycle stages +var c12: Dictionary # DT constellation +var c13: Dictionary # Twinning process and evolution +var c14: Dictionary # Fidelity and validity +var c15: Dictionary # DT technical connection +var c16: Dictionary # DT hosting/deployment +var c18: Dictionary # Horizontal integration +var c19: Dictionary # Data ownership and privacy +var c20: Dictionary # Standardization +var c21: Dictionary # Security and safety + +# ── Derived directed-relationship arrays ────────────────────────────────────── +var service_to_provided_thing: Array[GenericLinkedNodes] +var enabler_to_service: Array[GenericLinkedNodes] +var model_to_enabler: Array[GenericLinkedNodes] +var sensor_to_data_transmitted: Array[GenericLinkedNodes] +var data_to_enabler: Array[GenericLinkedNodes] +var data_transmitted_to_data: Array[GenericLinkedNodes] + +# ── RabbitMQ entity dictionaries ────────────────────────────────────────────── +var rabbit_exchange: Dictionary +var rabbit_route: Dictionary +var rabbit_source: Dictionary +var rabbit_message_listener: Dictionary + + +func _ready() -> void: FusekiSignals.fuseki_data_updated.connect(_on_data_updated) FusekiSignals.fuseki_data_clear.connect(empty) -func _on_data_updated(): + +func _on_data_updated() -> void: build_relations() -# Take a JSON from a Fuseki query and store the resulting information in -# variables reachable from the Godot application -func input_data_from_fuseki_JSON(json): + +## Receives a raw Fuseki JSON response for a single SPARQL query and routes +## the parsed result into the appropriate member variable. +## +## Expected JSON shape: +## { "head": { "vars": ["", "attribute", "value"] }, +## "results": { "bindings": [...] } } +func input_data_from_fuseki_JSON(json) -> void: if json == null: - printerr("Incorrect JSON: It is null") + printerr("Incorrect JSON: it is null") return if json["head"]["vars"].size() != 3: - printerr("Incorrect JSON head: should be [\"\", \"attribute\", \"value\"]") + printerr("Incorrect JSON head: expected [\"\", \"attribute\", \"value\"]") return - - var json_variable : String = json["head"]["vars"][0] - var value = parse_fuseki_json(json) - - var targets = { - FusekiConfig.JsonHead.SERVICE: "service", - FusekiConfig.JsonHead.ENABLER: "enabler", - FusekiConfig.JsonHead.MODEL: "model", - FusekiConfig.JsonHead.DATA_TRANSMITTED: "data_transmitted", - FusekiConfig.JsonHead.PROVIDED: "provided_thing", - FusekiConfig.JsonHead.SENSOR: "sensing_component", - FusekiConfig.JsonHead.ENV: "env", - FusekiConfig.JsonHead.SYSTEM_COMPONENT: "sys_component", - FusekiConfig.JsonHead.DATA: "data", - FusekiConfig.JsonHead.RABBIT_EXCHANGE: "rabbit_exchange", - FusekiConfig.JsonHead.RABBIT_ROUTE: "rabbit_route", - FusekiConfig.JsonHead.RABBIT_SOURCE: "rabbit_source", - FusekiConfig.JsonHead.RABBIT_MESSAGE_LISTENER: "rabbit_message_listener", - FusekiConfig.JsonHead.C1: "c1", - FusekiConfig.JsonHead.C2: "c2", - FusekiConfig.JsonHead.C4: "c4", - FusekiConfig.JsonHead.C5: "c5", - FusekiConfig.JsonHead.C7: "c7", - FusekiConfig.JsonHead.C8: "c8", - FusekiConfig.JsonHead.C9: "c9", + + # The first variable name identifies which entity type this query covers. + var json_variable: String = json["head"]["vars"][0] + var value = FusekiParser.parse_fuseki_json(json) + + # Map the SPARQL variable name to the corresponding FusekiData member. + var targets := { + FusekiConfig.JsonHead.SERVICE: "service", + FusekiConfig.JsonHead.ENABLER: "enabler", + FusekiConfig.JsonHead.MODEL: "model", + FusekiConfig.JsonHead.DATA_TRANSMITTED: "data_transmitted", + FusekiConfig.JsonHead.PROVIDED: "provided_thing", + FusekiConfig.JsonHead.SENSOR: "sensing_component", + FusekiConfig.JsonHead.ENV: "env", + FusekiConfig.JsonHead.SYSTEM_COMPONENT: "sys_component", + FusekiConfig.JsonHead.DATA: "data", + FusekiConfig.JsonHead.RABBIT_EXCHANGE: "rabbit_exchange", + FusekiConfig.JsonHead.RABBIT_ROUTE: "rabbit_route", + FusekiConfig.JsonHead.RABBIT_SOURCE: "rabbit_source", + FusekiConfig.JsonHead.RABBIT_MESSAGE_LISTENER:"rabbit_message_listener", + FusekiConfig.JsonHead.C1: "c1", + FusekiConfig.JsonHead.C2: "c2", + FusekiConfig.JsonHead.C4: "c4", + FusekiConfig.JsonHead.C5: "c5", + FusekiConfig.JsonHead.C7: "c7", + FusekiConfig.JsonHead.C8: "c8", + FusekiConfig.JsonHead.C9: "c9", FusekiConfig.JsonHead.C12: "c12", FusekiConfig.JsonHead.C13: "c13", FusekiConfig.JsonHead.C14: "c14", @@ -102,112 +130,62 @@ func input_data_from_fuseki_JSON(json): FusekiConfig.JsonHead.C20: "c20", FusekiConfig.JsonHead.C21: "c21", } - + if targets.has(json_variable): - set(targets[json_variable], value) # Fill the variables with data - - -#Parse the json from Fuseki into internal data structure -#Return an Array of GenericLinkedNodes or a Dictionary depending on the data type -static func parse_fuseki_json(json, is_link = false): - var json_head = json["head"]["vars"] - var json_results = json["results"]["bindings"] - var result_aggregator: = [] - for result in json_results: - if (result == null): - break - var tuple_aggregator = [] - for head in json_head: - if (result.has(head)): - var new_json_value = JsonValue.new() - new_json_value.json_name = head - new_json_value.json_value = parse_fuseki_value(result[head]["value"]) - tuple_aggregator.append(new_json_value) - result_aggregator.append(tuple_aggregator) - #Transform the result agregator in an array of GenericLinkedNodes or - #in a dictionary depending on the data type - return parse_link_result(result_aggregator) if (is_link) else parse_element_result(result_aggregator) - -#Remove oml link data from values -static func parse_fuseki_value(value) -> String: - if(value.contains("#")): - return value.split("#")[1] - return value - -#Transform the result agregator in an Array of GenericLinkedNodes -static func parse_link_result(result_agregator) -> Array[GenericLinkedNodes]: - var formated_result : Array[GenericLinkedNodes] = [] - for result in result_agregator: - var link = GenericLinkedNodes.new() - link.first_node_name = result[0].json_value - link.second_node_name = result[1].json_value - formated_result.append(link) - return formated_result - -#Transform the result agregator in a dictionary, in each entry corresponding -#to an element is a disctionary containing every attributes of this element -static func parse_element_result(result_agregator) -> Dictionary: - var formated_result : Dictionary = {} - for result in result_agregator: - var entry_name = result[0].json_value - if (not formated_result.has(entry_name)): - formated_result[entry_name] = {} - var attribute_name = result[1].json_value - var attribute_value = result[2].json_value - if attribute_name in formated_result[entry_name].keys() : - formated_result[entry_name][attribute_name].append(attribute_value) - else: - var entry_value : Dictionary = {} - entry_value[attribute_name] = [attribute_value] - formated_result[entry_name].merge(entry_value) - return formated_result - -#Build relations attributes -func build_relations(): - model_to_enabler = build_relations_from(model, FusekiConfig.RelationAttribute.MODEL_TO_ENABLER) - enabler_to_service = build_relations_from(enabler, FusekiConfig.RelationAttribute.ENABLER_TO_SERVICE) - service_to_provided_thing = build_relations_from(service, FusekiConfig.RelationAttribute.SERVICES_TO_PROVIDED) - sensor_to_data_transmitted = build_relations_from(data_transmitted, FusekiConfig.RelationAttribute.SENSOR_TO_DATA_TRANSMITTED, true) - data_transmitted_to_data = build_relations_from(data, FusekiConfig.RelationAttribute.DATA_TRANSMITTED_TO_DATA, true) - data_to_enabler = build_relations_from(data, FusekiConfig.RelationAttribute.DATA_TO_ENABLER) - -#Build relations between two elements from attributes -func build_relations_from(element_data : Dictionary, link_attribute : String, inversed : bool = false) -> Array[GenericLinkedNodes]: - var resulting_relations : Array[GenericLinkedNodes] = [] + set(targets[json_variable], value) + + + + + +## Rebuilds all directed-relationship arrays from the current entity data. +## Called automatically whenever FusekiSignals.fuseki_data_updated fires. +func build_relations() -> void: + model_to_enabler = build_relations_from(model, FusekiConfig.RelationAttribute.MODEL_TO_ENABLER) + enabler_to_service = build_relations_from(enabler, FusekiConfig.RelationAttribute.ENABLER_TO_SERVICE) + service_to_provided_thing = build_relations_from(service, FusekiConfig.RelationAttribute.SERVICES_TO_PROVIDED) + sensor_to_data_transmitted = build_relations_from(data_transmitted,FusekiConfig.RelationAttribute.SENSOR_TO_DATA_TRANSMITTED, true) + data_transmitted_to_data = build_relations_from(data, FusekiConfig.RelationAttribute.DATA_TRANSMITTED_TO_DATA, true) + data_to_enabler = build_relations_from(data, FusekiConfig.RelationAttribute.DATA_TO_ENABLER) + + +## Derives a relationship array from an entity dictionary by inspecting a +## specific link attribute. +## +## [param element_data] The source entity dictionary to scan. +## [param link_attribute] The attribute name whose values are linked entity names. +## [param inversed] When true, the linked-entity name becomes the source +## and the owning entity becomes the destination. Useful +## when the relation is stored on the "wrong" side of the edge. +func build_relations_from( + element_data: Dictionary, + link_attribute: String, + inversed: bool = false +) -> Array[GenericLinkedNodes]: + var resulting_relations: Array[GenericLinkedNodes] = [] for element_name in element_data.keys(): - if (link_attribute in element_data[element_name]): - var relation_list : Array = element_data[element_name][link_attribute] + if link_attribute in element_data[element_name]: + var relation_list: Array = element_data[element_name][link_attribute] for linked_name in relation_list: - var new_relation : GenericLinkedNodes = GenericLinkedNodes.new() - new_relation.source = element_name if (not inversed) else linked_name - new_relation.destination = linked_name if (not inversed) else element_name + var new_relation := GenericLinkedNodes.new() + new_relation.source = element_name if not inversed else linked_name + new_relation.destination = linked_name if not inversed else element_name resulting_relations.append(new_relation) return resulting_relations -#Empty all elements and relations -func empty(): - service = {} - enabler = {} - model = {} - provided_thing = {} - data_transmitted = {} - sensing_component = {} - env = {} - sys_component = {} - data = {} - - service_to_provided_thing = [] - enabler_to_service = [] - model_to_enabler = [] - sensor_to_data_transmitted = [] - data_to_enabler = [] - data_transmitted_to_data = [] - - rabbit_exchange = {} - rabbit_route = {} - rabbit_source = {} - rabbit_message_listener = {} - -#Call dump function -func dump(dump_path : String, to_console : bool = false): + +## Clears all cached entity data, characteristic data, relationship arrays, +## and RabbitMQ data. Called before each new Fuseki query cycle. +func empty() -> void: + for prop in get_property_list(): + var val = get(prop["name"]) + if typeof(val) == TYPE_DICTIONARY: + val.clear() + elif typeof(val) == TYPE_ARRAY: + val.clear() + + +## Convenience wrapper: dumps the architecture to [param dump_path] via +## [FusekiDataDumper]. Pass [param to_console] = true to print instead. +func dump(dump_path: String, to_console: bool = false) -> void: FusekiDataDumper.dump_architecture(self, dump_path, to_console) diff --git a/Fuseki/Data/FusekiParser.gd b/Fuseki/Data/FusekiParser.gd new file mode 100644 index 0000000..d165241 --- /dev/null +++ b/Fuseki/Data/FusekiParser.gd @@ -0,0 +1,68 @@ +## stateless utility class for parsing +class_name FusekiParser + +## Internal intermediate structure used during JSON parsing. +## Holds one column value from a SPARQL result row. +class JsonValue: + var json_name: String + var json_value: String + + +## Parses a Fuseki JSON response into either: +## - A Dictionary (when is_link = false, the default) — for entity data. +## - An Array[FusekiData.GenericLinkedNodes] (when is_link = true) — for relationship data. +static func parse_fuseki_json(json: Dictionary, is_link: bool = false): + var json_head = json["head"]["vars"] + var json_results = json["results"]["bindings"] + var result_aggregator := [] + + for result in json_results: + if result == null: + break + var tuple_aggregator := [] + for head in json_head: + if result.has(head): + var new_json_value := JsonValue.new() + new_json_value.json_name = head + new_json_value.json_value = parse_fuseki_value(result[head]["value"]) + tuple_aggregator.append(new_json_value) + result_aggregator.append(tuple_aggregator) + + return parse_link_result(result_aggregator) if is_link else parse_element_result(result_aggregator) + + +## Strips the namespace prefix from a URI value, keeping only the fragment +## after '#'. Non-URI values are returned unchanged. +static func parse_fuseki_value(value: String) -> String: + if value.contains("#"): + return value.split("#")[1] + return value + + +## Converts a flat result aggregator into an Array[FusekiData.GenericLinkedNodes]. +## Expects each inner array to have exactly two JsonValue entries. +static func parse_link_result(result_aggregator: Array) -> Array[FusekiData.GenericLinkedNodes]: + var formatted_result: Array[FusekiData.GenericLinkedNodes] = [] + for result in result_aggregator: + var link := FusekiData.GenericLinkedNodes.new() + link.source = result[0].json_value + link.destination = result[1].json_value + formatted_result.append(link) + return formatted_result + + +## Converts a flat result aggregator into a nested Dictionary. +static func parse_element_result(result_aggregator: Array) -> Dictionary: + var formatted_result: Dictionary = {} + for result in result_aggregator: + var entry_name: String = result[0].json_value + if not formatted_result.has(entry_name): + formatted_result[entry_name] = {} + if result.size() >= 3: + var attribute_name: String = result[1].json_value + var attribute_value: String = result[2].json_value + if attribute_name in formatted_result[entry_name]: + formatted_result[entry_name][attribute_name].append(attribute_value) + else: + formatted_result[entry_name][attribute_name] = [attribute_value] + return formatted_result diff --git a/Fuseki/Data/FusekiParser.gd.uid b/Fuseki/Data/FusekiParser.gd.uid new file mode 100644 index 0000000..4c467ca --- /dev/null +++ b/Fuseki/Data/FusekiParser.gd.uid @@ -0,0 +1 @@ +uid://c47oj0a3csd2j diff --git a/Fuseki/Data/FusekiSignals.gd b/Fuseki/Data/FusekiSignals.gd index ded4008..b35e761 100644 --- a/Fuseki/Data/FusekiSignals.gd +++ b/Fuseki/Data/FusekiSignals.gd @@ -1,4 +1,11 @@ +## Global signal bus for Fuseki-related events. +## Add this node as an AutoLoad singleton so signals can be emitted and +## connected from anywhere without needing a direct node reference. + extends Node +## Emitted after all Fuseki queries have completed and FusekiData has been populated. signal fuseki_data_updated + +## Emitted before a new round of Fuseki queries begins, so listeners can clear stale data. signal fuseki_data_clear diff --git a/Fuseki/Dump/FusekiDumper.gd b/Fuseki/Dump/FusekiDumper.gd index f34d6e3..cada518 100644 --- a/Fuseki/Dump/FusekiDumper.gd +++ b/Fuseki/Dump/FusekiDumper.gd @@ -1,307 +1,381 @@ +## Handles persisting and loading FusekiData to/from disk. +## +## Two output formats are supported: +## • YAML-like architecture dump — plain-text, one line per attribute. +## • HTML characteristics table — a fragment for embedding in reports. +## +## Both formats can also be printed to the Godot console (to_console = true). +## +## Loading (load_from_dump) is the reverse of the YAML dump: it reads the +## plain-text file back and reconstructs the FusekiData dictionaries. + extends Node class_name FusekiDataDumper @onready var file_path_input: TextEdit = $DumpPathEdit -@onready var file_dialog: FileDialog = $FileDialog - -# Configuration for dump sections - easier to maintain and extend -const DUMP_CONFIG = [ - {indicator = "Services :", field = "service"}, - {indicator = "Enablers :", field = "enabler"}, - {indicator = "Models :", field = "model"}, - {indicator = "ProvidedThings :", field = "provided_thing"}, - {indicator = "Data transmitted :", field = "data_transmitted"}, - {indicator = "Sensors :", field = "sensing_component"}, - {indicator = "System :", field = "sys_component"}, +@onready var file_dialog: FileDialog = $FileDialog + +var fuseki_data_manager: FusekiData + + +# ── Configuration tables ─────────────────────────────────────────────────────── + +## Drives both dump_architecture and load_from_dump. +## Each entry maps a section header string to a FusekiData field name. +## Order matters: load_from_dump uses adjacent indicators to delimit sections. +const DUMP_CONFIG := [ + {indicator = "Services :", field = "service"}, + {indicator = "Enablers :", field = "enabler"}, + {indicator = "Models :", field = "model"}, + {indicator = "ProvidedThings :", field = "provided_thing"}, + {indicator = "Data transmitted :", field = "data_transmitted"}, + {indicator = "Sensors :", field = "sensing_component"}, + {indicator = "System :", field = "sys_component"}, {indicator = "System environnement :", field = "env"}, - {indicator = "Data :", field = "data"}, - {indicator = "Rabbit exchange :", field = "rabbit_exchange"}, - {indicator = "Rabbit route :", field = "rabbit_route"}, - {indicator = "Rabbit source :", field = "rabbit_source"}, - {indicator = "Rabbit message listener :", field = "rabbit_message_listener"} + {indicator = "Data :", field = "data"}, + {indicator = "Rabbit exchange :", field = "rabbit_exchange"}, + {indicator = "Rabbit route :", field = "rabbit_route"}, + {indicator = "Rabbit source :", field = "rabbit_source"}, + {indicator = "Rabbit message listener :", field = "rabbit_message_listener"}, ] -# HTML characteristics configuration -const CHARACTERISTICS_CONFIG = [ - {id = "C1", name = "System under study", field = "c1", desc_only = true}, - {id = "C2", name = "Physical acting components", field = "c2", desc_only = false}, - {id = "C3", name = "Physical sensing components", field = "sensing_component", desc_only = false}, - {id = "C4", name = "Physical-to-virtual interaction", field = "c4", desc_only = false}, - {id = "C5", name = "Virtual-to-physical interaction", field = "c5", desc_only = true}, - {id = "C6", name = "DT services", field = "service", desc_only = false}, - {id = "C7", name = "Twinning time-scale", field = "c7", desc_only = true}, - {id = "C8", name = "Multiplicities", field = "c8", desc_only = true}, - {id = "C9", name = "Life-cycle stages", field = "c9", desc_only = true}, - {id = "C10", name = "DT models and data", fields = ["model", "data"], desc_only = false, is_composite = true}, - {id = "C11", name = "Tooling and enablers", field = "enabler", desc_only = false}, - {id = "C12", name = "DT constellation", field = "c12", desc_only = true}, - {id = "C13", name = "Twinning process and DT evolution", field = "c13", desc_only = true}, - {id = "C14", name = "Fidelity and validity considerations", field = "c14", desc_only = true}, - {id = "C15", name = "DT technical connection", field = "c15", desc_only = true}, - {id = "C16", name = "DT hosting/deployment", field = "c16", desc_only = true}, - {id = "C17", name = "Insights and decision making", field = "provided_thing", desc_only = false}, - {id = "C18", name = "Horizontal integration", field = "c18", desc_only = true}, - {id = "C19", name = "Data ownership and privacy", field = "c19", desc_only = true}, - {id = "C20", name = "Standardization", field = "c20", desc_only = true}, - {id = "C21", name = "Security and safety considerations", field = "c21", desc_only = true} +## Drives dump_characteristics_table. Each entry describes one row in the HTML table. +## Fields: +## id — HTML string for the characteristic id cell (may contain ). +## name — Human-readable characteristic name. +## field — FusekiData member to read (or 'fields' for composite entries). +## desc_only — When true, only the 'desc' attribute is shown (no entity names). +## is_composite (optional) — When true, 'fields' (Array) is used instead of 'field'. +const CHARACTERISTICS_CONFIG := [ + {id = "C1", name = "System under study", field = "c1", desc_only = true}, + {id = "C2", name = "Physical acting components", field = "c2", desc_only = false}, + {id = "C3", name = "Physical sensing components", field = "sensing_component",desc_only = false}, + {id = "C4", name = "Physical-to-virtual interaction", field = "c4", desc_only = false}, + {id = "C5", name = "Virtual-to-physical interaction", field = "c5", desc_only = true}, + {id = "C6", name = "DT services", field = "service", desc_only = false}, + {id = "C7", name = "Twinning time-scale", field = "c7", desc_only = true}, + {id = "C8", name = "Multiplicities", field = "c8", desc_only = true}, + {id = "C9", name = "Life-cycle stages", field = "c9", desc_only = true}, + {id = "C10", name = "DT models and data", fields = ["model", "data"], desc_only = false, is_composite = true}, + {id = "C11", name = "Tooling and enablers", field = "enabler", desc_only = false}, + {id = "C12", name = "DT constellation", field = "c12", desc_only = true}, + {id = "C13", name = "Twinning process and DT evolution", field = "c13", desc_only = true}, + {id = "C14", name = "Fidelity and validity considerations", field = "c14", desc_only = true}, + {id = "C15", name = "DT technical connection", field = "c15", desc_only = true}, + {id = "C16", name = "DT hosting/deployment", field = "c16", desc_only = true}, + {id = "C17", name = "Insights and decision making", field = "provided_thing", desc_only = false}, + {id = "C18", name = "Horizontal integration", field = "c18", desc_only = true}, + {id = "C19", name = "Data ownership and privacy", field = "c19", desc_only = true}, + {id = "C20", name = "Standardization", field = "c20", desc_only = true}, + {id = "C21", name = "Security and safety considerations", field = "c21", desc_only = true}, ] -var fuseki_data_manager: FusekiData +# ── Lifecycle ───────────────────────────────────────────────────────────────── + +## Injects the shared [FusekiData] manager used by button-driven operations. func set_fuseki_data_manager(manager: FusekiData) -> void: fuseki_data_manager = manager + +# ── Button callbacks ────────────────────────────────────────────────────────── + func _on_load_button_pressed() -> void: - var result = _load_file_content(file_path_input.text) + var result := _load_file_content(file_path_input.text) if result.success: load_from_dump(fuseki_data_manager, result.content) print("Data loaded successfully from: ", file_path_input.text) else: - print("Error loading file: ", result.error) + push_error("FusekiDumper: Error loading file: " + result.error) + func _on_dump_button_pressed() -> void: if not fuseki_data_manager: - print("Error: No FusekiData manager set") + push_error("FusekiDumper: No FusekiData manager set") return - - var result = dump_architecture(fuseki_data_manager, file_path_input.text) + + var result := dump_architecture(fuseki_data_manager, file_path_input.text) if result.success: print("Data dumped successfully to: ", file_path_input.text) else: - print("Error dumping data: ", result.error) + push_error("FusekiDumper: Error dumping data: " + result.error) -# Helper function to safely load file content + +# ── File I/O helpers ────────────────────────────────────────────────────────── + +## Returns {success, content} or {success, error}. func _load_file_content(path: String) -> Dictionary: - var file = FileAccess.open(path, FileAccess.READ) + var file := FileAccess.open(path, FileAccess.READ) if not file: return {success = false, error = "Could not open file: " + path} - - var content = file.get_as_text() + var content := file.get_as_text() file.close() return {success = true, content = content} -# Helper function to safely write file content + +## Returns {success} or {success, error}. func _write_file_content(path: String, content: String) -> Dictionary: - var file = FileAccess.open(path, FileAccess.WRITE) + var file := FileAccess.open(path, FileAccess.WRITE) if not file: return {success = false, error = "Could not create file: " + path} - file.store_string(content) file.close() return {success = true} + +# ── HTML characteristics table ──────────────────────────────────────────────── + +## Builds and writes (or prints) an HTML
fragment containing one row +## per DTDF characteristic. Returns {success} or {success, error}. static func dump_characteristics_table(data: FusekiData, dump_path: String, to_console: bool = false) -> Dictionary: if not data: return {success = false, error = "No data provided"} - - var html_content = _build_characteristics_html(data) - + + var html_content := _build_characteristics_html(data) + if to_console: print(html_content) return {success = true} else: - var dumper = FusekiDataDumper.new() - var result = dumper._write_file_content(dump_path, html_content) + var dumper := FusekiDataDumper.new() + var result := dumper._write_file_content(dump_path, html_content) if result.success: - print("HTML data dumped at: ", dump_path) + print("HTML characteristics table written to: ", dump_path) return result + static func _build_characteristics_html(data: FusekiData) -> String: - var html = PackedStringArray() - - html.append("
") - html.append(" ") - html.append(" ") - html.append(" ") - html.append(" ") - html.append(" ") - + var html := PackedStringArray() + html.append("""
CharacteristicsDescription
+ + + + + """) + for config in CHARACTERISTICS_CONFIG: - var description = _get_characteristic_description(data, config) + var description := _get_characteristic_description(data, config) html.append(_create_html_table_row(config.id, config.name, description)) - + html.append("
CharacteristicsDescription
") return "\n".join(html) + +## Resolves the description cell content for a single characteristic config entry. static func _get_characteristic_description(data: FusekiData, config: Dictionary) -> String: if config.get("is_composite", false): - # Handle composite fields (like C10 with models and data) - var parts = PackedStringArray() - for i in range(config.fields.size()): - var field_name = config.fields[i] + # Composite characteristic (e.g. C10: models + data): render each sub-field + # as a labelled block separated by a blank line. + var parts := PackedStringArray() + for field_name in config.fields: var field_data = data.get(field_name) - var field_label = field_name.capitalize() + ":" - parts.append(field_label + "
" + _format_dictionary_html(field_data, config.desc_only)) + var label = field_name.capitalize() + ":" + parts.append(label + "
" + _format_dictionary_html(field_data, config.desc_only)) return "

".join(parts) else: var field_data = data.get(config.field) return _format_dictionary_html(field_data, config.desc_only) + static func _create_html_table_row(id: String, characteristic: String, description: String) -> String: - return " \n " + id + \ - "\n " + characteristic + \ - "\n " + description + "\n " + return """ + %s + %s + %s + """ % [id, characteristic, description] + +## Formats a FusekiData entity dictionary as HTML. +## When [param desc_only] is true, only the 'desc' attribute value is shown; +## otherwise the entity name is prepended. static func _format_dictionary_html(dict: Dictionary, desc_only: bool = true) -> String: if not dict or dict.is_empty(): return "No data available" - - var results = PackedStringArray() - + + var results := PackedStringArray() for key in dict.keys(): - var entry = "" + var entry := "" if not desc_only: entry += str(key) - + var value = dict[key] - if value is Dictionary and value.has('desc'): + if value is Dictionary and value.has("desc"): if not desc_only: entry += ": " - var desc = str(value['desc']).strip_edges() - # Clean up array formatting + # Strip surrounding array brackets from the stored string value. + var desc: String = str(value["desc"]).strip_edges() desc = desc.replace("[\"", "").replace("\"]", "") entry += desc - + if not entry.is_empty(): results.append(entry) - + return "
".join(results) if not results.is_empty() else "No data available" + +# ── YAML-like architecture dump ─────────────────────────────────────────────── + +## Builds and writes (or prints) a plain-text architecture dump of all DTDF +## entities in [param data]. Returns {success} or {success, error}. static func dump_architecture(data: FusekiData, dump_path: String, to_console: bool = false) -> Dictionary: if not data: return {success = false, error = "No data provided"} - - var dump_content = _build_dump_content(data) - + + var dump_content := _build_dump_content(data) + if to_console: print(dump_content) return {success = true} else: - var dumper = FusekiDataDumper.new() + var dumper := FusekiDataDumper.new() return dumper._write_file_content(dump_path, dump_content) + static func _build_dump_content(data: FusekiData) -> String: - var content = PackedStringArray() - + var content := PackedStringArray() for config in DUMP_CONFIG: content.append(config.indicator) var field_data = data.get(config.field) content.append(_format_dictionary_dump(field_data)) - return "\n".join(content) + +## Serialises one entity dictionary to indented plain text: +## \t +## \t\t : //... static func _format_dictionary_dump(dict: Dictionary) -> String: - var lines = PackedStringArray() - + var lines := PackedStringArray() for key in dict.keys(): lines.append("\t" + key) var attributes = dict[key] if attributes is Dictionary: for attr_key in attributes.keys(): - var attr_value = _array_to_string(attributes[attr_key]) + var attr_value := _array_to_string(attributes[attr_key]) lines.append("\t\t" + attr_key + " : " + attr_value) - return "\n".join(lines) + +# ── Load from dump ──────────────────────────────────────────────────────────── + +## Parses a previously written architecture dump string back into [param fuseki_data]. +## Emits FusekiSignals.fuseki_data_updated on success. +## Returns {success, errors} where errors is a PackedStringArray of per-section messages. static func load_from_dump(fuseki_data: FusekiData, content: String) -> Dictionary: if not fuseki_data: return {success = false, error = "No FusekiData object provided"} - + + # Normalise line endings before splitting. content = content.replace("\r", "\n") - var lines = content.split("\n") - var success = true - var errors = PackedStringArray() - - # Load each section based on configuration + var lines := content.split("\n") + var success := true + var errors := PackedStringArray() + for i in range(DUMP_CONFIG.size()): var current_config = DUMP_CONFIG[i] - var next_indicator = DUMP_CONFIG[i + 1].indicator if i + 1 < DUMP_CONFIG.size() else "" - - var section_data = _extract_section(lines, current_config.indicator, next_indicator) + # Use the next section's indicator as the end boundary (empty string for last section). + var next_indicator: String = DUMP_CONFIG[i + 1].indicator if i + 1 < DUMP_CONFIG.size() else "" + + var section_data := _extract_section(lines, current_config.indicator, next_indicator) if section_data.success: fuseki_data.set(current_config.field, section_data.data) else: success = false - errors.append("Failed to load " + current_config.field + ": " + section_data.error) - + errors.append("Failed to load '" + current_config.field + "': " + section_data.error) + if success: FusekiSignals.fuseki_data_updated.emit() - + return {success = success, errors = errors} + +## Extracts the lines belonging to one dump section, delimited by indicator strings. +## Returns {success, data} or {success, error}. static func _extract_section(lines: PackedStringArray, start_indicator: String, end_indicator: String) -> Dictionary: - var start_idx = -1 - var end_idx = lines.size() - - # Find start position + var start_idx := -1 + var end_idx := lines.size() + + # Locate the start indicator line. for i in range(lines.size()): if lines[i] == start_indicator: start_idx = i break - + if start_idx == -1: return {success = false, error = "Start indicator not found: " + start_indicator} - - # Find end position + + # Locate the end indicator (next section header), if one exists. if not end_indicator.is_empty(): for i in range(start_idx + 1, lines.size()): if lines[i] == end_indicator: end_idx = i break - - var section_lines = lines.slice(start_idx + 1, end_idx) + + var section_lines := lines.slice(start_idx + 1, end_idx) return {success = true, data = _parse_dictionary_section(section_lines)} + +## Reconstructs an entity dictionary from a slice of indented dump lines. +## Lines starting with two tabs are attribute lines; one tab = entity name. static func _parse_dictionary_section(lines: PackedStringArray) -> Dictionary: - var elements = {} - var current_element = "" - + var elements: Dictionary = {} + var current_element := "" + for line in lines: - var clean_line = line.strip_edges() + var clean_line := line.strip_edges() if clean_line.is_empty(): continue - + if line.begins_with("\t\t"): # Attribute line if current_element.is_empty(): - continue # Skip orphaned attributes - - var attr_parts = clean_line.split(" : ", false, 1) + continue # Skip attributes that appear before any entity header. + var attr_parts := clean_line.split(" : ", false, 1) if attr_parts.size() == 2: - var attr_name = attr_parts[0].strip_edges() - var attr_value = _string_to_array(attr_parts[1].strip_edges()) + var attr_name := attr_parts[0].strip_edges() + var attr_value := _string_to_array(attr_parts[1].strip_edges()) elements[current_element][attr_name] = attr_value - - elif line.begins_with("\t"): # Element line + + elif line.begins_with("\t"): # Entity name line current_element = clean_line elements[current_element] = {} - + return elements + +# ── Serialisation helpers ───────────────────────────────────────────────────── + +## Joins an array of values with '/' for compact single-line storage. static func _array_to_string(array: Array) -> String: if array.is_empty(): return "" - - var string_array = PackedStringArray() + var string_array := PackedStringArray() for element in array: string_array.append(str(element)) - return "/".join(string_array) + +## Splits a '/' -delimited string back into an Array. Returns [] for empty input. static func _string_to_array(text: String) -> Array: if text.is_empty(): return [] return Array(text.split("/")) -# File picker functions + +# ── File-picker UI callbacks ─────────────────────────────────────────────────── + func _on_pick_button_pressed() -> void: file_dialog.visible = true + func _on_file_dialog_file_selected(path: String) -> void: file_path_input.text = path + +## Lock camera movement while the path TextEdit has focus so keyboard input +## is not inadvertently captured by the camera controller. func _on_dump_path_edit_focus_entered() -> void: CameraSignals.disable_camera_movement.emit() + func _on_dump_path_edit_focus_exited() -> void: CameraSignals.enable_camera_movement.emit() diff --git a/GenericDisplay/GenericDisplay.gd b/GenericDisplay/GenericDisplay.gd index 2e6c246..afce136 100644 --- a/GenericDisplay/GenericDisplay.gd +++ b/GenericDisplay/GenericDisplay.gd @@ -254,8 +254,6 @@ func update_chart(last_data) -> void: func set_default_style(): set_node_style(bg_color, true, false) set_node_style(border_color, false, true) - change_bg_color(bg_color) - change_border_color(border_color) change_text_color(Color.BLACK) func set_highlight_style(): diff --git a/Legends/Legends.gd b/Legends/Legends.gd index 33d7a83..3b7512b 100644 --- a/Legends/Legends.gd +++ b/Legends/Legends.gd @@ -41,17 +41,14 @@ func _on_node_style_option_item_selected(index: int) -> void: child.queue_free() match index: 1: - var deployment = StyleConfig.Deployment.new() - var deployment_map = deployment.get_script().get_script_constant_map() - for key in deployment_map: + var deployment_map = StyleConfig.Deployment.MAP + for key in deployment_map.keys(): instanciate_legend_element(key, deployment_map[key]) 2: - var implementation = StyleConfig.Implementation.new() - var implementation_map = implementation.get_script().get_script_constant_map() - for key in implementation_map: + var implementation_map = StyleConfig.Implementation.MAP + for key in implementation_map.keys(): instanciate_legend_element(key, implementation_map[key]) 3: - var timescale = StyleConfig.Timescale.new() - var timescale_map = timescale.get_script().get_script_constant_map() - for key in timescale_map: + var timescale_map = StyleConfig.Timescale.MAP + for key in timescale_map.keys(): instanciate_legend_element(key, timescale_map[key]) diff --git a/MainScene/CICDServer.gd b/MainScene/CICDServer.gd index f969cb3..74569e6 100644 --- a/MainScene/CICDServer.gd +++ b/MainScene/CICDServer.gd @@ -1,123 +1,159 @@ +## Exposes a local TCP server (default port 9090) used by CI/CD pipelines to +## trigger a full data refresh and capture pipeline: +## 1. Query the Fuseki server to reload the DT visualisation. +## 2. Dump the architecture as a YAML file. +## 3. Dump the characteristics table as an HTML file. +## 4. Capture a high-resolution screenshot by tiling viewport segments. +## Not started when running on the web platform. + extends Node -@export var margins : Vector2 = Vector2(50, 300) -@export var container : DT_PT -@export var to_hide : Array[Node] = [] +# ── Exported configuration ──────────────────────────────────────────────────── +## Extra whitespace (pixels) added around the DT container when compositing +## the screenshot. x = horizontal margin, y = vertical margin. +@export var margins: Vector2 = Vector2(50, 300) + +## The DT/PT container whose bounding rect defines the screenshot area. +@export var container: DT_PT +## UI nodes that should be hidden during screenshot capture so only the +## DT visualisation appears in the output image. +@export var to_hide: Array[Node] = [] + +# ── Server state ────────────────────────────────────────────────────────────── var server := TCPServer.new() -@export var port = 9090 -var screenshot_save_path = OS.get_user_data_dir() + "/latest_screenshot.png" -var dump_architecture_save_path = OS.get_user_data_dir() + "/data_dump.yaml" -var dump_characteristics_table_save_path = OS.get_user_data_dir() + "/characteristics-table.html" -func _ready(): +@export var port: int = 9090 + +# ── Output paths (written to the Godot user-data directory) ─────────────────── +var screenshot_save_path: String = OS.get_user_data_dir() + "/latest_screenshot.png" +var dump_architecture_save_path: String = OS.get_user_data_dir() + "/data_dump.yaml" +var dump_characteristics_table_save_path: String = OS.get_user_data_dir() + "/characteristics-table.html" + + +func _ready() -> void: + # The TCP server is only used in native (non-web) builds. if not OS.has_feature("web"): start_server() -func start_server(): + +func start_server() -> void: if server.listen(port) == OK: print("CICDServer started on port ", port) else: - print("Failed to start CICDServer") + push_error("CICDServer: Failed to listen on port " + str(port)) -func _process(_delta): + +func _process(_delta: float) -> void: if server.is_connection_available(): - var client = server.take_connection() + var client := server.take_connection() handle_request(client) -func handle_request(client: StreamPeerTCP): - # Call the Fuseki server + +## Handles a single incoming TCP connection by running the full +## capture+dump pipeline and returning an HTTP 200 response. +func handle_request(client: StreamPeerTCP) -> void: + # Trigger the Fuseki query so the visualisation reflects latest data. var fuseki_button = get_tree().root.get_node("MainScene/%FusekiCallerButton") fuseki_button._on_pressed() - - # Wait for the visualization to load + + # Allow 3 seconds for the visualisation to finish loading before capturing. await get_tree().create_timer(3).timeout - - # Dump the architecture as a yaml file + dump_architecture_yaml() - - # Dump the characteristics table as a html file dump_characteristics_table_html() - - # Capture a screenshot capture_image() - - # Return a success string - client.put_data("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 33\r\n\r\nScreenshot taken and data dumped!".to_utf8_buffer()) - - # Wait until the image is saved - await get_tree().create_timer(0.2).timeout - # Disconnect the client + # Minimal HTTP/1.1 response — body length must match the literal string below. + client.put_data( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 33\r\n\r\nScreenshot taken and data dumped!" + .to_utf8_buffer() + ) + + # Give the OS a moment to flush the image to disk before closing the socket. + await get_tree().create_timer(0.2).timeout client.disconnect_from_host() -func capture_image(): +## Captures a full-resolution screenshot of the DT container by tiling +## viewport-sized segments and blitting them into a single image. +func capture_image() -> void: print("[Start Capture...]") - - # Hide nodes (UI) + + # Temporarily hide UI nodes so they do not appear in the screenshot. for node_to_hide: Node in to_hide: node_to_hide.visible = false - - var camera=Camera2D.new() + + # Create a temporary camera fixed to the top-left of the scene. + var camera := Camera2D.new() self.add_child(camera) - camera.anchor_mode=Camera2D.ANCHOR_MODE_FIXED_TOP_LEFT + camera.anchor_mode = Camera2D.ANCHOR_MODE_FIXED_TOP_LEFT camera.enabled = true camera.make_current() - var full_rect = container.get_rect() - - # Apply margins around the screenshot container + + # Expand the bounding rect by the configured margins. + var full_rect := container.get_rect() full_rect.position.x -= margins.x / 2 full_rect.position.y -= margins.y / 2 full_rect.size.x += margins.x full_rect.size.y += margins.y - - var starting_position = full_rect.position - camera.position = starting_position - var tree=get_tree() + camera.position = full_rect.position + + # Pause the tree so the scene does not advance while we tile the screenshot. + var tree := get_tree() tree.paused = true await tree.process_frame var viewport_size = get_viewport().size / camera.zoom.x - var image = Image.create_empty(full_rect.size.x, full_rect.size.y, false, Image.FORMAT_RGBA8) - # Take multiple screenshots and merge them into one image + var image := Image.create_empty(full_rect.size.x, full_rect.size.y, false, Image.FORMAT_RGBA8) + + # Sweep the camera across the full rect, capturing one viewport tile at a time. while camera.position.y < full_rect.size.y: while camera.position.x < full_rect.size.x: await tree.process_frame - var segment = camera.get_viewport().get_texture().get_image() - - # Ensure segment uses the same format as the main image + var segment := camera.get_viewport().get_texture().get_image() + + # Normalise the segment format before blitting. if segment.get_format() != Image.FORMAT_RGBA8: segment.convert(Image.FORMAT_RGBA8) - - var target_position = Vector2i(int(camera.position.x - full_rect.position.x), int(camera.position.y - full_rect.position.y)) - var src_rect = Rect2i(Vector2i(0, 0), Vector2i(segment.get_width(), segment.get_height())) + + var target_position := Vector2i( + int(camera.position.x - full_rect.position.x), + int(camera.position.y - full_rect.position.y) + ) + var src_rect := Rect2i(Vector2i(0, 0), Vector2i(segment.get_width(), segment.get_height())) image.blit_rect(segment, src_rect, target_position) - + camera.position.x += viewport_size.x + # Return to the left edge and advance one row. camera.position.x = full_rect.position.x camera.position.y += viewport_size.y - - # Save the screenshot on the disk + image.save_png(screenshot_save_path) print("Screenshot saved at: ", screenshot_save_path) - get_tree().paused = false + + tree.paused = false camera.queue_free() - - # Show nodes + + # Restore previously hidden nodes. for node_to_hide: Node in to_hide: node_to_hide.visible = true - + print("[END Capture...]") -func dump_architecture_yaml(): + +## Writes the architecture YAML dump to [member dump_architecture_save_path]. +func dump_architecture_yaml() -> void: var dumper_controller = get_tree().root.get_node("MainScene/%Settings/%FusekiDumperController") - var fuseki_data : FusekiData = get_tree().root.get_node("MainScene/FusekiData") + var fuseki_data: FusekiData = get_tree().root.get_node("MainScene/FusekiData") dumper_controller.dump_architecture(fuseki_data, dump_architecture_save_path) -func dump_characteristics_table_html(): + +## Writes the characteristics-table HTML dump to +## [member dump_characteristics_table_save_path]. +func dump_characteristics_table_html() -> void: var dumper_controller = get_tree().root.get_node("MainScene/%Settings/%FusekiDumperController") - var fuseki_data : FusekiData = get_tree().root.get_node("MainScene/FusekiData") + var fuseki_data: FusekiData = get_tree().root.get_node("MainScene/FusekiData") dumper_controller.dump_characteristics_table(fuseki_data, dump_characteristics_table_save_path) diff --git a/MainScene/MainScene.tscn b/MainScene/MainScene.tscn index 7d6a3af..5875c99 100644 --- a/MainScene/MainScene.tscn +++ b/MainScene/MainScene.tscn @@ -346,10 +346,9 @@ layout_mode = 1 [node name="FusekiData" parent="." instance=ExtResource("5_4e51a")] -[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container", "to_hide")] +[node name="CICDServer" type="Node" parent="." node_paths=PackedStringArray("container")] script = ExtResource("15_gmiik") container = NodePath("../DTContainer") -to_hide = [NodePath("../ControlLayer")] [connection signal="toggled" from="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" to="ControlLayer/ControlPanel/MarginContainer/ControlContainer/RabbitButton" method="_on_toggled"] [connection signal="item_selected" from="ControlLayer/ViewPanel/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/NodeBGOption" to="DTContainer" method="_on_node_bg_option_item_selected"] diff --git a/MainScene/MainSceneController.gd b/MainScene/MainSceneController.gd index dd096d7..fcb874b 100644 --- a/MainScene/MainSceneController.gd +++ b/MainScene/MainSceneController.gd @@ -1,50 +1,67 @@ +## Root controller for the Main Scene. Wires together the core subsystems +## (Fuseki caller, data manager, DT container, and Fuseki dumper) and handles +## the platform-specific start-up path: +## - Native builds: RabbitMQ is connected for live data streaming. +## - Web builds: The control panel is hidden and YAML data is polled from +## the embedding page via JavaScriptBridge. + extends Node -#scene ressources + +# ── Scene references ────────────────────────────────────────────────────────── @onready var fuseki_caller = %FusekiCallerButton @onready var fuseki_dumper = $ControlLayer/SettingsBackground/Settings/%FusekiDumperController -@onready var fuseki_data : FusekiData = $FusekiData -@onready var dt_container : BoxContainer = $DTContainer +@onready var fuseki_data: FusekiData = $FusekiData +@onready var dt_container: BoxContainer = $DTContainer + -func _ready(): +func _ready() -> void: + # Inject the shared FusekiData manager into every subsystem that needs it. fuseki_caller.set_fuseki_data_manager(fuseki_data) fuseki_dumper.set_fuseki_data_manager(fuseki_data) dt_container.set_fuseki_data(fuseki_data) + + # React to Fuseki data updates to refresh the visualisation. FusekiSignals.fuseki_data_updated.connect(_update_fuseki_data) - + if OS.has_feature("web"): print("Detected running on the web") - # Hide UI + # Hide native-only UI elements that are not relevant in the web build. $ControlLayer/ControlPanel.hide() - - # Start checking for data after a short delay + + # Give the embedding page a moment to populate its data before polling. get_tree().create_timer(1.0).timeout.connect(_check_for_data) else: - # Use RabbitMQ if not on the web + # In native builds, RabbitMQ drives live data updates. RabbitMq.set_fuseki_data_manager(fuseki_data) -# Checks for YAML data from the web page it is embedded in -func _check_for_data(): + +## Polls the embedding web page for YAML data exposed on `window.parent.dataReady`. +## Retries every 0.5 s until data is available. +func _check_for_data() -> void: print("🔍 Checking for YAML data...") - - var js_code = """ + + var js_code := """ (function() { - // Check if parent window has data ready + // Return the pre-loaded data string if the parent page has it ready. if (window.parent && window.parent.dataReady) { return window.parent.dataReady; } return null; })() """ - + var window = JavaScriptBridge.get_interface("window") var result = window.eval(js_code) - - if result and result != null: + + if result: print("💡 Found YAML data") fuseki_dumper.load_from_dump(fuseki_data, result) else: print("⏳ No data yet, will try again...") get_tree().create_timer(0.5).timeout.connect(_check_for_data) -func _update_fuseki_data(): + +## Called when `FusekiSignals.fuseki_data_updated` fires; pushes fresh data +## into the DT container so the visualisation re-renders. +func _update_fuseki_data() -> void: dt_container.feed_fuseki_data(fuseki_data) diff --git a/MainScene/help_button.gd b/MainScene/help_button.gd index a36721f..ef96bc4 100644 --- a/MainScene/help_button.gd +++ b/MainScene/help_button.gd @@ -1,4 +1,5 @@ extends Button + func _on_help_button_pressed() -> void: $HelpPopup.popup() diff --git a/MainScene/python_runner.gd b/MainScene/python_runner.gd index 5f726cb..964ac9b 100644 --- a/MainScene/python_runner.gd +++ b/MainScene/python_runner.gd @@ -1,7 +1,11 @@ +## Shows the Script Runner panel when the Python Runner button is pressed, +## bringing it to the front so it overlays other UI elements. + extends Button @onready var script_runner_panel: Control = $"../../../ScriptRunnerBackground" -func _on_pressed(): + +func _on_pressed() -> void: script_runner_panel.visible = true script_runner_panel.move_to_front() diff --git a/MainScene/settings_button.gd b/MainScene/settings_button.gd index 710ff7b..77772be 100644 --- a/MainScene/settings_button.gd +++ b/MainScene/settings_button.gd @@ -1,12 +1,19 @@ +## Manages the Settings panel lifecycle: +## - Pressing the button shows the Settings panel and locks camera movement. +## - Receiving the "menu_closed" signal hides the panel and restores camera movement. + extends Button +@onready var settings_background: Control = get_node("/root/MainScene/ControlLayer/SettingsBackground") + + func _on_pressed() -> void: - #$SettingsPopup.popup() - get_node('/root/MainScene/ControlLayer/SettingsBackground').show() + settings_background.show() CameraSignals.disable_camera_movement.emit() CameraSignals.disable_camera_zoom.emit() + func _on_settings_settings_menu_closed() -> void: - get_node('/root/MainScene/ControlLayer/SettingsBackground').hide() + settings_background.hide() CameraSignals.enable_camera_movement.emit() CameraSignals.enable_camera_zoom.emit() diff --git a/README.md b/README.md index f345ef8..50ffbff 100644 --- a/README.md +++ b/README.md @@ -4,120 +4,207 @@ [![arXiv](https://img.shields.io/badge/arXiv-2508.18431-b31b1b.svg)](https://arxiv.org/pdf/2508.18431) [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/) -The purpose of this project is to provide users with a graphical and interactive representation of a Digital Twin. +DTInsight is a systematic and automated tool for producing **continuous reporting** for Digital Twins (DTs). It generates an interactive conceptual architecture visualization—called a **DT constellation**—from an ontological description of the DT, and integrates the output into a CI/CD-driven reporting page. -image +DTInsight conceptual architecture visualization + +## Key Features + +DTInsight is built around **three pillars of DT reporting**: + +| Pillar | What it does | +|---|---| +| **Explicit Reporting** | Model the DT using the 21 characteristics of the [DT Description Framework (DTDF)](https://github.com/oakeslabmtl/DTDF) in the Ontology Modeling Language (OML), providing a systematic and consistent description. | +| **Interactive Reporting** | Explore a DT constellation: a conceptual architecture showing data flow between Physical Twin components (Sensors, System, Environment, Operator, Machine) and DT capability layers (Models/Data, Enablers, Services). Supports real-time sensor data and 3D visualization. | +| **Continuous Reporting** | Auto-generate an up-to-date reporting page (characteristics table, architecture screenshot, interactive web embed) from a CI/CD pipeline on every commit. | ## YouTube Overview [![DTInsight video](https://img.youtube.com/vi/CD0pdK-eGXY/0.jpg)](https://www.youtube.com/watch?v=CD0pdK-eGXY) -## About this project +## Architecture & Technology Stack + +DTInsight is built with the [Godot Engine](https://godotengine.org/) (v4.5, .NET/C#) and uses GDScript for the UI and visualization logic. + +``` +DTInsight/ +├── MainScene/ # Main scene controller, CI/CD TCP server, settings +├── DTContainer/ # DT & PT constellation layout, link drawing, visual editing +├── GenericDisplay/ # Reusable DT component display, pop-up charts & scripts +├── Fuseki/ # SPARQL query catalogue, Fuseki server caller, data dump +├── RabbitMQ/ # RabbitMQ connection & real-time data handling (C#/.NET) +├── Camera/ # 2D camera controls (pan, zoom, keyboard) +├── Config/ # Global configuration (style, Fuseki, RabbitMQ, camera, etc.) +├── ControlPanel/ # Settings UI panel +├── Legends/ # Color legend for implementation status & timescale +├── Build/ # Pre-built exports (Linux, Web) +└── .github/workflows/ # GitHub Actions CI/CD for automated builds +``` + +**Key integrations:** +- **[Apache Jena Fuseki](https://jena.apache.org/documentation/fuseki2/)** — serves the DTDF ontology as an RDF triple store; DTInsight queries it via SPARQL over HTTP. +- **[RabbitMQ](https://www.rabbitmq.com/)** — message broker for subscribing to real-time sensor data streams (via the .NET RabbitMQ client library). +- **[openCAESAR Rosetta](https://github.com/opencaesar/oml-rosetta)** — recommended IDE for editing the OML description model. +- **GitHub Actions** — CI/CD workflow that exports Linux and Web builds on every push to `develop`. + +## Getting Started -This tool was built with the aim of being cross-compatible with any digital twin ontology built with the Ontology Modeling Language (OML) vocabulary available at [DTDF](https://github.com/oakeslabmtl/DTDF). To build such ontologies we recommend tools such as openCAESAR Rosetta available at [oml-rosetta](https://github.com/opencaesar/oml-rosetta). +### Prerequisites -To benefit from all of the real-time communication features you will need a related digital twin simulator publishing its data with RabbitMQ, similar to this project [example_digital-twin_incubator](https://github.com/INTO-CPS-Association/example_digital-twin_incubator), as well as an ontology providing the necessary data to connect it to the visualization. +- **Godot Engine 4.5** (.NET / C# version) — [download](https://godotengine.org/download) +- **.NET 8 SDK** — required for the RabbitMQ C# integration +- **Apache Jena Fuseki** — bundled via openCAESAR Rosetta, or standalone +- A **DTDF-compatible OML ontology** — see the [DTDF repository](https://github.com/oakeslabmtl/DTDF) +- *(Optional)* A running **RabbitMQ** instance publishing DT sensor data -## How to use +### Installation -To demonstrate how to use this tool we will take the example of the incubator project available with the links provided in the "About this project" section. +1. Clone the repository: + ```bash + git clone https://github.com/oakeslabmtl/DTInsight.git + ``` +2. Open the project in Godot Engine 4.5 (.NET version). +3. Build the C# solution to restore the RabbitMQ.Client NuGet package: + ```bash + dotnet restore + dotnet build + ``` +4. Run the project from the Godot editor or export it for your platform. + +## How to Use ### Settings -Click on the gear on the bottom left button to access the settings panel. +Click the **gear icon** (bottom-left) to open the settings panel. + +### Loading an Ontology + +#### Option A: Load from Fuseki (recommended for development) + +1. Open your OML ontology project in **openCAESAR Rosetta**. +2. In the **Gradle Tasks** tab, start the Fuseki server with the **`startFuseki`** task and run the **`owlLoad`** task to push the ontology data. + + Gradle tasks in Rosetta + + On Windows, you can also call these tasks with: + ``` + gradlew.bat startFuseki + ``` + and then + ``` + gradlew.bat owlLoad + ``` + +4. In DTInsight, click the **"Call Fuseki"** button: + + Call Fuseki button -### Load an ontology +#### Option B: Load from a dump file -#### Load from Fuseki +If a dump was previously exported, select the file with the file picker and click **"Load"**. -This method will be used while developing new ontologies and allow users to transfer a digital twin's ontology in the visualizer. +Load dump -In open CAESAR Rosetta, once the ontology is ready and buildable, you will need to access the "Gradle Tasks" tab. There you will be able to start a Fuseki server, it allows a network communication between Rosetta and the visualizer. Once the server starts, you will have to load the ontology's information in Fuseki with the task "LoadOML". +##### Creating a dump -image +After loading an ontology, enter a file path in the dump input box and click **"Dump"**. The resulting file can also be inspected in a text editor to verify loaded data. -With the Fuseki server ready your Digital Twin will be available in one click in the visualizer with the "Call Fuseki" button. +### Navigating the DT Constellation -image +Once loaded, you see a top-down conceptual architecture of the DT, structured as: -#### Load from a dump +- **Left side (Physical Twin):** Operator, Machine, System, System Environment, Sensors/Data Transmission +- **Right side (Digital Twin):** Models/Data → Enablers → Services -If a dump from an existing Digital Twin was created you will be able to access your visualization without going through the process of setting up a Fuseki server. You will only have to select the correct file from the file picker in the visualizer and click on the "Load" button. +Directional arrows show inter-component dependencies and data flow. -image +#### Camera Controls -##### Create a dump +| Action | QWERTY | AZERTY | Mouse | +|---|---|---|---| +| Move up | W | Z | Click + drag | +| Move down | S | S | Click + drag | +| Move left | A | Q | Click + drag | +| Move right | D | D | Click + drag | +| Zoom in | E | E | Scroll up | +| Zoom out | Q | A | Scroll down | -To create a dump, you will first have to load an ontology in the visualizer, then you will be able to put a dump file location in the correct input box and click on the "Dump" button. **This dump can be read to check on the informations currently loaded in the visualizer.** +#### Highlighting Relationships -### Navigate the Digital Twin +**Hover** over a component to highlight it and its connected components, revealing data-flow paths. **Click** to lock the highlight. -Once a Digital Twin is loaded into the visualizer, you will have a top-down view of its architecture and of its functional components. +Highlighting connected components -**Camera controls:** +#### Visual Editing -*QWERTY keyboard:* \ -W - Move up \ -S - Move down \ -A - Move left \ -D - Move Right \ -Q - Zoom out \ -E - Zoom in +Enable **Visual Editing** mode from the control panel to: +- **Add** new components to any container via the `+` buttons +- **Rename** or edit descriptions by right-clicking a component +- **Draw links** by dragging from one component to another +- **Delete links** by enabling link deletion mode and clicking on a link -*AZERTY keyboard:* \ -Z - Move up \ -S - Move down \ -Q - Move left \ -D - Move Right \ -A - Zoom out \ -E - Zoom in +### Real-Time Data (RabbitMQ) -*You can also move the camera by clicking and dragging it* +If your DT publishes sensor data via RabbitMQ and your ontology includes the necessary linking metadata: -**Highlighting relationships:** +1. Toggle **"Record data from RabbitMQ"** in the settings. +2. Real-time values will appear on the corresponding DT components. +3. Click the pop-up button on a component to view a **live chart** of the last 100 messages (supports numerical and boolean values). -By hovering the mouse pointer on top of a component in the visualization, you will be able to highlight the selected component as well as its connected components. This feature allows you to visualize interconnections within your Digital Twin and understand how the data flow into it. +Real-time data -image +> **Note:** If nothing appears after enabling real-time data, either the ontology is missing the RabbitMQ linking data or no messages are being published. Inspect a dump file to verify the data. -### Real-Time Data +### 3D Visualization (`.pck` resource pack) -If you have a Digital Twin publishing some data on RabbitMQ exchanges and an ontology providing the necessary linking data you will be able to access real time information on your Digital Twin components in the visualization. +1. Create a separate Godot project (same Godot version as DTInsight). +2. Create a scene called `main.tscn` with a `_on_message(message)` function to receive real-time data. +3. Export the project as a `.pck` file. +4. Ensure the OML description includes `DTDFVocab:HasVisualization true`. +5. In DTInsight, pick the `.pck` file to open the 3D visualization in a pop-up. -image +Visualization pick -To access this functionality, you will have to load an ontology and then click on the switch "Record data form RabbitMQ". Once this switch is activated, real time data will be displayed in the DIgital Twin's component as well as a button. This button allows you to access a real time graph recording the last hundred messages coming from RabbitMQ. (At this time this graph can display numerical or boolean values.) +The visualization reflects incoming sensor data (e.g., updated temperature labels, heater color changes). -**If nothing happens when enabling real time information, it either means that your ontology is missing the linking data or that there are no messages in the RabbitMQ exchange. You can explore a dump file with a text editor to check if linking data was correctly loaded.** +### Viewing Component Scripts -### Access Digital Twin Visualization +If the ontology contains relative file paths to DT source code: -#### Preparation +1. Select the **software folder** from the settings panel. -- Create or Open another Godot project containing your visualization. **Warning: Godot software version must match DTInsight's**. -- Create a scene called `main.tscn` that will host your visualization. -- Configure a function `_on_message(message)` that will receive raw real-time data from DTInsight. -- Export the project as a `.pck` + Script folder picker + Script folder selection -#### Use +2. Components with associated scripts will display a button with the script filename. Click to view. -If the OML description is properly configured (by having the `DTDFVocab:HasVisualization true` property), simply pick the exported `.pck` file and you will be able to open the visualization on DTInsight. + Script viewer -Screenshot 2025-07-11 133809 +## Continuous Report Generation (CI/CD) -### Access Digital Twin script +DTInsight integrates into a CI/CD pipeline (currently GitHub Actions) to automatically produce an up-to-date reporting page on every commit. The pipeline: -If you have your Digital Twin's scripts on your pc and an ontology providing a relative path to those files, you will be able to access those script files from the visualizer. +1. Loads the committed DTDF ontology into a Fuseki server. +2. Runs DTInsight (headless, via Linux `xvfb`) and triggers the internal TCP CI/CD server (port `9090`). +3. Outputs: + - **HTML characteristics table** — summary of the 21 DTDF characteristics + - **Architecture screenshot** — high-resolution tiled capture of the DT constellation + - **YAML architecture dump** — machine-readable description of the constellation +4. Deploys the reporting page as a static website (via [Hugo](https://gohugo.io/)). -To allow for better compatibility, the ontology should only contain relative paths to your files. Those path must be relative to a software folder that you will be able to select from the visualizer. +An example reporting page for the incubator DT is available at: [oakeslabmtl.github.io/DTDF](https://oakeslabmtl.github.io/DTDF/) -image +### Steps to set up continuous reporting for your DT -image +1. **Model your DT** in the DTDF using OML/OWL/RDF. +2. **Configure the static website** deployment (e.g., Hugo + GitHub Pages). +3. **Set up the CI/CD workflow** to load the ontology, run DTInsight, and deploy. -Once loaded if a script is associated with a Digital Twin component, a new button will be displayed in this component with the name of the script and by clicking it you will have access to the corresponding script. +## Related Resources -image +- **DTDF Ontology & Example DT:** [oakeslabmtl/DTDF](https://github.com/oakeslabmtl/DTDF) +- **openCAESAR Rosetta:** [opencaesar/oml-rosetta](https://github.com/opencaesar/oml-rosetta) +- **Example DT (Incubator):** [INTO-CPS-Association/example_digital-twin_incubator](https://github.com/INTO-CPS-Association/example_digital-twin_incubator) ## Citation diff --git a/script_runner_panel.gd b/script_runner_panel.gd index c1457b5..5b89587 100644 --- a/script_runner_panel.gd +++ b/script_runner_panel.gd @@ -1,48 +1,51 @@ extends Control -# On récupère les références vers nos nœuds pour pouvoir les modifier +# Get references to nodes for modification @onready var path_line_edit = $VBoxContainer/InputContainer/PathLineEdit @onready var file_dialog = $FileDialog -# Appelé quand on clique sur "Choisir fichier" +# Called when the user clicks "Pick File" func _on_pick_button_pressed(): - # Ouvre la fenêtre de sélection de fichier + # Opens the file selection popup file_dialog.popup_centered() -# Appelé quand l'utilisateur a choisi un fichier dans le FileDialog +# Called when the user has chosen a file in the FileDialog func _on_file_dialog_file_selected(path): - # On affiche le chemin dans le champ texte + # Display the path in the text field path_line_edit.text = path -# Appelé quand on clique sur "Run" (anciennement Apply) +# Called when the user clicks "Run" func _on_run_button_pressed(): var script_path = path_line_edit.text if script_path == "": - printerr("Erreur : Aucun fichier sélectionné.") + printerr("Error: No file selected.") return if not FileAccess.file_exists(script_path): - printerr("Erreur : Fichier Python introuvable à : ", script_path) + printerr("Error: Python file not found at: ", script_path) return - # Ton code d'exécution Python + # Python execution code setup var command = "python" + if not OS.has_feature("windows"): + command = "python3" + var args = [script_path] var output = [] - print("Lancement du script : ", script_path) + print("Launching script: ", script_path) - # Exécution (bloquante avec true, attention si le script est long) + # Execution (blocking with true; consider deferring if the script is long) var exit_code = OS.execute(command, args, output, true) if exit_code == 0: - print("Script exécuté avec succès : ", output) - # Optionnel : Fermer le popup après succès + print("Script executed successfully: ", output) + # Hide the popup on success _on_back_button_pressed() else: - printerr("Erreur Python (Code %s) : " % exit_code, output) + printerr("Python Error (Code %s): " % exit_code, output) -# Appelé quand on clique sur "Back" +# Called when the user clicks "Back" func _on_back_button_pressed(): - # On cache simplement ce panneau + # Simply hide this panel get_parent().visible = false