a whole load of work in progress
This commit is contained in:
		
							parent
							
								
									caec0b8ac9
								
							
						
					
					
						commit
						26ba098183
					
				
					 63 changed files with 3234 additions and 321 deletions
				
			
		
							
								
								
									
										5
									
								
								.cargo/config.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.cargo/config.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
[target.wasm32-unknown-unknown]
 | 
			
		||||
rustflags = [
 | 
			
		||||
    "-C", "target-feature=+bulk-memory",
 | 
			
		||||
    "-C", "target-feature=+simd128",
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
/target
 | 
			
		||||
/database
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										738
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										738
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -17,6 +17,39 @@ version = "1.0.2"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ahash"
 | 
			
		||||
version = "0.8.11"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "version_check",
 | 
			
		||||
 "zerocopy",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "android-tzdata"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "android_system_properties"
 | 
			
		||||
version = "0.1.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "arrayref"
 | 
			
		||||
version = "0.3.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "arrayvec"
 | 
			
		||||
version = "0.7.4"
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +81,8 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "async-trait",
 | 
			
		||||
 "axum-core",
 | 
			
		||||
 "axum-macros",
 | 
			
		||||
 "base64 0.21.7",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "http",
 | 
			
		||||
| 
						 | 
				
			
			@ -66,8 +101,10 @@ dependencies = [
 | 
			
		|||
 "serde_json",
 | 
			
		||||
 "serde_path_to_error",
 | 
			
		||||
 "serde_urlencoded",
 | 
			
		||||
 "sha1",
 | 
			
		||||
 "sync_wrapper 1.0.1",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-tungstenite",
 | 
			
		||||
 "tower",
 | 
			
		||||
 "tower-layer",
 | 
			
		||||
 "tower-service",
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +132,18 @@ dependencies = [
 | 
			
		|||
 "tracing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "axum-macros"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "heck",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "backtrace"
 | 
			
		||||
version = "0.3.71"
 | 
			
		||||
| 
						 | 
				
			
			@ -110,33 +159,57 @@ dependencies = [
 | 
			
		|||
 "rustc-demangle",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "base64"
 | 
			
		||||
version = "0.21.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "base64"
 | 
			
		||||
version = "0.22.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bitflags"
 | 
			
		||||
version = "2.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "block-buffer"
 | 
			
		||||
version = "0.10.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "generic-array",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bumpalo"
 | 
			
		||||
version = "3.16.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bytemuck"
 | 
			
		||||
version = "1.16.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "byteorder"
 | 
			
		||||
version = "1.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bytes"
 | 
			
		||||
version = "1.7.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "canvane"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "axum",
 | 
			
		||||
 "color-eyre",
 | 
			
		||||
 "copy_dir",
 | 
			
		||||
 "eyre",
 | 
			
		||||
 "haku",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tower-http",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-subscriber",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cc"
 | 
			
		||||
version = "1.1.8"
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +222,20 @@ version = "1.0.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "chrono"
 | 
			
		||||
version = "0.4.38"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "android-tzdata",
 | 
			
		||||
 "iana-time-zone",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "color-eyre"
 | 
			
		||||
version = "0.6.3"
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +272,87 @@ dependencies = [
 | 
			
		|||
 "walkdir",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "core-foundation-sys"
 | 
			
		||||
version = "0.8.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cpufeatures"
 | 
			
		||||
version = "0.2.12"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "crossbeam-utils"
 | 
			
		||||
version = "0.8.20"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "crypto-common"
 | 
			
		||||
version = "0.1.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "generic-array",
 | 
			
		||||
 "typenum",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dashmap"
 | 
			
		||||
version = "6.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "crossbeam-utils",
 | 
			
		||||
 "hashbrown",
 | 
			
		||||
 "lock_api",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "parking_lot_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "data-encoding"
 | 
			
		||||
version = "2.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "derive_more"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "derive_more-impl",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "derive_more-impl"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[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]]
 | 
			
		||||
name = "dlmalloc"
 | 
			
		||||
version = "0.2.6"
 | 
			
		||||
| 
						 | 
				
			
			@ -196,6 +364,12 @@ dependencies = [
 | 
			
		|||
 "windows-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "equivalent"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "eyre"
 | 
			
		||||
version = "0.6.12"
 | 
			
		||||
| 
						 | 
				
			
			@ -206,6 +380,18 @@ dependencies = [
 | 
			
		|||
 "once_cell",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fallible-iterator"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fallible-streaming-iterator"
 | 
			
		||||
version = "0.1.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fnv"
 | 
			
		||||
version = "1.0.7"
 | 
			
		||||
| 
						 | 
				
			
			@ -255,9 +441,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-sink",
 | 
			
		||||
 "futures-task",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "pin-utils",
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "getrandom"
 | 
			
		||||
version = "0.2.15"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "wasi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +478,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
 | 
			
		|||
[[package]]
 | 
			
		||||
name = "haku"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "tiny-skia",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "haku-cli"
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +499,30 @@ dependencies = [
 | 
			
		|||
 "log",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hashbrown"
 | 
			
		||||
version = "0.14.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "ahash",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hashlink"
 | 
			
		||||
version = "0.9.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "hashbrown",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "heck"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hermit-abi"
 | 
			
		||||
version = "0.3.9"
 | 
			
		||||
| 
						 | 
				
			
			@ -379,18 +615,70 @@ dependencies = [
 | 
			
		|||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "iana-time-zone"
 | 
			
		||||
version = "0.1.60"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "android_system_properties",
 | 
			
		||||
 "core-foundation-sys",
 | 
			
		||||
 "iana-time-zone-haiku",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "windows-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "iana-time-zone-haiku"
 | 
			
		||||
version = "0.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "idna"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unicode-bidi",
 | 
			
		||||
 "unicode-normalization",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "indenter"
 | 
			
		||||
version = "0.3.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "indexmap"
 | 
			
		||||
version = "2.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "equivalent",
 | 
			
		||||
 "hashbrown",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itoa"
 | 
			
		||||
version = "1.0.11"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "js-sys"
 | 
			
		||||
version = "0.3.69"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lazy_static"
 | 
			
		||||
version = "1.5.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -403,6 +691,23 @@ version = "0.2.155"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libm"
 | 
			
		||||
version = "0.2.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libsqlite3-sys"
 | 
			
		||||
version = "0.30.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "pkg-config",
 | 
			
		||||
 "vcpkg",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lock_api"
 | 
			
		||||
version = "0.4.12"
 | 
			
		||||
| 
						 | 
				
			
			@ -478,6 +783,15 @@ dependencies = [
 | 
			
		|||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-traits"
 | 
			
		||||
version = "0.2.19"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "object"
 | 
			
		||||
version = "0.32.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -566,6 +880,21 @@ version = "0.1.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pkg-config"
 | 
			
		||||
version = "0.3.30"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ppv-lite86"
 | 
			
		||||
version = "0.2.20"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "zerocopy",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "proc-macro2"
 | 
			
		||||
version = "1.0.86"
 | 
			
		||||
| 
						 | 
				
			
			@ -584,6 +913,36 @@ dependencies = [
 | 
			
		|||
 "proc-macro2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand"
 | 
			
		||||
version = "0.8.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "rand_chacha",
 | 
			
		||||
 "rand_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand_chacha"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "ppv-lite86",
 | 
			
		||||
 "rand_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand_core"
 | 
			
		||||
version = "0.6.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "getrandom",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "redox_syscall"
 | 
			
		||||
version = "0.5.3"
 | 
			
		||||
| 
						 | 
				
			
			@ -593,6 +952,45 @@ dependencies = [
 | 
			
		|||
 "bitflags",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rkgk"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "axum",
 | 
			
		||||
 "base64 0.22.1",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "color-eyre",
 | 
			
		||||
 "copy_dir",
 | 
			
		||||
 "dashmap",
 | 
			
		||||
 "derive_more",
 | 
			
		||||
 "eyre",
 | 
			
		||||
 "haku",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "rand_chacha",
 | 
			
		||||
 "rusqlite",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "toml",
 | 
			
		||||
 "tower-http",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-subscriber",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rusqlite"
 | 
			
		||||
version = "0.32.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags",
 | 
			
		||||
 "fallible-iterator",
 | 
			
		||||
 "fallible-streaming-iterator",
 | 
			
		||||
 "hashlink",
 | 
			
		||||
 "libsqlite3-sys",
 | 
			
		||||
 "smallvec",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustc-demangle"
 | 
			
		||||
version = "0.1.24"
 | 
			
		||||
| 
						 | 
				
			
			@ -628,18 +1026,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde"
 | 
			
		||||
version = "1.0.205"
 | 
			
		||||
version = "1.0.206"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
 | 
			
		||||
checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde_derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_derive"
 | 
			
		||||
version = "1.0.205"
 | 
			
		||||
version = "1.0.206"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
 | 
			
		||||
checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
| 
						 | 
				
			
			@ -648,9 +1046,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_json"
 | 
			
		||||
version = "1.0.122"
 | 
			
		||||
version = "1.0.124"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
 | 
			
		||||
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "itoa",
 | 
			
		||||
 "memchr",
 | 
			
		||||
| 
						 | 
				
			
			@ -668,6 +1066,15 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_spanned"
 | 
			
		||||
version = "0.6.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_urlencoded"
 | 
			
		||||
version = "0.7.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -680,6 +1087,17 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sha1"
 | 
			
		||||
version = "0.10.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "cpufeatures",
 | 
			
		||||
 "digest",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sharded-slab"
 | 
			
		||||
version = "0.1.7"
 | 
			
		||||
| 
						 | 
				
			
			@ -698,6 +1116,15 @@ dependencies = [
 | 
			
		|||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "slab"
 | 
			
		||||
version = "0.4.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "smallvec"
 | 
			
		||||
version = "1.13.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -714,6 +1141,12 @@ dependencies = [
 | 
			
		|||
 "windows-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "strict-num"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "syn"
 | 
			
		||||
version = "2.0.72"
 | 
			
		||||
| 
						 | 
				
			
			@ -737,6 +1170,26 @@ version = "1.0.1"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thiserror"
 | 
			
		||||
version = "1.0.63"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "thiserror-impl",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thiserror-impl"
 | 
			
		||||
version = "1.0.63"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thread_local"
 | 
			
		||||
version = "1.1.8"
 | 
			
		||||
| 
						 | 
				
			
			@ -747,6 +1200,47 @@ dependencies = [
 | 
			
		|||
 "once_cell",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tiny-skia"
 | 
			
		||||
version = "0.11.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "arrayref",
 | 
			
		||||
 "arrayvec",
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "log",
 | 
			
		||||
 "tiny-skia-path",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tiny-skia-path"
 | 
			
		||||
version = "0.11.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "arrayref",
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
 "libm",
 | 
			
		||||
 "strict-num",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tinyvec"
 | 
			
		||||
version = "1.8.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "tinyvec_macros",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tinyvec_macros"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio"
 | 
			
		||||
version = "1.39.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -776,6 +1270,18 @@ dependencies = [
 | 
			
		|||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio-tungstenite"
 | 
			
		||||
version = "0.21.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "log",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tungstenite",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio-util"
 | 
			
		||||
version = "0.7.11"
 | 
			
		||||
| 
						 | 
				
			
			@ -789,6 +1295,40 @@ dependencies = [
 | 
			
		|||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml"
 | 
			
		||||
version = "0.8.19"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "toml_edit",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_datetime"
 | 
			
		||||
version = "0.6.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml_edit"
 | 
			
		||||
version = "0.22.20"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "indexmap",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_spanned",
 | 
			
		||||
 "toml_datetime",
 | 
			
		||||
 "winnow",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower"
 | 
			
		||||
version = "0.4.13"
 | 
			
		||||
| 
						 | 
				
			
			@ -910,6 +1450,31 @@ dependencies = [
 | 
			
		|||
 "tracing-log",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tungstenite"
 | 
			
		||||
version = "0.21.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "byteorder",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "data-encoding",
 | 
			
		||||
 "http",
 | 
			
		||||
 "httparse",
 | 
			
		||||
 "log",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "sha1",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "url",
 | 
			
		||||
 "utf-8",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typenum"
 | 
			
		||||
version = "1.17.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicase"
 | 
			
		||||
version = "2.7.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -919,18 +1484,56 @@ dependencies = [
 | 
			
		|||
 "version_check",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-bidi"
 | 
			
		||||
version = "0.3.15"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-ident"
 | 
			
		||||
version = "1.0.12"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-normalization"
 | 
			
		||||
version = "0.1.23"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "tinyvec",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "url"
 | 
			
		||||
version = "2.5.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "form_urlencoded",
 | 
			
		||||
 "idna",
 | 
			
		||||
 "percent-encoding",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "utf-8"
 | 
			
		||||
version = "0.7.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "valuable"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "vcpkg"
 | 
			
		||||
version = "0.2.15"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "version_check"
 | 
			
		||||
version = "0.9.5"
 | 
			
		||||
| 
						 | 
				
			
			@ -953,6 +1556,60 @@ version = "0.11.0+wasi-snapshot-preview1"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-bindgen"
 | 
			
		||||
version = "0.2.92"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "wasm-bindgen-macro",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-bindgen-backend"
 | 
			
		||||
version = "0.2.92"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bumpalo",
 | 
			
		||||
 "log",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
 "wasm-bindgen-shared",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-bindgen-macro"
 | 
			
		||||
version = "0.2.92"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "quote",
 | 
			
		||||
 "wasm-bindgen-macro-support",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-bindgen-macro-support"
 | 
			
		||||
version = "0.2.92"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
 "wasm-bindgen-backend",
 | 
			
		||||
 "wasm-bindgen-shared",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-bindgen-shared"
 | 
			
		||||
version = "0.2.92"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winapi"
 | 
			
		||||
version = "0.3.9"
 | 
			
		||||
| 
						 | 
				
			
			@ -984,6 +1641,15 @@ version = "0.4.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-core"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-sys"
 | 
			
		||||
version = "0.52.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -1056,3 +1722,33 @@ name = "windows_x86_64_msvc"
 | 
			
		|||
version = "0.52.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winnow"
 | 
			
		||||
version = "0.6.18"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "memchr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "zerocopy"
 | 
			
		||||
version = "0.7.35"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "byteorder",
 | 
			
		||||
 "zerocopy-derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "zerocopy-derive"
 | 
			
		||||
version = "0.7.35"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,10 @@ log = "0.4.22"
 | 
			
		|||
[profile.wasm-dev]
 | 
			
		||||
inherits = "dev"
 | 
			
		||||
panic = "abort"
 | 
			
		||||
opt-level = 1
 | 
			
		||||
 | 
			
		||||
[profile.wasm-dev.package.tiny-skia]
 | 
			
		||||
opt-level = 3
 | 
			
		||||
 | 
			
		||||
[profile.wasm-release]
 | 
			
		||||
inherits = "release"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								Justfile
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								Justfile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
serve wasm_profile="wasm-dev": (wasm wasm_profile)
 | 
			
		||||
    cargo run -p canvane
 | 
			
		||||
    cargo run -p rkgk
 | 
			
		||||
 | 
			
		||||
wasm profile="wasm-dev":
 | 
			
		||||
    cargo build -p haku-wasm --target wasm32-unknown-unknown --profile {{profile}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "canvane"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
axum = "0.7.5"
 | 
			
		||||
color-eyre = "0.6.3"
 | 
			
		||||
copy_dir = "0.1.3"
 | 
			
		||||
eyre = "0.6.12"
 | 
			
		||||
haku.workspace = true
 | 
			
		||||
tokio = { version = "1.39.2", features = ["full"] }
 | 
			
		||||
tower-http = { version = "0.5.2", features = ["fs"] }
 | 
			
		||||
tracing = "0.1.40"
 | 
			
		||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
 | 
			
		||||
| 
						 | 
				
			
			@ -2,16 +2,19 @@
 | 
			
		|||
 | 
			
		||||
extern crate alloc;
 | 
			
		||||
 | 
			
		||||
use core::{alloc::Layout, ffi::CStr, slice, str};
 | 
			
		||||
use core::{alloc::Layout, slice};
 | 
			
		||||
 | 
			
		||||
use alloc::{boxed::Box, vec::Vec};
 | 
			
		||||
use haku::{
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsImage},
 | 
			
		||||
    compiler::{compile_expr, CompileError, Compiler, Diagnostic, Source},
 | 
			
		||||
    render::{Bitmap, Renderer, RendererLimits},
 | 
			
		||||
    sexp::{self, parse_toplevel, Ast, Parser},
 | 
			
		||||
    render::{
 | 
			
		||||
        tiny_skia::{Pixmap, PremultipliedColorU8},
 | 
			
		||||
        Renderer, RendererLimits,
 | 
			
		||||
    },
 | 
			
		||||
    sexp::{parse_toplevel, Ast, Parser},
 | 
			
		||||
    system::{ChunkId, System, SystemImage},
 | 
			
		||||
    value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
 | 
			
		||||
    value::{BytecodeLoc, Closure, FunctionName, Ref},
 | 
			
		||||
    vm::{Exception, Vm, VmImage, VmLimits},
 | 
			
		||||
};
 | 
			
		||||
use log::info;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +45,7 @@ struct Limits {
 | 
			
		|||
    call_stack_capacity: usize,
 | 
			
		||||
    ref_capacity: usize,
 | 
			
		||||
    fuel: usize,
 | 
			
		||||
    bitmap_stack_capacity: usize,
 | 
			
		||||
    pixmap_stack_capacity: usize,
 | 
			
		||||
    transform_stack_capacity: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +60,7 @@ impl Default for Limits {
 | 
			
		|||
            call_stack_capacity: 256,
 | 
			
		||||
            ref_capacity: 2048,
 | 
			
		||||
            fuel: 65536,
 | 
			
		||||
            bitmap_stack_capacity: 4,
 | 
			
		||||
            pixmap_stack_capacity: 4,
 | 
			
		||||
            transform_stack_capacity: 16,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +118,13 @@ unsafe extern "C" fn haku_instance_destroy(instance: *mut Instance) {
 | 
			
		|||
    drop(Box::from_raw(instance));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_reset(instance: *mut Instance) {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    instance.system.restore_image(&instance.system_image);
 | 
			
		||||
    instance.defs.restore_image(&instance.defs_image);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
 | 
			
		||||
    (*instance).exception.is_some()
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +157,19 @@ extern "C" fn haku_is_ok(code: StatusCode) -> bool {
 | 
			
		|||
    code == StatusCode::Ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn haku_is_diagnostics_emitted(code: StatusCode) -> bool {
 | 
			
		||||
    code == StatusCode::DiagnosticsEmitted
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn haku_is_exception(code: StatusCode) -> bool {
 | 
			
		||||
    matches!(
 | 
			
		||||
        code,
 | 
			
		||||
        StatusCode::EvalException | StatusCode::RenderException
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn haku_status_string(code: StatusCode) -> *const i8 {
 | 
			
		||||
    match code {
 | 
			
		||||
| 
						 | 
				
			
			@ -261,37 +284,49 @@ unsafe extern "C" fn haku_compile_brush(
 | 
			
		|||
    StatusCode::Ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct BitmapLock {
 | 
			
		||||
    bitmap: Option<Bitmap>,
 | 
			
		||||
struct PixmapLock {
 | 
			
		||||
    pixmap: Option<Pixmap>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn haku_bitmap_new(width: u32, height: u32) -> *mut BitmapLock {
 | 
			
		||||
    Box::leak(Box::new(BitmapLock {
 | 
			
		||||
        bitmap: Some(Bitmap::new(width, height)),
 | 
			
		||||
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapLock {
 | 
			
		||||
    Box::leak(Box::new(PixmapLock {
 | 
			
		||||
        pixmap: Some(Pixmap::new(width, height).expect("invalid pixmap size")),
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_bitmap_destroy(bitmap: *mut BitmapLock) {
 | 
			
		||||
    drop(Box::from_raw(bitmap))
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut PixmapLock) {
 | 
			
		||||
    drop(Box::from_raw(pixmap))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_bitmap_data(bitmap: *mut BitmapLock) -> *mut u8 {
 | 
			
		||||
    let bitmap = (*bitmap)
 | 
			
		||||
        .bitmap
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 {
 | 
			
		||||
    let pixmap = (*pixmap)
 | 
			
		||||
        .pixmap
 | 
			
		||||
        .as_mut()
 | 
			
		||||
        .expect("bitmap is already being rendered to");
 | 
			
		||||
        .expect("pixmap is already being rendered to");
 | 
			
		||||
 | 
			
		||||
    bitmap.pixels[..].as_mut_ptr() as *mut u8
 | 
			
		||||
    pixmap.pixels_mut().as_mut_ptr() as *mut u8
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) {
 | 
			
		||||
    let pixmap = (*pixmap)
 | 
			
		||||
        .pixmap
 | 
			
		||||
        .as_mut()
 | 
			
		||||
        .expect("pixmap is already being rendered to");
 | 
			
		||||
    pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_render_brush(
 | 
			
		||||
    instance: *mut Instance,
 | 
			
		||||
    brush: *const Brush,
 | 
			
		||||
    bitmap: *mut BitmapLock,
 | 
			
		||||
    pixmap_a: *mut PixmapLock,
 | 
			
		||||
    pixmap_b: *mut PixmapLock,
 | 
			
		||||
    translation_x: f32,
 | 
			
		||||
    translation_y: f32,
 | 
			
		||||
) -> StatusCode {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    let brush = &*brush;
 | 
			
		||||
| 
						 | 
				
			
			@ -320,18 +355,20 @@ unsafe extern "C" fn haku_render_brush(
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let bitmap_locked = (*bitmap)
 | 
			
		||||
        .bitmap
 | 
			
		||||
    let mut render = |pixmap: *mut PixmapLock| {
 | 
			
		||||
        let pixmap_locked = (*pixmap)
 | 
			
		||||
            .pixmap
 | 
			
		||||
            .take()
 | 
			
		||||
        .expect("bitmap is already being rendered to");
 | 
			
		||||
            .expect("pixmap is already being rendered to");
 | 
			
		||||
 | 
			
		||||
        let mut renderer = Renderer::new(
 | 
			
		||||
        bitmap_locked,
 | 
			
		||||
            pixmap_locked,
 | 
			
		||||
            &RendererLimits {
 | 
			
		||||
            bitmap_stack_capacity: instance.limits.bitmap_stack_capacity,
 | 
			
		||||
                pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
 | 
			
		||||
                transform_stack_capacity: instance.limits.transform_stack_capacity,
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
        renderer.translate(translation_x, translation_y);
 | 
			
		||||
        match renderer.render(&instance.vm, scribble) {
 | 
			
		||||
            Ok(()) => (),
 | 
			
		||||
            Err(exn) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -340,9 +377,24 @@ unsafe extern "C" fn haku_render_brush(
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    let bitmap_locked = renderer.finish();
 | 
			
		||||
        let pixmap_locked = renderer.finish();
 | 
			
		||||
 | 
			
		||||
        (*pixmap).pixmap = Some(pixmap_locked);
 | 
			
		||||
 | 
			
		||||
        StatusCode::Ok
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match render(pixmap_a) {
 | 
			
		||||
        StatusCode::Ok => (),
 | 
			
		||||
        other => return other,
 | 
			
		||||
    }
 | 
			
		||||
    if !pixmap_b.is_null() {
 | 
			
		||||
        match render(pixmap_b) {
 | 
			
		||||
            StatusCode::Ok => (),
 | 
			
		||||
            other => return other,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (*bitmap).bitmap = Some(bitmap_locked);
 | 
			
		||||
    instance.vm.restore_image(&instance.vm_image);
 | 
			
		||||
 | 
			
		||||
    StatusCode::Ok
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,3 +4,4 @@ version = "0.1.0"
 | 
			
		|||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
tiny-skia = { version = "0.11.4", default-features = false, features = ["no-std-float"] }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ use core::{
 | 
			
		|||
use alloc::vec::Vec;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    bytecode::{Chunk, DefError, DefId, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
 | 
			
		||||
    bytecode::{Chunk, DefError, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
 | 
			
		||||
    sexp::{Ast, NodeId, NodeKind, Span},
 | 
			
		||||
    system::System,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,66 +1,38 @@
 | 
			
		|||
use core::iter;
 | 
			
		||||
 | 
			
		||||
use alloc::vec::Vec;
 | 
			
		||||
use tiny_skia::{
 | 
			
		||||
    BlendMode, Color, LineCap, Paint, PathBuilder, Pixmap, Shader, Stroke as SStroke, Transform,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    value::{Ref, Rgba, Scribble, Shape, Stroke, Value, Vec4},
 | 
			
		||||
    value::{Ref, Rgba, Scribble, Shape, Stroke, Value},
 | 
			
		||||
    vm::{Exception, Vm},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct Bitmap {
 | 
			
		||||
    pub width: u32,
 | 
			
		||||
    pub height: u32,
 | 
			
		||||
    pub pixels: Vec<Rgba>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Bitmap {
 | 
			
		||||
    pub fn new(width: u32, height: u32) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            width,
 | 
			
		||||
            height,
 | 
			
		||||
            pixels: Vec::from_iter(
 | 
			
		||||
                iter::repeat(Rgba::default()).take(width as usize * height as usize),
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn pixel_index(&self, x: u32, y: u32) -> usize {
 | 
			
		||||
        x as usize + y as usize * self.width as usize
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self, x: u32, y: u32) -> Rgba {
 | 
			
		||||
        self.pixels[self.pixel_index(x, y)]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set(&mut self, x: u32, y: u32, rgba: Rgba) {
 | 
			
		||||
        let index = self.pixel_index(x, y);
 | 
			
		||||
        self.pixels[index] = rgba;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
pub use tiny_skia;
 | 
			
		||||
 | 
			
		||||
pub struct RendererLimits {
 | 
			
		||||
    pub bitmap_stack_capacity: usize,
 | 
			
		||||
    pub pixmap_stack_capacity: usize,
 | 
			
		||||
    pub transform_stack_capacity: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Renderer {
 | 
			
		||||
    bitmap_stack: Vec<Bitmap>,
 | 
			
		||||
    transform_stack: Vec<Vec4>,
 | 
			
		||||
    pixmap_stack: Vec<Pixmap>,
 | 
			
		||||
    transform_stack: Vec<Transform>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Renderer {
 | 
			
		||||
    pub fn new(bitmap: Bitmap, limits: &RendererLimits) -> Self {
 | 
			
		||||
        assert!(limits.bitmap_stack_capacity > 0);
 | 
			
		||||
    pub fn new(pixmap: Pixmap, limits: &RendererLimits) -> Self {
 | 
			
		||||
        assert!(limits.pixmap_stack_capacity > 0);
 | 
			
		||||
        assert!(limits.transform_stack_capacity > 0);
 | 
			
		||||
 | 
			
		||||
        let mut blend_stack = Vec::with_capacity(limits.bitmap_stack_capacity);
 | 
			
		||||
        blend_stack.push(bitmap);
 | 
			
		||||
        let mut blend_stack = Vec::with_capacity(limits.pixmap_stack_capacity);
 | 
			
		||||
        blend_stack.push(pixmap);
 | 
			
		||||
 | 
			
		||||
        let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity);
 | 
			
		||||
        transform_stack.push(Vec4::default());
 | 
			
		||||
        transform_stack.push(Transform::identity());
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            bitmap_stack: blend_stack,
 | 
			
		||||
            pixmap_stack: blend_stack,
 | 
			
		||||
            transform_stack,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -69,44 +41,21 @@ impl Renderer {
 | 
			
		|||
        Exception { message }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn transform(&self) -> &Vec4 {
 | 
			
		||||
        self.transform_stack.last().unwrap()
 | 
			
		||||
    fn transform(&self) -> Transform {
 | 
			
		||||
        self.transform_stack.last().copied().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn transform_mut(&mut self) -> &mut Vec4 {
 | 
			
		||||
    fn transform_mut(&mut self) -> &mut Transform {
 | 
			
		||||
        self.transform_stack.last_mut().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn bitmap(&self) -> &Bitmap {
 | 
			
		||||
        self.bitmap_stack.last().unwrap()
 | 
			
		||||
    pub fn translate(&mut self, x: f32, y: f32) {
 | 
			
		||||
        let translated = self.transform().post_translate(x, y);
 | 
			
		||||
        *self.transform_mut() = translated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn bitmap_mut(&mut self) -> &mut Bitmap {
 | 
			
		||||
        self.bitmap_stack.last_mut().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn translate(&mut self, translation: Vec4) {
 | 
			
		||||
        let transform = self.transform_mut();
 | 
			
		||||
        transform.x += translation.x;
 | 
			
		||||
        transform.y += translation.y;
 | 
			
		||||
        transform.z += translation.z;
 | 
			
		||||
        transform.w += translation.w;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_bitmap_coords(&self, point: Vec4) -> Option<(u32, u32)> {
 | 
			
		||||
        let transform = self.transform();
 | 
			
		||||
        let x = point.x + transform.x;
 | 
			
		||||
        let y = point.y + transform.y;
 | 
			
		||||
        if x >= 0.0 && y >= 0.0 {
 | 
			
		||||
            let (x, y) = (x as u32, y as u32);
 | 
			
		||||
            if x < self.bitmap().width && y < self.bitmap().height {
 | 
			
		||||
                Some((x, y))
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    fn pixmap_mut(&mut self) -> &mut Pixmap {
 | 
			
		||||
        self.pixmap_stack.last_mut().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,19 +75,75 @@ impl Renderer {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    fn render_stroke(&mut self, _vm: &Vm, _value: Value, stroke: &Stroke) -> Result<(), Exception> {
 | 
			
		||||
        let paint = Paint {
 | 
			
		||||
            shader: Shader::SolidColor(tiny_skia_color(stroke.color)),
 | 
			
		||||
            ..default_paint()
 | 
			
		||||
        };
 | 
			
		||||
        let transform = self.transform();
 | 
			
		||||
 | 
			
		||||
        match stroke.shape {
 | 
			
		||||
            Shape::Point(vec) => {
 | 
			
		||||
                if let Some((x, y)) = self.to_bitmap_coords(vec) {
 | 
			
		||||
                    // TODO: thickness
 | 
			
		||||
                    self.bitmap_mut().set(x, y, stroke.color);
 | 
			
		||||
                let mut pb = PathBuilder::new();
 | 
			
		||||
                pb.move_to(vec.x, vec.y);
 | 
			
		||||
                pb.line_to(vec.x, vec.y);
 | 
			
		||||
                let path = pb.finish().unwrap();
 | 
			
		||||
 | 
			
		||||
                self.pixmap_mut().stroke_path(
 | 
			
		||||
                    &path,
 | 
			
		||||
                    &paint,
 | 
			
		||||
                    &SStroke {
 | 
			
		||||
                        width: stroke.thickness,
 | 
			
		||||
                        line_cap: LineCap::Square,
 | 
			
		||||
                        ..Default::default()
 | 
			
		||||
                    },
 | 
			
		||||
                    transform,
 | 
			
		||||
                    None,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Shape::Line(start, end) => {
 | 
			
		||||
                let mut pb = PathBuilder::new();
 | 
			
		||||
                pb.move_to(start.x, start.y);
 | 
			
		||||
                pb.line_to(end.x, end.y);
 | 
			
		||||
                let path = pb.finish().unwrap();
 | 
			
		||||
 | 
			
		||||
                self.pixmap_mut().stroke_path(
 | 
			
		||||
                    &path,
 | 
			
		||||
                    &paint,
 | 
			
		||||
                    &SStroke {
 | 
			
		||||
                        width: stroke.thickness,
 | 
			
		||||
                        line_cap: LineCap::Square,
 | 
			
		||||
                        ..Default::default()
 | 
			
		||||
                    },
 | 
			
		||||
                    transform,
 | 
			
		||||
                    None,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn finish(mut self) -> Bitmap {
 | 
			
		||||
        self.bitmap_stack.drain(..).next().unwrap()
 | 
			
		||||
    pub fn finish(mut self) -> Pixmap {
 | 
			
		||||
        self.pixmap_stack.drain(..).next().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn default_paint() -> Paint<'static> {
 | 
			
		||||
    Paint {
 | 
			
		||||
        shader: Shader::SolidColor(Color::BLACK),
 | 
			
		||||
        blend_mode: BlendMode::SourceOver,
 | 
			
		||||
        anti_alias: false,
 | 
			
		||||
        force_hq_pipeline: false,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn tiny_skia_color(color: Rgba) -> Color {
 | 
			
		||||
    Color::from_rgba(
 | 
			
		||||
        color.r.clamp(0.0, 1.0),
 | 
			
		||||
        color.g.clamp(0.0, 1.0),
 | 
			
		||||
        color.b.clamp(0.0, 1.0),
 | 
			
		||||
        color.a.clamp(0.0, 1.0),
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -203,6 +203,7 @@ impl<'a> Parser<'a> {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn current(&self) -> char {
 | 
			
		||||
        assert_ne!(self.fuel.get(), 0, "parser is stuck");
 | 
			
		||||
        self.fuel.set(self.fuel.get() - 1);
 | 
			
		||||
| 
						 | 
				
			
			@ -228,7 +229,7 @@ pub fn skip_whitespace_and_comments(p: &mut Parser<'_>) {
 | 
			
		|||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            ';' => {
 | 
			
		||||
                while p.current() != '\n' {
 | 
			
		||||
                while p.current() != '\n' && p.current() != '\0' {
 | 
			
		||||
                    p.advance();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -134,7 +134,8 @@ pub mod fns {
 | 
			
		|||
            0x89 ".a" => rgba_a,
 | 
			
		||||
 | 
			
		||||
            0xc0 "to-shape" => to_shape_f,
 | 
			
		||||
            0xc1 "stroke" => stroke,
 | 
			
		||||
            0xc1 "line" => line,
 | 
			
		||||
            0xe0 "stroke" => stroke,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -388,14 +389,16 @@ pub mod fns {
 | 
			
		|||
        Ok(Value::Number(rgba.r))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn to_shape(value: Value, _vm: &Vm) -> Option<Shape> {
 | 
			
		||||
    fn to_shape(value: Value, vm: &Vm) -> Option<Shape> {
 | 
			
		||||
        match value {
 | 
			
		||||
            Value::Nil
 | 
			
		||||
            | Value::False
 | 
			
		||||
            | Value::True
 | 
			
		||||
            | Value::Number(_)
 | 
			
		||||
            | Value::Rgba(_)
 | 
			
		||||
            | Value::Ref(_) => None,
 | 
			
		||||
            Value::Nil | Value::False | Value::True | Value::Number(_) | Value::Rgba(_) => None,
 | 
			
		||||
            Value::Ref(id) => {
 | 
			
		||||
                if let Ref::Shape(shape) = vm.get_ref(id) {
 | 
			
		||||
                    Some(shape.clone())
 | 
			
		||||
                } else {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Value::Vec4(vec) => Some(Shape::Point(vec)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -413,6 +416,19 @@ pub mod fns {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn line(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
 | 
			
		||||
        if args.num() != 2 {
 | 
			
		||||
            return Err(vm.create_exception("(line) expects 2 arguments (line start end)"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        static ERROR: &str = "arguments to (line) must be (vec)";
 | 
			
		||||
        let start = args.get_vec4(vm, 0, ERROR)?;
 | 
			
		||||
        let end = args.get_vec4(vm, 1, ERROR)?;
 | 
			
		||||
 | 
			
		||||
        let id = vm.create_ref(Ref::Shape(Shape::Line(start, end)))?;
 | 
			
		||||
        Ok(Value::Ref(id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stroke(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
 | 
			
		||||
        if args.num() != 3 {
 | 
			
		||||
            return Err(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -146,6 +146,7 @@ pub struct Closure {
 | 
			
		|||
#[derive(Debug, Clone)]
 | 
			
		||||
pub enum Shape {
 | 
			
		||||
    Point(Vec4),
 | 
			
		||||
    Line(Vec4, Vec4),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								crates/rkgk/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								crates/rkgk/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "rkgk"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
axum = { version = "0.7.5", features = ["macros", "ws"] }
 | 
			
		||||
base64 = "0.22.1"
 | 
			
		||||
chrono = "0.4.38"
 | 
			
		||||
color-eyre = "0.6.3"
 | 
			
		||||
copy_dir = "0.1.3"
 | 
			
		||||
dashmap = "6.0.1"
 | 
			
		||||
derive_more = { version = "1.0.0", features = ["try_from"] }
 | 
			
		||||
eyre = "0.6.12"
 | 
			
		||||
haku.workspace = true
 | 
			
		||||
rand = "0.8.5"
 | 
			
		||||
rand_chacha = "0.3.1"
 | 
			
		||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
 | 
			
		||||
serde = { version = "1.0.206", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.124"
 | 
			
		||||
tokio = { version = "1.39.2", features = ["full"] }
 | 
			
		||||
toml = "0.8.19"
 | 
			
		||||
tower-http = { version = "0.5.2", features = ["fs"] }
 | 
			
		||||
tracing = "0.1.40"
 | 
			
		||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
 | 
			
		||||
							
								
								
									
										62
									
								
								crates/rkgk/src/api.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								crates/rkgk/src/api.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    extract::State,
 | 
			
		||||
    http::StatusCode,
 | 
			
		||||
    response::IntoResponse,
 | 
			
		||||
    routing::{get, post},
 | 
			
		||||
    Json, Router,
 | 
			
		||||
};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::Databases;
 | 
			
		||||
 | 
			
		||||
mod wall;
 | 
			
		||||
 | 
			
		||||
pub fn router<S>(dbs: Arc<Databases>) -> Router<S> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/login", post(login_new))
 | 
			
		||||
        .route("/wall", get(wall::wall))
 | 
			
		||||
        .with_state(dbs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct NewUserParams {
 | 
			
		||||
    nickname: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
#[serde(tag = "status", rename_all = "camelCase")]
 | 
			
		||||
enum NewUserResponse {
 | 
			
		||||
    #[serde(rename_all = "camelCase")]
 | 
			
		||||
    Ok { user_id: String },
 | 
			
		||||
 | 
			
		||||
    #[serde(rename_all = "camelCase")]
 | 
			
		||||
    Error { message: String },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn login_new(dbs: State<Arc<Databases>>, params: Json<NewUserParams>) -> impl IntoResponse {
 | 
			
		||||
    if !(1..=32).contains(¶ms.nickname.len()) {
 | 
			
		||||
        return (
 | 
			
		||||
            StatusCode::BAD_REQUEST,
 | 
			
		||||
            Json(NewUserResponse::Error {
 | 
			
		||||
                message: "nickname must be 1..=32 characters long".into(),
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match dbs.login.new_user(params.0.nickname).await {
 | 
			
		||||
        Ok(user_id) => (
 | 
			
		||||
            StatusCode::OK,
 | 
			
		||||
            Json(NewUserResponse::Ok {
 | 
			
		||||
                user_id: user_id.to_string(),
 | 
			
		||||
            }),
 | 
			
		||||
        ),
 | 
			
		||||
        Err(error) => (
 | 
			
		||||
            StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
            Json(NewUserResponse::Error {
 | 
			
		||||
                message: error.to_string(),
 | 
			
		||||
            }),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								crates/rkgk/src/api/wall.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								crates/rkgk/src/api/wall.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,155 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    extract::{
 | 
			
		||||
        ws::{Message, WebSocket},
 | 
			
		||||
        State, WebSocketUpgrade,
 | 
			
		||||
    },
 | 
			
		||||
    response::Response,
 | 
			
		||||
};
 | 
			
		||||
use eyre::{bail, Context, OptionExt};
 | 
			
		||||
use schema::{Error, LoginRequest, LoginResponse, Online, Version, WallInfo};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tokio::select;
 | 
			
		||||
use tracing::{error, info};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    login::database::LoginStatus,
 | 
			
		||||
    wall::{Event, JoinError, Session},
 | 
			
		||||
    Databases,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod schema;
 | 
			
		||||
 | 
			
		||||
pub async fn wall(State(dbs): State<Arc<Databases>>, ws: WebSocketUpgrade) -> Response {
 | 
			
		||||
    ws.on_upgrade(|ws| websocket(dbs, ws))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn to_message<T>(value: &T) -> Message
 | 
			
		||||
where
 | 
			
		||||
    T: Serialize,
 | 
			
		||||
{
 | 
			
		||||
    Message::Text(serde_json::to_string(value).expect("cannot serialize response to JSON"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn from_message<'de, T>(message: &'de Message) -> eyre::Result<T>
 | 
			
		||||
where
 | 
			
		||||
    T: Deserialize<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match message {
 | 
			
		||||
        Message::Text(json) => {
 | 
			
		||||
            serde_json::from_str(json).context("could not deserialize JSON text message")
 | 
			
		||||
        }
 | 
			
		||||
        _ => bail!("expected a text message"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn recv_expect(ws: &mut WebSocket) -> eyre::Result<Message> {
 | 
			
		||||
    Ok(ws
 | 
			
		||||
        .recv()
 | 
			
		||||
        .await
 | 
			
		||||
        .ok_or_eyre("connection closed unexpectedly")??)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn websocket(dbs: Arc<Databases>, mut ws: WebSocket) {
 | 
			
		||||
    match fallible_websocket(dbs, &mut ws).await {
 | 
			
		||||
        Ok(()) => (),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            _ = ws
 | 
			
		||||
                .send(to_message(&Error {
 | 
			
		||||
                    error: format!("{e:?}"),
 | 
			
		||||
                }))
 | 
			
		||||
                .await
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Result<()> {
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    let version = format!("{}-dev", env!("CARGO_PKG_VERSION"));
 | 
			
		||||
    #[cfg(not(debug_assertions))]
 | 
			
		||||
    let version = format!("{}", env!("CARGO_PKG_VERSION"));
 | 
			
		||||
 | 
			
		||||
    ws.send(to_message(&Version { version })).await?;
 | 
			
		||||
 | 
			
		||||
    let login_request: LoginRequest = from_message(&recv_expect(ws).await?)?;
 | 
			
		||||
    let user_id = *login_request.user_id();
 | 
			
		||||
 | 
			
		||||
    match dbs
 | 
			
		||||
        .login
 | 
			
		||||
        .log_in(user_id)
 | 
			
		||||
        .await
 | 
			
		||||
        .context("error while logging in")?
 | 
			
		||||
    {
 | 
			
		||||
        LoginStatus::ValidUser => (),
 | 
			
		||||
        LoginStatus::UserDoesNotExist => {
 | 
			
		||||
            ws.send(to_message(&LoginResponse::UserDoesNotExist))
 | 
			
		||||
                .await?;
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let wall_id = match login_request {
 | 
			
		||||
        LoginRequest::New { .. } => dbs.wall_broker.generate_id().await,
 | 
			
		||||
        LoginRequest::Join { wall, .. } => wall,
 | 
			
		||||
    };
 | 
			
		||||
    let wall = dbs.wall_broker.open(wall_id);
 | 
			
		||||
 | 
			
		||||
    let mut session_handle = match wall.join(Session::new(user_id)) {
 | 
			
		||||
        Ok(handle) => handle,
 | 
			
		||||
        Err(error) => {
 | 
			
		||||
            ws.send(to_message(&match error {
 | 
			
		||||
                // NOTE: Respond with the same error code, because it doesn't matter to the user -
 | 
			
		||||
                // either way the room is way too contended for them to join.
 | 
			
		||||
                JoinError::TooManyCurrentSessions => LoginResponse::TooManySessions,
 | 
			
		||||
                JoinError::IdsExhausted => LoginResponse::TooManySessions,
 | 
			
		||||
            }))
 | 
			
		||||
            .await?;
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut users_online = vec![];
 | 
			
		||||
    for online in wall.online() {
 | 
			
		||||
        let user_info = match dbs.login.user_info(online.user_id).await {
 | 
			
		||||
            Ok(Some(user_info)) => user_info,
 | 
			
		||||
            Ok(None) | Err(_) => {
 | 
			
		||||
                error!(?online, "could not get info about online user");
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        users_online.push(Online {
 | 
			
		||||
            session_id: online.session_id,
 | 
			
		||||
            nickname: user_info.nickname,
 | 
			
		||||
            cursor: online.cursor,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    let users_online = users_online;
 | 
			
		||||
 | 
			
		||||
    ws.send(to_message(&LoginResponse::LoggedIn {
 | 
			
		||||
        wall: wall_id,
 | 
			
		||||
        wall_info: WallInfo {
 | 
			
		||||
            chunk_size: wall.settings().chunk_size,
 | 
			
		||||
            online: users_online,
 | 
			
		||||
        },
 | 
			
		||||
        session_id: session_handle.session_id,
 | 
			
		||||
    }))
 | 
			
		||||
    .await?;
 | 
			
		||||
 | 
			
		||||
    loop {
 | 
			
		||||
        select! {
 | 
			
		||||
            Some(message) = ws.recv() => {
 | 
			
		||||
                let kind = from_message(&message?)?;
 | 
			
		||||
                wall.event(Event { session_id: session_handle.session_id, kind });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Ok(event) = session_handle.event_receiver.recv() => {
 | 
			
		||||
                ws.send(to_message(&event)).await?;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else => break,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								crates/rkgk/src/api/wall/schema.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								crates/rkgk/src/api/wall/schema.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    login::UserId,
 | 
			
		||||
    schema::Vec2,
 | 
			
		||||
    wall::{self, SessionId, WallId},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct Version {
 | 
			
		||||
    pub version: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct Error {
 | 
			
		||||
    pub error: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
 | 
			
		||||
#[serde(
 | 
			
		||||
    tag = "login",
 | 
			
		||||
    rename_all = "camelCase",
 | 
			
		||||
    rename_all_fields = "camelCase"
 | 
			
		||||
)]
 | 
			
		||||
pub enum LoginRequest {
 | 
			
		||||
    New { user: UserId },
 | 
			
		||||
    Join { user: UserId, wall: WallId },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl LoginRequest {
 | 
			
		||||
    pub fn user_id(&self) -> &UserId {
 | 
			
		||||
        match self {
 | 
			
		||||
            LoginRequest::New { user } => user,
 | 
			
		||||
            LoginRequest::Join { user, .. } => user,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct Online {
 | 
			
		||||
    pub session_id: SessionId,
 | 
			
		||||
    pub nickname: String,
 | 
			
		||||
    pub cursor: Option<Vec2>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct WallInfo {
 | 
			
		||||
    pub chunk_size: u32,
 | 
			
		||||
    pub online: Vec<Online>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
#[serde(
 | 
			
		||||
    tag = "response",
 | 
			
		||||
    rename_all = "camelCase",
 | 
			
		||||
    rename_all_fields = "camelCase"
 | 
			
		||||
)]
 | 
			
		||||
pub enum LoginResponse {
 | 
			
		||||
    LoggedIn {
 | 
			
		||||
        wall: WallId,
 | 
			
		||||
        wall_info: WallInfo,
 | 
			
		||||
        session_id: SessionId,
 | 
			
		||||
    },
 | 
			
		||||
    UserDoesNotExist,
 | 
			
		||||
    TooManySessions,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								crates/rkgk/src/binary.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								crates/rkgk/src/binary.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
use std::{error::Error, fmt};
 | 
			
		||||
 | 
			
		||||
pub struct Reader<'a> {
 | 
			
		||||
    slice: &'a [u8],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Reader<'a> {
 | 
			
		||||
    pub fn new(slice: &'a [u8]) -> Self {
 | 
			
		||||
        Self { slice }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_u8(&mut self) -> Result<u8, OutOfData> {
 | 
			
		||||
        if !self.slice.is_empty() {
 | 
			
		||||
            Ok(self.slice[0])
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(OutOfData)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_u16(&mut self) -> Result<u16, OutOfData> {
 | 
			
		||||
        if self.slice.len() >= 2 {
 | 
			
		||||
            Ok(u16::from_le_bytes([self.slice[0], self.slice[1]]))
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(OutOfData)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_u32(&mut self) -> Result<u32, OutOfData> {
 | 
			
		||||
        if self.slice.len() >= 4 {
 | 
			
		||||
            Ok(u32::from_le_bytes([
 | 
			
		||||
                self.slice[0],
 | 
			
		||||
                self.slice[1],
 | 
			
		||||
                self.slice[2],
 | 
			
		||||
                self.slice[3],
 | 
			
		||||
            ]))
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(OutOfData)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
pub struct OutOfData;
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for OutOfData {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        f.write_str("reader ran out of data")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Error for OutOfData {}
 | 
			
		||||
							
								
								
									
										8
									
								
								crates/rkgk/src/config.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								crates/rkgk/src/config.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::wall;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    pub wall: wall::Settings,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								crates/rkgk/src/id.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								crates/rkgk/src/id.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
use std::fmt;
 | 
			
		||||
 | 
			
		||||
use base64::Engine;
 | 
			
		||||
 | 
			
		||||
pub fn serialize(f: &mut fmt::Formatter<'_>, prefix: &str, bytes: &[u8; 32]) -> fmt::Result {
 | 
			
		||||
    f.write_str(prefix)?;
 | 
			
		||||
    let mut buffer = [b'0'; 43];
 | 
			
		||||
    base64::engine::general_purpose::STANDARD_NO_PAD
 | 
			
		||||
        .encode_slice(bytes, &mut buffer)
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    f.write_str(std::str::from_utf8(&buffer).unwrap())?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct InvalidId;
 | 
			
		||||
 | 
			
		||||
pub fn deserialize(s: &str, prefix: &str) -> Result<[u8; 32], InvalidId> {
 | 
			
		||||
    let mut bytes = [0; 32];
 | 
			
		||||
    let b64 = s.strip_prefix(prefix).ok_or(InvalidId)?;
 | 
			
		||||
    let decoded = base64::engine::general_purpose::STANDARD_NO_PAD
 | 
			
		||||
        .decode_slice(b64, &mut bytes)
 | 
			
		||||
        .map_err(|_| InvalidId)?;
 | 
			
		||||
    if decoded != bytes.len() {
 | 
			
		||||
        return Err(InvalidId);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(bytes)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								crates/rkgk/src/login.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								crates/rkgk/src/login.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    error::Error,
 | 
			
		||||
    fmt::{self},
 | 
			
		||||
    str::FromStr,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use rand::RngCore;
 | 
			
		||||
 | 
			
		||||
pub mod database;
 | 
			
		||||
 | 
			
		||||
pub use database::Database;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{id, serialization::DeserializeFromStr};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
pub struct UserId([u8; 32]);
 | 
			
		||||
 | 
			
		||||
impl UserId {
 | 
			
		||||
    pub fn new(rng: &mut dyn RngCore) -> Self {
 | 
			
		||||
        let mut bytes = [0; 32];
 | 
			
		||||
        rng.fill_bytes(&mut bytes[..]);
 | 
			
		||||
        Self(bytes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for UserId {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        id::serialize(f, "user_", &self.0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromStr for UserId {
 | 
			
		||||
    type Err = InvalidUserId;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Self, Self::Err> {
 | 
			
		||||
        id::deserialize(s, "user_")
 | 
			
		||||
            .map(Self)
 | 
			
		||||
            .map_err(|_| InvalidUserId)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Serialize for UserId {
 | 
			
		||||
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
        S: serde::Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        serializer.serialize_str(&self.to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'de> Deserialize<'de> for UserId {
 | 
			
		||||
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        D: serde::Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        deserializer.deserialize_str(DeserializeFromStr::new("user ID"))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
pub struct InvalidUserId;
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for InvalidUserId {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        f.write_str("invalid user ID")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Error for InvalidUserId {}
 | 
			
		||||
							
								
								
									
										166
									
								
								crates/rkgk/src/login/database.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								crates/rkgk/src/login/database.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,166 @@
 | 
			
		|||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use chrono::Utc;
 | 
			
		||||
use eyre::{eyre, Context};
 | 
			
		||||
use rand::SeedableRng;
 | 
			
		||||
use rusqlite::{Connection, OptionalExtension};
 | 
			
		||||
use tokio::sync::{mpsc, oneshot};
 | 
			
		||||
use tracing::instrument;
 | 
			
		||||
 | 
			
		||||
use super::UserId;
 | 
			
		||||
 | 
			
		||||
pub struct Settings {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct Database {
 | 
			
		||||
    command_tx: mpsc::Sender<Command>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum LoginStatus {
 | 
			
		||||
    ValidUser,
 | 
			
		||||
    UserDoesNotExist,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct UserInfo {
 | 
			
		||||
    pub nickname: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Command {
 | 
			
		||||
    NewUser {
 | 
			
		||||
        nickname: String,
 | 
			
		||||
        reply: oneshot::Sender<eyre::Result<UserId>>,
 | 
			
		||||
    },
 | 
			
		||||
    LogIn {
 | 
			
		||||
        user_id: UserId,
 | 
			
		||||
        reply: oneshot::Sender<LoginStatus>,
 | 
			
		||||
    },
 | 
			
		||||
    UserInfo {
 | 
			
		||||
        user_id: UserId,
 | 
			
		||||
        reply: oneshot::Sender<eyre::Result<Option<UserInfo>>>,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Database {
 | 
			
		||||
    pub async fn new_user(&self, nickname: String) -> eyre::Result<UserId> {
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        self.command_tx
 | 
			
		||||
            .send(Command::NewUser {
 | 
			
		||||
                nickname,
 | 
			
		||||
                reply: tx,
 | 
			
		||||
            })
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(|_| eyre!("database is too contended"))?;
 | 
			
		||||
        rx.await.map_err(|_| eyre!("database is not available"))?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn log_in(&self, user_id: UserId) -> eyre::Result<LoginStatus> {
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        self.command_tx
 | 
			
		||||
            .send(Command::LogIn { user_id, reply: tx })
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(|_| eyre!("database is too contended"))?;
 | 
			
		||||
        rx.await.map_err(|_| eyre!("database is not available"))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn user_info(&self, user_id: UserId) -> eyre::Result<Option<UserInfo>> {
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        self.command_tx
 | 
			
		||||
            .send(Command::UserInfo { user_id, reply: tx })
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(|_| eyre!("database is too contended"))?;
 | 
			
		||||
        rx.await.map_err(|_| eyre!("database is not available"))?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[instrument(name = "login::database::start", skip(settings))]
 | 
			
		||||
pub fn start(settings: &Settings) -> eyre::Result<Database> {
 | 
			
		||||
    let db = Connection::open(&settings.path).context("cannot open login database")?;
 | 
			
		||||
 | 
			
		||||
    db.execute(
 | 
			
		||||
        r#"
 | 
			
		||||
            CREATE TABLE IF NOT EXISTS
 | 
			
		||||
            t_users (
 | 
			
		||||
                user_index           INTEGER PRIMARY KEY,
 | 
			
		||||
                long_user_id         BLOB NOT NULL,
 | 
			
		||||
                nickname             TEXT NOT NULL,
 | 
			
		||||
                last_login_timestamp INTEGER NOT NULL
 | 
			
		||||
            );
 | 
			
		||||
        "#,
 | 
			
		||||
        (),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    let (command_tx, mut command_rx) = mpsc::channel(8);
 | 
			
		||||
 | 
			
		||||
    let mut user_id_rng = rand_chacha::ChaCha20Rng::from_entropy();
 | 
			
		||||
 | 
			
		||||
    tokio::task::spawn_blocking(move || {
 | 
			
		||||
        let mut s_insert_user = db
 | 
			
		||||
            .prepare(
 | 
			
		||||
                r#"
 | 
			
		||||
                    INSERT INTO t_users
 | 
			
		||||
                    (long_user_id, nickname, last_login_timestamp)
 | 
			
		||||
                    VALUES (?, ?, ?);
 | 
			
		||||
                "#,
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let mut s_log_in = db
 | 
			
		||||
            .prepare(
 | 
			
		||||
                r#"
 | 
			
		||||
                    UPDATE OR ABORT t_users
 | 
			
		||||
                    SET last_login_timestamp = ?
 | 
			
		||||
                    WHERE long_user_id = ?;
 | 
			
		||||
                "#,
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let mut s_user_info = db
 | 
			
		||||
            .prepare(
 | 
			
		||||
                r#"
 | 
			
		||||
                    SELECT nickname
 | 
			
		||||
                    FROM t_users
 | 
			
		||||
                    WHERE long_user_id = ?
 | 
			
		||||
                    LIMIT 1;
 | 
			
		||||
                "#,
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        while let Some(command) = command_rx.blocking_recv() {
 | 
			
		||||
            match command {
 | 
			
		||||
                Command::NewUser { nickname, reply } => {
 | 
			
		||||
                    let user_id = UserId::new(&mut user_id_rng);
 | 
			
		||||
                    let result = s_insert_user
 | 
			
		||||
                        .execute((user_id.0, nickname, Utc::now().timestamp()))
 | 
			
		||||
                        .context("could not execute query");
 | 
			
		||||
                    _ = reply.send(result.map(|_| user_id));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Command::LogIn { user_id, reply } => {
 | 
			
		||||
                    // TODO: User expiration.
 | 
			
		||||
                    let login_status = match s_log_in.execute((Utc::now().timestamp(), user_id.0)) {
 | 
			
		||||
                        Ok(_) => LoginStatus::ValidUser,
 | 
			
		||||
                        Err(_) => LoginStatus::UserDoesNotExist,
 | 
			
		||||
                    };
 | 
			
		||||
                    _ = reply.send(login_status);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Command::UserInfo { user_id, reply } => {
 | 
			
		||||
                    let result = s_user_info
 | 
			
		||||
                        .query_row((user_id.0,), |row| {
 | 
			
		||||
                            Ok(UserInfo {
 | 
			
		||||
                                nickname: row.get(0)?,
 | 
			
		||||
                            })
 | 
			
		||||
                        })
 | 
			
		||||
                        .optional()
 | 
			
		||||
                        .context("could not execute query");
 | 
			
		||||
                    _ = reply.send(result);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    Ok(Database { command_tx })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +1,32 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    fs::{copy, create_dir_all, remove_dir_all},
 | 
			
		||||
    path::Path,
 | 
			
		||||
    sync::Arc,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use axum::Router;
 | 
			
		||||
use config::Config;
 | 
			
		||||
use copy_dir::copy_dir;
 | 
			
		||||
use eyre::Context;
 | 
			
		||||
use tokio::net::TcpListener;
 | 
			
		||||
use tokio::{fs, net::TcpListener};
 | 
			
		||||
use tower_http::services::{ServeDir, ServeFile};
 | 
			
		||||
use tracing::{info, info_span};
 | 
			
		||||
use tracing_subscriber::fmt::format::FmtSpan;
 | 
			
		||||
 | 
			
		||||
mod api;
 | 
			
		||||
mod binary;
 | 
			
		||||
mod config;
 | 
			
		||||
mod id;
 | 
			
		||||
#[cfg(debug_assertions)]
 | 
			
		||||
mod live_reload;
 | 
			
		||||
mod login;
 | 
			
		||||
pub mod schema;
 | 
			
		||||
mod serialization;
 | 
			
		||||
mod wall;
 | 
			
		||||
 | 
			
		||||
struct Paths<'a> {
 | 
			
		||||
    target_dir: &'a Path,
 | 
			
		||||
    database_dir: &'a Path,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn build(paths: &Paths<'_>) -> eyre::Result<()> {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,28 +47,47 @@ fn build(paths: &Paths<'_>) -> eyre::Result<()> {
 | 
			
		|||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    color_eyre::install().unwrap();
 | 
			
		||||
    tracing_subscriber::fmt()
 | 
			
		||||
        .with_span_events(FmtSpan::ACTIVE)
 | 
			
		||||
        .init();
 | 
			
		||||
pub struct Databases {
 | 
			
		||||
    pub login: login::Database,
 | 
			
		||||
    pub wall_broker: wall::Broker,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn database(config: &Config, paths: &Paths<'_>) -> eyre::Result<Databases> {
 | 
			
		||||
    create_dir_all(paths.database_dir).context("cannot create directory for databases")?;
 | 
			
		||||
 | 
			
		||||
    let login = login::database::start(&login::database::Settings {
 | 
			
		||||
        path: paths.database_dir.join("login.db"),
 | 
			
		||||
    })
 | 
			
		||||
    .context("cannot start up login database")?;
 | 
			
		||||
 | 
			
		||||
    let wall_broker = wall::Broker::new(config.wall);
 | 
			
		||||
 | 
			
		||||
    Ok(Databases { login, wall_broker })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn fallible_main() -> eyre::Result<()> {
 | 
			
		||||
    let paths = Paths {
 | 
			
		||||
        target_dir: Path::new("target/site"),
 | 
			
		||||
        database_dir: Path::new("database"),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match build(&paths) {
 | 
			
		||||
        Ok(()) => (),
 | 
			
		||||
        Err(error) => eprintln!("{error:?}"),
 | 
			
		||||
    }
 | 
			
		||||
    let config: Config = toml::from_str(
 | 
			
		||||
        &fs::read_to_string("rkgk.toml")
 | 
			
		||||
            .await
 | 
			
		||||
            .context("cannot read config file")?,
 | 
			
		||||
    )
 | 
			
		||||
    .context("cannot deserialize config file")?;
 | 
			
		||||
 | 
			
		||||
    build(&paths)?;
 | 
			
		||||
    let dbs = Arc::new(database(&config, &paths)?);
 | 
			
		||||
 | 
			
		||||
    let app = Router::new()
 | 
			
		||||
        .route_service(
 | 
			
		||||
            "/",
 | 
			
		||||
            ServeFile::new(paths.target_dir.join("static/index.html")),
 | 
			
		||||
        )
 | 
			
		||||
        .nest_service("/static", ServeDir::new(paths.target_dir.join("static")));
 | 
			
		||||
        .nest_service("/static", ServeDir::new(paths.target_dir.join("static")))
 | 
			
		||||
        .nest("/api", api::router(dbs.clone()));
 | 
			
		||||
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    let app = app.nest("/dev/live-reload", live_reload::router());
 | 
			
		||||
| 
						 | 
				
			
			@ -67,4 +97,19 @@ async fn main() {
 | 
			
		|||
        .expect("cannot bind to port");
 | 
			
		||||
    info!("listening on port 8080");
 | 
			
		||||
    axum::serve(listener, app).await.expect("cannot serve app");
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    color_eyre::install().unwrap();
 | 
			
		||||
    tracing_subscriber::fmt()
 | 
			
		||||
        .with_span_events(FmtSpan::ACTIVE)
 | 
			
		||||
        .init();
 | 
			
		||||
 | 
			
		||||
    match fallible_main().await {
 | 
			
		||||
        Ok(_) => (),
 | 
			
		||||
        Err(error) => println!("{error:?}"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								crates/rkgk/src/schema.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								crates/rkgk/src/schema.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
 | 
			
		||||
pub struct Vec2 {
 | 
			
		||||
    pub x: f32,
 | 
			
		||||
    pub y: f32,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								crates/rkgk/src/serialization.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								crates/rkgk/src/serialization.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
use std::{fmt::Display, marker::PhantomData, str::FromStr};
 | 
			
		||||
 | 
			
		||||
use serde::de::{Error, Visitor};
 | 
			
		||||
 | 
			
		||||
pub struct DeserializeFromStr<T> {
 | 
			
		||||
    expecting: &'static str,
 | 
			
		||||
    _phantom: PhantomData<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> DeserializeFromStr<T> {
 | 
			
		||||
    pub fn new(expecting: &'static str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            expecting,
 | 
			
		||||
            _phantom: PhantomData,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'de, T> Visitor<'de> for DeserializeFromStr<T>
 | 
			
		||||
where
 | 
			
		||||
    T: FromStr,
 | 
			
		||||
    T::Err: Display,
 | 
			
		||||
{
 | 
			
		||||
    type Value = T;
 | 
			
		||||
 | 
			
		||||
    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
 | 
			
		||||
        formatter.write_str(self.expecting)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
 | 
			
		||||
    where
 | 
			
		||||
        E: serde::de::Error,
 | 
			
		||||
    {
 | 
			
		||||
        T::from_str(v).map_err(|e| Error::custom(e))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										246
									
								
								crates/rkgk/src/wall.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								crates/rkgk/src/wall.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,246 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    error::Error,
 | 
			
		||||
    fmt,
 | 
			
		||||
    str::FromStr,
 | 
			
		||||
    sync::{
 | 
			
		||||
        atomic::{self, AtomicU32},
 | 
			
		||||
        Arc, Weak,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use dashmap::DashMap;
 | 
			
		||||
use haku::render::tiny_skia::Pixmap;
 | 
			
		||||
use rand::RngCore;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tokio::sync::{broadcast, Mutex};
 | 
			
		||||
 | 
			
		||||
use crate::{id, login::UserId, schema::Vec2, serialization::DeserializeFromStr};
 | 
			
		||||
 | 
			
		||||
pub mod broker;
 | 
			
		||||
 | 
			
		||||
pub use broker::Broker;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 | 
			
		||||
pub struct WallId([u8; 32]);
 | 
			
		||||
 | 
			
		||||
impl WallId {
 | 
			
		||||
    pub fn new(rng: &mut dyn RngCore) -> Self {
 | 
			
		||||
        let mut bytes = [0; 32];
 | 
			
		||||
        rng.fill_bytes(&mut bytes);
 | 
			
		||||
        Self(bytes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for WallId {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        id::serialize(f, "wall_", &self.0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromStr for WallId {
 | 
			
		||||
    type Err = InvalidWallId;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Self, Self::Err> {
 | 
			
		||||
        id::deserialize(s, "wall_")
 | 
			
		||||
            .map(WallId)
 | 
			
		||||
            .map_err(|_| InvalidWallId)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Serialize for WallId {
 | 
			
		||||
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
        S: serde::Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        serializer.serialize_str(&self.to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'de> Deserialize<'de> for WallId {
 | 
			
		||||
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        D: serde::Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        deserializer.deserialize_str(DeserializeFromStr::new("wall ID"))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
 | 
			
		||||
pub struct SessionId(u32);
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
pub struct InvalidWallId;
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for InvalidWallId {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        f.write_str("invalid wall ID")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Error for InvalidWallId {}
 | 
			
		||||
 | 
			
		||||
pub struct Chunk {
 | 
			
		||||
    pixmap: Pixmap,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Chunk {
 | 
			
		||||
    pub fn new(size: u32) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            pixmap: Pixmap::new(size, size).unwrap(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
 | 
			
		||||
pub struct Settings {
 | 
			
		||||
    pub max_chunks: usize,
 | 
			
		||||
    pub max_sessions: usize,
 | 
			
		||||
    pub chunk_size: u32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Wall {
 | 
			
		||||
    settings: Settings,
 | 
			
		||||
 | 
			
		||||
    chunks: DashMap<(i32, i32), Arc<Mutex<Chunk>>>,
 | 
			
		||||
 | 
			
		||||
    sessions: DashMap<SessionId, Session>,
 | 
			
		||||
    session_id_counter: AtomicU32,
 | 
			
		||||
 | 
			
		||||
    event_sender: broadcast::Sender<Event>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Session {
 | 
			
		||||
    pub user_id: UserId,
 | 
			
		||||
    pub cursor: Option<Vec2>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct SessionHandle {
 | 
			
		||||
    pub wall: Weak<Wall>,
 | 
			
		||||
    pub event_receiver: broadcast::Receiver<Event>,
 | 
			
		||||
    pub session_id: SessionId,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct Event {
 | 
			
		||||
    pub session_id: SessionId,
 | 
			
		||||
    pub kind: EventKind,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize)]
 | 
			
		||||
#[serde(
 | 
			
		||||
    tag = "event",
 | 
			
		||||
    rename_all = "camelCase",
 | 
			
		||||
    rename_all_fields = "camelCase"
 | 
			
		||||
)]
 | 
			
		||||
pub enum EventKind {
 | 
			
		||||
    Cursor { position: Vec2 },
 | 
			
		||||
 | 
			
		||||
    SetBrush { brush: String },
 | 
			
		||||
    Plot { points: Vec<Vec2> },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct Online {
 | 
			
		||||
    pub session_id: SessionId,
 | 
			
		||||
    pub user_id: UserId,
 | 
			
		||||
    pub cursor: Option<Vec2>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Wall {
 | 
			
		||||
    pub fn new(settings: Settings) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            settings,
 | 
			
		||||
            chunks: DashMap::new(),
 | 
			
		||||
            sessions: DashMap::new(),
 | 
			
		||||
            session_id_counter: AtomicU32::new(0),
 | 
			
		||||
            event_sender: broadcast::channel(16).0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn settings(&self) -> &Settings {
 | 
			
		||||
        &self.settings
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_chunk(&self, at: (i32, i32)) -> Option<Arc<Mutex<Chunk>>> {
 | 
			
		||||
        self.chunks.get(&at).map(|chunk| Arc::clone(&chunk))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_or_create_chunk(&self, at: (i32, i32)) -> Arc<Mutex<Chunk>> {
 | 
			
		||||
        Arc::clone(
 | 
			
		||||
            &self
 | 
			
		||||
                .chunks
 | 
			
		||||
                .entry(at)
 | 
			
		||||
                .or_insert_with(|| Arc::new(Mutex::new(Chunk::new(self.settings.chunk_size)))),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn join(self: &Arc<Self>, session: Session) -> Result<SessionHandle, JoinError> {
 | 
			
		||||
        let session_id = SessionId(
 | 
			
		||||
            self.session_id_counter
 | 
			
		||||
                .fetch_add(1, atomic::Ordering::Relaxed),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        self.sessions.insert(session_id, session);
 | 
			
		||||
 | 
			
		||||
        Ok(SessionHandle {
 | 
			
		||||
            wall: Arc::downgrade(self),
 | 
			
		||||
            event_receiver: self.event_sender.subscribe(),
 | 
			
		||||
            session_id,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn online(&self) -> Vec<Online> {
 | 
			
		||||
        self.sessions
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|r| Online {
 | 
			
		||||
                session_id: *r.key(),
 | 
			
		||||
                user_id: r.user_id,
 | 
			
		||||
                cursor: r.value().cursor,
 | 
			
		||||
            })
 | 
			
		||||
            .collect()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn event(&self, event: Event) {
 | 
			
		||||
        if let Some(mut session) = self.sessions.get_mut(&event.session_id) {
 | 
			
		||||
            match &event.kind {
 | 
			
		||||
                EventKind::SetBrush { brush } => {}
 | 
			
		||||
 | 
			
		||||
                EventKind::Cursor { position } => {
 | 
			
		||||
                    session.cursor = Some(*position);
 | 
			
		||||
                }
 | 
			
		||||
                EventKind::Plot { points } => {}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = self.event_sender.send(event);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Session {
 | 
			
		||||
    pub fn new(user_id: UserId) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            user_id,
 | 
			
		||||
            cursor: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for SessionHandle {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        if let Some(wall) = self.wall.upgrade() {
 | 
			
		||||
            wall.sessions.remove(&self.session_id);
 | 
			
		||||
            // After the session is removed, the wall will be garbage collected later.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum JoinError {
 | 
			
		||||
    TooManyCurrentSessions,
 | 
			
		||||
    IdsExhausted,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum EventError {
 | 
			
		||||
    DeadSession,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								crates/rkgk/src/wall/broker.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								crates/rkgk/src/wall/broker.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use dashmap::DashMap;
 | 
			
		||||
use rand::SeedableRng;
 | 
			
		||||
use rand_chacha::ChaCha20Rng;
 | 
			
		||||
use tokio::sync::Mutex;
 | 
			
		||||
use tracing::info;
 | 
			
		||||
 | 
			
		||||
use super::{Settings, Wall, WallId};
 | 
			
		||||
 | 
			
		||||
/// The broker is the main way to access wall data.
 | 
			
		||||
///
 | 
			
		||||
/// It handles dynamically loading and unloading walls as they're needed.
 | 
			
		||||
/// It also handles database threads for each wall.
 | 
			
		||||
pub struct Broker {
 | 
			
		||||
    wall_settings: Settings,
 | 
			
		||||
    open_walls: DashMap<WallId, OpenWall>,
 | 
			
		||||
    rng: Mutex<ChaCha20Rng>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct OpenWall {
 | 
			
		||||
    wall: Arc<Wall>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Broker {
 | 
			
		||||
    pub fn new(wall_settings: Settings) -> Self {
 | 
			
		||||
        info!(?wall_settings, "Broker::new");
 | 
			
		||||
        Self {
 | 
			
		||||
            wall_settings,
 | 
			
		||||
            open_walls: DashMap::new(),
 | 
			
		||||
            rng: Mutex::new(ChaCha20Rng::from_entropy()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn generate_id(&self) -> WallId {
 | 
			
		||||
        // TODO: Will lock contention be an issue with generating wall IDs?
 | 
			
		||||
        // We only have one of these RNGs per rkgk instance.
 | 
			
		||||
        let mut rng = self.rng.lock().await;
 | 
			
		||||
        WallId::new(&mut *rng)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn open(&self, wall_id: WallId) -> Arc<Wall> {
 | 
			
		||||
        Arc::clone(
 | 
			
		||||
            &self
 | 
			
		||||
                .open_walls
 | 
			
		||||
                .entry(wall_id)
 | 
			
		||||
                .or_insert_with(|| OpenWall {
 | 
			
		||||
                    wall: Arc::new(Wall::new(self.wall_settings)),
 | 
			
		||||
                })
 | 
			
		||||
                .wall,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								rkgk.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								rkgk.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
[wall]
 | 
			
		||||
max_chunks = 65536
 | 
			
		||||
max_sessions = 128
 | 
			
		||||
chunk_size = 168
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								static/brush-editor.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								static/brush-editor.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
const defaultBrush = `
 | 
			
		||||
; This is your brush.
 | 
			
		||||
; Feel free to edit it to your liking!
 | 
			
		||||
(stroke
 | 
			
		||||
    8                       ; thickness
 | 
			
		||||
    (rgba 0.0 0.0 0.0 1.0)  ; color
 | 
			
		||||
    (vec))                  ; position
 | 
			
		||||
`.trim();
 | 
			
		||||
 | 
			
		||||
export class BrushEditor extends HTMLElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.classList.add("rkgk-panel");
 | 
			
		||||
 | 
			
		||||
        this.textArea = this.appendChild(document.createElement("pre"));
 | 
			
		||||
        this.textArea.classList.add("text-area");
 | 
			
		||||
        this.textArea.textContent = defaultBrush;
 | 
			
		||||
        this.textArea.contentEditable = true;
 | 
			
		||||
        this.textArea.spellcheck = false;
 | 
			
		||||
        this.textArea.addEventListener("input", () => {
 | 
			
		||||
            this.dispatchEvent(
 | 
			
		||||
                Object.assign(new Event(".codeChanged"), {
 | 
			
		||||
                    newCode: this.textArea.value,
 | 
			
		||||
                }),
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get code() {
 | 
			
		||||
        return this.textArea.textContent;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-brush-editor", BrushEditor);
 | 
			
		||||
							
								
								
									
										154
									
								
								static/canvas-renderer.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								static/canvas-renderer.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,154 @@
 | 
			
		|||
import { listen } from "./framework.js";
 | 
			
		||||
import { Viewport } from "./viewport.js";
 | 
			
		||||
 | 
			
		||||
class CanvasRenderer extends HTMLElement {
 | 
			
		||||
    viewport = new Viewport();
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.canvas = this.appendChild(document.createElement("canvas"));
 | 
			
		||||
        this.ctx = this.canvas.getContext("2d");
 | 
			
		||||
 | 
			
		||||
        let resizeObserver = new ResizeObserver(() => this.#updateSize());
 | 
			
		||||
        resizeObserver.observe(this);
 | 
			
		||||
 | 
			
		||||
        this.#cursorReportingBehaviour();
 | 
			
		||||
        this.#draggingBehaviour();
 | 
			
		||||
        this.#zoomingBehaviour();
 | 
			
		||||
        this.#paintingBehaviour();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize(wall, painter) {
 | 
			
		||||
        this.wall = wall;
 | 
			
		||||
        this.painter = painter;
 | 
			
		||||
        requestAnimationFrame(() => this.#render());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #updateSize() {
 | 
			
		||||
        this.canvas.width = this.clientWidth;
 | 
			
		||||
        this.canvas.height = this.clientHeight;
 | 
			
		||||
        // Rerender immediately after the canvas is resized, as its contents have now been invalidated.
 | 
			
		||||
        this.#render();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #render() {
 | 
			
		||||
        // NOTE: We should probably render on-demand only when it's needed.
 | 
			
		||||
        requestAnimationFrame(() => this.#render());
 | 
			
		||||
 | 
			
		||||
        this.#renderWall();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #renderWall() {
 | 
			
		||||
        if (this.wall == null) {
 | 
			
		||||
            console.debug("wall is not available, skipping rendering");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ctx.fillStyle = "white";
 | 
			
		||||
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
 | 
			
		||||
 | 
			
		||||
        this.ctx.save();
 | 
			
		||||
        this.ctx.translate(Math.floor(this.clientWidth / 2), Math.floor(this.clientHeight / 2));
 | 
			
		||||
        this.ctx.scale(this.viewport.zoom, this.viewport.zoom);
 | 
			
		||||
        this.ctx.translate(-this.viewport.panX, -this.viewport.panY);
 | 
			
		||||
 | 
			
		||||
        let visibleRect = this.viewport.getVisibleRect({
 | 
			
		||||
            width: this.clientWidth,
 | 
			
		||||
            height: this.clientHeight,
 | 
			
		||||
        });
 | 
			
		||||
        let left = Math.floor(visibleRect.x / this.wall.chunkSize);
 | 
			
		||||
        let top = Math.floor(visibleRect.y / this.wall.chunkSize);
 | 
			
		||||
        let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
 | 
			
		||||
        let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
 | 
			
		||||
        for (let chunkY = top; chunkY < bottom; ++chunkY) {
 | 
			
		||||
            for (let chunkX = left; chunkX < right; ++chunkX) {
 | 
			
		||||
                let x = chunkX * this.wall.chunkSize;
 | 
			
		||||
                let y = chunkY * this.wall.chunkSize;
 | 
			
		||||
 | 
			
		||||
                let chunk = this.wall.getChunk(chunkX, chunkY);
 | 
			
		||||
                if (chunk != null) {
 | 
			
		||||
                    this.ctx.drawImage(chunk.canvas, x, y);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ctx.restore();
 | 
			
		||||
 | 
			
		||||
        if (this.ctx.brushPreview != null) {
 | 
			
		||||
            this.ctx.drawImage(this.ctx.brushPreview, 0, 0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #cursorReportingBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let event = await listen([this, "mousemove"]);
 | 
			
		||||
            let [x, y] = this.viewport.toViewportSpace(
 | 
			
		||||
                event.clientX - this.clientLeft,
 | 
			
		||||
                event.offsetY - this.clientTop,
 | 
			
		||||
                {
 | 
			
		||||
                    width: this.clientWidth,
 | 
			
		||||
                    height: this.clientHeight,
 | 
			
		||||
                },
 | 
			
		||||
            );
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #draggingBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let mouseDown = await listen([this, "mousedown"]);
 | 
			
		||||
            if (mouseDown.button == 1) {
 | 
			
		||||
                mouseDown.preventDefault();
 | 
			
		||||
                while (true) {
 | 
			
		||||
                    let event = await listen([window, "mousemove"], [window, "mouseup"]);
 | 
			
		||||
                    if (event.type == "mousemove") {
 | 
			
		||||
                        this.viewport.panAround(event.movementX, event.movementY);
 | 
			
		||||
                        this.dispatchEvent(new Event(".viewportUpdate"));
 | 
			
		||||
                    } else if (event.type == "mouseup") {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #zoomingBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let event = await listen([this, "wheel"]);
 | 
			
		||||
 | 
			
		||||
            // TODO: Touchpad zoom
 | 
			
		||||
            this.viewport.zoomIn(event.deltaY > 0 ? -1 : 1);
 | 
			
		||||
            this.dispatchEvent(new Event(".viewportUpdate"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #paintingBehaviour() {
 | 
			
		||||
        const paint = (x, y) => {
 | 
			
		||||
            let [wallX, wallY] = this.viewport.toViewportSpace(x, y, {
 | 
			
		||||
                width: this.clientWidth,
 | 
			
		||||
                height: this.clientHeight,
 | 
			
		||||
            });
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let mouseDown = await listen([this, "mousedown"]);
 | 
			
		||||
            if (mouseDown.button == 0) {
 | 
			
		||||
                paint(mouseDown.offsetX, mouseDown.offsetY);
 | 
			
		||||
                while (true) {
 | 
			
		||||
                    let event = await listen([window, "mousemove"], [window, "mouseup"]);
 | 
			
		||||
                    if (event.type == "mousemove") {
 | 
			
		||||
                        paint(event.clientX - this.clientLeft, event.offsetY - this.clientTop);
 | 
			
		||||
                    } else if (event.type == "mouseup") {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-canvas-renderer", CanvasRenderer);
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraCode-VariableFont_wght.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraCode-VariableFont_wght.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Black.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Black.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-BlackItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-BlackItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Bold.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Bold.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-BoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-BoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraBold.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraBold.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraBoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraBoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraLight.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraLight.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraLightItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-ExtraLightItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Italic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Italic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Light.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Light.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-LightItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-LightItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Medium.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Medium.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-MediumItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-MediumItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-SemiBold.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-SemiBold.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-SemiBoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-SemiBoldItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-Thin.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-Thin.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/font/FiraSans-ThinItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/font/FiraSans-ThinItalic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										30
									
								
								static/framework.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								static/framework.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
export function listen(...listenerSpecs) {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
        let removeAllEventListeners;
 | 
			
		||||
 | 
			
		||||
        let listeners = listenerSpecs.map(([element, eventName]) => {
 | 
			
		||||
            let listener = (event) => {
 | 
			
		||||
                removeAllEventListeners();
 | 
			
		||||
                resolve(event);
 | 
			
		||||
            };
 | 
			
		||||
            element.addEventListener(eventName, listener);
 | 
			
		||||
            return { element, eventName, func: listener };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        removeAllEventListeners = () => {
 | 
			
		||||
            for (let listener of listeners) {
 | 
			
		||||
                listener.element.removeEventListener(listener.eventName, listener.func);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function debounce(time, fn) {
 | 
			
		||||
    let timeout = null;
 | 
			
		||||
    return (...args) => {
 | 
			
		||||
        if (timeout == null) {
 | 
			
		||||
            fn(...args);
 | 
			
		||||
            timeout = setTimeout(() => (timeout = null), time);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										203
									
								
								static/haku.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								static/haku.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,203 @@
 | 
			
		|||
let panicImpl;
 | 
			
		||||
let logImpl;
 | 
			
		||||
 | 
			
		||||
function makeLogFunction(level) {
 | 
			
		||||
    return (length, pMessage) => {
 | 
			
		||||
        logImpl(level, length, pMessage);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming(
 | 
			
		||||
    fetch(import.meta.resolve("./wasm/haku.wasm")),
 | 
			
		||||
    {
 | 
			
		||||
        env: {
 | 
			
		||||
            panic(length, pMessage) {
 | 
			
		||||
                panicImpl(length, pMessage);
 | 
			
		||||
            },
 | 
			
		||||
            trace: makeLogFunction("trace"),
 | 
			
		||||
            debug: makeLogFunction("debug"),
 | 
			
		||||
            info: makeLogFunction("info"),
 | 
			
		||||
            warn: makeLogFunction("warn"),
 | 
			
		||||
            error: makeLogFunction("error"),
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
let memory = hakuInstance.exports.memory;
 | 
			
		||||
let w = hakuInstance.exports;
 | 
			
		||||
 | 
			
		||||
let textEncoder = new TextEncoder();
 | 
			
		||||
function allocString(string) {
 | 
			
		||||
    let size = string.length * 3;
 | 
			
		||||
    let align = 1;
 | 
			
		||||
    let pString = w.haku_alloc(size, align);
 | 
			
		||||
 | 
			
		||||
    let buffer = new Uint8Array(memory.buffer, pString, size);
 | 
			
		||||
    let result = textEncoder.encodeInto(string, buffer);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        ptr: pString,
 | 
			
		||||
        length: result.written,
 | 
			
		||||
        size,
 | 
			
		||||
        align,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function freeString(alloc) {
 | 
			
		||||
    w.haku_free(alloc.ptr, alloc.size, alloc.align);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let textDecoder = new TextDecoder();
 | 
			
		||||
function readString(size, pString) {
 | 
			
		||||
    let buffer = new Uint8Array(memory.buffer, pString, size);
 | 
			
		||||
    return textDecoder.decode(buffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function readCString(pCString) {
 | 
			
		||||
    let memoryBuffer = new Uint8Array(memory.buffer);
 | 
			
		||||
 | 
			
		||||
    let pCursor = pCString;
 | 
			
		||||
    while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) {
 | 
			
		||||
        pCursor++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let size = pCursor - pCString;
 | 
			
		||||
    return readString(size, pCString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Panic extends Error {
 | 
			
		||||
    name = "Panic";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
panicImpl = (length, pMessage) => {
 | 
			
		||||
    throw new Panic(readString(length, pMessage));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
logImpl = (level, length, pMessage) => {
 | 
			
		||||
    console[level](readString(length, pMessage));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
w.haku_init_logging();
 | 
			
		||||
 | 
			
		||||
export class Pixmap {
 | 
			
		||||
    #pPixmap = 0;
 | 
			
		||||
 | 
			
		||||
    constructor(width, height) {
 | 
			
		||||
        this.#pPixmap = w.haku_pixmap_new(width, height);
 | 
			
		||||
        this.width = width;
 | 
			
		||||
        this.height = height;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        w.haku_pixmap_destroy(this.#pPixmap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clear(r, g, b, a) {
 | 
			
		||||
        w.haku_pixmap_clear(this.#pPixmap, r, g, b, a);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get ptr() {
 | 
			
		||||
        return this.#pPixmap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get imageData() {
 | 
			
		||||
        return new ImageData(
 | 
			
		||||
            new Uint8ClampedArray(
 | 
			
		||||
                memory.buffer,
 | 
			
		||||
                w.haku_pixmap_data(this.#pPixmap),
 | 
			
		||||
                this.width * this.height * 4,
 | 
			
		||||
            ),
 | 
			
		||||
            this.width,
 | 
			
		||||
            this.height,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Haku {
 | 
			
		||||
    #pInstance = 0;
 | 
			
		||||
    #pBrush = 0;
 | 
			
		||||
    #brushCode = null;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.#pInstance = w.haku_instance_new();
 | 
			
		||||
        this.#pBrush = w.haku_brush_new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setBrush(code) {
 | 
			
		||||
        w.haku_reset(this.#pInstance);
 | 
			
		||||
        // NOTE: Brush is invalid at this point, because we reset removes all defs and registered chunks.
 | 
			
		||||
 | 
			
		||||
        if (this.#brushCode != null) freeString(this.#brushCode);
 | 
			
		||||
        this.#brushCode = allocString(code);
 | 
			
		||||
 | 
			
		||||
        let statusCode = w.haku_compile_brush(
 | 
			
		||||
            this.#pInstance,
 | 
			
		||||
            this.#pBrush,
 | 
			
		||||
            this.#brushCode.length,
 | 
			
		||||
            this.#brushCode.ptr,
 | 
			
		||||
        );
 | 
			
		||||
        if (!w.haku_is_ok(statusCode)) {
 | 
			
		||||
            if (w.haku_is_diagnostics_emitted(statusCode)) {
 | 
			
		||||
                let diagnostics = [];
 | 
			
		||||
                for (let i = 0; i < w.haku_num_diagnostics(this.#pBrush); ++i) {
 | 
			
		||||
                    diagnostics.push({
 | 
			
		||||
                        start: w.haku_diagnostic_start(this.#pBrush, i),
 | 
			
		||||
                        end: w.haku_diagnostic_end(this.#pBrush, i),
 | 
			
		||||
                        message: readString(
 | 
			
		||||
                            w.haku_diagnostic_message_len(this.#pBrush, i),
 | 
			
		||||
                            w.haku_diagnostic_message(this.#pBrush, i),
 | 
			
		||||
                        ),
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                return {
 | 
			
		||||
                    status: "error",
 | 
			
		||||
                    errorKind: "diagnostics",
 | 
			
		||||
                    diagnostics,
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                return {
 | 
			
		||||
                    status: "error",
 | 
			
		||||
                    errorKind: "plain",
 | 
			
		||||
                    message: readCString(w.haku_status_string(statusCode)),
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderBrush(pixmap, translationX, translationY) {
 | 
			
		||||
        let statusCode = w.haku_render_brush(
 | 
			
		||||
            this.#pInstance,
 | 
			
		||||
            this.#pBrush,
 | 
			
		||||
            pixmap.ptr,
 | 
			
		||||
            // If we ever want to detect which pixels were touched (USING A SHADER.), we can use
 | 
			
		||||
            // this to rasterize the brush _twice_, and then we can detect which pixels are the same
 | 
			
		||||
            // between the two pixmaps.
 | 
			
		||||
            0,
 | 
			
		||||
            translationX,
 | 
			
		||||
            translationY,
 | 
			
		||||
        );
 | 
			
		||||
        if (!w.haku_is_ok(statusCode)) {
 | 
			
		||||
            if (w.haku_is_exception(statusCode)) {
 | 
			
		||||
                return {
 | 
			
		||||
                    status: "error",
 | 
			
		||||
                    errorKind: "exception",
 | 
			
		||||
                    description: readCString(w.haku_status_string(statusCode)),
 | 
			
		||||
                    message: readString(
 | 
			
		||||
                        w.haku_exception_message_len(this.#pInstance),
 | 
			
		||||
                        w.haku_exception_message(this.#pInstance),
 | 
			
		||||
                    ),
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                return {
 | 
			
		||||
                    status: "error",
 | 
			
		||||
                    errorKind: "plain",
 | 
			
		||||
                    message: readCString(w.haku_status_string(statusCode)),
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										232
									
								
								static/index.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								static/index.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,232 @@
 | 
			
		|||
/* Variables */
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --color-text: #111;
 | 
			
		||||
    --color-error: #db344b;
 | 
			
		||||
    
 | 
			
		||||
    --color-panel-border: rgba(0, 0, 0, 20%);
 | 
			
		||||
    --color-panel-background: #fff;
 | 
			
		||||
 | 
			
		||||
    --panel-border-radius: 16px;
 | 
			
		||||
    --panel-box-shadow: 0 0 0 1px var(--color-panel-border);
 | 
			
		||||
    --panel-padding: 12px;
 | 
			
		||||
    --dialog-backdrop: rgba(255, 255, 255, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Reset */ 
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: 100vw;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
 | 
			
		||||
    color: var(--color-text);
 | 
			
		||||
    line-height: 1.4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Fonts */
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: "Fira Sans";
 | 
			
		||||
    src:
 | 
			
		||||
        local("Fira Sans Regular"),
 | 
			
		||||
        url("font/FiraSans-Regular.ttf");
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: "Fira Sans";
 | 
			
		||||
    src:
 | 
			
		||||
        local("Fira Sans Bold"),
 | 
			
		||||
        url("font/FiraSans-Bold.ttf");
 | 
			
		||||
    font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: "Fira Code";
 | 
			
		||||
    src:
 | 
			
		||||
        local("Fira Code"),
 | 
			
		||||
        url("font/FiraCode-VariableFont_wght.ttf");
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    font-size: 87.5%;
 | 
			
		||||
    font-family: "Fira Sans", sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button, textarea, input {
 | 
			
		||||
    font-size: inherit;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Main container layout */
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    &>rkgk-canvas-renderer {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &>rkgk-reticle-renderer {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &>rkgk-brush-editor {
 | 
			
		||||
        width: 384px;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        margin: 16px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Buttons */
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
    border: 1px solid var(--color-panel-border);
 | 
			
		||||
    border-radius: 9999px;
 | 
			
		||||
    padding: 0.5rem 1.5rem;
 | 
			
		||||
    background-color: var(--color-panel-background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Text areas */ 
 | 
			
		||||
 | 
			
		||||
input {
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-bottom: 1px solid var(--color-panel-border);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*[contenteditable]:focus, input:focus {
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    outline: 1px solid #40b1f4;
 | 
			
		||||
    outline-offset: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Modal dialogs */
 | 
			
		||||
 | 
			
		||||
dialog:not([open]) {
 | 
			
		||||
    /* Weird this doesn't seem to work by default. */
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dialog::backdrop {
 | 
			
		||||
    background-color: var(--dialog-backdrop);
 | 
			
		||||
    backdrop-filter: blur(8px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Throbbers */
 | 
			
		||||
 | 
			
		||||
rkgk-throbber {
 | 
			
		||||
    display: inline;
 | 
			
		||||
 | 
			
		||||
    &.loading {
 | 
			
		||||
        &::before {
 | 
			
		||||
            /* This could use an entertaining animation. */
 | 
			
		||||
            content: "Please wait...";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.error {
 | 
			
		||||
        /* This could use an icon. */
 | 
			
		||||
        color: var(--color-error);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Panels */
 | 
			
		||||
 | 
			
		||||
.rkgk-panel {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background: var(--color-panel-background);
 | 
			
		||||
    padding: var(--panel-border-radius);
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 16px;
 | 
			
		||||
    box-shadow: var(--panel-box-shadow);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Canvas renderer */
 | 
			
		||||
 | 
			
		||||
rkgk-canvas-renderer {
 | 
			
		||||
    display: block;
 | 
			
		||||
 | 
			
		||||
    &>canvas {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Reticle renderer */
 | 
			
		||||
 | 
			
		||||
rkgk-reticle-renderer {
 | 
			
		||||
    display: block;
 | 
			
		||||
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    &>.reticles {
 | 
			
		||||
        position: relative;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rkgk-reticle {
 | 
			
		||||
    --color: black;
 | 
			
		||||
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    display: block;
 | 
			
		||||
 | 
			
		||||
    &>.container {
 | 
			
		||||
        &>.arrow {
 | 
			
		||||
            width: 24px;
 | 
			
		||||
            height: 24px;
 | 
			
		||||
            background-color: var(--color);
 | 
			
		||||
            clip-path: path("M 0,0 L 13,13 L 6,13 L 0,19 Z");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &>.nickname {
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 20px;
 | 
			
		||||
            left: 8px;
 | 
			
		||||
 | 
			
		||||
            color: white;
 | 
			
		||||
            background-color: var(--color);
 | 
			
		||||
            padding: 1px 6px;
 | 
			
		||||
            border-radius: 9999px;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Brush editor */
 | 
			
		||||
 | 
			
		||||
rkgk-brush-editor {
 | 
			
		||||
    &>.text-area {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        resize: none;
 | 
			
		||||
        font-family: "Fira Code", monospace;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Welcome screen */
 | 
			
		||||
 | 
			
		||||
rkgk-welcome {
 | 
			
		||||
    &>dialog {
 | 
			
		||||
        h3 {
 | 
			
		||||
            margin: 0.5rem 0;
 | 
			
		||||
            font-size: 2rem;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,17 +2,53 @@
 | 
			
		|||
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <title>canvane</title>
 | 
			
		||||
        <script src="static/index.js" type="module"></script>
 | 
			
		||||
        <title>rakugaki</title>
 | 
			
		||||
 | 
			
		||||
        <link rel="stylesheet" href="static/index.css">
 | 
			
		||||
        
 | 
			
		||||
        <script src="static/live-reload.js" type="module"></script>
 | 
			
		||||
 | 
			
		||||
        <script src="static/brush-editor.js" type="module"></script>
 | 
			
		||||
        <script src="static/canvas-renderer.js" type="module"></script>
 | 
			
		||||
        <script src="static/framework.js" type="module"></script>
 | 
			
		||||
        <script src="static/reticle-renderer.js" type="module"></script>
 | 
			
		||||
        <script src="static/session.js" type="module"></script>
 | 
			
		||||
        <script src="static/throbber.js" type="module"></script>
 | 
			
		||||
        <script src="static/viewport.js" type="module"></script>
 | 
			
		||||
        <script src="static/welcome.js" type="module"></script>        
 | 
			
		||||
        
 | 
			
		||||
        <script src="static/index.js" type="module"></script>
 | 
			
		||||
    </head>
 | 
			
		||||
 | 
			
		||||
    <body>
 | 
			
		||||
        <main>
 | 
			
		||||
            <canvas id="render" width="256" height="256">Please enable JavaScript</canvas>
 | 
			
		||||
            <br>
 | 
			
		||||
            <textarea id="code" cols="80" rows="25">(stroke 1 (rgba 0 0 0 255) (vec 32 32))</textarea>
 | 
			
		||||
            <p id="output" style="white-space: pre-wrap;"></p>
 | 
			
		||||
            <rkgk-canvas-renderer></rkgk-canvas-renderer>
 | 
			
		||||
            <rkgk-reticle-renderer></rkgk-reticle-renderer>
 | 
			
		||||
            <rkgk-brush-editor></rkgk-brush-editor>
 | 
			
		||||
 | 
			
		||||
            <rkgk-welcome>
 | 
			
		||||
                <dialog name="welcome-dialog" class="rkgk-panel">
 | 
			
		||||
                    <form method="dialog">
 | 
			
		||||
                        <h3>
 | 
			
		||||
                            My name is
 | 
			
		||||
                            <input
 | 
			
		||||
                                name="nickname"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                required minlength="1" maxlength="32"
 | 
			
		||||
                                placeholder="... (type here!)"
 | 
			
		||||
                                autocomplete="off"
 | 
			
		||||
                                autofocus></input>
 | 
			
		||||
                        </h3>
 | 
			
		||||
                        <p>This name will be visible to any friends drawing along with you, so choose something recognizable!<br>
 | 
			
		||||
                        Keep in mind you can always change it later.</p>
 | 
			
		||||
 | 
			
		||||
                        <div style="display: flex; flex-direction: row; align-items: center; justify-content: end; gap: 8px;">
 | 
			
		||||
                            <rkgk-throbber name="register-progress"></rkgk-throbber>
 | 
			
		||||
                            <button name="register">Register</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </dialog>
 | 
			
		||||
            </rkgk-welcome>
 | 
			
		||||
        </main>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										200
									
								
								static/index.js
									
										
									
									
									
								
							
							
						
						
									
										200
									
								
								static/index.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,154 +1,74 @@
 | 
			
		|||
let panicImpl;
 | 
			
		||||
let logImpl;
 | 
			
		||||
import { Painter } from "./painter.js";
 | 
			
		||||
import { Wall } from "./wall.js";
 | 
			
		||||
import { Haku } from "./haku.js";
 | 
			
		||||
import { getUserId, newSession, waitForLogin } from "./session.js";
 | 
			
		||||
import { debounce } from "./framework.js";
 | 
			
		||||
 | 
			
		||||
function makeLogFunction(level) {
 | 
			
		||||
    return (length, pMessage) => {
 | 
			
		||||
        logImpl(level, length, pMessage);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
let main = document.querySelector("main");
 | 
			
		||||
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
 | 
			
		||||
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
 | 
			
		||||
let brushEditor = main.querySelector("rkgk-brush-editor");
 | 
			
		||||
 | 
			
		||||
let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming(
 | 
			
		||||
    fetch(import.meta.resolve("./wasm/haku.wasm")),
 | 
			
		||||
    {
 | 
			
		||||
        env: {
 | 
			
		||||
            panic(length, pMessage) {
 | 
			
		||||
                panicImpl(length, pMessage);
 | 
			
		||||
            },
 | 
			
		||||
            trace: makeLogFunction("trace"),
 | 
			
		||||
            debug: makeLogFunction("debug"),
 | 
			
		||||
            info: makeLogFunction("info"),
 | 
			
		||||
            warn: makeLogFunction("warn"),
 | 
			
		||||
            error: makeLogFunction("error"),
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
let haku = new Haku();
 | 
			
		||||
let painter = new Painter(512);
 | 
			
		||||
 | 
			
		||||
let memory = hakuInstance.exports.memory;
 | 
			
		||||
let w = hakuInstance.exports;
 | 
			
		||||
reticleRenderer.connectViewport(canvasRenderer.viewport);
 | 
			
		||||
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.updateTransform());
 | 
			
		||||
 | 
			
		||||
let textEncoder = new TextEncoder();
 | 
			
		||||
function allocString(string) {
 | 
			
		||||
    let size = string.length * 3;
 | 
			
		||||
    let align = 1;
 | 
			
		||||
    let pString = w.haku_alloc(size, align);
 | 
			
		||||
// In the background, connect to the server.
 | 
			
		||||
(async () => {
 | 
			
		||||
    await waitForLogin();
 | 
			
		||||
    console.info("login ready! starting session");
 | 
			
		||||
 | 
			
		||||
    let buffer = new Uint8Array(memory.buffer, pString, size);
 | 
			
		||||
    let result = textEncoder.encodeInto(string, buffer);
 | 
			
		||||
    let session = await newSession(getUserId(), localStorage.getItem("rkgk.mostRecentWallId"));
 | 
			
		||||
    localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        ptr: pString,
 | 
			
		||||
        length: result.written,
 | 
			
		||||
        size,
 | 
			
		||||
        align,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
    let wall = new Wall(session.wallInfo.chunkSize);
 | 
			
		||||
    canvasRenderer.initialize(wall);
 | 
			
		||||
 | 
			
		||||
function freeString(alloc) {
 | 
			
		||||
    w.haku_free(alloc.ptr, alloc.size, alloc.align);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let textDecoder = new TextDecoder();
 | 
			
		||||
function readString(size, pString) {
 | 
			
		||||
    let buffer = new Uint8Array(memory.buffer, pString, size);
 | 
			
		||||
    return textDecoder.decode(buffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function readCString(pCString) {
 | 
			
		||||
    let memoryBuffer = new Uint8Array(memory.buffer);
 | 
			
		||||
 | 
			
		||||
    let pCursor = pCString;
 | 
			
		||||
    while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) {
 | 
			
		||||
        pCursor++;
 | 
			
		||||
    for (let onlineUser of session.wallInfo.online) {
 | 
			
		||||
        wall.onlineUsers.addUser(onlineUser.sessionId, { nickname: onlineUser.nickname });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let size = pCursor - pCString;
 | 
			
		||||
    return readString(size, pCString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Panic extends Error {
 | 
			
		||||
    name = "Panic";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
panicImpl = (length, pMessage) => {
 | 
			
		||||
    throw new Panic(readString(length, pMessage));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
logImpl = (level, length, pMessage) => {
 | 
			
		||||
    console[level](readString(length, pMessage));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
w.haku_init_logging();
 | 
			
		||||
 | 
			
		||||
/* ------ */
 | 
			
		||||
 | 
			
		||||
let renderCanvas = document.getElementById("render");
 | 
			
		||||
let codeTextArea = document.getElementById("code");
 | 
			
		||||
let outputP = document.getElementById("output");
 | 
			
		||||
 | 
			
		||||
let ctx = renderCanvas.getContext("2d");
 | 
			
		||||
 | 
			
		||||
function rerender() {
 | 
			
		||||
    console.log("rerender");
 | 
			
		||||
 | 
			
		||||
    let width = renderCanvas.width;
 | 
			
		||||
    let height = renderCanvas.height;
 | 
			
		||||
 | 
			
		||||
    let logs = [];
 | 
			
		||||
 | 
			
		||||
    let pInstance = w.haku_instance_new();
 | 
			
		||||
    let pBrush = w.haku_brush_new();
 | 
			
		||||
    let pBitmap = w.haku_bitmap_new(width, height);
 | 
			
		||||
    let code = allocString(codeTextArea.value);
 | 
			
		||||
    let deallocEverything = () => {
 | 
			
		||||
        freeString(code);
 | 
			
		||||
        w.haku_bitmap_destroy(pBitmap);
 | 
			
		||||
        w.haku_brush_destroy(pBrush);
 | 
			
		||||
        w.haku_instance_destroy(pInstance);
 | 
			
		||||
        outputP.textContent = logs.join("\n");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let compileStatusCode = w.haku_compile_brush(pInstance, pBrush, code.length, code.ptr);
 | 
			
		||||
    let pCompileStatusString = w.haku_status_string(compileStatusCode);
 | 
			
		||||
    logs.push(`compile: ${readCString(pCompileStatusString)}`);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < w.haku_num_diagnostics(pBrush); ++i) {
 | 
			
		||||
        let start = w.haku_diagnostic_start(pBrush, i);
 | 
			
		||||
        let end = w.haku_diagnostic_end(pBrush, i);
 | 
			
		||||
        let length = w.haku_diagnostic_message_len(pBrush, i);
 | 
			
		||||
        let pMessage = w.haku_diagnostic_message(pBrush, i);
 | 
			
		||||
        let message = readString(length, pMessage);
 | 
			
		||||
        logs.push(`${start}..${end}: ${message}`);
 | 
			
		||||
    session.addEventListener("error", (event) => console.error(event));
 | 
			
		||||
    session.addEventListener("action", (event) => {
 | 
			
		||||
        if (event.kind.event == "cursor") {
 | 
			
		||||
            let reticle = reticleRenderer.getOrAddReticle(wall.onlineUsers, event.sessionId);
 | 
			
		||||
            let { x, y } = event.kind.position;
 | 
			
		||||
            reticle.setCursor(x, y);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (w.haku_num_diagnostics(pBrush) > 0) {
 | 
			
		||||
        deallocEverything();
 | 
			
		||||
        return;
 | 
			
		||||
    let compileBrush = () => haku.setBrush(brushEditor.code);
 | 
			
		||||
    compileBrush();
 | 
			
		||||
    brushEditor.addEventListener(".codeChanged", () => compileBrush());
 | 
			
		||||
 | 
			
		||||
    let reportCursor = debounce(1000 / 60, (x, y) => session.reportCursor(x, y));
 | 
			
		||||
    canvasRenderer.addEventListener(".cursor", async (event) => {
 | 
			
		||||
        reportCursor(event.x, event.y);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    canvasRenderer.addEventListener(".paint", async (event) => {
 | 
			
		||||
        painter.renderBrush(haku);
 | 
			
		||||
        let imageBitmap = await painter.createImageBitmap();
 | 
			
		||||
 | 
			
		||||
        let left = event.x - painter.paintArea / 2;
 | 
			
		||||
        let top = event.y - painter.paintArea / 2;
 | 
			
		||||
 | 
			
		||||
        let leftChunk = Math.floor(left / wall.chunkSize);
 | 
			
		||||
        let topChunk = Math.floor(top / wall.chunkSize);
 | 
			
		||||
        let rightChunk = Math.ceil((left + painter.paintArea) / wall.chunkSize);
 | 
			
		||||
        let bottomChunk = Math.ceil((top + painter.paintArea) / wall.chunkSize);
 | 
			
		||||
        for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
 | 
			
		||||
            for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
 | 
			
		||||
                let chunk = wall.getOrCreateChunk(chunkX, chunkY);
 | 
			
		||||
                let x = Math.floor(-chunkX * wall.chunkSize + left);
 | 
			
		||||
                let y = Math.floor(-chunkY * wall.chunkSize + top);
 | 
			
		||||
                chunk.ctx.drawImage(imageBitmap, x, y);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    let renderStatusCode = w.haku_render_brush(pInstance, pBrush, pBitmap);
 | 
			
		||||
    let pRenderStatusString = w.haku_status_string(renderStatusCode);
 | 
			
		||||
    logs.push(`render: ${readCString(pRenderStatusString)}`);
 | 
			
		||||
 | 
			
		||||
    if (w.haku_has_exception(pInstance)) {
 | 
			
		||||
        let length = w.haku_exception_message_len(pInstance);
 | 
			
		||||
        let pMessage = w.haku_exception_message(pInstance);
 | 
			
		||||
        let message = readString(length, pMessage);
 | 
			
		||||
        logs.push(`exception: ${message}`);
 | 
			
		||||
 | 
			
		||||
        deallocEverything();
 | 
			
		||||
        return;
 | 
			
		||||
        }
 | 
			
		||||
        imageBitmap.close();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let pBitmapData = w.haku_bitmap_data(pBitmap);
 | 
			
		||||
    let bitmapDataBuffer = new Float32Array(memory.buffer, pBitmapData, width * height * 4);
 | 
			
		||||
    let imageData = new ImageData(width, height);
 | 
			
		||||
    for (let i = 0; i < bitmapDataBuffer.length; ++i) {
 | 
			
		||||
        imageData.data[i] = bitmapDataBuffer[i] * 255;
 | 
			
		||||
    }
 | 
			
		||||
    ctx.putImageData(imageData, 0, 0);
 | 
			
		||||
 | 
			
		||||
    deallocEverything();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rerender();
 | 
			
		||||
codeTextArea.addEventListener("input", rerender);
 | 
			
		||||
    session.eventLoop();
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								static/online-users.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								static/online-users.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
export class OnlineUsers extends EventTarget {
 | 
			
		||||
    #users = new Map();
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addUser(sessionId, userInfo) {
 | 
			
		||||
        this.#users.set(sessionId, userInfo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getUser(sessionId) {
 | 
			
		||||
        return this.#users.get(sessionId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeUser(sessionId) {
 | 
			
		||||
        this.#users.delete(sessionId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								static/painter.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								static/painter.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import { Pixmap } from "./haku.js";
 | 
			
		||||
 | 
			
		||||
export class Painter {
 | 
			
		||||
    #pixmap;
 | 
			
		||||
    imageBitmap;
 | 
			
		||||
 | 
			
		||||
    constructor(paintArea) {
 | 
			
		||||
        this.paintArea = paintArea;
 | 
			
		||||
        this.#pixmap = new Pixmap(paintArea, paintArea);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async createImageBitmap() {
 | 
			
		||||
        return await createImageBitmap(this.#pixmap.imageData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderBrush(haku) {
 | 
			
		||||
        this.#pixmap.clear(0, 0, 0, 0);
 | 
			
		||||
        let result = haku.renderBrush(this.#pixmap, this.paintArea / 2, this.paintArea / 2);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								static/reticle-renderer.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								static/reticle-renderer.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,130 @@
 | 
			
		|||
export class Reticle extends HTMLElement {
 | 
			
		||||
    #kind = null;
 | 
			
		||||
    #data = {};
 | 
			
		||||
 | 
			
		||||
    #container;
 | 
			
		||||
 | 
			
		||||
    constructor(nickname) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.nickname = nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.style.setProperty("--color", this.getColor());
 | 
			
		||||
 | 
			
		||||
        this.#container = this.appendChild(document.createElement("div"));
 | 
			
		||||
        this.#container.classList.add("container");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getColor() {
 | 
			
		||||
        let hash = 5381;
 | 
			
		||||
        for (let i = 0; i < this.nickname.length; ++i) {
 | 
			
		||||
            hash <<= 5;
 | 
			
		||||
            hash += hash;
 | 
			
		||||
            hash += this.nickname.charCodeAt(i);
 | 
			
		||||
            hash &= 0xffff;
 | 
			
		||||
        }
 | 
			
		||||
        return `oklch(70% 0.2 ${(hash / 0xffff) * 360}deg)`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #update(kind, data) {
 | 
			
		||||
        this.#data = data;
 | 
			
		||||
 | 
			
		||||
        if (kind != this.#kind) {
 | 
			
		||||
            this.classList = "";
 | 
			
		||||
            this.#container.replaceChildren();
 | 
			
		||||
            this.#kind = kind;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dispatchEvent(new Event(".update"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setCursor(x, y) {
 | 
			
		||||
        this.#update("cursor", { x, y });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(viewport, windowSize) {
 | 
			
		||||
        if (!this.rendered) {
 | 
			
		||||
            if (this.#kind == "cursor") {
 | 
			
		||||
                this.classList.add("cursor");
 | 
			
		||||
 | 
			
		||||
                let arrow = this.#container.appendChild(document.createElement("div"));
 | 
			
		||||
                arrow.classList.add("arrow");
 | 
			
		||||
 | 
			
		||||
                let nickname = this.#container.appendChild(document.createElement("div"));
 | 
			
		||||
                nickname.classList.add("nickname");
 | 
			
		||||
                nickname.textContent = this.nickname;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.#kind == "cursor") {
 | 
			
		||||
            let { x, y } = this.#data;
 | 
			
		||||
            let [viewportX, viewportY] = viewport.toScreenSpace(x, y, windowSize);
 | 
			
		||||
            this.style.transform = `translate(${viewportX}px, ${viewportY}px)`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.rendered = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-reticle", Reticle);
 | 
			
		||||
 | 
			
		||||
export class ReticleRenderer extends HTMLElement {
 | 
			
		||||
    #reticles = new Map();
 | 
			
		||||
    #reticlesDiv;
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.#reticlesDiv = this.appendChild(document.createElement("div"));
 | 
			
		||||
        this.#reticlesDiv.classList.add("reticles");
 | 
			
		||||
 | 
			
		||||
        this.updateTransform();
 | 
			
		||||
        let resizeObserver = new ResizeObserver(() => this.updateTransform());
 | 
			
		||||
        resizeObserver.observe(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectViewport(viewport) {
 | 
			
		||||
        this.viewport = viewport;
 | 
			
		||||
        this.updateTransform();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getOrAddReticle(onlineUsers, sessionId) {
 | 
			
		||||
        if (this.#reticles.has(sessionId)) {
 | 
			
		||||
            return this.#reticles.get(sessionId);
 | 
			
		||||
        } else {
 | 
			
		||||
            let reticle = new Reticle(onlineUsers.getUser(sessionId).nickname);
 | 
			
		||||
            reticle.addEventListener(".update", () => {
 | 
			
		||||
                if (this.viewport != null) {
 | 
			
		||||
                    reticle.render(this.viewport, {
 | 
			
		||||
                        width: this.clientWidth,
 | 
			
		||||
                        height: this.clientHeight,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            this.#reticles.set(sessionId, reticle);
 | 
			
		||||
            this.#reticlesDiv.appendChild(reticle);
 | 
			
		||||
            return reticle;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeReticle(sessionId) {
 | 
			
		||||
        if (this.#reticles.has(sessionId)) {
 | 
			
		||||
            let reticle = this.#reticles.get(sessionId);
 | 
			
		||||
            this.#reticles.delete(sessionId);
 | 
			
		||||
            this.#reticlesDiv.removeChild(reticle);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateTransform() {
 | 
			
		||||
        if (this.viewport == null) {
 | 
			
		||||
            console.debug("viewport is disconnected, skipping transform update");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let windowSize = { width: this.clientWidth, height: this.clientHeight };
 | 
			
		||||
        for (let [_, reticle] of this.#reticles) {
 | 
			
		||||
            reticle.render(this.viewport, windowSize);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-reticle-renderer", ReticleRenderer);
 | 
			
		||||
							
								
								
									
										200
									
								
								static/session.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								static/session.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,200 @@
 | 
			
		|||
import { listen } from "./framework.js";
 | 
			
		||||
 | 
			
		||||
let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}");
 | 
			
		||||
 | 
			
		||||
function saveLoginStorage() {
 | 
			
		||||
    localStorage.setItem("rkgk.login", JSON.stringify(loginStorage));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let resolveLoggedInPromise;
 | 
			
		||||
let loggedInPromise = new Promise((resolve) => (resolveLoggedInPromise = resolve));
 | 
			
		||||
 | 
			
		||||
export function isUserLoggedIn() {
 | 
			
		||||
    return loginStorage.userId != null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getUserId() {
 | 
			
		||||
    return loginStorage.userId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function waitForLogin() {
 | 
			
		||||
    return loggedInPromise;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (isUserLoggedIn()) {
 | 
			
		||||
    resolveLoggedInPromise();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function registerUser(nickname) {
 | 
			
		||||
    try {
 | 
			
		||||
        let response = await fetch("/api/login", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            body: JSON.stringify({ nickname }),
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (response.status == 500) {
 | 
			
		||||
            console.error("login service returned 500 status", response);
 | 
			
		||||
            return {
 | 
			
		||||
                status: "error",
 | 
			
		||||
                message:
 | 
			
		||||
                    "We're sorry, but we ran into some trouble registering your account. Please try again.",
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let responseText = await response.text();
 | 
			
		||||
        let responseJson = JSON.parse(responseText);
 | 
			
		||||
        if (responseJson.status != "ok") {
 | 
			
		||||
            console.error("registering user failed", responseJson);
 | 
			
		||||
            return {
 | 
			
		||||
                status: "error",
 | 
			
		||||
                message: "Something seems to have gone wrong. Please try again.",
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(responseJson);
 | 
			
		||||
        loginStorage.userId = responseJson.userId;
 | 
			
		||||
        console.info("user registered", loginStorage.userId);
 | 
			
		||||
        saveLoginStorage();
 | 
			
		||||
        resolveLoggedInPromise();
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error("registering user failed", error);
 | 
			
		||||
        return {
 | 
			
		||||
            status: "error",
 | 
			
		||||
            message: "Something seems to have gone wrong. Please try again.",
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Session extends EventTarget {
 | 
			
		||||
    constructor(userId) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.userId = userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #recvJson() {
 | 
			
		||||
        let event = await listen([this.ws, "message"]);
 | 
			
		||||
        if (typeof event.data == "string") {
 | 
			
		||||
            return JSON.parse(event.data);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error("received a binary message where a JSON text message was expected");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #sendJson(object) {
 | 
			
		||||
        console.debug("sendJson", object);
 | 
			
		||||
        this.ws.send(JSON.stringify(object));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #dispatchError(source, kind, message) {
 | 
			
		||||
        this.dispatchEvent(
 | 
			
		||||
            Object.assign(new Event("error"), {
 | 
			
		||||
                source,
 | 
			
		||||
                errorKind: kind,
 | 
			
		||||
                message,
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async join(wallId) {
 | 
			
		||||
        console.info("joining wall", wallId);
 | 
			
		||||
        this.wallId = wallId;
 | 
			
		||||
 | 
			
		||||
        this.ws = new WebSocket("/api/wall");
 | 
			
		||||
 | 
			
		||||
        this.ws.addEventListener("error", (event) => {
 | 
			
		||||
            console.error("WebSocket connection error", error);
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event("error"), event));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.ws.addEventListener("message", (event) => {
 | 
			
		||||
            if (typeof event.data == "string") {
 | 
			
		||||
                let json = JSON.parse(event.data);
 | 
			
		||||
                if (json.error != null) {
 | 
			
		||||
                    console.error("received error from server:", json.error);
 | 
			
		||||
                    this.#dispatchError(json, "protocol", json.error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await listen([this.ws, "open"]);
 | 
			
		||||
            await this.joinInner();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async joinInner() {
 | 
			
		||||
        let version = await this.#recvJson();
 | 
			
		||||
        console.info("protocol version", version.version);
 | 
			
		||||
        // TODO: This should probably verify that the version is compatible.
 | 
			
		||||
        // We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.
 | 
			
		||||
 | 
			
		||||
        if (this.wallId == null) {
 | 
			
		||||
            this.#sendJson({ login: "new", user: this.userId });
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#sendJson({ login: "join", user: this.userId, wall: this.wallId });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let loginResponse = await this.#recvJson();
 | 
			
		||||
        if (loginResponse.response == "loggedIn") {
 | 
			
		||||
            this.wallId = loginResponse.wall;
 | 
			
		||||
            this.wallInfo = loginResponse.wallInfo;
 | 
			
		||||
            this.sessionId = loginResponse.sessionId;
 | 
			
		||||
 | 
			
		||||
            console.info("logged in", this.wallId, this.sessionId);
 | 
			
		||||
            console.info("wall info:", this.wallInfo);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#dispatchError(
 | 
			
		||||
                loginResponse,
 | 
			
		||||
                loginResponse.response,
 | 
			
		||||
                "login failed; check error kind for details",
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async eventLoop() {
 | 
			
		||||
        try {
 | 
			
		||||
            while (true) {
 | 
			
		||||
                let event = await listen([this.ws, "message"]);
 | 
			
		||||
                if (typeof event.data == "string") {
 | 
			
		||||
                    await this.#processEvent(JSON.parse(event.data));
 | 
			
		||||
                } else {
 | 
			
		||||
                    console.warn("binary event not yet supported");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.#dispatchError(error, "protocol", `error in event loop: ${error.toString()}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #processEvent(event) {
 | 
			
		||||
        if (event.kind != null) {
 | 
			
		||||
            this.dispatchEvent(
 | 
			
		||||
                Object.assign(new Event("action"), {
 | 
			
		||||
                    sessionId: event.sessionId,
 | 
			
		||||
                    kind: event.kind,
 | 
			
		||||
                }),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async reportCursor(x, y) {
 | 
			
		||||
        this.#sendJson({
 | 
			
		||||
            event: "cursor",
 | 
			
		||||
            position: { x, y },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function newSession(userId, wallId) {
 | 
			
		||||
    let session = new Session(userId);
 | 
			
		||||
    await session.join(wallId);
 | 
			
		||||
    return session;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								static/throbber.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								static/throbber.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
export class Throbber extends HTMLElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {}
 | 
			
		||||
 | 
			
		||||
    beginLoading() {
 | 
			
		||||
        this.className = "loading";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showError(message) {
 | 
			
		||||
        this.className = "error";
 | 
			
		||||
        this.textContent = message;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-throbber", Throbber);
 | 
			
		||||
							
								
								
									
										47
									
								
								static/viewport.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								static/viewport.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
export class Viewport {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.panX = 0;
 | 
			
		||||
        this.panY = 0;
 | 
			
		||||
        this.zoomLevel = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get zoom() {
 | 
			
		||||
        return Math.pow(2, this.zoomLevel * 0.25);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    panAround(x, y) {
 | 
			
		||||
        this.panX -= x / this.zoom;
 | 
			
		||||
        this.panY -= y / this.zoom;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    zoomIn(delta) {
 | 
			
		||||
        this.zoomLevel += delta;
 | 
			
		||||
        this.zoomLevel = Math.max(-16, Math.min(20, this.zoomLevel));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getVisibleRect(windowSize) {
 | 
			
		||||
        let invZoom = 1 / this.zoom;
 | 
			
		||||
        let width = windowSize.width * invZoom;
 | 
			
		||||
        let height = windowSize.height * invZoom;
 | 
			
		||||
        return {
 | 
			
		||||
            x: this.panX - width / 2,
 | 
			
		||||
            y: this.panY - height / 2,
 | 
			
		||||
            width,
 | 
			
		||||
            height,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toViewportSpace(x, y, windowSize) {
 | 
			
		||||
        return [
 | 
			
		||||
            (x - windowSize.width / 2) / this.zoom + this.panX,
 | 
			
		||||
            (y - windowSize.height / 2) / this.zoom + this.panY,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toScreenSpace(x, y, windowSize) {
 | 
			
		||||
        return [
 | 
			
		||||
            (x - this.panX) * this.zoom + windowSize.width / 2,
 | 
			
		||||
            (y - this.panY) * this.zoom + windowSize.height / 2,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								static/wall.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								static/wall.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import { OnlineUsers } from "./online-users.js";
 | 
			
		||||
 | 
			
		||||
export class Chunk {
 | 
			
		||||
    constructor(size) {
 | 
			
		||||
        this.canvas = new OffscreenCanvas(size, size);
 | 
			
		||||
        this.ctx = this.canvas.getContext("2d");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Wall {
 | 
			
		||||
    #chunks = new Map();
 | 
			
		||||
    onlineUsers = new OnlineUsers();
 | 
			
		||||
 | 
			
		||||
    constructor(chunkSize) {
 | 
			
		||||
        this.chunkSize = chunkSize;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static chunkKey(x, y) {
 | 
			
		||||
        return `(${x},${y})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChunk(x, y) {
 | 
			
		||||
        return this.#chunks.get(Wall.chunkKey(x, y));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getOrCreateChunk(x, y) {
 | 
			
		||||
        let key = Wall.chunkKey(x, y);
 | 
			
		||||
        if (this.#chunks.has(key)) {
 | 
			
		||||
            return this.#chunks.get(key);
 | 
			
		||||
        } else {
 | 
			
		||||
            let chunk = new Chunk(this.chunkSize);
 | 
			
		||||
            this.#chunks.set(key, chunk);
 | 
			
		||||
            return chunk;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								static/welcome.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								static/welcome.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import { isUserLoggedIn, registerUser } from "./session.js";
 | 
			
		||||
 | 
			
		||||
export class Welcome extends HTMLElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.dialog = this.querySelector("dialog[name='welcome-dialog']");
 | 
			
		||||
        this.form = this.dialog.querySelector("form");
 | 
			
		||||
        this.nicknameField = this.querySelector("input[name='nickname']");
 | 
			
		||||
        this.registerButton = this.querySelector("button[name='register']");
 | 
			
		||||
        this.registerProgress = this.querySelector("rkgk-throbber[name='register-progress']");
 | 
			
		||||
 | 
			
		||||
        if (!isUserLoggedIn()) {
 | 
			
		||||
            this.dialog.showModal();
 | 
			
		||||
 | 
			
		||||
            // Require an account to use the website.
 | 
			
		||||
            this.dialog.addEventListener("close", (event) => event.preventDefault());
 | 
			
		||||
 | 
			
		||||
            this.form.addEventListener("submit", async (event) => {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
 | 
			
		||||
                this.registerProgress.beginLoading();
 | 
			
		||||
                let response = await registerUser(this.nicknameField.value);
 | 
			
		||||
                if (response.status != "ok") {
 | 
			
		||||
                    this.registerProgress.showError(response.message);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.dialog.close();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("rkgk-welcome", Welcome);
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue