#989: .fig pages keep room for their top-level layers past large library copies

This commit is contained in:
bflatastic
2026-10-24 13:02:25 +07:00
parent 9b78ff8c4a
commit b6489aba3b
2 changed files with 31 additions and 8 deletions
+10
View File
@@ -136,3 +136,13 @@ fn a_whole_file_past_the_cap_keeps_its_library_links_and_says_so_once() {
assert!(!warnings.iter().any(|w| w.contains("aren't in the file")), "{warnings:?}");
assert!(linked_to(&doc, "Library main") >= 190, "{warnings:?}");
}
#[test]
fn a_library_main_larger_than_the_cap_leaves_the_pages_room() {
// A main of 300 layers with room for 100: mapped first, it took the whole budget and the page
// came out empty.
let (doc, warnings) = import_at(&library_message(5, 300), 100);
let page = doc.nodes().find(|n| n.name == "Screens").expect("the page");
let top = page.children.len();
assert!(top >= 1, "the page kept {top} layers: {warnings:?}");
}
+21 -8
View File
@@ -404,6 +404,9 @@ struct Mapper<'a> {
/// Most layers produced or scanned, instance expansion included; the one place the cap comes
/// from (see [`super::layer_budget`]).
max_nodes: usize,
/// Layers kept back for the visible pages' top level while library copies are mapped, so the
/// copies can't take the whole budget and leave the pages empty.
reserved: usize,
/// Vector networks, each built once: the layers that show one (every instance of a main
/// holding it) name it by `vectorNetworkRef`. Kept out of the JSON, where a large file's
/// networks took gigabytes.
@@ -481,6 +484,7 @@ impl<'a> Mapper<'a> {
referenced_mains: BTreeSet::new(),
mapped_mains: BTreeSet::new(),
max_nodes,
reserved: 0,
networks: Vec::new(),
network_ids: HashMap::new(),
}
@@ -563,6 +567,11 @@ impl<'a> Mapper<'a> {
scope
}
/// How many layers may be produced now: the cap, less what is kept for the visible pages.
fn budget(&self) -> usize {
self.max_nodes.saturating_sub(self.reserved)
}
/// The DOCUMENT with its pages.
fn document(&mut self) -> Value {
let doc_id = self.nodes.iter().find(|(_, n)| n.get("type").and_then(Kv::str) == Some("DOCUMENT")).map(|(id, _)| id.clone());
@@ -589,11 +598,15 @@ impl<'a> Mapper<'a> {
// last layers are skipped rather than the mains every instance of them needs (#981).
let visible_pages: Vec<String> = visible.iter().map(|(p, _)| p.clone()).collect();
let required_mains = self.referenced_component_mains(&visible_pages);
// The pages and their top-level layers, up to half the cap, stay for the pages.
let top: usize = visible_pages.iter().map(|p| 1 + self.children(p).len()).sum();
self.reserved = top.min(self.max_nodes / 2);
for (id, canvas) in internal {
if let Some(context) = self.library_context(&id, canvas, &required_mains) {
self.library_contexts.push(context);
}
}
self.reserved = 0;
let out: Vec<Value> = visible.into_iter().map(|(p, n)| self.page(&p, n)).collect();
if out.is_empty() {
self.warn("the file has no pages");
@@ -838,7 +851,7 @@ impl<'a> Mapper<'a> {
self.warn(format!("layers nested deeper than {MAX_DEPTH} levels were skipped"));
return None;
}
if self.count >= self.max_nodes {
if self.count >= self.budget() {
let max = self.max_nodes;
self.warn(format!("the file has more than {max} layers; the rest were skipped"));
return None;
@@ -875,7 +888,7 @@ impl<'a> Mapper<'a> {
let connector_snapshot = own == "CONNECTOR" && self.connector_snapshot_supported(&view);
// Only a connector with a saved label needs the frame wrapper; an unlabelled one stays a single vector.
let connector_terminal =
connector_snapshot && (!connector_has_saved_label(&view) || depth >= MAX_DEPTH || self.count.saturating_add(1) >= self.max_nodes);
connector_snapshot && (!connector_has_saved_label(&view) || depth >= MAX_DEPTH || self.count.saturating_add(1) >= self.budget());
// A supported connector snapshot has its own note below instead of the generic stand-in one.
let stand = stand_in(own).filter(|_| !connector_snapshot);
let ty = match own {
@@ -1023,7 +1036,7 @@ impl<'a> Mapper<'a> {
fn shape_with_text(&mut self, o: &mut Obj, v: &View<'a>, own: &str, rest_id: &str) -> Option<Value> {
let shape = v.str("shapeWithTextType").unwrap_or("SQUARE");
let outlined = !v.list("fillGeometry").is_empty() || !v.list("strokeGeometry").is_empty();
if own != "SHAPE_WITH_TEXT" || matches!(shape, "SQUARE" | "ROUNDED_RECTANGLE") || !outlined || self.count >= self.max_nodes {
if own != "SHAPE_WITH_TEXT" || matches!(shape, "SQUARE" | "ROUNDED_RECTANGLE") || !outlined || self.count >= self.budget() {
return None;
}
let mut s = Obj::new();
@@ -1055,7 +1068,7 @@ impl<'a> Mapper<'a> {
/// layer filling it, inset a little; centred unless it's a note.
fn inner_text(&mut self, o: &Obj, v: &View<'a>, rest_id: &str, note: bool) -> Option<Value> {
let chars = own_text(v)?;
if self.count >= self.max_nodes {
if self.count >= self.budget() {
return None;
}
self.count += 1;
@@ -1190,7 +1203,7 @@ impl<'a> Mapper<'a> {
out.insert("opacity".into(), json!(root_opacity * line_opacity));
}
if connector_has_saved_label(v) {
let reason = if self.count >= self.max_nodes { "node budget" } else { "depth limit" };
let reason = if self.count >= self.budget() { "node budget" } else { "depth limit" };
self.warn(format!("connector label was skipped: {reason} prevents nested snapshot children"));
}
}
@@ -1220,7 +1233,7 @@ impl<'a> Mapper<'a> {
/// Snapshot the straight, manual connector contract carried by its generated children.
fn connector_snapshot_children(&mut self, v: &View<'a>, rest_id: &str) -> Vec<Value> {
if self.count >= self.max_nodes {
if self.count >= self.budget() {
self.warn(format!("the file has more than {} layers; connector snapshot children were skipped", self.max_nodes));
return Vec::new();
}
@@ -1283,7 +1296,7 @@ impl<'a> Mapper<'a> {
};
let background_view = View::of(background).with([background_style]);
if !background_view.list("fillPaints").is_empty() {
if self.count >= self.max_nodes {
if self.count >= self.budget() {
self.warn(format!("the file has more than {} layers; connector label background was skipped", self.max_nodes));
} else {
let mut rect = Obj::new();
@@ -1311,7 +1324,7 @@ impl<'a> Mapper<'a> {
transform: Affine,
view: &View<'a>,
) {
if self.count >= self.max_nodes {
if self.count >= self.budget() {
self.warn(format!("the file has more than {} layers; connector label was skipped", self.max_nodes));
return;
}