use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const NOTES_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Target {
Placement(String),
Cell(i32, i32),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum State {
Draft,
Open,
Claimed { by: String, until: u64 },
Resolved {
ops: Vec<String>,
summary: String,
#[serde(default)]
assumptions: Vec<String>,
#[serde(default)]
captures: Vec<String>,
},
Kept {
ops: Vec<String>,
summary: String,
#[serde(default)]
assumptions: Vec<String>,
#[serde(default)]
captures: Vec<String>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Note {
pub id: String,
pub targets: Vec<Target>,
pub text: String,
pub state: State,
#[serde(default)]
pub site: Option<Site>,
#[serde(default)]
pub history: Vec<Event>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub auto: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Event {
pub at: u64,
pub by: String,
pub what: What,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum What {
Written,
Edited { old: String },
Sent,
Claimed { until: u64 },
Renewed { until: u64 },
Released,
Expired,
Asked { question: String },
Answered { answer: String },
Resolved,
Kept,
Unkept,
WorkUndone,
}
fn now() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
}
impl Note {
fn log(&mut self, what: What) {
self.log_at(now(), "person", what);
}
fn log_at(&mut self, at: u64, by: &str, what: What) {
self.history.push(Event { at, by: by.to_owned(), what });
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Site {
pub cells: (i32, i32, i32, i32),
pub world: (f32, f32, f32, f32),
pub pieces: Vec<String>,
pub margin: i32,
pub camera: (f32, f32, f32, f32, f32),
}
pub const STATUSES: [&str; 3] = ["draft", "sent", "done"];
pub const DEFAULT_MARGIN: i32 = 1;
pub const LARGE_CHANGE: usize = 10;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Change {
pub cells: Option<(i32, i32, i32, i32)>,
pub pieces: usize,
pub person_placed: Vec<String>,
pub needs_unmapped: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Resolution {
pub summary: String,
pub assumptions: Vec<String>,
pub ops: Vec<String>,
pub captures: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Footprint {
pub op: String,
pub placements: Vec<String>,
pub cells: Vec<(i32, i32)>,
}
impl Footprint {
fn shares_with(&self, other: &Footprint) -> Option<String> {
if let Some(p) = self.placements.iter().find(|p| other.placements.contains(p)) {
return Some(format!("placement {p}"));
}
self.cells.iter().find(|c| other.cells.contains(c)).map(|(x, z)| format!("cell ({x},{z})"))
}
}
pub fn undo_blocker(note_ops: &[Footprint], later: &[Footprint]) -> Option<(String, String)> {
later
.iter()
.filter(|l| !note_ops.iter().any(|n| n.op == l.op))
.find_map(|l| note_ops.iter().find_map(|n| l.shares_with(n)).map(|s| (l.op.clone(), s)))
}
impl Site {
pub fn reach(&self) -> (i32, i32, i32, i32) {
let (a, b, c, d) = self.cells;
(a - self.margin, b - self.margin, c + self.margin, d + self.margin)
}
}
impl Note {
pub fn is_open(&self) -> bool {
self.state == State::Open
}
pub fn holder(&self) -> Option<&str> {
match &self.state {
State::Claimed { by, .. } => Some(by),
_ => None,
}
}
fn sent_at(&self) -> u64 {
self.history.iter().rev().find(|e| e.what == What::Sent).map_or(0, |e| e.at)
}
pub fn reach_rect(&self) -> Option<[f32; 4]> {
let t = crate::grid::TILE;
if let Some(s) = &self.site {
let (a, b, c, d) = s.reach();
return Some([a as f32 * t, b as f32 * t, (c + 1) as f32 * t, (d + 1) as f32 * t]);
}
let m = DEFAULT_MARGIN as f32 * t;
self.frame_rect().map(|r| [r[0] - m, r[1] - m, r[2] + m, r[3] + m])
}
fn require_holder(&self, id: &str, by: &str, did: &str) -> Result<(), String> {
match self.holder() {
Some(h) if h == by => Ok(()),
Some(h) => Err(format!("notes: {id} is claimed by {h}, not {by}, so it was not {did}")),
None => Err(format!("notes: {id} is not claimed by {by}, so it was not {did}")),
}
}
pub fn pending_question(&self) -> Option<&str> {
self.history
.iter()
.rev()
.find_map(|e| match &e.what {
What::Asked { question } => Some(Some(question.as_str())),
What::Answered { .. } => Some(None),
_ => None,
})
.flatten()
}
fn answered_since_claim(&self) -> bool {
let claim = self.history.iter().rposition(|e| matches!(e.what, What::Claimed { .. })).unwrap_or(0);
self.history[claim..].iter().any(|e| matches!(e.what, What::Answered { .. }))
}
pub fn area_cells(&self) -> Option<(i32, i32, i32, i32)> {
if let Some(s) = &self.site {
return Some(s.cells);
}
self.targets.iter().filter_map(|t| if let Target::Cell(x, z) = t { Some((*x, *z)) } else { None }).fold(
None,
|r: Option<(i32, i32, i32, i32)>, (x, z)| {
Some(r.map_or((x, z, x, z), |r| (r.0.min(x), r.1.min(z), r.2.max(x), r.3.max(z))))
},
)
}
pub fn reach_cells(&self) -> Option<(i32, i32, i32, i32)> {
let m = self.site.as_ref().map_or(DEFAULT_MARGIN, |s| s.margin);
self.area_cells().map(|(a, b, c, d)| (a - m, b - m, c + m, d + m))
}
pub fn is_draft(&self) -> bool {
self.state == State::Draft
}
pub fn status(&self) -> &'static str {
match self.state {
State::Draft => "draft",
State::Open => "sent",
State::Claimed { .. } => "claimed",
State::Resolved { .. } => "done",
State::Kept { .. } => "kept",
}
}
pub fn frame_rect(&self) -> Option<[f32; 4]> {
if let Some(s) = &self.site {
return Some([s.world.0, s.world.1, s.world.2, s.world.3]);
}
let t = crate::grid::TILE;
self.targets
.iter()
.filter_map(|t| if let Target::Cell(x, z) = t { Some((*x, *z)) } else { None })
.fold(None, |r: Option<[f32; 4]>, (x, z)| {
let (x0, z0, x1, z1) = (x as f32 * t, z as f32 * t, (x + 1) as f32 * t, (z + 1) as f32 * t);
Some(r.map_or([x0, z0, x1, z1], |r| [r[0].min(x0), r[1].min(z0), r[2].max(x1), r[3].max(z1)]))
})
}
pub fn stands(&self) -> bool {
!matches!(self.state, State::Resolved { .. } | State::Kept { .. })
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Notes {
pub version: u32,
pub notes: Vec<Note>,
#[serde(default = "default_lease")]
pub lease_secs: u64,
}
pub const DEFAULT_LEASE_SECS: u64 = 300;
fn default_lease() -> u64 {
DEFAULT_LEASE_SECS
}
impl Default for Notes {
fn default() -> Self {
Notes { version: NOTES_VERSION, notes: Vec::new(), lease_secs: DEFAULT_LEASE_SECS }
}
}
pub fn path_for_map(map_path: &Path) -> PathBuf {
let name = map_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let stem = name
.strip_suffix(".map.ron")
.or_else(|| name.strip_suffix(".ron"))
.unwrap_or(&name);
map_path.with_file_name(format!("{stem}.notes.ron"))
}
impl Notes {
pub fn from_ron(text: &str) -> Result<Notes, String> {
let notes: Notes = ron::from_str(text).map_err(|e| format!("notes: parse: {e}"))?;
if notes.version != NOTES_VERSION {
return Err(format!(
"notes: version {} is not the {NOTES_VERSION} this build reads",
notes.version
));
}
let mut seen = std::collections::BTreeSet::new();
for n in ¬es.notes {
if !seen.insert(n.id.as_str()) {
return Err(format!("notes: id {} appears twice", n.id));
}
}
Ok(notes)
}
pub fn to_ron(&self) -> Result<String, String> {
let mut text = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|e| format!("notes: serialize: {e}"))?;
if !text.ends_with('\n') {
text.push('\n');
}
Ok(text)
}
pub fn load(path: &Path) -> Result<Notes, String> {
match std::fs::read_to_string(path) {
Ok(text) => Notes::from_ron(&text).map_err(|e| format!("{}: {e}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Notes::default()),
Err(e) => Err(format!("notes: {}: {e}", path.display())),
}
}
pub fn save(&self, path: &Path) -> Result<(), String> {
if self.notes.is_empty() && !path.exists() {
return Ok(());
}
crate::ron_surgery::save_atomic(path, &self.to_ron()?)
}
pub fn get(&self, id: &str) -> Option<&Note> {
self.notes.iter().find(|n| n.id == id)
}
pub fn listing(&self, status: Option<&str>) -> Vec<&Note> {
self.notes.iter().rev().filter(|n| status.is_none_or(|s| n.status() == s)).collect()
}
pub fn open(&self) -> impl Iterator<Item = &Note> {
self.notes.iter().filter(|n| n.is_open())
}
pub fn resolved(&self) -> impl Iterator<Item = &Note> {
self.notes.iter().filter(|n| !n.stands())
}
fn next_id(&self) -> String {
let top = self
.notes
.iter()
.filter_map(|n| n.id.strip_prefix('n')?.parse::<u64>().ok())
.max()
.unwrap_or(0);
format!("n{}", top + 1)
}
fn get_mut(&mut self, id: &str) -> Result<&mut Note, String> {
self.notes.iter_mut().find(|n| n.id == id).ok_or_else(|| format!("notes: no note {id}"))
}
pub fn add(&mut self, targets: Vec<Target>, text: &str) -> Result<String, String> {
if targets.is_empty() {
return Err("notes: a note needs at least one target".to_owned());
}
if text.trim().is_empty() {
return Err("notes: a note needs some text".to_owned());
}
let id = self.next_id();
self.notes.push(Note { id: id.clone(), targets, text: text.to_owned(),
state: State::Open,
site: None,
history: Vec::new(),
auto: false,
});
self.get_mut(&id)?.log(What::Written);
Ok(id)
}
pub fn add_area(
&mut self,
map: &crate::map::Map,
cells: (i32, i32, i32, i32),
cell_size: f32,
camera: (f32, f32, f32, f32, f32),
text: &str,
) -> Result<String, String> {
let (x0, z0) = (cells.0.min(cells.2), cells.1.min(cells.3));
let (x1, z1) = (cells.0.max(cells.2), cells.1.max(cells.3));
let world = (x0 as f32 * cell_size, z0 as f32 * cell_size, (x1 + 1) as f32 * cell_size, (z1 + 1) as f32 * cell_size);
let pieces: Vec<String> = map
.placements
.iter()
.filter(|p| {
let (x, z) = (p.at.0, p.at.1);
x >= world.0 && x < world.2 && z >= world.1 && z < world.3
})
.map(|p| p.id.clone())
.collect();
let targets = if pieces.is_empty() {
let mut t = Vec::new();
for x in x0..=x1 {
for z in z0..=z1 {
t.push(Target::Cell(x, z));
}
}
t
} else {
pieces.iter().cloned().map(Target::Placement).collect()
};
let site = Site { cells: (x0, z0, x1, z1), world, pieces, margin: DEFAULT_MARGIN, camera };
let id = self.add(targets, text)?;
let note = self.get_mut(&id)?;
note.state = State::Draft;
note.site = Some(site);
Ok(id)
}
pub fn add_pieces(
&mut self,
map: &crate::map::Map,
ids: &[String],
cell_size: f32,
camera: (f32, f32, f32, f32, f32),
text: &str,
) -> Result<String, String> {
let mut found = Vec::new();
for id in ids {
let p = map
.placements
.iter()
.find(|p| &p.id == id)
.ok_or_else(|| format!("notes: no placement {id}"))?;
found.push(p);
}
if found.is_empty() {
return Err("notes: a note needs at least one target".to_owned());
}
let cell = |v: f32| (v / cell_size).floor() as i32;
let (mut x0, mut z0, mut x1, mut z1) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
for p in &found {
let (x, z) = (cell(p.at.0), cell(p.at.1));
(x0, z0, x1, z1) = (x0.min(x), z0.min(z), x1.max(x), z1.max(z));
}
let world = (x0 as f32 * cell_size, z0 as f32 * cell_size, (x1 + 1) as f32 * cell_size, (z1 + 1) as f32 * cell_size);
let pieces: Vec<String> = ids.to_vec();
let targets = pieces.iter().cloned().map(Target::Placement).collect();
let id = self.add(targets, text)?;
let note = self.get_mut(&id)?;
note.state = State::Draft;
note.site = Some(Site { cells: (x0, z0, x1, z1), world, pieces, margin: DEFAULT_MARGIN, camera });
Ok(id)
}
pub fn remove(&mut self, id: &str) -> Result<Note, String> {
let at = self.notes.iter().position(|n| n.id == id).ok_or_else(|| format!("notes: no note {id}"))?;
Ok(self.notes.remove(at))
}
pub fn edit(&mut self, id: &str, text: &str) -> Result<(), String> {
if text.trim().is_empty() {
return Err("notes: a note needs some text".to_owned());
}
let note = self.get_mut(id)?;
if !note.stands() {
return Err(format!("notes: {id} is resolved; reopen it to edit"));
}
let old = std::mem::replace(&mut note.text, text.to_owned());
note.log(What::Edited { old });
Ok(())
}
pub fn set_auto(&mut self, id: &str, auto: bool) -> Result<(), String> {
let note = self.get_mut(id)?;
if !note.stands() {
return Err(format!("notes: {id} is resolved; reopen it to change it"));
}
note.auto = auto;
Ok(())
}
pub fn send(&mut self, id: &str) -> Result<(), String> {
let note = self.get_mut(id)?;
if !note.is_draft() {
return Err(format!("notes: {id} is not a draft, so it was not sent"));
}
note.state = State::Open;
note.log(What::Sent);
Ok(())
}
pub fn send_all(&mut self) -> Result<Vec<String>, String> {
let ids: Vec<String> = self.notes.iter().filter(|n| n.is_draft()).map(|n| n.id.clone()).collect();
if ids.is_empty() {
return Err("notes: there are no drafts to send".to_owned());
}
for id in &ids {
self.send(id)?;
}
Ok(ids)
}
pub fn expire(&mut self, now: u64) -> Vec<String> {
let mut gone = Vec::new();
for n in &mut self.notes {
if let State::Claimed { until, .. } = n.state
&& until <= now
{
n.state = State::Open;
n.log_at(until, "lease", What::Expired);
gone.push(n.id.clone());
}
}
gone
}
pub fn claim(&mut self, id: &str, by: &str, now: u64) -> Result<u64, String> {
if by.trim().is_empty() {
return Err("notes: a claim names the agent making it".to_owned());
}
self.expire(now);
let note = self.get(id).ok_or_else(|| format!("notes: no note {id}"))?;
match ¬e.state {
State::Claimed { by: holder, until } => {
return Err(format!("notes: {id} is claimed by {holder} until {until}, so it was not claimed"));
}
State::Draft => return Err(format!("notes: {id} is a draft, not sent, so it was not claimed")),
State::Resolved { .. } | State::Kept { .. } => {
return Err(format!("notes: {id} is already resolved, so it was not claimed"));
}
State::Open => {}
}
if let Some(mine) = note.reach_rect() {
let key = (note.sent_at(), self.notes.iter().position(|n| n.id == id));
for (i, other) in self.notes.iter().enumerate() {
if other.id == id || !other.reach_rect().is_some_and(|r| rects_overlap(mine, r)) {
continue;
}
match &other.state {
State::Claimed { by: holder, .. } => {
return Err(format!(
"notes: {id} overlaps {}, which {holder} is working, so it waits until that resolves",
other.id
));
}
State::Open if (other.sent_at(), Some(i)) < key => {
return Err(format!(
"notes: {id} overlaps {}, which was sent first, so {} is worked first",
other.id, other.id
));
}
_ => {}
}
}
}
let until = now + self.lease_secs;
let note = self.get_mut(id)?;
note.state = State::Claimed { by: by.to_owned(), until };
note.log_at(now, by, What::Claimed { until });
Ok(until)
}
pub fn renew(&mut self, id: &str, by: &str, now: u64) -> Result<u64, String> {
self.expire(now);
let lease = self.lease_secs;
let note = self.get_mut(id)?;
match ¬e.state {
State::Claimed { by: holder, .. } if holder == by => {}
State::Claimed { by: holder, .. } => {
return Err(format!("notes: {id} is claimed by {holder}, not {by}, so it was not renewed"));
}
_ => return Err(format!("notes: {id} is not claimed by {by} (its lease may have expired), so it was not renewed")),
}
let until = now + lease;
note.state = State::Claimed { by: by.to_owned(), until };
note.log_at(now, by, What::Renewed { until });
Ok(until)
}
pub fn release(&mut self, id: &str, by: Option<&str>, now: u64) -> Result<(), String> {
self.expire(now);
let note = self.get_mut(id)?;
let State::Claimed { by: holder, .. } = ¬e.state else {
return Err(format!("notes: {id} is not claimed, so there is nothing to release"));
};
if by.is_some_and(|b| b != holder) {
return Err(format!("notes: {id} is claimed by {holder}, so only {holder} or the person may release it"));
}
note.state = State::Open;
note.log_at(now, by.unwrap_or("person"), What::Released);
Ok(())
}
pub fn resolve(&mut self, id: &str, ops: Vec<String>, summary: &str) -> Result<(), String> {
let note = self.get_mut(id)?;
if !note.is_open() && note.holder().is_none() {
return Err(format!("notes: {id} is already resolved"));
}
note.state = State::Resolved { ops, summary: summary.to_owned(), assumptions: Vec::new(), captures: Vec::new() };
Ok(())
}
pub fn ask(&mut self, id: &str, by: &str, question: &str, now: u64) -> Result<(), String> {
self.expire(now);
if question.trim().is_empty() {
return Err("notes: a question needs text".to_owned());
}
let note = self.get_mut(id)?;
note.require_holder(id, by, "asked")?;
if note.auto {
return Err(format!(
"notes: {id} is auto: the person asked you to decide open choices yourself, so do not ask. State what you assumed when you resolve it"
));
}
if note.pending_question().is_some() {
return Err(format!("notes: {id} already has a question waiting for the person's answer"));
}
note.log_at(now, by, What::Asked { question: question.to_owned() });
Ok(())
}
pub fn answer(&mut self, id: &str, answer: &str, now: u64) -> Result<(), String> {
if answer.trim().is_empty() {
return Err("notes: an answer needs text".to_owned());
}
let note = self.get_mut(id)?;
if note.pending_question().is_none() {
return Err(format!("notes: {id} has no question waiting, so there is nothing to answer"));
}
note.log_at(now, "person", What::Answered { answer: answer.to_owned() });
Ok(())
}
pub fn resolve_record(&mut self, id: &str, by: &str, record: Resolution, now: u64) -> Result<(), String> {
self.expire(now);
if record.summary.trim().is_empty() {
return Err("notes: a resolve needs a one-line summary".to_owned());
}
if record.captures.len() != 2 || record.captures.iter().any(|c| c.trim().is_empty()) {
return Err("notes: a resolve names two captures, the area before the work and after it".to_owned());
}
let note = self.get_mut(id)?;
note.require_holder(id, by, "resolved")?;
if note.pending_question().is_some() {
return Err(format!("notes: {id} has a question the person has not answered, so it was not resolved"));
}
note.state = State::Resolved {
ops: record.ops,
summary: record.summary,
assumptions: record.assumptions,
captures: record.captures,
};
note.log_at(now, by, What::Resolved);
Ok(())
}
pub fn fence(&self, id: &str, by: &str, change: &Change) -> Result<(), String> {
let note = self.get(id).ok_or_else(|| format!("notes: no note {id}"))?;
note.require_holder(id, by, "changed")?;
let reach = note.reach_cells();
if let (Some((x0, z0, x1, z1)), Some((rx0, rz0, rx1, rz1))) = (change.cells, reach)
&& (x0 < rx0 || z0 < rz0 || x1 > rx1 || z1 > rz1)
{
return Err(format!(
"notes: the change reaches cells ({x0},{z0})..({x1},{z1}), outside note {id}'s area plus its margin ({rx0},{rz0})..({rx1},{rz1}); nothing was changed"
));
}
let mut why = Vec::new();
if change.pieces > LARGE_CHANGE {
why.push(format!("it changes {} pieces, more than {LARGE_CHANGE}", change.pieces));
}
if let (Some((x0, z0, x1, z1)), Some((ax0, az0, ax1, az1))) = (change.cells, note.area_cells())
&& (x0 < ax0 || z0 < az0 || x1 > ax1 || z1 > az1)
{
why.push("it touches the margin around the area".to_owned());
}
if !change.person_placed.is_empty() {
why.push(format!("it removes {}, placed by a person", change.person_placed.join(", ")));
}
if let Some(what) = &change.needs_unmapped {
why.push(format!("the kit has nothing for \"{what}\""));
}
if why.is_empty() || note.answered_since_claim() {
return Ok(());
}
if note.auto {
return Err(format!(
"notes: {id} is auto, so it cannot ask, and {}; narrow the change or release the note for the person; nothing was changed",
why.join("; ")
));
}
let waiting = if note.pending_question().is_some() { " (a question is waiting for the person)" } else { "" };
Err(format!(
"notes: {id}: {}, so ask the person first with note_ask and go on once they answer{waiting}; nothing was changed",
why.join("; ")
))
}
pub fn reopen(&mut self, id: &str) -> Result<(), String> {
let note = self.get_mut(id)?;
if note.stands() {
return Err(format!("notes: {id} is already open"));
}
note.state = State::Open;
Ok(())
}
pub fn dangling(&self, map: &crate::map::Map) -> Vec<(String, String)> {
let mut out = Vec::new();
for n in &self.notes {
for t in &n.targets {
if let Target::Placement(p) = t
&& !map.placements.iter().any(|x| &x.id == p)
{
out.push((n.id.clone(), p.clone()));
}
}
}
out
}
pub fn keep(&mut self, id: &str, now: u64) -> Result<(), String> {
let note = self.get_mut(id)?;
let State::Resolved { ops, summary, assumptions, captures } = ¬e.state else {
return Err(format!("notes: {id} is not done, so there is no work to keep"));
};
note.state = State::Kept {
ops: ops.clone(),
summary: summary.clone(),
assumptions: assumptions.clone(),
captures: captures.clone(),
};
note.log_at(now, "person", What::Kept);
Ok(())
}
pub fn unkeep(&mut self, id: &str, now: u64) -> Result<(), String> {
let note = self.get_mut(id)?;
let State::Kept { ops, summary, assumptions, captures } = ¬e.state else {
return Err(format!("notes: {id} is not kept"));
};
note.state = State::Resolved {
ops: ops.clone(),
summary: summary.clone(),
assumptions: assumptions.clone(),
captures: captures.clone(),
};
note.log_at(now, "person", What::Unkept);
Ok(())
}
pub fn undo_work(&mut self, id: &str, note_ops: &[Footprint], later: &[Footprint], now: u64) -> Result<(), String> {
let note = self.get_mut(id)?;
if !matches!(note.state, State::Resolved { .. }) {
return Err(format!("notes: {id} is not done, so there is no work to undo"));
}
if let Some((op, what)) = undo_blocker(note_ops, later) {
return Err(format!(
"notes: {id}'s work was not undone: later operation {op} touched {what}, which the note's work made, so undoing would overwrite it. Undo {op} first"
));
}
note.state = State::Open;
note.log_at(now, "person", What::WorkUndone);
Ok(())
}
}
fn rects_overlap(a: [f32; 4], b: [f32; 4]) -> bool {
a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3]
}