qualia_core_db/domains/geospatial/
spatial_sync.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct PlantedAsset {
8 pub asset_id: String,
10 pub cell_id: u64,
12 pub position: [f64; 3],
14 pub rotation: [f64; 4],
16 pub creator_did: String,
18 pub lamport: u64,
20 pub deleted: bool,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct SpatialSyncCell {
27 pub cell_id: u64,
28 pub planted_assets: Vec<PlantedAsset>,
29}
30
31impl SpatialSyncCell {
32 pub fn new(cell_id: u64, planted_assets: Vec<PlantedAsset>) -> Self {
33 Self {
34 cell_id,
35 planted_assets,
36 }
37 }
38
39 pub fn from_assets(cell_id: u64, assets: &[PlantedAsset]) -> Self {
41 let mut planted_assets: Vec<PlantedAsset> = assets
42 .iter()
43 .filter(|a| a.cell_id == cell_id && !a.deleted)
44 .cloned()
45 .collect();
46 planted_assets.sort_by(|a, b| a.asset_id.cmp(&b.asset_id));
47 Self {
48 cell_id,
49 planted_assets,
50 }
51 }
52
53 pub fn merge(&mut self, incoming: &[PlantedAsset]) {
55 let merged = merge_planted_assets(&self.planted_assets, incoming);
56 self.planted_assets = merged
57 .into_iter()
58 .filter(|a| a.cell_id == self.cell_id && !a.deleted)
59 .collect();
60 }
61}
62
63pub fn merge_planted_assets(local: &[PlantedAsset], remote: &[PlantedAsset]) -> Vec<PlantedAsset> {
67 let mut state: HashMap<String, PlantedAsset> = HashMap::new();
68
69 for asset in local.iter().chain(remote.iter()) {
70 if let Some(existing) = state.get(&asset.asset_id) {
71 if asset.lamport > existing.lamport {
72 state.insert(asset.asset_id.clone(), asset.clone());
73 } else if asset.lamport == existing.lamport {
74 if asset.creator_did > existing.creator_did {
79 state.insert(asset.asset_id.clone(), asset.clone());
80 }
81 }
82 } else {
83 state.insert(asset.asset_id.clone(), asset.clone());
84 }
85 }
86
87 let mut merged: Vec<PlantedAsset> = state.into_values().collect();
89 merged.sort_by(|a, b| a.asset_id.cmp(&b.asset_id));
90 merged
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_merge_planted_assets() {
99 let asset1_v1 = PlantedAsset {
100 asset_id: "obj-1".into(),
101 cell_id: 1234,
102 position: [1.0, 2.0, 3.0],
103 rotation: [0.0, 0.0, 0.0, 1.0],
104 creator_did: "did:q42:alice".into(),
105 lamport: 1,
106 deleted: false,
107 };
108
109 let asset1_v2 = PlantedAsset {
110 asset_id: "obj-1".into(),
111 cell_id: 1234,
112 position: [1.0, 5.0, 3.0], rotation: [0.0, 0.0, 0.0, 1.0],
114 creator_did: "did:q42:alice".into(),
115 lamport: 2,
116 deleted: false,
117 };
118
119 let asset2 = PlantedAsset {
120 asset_id: "obj-2".into(),
121 cell_id: 1234,
122 position: [5.0, 5.0, 5.0],
123 rotation: [0.0, 0.0, 0.0, 1.0],
124 creator_did: "did:q42:bob".into(),
125 lamport: 1,
126 deleted: false,
127 };
128
129 let local = vec![asset1_v1.clone()];
130 let remote = vec![asset1_v2.clone(), asset2.clone()];
131
132 let merged = merge_planted_assets(&local, &remote);
133 assert_eq!(merged.len(), 2);
134
135 assert_eq!(merged[0].asset_id, "obj-1");
137 assert_eq!(merged[0].position[1], 5.0);
138
139 assert_eq!(merged[1].asset_id, "obj-2");
140 }
141
142 #[test]
143 fn spatial_sync_cell_filters_by_cell_and_tombstones() {
144 let active = PlantedAsset {
145 asset_id: "obj-a".into(),
146 cell_id: 42,
147 position: [0.0, 0.0, 0.0],
148 rotation: [0.0, 0.0, 0.0, 1.0],
149 creator_did: "did:q42:alice".into(),
150 lamport: 1,
151 deleted: false,
152 };
153 let tombstone = PlantedAsset {
154 asset_id: "obj-b".into(),
155 cell_id: 42,
156 position: [1.0, 0.0, 0.0],
157 rotation: [0.0, 0.0, 0.0, 1.0],
158 creator_did: "did:q42:bob".into(),
159 lamport: 2,
160 deleted: true,
161 };
162 let other_cell = PlantedAsset {
163 asset_id: "obj-c".into(),
164 cell_id: 99,
165 position: [2.0, 0.0, 0.0],
166 rotation: [0.0, 0.0, 0.0, 1.0],
167 creator_did: "did:q42:carol".into(),
168 lamport: 1,
169 deleted: false,
170 };
171
172 let cell = SpatialSyncCell::from_assets(42, &[active.clone(), tombstone, other_cell]);
173 assert_eq!(cell.cell_id, 42);
174 assert_eq!(cell.planted_assets.len(), 1);
175 assert_eq!(cell.planted_assets[0].asset_id, "obj-a");
176 }
177
178 #[test]
179 fn spatial_sync_cell_merge_applies_lww() {
180 let local = PlantedAsset {
181 asset_id: "obj-1".into(),
182 cell_id: 7,
183 position: [0.0, 0.0, 0.0],
184 rotation: [0.0, 0.0, 0.0, 1.0],
185 creator_did: "did:q42:alice".into(),
186 lamport: 1,
187 deleted: false,
188 };
189 let remote = PlantedAsset {
190 asset_id: "obj-1".into(),
191 cell_id: 7,
192 position: [5.0, 0.0, 0.0],
193 rotation: [0.0, 0.0, 0.0, 1.0],
194 creator_did: "did:q42:bob".into(),
195 lamport: 3,
196 deleted: false,
197 };
198
199 let mut cell = SpatialSyncCell::from_assets(7, &[local]);
200 cell.merge(&[remote]);
201 assert_eq!(cell.planted_assets.len(), 1);
202 assert_eq!(cell.planted_assets[0].position[0], 5.0);
203 }
204}