feat(input): Wayland-Tablet-Protokoll für aktive Stifte (Lazarite) und Touchscreen-Pinch-to-Zoom
Build & Publish Release / Build Packages & Publish to Gitea (push) Failing after 2m0s
Build & Publish Release / Build Packages & Publish to Gitea (push) Failing after 2m0s
This commit is contained in:
Generated
+125
-6
@@ -284,6 +284,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -573,6 +579,18 @@ dependencies = [
|
|||||||
"simdutf8",
|
"simdutf8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum_dispatch"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.27.2"
|
version = "0.27.2"
|
||||||
@@ -812,7 +830,7 @@ dependencies = [
|
|||||||
"presser",
|
"presser",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"winapi",
|
"winapi",
|
||||||
"windows",
|
"windows 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -866,6 +884,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -1021,6 +1045,7 @@ dependencies = [
|
|||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
"glam",
|
"glam",
|
||||||
|
"octotablet",
|
||||||
"pollster",
|
"pollster",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -1418,6 +1443,25 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "octotablet"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c4b9470450ffa669cb3be55ef863dfd9592a06284cb291546363c514bb4aa3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"cfg_aliases 0.2.2",
|
||||||
|
"enum_dispatch",
|
||||||
|
"raw-window-handle 0.6.2",
|
||||||
|
"smallvec",
|
||||||
|
"strum",
|
||||||
|
"thiserror",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"windows 0.54.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -1965,6 +2009,28 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -2519,7 +2585,7 @@ checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
@@ -2545,7 +2611,7 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
@@ -2574,7 +2640,7 @@ dependencies = [
|
|||||||
"bit-set",
|
"bit-set",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"block",
|
"block",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"d3d12",
|
"d3d12",
|
||||||
"glow",
|
"glow",
|
||||||
@@ -2661,7 +2727,19 @@ version = "0.52.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.52.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.54.0",
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2674,12 +2752,53 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
@@ -2915,7 +3034,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"calloop",
|
"calloop",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"cursor-icon",
|
"cursor-icon",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ semver = "1.0"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
|
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
octotablet = "0.1.0"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
||||||
|
|||||||
+609
-231
@@ -52,6 +52,49 @@ fn update_window_title(
|
|||||||
window.set_title(&title);
|
window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finalisiert einen Strich (mit Spline-Tessellierung, Ecken-Erkennung & RDP oder Auto-Snap)
|
||||||
|
/// und fügt ihn in die aktuelle Seite des Notizbuchs ein.
|
||||||
|
fn commit_stroke_to_notebook(
|
||||||
|
canvas: &mut CanvasState,
|
||||||
|
notebook: &mut Notebook,
|
||||||
|
manifest: &mut NotebookManifest,
|
||||||
|
autosave: &mut AutosaveController,
|
||||||
|
ui_state: &UiState,
|
||||||
|
window: &Window,
|
||||||
|
snapped_shape: Option<RecognizedShape>,
|
||||||
|
) {
|
||||||
|
let new_id = notebook.next_stroke_id();
|
||||||
|
let stroke_opt = if let Some(shape) = snapped_shape {
|
||||||
|
let stroke_width = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
||||||
|
let color = canvas.current_ink_color;
|
||||||
|
canvas.cancel_stroke();
|
||||||
|
Some(geometry::shape_to_stroke(
|
||||||
|
shape,
|
||||||
|
new_id,
|
||||||
|
color,
|
||||||
|
stroke_width,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
canvas.finish_stroke(new_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(stroke) = stroke_opt {
|
||||||
|
let page = notebook.current_page_mut();
|
||||||
|
page.add_stroke(stroke);
|
||||||
|
canvas.sync_page(page);
|
||||||
|
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||||
|
autosave.mark_page_dirty(notebook.current_page(), manifest);
|
||||||
|
update_window_title(
|
||||||
|
window,
|
||||||
|
notebook,
|
||||||
|
manifest,
|
||||||
|
ui_state.active_tool,
|
||||||
|
autosave.status(),
|
||||||
|
);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let event_loop = EventLoop::new()?;
|
let event_loop = EventLoop::new()?;
|
||||||
let window = Arc::new(
|
let window = Arc::new(
|
||||||
@@ -63,6 +106,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let mut canvas = pollster::block_on(CanvasState::new(window.clone()))?;
|
let mut canvas = pollster::block_on(CanvasState::new(window.clone()))?;
|
||||||
|
|
||||||
|
let mut tablet_manager = match octotablet::builder::Builder::default().build_shared(&window) {
|
||||||
|
Ok(mgr) => {
|
||||||
|
println!("[Input] Octotablet Stylus-/Tablet-Manager erfolgreich initialisiert (Wayland/X11 Tablet-Protokoll aktiv).");
|
||||||
|
Some(mgr)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("[Input] Hinweis: Octotablet Tablet-Manager nicht aktiv ({err}). Nutze Standard-Event-Pipeline.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Notizbuch aus Storage laden oder initialisieren (Session-Wiederherstellung)
|
// 1. Notizbuch aus Storage laden oder initialisieren (Session-Wiederherstellung)
|
||||||
let storage_base_dir = storage::default_storage_dir();
|
let storage_base_dir = storage::default_storage_dir();
|
||||||
let available_notebooks = storage::list_notebooks(&storage_base_dir);
|
let available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||||
@@ -135,7 +189,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut modifiers = ModifiersState::empty();
|
let mut modifiers = ModifiersState::empty();
|
||||||
|
|
||||||
let mut is_mouse_drawing = false;
|
let mut is_mouse_drawing = false;
|
||||||
let mut active_touch_id: Option<u64> = None;
|
|
||||||
|
// Multi-Touch & Pinch-to-Zoom Zustand
|
||||||
|
let mut active_touches: std::collections::HashMap<u64, (f32, f32)> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
let mut pinch_prev_distance: Option<f32> = None;
|
||||||
|
let mut pinch_prev_center: Option<(f32, f32)> = None;
|
||||||
|
let mut is_pinch_gesturing = false;
|
||||||
|
let mut is_touch_drawing = false;
|
||||||
|
let mut is_touch_panning = false;
|
||||||
|
let mut touch_pan_last_pos = (0.0_f32, 0.0_f32);
|
||||||
|
|
||||||
|
// Stylus / Digitizer Zustand (Octotablet / Wayland Tablet Protocol)
|
||||||
|
let mut is_stylus_drawing = false;
|
||||||
|
let mut is_stylus_in_proximity = false;
|
||||||
|
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
|
||||||
|
let mut stylus_barrel_pressed = false;
|
||||||
|
let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
|
||||||
|
|
||||||
// Viewport-Pan & Zoom-Zustand
|
// Viewport-Pan & Zoom-Zustand
|
||||||
let mut is_panning = false;
|
let mut is_panning = false;
|
||||||
@@ -201,6 +271,265 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stylus-/Tablet-Events via Octotablet abfragen (Wayland/X11 Tablet-Protokoll für aktive Stifte wie Lazarite)
|
||||||
|
if let Some(ref mut mgr) = tablet_manager {
|
||||||
|
if let Ok(tablet_events) = mgr.pump() {
|
||||||
|
let scale = window.scale_factor() as f32;
|
||||||
|
let now = start_time.elapsed().as_secs_f64();
|
||||||
|
|
||||||
|
for t_event in tablet_events {
|
||||||
|
if let octotablet::events::Event::Tool { tool, event } = t_event {
|
||||||
|
let tool_is_hardware_eraser =
|
||||||
|
tool.tool_type == Some(octotablet::tool::Type::Eraser);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
octotablet::events::ToolEvent::In { .. } => {
|
||||||
|
is_stylus_in_proximity = true;
|
||||||
|
stylus_tool_id = Some(tool.id());
|
||||||
|
}
|
||||||
|
octotablet::events::ToolEvent::Out
|
||||||
|
| octotablet::events::ToolEvent::Removed => {
|
||||||
|
is_stylus_in_proximity = false;
|
||||||
|
if stylus_tool_id == Some(tool.id()) {
|
||||||
|
stylus_tool_id = None;
|
||||||
|
if is_stylus_drawing {
|
||||||
|
is_stylus_drawing = false;
|
||||||
|
is_drawing = false;
|
||||||
|
commit_stroke_to_notebook(
|
||||||
|
&mut canvas,
|
||||||
|
&mut notebook,
|
||||||
|
&mut manifest,
|
||||||
|
&mut autosave,
|
||||||
|
&ui_state,
|
||||||
|
&window,
|
||||||
|
stroke_snapped_shape.take(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if is_erasing {
|
||||||
|
is_erasing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
octotablet::events::ToolEvent::Button { pressed, .. } => {
|
||||||
|
stylus_barrel_pressed = pressed;
|
||||||
|
if pressed && is_stylus_drawing {
|
||||||
|
is_stylus_drawing = false;
|
||||||
|
is_drawing = false;
|
||||||
|
canvas.cancel_stroke();
|
||||||
|
is_erasing = true;
|
||||||
|
window.request_redraw();
|
||||||
|
} else if !pressed
|
||||||
|
&& is_erasing
|
||||||
|
&& !tool_is_hardware_eraser
|
||||||
|
&& ui_state.active_tool != ActiveTool::Eraser
|
||||||
|
{
|
||||||
|
is_erasing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
octotablet::events::ToolEvent::Pose(pose) => {
|
||||||
|
is_stylus_in_proximity = true;
|
||||||
|
let sx = pose.position[0] * scale;
|
||||||
|
let sy = pose.position[1] * scale;
|
||||||
|
stylus_screen_pos = (sx, sy);
|
||||||
|
cursor_screen_pos = (sx, sy);
|
||||||
|
|
||||||
|
if let Some(p) = pose.pressure.get() {
|
||||||
|
current_pressure = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_erasing {
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
|
let page = notebook.current_page_mut();
|
||||||
|
let removed = page.erase_at(
|
||||||
|
page_pt,
|
||||||
|
eraser_radius,
|
||||||
|
stroke_max_radius,
|
||||||
|
);
|
||||||
|
if !removed.is_empty() {
|
||||||
|
canvas.sync_page(page);
|
||||||
|
autosave.mark_page_dirty(
|
||||||
|
notebook.current_page(),
|
||||||
|
&manifest,
|
||||||
|
);
|
||||||
|
update_window_title(
|
||||||
|
&window,
|
||||||
|
¬ebook,
|
||||||
|
&manifest,
|
||||||
|
ui_state.active_tool,
|
||||||
|
autosave.status(),
|
||||||
|
);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if is_stylus_drawing && is_drawing {
|
||||||
|
let dx = sx - stroke_hold_anchor_pos.0;
|
||||||
|
let dy = sy - stroke_hold_anchor_pos.1;
|
||||||
|
if (dx * dx + dy * dy).sqrt() > 3.0 {
|
||||||
|
stroke_last_motion_time = Instant::now();
|
||||||
|
stroke_hold_anchor_pos = (sx, sy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stroke_already_snapped {
|
||||||
|
let page_pt = canvas.screen_to_page_clamped(sx, sy);
|
||||||
|
canvas.extend_stroke(
|
||||||
|
page_pt.x,
|
||||||
|
page_pt.y,
|
||||||
|
current_pressure,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
octotablet::events::ToolEvent::Down => {
|
||||||
|
is_stylus_in_proximity = true;
|
||||||
|
stylus_tool_id = Some(tool.id());
|
||||||
|
let (sx, sy) = stylus_screen_pos;
|
||||||
|
|
||||||
|
let is_stylus_in_ui = egui_ctx.wants_pointer_input()
|
||||||
|
|| ui_state.is_point_in_ui(sx, sy)
|
||||||
|
|| (ui_state.show_page_jump_popup
|
||||||
|
&& egui_ctx.is_pointer_over_area())
|
||||||
|
|| (ui_state.is_document_menu_open
|
||||||
|
&& egui_ctx.is_pointer_over_area())
|
||||||
|
|| (ui_state.is_update_dialog_open
|
||||||
|
&& egui_ctx.is_pointer_over_area());
|
||||||
|
|
||||||
|
if is_stylus_in_ui {
|
||||||
|
egui_ctx.input_mut(|i| {
|
||||||
|
i.events.push(egui::Event::PointerMoved(egui::pos2(
|
||||||
|
sx / scale,
|
||||||
|
sy / scale,
|
||||||
|
)));
|
||||||
|
i.events.push(egui::Event::PointerButton {
|
||||||
|
pos: egui::pos2(sx / scale, sy / scale),
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
|
pressed: true,
|
||||||
|
modifiers: egui::Modifiers::default(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.request_redraw();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_erase = tool_is_hardware_eraser
|
||||||
|
|| stylus_barrel_pressed
|
||||||
|
|| ui_state.active_tool == ActiveTool::Eraser;
|
||||||
|
|
||||||
|
if should_erase {
|
||||||
|
is_erasing = true;
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
|
let page = notebook.current_page_mut();
|
||||||
|
let removed = page.erase_at(
|
||||||
|
page_pt,
|
||||||
|
eraser_radius,
|
||||||
|
stroke_max_radius,
|
||||||
|
);
|
||||||
|
if !removed.is_empty() {
|
||||||
|
canvas.sync_page(page);
|
||||||
|
autosave.mark_page_dirty(
|
||||||
|
notebook.current_page(),
|
||||||
|
&manifest,
|
||||||
|
);
|
||||||
|
update_window_title(
|
||||||
|
&window,
|
||||||
|
¬ebook,
|
||||||
|
&manifest,
|
||||||
|
ui_state.active_tool,
|
||||||
|
autosave.status(),
|
||||||
|
);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ui_state.active_tool.is_drawing()
|
||||||
|
|| ui_state.active_tool == ActiveTool::Shape
|
||||||
|
{
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
|
is_stylus_drawing = true;
|
||||||
|
is_drawing = true;
|
||||||
|
stroke_last_motion_time = Instant::now();
|
||||||
|
stroke_hold_anchor_pos = (sx, sy);
|
||||||
|
stroke_already_snapped = false;
|
||||||
|
stroke_snapped_shape = None;
|
||||||
|
canvas.start_stroke(
|
||||||
|
page_pt.x,
|
||||||
|
page_pt.y,
|
||||||
|
current_pressure,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
octotablet::events::ToolEvent::Up => {
|
||||||
|
let (sx, sy) = stylus_screen_pos;
|
||||||
|
let is_stylus_in_ui = egui_ctx.wants_pointer_input()
|
||||||
|
|| ui_state.is_point_in_ui(sx, sy)
|
||||||
|
|| (ui_state.show_page_jump_popup
|
||||||
|
&& egui_ctx.is_pointer_over_area())
|
||||||
|
|| (ui_state.is_document_menu_open
|
||||||
|
&& egui_ctx.is_pointer_over_area())
|
||||||
|
|| (ui_state.is_update_dialog_open
|
||||||
|
&& egui_ctx.is_pointer_over_area());
|
||||||
|
|
||||||
|
if is_stylus_in_ui {
|
||||||
|
egui_ctx.input_mut(|i| {
|
||||||
|
i.events.push(egui::Event::PointerButton {
|
||||||
|
pos: egui::pos2(sx / scale, sy / scale),
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
|
pressed: false,
|
||||||
|
modifiers: egui::Modifiers::default(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.request_redraw();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_erasing {
|
||||||
|
is_erasing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_stylus_drawing {
|
||||||
|
is_stylus_drawing = false;
|
||||||
|
is_drawing = false;
|
||||||
|
|
||||||
|
if !stroke_already_snapped {
|
||||||
|
let page_pt = canvas.screen_to_page_clamped(sx, sy);
|
||||||
|
canvas.extend_stroke(
|
||||||
|
page_pt.x,
|
||||||
|
page_pt.y,
|
||||||
|
current_pressure,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapped_shape =
|
||||||
|
stroke_snapped_shape.take().or_else(|| {
|
||||||
|
if ui_state.active_tool == ActiveTool::Shape {
|
||||||
|
geometry::detect_shape(&canvas.live_samples)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commit_stroke_to_notebook(
|
||||||
|
&mut canvas,
|
||||||
|
&mut notebook,
|
||||||
|
&mut manifest,
|
||||||
|
&mut autosave,
|
||||||
|
&ui_state,
|
||||||
|
&window,
|
||||||
|
snapped_shape,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw-and-Hold Überprüfung: Stift regungslos für > 400 ms gehalten (< 3 px Distanz)
|
// Draw-and-Hold Überprüfung: Stift regungslos für > 400 ms gehalten (< 3 px Distanz)
|
||||||
if is_drawing && !stroke_already_snapped {
|
if is_drawing && !stroke_already_snapped {
|
||||||
let elapsed = stroke_last_motion_time.elapsed();
|
let elapsed = stroke_last_motion_time.elapsed();
|
||||||
@@ -221,13 +550,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bei anstehenden Änderungen, aktivem Toast oder aktivem Zeichnen regelmäßig aufwachen
|
// Bei anstehenden Änderungen, aktivem Toast, Stylus in Proximity oder aktivem Zeichnen regelmäßig aufwachen
|
||||||
if new_status == AutosaveStatus::Pending
|
if new_status == AutosaveStatus::Pending
|
||||||
|| new_status == AutosaveStatus::Saving
|
|| new_status == AutosaveStatus::Saving
|
||||||
|| ui_state.export_message.is_some()
|
|| ui_state.export_message.is_some()
|
||||||
|| (is_drawing && !stroke_already_snapped)
|
|| (is_drawing && !stroke_already_snapped)
|
||||||
|
|| is_stylus_drawing
|
||||||
|
|| is_stylus_in_proximity
|
||||||
{
|
{
|
||||||
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(16)));
|
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
|
||||||
} else {
|
} else {
|
||||||
target.set_control_flow(ControlFlow::Wait);
|
target.set_control_flow(ControlFlow::Wait);
|
||||||
}
|
}
|
||||||
@@ -263,7 +594,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
is_erasing = false;
|
is_erasing = false;
|
||||||
is_mouse_drawing = false;
|
is_mouse_drawing = false;
|
||||||
active_touch_id = None;
|
active_touches.clear();
|
||||||
|
is_pinch_gesturing = false;
|
||||||
|
pinch_prev_distance = None;
|
||||||
|
pinch_prev_center = None;
|
||||||
|
is_touch_drawing = false;
|
||||||
|
is_touch_panning = false;
|
||||||
|
is_stylus_drawing = false;
|
||||||
|
is_stylus_in_proximity = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::Resized(physical_size) => {
|
WindowEvent::Resized(physical_size) => {
|
||||||
@@ -672,8 +1010,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
match state {
|
match state {
|
||||||
ElementState::Pressed => {
|
ElementState::Pressed => {
|
||||||
// Palm-Rejection: Keine Mausevents verarbeiten, wenn Touch aktiv ist
|
// Palm-Rejection: Keine Mausevents verarbeiten, wenn Stylus aktiv zeichnet
|
||||||
if active_touch_id.is_some() {
|
if is_stylus_drawing {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,9 +1206,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let now = start_time.elapsed().as_secs_f64();
|
let now = start_time.elapsed().as_secs_f64();
|
||||||
canvas.extend_stroke(page_pt.x, page_pt.y, current_pressure, now);
|
canvas.extend_stroke(page_pt.x, page_pt.y, current_pressure, now);
|
||||||
}
|
}
|
||||||
let new_id = notebook.next_stroke_id();
|
|
||||||
|
|
||||||
// Prüfen, ob Form durch Draw-and-Hold gesnappt ist oder im Shape-Modus gezeichnet wurde
|
|
||||||
let snapped_shape = stroke_snapped_shape.take().or_else(|| {
|
let snapped_shape = stroke_snapped_shape.take().or_else(|| {
|
||||||
if ui_state.active_tool == ActiveTool::Shape {
|
if ui_state.active_tool == ActiveTool::Shape {
|
||||||
geometry::detect_shape(&canvas.live_samples)
|
geometry::detect_shape(&canvas.live_samples)
|
||||||
@@ -879,24 +1214,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let stroke_opt = if let Some(shape) = snapped_shape {
|
commit_stroke_to_notebook(
|
||||||
let stroke_width = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
&mut canvas,
|
||||||
let color = canvas.current_ink_color;
|
&mut notebook,
|
||||||
canvas.cancel_stroke();
|
&mut manifest,
|
||||||
Some(geometry::shape_to_stroke(shape, new_id, color, stroke_width))
|
&mut autosave,
|
||||||
} else {
|
&ui_state,
|
||||||
canvas.finish_stroke(new_id)
|
&window,
|
||||||
};
|
snapped_shape,
|
||||||
|
);
|
||||||
if let Some(stroke) = stroke_opt {
|
|
||||||
let page = notebook.current_page_mut();
|
|
||||||
page.add_stroke(stroke);
|
|
||||||
canvas.sync_page(page);
|
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -916,8 +1242,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Palm-Rejection: Keine Cursor-Events verarbeiten, wenn Touch aktiv ist
|
// Wenn der Stylus aktiv zeichnet, keine Mauscursor-Bewegungen verarbeiten
|
||||||
if active_touch_id.is_none() {
|
if !is_stylus_drawing {
|
||||||
if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
match &mut selection_state.drag_mode {
|
match &mut selection_state.drag_mode {
|
||||||
@@ -1047,208 +1373,275 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let is_eraser_action = ui_state.active_tool == ActiveTool::Eraser;
|
let is_eraser_action = ui_state.active_tool == ActiveTool::Eraser;
|
||||||
|
|
||||||
|
// Hardware-Palm-Rejection: Wenn der Stylus in Proximity ist oder zeichnet, Touch ignorieren
|
||||||
|
if is_stylus_drawing || is_stylus_in_proximity {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match touch.phase {
|
match touch.phase {
|
||||||
TouchPhase::Started => {
|
TouchPhase::Started => {
|
||||||
// Palm-Rejection
|
|
||||||
if is_mouse_drawing || (active_touch_id.is_some() && active_touch_id != Some(touch.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event-Isolation für Touch-Eingaben
|
// Event-Isolation für Touch-Eingaben
|
||||||
let is_touch_in_ui = egui_ctx.wants_pointer_input()
|
let is_touch_in_ui = egui_ctx.wants_pointer_input()
|
||||||
|| ui_state.is_point_in_ui(tx, ty)
|
|| ui_state.is_point_in_ui(tx, ty)
|
||||||
|| (ui_state.show_page_jump_popup && egui_ctx.is_pointer_over_area())
|
|| (ui_state.show_page_jump_popup && egui_ctx.is_pointer_over_area())
|
||||||
|| (ui_state.is_document_menu_open && egui_ctx.is_pointer_over_area());
|
|| (ui_state.is_document_menu_open && egui_ctx.is_pointer_over_area())
|
||||||
|
|| (ui_state.is_update_dialog_open && egui_ctx.is_pointer_over_area());
|
||||||
if is_touch_in_ui {
|
if is_touch_in_ui {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
active_touch_id = Some(touch.id);
|
active_touches.insert(touch.id, (tx, ty));
|
||||||
current_pressure = pressure;
|
|
||||||
cursor_screen_pos = (tx, ty);
|
|
||||||
|
|
||||||
if is_eraser_action {
|
if active_touches.len() >= 2 {
|
||||||
is_erasing = true;
|
// Multi-Touch Geste: Sobald 2+ Finger aufliegen, Inking sofort abbrechen und zoomen/pannen
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
if is_touch_drawing {
|
||||||
let page = notebook.current_page_mut();
|
is_touch_drawing = false;
|
||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
is_drawing = false;
|
||||||
if !removed.is_empty() {
|
canvas.cancel_stroke();
|
||||||
canvas.sync_page(page);
|
window.request_redraw();
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if ui_state.active_tool == ActiveTool::Select {
|
is_touch_panning = false;
|
||||||
let screen_vec = Vec2::new(tx, ty);
|
is_pinch_gesturing = true;
|
||||||
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec);
|
|
||||||
match hit {
|
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
||||||
SelectionHandleHit::RotateHandle { center_page } => {
|
let p1 = pts[0];
|
||||||
|
let p2 = pts[1];
|
||||||
|
let center = ((p1.0 + p2.0) * 0.5, (p1.1 + p2.1) * 0.5);
|
||||||
|
let dist = ((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt();
|
||||||
|
pinch_prev_center = Some(center);
|
||||||
|
pinch_prev_distance = Some(dist);
|
||||||
|
} else if active_touches.len() == 1 && !is_pinch_gesturing {
|
||||||
|
cursor_screen_pos = (tx, ty);
|
||||||
|
current_pressure = pressure;
|
||||||
|
|
||||||
|
if !ui_state.touch_draw_enabled {
|
||||||
|
// Standard: 1 Finger scrollt / verschiebt den Canvas (Pan)
|
||||||
|
is_touch_panning = true;
|
||||||
|
touch_pan_last_pos = (tx, ty);
|
||||||
|
} else {
|
||||||
|
// Modus Finger-Zeichnen aktiv
|
||||||
|
if is_eraser_action {
|
||||||
|
is_erasing = true;
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
let start_angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x);
|
let page = notebook.current_page_mut();
|
||||||
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
selection_state.drag_mode = DragMode::Rotate {
|
if !removed.is_empty() {
|
||||||
center_page,
|
canvas.sync_page(page);
|
||||||
start_angle,
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
initial_strokes,
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
};
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectionHandleHit::CornerHandle {
|
|
||||||
corner_idx: _,
|
|
||||||
fixed_corner_page,
|
|
||||||
start_pos_page,
|
|
||||||
initial_box_size,
|
|
||||||
} => {
|
|
||||||
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
|
||||||
selection_state.drag_mode = DragMode::Scale {
|
|
||||||
fixed_corner_page,
|
|
||||||
start_pos_page,
|
|
||||||
initial_box_size,
|
|
||||||
initial_strokes,
|
|
||||||
};
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
SelectionHandleHit::Body => {
|
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
|
||||||
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
|
||||||
selection_state.drag_mode = DragMode::Move {
|
|
||||||
start_page: page_pt,
|
|
||||||
initial_strokes,
|
|
||||||
};
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectionHandleHit::None => {
|
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
|
||||||
if let Some(stroke) = notebook.current_page().find_stroke_at(page_pt, 25.0) {
|
|
||||||
let stroke_id = stroke.id;
|
|
||||||
if !selection_state.selected_ids.contains(&stroke_id) {
|
|
||||||
selection_state.select_single(stroke_id);
|
|
||||||
}
|
|
||||||
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
|
||||||
selection_state.drag_mode = DragMode::Move {
|
|
||||||
start_page: page_pt,
|
|
||||||
initial_strokes,
|
|
||||||
};
|
|
||||||
window.request_redraw();
|
|
||||||
} else {
|
|
||||||
selection_state.clear();
|
|
||||||
selection_state.drag_mode = DragMode::BoxSelect {
|
|
||||||
start_page: page_pt,
|
|
||||||
current_page: page_pt,
|
|
||||||
};
|
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if ui_state.active_tool == ActiveTool::Select {
|
||||||
|
let screen_vec = Vec2::new(tx, ty);
|
||||||
|
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec);
|
||||||
|
match hit {
|
||||||
|
SelectionHandleHit::RotateHandle { center_page } => {
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
|
let start_angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x);
|
||||||
|
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||||
|
selection_state.drag_mode = DragMode::Rotate {
|
||||||
|
center_page,
|
||||||
|
start_angle,
|
||||||
|
initial_strokes,
|
||||||
|
};
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectionHandleHit::CornerHandle {
|
||||||
|
corner_idx: _,
|
||||||
|
fixed_corner_page,
|
||||||
|
start_pos_page,
|
||||||
|
initial_box_size,
|
||||||
|
} => {
|
||||||
|
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||||
|
selection_state.drag_mode = DragMode::Scale {
|
||||||
|
fixed_corner_page,
|
||||||
|
start_pos_page,
|
||||||
|
initial_box_size,
|
||||||
|
initial_strokes,
|
||||||
|
};
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
SelectionHandleHit::Body => {
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
|
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||||
|
selection_state.drag_mode = DragMode::Move {
|
||||||
|
start_page: page_pt,
|
||||||
|
initial_strokes,
|
||||||
|
};
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectionHandleHit::None => {
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
|
if let Some(stroke) = notebook.current_page().find_stroke_at(page_pt, 25.0) {
|
||||||
|
let stroke_id = stroke.id;
|
||||||
|
if !selection_state.selected_ids.contains(&stroke_id) {
|
||||||
|
selection_state.select_single(stroke_id);
|
||||||
|
}
|
||||||
|
let initial_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||||
|
selection_state.drag_mode = DragMode::Move {
|
||||||
|
start_page: page_pt,
|
||||||
|
initial_strokes,
|
||||||
|
};
|
||||||
|
window.request_redraw();
|
||||||
|
} else {
|
||||||
|
selection_state.clear();
|
||||||
|
selection_state.drag_mode = DragMode::BoxSelect {
|
||||||
|
start_page: page_pt,
|
||||||
|
current_page: page_pt,
|
||||||
|
};
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ui_state.active_tool.is_drawing() || ui_state.active_tool == ActiveTool::Shape {
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
|
is_touch_drawing = true;
|
||||||
|
is_drawing = true;
|
||||||
|
stroke_last_motion_time = Instant::now();
|
||||||
|
stroke_hold_anchor_pos = (tx, ty);
|
||||||
|
stroke_already_snapped = false;
|
||||||
|
stroke_snapped_shape = None;
|
||||||
|
canvas.start_stroke(page_pt.x, page_pt.y, pressure, now);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ui_state.active_tool.is_drawing() || ui_state.active_tool == ActiveTool::Shape {
|
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
|
||||||
is_drawing = true;
|
|
||||||
stroke_last_motion_time = Instant::now();
|
|
||||||
stroke_hold_anchor_pos = (tx, ty);
|
|
||||||
stroke_already_snapped = false;
|
|
||||||
stroke_snapped_shape = None;
|
|
||||||
canvas.start_stroke(page_pt.x, page_pt.y, pressure, now);
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TouchPhase::Moved => {
|
TouchPhase::Moved => {
|
||||||
if is_mouse_drawing || active_touch_id != Some(touch.id) {
|
active_touches.insert(touch.id, (tx, ty));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_pressure = pressure;
|
if active_touches.len() >= 2 {
|
||||||
cursor_screen_pos = (tx, ty);
|
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
||||||
|
let p1 = pts[0];
|
||||||
|
let p2 = pts[1];
|
||||||
|
let curr_center = ((p1.0 + p2.0) * 0.5, (p1.1 + p2.1) * 0.5);
|
||||||
|
let curr_dist = ((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt();
|
||||||
|
|
||||||
if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
if let (Some(prev_dist), Some(prev_center)) = (pinch_prev_distance, pinch_prev_center) {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
let dx = curr_center.0 - prev_center.0;
|
||||||
match &mut selection_state.drag_mode {
|
let dy = curr_center.1 - prev_center.1;
|
||||||
DragMode::BoxSelect { current_page, .. } => {
|
if dx.abs() > 0.05 || dy.abs() > 0.05 {
|
||||||
*current_page = page_pt;
|
canvas.pan(dx, dy);
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
DragMode::Move { start_page, initial_strokes } => {
|
|
||||||
let delta = page_pt - *start_page;
|
|
||||||
let mut updated = initial_strokes.clone();
|
|
||||||
for s in &mut updated {
|
|
||||||
s.translate(delta);
|
|
||||||
}
|
|
||||||
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
DragMode::Rotate { center_page, start_angle, initial_strokes } => {
|
|
||||||
let current_angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x);
|
|
||||||
let delta_angle = current_angle - *start_angle;
|
|
||||||
let mut updated = initial_strokes.clone();
|
|
||||||
for s in &mut updated {
|
|
||||||
s.rotate(*center_page, delta_angle);
|
|
||||||
}
|
|
||||||
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
DragMode::Scale { fixed_corner_page, start_pos_page, initial_box_size: _, initial_strokes } => {
|
|
||||||
let orig_delta = *start_pos_page - *fixed_corner_page;
|
|
||||||
let new_delta = page_pt - *fixed_corner_page;
|
|
||||||
let scale_x = if orig_delta.x.abs() > 1.0 {
|
|
||||||
(new_delta.x / orig_delta.x).clamp(0.05, 20.0)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let scale_y = if orig_delta.y.abs() > 1.0 {
|
|
||||||
(new_delta.y / orig_delta.y).clamp(0.05, 20.0)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let factors = Vec2::new(scale_x, scale_y);
|
|
||||||
let mut updated = initial_strokes.clone();
|
|
||||||
for s in &mut updated {
|
|
||||||
s.scale(*fixed_corner_page, factors);
|
|
||||||
}
|
|
||||||
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
DragMode::None => {}
|
|
||||||
}
|
}
|
||||||
|
if prev_dist > 5.0 && curr_dist > 5.0 {
|
||||||
|
let zoom_factor = curr_dist / prev_dist;
|
||||||
|
if (zoom_factor - 1.0).abs() > 0.001 {
|
||||||
|
canvas.zoom_at(curr_center.0, curr_center.1, zoom_factor);
|
||||||
|
ui_state.current_zoom = canvas.camera.zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
} else if is_erasing {
|
pinch_prev_center = Some(curr_center);
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
pinch_prev_distance = Some(curr_dist);
|
||||||
let page = notebook.current_page_mut();
|
} else if active_touches.len() == 1 && !is_pinch_gesturing {
|
||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
if is_touch_panning {
|
||||||
if !removed.is_empty() {
|
let dx = tx - touch_pan_last_pos.0;
|
||||||
canvas.sync_page(page);
|
let dy = ty - touch_pan_last_pos.1;
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
touch_pan_last_pos = (tx, ty);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
canvas.pan(dx, dy);
|
||||||
|
window.request_redraw();
|
||||||
|
} else if is_touch_drawing {
|
||||||
|
current_pressure = pressure;
|
||||||
|
cursor_screen_pos = (tx, ty);
|
||||||
|
|
||||||
|
let dx = tx - stroke_hold_anchor_pos.0;
|
||||||
|
let dy = ty - stroke_hold_anchor_pos.1;
|
||||||
|
if (dx * dx + dy * dy).sqrt() > 3.0 {
|
||||||
|
stroke_last_motion_time = Instant::now();
|
||||||
|
stroke_hold_anchor_pos = (tx, ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stroke_already_snapped {
|
||||||
|
let page_pt = canvas.screen_to_page_clamped(tx, ty);
|
||||||
|
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
} else if is_erasing {
|
||||||
} else if is_drawing {
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
let dx = tx - stroke_hold_anchor_pos.0;
|
let page = notebook.current_page_mut();
|
||||||
let dy = ty - stroke_hold_anchor_pos.1;
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
if (dx * dx + dy * dy).sqrt() > 3.0 {
|
if !removed.is_empty() {
|
||||||
stroke_last_motion_time = Instant::now();
|
canvas.sync_page(page);
|
||||||
stroke_hold_anchor_pos = (tx, ty);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
}
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
|
window.request_redraw();
|
||||||
if !stroke_already_snapped {
|
}
|
||||||
let page_pt = canvas.screen_to_page_clamped(tx, ty);
|
}
|
||||||
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
} else if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
||||||
window.request_redraw();
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
|
match &mut selection_state.drag_mode {
|
||||||
|
DragMode::BoxSelect { current_page, .. } => {
|
||||||
|
*current_page = page_pt;
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
DragMode::Move { start_page, initial_strokes } => {
|
||||||
|
let delta = page_pt - *start_page;
|
||||||
|
let mut updated = initial_strokes.clone();
|
||||||
|
for s in &mut updated {
|
||||||
|
s.translate(delta);
|
||||||
|
}
|
||||||
|
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
||||||
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
DragMode::Rotate { center_page, start_angle, initial_strokes } => {
|
||||||
|
let current_angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x);
|
||||||
|
let delta_angle = current_angle - *start_angle;
|
||||||
|
let mut updated = initial_strokes.clone();
|
||||||
|
for s in &mut updated {
|
||||||
|
s.rotate(*center_page, delta_angle);
|
||||||
|
}
|
||||||
|
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
||||||
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
DragMode::Scale { fixed_corner_page, start_pos_page, initial_box_size: _, initial_strokes } => {
|
||||||
|
let orig_delta = *start_pos_page - *fixed_corner_page;
|
||||||
|
let new_delta = page_pt - *fixed_corner_page;
|
||||||
|
let scale_x = if orig_delta.x.abs() > 1.0 {
|
||||||
|
(new_delta.x / orig_delta.x).clamp(0.05, 20.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let scale_y = if orig_delta.y.abs() > 1.0 {
|
||||||
|
(new_delta.y / orig_delta.y).clamp(0.05, 20.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let factors = Vec2::new(scale_x, scale_y);
|
||||||
|
let mut updated = initial_strokes.clone();
|
||||||
|
for s in &mut updated {
|
||||||
|
s.scale(*fixed_corner_page, factors);
|
||||||
|
}
|
||||||
|
notebook.current_page_mut().update_strokes_ephemeral(&updated);
|
||||||
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
DragMode::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TouchPhase::Ended => {
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
if active_touch_id == Some(touch.id) {
|
active_touches.remove(&touch.id);
|
||||||
active_touch_id = None;
|
|
||||||
|
if active_touches.is_empty() {
|
||||||
|
is_pinch_gesturing = false;
|
||||||
|
pinch_prev_distance = None;
|
||||||
|
pinch_prev_center = None;
|
||||||
|
is_touch_panning = false;
|
||||||
|
|
||||||
if is_erasing {
|
if is_erasing {
|
||||||
is_erasing = false;
|
is_erasing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui_state.active_tool == ActiveTool::Select {
|
if ui_state.active_tool == ActiveTool::Select {
|
||||||
match std::mem::take(&mut selection_state.drag_mode) {
|
match std::mem::take(&mut selection_state.drag_mode) {
|
||||||
DragMode::BoxSelect { start_page, current_page } => {
|
DragMode::BoxSelect { start_page, current_page } => {
|
||||||
@@ -1273,14 +1666,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
DragMode::None => {}
|
DragMode::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if is_drawing {
|
|
||||||
|
if is_touch_drawing {
|
||||||
|
is_touch_drawing = false;
|
||||||
is_drawing = false;
|
is_drawing = false;
|
||||||
|
|
||||||
if !stroke_already_snapped {
|
if !stroke_already_snapped {
|
||||||
let page_pt = canvas.screen_to_page_clamped(tx, ty);
|
let page_pt = canvas.screen_to_page_clamped(tx, ty);
|
||||||
let now = start_time.elapsed().as_secs_f64();
|
|
||||||
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
||||||
}
|
}
|
||||||
let new_id = notebook.next_stroke_id();
|
|
||||||
|
|
||||||
let snapped_shape = stroke_snapped_shape.take().or_else(|| {
|
let snapped_shape = stroke_snapped_shape.take().or_else(|| {
|
||||||
if ui_state.active_tool == ActiveTool::Shape {
|
if ui_state.active_tool == ActiveTool::Shape {
|
||||||
@@ -1290,37 +1684,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let stroke_opt = if let Some(shape) = snapped_shape {
|
commit_stroke_to_notebook(
|
||||||
let stroke_width = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
&mut canvas,
|
||||||
let color = canvas.current_ink_color;
|
&mut notebook,
|
||||||
canvas.cancel_stroke();
|
&mut manifest,
|
||||||
Some(geometry::shape_to_stroke(shape, new_id, color, stroke_width))
|
&mut autosave,
|
||||||
} else {
|
&ui_state,
|
||||||
canvas.finish_stroke(new_id)
|
&window,
|
||||||
};
|
snapped_shape,
|
||||||
|
);
|
||||||
if let Some(stroke) = stroke_opt {
|
|
||||||
let page = notebook.current_page_mut();
|
|
||||||
page.add_stroke(stroke);
|
|
||||||
canvas.sync_page(page);
|
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if active_touches.len() == 1 {
|
||||||
}
|
pinch_prev_distance = None;
|
||||||
TouchPhase::Cancelled => {
|
pinch_prev_center = None;
|
||||||
if active_touch_id == Some(touch.id) {
|
let rem = *active_touches.values().next().unwrap();
|
||||||
active_touch_id = None;
|
touch_pan_last_pos = rem;
|
||||||
selection_state.drag_mode = DragMode::None;
|
|
||||||
if is_drawing {
|
|
||||||
is_drawing = false;
|
|
||||||
canvas.cancel_stroke();
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
is_erasing = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ pub struct UiState {
|
|||||||
pub updater_token_input: String,
|
pub updater_token_input: String,
|
||||||
pub show_updater_config: bool,
|
pub show_updater_config: bool,
|
||||||
pub update_modal_rect: Option<egui::Rect>,
|
pub update_modal_rect: Option<egui::Rect>,
|
||||||
|
/// Steuert, ob Touch-Eingaben mit einem Finger zeichnen oder das Dokument verschieben (Default: false = reine Gesten-Navigation & Zoom)
|
||||||
|
pub touch_draw_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -190,6 +192,7 @@ impl Default for UiState {
|
|||||||
updater_token_input: String::new(),
|
updater_token_input: String::new(),
|
||||||
show_updater_config: false,
|
show_updater_config: false,
|
||||||
update_modal_rect: None,
|
update_modal_rect: None,
|
||||||
|
touch_draw_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -950,7 +953,19 @@ pub fn render_ui(
|
|||||||
|
|
||||||
toolbar_divider(ui);
|
toolbar_divider(ui);
|
||||||
|
|
||||||
// --- 9. AUTOSAVE-STATUSANZEIGE ---
|
// --- 9. TOUCH-MODUS UMSCHALTER (Gesten vs. Zeichnen) ---
|
||||||
|
let (touch_icon, touch_tt) = if state.touch_draw_enabled {
|
||||||
|
("✏️ Touch: Stift", "Touch-Modus: Einzelfinger zeichnet, zwei Finger zoomen/verschieben. Klicken zum Umschalten auf reine Gesten-Navigation.")
|
||||||
|
} else {
|
||||||
|
("🖐️ Touch: Zoom/Pan", "Touch-Modus: Finger zoomen und verschieben die Seite (ideal für Stifte wie Lazarite!). Klicken für Finger-Zeichnen.")
|
||||||
|
};
|
||||||
|
if action_pill_button(ui, true, touch_icon, touch_tt).clicked() {
|
||||||
|
state.touch_draw_enabled = !state.touch_draw_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar_divider(ui);
|
||||||
|
|
||||||
|
// --- 10. AUTOSAVE-STATUSANZEIGE ---
|
||||||
let (dot_color, label_text) = match autosave_status {
|
let (dot_color, label_text) = match autosave_status {
|
||||||
AutosaveStatus::Saved => (egui::Color32::from_rgb(46, 204, 113), "Gespeichert"),
|
AutosaveStatus::Saved => (egui::Color32::from_rgb(46, 204, 113), "Gespeichert"),
|
||||||
AutosaveStatus::Saving => (egui::Color32::from_rgb(241, 196, 15), "Speichert..."),
|
AutosaveStatus::Saving => (egui::Color32::from_rgb(241, 196, 15), "Speichert..."),
|
||||||
@@ -2083,4 +2098,13 @@ mod tests {
|
|||||||
assert!(state.is_point_in_ui(rect.center().x, rect.center().y));
|
assert!(state.is_point_in_ui(rect.center().x, rect.center().y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_touch_draw_mode_toggle() {
|
||||||
|
let mut state = UiState::new();
|
||||||
|
assert!(!state.touch_draw_enabled); // Default: false = Gesten-Navigation & Zoom
|
||||||
|
|
||||||
|
state.touch_draw_enabled = true;
|
||||||
|
assert!(state.touch_draw_enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,4 +246,40 @@ mod tests {
|
|||||||
assert!((offset1[0] - offset2[0]).abs() < 1e-5);
|
assert!((offset1[0] - offset2[0]).abs() < 1e-5);
|
||||||
assert!((offset1[1] - offset2[1]).abs() < 1e-5);
|
assert!((offset1[1] - offset2[1]).abs() < 1e-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pinch_to_zoom_math() {
|
||||||
|
let mut vp = ViewportState::new();
|
||||||
|
let page_vp = PageViewport::calculate(1920.0, 1080.0, 24.0);
|
||||||
|
|
||||||
|
// Simulation von 2 Fingern:
|
||||||
|
// Finger 1 bei (400, 500), Finger 2 bei (600, 500) -> Distanz: 200, Zentrum: (500, 500)
|
||||||
|
let p1_start = (400.0_f32, 500.0_f32);
|
||||||
|
let p2_start = (600.0_f32, 500.0_f32);
|
||||||
|
let dist_start =
|
||||||
|
((p2_start.0 - p1_start.0).powi(2) + (p2_start.1 - p1_start.1).powi(2)).sqrt();
|
||||||
|
let center_start = (
|
||||||
|
(p1_start.0 + p2_start.0) * 0.5,
|
||||||
|
(p1_start.1 + p2_start.1) * 0.5,
|
||||||
|
);
|
||||||
|
assert_eq!(dist_start, 200.0);
|
||||||
|
assert_eq!(center_start, (500.0, 500.0));
|
||||||
|
|
||||||
|
// Finger auseinanderziehen und leicht verschieben:
|
||||||
|
// Finger 1 bei (350, 520), Finger 2 bei (650, 520) -> Distanz: 300, Zentrum: (500, 520)
|
||||||
|
let p1_end = (350.0_f32, 520.0_f32);
|
||||||
|
let p2_end = (650.0_f32, 520.0_f32);
|
||||||
|
let dist_end = ((p2_end.0 - p1_end.0).powi(2) + (p2_end.1 - p1_end.1).powi(2)).sqrt();
|
||||||
|
let center_end = ((p1_end.0 + p2_end.0) * 0.5, (p1_end.1 + p2_end.1) * 0.5);
|
||||||
|
assert_eq!(dist_end, 300.0);
|
||||||
|
assert_eq!(center_end, (500.0, 520.0));
|
||||||
|
|
||||||
|
let zoom_factor = dist_end / dist_start; // 1.5x Vergrößerung
|
||||||
|
let pan_delta = (center_end.0 - center_start.0, center_end.1 - center_start.1); // (0, +20)
|
||||||
|
|
||||||
|
vp.pan_by_screen_delta(pan_delta.0, pan_delta.1, &page_vp);
|
||||||
|
vp.zoom_at_cursor([center_end.0, center_end.1], zoom_factor, &page_vp);
|
||||||
|
|
||||||
|
assert!((vp.zoom - 1.5).abs() < 1e-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user