Repository: GraphiteEditor/Graphite
File: node-graph/gcore/src/vector/click_target.rs (Rust)

Code before:
	}

	pub fn target_type(&self) -> &ClickTargetType {
		&self.target_type
	}

	pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
		self.bounding_box
	}

	pub fn bounding_box_center(&self) -> Option<DVec2> {
		self.bounding_box.map(|bbox| bbox[0] + (bbox[1] - bbox[0]) / 2.)
	}

	pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
		match self.target_type {
			ClickTargetType::Subpath(ref subpath) =>
			{
			let read = self.bounding_box_cache.read().unwrap();
			if let Some((_, bounds)) = read.iter().find(|(t, _)| t.to_cols_array().iter().zip(transform.to_cols_array().iter()).all(|(a, b)|a.to_bits() == b.to_bits())){
				*bounds
			} else {
				std::mem::drop(read);
				let mut write = self.bounding_box_cache.write().unwrap();
				if write.len() >= 10 {
					write.clear();
				}
				let bounds = subpath.bounding_box_with_transform(transform);

				write.push((transform, bounds));
				bounds
			}
			}
			,
			// TODO: use point for calculation of bbox
			ClickTargetType::FreePoint(_) => self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]),
		}
	}

	pub fn apply_transform(&mut self, affine_transform: DAffine2) {
		match self.target_type {
			ClickTargetType::Subpath(ref mut subpath) => {
				subpath.apply_transform(affine_transform);
			}
			ClickTargetType::FreePoint(ref mut point) => {
				point.apply_transform(affine_transform);
			}
		}
		self.update_bbox();
	}


Review comment (performance):
Do we really want to clear the cache completely here?

Did you do performance tests on the number of max cache entries, could be interesting.

Code after:
		}
		// Check each position with matching fingerprint for exact rotation match
		while mask != 0 {
			let pos = mask.trailing_zeros() as usize;

			if rotation == self.elements[pos].0 {
				// Found cached rotation - apply scale and translation to cached bounds
				let transform = DAffine2::from_scale_angle_translation(scale, 0., translation);
				let new_bounds = self.elements[pos].1.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]);

				return Some(new_bounds);
			}
			mask &= !(1 << pos);
		}
		None
	}
	/// Computes and caches bounding box for the given rotation, then applies scale/translation.
	/// Returns the final transformed bounds.
	fn add_to_cache(&mut self, subpath: &Subpath<PointId>, rotation: f64, scale: DVec2, translation: DVec2, fingerprint: u8) -> BoundingBox {
		// Compute bounds for pure rotation (expensive operation we want to cache)
		let bounds = subpath.bounding_box_with_transform(DAffine2::from_angle(rotation));

		if bounds.is_none() {
			return bounds;
		}

		// Store in ring buffer at current write position
		let write_ptr = self.write_ptr;
		self.elements[write_ptr] = (rotation, bounds);

		// Update fingerprint byte for this position
		let mut bytes = self.fingerprints.to_le_bytes();
		bytes[write_ptr] = fingerprint;
		self.fingerprints = u64::from_le_bytes(bytes);

		// Advance write pointer (ring buffer behavior)
		self.write_ptr = (write_ptr + 1) % Self::CACHE_SIZE;

		// Apply scale and translation to cached rotated bounds
		let transform = DAffine2::from_scale_angle_translation(scale, 0., translation);
		bounds.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
	}
}

/// Represents a clickable target for the layer
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ClickTarget {
	target_type: ClickTargetType,
	stroke_width: f64,
	bounding_box: BoundingBox,
	#[serde(skip)]