Perf: persistent node map (imbl) and dirty tracking; commands now scale with what changed (node.create 0.2 ms at 20k nodes)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
077ed64739
commit
ab5914485e
|
|
@ -262,6 +262,12 @@ dependencies = [
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "archery"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca55ee147b1926dbea904f50fe4902494e97bc742205abbbf10c709e43815f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
@ -1319,6 +1325,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"figcraft-color",
|
"figcraft-color",
|
||||||
"figcraft-geom",
|
"figcraft-geom",
|
||||||
|
"imbl",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
|
|
@ -2050,6 +2057,28 @@ version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
|
checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imbl"
|
||||||
|
version = "7.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46bad832b9b463ed9398b8506488cc2e3b897a9d44f13af115f382aac71f4fec"
|
||||||
|
dependencies = [
|
||||||
|
"archery",
|
||||||
|
"equivalent",
|
||||||
|
"imbl-sized-chunks",
|
||||||
|
"rand_core",
|
||||||
|
"rand_xoshiro",
|
||||||
|
"serde_core",
|
||||||
|
"version_check",
|
||||||
|
"wide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imbl-sized-chunks"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0813be332553f857953298749fa19549e8b61b80589757c29b4e2a804fa9c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.2"
|
||||||
|
|
@ -3233,6 +3262,15 @@ dependencies = [
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xoshiro"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -3513,6 +3551,15 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "safe_arch"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -4735,6 +4782,16 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wide"
|
||||||
|
version = "0.7.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"safe_arch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ serde = { version = "1", features = ["derive", "rc"] }
|
||||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
|
imbl = { version = "7", features = ["serde"] }
|
||||||
|
|
||||||
# rendering / formats
|
# rendering / formats
|
||||||
tiny-skia = "0.12"
|
tiny-skia = "0.12"
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ figcraft-color = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
imbl = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,16 @@ pub enum DocError {
|
||||||
|
|
||||||
pub type DocResult<T> = Result<T, DocError>;
|
pub type DocResult<T> = Result<T, DocError>;
|
||||||
|
|
||||||
|
/// The set of touched node ids. Never part of equality or serialisation.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Dirty(pub std::collections::BTreeSet<NodeId>);
|
||||||
|
|
||||||
|
impl PartialEq for Dirty {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Raw image bytes (encoded PNG/JPEG/…) kept by content hash.
|
/// Raw image bytes (encoded PNG/JPEG/…) kept by content hash.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ImageData {
|
pub struct ImageData {
|
||||||
|
|
@ -79,7 +89,10 @@ pub struct Document {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Page ids in order.
|
/// Page ids in order.
|
||||||
pub pages: Vec<NodeId>,
|
pub pages: Vec<NodeId>,
|
||||||
nodes: BTreeMap<NodeId, Arc<Node>>,
|
nodes: imbl::OrdMap<NodeId, Arc<Node>>,
|
||||||
|
/// Nodes touched since the last [`take_dirty`](Self::take_dirty) (edited, added or removed).
|
||||||
|
#[serde(skip)]
|
||||||
|
dirty: Dirty,
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub styles: Vec<Style>,
|
pub styles: Vec<Style>,
|
||||||
|
|
@ -108,7 +121,8 @@ impl Document {
|
||||||
let mut doc = Self {
|
let mut doc = Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
pages: Vec::new(),
|
pages: Vec::new(),
|
||||||
nodes: BTreeMap::new(),
|
nodes: imbl::OrdMap::new(),
|
||||||
|
dirty: Dirty::default(),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
styles: Vec::new(),
|
styles: Vec::new(),
|
||||||
variables: Variables::default(),
|
variables: Variables::default(),
|
||||||
|
|
@ -121,6 +135,16 @@ impl Document {
|
||||||
doc
|
doc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Node ids touched since the last call (edited, added or removed), clearing the record.
|
||||||
|
pub fn take_dirty(&mut self) -> std::collections::BTreeSet<NodeId> {
|
||||||
|
std::mem::take(&mut self.dirty.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Node ids touched so far (without clearing).
|
||||||
|
pub fn dirty(&self) -> &std::collections::BTreeSet<NodeId> {
|
||||||
|
&self.dirty.0
|
||||||
|
}
|
||||||
|
|
||||||
/// A fresh unused id.
|
/// A fresh unused id.
|
||||||
pub fn alloc_id(&mut self) -> NodeId {
|
pub fn alloc_id(&mut self) -> NodeId {
|
||||||
let id = NodeId(self.next_id);
|
let id = NodeId(self.next_id);
|
||||||
|
|
@ -138,7 +162,11 @@ impl Document {
|
||||||
|
|
||||||
/// Mutable access (copy-on-write when a snapshot shares the node).
|
/// Mutable access (copy-on-write when a snapshot shares the node).
|
||||||
pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
|
pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
|
||||||
self.nodes.get_mut(&id).map(Arc::make_mut)
|
let n = self.nodes.get_mut(&id).map(Arc::make_mut);
|
||||||
|
if n.is_some() {
|
||||||
|
self.dirty.0.insert(id);
|
||||||
|
}
|
||||||
|
n
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: NodeId) -> DocResult<&Node> {
|
pub fn get(&self, id: NodeId) -> DocResult<&Node> {
|
||||||
|
|
@ -177,6 +205,7 @@ impl Document {
|
||||||
0.0,
|
0.0,
|
||||||
);
|
);
|
||||||
self.nodes.insert(id, Arc::new(node));
|
self.nodes.insert(id, Arc::new(node));
|
||||||
|
self.dirty.0.insert(id);
|
||||||
self.pages.push(id);
|
self.pages.push(id);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +243,7 @@ impl Document {
|
||||||
}
|
}
|
||||||
node.parent = Some(parent);
|
node.parent = Some(parent);
|
||||||
self.nodes.insert(id, Arc::new(node));
|
self.nodes.insert(id, Arc::new(node));
|
||||||
|
self.dirty.0.insert(id);
|
||||||
let p = self.get_mut(parent)?;
|
let p = self.get_mut(parent)?;
|
||||||
let at = index.unwrap_or(p.children.len()).min(p.children.len());
|
let at = index.unwrap_or(p.children.len()).min(p.children.len());
|
||||||
p.children.insert(at, id);
|
p.children.insert(at, id);
|
||||||
|
|
@ -294,6 +324,7 @@ impl Document {
|
||||||
}
|
}
|
||||||
for d in self.subtree(id) {
|
for d in self.subtree(id) {
|
||||||
self.nodes.remove(&d);
|
self.nodes.remove(&d);
|
||||||
|
self.dirty.0.insert(d);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -511,6 +542,7 @@ impl Document {
|
||||||
self.next_id = new_id.0.saturating_add(1);
|
self.next_id = new_id.0.saturating_add(1);
|
||||||
}
|
}
|
||||||
self.nodes.insert(*new_id, Arc::new(c));
|
self.nodes.insert(*new_id, Arc::new(c));
|
||||||
|
self.dirty.0.insert(*new_id);
|
||||||
}
|
}
|
||||||
let p = self.get_mut(parent)?;
|
let p = self.get_mut(parent)?;
|
||||||
let at = index.unwrap_or(p.children.len()).min(p.children.len());
|
let at = index.unwrap_or(p.children.len()).min(p.children.len());
|
||||||
|
|
@ -593,7 +625,7 @@ impl Document {
|
||||||
/// Check structural invariants (used by tests and after loading untrusted files).
|
/// Check structural invariants (used by tests and after loading untrusted files).
|
||||||
pub fn validate(&self) -> Vec<String> {
|
pub fn validate(&self) -> Vec<String> {
|
||||||
let mut issues = Vec::new();
|
let mut issues = Vec::new();
|
||||||
for (id, n) in &self.nodes {
|
for (id, n) in self.nodes.iter() {
|
||||||
if n.id != *id {
|
if n.id != *id {
|
||||||
issues.push(format!("node key {id} has id {}", n.id));
|
issues.push(format!("node key {id} has id {}", n.id));
|
||||||
}
|
}
|
||||||
|
|
@ -653,7 +685,7 @@ impl Document {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.pages.retain(|p| self.nodes.get(p).is_some_and(|n| n.is_page()));
|
self.pages.retain(|p| self.nodes.get(p).is_some_and(|n| n.is_page()));
|
||||||
for (k, n) in &self.nodes {
|
for (k, n) in self.nodes.iter() {
|
||||||
if n.is_page() && !self.pages.contains(k) {
|
if n.is_page() && !self.pages.contains(k) {
|
||||||
self.pages.push(*k);
|
self.pages.push(*k);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||||
Some(Value::Array(a)) => {
|
Some(Value::Array(a)) => {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for (i, c) in a.iter().enumerate() {
|
for (i, c) in a.iter().enumerate() {
|
||||||
let cmd = c.get("command").and_then(Value::as_str).ok_or_else(|| bad("command.batch", format!("step {i} has no `command`")))?;
|
let cmd =
|
||||||
|
c.get("command").and_then(Value::as_str).ok_or_else(|| bad("command.batch", format!("step {i} has no `command`")))?;
|
||||||
out.push((cmd.to_string(), c.get("params").cloned().unwrap_or(json!({}))));
|
out.push((cmd.to_string(), c.get("params").cloned().unwrap_or(json!({}))));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ fn diff_groups(a: &Node, b: &Node) -> Vec<&'static str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// After an edit: record overrides inside instances, then rebuild instances of edited components.
|
/// After an edit: record overrides inside instances, then rebuild instances of edited components.
|
||||||
pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId]) {
|
pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId], touched: &BTreeSet<NodeId>) {
|
||||||
// 1. Overrides for layers edited inside instances.
|
// 1. Overrides for layers edited inside instances.
|
||||||
for id in changed {
|
for id in changed {
|
||||||
let Some(n) = doc.node(*id) else { continue };
|
let Some(n) = doc.node(*id) else { continue };
|
||||||
|
|
@ -389,7 +389,7 @@ pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId]
|
||||||
comps.insert(c);
|
comps.insert(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in before.ids() {
|
for &id in touched {
|
||||||
if !doc.contains(id)
|
if !doc.contains(id)
|
||||||
&& let Some(c) = owning_component(before, id)
|
&& let Some(c) = owning_component(before, id)
|
||||||
&& doc.contains(c)
|
&& doc.contains(c)
|
||||||
|
|
|
||||||
|
|
@ -620,8 +620,9 @@ pub fn changed_nodes(doc: &Document, before: &Document) -> Vec<NodeId> {
|
||||||
/// Keep derived state consistent after edits: text boxes resize to their content, groups wrap
|
/// Keep derived state consistent after edits: text boxes resize to their content, groups wrap
|
||||||
/// their children, instances follow their main components, auto layout re-flows.
|
/// their children, instances follow their main components, auto layout re-flows.
|
||||||
pub fn normalize(doc: &mut Document, before: &Document) {
|
pub fn normalize(doc: &mut Document, before: &Document) {
|
||||||
let changed = changed_nodes(doc, before);
|
// Only what was touched since the last normalisation (edited, added or removed).
|
||||||
if changed.is_empty() && doc.node_count() == before.node_count() {
|
let mut touched: BTreeSet<NodeId> = doc.take_dirty();
|
||||||
|
if touched.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let m = measure::TextMeasure;
|
let m = measure::TextMeasure;
|
||||||
|
|
@ -633,21 +634,28 @@ pub fn normalize(doc: &mut Document, before: &Document) {
|
||||||
cmd::styles::propagate_style(doc, &sid);
|
cmd::styles::propagate_style(doc, &sid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let changed = changed_nodes(doc, before);
|
touched.extend(doc.take_dirty());
|
||||||
|
let live = |doc: &Document, t: &BTreeSet<NodeId>| -> Vec<NodeId> { t.iter().copied().filter(|i| doc.contains(*i)).collect() };
|
||||||
// 1. Text auto-size.
|
// 1. Text auto-size.
|
||||||
for id in &changed {
|
for id in live(doc, &touched) {
|
||||||
let Some(n) = doc.node(*id) else { continue };
|
let Some(n) = doc.node(id) else { continue };
|
||||||
if let Some((w, h)) = figcraft_text::auto_size(n)
|
if let Some((w, h)) = figcraft_text::auto_size(n)
|
||||||
&& ((w - n.width).abs() > 1e-6 || (h - n.height).abs() > 1e-6)
|
&& ((w - n.width).abs() > 1e-6 || (h - n.height).abs() > 1e-6)
|
||||||
{
|
{
|
||||||
figcraft_layout::set_size(doc, *id, w, h);
|
figcraft_layout::set_size(doc, id, w, h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
touched.extend(doc.take_dirty());
|
||||||
// 2. Instances follow edited main components (and record overrides inside instances).
|
// 2. Instances follow edited main components (and record overrides inside instances).
|
||||||
components::sync_after_edit(doc, before, &changed);
|
let changed = live(doc, &touched);
|
||||||
|
components::sync_after_edit(doc, before, &changed, &touched);
|
||||||
|
touched.extend(doc.take_dirty());
|
||||||
|
// Parents of removed nodes (as they were before) count as touched.
|
||||||
|
let removed_parents: Vec<NodeId> =
|
||||||
|
touched.iter().filter(|i| !doc.contains(**i)).filter_map(|i| before.node(*i).and_then(|n| n.parent)).filter(|p| doc.contains(*p)).collect();
|
||||||
// 3. Groups / booleans wrap their children.
|
// 3. Groups / booleans wrap their children.
|
||||||
let mut fit: BTreeSet<NodeId> = BTreeSet::new();
|
let mut fit: BTreeSet<NodeId> = BTreeSet::new();
|
||||||
for id in changed_nodes(doc, before) {
|
for id in live(doc, &touched).into_iter().chain(removed_parents.iter().copied()) {
|
||||||
if doc.node(id).is_some_and(|n| n.kind.bounds_follow_children()) {
|
if doc.node(id).is_some_and(|n| n.kind.bounds_follow_children()) {
|
||||||
fit.insert(id);
|
fit.insert(id);
|
||||||
}
|
}
|
||||||
|
|
@ -659,56 +667,37 @@ pub fn normalize(doc: &mut Document, before: &Document) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Removed children change their parents too.
|
|
||||||
for id in before.ids() {
|
|
||||||
if !doc.contains(id)
|
|
||||||
&& let Some(p) = before.node(id).and_then(|n| n.parent)
|
|
||||||
&& doc.node(p).is_some_and(|n| n.kind.bounds_follow_children())
|
|
||||||
{
|
|
||||||
fit.insert(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Deepest first.
|
|
||||||
let mut fit: Vec<NodeId> = fit.into_iter().collect();
|
let mut fit: Vec<NodeId> = fit.into_iter().collect();
|
||||||
fit.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
fit.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
||||||
for id in fit {
|
for id in fit {
|
||||||
if doc.node(id).is_some_and(|n| n.children.is_empty()) {
|
if doc.node(id).is_some_and(|n| n.children.is_empty()) {
|
||||||
// Empty groups disappear, like Figma.
|
// Empty groups disappear, like the incumbent.
|
||||||
let _ = doc.remove(id);
|
let _ = doc.remove(id);
|
||||||
} else {
|
} else {
|
||||||
let _ = doc.fit_to_children(id);
|
let _ = doc.fit_to_children(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
touched.extend(doc.take_dirty());
|
||||||
// 4. Auto layout for affected frames.
|
// 4. Auto layout for affected frames.
|
||||||
let mut frames: BTreeSet<NodeId> = BTreeSet::new();
|
let mut frames: BTreeSet<NodeId> = BTreeSet::new();
|
||||||
let changed = changed_nodes(doc, before);
|
for id in live(doc, &touched).into_iter().chain(removed_parents) {
|
||||||
for id in &changed {
|
if !doc.contains(id) {
|
||||||
let mut chain = vec![*id];
|
continue;
|
||||||
chain.extend(doc.ancestors(*id));
|
}
|
||||||
|
let mut chain = vec![id];
|
||||||
|
chain.extend(doc.ancestors(id));
|
||||||
for c in chain {
|
for c in chain {
|
||||||
if doc.node(c).is_some_and(|n| n.layout.is_some()) {
|
if doc.node(c).is_some_and(|n| n.layout.is_some()) {
|
||||||
frames.insert(c);
|
frames.insert(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in before.ids() {
|
|
||||||
if !doc.contains(id)
|
|
||||||
&& let Some(p) = before.node(id).and_then(|n| n.parent)
|
|
||||||
{
|
|
||||||
let mut chain = vec![p];
|
|
||||||
chain.extend(doc.ancestors(p));
|
|
||||||
for c in chain {
|
|
||||||
if doc.node(c).is_some_and(|n| n.layout.is_some()) {
|
|
||||||
frames.insert(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut frames: Vec<NodeId> = frames.into_iter().collect();
|
let mut frames: Vec<NodeId> = frames.into_iter().collect();
|
||||||
frames.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
frames.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
||||||
for f in frames {
|
for f in frames {
|
||||||
figcraft_layout::layout_frame(doc, f, &m);
|
figcraft_layout::layout_frame(doc, f, &m);
|
||||||
}
|
}
|
||||||
|
doc.take_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,8 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||||
// Work in a padded buffer: everything beyond the layer is "outside the shape", so
|
// Work in a padded buffer: everything beyond the layer is "outside the shape", so
|
||||||
// the blur pulls shadow in from every edge, not just the offset side.
|
// the blur pulls shadow in from every edge, not just the offset side.
|
||||||
let (dx, dy) = (s.offset.0 * ctx.scale, s.offset.1 * ctx.scale);
|
let (dx, dy) = (s.offset.0 * ctx.scale, s.offset.1 * ctx.scale);
|
||||||
let pad = ((s.radius / 2.0 * ctx.scale * 3.0) + s.spread.abs() * ctx.scale + dx.abs().max(dy.abs())).ceil().clamp(0.0, 2048.0) as u32 + 2;
|
let pad =
|
||||||
|
((s.radius / 2.0 * ctx.scale * 3.0) + s.spread.abs() * ctx.scale + dx.abs().max(dy.abs())).ceil().clamp(0.0, 2048.0) as u32 + 2;
|
||||||
let Some(mut big) = Pixmap::new(lw.max(1) + pad * 2, lh.max(1) + pad * 2) else { continue };
|
let Some(mut big) = Pixmap::new(lw.max(1) + pad * 2, lh.max(1) + pad * 2) else { continue };
|
||||||
big.fill(tiny_skia::Color::WHITE);
|
big.fill(tiny_skia::Color::WHITE);
|
||||||
let ox = pad as f32 + dx as f32;
|
let ox = pad as f32 + dx as f32;
|
||||||
|
|
@ -387,7 +388,14 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||||
fx::morph(&mut big, (s.spread * ctx.scale).round() as i32);
|
fx::morph(&mut big, (s.spread * ctx.scale).round() as i32);
|
||||||
fx::blur(&mut big, s.radius / 2.0 * ctx.scale);
|
fx::blur(&mut big, s.radius / 2.0 * ctx.scale);
|
||||||
let Some(mut cropped) = Pixmap::new(lw.max(1), lh.max(1)) else { continue };
|
let Some(mut cropped) = Pixmap::new(lw.max(1), lh.max(1)) else { continue };
|
||||||
cropped.draw_pixmap(-(pad as i32), -(pad as i32), big.as_ref(), &PixmapPaint { blend_mode: tiny_skia::BlendMode::Source, ..Default::default() }, Transform::identity(), None);
|
cropped.draw_pixmap(
|
||||||
|
-(pad as i32),
|
||||||
|
-(pad as i32),
|
||||||
|
big.as_ref(),
|
||||||
|
&PixmapPaint { blend_mode: tiny_skia::BlendMode::Source, ..Default::default() },
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
let shifted = &mut cropped;
|
let shifted = &mut cropped;
|
||||||
fx::mask_by(shifted, &sa, false);
|
fx::mask_by(shifted, &sa, false);
|
||||||
fx::tint(shifted, [s.color.r, s.color.g, s.color.b, s.color.a]);
|
fx::tint(shifted, [s.color.r, s.color.g, s.color.b, s.color.a]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue