Release v0.6.25: recognize pencil strokes as straight lines
This commit is contained in:
+1
-1
@@ -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()
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
5976c1363e4342830f503cba6d17b3ec1386741f636932117d9b08f55004af92 KSwitcher-win-x64.zip
|
||||
69efef3571e2836875fc36dbd42a62d642471e7cd275343959dfb575e5848bd5 KSwitcher-win-x64.zip
|
||||
|
||||
+3
-3
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user