mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Security: linear work for shortcut review, gradients, recovery names, include cycles, batch references, DOCX style ids, CAD layouts and long strokes
CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185: - Shortcut import review: example names for each code in one pass over the rows. - Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its increasing positions with one advancing search (`Gradient::sample_increasing_with`). - Recovery: image entry names are looked up by address, and dead ones swept when the map doubles, not on every image. - Script checker: include cycles from strongly connected components (iterative Tarjan), not a reachability row per file. - Batch step references copy at most 256 MB of values for one step. - DOCX: style ids over 256 bytes are ignored where defined and where used. - DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit. - Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep at most 100,000 samples, and the stroke kept is previewed in full before it is committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
bc56851904
commit
20bba9ebae
@@ -667,12 +667,14 @@ pub struct LiquifyTool {
|
||||
hover: Option<Point>,
|
||||
/// Time held still since the last sample or repeat (seconds).
|
||||
held: f64,
|
||||
/// How many samples the last preview of the stroke had.
|
||||
previewed: usize,
|
||||
}
|
||||
|
||||
impl LiquifyTool {
|
||||
pub fn new(id: &str) -> Self {
|
||||
let kind = LiquifyKind::parse(id).unwrap_or(LiquifyKind::Warp);
|
||||
Self { params: LiquifyParams::new(kind), show_brush: true, points: vec![], active: false, sizing: None, hover: None, held: 0.0 }
|
||||
Self { params: LiquifyParams::new(kind), show_brush: true, points: vec![], active: false, sizing: None, hover: None, held: 0.0, previewed: 0 }
|
||||
}
|
||||
|
||||
/// The command + params for the stroke so far (with each sample's pressure while Use Pressure
|
||||
@@ -687,6 +689,26 @@ impl LiquifyTool {
|
||||
("object.liquify".into(), v)
|
||||
}
|
||||
|
||||
/// The preview of the stroke so far, when [`crate::previews_at`] its length.
|
||||
fn preview(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
if !crate::previews_at(self.points.len()) {
|
||||
return vec![];
|
||||
}
|
||||
self.previewed = self.points.len();
|
||||
let (c, v) = self.command(cx);
|
||||
vec![Action::Preview(c, v)]
|
||||
}
|
||||
|
||||
/// The preview of the whole stroke, when samples came after the last one shown: what is
|
||||
/// committed is the last preview.
|
||||
fn stale_preview(&mut self, cx: &ToolContext) -> Option<Action> {
|
||||
(self.previewed != self.points.len() && !self.points.is_empty()).then(|| {
|
||||
self.previewed = self.points.len();
|
||||
let (c, v) = self.command(cx);
|
||||
Action::Preview(c, v)
|
||||
})
|
||||
}
|
||||
|
||||
/// Alt-drag from `s` to `p`: the brush grows or shrinks from its size at the press (its edge
|
||||
/// follows the pointer); with Shift it keeps its proportions.
|
||||
fn resize(&mut self, s: Sizing, p: Point, proportional: bool) {
|
||||
@@ -720,6 +742,7 @@ impl Tool for LiquifyTool {
|
||||
self.points = vec![sample];
|
||||
self.active = true;
|
||||
self.held = 0.0;
|
||||
self.previewed = self.points.len();
|
||||
let (c, v) = self.command(cx);
|
||||
vec![Action::Begin(self.params.kind.label().into()), Action::Preview(c, v)]
|
||||
}
|
||||
@@ -729,13 +752,15 @@ impl Tool for LiquifyTool {
|
||||
self.hover = Some(s.start);
|
||||
return vec![];
|
||||
}
|
||||
if !self.active || self.points.last().is_some_and(|(l, _)| l.distance(p) < cx.tol(2.0)) {
|
||||
if !self.active
|
||||
|| self.points.last().is_some_and(|(l, _)| l.distance(p) < cx.tol(2.0))
|
||||
|| self.points.len() >= crate::MAX_STROKE_SAMPLES
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
self.points.push(sample);
|
||||
self.held = 0.0;
|
||||
let (c, v) = self.command(cx);
|
||||
vec![Action::Preview(c, v)]
|
||||
self.preview(cx)
|
||||
}
|
||||
PointerKind::Up => {
|
||||
if self.sizing.take().is_some() {
|
||||
@@ -745,8 +770,9 @@ impl Tool for LiquifyTool {
|
||||
return vec![];
|
||||
}
|
||||
self.active = false;
|
||||
let out = self.stale_preview(cx).into_iter().chain([Action::Commit]).collect();
|
||||
self.points.clear();
|
||||
vec![Action::Commit]
|
||||
out
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
@@ -790,10 +816,15 @@ impl Tool for LiquifyTool {
|
||||
fn busy(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.hover = None;
|
||||
self.sizing = None;
|
||||
if std::mem::take(&mut self.active) { vec![Action::Commit] } else { vec![] }
|
||||
if !std::mem::take(&mut self.active) {
|
||||
return vec![];
|
||||
}
|
||||
let out = self.stale_preview(cx).into_iter().chain([Action::Commit]).collect();
|
||||
self.points.clear();
|
||||
out
|
||||
}
|
||||
/// Held still: every [`HOLD_EVERY`] the last sample repeats (one more dab there).
|
||||
fn tick(&mut self, cx: &ToolContext, dt: f64) -> Vec<Action> {
|
||||
@@ -809,9 +840,9 @@ impl Tool for LiquifyTool {
|
||||
}
|
||||
self.held = self.held.clamp(0.0, HOLD_EVERY);
|
||||
let Some(&last) = self.points.last().filter(|_| n > 0) else { return vec![] };
|
||||
self.points.extend(std::iter::repeat_n(last, n as usize));
|
||||
let (c, v) = self.command(cx);
|
||||
vec![Action::Preview(c, v)]
|
||||
let room = crate::MAX_STROKE_SAMPLES.saturating_sub(self.points.len());
|
||||
self.points.extend(std::iter::repeat_n(last, (n as usize).min(room)));
|
||||
self.preview(cx)
|
||||
}
|
||||
fn wants_ticks(&self) -> bool {
|
||||
self.active && self.params.kind.holds()
|
||||
|
||||
@@ -21,6 +21,8 @@ use crate::guides::DrawSnap;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
pub struct GestureTool {
|
||||
/// How many points the last preview of the stroke had ([`crate::previews_at`]).
|
||||
previewed: usize,
|
||||
id: &'static str,
|
||||
points: Vec<Point>,
|
||||
/// The pen pressure of each of `points` (the Paintbrush only).
|
||||
@@ -79,6 +81,7 @@ impl GestureTool {
|
||||
let fidelity = if id == "smooth" { 2.5 } else { 1.5 };
|
||||
Self {
|
||||
id,
|
||||
previewed: 0,
|
||||
points: vec![],
|
||||
pressures: vec![],
|
||||
active: false,
|
||||
@@ -161,6 +164,7 @@ impl GestureTool {
|
||||
fn clear(&mut self) {
|
||||
self.points.clear();
|
||||
self.pressures.clear();
|
||||
self.previewed = 0;
|
||||
}
|
||||
|
||||
/// The stroke's points, each `[x, y, pressure]` when a pen pressed less than fully anywhere
|
||||
@@ -340,7 +344,7 @@ impl Tool for GestureTool {
|
||||
return if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] };
|
||||
}
|
||||
self.points.truncate(1);
|
||||
} else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) {
|
||||
} else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) || self.points.len() >= crate::MAX_STROKE_SAMPLES {
|
||||
return vec![];
|
||||
}
|
||||
self.push(p, ev.pressure);
|
||||
@@ -352,7 +356,10 @@ impl Tool for GestureTool {
|
||||
self.began = true;
|
||||
out.push(Action::Begin(self.label().into()));
|
||||
}
|
||||
out.push(Action::Preview("path.freehand".into(), self.freehand_params(false)));
|
||||
if crate::previews_at(self.points.len()) {
|
||||
self.previewed = self.points.len();
|
||||
out.push(Action::Preview("path.freehand".into(), self.freehand_params(false)));
|
||||
}
|
||||
out
|
||||
}
|
||||
PointerKind::Up => {
|
||||
@@ -422,12 +429,15 @@ impl Tool for GestureTool {
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
let began = self.began;
|
||||
// Drags past the last preview are in the stroke kept, too.
|
||||
let stale = (began && self.previewed != self.points.len() && self.draws_path() && !self.smoothing)
|
||||
.then(|| Action::Preview("path.freehand".into(), self.freehand_params(false)));
|
||||
self.snap.clear();
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.straight = false;
|
||||
self.clear();
|
||||
if began { vec![Action::Commit] } else { vec![] }
|
||||
if began { stale.into_iter().chain([Action::Commit]).collect() } else { vec![] }
|
||||
}
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let mut o = self.snap.guides().to_vec();
|
||||
|
||||
@@ -887,6 +887,20 @@ impl Tool for NoopTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The most samples a drawn stroke (freehand, Liquify) keeps: later drags add nothing to it.
|
||||
pub(crate) const MAX_STROKE_SAMPLES: usize = 100_000;
|
||||
|
||||
/// Samples up to which every drag previews the whole stroke; past it, a drag previews it every
|
||||
/// `n / PREVIEW_ALL_UNTIL` samples. Each preview carries every sample so far, so previewing at
|
||||
/// every sample cost the square of the stroke's length (a gesture of many drags sent at once
|
||||
/// spent seconds); this way it is about `PREVIEW_ALL_UNTIL` per sample.
|
||||
const PREVIEW_ALL_UNTIL: usize = 1000;
|
||||
|
||||
/// Whether a drag that made the stroke `n` samples long previews it ([`PREVIEW_ALL_UNTIL`]).
|
||||
pub(crate) fn previews_at(n: usize) -> bool {
|
||||
n <= PREVIEW_ALL_UNTIL || n.is_multiple_of(n / PREVIEW_ALL_UNTIL)
|
||||
}
|
||||
|
||||
pub(crate) fn json_ids(ids: &[NodeId]) -> Value {
|
||||
Value::Array(ids.iter().map(|i| Value::from(i.0)).collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user