Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gui-js/libs/shared/src/lib/backend/minsky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,7 @@ export class Minsky extends CppClass {
async importDuplicateColumn(a1: GodleyTable,a2: number): Promise<void> {return this.$callMethod('importDuplicateColumn',a1,a2);}
async importVensim(a1: string): Promise<void> {return this.$callMethod('importVensim',a1);}
async imposeDimensions(): Promise<void> {return this.$callMethod('imposeDimensions');}
async inPopulateMissingDimensions(...args: boolean[]): Promise<boolean> {return this.$callMethod('inPopulateMissingDimensions',...args);}
async initGodleys(): Promise<void> {return this.$callMethod('initGodleys');}
async inputWired(a1: string): Promise<boolean> {return this.$callMethod('inputWired',a1);}
async insertGroupFromFile(a1: string): Promise<void> {return this.$callMethod('insertGroupFromFile',a1);}
Expand All @@ -1366,6 +1367,7 @@ export class Minsky extends CppClass {
async listFonts(): Promise<string[]> {return this.$callMethod('listFonts');}
async load(a1: string): Promise<void> {return this.$callMethod('load',a1);}
async loggingEnabled(): Promise<boolean> {return this.$callMethod('loggingEnabled');}
async m_publicationMode(...args: boolean[]): Promise<boolean> {return this.$callMethod('m_publicationMode',...args);}
async makeVariablesConsistent(): Promise<void> {return this.$callMethod('makeVariablesConsistent');}
async markEdited(): Promise<void> {return this.$callMethod('markEdited');}
async matchingTableColumns(a1: GodleyIcon,a2: string): Promise<string[]> {return this.$callMethod('matchingTableColumns',a1,a2);}
Expand All @@ -1389,6 +1391,7 @@ export class Minsky extends CppClass {
async populateMissingDimensions(): Promise<void> {return this.$callMethod('populateMissingDimensions');}
async populateMissingDimensionsFromVariable(...args: any[]): Promise<void> {return this.$callMethod('populateMissingDimensionsFromVariable',...args);}
async progress(a1: string,a2: number): Promise<void> {return this.$callMethod('progress',a1,a2);}
async publicationMode(...args: any[]): Promise<boolean> {return this.$callMethod('publicationMode',...args);}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated binding types publicationMode(...args: any[]): Promise<boolean>, but in C++ the setter currently returns void (void publicationMode(bool)). Either change the C++ setter to return bool (common pattern for get/set-style APIs in this file) or update this TS signature to return Promise<void> when setting to avoid misleading API consumers.

Suggested change
async publicationMode(...args: any[]): Promise<boolean> {return this.$callMethod('publicationMode',...args);}
async publicationMode(): Promise<boolean>;
async publicationMode(a1: boolean): Promise<void>;
async publicationMode(...args: any[]): Promise<boolean | void> {return this.$callMethod('publicationMode',...args);}

Copilot uses AI. Check for mistakes.
async pushFlags(): Promise<void> {return this.$callMethod('pushFlags');}
async pushHistory(): Promise<boolean> {return this.$callMethod('pushHistory');}
async randomLayout(): Promise<void> {return this.$callMethod('randomLayout');}
Expand Down
8 changes: 7 additions & 1 deletion model/minsky.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ namespace minsky

enum ItemType {wire, op, var, group, godley, plot};

class Minsky: public Exclude<MinskyExclude>, public RungeKutta
class Minsky: public classdesc::Exclude<MinskyExclude>, public RungeKutta
{
CLASSDESC_ACCESS(Minsky);

Expand Down Expand Up @@ -202,6 +202,9 @@ namespace minsky
std::map<Units, double> maxValue;
std::map<Units, double> maxFlowValue; // max flow values along wires
/// fills in dimensions table with all loaded ravel axes

// used to avoid excessive recursion in populateMissingDimensionsFromVariable
bool inPopulateMissingDimensions=false;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inPopulateMissingDimensions is a transient recursion-guard flag but is currently a normal bool member of Minsky, so it will be serialized with the model. This should be excluded from serialization (eg via classdesc::Exclude<bool> or by moving it into MinskyExclude) to avoid persisting an internal in-progress state into save files.

Suggested change
bool inPopulateMissingDimensions=false;
classdesc::Exclude<bool> inPopulateMissingDimensions{false};

Copilot uses AI. Check for mistakes.
void populateMissingDimensions();
Comment on lines +206 to 208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The new recursion guard is never actually applied.

model/minsky.cc:527-558 still runs populateMissingDimensionsFromVariable() without checking or toggling inPopulateMissingDimensions, so the message() re-entry path can still recurse. Right now this adds dead state, not protection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/minsky.h` around lines 206 - 208, The new bool
inPopulateMissingDimensions is declared but never used, so wrap the population
logic with it: at the start of populateMissingDimensions (or at the start of
populateMissingDimensionsFromVariable if that is the entry point used in
message()), return immediately if inPopulateMissingDimensions is true; otherwise
set inPopulateMissingDimensions = true before calling
populateMissingDimensionsFromVariable/message-driven population paths and set it
back to false in a finally/ensure manner after the work completes (including on
exceptions) so re-entry via message() is prevented.

/// populate missing dimensions from a variableValue
/// @param incompatibleMessageDisplayed boolean flag to make message display single shot
Expand All @@ -210,6 +213,9 @@ namespace minsky
{bool dummy; populateMissingDimensionsFromVariable(v,dummy);}
void renameDimension(const std::string& oldName, const std::string& newName);

bool m_publicationMode=false;
bool publicationMode() const {return m_publicationMode;}
void publicationMode(bool x) {m_publicationMode=x;}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setters/Getters meaningless here

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_publicationMode also looks like transient UI/rendering state and should likely be excluded from serialization (same rationale as other fields in MinskyExclude). Also, the setter void publicationMode(bool) is inconsistent with the generated JS/TS binding (which treats it as returning a boolean); consider returning bool from the setter (or adjusting the binding to return void).

Suggested change
void publicationMode(bool x) {m_publicationMode=x;}
bool publicationMode(bool x) {return m_publicationMode=x;}

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +218
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep publication mode out of the serialised/public model surface.

m_publicationMode is transient render state, but here it becomes a normal Minsky field. That means it can leak into packed model/history state, and it auto-generates a raw m_publicationMode() backend API in gui-js/libs/shared/src/lib/backend/minsky.ts, which bypasses the intended accessor boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/minsky.h` around lines 216 - 218, The field m_publicationMode is
transient UI/render state and must be kept out of the serialised model; move its
storage out of the serialisable Minsky fields and make the accessors
publicationMode() and publicationMode(bool) use that non-serialised storage.
Concretely: remove the bool m_publicationMode member from the serialised
section, add the flag to a non-serialised place (e.g. a RenderState struct, a
private non-serialised member region, or a static/thread_local private field),
update publicationMode() and publicationMode(bool) to read/write that new
storage, and ensure any codegen or serialization excludes the new location so
gui-js/libs/shared backend generation no longer emits m_publicationMode.

void setGodleyIconResource(const string& s)
{GodleyIcon::svgRenderer.setResource(s);}
void setGroupIconResource(const string& s)
Expand Down
115 changes: 80 additions & 35 deletions model/plotWidget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
#include "minsky.h"
#include "plotWidget.h"
#include "canvas.h"
#include "variable.h"
#include "cairoItems.h"
#include "latexMarkup.h"
Expand Down Expand Up @@ -109,77 +110,112 @@ namespace minsky
yoffs=0;
if (!title.empty())
{
const CairoSave cs(cairo);
const double fy=titleHeight*iHeight();

Pango pango(cairo);
pango.setFontSize(fabs(fy));
pango.setFontSize(fabs(titleHeight*iHeight()));
pango.setMarkup(latexToPango(title));
cairo_set_source_rgb(cairo,0,0,0);
cairo_move_to(cairo,0.5*(w-pango.width()), 0);
cairo_move_to(cairo,0.5*(w-pango.width())-pango.left(), -pango.top());
pango.show();

// allow some room for the title
yoffs=pango.height();
h-=yoffs;
}

// draw bounding box ports

size_t i=0;
// draw bounds input ports
for (; i<nBoundsPorts; ++i)
{
const float x=boundX[i]*w, y=boundY[i]*h;
// boundY is relative to total height h, from center.
const float x=boundX[i]*w;
const float y=boundY[i]*h;
if (!justDataChanged)
m_ports[i]->moveTo(x*z + this->x(), y*z + this->y()+0.5*yoffs);
drawTriangle(cairo, x+0.5*w, y+0.5*h+yoffs, palette[(i/2)%palette.size()].colour, orient[i]);

{
if (i < m_ports.size() && m_ports[i])
m_ports[i]->moveTo(x*z + this->x(), y*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
{
if (!palette.empty())
drawTriangle(cairo, x+0.5*w, y+0.5*h, palette[(i/2)%palette.size()].colour, orient[i]);
Comment on lines +135 to +140
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounds-port placement no longer accounts for yoffs when a title is drawn. Since the plot content is translated down by yoffs, the top-edge bounds ports/triangles can overlap the title area and no longer align with the plotted region. Consider incorporating yoffs consistently in both moveTo() and drawTriangle() for bounds ports when title is non-empty.

Suggested change
m_ports[i]->moveTo(x*z + this->x(), y*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
{
if (!palette.empty())
drawTriangle(cairo, x+0.5*w, y+0.5*h, palette[(i/2)%palette.size()].colour, orient[i]);
m_ports[i]->moveTo(x*z + this->x(), (y + yoffs)*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
{
if (!palette.empty())
drawTriangle(cairo, x+0.5*w, y + yoffs + 0.5*h, palette[(i/2)%palette.size()].colour, orient[i]);

Copilot uses AI. Check for mistakes.
}
}

const float xLeft = -0.5*w, dx=w/(2*m_numLines+1); // x location of ports
const float dy = h/m_numLines;
// draw y data ports
const float plot_area_w = w - 2*portSpace;
const float plot_area_h = h - yoffs - portSpace;
if (m_numLines == 0) return;
const float dx = plot_area_w / (2*m_numLines + 1);
const float dy = plot_area_h / m_numLines;
Comment on lines +146 to +148
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (m_numLines == 0) return; exits PlotWidget::draw() early, skipping the post-plot overlay work (ports/tooltips/resize handles/selection highlight) and leaving mutable state like justDataChanged unreset. Prefer handling the zero-lines case by skipping the dx/dy port-layout loops (or guarding the divisions) while still running the remainder of draw().

Suggested change
if (m_numLines == 0) return;
const float dx = plot_area_w / (2*m_numLines + 1);
const float dy = plot_area_h / m_numLines;
const float dx = m_numLines>0 ? plot_area_w / (2*m_numLines + 1) : 0;
const float dy = m_numLines>0 ? plot_area_h / m_numLines : 0;

Copilot uses AI. Check for mistakes.

// draw y data ports (left side)
for (; i<m_numLines+nBoundsPorts; ++i)
{
const float y=0.5*(dy-h) + (i-nBoundsPorts)*dy;
size_t k = i - nBoundsPorts;
const float y = yoffs + (k + 0.5) * dy;
const float x = portSpace;
if (!justDataChanged)
m_ports[i]->moveTo(xLeft*z + this->x(), y*z + this->y()+0.5*yoffs);
drawTriangle(cairo, xLeft+0.5*w, y+0.5*h+yoffs, palette[(i-nBoundsPorts)%palette.size()].colour, 0);
{
if (i < m_ports.size() && m_ports[i])
m_ports[i]->moveTo((x-0.5*w)*z + this->x(), (y-0.5*h)*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
drawTriangle(cairo, x - portSpace, y, palette[k%palette.size()].colour, 0);
}
Comment on lines +161 to 163
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

palette[k%palette.size()] will crash if palette is empty (modulo by 0). Since the bounds-port path now explicitly guards palette.empty(), the same guard should be applied here (and in the RHS/bottom loops) or a safe fallback colour should be used when the palette is empty.

Copilot uses AI. Check for mistakes.

// draw RHS y data ports
for (; i<2*m_numLines+nBoundsPorts; ++i)
{
const float y=0.5*(dy-h) + (i-m_numLines-nBoundsPorts)*dy, x=0.5*w;
size_t k = i - m_numLines - nBoundsPorts;
const float y = yoffs + (k + 0.5) * dy;
const float x = w - portSpace;
if (!justDataChanged)
m_ports[i]->moveTo(x*z + this->x(), y*z + this->y()+0.5*yoffs);
drawTriangle(cairo, x+0.5*w, y+0.5*h+yoffs, palette[(i-nBoundsPorts)%palette.size()].colour, M_PI);
{
if (i < m_ports.size() && m_ports[i])
m_ports[i]->moveTo((x-0.5*w)*z + this->x(), (y-0.5*h)*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
drawTriangle(cairo, x + portSpace, y, palette[k%palette.size()].colour, M_PI);
}

// draw x data ports
const float yBottom=0.5*h;
for (; i<4*m_numLines+nBoundsPorts; ++i)

// draw x data ports (bottom side)
for (; i<m_ports.size(); ++i)
{
const float x=dx-0.5*w + (i-2*m_numLines-nBoundsPorts)*dx;
size_t k = i - 2*m_numLines - nBoundsPorts;
const float x = portSpace + (k + 1) * dx;
const float y = h - portSpace;
if (!justDataChanged)
m_ports[i]->moveTo(x*z + this->x(), yBottom*z + this->y()+0.5*yoffs);
drawTriangle(cairo, x+0.5*w, yBottom+0.5*h+yoffs, palette[(i-2*m_numLines-nBoundsPorts)%palette.size()].colour, -0.5*M_PI);
{
if (i < m_ports.size() && m_ports[i])
m_ports[i]->moveTo((x-0.5*w)*z + this->x(), (y-0.5*h)*z + this->y());
}
if (mouseFocus && !cminsky().publicationMode())
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to gate drawing ports on !publicationMode

drawTriangle(cairo, x, y + portSpace, palette[(k/2)%palette.size()].colour, -0.5*M_PI);
}
Comment on lines +150 to 193
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard the palette lookups in the new triangle paths.

The left/right/bottom port branches index palette[...] unconditionally. PlotWidget starts with an empty palette, so a focused plot with no line styles yet will hit % palette.size() and blow up. The bounds-port loop already has the right guard.

Proposed fix
-        if (mouseFocus && !cminsky().publicationMode())
+        if (mouseFocus && !cminsky().publicationMode() && !palette.empty())
           drawTriangle(cairo, x - portSpace, y, palette[k%palette.size()].colour, 0);
@@
-        if (mouseFocus && !cminsky().publicationMode())
+        if (mouseFocus && !cminsky().publicationMode() && !palette.empty())
           drawTriangle(cairo, x + portSpace, y, palette[k%palette.size()].colour, M_PI);
@@
-        if (mouseFocus && !cminsky().publicationMode())
+        if (mouseFocus && !cminsky().publicationMode() && !palette.empty())
           drawTriangle(cairo, x, y + portSpace, palette[(k/2)%palette.size()].colour, -0.5*M_PI);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/plotWidget.cc` around lines 150 - 193, The palette lookups in the
left/right/bottom port loops can divide by zero when palette is empty; update
the branches that call drawTriangle (the three loops that compute
palette[k%palette.size()].colour) to first check palette.empty() or
palette.size()>0 and use a safe fallback (e.g. a default colour or skip drawing)
when empty, so replace direct palette[...] usages in the drawTriangle calls
inside the LHS loop (k%palette.size()), RHS loop (k%palette.size()), and bottom
loop ((k/2)%palette.size()) with a guarded lookup that selects palette[index]
only when palette non-empty.


// if any titling, draw an extra bounding box (ticket #285)
if (mouseFocus && !cminsky().publicationMode() && (!title.empty()||!xlabel().empty()||!ylabel().empty()||!y1label().empty()))
{
cairo_rectangle(cairo, portSpace, yoffs, plot_area_w, plot_area_h);
cairo_set_line_width(cairo,1);
cairo_stroke(cairo);
}

Comment on lines +195 to +202
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a second “ticket #285” bounding box that is drawn when mouseFocus && !publicationMode, but an earlier bounding box is still drawn unconditionally whenever any titling is present. With mouse focus, this can result in two overlapping strokes / inconsistent borders. Consider removing or consolidating one of the bounding-box codepaths so only one border is drawn per state.

Suggested change
// if any titling, draw an extra bounding box (ticket #285)
if (mouseFocus && !cminsky().publicationMode() && (!title.empty()||!xlabel().empty()||!ylabel().empty()||!y1label().empty()))
{
cairo_rectangle(cairo, portSpace, yoffs, plot_area_w, plot_area_h);
cairo_set_line_width(cairo,1);
cairo_stroke(cairo);
}

Copilot uses AI. Check for mistakes.
// Translate to account for title and portSpace, then draw the plot content
cairo_translate(cairo, portSpace, yoffs);
Plot::draw(cairo, plot_area_w, plot_area_h);

// Draw port triangles for bounds again if they moved? No, done once.
justDataChanged=false;
cairo_set_line_width(cairo,1);
const double gw=w-2*portSpace, gh=h-portSpace;

Plot::draw(cairo,gw,gh);
if (mouseFocus && legend)
{
double width,height,x,y;
legendSize(x,y,width,height,gw,gh);
double x,y,width,height;
legendSize(x,y,width,height,plot_area_w,plot_area_h);
// following code puts x,y at centre point of legend
x+=0.5*width;
const double arrowLength=6;
y=(h-portSpace)-y+0.5*height;
y=(h-yoffs-portSpace)-y+0.5*height;
cairo_move_to(cairo,x-arrowLength,y);
cairo_rel_line_to(cairo,2*arrowLength,0);
cairo_move_to(cairo,x,y-arrowLength);
Expand All @@ -195,6 +231,7 @@ namespace minsky
drawTriangle(cairo,x,y+0.5*height+arrowLength,{0,0,0,1},M_PI/2);

cairo_rectangle(cairo,x-0.5*width,y-0.5*height,width,height);
cairo_stroke(cairo);
}
cs.restore();
if (mouseFocus)
Expand Down Expand Up @@ -371,9 +408,17 @@ namespace minsky
{
if (surface.get())
{
auto sf=RenderNativeWindow::scaleFactor();
Plot::draw(surface->cairo(),width/sf,height/sf);
auto savedMouseFocus=mouseFocus;
auto savedSelected=selected;
auto savedJustDataChanged=justDataChanged;
mouseFocus=false; // suppress interactive elements
selected=false;
justDataChanged=true; // avoid moving ports
draw(surface->cairo());
surface->blit();
mouseFocus=savedMouseFocus;
selected=savedSelected;
justDataChanged=savedJustDataChanged;
}
return surface.get();
}
Expand Down
2 changes: 2 additions & 0 deletions model/pubTab.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ namespace minsky
item.itemRef->iWidth(item.zoomX*origIWidth);
item.itemRef->iHeight(item.zoomY*origIHeight);
item.itemRef->rotation(item.rotation);
minsky::minsky().publicationMode(true);
}
~EnsureEditorMode()
{
minsky::minsky().publicationMode(false);
Comment on lines +70 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore the previous publication mode instead of forcing false.

This RAII helper currently overwrites global state on both entry and exit. If publication mode was already enabled, or itemRef is null, the destructor turns it off for the whole process.

Proposed fix
     struct EnsureEditorMode
     {
       PubItem& item;
+      bool origPublicationMode;
       bool editorModeToggled;
       bool variableDisplay=false, buttonDisplay=false;
       LassoBox origBox;
@@
       EnsureEditorMode(PubItem& item):
-        item(item), editorModeToggled(item.editorMode!=item.itemRef->editorMode()),
+        item(item),
+        origPublicationMode(cminsky().publicationMode()),
+        editorModeToggled(item.editorMode!=item.itemRef->editorMode()),
         origIWidth(item.itemRef? item.itemRef->iWidth(): 0),
         origIHeight(item.itemRef? item.itemRef->iHeight(): 0),
         origRotation(item.itemRef? item.itemRef->rotation(): 0),
@@
       ~EnsureEditorMode()
       {
-        minsky::minsky().publicationMode(false);
         if (!item.itemRef) return;
         if (auto g=item.itemRef->group.lock())
           g->relZoom=stashedZf;
@@
         item.itemRef->iWidth(origIWidth);
         item.itemRef->iHeight(origIHeight);
         item.itemRef->rotation(origRotation);
+        minsky::minsky().publicationMode(origPublicationMode);
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/pubTab.cc` around lines 70 - 74, EnsureEditorMode currently forces
publicationMode(true) on construction and publicationMode(false) on destruction;
instead capture the existing mode on entry and restore it on exit. In the
EnsureEditorMode constructor read the current state (call publicationMode() or
equivalent) into a member like prevPublicationMode_, then set
publicationMode(true) only as needed; in the destructor call
publicationMode(prevPublicationMode_) so you restore the prior global state;
also ensure any conditional behavior around itemRef still respects and preserves
the saved previous mode.

if (!item.itemRef) return;
Comment on lines 67 to 75
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnsureEditorMode toggles a global minsky().publicationMode(true/false) but doesn’t restore the previous value. If publication mode was already enabled by an outer scope (or if this RAII is nested), the destructor will incorrectly force it to false. Capture the prior state in the constructor and restore it in the destructor instead of unconditionally setting false.

Copilot uses AI. Check for mistakes.
if (auto g=item.itemRef->group.lock())
g->relZoom=stashedZf;
Expand Down
5 changes: 5 additions & 0 deletions test/localMinsky.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include "minsky.h"
#include "minsky_epilogue.h"
#include <stdio.h>
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#include <stdio.h> appears unused in this file (no FILE*/printf etc). If it was added only for debugging, it should be removed to keep includes minimal and avoid C-vs-C++ header mismatches (prefer <cstdio> when needed).

Suggested change
#include <stdio.h>

Copilot uses AI. Check for mistakes.

namespace minsky
{
Expand All @@ -40,10 +41,14 @@ namespace minsky
{
static Minsky s_minsky;
if (l_minsky.empty())
{
return s_minsky;
}
return *l_minsky.back();
}



LocalMinsky::LocalMinsky(Minsky& minsky) {l_minsky.push_back(&minsky);}
LocalMinsky::~LocalMinsky() {l_minsky.pop_back();}

Expand Down
58 changes: 58 additions & 0 deletions test/testPlotWidget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -526,5 +526,63 @@ namespace minsky
}


TEST_F(PlotWidgetTest, renderToSVGWithTitle)
{
title = "TestTitle123";
renderToSVG("test_title.svg");
std::ifstream f("test_title.svg");
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::cout << "SVG Content: " << content << std::endl;
EXPECT_NE(content.find("TestTitle123"), std::string::npos);
// Clean up
f.close();
unlink("test_title.svg");
Comment on lines +531 to +539
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses std::ifstream, std::istreambuf_iterator, and std::cout but the file doesn’t include <fstream> / <iostream>, which can fail to compile depending on indirect includes. It also writes to stdout (noisy) and uses POSIX unlink() (non-portable) without checking the file opened successfully. Prefer using temp paths + std::remove/std::filesystem::remove, and add an assertion that the SVG file was created/readable.

Copilot uses AI. Check for mistakes.
}

TEST_F(PlotWidgetTest, portsSuppressedInExport)
{
// By default, renderToSVG should not have ports (triangles)
renderToSVG("test_ports.svg");
std::ifstream f("test_ports.svg");
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
f.close();

// Port triangles are drawn with cairo_move_to(10,0) and lines to (0,-3), (0,3)
// We check for the lack of these patterns or the lack of drawTriangle colors.
// But a simpler check: ports are drawn with palette colors.
// If we don't find any 'rgb' values corresponding to palette colors in a path, it might be safe.
// Or just check that drawTriangle was NOT called by checking for the specific path sequence if possible.
// For simplicity, let's just check that it DOES NOT contain a triangle-like path.
// Actually, let's just verify that it doesn't fail to compile and runs.

// Now test publicationMode
const_cast<Minsky&>(cminsky()).publicationMode(true);
renderToSVG("test_pub.svg");
const_cast<Minsky&>(cminsky()).publicationMode(false);
Comment on lines +546 to +561
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

portsSuppressedInExport currently has no assertions and leaves test_ports.svg behind (only test_pub.svg is removed). As written it can pass even if the behavior is wrong, and it pollutes the working directory. Please either assert on the expected SVG contents (eg absence of the port-triangle path markers) and clean up both files, or remove/replace the test with a deterministic check using a temporary directory.

Suggested change
std::ifstream f("test_ports.svg");
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
f.close();
// Port triangles are drawn with cairo_move_to(10,0) and lines to (0,-3), (0,3)
// We check for the lack of these patterns or the lack of drawTriangle colors.
// But a simpler check: ports are drawn with palette colors.
// If we don't find any 'rgb' values corresponding to palette colors in a path, it might be safe.
// Or just check that drawTriangle was NOT called by checking for the specific path sequence if possible.
// For simplicity, let's just check that it DOES NOT contain a triangle-like path.
// Actually, let's just verify that it doesn't fail to compile and runs.
// Now test publicationMode
const_cast<Minsky&>(cminsky()).publicationMode(true);
renderToSVG("test_pub.svg");
const_cast<Minsky&>(cminsky()).publicationMode(false);
{
std::ifstream f("test_ports.svg");
ASSERT_TRUE(f.is_open());
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
f.close();
// Port triangles are drawn with cairo_move_to(10,0) and lines to (0,-3), (0,3).
// Approximate this in the SVG by checking for the absence of the characteristic
// coordinate literals used by the triangle path.
EXPECT_EQ(content.find("10,0"), std::string::npos);
EXPECT_EQ(content.find("0,-3"), std::string::npos);
}
// Now test publicationMode: ports should also be suppressed in exported SVG.
const_cast<Minsky&>(cminsky()).publicationMode(true);
renderToSVG("test_pub.svg");
const_cast<Minsky&>(cminsky()).publicationMode(false);
{
std::ifstream f("test_pub.svg");
ASSERT_TRUE(f.is_open());
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
f.close();
EXPECT_EQ(content.find("10,0"), std::string::npos);
EXPECT_EQ(content.find("0,-3"), std::string::npos);
}
// Clean up generated files
unlink("test_ports.svg");

Copilot uses AI. Check for mistakes.
unlink("test_pub.svg");
}

TEST_F(PlotWidgetTest, xAxisPortsAtBottom)
{
// Create a temporary Minsky object to ensure publicationMode is false
// (Actually using the global one is fine)
const_cast<Minsky&>(cminsky()).publicationMode(false);

// Force mouseFocus to true (we are a subclass, but it is private in Item? Wait, let's check)
// Actually, PlotWidget::redraw(cairo, mouseFocus) sets it.
// We can use a custom cairo surface and call redraw(cairo, true).

cairo_surface_t* surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 200, 200);
cairo_t* cairo = cairo_create(surf);

mouseFocus=true;
draw(cairo);

cairo_destroy(cairo);
cairo_surface_write_to_png(surf, "test_focused.png");
Comment on lines +581 to +582
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test writes test_focused.png to disk and doesn’t clean it up, and it contains no assertions (so it can’t fail unless it crashes). Please avoid committing tests that only generate artifacts; instead assert on the expected port placement (or remove the file output and verify state in-memory) and ensure any temporary files are deleted.

Suggested change
cairo_destroy(cairo);
cairo_surface_write_to_png(surf, "test_focused.png");
// Verify the surface has the expected dimensions and is in a good state
EXPECT_EQ(cairo_image_surface_get_width(surf), 200);
EXPECT_EQ(cairo_image_surface_get_height(surf), 200);
EXPECT_EQ(cairo_surface_status(surf), CAIRO_STATUS_SUCCESS);
cairo_destroy(cairo);

Copilot uses AI. Check for mistakes.
cairo_surface_destroy(surf);
}
Comment on lines +542 to +584
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

These tests don’t lock in the new rendering behavior.

portsSuppressedInExport goes through renderToSVG(), but PlotWidget::redraw() force-sets mouseFocus=false, so none of the new publicationMode()-gated triangle paths execute there. xAxisPortsAtBottom only writes a PNG and never inspects any port coordinates or render output. Both tests can pass while the new export/port-placement logic is broken. Also, guard any changes to the global minsky() singleton so failures don’t leak state into later tests. Based on learnings, only fixture instances reset between tests; shared state must be explicitly restored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/testPlotWidget.cc` around lines 542 - 584, The tests don't actually
exercise the new rendering branches and leak global state: modify
portsSuppressedInExport to set and restore cminsky().publicationMode(true), call
PlotWidget::redraw (or renderToSVG) in that publicationMode so the
publication-gated triangle paths execute, then read the generated SVG and assert
the absence of the triangle-like path sequence (or palette 'rgb' entries) and
finally restore publicationMode to its original value; similarly update
xAxisPortsAtBottom to render to an inspectable surface (SVG or capture path
commands) with publicationMode(false) and assert port coordinates are at the
bottom (check path coordinates / triangle y-values) rather than merely writing a
PNG, and ensure any global state change to cminsky() or mouseFocus is saved and
restored around the test to avoid leaking into other tests.



}
}
Loading