mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 12:13:19 +00:00
Merge pull request #5972 from dotCooCoo/fix/selection-overlay-per-frame
Canvas: selection outlines and anchors are reused between frames
This commit is contained in:
@@ -80,6 +80,7 @@ impl VectorcraftApp {
|
||||
print_tiling: None,
|
||||
live_paint_gaps: None,
|
||||
hidden_chars: Default::default(),
|
||||
selection_marks: Default::default(),
|
||||
selection_box: None,
|
||||
selection_bounds: None,
|
||||
content_bounds: None,
|
||||
|
||||
@@ -41,6 +41,9 @@ pub struct CanvasCache {
|
||||
/// The marks Type → Show Hidden Characters drew on each text object in the last frame. A frame
|
||||
/// that does not draw them drops them ([`canvas::show`]).
|
||||
pub hidden_chars: canvas::HiddenChars,
|
||||
/// The selected objects' outlines, anchors and handles as drawn in the last frame. A frame that
|
||||
/// does not draw them drops them ([`canvas::show`]).
|
||||
pub selection_marks: canvas::SelectionMarks,
|
||||
/// [`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).
|
||||
|
||||
@@ -28,6 +28,7 @@ mod outline;
|
||||
mod overlays;
|
||||
mod panes;
|
||||
mod raster;
|
||||
mod selection_marks;
|
||||
mod task_bar;
|
||||
mod tool_overlays;
|
||||
mod view;
|
||||
@@ -65,6 +66,7 @@ pub use panes::Panes;
|
||||
pub(crate) use panes::{consolidate, pane, tile, tiled_area};
|
||||
use raster::{changed_pixels, pixel_region, upload};
|
||||
pub(crate) use raster::{color_image, color_image_owned};
|
||||
pub use selection_marks::SelectionMarks;
|
||||
use task_bar::task_bar;
|
||||
pub(crate) use task_bar::{has_colors, task_bar_rect};
|
||||
pub use tool_overlays::HiddenChars;
|
||||
@@ -193,6 +195,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// that does not draw the overlay drops them, for example with the option off, with the edges
|
||||
// hidden, on the Home screen or with no document open.
|
||||
let hidden_chars = std::mem::take(&mut app.canvas.hidden_chars);
|
||||
// The selection's kept marks. Likewise, a frame that does not draw them drops them.
|
||||
let selection_marks = std::mem::take(&mut app.canvas.selection_marks);
|
||||
// A release ends the Image Trace panel's eye-button hold, also when the panel is no longer
|
||||
// drawn, so a later press elsewhere does not show the source image.
|
||||
if app.ui.trace_hold_source.is_some() && !ui.input(|i| i.pointer.any_down()) {
|
||||
@@ -336,7 +340,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
original_path_overlay(app, &painter, &xf);
|
||||
|
||||
let overlays = selection_visuals(app, &painter, &xf, (hidden_chars, hover_still), revolve_edit, view_info);
|
||||
let overlays = selection_visuals(app, &painter, &xf, (hidden_chars, selection_marks, hover_still), revolve_edit, view_info);
|
||||
draw_overlays(&painter, &xf, &overlays, &t, HandleLook::of(&app.session.prefs));
|
||||
if !revolve_edit {
|
||||
shaper_widget_overlay(app, &painter, &xf);
|
||||
@@ -894,7 +898,7 @@ fn selection_visuals(
|
||||
app: &mut VectorcraftApp,
|
||||
painter: &egui::Painter,
|
||||
xf: &Xf,
|
||||
(hidden_chars, hover_still): (HiddenChars, bool),
|
||||
(hidden_chars, mut marks, hover_still): (HiddenChars, SelectionMarks, bool),
|
||||
revolve_edit: bool,
|
||||
view_info: vectorcraft_engine::ViewInfo,
|
||||
) -> Vec<Overlay> {
|
||||
@@ -910,7 +914,8 @@ fn selection_visuals(
|
||||
if hover_still {
|
||||
hover_highlight(app, painter, xf);
|
||||
}
|
||||
selection_overlay(app, painter, xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. })));
|
||||
selection_overlay(app, painter, xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. })), &mut marks);
|
||||
app.canvas.selection_marks = marks;
|
||||
draw_inside_overlay(app, painter, xf);
|
||||
mirror_axis_overlay(app, painter, xf);
|
||||
grid_repeat_overlay(app, painter, xf);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::artboards::names_an_artboard;
|
||||
use super::input::marquee_through;
|
||||
use super::outline::{KEY_OUTLINE, c32, node_outline, selection_hit_at, stroke_path, to_screen_path, too_many_anchors, walk_drawn};
|
||||
use super::{Color32, CornerRadius, NodeKind, Overlay, Point, Pos2, Shape, Stroke, StrokeKind, Tokens, VectorcraftApp, Xf, vec2};
|
||||
use super::outline::{KEY_OUTLINE, c32, node_outline, selection_hit_at, stroke_path, to_screen_path, too_many_anchors};
|
||||
use super::selection_marks::{MarkOptions, SelectionMarks};
|
||||
use super::{Color32, CornerRadius, NodeKind, Overlay, Pos2, Shape, Stroke, StrokeKind, Tokens, VectorcraftApp, Xf, vec2};
|
||||
|
||||
pub(super) fn hover_highlight(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(h) = app.hover_doc else { return };
|
||||
@@ -54,9 +55,9 @@ pub(super) fn selected_anchor(c: Color32) -> Color32 {
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct HandleLook<'a> {
|
||||
/// Points bigger (or smaller) than the default size 3, per step of Size (1–7).
|
||||
grow: f32,
|
||||
pub(super) grow: f32,
|
||||
/// `solid` (the default), `hollow` or `large`.
|
||||
style: &'a str,
|
||||
pub(super) style: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> HandleLook<'a> {
|
||||
@@ -65,17 +66,21 @@ impl<'a> HandleLook<'a> {
|
||||
}
|
||||
|
||||
/// A direction handle's end at `c`: a solid dot, a hollow one or a larger solid one.
|
||||
fn draw(self, p: &egui::Painter, c: Pos2, color: Color32) {
|
||||
pub(super) fn shapes(self, c: Pos2, color: Color32) -> [Option<Shape>; 2] {
|
||||
let r = 2.75 + self.grow / 2.0;
|
||||
let hollow = self.style == "hollow";
|
||||
p.circle_filled(c, if self.style == "large" { r + 1.5 } else { r }, if hollow { Color32::WHITE } else { color });
|
||||
if hollow {
|
||||
p.circle_stroke(c, r, Stroke::new(1.0, color));
|
||||
let dot = Shape::circle_filled(c, if self.style == "large" { r + 1.5 } else { r }, if hollow { Color32::WHITE } else { color });
|
||||
[Some(dot), hollow.then(|| Shape::circle_stroke(c, r, Stroke::new(1.0, color)))]
|
||||
}
|
||||
|
||||
pub(super) fn draw(self, p: &egui::Painter, c: Pos2, color: Color32) {
|
||||
for shape in self.shapes(c, color).into_iter().flatten() {
|
||||
p.add(shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) {
|
||||
pub(super) fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) {
|
||||
let r = egui::Rect::from_center_size(c, vec2(size, size));
|
||||
if filled {
|
||||
p.rect_filled(r, 0.0, color);
|
||||
@@ -85,11 +90,23 @@ fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size:
|
||||
}
|
||||
}
|
||||
|
||||
fn curve_point(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) {
|
||||
pub(super) fn curve_point(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) {
|
||||
p.circle(c, size / 2.0, if filled { color } else { Color32::WHITE }, Stroke::new(1.0, color));
|
||||
}
|
||||
|
||||
pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool) {
|
||||
/// How the current tool and preferences draw the selection's marks.
|
||||
pub(super) fn mark_options(app: &VectorcraftApp, curve_points_from_tool: bool) -> MarkOptions<'_> {
|
||||
let tool = app.session.tool_id();
|
||||
MarkOptions {
|
||||
direct: vectorcraft_tools::catalog::edits_anchors(tool),
|
||||
curvature: tool == "curvature",
|
||||
curve_points_from_tool,
|
||||
look: HandleLook::of(&app.session.prefs),
|
||||
all_handles: app.session.prefs.show_handles_multiple_anchors,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool, marks: &mut SelectionMarks) {
|
||||
// The Selection tool's bounding box (rotated with the objects after a rotation), also shown by
|
||||
// the shape tools that keep a shape just drawn ready to adjust while they draw none (#5477). It
|
||||
// hides while a drag moves or resizes the selection, so only the art is seen going with the
|
||||
@@ -121,130 +138,29 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf:
|
||||
let symbols = st.doc.symbol_boxes();
|
||||
let tool = app.session.tool_id();
|
||||
let direct = vectorcraft_tools::catalog::edits_anchors(tool);
|
||||
let curvature = tool == "curvature";
|
||||
// Selection & Anchor Display › Size (1–7, 3 the default): anchors, handles and the bounding
|
||||
// box's handles a point bigger or smaller per step.
|
||||
let look = HandleLook::of(&app.session.prefs);
|
||||
let grow = look.grow;
|
||||
let options = mark_options(app, curve_points_from_tool);
|
||||
let grow = options.look.grow;
|
||||
let anchor = |direct: bool| grow + if direct { 5.0 } else { 4.0 };
|
||||
// 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);
|
||||
// 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))));
|
||||
}
|
||||
// The key object (Align to Key Object): its outline drawn thicker, or its bounds when it has
|
||||
// too many paths to outline.
|
||||
if let Some((k, n)) = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))) {
|
||||
let outline = if too_many_anchors([n]) {
|
||||
n.geometric_bounds_with(&symbols).map(|b| vectorcraft_geom::shapes::rectangle(b).to_bezpath())
|
||||
} else {
|
||||
Some(node_outline(n, &symbols))
|
||||
};
|
||||
if let Some(bp) = outline {
|
||||
stroke_path(p, &bp, xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k))));
|
||||
}
|
||||
}
|
||||
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);
|
||||
// Path outlines (and area type / placed document / image / mesh / symbol outlines;
|
||||
// point type baselines and origin are drawn below).
|
||||
if !matches!(n.kind, NodeKind::Text(ref tx) if matches!(tx.kind, vectorcraft_doc::TextKind::Point)) {
|
||||
stroke_path(p, &node_outline(n, &symbols), xf, Stroke::new(1.0, color));
|
||||
}
|
||||
// Anchors (and handles for selected anchors in direct mode); a gradient mesh's or a mesh
|
||||
// envelope's points.
|
||||
walk_drawn(n, &mut |c| {
|
||||
let envelope = vectorcraft_doc::live::envelope_overlay(c).and_then(|(_, grid)| grid);
|
||||
let grid = match &c.kind {
|
||||
NodeKind::Mesh(m) => Some(m),
|
||||
_ => envelope.as_ref(),
|
||||
};
|
||||
if let Some(grid) = grid {
|
||||
for q in &grid.points {
|
||||
anchor_square(p, xf.to_screen(q.p), color, false, anchor(direct));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let NodeKind::Path { path, .. } = &c.kind else { return };
|
||||
// The Curvature tool owns its points while drawing; existing paths use this overlay.
|
||||
if curvature && curve_points_from_tool {
|
||||
return;
|
||||
}
|
||||
let selected_segments = st.selection.segments.get(&c.id);
|
||||
if let Some(segments) = selected_segments {
|
||||
for &(si, seg) in segments {
|
||||
let Some(sp) = path.subpaths.get(si).filter(|sp| seg < sp.segment_count()) else { continue };
|
||||
let curve = sp.segment(seg);
|
||||
let mut outline = vectorcraft_geom::BezPath::new();
|
||||
outline.move_to(curve.p0);
|
||||
if sp.segment_is_line(seg) {
|
||||
outline.line_to(curve.p3);
|
||||
} else {
|
||||
outline.curve_to(curve.p1, curve.p2, curve.p3);
|
||||
}
|
||||
stroke_path(p, &outline, xf, Stroke::new(2.0, color));
|
||||
}
|
||||
}
|
||||
for (si, ai, a) in path.anchors() {
|
||||
// An object selected whole has all its anchors selected, and so do the paths inside a
|
||||
// group, compound path or clip group selected whole. Direction handles show only on a
|
||||
// path that is selected itself: those are the handles that `handle_anchors` in
|
||||
// `vectorcraft-tools` lets the tools drag.
|
||||
let (sel, handles_shown) = match partial {
|
||||
Some(set) => {
|
||||
let sel = set.contains(&(si, ai));
|
||||
(sel, sel)
|
||||
}
|
||||
None => (true, direct && c.id == *id),
|
||||
};
|
||||
let sp = xf.to_screen(a.p);
|
||||
if !curvature && handles_shown {
|
||||
with_handles += 1;
|
||||
let sides = path.subpaths.get(si).map(|subpath| st.selection.handle_sides(c.id, si, ai, subpath)).unwrap_or((true, true));
|
||||
handles.extend(
|
||||
[(a.h_in, sides.0), (a.h_out, sides.1)]
|
||||
.into_iter()
|
||||
.filter(|(h, show)| *show && h.distance(a.p) > 1e-6)
|
||||
.map(|(h, _)| (sp, xf.to_screen(h), color)),
|
||||
);
|
||||
}
|
||||
let anchor_selected = sel && selected_segments.is_none();
|
||||
let ink = if anchor_selected && partial.is_some() { selected_anchor(color) } else { color };
|
||||
if curvature {
|
||||
curve_point(p, sp, ink, anchor_selected && partial.is_some(), grow + 6.0);
|
||||
} else {
|
||||
anchor_square(p, sp, ink, anchor_selected, anchor(partial.is_some() || direct));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Centre point (Attributes panel → Show Center).
|
||||
if n.shows_center()
|
||||
&& let Some(b) = n.geometric_bounds_with(&symbols)
|
||||
{
|
||||
anchor_square(p, xf.to_screen(b.center()), color, true, anchor(false));
|
||||
}
|
||||
// Point type: each line's baseline, where a click selects it (`hit`), whatever its
|
||||
// alignment and rotation, and the anchor (area type shows its frame instead, which may
|
||||
// not be a rectangle, and type on a path its path).
|
||||
if let NodeKind::Text(tx) = &n.kind
|
||||
&& matches!(tx.kind, vectorcraft_doc::TextKind::Point)
|
||||
{
|
||||
for (a, b) in tx.baselines() {
|
||||
p.line_segment([xf.to_screen(tx.xf * a), xf.to_screen(tx.xf * b)], Stroke::new(1.0, color));
|
||||
}
|
||||
p.circle_filled(xf.to_screen(tx.xf * Point::ZERO), 2.5, color);
|
||||
}
|
||||
}
|
||||
if app.session.prefs.show_handles_multiple_anchors || with_handles <= 1 {
|
||||
for (sp, hp, color) in handles {
|
||||
p.line_segment([sp, hp], Stroke::new(1.0, color));
|
||||
look.draw(p, hp, color);
|
||||
}
|
||||
let key = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?)));
|
||||
let (key_bounds, key_outline) = match key {
|
||||
Some((k, n)) if too_many_anchors([n]) => (Some((k, n)), None),
|
||||
Some((k, _)) => (None, Some(k)),
|
||||
None => (None, None),
|
||||
};
|
||||
if let Some((k, n)) = key_bounds
|
||||
&& let Some(b) = n.geometric_bounds_with(&symbols)
|
||||
{
|
||||
stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k))));
|
||||
}
|
||||
marks.paint(st, p, xf, &options, key_outline, if big { &[] } else { &st.selection.objects });
|
||||
// The spine of each selected blend (or of the blend a selected key object belongs to).
|
||||
let mut spines = vec![];
|
||||
for id in &st.selection.objects {
|
||||
|
||||
@@ -0,0 +1,941 @@
|
||||
//! The selected objects' outlines, anchors and handles, kept between frames.
|
||||
//!
|
||||
//! Outlines, handles and slanted lines are kept as triangles. Anchor squares, and level or upright
|
||||
//! lines that are not handles, are kept as document points. Each selected object has an
|
||||
//! [`ObjectMarks`], valid for one node (by [`Arc`] identity), layer color and set of selected
|
||||
//! anchors, for the document and revision if the object uses symbols, and within [`MAX_PAN`]. The
|
||||
//! marks of all objects are joined into one mesh, in the order they are drawn. A frame with nothing
|
||||
//! changed draws that mesh again. A pan moves the triangles of outlines and handles, and places the
|
||||
//! anchor squares again, because a square's edges are rounded to whole pixels where it lands. Each
|
||||
//! subpath, handle and square is tested against the view on its own: one out of view adds nothing
|
||||
//! to the mesh, and a subpath or handle gets its triangles the first time it is in view. A frame
|
||||
//! with a new zoom, rotation or display scale draws plain shapes and keeps nothing, and the next
|
||||
//! frame with the same view builds the marks.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::ops::Range;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use egui::emath::{GuiRounding as _, TSTransform};
|
||||
use egui::epaint::{ClippedShape, Primitive, RectShape, TessellationOptions, Tessellator, Vertex};
|
||||
use egui::{Mesh, Vec2};
|
||||
use vectorcraft_doc::{AnchorRef, NodeId, SegmentRef};
|
||||
|
||||
use super::outline::{KEY_OUTLINE, c32, node_outline, to_screen_path, walk_drawn};
|
||||
use super::overlays::{HandleLook, anchor_square, selected_anchor};
|
||||
use super::{BezPath, Color32, Node, NodeKind, Point, Pos2, Shape, Stroke, StrokeKind, SymbolBoxes, Xf, pos2, vec2};
|
||||
|
||||
/// How far a pan moves an object's kept marks, on either axis, before the object is built again.
|
||||
/// Screen coordinates are `f32`, which holds a coordinate under 2^16 to within 1/256 px, and a point
|
||||
/// farther out than this plus the width of a window stays off screen.
|
||||
const MAX_PAN: f64 = 16_384.0;
|
||||
|
||||
/// The most points and vertices kept. Past this the marks are dropped after the frame that built
|
||||
/// them.
|
||||
const MAX_KEPT: usize = 8_000_000;
|
||||
|
||||
/// The most anchor square meshes kept, one per color, style and size in pixels.
|
||||
const MAX_TEMPLATES: usize = 256;
|
||||
|
||||
/// The farthest a handle's dot and its stroke reach from the end of its line, at the default size.
|
||||
const HANDLE_REACH: f32 = 6.0;
|
||||
|
||||
/// Everything that decides how marks look, apart from the object and the view's position.
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct MarkLook {
|
||||
zoom: f64,
|
||||
rot: f64,
|
||||
pixels_per_point: f32,
|
||||
tessellation: TessellationOptions,
|
||||
/// Disc texture coordinates scale with the size of the font texture.
|
||||
font_image_size: [usize; 2],
|
||||
direct: bool,
|
||||
curvature: bool,
|
||||
curve_points_from_tool: bool,
|
||||
grow: f32,
|
||||
handle_style: String,
|
||||
segments: BTreeMap<NodeId, BTreeSet<SegmentRef>>,
|
||||
}
|
||||
|
||||
/// The options [`SelectionMarks::paint`] draws with.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct MarkOptions<'a> {
|
||||
/// Whether the tool edits anchors.
|
||||
pub direct: bool,
|
||||
/// Whether the tool is the Curvature tool.
|
||||
pub curvature: bool,
|
||||
/// Whether the Curvature tool draws its own points.
|
||||
pub curve_points_from_tool: bool,
|
||||
/// Selection & Anchor Display › Size and Handles.
|
||||
pub look: HandleLook<'a>,
|
||||
/// Selection & Anchor Display › Show handles when multiple anchors are selected.
|
||||
pub all_handles: bool,
|
||||
}
|
||||
|
||||
/// A view's position: the document point at the middle of the canvas, and that middle on screen.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct ViewAt {
|
||||
origin: Pos2,
|
||||
center: Point,
|
||||
}
|
||||
|
||||
/// [`Xf`] with its rotation worked out.
|
||||
struct ViewMap {
|
||||
sin: f64,
|
||||
cos: f64,
|
||||
zoom: f64,
|
||||
at: ViewAt,
|
||||
}
|
||||
|
||||
impl ViewMap {
|
||||
fn new(xf: &Xf) -> Self {
|
||||
let (sin, cos) = xf.rot.sin_cos();
|
||||
Self { sin, cos, zoom: xf.zoom, at: ViewAt { origin: xf.rect.center(), center: xf.center } }
|
||||
}
|
||||
|
||||
/// A distance in the document, scaled and rotated by the view.
|
||||
fn turned(&self, dx: f64, dy: f64) -> [f64; 2] {
|
||||
let (x, y) = (dx * self.zoom, dy * self.zoom);
|
||||
[x * self.cos - y * self.sin, x * self.sin + y * self.cos]
|
||||
}
|
||||
|
||||
/// The same result as [`Xf::to_screen`].
|
||||
fn to_screen(&self, p: Point) -> Pos2 {
|
||||
let [x, y] = self.turned(p.x - self.at.center.x, p.y - self.at.center.y);
|
||||
pos2(self.at.origin.x + x as f32, self.at.origin.y + y as f32)
|
||||
}
|
||||
|
||||
/// How far on screen a point moves between the view position `from` and this one.
|
||||
fn moved_from(&self, from: ViewAt) -> [f64; 2] {
|
||||
let [x, y] = self.turned(from.center.x - self.at.center.x, from.center.y - self.at.center.y);
|
||||
[x + f64::from(self.at.origin.x - from.origin.x), y + f64::from(self.at.origin.y - from.origin.y)]
|
||||
}
|
||||
|
||||
fn moved_vec(&self, from: ViewAt) -> Vec2 {
|
||||
let [x, y] = self.moved_from(from);
|
||||
vec2(x as f32, y as f32)
|
||||
}
|
||||
}
|
||||
|
||||
/// The triangles of one item in its run's mesh.
|
||||
#[derive(Clone)]
|
||||
struct Span {
|
||||
vertices: Range<u32>,
|
||||
indices: Range<u32>,
|
||||
}
|
||||
|
||||
/// A polyline, disc or slanted line in a [`Mark::Rigid`].
|
||||
struct RigidPiece {
|
||||
/// The shape, in screen coordinates at the object's `base`, until it is first on screen.
|
||||
shape: Option<Shape>,
|
||||
/// Where the shape draws at the object's `base`.
|
||||
bounds: egui::Rect,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
/// A direction handle in a [`Mark::Handles`]: the line from its anchor and the dot at its end.
|
||||
struct HandleLine {
|
||||
from: Point,
|
||||
to: Point,
|
||||
/// Whether the line is, or is nearly, level or upright. Its triangles then move only by whole
|
||||
/// pixels, and any other move draws it again.
|
||||
rounded: bool,
|
||||
/// The view position its triangles were drawn for.
|
||||
at: ViewAt,
|
||||
/// Its triangles, from the first time it is on screen.
|
||||
span: Option<Span>,
|
||||
}
|
||||
|
||||
/// One run of an object's marks.
|
||||
enum Mark {
|
||||
/// Marks whose triangles keep their form when moved: polylines, discs and slanted lines.
|
||||
/// `mesh` holds the triangles of the pieces that have been on screen.
|
||||
Rigid { pieces: Vec<RigidPiece>, mesh: Mesh, points: usize },
|
||||
/// Direction handles of one color. `mesh` holds the triangles of those that have been on screen.
|
||||
Handles { color: Color32, lines: Vec<HandleLine>, mesh: Mesh },
|
||||
/// Anchor squares in one style, at these document points.
|
||||
Squares { color: Color32, filled: bool, size: f32, at: Vec<Point> },
|
||||
/// A line that is, or is nearly, level or upright: the tessellator rounds those to whole pixels.
|
||||
Segment { a: Point, b: Point, stroke: Stroke },
|
||||
}
|
||||
|
||||
impl Mark {
|
||||
fn kept(&self) -> usize {
|
||||
match self {
|
||||
Self::Rigid { mesh, points, .. } => mesh.vertices.len().max(*points),
|
||||
Self::Handles { lines, mesh, .. } => mesh.vertices.len() + lines.len(),
|
||||
Self::Squares { at, .. } => at.len(),
|
||||
Self::Segment { .. } => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a list of [`Mark`]s, joining neighbors that can be drawn as one run.
|
||||
struct MarkList<'a> {
|
||||
map: &'a ViewMap,
|
||||
marks: Vec<Mark>,
|
||||
}
|
||||
|
||||
impl<'a> MarkList<'a> {
|
||||
fn new(map: &'a ViewMap) -> Self {
|
||||
Self { map, marks: vec![] }
|
||||
}
|
||||
|
||||
fn rigid(&mut self, shape: Shape) {
|
||||
let count = match &shape {
|
||||
Shape::Path(path) => path.points.len(),
|
||||
_ => 1,
|
||||
};
|
||||
let piece = RigidPiece { bounds: shape.visual_bounding_rect(), shape: Some(shape), span: Span { vertices: 0..0, indices: 0..0 } };
|
||||
if let Some(Mark::Rigid { pieces, points, .. }) = self.marks.last_mut() {
|
||||
*points += count;
|
||||
pieces.push(piece);
|
||||
} else {
|
||||
self.marks.push(Mark::Rigid { pieces: vec![piece], mesh: Mesh::default(), points: count });
|
||||
}
|
||||
}
|
||||
|
||||
/// A direction handle: its line from `from` and the dot at `to`.
|
||||
fn handle(&mut self, from: Point, to: Point, color: Color32) {
|
||||
let rounded = is_rounded_line(self.map.to_screen(from), self.map.to_screen(to));
|
||||
let line = HandleLine { from, to, rounded, at: self.map.at, span: None };
|
||||
if let Some(Mark::Handles { color: c, lines, .. }) = self.marks.last_mut()
|
||||
&& *c == color
|
||||
{
|
||||
lines.push(line);
|
||||
} else {
|
||||
self.marks.push(Mark::Handles { color, lines: vec![line], mesh: Mesh::default() });
|
||||
}
|
||||
}
|
||||
|
||||
fn outline(&mut self, path: &BezPath, xf: &Xf, stroke: Stroke) {
|
||||
for line in to_screen_path(path, xf) {
|
||||
self.rigid(Shape::line(line, stroke));
|
||||
}
|
||||
}
|
||||
|
||||
fn square(&mut self, p: Point, color: Color32, filled: bool, size: f32) {
|
||||
if let Some(Mark::Squares { color: c, filled: f, size: s, at }) = self.marks.last_mut()
|
||||
&& (*c, *f, *s) == (color, filled, size)
|
||||
{
|
||||
at.push(p);
|
||||
} else {
|
||||
self.marks.push(Mark::Squares { color, filled, size, at: vec![p] });
|
||||
}
|
||||
}
|
||||
|
||||
fn line(&mut self, from: Point, to: Point, stroke: Stroke) {
|
||||
let (sa, sb) = (self.map.to_screen(from), self.map.to_screen(to));
|
||||
if is_rounded_line(sa, sb) {
|
||||
self.marks.push(Mark::Segment { a: from, b: to, stroke });
|
||||
} else {
|
||||
self.rigid(Shape::line_segment([sa, sb], stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a line from `a` to `b` is, or after a move could be, exactly level or upright.
|
||||
fn is_rounded_line(a: Pos2, b: Pos2) -> bool {
|
||||
(a.x - b.x).abs() < 0.002 || (a.y - b.y).abs() < 0.002
|
||||
}
|
||||
|
||||
/// The marks of one object (or the key object's outline), and what they were built from.
|
||||
struct ObjectMarks {
|
||||
/// Holds the allocation, so that no later node can have this address. While it exists,
|
||||
/// `Arc::make_mut` moves an edited node to a new address.
|
||||
node: Weak<Node>,
|
||||
color: Color32,
|
||||
partial: Option<BTreeSet<AnchorRef>>,
|
||||
/// The document and revision, for an object that measures symbols' art.
|
||||
symbols: Option<(u64, u64)>,
|
||||
/// The view position when the marks were built.
|
||||
base: ViewAt,
|
||||
marks: Vec<Mark>,
|
||||
handles: Vec<Mark>,
|
||||
/// The anchors whose handles `handles` shows.
|
||||
with_handles: usize,
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
impl ObjectMarks {
|
||||
fn current(&self, node: &Arc<Node>, color: Color32, partial: Option<&BTreeSet<AnchorRef>>, source: (u64, u64), map: &ViewMap) -> bool {
|
||||
let [x, y] = map.moved_from(self.base);
|
||||
Weak::as_ptr(&self.node) == Arc::as_ptr(node)
|
||||
&& self.color == color
|
||||
&& self.partial.as_ref() == partial
|
||||
&& self.symbols.is_none_or(|s| s == source)
|
||||
&& x.abs() <= MAX_PAN
|
||||
&& y.abs() <= MAX_PAN
|
||||
}
|
||||
}
|
||||
|
||||
/// The node `id` as held in the document, for its identity.
|
||||
fn node_arc(doc: &vectorcraft_doc::Document, id: NodeId) -> Option<&Arc<Node>> {
|
||||
let path = doc.index_path(id)?;
|
||||
let (first, rest) = path.split_first()?;
|
||||
let mut node = doc.layers.get(*first)?;
|
||||
for i in rest {
|
||||
node = node.children()?.get(*i)?;
|
||||
}
|
||||
Some(node)
|
||||
}
|
||||
|
||||
/// A square's triangles, relative to the corner it was rounded to.
|
||||
struct Template {
|
||||
color: Color32,
|
||||
filled: bool,
|
||||
size: f32,
|
||||
pixels: (i32, i32),
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
/// A handle's dot in one color, around the origin.
|
||||
struct Dot {
|
||||
color: Color32,
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
/// A disc in one style, around the origin.
|
||||
struct Disc {
|
||||
radius: f32,
|
||||
fill: Color32,
|
||||
stroke: Stroke,
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
/// What one anchor square draws: the fill, then the stroke of a hollow one.
|
||||
fn square_shapes(rect: egui::Rect, color: Color32, filled: bool) -> [Option<RectShape>; 2] {
|
||||
if filled {
|
||||
[Some(RectShape::filled(rect, 0.0, color)), None]
|
||||
} else {
|
||||
[Some(RectShape::filled(rect, 0.0, Color32::WHITE)), Some(RectShape::stroke(rect, 0.0, Stroke::new(1.0, color), StrokeKind::Inside))]
|
||||
}
|
||||
}
|
||||
|
||||
/// `shapes` as the frame's tessellator draws them, with nothing culled.
|
||||
fn tessellated(ctx: &egui::Context, shapes: Vec<Shape>, pixels_per_point: f32) -> Mesh {
|
||||
let clipped = shapes.into_iter().map(|shape| ClippedShape {
|
||||
clip_rect: egui::Rect::EVERYTHING,
|
||||
shape,
|
||||
transform_after_tessellation: TSTransform::IDENTITY,
|
||||
});
|
||||
let primitives = ctx.tessellate(clipped.collect(), pixels_per_point);
|
||||
primitives
|
||||
.into_iter()
|
||||
.find_map(|c| match c.primitive {
|
||||
Primitive::Mesh(mesh) => Some(mesh),
|
||||
Primitive::Callback(_) => None,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Joins marks into the frame's mesh.
|
||||
struct Composer<'a> {
|
||||
ctx: &'a egui::Context,
|
||||
map: &'a ViewMap,
|
||||
clip: egui::Rect,
|
||||
pixels_per_point: f32,
|
||||
/// Squares this size or smaller are drawn as lines, which round differently: see
|
||||
/// [`Tessellator::tessellate_rect`].
|
||||
thin: f32,
|
||||
/// Draws lines and squares that rounding keeps from being reused, culled by `clip`.
|
||||
tessellator: Tessellator,
|
||||
/// Draws what is kept for later frames, with nothing culled. Like `tessellator`, it has no discs,
|
||||
/// so neither draws circles: those come from `discs`, stamped where they land.
|
||||
keeper: Tessellator,
|
||||
/// Draws filled squares without the soft edge, when that edge falls between pixel centers: on a
|
||||
/// square rounded to whole pixels, each pixel center is then fully in or fully out.
|
||||
crisp: Option<Tessellator>,
|
||||
/// Whether the tessellator rounds rectangles to whole pixels, as [`Self::squares`] assumes.
|
||||
rounds_rects: bool,
|
||||
look: HandleLook<'a>,
|
||||
templates: &'a mut Vec<Template>,
|
||||
dots: &'a mut Vec<Dot>,
|
||||
discs: &'a mut Vec<Disc>,
|
||||
out: Mesh,
|
||||
}
|
||||
|
||||
/// Append `mesh` to `out`, moved by `delta`.
|
||||
fn stamp(out: &mut Mesh, mesh: &Mesh, delta: Vec2) {
|
||||
let first = out.vertices.len() as u32;
|
||||
out.vertices.extend(mesh.vertices.iter().map(|v| Vertex { pos: v.pos + delta, ..*v }));
|
||||
out.indices.extend(mesh.indices.iter().map(|i| i + first));
|
||||
}
|
||||
|
||||
/// Append the triangles of `span` in `mesh` to `out`, moved by `delta`.
|
||||
fn stamp_span(out: &mut Mesh, mesh: &Mesh, span: &Span, delta: Vec2) {
|
||||
let vertices = mesh.vertices.get(span.vertices.start as usize..span.vertices.end as usize);
|
||||
let indices = mesh.indices.get(span.indices.start as usize..span.indices.end as usize);
|
||||
let (Some(vertices), Some(indices)) = (vertices, indices) else { return };
|
||||
let first = out.vertices.len() as u32;
|
||||
out.vertices.extend(vertices.iter().map(|v| Vertex { pos: v.pos + delta, ..*v }));
|
||||
out.indices.extend(indices.iter().map(|i| i - span.vertices.start + first));
|
||||
}
|
||||
|
||||
/// Run `draw` on `mesh`, and return the triangles it added.
|
||||
fn drawn_span(mesh: &mut Mesh, draw: impl FnOnce(&mut Mesh)) -> Span {
|
||||
let (vertices, indices) = (mesh.vertices.len() as u32, mesh.indices.len() as u32);
|
||||
draw(mesh);
|
||||
Span { vertices: vertices..mesh.vertices.len() as u32, indices: indices..mesh.indices.len() as u32 }
|
||||
}
|
||||
|
||||
impl Composer<'_> {
|
||||
fn marks(&mut self, marks: &mut [Mark], base: ViewAt) {
|
||||
let delta = self.map.moved_vec(base);
|
||||
let margin = 2.0 / self.pixels_per_point;
|
||||
for mark in marks {
|
||||
match mark {
|
||||
Mark::Rigid { pieces, mesh, .. } => {
|
||||
for piece in pieces {
|
||||
if !piece.bounds.translate(delta).expand(margin).intersects(self.clip) {
|
||||
continue;
|
||||
}
|
||||
if let Some(shape) = piece.shape.take() {
|
||||
piece.span = self.rigid_span(shape, mesh);
|
||||
}
|
||||
stamp_span(&mut self.out, mesh, &piece.span, delta);
|
||||
}
|
||||
}
|
||||
Mark::Handles { color, lines, mesh } => self.handles(*color, lines, mesh),
|
||||
Mark::Squares { color, filled, size, at } => self.squares(*color, *filled, *size, at),
|
||||
Mark::Segment { a, b, stroke } => {
|
||||
let points = [self.map.to_screen(*a), self.map.to_screen(*b)];
|
||||
self.tessellator.tessellate_line_segment(points, *stroke, &mut self.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw `shape` into `mesh`.
|
||||
fn rigid_span(&mut self, shape: Shape, mesh: &mut Mesh) -> Span {
|
||||
drawn_span(mesh, |mesh| match shape {
|
||||
Shape::Circle(circle) => {
|
||||
let index = self.disc(&circle);
|
||||
if let Some(disc) = self.discs.get(index) {
|
||||
stamp(mesh, &disc.mesh, circle.center.to_vec2());
|
||||
}
|
||||
}
|
||||
shape => self.keeper.tessellate_shape(shape, mesh),
|
||||
})
|
||||
}
|
||||
|
||||
/// The index of the disc in the style of `circle`, made when there is none.
|
||||
fn disc(&mut self, circle: &egui::epaint::CircleShape) -> usize {
|
||||
let same = |d: &Disc| d.radius == circle.radius && d.fill == circle.fill && d.stroke == circle.stroke;
|
||||
if let Some(index) = self.discs.iter().position(same) {
|
||||
return index;
|
||||
}
|
||||
let shape = Shape::Circle(egui::epaint::CircleShape { center: Pos2::ZERO, ..*circle });
|
||||
let mesh = tessellated(self.ctx, vec![shape], self.pixels_per_point);
|
||||
if self.discs.len() >= MAX_TEMPLATES {
|
||||
self.discs.clear();
|
||||
}
|
||||
self.discs.push(Disc { radius: circle.radius, fill: circle.fill, stroke: circle.stroke, mesh });
|
||||
self.discs.len() - 1
|
||||
}
|
||||
|
||||
/// Add the direction handles in view to the frame, drawing those not yet in `mesh` into it.
|
||||
fn handles(&mut self, color: Color32, lines: &mut [HandleLine], mesh: &mut Mesh) {
|
||||
let index = match self.dots.iter().position(|d| d.color == color) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let shapes = self.look.shapes(Pos2::ZERO, color).into_iter().flatten().collect();
|
||||
let mesh = tessellated(self.ctx, shapes, self.pixels_per_point);
|
||||
if self.dots.len() >= MAX_TEMPLATES {
|
||||
self.dots.clear();
|
||||
}
|
||||
self.dots.push(Dot { color, mesh });
|
||||
self.dots.len() - 1
|
||||
}
|
||||
};
|
||||
let Some(dot) = self.dots.get(index) else { return };
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let reach = HANDLE_REACH + self.look.grow.max(0.0) / 2.0;
|
||||
for line in lines {
|
||||
let (from, to) = (self.map.to_screen(line.from), self.map.to_screen(line.to));
|
||||
if !egui::Rect::from_two_pos(from, to).expand(reach).intersects(self.clip) {
|
||||
continue;
|
||||
}
|
||||
let moved = self.map.moved_vec(line.at);
|
||||
let whole_pixels = (moved * self.pixels_per_point).round() == moved * self.pixels_per_point;
|
||||
let keeper = &mut self.keeper;
|
||||
let mut draw = |mesh: &mut Mesh| {
|
||||
keeper.tessellate_line_segment([from, to], stroke, mesh);
|
||||
stamp(mesh, &dot.mesh, to.to_vec2());
|
||||
};
|
||||
match &line.span {
|
||||
Some(span) if !line.rounded || whole_pixels => stamp_span(&mut self.out, mesh, span, moved),
|
||||
Some(_) => draw(&mut self.out),
|
||||
None => {
|
||||
let span = drawn_span(mesh, draw);
|
||||
stamp_span(&mut self.out, mesh, &span, Vec2::ZERO);
|
||||
line.at = self.map.at;
|
||||
line.span = Some(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn squares(&mut self, color: Color32, filled: bool, size: f32, at: &[Point]) {
|
||||
let stamps = self.rounds_rects && size > self.thin;
|
||||
let mut template = None;
|
||||
let mut last = None;
|
||||
for p in at {
|
||||
let rect = egui::Rect::from_center_size(self.map.to_screen(*p), vec2(size, size));
|
||||
if !rect.intersects(self.clip) {
|
||||
continue;
|
||||
}
|
||||
if !stamps {
|
||||
for shape in square_shapes(rect, color, filled).into_iter().flatten() {
|
||||
self.tessellator.tessellate_rect(&shape, &mut self.out);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let rounded = rect.round_to_pixels(self.pixels_per_point);
|
||||
// A filled square on the same pixels as the last one, in the same run, adds nothing.
|
||||
if filled && last == Some(rounded) {
|
||||
continue;
|
||||
}
|
||||
last = Some(rounded);
|
||||
let pixels = ((rounded.width() * self.pixels_per_point).round() as i32, (rounded.height() * self.pixels_per_point).round() as i32);
|
||||
let index = match template {
|
||||
Some((p, index)) if p == pixels => index,
|
||||
_ => self.template(color, filled, size, pixels, rect),
|
||||
};
|
||||
template = Some((pixels, index));
|
||||
let Some(found) = self.templates.get(index) else { continue };
|
||||
stamp(&mut self.out, &found.mesh, rounded.min.to_vec2());
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the template for a square in this style, made from `rect` when there is none.
|
||||
fn template(&mut self, color: Color32, filled: bool, size: f32, pixels: (i32, i32), rect: egui::Rect) -> usize {
|
||||
let same = |t: &Template| t.color == color && t.filled == filled && t.size == size && t.pixels == pixels;
|
||||
if let Some(index) = self.templates.iter().position(same) {
|
||||
return index;
|
||||
}
|
||||
let mut mesh = Mesh::default();
|
||||
let tessellator = match &mut self.crisp {
|
||||
Some(crisp) if filled => crisp,
|
||||
_ => &mut self.keeper,
|
||||
};
|
||||
for shape in square_shapes(rect, color, filled).into_iter().flatten() {
|
||||
tessellator.tessellate_rect(&shape, &mut mesh);
|
||||
}
|
||||
let corner = rect.round_to_pixels(self.pixels_per_point).min.to_vec2();
|
||||
for v in &mut mesh.vertices {
|
||||
v.pos -= corner;
|
||||
}
|
||||
if self.templates.len() >= MAX_TEMPLATES {
|
||||
self.templates.clear();
|
||||
}
|
||||
self.templates.push(Template { color, filled, size, pixels, mesh });
|
||||
self.templates.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// What the frame's mesh was joined for.
|
||||
#[derive(PartialEq)]
|
||||
struct Shown {
|
||||
at: ViewAt,
|
||||
clip: egui::Rect,
|
||||
all_handles: bool,
|
||||
}
|
||||
|
||||
/// The kept marks of the selection ([`CanvasCache::selection_marks`](crate::caches::CanvasCache)).
|
||||
#[derive(Default)]
|
||||
pub struct SelectionMarks {
|
||||
look: Option<MarkLook>,
|
||||
objects: HashMap<NodeId, ObjectMarks>,
|
||||
key: Option<(NodeId, ObjectMarks)>,
|
||||
templates: Vec<Template>,
|
||||
dots: Vec<Dot>,
|
||||
discs: Vec<Disc>,
|
||||
frame: Arc<Mesh>,
|
||||
shown: Option<Shown>,
|
||||
order: Vec<NodeId>,
|
||||
pass: u64,
|
||||
kept: usize,
|
||||
#[cfg(test)]
|
||||
builds: usize,
|
||||
#[cfg(test)]
|
||||
composes: usize,
|
||||
}
|
||||
|
||||
impl SelectionMarks {
|
||||
/// Draw the key object `key`'s outline and the marks of `objects`, in that order, reusing what
|
||||
/// is still valid. Entries of any other object are dropped.
|
||||
pub(super) fn paint(
|
||||
&mut self,
|
||||
st: &vectorcraft_engine::DocState,
|
||||
p: &egui::Painter,
|
||||
xf: &Xf,
|
||||
options: &MarkOptions,
|
||||
key: Option<NodeId>,
|
||||
objects: &[NodeId],
|
||||
) {
|
||||
let ctx = p.ctx();
|
||||
let look = MarkLook {
|
||||
zoom: xf.zoom,
|
||||
rot: xf.rot,
|
||||
pixels_per_point: ctx.pixels_per_point(),
|
||||
tessellation: ctx.tessellation_options(|o| *o),
|
||||
font_image_size: ctx.fonts(|f| f.font_image_size()),
|
||||
direct: options.direct,
|
||||
curvature: options.curvature,
|
||||
curve_points_from_tool: options.curve_points_from_tool,
|
||||
grow: options.look.grow,
|
||||
handle_style: options.look.style.to_owned(),
|
||||
segments: st.selection.segments.clone(),
|
||||
};
|
||||
let pixels_per_point = look.pixels_per_point;
|
||||
let tessellation = look.tessellation;
|
||||
if self.look.as_ref() != Some(&look) {
|
||||
self.clear();
|
||||
self.look = Some(look);
|
||||
draw_once(st, p, xf, options, key, objects);
|
||||
return;
|
||||
}
|
||||
let map = ViewMap::new(xf);
|
||||
let symbols = st.doc.symbol_boxes();
|
||||
let source = (st.uid, st.revision);
|
||||
let mut changed = self.update_key(st, key, xf, &map, &symbols, source);
|
||||
self.pass = self.pass.wrapping_add(1);
|
||||
let pass = self.pass;
|
||||
for id in objects {
|
||||
let Some(node) = node_arc(&st.doc, *id) else { continue };
|
||||
let color = c32(st.doc.layer_color(*id));
|
||||
match self.objects.get_mut(id) {
|
||||
Some(entry) if entry.current(node, color, st.selection.partial(*id), source, &map) => entry.seen = pass,
|
||||
_ => {
|
||||
let mut entry = object_marks(st, *id, node, color, source, xf, &map, options, &symbols);
|
||||
entry.seen = pass;
|
||||
self.objects.insert(*id, entry);
|
||||
changed = true;
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.builds += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let kept = self.objects.len();
|
||||
self.objects.retain(|_, entry| entry.seen == pass);
|
||||
changed |= self.objects.len() != kept;
|
||||
let shown = Shown { at: map.at, clip: p.clip_rect(), all_handles: options.all_handles };
|
||||
if changed || self.shown.as_ref() != Some(&shown) || self.order != objects {
|
||||
self.order.clear();
|
||||
self.order.extend_from_slice(objects);
|
||||
self.compose(ctx, &map, shown.clip, (pixels_per_point, tessellation), options);
|
||||
self.shown = Some(shown);
|
||||
}
|
||||
if !self.frame.is_empty() {
|
||||
p.add(Shape::Mesh(self.frame.clone()));
|
||||
}
|
||||
self.trim(MAX_KEPT);
|
||||
}
|
||||
|
||||
/// Drop everything when more than `limit` points and vertices are kept.
|
||||
fn trim(&mut self, limit: usize) {
|
||||
if self.kept > limit {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring the key object's outline up to date. True when it changed.
|
||||
fn update_key(
|
||||
&mut self,
|
||||
st: &vectorcraft_engine::DocState,
|
||||
key: Option<NodeId>,
|
||||
xf: &Xf,
|
||||
map: &ViewMap,
|
||||
symbols: &SymbolBoxes,
|
||||
source: (u64, u64),
|
||||
) -> bool {
|
||||
let Some((id, node)) = key.and_then(|id| Some((id, node_arc(&st.doc, id)?))) else { return self.key.take().is_some() };
|
||||
let color = c32(st.doc.layer_color(id));
|
||||
if self.key.as_ref().is_some_and(|(k, entry)| *k == id && entry.current(node, color, None, source, map)) {
|
||||
return false;
|
||||
}
|
||||
self.key = Some((id, key_marks(node, color, source, xf, map, symbols)));
|
||||
true
|
||||
}
|
||||
|
||||
/// Join the marks in the order they are drawn: the key object's outline, each object's marks,
|
||||
/// and then all the handles.
|
||||
fn compose(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
map: &ViewMap,
|
||||
clip: egui::Rect,
|
||||
(pixels_per_point, tessellation): (f32, TessellationOptions),
|
||||
options: &MarkOptions,
|
||||
) {
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.composes += 1;
|
||||
}
|
||||
let Self { objects, key, templates, dots, discs, frame, order, kept, .. } = self;
|
||||
let all_handles = options.all_handles;
|
||||
let mut out = Arc::try_unwrap(std::mem::take(frame)).unwrap_or_default();
|
||||
out.clear();
|
||||
let font_image_size = ctx.fonts(|f| f.font_image_size());
|
||||
let keeper = Tessellator::new(pixels_per_point, tessellation, font_image_size, vec![]);
|
||||
let mut tessellator = keeper.clone();
|
||||
tessellator.set_clip_rect(clip);
|
||||
let feathering = if tessellation.feathering { tessellation.feathering_size_in_pixels / pixels_per_point } else { 0.0 };
|
||||
let crisp = (tessellation.feathering && tessellation.feathering_size_in_pixels == 1.0)
|
||||
.then(|| Tessellator::new(pixels_per_point, TessellationOptions { feathering: false, ..tessellation }, font_image_size, vec![]));
|
||||
let mut composer = Composer {
|
||||
ctx,
|
||||
map,
|
||||
clip,
|
||||
pixels_per_point,
|
||||
thin: 2.0 * feathering,
|
||||
tessellator,
|
||||
keeper,
|
||||
crisp,
|
||||
rounds_rects: tessellation.round_rects_to_pixels,
|
||||
look: options.look,
|
||||
templates,
|
||||
dots,
|
||||
discs,
|
||||
out,
|
||||
};
|
||||
if let Some((_, entry)) = key {
|
||||
composer.marks(&mut entry.marks, entry.base);
|
||||
}
|
||||
for id in order.iter() {
|
||||
if let Some(entry) = objects.get_mut(id) {
|
||||
composer.marks(&mut entry.marks, entry.base);
|
||||
}
|
||||
}
|
||||
let with_handles: usize = order.iter().filter_map(|id| objects.get(id)).map(|entry| entry.with_handles).sum();
|
||||
if all_handles || with_handles <= 1 {
|
||||
for id in order.iter() {
|
||||
if let Some(entry) = objects.get_mut(id) {
|
||||
composer.marks(&mut entry.handles, entry.base);
|
||||
}
|
||||
}
|
||||
}
|
||||
let out = composer.out;
|
||||
let in_entries: usize = objects
|
||||
.values()
|
||||
.chain(key.iter().map(|(_, entry)| entry))
|
||||
.flat_map(|entry| entry.marks.iter().chain(&entry.handles))
|
||||
.map(Mark::kept)
|
||||
.sum();
|
||||
*kept = in_entries + out.vertices.len();
|
||||
*frame = Arc::new(out);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
*self = Self {
|
||||
#[cfg(test)]
|
||||
builds: self.builds,
|
||||
#[cfg(test)]
|
||||
composes: self.composes,
|
||||
..Self::default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n` or anything inside it is a symbol instance, whose outline is measured from art
|
||||
/// outside the node.
|
||||
fn uses_symbols(n: &Node) -> bool {
|
||||
let mut found = false;
|
||||
n.walk(&mut |c| found |= matches!(c.kind, NodeKind::SymbolInstance { .. }));
|
||||
found
|
||||
}
|
||||
|
||||
/// Build the key object's outline.
|
||||
fn key_marks(node: &Arc<Node>, color: Color32, source: (u64, u64), xf: &Xf, map: &ViewMap, symbols: &SymbolBoxes) -> ObjectMarks {
|
||||
let mut list = MarkList::new(map);
|
||||
list.outline(&node_outline(node, symbols), xf, Stroke::new(KEY_OUTLINE, color));
|
||||
ObjectMarks {
|
||||
node: Arc::downgrade(node),
|
||||
color,
|
||||
partial: None,
|
||||
symbols: uses_symbols(node).then_some(source),
|
||||
base: map.at,
|
||||
marks: list.marks,
|
||||
handles: vec![],
|
||||
with_handles: 0,
|
||||
seen: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the key object's outline and the marks of `objects` as plain shapes, keeping nothing.
|
||||
fn draw_once(st: &vectorcraft_engine::DocState, p: &egui::Painter, xf: &Xf, options: &MarkOptions, key: Option<NodeId>, objects: &[NodeId]) {
|
||||
let map = ViewMap::new(xf);
|
||||
let symbols = st.doc.symbol_boxes();
|
||||
let source = (st.uid, st.revision);
|
||||
if let Some((id, node)) = key.and_then(|id| Some((id, node_arc(&st.doc, id)?))) {
|
||||
add_marks(p, &map, options.look, key_marks(node, c32(st.doc.layer_color(id)), source, xf, &map, &symbols).marks);
|
||||
}
|
||||
let (mut handles, mut with_handles) = (vec![], 0);
|
||||
for id in objects {
|
||||
let Some(node) = node_arc(&st.doc, *id) else { continue };
|
||||
let entry = object_marks(st, *id, node, c32(st.doc.layer_color(*id)), source, xf, &map, options, &symbols);
|
||||
add_marks(p, &map, options.look, entry.marks);
|
||||
handles.push(entry.handles);
|
||||
with_handles += entry.with_handles;
|
||||
}
|
||||
if options.all_handles || with_handles <= 1 {
|
||||
for marks in handles {
|
||||
add_marks(p, &map, options.look, marks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `marks` to the painter one shape at a time.
|
||||
fn add_marks(p: &egui::Painter, map: &ViewMap, look: HandleLook, marks: Vec<Mark>) {
|
||||
for mark in marks {
|
||||
match mark {
|
||||
Mark::Rigid { pieces, .. } => {
|
||||
for shape in pieces.into_iter().filter_map(|piece| piece.shape) {
|
||||
p.add(shape);
|
||||
}
|
||||
}
|
||||
Mark::Handles { color, lines, .. } => {
|
||||
for line in lines {
|
||||
let (from, to) = (map.to_screen(line.from), map.to_screen(line.to));
|
||||
p.line_segment([from, to], Stroke::new(1.0, color));
|
||||
look.draw(p, to, color);
|
||||
}
|
||||
}
|
||||
Mark::Squares { color, filled, size, at } => {
|
||||
for point in at {
|
||||
anchor_square(p, map.to_screen(point), color, filled, size);
|
||||
}
|
||||
}
|
||||
Mark::Segment { a, b, stroke } => {
|
||||
p.line_segment([map.to_screen(a), map.to_screen(b)], stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the marks of the selected object `id`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn object_marks(
|
||||
st: &vectorcraft_engine::DocState,
|
||||
id: NodeId,
|
||||
node: &Arc<Node>,
|
||||
color: Color32,
|
||||
source: (u64, u64),
|
||||
xf: &Xf,
|
||||
map: &ViewMap,
|
||||
options: &MarkOptions,
|
||||
symbols: &SymbolBoxes,
|
||||
) -> ObjectMarks {
|
||||
let MarkOptions { direct, curvature, curve_points_from_tool, look, .. } = *options;
|
||||
let n: &Node = node;
|
||||
let partial = st.selection.partial(id);
|
||||
let anchor = |direct: bool| look.grow + if direct { 5.0 } else { 4.0 };
|
||||
let (mut marks, mut handles, mut with_handles) = (MarkList::new(map), MarkList::new(map), 0);
|
||||
// Path outlines (and area type / placed document / image / mesh / symbol outlines;
|
||||
// point type baselines and origin are drawn below).
|
||||
if !matches!(n.kind, NodeKind::Text(ref tx) if matches!(tx.kind, vectorcraft_doc::TextKind::Point)) {
|
||||
marks.outline(&node_outline(n, symbols), xf, Stroke::new(1.0, color));
|
||||
}
|
||||
// Anchors (and handles for selected anchors in direct mode); a gradient mesh's or a mesh
|
||||
// envelope's points.
|
||||
walk_drawn(n, &mut |c| {
|
||||
let envelope = vectorcraft_doc::live::envelope_overlay(c).and_then(|(_, grid)| grid);
|
||||
let grid = match &c.kind {
|
||||
NodeKind::Mesh(m) => Some(m),
|
||||
_ => envelope.as_ref(),
|
||||
};
|
||||
if let Some(grid) = grid {
|
||||
for q in &grid.points {
|
||||
marks.square(q.p, color, false, anchor(direct));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let NodeKind::Path { path, .. } = &c.kind else { return };
|
||||
// The Curvature tool owns its points while drawing; existing paths use this overlay.
|
||||
if curvature && curve_points_from_tool {
|
||||
return;
|
||||
}
|
||||
let selected_segments = st.selection.segments.get(&c.id);
|
||||
if let Some(segments) = selected_segments {
|
||||
for &(si, seg) in segments {
|
||||
let Some(sp) = path.subpaths.get(si).filter(|sp| seg < sp.segment_count()) else { continue };
|
||||
let curve = sp.segment(seg);
|
||||
let mut outline = vectorcraft_geom::BezPath::new();
|
||||
outline.move_to(curve.p0);
|
||||
if sp.segment_is_line(seg) {
|
||||
outline.line_to(curve.p3);
|
||||
} else {
|
||||
outline.curve_to(curve.p1, curve.p2, curve.p3);
|
||||
}
|
||||
marks.outline(&outline, xf, Stroke::new(2.0, color));
|
||||
}
|
||||
}
|
||||
for (si, ai, a) in path.anchors() {
|
||||
// An object selected whole has all its anchors selected, and so do the paths inside a
|
||||
// group, compound path or clip group selected whole. Direction handles show only on a
|
||||
// path that is selected itself: those are the handles that `handle_anchors` in
|
||||
// `vectorcraft-tools` lets the tools drag.
|
||||
let (sel, handles_shown) = match partial {
|
||||
Some(set) => {
|
||||
let sel = set.contains(&(si, ai));
|
||||
(sel, sel)
|
||||
}
|
||||
None => (true, direct && c.id == id),
|
||||
};
|
||||
if !curvature && handles_shown {
|
||||
with_handles += 1;
|
||||
let sides = path.subpaths.get(si).map(|subpath| st.selection.handle_sides(c.id, si, ai, subpath)).unwrap_or((true, true));
|
||||
for (h, _) in [(a.h_in, sides.0), (a.h_out, sides.1)].into_iter().filter(|(h, show)| *show && h.distance(a.p) > 1e-6) {
|
||||
handles.handle(a.p, h, color);
|
||||
}
|
||||
}
|
||||
let anchor_selected = sel && selected_segments.is_none();
|
||||
let ink = if anchor_selected && partial.is_some() { selected_anchor(color) } else { color };
|
||||
if curvature {
|
||||
let (fill, size) = (if anchor_selected && partial.is_some() { ink } else { Color32::WHITE }, look.grow + 6.0);
|
||||
marks.rigid(Shape::Circle(egui::epaint::CircleShape {
|
||||
center: map.to_screen(a.p),
|
||||
radius: size / 2.0,
|
||||
fill,
|
||||
stroke: Stroke::new(1.0, ink),
|
||||
}));
|
||||
} else {
|
||||
marks.square(a.p, ink, anchor_selected, anchor(partial.is_some() || direct));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Centre point (Attributes panel → Show Center).
|
||||
if n.shows_center()
|
||||
&& let Some(b) = n.geometric_bounds_with(symbols)
|
||||
{
|
||||
marks.square(b.center(), color, true, anchor(false));
|
||||
}
|
||||
// Point type: each line's baseline, where a click selects it (`hit`), whatever its
|
||||
// alignment and rotation, and the anchor (area type shows its frame instead, which may
|
||||
// not be a rectangle, and type on a path its path).
|
||||
if let NodeKind::Text(tx) = &n.kind
|
||||
&& matches!(tx.kind, vectorcraft_doc::TextKind::Point)
|
||||
{
|
||||
for (a, b) in tx.baselines() {
|
||||
marks.line(tx.xf * a, tx.xf * b, Stroke::new(1.0, color));
|
||||
}
|
||||
marks.rigid(Shape::circle_filled(map.to_screen(tx.xf * Point::ZERO), 2.5, color));
|
||||
}
|
||||
ObjectMarks {
|
||||
node: Arc::downgrade(node),
|
||||
color,
|
||||
partial: partial.cloned(),
|
||||
symbols: uses_symbols(n).then_some(source),
|
||||
base: map.at,
|
||||
marks: marks.marks,
|
||||
handles: handles.marks,
|
||||
with_handles,
|
||||
seen: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,629 @@
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::super::overlays::{anchor_square, curve_point, mark_options};
|
||||
use super::super::{VectorcraftApp, show};
|
||||
use super::*;
|
||||
|
||||
const SIZE: (f32, f32) = (400.0, 300.0);
|
||||
|
||||
fn id_of(made: &serde_json::Value) -> NodeId {
|
||||
NodeId(made["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// A curve with handles, a live rectangle, a group, point type and a symbol instance, all selected.
|
||||
fn scene() -> (VectorcraftApp, NodeId) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let curve = id_of(&app.run("path.create", json!({"d": "M40 40 C60 10 90 10 110 40 S160 70 180 40 L180 90"})).unwrap());
|
||||
app.run("shape.rectangle", json!({"x": 200.5, "y": 30.25, "width": 60, "height": 40})).unwrap();
|
||||
let a = id_of(&app.run("shape.ellipse", json!({"x": 40, "y": 120, "width": 50, "height": 30})).unwrap());
|
||||
let b = id_of(&app.run("shape.rectangle", json!({"x": 100, "y": 120, "width": 30, "height": 30})).unwrap());
|
||||
app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap();
|
||||
app.run("object.group", json!({})).unwrap();
|
||||
app.run("text.create", json!({"x": 200, "y": 150, "text": "Mark", "size": 20})).unwrap();
|
||||
let art = id_of(&app.run("shape.rectangle", json!({"x": 280, "y": 200, "width": 20, "height": 20})).unwrap());
|
||||
app.run("symbol.new", json!({"name": "Mark", "ids": [art.0]})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
(app, curve)
|
||||
}
|
||||
|
||||
fn xf(zoom: f64, center: (f64, f64), rot_degrees: f64) -> Xf {
|
||||
Xf {
|
||||
rect: egui::Rect::from_min_size(Pos2::ZERO, vec2(SIZE.0, SIZE.1)),
|
||||
zoom,
|
||||
center: Point::new(center.0, center.1),
|
||||
rot: rot_degrees.to_radians(),
|
||||
}
|
||||
}
|
||||
|
||||
const VIEW: (f64, f64) = (200.0, 150.0);
|
||||
|
||||
/// The marks drawn one shape at a time, as a frame draws them with nothing kept.
|
||||
fn plain(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool) {
|
||||
let st = app.session.active().unwrap();
|
||||
let MarkOptions { direct, curvature, look, all_handles, .. } = mark_options(app, curve_points_from_tool);
|
||||
let symbols = st.doc.symbol_boxes();
|
||||
let anchor = |direct: bool| look.grow + if direct { 5.0 } else { 4.0 };
|
||||
let (mut handles, mut with_handles) = (vec![], 0);
|
||||
if let Some((k, n)) = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))) {
|
||||
for line in to_screen_path(&node_outline(n, &symbols), xf) {
|
||||
p.add(Shape::line(line, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k)))));
|
||||
}
|
||||
}
|
||||
for id in &st.selection.objects {
|
||||
let Some(n) = st.doc.node(*id) else { continue };
|
||||
let color = c32(st.doc.layer_color(*id));
|
||||
let partial = st.selection.partial(*id);
|
||||
let stroke = |bp: &BezPath, width: f32| {
|
||||
for line in to_screen_path(bp, xf) {
|
||||
p.add(Shape::line(line, Stroke::new(width, color)));
|
||||
}
|
||||
};
|
||||
if !matches!(n.kind, NodeKind::Text(ref tx) if matches!(tx.kind, vectorcraft_doc::TextKind::Point)) {
|
||||
stroke(&node_outline(n, &symbols), 1.0);
|
||||
}
|
||||
walk_drawn(n, &mut |c| {
|
||||
let envelope = vectorcraft_doc::live::envelope_overlay(c).and_then(|(_, grid)| grid);
|
||||
let grid = match &c.kind {
|
||||
NodeKind::Mesh(m) => Some(m),
|
||||
_ => envelope.as_ref(),
|
||||
};
|
||||
if let Some(grid) = grid {
|
||||
for q in &grid.points {
|
||||
anchor_square(p, xf.to_screen(q.p), color, false, anchor(direct));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let NodeKind::Path { path, .. } = &c.kind else { return };
|
||||
if curvature && curve_points_from_tool {
|
||||
return;
|
||||
}
|
||||
let selected_segments = st.selection.segments.get(&c.id);
|
||||
if let Some(segments) = selected_segments {
|
||||
for &(si, seg) in segments {
|
||||
let Some(sp) = path.subpaths.get(si).filter(|sp| seg < sp.segment_count()) else { continue };
|
||||
let curve = sp.segment(seg);
|
||||
let mut outline = vectorcraft_geom::BezPath::new();
|
||||
outline.move_to(curve.p0);
|
||||
if sp.segment_is_line(seg) {
|
||||
outline.line_to(curve.p3);
|
||||
} else {
|
||||
outline.curve_to(curve.p1, curve.p2, curve.p3);
|
||||
}
|
||||
stroke(&outline, 2.0);
|
||||
}
|
||||
}
|
||||
for (si, ai, a) in path.anchors() {
|
||||
let (sel, handles_shown) = match partial {
|
||||
Some(set) => {
|
||||
let sel = set.contains(&(si, ai));
|
||||
(sel, sel)
|
||||
}
|
||||
None => (true, direct && c.id == *id),
|
||||
};
|
||||
let sp = xf.to_screen(a.p);
|
||||
if !curvature && handles_shown {
|
||||
with_handles += 1;
|
||||
let sides = path.subpaths.get(si).map(|subpath| st.selection.handle_sides(c.id, si, ai, subpath)).unwrap_or((true, true));
|
||||
handles.extend(
|
||||
[(a.h_in, sides.0), (a.h_out, sides.1)]
|
||||
.into_iter()
|
||||
.filter(|(h, show)| *show && h.distance(a.p) > 1e-6)
|
||||
.map(|(h, _)| (sp, xf.to_screen(h), color)),
|
||||
);
|
||||
}
|
||||
let anchor_selected = sel && selected_segments.is_none();
|
||||
let ink = if anchor_selected && partial.is_some() { selected_anchor(color) } else { color };
|
||||
if curvature {
|
||||
curve_point(p, sp, ink, anchor_selected && partial.is_some(), look.grow + 6.0);
|
||||
} else {
|
||||
anchor_square(p, sp, ink, anchor_selected, anchor(partial.is_some() || direct));
|
||||
}
|
||||
}
|
||||
});
|
||||
if n.shows_center()
|
||||
&& let Some(b) = n.geometric_bounds_with(&symbols)
|
||||
{
|
||||
anchor_square(p, xf.to_screen(b.center()), color, true, anchor(false));
|
||||
}
|
||||
if let NodeKind::Text(tx) = &n.kind
|
||||
&& matches!(tx.kind, vectorcraft_doc::TextKind::Point)
|
||||
{
|
||||
for (a, b) in tx.baselines() {
|
||||
p.line_segment([xf.to_screen(tx.xf * a), xf.to_screen(tx.xf * b)], Stroke::new(1.0, color));
|
||||
}
|
||||
p.circle_filled(xf.to_screen(tx.xf * Point::ZERO), 2.5, color);
|
||||
}
|
||||
}
|
||||
if all_handles || with_handles <= 1 {
|
||||
for (sp, hp, color) in handles {
|
||||
p.line_segment([sp, hp], Stroke::new(1.0, color));
|
||||
for shape in look.shapes(hp, color).into_iter().flatten() {
|
||||
p.add(shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied pixels of a frame.
|
||||
struct Picture {
|
||||
size: (usize, usize),
|
||||
pixels: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// The font texture at `uv`, filtered linearly as the GPU does.
|
||||
fn sample(atlas: &egui::ColorImage, uv: (f32, f32)) -> [f32; 4] {
|
||||
let (w, h) = (atlas.width(), atlas.height());
|
||||
let (x, y) = (uv.0 * w as f32 - 0.5, uv.1 * h as f32 - 0.5);
|
||||
let (fx, fy) = (x - x.floor(), y - y.floor());
|
||||
let texel =
|
||||
|dx: f32, dy: f32| atlas[(((x.floor() + dx).max(0.0) as usize).min(w - 1), ((y.floor() + dy).max(0.0) as usize).min(h - 1))].to_array();
|
||||
let (a, b, c, d) = (texel(0.0, 0.0), texel(1.0, 0.0), texel(0.0, 1.0), texel(1.0, 1.0));
|
||||
std::array::from_fn(|i| {
|
||||
let top = f32::from(a[i]) * (1.0 - fx) + f32::from(b[i]) * fx;
|
||||
let bottom = f32::from(c[i]) * (1.0 - fx) + f32::from(d[i]) * fx;
|
||||
top * (1.0 - fy) + bottom * fy
|
||||
})
|
||||
}
|
||||
|
||||
fn rasterize(primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, atlas: &egui::ColorImage) -> Picture {
|
||||
let size = ((SIZE.0 * pixels_per_point).ceil() as usize, (SIZE.1 * pixels_per_point).ceil() as usize);
|
||||
let mut pixels = vec![[0.0f32; 4]; size.0 * size.1];
|
||||
// Vertices snap to 1/256 of a pixel, as on a GPU, so that the edge tests are exact and a pixel
|
||||
// center on an edge two triangles share belongs to exactly one of them.
|
||||
const GRID: f32 = 256.0;
|
||||
let cross =
|
||||
|a: (i64, i64), b: (i64, i64), c: (i64, i64)| i128::from(b.0 - a.0) * i128::from(c.1 - a.1) - i128::from(b.1 - a.1) * i128::from(c.0 - a.0);
|
||||
for primitive in primitives {
|
||||
let Primitive::Mesh(mesh) = &primitive.primitive else { continue };
|
||||
let clip = primitive.clip_rect;
|
||||
for [i, j, k] in mesh.indices.as_chunks::<3>().0 {
|
||||
let mut v = [mesh.vertices[*i as usize], mesh.vertices[*j as usize], mesh.vertices[*k as usize]];
|
||||
let at = |v: &Vertex| ((v.pos.x * pixels_per_point * GRID).round() as i64, (v.pos.y * pixels_per_point * GRID).round() as i64);
|
||||
let area = cross(at(&v[0]), at(&v[1]), at(&v[2]));
|
||||
if area == 0 {
|
||||
continue;
|
||||
}
|
||||
if area < 0 {
|
||||
v.swap(1, 2);
|
||||
}
|
||||
let area = area.abs() as f32;
|
||||
let [a, b, c] = [at(&v[0]), at(&v[1]), at(&v[2])];
|
||||
let range = |lo: i64, hi: i64, clip_lo: f32, clip_hi: f32, n: usize| {
|
||||
let lo = (lo as f32 / GRID).max(clip_lo * pixels_per_point).floor().max(0.0) as usize;
|
||||
let hi = ((hi as f32 / GRID).min(clip_hi * pixels_per_point).ceil().max(0.0) as usize).min(n);
|
||||
lo..hi
|
||||
};
|
||||
let upper_left = |p: (i64, i64), q: (i64, i64)| q.1 < p.1 || (q.1 == p.1 && q.0 > p.0);
|
||||
for y in range(a.1.min(b.1).min(c.1), a.1.max(b.1).max(c.1), clip.min.y, clip.max.y, size.1) {
|
||||
for x in range(a.0.min(b.0).min(c.0), a.0.max(b.0).max(c.0), clip.min.x, clip.max.x, size.0) {
|
||||
let q = (x as i64 * 256 + 128, y as i64 * 256 + 128);
|
||||
let (w0, w1, w2) = (cross(b, c, q), cross(c, a, q), cross(a, b, q));
|
||||
let inside = |w: i128, p, q| w > 0 || (w == 0 && upper_left(p, q));
|
||||
if !(inside(w0, b, c) && inside(w1, c, a) && inside(w2, a, b)) {
|
||||
continue;
|
||||
}
|
||||
let (w0, w1, w2) = (w0 as f32 / area, w1 as f32 / area, w2 as f32 / area);
|
||||
let uv = (w0 * v[0].uv.x + w1 * v[1].uv.x + w2 * v[2].uv.x, w0 * v[0].uv.y + w1 * v[1].uv.y + w2 * v[2].uv.y);
|
||||
let texel = sample(atlas, uv);
|
||||
let dst = &mut pixels[y * size.0 + x];
|
||||
let channels: [[u8; 4]; 3] = [v[0].color.to_array(), v[1].color.to_array(), v[2].color.to_array()];
|
||||
let src: [f32; 4] = std::array::from_fn(|i| {
|
||||
(w0 * f32::from(channels[0][i]) + w1 * f32::from(channels[1][i]) + w2 * f32::from(channels[2][i])) * texel[i] / 255.0
|
||||
});
|
||||
let keep = 1.0 - src[3] / 255.0;
|
||||
for i in 0..4 {
|
||||
dst[i] = src[i] + dst[i] * keep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Picture { size, pixels }
|
||||
}
|
||||
|
||||
fn picture(ctx: &egui::Context, pixels_per_point: f32, draw: impl FnOnce(&egui::Painter)) -> Picture {
|
||||
// The window is larger than the picture at each display scale, and a new scale applies to the
|
||||
// pass after it is set.
|
||||
let raw = || egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(4.0 * SIZE.0, 4.0 * SIZE.1))),
|
||||
max_texture_side: Some(8192),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.set_pixels_per_point(pixels_per_point);
|
||||
ctx.run_ui(raw(), |_| {}).textures_delta.clear();
|
||||
let mut draw = Some(draw);
|
||||
let mut out = ctx.run_ui(raw(), |ui| {
|
||||
if let Some(draw) = draw.take() {
|
||||
draw(&ui.painter_at(egui::Rect::from_min_size(Pos2::ZERO, vec2(SIZE.0, SIZE.1))));
|
||||
}
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let primitives = ctx.tessellate(std::mem::take(&mut out.shapes), out.pixels_per_point);
|
||||
rasterize(&primitives, out.pixels_per_point, &ctx.fonts(|f| f.image()))
|
||||
}
|
||||
|
||||
/// The most a color channel may differ, out of 255: kept vertices move by rounding errors far under
|
||||
/// a pixel.
|
||||
const CHANNEL: f32 = 3.0;
|
||||
|
||||
fn assert_same(a: &Picture, b: &Picture, what: &str, allowed_per_mille: usize) {
|
||||
assert_eq!(a.size, b.size, "{what}");
|
||||
let drawn = b.pixels.iter().filter(|p| p[3] > 0.0).count();
|
||||
assert!(drawn > 0, "{what}: the picture is empty");
|
||||
let differ: Vec<usize> = (0..b.pixels.len()).filter(|i| a.pixels[*i].iter().zip(&b.pixels[*i]).any(|(x, y)| (x - y).abs() > CHANNEL)).collect();
|
||||
let first: Vec<_> = differ.iter().take(4).map(|i| ((i % a.size.0, i / a.size.0), a.pixels[*i], b.pixels[*i])).collect();
|
||||
assert!(
|
||||
differ.len() * 1000 <= drawn * allowed_per_mille,
|
||||
"{what}: {} of {drawn} drawn pixels differ, first at (pixel, got, want): {first:?}",
|
||||
differ.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn paint(marks: &mut SelectionMarks, app: &VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool) {
|
||||
let st = app.session.active().unwrap();
|
||||
let key = st.selection.key;
|
||||
marks.paint(st, p, xf, &mark_options(app, curve_points_from_tool), key, &st.selection.objects);
|
||||
}
|
||||
|
||||
/// Kept marks, marks built from nothing and plain shapes all draw the same pixels. Kept marks may
|
||||
/// differ at up to 1 pixel in 1,000: at a corner of exactly 90 degrees on a rotated view, plain
|
||||
/// shapes take a miter or a cut corner by a rounding error of the corner's position, which a pan
|
||||
/// changes, and moved triangles keep the corner they were built with. An anchor square left stale
|
||||
/// covers more than this limit.
|
||||
#[track_caller]
|
||||
fn assert_matches_plain(ctx: &egui::Context, marks: &mut SelectionMarks, app: &VectorcraftApp, xf: &Xf, pixels_per_point: f32, what: &str) {
|
||||
let want = picture(ctx, pixels_per_point, |p| plain(app, p, xf, false));
|
||||
for frame in ["this frame", "the next frame"] {
|
||||
let kept = picture(ctx, pixels_per_point, |p| paint(marks, app, p, xf, false));
|
||||
assert_same(&kept, &want, &format!("{what}, kept, {frame}"), 1);
|
||||
}
|
||||
let mut fresh = SelectionMarks::default();
|
||||
for frame in ["drawn once", "built from nothing"] {
|
||||
let cold = picture(ctx, pixels_per_point, |p| paint(&mut fresh, app, p, xf, false));
|
||||
assert_same(&cold, &want, &format!("{what}, {frame}"), 0);
|
||||
}
|
||||
assert!(fresh.builds > 0, "{what}: the second frame builds the marks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_draw_the_same_pixels_as_plain_shapes_at_each_view() {
|
||||
let (mut app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
for tool in ["selection", "directSelection", "curvature"] {
|
||||
app.select_tool(tool);
|
||||
for pixels_per_point in [1.0, 1.25, 2.0] {
|
||||
let mut marks = SelectionMarks::default();
|
||||
for zoom in [0.5, 1.0, 2.37, 8.0] {
|
||||
for rot in [0.0, 30.0, 90.0] {
|
||||
let view = xf(zoom, VIEW, rot);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, pixels_per_point, &format!("{tool} {pixels_per_point}x {zoom} {rot}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pan_moves_the_marks_without_building_them() {
|
||||
let (mut app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
for tool in ["selection", "directSelection"] {
|
||||
app.select_tool(tool);
|
||||
for (pixels_per_point, rot) in [(1.0, 0.0), (1.25, 0.0), (2.0, 30.0)] {
|
||||
let mut marks = SelectionMarks::default();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &xf(1.5, VIEW, rot), pixels_per_point, &format!("{tool}, before the pan"));
|
||||
let builds = marks.builds;
|
||||
for (dx, dy) in [(10.0, 0.0), (3.0, 7.0), (0.37, 0.61), (-31.3, 12.9), (160.0, 90.0)] {
|
||||
let view = xf(1.5, (VIEW.0 + dx, VIEW.1 + dy), rot);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, pixels_per_point, &format!("{tool}, {pixels_per_point}x pan {dx} {dy}"));
|
||||
}
|
||||
assert_eq!(marks.builds, builds, "{tool}: a pan builds nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_pan_at_high_zoom_builds_the_marks_again_and_stays_exact() {
|
||||
let (app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
// 2,000 points per document point: the art runs far past what `f32` holds to a fine fraction.
|
||||
let zoom = 2_000.0;
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &xf(zoom, (180.0, 41.0), 0.0), 1.0, "the top of the curve's straight end");
|
||||
let builds = marks.builds;
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &xf(zoom, (180.0, 89.0), 0.0), 1.0, "96,000 points along the straight end");
|
||||
assert!(marks.builds > builds, "a pan past MAX_PAN builds the marks again");
|
||||
}
|
||||
|
||||
/// Marks land exactly where the rest of the canvas puts a point, also at the canvas's far edge at
|
||||
/// the highest zoom.
|
||||
#[test]
|
||||
fn marks_are_placed_exactly_where_the_view_puts_a_point() {
|
||||
let far = vectorcraft_doc::MAX_COORD - 1.0;
|
||||
for (center, zoom, rot) in [(VIEW, 1.0, 0.0), (VIEW, 2.37, 30.0), ((far, far), 640.0, 0.0), ((far, -far), 640.0, 30.0)] {
|
||||
let view = xf(zoom, center, rot);
|
||||
let map = ViewMap::new(&view);
|
||||
for i in 0..2_000 {
|
||||
let t = f64::from(i) / 7.0;
|
||||
let p = Point::new(center.0 + t.sin() * 0.3, center.1 + t.cos() * 0.2);
|
||||
assert_eq!(map.to_screen(p), view.to_screen(p), "{p:?} at zoom {zoom}, rotation {rot}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_idle_frame_draws_the_mesh_it_joined() {
|
||||
let (app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
let view = xf(1.0, VIEW, 0.0);
|
||||
picture(&ctx, 1.0, |p| paint(&mut marks, &app, p, &view, false));
|
||||
assert_eq!((marks.builds, marks.composes), (0, 0), "the first frame at a view keeps nothing");
|
||||
picture(&ctx, 1.0, |p| paint(&mut marks, &app, p, &view, false));
|
||||
let (builds, composes) = (marks.builds, marks.composes);
|
||||
assert!(builds > 0 && composes == 1);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "an idle frame");
|
||||
assert_eq!((marks.builds, marks.composes), (builds, composes), "an idle frame builds and joins nothing");
|
||||
}
|
||||
|
||||
/// While the zoom or rotation changes in each frame, nothing is built or kept. The picture stays the
|
||||
/// same, and the first frame that repeats a view builds the marks.
|
||||
#[test]
|
||||
fn a_zoom_keeps_nothing_until_the_view_stays() {
|
||||
let (mut app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
app.select_tool("directSelection");
|
||||
let mut marks = SelectionMarks::default();
|
||||
let settled = xf(1.5, VIEW, 0.0);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &settled, 1.0, "before the zoom");
|
||||
let builds = marks.builds;
|
||||
for (zoom, rot) in [(1.6, 0.0), (1.7, 0.0), (1.7, 15.0), (1.9, 15.0), (2.37, 40.0)] {
|
||||
let view = xf(zoom, VIEW, rot);
|
||||
let want = picture(&ctx, 1.0, |p| plain(&app, p, &view, false));
|
||||
let drawn = picture(&ctx, 1.0, |p| paint(&mut marks, &app, p, &view, false));
|
||||
assert_same(&drawn, &want, &format!("zoom {zoom}, rotation {rot}"), 0);
|
||||
assert_eq!(marks.builds, builds, "zoom {zoom}, rotation {rot} builds nothing");
|
||||
assert!(marks.objects.is_empty() && marks.frame.is_empty(), "zoom {zoom}, rotation {rot} keeps nothing");
|
||||
}
|
||||
let last = xf(2.37, VIEW, 40.0);
|
||||
let want = picture(&ctx, 1.0, |p| plain(&app, p, &last, false));
|
||||
let drawn = picture(&ctx, 1.0, |p| paint(&mut marks, &app, p, &last, false));
|
||||
assert_same(&drawn, &want, "the same view again", 1);
|
||||
assert!(marks.builds > builds && !marks.objects.is_empty(), "the same view again builds the marks");
|
||||
}
|
||||
|
||||
/// Whether every vertex of the kept frame lies within a few pixels of the picture.
|
||||
fn all_near_the_picture(marks: &SelectionMarks) -> bool {
|
||||
let near = egui::Rect::from_min_size(Pos2::ZERO, vec2(SIZE.0, SIZE.1)).expand(8.0);
|
||||
marks.frame.vertices.iter().all(|v| near.contains(v.pos))
|
||||
}
|
||||
|
||||
/// A group of rectangles 1,000 points apart keeps triangles only for the ones in view. One that
|
||||
/// comes into view gets its triangles then.
|
||||
#[test]
|
||||
fn a_frame_holds_no_triangles_of_a_groups_subpaths_out_of_view() {
|
||||
for tool in ["selection", "directSelection"] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
for i in 0..12 {
|
||||
app.run("shape.rectangle", json!({"x": 40 + i * 1000, "y": 40, "width": 60, "height": 40})).unwrap();
|
||||
}
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("object.group", json!({})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.select_tool(tool);
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
for (dx, seen) in [(0.0, 1), (1000.0, 2), (1000.37, 2), (11_000.0, 3), (0.0, 3)] {
|
||||
let view = xf(1.0, (VIEW.0 + dx, VIEW.1), 0.0);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, &format!("{tool}, pan {dx}"));
|
||||
assert!(!marks.frame.is_empty(), "{tool}, pan {dx}: a rectangle is in view");
|
||||
assert!(all_near_the_picture(&marks), "{tool}, pan {dx}: triangles of a rectangle out of view");
|
||||
let waiting: usize = marks
|
||||
.objects
|
||||
.values()
|
||||
.flat_map(|entry| &entry.marks)
|
||||
.map(|mark| match mark {
|
||||
Mark::Rigid { pieces, .. } => pieces.iter().filter(|piece| piece.shape.is_some()).count(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
assert_eq!(waiting, 12 - seen, "{tool}, pan {dx}: rectangles not yet in view get no triangles");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many triangles the direction handles add to a frame of a direct-selected path with `anchors`
|
||||
/// anchors 1,000 points apart.
|
||||
fn handle_triangles(anchors: usize, view: &Xf) -> (usize, VectorcraftApp, SelectionMarks, egui::Context) {
|
||||
let mut d = String::from("M40 150");
|
||||
for i in 0..anchors - 1 {
|
||||
let (from, to) = (40 + i * 1000, 40 + (i + 1) * 1000);
|
||||
// Handles alternate between level, which the tessellator rounds to whole pixels, and slanted.
|
||||
let y = if i % 2 == 0 { 150 } else { 110 };
|
||||
d += &format!(" C{} {y} {} 190 {to} 150", from + 30, to - 30);
|
||||
}
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("path.create", json!({"d": d})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.select_tool("directSelection");
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
let mut triangles = |app: &mut VectorcraftApp, all_handles: bool| {
|
||||
app.session.prefs.show_handles_multiple_anchors = all_handles;
|
||||
for _ in 0..2 {
|
||||
picture(&ctx, 1.0, |p| paint(&mut marks, app, p, view, false));
|
||||
}
|
||||
marks.frame.indices.len() / 3
|
||||
};
|
||||
let without = triangles(&mut app, false);
|
||||
let with = triangles(&mut app, true);
|
||||
(with - without, app, marks, ctx)
|
||||
}
|
||||
|
||||
/// A frame of a long path shows the handles in view and holds the triangles of those alone, exactly
|
||||
/// as many as a path that ends just past the view. Handles that come into view draw correctly, level
|
||||
/// ones after a move by a fraction of a pixel too.
|
||||
#[test]
|
||||
fn a_frame_holds_no_triangles_of_handles_out_of_view() {
|
||||
let view = xf(1.0, VIEW, 0.0);
|
||||
let (short, ..) = handle_triangles(2, &view);
|
||||
let (long, app, mut marks, ctx) = handle_triangles(40, &view);
|
||||
assert!(short > 0, "the handle in view draws");
|
||||
assert_eq!(long, short, "the long path's handles out of view add nothing");
|
||||
for (dx, dy) in [(0.0, 0.0), (0.0, 0.37), (1000.0, 0.0), (1000.61, 0.61), (2000.0, 0.0), (2000.0, 0.5), (5000.5, 0.25), (0.0, 0.0)] {
|
||||
let view = xf(1.0, (VIEW.0 + dx, VIEW.1 + dy), 0.0);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, &format!("pan {dx} {dy}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_stale_marks_after_a_change_with_the_selection_tool() {
|
||||
no_stale_marks_after_a_change("selection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_stale_marks_after_a_change_with_the_direct_selection_tool() {
|
||||
no_stale_marks_after_a_change("directSelection");
|
||||
}
|
||||
|
||||
/// Each change draws what plain shapes draw, and builds no more than the change touches.
|
||||
fn no_stale_marks_after_a_change(tool: &str) {
|
||||
let (mut app, curve) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
let view = xf(1.5, VIEW, 30.0);
|
||||
app.select_tool(tool);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "at the start");
|
||||
let all = app.session.active().unwrap().selection.objects.clone();
|
||||
let ids = |list: &[NodeId]| json!({"ids": list.iter().map(|i| i.0).collect::<Vec<_>>()});
|
||||
|
||||
// An edit of one object builds that object, and the symbol instance that measures the art of a
|
||||
// document at a new revision.
|
||||
let builds = marks.builds;
|
||||
app.run("select.set", json!({"ids": [curve.0]})).unwrap();
|
||||
app.run("object.nudge", json!({"dx": 12, "dy": -5})).unwrap();
|
||||
app.run("select.set", ids(&all)).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after an edit");
|
||||
assert_eq!(marks.builds, builds + 2, "an edit of one object leaves the others as they were");
|
||||
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.run("select.set", ids(&all)).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after an undo");
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
app.run("select.set", ids(&all)).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after a redo");
|
||||
|
||||
// A layer color.
|
||||
let layer = app.session.active().unwrap().doc.layers[0].id;
|
||||
app.run("layer.setProps", json!({"id": layer.0, "color": "#ff00aa"})).unwrap();
|
||||
app.run("select.set", ids(&all)).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after a layer color change");
|
||||
|
||||
// A change of the selection: fewer objects, other anchors, a segment, the key object.
|
||||
app.run("select.set", ids(&all[..2])).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with fewer objects");
|
||||
assert_eq!(marks.objects.len(), 2, "an object that left the selection has no entry");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
st.selection.anchors.insert(curve, [(0, 1)].into_iter().collect());
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with one anchor of the curve selected");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
st.selection.anchors.insert(curve, [(0, 2), (0, 3)].into_iter().collect());
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with other anchors of the curve selected");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
st.selection.segments.insert(curve, [(0, 2)].into_iter().collect());
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with a segment of the curve selected");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
st.selection.segments.clear();
|
||||
st.selection.anchors.clear();
|
||||
st.selection.key = Some(curve);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with a key object");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
st.selection.key = Some(all[1]);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with another key object");
|
||||
app.session.active_mut().unwrap().selection.key = None;
|
||||
|
||||
// The tool and the display preferences.
|
||||
app.select_tool(if tool == "selection" { "directSelection" } else { "selection" });
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with the other tool");
|
||||
app.select_tool(tool);
|
||||
app.session.prefs.anchor_size = 6;
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with larger anchors");
|
||||
app.session.prefs.handle_style = "hollow".into();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with hollow handles");
|
||||
app.session.prefs.show_handles_multiple_anchors = !app.session.prefs.show_handles_multiple_anchors;
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with the handles option changed");
|
||||
app.session.prefs.anchor_size = 1;
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "with the smallest anchors, which draw as lines");
|
||||
}
|
||||
|
||||
/// An instance's outline comes from its symbol's art, which is not part of the instance's node.
|
||||
#[test]
|
||||
fn an_edit_of_a_symbol_outlines_its_instance_again() {
|
||||
let (mut app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
let view = xf(2.0, VIEW, 0.0);
|
||||
let instance = {
|
||||
let st = app.session.active().unwrap();
|
||||
st.selection.objects.iter().copied().find(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::SymbolInstance { .. }))).unwrap()
|
||||
};
|
||||
app.run("select.set", json!({"ids": [instance.0]})).unwrap();
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "before the edit");
|
||||
let before = app.session.active().unwrap().doc.node(instance).unwrap().geometric_bounds_with(&app.session.active().unwrap().doc.symbol_boxes());
|
||||
app.run("symbol.edit", json!({"name": "Mark"})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("object.scale", json!({"sx": 3, "sy": 2})).unwrap();
|
||||
app.run("symbol.exitEditing", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [instance.0]})).unwrap();
|
||||
let after = app.session.active().unwrap().doc.node(instance).unwrap().geometric_bounds_with(&app.session.active().unwrap().doc.symbol_boxes());
|
||||
assert_ne!(before, after, "the edit changes what the instance measures");
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after the edit");
|
||||
}
|
||||
|
||||
/// A frame of the whole canvas keeps entries for the selected objects only.
|
||||
#[test]
|
||||
fn the_canvas_drops_entries_it_does_not_draw() {
|
||||
let (mut app, curve) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut VectorcraftApp| {
|
||||
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(&mut app);
|
||||
frame(&mut app);
|
||||
let all = app.session.active().unwrap().selection.objects.len();
|
||||
assert_eq!(app.canvas.selection_marks.objects.len(), all);
|
||||
app.run("select.set", json!({"ids": [curve.0]})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.canvas.selection_marks.objects.len(), 1, "objects that left the selection");
|
||||
app.run("edit.clear", json!({"ids": [curve.0]})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.canvas.selection_marks.objects.len(), 0, "an object that left the document");
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
frame(&mut app);
|
||||
frame(&mut app);
|
||||
assert!(!app.canvas.selection_marks.objects.is_empty());
|
||||
app.run("view.edges", json!({})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.canvas.selection_marks.objects.len(), 0, "hidden edges keep nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn past_the_limit_the_marks_are_dropped_and_built_again() {
|
||||
let (app, _) = scene();
|
||||
let ctx = egui::Context::default();
|
||||
let mut marks = SelectionMarks::default();
|
||||
let view = xf(1.0, VIEW, 0.0);
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "at the limit");
|
||||
assert!(marks.kept > 0);
|
||||
marks.trim(marks.kept);
|
||||
assert!(!marks.objects.is_empty(), "at the limit the marks stay");
|
||||
marks.trim(marks.kept - 1);
|
||||
assert!(marks.objects.is_empty() && marks.frame.is_empty(), "past the limit nothing stays");
|
||||
assert_matches_plain(&ctx, &mut marks, &app, &view, 1.0, "after dropping them");
|
||||
}
|
||||
@@ -1621,6 +1621,9 @@ fn frame_output(app: &mut VectorcraftApp, ctx: &egui::Context) -> (Vec<Shape>, e
|
||||
s => out.push(s),
|
||||
}
|
||||
}
|
||||
// With no kept marks the frame draws the selection's marks as separate shapes, which callers
|
||||
// read. `selection_marks::tests` compares them with the kept mesh.
|
||||
app.canvas.selection_marks = Default::default();
|
||||
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
let out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
let mut v = vec![];
|
||||
|
||||
@@ -1332,6 +1332,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The renderer keeps the path of a path or compound path of 1,024 anchors or more by object, with the bounds of its segments and of its miter spikes in runs of 128 segments. When anchors move and the subpaths and anchor counts stay the same, the next frame changes only the segments that start or end at a moved anchor and measures again only the runs that hold them or the segments with length before them, instead of making the whole path again and measuring it whole for culling. The pixels and the cull bounds are the same. On Windows at 1600 × 1200, the frame after one anchor moves takes 8.2 ms instead of 30.1 ms with an open path of 200,000 anchors, 4.3 ms instead of 10.4 ms with a closed path of 50,000 anchors, and 6.6 ms instead of 11.9 ms with the filled closed path of 50,000 anchors and a dashed stroke (medians of five runs of `anchor_bench`).
|
||||
Type › Show Hidden Characters keeps the marks it places on each text object from one frame to the next. The canvas lays a text object out again when an edit changes it or its threaded story, or when the last frame that showed the marks did not draw it, and it lays out all type in view again when the fonts or the hyphenation exceptions change. With 1,000 two-line labels in view, drawing the marks takes 0.8 ms per frame in a release build instead of 19.7 ms.
|
||||
A selection with more than 100,000 points to outline shows its bounds instead of each object's outline and anchors. Paths count their anchors. Area type and type on a path count the anchors of their frame or path, and point type counts six: its box's corners and the ends of its baseline marker. Images, symbol instances, placed documents, and area type or type on a path in perspective count the four corners of their box, and gradient meshes and envelopes count their points and the ends of their mesh lines. The hover highlight leaves out an object past that count, and such an object shows its bounds as the key object. With 32,768 type objects, images, symbol instances, gradient meshes or envelopes selected, the selection's outlines and anchors add fewer than 100 vertices to the canvas's mesh (#2460).
|
||||
The canvas keeps the selected objects' outlines, anchors and handles from one frame to the next as one mesh. It builds an object's marks again when the object, its layer color or its selected anchors change. A pan moves the kept outlines and handles and places the anchor squares again. Each subpath and handle gets its triangles the first time it is in view. A frame with a new zoom or rotation draws the marks one shape at a time and keeps nothing, and the next frame at that view builds them. The pixels are the same, apart from two small differences: a kept outline's corner of exactly 90 degrees in a rotated view can keep its miter or cut corner after a pan, and kept vertices can move by rounding errors. On Windows at 1440 × 900, in a release build, with 2,000 selected circles of 50 anchors each in 10 fill colors, an idle frame takes 12.8 ms instead of 59.9 ms with the Selection tool and 29.8 ms instead of 192.1 ms with Direct Selection (medians of five runs). A frame during a zoom takes about what it did: 86.7 ms instead of 80.0 ms with the Selection tool.
|
||||
Delete, Cut, Group, Ungroup, Make and Release Compound Path, Make Clipping Mask, Send to Current Layer, Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs, Transform Each with Copy and each move of an Alt-drag take time in proportion to the size of the document (#1429).
|
||||
In the web build the status bar's render and UI times are measured with the browser's `performance.now()`, and the Layers panel stops rendering row thumbnails in a frame once they have taken 8 ms.
|
||||
On the desktop, the Layers panel renders a row's thumbnail on its thumbnail thread when the row's paths, and the paths of its opacity masks that are not disabled, hold more than 2,000 anchors in all. It already did this for the first thumbnail of a row of more than 200 objects. Until its first thumbnail is ready, the row shows boxes at the bounds of up to 41 of its paths, each in its path's solid fill or stroke color or in gray, when its own paths, without its opacity masks, hold at most 2,000 anchors in all, and an empty white box otherwise. With one path of 200,000 anchors, the first frame after the panel opens takes 2.4 ms instead of 393 ms (release build, median of three runs).
|
||||
|
||||
Reference in New Issue
Block a user