mirror of
https://github.com/penpot/penpot.git
synced 2026-08-16 09:49:05 +00:00
490 lines
17 KiB
Rust
490 lines
17 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::VecDeque;
|
|
use std::iter;
|
|
|
|
use crate::performance;
|
|
use crate::shapes;
|
|
use crate::shapes::Shape;
|
|
use crate::uuid::Uuid;
|
|
|
|
use crate::shapes::StructureEntry;
|
|
use crate::skia;
|
|
|
|
use std::cell::OnceCell;
|
|
|
|
use crate::math;
|
|
use crate::math::bools as math_bools;
|
|
use crate::math::Matrix;
|
|
|
|
const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
|
|
|
|
/// A pool allocator for `Shape` objects that attempts to minimize memory reallocations.
|
|
///
|
|
/// `ShapesPoolImpl` pre-allocates a contiguous vector of `Shape` instances,
|
|
/// which can be reused and indexed efficiently. This design helps avoid
|
|
/// memory reallocation overhead by reserving enough space in advance.
|
|
///
|
|
/// # Memory Layout
|
|
///
|
|
/// Shapes are stored in a `Vec<Shape>`, which keeps the `Shape` instances
|
|
/// in a contiguous memory block.
|
|
///
|
|
/// # Index-based Design
|
|
///
|
|
/// All auxiliary HashMaps (modifiers, structure, scale_content, modified_shape_cache)
|
|
/// use `usize` indices instead of `&'a Uuid` references. This eliminates:
|
|
/// - Unsafe lifetime extensions
|
|
/// - The need for `rebuild_references()` after Vec reallocation
|
|
/// - Complex lifetime annotations
|
|
///
|
|
/// The `uuid_to_idx` HashMap maps `Uuid` (owned) to indices, avoiding lifetime issues.
|
|
///
|
|
pub struct ShapesPoolImpl {
|
|
shapes: Vec<Shape>,
|
|
counter: usize,
|
|
|
|
/// Maps UUID to index in the shapes Vec. Uses owned Uuid, no lifetime needed.
|
|
uuid_to_idx: HashMap<Uuid, usize>,
|
|
|
|
/// Cache for modified shapes, keyed by index
|
|
modified_shape_cache: HashMap<usize, OnceCell<Shape>>,
|
|
/// Transform modifiers, keyed by index
|
|
modifiers: HashMap<usize, skia::Matrix>,
|
|
/// UUIDs of shapes that have an active transform modifier, kept in sync
|
|
/// with `modifiers`. Stored explicitly so that `modifier_ids()` is O(K)
|
|
/// (K = number of modified shapes) instead of O(N_shapes) — avoids
|
|
/// building a full reverse-index HashMap on every call.
|
|
modifier_uuids: Vec<Uuid>,
|
|
/// Structure entries, keyed by index
|
|
structure: HashMap<usize, Vec<StructureEntry>>,
|
|
/// Scale content values, keyed by index
|
|
scale_content: HashMap<usize, f32>,
|
|
}
|
|
|
|
// Type aliases - no longer need lifetimes!
|
|
pub type ShapesPool = ShapesPoolImpl;
|
|
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl;
|
|
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl;
|
|
|
|
impl ShapesPoolImpl {
|
|
pub fn new() -> Self {
|
|
ShapesPoolImpl {
|
|
shapes: vec![],
|
|
counter: 0,
|
|
uuid_to_idx: HashMap::default(),
|
|
|
|
modified_shape_cache: HashMap::default(),
|
|
modifiers: HashMap::default(),
|
|
modifier_uuids: Vec::new(),
|
|
structure: HashMap::default(),
|
|
scale_content: HashMap::default(),
|
|
}
|
|
}
|
|
|
|
pub fn initialize(&mut self, capacity: usize) {
|
|
performance::begin_measure!("shapes_pool_initialize");
|
|
self.counter = 0;
|
|
self.uuid_to_idx = HashMap::with_capacity(capacity);
|
|
|
|
let additional = capacity as i32 - self.shapes.len() as i32;
|
|
if additional <= 0 {
|
|
return;
|
|
}
|
|
|
|
// Reserve extra capacity to avoid future reallocations
|
|
let target_capacity = (capacity as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
|
|
self.shapes
|
|
.reserve_exact(target_capacity.saturating_sub(self.shapes.len()));
|
|
|
|
self.shapes
|
|
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional as usize));
|
|
performance::end_measure!("shapes_pool_initialize");
|
|
}
|
|
|
|
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
|
|
if self.counter >= self.shapes.len() {
|
|
// We need more space
|
|
let current_capacity = self.shapes.capacity();
|
|
// Ensure we add at least 1 shape when the pool is empty
|
|
let additional =
|
|
((self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize).max(1);
|
|
let needed_capacity = self.shapes.len() + additional;
|
|
|
|
if needed_capacity > current_capacity {
|
|
// Reserve extra space to minimize future reallocations
|
|
let extra_reserve = (needed_capacity as f32 * 0.5) as usize;
|
|
self.shapes
|
|
.reserve(needed_capacity + extra_reserve - current_capacity);
|
|
}
|
|
|
|
self.shapes
|
|
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional));
|
|
}
|
|
|
|
let idx = self.counter;
|
|
let new_shape = &mut self.shapes[idx];
|
|
new_shape.id = id;
|
|
|
|
// Simply store the UUID -> index mapping. No unsafe lifetime tricks needed!
|
|
self.uuid_to_idx.insert(id, idx);
|
|
self.counter += 1;
|
|
|
|
&mut self.shapes[idx]
|
|
}
|
|
// No longer needed! Index-based storage means no references to rebuild.
|
|
// The old rebuild_references() function has been removed entirely.
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.uuid_to_idx.len()
|
|
}
|
|
|
|
pub fn has(&self, id: &Uuid) -> bool {
|
|
self.uuid_to_idx.contains_key(id)
|
|
}
|
|
|
|
pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut Shape> {
|
|
let idx = *self.uuid_to_idx.get(id)?;
|
|
Some(&mut self.shapes[idx])
|
|
}
|
|
|
|
/// Returns the current transform modifier matrix for the shape, if any.
|
|
pub fn get_modifier(&self, id: &Uuid) -> Option<&skia::Matrix> {
|
|
let idx = *self.uuid_to_idx.get(id)?;
|
|
self.modifiers.get(&idx)
|
|
}
|
|
|
|
/// Get a shape by UUID without applying modifiers/structure/scale-content.
|
|
pub fn get_raw(&self, id: &Uuid) -> Option<&Shape> {
|
|
let idx = *self.uuid_to_idx.get(id)?;
|
|
Some(&self.shapes[idx])
|
|
}
|
|
|
|
/// Get a shape by UUID. Returns the modified shape if modifiers/structure
|
|
/// are applied, otherwise returns the base shape.
|
|
pub fn get(&self, id: &Uuid) -> Option<&Shape> {
|
|
let idx = *self.uuid_to_idx.get(id)?;
|
|
|
|
let shape = &self.shapes[idx];
|
|
|
|
// Check if this shape needs modification (has modifiers, structure changes, or is a bool)
|
|
let needs_modification = shape.is_bool()
|
|
|| self.modifiers.contains_key(&idx)
|
|
|| self.structure.contains_key(&idx)
|
|
|| self.scale_content.contains_key(&idx);
|
|
|
|
if needs_modification {
|
|
// Check if we have a cached modified version
|
|
if let Some(cell) = self.modified_shape_cache.get(&idx) {
|
|
Some(cell.get_or_init(|| {
|
|
let mut modified_shape =
|
|
shape.transformed(self.modifiers.get(&idx), self.structure.get(&idx));
|
|
|
|
if self.to_update_bool(&modified_shape) {
|
|
math_bools::update_bool_to_path(&mut modified_shape, self);
|
|
}
|
|
|
|
if let Some(scale) = self.scale_content.get(&idx) {
|
|
modified_shape.scale_content(*scale);
|
|
}
|
|
modified_shape
|
|
}))
|
|
} else {
|
|
Some(shape)
|
|
}
|
|
} else {
|
|
if let Some(cell) = self.modified_shape_cache.get(&idx) {
|
|
return Some(cell.get_or_init(|| {
|
|
if let Some(m) = self.find_nearest_ancestor_modifier(idx) {
|
|
shape.transformed(Some(&m), None)
|
|
} else {
|
|
shape.clone()
|
|
}
|
|
}));
|
|
}
|
|
Some(shape)
|
|
}
|
|
}
|
|
|
|
// Given an id, returns the depth in the tree-shaped structure
|
|
// of shapes.
|
|
pub fn get_depth(&self, id: &Uuid) -> usize {
|
|
if id == &Uuid::nil() {
|
|
return 0;
|
|
}
|
|
|
|
let Some(idx) = self.uuid_to_idx.get(id) else {
|
|
return 0;
|
|
};
|
|
|
|
let shape = &self.shapes[*idx];
|
|
|
|
let Some(parent_id) = shape.parent_id else {
|
|
return 0;
|
|
};
|
|
|
|
self.get_depth(&parent_id) + 1
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn iter(&self) -> std::slice::Iter<'_, Shape> {
|
|
self.shapes.iter()
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Shape> {
|
|
self.shapes.iter_mut()
|
|
}
|
|
|
|
fn clean_shape_cache(&mut self) {
|
|
self.modified_shape_cache.clear()
|
|
}
|
|
|
|
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
|
let mut ids = Vec::<Uuid>::new();
|
|
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
|
|
|
for (uuid, matrix) in modifiers {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
modifiers_with_idx.insert(idx, matrix);
|
|
ids.push(uuid);
|
|
}
|
|
}
|
|
|
|
// Expand every root modifier to its full descendant subtree.
|
|
// When CLJS sends only root shapes (translation on drag), descendants
|
|
// need the same matrix.
|
|
// For resize/rotate, propagate-modifiers already includes all descendants.
|
|
// Descendants are NOT pushed into `ids` / `modifier_uuids`: tile invalidation
|
|
// via rebuild_modifier_tiles only runs for roots, which is sufficient because
|
|
// descendants always lie inside the parent's bounding box and are therefore
|
|
// covered by the parent's old/new tile ranges.
|
|
let root_pairs: Vec<(usize, skia::Matrix)> = ids
|
|
.iter()
|
|
.filter_map(|uuid| {
|
|
let idx = self.uuid_to_idx.get(uuid).copied()?;
|
|
let matrix = modifiers_with_idx.get(&idx).copied()?;
|
|
Some((idx, matrix))
|
|
})
|
|
.collect();
|
|
|
|
let mut descendants_idxs: Vec<usize> = Vec::new();
|
|
for (root_idx, matrix) in root_pairs {
|
|
for descendant_idx in self.collect_all_descendants(root_idx) {
|
|
if let std::collections::hash_map::Entry::Vacant(e) =
|
|
modifiers_with_idx.entry(descendant_idx)
|
|
{
|
|
e.insert(matrix);
|
|
descendants_idxs.push(descendant_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.modifiers = modifiers_with_idx;
|
|
|
|
for descendant_idx in descendants_idxs {
|
|
self.modified_shape_cache
|
|
.insert(descendant_idx, OnceCell::new());
|
|
}
|
|
|
|
// Compute ancestors before consuming `ids` so we can move it into
|
|
// `modifier_uuids` without a clone.
|
|
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
|
// rebuild_modifier_tiles doesn't process every descendant individually.
|
|
self.modifier_uuids = ids;
|
|
for uuid in all_ids {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
self.modified_shape_cache.insert(idx, OnceCell::new());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
|
|
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
|
// Initialize the cache cells for affected shapes
|
|
let mut structure_with_idx = HashMap::with_capacity(structure.len());
|
|
let mut ids = Vec::<Uuid>::new();
|
|
|
|
for (uuid, entries) in structure {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
structure_with_idx.insert(idx, entries);
|
|
ids.push(uuid);
|
|
}
|
|
}
|
|
self.structure = structure_with_idx;
|
|
|
|
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
|
for uuid in all_ids {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
self.modified_shape_cache.insert(idx, OnceCell::new());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_scale_content(&mut self, scale_content: HashMap<Uuid, f32>) {
|
|
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
|
// Initialize the cache cells for affected shapes
|
|
let mut scale_content_with_idx = HashMap::with_capacity(scale_content.len());
|
|
let mut ids = Vec::<Uuid>::new();
|
|
|
|
for (uuid, value) in scale_content {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
scale_content_with_idx.insert(idx, value);
|
|
ids.push(uuid);
|
|
}
|
|
}
|
|
self.scale_content = scale_content_with_idx;
|
|
|
|
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
|
for uuid in all_ids {
|
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
|
self.modified_shape_cache.insert(idx, OnceCell::new());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clears transient per-frame state (modifiers, structure, scale_content)
|
|
/// and returns the list of UUIDs that had a `modifier` applied at the
|
|
/// moment of cleaning. The caller can use that list to re-sync the tile
|
|
/// index / tile cache for those shapes: after cleaning their modifier is
|
|
/// gone, but if we don't touch their tiles they keep pointing at the
|
|
/// previous modified position and the tile texture cache may serve stale
|
|
/// pixels.
|
|
pub fn clean_all(&mut self) -> Vec<Uuid> {
|
|
self.clean_shape_cache();
|
|
|
|
// `modifier_uuids` is kept in sync with `modifiers` by `set_modifiers`,
|
|
// so we can take it directly — no need to rebuild a reverse index.
|
|
let modified_uuids = std::mem::take(&mut self.modifier_uuids);
|
|
|
|
self.modifiers = HashMap::default();
|
|
self.structure = HashMap::default();
|
|
self.scale_content = HashMap::default();
|
|
|
|
modified_uuids
|
|
}
|
|
|
|
/// UUIDs of all shapes that currently have a transform modifier.
|
|
/// Used by the throttled drag path so per-rAF tile invalidation can
|
|
/// be done once with the current modifier set instead of once per
|
|
/// pointer move.
|
|
///
|
|
/// Returns a reference to avoid allocation on every call — callers
|
|
/// inside hot render loops should hold this reference rather than
|
|
/// calling `modifier_ids()` repeatedly.
|
|
pub fn modifier_ids(&self) -> &[Uuid] {
|
|
&self.modifier_uuids
|
|
}
|
|
|
|
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
|
let Some(shape) = self.get(id) else {
|
|
panic!("Subtree not found");
|
|
};
|
|
|
|
let mut shapes = vec![];
|
|
let mut new_idx = 0;
|
|
let mut uuid_to_idx = HashMap::default();
|
|
|
|
for child_id in shape.all_children_iter(self, true, true) {
|
|
let Some(child_shape) = self.get(&child_id) else {
|
|
panic!("Not found");
|
|
};
|
|
shapes.push(child_shape.clone());
|
|
uuid_to_idx.insert(child_id, new_idx);
|
|
new_idx += 1;
|
|
}
|
|
|
|
ShapesPoolImpl {
|
|
shapes,
|
|
counter: new_idx,
|
|
uuid_to_idx,
|
|
modified_shape_cache: HashMap::default(),
|
|
modifiers: HashMap::default(),
|
|
modifier_uuids: Vec::new(),
|
|
structure: HashMap::default(),
|
|
scale_content: HashMap::default(),
|
|
}
|
|
}
|
|
|
|
fn collect_all_descendants(&self, idx: usize) -> Vec<usize> {
|
|
let mut result = Vec::new();
|
|
let mut queue: VecDeque<&Uuid> = VecDeque::new();
|
|
let shape = &self.shapes[idx];
|
|
for child_id in shape.children_ids_iter(false) {
|
|
queue.push_back(child_id);
|
|
}
|
|
while let Some(child_id) = queue.pop_front() {
|
|
if let Some(&child_idx) = self.uuid_to_idx.get(child_id) {
|
|
result.push(child_idx);
|
|
let child_shape = &self.shapes[child_idx];
|
|
for grandchild_id in child_shape.children_ids_iter(false) {
|
|
queue.push_back(grandchild_id);
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn find_nearest_ancestor_modifier(&self, idx: usize) -> Option<Matrix> {
|
|
let mut current_idx = idx;
|
|
loop {
|
|
let shape = &self.shapes[current_idx];
|
|
let parent_id = shape.parent_id?;
|
|
if parent_id == Uuid::nil() {
|
|
return None;
|
|
}
|
|
let &parent_idx = self.uuid_to_idx.get(&parent_id)?;
|
|
if let Some(matrix) = self.modifiers.get(&parent_idx) {
|
|
return Some(*matrix);
|
|
}
|
|
current_idx = parent_idx;
|
|
}
|
|
}
|
|
|
|
fn to_update_bool(&self, shape: &Shape) -> bool {
|
|
if !shape.is_bool() {
|
|
return false;
|
|
}
|
|
|
|
let default = &Matrix::default();
|
|
|
|
// Get parent modifier by index
|
|
let parent_idx = self.uuid_to_idx.get(&shape.id);
|
|
let parent_modifier = parent_idx
|
|
.and_then(|idx| self.modifiers.get(idx))
|
|
.unwrap_or(default);
|
|
|
|
// Returns true if the transform of any child is different to the parent's
|
|
shape.all_children_iter(self, true, false).any(|child_id| {
|
|
let child_modifier = self
|
|
.uuid_to_idx
|
|
.get(&child_id)
|
|
.and_then(|idx| self.modifiers.get(idx))
|
|
.unwrap_or(default);
|
|
!math::is_close_matrix(parent_modifier, child_modifier)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for ShapesPoolImpl {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Clone for ShapesPoolImpl {
|
|
fn clone(&self) -> Self {
|
|
ShapesPoolImpl {
|
|
shapes: self.shapes.clone(),
|
|
counter: self.counter,
|
|
uuid_to_idx: self.uuid_to_idx.clone(),
|
|
// The modified_shape_cache is a derived/computed cache; reset it on clone
|
|
// so it gets lazily rebuilt on demand rather than cloning OnceCell state.
|
|
modified_shape_cache: HashMap::default(),
|
|
modifiers: self.modifiers.clone(),
|
|
modifier_uuids: self.modifier_uuids.clone(),
|
|
structure: self.structure.clone(),
|
|
scale_content: self.scale_content.clone(),
|
|
}
|
|
}
|
|
}
|