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
+127 -24
View File
@@ -3,6 +3,8 @@
#include "../resources/resource.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <unordered_set>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
@@ -197,7 +199,7 @@ void updateStatus(Capture &c)
else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps";
else if(part==MarkPart::Thickness) text=L"Drag to change thickness | Mouse wheel over object = size";
else if(part==MarkPart::Radius) text=L"Drag to resize the number | Mouse wheel = diameter";
else if(part==MarkPart::Start || part==MarkPart::End) text=L"Drag handle to resize | Arrow endpoints can snap to numbered markers";
else if(part==MarkPart::Start || part==MarkPart::End) text=L"Drag handle to resize | Endpoints can snap to objects and other arrows";
else if(c.selected>=0) {
const auto &m=c.marks[c.selected];
if(c.selectedLabel || part==MarkPart::Label)
@@ -274,6 +276,7 @@ void finishInlineText(HWND w,Capture &c,bool commit)
}
if(!empty) c.textDefaults=c.textDraft;
}
if(commit) resolveScreenshotLinks(c.marks);
c.textIndex=-1; c.editingLabel=false;
InvalidateRect(w,nullptr,FALSE);
}
@@ -892,6 +895,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if(chooseTextFont(w,draft)) {
c->remember(c->marks); c->marks[c->selected]=draft;
c->textDefaults=draft.isLine()?screenshotArrowLabel(draft):draft;
resolveScreenshotLinks(c->marks);
}
} else chooseTextFont(w,c->textDefaults);
SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0;
@@ -945,7 +949,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
auto &mark=c->marks[c->selected];
int &value=mark.isText()?mark.fontSize:mark.thickness;
int next=std::clamp(value+(LOWORD(wp)==16?1:-1),mark.isText()?6:mark.isHighlight()?4:1,mark.isText()?200:mark.isHighlight()?100:30);
if(next!=value) { c->remember(c->marks); value=next; }
if(next!=value) { c->remember(c->marks); value=next; resolveScreenshotLinks(c->marks); }
}
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
if (LOWORD(wp)==12 || LOWORD(wp)==18)
@@ -1135,7 +1139,8 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (!c->editing) { finishSelection(w,*c); return 0; }
else if(c->part!=MarkPart::None)
{
if(c->part==MarkPart::Start || c->part==MarkPart::End)
if((c->part==MarkPart::Start || c->part==MarkPart::End) &&
(c->start.x!=c->end.x || c->start.y!=c->end.y))
{
auto &mark=c->marks[c->selected];
linkScreenshotEndpoint(mark,c->part==MarkPart::Start,c->end,c->marks,6);
@@ -1362,27 +1367,107 @@ ScreenshotMark cloneScreenshotMark(const ScreenshotMark &original,const std::vec
{
auto result=original;
result.fromNumber=result.toNumber=0;
result.id=0; result.fromLink={}; result.toLink={};
if(result.number) result.number=nextScreenshotNumber(marks);
auto bounds=markBounds(result);
LONG dx=bounds.right+16<width?16:-16;
LONG dy=bounds.bottom+16<height?16:-16;
return dragScreenshotMark(result,MarkPart::Body,{0,0},{dx,dy},width,height);
}
void linkScreenshotEndpoint(ScreenshotMark &arrow, bool start, POINT drop,
const std::vector<ScreenshotMark> &marks, double tolerance)
namespace {
void assignMarkIds(std::vector<ScreenshotMark> &marks)
{
auto &link=start?arrow.fromNumber:arrow.toNumber;
link=0;
if(arrow.shape!=ScreenshotShape::None || arrow.isText()) return;
for(auto it=marks.rbegin();it!=marks.rend();++it)
if(it->number && std::hypot(double(drop.x-it->from.x),double(drop.y-it->from.y))<=it->radius+tolerance)
{
link=it->number;
break;
static unsigned long long next=1;
for(const auto &m:marks) next=std::max(next,m.id+1);
std::unordered_set<unsigned long long> used;
for(auto &m:marks) {
if(!m.id || !used.insert(m.id).second) { m.id=next++; used.insert(m.id); }
}
}
POINT objectAnchor(const ScreenshotMark &m,const ScreenshotLink &link)
{
if(m.isText() || m.shape!=ScreenshotShape::None) {
auto r=m.isText()?plainTextBounds(m):region(m.from,m.to);
POINT p{LONG(std::lround(r.left+(r.right-r.left)*link.u)),LONG(std::lround(r.top+(r.bottom-r.top)*link.v))};
return m.isText()?rotateTextPoint(m,p):p;
}
size_t segments=m.points.size()>1?m.points.size()-1:1;
double along=std::clamp(link.u,0.0,1.0)*segments;
size_t i=std::min(size_t(along),segments-1); double t=along-i;
POINT a=m.points.size()>1?m.points[i]:m.from,b=m.points.size()>1?m.points[i+1]:m.to;
return {LONG(std::lround(a.x+(b.x-a.x)*t)),LONG(std::lround(a.y+(b.y-a.y)*t))};
}
bool findObjectAnchor(const ScreenshotMark &m,POINT drop,double tolerance,ScreenshotLink &link)
{
link={m.id,0,0};
if(m.isText() || m.shape!=ScreenshotShape::None) {
auto r=m.isText()?plainTextBounds(m):region(m.from,m.to);
POINT p=m.isText()?rotateTextPoint(m,drop,-1):drop;
double w=std::max(1L,r.right-r.left),h=std::max(1L,r.bottom-r.top);
double x=(p.x-r.left)/w,y=(p.y-r.top)/h;
bool inside=x>=0 && x<=1 && y>=0 && y<=1;
if(m.shape==ScreenshotShape::Circle || m.shape==ScreenshotShape::Ellipse) {
double dx=2*x-1,dy=2*y-1,length=std::hypot(dx,dy);
inside=length<=1;
if(length<0.001) { dx=1; dy=0; length=1; }
link.u=(1+dx/length)/2; link.v=(1+dy/length)/2;
} else {
link.u=std::clamp(x,0.0,1.0); link.v=std::clamp(y,0.0,1.0);
if(inside) {
double edges[]{x*w,(1-x)*w,y*h,(1-y)*h};
auto side=std::min_element(edges,edges+4)-edges;
if(side<2) link.u=side==0?0:1; else link.v=side==2?0:1;
}
}
auto anchor=objectAnchor(m,link);
return inside || std::hypot(double(drop.x-anchor.x),double(drop.y-anchor.y))<=tolerance;
}
double best=1e100;
size_t segments=m.points.size()>1?m.points.size()-1:1;
for(size_t i=0;i<segments;++i) {
POINT a=m.points.size()>1?m.points[i]:m.from,b=m.points.size()>1?m.points[i+1]:m.to;
double dx=b.x-a.x,dy=b.y-a.y,length=dx*dx+dy*dy;
double t=length?std::clamp(((drop.x-a.x)*dx+(drop.y-a.y)*dy)/length,0.0,1.0):0;
double distance=std::hypot(drop.x-a.x-t*dx,drop.y-a.y-t*dy);
if(distance<best) { best=distance; link.u=(i+t)/segments; }
}
return best<=tolerance+m.thickness/2.0;
}
}
void linkScreenshotEndpoint(ScreenshotMark &arrow, bool start, POINT drop,
std::vector<ScreenshotMark> &marks, double tolerance)
{
assignMarkIds(marks);
auto &number=start?arrow.fromNumber:arrow.toNumber;
auto &link=start?arrow.fromLink:arrow.toLink;
number=0; link={};
if(!arrow.isLine()) return;
// Number badges are drawn above other objects.
for(auto it=marks.rbegin();it!=marks.rend();++it)
if(it->number && std::hypot(double(drop.x-it->from.x),double(drop.y-it->from.y))<=it->radius+tolerance) {
number=it->number; return;
}
auto wouldCycle=[&](unsigned long long id) {
std::vector<unsigned long long> pending{id}; std::unordered_set<unsigned long long> seen;
while(!pending.empty()) {
auto current=pending.back(); pending.pop_back();
if(arrow.id && current==arrow.id) return true;
if(!current || !seen.insert(current).second) continue;
for(const auto &m:marks) if(m.id==current) {
pending.push_back(m.fromLink.target); pending.push_back(m.toLink.target); break;
}
}
return false;
};
for(auto it=marks.rbegin();it!=marks.rend();++it) {
if(it->number || &*it==&arrow || (arrow.id && it->id==arrow.id)) continue;
ScreenshotLink candidate;
if(findObjectAnchor(*it,drop,tolerance,candidate) && !wouldCycle(it->id)) { link=candidate; return; }
}
}
void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
{
assignMarkIds(marks);
auto badge=[&](int number) -> const ScreenshotMark * {
if(number) for(const auto &m:marks) if(m.number==number) return &m;
return nullptr;
@@ -1392,17 +1477,32 @@ void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
if(length<0.001) { dx=start?1:-1; dy=0; length=1; }
return {m.from.x+LONG(std::lround(m.radius*dx/length)),m.from.y+LONG(std::lround(m.radius*dy/length))};
};
for(auto &m:marks)
{
if(m.number || m.isHighlight() || m.isText() || m.shape!=ScreenshotShape::None) continue;
auto from=badge(m.fromNumber),to=badge(m.toNumber);
if(!from) m.fromNumber=0;
if(!to) m.toNumber=0;
POINT start=from?from->from:m.from, end=to?to->from:m.to;
if(from) m.from=rim(*from,end,true);
if(to) m.to=rim(*to,start,false);
if(m.freehand && !m.points.empty()) { m.points.front()=m.from; m.points.back()=m.to; }
}
std::vector<int> state(marks.size());
std::function<void(size_t)> resolve=[&](size_t index) {
if(state[index]) return;
state[index]=1;
auto &m=marks[index];
if(m.isLine()) {
auto attach=[&](ScreenshotLink &link,POINT &point) {
if(!link.target) return;
for(size_t i=0;i<marks.size();++i) if(marks[i].id==link.target) {
if(state[i]==1) { link={}; return; } // Reject cyclic dependencies.
resolve(i); point=objectAnchor(marks[i],link); return;
}
link={}; // Deleted targets leave endpoints at their last position.
};
attach(m.fromLink,m.from); attach(m.toLink,m.to);
auto from=badge(m.fromNumber),to=badge(m.toNumber);
if(!from) m.fromNumber=0;
if(!to) m.toNumber=0;
POINT start=from?from->from:m.from, end=to?to->from:m.to;
if(from) m.from=rim(*from,end,true);
if(to) m.to=rim(*to,start,false);
if(m.freehand && !m.points.empty()) { m.points.front()=m.from; m.points.back()=m.to; }
}
state[index]=2;
};
for(size_t i=0;i<marks.size();++i) resolve(i);
}
MarkPart hitScreenshotMark(const ScreenshotMark &m, POINT p, double tolerance)
{
@@ -1470,6 +1570,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
POINT position, int width, int height, bool snapRotation)
{
auto m=original;
if(position.x==origin.x && position.y==origin.y) return m;
if(part==MarkPart::Rotation && (m.isText() || (m.isLine() && !m.label.empty()))) {
if(position.x==origin.x && position.y==origin.y) return m;
auto text=m.isText()?m:screenshotArrowLabel(m);
@@ -1505,6 +1606,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
else if(part==MarkPart::Body)
{
m.fromNumber=m.toNumber=0;
m.fromLink={}; m.toLink={};
auto shift=[&](LONG delta,LONG first,LONG last,int extent) {
LONG low=-first,high=LONG(extent-1)-last;
if(low>high && m.isText()) {
@@ -1524,6 +1626,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
else if(part==MarkPart::Start || part==MarkPart::End)
{
(part==MarkPart::Start?m.fromNumber:m.toNumber)=0;
(part==MarkPart::Start?m.fromLink:m.toLink)={};
auto &endpoint=part==MarkPart::Start?m.from:m.to;
endpoint.x=std::clamp(endpoint.x+position.x-origin.x,0L,LONG(width-1));
endpoint.y=std::clamp(endpoint.y+position.y-origin.y,0L,LONG(height-1));