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/canvas.cc b/model/canvas.cc index 75daea5d0..37400788e 100644 --- a/model/canvas.cc +++ b/model/canvas.cc @@ -426,9 +426,24 @@ 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 + 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->clickType(x,y)!=ClickType::outside;}); + } return item; } diff --git a/model/group.cc b/model/group.cc index 5dbf77cda..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; @@ -912,10 +914,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())& vars, float edgeX) { + 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; + + // 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()); + + // 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.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; + 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) + { + bottom = b; + top = t; + } + else if (i % 2) + top -= (b - t); + else + bottom += (b - t); + } + }; + + 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 for (auto& v: outVariables) if (RenderVariable(*v).inImage(x,y)) return v; + return nullptr; } 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 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"