Release v0.6.25: recognize pencil strokes as straight lines

This commit is contained in:
2026-11-01 23:00:59 +02:00
parent 20f3b72287
commit 908985d775
11 changed files with 80 additions and 21 deletions
+1 -1
View File
@@ -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()
+4 -1
View File
@@ -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.
Binary file not shown.
+6 -9
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
5976c1363e4342830f503cba6d17b3ec1386741f636932117d9b08f55004af92 KSwitcher-win-x64.zip
69efef3571e2836875fc36dbd42a62d642471e7cd275343959dfb575e5848bd5 KSwitcher-win-x64.zip
+3 -3
View File
@@ -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"
+3 -1
View File
@@ -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);
+24 -2
View File
@@ -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<POINT> improveScreenshotStroke(const std::vector<POINT> &input, ScreenshotShape *shape=nullptr) {
inline std::vector<POINT> improveScreenshotStroke(const std::vector<POINT> &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<Point> 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;
+1 -1
View File
@@ -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;
+8 -2
View File
@@ -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) {
+29
View File
@@ -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<POINT> 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<std::vector<POINT>>{
{{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<std::vector<POINT>>{
{{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<POINT> corners) {
std::vector<POINT> p;
for(size_t i=1;i<corners.size();++i) for(int j=0;j<25;++j) {