Repository: CorsixTH/CorsixTH
Pull request #2850: Extend animview
Author: login: Alberth289346
Labels: name: C:AnimView/SpriteViewer
description: Component
name: PR:Feature
description: PR adds a new or missing feature to the game.
name: PR:DevTools/Repo
description: PRs that deal with developer operations.
Created: 2025-05-05T14:27:14Z | Merged: 2025-05-07T21:52:21Z

Adds the bottom row:

![Screenshot from 2025-05-05 16-14-05](https://github.com/user-attachments/assets/2743b393-629d-4d4a-9a1c-2a983b63e9fd)

- You can enter a mood position, and press "This frame" to set the position.
Both pixel and tile positions work, except the latter may be less accurate.
- You can click the arrow buttons to move the mood 1 pixel in the indicated position.

Each time you set a mood, a text line is printed to `stdout` of the form `frame, {x, y, "px"},` and a comment what animation it is from.
```
2, {5, 20, "px"}, -- Anim 0
3, {15, 20, "px"}, -- Anim 0
```


Diff:
diff --git a/AnimView/frmMain.cpp b/AnimView/frmMain.cpp
index e900941de7..e06b02fb64 100644
--- a/AnimView/frmMain.cpp
+++ b/AnimView/frmMain.cpp
@@ -65,6 +65,11 @@ BEGIN_EVENT_TABLE(frmMain, wxFrame)
   EVT_TIMER(ID_TIMER_ANIMATE, frmMain::_onTimer)
   EVT_CHECKBOX(ID_DRAW_MOOD, frmMain::_onToggleDrawMood)
   EVT_CHECKBOX(ID_DRAW_COORDINATES, frmMain::_onToggleDrawCoordinates)
+  EVT_BUTTON(ID_MOOD_FRAME, frmMain::_onMoodFrameApply)
+  EVT_BUTTON(ID_MOOD_LEFT, frmMain::_onMoodLeft)
+  EVT_BUTTON(ID_MOOD_RIGHT, frmMain::_onMoodRight)
+  EVT_BUTTON(ID_MOOD_UP, frmMain::_onMoodUp)
+  EVT_BUTTON(ID_MOOD_DOWN, frmMain::_onMoodDown)
 END_EVENT_TABLE()
 
 namespace {
@@ -291,6 +296,32 @@ frmMain::frmMain()
       m_txtMoodPosition[1] = new wxTextCtrl(this, wxID_ANY, L"{0, 0, \"px\"}"),
       wxSizerFlags(1).Expand());
   pMoodOverlay->Add(pMoodRow, wxSizerFlags(1).Expand().Border(wxALL, 2));
+
+  wxSizerFlags moodAdjustFlags(0);
+  moodAdjustFlags.Align(wxALIGN_CENTER).Border(wxALL, 1);
+
+  layerSizerFlags.Align(wxALIGN_CENTER).Border(wxALL, 1);
+  wxBoxSizer* pMoodAdjustRow = new wxBoxSizer(wxHORIZONTAL);
+  m_txtMoodFramePos =
+      new wxTextCtrl(this, ID_MOOD_POS, L"{0.0, 0.0}", wxDefaultPosition,
+                     wxDefaultSize, wxTE_CENTRE);
+  pMoodAdjustRow->Add(m_txtMoodFramePos, wxSizerFlags(1).Expand());
+  pMoodAdjustRow->Add(new wxButton(this, ID_MOOD_FRAME, L"Set marker"),
+                      moodAdjustFlags);
+  pMoodAdjustRow->Add(new wxButton(this, ID_MOOD_LEFT, L"<", wxDefaultPosition,
+                                   wxDefaultSize, wxBU_EXACTFIT),
+                      moodAdjustFlags);
+  pMoodAdjustRow->Add(new wxButton(this, ID_MOOD_RIGHT, L">", wxDefaultPosition,
+                                   wxDefaultSize, wxBU_EXACTFIT),
+                      moodAdjustFlags);
+  pMoodAdjustRow->Add(new wxButton(this, ID_MOOD_UP, L"^", wxDefaultPosition,
+                                   wxDefaultSize, wxBU_EXACTFIT),
+                      moodAdjustFlags);
+  pMoodAdjustRow->Add(new wxButton(this, ID_MOOD_DOWN, L"v", wxDefaultPosition,
+                                   wxDefaultSize, wxBU_EXACTFIT),
+                      moodAdjustFlags);
+  pMoodOverlay->Add(pMoodAdjustRow, wxSizerFlags(1).Expand().Border(wxALL, 2));
+
   pMoodOverlay->Add(
       new wxCheckBox(this, ID_DRAW_COORDINATES, L"Draw tile coordinates"),
       fillSizerFlags);
@@ -680,8 +711,12 @@ void frmMain::_onPanelPaint(wxPaintEvent& evt) {
 }
 
 void frmMain::_onPanelClick(wxMouseEvent& evt) {
-  m_iMoodDrawX = evt.GetX() - 143;
-  m_iMoodDrawY = evt.GetY() - 203;
+  updateMoodPosition(evt.GetX() - 143, evt.GetY() - 203);
+}
+
+void frmMain::updateMoodPosition(int centerX, int centerY) {
+  m_iMoodDrawX = centerX;
+  m_iMoodDrawY = centerY;
   {
     double fX = (double)m_iMoodDrawX;
     double fY = (double)m_iMoodDrawY;
@@ -694,9 +729,161 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+/**
+   Match the 'text' against the pattern in 'p'. The pattern should have 2
+   numbers.
+   @param p: Pattern to match. ' ' means optional white space, 'I' means a
+   possibly negative integer, 'R' means a possibly negative real or integer, all
+   other character match with themselves.
+   @param text: Text to completely match.
+   @param num1: The text of the first number if successful.
+   @param num2: The text of the second number if successful.
+   @return: Whether the match was successful.
+*/
+static bool matchText(const char* p, const std::string& text, std::string& num1,
+                      std::string& num2) {
+  bool filled_num1 = false;
+  bool filled_num2 = false;
+
+  const char* const c_start = text.c_str();  // Pointer to the first input.
+  const char* c = c_start;  // Pointer to the current input character.
+  while (*p) {
+    // Dispatch on the current pattern character.
+    switch (*p) {
+      case ' ':  // Optional spaces.
+        while (*c == ' ') c++;
+        break;
+
+      case 'R':    // Possibly negative real number without exponent.
+      case 'I': {  // Possibly negative integer number.
+        bool seen_digit = false;
+        int num_start = c - c_start;
+
+        if (*c == '-') c++;
+        while (*c >= '0' && *c <= '9') {
+          c++;
+          seen_digit = true;
+        }
+        if (*c == '.') {
+          if (*p != 'R') return false;
+          c++;
+        }
+        while (*c >= '0' && *c <= '9') {
+          c++;
+          seen_digit = true;
+        }
+        if (!seen_digit) return false;
+
+        // Store the number text for value conversion by the caller.
+        int num_length = (c - c_start) - num_start;
+        if (!filled_num1) {
+          num1 = text.substr(num_start, num_length);
+          filled_num1 = true;
+        } else if (!filled_num2) {
+          num2 = text.substr(num_start, num_length);
+          filled_num2 = true;
+        }
+        break;
+      }
+
+      default:  // Input should match the pattern character exactly.
+        if (*p != *c) return false;
+        c++;
+        break;
+    }
+    p++;
+  }
+
+  return *p == '\0' && *c == '\0' && filled_num2;
+}
+
+/**
+  Match text of the form '{real, real}' with 'real' a possibly negative
+  real number without exponent.
+  @param text Input text to match.
+  @param x Value of the first real in the text.
+  @param y Value of the second real in the text.
+  @return Whether the input text was successfully matched.
+ */
+static bool matchTilePos(const std::string& text, double& x, double& y) {
+  std::string num1, num2;
+  if (matchText(" { R , R } ", text, num1, num2)) {
+    try {
+      x = std::stod(num1.c_str());
+      y = std::stod(num2.c_str());
+      return true;
+    } catch (...) {
+      // Drop into fail return value.
+    }
+  }
+  return false;
+}
+
+/**
+  Match text of the form '{int, int, "px"}' with 'int' a possibly negative
+  integer number.
+  @param text Input text to match.
+  @param x Value of the first integer in the text.
+  @param y Value of the second integer in the text.
+  @return Whether the input text was successfully matched.
+ */
+static bool matchPixelPos(const std::string& text, int& x, int& y) {
+  std::string num1, num2;
+  if (matchText(" { I , I , \"px\" } ", text, num1, num2)) {
+    try {
+      x = std::stoi(num1.c_str());
+      y = std::stoi(num2.c_str());
+      return true;
+    } catch (...) {
+      // Drop into fail return value.
+    }
+  }
+  return false;
+}
+
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  // Try to recognize a tile position.
+  double x_tile, y_tile;
+  if (matchTilePos(text, x_tile, y_tile)) {
+    // Convert the fractions to a pixel position.
+    x_tile -= y_tile;
+    x_tile /= 2.0;
+    y_tile += x_tile;
+    updateMoodPosition(round(x_tile * 64.0), round(y_tile * 32.0));
+  }
+
+  // Try to recognize a pixel position.
+  int x_pixel, y_pixel;
+  if (matchPixelPos(text, x_pixel, y_pixel)) {
+    updateMoodPosition(x_pixel, y_pixel);
+  }
+}
+
+void frmMain::_onMoodLeft(wxCommandEvent& evt) {
+  updateMoodPosition(m_iMoodDrawX - 1, m_iMoodDrawY);
+}
+
+void frmMain::_onMoodRight(wxCommandEvent& evt) {
+  updateMoodPosition(m_iMoodDrawX + 1, m_iMoodDrawY);
+}
+
+void frmMain::_onMoodUp(wxCommandEvent& evt) {
+  updateMoodPosition(m_iMoodDrawX, m_iMoodDrawY - 1);
+}
+
+void frmMain::_onMoodDown(wxCommandEvent& evt) {
+  updateMoodPosition(m_iMoodDrawX, m_iMoodDrawY + 1);
+}
+
 void frmMain::_onSearchLayerId(wxCommandEvent& evt) {
   long input = ::wxGetNumberFromUser(
       L"Enter the layer number to search in (0 - 12)", L"Layer:",
diff --git a/AnimView/frmMain.h b/AnimView/frmMain.h
index 013f7913fc..09a3e548f3 100644
--- a/AnimView/frmMain.h
+++ b/AnimView/frmMain.h
@@ -67,6 +67,12 @@ class frmMain : public wxFrame {
     ID_EXPORT,
     ID_DRAW_MOOD,
     ID_DRAW_COORDINATES,
+    ID_MOOD_POS,
+    ID_MOOD_FRAME,
+    ID_MOOD_LEFT,
+    ID_MOOD_RIGHT,
+    ID_MOOD_UP,
+    ID_MOOD_DOWN,
     ID_LAYER_CHECKS,  // Must be last ID
   };
 
@@ -101,6 +107,11 @@ class frmMain : public wxFrame {
   void _onPanelPaint(wxPaintEvent& evt);
   void _onPanelClick(wxMouseEvent& evt);
   void _onTimer(wxTimerEvent& evt);
+  void _onMoodFrameApply(wxCommandEvent& evt);
+  void _onMoodLeft(wxCommandEvent& evt);
+  void _onMoodRight(wxCommandEvent& evt);
+  void _onMoodUp(wxCommandEvent& evt);
+  void _onMoodDown(wxCommandEvent& evt);
 
   void _onAnimChange(size_t iIndex);
 
@@ -131,10 +142,19 @@ class frmMain : public wxFrame {
   wxTextCtrl* m_txtFrameCount;
   wxTextCtrl* m_txtFrameFlags[2];
   wxTextCtrl* m_txtMoodPosition[2];
+  wxTextCtrl* m_txtMoodFramePos;
   wxCheckBox* m_chkFrameFlags[16];
   wxListBox* m_lstSearchResults;
   wxPanel* m_panFrame;
   DECLARE_EVENT_TABLE()
+
+ private:
+  /**
+      Update the mood position of the current frame to the given coordinate.
+      @param centerX X coordinate of the center.
+      @param centerY Y coordinate of the center.
+   */
+  void updateMoodPosition(int centerX, int centerY);
 };
 
 #endif


Reviews:
user: login: TheCycoONE
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T16:25:09Z
user: login: Alberth289346
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T18:58:58Z
user: login: Alberth289346
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T18:59:58Z
user: login: TheCycoONE
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T00:32:59Z
user: login: Alberth289346
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T15:50:22Z
user: login: Alberth289346
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T15:52:35Z

Review comments:
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: TheCycoONE
body: `(-?\d+(?:\.\d+)?)` maybe?

For a group containing a potentially negative number with an optional decimal followed by one or more digits?
created_at: 2025-05-05T16:25:09Z
author_association: MEMBER
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: Alberth289346
body: What syntax is that?  I have been looking at the c++ standard, and they have https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04 

At least `(?:` isn't there.

Maybe it's simpler to write a character scanner. There is nothing complicated in there.
created_at: 2025-05-05T18:58:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: Alberth289346
body: https://en.cppreference.com/w/cpp/regex  is the regex cpp library
created_at: 2025-05-05T18:59:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: TheCycoONE
body: The default c++ regex is ECMAScript - https://en.cppreference.com/w/cpp/regex/ecmascript check the section on sub-expressions. 

Specifically it's a subexpression that doesn't get captured as a match group.
created_at: 2025-05-06T00:32:59Z
author_association: MEMBER
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: Alberth289346
body: Ah ok. I know `(?:` from Python, but didn't find it in the Re documentation that I read. Should have picked a different one :)

created_at: 2025-05-06T15:50:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -694,9 +731,68 @@ void frmMain::_onPanelClick(wxMouseEvent& evt) {
   }
   m_txtMoodPosition[1]->SetValue(
       wxString::Format(L"{%i, %i, \"px\"}", m_iMoodDrawX, m_iMoodDrawY));
+  printf("%ld, {%i, %i, \"px\"}, -- Anim %ld\n", m_iCurrentFrame, m_iMoodDrawX,
+         m_iMoodDrawY, m_iCurrentAnim);
+
   if (m_bDrawMood) m_panFrame->Refresh(false);
 }
 
+void frmMain::_onMoodFrameApply(wxCommandEvent& evt) {
+  wxString wxText = m_txtMoodFramePos->GetLineText(0);
+  std::string text = wxText.ToStdString();
+
+  std::regex tilePosRegex(
+      "\\{ *([-0-9]+[.][0-9]*) *, *([-0-9]+[.][0-9]*) *\\}");
+  std::regex pixelPosRegex("\\{ *([-0-9]+) *, *([-0-9]+) *, *[\"]px[\"] *\\}");
path: AnimView/frmMain.cpp
user: login: Alberth289346
body: In the mean time, I switched to c character matching which does the job as well.
created_at: 2025-05-06T15:52:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: TheCycoONE
created_at: 2025-05-05T16:28:51Z
body: Looks ok, what happens with whole number tiles or is that an unexpected use case?
author_association: MEMBER
user: login: ARGAMX
created_at: 2025-05-05T17:28:15Z
body: This window will be even bigger at height now? 
It already can't fit in height on 1280x800 resolution. 
Although this is not a big problem. 
But it seems to be slowly growing in height.

![Screenshot 2025-05-05 at 20 23 21](https://github.com/user-attachments/assets/82931fb4-0f29-4e15-89cb-4c43d2e0804e)


author_association: MEMBER
user: login: TheCycoONE
created_at: 2025-05-05T18:12:10Z
body: > This window will be even bigger at height now? It already can't fit in height on 1280x800 resolution. Although this is not a big problem. But it seems to be slowly growing in height.
> 

As a dev tool I'm not too concerned, as long as it works for the dev that needs it. In this case I assume @Alberth289346 added the extra functionality for himself.

Another ticket that splits this into tabs or collapsible sections or even adds a scrollbar would be welcome.

author_association: MEMBER
user: login: Alberth289346
created_at: 2025-05-05T18:40:27Z
body: > Looks ok, what happens with whole number tiles or is that an unexpected use case?

You get entire tiles offset. It does happen with larger machines.
author_association: CONTRIBUTOR
user: login: Alberth289346
created_at: 2025-05-05T19:15:18Z
body: > This window will be even bigger at height now?

No, the left side where I added the row is still shorter than the right side.
author_association: CONTRIBUTOR
user: login: TheCycoONE
created_at: 2025-05-07T21:39:28Z
body: I'm ok with merging.
author_association: MEMBER