1use crate::types::*;
2use crate::rga::{RgaArray, RgaItem, ItemState, StrokeId};
3
4const MAX_UNDO_DEPTH: usize = 200;
7
8pub const MAX_POINTS_PER_STROKE: usize = 50_000;
15
16pub const MAX_STROKES: usize = 100_000;
19
20pub const MAX_ACTORS: usize = 10_000;
23use crate::stroke::*;
24use std::collections::HashMap;
25
26pub struct LwwMap<K: Eq + std::hash::Hash, V: Clone> {
30 entries: HashMap<K, LwwRegister<Option<V>>>,
31}
32
33impl<K: Eq + std::hash::Hash, V: Clone> LwwMap<K, V> {
34 pub fn new() -> Self {
35 Self { entries: HashMap::new() }
36 }
37
38 pub fn set(&mut self, key: K, value: V, ts: OpId) {
39 self.entries
40 .entry(key)
41 .and_modify(|reg| { reg.apply(Some(value.clone()), ts); })
42 .or_insert_with(|| LwwRegister::new(Some(value), ts));
43 }
44
45 pub fn delete(&mut self, key: K, ts: OpId) {
46 self.entries
47 .entry(key)
48 .and_modify(|reg| { reg.apply(None, ts); })
49 .or_insert_with(|| LwwRegister::new(None, ts));
50 }
51
52 pub fn get(&self, key: &K) -> Option<&V> {
53 self.entries.get(key)?.value.as_ref()
54 }
55
56 pub fn iter(&self) -> impl Iterator<Item = (&K, &Option<V>)> {
57 self.entries.iter().map(|(k, reg)| (k, ®.value))
58 }
59}
60
61impl<K: Eq + std::hash::Hash, V: Clone> Default for LwwMap<K, V> {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub enum MetadataKey {
72 ViewportX,
73 ViewportY,
74 ViewportZoom,
75 BackgroundColor,
76 GridEnabled,
77 GridSpacing,
78 Custom(String),
79}
80
81#[derive(Debug, Clone)]
83pub enum MetadataValue {
84 F64(f64),
85 Bool(bool),
86 U32(u32),
87 String(String),
88}
89
90#[derive(Debug, Clone)]
95pub enum Operation {
96 InsertStroke {
97 id: OpId,
98 origin_left: OpId,
99 origin_right: OpId,
100 data: StrokeData,
101 properties: StrokeProperties,
102 },
103 DeleteStroke {
104 id: OpId,
106 target: StrokeId,
108 },
109 UpdateProperty {
110 id: OpId,
111 target: StrokeId,
112 update: PropertyUpdate,
113 },
114 UpdateMetadata {
115 id: OpId,
116 key: MetadataKey,
117 value: Option<MetadataValue>,
119 },
120}
121
122#[derive(Debug, Clone)]
123pub enum PropertyUpdate {
124 Color(u32),
125 StrokeWidth(f32),
126 Opacity(f32),
127 Transform(Transform2D),
128}
129
130impl Operation {
131 pub fn id(&self) -> OpId {
133 match self {
134 Operation::InsertStroke { id, .. } => *id,
135 Operation::DeleteStroke { id, .. } => *id,
136 Operation::UpdateProperty { id, .. } => *id,
137 Operation::UpdateMetadata { id, .. } => *id,
138 }
139 }
140
141 pub fn target_stroke(&self) -> Option<StrokeId> {
143 match self {
144 Operation::InsertStroke { id, .. } => Some(*id),
145 Operation::DeleteStroke { target, .. } => Some(*target),
146 Operation::UpdateProperty { target, .. } => Some(*target),
147 Operation::UpdateMetadata { .. } => None,
148 }
149 }
150}
151
152pub struct Document {
156 pub local_actor: ActorId,
159 pub(crate) clock: LamportTs,
161
162 pub version: VectorClock,
166
167 pub(crate) stroke_order: RgaArray,
171 pub(crate) stroke_store: StrokeStore,
174 pub(crate) metadata: LwwMap<MetadataKey, MetadataValue>,
176
177 pub(crate) pending_ops: Vec<Operation>,
181
182 pub(crate) min_version: VectorClock,
188 pub(crate) gc_generation: u64,
190
191 undo_stack: Vec<StrokeId>,
194
195 pub simplify_epsilon: f32,
199}
200
201impl Document {
202 pub fn new(actor: ActorId) -> Self {
203 Self {
204 local_actor: actor,
205 clock: LamportTs(0),
206 version: VectorClock::default(),
207 stroke_order: RgaArray::new(),
208 stroke_store: StrokeStore::new(),
209 metadata: LwwMap::new(),
210 pending_ops: Vec::new(),
211 min_version: VectorClock::default(),
212 gc_generation: 0,
213 undo_stack: Vec::new(),
214 simplify_epsilon: 0.5,
215 }
216 }
217
218 pub fn take_pending_ops(&mut self) -> Vec<Operation> {
221 std::mem::take(&mut self.pending_ops)
222 }
223
224 pub(crate) fn next_op_id(&mut self) -> OpId {
226 let ts = self.clock.tick();
227 let id = OpId { lamport: ts, actor: self.local_actor };
228 self.version.advance(self.local_actor, ts.0);
229 id
230 }
231
232 pub fn insert_stroke(&mut self, mut data: StrokeData, properties: StrokeProperties) -> StrokeId {
239 if self.simplify_epsilon > 0.0 && data.points.len() > 2 {
242 data.simplify(self.simplify_epsilon);
243 }
244
245 let id = self.next_op_id();
246
247 let origin_left = self
248 .stroke_order
249 .visible_items()
250 .last()
251 .map(|item| item.id)
252 .unwrap_or(OpId::ZERO);
253
254 let item = RgaItem {
255 id,
256 origin_left,
257 origin_right: OpId::ZERO,
258 content: id,
259 state: ItemState::Active,
260 };
261
262 self.stroke_order.integrate(item);
263 self.stroke_store.insert(id, data.clone(), properties.clone());
264
265 self.pending_ops.push(Operation::InsertStroke {
266 id,
267 origin_left,
268 origin_right: OpId::ZERO,
269 data,
270 properties,
271 });
272
273 if self.undo_stack.len() >= MAX_UNDO_DEPTH {
275 self.undo_stack.remove(0);
276 }
277 self.undo_stack.push(id);
278
279 id
280 }
281
282 pub fn delete_stroke(&mut self, target: StrokeId) -> bool {
284 if !self.stroke_store.contains(&target) {
285 return false;
286 }
287 let id = self.next_op_id();
288 let deleted = self.stroke_order.mark_deleted(target, id);
289 if deleted {
290 self.pending_ops.push(Operation::DeleteStroke { id, target });
291 }
292 deleted
293 }
294
295 pub fn update_color(&mut self, target: StrokeId, color: u32) -> bool {
297 let id = self.next_op_id();
298 if let Some((_, props)) = self.stroke_store.get_mut(&target) {
299 props.color.apply(color, id);
300 self.pending_ops.push(Operation::UpdateProperty {
301 id,
302 target,
303 update: PropertyUpdate::Color(color),
304 });
305 true
306 } else {
307 false
308 }
309 }
310
311 pub fn update_stroke_width(&mut self, target: StrokeId, width: f32) -> bool {
313 let id = self.next_op_id();
314 if let Some((_, props)) = self.stroke_store.get_mut(&target) {
315 props.stroke_width.apply(width, id);
316 self.pending_ops.push(Operation::UpdateProperty {
317 id,
318 target,
319 update: PropertyUpdate::StrokeWidth(width),
320 });
321 true
322 } else {
323 false
324 }
325 }
326
327 pub fn update_opacity(&mut self, target: StrokeId, opacity: f32) -> bool {
329 let id = self.next_op_id();
330 if let Some((_, props)) = self.stroke_store.get_mut(&target) {
331 props.opacity.apply(opacity, id);
332 self.pending_ops.push(Operation::UpdateProperty {
333 id,
334 target,
335 update: PropertyUpdate::Opacity(opacity),
336 });
337 true
338 } else {
339 false
340 }
341 }
342
343 pub fn update_transform(&mut self, target: StrokeId, transform: Transform2D) -> bool {
345 let id = self.next_op_id();
346 if let Some((_, props)) = self.stroke_store.get_mut(&target) {
347 props.transform.apply(transform, id);
348 self.pending_ops.push(Operation::UpdateProperty {
349 id,
350 target,
351 update: PropertyUpdate::Transform(transform),
352 });
353 true
354 } else {
355 false
356 }
357 }
358
359 pub fn set_metadata(&mut self, key: MetadataKey, value: MetadataValue) {
361 let id = self.next_op_id();
362 self.metadata.set(key.clone(), value.clone(), id);
363 self.pending_ops.push(Operation::UpdateMetadata {
364 id,
365 key,
366 value: Some(value),
367 });
368 }
369
370 pub fn apply_remote(&mut self, op: Operation) -> Option<StrokeId> {
375 let op_id = op.id();
377 self.clock.merge(op_id.lamport);
378 self.version.advance(op_id.actor, op_id.lamport.0);
379
380 let affected = op.target_stroke();
381
382 match op {
383 Operation::InsertStroke { id, origin_left, origin_right, data, properties } => {
384 if !self.stroke_store.contains(&id) {
386 if self.stroke_order.total_count >= MAX_STROKES {
390 return affected;
391 }
392 let item = RgaItem {
393 id,
394 origin_left,
395 origin_right,
396 content: id,
397 state: ItemState::Active,
398 };
399 self.stroke_order.integrate(item);
400 self.stroke_store.insert(id, data, properties);
401 }
402 }
403
404 Operation::DeleteStroke { id, target } => {
405 self.stroke_order.mark_deleted(target, id);
406 }
408
409 Operation::UpdateProperty { id, target, update } => {
410 if let Some((_, props)) = self.stroke_store.get_mut(&target) {
411 match update {
412 PropertyUpdate::Color(v) => { props.color.apply(v, id); }
413 PropertyUpdate::StrokeWidth(v) => { props.stroke_width.apply(v, id); }
414 PropertyUpdate::Opacity(v) => { props.opacity.apply(v, id); }
415 PropertyUpdate::Transform(v) => { props.transform.apply(v, id); }
416 }
417 }
418 }
419
420 Operation::UpdateMetadata { id, key, value } => {
421 match value {
422 Some(v) => self.metadata.set(key, v, id),
423 None => self.metadata.delete(key, id),
424 }
425 }
426 }
427
428 affected
429 }
430
431 pub fn simplify_stroke(&mut self, target: StrokeId, epsilon: f32) -> usize {
441 if let Some(data) = self.stroke_store.get_data_mut(&target) {
442 data.simplify(epsilon)
443 } else {
444 0
445 }
446 }
447
448 pub fn undo_last_stroke(&mut self) -> Option<StrokeId> {
458 while let Some(id) = self.undo_stack.pop() {
459 if self.delete_stroke(id) {
461 return Some(id);
462 }
463 }
465 None
466 }
467
468 pub fn undo_depth(&self) -> usize {
470 self.undo_stack.len()
471 }
472
473 pub fn visible_stroke_ids(&self) -> Vec<StrokeId> {
477 self.stroke_order
478 .visible_items()
479 .map(|item| item.content)
480 .collect()
481 }
482
483 pub fn get_stroke(&self, id: &StrokeId) -> Option<(&StrokeData, &StrokeProperties)> {
485 self.stroke_store.get(id)
486 }
487
488 pub fn stats(&self) -> DocumentStats {
491 DocumentStats {
492 total_items: self.stroke_order.total_count,
493 visible_items: self.stroke_order.total_count - self.stroke_order.tombstone_count,
494 tombstones: self.stroke_order.tombstone_count,
495 tombstone_ratio: self.stroke_order.tombstone_ratio(),
496 gc_generation: self.gc_generation,
497 pending_ops: self.pending_ops.len(),
498 }
499 }
500}
501
502#[derive(Debug, Clone)]
504pub struct DocumentStats {
505 pub total_items: usize,
506 pub visible_items: usize,
507 pub tombstones: usize,
508 pub tombstone_ratio: f64,
509 pub gc_generation: u64,
510 pub pending_ops: usize,
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 fn make_doc(actor: u64) -> Document {
518 Document::new(ActorId(actor))
519 }
520
521 fn simple_stroke(points: &[(f32, f32)]) -> (StrokeData, StrokeProperties) {
522 let pts: Box<[StrokePoint]> = points.iter().map(|&(x, y)| StrokePoint::basic(x, y)).collect();
523 let data = StrokeData::new(pts, ToolKind::Pen);
524 let props = StrokeProperties::new(0xFF0000FF, 2.0, 1.0, OpId::ZERO);
525 (data, props)
526 }
527
528 #[test]
529 fn insert_and_visible() {
530 let mut doc = make_doc(1);
531 let (data, props) = simple_stroke(&[(0.0, 0.0), (10.0, 10.0)]);
532 let id = doc.insert_stroke(data, props);
533 let visible = doc.visible_stroke_ids();
534 assert_eq!(visible, vec![id]);
535 }
536
537 #[test]
538 fn delete_stroke() {
539 let mut doc = make_doc(1);
540 let (data, props) = simple_stroke(&[(0.0, 0.0)]);
541 let id = doc.insert_stroke(data, props);
542 assert!(doc.delete_stroke(id));
543 assert!(doc.visible_stroke_ids().is_empty());
544 }
545
546 #[test]
547 fn remote_insert_merge() {
548 let mut doc_a = make_doc(1);
549 let mut doc_b = make_doc(2);
550
551 let (data, props) = simple_stroke(&[(0.0, 0.0)]);
552 let id_a = doc_a.insert_stroke(data.clone(), props.clone());
553
554 let ops = std::mem::take(&mut doc_a.pending_ops);
556 for op in ops {
557 doc_b.apply_remote(op);
558 }
559
560 let visible_b = doc_b.visible_stroke_ids();
561 assert_eq!(visible_b, vec![id_a]);
562 }
563
564 #[test]
565 fn concurrent_insert_convergence() {
566 let mut doc_a = make_doc(1);
567 let mut doc_b = make_doc(2);
568
569 let (data, props) = simple_stroke(&[(1.0, 1.0)]);
570 let _id_a = doc_a.insert_stroke(data.clone(), props.clone());
571 let _id_b = doc_b.insert_stroke(data.clone(), props.clone());
572
573 let ops_a = std::mem::take(&mut doc_a.pending_ops);
574 let ops_b = std::mem::take(&mut doc_b.pending_ops);
575
576 for op in ops_b.clone() { doc_a.apply_remote(op); }
577 for op in ops_a.clone() { doc_b.apply_remote(op); }
578
579 let visible_a = doc_a.visible_stroke_ids();
580 let visible_b = doc_b.visible_stroke_ids();
581 assert_eq!(visible_a, visible_b, "docs must converge");
582 assert_eq!(visible_a.len(), 2);
583 }
584
585 #[test]
586 fn undo_last_stroke_basic() {
587 let mut doc = make_doc(1);
588 doc.simplify_epsilon = 0.0;
590
591 let (d1, p1) = simple_stroke(&[(0.0, 0.0)]);
592 let (d2, p2) = simple_stroke(&[(1.0, 1.0)]);
593 let id1 = doc.insert_stroke(d1, p1);
594 let _id2 = doc.insert_stroke(d2, p2);
595 assert_eq!(doc.visible_stroke_ids().len(), 2);
596 assert_eq!(doc.undo_depth(), 2);
597
598 let undone = doc.undo_last_stroke();
600 assert!(undone.is_some());
601 assert_eq!(doc.visible_stroke_ids(), vec![id1]);
602 assert_eq!(doc.undo_depth(), 1);
603
604 let undone2 = doc.undo_last_stroke();
606 assert!(undone2.is_some());
607 assert!(doc.visible_stroke_ids().is_empty());
608 assert_eq!(doc.undo_depth(), 0);
609
610 assert!(doc.undo_last_stroke().is_none());
612 }
613
614 #[test]
615 fn undo_skips_remotely_deleted_strokes() {
616 let mut doc = make_doc(1);
617 doc.simplify_epsilon = 0.0;
618
619 let (d, p) = simple_stroke(&[(0.0, 0.0)]);
620 let id = doc.insert_stroke(d, p);
621 assert_eq!(doc.undo_depth(), 1);
622
623 let del_op = Operation::DeleteStroke {
625 id: OpId { lamport: LamportTs(99), actor: ActorId(2) },
626 target: id,
627 };
628 doc.apply_remote(del_op);
629 assert!(doc.visible_stroke_ids().is_empty());
630
631 let result = doc.undo_last_stroke();
633 assert!(result.is_none(), "nothing left to undo");
634 }
635
636 #[test]
637 fn undo_generates_pending_delete_op() {
638 let mut doc = make_doc(1);
639 doc.simplify_epsilon = 0.0;
640
641 let (d, p) = simple_stroke(&[(0.0, 0.0)]);
642 doc.insert_stroke(d, p);
643 let _ = std::mem::take(&mut doc.pending_ops); doc.undo_last_stroke();
646 let ops = std::mem::take(&mut doc.pending_ops);
647 assert_eq!(ops.len(), 1, "undo should produce a DeleteStroke op");
648 assert!(matches!(ops[0], Operation::DeleteStroke { .. }));
649 }
650
651 #[test]
652 fn auto_simplify_enabled_by_default() {
653 let mut doc = make_doc(1);
654 let pts: Box<[StrokePoint]> = (0..100)
656 .map(|i| StrokePoint::basic(i as f32, i as f32))
657 .collect();
658 let data = StrokeData::new(pts, ToolKind::Pen);
659 let props = StrokeProperties::new(0xFF0000FF, 2.0, 1.0, OpId::ZERO);
660 let id = doc.insert_stroke(data, props);
661
662 let (stored, _) = doc.get_stroke(&id).unwrap();
664 assert!(stored.points.len() < 100, "auto-simplify should reduce points");
665 assert_eq!(stored.points.len(), 2, "straight line → 2 points");
666 }
667
668 #[test]
669 fn auto_simplify_disabled() {
670 let mut doc = make_doc(1);
671 doc.simplify_epsilon = 0.0; let pts: Box<[StrokePoint]> = (0..100)
674 .map(|i| StrokePoint::basic(i as f32, i as f32))
675 .collect();
676 let n = pts.len();
677 let data = StrokeData::new(pts, ToolKind::Pen);
678 let props = StrokeProperties::new(0xFF0000FF, 2.0, 1.0, OpId::ZERO);
679 let id = doc.insert_stroke(data, props);
680
681 let (stored, _) = doc.get_stroke(&id).unwrap();
682 assert_eq!(stored.points.len(), n, "no simplification when epsilon=0");
683 }
684}