Skip to content

chore(release): v1.7.0-rc2 - #87

Open
ca1e wants to merge 177 commits into
mainfrom
dev
Open

ca1e wants to merge 177 commits into
mainfrom
dev

Conversation

@ca1e

@ca1e ca1e commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

发布版本

v1.7.0-rc2

变更内容

  • 跨平台GUI更新
  • 问题修复

验证

  • CI 通过
  • 预发环境验证通过
  • 核心流程回归通过

注意事项

  • 无

关联 Issue / PR

发布检查清单

  • 版本号已更新
  • CHANGELOG 已更新
  • [ x] CI 全部通过
  • [ x] 无未合并的阻塞 PR

zhantss and others added 30 commits September 20, 2026 14:32
ControllerService hardcoded SdlKeyMappingDefaults.Create() and never read keymapping.json, so edits in the key-mapping window never took effect. The runtime now resolves the effective config through an in-memory KeyMappingStore (legacy WinForms-VK files fall back to SDL defaults) and hot-reloads the active binder on save. Also unifies the duplicated SDL scancode tables into SdlScancodeMap (fixes digits/F-keys/numpad being dropped on the virtual-pad overlay path) and swaps the key map atomically to avoid a data race with the SDL event thread.
…ion pattern

Make IConnection public and add a protected virtual NintendoSwitch.CreateConnection seam so tests can inject a fake connection. New EasyCon.SDLInput.Tests drives the real SdlKeyboardInputBinder.HandleKeyEvent path and asserts the HID packet dequeued to the fake serial port: default/numpad/stick mappings, unbound/disabled/Esc cases, golden bytes, plus config cache and legacy-fallback resolution. The pattern is documented in AGENTS.md for reuse by other modules.
The key-mapping mockup is working reference material, not a shipped asset. Move docs/design to a local .design folder and ignore it so it cannot be committed by accident.
Avalonia.Headless renders the mapping window off-screen so its layout and pixels can be asserted in tests, and Svg.Controls.Skia.Avalonia draws the controller asset. Versions are declared in Directory.Packages.props per central package management.
The mapping canvas needs over a hundred keycap and button glyphs. Keeping them as individual XAML resources does not scale, so the path data now lives in Resources/Icons.json and a small IconView control draws it, scaling a 64x64 design canvas into its bounds and caching parsed geometry. This replaces the previous per-icon SVG resource folder with a single embedded asset.
ThemeManager now publishes the Mapping colour and brush pairs (background, accent, title, muted, unmapped, highlight, conflict) so the canvas follows the active colour scheme. The accent stays one fixed muted steel blue in every scheme, and the conflict red is shared for the same reason: the controller art should read identically regardless of theme.
Esc and Backspace now clear a binding instead of being bound themselves, and OS-reserved keys (Win, Apps, PrintScreen, Pause, CapsLock, NumLock, ScrollLock) are refused. Those either never reach the application or leave the keyboard in a changed global state, so binding them is a trap. The rule is a policy layer on top of the existing map, so FromAvaloniaKey is untouched and existing configs still display correctly.
The controller SVG (34 KB, about 50 symbols) and Icons.json (135 KB) were parsed lazily the first time the key mapping window opened, which showed up as a stall. Both are now warmed once the main window is up: the JSON parse on a background thread, the Skia build on the UI thread after the first frame so startup itself is not slowed. The window reuses one shared SvgSource instead of re-parsing per instance.
…as view model

The window moved from a list to a fixed 1440x960 design canvas, so a row is now an observable KeyMappingRow carrying its canvas coordinates plus listening, highlight and conflict state, and the window view model owns the row set, key capture and conflict recomputation. Conflicts are accepted and marked rather than blocked: refusing a duplicate interrupts editing, and the red marker already makes the clash obvious.
Replaces the old list UI with the redesigned canvas: an NS2 controller SVG with live hit testing, leader lines out to 26 keycap and Switch icon pairs, and a legend. Controller groups grey out when unbound and highlight while listening by injecting CSS into the SVG, and conflicting bindings render red on both the keycap and the Switch icon. A smoke test guards that the controller asset still parses and renders. Also pins the status bar to a minimum height: it sat in an Auto row, so an emoji in the status text raised its line box by 2px, which rescaled the Viewbox and visibly shifted every row.
Adds a headless Avalonia test project that renders the mapping window in all three colour schemes to PNG for visual inspection, and locks the behaviours built above: conflicts are accepted and flagged on both rows, the highlight clears once a binding is applied, Esc and Backspace clear, reserved keys are refused while listening stays active, and the status bar can no longer resize the canvas.
…proj

insert_final_newline=false in .editorconfig only covers *.cs, so project files keep their trailing newline like the other eleven csproj files. The newline was removed incidentally while embedding the icon and controller SVG resources.
…closed shape

The shoulder/trigger arcs (l-zl, r-zr) are open paths (M + A, no z) but carried the ec-shell class, which sets fill. SVG fills an open path as if it were implicitly closed, so the arc and its chord enclosed a 5% opacity lens: invisible on the dark scheme, a dirty pale wedge across the shoulder area of both Joy-Cons on the light scheme. The rule that was meant to prevent this (.ec-shoulders path { fill: none; }) is in the stylesheet but referenced by nothing after these arcs became ec-shell so they could act as the colour and hit-test targets for the L/ZL and R/ZR rows. An id-selector rule now clears the fill on those two arcs only, leaving the closed body shells filled as intended.
… Skia PNG crash

Both screenshot tests now carry [Explicit] + [Category(Manual)] and SaveFrame encodes the frame with SkiaSharp directly instead of WriteableBitmap.Save. That call goes through Avalonia.Skia.ImageSavingHelper to the SkiaSharp native PNG encoder and crashes the test host natively at random (Fatal error at sk_pngencoder_encode), which showed up as runs that silently executed fewer tests and exited non-zero; ci.yml runs ci\test.bat, so it would have turned a dev -> main PR red intermittently. A six-run control with the old call also passed, so the crash rate is low and this mitigation is not proven - but taking the Save path out of CI is deterministic by construction: the screenshots are a debugging aid, not an assertion. The crash, its stack and the documented way to regenerate the PNGs are recorded in AGENTS.md and in the fixture summary.
The test expects msvcrt.dll to fail loading, which only happens off
Windows. CI runs on windows-latest, where the FFI call succeeds and
Assert.Catch fails. Add [Platform("Linux,MacOsX")] to skip it on Windows.
按键映射功能修复与UI优化
Apply the fix wave from ARCHITECTURE_REVIEW_REPORT.md (94 findings),
verified by a second review pass; the regressions the wave itself
introduced are fixed here before landing:

- ecs_vm.c: the load-time validator no longer rejects legal KeyV images
  (a is a key code, only b is a target slot), and GetFI/PutFI compare
  ext unsigned so 0xFFFFFFFF cannot slip past the slot check; corpus
  gains a variable-duration key case and a validator test that provably
  fails against the old over-strict check
- Capture: dispose the cloned target Mats in the Search hot path
- Device: TTLSerialClient no longer double-fires StatusChanged on Error
- UiTests: double-buffer polling tolerates the transient disposed buffer
  between pool rebuild and the first resized frame

Wave highlights:

- ecs_vm.c: load-time static validator for slot/ext operands (P0); CI
  CVM differential now really runs on Windows and the macOS job runs
  dotnet test
- Device: reconnect races removed (cancellable loop, serialized connect)
- AI agent: MCP stderr drained (4KB pipe deadlock), ToolRegistry made
  concurrent-safe, provider/tool error paths surfaced and tested
- OCR: init failures surfaced instead of being swallowed into empty text
- LSP: keyword/builtin tables derived from the lexer/manifest single
  source; CLI failures return non-zero exit codes
- UI: view models moved into EasyCon2.Avalonia.Core (compiler-enforced
  no-Avalonia zone), view components moved to EasyCon2.Avalonia; dead
  WinInput project, ProjectManager, IFirmwareService, Core Assist
  WebSocket, ImgLabelX/HSVColor/OCRDetect and bundled deps DLLs removed
- Docs updated (Framework/VM2/GETTING_STARTED/VmSemanticContract/Device)
  and the review report checked in
修复桌面 VM 槽位限制、模块加载与相对 FFI 路径
SlenderData and others added 30 commits October 25, 2026 13:25
…ions

The previous small-screen change only let the preview shrink: enlarging
the window left it at 822x282 with empty space around it, and making the
window shorter just added a scroll bar. The preview now follows the
window size.

- Lay the window out as a grid whose first row takes the remaining space;
  the preview's Viewbox scales up and down (Stretch=Uniform) to fill it
  while keeping the canvas aspect ratio
- Buttons, inputs and texts keep their normal size; parameter groups
  still wrap when the window is narrow
- Drop SizeToContent and the scroll viewer; default to 900x500 and a
  480x480 minimum, which fits every control even in English
- Tests: nothing overflows at the minimum size, the preview grows with a
  larger window and shrinks when the window gets shorter, and clicks map
  to canvas coordinates whether the preview is scaled up or down
The ink threshold was hard-coded at half brightness, and the preview
showed the original picture, so there was no way to tell or tune which
pixels would actually be drawn.

- Add an explicit binarization stage: InkMask.Binarize turns the canvas
  into a per-pixel ink mask using a threshold percentage and the invert
  option; SearchLines now scans the mask, and Reverse moves out of
  DrawingBoardSettings into the binarization options
- Add a threshold slider (0-100%, default 50%, the legacy boundary); the
  only difference from the legacy rule at 50% is that a pixel exactly on
  the boundary (R+G+B = 382), which used to keep the current run state,
  now counts as ink
- The preview shows the mask (black = will be drawn) and refreshes as the
  threshold or invert option changes; "Show original" switches back to
  the picture
- Drawing takes a snapshot of the mask when it starts, so adjusting the
  options mid-draw does not affect the running drawing
- Tests cover the legacy boundary, threshold and invert combinations, the
  mask preview, preview updates and that the threshold changes what is
  drawn
Only 320x120 pictures were accepted; anything else was rejected and had
to be resized by hand. Pictures of any size are now fitted onto the
320x120 canvas.

- Add IImageProcessor.FitTo with three modes: fit with letterbox
  (default, keeps the whole picture undistorted; the transparent margins
  count as white and are not drawn), fill with a centered crop, and
  stretch; SkiaImageProcessor resamples with a Mitchell cubic filter
- A picture that is already 320x120 is returned untouched, so pixel art
  keeps its exact pixels
- The view model keeps the decoded original and refits from it when the
  mode changes, without reading the file again
- Show the original size next to the fit selector; drop the "must be
  320x120" message
- Rename the stop-drawing CancellationTokenSource to _drawCancellation
- Tests cover the fitting modes against SkiaSharp and the view model's
  default mode, refitting and size display
Messages_AreOwnedByTheDrawingBoardWindow failed about one run in five
with "the calling thread cannot access this object". After the message
box closed, the continuation of the awaited dialog resumed through
NUnit's synchronization context on a thread-pool thread, where
AsyncRelayCommand raised CanExecuteChanged and the button read its
Command property off the UI thread.

Run the test under AvaloniaSynchronizationContext so the continuation is
posted to the dispatcher and executed by RunJobs on the UI thread. The
app itself always runs under Avalonia's context, so only the test was
affected. 10 consecutive full UI test runs pass.
A plain threshold turns grayscale photos into flat black and white
blobs. Add an optional dithering mode that spreads each pixel's
quantization error to its unprocessed neighbours (7/16, 3/16, 5/16,
1/16), so shades come out as dot density.

- BinarizeOptions gains Dither (off by default; when off the mask is
  unchanged); threshold and invert keep working with dithering on
- Pure black and white areas, including letterbox margins, stay exactly
  as without dithering, so nothing is drawn in blank space
- Add a Dither checkbox with a tooltip noting that dots are more
  scattered and drawing takes longer
- Tests: mid gray becomes about half dots, pure black/white are
  unchanged (also inverted), dot density follows a gradient, and the
  option updates the mask immediately
The dither option was a single Floyd-Steinberg checkbox. Replace it with
a dropdown of algorithms, since they trade quality against the number of
segments, which is what drives drawing time:

- Off (default), Floyd-Steinberg, Floyd-Steinberg with serpentine
  scanning (odd rows right to left, fewer diagonal streaks), Atkinson
  (diffuses only 3/4 of the error: fewer dots in light areas, more
  contrast, fewest segments), Jarvis-Judice-Ninke and Stucki (12-neighbour
  kernels, smoother), and 8x8 Bayer ordered dithering (regular grid)
- Error diffusion is one routine driven by a kernel table; serpentine
  rows mirror the kernel horizontally. Bayer shifts the threshold per
  grid cell around the slider value, so the threshold and invert options
  work with every algorithm
- At 50% pure black and white stay unchanged with every algorithm
- On a 400x400 radial gradient (fill mode, default timings) the estimate
  ranges from 81 min (Atkinson) to about 103 min (Floyd-Steinberg/Bayer)
- Tests run the shared properties for every algorithm and check that
  Atkinson uses fewer dots in light areas, that serpentine changes the
  pattern but not the density, that Bayer repeats every 8 pixels, and
  that the dropdown lists every mode in enum order
Pictures taken or saved in the wrong orientation had to be fixed in an
external editor before drawing. Add orientation options that apply to
the decoded original before it is fitted onto the canvas:

- Rotate 0/90/180/270 degrees clockwise (dropdown), then flip
  horizontally and/or vertically relative to the rotated picture
- PixelImage.Orient only moves pixels (no resampling, so pixel art stays
  sharp) and returns the same image when nothing changes; 90/270 swap
  the dimensions, so a rotated picture is fitted as portrait
- Like the fit mode, changing orientation recomputes from the decoded
  original without reading the file again; the size label keeps showing
  the original size, and "show original" shows the rotated/flipped one
- The extra row wraps the parameters onto a third line at the default
  width, so raise the default window height from 500 to 560 to keep the
  preview at nearly full width
- Tests pin every rotation/flip combination on a numbered 3x2 picture,
  the identity shortcut, how orientation moves what gets drawn, and that
  a 90-degree rotation fits a portrait picture
The estimate only appeared after clicking the legacy "估算耗时" button,
and went stale as soon as the picture or a parameter changed. Remove the
button and keep the bottom-right estimate up to date instead.

- Recompute the estimate whenever the mask changes (loading, rotation,
  flips, fit mode, threshold, invert, dithering) and whenever the start
  position, size, key hold or delay changes
- With no picture it still shows "耗时:"; an invalid parameter shows
  "耗时:参数有误" instead of opening a message box while typing
- A full dithered canvas takes only milliseconds to estimate, so the
  value follows slider drags in real time
- Rename EvaluateText to EstimateText and drop EvaluateCommand and the
  Text.DrawingBoard.Estimate button label
- Tests cover the estimate appearing on load, following picture and
  parameter changes, and the invalid-parameter hint without a dialog
In the game A is the pen and B the eraser. For pictures that are mostly
black it is much faster to fill the whole canvas by hand with the
largest brush and then erase the blank parts than to draw every ink
point. Add an eraser mode for that workflow:

- When enabled, plan strokes over the blank points (the inverted mask)
  and hold B instead of A; scanning, ordering and movement are the same
  as in pen mode
- The preview keeps showing the final picture, which is identical in
  both modes, and the bottom-right estimate switches with the mode so
  the two can be compared
- Show the preparation steps below the parameters while the mode is on
  (fill the canvas black, pick the smallest brush, cursor to the top
  left; only the drawing region is processed and the rest stays black)
- The mode is snapshotted with the mask when drawing starts
- Add InkMask.Inverted and a pen key parameter to SplatoonDrawing
- Tests: B is held and only blank points in the region are erased, a
  mostly black picture estimates far faster in eraser mode, the preview
  is unchanged, and nothing overflows at the minimum window size with the
  help text shown
There was no way to keep the processed result: after tuning rotation,
fit, threshold and dithering, the work was lost when the window closed.
Add a "Save Picture" button next to "Load Picture".

- Save exactly what the preview shows at 320x120: by default the
  black-and-white mask (what will be drawn), or, with "Show original",
  the rotated and fitted original
- A saved mask is pure black and white at canvas size, so loading it
  again skips resampling and binarizes to the same mask at the default
  50% threshold
- Suggest "<source name>-320x120.png"; log the saved path, and report a
  failure in a message box
- Add IImageProcessor.SavePng (lossless SkiaSharp PNG encode, alpha
  preserved); the button is disabled until a picture is loaded
- Tests cover availability, saving the mask or the original, cancelling
  the dialog, failures, a lossless round trip and an unwritable path
Three picture-input improvements in one change:

Drag and drop
- Dropping a file anywhere on the drawing board window loads it, reusing
  the existing FileDropBehavior

More formats
- The open dialog now lists every format SkiaSharp decodes here: PNG,
  JPG/JPEG, BMP, GIF, WebP, ICO and WBMP (HEIC, TIFF, PSD, TGA and
  JPEG 2000 were probed and do not decode)
- The load-failure message names the supported formats and suggests
  converting HEIC/TIFF first; the load button tooltip mentions dropping

Cover framing
- In fill (crop) mode, add free rotation (-180 to 180 degrees, applied
  after the 90-degree steps and flips), zoom (100-400%) and horizontal
  and vertical framing sliders, plus a "Reset framing" button; they are
  shown only in fill mode and only refit there
- The base scale grows with the angle so a centered picture still covers
  the whole canvas at any angle; framing moves within the picture's
  overflow (with rotation, extreme positions may expose transparent
  corners, which count as white)
- FitTo takes an optional CoverTransform; the identity transform renders
  exactly like the previous centered crop, and the same-size shortcut is
  skipped when framing is adjusted
- Reset recomputes once instead of once per slider

Tests cover cover-only behaviour, the transform passed to FitTo, reset,
dropping, the dialog filter, decoding each listed format, framing,
zoom, 90-degree and arbitrary-angle coverage, and the drop wiring.
Pure move, no behaviour change: the drawing board view model had grown
to 536 lines and its test file to 1220, mixing unrelated concerns.

- DrawingBoardViewModel becomes partial files by responsibility: core
  (dependencies, position, localized texts, disposal), .Picture (load,
  drop, save and the orient/fit/framing/binarize pipeline), .Settings
  (parameter inputs, validation and the automatic estimate) and
  .Drawing (start/stop, background run, progress, eraser mode)
- Move DitherMode and the dithering kernels/algorithms from InkMask into
  Dithering; InkMask keeps the mask, binarization, inversion and preview
- Split DrawingBoardTests into SplatoonDrawingTests, InkMaskTests,
  PixelImageOrientTests and DrawingBoardPicture/Run/SettingsTests, with
  shared builders and fakes in DrawingBoardTestKit and the zh_CN locale
  setup in DrawingBoardTestBase
- Move the FitTo and cover framing tests from SkiaImageProcessorTests
  into SkiaImageFitTests

Moved code lines were compared before and after; only access modifiers,
class headers and the dither dispatch location changed. Test counts are
unchanged (Core 319, UI 81) and all pass.
…election

Pause and resume
- Add a Pause/Continue button while drawing. The pause takes effect
  before the next key press; if the pen is down it is lifted first and
  pressed again in place on resume (redrawing the same cell is harmless).
  The controller lease is kept, so scripts and manual input stay blocked.
  Stopping or closing the window cancels a paused draw immediately.
- Paused time is excluded from the elapsed clock and the remaining-time
  calibration; the planned progress still ends at exactly 100%.

Progress
- The status line now shows completed/total segments, e.g.
  "37% · 第 1234/5678 段 · 已用 12:05 · 剩余约 20:13", and "已暂停 · …"
  while paused.
- While drawing, the preview shows an estimate of the in-game canvas:
  pen mode draws done ink black and pending ink light gray; eraser mode
  shows erased cells white, pending cells dark gray and the rest black.
  The cursor is marked with a red dot and ring. The image refreshes with
  the throttled progress and immediately on pause, stays after finishing
  or stopping, and is cleared once the picture or its options change.
- SplatoonDrawing reports the cursor and pen state after every step;
  the key sequence is unchanged when not paused.

Drawing region
- Remove the start row/column and row/column count inputs: drawing
  always covers the whole canvas from the top-left by default, since
  exact coordinates are hard to confirm in the game.
- Add an advanced "select drawing region" option: drag on the preview to
  select a rectangle, framed in translucent orange, with a "whole canvas"
  reset and a text summary. The region cannot change while drawing, and
  outside it the progress preview shows the untouched canvas.
- The saved picture never includes the progress or region overlays.

Tests cover pausing with the pen down, progress unaffected by pauses,
cancelling while paused, step reporting, overlay colours and cursor
ring, region outline and restriction, pause/continue texts, stopping
while paused, segment counting, the finished preview, region selection
by dragging (including in the headless window) and the region lock while
drawing.
…and outline

Add an "advanced picture adjustments" expander (collapsed by default)
with four options; their defaults leave every result unchanged. The
pipeline is now: orient -> fit (optionally nearest-neighbour) -> tone ->
threshold/dither -> invert -> remove specks -> outline.

- Pixel art: FitTo gains a nearest-neighbour mode, so enlarged pixel art
  keeps hard edges instead of grey transitions that thicken or break
  lines after binarization
- Brightness, contrast and gamma (ToneAdjust) are applied to the
  composited brightness before the threshold and dithering; a reset
  button restores them with a single recompute
- Remove specks: drop 8-connected ink blobs of at most N points (0-20,
  shown as "关闭" at 0). On a radial gradient with Atkinson dithering,
  N=3 cuts the estimate from 83 to 66 minutes
- Outline mode keeps only ink cells with a blank (or canvas-edge)
  4-neighbour, hollowing solid areas while lines and dots stay
- The controls below the preview are grouped so the window's minimum
  height follows their height; expanding the adjustments grows the
  window instead of pushing controls out of view. Default height is now
  600 to keep the preview near full width
- Put the new options in DrawingBoardViewModel.Adjust and the mask
  filters in InkMask.Filters

Tests cover the tone math and its effect on binarization, speck removal
by blob size and connectivity, outlines, the view model wiring
(nearest-neighbour refit, mask/estimate updates, single-update reset),
nearest-neighbour scaling without grey pixels, and no overflow at the
minimum size with the adjustments expanded.
…verted

Outline mode kept every ink cell next to a blank cell or the canvas
edge. With "invert" on, the white background becomes ink, so the whole
canvas border was kept as an outline: a black disc on white produced
1048 outline cells, 876 of them a frame around the canvas that is not in
the picture and adds minutes of drawing.

The canvas edge no longer counts as an outline; a cell is kept only when
a 4-neighbour inside the canvas is blank. Inverted, the same disc now
yields a 172-cell ring just outside the disc, matching the 168-cell ring
inside it without inversion. An ink area touching the canvas edge no
longer gets a contour along that edge, and a fully inked canvas has no
outline at all.

Tests pin the inverted disc (no border cells, ring size close to the
non-inverted one) and the fully inked canvas, and update the corner
block expectation.
Invert was applied right after thresholding, before speck removal and
the outline, so with outline mode on it merely outlined the background
instead of the subject: the result looked almost the same whether or
not invert was checked. The previous fix (0d2d93e) only removed the
canvas frame and did not address this ordering.

The pipeline is now orient -> fit -> tone -> threshold/dither -> remove
specks -> outline -> invert: invert swaps black and white in the fully
processed result, so a black outline becomes white lines on black and
speck removal works on the subject's dark specks before inversion.
Without speck removal or outline the result is unchanged, since
inverting before or after a plain threshold is the same.

- Binarize with Reverse=false in the view model and invert the final
  mask; BinarizeOptions.Reverse keeps its meaning for direct callers
- Add a tooltip to the invert checkbox stating it is applied last
- Keep the canvas-edge rule from 0d2d93e (edges are not contours) and
  reword its rationale
- Tests: invert after outline equals the inverted outline, a removed
  speck turns into ink after inversion, and invert alone still matches
  the plain inverted threshold
With a drawing region selected, the progress preview rendered everything
outside the region as the untouched canvas (blank in pen mode, black in
eraser mode), so the rest of the picture vanished as soon as drawing
started. The progress image also stayed after stopping, and reselecting
a region did not bring the full preview back, leaving nothing to select
against.

- Render cells outside the region faintly instead: ink as very light
  grey in pen mode, would-be-erased cells as near-black in eraser mode,
  distinct from done and pending cells inside the region
- Clear the leftover progress image as soon as the region is reselected,
  reset to the whole canvas or region selection is toggled, so the full
  preview is shown while choosing the next region
- Drop InkMask.Within, which only served the old blank-outside rendering

Tests cover the faint outside rendering in both modes, and stopping a
region draw then reselecting by dragging, which now shows the whole
picture again.
…s tabs

Put load/save and start/pause/stop in a toolbar above the preview, a
single-line status row (region, position, estimate) and the progress bar
below it, and the parameters in four tabs that follow the processing
order: picture, tone, black & white, drawing. The preparation tip at the
bottom switches to the eraser steps in eraser mode. Status texts are
shorter and truncate instead of wrapping.
Add a Reset button that restores every option to its default while
keeping the loaded picture, recomputing once at the end; it is disabled
while drawing. Show a drop hint in the empty preview. Move the eraser
steps into the eraser checkbox's tooltip and keep the bottom tip fixed.
Remove the clicked-position readout; clicks on the preview now only
start a region selection.
…ming overhead

Homing: the Switch may drop the first input frame after connecting. When
that first input was the pen press, the whole first row went missing, and
any mispositioned start shifted the entire picture. Drawing now starts by
pushing the left stick fully up-left for 50 ms (a sacrificial input) and
then for 4 s, so the cursor lands in the top-left corner from anywhere.
It is a default-on option; turning it off warns that dropped frames can
cause unpredictable misalignment.

Estimates: the plan only summed key durations and delays, but every click,
pen press and release also costs a device round trip, so pictures with
many segments (dithering) ran far longer than estimated. Each operation
type's overhead is now measured during drawing; the remaining time uses
those per-type figures instead of a global ratio, and later estimates in
the same session include the learned overhead (also written to the log).

Also fixes a crash: localized text was formatted on the drawing thread
(learned-overhead log, error message), which threw a cross-thread
exception on stop and took the app down. Both now run on the UI thread,
guarded by a regression test.

A replay test checks that the emitted key sequence paints exactly the ink
pixels, including the first and last columns.
…rules

Mark the reference timings as planned time, explain why the first
estimate after launch is short, add troubleshooting rows for a missing
first row and for turning auto homing off, and record the maintainer
rules: how overhead is measured and learned, and that localized text
must only be formatted on the UI thread.
Once the macOS screen saver started, every wait of the draw loop overran
by about 20 ms, so a picture estimated at 47 minutes took 66. Sleep on
either platform also interrupts the draw. While drawing, the run now
holds an NSProcessInfo activity (user initiated, idle display sleep
disabled, latency critical) on macOS and SetThreadExecutionState on
Windows, released when drawing ends. It is best effort and never fails
the draw.

Exposed as a "keep awake while drawing" option, on by default; the
runner interface takes the flag per run.
feat(device): 完善 pokeldn 串口控制并支持 ESP32-S3 服务连接
refactor(device): 分离通用串口协议与蓝牙桥接入口
Add ezcon init with canonical project templates and conflict checks. Reject explicit import cycles after graph discovery and cover scaffold safety and module cycles with regression tests. Document project layout, import rules, and PATH-based CLI usage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants