diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a7de56..23cc56b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.24) -project(KSwitcher VERSION 0.6.24 LANGUAGES CXX RC) +project(KSwitcher VERSION 0.6.25 LANGUAGES CXX RC) if(NOT WIN32) message(FATAL_ERROR "KSwitcher uses native Windows APIs") endif() diff --git a/README.md b/README.md index 042f439..179af16 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,10 @@ actual image scale. Zoom and pan do not change annotations, export resolution or The wheel without Ctrl still adjusts the object under the cursor. **Freehand line (F)** draws an opaque curve by following the mouse. The curve is one object: -after mouse release, light smoothing reduces jitter. Confidently recognized closed outlines become +after mouse release, nearly straight strokes become editable straight lines at their drawn angle, +preserving endpoints, color, thickness, line style and arrowheads. Short strokes, broad curves and +retraced strokes are not straightened. Other strokes receive light smoothing to reduce jitter. +Confidently recognized closed outlines become rectangles, circles, ellipses, triangles or trapezoids (including tilted outlines), stored as shape objects. Drag either bounding-box corner handle to resize the entire recognized outline; circles keep equal width and height. Shapes can also be moved, recolored and cloned. diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index 961d645..9e8cd08 100644 Binary files a/Release/KSwitcher-win-x64.zip and b/Release/KSwitcher-win-x64.zip differ diff --git a/Release/README.md b/Release/README.md index 0f871d6..92e851c 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,20 +1,17 @@ # Latest Windows release -## v0.6.24 — 2026-11-01 +## v0.6.25 — 2026-11-01 ## Changes -- Smooth pencil strokes and recognize rectangles, circles, ellipses, triangles and trapezoids, including tilted contours. Ctrl+Z restores the original stroke; Ctrl+Y reapplies the improvement. -- Recognized outlines become editable shapes. Add Triangle (G) and Trapezoid (B) toolbar tools; triangles have three independently draggable vertices. -- Rotate all shapes with their round handle, with Shift snapping to 15-degree steps. Resizing, cloning, connector attachments and PNG export follow shape rotation. -- Zoom with Ctrl++ / Ctrl+- or Ctrl+mouse wheel, reset with Ctrl+0 and pan with middle-button dragging. Zooming does not change image resolution or undo history. +- Pencil (F) automatically recognizes nearly straight strokes on mouse release and turns them into editable straight lines at the drawn angle. +- Preserve endpoints, color, thickness, line style and arrowheads. Ctrl+Z restores the exact original stroke; a second undo removes it, and Ctrl+Y reapplies recognition. +- Reject short gestures, noticeable curves, spikes and substantial backtracking. Existing shape recognition, smoothing, linked strokes and Shift constraints retain their behavior. ## Validation -423 native regressions, 473 input/UI checks, shared wheel handling, screenshot editor and shape/zoom regressions, PNG save regressions, notification registration, 48 updater checks and updater progress checks passed. +423 native regressions, 473 input/UI checks, line/shape recognition and screenshot editor regressions, PNG save regressions, shared wheel handling, notification registration, 48 updater checks and updater progress checks. The user also confirmed the feature in the development editor. ## Limitations -Shape recognition is heuristic: ambiguous outlines may remain smoothed strokes or require Ctrl+Z to restore the original. Small, linked and Shift-constrained pencil strokes are not automatically improved. Triangle vertices are edited independently; trapezoids use bounding-box resize handles. Canvas zoom is limited to 0.1×–16× the initial fit. PNG exports are flattened images, not editable projects. - -Existing browser-selection verification and clipboard/accessibility limitations remain unchanged. The bundled Ukrainian dictionary includes `поточні`; updates preserve existing dictionaries. If missing, add it under Settings → Auto-correction options → My dictionary → Ukrainian, then OK → Save. +Recognition is heuristic and conservative. Endpoints must be at least 24 source-image pixels apart; uncertain strokes may remain freehand. Use Straight line (L) when an exact line is needed. Linked strokes and strokes drawn partly with Shift are not automatically straightened. Existing browser-selection and clipboard/accessibility limitations remain unchanged. **Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation. For manual installation, exit KSwitcher, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries. diff --git a/Release/SHA256SUMS.txt b/Release/SHA256SUMS.txt index 3840546..2c016be 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -5976c1363e4342830f503cba6d17b3ec1386741f636932117d9b08f55004af92 KSwitcher-win-x64.zip +69efef3571e2836875fc36dbd42a62d642471e7cd275343959dfb575e5848bd5 KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index 3e40bdc..61e0b26 100644 --- a/resources/app.rc +++ b/resources/app.rc @@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur" 1 RT_MANIFEST "app.manifest" 201 RCDATA "update.ps1" 1 VERSIONINFO - FILEVERSION 0,6,24,0 - PRODUCTVERSION 0,6,24,0 + FILEVERSION 0,6,25,0 + PRODUCTVERSION 0,6,25,0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN @@ -20,7 +20,7 @@ BEGIN BEGIN VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility" VALUE "ProductName", "KSwitcher" - VALUE "FileVersion", "0.6.24" + VALUE "FileVersion", "0.6.25" END END BLOCK "VarFileInfo" diff --git a/src/screenshot.cpp b/src/screenshot.cpp index 98d9148..5a8129c 100644 --- a/src/screenshot.cpp +++ b/src/screenshot.cpp @@ -1537,12 +1537,14 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) !c->marks.back().fromNumber && !c->marks.back().toNumber && !c->marks.back().fromLink.target && !c->marks.back().toLink.target) { ScreenshotShape recognized{}; - auto improved=improveScreenshotStroke(c->marks.back().points,&recognized); + bool straightLine=false; + auto improved=improveScreenshotStroke(c->marks.back().points,&recognized,&straightLine); if(!improved.empty()) { c->remember(c->marks); auto &mark=c->marks.back(); mark.points=std::move(improved); mark.from=mark.points.front(); mark.to=mark.points.back(); + if(straightLine) { mark.points.clear(); mark.freehand=false; } if(recognized!=ScreenshotShape::None) { mark.shape=recognized; mark.freehand=false; mark.ends=LineEnds::None; auto bounds=markBounds(mark); diff --git a/src/stroke_improvement.h b/src/stroke_improvement.h index f304505..ca81b3b 100644 --- a/src/stroke_improvement.h +++ b/src/stroke_improvement.h @@ -40,14 +40,36 @@ inline double cross(Point a, Point b, Point c) { return (b.x-a.x)*(c.y-a.y)-(b.y namespace ks { // Returns a replacement path; an empty result means no visible improvement. // Report recognized geometry separately from a smoothed freehand path. -inline std::vector improveScreenshotStroke(const std::vector &input, ScreenshotShape *shape=nullptr) { +inline std::vector improveScreenshotStroke(const std::vector &input, ScreenshotShape *shape=nullptr, + bool *straightLine=nullptr) { if(shape) *shape=ScreenshotShape::None; + if(straightLine) *straightLine=false; using namespace stroke_detail; - if(input.size()<5) return {}; + if(input.size()<3) return {}; std::vector raw; for(auto p:input) if(raw.empty() || distance(raw.back(),{double(p.x),double(p.y)})>0) raw.push_back({double(p.x),double(p.y)}); double total=length(raw); + if(raw.size()>=3) { + double span=distance(raw.front(),raw.back()); + if(span>=24 && total<=span*1.35) { + double tolerance=std::clamp(span*.03,2.0,6.0),maximum=0,backtracking=0,previous=0; + Point a=raw.front(),b=raw.back(); + // Check every original point so a short excursion cannot disappear in resampling. + for(auto p:raw) { + maximum=std::max(maximum,segmentDistance(p,a,b)); + double along=((p.x-a.x)*(b.x-a.x)+(p.y-a.y)*(b.y-a.y))/span; + backtracking+=std::max(0.0,previous-along); previous=along; + } + double squared=0; + for(auto p:sample(raw,64)) { double d=segmentDistance(p,a,b); squared+=d*d; } + if(maximum<=tolerance && std::sqrt(squared/64)<=tolerance*.7 && + backtracking<=std::max(2.0,span*.02)) { + if(straightLine) *straightLine=true; + return {input.front(),input.back()}; + } + } + } if(raw.size()<5 || total<16) return {}; auto points=sample(raw,128); double left=points[0].x,right=left,top=points[0].y,bottom=top; diff --git a/src/update.h b/src/update.h index 09b4e2f..78b0b19 100644 --- a/src/update.h +++ b/src/update.h @@ -2,7 +2,7 @@ #include "platform.h" namespace ks { -inline constexpr wchar_t AppVersion[] = L"0.6.24"; +inline constexpr wchar_t AppVersion[] = L"0.6.25"; inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; diff --git a/tests/screenshot_ui.cpp b/tests/screenshot_ui.cpp index 4c4d8c8..9374947 100644 --- a/tests/screenshot_ui.cpp +++ b/tests/screenshot_ui.cpp @@ -259,15 +259,21 @@ int main(int argc, char **argv) auto original=c.stroke.points; mouse(WM_LBUTTONUP,c.stroke.to); require(c.marks.size()==1,"improved stroke is one object"); + require(!c.marks[0].freehand && c.marks[0].points.empty() && + c.marks[0].shape==ks::ScreenshotShape::None && !c.marks[0].isHighlight(), + "near-straight pencil stroke becomes an editable line on mouse release"); + require(c.marks[0].color==c.currentColor && c.marks[0].thickness==3 && + c.marks[0].style==c.lineStyle && c.marks[0].ends==c.freehandEnds, + "line recognition preserves drawing style"); auto improved=c.marks.back().points; SendMessageW(window,WM_COMMAND,12,0); - require(c.marks.size()==1 && c.marks.back().points.size()==original.size() && + require(c.marks.size()==1 && c.marks.back().freehand && 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"); + require(c.marks.size()==1 && !c.marks.back().freehand && 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) { diff --git a/tests/stroke_improvement.h b/tests/stroke_improvement.h index 0883c4d..aa06ee7 100644 --- a/tests/stroke_improvement.h +++ b/tests/stroke_improvement.h @@ -1,6 +1,35 @@ #pragma once inline void testStrokeImprovement() { auto require=[](bool ok,const char *message){if(!ok) throw std::runtime_error(message);}; + for(double angle:{0.0,.37,1.57079632679,2.8}) for(bool reverse:{false,true}) { + std::vector p; + for(int i=0;i<=20;++i) { + double x=i*10,y=i==0 || i==20?0:(i%3)-1; + p.push_back({LONG(std::lround(300+x*std::cos(angle)-y*std::sin(angle))), + LONG(std::lround(300+x*std::sin(angle)+y*std::cos(angle)))}); + } + if(reverse) std::reverse(p.begin(),p.end()); + bool line=false; ks::ScreenshotShape shape{}; + auto result=ks::improveScreenshotStroke(p,&shape,&line); + require(line && shape==ks::ScreenshotShape::None && result.size()==2 && + result.front().x==p.front().x && result.front().y==p.front().y && + result.back().x==p.back().x && result.back().y==p.back().y, + "recognize noisy lines at arbitrary angles and preserve directed endpoints"); + } + for(auto p:std::vector>{ + {{0,0},{50,1},{100,0}}, {{0,0},{0,0},{50,0},{100,0}}}) { + bool line=false; + require(ks::improveScreenshotStroke(p,nullptr,&line).size()==2 && line, + "recognize sparsely sampled lines and repeated mouse positions"); + } + for(auto p:std::vector>{ + {{0,0},{8,1},{16,0}}, {{0,0},{50,0},{40,0},{80,0},{100,0}}, + {{0,0},{25,8},{50,12},{75,8},{100,0}}, + {{0,0},{50,0},{51,10},{52,0},{100,0}}, {{0,0},{0,0},{0,0}}}) { + bool line=true; + ks::improveScreenshotStroke(p,nullptr,&line); + require(!line,"do not straighten short strokes, retracing, curves, spikes or stationary input"); + } auto path=[](std::vector corners) { std::vector p; for(size_t i=1;i