Release v0.6.24: smart shapes, rotation and canvas zoom
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
// Including the implementation lets this integration test drive private editor state directly.
|
||||
#include "../src/screenshot.cpp"
|
||||
#include <iostream>
|
||||
#include "stroke_improvement.h"
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||
try {
|
||||
testStrokeImprovement();
|
||||
if(argc>1 && std::string(argv[1])=="--save-only") {
|
||||
auto require=[](bool ok,const char *message){if(!ok) throw std::runtime_error(message);};
|
||||
auto root=std::filesystem::temp_directory_path()/(L"KSwitcher-save-"+std::to_wstring(GetCurrentProcessId()));
|
||||
@@ -81,6 +83,169 @@ int main(int argc, char **argv)
|
||||
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
||||
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
||||
"new screenshot editor starts with the pencil tool");
|
||||
{
|
||||
require(GetDlgItem(window,36)!=nullptr,"triangle has a dedicated toolbar button");
|
||||
SendMessageW(window,WM_KEYDOWN,'G',0);
|
||||
require(c.shape==ks::ScreenshotShape::Triangle && c.arrow && !c.freehand,"G selects triangle drawing tool");
|
||||
auto mouse=[&](UINT message,POINT p) {
|
||||
int x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
|
||||
int y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
|
||||
SendMessageW(window,message,message==WM_LBUTTONUP?0:MK_LBUTTON,MAKELPARAM(x,y));
|
||||
};
|
||||
mouse(WM_LBUTTONDOWN,{80,80}); mouse(WM_LBUTTONUP,{220,180});
|
||||
require(c.marks.size()==1 && c.marks[0].shape==ks::ScreenshotShape::Triangle && c.marks[0].points.size()==4,
|
||||
"triangle tool creates a closed three-vertex shape");
|
||||
auto triangle=c.marks[0];
|
||||
for(double angle:{0.0,37.0,90.0,-125.0}) for(int i=0;i<3;++i) {
|
||||
auto original=triangle; original.rotation=angle;
|
||||
auto vertices=ks::shapeOutline(original);
|
||||
auto part=ks::MarkPart(int(ks::MarkPart::Vertex0)+i);
|
||||
require(ks::hitScreenshotMark(original,vertices[i],4)==part,"each rotated triangle vertex has its own handle");
|
||||
POINT target{vertices[i].x+13,vertices[i].y-9};
|
||||
auto changed=ks::dragScreenshotMark(original,part,vertices[i],target,320,240);
|
||||
auto moved=ks::shapeOutline(changed);
|
||||
for(int j=0;j<3;++j) {
|
||||
auto expected=j==i?target:vertices[j];
|
||||
require(std::abs(moved[j].x-expected.x)<=2 && std::abs(moved[j].y-expected.y)<=2,
|
||||
"dragging one vertex preserves both other world vertices after rotation");
|
||||
}
|
||||
require(changed.points.front().x==changed.points.back().x && changed.points.front().y==changed.points.back().y,
|
||||
"triangle remains closed after editing any vertex");
|
||||
}
|
||||
auto vertices=ks::shapeOutline(triangle);
|
||||
mouse(WM_LBUTTONDOWN,vertices[0]);
|
||||
require(c.part==ks::MarkPart::Vertex0,"mouse selects apex vertex");
|
||||
mouse(WM_LBUTTONUP,{vertices[0].x+20,vertices[0].y+10});
|
||||
require(c.marks[0].points[0].x==triangle.points[0].x+20 && c.marks[0].points[1].x==triangle.points[1].x,
|
||||
"mouse vertex drag changes only requested corner");
|
||||
SendMessageW(window,WM_COMMAND,12,0);
|
||||
require(c.marks[0].points[0].x==triangle.points[0].x,"undo restores triangle vertex");
|
||||
SendMessageW(window,WM_COMMAND,18,0);
|
||||
require(c.marks[0].points[0].x==triangle.points[0].x+20,"redo reapplies triangle vertex edit");
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
require(GetDlgItem(window,37)!=nullptr,"trapezoid has a dedicated toolbar button");
|
||||
SendMessageW(window,WM_KEYDOWN,'B',0);
|
||||
require(c.shape==ks::ScreenshotShape::Trapezoid && c.arrow && !c.freehand,"B selects trapezoid tool");
|
||||
mouse(WM_LBUTTONDOWN,{60,70}); mouse(WM_LBUTTONUP,{240,180});
|
||||
require(c.marks.size()==1 && c.marks[0].shape==ks::ScreenshotShape::Trapezoid && c.marks[0].points.size()==5 && !c.marks[0].isHighlight(),
|
||||
"trapezoid tool creates a closed shape with unequal parallel bases");
|
||||
auto trapezoid=c.marks[0];
|
||||
require(trapezoid.points[0].y==trapezoid.points[1].y && trapezoid.points[2].y==trapezoid.points[3].y &&
|
||||
trapezoid.points[1].x-trapezoid.points[0].x<trapezoid.points[2].x-trapezoid.points[3].x,"toolbar trapezoid has parallel unequal bases");
|
||||
mouse(WM_LBUTTONDOWN,trapezoid.to); mouse(WM_LBUTTONUP,{260,200});
|
||||
require(c.marks[0].to.x==260 && c.marks[0].points[0].y==c.marks[0].points[1].y,"trapezoid resizes as a shape");
|
||||
auto grip=ks::screenshotShapeRotationHandle(c.marks[0]);
|
||||
auto rotated=ks::dragScreenshotMark(c.marks[0],ks::MarkPart::Rotation,grip,{280,135},320,240);
|
||||
require(rotated.rotation!=0 && rotated.shape==ks::ScreenshotShape::Trapezoid,"trapezoid supports shape rotation");
|
||||
SendMessageW(window,WM_COMMAND,12,0); require(c.marks[0].to.x==240,"undo trapezoid resize");
|
||||
SendMessageW(window,WM_COMMAND,12,0); require(c.marks.empty(),"undo trapezoid creation");
|
||||
SendMessageW(window,WM_COMMAND,18,0); require(c.marks[0].shape==ks::ScreenshotShape::Trapezoid,"redo trapezoid creation");
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
SendMessageW(window,WM_COMMAND,21,0);
|
||||
}
|
||||
{
|
||||
for(auto kind:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse,ks::ScreenshotShape::Triangle})
|
||||
for(bool recognized:{false,true}) {
|
||||
if(kind==ks::ScreenshotShape::Triangle && !recognized) continue;
|
||||
ks::Mark shape{{100,90},{200,170},0}; shape.shape=kind; shape.ends=ks::LineEnds::None;
|
||||
if(kind==ks::ScreenshotShape::Circle) shape.to.y=190;
|
||||
if(recognized) shape.points=kind==ks::ScreenshotShape::Triangle?
|
||||
std::vector<POINT>{{150,90},{200,170},{100,170},{150,90}}:ks::shapeOutline(shape);
|
||||
auto grip=ks::screenshotShapeRotationHandle(shape);
|
||||
POINT center{(shape.from.x+shape.to.x)/2,(shape.from.y+shape.to.y)/2};
|
||||
POINT right{center.x+center.y-grip.y,center.y};
|
||||
auto rotated=ks::dragScreenshotMark(shape,ks::MarkPart::Rotation,grip,right,320,240);
|
||||
require(std::abs(rotated.rotation-90)<.01 && rotated.from.x==shape.from.x && rotated.to.y==shape.to.y,
|
||||
"all native and recognized shapes rotate around their center");
|
||||
auto corner=ks::rotateShapePoint(rotated,rotated.to);
|
||||
require(ks::hitScreenshotMark(rotated,corner,4)==(kind==ks::ScreenshotShape::Triangle?ks::MarkPart::Vertex1:ks::MarkPart::End),"rotated resize handles remain hittable");
|
||||
auto fixed=ks::rotateShapePoint(rotated,rotated.from);
|
||||
auto resized=ks::dragScreenshotMark(rotated,ks::MarkPart::End,corner,{corner.x-15,corner.y+20},320,240);
|
||||
auto stillFixed=ks::rotateShapePoint(resized,resized.from);
|
||||
require(std::abs(stillFixed.x-fixed.x)<=1 && std::abs(stillFixed.y-fixed.y)<=1 && resized.rotation==90,
|
||||
"resizing rotated shapes keeps the opposite world corner fixed");
|
||||
auto snapped=ks::dragScreenshotMark(shape,ks::MarkPart::Rotation,grip,{center.x+60,center.y-20},320,240,true);
|
||||
require(std::abs(std::remainder(snapped.rotation,15))<.01,"Shift snaps shape rotation to 15 degrees");
|
||||
auto cloned=ks::cloneScreenshotMark(rotated,{rotated},320,240);
|
||||
require(cloned.rotation==rotated.rotation && cloned.shape==kind,"clone preserves shape rotation");
|
||||
auto boundary=ks::shapeOutline(shape).front();
|
||||
std::vector<ks::Mark> linked{shape,{{20,20},boundary,0}};
|
||||
ks::linkScreenshotEndpoint(linked[1],false,boundary,linked,4);
|
||||
require(linked[1].toLink.target!=0,"connector attaches to shape before rotation");
|
||||
linked[0].rotation=90; ks::resolveScreenshotLinks(linked);
|
||||
auto expected=ks::rotateShapePoint(linked[0],boundary);
|
||||
require(std::abs(linked[1].to.x-expected.x)<=1 && std::abs(linked[1].to.y-expected.y)<=1,
|
||||
"connector follows rotated shape boundary");
|
||||
}
|
||||
ks::Mark shape{{100,90},{200,170},0}; shape.shape=ks::ScreenshotShape::Rectangle;
|
||||
c.marks={shape}; c.selected=0;
|
||||
auto mouse=[&](UINT message,POINT p) {
|
||||
auto x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
|
||||
auto y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
|
||||
SendMessageW(window,message,message==WM_LBUTTONUP?0:MK_LBUTTON,MAKELPARAM(x,y));
|
||||
};
|
||||
auto grip=ks::screenshotShapeRotationHandle(shape,double(c.unit)*c.image->width/(c.view.right-c.view.left));
|
||||
require(ks::markAt(c,grip).second==ks::MarkPart::Rotation,"shape rotation grip is distinct from thickness grip");
|
||||
mouse(WM_LBUTTONDOWN,grip); mouse(WM_LBUTTONUP,{150+130-grip.y,130});
|
||||
require(std::abs(c.marks[0].rotation-90)<1,"mouse rotation updates selected shape");
|
||||
SendMessageW(window,WM_COMMAND,12,0); require(c.marks[0].rotation==0,"undo restores original shape orientation");
|
||||
SendMessageW(window,WM_COMMAND,18,0); require(std::abs(c.marks[0].rotation-90)<1,"redo restores shape rotation");
|
||||
c.selected=0;
|
||||
ks::Surface rotatedPixels(320,240); PatBlt(rotatedPixels.dc,0,0,320,240,WHITENESS);
|
||||
auto rendered=ks::decodeImage(ks::screenshotClip(rotatedPixels.dc,320,240,c.marks));
|
||||
auto pixel=(100*320+190)*4;
|
||||
require(rendered.bgra[pixel+2]==GetRValue(shape.color) && rendered.bgra[pixel+1]==GetGValue(shape.color),
|
||||
"export renders rotated shape at its new boundary");
|
||||
POINT center{(c.canvas.left+c.canvas.right)/2,(c.canvas.top+c.canvas.bottom)/2};
|
||||
ks::zoomCanvas(window,c,2,center);
|
||||
grip=ks::screenshotShapeRotationHandle(c.marks[0],double(c.unit)*c.image->width/(c.view.right-c.view.left));
|
||||
require(ks::markAt(c,grip).second==ks::MarkPart::Rotation,"rotation grip works on zoomed canvas");
|
||||
ks::zoomCanvas(window,c,1,center,true);
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
}
|
||||
{
|
||||
BYTE savedKeys[256]{},keys[256]{}; GetKeyboardState(savedKeys);
|
||||
std::copy(savedKeys,savedKeys+256,keys); keys[VK_CONTROL]=0x80; SetKeyboardState(keys);
|
||||
auto fit=c.view;
|
||||
SendMessageW(window,WM_KEYDOWN,VK_OEM_PLUS,0);
|
||||
require(c.zoom==1.25 && c.view.right-c.view.left>fit.right-fit.left,"Ctrl+plus zooms canvas");
|
||||
SendMessageW(window,WM_KEYDOWN,VK_SUBTRACT,0);
|
||||
require(std::abs(c.zoom-1)<.001,"Ctrl+numpad minus zooms out");
|
||||
SendMessageW(window,WM_KEYDOWN,VK_ADD,0);
|
||||
SendMessageW(window,WM_KEYDOWN,VK_OEM_MINUS,0);
|
||||
require(std::abs(c.zoom-1)<.001,"both keyboard plus/minus variants work");
|
||||
POINT center{(c.canvas.left+c.canvas.right)/2,(c.canvas.top+c.canvas.bottom)/2};
|
||||
ks::zoomCanvas(window,c,4,center);
|
||||
POINT anchor{center.x+12,center.y+8},screen=anchor; ClientToScreen(window,&screen);
|
||||
auto before=ks::imagePoint(c,anchor,false); double zoom=c.zoom;
|
||||
SendMessageW(window,WM_MOUSEWHEEL,MAKEWPARAM(MK_CONTROL,60),MAKELPARAM(screen.x,screen.y));
|
||||
require(c.zoom==zoom,"partial wheel delta waits for a full step");
|
||||
SendMessageW(window,WM_MOUSEWHEEL,MAKEWPARAM(MK_CONTROL,60),MAKELPARAM(screen.x,screen.y));
|
||||
auto after=ks::imagePoint(c,anchor,false);
|
||||
require(c.zoom>zoom && std::abs(before.x-after.x)<=1 && std::abs(before.y-after.y)<=1,
|
||||
"Ctrl+wheel zoom keeps image point under cursor");
|
||||
require(!c.history.canUndo(),"zoom does not add image edits to undo history");
|
||||
auto view=c.view;
|
||||
SendMessageW(window,WM_MBUTTONDOWN,MK_MBUTTON,MAKELPARAM(center.x,center.y));
|
||||
SendMessageW(window,WM_MOUSEMOVE,MK_MBUTTON,MAKELPARAM(center.x+15,center.y+10));
|
||||
SendMessageW(window,WM_MBUTTONUP,0,MAKELPARAM(center.x+15,center.y+10));
|
||||
require(c.view.left==view.left+15 && c.view.top==view.top+10 && !c.panning,"middle drag pans enlarged canvas");
|
||||
keys[VK_CONTROL]=0; SetKeyboardState(keys);
|
||||
auto a=ks::imagePoint(c,center),b=ks::imagePoint(c,{center.x+30,center.y+20});
|
||||
SendMessageW(window,WM_LBUTTONDOWN,MK_LBUTTON,MAKELPARAM(center.x,center.y));
|
||||
SendMessageW(window,WM_LBUTTONUP,0,MAKELPARAM(center.x+30,center.y+20));
|
||||
require(c.marks.size()==1 && c.marks[0].from.x==a.x && c.marks[0].to.x==b.x && c.marks[0].to.y==b.y,
|
||||
"drawing after zoom and pan uses image coordinates");
|
||||
auto original=c.marks[0];
|
||||
keys[VK_CONTROL]=0x80; SetKeyboardState(keys);
|
||||
SendMessageW(window,WM_KEYDOWN,'0',0);
|
||||
require(c.zoom==1 && c.panX==0 && c.panY==0 && c.marks[0].from.x==original.from.x && c.marks[0].to.x==original.to.x,
|
||||
"Ctrl+0 resets view without transforming annotations");
|
||||
ks::zoomCanvas(window,c,1e6,center); require(c.zoom==16,"zoom has upper bound");
|
||||
ks::zoomCanvas(window,c,1e-6,center); require(c.zoom==.1,"zoom has lower bound");
|
||||
SendMessageW(window,WM_KEYDOWN,'0',0); SetKeyboardState(savedKeys);
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
}
|
||||
{
|
||||
BYTE savedKeys[256]{},keys[256]{}; GetKeyboardState(savedKeys);
|
||||
std::copy(savedKeys,savedKeys+256,keys); keys[VK_SHIFT]=0; SetKeyboardState(keys);
|
||||
@@ -89,6 +254,51 @@ int main(int argc, char **argv)
|
||||
int y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
|
||||
SendMessageW(window,message,(message==WM_LBUTTONUP?0:MK_LBUTTON)|(shift?MK_SHIFT:0),MAKELPARAM(x,y));
|
||||
};
|
||||
mouse(WM_LBUTTONDOWN,{30,190});
|
||||
for(int x=35;x<=200;x+=5) mouse(WM_MOUSEMOVE,{x,190+(x%10?2:-2)});
|
||||
auto original=c.stroke.points;
|
||||
mouse(WM_LBUTTONUP,c.stroke.to);
|
||||
require(c.marks.size()==1,"improved stroke is one object");
|
||||
auto improved=c.marks.back().points;
|
||||
SendMessageW(window,WM_COMMAND,12,0);
|
||||
require(c.marks.size()==1 && c.marks.back().points.size()==original.size() &&
|
||||
std::equal(original.begin(),original.end(),c.marks.back().points.begin(),
|
||||
[](POINT a,POINT b){return a.x==b.x && a.y==b.y;}),"first undo restores exact raw stroke");
|
||||
SendMessageW(window,WM_COMMAND,12,0);
|
||||
require(c.marks.empty(),"second undo removes stroke");
|
||||
SendMessageW(window,WM_COMMAND,18,0); SendMessageW(window,WM_COMMAND,18,0);
|
||||
require(c.marks.size()==1 && c.marks.back().points.size()==improved.size(),"redo restores improvement");
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
mouse(WM_LBUTTONDOWN,{180,120});
|
||||
for(int i=1;i<=120;++i) {
|
||||
double a=i*2*3.141592653589793/120;
|
||||
mouse(WM_MOUSEMOVE,{LONG(std::lround(130+50*std::cos(a))),LONG(std::lround(120+50*std::sin(a)))});
|
||||
}
|
||||
mouse(WM_LBUTTONUP,c.stroke.to);
|
||||
require(c.marks.size()==1 && c.marks[0].shape==ks::ScreenshotShape::Circle &&
|
||||
!c.marks[0].freehand && !c.marks[0].isHighlight(),"recognized circle is a shape, not a pencil or highlight");
|
||||
auto circle=c.marks[0];
|
||||
require(ks::hitScreenshotMark(circle,circle.to,6)==ks::MarkPart::End,"recognized shape exposes bounding-box resize handle");
|
||||
mouse(WM_LBUTTONDOWN,circle.to);
|
||||
mouse(WM_MOUSEMOVE,{circle.to.x+30,circle.to.y+20});
|
||||
mouse(WM_LBUTTONUP,{circle.to.x+30,circle.to.y+20});
|
||||
require(c.marks[0].to.x>circle.to.x && c.marks[0].to.x-c.marks[0].from.x==c.marks[0].to.y-c.marks[0].from.y &&
|
||||
c.marks[0].points[0].x>circle.points[0].x,"dragging recognized circle handle scales the whole circle proportionally");
|
||||
SendMessageW(window,WM_COMMAND,12,0);
|
||||
require(c.marks[0].shape==ks::ScreenshotShape::Circle && c.marks[0].to.x==circle.to.x,"undo restores shape size");
|
||||
SendMessageW(window,WM_COMMAND,12,0);
|
||||
require(c.marks[0].freehand && c.marks[0].shape==ks::ScreenshotShape::None,"undo recognition restores editable pencil stroke");
|
||||
SendMessageW(window,WM_COMMAND,18,0);
|
||||
require(c.marks[0].shape==ks::ScreenshotShape::Circle,"redo restores shape type");
|
||||
for(auto kind:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Ellipse,ks::ScreenshotShape::Triangle}) {
|
||||
auto shape=circle; shape.shape=kind;
|
||||
auto resized=ks::dragScreenshotMark(shape,ks::MarkPart::End,shape.to,{shape.to.x+50,shape.to.y+20},320,240);
|
||||
require(resized.points[0].x!=shape.points[0].x && resized.points[24].y!=shape.points[24].y,
|
||||
"shape resize transforms all outline points in both axes");
|
||||
auto moved=ks::cloneScreenshotMark(resized,{resized},320,240);
|
||||
require(moved.shape==kind && !moved.isHighlight(),"clone preserves recognized shape type");
|
||||
}
|
||||
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||
mouse(WM_LBUTTONDOWN,{40,40},true);
|
||||
mouse(WM_MOUSEMOVE,{100,50},true);
|
||||
require(c.stroke.to.x==100 && c.stroke.to.y==40,"Shift constrains pencil horizontally");
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
inline void testStrokeImprovement() {
|
||||
auto require=[](bool ok,const char *message){if(!ok) throw std::runtime_error(message);};
|
||||
auto path=[](std::vector<POINT> corners) {
|
||||
std::vector<POINT> p;
|
||||
for(size_t i=1;i<corners.size();++i) for(int j=0;j<25;++j) {
|
||||
double t=j/25.0;
|
||||
p.push_back({LONG(std::lround(corners[i-1].x*(1-t)+corners[i].x*t)),
|
||||
LONG(std::lround(corners[i-1].y*(1-t)+corners[i].y*t))});
|
||||
}
|
||||
p.push_back(corners.back()); return p;
|
||||
};
|
||||
for(bool tilted:{false,true}) {
|
||||
auto p=path({{40,40},{240,40},{240,140},{40,140},{40,40}});
|
||||
if(tilted) for(auto &q:p) {double x=q.x,y=q.y;q={LONG(std::lround(x*.866-y*.5+100)),LONG(std::lround(x*.5+y*.866))};}
|
||||
for(size_t i=1;i+1<p.size();++i) p[i].y+=int(i%3)-1;
|
||||
auto result=ks::improveScreenshotStroke(p);
|
||||
require(result.size()==5,"recognize noisy and tilted rectangles");
|
||||
double ax=result[1].x-result[0].x,ay=result[1].y-result[0].y;
|
||||
double bx=result[2].x-result[1].x,by=result[2].y-result[1].y;
|
||||
require(std::abs(ax*bx+ay*by)<300,"rectangle has perpendicular sides");
|
||||
}
|
||||
auto triangle=ks::improveScreenshotStroke(path({{130,30},{240,180},{30,180},{130,30}}));
|
||||
for(auto corners:std::vector<std::vector<POINT>>{
|
||||
{{435,172},{455,135},{481,76},{514,78},{567,78},{605,76},{631,103},{652,135},{681,171},{609,172},{539,174},{435,172}},
|
||||
{{531,311},{553,280},{582,235},{616,236},{672,233},{686,264},{693,287},{710,318},{640,314},{531,311}},
|
||||
{{70,40},{160,40},{220,150},{20,150},{70,40}}})
|
||||
for(double angle:{0.0,.6,2.2}) for(bool reverse:{false,true}) {
|
||||
auto p=path(corners);
|
||||
for(auto &q:p) { double x=q.x,y=q.y; q={LONG(std::lround(x*std::cos(angle)-y*std::sin(angle)+700)),
|
||||
LONG(std::lround(x*std::sin(angle)+y*std::cos(angle)+700))}; }
|
||||
if(reverse) std::reverse(p.begin(),p.end());
|
||||
ks::ScreenshotShape kind{}; auto result=ks::improveScreenshotStroke(p,&kind);
|
||||
require(kind==ks::ScreenshotShape::Trapezoid && result.size()==5,"recognize hand-drawn and tilted trapezoids");
|
||||
double ax=result[1].x-result[0].x,ay=result[1].y-result[0].y;
|
||||
double bx=result[3].x-result[2].x,by=result[3].y-result[2].y;
|
||||
require(std::abs(ax*by-ay*bx)/(std::hypot(ax,ay)*std::hypot(bx,by))<.025,"recognized trapezoid bases are parallel");
|
||||
}
|
||||
// Approximate the reported skewed square, including its small closing gap.
|
||||
auto unevenBox=path({{662,297},{663,213},{659,199},{659,181},{691,181},{718,189},
|
||||
{753,195},{786,195},{775,217},{771,249},{771,289},{766,313},
|
||||
{745,311},{716,304},{689,300},{671,300}});
|
||||
for(double factor:{.5,1.0,2.5}) for(bool reverse:{false,true}) {
|
||||
auto p=unevenBox;
|
||||
for(auto &q:p) {q.x=LONG(std::lround((q.x-650)*factor+50));q.y=LONG(std::lround((q.y-170)*factor+50));}
|
||||
if(reverse) std::reverse(p.begin(),p.end());
|
||||
ks::ScreenshotShape kind{};
|
||||
auto result=ks::improveScreenshotStroke(p,&kind);
|
||||
require(kind==ks::ScreenshotShape::Rectangle && result.size()==5,"uneven square must not become a circle");
|
||||
}
|
||||
require(triangle.size()==4,"recognize triangle");
|
||||
for(double ry:{50.0,90.0}) {
|
||||
std::vector<POINT> p;
|
||||
for(int i=0;i<=120;++i) {double a=i*2*3.141592653589793/120;
|
||||
p.push_back({LONG(std::lround(150+90*std::cos(a))),LONG(std::lround(150+ry*std::sin(a)))});}
|
||||
auto result=ks::improveScreenshotStroke(p);
|
||||
require(result.size()==97 && result.front().x==result.back().x && result.front().y==result.back().y,
|
||||
"recognize ellipse and circle as closed outlines");
|
||||
}
|
||||
auto open=path({{20,20},{200,20},{200,150},{60,150}});
|
||||
// Hand-traced approximations of the uneven loops in the reported screenshot.
|
||||
for(auto corners:std::vector<std::vector<POINT>>{
|
||||
{{424,408},{449,407},{474,418},{487,439},{488,461},{475,485},{453,488},{432,479},{412,458},{410,435},{416,418},{431,409}},
|
||||
{{352,474},{373,482},{389,501},{392,518},{378,529},{351,531},{331,519},{324,504},{326,486},{339,477},{352,474}},
|
||||
{{246,236},{221,242},{192,256},{166,271},{151,289},{154,303},{172,316},{195,315},{226,305},{246,291},{250,272},{252,252},{246,236}}}) {
|
||||
auto result=ks::improveScreenshotStroke(path(corners));
|
||||
require(result.size()==97,"recognize uneven hand-drawn loops from screenshot");
|
||||
}
|
||||
for(double size:{35.0,100.0,300.0}) for(double aspect:{.5,1.0}) for(int direction:{-1,1}) {
|
||||
std::vector<POINT> p;
|
||||
for(int i=0;i<=120;++i) {
|
||||
double a=direction*(.3+i*5.85/120),r=1+.07*std::sin(3*a)+.035*std::cos(7*a);
|
||||
double x=size*r*std::cos(a),y=size*aspect*r*std::sin(a);
|
||||
p.push_back({LONG(std::lround(500+x*.866-y*.5)),LONG(std::lround(500+x*.5+y*.866))});
|
||||
}
|
||||
require(ks::improveScreenshotStroke(p).size()==97,"recognize wobbly tilted ovals with closing gaps at different sizes");
|
||||
}
|
||||
for(double turns:{.72,1.8}) {
|
||||
std::vector<POINT> p;
|
||||
for(int i=0;i<=120;++i) {double a=i*turns*2*3.141592653589793/120;
|
||||
p.push_back({LONG(std::lround(150+90*std::cos(a))),LONG(std::lround(150+70*std::sin(a)))});}
|
||||
auto result=ks::improveScreenshotStroke(p);
|
||||
require(result.size()!=97,"do not close broad arcs or repeated loops into ovals");
|
||||
}
|
||||
auto smoothed=ks::improveScreenshotStroke(open);
|
||||
require(smoothed.size()>5 && smoothed.front().x==20 && smoothed.back().x==60,"open outline stays open");
|
||||
auto scribble=path({{20,20},{200,150},{200,20},{20,150},{20,20}});
|
||||
require(ks::improveScreenshotStroke(scribble).size()>97,"crossed scribble is not a shape");
|
||||
require(ks::improveScreenshotStroke({{1,1},{1,1},{1,1},{1,1},{1,1}}).empty(),"stationary stroke unchanged");
|
||||
std::vector<POINT> jitter;
|
||||
for(int i=0;i<=100;++i) jitter.push_back({20+i*2,100+(i%2?2:-2)});
|
||||
auto smooth=ks::improveScreenshotStroke(jitter);
|
||||
double deviation=0; for(auto p:smooth) deviation+=std::abs(p.y-100);
|
||||
require(deviation/smooth.size()<1.5 && smooth.front().y==jitter.front().y && smooth.back().y==jitter.back().y,
|
||||
"smoothing reduces jitter and preserves endpoints");
|
||||
}
|
||||
Reference in New Issue
Block a user