Release v0.6.5: preserve caption links and connect arrows to other objects

This commit is contained in:
2026-10-29 17:15:47 +02:00
parent b8bb14a985
commit bbf20e8270
10 changed files with 235 additions and 34 deletions
+61
View File
@@ -179,6 +179,67 @@ int main(int argc, char **argv) {
require(detached.fromNumber==0,"dropping away from badges remains detached");
auto translated=ks::dragScreenshotMark(linked[2],ks::MarkPart::Body,{80,35},{85,40},160,100);
require(translated.fromNumber==0 && translated.toNumber==0,"moving whole arrow detaches both endpoints");
for(auto shape : {ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) {
std::vector<ks::ScreenshotMark> objects={{{100,50},{160,110},0},{{10,80},{100,80},0},{{10,140},{55,80},0}};
objects[0].shape=shape;
ks::linkScreenshotEndpoint(objects[1],false,{100,80},objects,6);
ks::resolveScreenshotLinks(objects);
require(objects[1].toLink.target==objects[0].id && objects[1].to.x==100,"arrow attaches to shape boundary");
ks::linkScreenshotEndpoint(objects[2],false,{55,80},objects,6);
ks::resolveScreenshotLinks(objects);
require(objects[2].toLink.target==objects[1].id,"arrow attaches to another arrow body");
auto targetId=objects[0].id,arrowId=objects[1].id;
auto snapshot=objects; ks::ScreenshotHistory linksHistory; linksHistory.remember(objects);
// Reverse storage order so dependency resolution cannot rely on insertion order.
std::reverse(objects.begin(),objects.end());
objects[2]=ks::dragScreenshotMark(objects[2],ks::MarkPart::Body,{0,0},{20,20},400,300);
ks::resolveScreenshotLinks(objects);
require(objects[1].to.x==120 && objects[1].to.y==100 && objects[0].to.x==65 && objects[0].to.y==90,
"shape-arrow-arrow chain updates in dependency order");
require(linksHistory.undo(objects) && objects[1].to.x==100 && objects[2].toLink.target==arrowId,
"undo restores geometry and generic links");
require(linksHistory.redo(objects) && objects[0].to.x==65,"redo restores moved chain");
auto resized=objects;
auto corner=resized[2].to;
resized[2]=ks::dragScreenshotMark(resized[2],ks::MarkPart::End,corner,{corner.x+20,corner.y+20},400,300);
ks::resolveScreenshotLinks(resized);
require(resized[1].to.x==120 && resized[1].to.y==110 && resized[0].to.y==95,
"resizing shapes updates both levels of attached arrows");
ks::linkScreenshotEndpoint(objects[1],true,objects[0].from,objects,6);
require(!objects[1].fromLink.target,"cyclic arrow links are rejected");
auto clone=ks::cloneScreenshotMark(objects[1],objects,400,300);
require(!clone.id && !clone.fromLink.target && !clone.toLink.target,"cloned connector has independent identity and no links");
auto endpoint=objects[1].to;
objects.erase(objects.begin()+2); ks::resolveScreenshotLinks(objects);
require(!objects[1].toLink.target && objects[1].to.x==endpoint.x && objects[0].toLink.target==arrowId,
"deleting shape detaches only its dependents without moving endpoints");
objects.push_back(snapshot[0]); objects.back().id=0; ks::resolveScreenshotLinks(objects);
require(objects.back().id!=targetId && !objects[1].toLink.target,"new objects cannot revive deleted links");
}
{
std::vector<ks::ScreenshotMark> objects={{{80,40},{80,40},0},{{10,40},{80,40},0}};
objects[0].text=L"Target"; objects[0].fontSize=20;
auto bounds=ks::screenshotTextBounds(objects[0]);
ks::linkScreenshotEndpoint(objects[1],false,{bounds.left,(bounds.top+bounds.bottom)/2},objects,6);
ks::resolveScreenshotLinks(objects);
require(objects[1].toLink.target==objects[0].id,"arrow attaches to text block");
auto old=objects[1].to;
objects[0]=ks::dragScreenshotMark(objects[0],ks::MarkPart::Body,{0,0},{30,20},400,300);
ks::resolveScreenshotLinks(objects);
require(objects[1].to.x==old.x+30 && objects[1].to.y==old.y+20,"text attachment follows movement");
objects[0].rotation=90; ks::resolveScreenshotLinks(objects);
require(objects[1].to.x!=old.x+30 || objects[1].to.y!=old.y+20,"text attachment follows rotation");
}
for(bool freehand : {false,true}) {
std::vector<ks::ScreenshotMark> objects={{{80,60},{160,80},0},{{10,140},{110,60},0}};
objects[0].points={{80,60},{140,60},{160,80}}; objects[0].freehand=freehand;
ks::linkScreenshotEndpoint(objects[1],false,{110,60},objects,6);
ks::resolveScreenshotLinks(objects);
require(objects[1].toLink.target==objects[0].id,"arrow attaches to freehand and highlight strokes");
objects[0]=ks::dragScreenshotMark(objects[0],ks::MarkPart::Body,{0,0},{20,20},400,300);
ks::resolveScreenshotLinks(objects);
require(objects[1].to.x==130 && objects[1].to.y==80,"stroke attachment follows the original segment");
}
auto lastPoint=linked[2].from;
linked.erase(linked.begin());
ks::resolveScreenshotLinks(linked);
+19
View File
@@ -120,6 +120,25 @@ int main()
std::abs(std::hypot(double(c.marks[2].to.x-end.x),double(c.marks[2].to.y-end.y))-15)<1,
"connector endpoints follow both badge rims");
}
// Selecting a linked connector (also the first click of a double-click)
// must not detach it before opening its caption editor.
for(int entry : {0,1,2}) {
c.selected=-1;
POINT body{110,c.marks[0].from.y};
if(freehand) body={(c.marks[2].points[0].x+c.marks[2].points[1].x)/2,
(c.marks[2].points[0].y+c.marks[2].points[1].y)/2};
mouse(WM_LBUTTONDOWN,body); mouse(WM_LBUTTONUP,body);
require(c.selected==2 && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,
"clicking a connector without moving preserves both links");
if(entry==0) { mouse(WM_LBUTTONDBLCLK,body); mouse(WM_LBUTTONUP,body); }
else if(entry==1) SendMessageW(window,WM_KEYDOWN,VK_F2,0);
else SendMessageW(window,WM_COMMAND,35,0);
require(c.inlineText && c.editingLabel,"linked connector caption opens through every entry point");
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Linked caption");
ks::finishInlineText(window,c,true);
require(c.marks[2].label==L"Linked caption" && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,
"adding or editing a caption preserves both connector links");
}
c.selected=2;
require(ks::markAt(c,c.marks[1].from).first==1,"selected connector cannot steal a badge click");
auto outside=c.marks[2].to; outside.x-=4;