diff --git a/src/foldhaus_app.cpp b/src/foldhaus_app.cpp index 407082c..7300131 100644 --- a/src/foldhaus_app.cpp +++ b/src/foldhaus_app.cpp @@ -1,16 +1,6 @@ #include "foldhaus_platform.h" #include "foldhaus_app.h" -internal void -SetPanelDefinitionExternal(panel* Panel, s32 OldPanelDefinitionIndex, s32 NewPanelDefinitionIndex) -{ - if(OldPanelDefinitionIndex >= 0) - { - GlobalPanelDefs[OldPanelDefinitionIndex].Cleanup(Panel); - } - GlobalPanelDefs[NewPanelDefinitionIndex].Init(Panel); -} - internal v4 MouseToWorldRay(r32 MouseX, r32 MouseY, camera* Camera, rect WindowBounds) { @@ -203,7 +193,7 @@ INITIALIZE_APPLICATION(InitializeApplication) InitializePanelSystem(&State->PanelSystem); panel* Panel = TakeNewPanel(&State->PanelSystem); - SetPanelDefinition(Panel, 0); + SetPanelDefinition(Panel, 0, State); } internal void @@ -223,6 +213,7 @@ HandleInput (app_state* State, rect WindowBounds, input_queue InputQueue, mouse_ { panel_and_bounds PanelWithMouseOverIt = GetPanelContainingPoint(Mouse.Pos, &State->PanelSystem, WindowBounds); if (!PanelWithMouseOverIt.Panel) { return; } + State->HotPanel = PanelWithMouseOverIt.Panel; panel_definition PanelDefinition = GlobalPanelDefs[PanelWithMouseOverIt.Panel->PanelDefinitionIndex]; if (!PanelDefinition.InputCommands) { return; } diff --git a/src/foldhaus_app.h b/src/foldhaus_app.h index 3c60263..c535528 100644 --- a/src/foldhaus_app.h +++ b/src/foldhaus_app.h @@ -10,7 +10,9 @@ #include "foldhaus_assembly.h" #include "assembly_parser.h" + #include "foldhaus_node.h" + #include "assembly_parser.cpp" #include "test_patterns.h" @@ -67,6 +69,7 @@ struct app_state animation_block_handle SelectedAnimationBlockHandle; panel_system PanelSystem; + panel* HotPanel; }; internal void OpenColorPicker(app_state* State, v4* Address); @@ -198,10 +201,19 @@ TestPatternThree(assembly* Assembly, r32 Time) #include "foldhaus_text_entry.cpp" #include "foldhaus_search_lister.cpp" -#define PANEL_INIT_PROC(name) void name(panel* Panel) +#include "foldhaus_default_nodes.h" +#include "generated/foldhaus_nodes_generated.cpp" +#include "foldhaus_node.cpp" + +FOLDHAUS_INPUT_COMMAND_PROC(EndCurrentOperationMode) +{ + DeactivateCurrentOperationMode(&State->Modes); +} + +#define PANEL_INIT_PROC(name) void name(panel* Panel, app_state* State) typedef PANEL_INIT_PROC(panel_init_proc); -#define PANEL_CLEANUP_PROC(name) void name(panel* Panel) +#define PANEL_CLEANUP_PROC(name) void name(panel* Panel, app_state* State) typedef PANEL_CLEANUP_PROC(panel_cleanup_proc); #define PANEL_RENDER_PROC(name) void name(panel Panel, rect PanelBounds, render_command_buffer* RenderBuffer, app_state* State, context Context, mouse_state Mouse) @@ -212,6 +224,7 @@ typedef PANEL_RENDER_PROC(panel_render_proc); #include "panels/foldhaus_panel_dmx_view.h" #include "panels/foldhaus_panel_animation_timeline.h" #include "panels/foldhaus_panel_hierarchy.h" +#include "panels/foldhaus_panel_node_graph.h" #include "generated/foldhaus_panels_generated.h" diff --git a/src/foldhaus_interface.cpp b/src/foldhaus_interface.cpp index 1454635..f94e3a9 100644 --- a/src/foldhaus_interface.cpp +++ b/src/foldhaus_interface.cpp @@ -141,6 +141,19 @@ enum panel_edit_mode PanelEdit_Count, }; +internal void +SetPanelDefinition(panel* Panel, s32 NewPanelDefinitionIndex, app_state* State) +{ + s32 OldPanelDefinitionIndex = Panel->PanelDefinitionIndex; + Panel->PanelDefinitionIndex = NewPanelDefinitionIndex; + + if(OldPanelDefinitionIndex >= 0) + { + GlobalPanelDefs[OldPanelDefinitionIndex].Cleanup(Panel, State); + } + GlobalPanelDefs[NewPanelDefinitionIndex].Init(Panel, State); +} + // // Drag Panel Border Operation Mode @@ -359,6 +372,12 @@ FOLDHAUS_INPUT_COMMAND_PROC(EndSplitPanelOperation) SplitPanelHorizontally(Panel, YPercent, PanelBounds, &State->PanelSystem); } + Panel->Left->Panel.PanelDefinitionIndex = Panel->PanelDefinitionIndex; + Panel->Left->Panel.PanelStateMemory = Panel->PanelStateMemory; + Panel->Left->Panel.PanelStateMemorySize = Panel->PanelStateMemorySize; + + SetPanelDefinition(&Panel->Right->Panel, Panel->PanelDefinitionIndex, State); + DeactivateCurrentOperationMode(&State->Modes); } @@ -497,7 +516,7 @@ DrawPanelBorder(panel Panel, v2 PanelMin, v2 PanelMax, v4 Color, mouse_state Mou } internal void -DrawPanelFooter(panel* Panel, render_command_buffer* RenderBuffer, rect FooterBounds, interface_config Interface, mouse_state Mouse) +DrawPanelFooter(panel* Panel, render_command_buffer* RenderBuffer, rect FooterBounds, mouse_state Mouse, app_state* State) { PushRenderQuad2D(RenderBuffer, FooterBounds.Min, v2{FooterBounds.Max.x, FooterBounds.Min.y + 25}, v4{.5f, .5f, .5f, 1.f}); PushRenderQuad2D(RenderBuffer, FooterBounds.Min, FooterBounds.Min + v2{25, 25}, WhiteV4); @@ -525,10 +544,10 @@ DrawPanelFooter(panel* Panel, render_command_buffer* RenderBuffer, rect FooterBo string DefName = MakeString(Def.PanelName, Def.PanelNameLength); button_result DefinitionButton = EvaluateButton(RenderBuffer, ButtonMin, ButtonMin + ButtonDimension, - DefName, Interface, Mouse); + DefName, State->Interface, Mouse); if (DefinitionButton.Pressed) { - SetPanelDefinition(Panel, i); + SetPanelDefinition(Panel, i, State); Panel->PanelSelectionMenuOpen = false; } @@ -539,7 +558,7 @@ DrawPanelFooter(panel* Panel, render_command_buffer* RenderBuffer, rect FooterBo button_result ButtonResult = EvaluateButton(RenderBuffer, PanelSelectButtonMin, PanelSelectButtonMax, - MakeStringLiteral("Select"), Interface, Mouse); + MakeStringLiteral("Select"), State->Interface, Mouse); if (ButtonResult.Pressed) { Panel->PanelSelectionMenuOpen = !Panel->PanelSelectionMenuOpen; @@ -565,7 +584,7 @@ RenderPanel(panel* Panel, rect PanelBounds, rect WindowBounds, render_command_bu Definition.Render(*Panel, PanelViewBounds, RenderBuffer, State, Context, Mouse); PushRenderOrthographic(RenderBuffer, WindowBounds.Min.x, WindowBounds.Min.y, WindowBounds.Max.x, WindowBounds.Max.y); - DrawPanelFooter(Panel, RenderBuffer, FooterBounds, State->Interface, Mouse); + DrawPanelFooter(Panel, RenderBuffer, FooterBounds, Mouse, State); } internal void diff --git a/src/foldhaus_node.cpp b/src/foldhaus_node.cpp index f7658d5..f611654 100644 --- a/src/foldhaus_node.cpp +++ b/src/foldhaus_node.cpp @@ -189,7 +189,7 @@ PushNodeOnListFromSpecification (node_list* List, node_type Type, v2 Min, memory Node->PersistentData = PushArray(Storage, u8, Spec.DataStructSize); node_struct_member* MemberList = Spec.MemberList; - for (s32 MemberIdx = 0; MemberIdx < Spec.MemberListLength; MemberIdx++) + for (u32 MemberIdx = 0; MemberIdx < Spec.MemberListLength; MemberIdx++) { node_struct_member Member = MemberList[MemberIdx]; InitializeNodeConnection(Node->Connections + MemberIdx, Member, Node); diff --git a/src/foldhaus_node.h b/src/foldhaus_node.h index 309277d..7792d40 100644 --- a/src/foldhaus_node.h +++ b/src/foldhaus_node.h @@ -169,9 +169,9 @@ struct node_specification s32 NameLength; node_struct_member* MemberList; - s32 DataStructSize; + u32 DataStructSize; - s32 MemberListLength; + u32 MemberListLength; b32 IsPattern; }; diff --git a/src/foldhaus_panel.h b/src/foldhaus_panel.h index 06fb431..ca49346 100644 --- a/src/foldhaus_panel.h +++ b/src/foldhaus_panel.h @@ -34,6 +34,9 @@ struct panel // Probably belongs in a more generalized PanelInterfaceState or something b32 PanelSelectionMenuOpen; + u8* PanelStateMemory; + u32 PanelStateMemorySize; + union{ panel_entry* Left; panel_entry* Top; @@ -80,8 +83,6 @@ struct panel_layout u32 PanelsMax; }; -internal void SetPanelDefinitionExternal(panel* Panel, s32 OldPanelDefinitionIndex, s32 NewPanelDefinitionIndex); - ///////////////////////////////// // // Book-Keeping @@ -201,14 +202,6 @@ ConsolidatePanelsKeepOne(panel* Parent, panel_entry* PanelEntryToKeep, panel_sys FreePanelEntryRecursive(PanelEntryToDestroy, PanelSystem); } -internal void -SetPanelDefinition(panel* Panel, s32 NewDefinitionIndex) -{ - s32 OldDefinitionIndex = Panel->PanelDefinitionIndex; - Panel->PanelDefinitionIndex = NewDefinitionIndex; - SetPanelDefinitionExternal(Panel, OldDefinitionIndex, NewDefinitionIndex); -} - ///////////////////////////////// // // Rendering And Interaction diff --git a/src/generated/foldhaus_panels_generated.h b/src/generated/foldhaus_panels_generated.h index 75cd072..18cf897 100644 --- a/src/generated/foldhaus_panels_generated.h +++ b/src/generated/foldhaus_panels_generated.h @@ -8,11 +8,12 @@ struct panel_definition input_command* InputCommands; }; -global_variable s32 GlobalPanelDefsCount = 5; +global_variable s32 GlobalPanelDefsCount = 6; global_variable panel_definition GlobalPanelDefs[] = { { "Sculpture View", 14, SculptureView_Init, SculptureView_Cleanup, SculptureView_Render, SculptureView_Commands}, { "Animation Timeline", 18, AnimationTimeline_Init, AnimationTimeline_Cleanup, AnimationTimeline_Render, AnimationTimeline_Commands }, { "DMX View", 8, DMXView_Init, DMXView_Cleanup, DMXView_Render, 0 }, { "Profiler", 8, ProfilerView_Init, ProfilerView_Cleanup, ProfilerView_Render, 0 }, { "Hierarchy", 9, HierarchyView_Init, HierarchyView_Cleanup, HierarchyView_Render, 0 }, + { "Node Graph", 10, NodeGraph_Init, NodeGraph_Cleanup, NodeGraph_Render, NodeGraph_Commands }, }; \ No newline at end of file diff --git a/src/panels/foldhaus_panel_node_graph.h b/src/panels/foldhaus_panel_node_graph.h new file mode 100644 index 0000000..102e74b --- /dev/null +++ b/src/panels/foldhaus_panel_node_graph.h @@ -0,0 +1,309 @@ +struct node_graph_state +{ + v2 ViewOffset; +}; + + +// +// Pan Node Graph +// + +OPERATION_STATE_DEF(pan_node_graph_operation_state) +{ + v2 InitialViewOffset; + + // TODO(Peter): I DON"T LIKE THIS!!!! + // We should have a way to access the panel that created an operation mode or something + v2* ViewOffset; +}; + +OPERATION_RENDER_PROC(UpdatePanNodeGraph) +{ + pan_node_graph_operation_state* OpState = (pan_node_graph_operation_state*)Operation.OpStateMemory; + v2 MouseDelta = Mouse.Pos - Mouse.DownPos; + *OpState->ViewOffset = MouseDelta + OpState->InitialViewOffset; +} + +input_command PanNodeGraphCommands[] = { + { KeyCode_MouseLeftButton, KeyCode_Invalid, Command_Ended, EndCurrentOperationMode }, +}; + +FOLDHAUS_INPUT_COMMAND_PROC(BeginPanNodeGraph) +{ + operation_mode* PanNodeGraph = ActivateOperationModeWithCommands(&State->Modes, PanNodeGraphCommands, UpdatePanNodeGraph); + pan_node_graph_operation_state* OpState = CreateOperationState(PanNodeGraph, &State->Modes, pan_node_graph_operation_state); + + panel* NodeGraphPanel = State->HotPanel; + node_graph_state* NodeGraphState = (node_graph_state*)NodeGraphPanel->PanelStateMemory; + OpState->InitialViewOffset = NodeGraphState->ViewOffset; + OpState->ViewOffset = &NodeGraphState->ViewOffset; +} + +// +// Node Graph Panel +// + +input_command NodeGraph_Commands[] = { + { KeyCode_MouseLeftButton, KeyCode_Invalid, Command_Began, BeginPanNodeGraph } +}; + +PANEL_INIT_PROC(NodeGraph_Init) +{ + // TODO(Peter): We aren't able to free this memory. We need a system for + // taking fixed size chunks off the Memory stack and then reusing them. THis + // should probably live outside the paneling system. + Panel->PanelStateMemory = (u8*)PushStruct(&State->Permanent, node_graph_state); +} + +PANEL_CLEANUP_PROC(NodeGraph_Cleanup) +{ + +} + +internal void +DrawGrid (v2 Offset, v2 GridSquareDim, rect PanelBounds, render_command_buffer* RenderBuffer) +{ + r32 LineValue = .16f; + v4 LineColor = v4{LineValue, LineValue, LineValue, 1.f}; + + v2 GridSquareOffset = v2{ + GSModF(Offset.x, GridSquareDim.x), + GSModF(Offset.y, GridSquareDim.y), + }; + v2 GridOrigin = PanelBounds.Min + GridSquareOffset; + + // Draw Vertical Lines + r32 XOffset = 0; + while (GridOrigin.x + XOffset < PanelBounds.Max.x) + { + v2 LineMin = v2{ GridOrigin.x + XOffset, PanelBounds.Min.y }; + v2 LineMax = v2{ LineMin.x + 1, PanelBounds.Max.y }; + PushRenderQuad2D(RenderBuffer, LineMin, LineMax, LineColor); + + XOffset += GridSquareDim.x; + } + + // Draw Horizontal Lines + r32 YOffset = 0; + while (GridOrigin.y + YOffset < PanelBounds.Max.y) + { + v2 LineMin = v2{ PanelBounds.Min.x, GridOrigin.y + YOffset }; + v2 LineMax = v2{ PanelBounds.Max.x, LineMin.y + 1, }; + PushRenderQuad2D(RenderBuffer, LineMin, LineMax, LineColor); + + YOffset += GridSquareDim.y; + } + +} + +internal void +DrawNodePorts(node_specification Spec, b32 InputMask, v2 Position, r32 LineHeight, string_alignment TextAlign, v2 TextOffset, interface_config Interface, render_command_buffer* RenderBuffer) +{ + v2 LinePosition = Position; + for (u32 i = 0; i < Spec.MemberListLength; i++) + { + node_struct_member Member = Spec.MemberList[i]; + if ((Member.IsInput & InputMask) > 0) + { + string MemberName = MakeString(Member.Name, CharArrayLength(Member.Name)); + DrawString(RenderBuffer, MemberName, Interface.Font, LinePosition + TextOffset, WhiteV4, TextAlign); + + LinePosition.y -= LineHeight; + } + } + +} + +internal void +DrawNode (v2 Position, node_specification NodeSpecification, r32 NodeWidth, r32 LineHeight, interface_config Interface, render_command_buffer* RenderBuffer) +{ + u32 InputMembers = 0; + u32 OutputMembers = 0; + for (u32 i = 0; i < NodeSpecification.MemberListLength; i++) + { + node_struct_member Member = NodeSpecification.MemberList[i]; + if ((Member.IsInput & IsOutputMember) > 0) { OutputMembers++; } + if ((Member.IsInput & IsInputMember) > 0) { InputMembers++; } + } + u32 LineCount = 1 + GSMax(InputMembers, OutputMembers); + + v2 NodeDim = v2{ + NodeWidth, + (LineHeight * LineCount) + Interface.Margin.y, + }; + rect NodeBounds = rect{ + v2{ Position.x, Position.y - NodeDim.y }, + v2{ Position.x + NodeDim.x, Position.y }, + }; + + PushRenderQuad2D(RenderBuffer, NodeBounds.Min, NodeBounds.Max, v4{.16f, .16f, .16f, 1.f}); + + v2 LinePosition = v2{ NodeBounds.Min.x, NodeBounds.Max.y - LineHeight }; + v2 TextOffset = v2{Interface.Margin.x, 0}; + + PushRenderQuad2D(RenderBuffer, LinePosition, LinePosition + v2{NodeWidth, LineHeight}, v4{1.f, .24f, .39f, 1.f}); + string NodeName = MakeString(NodeSpecification.Name, NodeSpecification.NameLength); + DrawString(RenderBuffer, NodeName, Interface.Font, LinePosition + TextOffset, WhiteV4); + LinePosition.y -= LineHeight; + + // Draw Ports + DrawNodePorts(NodeSpecification, IsInputMember, LinePosition, LineHeight, Align_Left, TextOffset, Interface, RenderBuffer); + + v2 OutputLinePosition = v2{LinePosition.x + NodeDim.x, LinePosition.y }; + v2 OutputTextOffset = v2{-TextOffset.x, TextOffset.y}; + DrawNodePorts(NodeSpecification, IsOutputMember, OutputLinePosition, LineHeight, Align_Right, OutputTextOffset, Interface, RenderBuffer); +} + +struct temp_node_connection +{ + u32 DownstreamNodeIndex; + u32 DownstreamNodePort; + + u32 UpstreamNodeIndex; + u32 UpstreamNodePort; +}; + +struct visual_node +{ + node_specification Spec; + v2 Position; +}; + +#define TEMP_NODE_LIST_MAX 10 +global_variable u32 TEMP_NodeListUsed = 0; +global_variable node_specification TEMP_NodeList[TEMP_NODE_LIST_MAX]; + +#define TEMP_CONNECTIONS_LIST_MAX 10 +global_variable u32 TEMP_NodeConnectionsUsed = 0; +global_variable temp_node_connection TEMP_NodeConnections[TEMP_CONNECTIONS_LIST_MAX]; + +internal void +PushNodeOnNodeList(node_specification Spec) +{ + if (TEMP_NodeListUsed < TEMP_NODE_LIST_MAX) + { + u32 Index = TEMP_NodeListUsed++; + TEMP_NodeList[Index] = Spec; + } +} + +internal visual_node* +ArrangeNodes(node_specification* NodeList, u32 NodesCount, temp_node_connection* ConnectionList, u32 ConnectionsCount, r32 NodeWidth, r32 LayerDistance, memory_arena* Storage) +{ + // Figure out how to arrange nodes + u32 LayerCount = 1; + + u32* NodeLayers = PushArray(Storage, u32, NodesCount); + GSZeroMemory((u8*)NodeLayers, sizeof(u32) * NodesCount); + + for (u32 c = 0; c < ConnectionsCount; c++) + { + temp_node_connection Connection = TEMP_NodeConnections[c]; + + u32 UpstreamNodeInitialLayer = NodeLayers[Connection.UpstreamNodeIndex]; + u32 DownstreamNodeLayer = NodeLayers[Connection.DownstreamNodeIndex]; + + NodeLayers[Connection.UpstreamNodeIndex] = GSMax(UpstreamNodeInitialLayer, DownstreamNodeLayer + 1); + LayerCount = GSMax(NodeLayers[Connection.UpstreamNodeIndex] + 1, LayerCount); + } + + // Place Layer Columns + v2* LayerPositions = PushArray(Storage, v2, LayerCount); + for (u32 l = 0; l < LayerCount; l++) + { + u32 FromRight = LayerCount - l; + LayerPositions[l] = v2{ (NodeWidth + LayerDistance) * FromRight, 0 }; + } + + // Place nodes + visual_node* VisualNodes = PushArray(Storage, visual_node, NodesCount); + for (u32 n = 0; n < NodesCount; n++) + { + VisualNodes[n].Spec = TEMP_NodeList[n]; + + u32 NodeLayer = NodeLayers[n]; + VisualNodes[n].Position = LayerPositions[NodeLayer]; + LayerPositions[NodeLayer].y -= 200; + } + + return VisualNodes; +} + +internal +PANEL_RENDER_PROC(NodeGraph_Render) +{ + node_graph_state* GraphState = (node_graph_state*)Panel.PanelStateMemory; + + rect NodeSelectionWindowBounds = rect{ + PanelBounds.Min, + v2{PanelBounds.Min.x + 300, PanelBounds.Max.y}, + }; + rect GraphBounds = rect{ + v2{NodeSelectionWindowBounds.Max.x, PanelBounds.Min.y}, + PanelBounds.Max, + }; + + r32 NodeWidth = 150; + r32 LayerDistance = 100; + r32 LineHeight = (State->Interface.Font->PixelHeight + (2 * State->Interface.Margin.y)); + + visual_node* VisualNodes = ArrangeNodes(&TEMP_NodeList[0], TEMP_NodeListUsed, + &TEMP_NodeConnections[0], TEMP_NodeConnectionsUsed, + NodeWidth, LayerDistance, + &State->Transient); + + DrawGrid(GraphState->ViewOffset, v2{100, 100}, GraphBounds, RenderBuffer); + + render_quad_batch_constructor ConnectionsLayer = PushRenderQuad2DBatch(RenderBuffer, TEMP_NodeConnectionsUsed); + for (u32 i = 0; i < TEMP_NodeConnectionsUsed; i++) + { + temp_node_connection Connection = TEMP_NodeConnections[i]; + visual_node UpstreamNode = VisualNodes[Connection.UpstreamNodeIndex]; + visual_node DownstreamNode = VisualNodes[Connection.DownstreamNodeIndex]; + + v2 LineStart = GraphState->ViewOffset + UpstreamNode.Position + v2{NodeWidth, 0} - (v2{0, LineHeight} * (Connection.UpstreamNodePort + 2)) + v2{0, LineHeight / 3}; + v2 LineEnd = GraphState->ViewOffset + DownstreamNode.Position - (v2{0, LineHeight} * (Connection.DownstreamNodePort + 2)) + v2{0, LineHeight / 3}; + + PushLine2DOnBatch(&ConnectionsLayer, LineStart, LineEnd, 1.5f, WhiteV4); + } + + for (u32 i = 0; i < TEMP_NodeListUsed; i++) + { + visual_node VisualNode = VisualNodes[i]; + DrawNode(VisualNode.Position + GraphState->ViewOffset, VisualNode.Spec, NodeWidth, LineHeight, State->Interface, RenderBuffer); + } + + // Node Selection Panel + v4 LineBGColors[] = { + { .16f, .16f, .16f, 1.f }, + { .18f, .18f, .18f, 1.f }, + }; + + interface_list List = {}; + List.LineBGColors = LineBGColors; + List.LineBGColorsCount = sizeof(LineBGColors) / sizeof(LineBGColors[0]); + List.LineBGHoverColor = v4{ .22f, .22f, .22f, 1.f }; + List.TextColor = WhiteV4; + List.ListBounds = NodeSelectionWindowBounds; + List.ListElementDimensions = v2{ + Width(NodeSelectionWindowBounds), + (r32)(State->Interface.Font->PixelHeight + 8), + }; + List.ElementLabelIndent = v2{10, 4}; + + string TitleString = MakeStringLiteral("Available Nodes"); + DrawListElement(TitleString, &List, Mouse, RenderBuffer, State->Interface); + + for (s32 i = 0; i < NodeSpecificationsCount; i++) + { + node_specification Spec = NodeSpecifications[i]; + string NodeName = MakeString(Spec.Name, Spec.NameLength); + rect ElementBounds = DrawListElement(NodeName, &List, Mouse, RenderBuffer, State->Interface); + + if (MouseButtonTransitionedDown(Mouse.LeftButtonState) + && PointIsInRect(Mouse.DownPos, ElementBounds)) + { + PushNodeOnNodeList(Spec); + } + } +} \ No newline at end of file