Release v0.6.5: preserve caption links and connect arrows to other objects
This commit is contained in:
+127
-24
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user