Skip to content
Snippets Groups Projects
Commit 5019bf53 authored by Moritz Langenstein's avatar Moritz Langenstein
Browse files

(ml5717) Added optional pseudo-users for multitouch drawing

parent ec6c30cb
No related branches found
No related tags found
No related merge requests found
......@@ -19,6 +19,7 @@ serde_derive = "1.0.104"
serde-wasm-bindgen = "0.1.3"
vec_map = { path = "vec-map", features = ["serde"] }
bincode = "1.2.1"
fixedbitset = "0.2.0"
# web-sys = { version = "0.3.33", features = ["console"] }
# The `console_error_panic_hook` crate provides better debugging of panics by
......
use fixedbitset::FixedBitSet;
use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor};
use serde::ser::{Serialize, SerializeSeq, SerializeStruct, SerializeTuple, Serializer};
use std::cmp;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::ops::{Deref, DerefMut};
use uuid::Uuid;
......@@ -627,6 +630,7 @@ pub trait EventListener {
pub struct CRDT {
user: Option<u128>,
active_strokes: (FixedBitSet, HashMap<StrokeID, u8>),
crdt: Dirty<HashMap<u128, Dirty<User>>>,
deltas: HashMap<StrokeID, StrokeDelta>,
......@@ -638,6 +642,14 @@ impl CRDT {
pub fn new(event_listener: Box<dyn EventListener>) -> CRDT {
CRDT {
user: None,
active_strokes: (
{
let mut set = FixedBitSet::with_capacity(std::u8::MAX as usize);
set.insert_range(..);
set
},
HashMap::with_capacity(std::u8::MAX as usize),
),
crdt: Dirty::new(HashMap::new(), false),
deltas: HashMap::new(),
......@@ -669,12 +681,48 @@ impl CRDT {
self.user.map(CRDT::uuid_to_string)
}
pub fn canonicalise_user(uuid: &str) -> Option<String> {
match Uuid::parse_str(uuid) {
Ok(uuid) => Some(CRDT::uuid_to_string(
(uuid.as_u128() & 0xFFFFFFFF_FFFF_0FFF_0FFF_FFFFFFFFFFFFu128)
| 0x00000000_0000_C000_3000_000000000000u128,
)),
Err(_) => None,
}
}
pub fn canonicalise_stroke_author(stroke_id: &str) -> Option<String> {
let (author, stroke) = match stroke_id.rfind('-') {
Some(split) => stroke_id.split_at(split),
None => return None,
};
match stroke[1..].parse::<usize>() {
Ok(_) => CRDT::canonicalise_user(author),
Err(_) => None,
}
}
pub fn add_stroke(&mut self, x: i32, y: i32, weight: f32, colour: &str) -> Option<String> {
let user = match self.user {
Some(user) => user,
None => return None,
};
let active_id: u8 = match self
.active_strokes
.0
.ones()
.next()
.and_then(|active_id| active_id.try_into().ok())
{
Some(active_id) => active_id,
None => return None,
};
let user = user
^ ((((active_id & 0xF0u8) as u128) << 72) | (((active_id & 0x0Fu8) as u128) << 60));
let point = match Point::try_new(x, y, weight, colour) {
Some(point) => point,
None => return None,
......@@ -690,6 +738,11 @@ impl CRDT {
None => 0,
};
self.active_strokes.0.set(active_id as usize, false);
self.active_strokes
.1
.insert(StrokeID::new(user, stroke_idx), active_id);
let mut stroke = Stroke::new(stroke_idx);
stroke.points.push(point.clone());
......@@ -708,7 +761,10 @@ impl CRDT {
Some(CRDT::uuid_to_string(user) + "-" + &stroke_idx.to_string())
}
fn split_stroke_id(&self, stroke_id: &str) -> Option<(&Dirty<User>, u128, usize)> {
fn split_stroke_id<'crdt>(
crdt: &'crdt Dirty<HashMap<u128, Dirty<User>>>,
stroke_id: &str,
) -> Option<(&'crdt Dirty<User>, u128, usize)> {
let (author, stroke) = match stroke_id.rfind('-') {
Some(split) => stroke_id.split_at(split),
None => return None,
......@@ -719,7 +775,7 @@ impl CRDT {
None => return None,
};
let entry = match self.crdt.get(&author) {
let entry = match crdt.get(&author) {
Some(entry) => entry,
None => return None,
};
......@@ -730,7 +786,10 @@ impl CRDT {
}
}
fn split_stroke_id_mut(&mut self, stroke_id: &str) -> Option<(&mut Dirty<User>, u128, usize)> {
fn split_stroke_id_mut<'crdt>(
crdt: &'crdt mut Dirty<HashMap<u128, Dirty<User>>>,
stroke_id: &str,
) -> Option<(&'crdt mut Dirty<User>, u128, usize)> {
let (author, stroke) = match stroke_id.rfind('-') {
Some(split) => stroke_id.split_at(split),
None => return None,
......@@ -741,7 +800,7 @@ impl CRDT {
None => return None,
};
let entry = match self.crdt.get_mut(&author) {
let entry = match crdt.get_mut(&author) {
Some(entry) => entry,
None => return None,
};
......@@ -760,16 +819,26 @@ impl CRDT {
weight: f32,
colour: &str,
) -> bool {
let (entry, user, stroke_idx) = match self.split_stroke_id_mut(stroke_id) {
let (entry, user, stroke_idx) = match Self::split_stroke_id_mut(&mut self.crdt, stroke_id) {
Some(stroke) => stroke,
None => return false,
};
if !self
.active_strokes
.1
.contains_key(&StrokeID::new(user, stroke_idx))
{
return false;
};
let idx = match entry.get_stroke_idx(stroke_idx) {
Some(idx) if idx == (entry.strokes.len() - 1) => idx,
Some(idx) => idx,
_ => return false,
};
assert!(idx == (entry.strokes.len() - 1));
let stroke = match entry.strokes.get_mut(idx) {
Some(stroke) => stroke,
_ => return false,
......@@ -796,12 +865,28 @@ impl CRDT {
true
}
pub fn end_stroke(&mut self, stroke_id: &str) -> bool {
let (_entry, user, stroke_idx) = match Self::split_stroke_id(&self.crdt, stroke_id) {
Some(stroke) => stroke,
None => return false,
};
match self.active_strokes.1.entry(StrokeID::new(user, stroke_idx)) {
Occupied(entry) => {
let (_stroke_id, active_id) = entry.remove_entry();
self.active_strokes.0.insert(active_id as usize);
true
}
Vacant(_) => false,
}
}
pub fn erase_stroke(&mut self, stroke_id: &str, from: f32, to: f32) -> bool {
if from < 0.0 || to < from {
return false;
};
let (entry, user, stroke_idx) = match self.split_stroke_id_mut(stroke_id) {
let (entry, user, stroke_idx) = match Self::split_stroke_id_mut(&mut self.crdt, stroke_id) {
Some(stroke) => stroke,
None => return false,
};
......@@ -834,7 +919,7 @@ impl CRDT {
}
pub fn get_stroke_points(&self, stroke_id: &str) -> Option<&VecMap<Point>> {
let (entry, _user, stroke_idx) = match self.split_stroke_id(stroke_id) {
let (entry, _user, stroke_idx) = match Self::split_stroke_id(&self.crdt, stroke_id) {
Some(stroke) => stroke,
None => return None,
};
......@@ -848,7 +933,7 @@ impl CRDT {
}
pub fn get_stroke_intervals(&self, stroke_id: &str) -> Option<&IntervalUnion<f32>> {
let (entry, _user, stroke_idx) = match self.split_stroke_id(stroke_id) {
let (entry, _user, stroke_idx) = match Self::split_stroke_id(&self.crdt, stroke_id) {
Some(stroke) => stroke,
None => return None,
};
......@@ -986,26 +1071,18 @@ impl CRDT {
pub fn fetch_deltas_from_state_vector(&self, user: String, remote_state: &StateVec) {
let mut deltas = HashMap::new();
//web_sys::console::log_1(&format!("remote state: {:?}", remote_state).into());
//web_sys::console::log_1(&format!("local state: {:?}", self.get_state_vector()).into());
for (user, strokes_state) in &self.get_state_vector().strokes {
let strokes = &self.crdt.get(user).unwrap().strokes;
if let Some(remote_strokes_state) = remote_state.strokes.get(user) {
//web_sys::console::log_1(&format!("remote strokes state: {:?}", remote_strokes_state).into());
//web_sys::console::log_1(&format!("local strokes state: {:?}", strokes_state).into());
let mut difference = (*strokes_state).clone();
difference.difference(remote_strokes_state);
//web_sys::console::log_1(&format!("strokes state difference: {:?}", difference).into());
if difference.is_empty() {
continue;
};
for interval in difference {
for mut interval in difference {
loop {
let stroke_idx = match strokes
.binary_search_by_key(&interval.from, |stroke| stroke.index)
......@@ -1017,17 +1094,11 @@ impl CRDT {
let stroke = strokes.get(stroke_idx).unwrap();
let points_len = stroke.points.vec_len();
//web_sys::console::log_1(&format!("stroke_idx {:?}", stroke_idx).into());
//web_sys::console::log_1(&format!("stroke {:?}", stroke).into());
//web_sys::console::log_1(&format!("points_len {:?}", points_len).into());
let mut stroke_delta = StrokeDelta::new();
stroke_delta
.points
.reserve(cmp::min(points_len, interval.to - interval.from + 1));
//web_sys::console::log_1(&format!("point_idx range {:?}", (interval.from - stroke.index)..=cmp::min(interval.to - stroke.index, points_len - 1)).into());
for point_idx in (interval.from - stroke.index)
..=cmp::min(interval.to - stroke.index, points_len - 1)
{
......@@ -1040,6 +1111,8 @@ impl CRDT {
if interval.to < (stroke.index + points_len) {
break;
} else {
interval.from = stroke.index + points_len
};
}
}
......@@ -1063,14 +1136,8 @@ impl CRDT {
}
}
//web_sys::console::log_1(&format!("deltas {:?}", deltas).into());
self.event_listener.on_deltas_from_state(user, deltas)
}
// TODO: add endStroke() method that finishes the named stroke
// TODO: update addStroke() to use a new username (XOR variant + version bits in UUID) if last stroke still unfinished
// TODO: keep track of users with unfinished strokes to use "fake" usernames optimally
// TODO: intervals in StateVec
}
......@@ -78,16 +78,35 @@ impl WasmCRDT {
self.0.get_user()
}
pub fn canonicalise_user(user: &str) -> Option<String> {
CRDT::canonicalise_user(user)
}
pub fn canonicalise_stroke_author(stroke_id: &str) -> Option<String> {
CRDT::canonicalise_stroke_author(stroke_id)
}
pub fn add_stroke(&mut self, x: i32, y: i32, weight: f32, colour: &str) -> Option<String> {
self.0.add_stroke(x, y, weight, colour)
}
pub fn add_point(&mut self, stroke: &str, x: i32, y: i32, weight: f32, colour: &str) -> bool {
self.0.add_point(stroke, x, y, weight, colour)
pub fn add_point(
&mut self,
stroke_id: &str,
x: i32,
y: i32,
weight: f32,
colour: &str,
) -> bool {
self.0.add_point(stroke_id, x, y, weight, colour)
}
pub fn end_stroke(&mut self, stroke_id: &str) -> bool {
self.0.end_stroke(stroke_id)
}
pub fn erase_stroke(&mut self, stroke: &str, from: f32, to: f32) -> bool {
self.0.erase_stroke(stroke, from, to)
pub fn erase_stroke(&mut self, stroke_id: &str, from: f32, to: f32) -> bool {
self.0.erase_stroke(stroke_id, from, to)
}
pub fn get_stroke_points(&self, stroke_id: &str) -> Result<JsValue, JsValue> {
......
import * as wasm from "drawing-crdt"
import { WasmCRDT } from "drawing-crdt"
const broadcasts = []
const crdt = new wasm.WasmCRDT({
const crdt = new WasmCRDT({
on_stroke: (stroke, points) => {
console.log("on_stroke:", stroke, points)
},
......@@ -21,6 +21,22 @@ const crdt = new wasm.WasmCRDT({
console.log("CRDT original StateVec", crdt.get_state_vector())
crdt.set_user("36577c51-a80b-47d6-b3c3-cfb11f705b87")
console.log("original user:", crdt.get_user())
console.log("canonical user:", WasmCRDT.canonicalise_user(crdt.get_user()))
console.log("canonical stroke author 1:", WasmCRDT.canonicalise_stroke_author(crdt.add_stroke(4, 2, 3.14, "ffff00")))
crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.add_stroke(4, 2, 3.14, "ffff00")
let end_id = crdt.add_stroke(4, 2, 3.14, "ffff00")
console.log("canonical stroke author 4:", WasmCRDT.canonicalise_stroke_author(end_id))
crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.end_stroke(end_id)
let stroke_id = crdt.add_stroke(4, 2, 3.14, "ffff00")
crdt.add_point(stroke_id, 2, 4, 4.13, "0000ff")
......@@ -46,7 +62,7 @@ console.log("pre fetch deltas")
crdt.fetch_deltas();
console.log("post fetch deltas")
const crdt2 = new wasm.WasmCRDT({
const crdt2 = new WasmCRDT({
on_stroke: (stroke, points) => {
console.log("on_stroke2:", stroke, points)
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment