Files
figcraft/crates/doc/src/ids.rs
T
MortenHustedandClaude Opus 5.5 024b7fe452 M3: importing a file keeps its own variables and styles when their ids are already taken
`file.import` of a FigCraft file kept the document's style, variable or
collection for every id both files used, and the pasted layers kept
those ids. Ids are numbered per file, so two files made apart share
them, and the imported layers were bound to the document's unrelated
variables, modes and styles.

`append_pages` now gives the incoming file new ids first (`Renames`):
- an incoming style, variable or collection is shared, as before, only
  when the document has the same under its id and everything it names
  is shared too (the same file imported again); a collection and its
  variables are shared only together; library copies keep their ids;
- every other id the document already uses gets a new one from its
  allocators, which now take ids to avoid (the incoming file's own);
- layers' style links, bindings, explicit modes and prototype actions,
  styles' bindings, variables' collections, aliases, calculations and
  extensions, and the renamed collections' mode ids follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:14:20 +01:00

162 lines
7.7 KiB
Rust

//! Ids of document-level objects (styles, variables, variable collections and modes, comment
//! threads) that stay unique when several people create them at the same time.
//!
//! Outside a live session (no client id, see [`Document::set_client_id`]) new ids are made exactly
//! as before: `S:<k>` with the first free `k` from the number of styles, `VariableID:<n>` and
//! `VariableCollectionId:<n>` one above the highest, `<collection>/mode<k>` with the first free `k`
//! from the number of modes, and comment ids one above the highest.
//!
//! During a session every new one takes its number from the document's id allocator, which hands
//! out `client_id << 32 | counter` like node ids and never hands a number out twice: `S:<n>`,
//! `VariableID:<n>`, `VariableCollectionId:<n>`, `<collection>/mode<n>` and comment id `n`. The
//! number's high 32 bits are the client whose space it lies in ([`space_of`]); ids from before
//! (small numbers) and ids of other forms (library copies `library:key`, imported files) are in
//! space 0. The session server accepts a new object only with an id in its creator's space or
//! space 0, so two people never make the same id.
use std::collections::BTreeSet;
use crate::{CollectionId, Document, ModeId, NodeId, StyleId, VariableId};
/// What style ids start with.
pub const STYLE_ID_PREFIX: &str = "S:";
/// What variable ids start with.
pub const VARIABLE_ID_PREFIX: &str = "VariableID:";
/// What variable collection ids start with.
pub const COLLECTION_ID_PREFIX: &str = "VariableCollectionId:";
/// What separates a mode id's collection from its number.
pub const MODE_ID_INFIX: &str = "/mode";
/// The client id space number `n` lies in (its high 32 bits; 0: made outside a live session).
pub fn space_of(n: u64) -> u32 {
u32::try_from(n >> 32).unwrap_or(u32::MAX)
}
/// The number of id `<prefix><digits>`, if it has that form.
pub fn id_number(id: &str, prefix: &str) -> Option<u64> {
digits(id.strip_prefix(prefix)?)
}
/// The client space of id `<prefix><n>` (0 for ids of any other form).
pub fn id_space(id: &str, prefix: &str) -> u32 {
id_number(id, prefix).map_or(0, space_of)
}
/// The number at the end of mode id `<collection>/mode<n>`.
pub fn mode_number(id: &str) -> Option<u64> {
digits(id.rsplit_once(MODE_ID_INFIX)?.1)
}
/// `s` as a number when it is only ASCII digits.
fn digits(s: &str) -> Option<u64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
impl Document {
/// A number for a new document-level id in this document's client space, above every number
/// of `taken` in that space. `None` outside a live session (ids are then made as before) or
/// when the space is full.
fn session_number(&mut self, taken: impl IntoIterator<Item = u64>) -> Option<u64> {
let client = self.client_id();
if client == 0 {
return None;
}
// Numbers of this space the allocator hasn't seen (a document rebuilt from a snapshot)
// stay taken.
let highest = taken.into_iter().filter(|n| space_of(*n) == client).max();
if let Some(h) = highest {
self.reserve_ids(NodeId(h));
}
let n = self.alloc_id().0;
(space_of(n) == client && highest.is_none_or(|h| n > h)).then_some(n)
}
/// A new style id.
pub fn new_style_id(&mut self) -> StyleId {
self.new_style_id_avoiding(&BTreeSet::new())
}
/// A new style id that is none of `avoid` either: ids of styles about to come in with a file
/// being imported.
pub fn new_style_id_avoiding(&mut self, avoid: &BTreeSet<StyleId>) -> StyleId {
let taken: Vec<u64> = self.styles.iter().map(|s| &s.id).chain(avoid).filter_map(|id| id_number(id, STYLE_ID_PREFIX)).collect();
if let Some(n) = self.session_number(taken) {
return format!("{STYLE_ID_PREFIX}{n}");
}
(self.styles.len() + 1..)
.map(|k| format!("{STYLE_ID_PREFIX}{k}"))
.find(|id| self.style(id).is_none() && !avoid.contains(id))
.unwrap_or_else(|| format!("{STYLE_ID_PREFIX}0"))
}
/// A new variable id.
pub fn new_variable_id(&mut self) -> VariableId {
self.new_variable_id_avoiding(&BTreeSet::new())
}
/// A new variable id that is none of `avoid` either (see [`Self::new_style_id_avoiding`]).
pub fn new_variable_id_avoiding(&mut self, avoid: &BTreeSet<VariableId>) -> VariableId {
let taken = self.variables.variables.iter().map(|v| &v.id).chain(avoid).filter_map(|id| id_number(id, VARIABLE_ID_PREFIX)).collect();
self.new_numbered_id(VARIABLE_ID_PREFIX, taken)
}
/// A new variable collection id.
pub fn new_collection_id(&mut self) -> CollectionId {
self.new_collection_id_avoiding(&BTreeSet::new())
}
/// A new variable collection id that is none of `avoid` either (see
/// [`Self::new_style_id_avoiding`]).
pub fn new_collection_id_avoiding(&mut self, avoid: &BTreeSet<CollectionId>) -> CollectionId {
let taken = self.variables.collections.iter().map(|c| &c.id).chain(avoid).filter_map(|id| id_number(id, COLLECTION_ID_PREFIX)).collect();
self.new_numbered_id(COLLECTION_ID_PREFIX, taken)
}
/// `<prefix><n>`: from this client's space in a session, else one above the highest of `taken`.
fn new_numbered_id(&mut self, prefix: &str, taken: Vec<u64>) -> String {
let n = match self.session_number(taken.iter().copied()) {
Some(n) => n,
None => taken.iter().max().map_or(1, |m| m.saturating_add(1)),
};
format!("{prefix}{n}")
}
/// A new id for a mode of collection `collection` (`None`: no such collection).
pub fn new_mode_id(&mut self, collection: &str) -> Option<ModeId> {
let modes = &self.variables.collections.iter().find(|c| c.id == collection)?.modes;
let count = modes.len();
let free = |doc: &Document, id: &str| doc.variables.collections.iter().any(|c| c.id == collection && c.modes.iter().all(|m| m.id != id));
let taken: Vec<u64> = self.variables.collections.iter().flat_map(|c| c.modes.iter().filter_map(|m| mode_number(&m.id))).collect();
if let Some(n) = self.session_number(taken) {
let id = format!("{collection}{MODE_ID_INFIX}{n}");
if free(self, &id) {
return Some(id);
}
}
(count + 1..).map(|k| format!("{collection}{MODE_ID_INFIX}{k}")).find(|id| free(self, id))
}
/// A new comment thread id.
pub fn new_comment_id(&mut self) -> u64 {
let taken: Vec<u64> = self.comments.iter().map(|c| c.id).collect();
self.session_number(taken).unwrap_or_else(|| self.next_comment_id())
}
/// The highest client id space any id in the document lies in (layers, comments, styles,
/// variables, collections, modes): a session hands out client ids above it, so ids made in
/// earlier sessions are never made again. Space `u32::MAX` doesn't count: numbers too big for
/// any client's space land there ([`space_of`]) and no session hands that client id out.
pub fn highest_id_space(&self) -> u32 {
let nodes = self.ids().map(|id| id.0);
let comments = self.comments.iter().map(|c| c.id);
let styles = self.styles.iter().filter_map(|s| id_number(&s.id, STYLE_ID_PREFIX));
let vars = self.variables.variables.iter().filter_map(|v| id_number(&v.id, VARIABLE_ID_PREFIX));
let colls = self.variables.collections.iter().filter_map(|c| id_number(&c.id, COLLECTION_ID_PREFIX));
let modes = self.variables.collections.iter().flat_map(|c| c.modes.iter().filter_map(|m| mode_number(&m.id)));
nodes.chain(comments).chain(styles).chain(vars).chain(colls).chain(modes).map(space_of).filter(|s| *s != u32::MAX).max().unwrap_or(0)
}
}