From f5537bdfeb7a4716b56d42860cb09119f5862698 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Mon, 23 Oct 2023 12:42:15 +0200 Subject: [PATCH] New :angel: scripts: GridViewer and (alpha) RigEditor --- resources/scripts/example_RigEditor_alpha.as | 550 +++++++++++++++++++ resources/scripts/gridviewer_utils.as | 81 +++ 2 files changed, 631 insertions(+) create mode 100644 resources/scripts/example_RigEditor_alpha.as create mode 100644 resources/scripts/gridviewer_utils.as diff --git a/resources/scripts/example_RigEditor_alpha.as b/resources/scripts/example_RigEditor_alpha.as new file mode 100644 index 0000000000..55b14758a8 --- /dev/null +++ b/resources/scripts/example_RigEditor_alpha.as @@ -0,0 +1,550 @@ +/// \file Prototype truck editor, Oct 2023 +/// \brief Showcases truck file editing features +/// see https://github.com/RigsOfRods/rigs-of-rods/pull/3048 +/// Written and auto-indented using script_editor.as! +// =================================================== + +#include "gridviewer_utils.as" + +class RigEditor +{ + // ----- config ----- + + int m_statusbar_height_pixels = 25; + + // ---- variables ----- + + GenericDocumentClass@ m_displayed_document = null; + int m_hovered_token_pos = -1; + int m_focused_token_pos = -1; + string m_error_str; + string m_project_creation_pending; // name of the truck file which will become available in modcache. + CacheEntryClass@ m_project_entry; + GridViewer viewer_x; + GridViewer viewer_y; + GridViewer viewer_z; + dictionary@ m_modcache_results = null; + CacheEntryClass@ m_awaiting_load_bundle_entry = null; + color node_color = color(0.8, 0.9, 0.2, 1.0); + float node_radius = 1.f; + + // ---- functions ---- + + //#region Draw surrounding window + + void drawWindow() + { + + // Begin window + + string caption = "RigEditor"; + if (@m_project_entry != null) + { + caption += " ("+m_project_entry.fname+")"; + } + int flags = ImGuiWindowFlags_MenuBar; + ImGui::Begin(caption, /*open:*/true, flags); + + // Draw menu bar + + this.drawMenubar(); + + // Split off top-pane (all editing windows) from bottom statusbar + vector2 fullsize_with_statusbar = ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + vector2 fullsize = fullsize_with_statusbar - vector2(0, m_statusbar_height_pixels); + if (ImGui::BeginChild("mainPane", fullsize)) + { + + // Draw the document pane (left side) + + + vector2 leftpane_size = fullsize * vector2(0.3, 1.f); + + if (ImGui::BeginChild("leftPane", leftpane_size)) + { + + if (ImGui::BeginChild("docBody", leftpane_size * vector2(1.f, 0.8))) + { + if (@m_displayed_document != null) + { + this.drawDocumentBody(); + } + } + ImGui::EndChild(); // "docBody" + + if ( m_focused_token_pos > -1) + { + ImGui::Separator(); + this.drawTokenEditPanel(); + } + } + ImGui::EndChild(); // "leftPane" + ImGui::SameLine(); + // Draw the right side + vector2 rightpane_size = fullsize - vector2(leftpane_size.x, 0); + if (ImGui::BeginChild("rightPane", rightpane_size)) + { + + vector2 views_size = fullsize * vector2(0.7, 1.f); + vector2 qsize = views_size*vector2(0.5, 0.5); + + viewer_x.begin(qsize); + drawNodes(viewer_x); + viewer_x.end(); + + ImGui::SameLine(); + + viewer_y.begin(qsize); + drawNodes(viewer_y); + viewer_y.end(); + + viewer_z.begin(qsize); + drawNodes(viewer_z); + viewer_z.end(); + } + ImGui::EndChild(); // "rightPane" + } + ImGui::EndChild(); // "mainPane" + + // draw statusbar + + this.drawStatusbar(); + + // End window + + ImGui::End(); + + } + + void drawMenubar() + { + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Open project")) + { + if (@m_modcache_results == null || ImGui::Button("Refresh")) + { + @m_modcache_results = modcache.query({ + {'filter_type', LOADER_TYPE_ALLBEAM}, + {'filter_category_id', 8990} // CID_Projects + }); + } + + if (@m_modcache_results != null) + { + array result_entries = cast>(m_modcache_results['entries']); + for (uint i = 0; i < result_entries.length(); i++) + { + ImGui::PushID(i); + + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::Text(result_entries[i].dname); + ImGui::SameLine(); + ImGui::TextDisabled("("+result_entries[i].fname+")"); + ImGui::SameLine(); + if (ImGui::SmallButton("Load")) + { + game.pushMessage(MSG_EDI_LOAD_BUNDLE_REQUESTED, { {'cache_entry', result_entries[i]} }); + // we will receive `MODCACHEACTIVITY_BUNDLE_LOADED` - see `eventCallbackEx()` at the end of file. + @m_awaiting_load_bundle_entry = result_entries[i]; + game.registerForEvent(SE_GENERIC_MODCACHE_ACTIVITY); + } + + ImGui::PopID(); // i + } + } + else + { + ImGui::Text("ModCache query FAILED!"); + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Manage projects")) + { + ImGui::TextDisabled("Create a project:"); + + BeamClass@ actor = game.getCurrentTruck(); + if (@actor != null) + { + // Actor name and "View document" button + ImGui::PushID("actor"); + ImGui::AlignTextToFramePadding(); + ImGui::Text("You are driving " + actor.getTruckName()); + ImGui::SameLine(); + if (@m_displayed_document == null) + { + if (ImGui::SmallButton("Import as project")) + { + this.createProjectFromActorAsync(actor); + } + } + + ImGui::PopID(); //"actor" + } + else + { + ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); + } + + ImGui::EndMenu(); + } + + + ImGui::EndMenuBar(); + } + } + + void drawStatusbar() + { + if (m_error_str != "") + { + ImGui::Text("ERROR! " + m_error_str); + if (ImGui::SmallButton("Clear")) + { + m_error_str = ""; + } + return; + } + else if (@m_displayed_document != null) + { + ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); + ImGui::SameLine(); + if (ImGui::SmallButton("Save file")) + { + m_displayed_document.saveToResource(m_project_entry.fname, m_project_entry.resource_group); + } + } + else if (m_project_creation_pending != "") + { + ImGui::Text ("Waiting for project to be created..."); + } + else if (@m_awaiting_load_bundle_entry != null) + { + ImGui::Text ("Loading bundle '"+m_awaiting_load_bundle_entry.dname+"'..."); + } + else + { + ImGui::Text("Ready to load or import project"); + } + } + //#endregion + + // #region Project management + + void createProjectFromActorAsync(BeamClass@ actor ) + { + // Fetch the current actor cache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); + if (@src_entry == null) + m_error_str = "Failed to load cache entry!"; + + // request project to be created from that cache entry + string proj_name = "project1"; + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', proj_name}, + {'source_entry', src_entry} + }); + + // Now we have to wait until next frame for the project to be created + m_project_creation_pending = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. + } + + void loadAndFixupDocument() + { + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (!doc.loadFromResource(m_project_entry.fname, m_project_entry.resource_group, flags)) + { + m_error_str = "Project file failed to load!"; + return; + } + @m_displayed_document = doc; + + // fixup the document.. + + GenericDocContextClass@ ctx = GenericDocContextClass(m_displayed_document); + // >> seek the name + while (!ctx.endOfFile() && !ctx.isTokString()) { + ctx.seekNextLine(); + } + // >> change the name + if (!ctx.endOfFile()) { + ctx.setTokString(0, m_project_entry.dname); + } + // >> seek fileinfo + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo')) { + ctx.seekNextLine(); + } + // change the fileinfo param #2 categoryid + if (!ctx.endOfFile(2)) { + ctx.setTokFloat(2, 8990); // special "Project" category + } + } + + // #endregion + + // #region Tokenized document drawing + + void drawDocumentBody() + { + ImGui::PushID("docBody"); + bool hover_found = false; + + GenericDocContextClass reader(m_displayed_document); + while (!reader.endOfFile()) + { + + switch (reader.tokenType()) + { + // These tokens are always at start of line + case TOKEN_TYPE_KEYWORD: { + ImGui::TextColored(tokenColor(reader), reader.getTokKeyword()); + break; + } + case TOKEN_TYPE_COMMENT: { + ImGui::TextColored(tokenColor(reader), ";" + reader.getTokComment()); + break; + } + + // Linebreak is implicit in DearIMGUI, no action needed + case TOKEN_TYPE_LINEBREAK: { + if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) + { + ImGui::SameLine(); + } + ImGui::TextColored(tokenColor(reader), "
"); + // ImGui::SameLine(); ImGui::Text(""); // hack to fix highlight of last token on line. + break; + } + + // Other tokens come anywhere - delimiting logic is needed + default: { + if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) + { + ImGui::SameLine(); + // string delimiter = (reader.tokenType(-1) == TOKEN_TYPE_KEYWORD) ? " " : ", "; + // ImGui::Text(delimiter); + // ImGui::SameLine(); + } + } + + switch (reader.tokenType()) + { + case TOKEN_TYPE_STRING: { + ImGui::TextColored(tokenColor(reader), "\"" + reader.getTokString() + "\""); + break; + } + case TOKEN_TYPE_NUMBER: { + ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); + break; + } + case TOKEN_TYPE_BOOL: { + ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); + break; + } + } + } + + if (ImGui::IsItemHovered()) { + m_hovered_token_pos = reader.getPos() ; + hover_found = true; + } + + if (ImGui::IsItemClicked(0)) + { + m_focused_token_pos = reader.getPos(); + } + + reader.moveNext(); + + } + + if (!hover_found) + { + m_hovered_token_pos = -1; + } + + ImGui::PopID(); // "docBody" + } + + void drawTokenEditPanel() + { + GenericDocContextClass reader(m_displayed_document); + while (!reader.endOfFile() && (reader.getPos() != uint(m_focused_token_pos))) + { + reader.moveNext(); + } + + if (reader.endOfFile()) + { + ImGui::Text("EOF!!"); + } + else + { + ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); + ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); + } + } + + // #endregion + + // #region GridViewer node drawing + + void drawNodes(GridViewer @viewer) + { + // This must run in between `GridViewer.begin()` and `GridViewer.end()` ! + // ---------------------------------------------------------------------- + + if (@m_displayed_document == null) + { + return; + } + + // Do it the slow way, reading directly from the GenericDocumentClass without any sort of caching. + GenericDocContextClass ctx(m_displayed_document); + + // Seek to 'nodes' (assume there's just 1 'nodes' segment in the file - usual case) + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != "nodes")) + { + ctx.seekNextLine(); + } + ctx.seekNextLine(); + while (!ctx.endOfFile()) + { + //int node_num = int(ctx.getTokFloat(0)); + vector3 node_pos; + if (ctx.isTokFloat(1) && ctx.isTokFloat(2) && ctx.isTokFloat(3)) + { + node_pos.x = ctx.getTokFloat(1); + node_pos.y = ctx.getTokFloat(2); + node_pos.z = ctx.getTokFloat(3); + ImGui::GetWindowDrawList().AddCircleFilled( viewer.projectPos(node_pos), node_radius, node_color); + } + + ctx.seekNextLine(); + + + if (ctx.isTokKeyword() && ctx.getTokKeyword() != "set_node_defaults") + { + break; + } + } + } + + // #endregion + + // #region Token drawing helpers + string tokenTypeStr(TokenType t) + { + switch (t) + { + case TOKEN_TYPE_NUMBER: return "Number"; + case TOKEN_TYPE_STRING: return "String"; + case TOKEN_TYPE_BOOL: return "Boolean"; + case TOKEN_TYPE_COMMENT: return "Comment"; + case TOKEN_TYPE_LINEBREAK: return "Line break"; + case TOKEN_TYPE_KEYWORD: return "Keyword"; + } + return "?"; + } + + color tokenColor(GenericDocContextClass@ reader) + { + if (m_focused_token_pos > -1 && reader.getPos() == uint(m_focused_token_pos)) + { + return color(0.9f, 0.1f, 0.1f, 1.f); + } + + if (m_hovered_token_pos > -1 && reader.getPos() == uint(m_hovered_token_pos)) + { + return color(0.1f, 1.f, 0.1f, 1.f); + } + + switch (reader.tokenType()) + { + case TOKEN_TYPE_KEYWORD: return color(1.f, 1.f, 0.f, 1.f); + case TOKEN_TYPE_COMMENT: return color(0.5f, 0.5f, 0.5f, 1.f); + case TOKEN_TYPE_STRING: return color(0.f, 1.f, 1.f, 1.f); + case TOKEN_TYPE_NUMBER: return color(0.9, 0.9, 0.9, 1.f); + case TOKEN_TYPE_BOOL: return color(1.f, 0.f, 1.f, 1.f); + case TOKEN_TYPE_LINEBREAK: return color(0.66f, 0.55f, 0.33f, 1.f); + } + return color(0.9, 0.9, 0.9, 1.f); + } + // #endregion + + // #region Event handling or polling + void checkForCreatedProject() + { + // there's no notification event for completed request, we must poll like this. + if (m_project_creation_pending != "") + { + @m_project_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, m_project_creation_pending); + if (@m_project_entry != null) + { + // success!! stop polling and open the document. + m_project_creation_pending=""; + this.loadAndFixupDocument(); + } + } + } + + void onEventBundleLoaded(int cache_number) + { + //game.log("DBG onEventBundleLoaded(): number "+cache_number); + if (cache_number == m_awaiting_load_bundle_entry.number) + { + @m_project_entry = @m_awaiting_load_bundle_entry; + @m_awaiting_load_bundle_entry= null; + + game.unRegisterEvent(SE_GENERIC_MODCACHE_ACTIVITY); + this.loadAndFixupDocument(); + } + } + // #endregion + + void update(float dt) + { + this.checkForCreatedProject(); + this.drawWindow(); + } +} + +RigEditor editor; + +// STARTUP +void main() +{ + editor.viewer_x.childWindowID = "demoX"; + editor.viewer_x.hAxis=1; + editor.viewer_x.vAxis=2; // axes: YZ + + editor.viewer_y.childWindowID = "demoY"; + editor.viewer_y.hAxis=0; + editor.viewer_y.vAxis=2; // axes: XZ + + editor.viewer_z.childWindowID = "demoZ"; // axes XY (default) + +} + +// RENDERING +void frameStep(float dt) +{ + editor.update(dt); +} + +// NOTIFICATIONS +void eventCallbackEx(scriptEvents ev, int a1, int a2, int a3, int a4, string a5, string a6, string a7, string a8) +{ + if (ev == SE_GENERIC_MODCACHE_ACTIVITY && a1 == MODCACHEACTIVITY_BUNDLE_LOADED) + { + editor.onEventBundleLoaded(a2); + } +} diff --git a/resources/scripts/gridviewer_utils.as b/resources/scripts/gridviewer_utils.as new file mode 100644 index 0000000000..8d34a94e51 --- /dev/null +++ b/resources/scripts/gridviewer_utils.as @@ -0,0 +1,81 @@ +/// \title 2D GRID VIEWER +/// \brief Generic scrolling & zooming UI for drawing via ImDrawList +// =================================================== + +class GridViewer +{ + // CFG: + string childWindowID; + float zoomMax = 100.f; + float zoomStep = 1.f; + int hAxis=0; + int vAxis=1; + array axisLineColors = { color(1,0,0,1), color(0.2,0.3,1,1), color(0.1, 0.9, 0.05, 1) }; + vector2 scrollRegionPadding = vector2(50,50); + vector2 controlsBoxPadding = vector2(4,4); + int gridSizeMax = 10000; + + // STATE: + float zoom = 1.f; + vector2 contentRegionSize; + vector2 midWindowScreenPos; + vector2 childScreenPos; + vector2 nodesMin; + vector2 nodesMax; + int gridSize = 25; + int gridSizeStep = 5; + + // FUNCS: + void begin(vector2 size) { + int flags = 1 << 11; // _HorizontalScrollbar + this.childScreenPos=ImGui::GetCursorScreenPos(); + ImGui::BeginChild(childWindowID, size, true, flags); + contentRegionSize=ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + midWindowScreenPos = ImGui::GetWindowPos()+ contentRegionSize/2; + nodesMin=vector2(FLT_MAX, FLT_MAX); + nodesMax=vector2(-999999, -999999); + } + + void drawControls() { + ImGui::TextDisabled(this.childWindowID); + ImGui::SameLine(); + ImGui::SetNextItemWidth(65); + ImGui::SliderFloat('Zoom', zoom, zoomStep, zoomMax); + ImGui::SameLine(); + ImGui::SetNextItemWidth(65); + ImGui::SliderInt("Grid", gridSize, gridSizeStep, gridSizeMax); + } + + vector2 projectPos(vector3 posIn) { + // zoom only, without scrolling + vector2 pos= midWindowScreenPos+vector2(posIn[hAxis], posIn[vAxis])*zoom; + nodesMin=vector2(fmin(nodesMin.x, pos.x), fmin(nodesMin.y, pos.y)); + nodesMax=vector2(fmax(nodesMax.x, pos.x), fmax(nodesMax.y, pos.y)); + // add scrolling + pos += vector2( + ImGui::GetScrollMaxX() - 2*ImGui::GetScrollX(), + ImGui::GetScrollMaxY() - 2*ImGui::GetScrollY()); + return pos; + } + + void drawAxisLines() { + vector2 origin = this.projectPos(vector3(0,0,0)); + ImGui::GetWindowDrawList().AddLine(origin+vector2(0, -9999), origin+vector2(0,9999), axisLineColors[vAxis]); + ImGui::GetWindowDrawList().AddLine(origin+vector2(-9999, 0), origin+vector2(9999, 0), axisLineColors[hAxis]); + } + + void end() { + drawAxisLines(); + // draw dummies - force scroll region + ImGui::SetCursorPos((nodesMin-scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); + ImGui::SetCursorPos((nodesMax+scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); + + // controls go on the top of the child, scrolling is cancelled out. + ImGui::SetCursorPos(vector2(ImGui::GetScrollX(), ImGui::GetScrollY()) + controlsBoxPadding); + drawControls(); + ImGui::EndChild(); + } + + float fmax(float a, float b) { return a>b?a:b; } + float fmin(float a, float b) { return a