From f4c90da4853ab60f7de199fa8195da14958e5f67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:13:12 +0000 Subject: [PATCH 01/10] Initial plan From 99655fe1e3467dd575e6eb44fc81ac3b4b922e2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:22:05 +0000 Subject: [PATCH 02/10] Fix: Check edge I/O variables before early return in Group::clickType Move the select(x,y) check before the displayContents() early return to ensure edge I/O variables are always checked first, allowing wire pulling from group outputs even when zoomed to display contents. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/group.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/group.cc b/model/group.cc index 5dbf77cda..fc3b583f5 100644 --- a/model/group.cc +++ b/model/group.cc @@ -912,10 +912,10 @@ namespace minsky auto z=zoomFactor(); const double w=0.5*iWidth()*z, h=0.5*iHeight()*z; if (onResizeHandle(x,y)) return ClickType::onResize; - if (displayContents() && inIORegion(x,y)==IORegion::none) - return ClickType::outside; if (auto item=select(x,y)) return item->clickType(x,y); + if (displayContents() && inIORegion(x,y)==IORegion::none) + return ClickType::outside; if ((abs(x-this->x())y()) Date: Wed, 18 Feb 2026 21:22:43 +0000 Subject: [PATCH 03/10] Complete fix for wire pulling from group outputs when zoomed Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- package-lock.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 283967596..a3ea4db8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -227,6 +228,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 5513afb2f104497540fb4366f17a5050f7c3cdc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:42:55 +0000 Subject: [PATCH 04/10] Fix itemAt to return edge I/O variables directly instead of groups When clicking on edge I/O variables, itemAt now checks group.select() and returns the variable directly, rather than returning the group. This ensures itemFocus is set to the variable, allowing wire dragging to work correctly. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/canvas.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/model/canvas.cc b/model/canvas.cc index 75daea5d0..05e614c80 100644 --- a/model/canvas.cc +++ b/model/canvas.cc @@ -426,9 +426,17 @@ namespace minsky return false; }); if (!item) - item=model->findAny - (&Group::groups, [&](const GroupPtr& i) - {return i->visible() && i->clickType(x,y)!=ClickType::outside;}); + { + // Check if clicking on a group's edge I/O variable + if (auto g=model->findAny + (&Group::groups, [&](const GroupPtr& i) + {return i->visible() && i->select(x,y);})) + if (auto selected=g->select(x,y)) + return selected; + item=model->findAny + (&Group::groups, [&](const GroupPtr& i) + {return i->visible() && i->clickType(x,y)!=ClickType::outside;}); + } return item; } From eba73d64a36368b5acc73c4a67f6e33b173bf849 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:43:39 +0000 Subject: [PATCH 05/10] Optimize itemAt to avoid redundant select() calls Refactored to use recursiveDo instead of findAny twice, eliminating the duplicate call to select(x,y) on the same group. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/canvas.cc | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/model/canvas.cc b/model/canvas.cc index 05e614c80..37400788e 100644 --- a/model/canvas.cc +++ b/model/canvas.cc @@ -428,14 +428,21 @@ namespace minsky if (!item) { // Check if clicking on a group's edge I/O variable - if (auto g=model->findAny + model->recursiveDo(&Group::groups, + [&](Groups&, Groups::const_iterator i) + { + if ((*i)->visible()) + if (auto selected=(*i)->select(x,y)) + { + item=selected; + return true; + } + return false; + }); + if (!item) + item=model->findAny (&Group::groups, [&](const GroupPtr& i) - {return i->visible() && i->select(x,y);})) - if (auto selected=g->select(x,y)) - return selected; - item=model->findAny - (&Group::groups, [&](const GroupPtr& i) - {return i->visible() && i->clickType(x,y)!=ClickType::outside;}); + {return i->visible() && i->clickType(x,y)!=ClickType::outside;}); } return item; } From 8b809551f250af51de6527f73e12a36e1b9e4db8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:30:21 +0000 Subject: [PATCH 06/10] Fix Group::select to calculate edge variable positions for hit testing Edge variables are only moved to their edge positions during drawing, so their stored x/y values don't reflect their actual rendered positions. Modified select() to calculate where each edge variable would be positioned (using same logic as draw1edge) and check if the click is within those calculated positions. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/group.cc | 58 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/model/group.cc b/model/group.cc index fc3b583f5..b367251d5 100644 --- a/model/group.cc +++ b/model/group.cc @@ -1182,12 +1182,58 @@ namespace minsky ItemPtr Group::select(float x, float y) const { - for (auto& v: inVariables) - if (RenderVariable(*v).inImage(x,y)) - return v; - for (auto& v: outVariables) - if (RenderVariable(*v).inImage(x,y)) - return v; + // Edge variables are positioned during drawing, but we need to check + // their positions for hit testing. Temporarily position them at their + // correct edge locations. + float left, right; + margins(left, right); + const float z = zoomFactor(); + const Rotate r(rotation(), 0, 0); + + // Helper to position and check variables on one edge + auto checkEdge = [&](const vector& vars, float edgeX) -> ItemPtr { + float top = 0, bottom = 0; + for (size_t i = 0; i < vars.size(); ++i) + { + auto& v = vars[i]; + float edgeY = 0; + auto vz = v->zoomFactor(); + auto t = v->bb.top() * vz, b = v->bb.bottom() * vz; + if (i > 0) edgeY = i % 2 ? top - b : bottom - t; + + // Calculate where this variable would be positioned + const float varX = r.x(edgeX, edgeY) + this->x(); + const float varY = r.y(edgeX, edgeY) + this->y(); + + // Check if click is within this variable's bounds at the edge position + const float dx = x - varX, dy = y - varY; + const RenderVariable rv(*v); + const float rx = dx * cos(v->rotation() * M_PI / 180) - dy * sin(v->rotation() * M_PI / 180); + const float ry = dy * cos(v->rotation() * M_PI / 180) + dx * sin(v->rotation() * M_PI / 180); + if (rx >= -rv.width() && rx <= rv.width() && ry >= -rv.height() && ry <= rv.height()) + return v; + + if (i == 0) + { + bottom = b; + top = t; + } + else if (i % 2) + top -= v->height(); + else + bottom += v->height(); + } + return nullptr; + }; + + // Check input variables on the left edge + if (auto v = checkEdge(inVariables, -0.5 * (iWidth() * z - left))) + return v; + + // Check output variables on the right edge + if (auto v = checkEdge(outVariables, 0.5 * (iWidth() * z - right))) + return v; + return nullptr; } From 0f4dc7c257fcd7aeedbae2527450bbcc652c95b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:31:11 +0000 Subject: [PATCH 07/10] Optimize rotation calcs and fix height consistency in select() - Cache rotation angle conversion and trig calculations to avoid redundant computations - Use (b - t) instead of v->height() for consistency with bounding box calculations Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/group.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/model/group.cc b/model/group.cc index b367251d5..f028493ea 100644 --- a/model/group.cc +++ b/model/group.cc @@ -1208,8 +1208,11 @@ namespace minsky // Check if click is within this variable's bounds at the edge position const float dx = x - varX, dy = y - varY; const RenderVariable rv(*v); - const float rx = dx * cos(v->rotation() * M_PI / 180) - dy * sin(v->rotation() * M_PI / 180); - const float ry = dy * cos(v->rotation() * M_PI / 180) + dx * sin(v->rotation() * M_PI / 180); + const float vRotRad = v->rotation() * M_PI / 180; + const float cosRot = cos(vRotRad); + const float sinRot = sin(vRotRad); + const float rx = dx * cosRot - dy * sinRot; + const float ry = dy * cosRot + dx * sinRot; if (rx >= -rv.width() && rx <= rv.width() && ry >= -rv.height() && ry <= rv.height()) return v; @@ -1219,9 +1222,9 @@ namespace minsky top = t; } else if (i % 2) - top -= v->height(); + top -= (b - t); else - bottom += v->height(); + bottom += (b - t); } return nullptr; }; From 4bb386ccda82aa972b06efd32e802600d9cd4a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:40:08 +0000 Subject: [PATCH 08/10] Fix edge variable and port positioning for hit testing Created positionEdgeVariables() method that positions both edge variables and their ports at correct locations. This ensures port hit testing works correctly when clicking on edge I/O variables. The method is called from select() before hit testing. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/group.cc | 63 ++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/model/group.cc b/model/group.cc index f028493ea..f82cd67e4 100644 --- a/model/group.cc +++ b/model/group.cc @@ -1180,18 +1180,16 @@ namespace minsky return rotFactor; } - ItemPtr Group::select(float x, float y) const + void Group::positionEdgeVariables() const { - // Edge variables are positioned during drawing, but we need to check - // their positions for hit testing. Temporarily position them at their - // correct edge locations. + // Position edge variables at their correct edge locations + // This is needed for hit testing since ports are positioned relative to variables float left, right; margins(left, right); const float z = zoomFactor(); const Rotate r(rotation(), 0, 0); - // Helper to position and check variables on one edge - auto checkEdge = [&](const vector& vars, float edgeX) -> ItemPtr { + auto positionEdge = [&](const vector& vars, float edgeX) { float top = 0, bottom = 0; for (size_t i = 0; i < vars.size(); ++i) { @@ -1201,20 +1199,21 @@ namespace minsky auto t = v->bb.top() * vz, b = v->bb.bottom() * vz; if (i > 0) edgeY = i % 2 ? top - b : bottom - t; - // Calculate where this variable would be positioned - const float varX = r.x(edgeX, edgeY) + this->x(); - const float varY = r.y(edgeX, edgeY) + this->y(); + // Move variable to edge position (same as in draw1edge) + v->moveTo(r.x(edgeX, edgeY) + this->x(), r.y(edgeX, edgeY) + this->y()); + v->rotation(rotation()); - // Check if click is within this variable's bounds at the edge position - const float dx = x - varX, dy = y - varY; + // Also position ports (simplified version of what happens in Variable::draw) + // This ensures port hit testing works correctly const RenderVariable rv(*v); - const float vRotRad = v->rotation() * M_PI / 180; - const float cosRot = cos(vRotRad); - const float sinRot = sin(vRotRad); - const float rx = dx * cosRot - dy * sinRot; - const float ry = dy * cosRot + dx * sinRot; - if (rx >= -rv.width() && rx <= rv.width() && ry >= -rv.height() && ry <= rv.height()) - return v; + const double w = std::max(rv.width(), 0.5 * v->iWidth()); + const double angle = v->rotation() * M_PI / 180.0; + const double sa = sin(angle), ca = cos(angle); + const double x0 = vz * w, y0 = 0, x1 = -vz * w + 2, y1 = 0; + if (v->portsSize() > 0) + v->ports(0).lock()->moveTo(v->x() + (x0 * ca - y0 * sa), v->y() + (y0 * ca + x0 * sa)); + if (v->portsSize() > 1) + v->ports(1).lock()->moveTo(v->x() + (x1 * ca - y1 * sa), v->y() + (y1 * ca + x1 * sa)); if (i == 0) { @@ -1222,20 +1221,30 @@ namespace minsky top = t; } else if (i % 2) - top -= (b - t); + top -= v->height(); else - bottom += (b - t); + bottom += v->height(); } - return nullptr; }; - // Check input variables on the left edge - if (auto v = checkEdge(inVariables, -0.5 * (iWidth() * z - left))) - return v; + positionEdge(inVariables, -0.5 * (iWidth() * z - left)); + positionEdge(outVariables, 0.5 * (iWidth() * z - right)); + } + + ItemPtr Group::select(float x, float y) const + { + // Position edge variables at their correct locations for accurate hit testing + positionEdgeVariables(); + + // Check input variables + for (auto& v: inVariables) + if (RenderVariable(*v).inImage(x,y)) + return v; - // Check output variables on the right edge - if (auto v = checkEdge(outVariables, 0.5 * (iWidth() * z - right))) - return v; + // Check output variables + for (auto& v: outVariables) + if (RenderVariable(*v).inImage(x,y)) + return v; return nullptr; } From e1d19f206d42b619dfd5576987f111ae628d11e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:41:03 +0000 Subject: [PATCH 09/10] Fix height calculation to use zoom-adjusted values Use (b - t) instead of v->height() for consistency with zoom-scaled bounding box calculations. This ensures correct vertical spacing of edge variables. Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- model/group.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/group.cc b/model/group.cc index f82cd67e4..d61cc362a 100644 --- a/model/group.cc +++ b/model/group.cc @@ -1221,9 +1221,9 @@ namespace minsky top = t; } else if (i % 2) - top -= v->height(); + top -= (b - t); else - bottom += v->height(); + bottom += (b - t); } }; From 005acb00afc96e9209838dfdf92e5c7b17b6d005 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Thu, 19 Feb 2026 18:38:13 +1100 Subject: [PATCH 10/10] Fix compilation errors. --- gui-js/libs/shared/src/lib/backend/minsky.ts | 2 ++ model/group.cc | 4 +++- model/group.h | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gui-js/libs/shared/src/lib/backend/minsky.ts b/gui-js/libs/shared/src/lib/backend/minsky.ts index bb4abf247..11da11a7d 100644 --- a/gui-js/libs/shared/src/lib/backend/minsky.ts +++ b/gui-js/libs/shared/src/lib/backend/minsky.ts @@ -1107,6 +1107,7 @@ export class Group extends Item { async portY(a1: number): Promise {return this.$callMethod('portY',a1);} async ports(a1: number): Promise {return this.$callMethod('ports',a1);} async portsSize(): Promise {return this.$callMethod('portsSize');} + async positionEdgeVariables(): Promise {return this.$callMethod('positionEdgeVariables');} async px(...args: number[]): Promise {return this.$callMethod('px',...args);} async py(...args: number[]): Promise {return this.$callMethod('py',...args);} async pz(...args: number[]): Promise {return this.$callMethod('pz',...args);} @@ -2030,6 +2031,7 @@ export class Selection extends CppClass { async portY(a1: number): Promise {return this.$callMethod('portY',a1);} async ports(a1: number): Promise {return this.$callMethod('ports',a1);} async portsSize(): Promise {return this.$callMethod('portsSize');} + async positionEdgeVariables(): Promise {return this.$callMethod('positionEdgeVariables');} async randomLayout(): Promise {return this.$callMethod('randomLayout');} async relZoom(...args: number[]): Promise {return this.$callMethod('relZoom',...args);} async removeDisplayPlot(): Promise {return this.$callMethod('removeDisplayPlot');} diff --git a/model/group.cc b/model/group.cc index d61cc362a..8c5bd2764 100644 --- a/model/group.cc +++ b/model/group.cc @@ -33,6 +33,8 @@ using namespace std; using namespace ecolab::cairo; +#include + // size of the top and bottom margins of the group icon static const int topMargin=10; @@ -1206,7 +1208,7 @@ namespace minsky // Also position ports (simplified version of what happens in Variable::draw) // This ensures port hit testing works correctly const RenderVariable rv(*v); - const double w = std::max(rv.width(), 0.5 * v->iWidth()); + const double w = std::max(rv.width(), 0.5f * v->iWidth()); const double angle = v->rotation() * M_PI / 180.0; const double sa = sin(angle), ca = cos(angle); const double x0 = vz * w, y0 = 0, x1 = -vz * w + 2, y1 = 0; diff --git a/model/group.h b/model/group.h index d339efe3b..91511fb8d 100644 --- a/model/group.h +++ b/model/group.h @@ -372,6 +372,8 @@ namespace minsky /// scaling factor to allow a rotated icon to fit on the bitmap float rotFactor() const; + + void positionEdgeVariables() const; /// returns the variable if point (x,y) is within a /// I/O variable icon, null otherwise, indicating that the Group