mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Merge pull request #4097 from dotCooCoo/feat/windows-pen-eraser-end
Pen tablets: a pen's eraser end selects the Eraser on Windows
This commit is contained in:
@@ -53,9 +53,10 @@ winsafe = { version = "0.0.29", default-features = false, features = ["user"], o
|
||||
# DirectWrite's system font collection (src/system_fonts.rs): fonts that font services such as
|
||||
# Adobe Fonts load in place (#579). MIT OR Apache-2.0, already in the build through accesskit.
|
||||
# Win32_UI_WindowsAndMessaging has `MSG` and `WM_POINTERDOWN`, which src/windows_stylus.rs uses to
|
||||
# read a pen's barrel button from its pointer messages.
|
||||
# read a pen's barrel button from its pointer messages. Win32_UI_Input_Pointer has
|
||||
# `GetPointerPenInfo`, which it uses to read the pen's pressure and which end of the pen is down.
|
||||
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_UI_WindowsAndMessaging"] }
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# The macOS menu bar (src/mac_menu.rs): Tauri's menu crate, AppKit menus behind a safe API (no GTK
|
||||
|
||||
@@ -614,6 +614,9 @@ fn services(parent: Parent, ui: egui::Context) -> Services {
|
||||
// macOS reads it from AppKit's events (#852).
|
||||
#[cfg(target_os = "macos")]
|
||||
stylus: mac_tablet::start(),
|
||||
// Windows reads it from the pen's pointer messages (`windows_stylus`).
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
stylus: Some(windows_stylus::start()),
|
||||
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
|
||||
write: Some(Box::new(write_file)),
|
||||
exists: Some(Box::new(|p: &str| std::path::Path::new(p).exists())),
|
||||
@@ -974,9 +977,9 @@ fn main() -> std::process::ExitCode {
|
||||
}
|
||||
options
|
||||
};
|
||||
// With Windows Ink, winit reports a pen as a touch and drops its barrel button; the message
|
||||
// hook notes each pointer contact and whether it starts with the barrel button held
|
||||
// (`windows_stylus`).
|
||||
// With Windows Ink, winit reports a pen as a touch and drops its barrel button and which end
|
||||
// is down; the message hook notes each pointer contact and whether it starts with the barrel
|
||||
// button held, and writes the pen's end into the pen feed (`windows_stylus`).
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
let options = {
|
||||
use winit::platform::windows::EventLoopBuilderExtWindows as _;
|
||||
|
||||
@@ -5,30 +5,74 @@
|
||||
//! second-button flag ("analogous to a mouse right button down",
|
||||
//! <https://learn.microsoft.com/en-us/windows/win32/inputmsg/wm-pointerdown>): [`msg_hook`] notes
|
||||
//! it, and [`raw_input_hook`] makes the primary button of that contact the secondary one.
|
||||
//!
|
||||
//! winit also drops which end of the pen is down. For each pointer message of a pen, [`msg_hook`]
|
||||
//! reads the pen's `POINTER_PEN_INFO` and writes its pressure and end into the canvas's pen feed
|
||||
//! ([`start`]): `PEN_FLAG_INVERTED` ("The pen is inverted") or `PEN_FLAG_ERASER` ("The eraser
|
||||
//! button is pressed") is the eraser end
|
||||
//! (<https://learn.microsoft.com/en-us/windows/win32/inputmsg/pen-flags-constants>). Pointer
|
||||
//! messages also come while the pen hovers, so turning the pen over selects the Eraser tool before
|
||||
//! it touches. A message of another pointer, a pen leaving detection range or the window, or a
|
||||
//! mouse message that Windows did not make from a pen or a touch
|
||||
//! (<https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages>)
|
||||
//! clears the feed.
|
||||
#![cfg_attr(not(all(windows, not(target_vendor = "win7"))), allow(dead_code))]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, TouchId, TouchPhase};
|
||||
use vectorcraft_ui_egui::stylus::{PenSample, StylusFeed};
|
||||
|
||||
/// `POINTER_MESSAGE_FLAG_SECONDBUTTON` (`winuser.h`): in the high word of a pointer message's
|
||||
/// `wParam`, a pen in contact with its barrel button pressed.
|
||||
const POINTER_MESSAGE_FLAG_SECONDBUTTON: usize = 0x0020;
|
||||
/// `POINTER_MESSAGE_FLAG_INRANGE` (`winuser.h`): not set on the message of a pointer that has left
|
||||
/// detection range.
|
||||
const POINTER_MESSAGE_FLAG_INRANGE: usize = 0x0002;
|
||||
/// At most this many pointer contacts wait for their touch event.
|
||||
const MAX_PENDING: usize = 16;
|
||||
/// `PEN_FLAG_INVERTED` and `PEN_FLAG_ERASER` (`winuser.h`), in `POINTER_PEN_INFO::penFlags`.
|
||||
const PEN_FLAG_INVERTED: u32 = 0x0002;
|
||||
const PEN_FLAG_ERASER: u32 = 0x0004;
|
||||
/// `PEN_MASK_PRESSURE` (`winuser.h`), in `POINTER_PEN_INFO::penMask`: the pen reports pressure.
|
||||
const PEN_MASK_PRESSURE: u32 = 0x0001;
|
||||
/// `MI_WP_SIGNATURE` and `SIGNATURE_MASK`: the extra information of a mouse message that Windows
|
||||
/// made from a pen or a touch.
|
||||
const MI_WP_SIGNATURE: u32 = 0xFF51_5700;
|
||||
const SIGNATURE_MASK: u32 = 0xFFFF_FF00;
|
||||
/// `WM_MOUSEFIRST` to `WM_MOUSELAST` (`winuser.h`).
|
||||
const MOUSE_MESSAGES: std::ops::RangeInclusive<u32> = 0x0200..=0x020E;
|
||||
/// `WM_POINTERUPDATE`, `WM_POINTERDOWN`, `WM_POINTERUP`, `WM_POINTERENTER` and `WM_POINTERLEAVE`
|
||||
/// (`winuser.h`).
|
||||
const WM_POINTERUPDATE: u32 = 0x0245;
|
||||
const WM_POINTERDOWN: u32 = 0x0246;
|
||||
const WM_POINTERUP: u32 = 0x0247;
|
||||
const WM_POINTERENTER: u32 = 0x0249;
|
||||
const WM_POINTERLEAVE: u32 = 0x024A;
|
||||
|
||||
thread_local! {
|
||||
static STROKES: RefCell<BarrelStrokes> = RefCell::new(BarrelStrokes::default());
|
||||
}
|
||||
|
||||
/// The pen feed [`msg_hook`] writes, once [`start`] has made it.
|
||||
static FEED: OnceLock<StylusFeed> = OnceLock::new();
|
||||
|
||||
/// The canvas's pen feed, which [`msg_hook`] writes on the UI thread before winit handles each
|
||||
/// message, so a press never waits for it.
|
||||
pub fn start() -> StylusFeed {
|
||||
FEED.get_or_init(StylusFeed::on_ui_thread).clone()
|
||||
}
|
||||
|
||||
/// winit's message hook (`EventLoopBuilderExtWindows::with_msg_hook`), which winit calls for each
|
||||
/// message it takes from the thread's queue, before dispatching it: notes each pointer contact's id
|
||||
/// and whether the barrel button was held.
|
||||
/// and whether the barrel button was held, and writes the pen's pressure and end into the feed.
|
||||
/// It never consumes the message.
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn msg_hook(msg: *const std::ffi::c_void) -> bool {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{MSG, WM_POINTERDOWN};
|
||||
use windows::Win32::UI::Input::Pointer::{GetPointerPenInfo, POINTER_FLAG_INCONTACT, POINTER_PEN_INFO};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetMessageExtraInfo, MSG};
|
||||
// SAFETY: winit passes the `MSG` that `PeekMessageW` has just filled, which lives for this
|
||||
// call; `as_ref` gives `None` for a null pointer.
|
||||
let Some(msg) = (unsafe { msg.cast::<MSG>().as_ref() }) else {
|
||||
@@ -41,9 +85,63 @@ pub fn msg_hook(msg: *const std::ffi::c_void) -> bool {
|
||||
}
|
||||
});
|
||||
}
|
||||
let Some(feed) = FEED.get() else {
|
||||
return false;
|
||||
};
|
||||
let read_pen = || {
|
||||
let mut info = POINTER_PEN_INFO::default();
|
||||
// SAFETY: `info` is a `POINTER_PEN_INFO` that lives for the call. The call fails for a
|
||||
// pointer that is not a pen.
|
||||
unsafe { GetPointerPenInfo((msg.wParam.0 & 0xffff) as u32, &mut info) }.ok()?;
|
||||
let in_contact = info.pointerInfo.pointerFlags.0 & POINTER_FLAG_INCONTACT.0 != 0;
|
||||
Some(pen_sample(info.pressure, info.penMask, info.penFlags, in_contact))
|
||||
};
|
||||
// SAFETY: `GetMessageExtraInfo` reads the extra information of the thread's last message.
|
||||
let from_mouse = || is_mouse(unsafe { GetMessageExtraInfo() }.0 as u32);
|
||||
if let Some(sample) = feed_update(msg.message, msg.wParam.0, read_pen, from_mouse) {
|
||||
feed.set_sample(sample);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// What a pen whose `POINTER_PEN_INFO` holds `pressure` (0..1024), `pen_mask` and `pen_flags`
|
||||
/// reports: the pressure as winit gives it to a touch, 0 while it hovers, a full press for a pen
|
||||
/// in contact that reports none; and the eraser end while it is inverted or its eraser button is
|
||||
/// pressed.
|
||||
fn pen_sample(pressure: u32, pen_mask: u32, pen_flags: u32, in_contact: bool) -> PenSample {
|
||||
let pressure = match pressure {
|
||||
1..=1024 if pen_mask & PEN_MASK_PRESSURE != 0 => pressure as f32 / 1024.0,
|
||||
_ if in_contact => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
PenSample { pressure, eraser: pen_flags & (PEN_FLAG_INVERTED | PEN_FLAG_ERASER) != 0 }
|
||||
}
|
||||
|
||||
/// Whether a mouse message with this extra information (`GetMessageExtraInfo`) comes from a mouse,
|
||||
/// not from a pen or a touch.
|
||||
fn is_mouse(extra_info: u32) -> bool {
|
||||
extra_info & SIGNATURE_MASK != MI_WP_SIGNATURE
|
||||
}
|
||||
|
||||
/// What the feed is set to for a message with this `wParam`: `Some(Some(sample))` for a pointer
|
||||
/// message of a pen in range (`read_pen`), `Some(None)` for a pointer message of another pointer,
|
||||
/// a pointer leaving detection range or the window, or a mouse message from a mouse
|
||||
/// (`from_mouse`), and `None` to leave it as it is.
|
||||
fn feed_update(
|
||||
message: u32,
|
||||
wparam: usize,
|
||||
read_pen: impl FnOnce() -> Option<PenSample>,
|
||||
from_mouse: impl FnOnce() -> bool,
|
||||
) -> Option<Option<PenSample>> {
|
||||
match message {
|
||||
WM_POINTERENTER | WM_POINTERUPDATE | WM_POINTERDOWN | WM_POINTERUP if (wparam >> 16) & POINTER_MESSAGE_FLAG_INRANGE == 0 => Some(None),
|
||||
WM_POINTERENTER | WM_POINTERUPDATE | WM_POINTERDOWN | WM_POINTERUP => Some(read_pen()),
|
||||
WM_POINTERLEAVE => Some(None),
|
||||
m if MOUSE_MESSAGES.contains(&m) && from_mouse() => Some(None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Before egui reads a frame's input: the primary button of a contact that started with the
|
||||
/// barrel button held becomes the secondary button.
|
||||
pub fn raw_input_hook(raw: &mut egui::RawInput) {
|
||||
@@ -287,6 +385,59 @@ mod tests {
|
||||
assert_eq!(buttons(&rewrite(&mut strokes, &events)), [PRIMARY_DOWN]);
|
||||
}
|
||||
|
||||
/// `PEN_FLAG_BARREL` (`winuser.h`).
|
||||
const PEN_FLAG_BARREL: u32 = 0x0001;
|
||||
|
||||
#[test]
|
||||
fn an_inverted_pen_or_its_eraser_button_is_the_eraser_end() {
|
||||
let eraser = |flags| pen_sample(512, PEN_MASK_PRESSURE, flags, true).eraser;
|
||||
assert!(!eraser(0), "the tip");
|
||||
assert!(!eraser(PEN_FLAG_BARREL), "the barrel button");
|
||||
assert!(eraser(PEN_FLAG_INVERTED), "turned over");
|
||||
assert!(eraser(PEN_FLAG_ERASER), "the eraser button");
|
||||
assert!(eraser(PEN_FLAG_INVERTED | PEN_FLAG_ERASER));
|
||||
assert!(eraser(PEN_FLAG_INVERTED | PEN_FLAG_BARREL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pens_pressure_is_the_one_winit_gives_its_touch() {
|
||||
assert_eq!(pen_sample(512, PEN_MASK_PRESSURE, 0, true).pressure, 0.5);
|
||||
assert_eq!(pen_sample(1024, PEN_MASK_PRESSURE, 0, true).pressure, 1.0);
|
||||
assert_eq!(pen_sample(0, PEN_MASK_PRESSURE, PEN_FLAG_INVERTED, false), PenSample { pressure: 0.0, eraser: true }, "hovering");
|
||||
assert_eq!(pen_sample(0, 0, 0, true).pressure, 1.0, "a pen in contact that reports no pressure presses fully");
|
||||
assert_eq!(pen_sample(300, 0, 0, true).pressure, 1.0, "a pressure without the pressure mask is not one");
|
||||
assert_eq!(pen_sample(0, PEN_MASK_PRESSURE, 0, true).pressure, 1.0, "as winit, which gives no force for 0");
|
||||
assert_eq!(pen_sample(2000, PEN_MASK_PRESSURE, 0, false).pressure, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_messages_from_a_pen_or_a_touch_are_told_apart() {
|
||||
assert!(is_mouse(0), "a mouse");
|
||||
assert!(!is_mouse(0xFF51_5701), "a pen");
|
||||
assert!(!is_mouse(0xFF51_5780), "a touch");
|
||||
assert!(is_mouse(0xFF51_5600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_messages_set_the_feed_and_other_pointers_clear_it() {
|
||||
let pen = PenSample { pressure: 0.0, eraser: true };
|
||||
// `POINTER_MESSAGE_FLAG_INRANGE` in the high word of `wParam`, pointer 7 in the low word.
|
||||
let in_range = (POINTER_MESSAGE_FLAG_INRANGE << 16) | 7;
|
||||
let no_mouse = || -> bool { panic!("a pointer message is not a mouse message") };
|
||||
let no_pen = || -> Option<PenSample> { panic!("no pen to read") };
|
||||
for m in [WM_POINTERENTER, WM_POINTERUPDATE, WM_POINTERDOWN, WM_POINTERUP] {
|
||||
assert_eq!(feed_update(m, in_range, || Some(pen), no_mouse), Some(Some(pen)), "{m:#x}");
|
||||
assert_eq!(feed_update(m, in_range, || None, no_mouse), Some(None), "a touch: {m:#x}");
|
||||
assert_eq!(feed_update(m, 7, no_pen, no_mouse), Some(None), "out of range: {m:#x}");
|
||||
}
|
||||
assert_eq!(feed_update(WM_POINTERLEAVE, in_range, no_pen, no_mouse), Some(None));
|
||||
// WM_MOUSEMOVE and WM_LBUTTONDOWN.
|
||||
assert_eq!(feed_update(0x0200, 0, no_pen, || true), Some(None), "a mouse moved");
|
||||
assert_eq!(feed_update(0x0201, 0, no_pen, || false), None, "made from a pen");
|
||||
// WM_KEYDOWN.
|
||||
assert_eq!(feed_update(0x0100, 0, no_pen, || -> bool { panic!("not a mouse message") }), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contacts_whose_touch_never_arrives_are_bounded() {
|
||||
let mut strokes = BarrelStrokes::default();
|
||||
|
||||
@@ -332,7 +332,7 @@ pub struct Services {
|
||||
/// pointer visible next to the tool glyph ([`cursors::hide_system_pointer`], #2450).
|
||||
pub no_cursor_theme: bool,
|
||||
/// The pen pressure and pen end the host reads from the system itself (desktop Linux under
|
||||
/// X11 and XWayland). The canvas uses the pressure when a frame's events carry no touch force,
|
||||
/// X11 and XWayland, macOS and Windows). The canvas uses the pressure when a frame's events carry no touch force,
|
||||
/// and the end to switch to the Eraser tool and back. Without it, pen pressure comes from
|
||||
/// touch events only ([`stylus`]).
|
||||
pub stylus: Option<stylus::StylusFeed>,
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
//! from XInput2 on a connection of its own and writes it into a [`StylusFeed`]
|
||||
//! ([`crate::Services::stylus`]). On macOS, where winit reports none either, the app reads each
|
||||
//! event's pressure from AppKit before the window handles it, and also keeps it for that event's
|
||||
//! pointer move ([`StylusFeed::set_move`]), so the moves of a fast stroke each get their own. The
|
||||
//! canvas uses the feed when a frame's events carry no touch force.
|
||||
//! pointer move ([`StylusFeed::set_move`]), so the moves of a fast stroke each get their own. On
|
||||
//! Windows, where winit drops which end of the pen is down, the app reads the pressure and the end
|
||||
//! from each pointer message of a pen before winit handles it. The canvas uses the feed when a
|
||||
//! frame's events carry no touch force.
|
||||
//!
|
||||
//! The feed also says which end of the pen moved. Turning the pen to its eraser end selects the
|
||||
//! Eraser tool, and turning it back to the tip selects the tool used before
|
||||
|
||||
+3
-2
@@ -443,9 +443,10 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
- **Done:** pen pressure on macOS (#852), read from AppKit's tablet events before the window gets them, each
|
||||
move of a stroke with its own pressure, and the pen's eraser end there selecting the Eraser tool, as on Linux
|
||||
under X11 and XWayland; not yet tried on a Mac with a tablet.
|
||||
under X11 and XWayland; not yet tried on a Mac with a tablet. The pen's eraser end on Windows with Windows Ink,
|
||||
read from the pen's pointer messages; not yet tried with a pen.
|
||||
- **Missing:** pen pressure on native Wayland (#491, #764) — winit 0.30 reports none there; tilt, bearing and
|
||||
barrel rotation for brushes (6D Art Pen, #372); the eraser end on Windows, native Wayland and the web.
|
||||
barrel rotation for brushes (6D Art Pen, #372); the eraser end on native Wayland and the web.
|
||||
- **Estimate:** 4–10 h, partly waiting on winit 0.31; needs pen hardware to verify.
|
||||
|
||||
## G5 Brushes, symbols and libraries
|
||||
|
||||
@@ -12,7 +12,7 @@ documentation (GPU Performance, touch workspace, pen pressure pages); ours from
|
||||
**~35% (estimated), 25–45 h.** VectorCraft rasterizes the canvas on the CPU (multithreaded `vello_cpu`) and uses
|
||||
the GPU only to composite, so any GPU that can show a window works, at the cost of GPU-accelerated preview
|
||||
on huge documents. VectorCraft reads pen pressure on Windows, on the web, on macOS, and on Linux under X11 and XWayland, and it
|
||||
reads the pen's eraser end on macOS and on Linux under X11 and XWayland.
|
||||
reads the pen's eraser end on Windows, on macOS and on Linux under X11 and XWayland.
|
||||
|
||||
| Feature | Illustrator | VectorCraft macOS | Windows | Linux | Web | Status / hours |
|
||||
|---|---|---|---|---|---|---|
|
||||
@@ -22,7 +22,7 @@ reads the pen's eraser end on macOS and on Linux under X11 and XWayland.
|
||||
| Restart on another GPU when the first fails to present | — | yes (#502, #964) | yes | yes | restart on lost device (#369) | done |
|
||||
| Pen pressure | yes (Wacom, Windows Ink, macOS tablet events) | yes, read from AppKit's tablet events before the window gets them (winit reports none), each move of a stroke with its own pressure, mouse event coalescing off (#852; not yet tried on a Mac) | yes (pointer force via `WM_POINTER`) | under X11 and XWayland (Preferences › Performance › Linux Display Server, #1151, #2028), read from XInput2 raw events (#764); **no** on native Wayland, where winit reports none and the pen may not reach the window (#491) | browser pointer events, with coalesced events; Apple Pencil on iPad (hover, palm rejection: fingers navigate after a pen contact until reload) | partial: 2–4 h for Wayland (winit 0.31), plus a check on a Mac |
|
||||
| Pen tilt, bearing, barrel rotation (6D Art Pen) | yes, drives brushes | no | no | no | no | missing: 4–8 h after the input layer (#372) |
|
||||
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | no | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | no | partial |
|
||||
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | with Windows Ink, an inverted pen or its eraser button (`PEN_FLAG_INVERTED`, `PEN_FLAG_ERASER` in the pen's pointer messages) selects the Eraser tool and the tip brings back the tool used before, as on Linux (not yet tried with a pen) | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | no | partial |
|
||||
| Touch gestures (pinch zoom, two-finger pan, rotate view, Touch workspace) | yes (Windows touch, Surface) | trackpad pinch, scroll and rotate | pinch, pan; two-finger tap undo, three-finger redo (#585) | same where winit reports touch | eframe until the first pen contact, then Pointer Events (pen and fingers told apart); long press opens the context menu | partial: touch workspace and touch-screen rotation missing, 3–5 h |
|
||||
| Trackpad (smooth scroll, pinch) | yes | yes | precision touchpads | yes | yes | done |
|
||||
| HiDPI / Retina, per-monitor scale | yes | yes | yes | yes | yes | done; a window dragged between monitors of different scale can still grow on Windows (#1459, open); the custom title bar's window on Windows stays truly borderless so the pointer lines up with what is drawn (#1791, #1758; awaiting the reporter's confirmation) |
|
||||
|
||||
@@ -996,6 +996,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Linux pen pressure (part of #764):** when the window is on X11, XWayland included, the desktop app reads a drawing tablet's pen pressure from XInput2 raw events on a second X connection, and the Paintbrush's pressure-sensitive brushes and the Liquify tools' Use Pressure Pen get it.
|
||||
- **Linux pen eraser end (part of #764):** when the window is on X11, XWayland included, turning a drawing tablet's pen to its eraser end selects the Eraser tool, and turning it back to the tip selects the tool used before, unless another tool was chosen meanwhile. A turn during a press takes effect after the release, and a turn made while the pointer is over another window takes effect when it comes back over VectorCraft's window.
|
||||
- **Pen barrel button on Windows:** with Windows Ink, a pen that touches down with its barrel button held acts as the right mouse button until it lifts, so a tap opens the canvas context menu; on Linux the barrel button is the right button (#3659). A touch with the tip alone is the left button, with pressure, as before.
|
||||
- **Pen eraser end on Windows:** with Windows Ink, turning a pen over, or holding its eraser button, selects the Eraser tool, and the tip selects the tool used before, as on Linux and macOS; the switch comes while the pen hovers, so the eraser end erases from its first touch.
|
||||
- **Control transport:** idle sockets expire after 5 minutes, partial lines after an absolute 30 seconds from their first byte, and reply writes after 60 seconds (#1726). Real TCP regressions cover stalled peers and slot recovery; command execution has its separate existing timeout. MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727). MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728). The MCP server reconnects to the app before a call when the connection was closed or holds stray bytes, after an app restart or an idle expiry (#1739).
|
||||
- **MCP vertical type:** `add_text {vertical: true}` forwards to `text.create` or `text.createInPath`. Regression coverage checks point type, rectangular area type, type in a path and type on a path with true, false and omitted direction. This exposes an existing engine option; it does not change glyph orientation, CJK composition or the type/automation scores.
|
||||
- **Malformed MCP replies:** invalid JSON from the app drops the connection, reports that the operation may have completed and leaves the next call to reconnect; it never replays the edit (#1761). TCP regressions cover malformed and uncorrelated replies, connection closure and a subsequent inspection. Envelope regressions also verify zero backend calls and unchanged initialization state for invalid versions.
|
||||
|
||||
Reference in New Issue
Block a user