fix(updater): Sichere In-App Updates mit ureq, SHA256-Integrität und atomarem AppImage-Tausch
This commit is contained in:
Generated
+211
-1
@@ -118,6 +118,12 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -151,6 +157,15 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-sys"
|
name = "block-sys"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -375,6 +390,15 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@@ -390,6 +414,16 @@ version = "0.8.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cursor-icon"
|
name = "cursor-icon"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -403,7 +437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
|
checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"libloading 0.7.4",
|
"libloading 0.8.9",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -413,6 +447,16 @@ version = "0.5.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -644,6 +688,16 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gethostname"
|
name = "gethostname"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -654,6 +708,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -958,9 +1023,13 @@ dependencies = [
|
|||||||
"glam",
|
"glam",
|
||||||
"pollster",
|
"pollster",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"ureq",
|
||||||
|
"url",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
@@ -1609,6 +1678,20 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1647,6 +1730,41 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -1687,6 +1805,12 @@ dependencies = [
|
|||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -1730,6 +1854,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -1830,6 +1965,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 = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
@@ -2041,6 +2182,12 @@ dependencies = [
|
|||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
@@ -2065,6 +2212,30 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"flate2",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -2099,6 +2270,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.4+wasi-0.2.12"
|
version = "1.0.4+wasi-0.2.12"
|
||||||
@@ -2310,6 +2487,24 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -2503,6 +2698,15 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -2895,6 +3099,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ egui = "0.27"
|
|||||||
egui-wgpu = "0.27"
|
egui-wgpu = "0.27"
|
||||||
egui-winit = { version = "0.27", default-features = false, features = ["links"] }
|
egui-winit = { version = "0.27", default-features = false, features = ["links"] }
|
||||||
webbrowser = "0.8"
|
webbrowser = "0.8"
|
||||||
|
semver = "1.0"
|
||||||
|
sha2 = "0.10"
|
||||||
|
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
[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
|
||||||
|
|||||||
+7
-2
@@ -165,13 +165,18 @@ impl AutosaveController {
|
|||||||
}
|
}
|
||||||
AutosaveResponse::ManifestSaved { result } => {
|
AutosaveResponse::ManifestSaved { result } => {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
eprintln!("[Autosave-Fehler] Manifest konnte nicht gespeichert werden: {e}");
|
eprintln!(
|
||||||
|
"[Autosave-Fehler] Manifest konnte nicht gespeichert werden: {e}"
|
||||||
|
);
|
||||||
self.status = AutosaveStatus::Error;
|
self.status = AutosaveStatus::Error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.in_flight_count == 0 && self.pending_pages.is_empty() && self.status != AutosaveStatus::Error {
|
if self.in_flight_count == 0
|
||||||
|
&& self.pending_pages.is_empty()
|
||||||
|
&& self.status != AutosaveStatus::Error
|
||||||
|
{
|
||||||
self.status = AutosaveStatus::Saved;
|
self.status = AutosaveStatus::Saved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-9
@@ -39,13 +39,7 @@ pub fn generate_paper_quad() -> Vec<InkVertex> {
|
|||||||
|
|
||||||
/// Fügt ein Quad (2 Dreiecke) zur Vertexliste hinzu.
|
/// Fügt ein Quad (2 Dreiecke) zur Vertexliste hinzu.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn push_quad(
|
fn push_quad(vertices: &mut Vec<InkVertex>, x_min: f32, y_min: f32, x_max: f32, y_max: f32) {
|
||||||
vertices: &mut Vec<InkVertex>,
|
|
||||||
x_min: f32,
|
|
||||||
y_min: f32,
|
|
||||||
x_max: f32,
|
|
||||||
y_max: f32,
|
|
||||||
) {
|
|
||||||
vertices.push(InkVertex::new(x_min, y_min, BACKGROUND_PATTERN_COLOR));
|
vertices.push(InkVertex::new(x_min, y_min, BACKGROUND_PATTERN_COLOR));
|
||||||
vertices.push(InkVertex::new(x_max, y_min, BACKGROUND_PATTERN_COLOR));
|
vertices.push(InkVertex::new(x_max, y_min, BACKGROUND_PATTERN_COLOR));
|
||||||
vertices.push(InkVertex::new(x_min, y_max, BACKGROUND_PATTERN_COLOR));
|
vertices.push(InkVertex::new(x_min, y_max, BACKGROUND_PATTERN_COLOR));
|
||||||
@@ -102,7 +96,13 @@ pub fn generate_background_mesh(background: PageBackground) -> Vec<InkVertex> {
|
|||||||
while y < PAGE_HEIGHT {
|
while y < PAGE_HEIGHT {
|
||||||
let mut x = GRID_5MM_SPACING;
|
let mut x = GRID_5MM_SPACING;
|
||||||
while x < PAGE_WIDTH {
|
while x < PAGE_WIDTH {
|
||||||
push_quad(&mut vertices, x - half_d, y - half_d, x + half_d, y + half_d);
|
push_quad(
|
||||||
|
&mut vertices,
|
||||||
|
x - half_d,
|
||||||
|
y - half_d,
|
||||||
|
x + half_d,
|
||||||
|
y + half_d,
|
||||||
|
);
|
||||||
x += GRID_5MM_SPACING;
|
x += GRID_5MM_SPACING;
|
||||||
}
|
}
|
||||||
y += GRID_5MM_SPACING;
|
y += GRID_5MM_SPACING;
|
||||||
@@ -132,7 +132,11 @@ mod tests {
|
|||||||
|
|
||||||
let grid = generate_background_mesh(PageBackground::Grid5mm);
|
let grid = generate_background_mesh(PageBackground::Grid5mm);
|
||||||
assert!(!grid.is_empty());
|
assert!(!grid.is_empty());
|
||||||
assert_eq!(grid.len() % 6, 0, "Muss ein Vielfaches von 6 Vertizes sein (Dreiecke)");
|
assert_eq!(
|
||||||
|
grid.len() % 6,
|
||||||
|
0,
|
||||||
|
"Muss ein Vielfaches von 6 Vertizes sein (Dreiecke)"
|
||||||
|
);
|
||||||
|
|
||||||
let lined = generate_background_mesh(PageBackground::Lined);
|
let lined = generate_background_mesh(PageBackground::Lined);
|
||||||
assert!(!lined.is_empty());
|
assert!(!lined.is_empty());
|
||||||
|
|||||||
+35
-23
@@ -11,9 +11,7 @@ use std::sync::Arc;
|
|||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
use crate::background::{
|
use crate::background::{generate_background_mesh, generate_paper_quad, DESK_COLOR};
|
||||||
generate_background_mesh, generate_paper_quad, DESK_COLOR,
|
|
||||||
};
|
|
||||||
use crate::document::{Page, PageBackground, PageViewport, Stroke, StylusSample};
|
use crate::document::{Page, PageBackground, PageViewport, Stroke, StylusSample};
|
||||||
use crate::filter::OneEuroFilter;
|
use crate::filter::OneEuroFilter;
|
||||||
use crate::spline::{self, ControlPoint, SplineConfig};
|
use crate::spline::{self, ControlPoint, SplineConfig};
|
||||||
@@ -143,7 +141,8 @@ impl CanvasState {
|
|||||||
/// Initialisiert den CanvasState für das übergebene Winit-Fenster.
|
/// Initialisiert den CanvasState für das übergebene Winit-Fenster.
|
||||||
pub async fn new(window: Arc<Window>) -> Result<Self, Box<dyn std::error::Error>> {
|
pub async fn new(window: Arc<Window>) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
let viewport = PageViewport::calculate(size.width as f32, size.height as f32, Self::VIEWPORT_MARGIN);
|
let viewport =
|
||||||
|
PageViewport::calculate(size.width as f32, size.height as f32, Self::VIEWPORT_MARGIN);
|
||||||
|
|
||||||
// 1. WGPU Instance & Surface
|
// 1. WGPU Instance & Surface
|
||||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
@@ -187,7 +186,10 @@ impl CanvasState {
|
|||||||
let present_mode = if std::env::var("INK_FORCE_FIFO").is_ok() {
|
let present_mode = if std::env::var("INK_FORCE_FIFO").is_ok() {
|
||||||
eprintln!("Erzwinge PresentMode::Fifo (via INK_FORCE_FIFO)");
|
eprintln!("Erzwinge PresentMode::Fifo (via INK_FORCE_FIFO)");
|
||||||
wgpu::PresentMode::Fifo
|
wgpu::PresentMode::Fifo
|
||||||
} else if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
|
} else if surface_caps
|
||||||
|
.present_modes
|
||||||
|
.contains(&wgpu::PresentMode::Mailbox)
|
||||||
|
{
|
||||||
eprintln!("Verwende Zero-Latency PresentMode::Mailbox");
|
eprintln!("Verwende Zero-Latency PresentMode::Mailbox");
|
||||||
wgpu::PresentMode::Mailbox
|
wgpu::PresentMode::Mailbox
|
||||||
} else {
|
} else {
|
||||||
@@ -358,12 +360,7 @@ impl CanvasState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 8. egui Renderer für das schwebende UI initialisieren
|
// 8. egui Renderer für das schwebende UI initialisieren
|
||||||
let egui_renderer = egui_wgpu::Renderer::new(
|
let egui_renderer = egui_wgpu::Renderer::new(&device, config.format, None, 1);
|
||||||
&device,
|
|
||||||
config.format,
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
surface,
|
surface,
|
||||||
@@ -410,7 +407,11 @@ impl CanvasState {
|
|||||||
transform_scale: scale,
|
transform_scale: scale,
|
||||||
transform_offset: offset,
|
transform_offset: offset,
|
||||||
};
|
};
|
||||||
self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[camera_uniforms]));
|
self.queue.write_buffer(
|
||||||
|
&self.camera_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&[camera_uniforms]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Passt die Surface und Projektionsmatrizen an eine neue Fenstergröße an.
|
/// Passt die Surface und Projektionsmatrizen an eine neue Fenstergröße an.
|
||||||
@@ -433,13 +434,15 @@ impl CanvasState {
|
|||||||
|
|
||||||
/// Verschiebt den Bildausschnitt um Bildschirm-Pixel delta.
|
/// Verschiebt den Bildausschnitt um Bildschirm-Pixel delta.
|
||||||
pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
|
pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
|
||||||
self.camera.pan_by_screen_delta(delta_x, delta_y, &self.viewport);
|
self.camera
|
||||||
|
.pan_by_screen_delta(delta_x, delta_y, &self.viewport);
|
||||||
self.update_uniforms();
|
self.update_uniforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zoomt zentriert auf eine Cursor-Bildschirmposition.
|
/// Zoomt zentriert auf eine Cursor-Bildschirmposition.
|
||||||
pub fn zoom_at(&mut self, cursor_x: f32, cursor_y: f32, factor: f32) {
|
pub fn zoom_at(&mut self, cursor_x: f32, cursor_y: f32, factor: f32) {
|
||||||
self.camera.zoom_at_cursor([cursor_x, cursor_y], factor, &self.viewport);
|
self.camera
|
||||||
|
.zoom_at_cursor([cursor_x, cursor_y], factor, &self.viewport);
|
||||||
self.update_uniforms();
|
self.update_uniforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,12 +454,14 @@ impl CanvasState {
|
|||||||
|
|
||||||
/// Transformiert Fenster-Pixel in Seitenkoordinaten.
|
/// Transformiert Fenster-Pixel in Seitenkoordinaten.
|
||||||
pub fn screen_to_page(&self, screen_x: f32, screen_y: f32) -> Option<glam::Vec2> {
|
pub fn screen_to_page(&self, screen_x: f32, screen_y: f32) -> Option<glam::Vec2> {
|
||||||
self.camera.screen_to_page(screen_x, screen_y, &self.viewport)
|
self.camera
|
||||||
|
.screen_to_page(screen_x, screen_y, &self.viewport)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transformiert Fenster-Pixel in Seitenkoordinaten und klemmt auf die Seite.
|
/// Transformiert Fenster-Pixel in Seitenkoordinaten und klemmt auf die Seite.
|
||||||
pub fn screen_to_page_clamped(&self, screen_x: f32, screen_y: f32) -> glam::Vec2 {
|
pub fn screen_to_page_clamped(&self, screen_x: f32, screen_y: f32) -> glam::Vec2 {
|
||||||
self.camera.screen_to_page_clamped(screen_x, screen_y, &self.viewport)
|
self.camera
|
||||||
|
.screen_to_page_clamped(screen_x, screen_y, &self.viewport)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transformiert einen Punkt aus DIN-A4-Seitenkoordinaten in Fenster-Bildschirmkoordinaten.
|
/// Transformiert einen Punkt aus DIN-A4-Seitenkoordinaten in Fenster-Bildschirmkoordinaten.
|
||||||
@@ -471,7 +476,8 @@ impl CanvasState {
|
|||||||
self.live_vertex_count = self.live_vertices.len() as u32;
|
self.live_vertex_count = self.live_vertices.len() as u32;
|
||||||
|
|
||||||
if self.live_vertices.len() > self.live_vertex_capacity {
|
if self.live_vertices.len() > self.live_vertex_capacity {
|
||||||
self.live_vertex_capacity = (self.live_vertices.len() * 2).max(Self::INITIAL_LIVE_CAPACITY);
|
self.live_vertex_capacity =
|
||||||
|
(self.live_vertices.len() * 2).max(Self::INITIAL_LIVE_CAPACITY);
|
||||||
self.live_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
self.live_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Live Stroke Vertex Buffer (Reallocated)"),
|
label: Some("Live Stroke Vertex Buffer (Reallocated)"),
|
||||||
size: (self.live_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
size: (self.live_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
||||||
@@ -522,7 +528,8 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !mesh.is_empty() {
|
if !mesh.is_empty() {
|
||||||
self.queue.write_buffer(&self.background_buffer, 0, bytemuck::cast_slice(&mesh));
|
self.queue
|
||||||
|
.write_buffer(&self.background_buffer, 0, bytemuck::cast_slice(&mesh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +546,8 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if page.vertex_cache.len() > self.committed_vertex_capacity {
|
if page.vertex_cache.len() > self.committed_vertex_capacity {
|
||||||
self.committed_vertex_capacity = (page.vertex_cache.len() * 2).max(Self::INITIAL_COMMITTED_CAPACITY);
|
self.committed_vertex_capacity =
|
||||||
|
(page.vertex_cache.len() * 2).max(Self::INITIAL_COMMITTED_CAPACITY);
|
||||||
self.committed_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
self.committed_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Committed Strokes Vertex Buffer (Reallocated)"),
|
label: Some("Committed Strokes Vertex Buffer (Reallocated)"),
|
||||||
size: (self.committed_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
size: (self.committed_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
||||||
@@ -655,7 +663,8 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let color = self.current_ink_color;
|
let color = self.current_ink_color;
|
||||||
let raw_strip = spline::generate_triangle_strip(&self.live_control_points, &self.spline_config);
|
let raw_strip =
|
||||||
|
spline::generate_triangle_strip(&self.live_control_points, &self.spline_config);
|
||||||
self.live_vertices = raw_strip
|
self.live_vertices = raw_strip
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| InkVertex::new(v.position[0], v.position[1], color))
|
.map(|v| InkVertex::new(v.position[0], v.position[1], color))
|
||||||
@@ -667,7 +676,8 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.live_vertices.len() > self.live_vertex_capacity {
|
if self.live_vertices.len() > self.live_vertex_capacity {
|
||||||
self.live_vertex_capacity = (self.live_vertices.len() * 2).max(Self::INITIAL_LIVE_CAPACITY);
|
self.live_vertex_capacity =
|
||||||
|
(self.live_vertices.len() * 2).max(Self::INITIAL_LIVE_CAPACITY);
|
||||||
self.live_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
self.live_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Live Stroke Vertex Buffer (Reallocated)"),
|
label: Some("Live Stroke Vertex Buffer (Reallocated)"),
|
||||||
size: (self.live_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
size: (self.live_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
||||||
@@ -694,7 +704,8 @@ impl CanvasState {
|
|||||||
) -> Result<(), wgpu::SurfaceError> {
|
) -> Result<(), wgpu::SurfaceError> {
|
||||||
// Vor dem Renderpass: egui-Texturen aktualisieren
|
// Vor dem Renderpass: egui-Texturen aktualisieren
|
||||||
for (id, image_delta) in &textures_delta.set {
|
for (id, image_delta) in &textures_delta.set {
|
||||||
self.egui_renderer.update_texture(&self.device, &self.queue, *id, image_delta);
|
self.egui_renderer
|
||||||
|
.update_texture(&self.device, &self.queue, *id, image_delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = self.surface.get_current_texture()?;
|
let output = self.surface.get_current_texture()?;
|
||||||
@@ -784,7 +795,8 @@ impl CanvasState {
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.egui_renderer.render(&mut egui_pass, clipped_primitives, screen_descriptor);
|
self.egui_renderer
|
||||||
|
.render(&mut egui_pass, clipped_primitives, screen_descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
|||||||
+75
-21
@@ -115,12 +115,20 @@ pub struct Stroke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Stroke {
|
impl Stroke {
|
||||||
pub fn new(id: u64, points: Vec<StylusSample>, vertices: Vec<InkVertex>, color: [f32; 4]) -> Self {
|
pub fn new(
|
||||||
|
id: u64,
|
||||||
|
points: Vec<StylusSample>,
|
||||||
|
vertices: Vec<InkVertex>,
|
||||||
|
color: [f32; 4],
|
||||||
|
) -> Self {
|
||||||
let aabb = if !points.is_empty() {
|
let aabb = if !points.is_empty() {
|
||||||
let pts: Vec<glam::Vec2> = points.iter().map(|s| s.position).collect();
|
let pts: Vec<glam::Vec2> = points.iter().map(|s| s.position).collect();
|
||||||
Aabb::from_points(&pts)
|
Aabb::from_points(&pts)
|
||||||
} else if !vertices.is_empty() {
|
} else if !vertices.is_empty() {
|
||||||
let pts: Vec<glam::Vec2> = vertices.iter().map(|v| glam::Vec2::new(v.position[0], v.position[1])).collect();
|
let pts: Vec<glam::Vec2> = vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| glam::Vec2::new(v.position[0], v.position[1]))
|
||||||
|
.collect();
|
||||||
Aabb::from_points(&pts)
|
Aabb::from_points(&pts)
|
||||||
} else {
|
} else {
|
||||||
Aabb::new(glam::Vec2::ZERO, glam::Vec2::ZERO)
|
Aabb::new(glam::Vec2::ZERO, glam::Vec2::ZERO)
|
||||||
@@ -171,7 +179,12 @@ impl Stroke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft, ob dieser Strich von einem Radierer an Position `eraser_pos` mit Radius `eraser_radius` getroffen wird.
|
/// Prüft, ob dieser Strich von einem Radierer an Position `eraser_pos` mit Radius `eraser_radius` getroffen wird.
|
||||||
pub fn intersects_eraser(&self, eraser_pos: glam::Vec2, eraser_radius: f32, max_stroke_radius: f32) -> bool {
|
pub fn intersects_eraser(
|
||||||
|
&self,
|
||||||
|
eraser_pos: glam::Vec2,
|
||||||
|
eraser_radius: f32,
|
||||||
|
max_stroke_radius: f32,
|
||||||
|
) -> bool {
|
||||||
// 1. Vorabtest: Schneidet der Radierer die vergrößerte AABB?
|
// 1. Vorabtest: Schneidet der Radierer die vergrößerte AABB?
|
||||||
let total_margin = eraser_radius + max_stroke_radius;
|
let total_margin = eraser_radius + max_stroke_radius;
|
||||||
if !self.aabb.expanded(total_margin).contains_point(eraser_pos) {
|
if !self.aabb.expanded(total_margin).contains_point(eraser_pos) {
|
||||||
@@ -254,9 +267,7 @@ impl Stroke {
|
|||||||
|
|
||||||
/// Skaliert den Strich bezüglich `origin` um die Faktoren `factors`.
|
/// Skaliert den Strich bezüglich `origin` um die Faktoren `factors`.
|
||||||
pub fn scale(&mut self, origin: glam::Vec2, factors: glam::Vec2) {
|
pub fn scale(&mut self, origin: glam::Vec2, factors: glam::Vec2) {
|
||||||
let sc = |pos: glam::Vec2| -> glam::Vec2 {
|
let sc = |pos: glam::Vec2| -> glam::Vec2 { origin + (pos - origin) * factors };
|
||||||
origin + (pos - origin) * factors
|
|
||||||
};
|
|
||||||
|
|
||||||
for pt in &mut self.points {
|
for pt in &mut self.points {
|
||||||
pt.position = sc(pt.position);
|
pt.position = sc(pt.position);
|
||||||
@@ -402,7 +413,12 @@ impl Page {
|
|||||||
/// Radiert alle Striche an der Position `eraser_pos` mit Radius `eraser_radius`.
|
/// Radiert alle Striche an der Position `eraser_pos` mit Radius `eraser_radius`.
|
||||||
///
|
///
|
||||||
/// Gibt die gelöschten Striche zurück (falls welche getroffen wurden).
|
/// Gibt die gelöschten Striche zurück (falls welche getroffen wurden).
|
||||||
pub fn erase_at(&mut self, eraser_pos: glam::Vec2, eraser_radius: f32, max_stroke_radius: f32) -> Vec<Stroke> {
|
pub fn erase_at(
|
||||||
|
&mut self,
|
||||||
|
eraser_pos: glam::Vec2,
|
||||||
|
eraser_radius: f32,
|
||||||
|
max_stroke_radius: f32,
|
||||||
|
) -> Vec<Stroke> {
|
||||||
let mut hit_indices = Vec::new();
|
let mut hit_indices = Vec::new();
|
||||||
|
|
||||||
for (i, stroke) in self.strokes.iter().enumerate() {
|
for (i, stroke) in self.strokes.iter().enumerate() {
|
||||||
@@ -422,7 +438,8 @@ impl Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removed.reverse();
|
removed.reverse();
|
||||||
self.history.push(HistoryAction::RemoveStrokes(removed.clone()));
|
self.history
|
||||||
|
.push(HistoryAction::RemoveStrokes(removed.clone()));
|
||||||
self.is_dirty = true;
|
self.is_dirty = true;
|
||||||
|
|
||||||
removed
|
removed
|
||||||
@@ -473,7 +490,11 @@ impl Page {
|
|||||||
|
|
||||||
/// Liefert Klone der Striche mit den angegebenen IDs.
|
/// Liefert Klone der Striche mit den angegebenen IDs.
|
||||||
pub fn get_strokes(&self, ids: &[u64]) -> Vec<Stroke> {
|
pub fn get_strokes(&self, ids: &[u64]) -> Vec<Stroke> {
|
||||||
self.strokes.iter().filter(|s| ids.contains(&s.id)).cloned().collect()
|
self.strokes
|
||||||
|
.iter()
|
||||||
|
.filter(|s| ids.contains(&s.id))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aktualisiert eine Menge geänderter Striche transaktional im History-Stack.
|
/// Aktualisiert eine Menge geänderter Striche transaktional im History-Stack.
|
||||||
@@ -482,7 +503,12 @@ impl Page {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ids: Vec<u64> = modified.iter().map(|s| s.id).collect();
|
let ids: Vec<u64> = modified.iter().map(|s| s.id).collect();
|
||||||
let before: Vec<Stroke> = self.strokes.iter().filter(|s| ids.contains(&s.id)).cloned().collect();
|
let before: Vec<Stroke> = self
|
||||||
|
.strokes
|
||||||
|
.iter()
|
||||||
|
.filter(|s| ids.contains(&s.id))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
for m in modified.iter() {
|
for m in modified.iter() {
|
||||||
if let Some(pos) = self.strokes.iter().position(|s| s.id == m.id) {
|
if let Some(pos) = self.strokes.iter().position(|s| s.id == m.id) {
|
||||||
@@ -517,7 +543,8 @@ impl Page {
|
|||||||
self.strokes[pos] = m.clone();
|
self.strokes[pos] = m.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.history.push(HistoryAction::ModifyStrokes { before, after });
|
self.history
|
||||||
|
.push(HistoryAction::ModifyStrokes { before, after });
|
||||||
self.is_dirty = true;
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +561,8 @@ impl Page {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
self.history.push(HistoryAction::RemoveStrokes(removed.clone()));
|
self.history
|
||||||
|
.push(HistoryAction::RemoveStrokes(removed.clone()));
|
||||||
self.is_dirty = true;
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
removed
|
removed
|
||||||
@@ -545,7 +573,8 @@ impl Page {
|
|||||||
if new_strokes.is_empty() {
|
if new_strokes.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.history.push(HistoryAction::AddStrokes(new_strokes.clone()));
|
self.history
|
||||||
|
.push(HistoryAction::AddStrokes(new_strokes.clone()));
|
||||||
self.strokes.extend(new_strokes);
|
self.strokes.extend(new_strokes);
|
||||||
self.is_dirty = true;
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
@@ -653,7 +682,11 @@ impl Notebook {
|
|||||||
|
|
||||||
/// Text für das On-Screen-Display (OSD), z. B. "Seite 3 / 12".
|
/// Text für das On-Screen-Display (OSD), z. B. "Seite 3 / 12".
|
||||||
pub fn osd_text(&self) -> String {
|
pub fn osd_text(&self) -> String {
|
||||||
format!("Seite {} / {}", self.current_page_index + 1, self.pages.len())
|
format!(
|
||||||
|
"Seite {} / {}",
|
||||||
|
self.current_page_index + 1,
|
||||||
|
self.pages.len()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,13 +820,25 @@ mod tests {
|
|||||||
let stroke_radius = 2.0;
|
let stroke_radius = 2.0;
|
||||||
|
|
||||||
// Treffer: Radierer bei (150, 105) (Abstand 5 <= 12)
|
// Treffer: Radierer bei (150, 105) (Abstand 5 <= 12)
|
||||||
assert!(stroke.intersects_eraser(glam::Vec2::new(150.0, 105.0), eraser_radius, stroke_radius));
|
assert!(stroke.intersects_eraser(
|
||||||
|
glam::Vec2::new(150.0, 105.0),
|
||||||
|
eraser_radius,
|
||||||
|
stroke_radius
|
||||||
|
));
|
||||||
|
|
||||||
// Kein Treffer: Radierer bei (150, 150) (Abstand 50 > 12)
|
// Kein Treffer: Radierer bei (150, 150) (Abstand 50 > 12)
|
||||||
assert!(!stroke.intersects_eraser(glam::Vec2::new(150.0, 150.0), eraser_radius, stroke_radius));
|
assert!(!stroke.intersects_eraser(
|
||||||
|
glam::Vec2::new(150.0, 150.0),
|
||||||
|
eraser_radius,
|
||||||
|
stroke_radius
|
||||||
|
));
|
||||||
|
|
||||||
// Kein Treffer: Weit außerhalb der AABB
|
// Kein Treffer: Weit außerhalb der AABB
|
||||||
assert!(!stroke.intersects_eraser(glam::Vec2::new(500.0, 500.0), eraser_radius, stroke_radius));
|
assert!(!stroke.intersects_eraser(
|
||||||
|
glam::Vec2::new(500.0, 500.0),
|
||||||
|
eraser_radius,
|
||||||
|
stroke_radius
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -966,9 +1011,12 @@ mod tests {
|
|||||||
let mut moved = s.clone();
|
let mut moved = s.clone();
|
||||||
moved.translate(glam::Vec2::new(20.0, 30.0));
|
moved.translate(glam::Vec2::new(20.0, 30.0));
|
||||||
page.update_strokes_ephemeral(&[moved.clone()]);
|
page.update_strokes_ephemeral(&[moved.clone()]);
|
||||||
assert_eq!(page.strokes[0].points[0].position, glam::Vec2::new(30.0, 40.0));
|
assert_eq!(
|
||||||
|
page.strokes[0].points[0].position,
|
||||||
|
glam::Vec2::new(30.0, 40.0)
|
||||||
|
);
|
||||||
// History should not have been updated yet
|
// History should not have been updated yet
|
||||||
assert_eq!(page.history.can_redo(), false);
|
assert!(!page.history.can_redo());
|
||||||
|
|
||||||
// Transactional commit (mouse release)
|
// Transactional commit (mouse release)
|
||||||
let before = vec![s.clone()];
|
let before = vec![s.clone()];
|
||||||
@@ -977,10 +1025,16 @@ mod tests {
|
|||||||
|
|
||||||
// Verify Undo restores original position
|
// Verify Undo restores original position
|
||||||
assert!(page.undo());
|
assert!(page.undo());
|
||||||
assert_eq!(page.strokes[0].points[0].position, glam::Vec2::new(10.0, 10.0));
|
assert_eq!(
|
||||||
|
page.strokes[0].points[0].position,
|
||||||
|
glam::Vec2::new(10.0, 10.0)
|
||||||
|
);
|
||||||
|
|
||||||
// Verify Redo applies modified position
|
// Verify Redo applies modified position
|
||||||
assert!(page.redo());
|
assert!(page.redo());
|
||||||
assert_eq!(page.strokes[0].points[0].position, glam::Vec2::new(30.0, 40.0));
|
assert_eq!(
|
||||||
|
page.strokes[0].points[0].position,
|
||||||
|
glam::Vec2::new(30.0, 40.0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-25
@@ -78,12 +78,8 @@ pub fn generate_pdf_document(
|
|||||||
|
|
||||||
// 1. Erstes Dokument mit Seite 1 erstellen
|
// 1. Erstes Dokument mit Seite 1 erstellen
|
||||||
let title = "ink-notes Notebook";
|
let title = "ink-notes Notebook";
|
||||||
let (doc, page1_idx, layer1_bg) = PdfDocument::new(
|
let (doc, page1_idx, layer1_bg) =
|
||||||
title,
|
PdfDocument::new(title, Mm(A4_WIDTH_MM), Mm(A4_HEIGHT_MM), "Background");
|
||||||
Mm(A4_WIDTH_MM),
|
|
||||||
Mm(A4_HEIGHT_MM),
|
|
||||||
"Background",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Seite 1 befüllen
|
// Seite 1 befüllen
|
||||||
let page1_ref = doc.get_page(page1_idx);
|
let page1_ref = doc.get_page(page1_idx);
|
||||||
@@ -95,11 +91,7 @@ pub fn generate_pdf_document(
|
|||||||
|
|
||||||
// 2. Alle nachfolgenden Seiten hinzufügen
|
// 2. Alle nachfolgenden Seiten hinzufügen
|
||||||
for page in ¬ebook.pages[1..] {
|
for page in ¬ebook.pages[1..] {
|
||||||
let (page_idx, layer_bg) = doc.add_page(
|
let (page_idx, layer_bg) = doc.add_page(Mm(A4_WIDTH_MM), Mm(A4_HEIGHT_MM), "Background");
|
||||||
Mm(A4_WIDTH_MM),
|
|
||||||
Mm(A4_HEIGHT_MM),
|
|
||||||
"Background",
|
|
||||||
);
|
|
||||||
let page_ref = doc.get_page(page_idx);
|
let page_ref = doc.get_page(page_idx);
|
||||||
let layer_bg_ref = page_ref.get_layer(layer_bg);
|
let layer_bg_ref = page_ref.get_layer(layer_bg);
|
||||||
render_background_layer(&layer_bg_ref, page.background);
|
render_background_layer(&layer_bg_ref, page.background);
|
||||||
@@ -112,10 +104,7 @@ pub fn generate_pdf_document(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
||||||
fn render_background_layer(
|
fn render_background_layer(layer: &printpdf::PdfLayerReference, background: PageBackground) {
|
||||||
layer: &printpdf::PdfLayerReference,
|
|
||||||
background: PageBackground,
|
|
||||||
) {
|
|
||||||
let grid_color = Color::Rgb(Rgb::new(0.88, 0.88, 0.88, None));
|
let grid_color = Color::Rgb(Rgb::new(0.88, 0.88, 0.88, None));
|
||||||
layer.set_outline_color(grid_color.clone());
|
layer.set_outline_color(grid_color.clone());
|
||||||
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
||||||
@@ -203,11 +192,7 @@ fn render_background_layer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rendert alle Striche einer Seite auf Layer 2 ("Ink").
|
/// Rendert alle Striche einer Seite auf Layer 2 ("Ink").
|
||||||
fn render_ink_layer(
|
fn render_ink_layer(layer: &printpdf::PdfLayerReference, page: &Page, strategy: ExportStrategy) {
|
||||||
layer: &printpdf::PdfLayerReference,
|
|
||||||
page: &Page,
|
|
||||||
strategy: ExportStrategy,
|
|
||||||
) {
|
|
||||||
for stroke in &page.strokes {
|
for stroke in &page.strokes {
|
||||||
if let Some(ref shape) = stroke.shape {
|
if let Some(ref shape) = stroke.shape {
|
||||||
render_recognized_shape(layer, shape, stroke.color);
|
render_recognized_shape(layer, shape, stroke.color);
|
||||||
@@ -326,7 +311,12 @@ fn render_recognized_shape(
|
|||||||
layer.add_polygon(poly);
|
layer.add_polygon(poly);
|
||||||
}
|
}
|
||||||
|
|
||||||
RecognizedShape::Octahedron { center, radius_x, radius_y, depth } => {
|
RecognizedShape::Octahedron {
|
||||||
|
center,
|
||||||
|
radius_x,
|
||||||
|
radius_y,
|
||||||
|
depth,
|
||||||
|
} => {
|
||||||
let edges = crate::geometry::octahedron_edges(*center, *radius_x, *radius_y, *depth);
|
let edges = crate::geometry::octahedron_edges(*center, *radius_x, *radius_y, *depth);
|
||||||
for (p1, p2) in edges {
|
for (p1, p2) in edges {
|
||||||
let (x1, y1) = page_to_pdf_mm(p1.x, p1.y);
|
let (x1, y1) = page_to_pdf_mm(p1.x, p1.y);
|
||||||
@@ -514,7 +504,8 @@ fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
|||||||
layer.set_line_join_style(printpdf::LineJoinStyle::Round);
|
layer.set_line_join_style(printpdf::LineJoinStyle::Round);
|
||||||
|
|
||||||
// Durchschnittliche Linienstärke aus dem Druck berechnen
|
// Durchschnittliche Linienstärke aus dem Druck berechnen
|
||||||
let avg_pressure = stroke.points.iter().map(|p| p.pressure).sum::<f32>() / stroke.points.len() as f32;
|
let avg_pressure =
|
||||||
|
stroke.points.iter().map(|p| p.pressure).sum::<f32>() / stroke.points.len() as f32;
|
||||||
let radius_mm = (avg_pressure * 0.75 + 0.25).max(0.25);
|
let radius_mm = (avg_pressure * 0.75 + 0.25).max(0.25);
|
||||||
let thickness_pt = radius_mm * 2.0 * (72.0 / 25.4); // mm in DTP-Punkte
|
let thickness_pt = radius_mm * 2.0 * (72.0 / 25.4); // mm in DTP-Punkte
|
||||||
layer.set_outline_thickness(thickness_pt);
|
layer.set_outline_thickness(thickness_pt);
|
||||||
@@ -693,11 +684,18 @@ mod tests {
|
|||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let pdf_path = temp_dir.join("ink_notes_test_export.pdf");
|
let pdf_path = temp_dir.join("ink_notes_test_export.pdf");
|
||||||
|
|
||||||
let export_res = export_notebook_to_pdf(¬ebook, &pdf_path, ExportStrategy::FilledPolygon);
|
let export_res =
|
||||||
assert!(export_res.is_ok(), "PDF-Export zu Datei muss erfolgreich sein");
|
export_notebook_to_pdf(¬ebook, &pdf_path, ExportStrategy::FilledPolygon);
|
||||||
|
assert!(
|
||||||
|
export_res.is_ok(),
|
||||||
|
"PDF-Export zu Datei muss erfolgreich sein"
|
||||||
|
);
|
||||||
|
|
||||||
// Datei-Existenz und Größe prüfen
|
// Datei-Existenz und Größe prüfen
|
||||||
assert!(pdf_path.exists(), "Die generierte PDF-Datei muss existieren");
|
assert!(
|
||||||
|
pdf_path.exists(),
|
||||||
|
"Die generierte PDF-Datei muss existieren"
|
||||||
|
);
|
||||||
let metadata = std::fs::metadata(&pdf_path).unwrap();
|
let metadata = std::fs::metadata(&pdf_path).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
metadata.len() > 1024,
|
metadata.len() > 1024,
|
||||||
|
|||||||
+28
-7
@@ -265,9 +265,18 @@ mod tests {
|
|||||||
// 4. dt > 1.0 s (großer Zeitsprung z.B. auf t = 5.0 s): Muss reset() auslösen
|
// 4. dt > 1.0 s (großer Zeitsprung z.B. auf t = 5.0 s): Muss reset() auslösen
|
||||||
let (x4, y4, p4) = filter.filter(50.0, 100.0, 0.9, 5.0);
|
let (x4, y4, p4) = filter.filter(50.0, 100.0, 0.9, 5.0);
|
||||||
// Nach dem Reset muss der Filter sofort exakt den neuen Punkt als Startpunkt annehmen
|
// Nach dem Reset muss der Filter sofort exakt den neuen Punkt als Startpunkt annehmen
|
||||||
assert_eq!(x4, 50.0, "Nach Reset muss der neue X-Wert übernommen werden");
|
assert_eq!(
|
||||||
assert_eq!(y4, 100.0, "Nach Reset muss der neue Y-Wert übernommen werden");
|
x4, 50.0,
|
||||||
assert_eq!(p4, 0.9, "Nach Reset muss der neue Druckwert übernommen werden");
|
"Nach Reset muss der neue X-Wert übernommen werden"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
y4, 100.0,
|
||||||
|
"Nach Reset muss der neue Y-Wert übernommen werden"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
p4, 0.9,
|
||||||
|
"Nach Reset muss der neue Druckwert übernommen werden"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -344,9 +353,18 @@ mod tests {
|
|||||||
|
|
||||||
// 2. Vergiftungsversuch mit NaN und Infinity
|
// 2. Vergiftungsversuch mit NaN und Infinity
|
||||||
let (x_nan, y_nan, p_nan) = filter.filter(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.02);
|
let (x_nan, y_nan, p_nan) = filter.filter(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.02);
|
||||||
assert!(x_nan.is_finite(), "Filter darf kein NaN bei Vergiftung ausgeben");
|
assert!(
|
||||||
assert!(y_nan.is_finite(), "Filter darf kein Inf bei Vergiftung ausgeben");
|
x_nan.is_finite(),
|
||||||
assert!(p_nan.is_finite(), "Filter darf kein -Inf bei Vergiftung ausgeben");
|
"Filter darf kein NaN bei Vergiftung ausgeben"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
y_nan.is_finite(),
|
||||||
|
"Filter darf kein Inf bei Vergiftung ausgeben"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
p_nan.is_finite(),
|
||||||
|
"Filter darf kein -Inf bei Vergiftung ausgeben"
|
||||||
|
);
|
||||||
|
|
||||||
let (x_nan2, y_nan2, p_nan2) = filter.filter(12.0, 22.0, 0.6, f64::NAN);
|
let (x_nan2, y_nan2, p_nan2) = filter.filter(12.0, 22.0, 0.6, f64::NAN);
|
||||||
assert!(x_nan2.is_finite());
|
assert!(x_nan2.is_finite());
|
||||||
@@ -364,6 +382,9 @@ mod tests {
|
|||||||
assert!(x3.is_finite());
|
assert!(x3.is_finite());
|
||||||
assert!(y3.is_finite());
|
assert!(y3.is_finite());
|
||||||
assert!(p3.is_finite());
|
assert!(p3.is_finite());
|
||||||
assert!(x3 > x2, "Filter muss sich nach Vergiftung weiter normal anpassen");
|
assert!(
|
||||||
|
x3 > x2,
|
||||||
|
"Filter muss sich nach Vergiftung weiter normal anpassen"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+195
-58
@@ -3,9 +3,9 @@
|
|||||||
//! Erkennt Geraden, Rechtecke, Kreise und Koordinatensysteme (Achsen)
|
//! Erkennt Geraden, Rechtecke, Kreise und Koordinatensysteme (Achsen)
|
||||||
//! und erzeugt saubere Vektor-Primitiven und GPU-Triangle-Meshes.
|
//! und erzeugt saubere Vektor-Primitiven und GPU-Triangle-Meshes.
|
||||||
|
|
||||||
use std::f32::consts::PI;
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use crate::canvas::InkVertex;
|
use crate::canvas::InkVertex;
|
||||||
use crate::document::{Aabb, Stroke, StylusSample};
|
use crate::document::{Aabb, Stroke, StylusSample};
|
||||||
@@ -117,7 +117,12 @@ impl RecognizedShape {
|
|||||||
*c += delta;
|
*c += delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::CoordinateSystem { origin, x_axis_end, y_axis_end, .. } => {
|
Self::CoordinateSystem {
|
||||||
|
origin,
|
||||||
|
x_axis_end,
|
||||||
|
y_axis_end,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
*origin += delta;
|
*origin += delta;
|
||||||
*x_axis_end += delta;
|
*x_axis_end += delta;
|
||||||
*y_axis_end += delta;
|
*y_axis_end += delta;
|
||||||
@@ -159,7 +164,12 @@ impl RecognizedShape {
|
|||||||
*c = rot(*c);
|
*c = rot(*c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::CoordinateSystem { origin: o, x_axis_end, y_axis_end, .. } => {
|
Self::CoordinateSystem {
|
||||||
|
origin: o,
|
||||||
|
x_axis_end,
|
||||||
|
y_axis_end,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
*o = rot(*o);
|
*o = rot(*o);
|
||||||
*x_axis_end = rot(*x_axis_end);
|
*x_axis_end = rot(*x_axis_end);
|
||||||
*y_axis_end = rot(*y_axis_end);
|
*y_axis_end = rot(*y_axis_end);
|
||||||
@@ -177,9 +187,7 @@ impl RecognizedShape {
|
|||||||
|
|
||||||
/// Skaliert die geometrische Form bezüglich `origin` um den Faktor `factors`.
|
/// Skaliert die geometrische Form bezüglich `origin` um den Faktor `factors`.
|
||||||
pub fn scale(&mut self, origin: Vec2, factors: Vec2) {
|
pub fn scale(&mut self, origin: Vec2, factors: Vec2) {
|
||||||
let sc = |pt: Vec2| -> Vec2 {
|
let sc = |pt: Vec2| -> Vec2 { origin + (pt - origin) * factors };
|
||||||
origin + (pt - origin) * factors
|
|
||||||
};
|
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Line { start, end } => {
|
Self::Line { start, end } => {
|
||||||
@@ -200,7 +208,12 @@ impl RecognizedShape {
|
|||||||
*c = sc(*c);
|
*c = sc(*c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::CoordinateSystem { origin: o, x_axis_end, y_axis_end, .. } => {
|
Self::CoordinateSystem {
|
||||||
|
origin: o,
|
||||||
|
x_axis_end,
|
||||||
|
y_axis_end,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
*o = sc(*o);
|
*o = sc(*o);
|
||||||
*x_axis_end = sc(*x_axis_end);
|
*x_axis_end = sc(*x_axis_end);
|
||||||
*y_axis_end = sc(*y_axis_end);
|
*y_axis_end = sc(*y_axis_end);
|
||||||
@@ -210,7 +223,12 @@ impl RecognizedShape {
|
|||||||
*v = sc(*v);
|
*v = sc(*v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Octahedron { center, radius_x, radius_y, depth } => {
|
Self::Octahedron {
|
||||||
|
center,
|
||||||
|
radius_x,
|
||||||
|
radius_y,
|
||||||
|
depth,
|
||||||
|
} => {
|
||||||
*center = sc(*center);
|
*center = sc(*center);
|
||||||
*radius_x *= factors.x.abs();
|
*radius_x *= factors.x.abs();
|
||||||
*radius_y *= factors.y.abs();
|
*radius_y *= factors.y.abs();
|
||||||
@@ -415,9 +433,8 @@ pub fn detect_rectangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
|
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
|
||||||
let simplified = ramer_douglas_peucker(points, epsilon);
|
let simplified = ramer_douglas_peucker(points, epsilon);
|
||||||
|
|
||||||
let corners: Vec<Vec2> = if simplified.len() == 5
|
let corners: Vec<Vec2> =
|
||||||
&& (simplified[0] - simplified[4]).length() < epsilon * 2.5
|
if simplified.len() == 5 && (simplified[0] - simplified[4]).length() < epsilon * 2.5 {
|
||||||
{
|
|
||||||
simplified[0..4].to_vec()
|
simplified[0..4].to_vec()
|
||||||
} else if simplified.len() == 4 {
|
} else if simplified.len() == 4 {
|
||||||
simplified
|
simplified
|
||||||
@@ -535,7 +552,12 @@ pub fn extract_k_corners(points: &[Vec2], k: usize) -> Option<Vec<Vec2>> {
|
|||||||
let mut selected_indices: Vec<usize> = scored.iter().take(k).map(|&(idx, _)| idx).collect();
|
let mut selected_indices: Vec<usize> = scored.iter().take(k).map(|&(idx, _)| idx).collect();
|
||||||
selected_indices.sort_unstable();
|
selected_indices.sort_unstable();
|
||||||
|
|
||||||
Some(selected_indices.into_iter().map(|idx| points[idx]).collect())
|
Some(
|
||||||
|
selected_indices
|
||||||
|
.into_iter()
|
||||||
|
.map(|idx| points[idx])
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 4. Dreieck-Erkennung: Geschlossener Pfad mit 3 dominanten Ecken.
|
/// 4. Dreieck-Erkennung: Geschlossener Pfad mit 3 dominanten Ecken.
|
||||||
@@ -561,7 +583,8 @@ pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let epsilon = (total_len * 0.07).clamp(8.0, 50.0);
|
let epsilon = (total_len * 0.07).clamp(8.0, 50.0);
|
||||||
let simplified = ramer_douglas_peucker(points, epsilon);
|
let simplified = ramer_douglas_peucker(points, epsilon);
|
||||||
|
|
||||||
let corners: Vec<Vec2> = if (simplified.len() == 4 && (simplified[0] - simplified[3]).length() < epsilon * 2.5)
|
let corners: Vec<Vec2> = if (simplified.len() == 4
|
||||||
|
&& (simplified[0] - simplified[3]).length() < epsilon * 2.5)
|
||||||
|| simplified.len() == 3
|
|| simplified.len() == 3
|
||||||
{
|
{
|
||||||
simplified[0..3].to_vec()
|
simplified[0..3].to_vec()
|
||||||
@@ -617,9 +640,19 @@ pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let snapped_angle = snap_angle(base_angle, 15.0);
|
let snapped_angle = snap_angle(base_angle, 15.0);
|
||||||
|
|
||||||
let c0 = center + Vec2::new(snapped_angle.cos(), snapped_angle.sin()) * r;
|
let c0 = center + Vec2::new(snapped_angle.cos(), snapped_angle.sin()) * r;
|
||||||
let c1 = center + Vec2::new((snapped_angle + 2.0 * PI / 3.0).cos(), (snapped_angle + 2.0 * PI / 3.0).sin()) * r;
|
let c1 = center
|
||||||
let c2 = center + Vec2::new((snapped_angle + 4.0 * PI / 3.0).cos(), (snapped_angle + 4.0 * PI / 3.0).sin()) * r;
|
+ Vec2::new(
|
||||||
return Some(RecognizedShape::Triangle { corners: [c0, c1, c2] });
|
(snapped_angle + 2.0 * PI / 3.0).cos(),
|
||||||
|
(snapped_angle + 2.0 * PI / 3.0).sin(),
|
||||||
|
) * r;
|
||||||
|
let c2 = center
|
||||||
|
+ Vec2::new(
|
||||||
|
(snapped_angle + 4.0 * PI / 3.0).cos(),
|
||||||
|
(snapped_angle + 4.0 * PI / 3.0).sin(),
|
||||||
|
) * r;
|
||||||
|
return Some(RecognizedShape::Triangle {
|
||||||
|
corners: [c0, c1, c2],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Rechtwinklig? (Ein Winkel nahe 90°)
|
// 2. Rechtwinklig? (Ein Winkel nahe 90°)
|
||||||
@@ -639,7 +672,11 @@ pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let len2 = v2.length();
|
let len2 = v2.length();
|
||||||
|
|
||||||
v1 = Vec2::new(a1_snapped.cos(), a1_snapped.sin()) * len1;
|
v1 = Vec2::new(a1_snapped.cos(), a1_snapped.sin()) * len1;
|
||||||
let sign = if (v1.x * v2.y - v1.y * v2.x) >= 0.0 { 1.0 } else { -1.0 };
|
let sign = if (v1.x * v2.y - v1.y * v2.x) >= 0.0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
-1.0
|
||||||
|
};
|
||||||
v2 = Vec2::new(-v1.y * sign, v1.x * sign).normalize_or_zero() * len2;
|
v2 = Vec2::new(-v1.y * sign, v1.x * sign).normalize_or_zero() * len2;
|
||||||
|
|
||||||
let new_p1 = corner_v + v1;
|
let new_p1 = corner_v + v1;
|
||||||
@@ -667,7 +704,9 @@ pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let half_base = ((a - mid_ac).length() + (c - mid_ac).length()) * 0.5;
|
let half_base = ((a - mid_ac).length() + (c - mid_ac).length()) * 0.5;
|
||||||
let new_a = mid_ac + perp * half_base;
|
let new_a = mid_ac + perp * half_base;
|
||||||
c = mid_ac - perp * half_base;
|
c = mid_ac - perp * half_base;
|
||||||
return Some(RecognizedShape::Triangle { corners: [new_a, b, c] });
|
return Some(RecognizedShape::Triangle {
|
||||||
|
corners: [new_a, b, c],
|
||||||
|
});
|
||||||
} else if (l_bc - l_ca).abs() / l_bc.max(l_ca) < 0.12 {
|
} else if (l_bc - l_ca).abs() / l_bc.max(l_ca) < 0.12 {
|
||||||
let mid_ab = (a + b) * 0.5;
|
let mid_ab = (a + b) * 0.5;
|
||||||
let axis = (c - mid_ab).normalize_or_zero();
|
let axis = (c - mid_ab).normalize_or_zero();
|
||||||
@@ -675,7 +714,9 @@ pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let half_base = ((a - mid_ab).length() + (b - mid_ab).length()) * 0.5;
|
let half_base = ((a - mid_ab).length() + (b - mid_ab).length()) * 0.5;
|
||||||
let new_a = mid_ab + perp * half_base;
|
let new_a = mid_ab + perp * half_base;
|
||||||
b = mid_ab - perp * half_base;
|
b = mid_ab - perp * half_base;
|
||||||
return Some(RecognizedShape::Triangle { corners: [new_a, b, c] });
|
return Some(RecognizedShape::Triangle {
|
||||||
|
corners: [new_a, b, c],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(RecognizedShape::Triangle { corners: [a, b, c] })
|
Some(RecognizedShape::Triangle { corners: [a, b, c] })
|
||||||
@@ -701,7 +742,8 @@ pub fn detect_rhombus(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
|
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
|
||||||
let simplified = ramer_douglas_peucker(points, epsilon);
|
let simplified = ramer_douglas_peucker(points, epsilon);
|
||||||
|
|
||||||
let corners: Vec<Vec2> = if simplified.len() == 5 && (simplified[0] - simplified[4]).length() < epsilon * 2.5 {
|
let corners: Vec<Vec2> =
|
||||||
|
if simplified.len() == 5 && (simplified[0] - simplified[4]).length() < epsilon * 2.5 {
|
||||||
simplified[0..4].to_vec()
|
simplified[0..4].to_vec()
|
||||||
} else if simplified.len() == 4 {
|
} else if simplified.len() == 4 {
|
||||||
simplified
|
simplified
|
||||||
@@ -756,7 +798,11 @@ pub fn detect_rhombus(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
let snapped_a1 = snap_angle(angle1, 15.0);
|
let snapped_a1 = snap_angle(angle1, 15.0);
|
||||||
dir1 = Vec2::new(snapped_a1.cos(), snapped_a1.sin());
|
dir1 = Vec2::new(snapped_a1.cos(), snapped_a1.sin());
|
||||||
|
|
||||||
let sign = if (dir1.x * d2.y - dir1.y * d2.x) >= 0.0 { 1.0 } else { -1.0 };
|
let sign = if (dir1.x * d2.y - dir1.y * d2.x) >= 0.0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
-1.0
|
||||||
|
};
|
||||||
let dir2 = Vec2::new(-dir1.y * sign, dir1.x * sign);
|
let dir2 = Vec2::new(-dir1.y * sign, dir1.x * sign);
|
||||||
|
|
||||||
let h1 = len1 * 0.5;
|
let h1 = len1 * 0.5;
|
||||||
@@ -806,7 +852,10 @@ pub fn detect_polygon(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_r_diff = radii.iter().map(|r| (r - mean_r).abs()).fold(0.0f32, f32::max);
|
let max_r_diff = radii
|
||||||
|
.iter()
|
||||||
|
.map(|r| (r - mean_r).abs())
|
||||||
|
.fold(0.0f32, f32::max);
|
||||||
|
|
||||||
let snapped_vertices = if max_r_diff / mean_r < 0.25 {
|
let snapped_vertices = if max_r_diff / mean_r < 0.25 {
|
||||||
let base_dir = (corners[0] - center).normalize_or_zero();
|
let base_dir = (corners[0] - center).normalize_or_zero();
|
||||||
@@ -830,7 +879,9 @@ pub fn detect_polygon(points: &[Vec2]) -> Option<RecognizedShape> {
|
|||||||
|
|
||||||
fn ramer_douucker_closed(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
|
fn ramer_douucker_closed(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
|
||||||
let simplified = ramer_douglas_peucker(points, epsilon);
|
let simplified = ramer_douglas_peucker(points, epsilon);
|
||||||
if simplified.len() >= 2 && (simplified[0] - *simplified.last().unwrap()).length() < epsilon * 2.0 {
|
if simplified.len() >= 2
|
||||||
|
&& (simplified[0] - *simplified.last().unwrap()).length() < epsilon * 2.0
|
||||||
|
{
|
||||||
simplified[0..simplified.len() - 1].to_vec()
|
simplified[0..simplified.len() - 1].to_vec()
|
||||||
} else {
|
} else {
|
||||||
simplified
|
simplified
|
||||||
@@ -1043,8 +1094,16 @@ pub fn closed_polygon_to_mesh(
|
|||||||
let n_next = Vec2::new(-d_next.y, d_next.x);
|
let n_next = Vec2::new(-d_next.y, d_next.x);
|
||||||
let corner_norm = (n_prev + n_next).normalize_or_zero() * half_w;
|
let corner_norm = (n_prev + n_next).normalize_or_zero() * half_w;
|
||||||
|
|
||||||
vertices.push(InkVertex::new(curr.x + corner_norm.x, curr.y + corner_norm.y, color));
|
vertices.push(InkVertex::new(
|
||||||
vertices.push(InkVertex::new(curr.x - corner_norm.x, curr.y - corner_norm.y, color));
|
curr.x + corner_norm.x,
|
||||||
|
curr.y + corner_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
curr.x - corner_norm.x,
|
||||||
|
curr.y - corner_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
|
||||||
let edge_len = (next - curr).length();
|
let edge_len = (next - curr).length();
|
||||||
let steps = ((edge_len / 10.0).ceil() as usize).max(2);
|
let steps = ((edge_len / 10.0).ceil() as usize).max(2);
|
||||||
@@ -1069,7 +1128,12 @@ pub fn closed_polygon_to_mesh(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Berechnet die 12 Kanten der 3D-Schrägbild-Projektion eines Oktaeders (Doppelpyramide).
|
/// Berechnet die 12 Kanten der 3D-Schrägbild-Projektion eines Oktaeders (Doppelpyramide).
|
||||||
pub fn octahedron_edges(center: Vec2, radius_x: f32, radius_y: f32, depth: f32) -> Vec<(Vec2, Vec2)> {
|
pub fn octahedron_edges(
|
||||||
|
center: Vec2,
|
||||||
|
radius_x: f32,
|
||||||
|
radius_y: f32,
|
||||||
|
depth: f32,
|
||||||
|
) -> Vec<(Vec2, Vec2)> {
|
||||||
let top = center + Vec2::new(0.0, -radius_y);
|
let top = center + Vec2::new(0.0, -radius_y);
|
||||||
let bottom = center + Vec2::new(0.0, radius_y);
|
let bottom = center + Vec2::new(0.0, radius_y);
|
||||||
let left = center + Vec2::new(-radius_x, 0.0);
|
let left = center + Vec2::new(-radius_x, 0.0);
|
||||||
@@ -1143,8 +1207,16 @@ pub fn shape_to_mesh(
|
|||||||
let angle = (i as f32 / segments as f32) * 2.0 * PI;
|
let angle = (i as f32 / segments as f32) * 2.0 * PI;
|
||||||
let (sin, cos) = angle.sin_cos();
|
let (sin, cos) = angle.sin_cos();
|
||||||
|
|
||||||
vertices.push(InkVertex::new(center.x + cos * r_out, center.y + sin * r_out, color));
|
vertices.push(InkVertex::new(
|
||||||
vertices.push(InkVertex::new(center.x + cos * r_in, center.y + sin * r_in, color));
|
center.x + cos * r_out,
|
||||||
|
center.y + sin * r_out,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
center.x + cos * r_in,
|
||||||
|
center.y + sin * r_in,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
|
||||||
if i < segments {
|
if i < segments {
|
||||||
samples.push(StylusSample {
|
samples.push(StylusSample {
|
||||||
@@ -1158,23 +1230,20 @@ pub fn shape_to_mesh(
|
|||||||
(samples, vertices)
|
(samples, vertices)
|
||||||
}
|
}
|
||||||
|
|
||||||
RecognizedShape::Rectangle { corners } => {
|
RecognizedShape::Rectangle { corners } => closed_polygon_to_mesh(corners, half_w, color),
|
||||||
closed_polygon_to_mesh(corners, half_w, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
RecognizedShape::Triangle { corners } => {
|
RecognizedShape::Triangle { corners } => closed_polygon_to_mesh(corners, half_w, color),
|
||||||
closed_polygon_to_mesh(corners, half_w, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
RecognizedShape::Rhombus { corners } => {
|
RecognizedShape::Rhombus { corners } => closed_polygon_to_mesh(corners, half_w, color),
|
||||||
closed_polygon_to_mesh(corners, half_w, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
RecognizedShape::Polygon { vertices } => {
|
RecognizedShape::Polygon { vertices } => closed_polygon_to_mesh(vertices, half_w, color),
|
||||||
closed_polygon_to_mesh(vertices, half_w, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
RecognizedShape::Octahedron { center, radius_x, radius_y, depth } => {
|
RecognizedShape::Octahedron {
|
||||||
|
center,
|
||||||
|
radius_x,
|
||||||
|
radius_y,
|
||||||
|
depth,
|
||||||
|
} => {
|
||||||
let edges = octahedron_edges(*center, *radius_x, *radius_y, *depth);
|
let edges = octahedron_edges(*center, *radius_x, *radius_y, *depth);
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
let mut samples = Vec::new();
|
let mut samples = Vec::new();
|
||||||
@@ -1228,10 +1297,26 @@ pub fn shape_to_mesh(
|
|||||||
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
|
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
|
||||||
|
|
||||||
// Y-Linienband
|
// Y-Linienband
|
||||||
vertices.push(InkVertex::new(origin.x + y_norm.x, origin.y + y_norm.y, color));
|
vertices.push(InkVertex::new(
|
||||||
vertices.push(InkVertex::new(origin.x - y_norm.x, origin.y - y_norm.y, color));
|
origin.x + y_norm.x,
|
||||||
vertices.push(InkVertex::new(y_axis_end.x + y_norm.x, y_axis_end.y + y_norm.y, color));
|
origin.y + y_norm.y,
|
||||||
vertices.push(InkVertex::new(y_axis_end.x - y_norm.x, y_axis_end.y - y_norm.y, color));
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
origin.x - y_norm.x,
|
||||||
|
origin.y - y_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
y_axis_end.x + y_norm.x,
|
||||||
|
y_axis_end.y + y_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
y_axis_end.x - y_norm.x,
|
||||||
|
y_axis_end.y - y_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
|
||||||
// Degenerate Dreiecke für Übergang zur X-Achse
|
// Degenerate Dreiecke für Übergang zur X-Achse
|
||||||
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
||||||
@@ -1244,9 +1329,21 @@ pub fn shape_to_mesh(
|
|||||||
|
|
||||||
// 2. X-Linienband
|
// 2. X-Linienband
|
||||||
vertices.push(next_v);
|
vertices.push(next_v);
|
||||||
vertices.push(InkVertex::new(origin.x - x_norm.x, origin.y - x_norm.y, color));
|
vertices.push(InkVertex::new(
|
||||||
vertices.push(InkVertex::new(x_axis_end.x + x_norm.x, x_axis_end.y + x_norm.y, color));
|
origin.x - x_norm.x,
|
||||||
vertices.push(InkVertex::new(x_axis_end.x - x_norm.x, x_axis_end.y - x_norm.y, color));
|
origin.y - x_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
x_axis_end.x + x_norm.x,
|
||||||
|
x_axis_end.y + x_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
vertices.push(InkVertex::new(
|
||||||
|
x_axis_end.x - x_norm.x,
|
||||||
|
x_axis_end.y - x_norm.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
|
||||||
// 3. Optionale Pfeilspitzen
|
// 3. Optionale Pfeilspitzen
|
||||||
if *with_arrows {
|
if *with_arrows {
|
||||||
@@ -1328,7 +1425,10 @@ mod tests {
|
|||||||
for i in 0..=50 {
|
for i in 0..=50 {
|
||||||
let t = i as f32 / 50.0;
|
let t = i as f32 / 50.0;
|
||||||
let jitter = (i as f32 * 0.7).sin() * 1.5;
|
let jitter = (i as f32 * 0.7).sin() * 1.5;
|
||||||
points.push(Vec2::new(100.0 + t * 400.0 + jitter, 100.0 + t * 300.0 - jitter));
|
points.push(Vec2::new(
|
||||||
|
100.0 + t * 400.0 + jitter,
|
||||||
|
100.0 + t * 300.0 - jitter,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let detected = detect_line(&points);
|
let detected = detect_line(&points);
|
||||||
@@ -1380,7 +1480,12 @@ mod tests {
|
|||||||
let v1 = (prev - curr).normalize();
|
let v1 = (prev - curr).normalize();
|
||||||
let v2 = (next - curr).normalize();
|
let v2 = (next - curr).normalize();
|
||||||
let dot = v1.dot(v2).abs();
|
let dot = v1.dot(v2).abs();
|
||||||
assert!(dot < 1e-4, "Winkel an Ecke {} ist nicht rechtwinklig: dot={}", i, dot);
|
assert!(
|
||||||
|
dot < 1e-4,
|
||||||
|
"Winkel an Ecke {} ist nicht rechtwinklig: dot={}",
|
||||||
|
i,
|
||||||
|
dot
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!("Falscher Formtyp erkannt");
|
panic!("Falscher Formtyp erkannt");
|
||||||
@@ -1402,7 +1507,11 @@ mod tests {
|
|||||||
let detected = detect_circle(&points);
|
let detected = detect_circle(&points);
|
||||||
assert!(detected.is_some(), "Kreis sollte erkannt werden");
|
assert!(detected.is_some(), "Kreis sollte erkannt werden");
|
||||||
|
|
||||||
if let Some(RecognizedShape::Circle { center: c, radius: r }) = detected {
|
if let Some(RecognizedShape::Circle {
|
||||||
|
center: c,
|
||||||
|
radius: r,
|
||||||
|
}) = detected
|
||||||
|
{
|
||||||
assert!((c - center).length() < 5.0, "Zentrum weicht ab: {:?}", c);
|
assert!((c - center).length() < 5.0, "Zentrum weicht ab: {:?}", c);
|
||||||
assert!((r - 54.0).abs() < 5.0, "Radius weicht ab: {}", r);
|
assert!((r - 54.0).abs() < 5.0, "Radius weicht ab: {}", r);
|
||||||
} else {
|
} else {
|
||||||
@@ -1431,14 +1540,27 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let detected = detect_coordinate_system(&points);
|
let detected = detect_coordinate_system(&points);
|
||||||
assert!(detected.is_some(), "Koordinatensystem sollte erkannt werden");
|
assert!(
|
||||||
|
detected.is_some(),
|
||||||
|
"Koordinatensystem sollte erkannt werden"
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(RecognizedShape::CoordinateSystem { origin: o, x_axis_end, y_axis_end, .. }) = detected {
|
if let Some(RecognizedShape::CoordinateSystem {
|
||||||
|
origin: o,
|
||||||
|
x_axis_end,
|
||||||
|
y_axis_end,
|
||||||
|
..
|
||||||
|
}) = detected
|
||||||
|
{
|
||||||
assert!((o - origin).length() < 2.0, "Ursprung weicht ab");
|
assert!((o - origin).length() < 2.0, "Ursprung weicht ab");
|
||||||
let v_x = (x_axis_end - o).normalize();
|
let v_x = (x_axis_end - o).normalize();
|
||||||
let v_y = (y_axis_end - o).normalize();
|
let v_y = (y_axis_end - o).normalize();
|
||||||
let dot = v_x.dot(v_y).abs();
|
let dot = v_x.dot(v_y).abs();
|
||||||
assert!(dot < 1e-4, "Achsen sind nach Orthogonalitäts-Snap nicht exakt 90°: dot={}", dot);
|
assert!(
|
||||||
|
dot < 1e-4,
|
||||||
|
"Achsen sind nach Orthogonalitäts-Snap nicht exakt 90°: dot={}",
|
||||||
|
dot
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
panic!("Falscher Formtyp erkannt");
|
panic!("Falscher Formtyp erkannt");
|
||||||
}
|
}
|
||||||
@@ -1476,7 +1598,10 @@ mod tests {
|
|||||||
let ab = (corners[1] - corners[0]).length();
|
let ab = (corners[1] - corners[0]).length();
|
||||||
let bc = (corners[2] - corners[1]).length();
|
let bc = (corners[2] - corners[1]).length();
|
||||||
let ca = (corners[0] - corners[2]).length();
|
let ca = (corners[0] - corners[2]).length();
|
||||||
assert!((ab - bc).abs() < 5.0, "Gleichseitiges Dreieck sollte gesnappt sein");
|
assert!(
|
||||||
|
(ab - bc).abs() < 5.0,
|
||||||
|
"Gleichseitiges Dreieck sollte gesnappt sein"
|
||||||
|
);
|
||||||
assert!((bc - ca).abs() < 5.0);
|
assert!((bc - ca).abs() < 5.0);
|
||||||
} else {
|
} else {
|
||||||
panic!("Falscher Formtyp erkannt");
|
panic!("Falscher Formtyp erkannt");
|
||||||
@@ -1520,7 +1645,10 @@ mod tests {
|
|||||||
let l1 = (corners[2] - corners[1]).length();
|
let l1 = (corners[2] - corners[1]).length();
|
||||||
let l2 = (corners[3] - corners[2]).length();
|
let l2 = (corners[3] - corners[2]).length();
|
||||||
let l3 = (corners[0] - corners[3]).length();
|
let l3 = (corners[0] - corners[3]).length();
|
||||||
assert!((l0 - l1).abs() < 1e-3, "Alle 4 Seiten einer Raute müssen exakt gleich lang sein");
|
assert!(
|
||||||
|
(l0 - l1).abs() < 1e-3,
|
||||||
|
"Alle 4 Seiten einer Raute müssen exakt gleich lang sein"
|
||||||
|
);
|
||||||
assert!((l1 - l2).abs() < 1e-3);
|
assert!((l1 - l2).abs() < 1e-3);
|
||||||
assert!((l2 - l3).abs() < 1e-3);
|
assert!((l2 - l3).abs() < 1e-3);
|
||||||
} else {
|
} else {
|
||||||
@@ -1549,7 +1677,10 @@ mod tests {
|
|||||||
points.push(points[0]);
|
points.push(points[0]);
|
||||||
|
|
||||||
let detected = detect_polygon(&points);
|
let detected = detect_polygon(&points);
|
||||||
assert!(detected.is_some(), "Oktagon / Achteck sollte erkannt werden");
|
assert!(
|
||||||
|
detected.is_some(),
|
||||||
|
"Oktagon / Achteck sollte erkannt werden"
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(RecognizedShape::Polygon { vertices }) = detected {
|
if let Some(RecognizedShape::Polygon { vertices }) = detected {
|
||||||
assert_eq!(vertices.len(), 8, "Achteck muss 8 Vertizes haben");
|
assert_eq!(vertices.len(), 8, "Achteck muss 8 Vertizes haben");
|
||||||
@@ -1587,7 +1718,13 @@ mod tests {
|
|||||||
let detected = detect_octahedron(&points);
|
let detected = detect_octahedron(&points);
|
||||||
assert!(detected.is_some(), "Oktaeder sollte erkannt werden");
|
assert!(detected.is_some(), "Oktaeder sollte erkannt werden");
|
||||||
|
|
||||||
if let Some(RecognizedShape::Octahedron { center, radius_x, radius_y, depth }) = detected {
|
if let Some(RecognizedShape::Octahedron {
|
||||||
|
center,
|
||||||
|
radius_x,
|
||||||
|
radius_y,
|
||||||
|
depth,
|
||||||
|
}) = detected
|
||||||
|
{
|
||||||
assert!((center.x - 200.0).abs() < 5.0);
|
assert!((center.x - 200.0).abs() < 5.0);
|
||||||
assert!((center.y - 200.0).abs() < 5.0);
|
assert!((center.y - 200.0).abs() < 5.0);
|
||||||
assert!(radius_x > 100.0);
|
assert!(radius_x > 100.0);
|
||||||
|
|||||||
+8
-4
@@ -83,7 +83,8 @@ impl HistoryStack {
|
|||||||
if let Some(pos) = strokes.iter().position(|s| s.id == new.id) {
|
if let Some(pos) = strokes.iter().position(|s| s.id == new.id) {
|
||||||
strokes[pos] = old.clone();
|
strokes[pos] = old.clone();
|
||||||
}
|
}
|
||||||
self.redo_stack.push(HistoryAction::ReplaceStroke { old, new });
|
self.redo_stack
|
||||||
|
.push(HistoryAction::ReplaceStroke { old, new });
|
||||||
}
|
}
|
||||||
HistoryAction::ModifyStrokes { before, after } => {
|
HistoryAction::ModifyStrokes { before, after } => {
|
||||||
for b in &before {
|
for b in &before {
|
||||||
@@ -91,7 +92,8 @@ impl HistoryStack {
|
|||||||
strokes[pos] = b.clone();
|
strokes[pos] = b.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.redo_stack.push(HistoryAction::ModifyStrokes { before, after });
|
self.redo_stack
|
||||||
|
.push(HistoryAction::ModifyStrokes { before, after });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,8 @@ impl HistoryStack {
|
|||||||
if let Some(pos) = strokes.iter().position(|s| s.id == old.id) {
|
if let Some(pos) = strokes.iter().position(|s| s.id == old.id) {
|
||||||
strokes[pos] = new.clone();
|
strokes[pos] = new.clone();
|
||||||
}
|
}
|
||||||
self.undo_stack.push(HistoryAction::ReplaceStroke { old, new });
|
self.undo_stack
|
||||||
|
.push(HistoryAction::ReplaceStroke { old, new });
|
||||||
}
|
}
|
||||||
HistoryAction::ModifyStrokes { before, after } => {
|
HistoryAction::ModifyStrokes { before, after } => {
|
||||||
for a in &after {
|
for a in &after {
|
||||||
@@ -134,7 +137,8 @@ impl HistoryStack {
|
|||||||
strokes[pos] = a.clone();
|
strokes[pos] = a.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.undo_stack.push(HistoryAction::ModifyStrokes { before, after });
|
self.undo_stack
|
||||||
|
.push(HistoryAction::ModifyStrokes { before, after });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-6
@@ -4,7 +4,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::{
|
use winit::{
|
||||||
event::{ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent},
|
event::{
|
||||||
|
ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent,
|
||||||
|
},
|
||||||
event_loop::{ControlFlow, EventLoop},
|
event_loop::{ControlFlow, EventLoop},
|
||||||
keyboard::{Key, ModifiersState, NamedKey},
|
keyboard::{Key, ModifiersState, NamedKey},
|
||||||
window::{Window, WindowBuilder},
|
window::{Window, WindowBuilder},
|
||||||
@@ -16,7 +18,9 @@ use ink_notes::canvas::CanvasState;
|
|||||||
use ink_notes::document::{Notebook, PageBackground};
|
use ink_notes::document::{Notebook, PageBackground};
|
||||||
use ink_notes::export::{export_notebook_to_pdf, ExportStrategy};
|
use ink_notes::export::{export_notebook_to_pdf, ExportStrategy};
|
||||||
use ink_notes::geometry::{self, RecognizedShape};
|
use ink_notes::geometry::{self, RecognizedShape};
|
||||||
use ink_notes::selection::{render_selection_overlay, DragMode, SelectionHandleHit, SelectionState};
|
use ink_notes::selection::{
|
||||||
|
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
||||||
|
};
|
||||||
use ink_notes::storage::{self, NotebookManifest};
|
use ink_notes::storage::{self, NotebookManifest};
|
||||||
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
||||||
use ink_notes::updater::UpdaterController;
|
use ink_notes::updater::UpdaterController;
|
||||||
@@ -76,12 +80,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok((nb, m)) => (nb, m),
|
Ok((nb, m)) => (nb, m),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("[Storage-Fehler] Konnte Notizbuch nicht laden ({err}). Erstelle neues Notizbuch.");
|
eprintln!("[Storage-Fehler] Konnte Notizbuch nicht laden ({err}). Erstelle neues Notizbuch.");
|
||||||
(Notebook::new(), NotebookManifest::new(&initial_id, &initial_title))
|
(
|
||||||
|
Notebook::new(),
|
||||||
|
NotebookManifest::new(&initial_id, &initial_title),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("[Storage] Erstelle neues Notizbuch in: {}", storage_dir.display());
|
println!(
|
||||||
(Notebook::new(), NotebookManifest::new(&initial_id, &initial_title))
|
"[Storage] Erstelle neues Notizbuch in: {}",
|
||||||
|
storage_dir.display()
|
||||||
|
);
|
||||||
|
(
|
||||||
|
Notebook::new(),
|
||||||
|
NotebookManifest::new(&initial_id, &initial_title),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Asynchronen Autosave-Controller mit 1.5s Debounce starten
|
// 2. Asynchronen Autosave-Controller mit 1.5s Debounce starten
|
||||||
@@ -139,7 +152,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let eraser_radius = 30.0_f32; // 3 mm in DIN-A4-Einheiten
|
let eraser_radius = 30.0_f32; // 3 mm in DIN-A4-Einheiten
|
||||||
let stroke_max_radius = 12.0_f32;
|
let stroke_max_radius = 12.0_f32;
|
||||||
|
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(
|
||||||
|
&window,
|
||||||
|
¬ebook,
|
||||||
|
&manifest,
|
||||||
|
ui_state.active_tool,
|
||||||
|
autosave.status(),
|
||||||
|
);
|
||||||
|
|
||||||
println!("=== ink-notes (Multi-Color & Objektauswahl/Transformation) bereit ===");
|
println!("=== ink-notes (Multi-Color & Objektauswahl/Transformation) bereit ===");
|
||||||
println!("Toolbar:");
|
println!("Toolbar:");
|
||||||
|
|||||||
+23
-6
@@ -2,8 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Stellt transaktionale Manipulationen für einzelne oder mehrere Striche und geometrische Formen bereit.
|
//! Stellt transaktionale Manipulationen für einzelne oder mehrere Striche und geometrische Formen bereit.
|
||||||
|
|
||||||
use std::f32::consts::FRAC_PI_2;
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
use crate::canvas::CanvasState;
|
use crate::canvas::CanvasState;
|
||||||
use crate::document::{Aabb, Page, Stroke};
|
use crate::document::{Aabb, Page, Stroke};
|
||||||
@@ -285,7 +285,11 @@ pub fn render_selection_overlay(
|
|||||||
));
|
));
|
||||||
|
|
||||||
// 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box)
|
// 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box)
|
||||||
if let DragMode::BoxSelect { start_page, current_page } = selection.drag_mode {
|
if let DragMode::BoxSelect {
|
||||||
|
start_page,
|
||||||
|
current_page,
|
||||||
|
} = selection.drag_mode
|
||||||
|
{
|
||||||
let p1 = canvas.page_to_screen(start_page);
|
let p1 = canvas.page_to_screen(start_page);
|
||||||
let p2 = canvas.page_to_screen(current_page);
|
let p2 = canvas.page_to_screen(current_page);
|
||||||
let min_x = p1[0].min(p2[0]);
|
let min_x = p1[0].min(p2[0]);
|
||||||
@@ -304,7 +308,10 @@ pub fn render_selection_overlay(
|
|||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
rect,
|
rect,
|
||||||
egui::Rounding::same(4.0_f32),
|
egui::Rounding::same(4.0_f32),
|
||||||
egui::Stroke::new(1.5_f32, egui::Color32::from_rgba_premultiplied(80, 160, 255, 220)),
|
egui::Stroke::new(
|
||||||
|
1.5_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(80, 160, 255, 220),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -357,7 +364,11 @@ pub fn render_selection_overlay(
|
|||||||
|
|
||||||
for c in corners {
|
for c in corners {
|
||||||
let handle_rect = egui::Rect::from_center_size(c, egui::vec2(handle_size, handle_size));
|
let handle_rect = egui::Rect::from_center_size(c, egui::vec2(handle_size, handle_size));
|
||||||
painter.rect_filled(handle_rect, egui::Rounding::same(1.5_f32), egui::Color32::WHITE);
|
painter.rect_filled(
|
||||||
|
handle_rect,
|
||||||
|
egui::Rounding::same(1.5_f32),
|
||||||
|
egui::Color32::WHITE,
|
||||||
|
);
|
||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
handle_rect,
|
handle_rect,
|
||||||
egui::Rounding::same(1.5_f32),
|
egui::Rounding::same(1.5_f32),
|
||||||
@@ -369,7 +380,11 @@ pub fn render_selection_overlay(
|
|||||||
let badge_text = format!(
|
let badge_text = format!(
|
||||||
"{} {} | [R] Drehen [Entf] Löschen",
|
"{} {} | [R] Drehen [Entf] Löschen",
|
||||||
selection.count(),
|
selection.count(),
|
||||||
if selection.count() == 1 { "Objekt" } else { "Objekte" }
|
if selection.count() == 1 {
|
||||||
|
"Objekt"
|
||||||
|
} else {
|
||||||
|
"Objekte"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
let badge_pos = egui::pos2(rect.center().x, rect.max.y + 16.0);
|
let badge_pos = egui::pos2(rect.center().x, rect.max.y + 16.0);
|
||||||
painter.text(
|
painter.text(
|
||||||
@@ -459,7 +474,9 @@ mod tests {
|
|||||||
let orig_pos = page.strokes[0].points[0].position;
|
let orig_pos = page.strokes[0].points[0].position;
|
||||||
assert!(sel.rotate_selected_90_deg(&mut page));
|
assert!(sel.rotate_selected_90_deg(&mut page));
|
||||||
let rotated_pos = page.strokes[0].points[0].position;
|
let rotated_pos = page.strokes[0].points[0].position;
|
||||||
assert!((orig_pos.x - rotated_pos.x).abs() > 0.1 || (orig_pos.y - rotated_pos.y).abs() > 0.1);
|
assert!(
|
||||||
|
(orig_pos.x - rotated_pos.x).abs() > 0.1 || (orig_pos.y - rotated_pos.y).abs() > 0.1
|
||||||
|
);
|
||||||
|
|
||||||
// Undo rotate
|
// Undo rotate
|
||||||
assert!(page.undo());
|
assert!(page.undo());
|
||||||
|
|||||||
+34
-15
@@ -355,11 +355,11 @@ fn rdp_mark(positions: &[glam::Vec2], start: usize, end: usize, epsilon: f32, ke
|
|||||||
let mut max_dist = 0.0_f32;
|
let mut max_dist = 0.0_f32;
|
||||||
let mut max_idx = start;
|
let mut max_idx = start;
|
||||||
|
|
||||||
for i in (start + 1)..end {
|
for (offset, &pos) in positions[(start + 1)..end].iter().enumerate() {
|
||||||
let dist = point_to_segment_distance(positions[i], a, b);
|
let dist = point_to_segment_distance(pos, a, b);
|
||||||
if dist > max_dist {
|
if dist > max_dist {
|
||||||
max_dist = dist;
|
max_dist = dist;
|
||||||
max_idx = i;
|
max_idx = start + 1 + offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,10 +418,7 @@ pub fn detect_corners(samples: &[StylusSample], threshold_deg: f32) -> Vec<usize
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tastet einen zusammenhängenden, glatten Teilabschnitt (Run) zwischen Ecken ab.
|
/// Tastet einen zusammenhängenden, glatten Teilabschnitt (Run) zwischen Ecken ab.
|
||||||
fn sample_smooth_run(
|
fn sample_smooth_run(control_points: &[ControlPoint], config: &SplineConfig) -> Vec<SplineSample> {
|
||||||
control_points: &[ControlPoint],
|
|
||||||
config: &SplineConfig,
|
|
||||||
) -> Vec<SplineSample> {
|
|
||||||
match control_points.len() {
|
match control_points.len() {
|
||||||
0 => Vec::new(),
|
0 => Vec::new(),
|
||||||
1 => {
|
1 => {
|
||||||
@@ -621,7 +618,12 @@ pub fn generate_finalized_triangle_strip(
|
|||||||
/// 3. Ramer-Douglas-Peucker (RDP) Dezimierung (Toleranz ca. 0.05 bis 0.1 mm)
|
/// 3. Ramer-Douglas-Peucker (RDP) Dezimierung (Toleranz ca. 0.05 bis 0.1 mm)
|
||||||
/// 4. Generierung der finalen Triangle-Strips mit runden Kappen an Start und Ende.
|
/// 4. Generierung der finalen Triangle-Strips mit runden Kappen an Start und Ende.
|
||||||
pub fn finalize_stroke(raw_samples: &[StylusSample]) -> Stroke {
|
pub fn finalize_stroke(raw_samples: &[StylusSample]) -> Stroke {
|
||||||
finalize_stroke_with_config(0, raw_samples, [0.0, 0.0, 0.0, 1.0], &SplineConfig::default())
|
finalize_stroke_with_config(
|
||||||
|
0,
|
||||||
|
raw_samples,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
&SplineConfig::default(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vollständige Finalisierung mit konfigurierbarer Stroke-ID, Farbe und SplineConfig.
|
/// Vollständige Finalisierung mit konfigurierbarer Stroke-ID, Farbe und SplineConfig.
|
||||||
@@ -808,9 +810,10 @@ mod tests {
|
|||||||
assert!(!stroke.vertices.is_empty());
|
assert!(!stroke.vertices.is_empty());
|
||||||
|
|
||||||
// 2. Der Eckpunkt (100.0, 0.0) muss in den Strichpunkten erhalten bleiben
|
// 2. Der Eckpunkt (100.0, 0.0) muss in den Strichpunkten erhalten bleiben
|
||||||
let corner_found = stroke.points.iter().any(|s| {
|
let corner_found = stroke
|
||||||
(s.position.x - 100.0).abs() < 1e-3 && (s.position.y - 0.0).abs() < 1e-3
|
.points
|
||||||
});
|
.iter()
|
||||||
|
.any(|s| (s.position.x - 100.0).abs() < 1e-3 && (s.position.y - 0.0).abs() < 1e-3);
|
||||||
assert!(
|
assert!(
|
||||||
corner_found,
|
corner_found,
|
||||||
"Eckpunkt (100.0, 0.0) muss in den finalen Strichpunkten erhalten bleiben!"
|
"Eckpunkt (100.0, 0.0) muss in den finalen Strichpunkten erhalten bleiben!"
|
||||||
@@ -837,7 +840,11 @@ mod tests {
|
|||||||
|
|
||||||
// 4. Ecken-Erkennung prüft den 90°-Winkel (> 65 Grad)
|
// 4. Ecken-Erkennung prüft den 90°-Winkel (> 65 Grad)
|
||||||
let corners = detect_corners(&stroke.points, CORNER_ANGLE_THRESHOLD_DEG);
|
let corners = detect_corners(&stroke.points, CORNER_ANGLE_THRESHOLD_DEG);
|
||||||
assert_eq!(corners.len(), 1, "Exakt eine 90-Grad-Ecke muss erkannt werden");
|
assert_eq!(
|
||||||
|
corners.len(),
|
||||||
|
1,
|
||||||
|
"Exakt eine 90-Grad-Ecke muss erkannt werden"
|
||||||
|
);
|
||||||
let corner_idx = corners[0];
|
let corner_idx = corners[0];
|
||||||
let corner_pt = stroke.points[corner_idx];
|
let corner_pt = stroke.points[corner_idx];
|
||||||
assert!((corner_pt.position.x - 100.0).abs() < 1e-3);
|
assert!((corner_pt.position.x - 100.0).abs() < 1e-3);
|
||||||
@@ -847,9 +854,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_pressure_smoothing() {
|
fn test_pressure_smoothing() {
|
||||||
let samples = vec![
|
let samples = vec![
|
||||||
StylusSample { position: glam::Vec2::new(0.0, 0.0), pressure: 0.2, timestamp: 0.0 },
|
StylusSample {
|
||||||
StylusSample { position: glam::Vec2::new(1.0, 0.0), pressure: 0.8, timestamp: 0.1 },
|
position: glam::Vec2::new(0.0, 0.0),
|
||||||
StylusSample { position: glam::Vec2::new(2.0, 0.0), pressure: 0.2, timestamp: 0.2 },
|
pressure: 0.2,
|
||||||
|
timestamp: 0.0,
|
||||||
|
},
|
||||||
|
StylusSample {
|
||||||
|
position: glam::Vec2::new(1.0, 0.0),
|
||||||
|
pressure: 0.8,
|
||||||
|
timestamp: 0.1,
|
||||||
|
},
|
||||||
|
StylusSample {
|
||||||
|
position: glam::Vec2::new(2.0, 0.0),
|
||||||
|
pressure: 0.2,
|
||||||
|
timestamp: 0.2,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let smoothed = smooth_pressure(&samples);
|
let smoothed = smooth_pressure(&samples);
|
||||||
|
|||||||
+37
-12
@@ -69,7 +69,12 @@ impl StrokeData {
|
|||||||
/// Wandelt die semantischen Daten deterministisch zurück in einen `Stroke` um,
|
/// Wandelt die semantischen Daten deterministisch zurück in einen `Stroke` um,
|
||||||
/// inklusive Wiederherstellung des Triangle-Strip-Vertexpuffers und der AABB.
|
/// inklusive Wiederherstellung des Triangle-Strip-Vertexpuffers und der AABB.
|
||||||
pub fn to_stroke(&self) -> Stroke {
|
pub fn to_stroke(&self) -> Stroke {
|
||||||
let stylus_samples: Vec<StylusSample> = self.samples.iter().copied().map(StylusSample::from).collect();
|
let stylus_samples: Vec<StylusSample> = self
|
||||||
|
.samples
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(StylusSample::from)
|
||||||
|
.collect();
|
||||||
let vertices = if let Some(ref shape) = self.shape {
|
let vertices = if let Some(ref shape) = self.shape {
|
||||||
let (_, shape_verts) = crate::geometry::shape_to_mesh(shape, 20.0, self.color);
|
let (_, shape_verts) = crate::geometry::shape_to_mesh(shape, 20.0, self.color);
|
||||||
shape_verts
|
shape_verts
|
||||||
@@ -207,7 +212,11 @@ pub fn reconstruct_stroke_vertices(samples: &[StylusSample], color: [f32; 4]) ->
|
|||||||
let angle = (i as f32 / segments as f32) * std::f32::consts::TAU;
|
let angle = (i as f32 / segments as f32) * std::f32::consts::TAU;
|
||||||
let offset = glam::Vec2::new(angle.cos() * radius, angle.sin() * radius);
|
let offset = glam::Vec2::new(angle.cos() * radius, angle.sin() * radius);
|
||||||
vertices.push(InkVertex::new(pt.position.x, pt.position.y, color));
|
vertices.push(InkVertex::new(pt.position.x, pt.position.y, color));
|
||||||
vertices.push(InkVertex::new(pt.position.x + offset.x, pt.position.y + offset.y, color));
|
vertices.push(InkVertex::new(
|
||||||
|
pt.position.x + offset.x,
|
||||||
|
pt.position.y + offset.y,
|
||||||
|
color,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return vertices;
|
return vertices;
|
||||||
}
|
}
|
||||||
@@ -241,7 +250,10 @@ pub fn reconstruct_stroke_vertices(samples: &[StylusSample], color: [f32; 4]) ->
|
|||||||
/// Schreibt Inhalt atomar auf die Festplatte (über temporäre Datei + `std::fs::rename`).
|
/// Schreibt Inhalt atomar auf die Festplatte (über temporäre Datei + `std::fs::rename`).
|
||||||
pub fn atomic_write_file(path: &Path, content: &[u8]) -> Result<(), io::Error> {
|
pub fn atomic_write_file(path: &Path, content: &[u8]) -> Result<(), io::Error> {
|
||||||
let dir = path.parent().ok_or_else(|| {
|
let dir = path.parent().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::NotFound, "Übergeordnetes Verzeichnis nicht gefunden")
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Übergeordnetes Verzeichnis nicht gefunden",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
fs::create_dir_all(dir)?;
|
fs::create_dir_all(dir)?;
|
||||||
|
|
||||||
@@ -475,7 +487,7 @@ pub fn list_notebooks(base_dir: &Path) -> Vec<NotebookSummary> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Nach letztem Änderungsdatum absteigend sortieren (neueste zuerst)
|
// Nach letztem Änderungsdatum absteigend sortieren (neueste zuerst)
|
||||||
list.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
list.sort_by_key(|a| std::cmp::Reverse(a.modified_at));
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +625,11 @@ mod tests {
|
|||||||
assert_eq!(loaded_stroke.color, [0.1, 0.2, 0.8, 1.0]);
|
assert_eq!(loaded_stroke.color, [0.1, 0.2, 0.8, 1.0]);
|
||||||
assert_eq!(loaded_stroke.points.len(), 3);
|
assert_eq!(loaded_stroke.points.len(), 3);
|
||||||
|
|
||||||
for (orig, loaded) in page.strokes[0].points.iter().zip(loaded_stroke.points.iter()) {
|
for (orig, loaded) in page.strokes[0]
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.zip(loaded_stroke.points.iter())
|
||||||
|
{
|
||||||
assert!((orig.position.x - loaded.position.x).abs() < 1e-4);
|
assert!((orig.position.x - loaded.position.x).abs() < 1e-4);
|
||||||
assert!((orig.position.y - loaded.position.y).abs() < 1e-4);
|
assert!((orig.position.y - loaded.position.y).abs() < 1e-4);
|
||||||
assert!((orig.pressure - loaded.pressure).abs() < 1e-4);
|
assert!((orig.pressure - loaded.pressure).abs() < 1e-4);
|
||||||
@@ -633,12 +649,16 @@ mod tests {
|
|||||||
|
|
||||||
// Beschädigtes, unvollständiges JSON manuell schreiben
|
// Beschädigtes, unvollständiges JSON manuell schreiben
|
||||||
let corrupted_file = path.join("page_99.json");
|
let corrupted_file = path.join("page_99.json");
|
||||||
let truncated_json = br#"{"id": 99, "background": "Grid5mm", "strokes": [{"id": 1, "color": [0.1, 0.2"#;
|
let truncated_json =
|
||||||
|
br#"{"id": 99, "background": "Grid5mm", "strokes": [{"id": 1, "color": [0.1, 0.2"#;
|
||||||
fs::write(&corrupted_file, truncated_json).expect("Konnte Datei nicht schreiben");
|
fs::write(&corrupted_file, truncated_json).expect("Konnte Datei nicht schreiben");
|
||||||
|
|
||||||
// Laden muss einen abgefangenen CorruptedPage-Fehler liefern und darf nicht panicken
|
// Laden muss einen abgefangenen CorruptedPage-Fehler liefern und darf nicht panicken
|
||||||
let load_res = load_page(path, 99);
|
let load_res = load_page(path, 99);
|
||||||
assert!(load_res.is_err(), "Laden beschädigter Daten muss fehlschlagen");
|
assert!(
|
||||||
|
load_res.is_err(),
|
||||||
|
"Laden beschädigter Daten muss fehlschlagen"
|
||||||
|
);
|
||||||
|
|
||||||
match load_res.unwrap_err() {
|
match load_res.unwrap_err() {
|
||||||
StorageError::CorruptedPage(id, msg) => {
|
StorageError::CorruptedPage(id, msg) => {
|
||||||
@@ -729,16 +749,21 @@ mod tests {
|
|||||||
// 3. Auflisten
|
// 3. Auflisten
|
||||||
let summaries = list_notebooks(base_dir);
|
let summaries = list_notebooks(base_dir);
|
||||||
assert_eq!(summaries.len(), 2);
|
assert_eq!(summaries.len(), 2);
|
||||||
assert!(summaries.iter().any(|s| s.id == m1.id && s.title == "Mathe Vorlesung"));
|
assert!(summaries
|
||||||
assert!(summaries.iter().any(|s| s.id == m2.id && s.title == "Skizzen & Entwürfe"));
|
.iter()
|
||||||
|
.any(|s| s.id == m1.id && s.title == "Mathe Vorlesung"));
|
||||||
|
assert!(summaries
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.id == m2.id && s.title == "Skizzen & Entwürfe"));
|
||||||
|
|
||||||
// 4. Umbenennen
|
// 4. Umbenennen
|
||||||
rename_notebook(&dir1, &mut m1, "Höhere Mathematik")
|
rename_notebook(&dir1, &mut m1, "Höhere Mathematik").expect("Umbenennen muss klappen");
|
||||||
.expect("Umbenennen muss klappen");
|
|
||||||
assert_eq!(m1.title, "Höhere Mathematik");
|
assert_eq!(m1.title, "Höhere Mathematik");
|
||||||
|
|
||||||
let updated_summaries = list_notebooks(base_dir);
|
let updated_summaries = list_notebooks(base_dir);
|
||||||
assert!(updated_summaries.iter().any(|s| s.id == m1.id && s.title == "Höhere Mathematik"));
|
assert!(updated_summaries
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.id == m1.id && s.title == "Höhere Mathematik"));
|
||||||
|
|
||||||
// 5. Löschen
|
// 5. Löschen
|
||||||
delete_notebook(&dir2).expect("Löschen muss klappen");
|
delete_notebook(&dir2).expect("Löschen muss klappen");
|
||||||
|
|||||||
@@ -81,11 +81,8 @@ pub const PEN_WIDTH_PRESETS: [(f32, &str); 4] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Dicken-Presets für den Textmarker:
|
/// Dicken-Presets für den Textmarker:
|
||||||
pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] = [
|
pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] =
|
||||||
(4.0, "4.0 mm"),
|
[(4.0, "4.0 mm"), (6.0, "6.0 mm"), (8.0, "8.0 mm")];
|
||||||
(6.0, "6.0 mm"),
|
|
||||||
(8.0, "8.0 mm"),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Aktionen, die von der UI an die Anwendungslogik signalisiert werden.
|
/// Aktionen, die von der UI an die Anwendungslogik signalisiert werden.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -111,7 +108,10 @@ pub enum UiAction {
|
|||||||
// Dokumenten- & Notizbuch-Verwaltung
|
// Dokumenten- & Notizbuch-Verwaltung
|
||||||
OpenNotebook(String),
|
OpenNotebook(String),
|
||||||
CreateNotebook(String),
|
CreateNotebook(String),
|
||||||
RenameNotebook { id: String, new_title: String },
|
RenameNotebook {
|
||||||
|
id: String,
|
||||||
|
new_title: String,
|
||||||
|
},
|
||||||
DeleteNotebook(String),
|
DeleteNotebook(String),
|
||||||
RefreshNotebooks,
|
RefreshNotebooks,
|
||||||
// Gitea Release & Updater
|
// Gitea Release & Updater
|
||||||
@@ -119,7 +119,10 @@ pub enum UiAction {
|
|||||||
DownloadUpdateAsset(crate::updater::GiteaAsset),
|
DownloadUpdateAsset(crate::updater::GiteaAsset),
|
||||||
InstallOrOpenUpdate(std::path::PathBuf),
|
InstallOrOpenUpdate(std::path::PathBuf),
|
||||||
OpenUrlInBrowser(String),
|
OpenUrlInBrowser(String),
|
||||||
SaveUpdaterConfig { server_url: String, auth_token: Option<String> },
|
SaveUpdaterConfig {
|
||||||
|
server_url: String,
|
||||||
|
auth_token: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zustand der grafischen Benutzeroberfläche.
|
/// Zustand der grafischen Benutzeroberfläche.
|
||||||
@@ -312,25 +315,36 @@ fn tool_pill_button_with_indicator(
|
|||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
|
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(110, 165, 255, 190)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(110, 165, 255, 190),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else if response.hovered() {
|
} else if response.hovered() {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
||||||
egui::Color32::from_gray(215),
|
egui::Color32::from_gray(215),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 15)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 15),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Button-Hintergrund
|
// Button-Hintergrund
|
||||||
ui.painter().rect_filled(rect, egui::Rounding::same(8.0), bg);
|
ui.painter()
|
||||||
ui.painter().rect_stroke(rect, egui::Rounding::same(8.0), stroke);
|
.rect_filled(rect, egui::Rounding::same(8.0), bg);
|
||||||
|
ui.painter()
|
||||||
|
.rect_stroke(rect, egui::Rounding::same(8.0), stroke);
|
||||||
|
|
||||||
// Text & Caret
|
// Text & Caret
|
||||||
let label = if is_menu_open {
|
let label = if is_menu_open {
|
||||||
@@ -359,7 +373,8 @@ fn tool_pill_button_with_indicator(
|
|||||||
(indicator_color[2] * 255.0) as u8,
|
(indicator_color[2] * 255.0) as u8,
|
||||||
((indicator_color[3] * 255.0) as u8).max(180),
|
((indicator_color[3] * 255.0) as u8).max(180),
|
||||||
);
|
);
|
||||||
ui.painter().rect_filled(bar_rect, egui::Rounding::same(1.5), bar_color);
|
ui.painter()
|
||||||
|
.rect_filled(bar_rect, egui::Rounding::same(1.5), bar_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
response.on_hover_text(tooltip)
|
response.on_hover_text(tooltip)
|
||||||
@@ -376,13 +391,19 @@ fn tool_pill_button(
|
|||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
|
egui::Color32::from_rgba_premultiplied(45, 80, 145, 230),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(110, 165, 255, 190)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(110, 165, 255, 190),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
||||||
egui::Color32::from_gray(215),
|
egui::Color32::from_gray(215),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 15)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 15),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -415,22 +436,33 @@ fn stroke_preview_button(
|
|||||||
let (bg, border) = if is_active {
|
let (bg, border) = if is_active {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(50, 85, 150, 220),
|
egui::Color32::from_rgba_premultiplied(50, 85, 150, 220),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(120, 175, 255, 200)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(120, 175, 255, 200),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else if response.hovered() {
|
} else if response.hovered() {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 14)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 14),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter().rect_filled(rect, egui::Rounding::same(6.0), bg);
|
ui.painter()
|
||||||
ui.painter().rect_stroke(rect, egui::Rounding::same(6.0), border);
|
.rect_filled(rect, egui::Rounding::same(6.0), bg);
|
||||||
|
ui.painter()
|
||||||
|
.rect_stroke(rect, egui::Rounding::same(6.0), border);
|
||||||
|
|
||||||
// Horizontale Vorschau-Linie zeichnen
|
// Horizontale Vorschau-Linie zeichnen
|
||||||
let line_thickness = (stroke_width_mm * 2.5).clamp(1.0, 9.0);
|
let line_thickness = (stroke_width_mm * 2.5).clamp(1.0, 9.0);
|
||||||
@@ -456,7 +488,11 @@ fn stroke_preview_button(
|
|||||||
egui::Align2::CENTER_CENTER,
|
egui::Align2::CENTER_CENTER,
|
||||||
label_text,
|
label_text,
|
||||||
egui::FontId::proportional(10.0),
|
egui::FontId::proportional(10.0),
|
||||||
if is_active { egui::Color32::WHITE } else { egui::Color32::from_gray(190) },
|
if is_active {
|
||||||
|
egui::Color32::WHITE
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_gray(190)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,30 +500,28 @@ fn stroke_preview_button(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Kompakter Strichbreiten-Preset-Button für die Haupt-Toolbar.
|
/// Kompakter Strichbreiten-Preset-Button für die Haupt-Toolbar.
|
||||||
fn preset_width_button(
|
fn preset_width_button(ui: &mut egui::Ui, is_active: bool, text: &str) -> egui::Response {
|
||||||
ui: &mut egui::Ui,
|
|
||||||
is_active: bool,
|
|
||||||
text: &str,
|
|
||||||
) -> egui::Response {
|
|
||||||
let (bg, text_color, stroke) = if is_active {
|
let (bg, text_color, stroke) = if is_active {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(50, 85, 150, 230),
|
egui::Color32::from_rgba_premultiplied(50, 85, 150, 230),
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(120, 175, 255, 190)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(120, 175, 255, 190),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 6),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 6),
|
||||||
egui::Color32::from_gray(190),
|
egui::Color32::from_gray(190),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 12)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 12),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let button = egui::Button::new(
|
let button = egui::Button::new(egui::RichText::new(text).color(text_color).size(11.5))
|
||||||
egui::RichText::new(text)
|
|
||||||
.color(text_color)
|
|
||||||
.size(11.5),
|
|
||||||
)
|
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.stroke(stroke)
|
.stroke(stroke)
|
||||||
.rounding(egui::Rounding::same(6.0))
|
.rounding(egui::Rounding::same(6.0))
|
||||||
@@ -512,20 +546,22 @@ fn action_pill_button(
|
|||||||
let (bg, stroke) = if enabled {
|
let (bg, stroke) = if enabled {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 10),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 10),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 22)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 22),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
egui::Color32::from_rgba_premultiplied(255, 255, 255, 3),
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 3),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 8)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let button = egui::Button::new(
|
let button = egui::Button::new(egui::RichText::new(text).color(text_color).size(12.5))
|
||||||
egui::RichText::new(text)
|
|
||||||
.color(text_color)
|
|
||||||
.size(12.5),
|
|
||||||
)
|
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.stroke(stroke)
|
.stroke(stroke)
|
||||||
.rounding(egui::Rounding::same(7.0))
|
.rounding(egui::Rounding::same(7.0))
|
||||||
@@ -541,7 +577,10 @@ fn toolbar_divider(ui: &mut egui::Ui) {
|
|||||||
ui.painter().vline(
|
ui.painter().vline(
|
||||||
rect.center().x,
|
rect.center().x,
|
||||||
rect.y_range(),
|
rect.y_range(),
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 25)),
|
egui::Stroke::new(
|
||||||
|
1.0_f32,
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
}
|
}
|
||||||
@@ -1014,14 +1053,22 @@ pub fn render_ui(
|
|||||||
.color(egui::Color32::WHITE)
|
.color(egui::Color32::WHITE)
|
||||||
.size(12.5),
|
.size(12.5),
|
||||||
);
|
);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(
|
||||||
|
egui::Layout::right_to_left(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::Button::new(egui::RichText::new("✕").size(11.0)).frame(false))
|
.add(
|
||||||
|
egui::Button::new(
|
||||||
|
egui::RichText::new("✕").size(11.0),
|
||||||
|
)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.is_pen_menu_open = false;
|
state.is_pen_menu_open = false;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -1040,7 +1087,8 @@ pub fn render_ui(
|
|||||||
let is_sel = (state.pen_color[0] - color[0]).abs() < 0.05
|
let is_sel = (state.pen_color[0] - color[0]).abs() < 0.05
|
||||||
&& (state.pen_color[1] - color[1]).abs() < 0.05
|
&& (state.pen_color[1] - color[1]).abs() < 0.05
|
||||||
&& (state.pen_color[2] - color[2]).abs() < 0.05;
|
&& (state.pen_color[2] - color[2]).abs() < 0.05;
|
||||||
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked() {
|
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked()
|
||||||
|
{
|
||||||
state.pen_color = color;
|
state.pen_color = color;
|
||||||
actions.push(UiAction::SetColor(color));
|
actions.push(UiAction::SetColor(color));
|
||||||
}
|
}
|
||||||
@@ -1079,7 +1127,14 @@ pub fn render_ui(
|
|||||||
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
|
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
|
||||||
for (preset, label) in PEN_WIDTH_PRESETS {
|
for (preset, label) in PEN_WIDTH_PRESETS {
|
||||||
let is_sel = (state.pen_stroke_width_mm - preset).abs() < 0.05;
|
let is_sel = (state.pen_stroke_width_mm - preset).abs() < 0.05;
|
||||||
if stroke_preview_button(ui, is_sel, preset, label, state.pen_color).clicked()
|
if stroke_preview_button(
|
||||||
|
ui,
|
||||||
|
is_sel,
|
||||||
|
preset,
|
||||||
|
label,
|
||||||
|
state.pen_color,
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
{
|
{
|
||||||
state.pen_stroke_width_mm = preset;
|
state.pen_stroke_width_mm = preset;
|
||||||
actions.push(UiAction::SetStrokeWidthMm(preset));
|
actions.push(UiAction::SetStrokeWidthMm(preset));
|
||||||
@@ -1145,14 +1200,22 @@ pub fn render_ui(
|
|||||||
.color(egui::Color32::WHITE)
|
.color(egui::Color32::WHITE)
|
||||||
.size(12.5),
|
.size(12.5),
|
||||||
);
|
);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(
|
||||||
|
egui::Layout::right_to_left(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::Button::new(egui::RichText::new("✕").size(11.0)).frame(false))
|
.add(
|
||||||
|
egui::Button::new(
|
||||||
|
egui::RichText::new("✕").size(11.0),
|
||||||
|
)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.is_highlighter_menu_open = false;
|
state.is_highlighter_menu_open = false;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -1168,10 +1231,12 @@ pub fn render_ui(
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
|
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
|
||||||
for (color, label) in HIGHLIGHTER_COLOR_PRESETS {
|
for (color, label) in HIGHLIGHTER_COLOR_PRESETS {
|
||||||
let is_sel = (state.highlighter_color[0] - color[0]).abs() < 0.05
|
let is_sel = (state.highlighter_color[0] - color[0]).abs()
|
||||||
|
< 0.05
|
||||||
&& (state.highlighter_color[1] - color[1]).abs() < 0.05
|
&& (state.highlighter_color[1] - color[1]).abs() < 0.05
|
||||||
&& (state.highlighter_color[2] - color[2]).abs() < 0.05;
|
&& (state.highlighter_color[2] - color[2]).abs() < 0.05;
|
||||||
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked() {
|
if color_circle_swatch(ui, color, is_sel, label, 22.0).clicked()
|
||||||
|
{
|
||||||
state.highlighter_color = color;
|
state.highlighter_color = color;
|
||||||
actions.push(UiAction::SetColor(color));
|
actions.push(UiAction::SetColor(color));
|
||||||
}
|
}
|
||||||
@@ -1208,7 +1273,8 @@ pub fn render_ui(
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
|
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
|
||||||
for (preset, label) in HIGHLIGHTER_WIDTH_PRESETS {
|
for (preset, label) in HIGHLIGHTER_WIDTH_PRESETS {
|
||||||
let is_sel = (state.highlighter_stroke_width_mm - preset).abs() < 0.1;
|
let is_sel =
|
||||||
|
(state.highlighter_stroke_width_mm - preset).abs() < 0.1;
|
||||||
if stroke_preview_button(
|
if stroke_preview_button(
|
||||||
ui,
|
ui,
|
||||||
is_sel,
|
is_sel,
|
||||||
@@ -1236,13 +1302,18 @@ pub fn render_ui(
|
|||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(
|
||||||
egui::Slider::new(&mut state.highlighter_stroke_width_mm, 2.0..=16.0)
|
egui::Slider::new(
|
||||||
|
&mut state.highlighter_stroke_width_mm,
|
||||||
|
2.0..=16.0,
|
||||||
|
)
|
||||||
.suffix(" mm")
|
.suffix(" mm")
|
||||||
.step_by(0.5),
|
.step_by(0.5),
|
||||||
)
|
)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
actions.push(UiAction::SetStrokeWidthMm(state.highlighter_stroke_width_mm));
|
actions.push(UiAction::SetStrokeWidthMm(
|
||||||
|
state.highlighter_stroke_width_mm,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1299,7 +1370,9 @@ pub fn render_ui(
|
|||||||
);
|
);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| {
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(220.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
for idx in 0..notebook.pages.len() {
|
for idx in 0..notebook.pages.len() {
|
||||||
let is_current = idx == notebook.current_page_index;
|
let is_current = idx == notebook.current_page_index;
|
||||||
let text = format!("Seite {}", idx + 1);
|
let text = format!("Seite {}", idx + 1);
|
||||||
@@ -1471,14 +1544,14 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if state.available_notebooks.len() > 1 {
|
if state.available_notebooks.len() > 1
|
||||||
if ui.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
|
&& ui
|
||||||
|
.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
|
||||||
.on_hover_text("Notizbuch unwiderruflich löschen")
|
.on_hover_text("Notizbuch unwiderruflich löschen")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
|
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button(egui::RichText::new("✏").size(11.0))
|
if ui.button(egui::RichText::new("✏").size(11.0))
|
||||||
.on_hover_text("Titel umbenennen")
|
.on_hover_text("Titel umbenennen")
|
||||||
@@ -1701,11 +1774,11 @@ pub fn render_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
if !release.html_url.is_empty() {
|
if !release.html_url.is_empty()
|
||||||
if ui.button(egui::RichText::new("🌐 Release auf Gitea öffnen").size(12.0)).clicked() {
|
&& ui.button(egui::RichText::new("🌐 Release auf Gitea öffnen").size(12.0)).clicked()
|
||||||
|
{
|
||||||
actions.push(UiAction::OpenUrlInBrowser(release.html_url.clone()));
|
actions.push(UiAction::OpenUrlInBrowser(release.html_url.clone()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
crate::updater::UpdaterStatus::Downloading { asset_name, percent } => {
|
crate::updater::UpdaterStatus::Downloading { asset_name, percent } => {
|
||||||
@@ -1810,7 +1883,12 @@ pub fn render_ui(
|
|||||||
.order(egui::Order::Tooltip)
|
.order(egui::Order::Tooltip)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
egui::Frame::none()
|
egui::Frame::none()
|
||||||
.fill(egui::Color32::from_rgba_unmultiplied(20, 25, 30, alpha.min(235)))
|
.fill(egui::Color32::from_rgba_unmultiplied(
|
||||||
|
20,
|
||||||
|
25,
|
||||||
|
30,
|
||||||
|
alpha.min(235),
|
||||||
|
))
|
||||||
.rounding(egui::Rounding::same(10.0))
|
.rounding(egui::Rounding::same(10.0))
|
||||||
.stroke(egui::Stroke::new(
|
.stroke(egui::Stroke::new(
|
||||||
1.0_f32,
|
1.0_f32,
|
||||||
@@ -1826,7 +1904,9 @@ pub fn render_ui(
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(format!("✓ {}", msg))
|
egui::RichText::new(format!("✓ {}", msg))
|
||||||
.color(egui::Color32::from_rgba_unmultiplied(220, 255, 230, alpha))
|
.color(egui::Color32::from_rgba_unmultiplied(
|
||||||
|
220, 255, 230, alpha,
|
||||||
|
))
|
||||||
.size(13.0)
|
.size(13.0)
|
||||||
.strong(),
|
.strong(),
|
||||||
);
|
);
|
||||||
|
|||||||
+716
-141
File diff suppressed because it is too large
Load Diff
+8
-6
@@ -3,8 +3,8 @@
|
|||||||
//! Berechnet die affine Transformation zwischen Bildschirmkoordinaten (Window-Pixel),
|
//! Berechnet die affine Transformation zwischen Bildschirmkoordinaten (Window-Pixel),
|
||||||
//! virtuellen DIN-A4-Seitenkoordinaten ([0, 2100] x [0, 2970]) und dem GPU-NDC-Raum ([-1, 1]).
|
//! virtuellen DIN-A4-Seitenkoordinaten ([0, 2100] x [0, 2970]) und dem GPU-NDC-Raum ([-1, 1]).
|
||||||
|
|
||||||
use glam::{Mat4, Vec2, Vec4};
|
|
||||||
use crate::document::{PageViewport, PAGE_HEIGHT, PAGE_WIDTH};
|
use crate::document::{PageViewport, PAGE_HEIGHT, PAGE_WIDTH};
|
||||||
|
use glam::{Mat4, Vec2, Vec4};
|
||||||
|
|
||||||
/// Minimaler und maximaler Zoomfaktor.
|
/// Minimaler und maximaler Zoomfaktor.
|
||||||
pub const MIN_ZOOM: f32 = 0.25;
|
pub const MIN_ZOOM: f32 = 0.25;
|
||||||
@@ -40,7 +40,12 @@ impl ViewportState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Passt den Pan-Offset um eine Bildschirmverschiebung (in Fensterpixeln) an.
|
/// Passt den Pan-Offset um eine Bildschirmverschiebung (in Fensterpixeln) an.
|
||||||
pub fn pan_by_screen_delta(&mut self, delta_x: f32, delta_y: f32, page_viewport: &PageViewport) {
|
pub fn pan_by_screen_delta(
|
||||||
|
&mut self,
|
||||||
|
delta_x: f32,
|
||||||
|
delta_y: f32,
|
||||||
|
page_viewport: &PageViewport,
|
||||||
|
) {
|
||||||
let base_scale = page_viewport.page_width / PAGE_WIDTH;
|
let base_scale = page_viewport.page_width / PAGE_WIDTH;
|
||||||
if base_scale * self.zoom > 1e-6 {
|
if base_scale * self.zoom > 1e-6 {
|
||||||
self.pan_offset[0] += delta_x / (base_scale * self.zoom);
|
self.pan_offset[0] += delta_x / (base_scale * self.zoom);
|
||||||
@@ -122,10 +127,7 @@ impl ViewportState {
|
|||||||
page_viewport: &PageViewport,
|
page_viewport: &PageViewport,
|
||||||
) -> Vec2 {
|
) -> Vec2 {
|
||||||
let pt = self.screen_to_page_unclamped(screen_x, screen_y, page_viewport);
|
let pt = self.screen_to_page_unclamped(screen_x, screen_y, page_viewport);
|
||||||
Vec2::new(
|
Vec2::new(pt.x.clamp(0.0, PAGE_WIDTH), pt.y.clamp(0.0, PAGE_HEIGHT))
|
||||||
pt.x.clamp(0.0, PAGE_WIDTH),
|
|
||||||
pt.y.clamp(0.0, PAGE_HEIGHT),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transformiert einen DIN-A4-Seitenpunkt in Fenster-Pixelkoordinaten.
|
/// Transformiert einen DIN-A4-Seitenpunkt in Fenster-Pixelkoordinaten.
|
||||||
|
|||||||
Reference in New Issue
Block a user