Skip to content

Export plot title to images. For #1259#625

Open
highperformancecoder wants to merge 1 commit intomasterfrom
bugfix-1259-plot-title-export
Open

Export plot title to images. For #1259#625
highperformancecoder wants to merge 1 commit intomasterfrom
bugfix-1259-plot-title-export

Conversation

@highperformancecoder
Copy link
Owner

@highperformancecoder highperformancecoder commented Mar 12, 2026

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added publication mode feature to suppress interactive elements when exporting plots.
  • Bug Fixes

    • Improved plot rendering with enhanced title positioning and dimensions handling.
    • Refined port placement calculations for more accurate chart layout.
  • Tests

    • Added test coverage for plot widget rendering, title export, and port visibility in export mode.

Copilot AI review requested due to automatic review settings March 12, 2026 05:14
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

This pull request adds publication mode support to the Minsky model. It introduces TypeScript wrapper methods for publication mode control, extends the C++ Minsky class with publication mode state and a recursion guard flag, refactors plot widget rendering to suppress interactive port triangles when in publication mode, and updates editor mode to toggle publication state on initialization and cleanup.

Changes

Cohort / File(s) Summary
TypeScript API Wrapper
gui-js/libs/shared/src/lib/backend/minsky.ts
Added three new public async methods: inPopulateMissingDimensions(), m_publicationMode(), and publicationMode() that delegate to native methods and return boolean results.
C++ Minsky Core
model/minsky.h
Updated class inheritance syntax with classdesc:: prefix, added inPopulateMissingDimensions flag to guard recursion, and added m_publicationMode member with publicationMode() accessor/mutator methods.
Plot Widget Rendering
model/plotWidget.cc
Major refactoring of rendering logic including computed font sizing, simplified port placement calculations, conditional triangle drawing based on publication mode and mouse focus, plot area calculations accounting for port spacing, and legend positioning updates.
Editor Mode State Management
model/pubTab.cc
Added publication mode toggling in EnsureEditorMode constructor (enable) and destructor (disable) to control interactive element visibility.
Test Suite
test/testPlotWidget.cc
Added three new test cases: renderToSVGWithTitle() for title rendering validation, portsSuppressedInExport() for publication mode behavior, and xAxisPortsAtBottom() for port positioning with Cairo surface.
Utility
test/localMinsky.cc
Minor code style adjustment wrapping conditional block with braces and added stdio.h include.

Sequence Diagram(s)

sequenceDiagram
    participant GUI as GUI / Editor
    participant Minsky as Minsky Core
    participant PlotWidget as Plot Widget
    participant Cairo as Cairo Renderer

    GUI->>Minsky: publicationMode(true)
    Note over Minsky: m_publicationMode = true
    
    GUI->>PlotWidget: redraw()
    PlotWidget->>Minsky: publicationMode()
    Minsky-->>PlotWidget: true
    
    alt Publication Mode Active
        PlotWidget->>PlotWidget: Skip port triangles
        PlotWidget->>PlotWidget: Hide interactive elements
    else Normal Mode
        PlotWidget->>PlotWidget: Draw port triangles
        PlotWidget->>PlotWidget: Show interactive elements
    end
    
    PlotWidget->>Cairo: Render plot area
    Cairo-->>PlotWidget: Rendered content
    
    GUI->>Minsky: publicationMode(false)
    Note over Minsky: m_publicationMode = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop, skip, and jump through render's grand hall,
We've tucked interactive bits when publication's call,
With publication mode flags set high and serene,
The cleanest of exports the user will see!
✨📊

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Export plot title to images' directly addresses the main change across multiple files that adds infrastructure for exporting plot titles to images and rendering control, including publication mode support and title positioning adjustments in plotWidget rendering.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-1259-plot-title-export

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a “publication mode” rendering path intended to suppress interactive plot UI elements (ports/triangles) during publication-tab rendering and exports, while also adjusting PlotWidget layout for titles/ports and adding regression tests around SVG output.

Changes:

  • Add a new Minsky::publicationMode flag and propagate it into PlotWidget and publication-tab rendering.
  • Rework PlotWidget::draw() layout (title offset, port placement, conditional triangle/bounding-box drawing) and adjust native-window redraw to render via PlotWidget::draw().
  • Add tests around SVG title rendering / port suppression and update JS/TS backend bindings for new flags.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
model/plotWidget.cc Updates plot drawing/layout and suppresses interactive markers based on mouse focus and publication mode; changes redraw behavior.
model/pubTab.cc Toggles publication mode while rendering publication-tab items.
model/minsky.h Adds publication-mode state and a recursion-guard flag; qualifies classdesc::Exclude.
gui-js/libs/shared/src/lib/backend/minsky.ts Exposes new fields/methods in the generated TS backend wrapper.
test/testPlotWidget.cc Adds new PlotWidget SVG/export-related tests.
test/localMinsky.cc Minor local-minsky helper change (extra include / formatting).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +146 to +148
if (m_numLines == 0) return;
const float dx = plot_area_w / (2*m_numLines + 1);
const float dy = plot_area_h / m_numLines;
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.
Comment on lines +135 to +140
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]);
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.
Comment on lines +195 to +202
// 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);
}

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.
Comment on lines +161 to 163
if (mouseFocus && !cminsky().publicationMode())
drawTriangle(cairo, x - portSpace, y, palette[k%palette.size()].colour, 0);
}
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.
Comment on lines 67 to 75
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);
if (!item.itemRef) return;
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.
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.
Comment on lines +531 to +539
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");
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.
Comment on lines +546 to +561
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);
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.
Comment on lines +581 to +582
cairo_destroy(cairo);
cairo_surface_write_to_png(surf, "test_focused.png");
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.

#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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
model/plotWidget.cc (1)

411-421: Restore the saved flags with RAII.

If draw(surface->cairo()) or surface->blit() throws, mouseFocus, selected, and justDataChanged stay clobbered on the widget. A scoped restore would make the export path resilient.

Proposed fix
         auto savedMouseFocus=mouseFocus;
         auto savedSelected=selected;
         auto savedJustDataChanged=justDataChanged;
         mouseFocus=false; // suppress interactive elements
         selected=false;
         justDataChanged=true; // avoid moving ports
+        const OnStackExit restore([&] {
+          mouseFocus=savedMouseFocus;
+          selected=savedSelected;
+          justDataChanged=savedJustDataChanged;
+        });
         draw(surface->cairo());
         surface->blit();
-        mouseFocus=savedMouseFocus;
-        selected=savedSelected;
-        justDataChanged=savedJustDataChanged;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/plotWidget.cc` around lines 411 - 421, Wrap the manual save/restore of
mouseFocus, selected, and justDataChanged in a small RAII guard so the original
values are restored even if draw(surface->cairo()) or surface->blit() throws;
create a local scope-guard type (e.g. RestoreWidgetFlags) or use std::scope_exit
if available, have its constructor capture the three original values and set
mouseFocus=false, selected=false, justDataChanged=true, and its destructor
restore the captured values, then instantiate the guard immediately before
calling draw(...) and surface->blit() and remove the manual restores that
follow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model/minsky.h`:
- Around line 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.
- Around line 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.

In `@model/plotWidget.cc`:
- Around line 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.

In `@model/pubTab.cc`:
- Around line 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.

In `@test/testPlotWidget.cc`:
- Around line 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.

---

Nitpick comments:
In `@model/plotWidget.cc`:
- Around line 411-421: Wrap the manual save/restore of mouseFocus, selected, and
justDataChanged in a small RAII guard so the original values are restored even
if draw(surface->cairo()) or surface->blit() throws; create a local scope-guard
type (e.g. RestoreWidgetFlags) or use std::scope_exit if available, have its
constructor capture the three original values and set mouseFocus=false,
selected=false, justDataChanged=true, and its destructor restore the captured
values, then instantiate the guard immediately before calling draw(...) and
surface->blit() and remove the manual restores that follow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7fba824-852c-4445-b395-47f5594b0b8e

📥 Commits

Reviewing files that changed from the base of the PR and between ab53be1 and 03868d6.

📒 Files selected for processing (6)
  • gui-js/libs/shared/src/lib/backend/minsky.ts
  • model/minsky.h
  • model/plotWidget.cc
  • model/pubTab.cc
  • test/localMinsky.cc
  • test/testPlotWidget.cc

Comment on lines +206 to 208
// used to avoid excessive recursion in populateMissingDimensionsFromVariable
bool inPopulateMissingDimensions=false;
void populateMissingDimensions();
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.

Comment on lines +216 to +218
bool m_publicationMode=false;
bool publicationMode() const {return m_publicationMode;}
void publicationMode(bool x) {m_publicationMode=x;}
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.

Comment on lines +150 to 193
// 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);
}

// 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())
drawTriangle(cairo, x, y + portSpace, palette[(k/2)%palette.size()].colour, -0.5*M_PI);
}
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.

Comment on lines +70 to +74
minsky::minsky().publicationMode(true);
}
~EnsureEditorMode()
{
minsky::minsky().publicationMode(false);
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.

Comment on lines +542 to +584
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);
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");
cairo_surface_destroy(surf);
}
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.


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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants