diff --git a/crates/cad/src/import/reader.rs b/crates/cad/src/import/reader.rs index 139f35771..8733c96c8 100644 --- a/crates/cad/src/import/reader.rs +++ b/crates/cad/src/import/reader.rs @@ -439,13 +439,15 @@ fn layouts(objects: &[Record<'_>], block_records: &HashMap, enti continue; } let Some(block) = block else { continue }; - if !seen.insert(name.to_ascii_lowercase()) { + let key = name.to_ascii_lowercase(); + let cost = name.len() + block.len(); + // A layout past the allowance is skipped alone: a later short one is still offered, and a + // skipped one reserves neither its name nor its bytes. + if seen.contains(&key) || name_bytes.saturating_add(cost) > MAX_LAYOUT_NAME_BYTES { continue; } - name_bytes = name_bytes.saturating_add(name.len() + block.len()); - if name_bytes > MAX_LAYOUT_NAME_BYTES { - break; - } + seen.insert(key); + name_bytes += cost; paper.push((own.int_or(71, i64::MAX), LayoutDef { name: name.to_string(), block: Some(block.clone()) })); } paper.sort_by_key(|(order, _)| *order); diff --git a/crates/color/src/gradient.rs b/crates/color/src/gradient.rs index 4af32cf85..f23eb18e6 100644 --- a/crates/color/src/gradient.rs +++ b/crates/color/src/gradient.rs @@ -213,6 +213,10 @@ impl Gradient { /// the one before it stopped, so the work is the samples plus the stops, not their product. pub fn sample_increasing_with(&self, ts: impl IntoIterator, mix: impl Fn(&Color, &Color, f32) -> Color) -> Vec<(Color, f32)> { let stops = &self.stops; + // The shared index assumes ordered stops; others (a file's, unsorted) sample one by one. + if stops.windows(2).any(|w| w[1].offset < w[0].offset) { + return ts.into_iter().map(|t| self.sample_with(t, &mix)).collect(); + } let mut j = 0; ts.into_iter() .map(|t| { @@ -241,6 +245,8 @@ impl Gradient { /// [`Self::expanded_stops`], each stop that stands for a midpoint with that midpoint (`None` /// on the gradient's own stops and a Perceptual gradient's other in-between stops). pub fn expanded(&self) -> impl Iterator)> + '_ { + // Direct interpolation matches `sample` only for strictly increasing offsets. + let increasing = self.stops.windows(2).all(|w| w[0].offset < w[1].offset); self.stops.iter().enumerate().flat_map(move |(i, s)| { let next = self.stops.get(i + 1); let off_centre = (s.midpoint - 0.5).abs() > 1e-3; @@ -249,7 +255,8 @@ impl Gradient { let t = s.offset + (n.offset - s.offset) * s.midpoint; // Mixed from the two stops it lies between, as `sample` would: searching all the // stops again for each midpoint made many stops cost their square. - let (c, o) = between(s, n, t, &|a: &Color, b: &Color, v| self.method.mix(a, b, v)); + let mix = |a: &Color, b: &Color, v| self.method.mix(a, b, v); + let (c, o) = if increasing { between(s, n, t, &mix) } else { self.sample_with(t, mix) }; (t, c, o, Some(s.midpoint)) }); // Perceptual: stops along the mix (`v`) where a straight line would stray from it, the diff --git a/crates/tools/src/distort/liquify.rs b/crates/tools/src/distort/liquify.rs index b69c2e348..9f3583e2a 100644 --- a/crates/tools/src/distort/liquify.rs +++ b/crates/tools/src/distort/liquify.rs @@ -841,6 +841,10 @@ 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![] }; let room = crate::MAX_STROKE_SAMPLES.saturating_sub(self.points.len()); + // A full stroke takes no sample: the preview it already has stands. + if room == 0 { + return vec![]; + } self.points.extend(std::iter::repeat_n(last, (n as usize).min(room))); self.preview(cx) } diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index efd9c7ab8..e9dca9f6b 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -56,6 +56,9 @@ pub struct GestureTool { shift_raw: Vec, /// Where the pointer was for the Shift-constrained point the next [`Self::push`] adds. shift_next: Option, + /// The angle of the line the dragged points were last moved onto: while it holds they are on + /// it, and the next drag event need not move them again. + shift_line: Option, /// "Merge only with selection": a Blob Brush stroke merges only with selected paths. pub merge_only_selection: bool, /// "Edit selected paths within N pixels". @@ -98,6 +101,7 @@ impl GestureTool { straight: false, shift_raw: vec![], shift_next: None, + shift_line: None, merge_only_selection: false, edit_within: 12.0, close_within: 15.0, @@ -143,6 +147,7 @@ impl GestureTool { } let Some(&start) = self.points.first().filter(|_| self.id == "blobBrush" && m.shift) else { self.shift_raw.clear(); + self.shift_line = None; return p; }; self.shift_next = Some(p); @@ -154,8 +159,11 @@ impl GestureTool { // `line` is the line's angle, measured counterclockwise on screen as the Constrain Angle is measured. let line = (-d.y).atan2(d.x).to_degrees(); let on_line = |q: Point| start + vectorcraft_geom::constrain_angle_from(q - start, 180.0, line); - for (q, raw) in self.points.iter_mut().skip(1).rev().zip(self.shift_raw.iter().rev()) { - *q = on_line(*raw); + if self.shift_line != Some(line) { + for (q, raw) in self.points.iter_mut().skip(1).rev().zip(self.shift_raw.iter().rev()) { + *q = on_line(*raw); + } + self.shift_line = Some(line); } on_line(p) } @@ -165,6 +173,7 @@ impl GestureTool { self.points.clear(); self.pressures.clear(); self.previewed = 0; + self.shift_line = None; } /// The stroke's points, each `[x, y, pressure]` when a pen pressed less than fully anywhere