# Conflicts: # ROADMAP.md
This commit is contained in:
commit
d7b4d85d3c
File diff suppressed because one or more lines are too long
|
|
@ -640,6 +640,11 @@ fn trace_params(p: &Value) -> Result<tr::TraceParams> {
|
|||
serde_json::from_value(v).map_err(|e| bad("imageTrace.make", e.to_string()))
|
||||
}
|
||||
|
||||
/// The most anchor points an Image Trace may make: a photo traced at high fidelity can make
|
||||
/// millions, more than the canvas draws smoothly or a laptop has memory for, so the trace is
|
||||
/// refused with advice instead (#525).
|
||||
pub const MAX_TRACE_ANCHORS: usize = 2_000_000;
|
||||
|
||||
fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
||||
const C: &str = "imageTrace.make";
|
||||
let params = trace_params(p)?;
|
||||
|
|
@ -671,7 +676,7 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
|||
};
|
||||
let blob = s.doc()?.doc.images.get(&img.key).ok_or_else(|| EngineError::Other(format!("image data `{}` is missing", img.key)))?;
|
||||
let raster = tr::Raster::decode(&blob.bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let res = tr::trace(&raster, ¶ms);
|
||||
let res = tr::trace_within(&raster, ¶ms, MAX_TRACE_ANCHORS).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
// Pixel space of the decoded raster → the image object's pixel space → document.
|
||||
let sx = img.width.max(1) as f64 / raster.width.max(1) as f64;
|
||||
let sy = img.height.max(1) as f64 / raster.height.max(1) as f64;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ mod fit;
|
|||
mod mosaic;
|
||||
mod quantize;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
|
|
@ -41,8 +43,18 @@ pub enum TraceError {
|
|||
Decode(String),
|
||||
#[error("image is empty")]
|
||||
Empty,
|
||||
#[error("the image is {width} × {height} pixels, more than Image Trace takes ({max} megapixels)")]
|
||||
TooLarge { width: u32, height: u32, max: u64 },
|
||||
#[error(
|
||||
"this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset"
|
||||
)]
|
||||
TooComplex { max: usize },
|
||||
}
|
||||
|
||||
/// The most pixels an image to trace may have (64 megapixels: 256 MB decoded); larger images are
|
||||
/// refused instead of running out of memory.
|
||||
pub const MAX_PIXELS: u64 = 64 << 20;
|
||||
|
||||
/// An RGBA8 raster (row-major, top row first).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Raster {
|
||||
|
|
@ -67,13 +79,19 @@ impl Raster {
|
|||
}
|
||||
Self { width, height, rgba }
|
||||
}
|
||||
/// Decode PNG / JPEG / WebP / GIF bytes.
|
||||
/// Decode PNG / JPEG / WebP / GIF bytes (at most [`MAX_PIXELS`], checked before decoding).
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, TraceError> {
|
||||
let img = image::load_from_memory(bytes).map_err(|e| TraceError::Decode(e.to_string()))?.to_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
let decode_err = |e: image::ImageError| TraceError::Decode(e.to_string());
|
||||
let reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().map_err(|e| TraceError::Decode(e.to_string()))?;
|
||||
let (w, h) = reader.into_dimensions().map_err(decode_err)?;
|
||||
if w == 0 || h == 0 {
|
||||
return Err(TraceError::Empty);
|
||||
}
|
||||
if u64::from(w) * u64::from(h) > MAX_PIXELS {
|
||||
return Err(TraceError::TooLarge { width: w, height: h, max: MAX_PIXELS >> 20 });
|
||||
}
|
||||
let img = image::load_from_memory(bytes).map_err(decode_err)?.to_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
Ok(Self { width: w, height: h, rgba: img.into_raw() })
|
||||
}
|
||||
/// Encode as PNG.
|
||||
|
|
@ -262,17 +280,25 @@ fn is_white(c: [u8; 3]) -> bool {
|
|||
|
||||
/// Trace `img` with `params`.
|
||||
pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult {
|
||||
// No trace has more than `usize::MAX` anchors.
|
||||
trace_within(img, params, usize::MAX).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Trace `img` with `params`, giving up with [`TraceError::TooComplex`] once the paths have more
|
||||
/// than `max_anchors` anchor points (a photo traced at high fidelity can make millions). The
|
||||
/// colour layers are traced in parallel where threads are available.
|
||||
pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return TraceResult::default();
|
||||
if w == 0 || h == 0 || img.rgba.len() != w * h * 4 {
|
||||
return Ok(TraceResult::default());
|
||||
}
|
||||
let mut q = quantize(img, params);
|
||||
denoise(&mut q.labels, w, h, params.noise as usize);
|
||||
// Pixel count per palette entry.
|
||||
let mut counts = vec![0usize; q.palette.len()];
|
||||
for &l in &q.labels {
|
||||
if l != TRANSPARENT {
|
||||
counts[l as usize] += 1;
|
||||
if let Some(c) = counts.get_mut(l as usize) {
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
// Layers bottom → top: largest area first.
|
||||
|
|
@ -282,46 +308,117 @@ pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult {
|
|||
for (r, &i) in order.iter().enumerate() {
|
||||
rank[i] = r;
|
||||
}
|
||||
let opts = fit::FitOptions {
|
||||
polygon_tol: params.polygon_tolerance(),
|
||||
fit_tol: params.fit_tolerance(),
|
||||
corner_angle: params.corner_angle(),
|
||||
snap_lines: params.snap_curves_to_lines,
|
||||
let layers: Vec<(usize, usize)> =
|
||||
order.iter().copied().enumerate().filter(|&(_, ci)| !(params.ignore_white && is_white(q.palette[ci]))).collect();
|
||||
let job = Layers {
|
||||
q: &q,
|
||||
rank: &rank,
|
||||
params,
|
||||
opts: fit::FitOptions {
|
||||
polygon_tol: params.polygon_tolerance(),
|
||||
fit_tol: params.fit_tolerance(),
|
||||
corner_angle: params.corner_angle(),
|
||||
snap_lines: params.snap_curves_to_lines,
|
||||
},
|
||||
w,
|
||||
h,
|
||||
anchors: AtomicUsize::new(0),
|
||||
max_anchors,
|
||||
};
|
||||
let min_hole = params.noise.max(1) as i64;
|
||||
let mut out = TraceResult::default();
|
||||
let mut mask = vec![false; w * h];
|
||||
for (r, &ci) in order.iter().enumerate() {
|
||||
for (paths, &(_, ci)) in parallel_map(&layers, |&(r, ci)| job.layer(r, ci)).into_iter().zip(&layers) {
|
||||
out.paths.extend(paths.ok_or(TraceError::TooComplex { max: max_anchors })?);
|
||||
let color = q.palette[ci];
|
||||
if params.ignore_white && is_white(color) {
|
||||
continue;
|
||||
}
|
||||
let overlapping = params.method == Method::Overlapping;
|
||||
for (m, &l) in mask.iter_mut().zip(&q.labels) {
|
||||
*m = l != TRANSPARENT && if overlapping { rank[l as usize] >= r } else { l as usize == ci };
|
||||
}
|
||||
for comp in trace_mask(&mask, w, h) {
|
||||
let mut subs = Vec::with_capacity(1 + comp.holes.len());
|
||||
if let Some(sp) = fit::fit_loop(&comp.outer, &opts) {
|
||||
subs.push(sp);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
for hole in &comp.holes {
|
||||
if hole.area2.abs() / 2 < min_hole && params.noise > 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(sp) = fit::fit_loop(hole, &opts) {
|
||||
subs.push(sp);
|
||||
}
|
||||
}
|
||||
out.paths.push(TracedPath { path: PathData::new(subs), color, pixels: comp.pixels });
|
||||
}
|
||||
if !out.palette.contains(&color) {
|
||||
out.palette.push(color);
|
||||
}
|
||||
}
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// What tracing one colour layer needs.
|
||||
struct Layers<'a> {
|
||||
q: &'a Quantized,
|
||||
/// Each palette entry's layer, bottom (0) up.
|
||||
rank: &'a [usize],
|
||||
params: &'a TraceParams,
|
||||
opts: fit::FitOptions,
|
||||
w: usize,
|
||||
h: usize,
|
||||
/// Anchor points made so far, by every layer.
|
||||
anchors: AtomicUsize,
|
||||
max_anchors: usize,
|
||||
}
|
||||
|
||||
impl Layers<'_> {
|
||||
/// The paths of layer `r` (palette entry `ci`), or `None` once the anchors made pass the
|
||||
/// maximum.
|
||||
fn layer(&self, r: usize, ci: usize) -> Option<Vec<TracedPath>> {
|
||||
let (params, opts) = (self.params, &self.opts);
|
||||
let color = *self.q.palette.get(ci)?;
|
||||
let overlapping = params.method == Method::Overlapping;
|
||||
let mask: Vec<bool> = self
|
||||
.q
|
||||
.labels
|
||||
.iter()
|
||||
.map(|&l| l != TRANSPARENT && if overlapping { self.rank.get(l as usize).is_some_and(|&lr| lr >= r) } else { l as usize == ci })
|
||||
.collect();
|
||||
let min_hole = params.noise.max(1) as i64;
|
||||
let mut paths = vec![];
|
||||
for comp in trace_mask(&mask, self.w, self.h) {
|
||||
let Some(outer) = fit::fit_loop(&comp.outer, opts) else { continue };
|
||||
let mut subs = vec![outer];
|
||||
for hole in &comp.holes {
|
||||
if hole.area2.abs() / 2 < min_hole && params.noise > 0 {
|
||||
continue;
|
||||
}
|
||||
subs.extend(fit::fit_loop(hole, opts));
|
||||
}
|
||||
let path = PathData::new(subs);
|
||||
let n = path.anchor_count();
|
||||
if self.anchors.fetch_add(n, Ordering::Relaxed).saturating_add(n) > self.max_anchors {
|
||||
return None;
|
||||
}
|
||||
paths.push(TracedPath { path, color, pixels: comp.pixels });
|
||||
}
|
||||
Some(paths)
|
||||
}
|
||||
}
|
||||
|
||||
/// `f` of each of `items`, in order, spread over the available cores.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn parallel_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get()).min(items.len());
|
||||
if threads <= 1 {
|
||||
return items.iter().map(f).collect();
|
||||
}
|
||||
let next = AtomicUsize::new(0);
|
||||
let mut done: Vec<(usize, R)> = std::thread::scope(|s| {
|
||||
let workers: Vec<_> = (0..threads)
|
||||
.map(|_| {
|
||||
s.spawn(|| {
|
||||
let mut mine = vec![];
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(item) = items.get(i) else { break };
|
||||
mine.push((i, f(item)));
|
||||
}
|
||||
mine
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// A worker's panic goes on in this thread, as if it had run here (the engine's guard
|
||||
// reports it).
|
||||
workers.into_iter().flat_map(|w| w.join().unwrap_or_else(|e| std::panic::resume_unwind(e))).collect()
|
||||
});
|
||||
done.sort_by_key(|(i, _)| *i);
|
||||
done.into_iter().map(|(_, r)| r).collect()
|
||||
}
|
||||
|
||||
/// `f` of each of `items`, in order (no threads on the web).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn parallel_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
|
||||
items.iter().map(f).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -278,3 +278,73 @@ fn timing_1000x1000_bw_under_300ms() {
|
|||
assert!(!res.paths.is_empty());
|
||||
assert!(ms < 300.0, "{ms} ms");
|
||||
}
|
||||
|
||||
/// Stripes of `n` colours, each with a few dots of the next one: many layers, many small shapes.
|
||||
fn stripes(size: u32, n: u32) -> Raster {
|
||||
let colour = |i: u32| [(i * 37 % 256) as u8, (i * 91 % 256) as u8, (i * 53 % 256) as u8, 255];
|
||||
Raster::from_fn(size, size, |x, y| {
|
||||
let band = x * n / size;
|
||||
if (x % 9 < 3) && (y % 9 < 3) { colour(band + 1) } else { colour(band) }
|
||||
})
|
||||
}
|
||||
|
||||
/// #525: past the anchor budget the trace gives up with advice instead of making millions of
|
||||
/// anchors; under it, it is the plain trace.
|
||||
#[test]
|
||||
fn a_trace_past_its_anchor_budget_is_refused() {
|
||||
let img = stripes(120, 8);
|
||||
let p = TraceParams { mode: Mode::Color, colors: 16, noise: 1, ..TraceParams::default() };
|
||||
let full = trace(&img, &p);
|
||||
let anchors = full.anchor_count();
|
||||
assert!(anchors > 100, "{anchors} anchors");
|
||||
assert!(matches!(trace_within(&img, &p, anchors / 2), Err(TraceError::TooComplex { max }) if max == anchors / 2));
|
||||
assert_eq!(trace_within(&img, &p, anchors).unwrap(), full);
|
||||
}
|
||||
|
||||
/// Layers traced in parallel come out in the same order every time: bottom (largest) first.
|
||||
#[test]
|
||||
fn parallel_layers_keep_their_order() {
|
||||
let img = stripes(90, 12);
|
||||
let p = TraceParams { mode: Mode::Color, colors: 24, noise: 1, method: Method::Overlapping, ..TraceParams::default() };
|
||||
let a = trace(&img, &p);
|
||||
assert!(a.palette.len() > 8, "{} colours", a.palette.len());
|
||||
for _ in 0..4 {
|
||||
assert_eq!(trace(&img, &p), a);
|
||||
}
|
||||
// Overlapping: each layer's shapes cover the layers above, so the pixel counts fall upwards.
|
||||
let firsts: Vec<usize> = a.palette.iter().filter_map(|c| a.paths.iter().find(|t| t.color == *c).map(|t| t.pixels)).collect();
|
||||
assert!(firsts.windows(2).all(|w| w[0] >= w[1]), "{firsts:?}");
|
||||
}
|
||||
|
||||
/// A PNG whose header says `w` × `h` pixels, with no image data.
|
||||
fn png_header(w: u32, h: u32) -> Vec<u8> {
|
||||
fn crc(bytes: &[u8]) -> u32 {
|
||||
let mut c = !0u32;
|
||||
for &b in bytes {
|
||||
c ^= u32::from(b);
|
||||
for _ in 0..8 {
|
||||
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
|
||||
}
|
||||
}
|
||||
!c
|
||||
}
|
||||
let mut ihdr = b"IHDR".to_vec();
|
||||
ihdr.extend(w.to_be_bytes());
|
||||
ihdr.extend(h.to_be_bytes());
|
||||
ihdr.extend([8, 6, 0, 0, 0]);
|
||||
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||
for chunk in [ihdr, b"IDAT".to_vec(), b"IEND".to_vec()] {
|
||||
png.extend((chunk.len() as u32 - 4).to_be_bytes());
|
||||
png.extend(&chunk);
|
||||
png.extend(crc(&chunk).to_be_bytes());
|
||||
}
|
||||
png
|
||||
}
|
||||
|
||||
/// An image too large to trace is refused from its header, before it is decoded.
|
||||
#[test]
|
||||
fn an_image_too_large_to_trace_is_refused_before_decoding() {
|
||||
let err = Raster::decode(&png_header(30_000, 30_000)).unwrap_err();
|
||||
assert!(matches!(err, TraceError::TooLarge { width: 30_000, height: 30_000, .. }), "{err}");
|
||||
assert!(matches!(Raster::decode(&png_header(64, 64)), Err(TraceError::Decode(_))), "a small header goes on to decoding");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,27 @@ fn node_outline(n: &Node) -> BezPath {
|
|||
bp
|
||||
}
|
||||
|
||||
/// The most anchor points the selection's outlines and anchors, and the hover highlight, are drawn
|
||||
/// for: an Image Trace of a photo selects hundreds of thousands of paths at once, whose outlines
|
||||
/// would make a mesh larger than the GPU takes in one buffer (#525).
|
||||
const OVERLAY_MAX_ANCHORS: usize = 100_000;
|
||||
|
||||
/// Whether `nodes` have more than [`OVERLAY_MAX_ANCHORS`] anchor points to outline (counting
|
||||
/// stops there).
|
||||
fn too_many_anchors<'a>(nodes: impl IntoIterator<Item = &'a Node>) -> bool {
|
||||
fn over(n: &Node, left: &mut usize) -> bool {
|
||||
if let NodeKind::Path { path, .. } = &n.kind {
|
||||
match left.checked_sub(path.anchor_count()) {
|
||||
Some(l) => *left = l,
|
||||
None => return true,
|
||||
}
|
||||
}
|
||||
!matches!(n.kind, NodeKind::Envelope { .. }) && n.children().into_iter().flatten().any(|c| over(c, left))
|
||||
}
|
||||
let mut left = OVERLAY_MAX_ANCHORS;
|
||||
nodes.into_iter().any(|n| over(n, &mut left))
|
||||
}
|
||||
|
||||
/// [`Node::walk`] over what a selection highlight shows: an envelope's content is left out (the
|
||||
/// envelope shows its mesh instead).
|
||||
fn walk_drawn<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
|
||||
|
|
@ -1160,7 +1181,7 @@ fn hover_highlight(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
|||
if st.selection.contains(id) {
|
||||
return;
|
||||
}
|
||||
if let Some(n) = st.doc.node(id) {
|
||||
if let Some(n) = st.doc.node(id).filter(|n| !too_many_anchors([*n])) {
|
||||
let color = c32(st.doc.layer_color(id));
|
||||
stroke_path(p, &node_outline(n), xf, Stroke::new(1.5, color));
|
||||
}
|
||||
|
|
@ -1351,6 +1372,9 @@ fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
|||
&& app.ui.view.bounding_box
|
||||
&& app.session.active().is_some_and(|st| !st.selection.is_empty() && st.selection.anchors.is_empty());
|
||||
let bbox = if show_box { app.selection_box() } else { None };
|
||||
let st = app.session.active();
|
||||
let big = st.is_some_and(|st| too_many_anchors(st.selection.objects.iter().filter_map(|id| st.doc.node(*id))));
|
||||
let big_bounds = if big { app.selection_bounds() } else { None };
|
||||
let app = &*app;
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let tool = app.session.tool_id();
|
||||
|
|
@ -1363,7 +1387,11 @@ fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
|||
// The selected anchors' handles (drawn once they are counted: Show handles when multiple
|
||||
// anchors are selected off shows them for a single one only).
|
||||
let (mut handles, mut with_handles) = (vec![], 0);
|
||||
for id in &st.selection.objects {
|
||||
// Too many paths to outline: the selection's bounds stand for them.
|
||||
if let Some((b, id)) = big_bounds.zip(st.selection.objects.first()) {
|
||||
stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(1.0, c32(st.doc.layer_color(*id))));
|
||||
}
|
||||
for id in st.selection.objects.iter().filter(|_| !big) {
|
||||
let Some(n) = st.doc.node(*id) else { continue };
|
||||
let color = c32(st.doc.layer_color(*id));
|
||||
let partial = st.selection.partial(*id);
|
||||
|
|
@ -1633,11 +1661,13 @@ pub(crate) fn task_bar_rect(ctx: &egui::Context) -> Option<egui::Rect> {
|
|||
/// selection, pinned it stays put ([`crate::state::TaskBarPlace`]).
|
||||
fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.active() else { return };
|
||||
if st.selection.is_empty() || !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection") {
|
||||
if app.session.active().is_none_or(|st| st.selection.is_empty())
|
||||
|| !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(b) = st.doc.bounds_of(&st.selection.objects, true) else { return };
|
||||
let Some(b) = app.selection_bounds() else { return };
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let n = st.selection.len();
|
||||
let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned();
|
||||
let is_group = first.as_ref().is_some_and(|f| matches!(f.kind, NodeKind::Group { .. }));
|
||||
|
|
@ -2024,6 +2054,29 @@ mod tests {
|
|||
assert_eq!(cmd_drag(&mut app, inside, inside), "selection");
|
||||
}
|
||||
|
||||
/// #525: a selection with more anchors than the overlay draws (a traced photo) shows its bounds,
|
||||
/// not every outline and anchor, which made a mesh larger than the GPU takes; an ordinary one
|
||||
/// still shows its outline and anchors.
|
||||
#[test]
|
||||
fn a_huge_selection_shows_its_bounds_instead_of_every_outline() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let points = |n: usize| (0..n).map(|i| format!("L{} {}", 20.0 + 360.0 * i as f64 / n as f64, 100 + (i % 2) * 50)).collect::<String>();
|
||||
let ctx = egui::Context::default();
|
||||
let mut drawn = |n: usize| {
|
||||
app.session.execute("path.create", &json!({"d": format!("M20 20 {} Z", points(n))})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let shapes = shapes(&mut app, &ctx);
|
||||
let points: usize = shapes.iter().map(|s| if let Shape::Path(ps) = s { ps.points.len() } else { 0 }).sum();
|
||||
let squares = shapes.iter().filter(|s| matches!(s, Shape::Rect(_))).count();
|
||||
(points, squares)
|
||||
};
|
||||
let (points, squares) = drawn(OVERLAY_MAX_ANCHORS + 10);
|
||||
assert!(points < 100 && squares < 100, "{points} outline points, {squares} squares");
|
||||
let (points, squares) = drawn(200);
|
||||
assert!(points > 200 && squares > 200, "{points} outline points, {squares} squares");
|
||||
}
|
||||
|
||||
/// One headless canvas frame → the shapes drawn, `Shape::Vec`s flattened.
|
||||
fn shapes(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<Shape> {
|
||||
let (shapes, mut delta) = frame_output(app, ctx);
|
||||
|
|
|
|||
|
|
@ -3415,6 +3415,10 @@ axis Both Ambos
|
|||
@msg select a stroke with a variable width selecciona un trazo con anchura variable
|
||||
@msg select a symbol instance selecciona una instancia de símbolo
|
||||
@msg select an Image Trace object selecciona un objeto de calco de imagen
|
||||
@msg could not decode image: {0} no se pudo decodificar la imagen: {0}
|
||||
@msg image is empty la imagen está vacía
|
||||
@msg the image is {width} × {height} pixels, more than Image Trace takes ({max} megapixels) la imagen mide {width} × {height} píxeles, más de lo que admite Calco de imagen ({max} megapíxeles)
|
||||
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset este calco crearía más de {max} puntos de ancla, más de los que el documento puede mostrar con fluidez: sube Ruido, baja Trazados o Colores, o elige un preajuste de menor fidelidad
|
||||
@msg select an envelope selecciona una envolvente
|
||||
@msg select an image selecciona una imagen
|
||||
@msg select an object in perspective selecciona un objeto en perspectiva
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3415,6 +3415,10 @@ axis Both Entrambi
|
|||
@msg select a stroke with a variable width seleziona una traccia con larghezza variabile
|
||||
@msg select a symbol instance seleziona un'istanza di simbolo
|
||||
@msg select an Image Trace object seleziona un oggetto di ricalco immagine
|
||||
@msg could not decode image: {0} impossibile decodificare l'immagine: {0}
|
||||
@msg image is empty l'immagine è vuota
|
||||
@msg the image is {width} × {height} pixels, more than Image Trace takes ({max} megapixels) l'immagine misura {width} × {height} pixel, più di quanto accetti Ricalco immagine ({max} megapixel)
|
||||
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset questo ricalco creerebbe più di {max} punti di ancoraggio, più di quanti il documento possa mostrare in modo fluido: aumenta Disturbo, riduci Tracciati o Colori, oppure scegli un preset a fedeltà inferiore
|
||||
@msg select an envelope seleziona un involucro
|
||||
@msg select an image seleziona un'immagine
|
||||
@msg select an object in perspective seleziona un oggetto in prospettiva
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -711,7 +711,7 @@ fn complete_languages_translate_every_message() {
|
|||
const COMPLETE_MESSAGES: &[&str] = &["es", "it"];
|
||||
|
||||
/// Crates whose error and status messages reach the status bar.
|
||||
const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad"];
|
||||
const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace"];
|
||||
|
||||
/// Where a message literal starts: a status, an error value, a `thiserror` message.
|
||||
const MESSAGE_MARKERS: &[&str] = &[".status(", "ui.status = ", "status = ", "Other(", "Err(", "ok_or(", "ok_or_else(|| ", "#[error("];
|
||||
|
|
|
|||
|
|
@ -249,6 +249,8 @@ pub struct CanvasCache {
|
|||
pub print_tiling: Option<PrintTilingCache>,
|
||||
/// [`VectorcraftApp::selection_box`] for (document uid, revision, Use Preview Bounds).
|
||||
pub selection_box: Option<((u64, u64, bool), Option<vectorcraft_doc::OrientedBox>)>,
|
||||
/// [`VectorcraftApp::selection_bounds`] for (document uid, revision).
|
||||
pub selection_bounds: Option<((u64, u64), Option<vectorcraft_geom::Rect>)>,
|
||||
/// The tools' cursors as OS cursor bitmaps.
|
||||
pub cursors: cursors::Images,
|
||||
}
|
||||
|
|
@ -389,6 +391,7 @@ impl VectorcraftApp {
|
|||
slices: None,
|
||||
print_tiling: None,
|
||||
selection_box: None,
|
||||
selection_bounds: None,
|
||||
cursors: Default::default(),
|
||||
},
|
||||
perf: Perf::default(),
|
||||
|
|
@ -704,6 +707,22 @@ impl VectorcraftApp {
|
|||
self.canvas.selection_box = Some((key, b));
|
||||
b
|
||||
}
|
||||
|
||||
/// The selection's visual bounds (stroke and effects included), square to the page: measured
|
||||
/// once per revision, as the canvas reads it every frame (a traced photo selects a group of
|
||||
/// hundreds of thousands of paths).
|
||||
pub fn selection_bounds(&mut self) -> Option<vectorcraft_geom::Rect> {
|
||||
let st = self.session.active()?;
|
||||
let key = (st.uid, st.revision);
|
||||
if let Some((k, b)) = self.canvas.selection_bounds
|
||||
&& k == key
|
||||
{
|
||||
return b;
|
||||
}
|
||||
let b = st.doc.bounds_of(&st.selection.objects, true);
|
||||
self.canvas.selection_bounds = Some((key, b));
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// eframe isn't a dependency of this crate (the host owns the event loop); these entry points are
|
||||
|
|
|
|||
Loading…
Reference in New Issue