mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
3D and Materials: one element limit per effect, pasted graphics keep their symbols, safer handle drags
- The graphics of one 3D effect share one limit on drawn path elements across every member and opacity group, so a group effect with many members no longer multiplies the limit. - Copy and paste follow the symbols that mapped graphics name, in object and graphic style appearances: a document without the symbol gets it, and one whose symbol of the same name is other art gets the copied symbol under a free name, which the graphic then names. - A face's inverse map refuses the points its page map refuses, so a handle drag cannot save an offset whose handle is not shown. - A press on a graphic's handle with Space held, or with the Hand, Zoom or Rotate View tool, pans, zooms or turns the view. - A handle drag ends, keeping its preview, when the selection or the document changes while the pointer is down.
This commit is contained in:
@@ -812,7 +812,8 @@ type Cast<'a> = (vectorcraft_three_d::ShadowCaster, vectorcraft_three_d::Shadow,
|
||||
/// for each planar face with a graphic, the art and the face. `caps` are the faces the viewer sees
|
||||
/// of the ends of the object's solids, each with its end; `bounds` are the bounds the solids were
|
||||
/// built with; `opacity` is the opacity of the fill or stroke those solids are painted with, which
|
||||
/// the graphics take too.
|
||||
/// the graphics take too. `elements` is the effect's count of drawn path elements, shared by every
|
||||
/// call for one effect ([`graphics::face_art`]).
|
||||
fn graphic_art(
|
||||
doc: Option<&Document>,
|
||||
id: NodeId,
|
||||
@@ -820,6 +821,7 @@ fn graphic_art(
|
||||
bounds: Rect,
|
||||
caps: &[&Facet],
|
||||
opacity: f32,
|
||||
elements: &mut usize,
|
||||
) -> Vec<(PlanarFace, Arc<Node>)> {
|
||||
let list = graphics::mapped_graphics(p);
|
||||
if list.is_empty() || !takes_graphics(effect) {
|
||||
@@ -835,7 +837,7 @@ fn graphic_art(
|
||||
} else {
|
||||
vectorcraft_three_d::extrude_face_map(bounds, extrude_options(p), view, face)
|
||||
}?;
|
||||
graphics::face_art(doc, id, &list, face, &map, bounds.center(), &on).map(|mut art| {
|
||||
graphics::face_art(doc, id, &list, (face, &map), bounds.center(), &on, elements).map(|mut art| {
|
||||
art.opacity *= opacity;
|
||||
(face, Arc::new(art))
|
||||
})
|
||||
@@ -864,6 +866,8 @@ fn masks_surface(paint: &Paint, colored: bool) -> bool {
|
||||
/// drawn right after (the last of those solids holding its face) and its face. With `per_member`
|
||||
/// (a member's opacity mask or blending mode applies to its surfaces) each member's solids get
|
||||
/// graphics of their own, clipped to that member's caps, so they stay in that member's scopes.
|
||||
/// All the groups share one limit on the path elements drawn: once it is reached, later groups
|
||||
/// draw no graphics.
|
||||
fn placed_graphics(
|
||||
doc: Option<&Document>,
|
||||
id: NodeId,
|
||||
@@ -886,11 +890,12 @@ fn placed_graphics(
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let mut elements = 0usize;
|
||||
for group in groups {
|
||||
let caps: Vec<&Facet> = group.iter().flat_map(|f| &f.caps).collect();
|
||||
let opacity = group.first().map_or(1.0, |f| f.opacity);
|
||||
let blend = group.first().map_or(BlendMode::Normal, |f| f.blend);
|
||||
for (face, mut art) in graphic_art(doc, id, effect, bounds, &caps, opacity) {
|
||||
for (face, mut art) in graphic_art(doc, id, effect, bounds, &caps, opacity, &mut elements) {
|
||||
if let Some(f) = group.iter().rev().find(|f| f.caps.iter().any(|c| c.end == Some(face))) {
|
||||
Arc::make_mut(&mut art).blend = blend;
|
||||
out.push((f.at, face, art));
|
||||
|
||||
@@ -316,22 +316,24 @@ fn cubic_to(out: &mut BezPath, c: CubicBez, to: &dyn Fn(Point) -> Option<Point>)
|
||||
/// The graphics `graphics` lists for `face`, drawn on caps `caps` (the faces of that end of the
|
||||
/// object's solids that the viewer sees) through `map`, for an object whose bounds have
|
||||
/// center `center`: one clipping group of every such graphic, shaded with the first cap's light.
|
||||
/// A graphic whose symbol `doc` does not have draws nothing. `None` when there is nothing to draw
|
||||
/// or the art is past the limits.
|
||||
/// A graphic whose symbol `doc` does not have draws nothing. `elements` counts the path elements
|
||||
/// the effect's graphics have drawn so far, after a perspective cuts their curves, and is shared by
|
||||
/// every call for one effect. `None` when there is nothing to draw or that count passes
|
||||
/// [`MAX_ELEMENTS`].
|
||||
pub(super) fn face_art(
|
||||
doc: Option<&Document>,
|
||||
id: NodeId,
|
||||
graphics: &[MappedGraphic],
|
||||
face: PlanarFace,
|
||||
map: &FaceMap,
|
||||
(face, map): (PlanarFace, &FaceMap),
|
||||
center: Point,
|
||||
caps: &[&Facet],
|
||||
elements: &mut usize,
|
||||
) -> Option<Node> {
|
||||
let doc = doc?;
|
||||
let light = caps.first()?.illumination;
|
||||
let affine = map.local_affine(center).filter(|_| map.is_affine());
|
||||
let cuts = if affine.is_some() { 1 } else { PERSPECTIVE_PIECES };
|
||||
let mut children = vec![];
|
||||
let mut elements = 0usize;
|
||||
for g in graphics.iter().filter(|g| g.face == face) {
|
||||
let Some(symbol) = doc.symbols.iter().find(|s| s.name == g.symbol) else { continue };
|
||||
// A symbol past the limits is left out; the other graphics still draw.
|
||||
@@ -351,8 +353,9 @@ pub(super) fn face_art(
|
||||
let gradient_map = local * place;
|
||||
for piece in pieces {
|
||||
let placed = piece.path.transformed(place);
|
||||
elements = elements.saturating_add(placed.subpaths.iter().map(|s| s.anchors.len() + 1).sum());
|
||||
if elements.saturating_mul(if affine.is_some() { 1 } else { PERSPECTIVE_PIECES }) > MAX_ELEMENTS {
|
||||
let added: usize = placed.subpaths.iter().map(|s| s.anchors.len() + 1).sum();
|
||||
*elements = elements.saturating_add(added.saturating_mul(cuts));
|
||||
if *elements > MAX_ELEMENTS {
|
||||
return None;
|
||||
}
|
||||
let mut rule = piece.rule;
|
||||
|
||||
@@ -314,6 +314,29 @@ fn members_with_their_own_blending_get_graphics_of_their_own() {
|
||||
assert_eq!(blended, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn members_with_graphics_of_their_own_share_one_element_limit() {
|
||||
// A red polygon of 30,000 sides: cut by a perspective, one copy fits the limit, two do not.
|
||||
let mut d = document();
|
||||
let mut poly = BezPath::new();
|
||||
for k in 0..30_000 {
|
||||
let a = k as f64 / 30_000.0 * std::f64::consts::TAU;
|
||||
let p = Point::new(5.0 + 5.0 * a.cos(), 5.0 + 5.0 * a.sin());
|
||||
if k == 0 { poly.move_to(p) } else { poly.line_to(p) }
|
||||
}
|
||||
poly.close_path();
|
||||
let art = Node::path(NodeId(0), PathData::from_bezpath(&poly), Appearance::basic(Paint::solid(red()), Paint::None, 0.0));
|
||||
d.symbols.push(Symbol { dynamic: false, name: "Dense".into(), art: Arc::new(art) });
|
||||
let a = square(1, Rect::new(0.0, 0.0, 50.0, 100.0), Paint::solid(Color::rgb(0.5, 0.5, 0.5)));
|
||||
let mut b = square(2, Rect::new(50.0, 0.0, 100.0, 100.0), Paint::solid(Color::rgb(0.5, 0.5, 0.5)));
|
||||
b.blend = BlendMode::Multiply;
|
||||
let mut group = Node::group(NodeId(3), vec![Arc::new(a), Arc::new(b)]);
|
||||
let p = json!({"rotationX": 0.0, "rotationY": 0.0, "rotationZ": 0.0, "perspectiveAngle": 60.0, "shade": false, "graphics": [{"symbol": "Dense", "scale": 1000.0}]});
|
||||
group.appearance.effects.push(crate::new_effect(EXTRUDE, &p).unwrap());
|
||||
// The first member's graphic is drawn; the second would pass the limit, so it draws none.
|
||||
assert_eq!(graphics_in(&solid_art_in(Some(&d), &group).unwrap()).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plane_and_a_hollow_solid() {
|
||||
let d = document();
|
||||
|
||||
@@ -86,6 +86,10 @@ fn paint_refs(p: &Paint, f: Visit) {
|
||||
}
|
||||
|
||||
fn appearance_refs(a: &Appearance, f: Visit) {
|
||||
// The graphics mapped onto the faces of its 3D effects name symbols.
|
||||
for s in vectorcraft_render::effects::appearance_graphic_symbols(a) {
|
||||
f(Ref::Name(Res::Symbol, &s));
|
||||
}
|
||||
for it in &a.items {
|
||||
paint_refs(it.paint(), f);
|
||||
if let AppearanceItem::Stroke(l) = it
|
||||
@@ -620,6 +624,17 @@ impl Relink {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A 3D effect's mapped graphics follow their symbols' names in the document. Each name is
|
||||
// looked up once, so renames that chain (A to B, B to C) do not apply twice.
|
||||
let items = a.items.iter_mut().flat_map(|i| i.effects_mut().iter_mut());
|
||||
for e in a.effects.iter_mut().chain(items).filter(|e| vectorcraft_render::effects::takes_graphics(&e.id)) {
|
||||
let list = e.params.get_mut(vectorcraft_render::effects::GRAPHICS).and_then(Value::as_array_mut);
|
||||
for g in list.into_iter().flatten() {
|
||||
if let Some(Value::String(s)) = g.get_mut("symbol") {
|
||||
self.name(Res::Symbol, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
for it in &mut a.items {
|
||||
self.paint(it.paint_mut());
|
||||
if let AppearanceItem::Stroke(l) = it
|
||||
|
||||
@@ -196,6 +196,33 @@ fn a_rename_reaches_graphics_in_graph_designs_and_opacity_masks() {
|
||||
assert_eq!(mask, Some(vec!["Label".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasted_into_another_document_a_graphic_brings_its_symbol() {
|
||||
let (mut s, id) = setup("threeD.extrude");
|
||||
s.execute("threeD.mapGraphic", &json!({"symbol": "Mark"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
// The symbol the pasted object's graphic names, and the size of that symbol's art.
|
||||
let pasted = |s: &mut Session| {
|
||||
s.execute("edit.paste", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let n = st.doc.node(st.selection.objects[0]).unwrap();
|
||||
let name = n.appearance.effects[0].params["graphics"][0]["symbol"].as_str().unwrap().to_string();
|
||||
let size = vectorcraft_render::effects::graphic_bounds(&st.doc, &name).map(|r| (r.width(), r.height()));
|
||||
(name, size)
|
||||
};
|
||||
// A document without the symbol gets it.
|
||||
s.execute("file.new", &json!({"width":400,"height":400})).unwrap();
|
||||
assert_eq!(pasted(&mut s), ("Mark".to_string(), Some((11.0, 21.0))));
|
||||
// A document whose symbol "Mark" is other art gets the copied symbol under a free name.
|
||||
s.execute("file.new", &json!({"width":400,"height":400})).unwrap();
|
||||
let other = s.execute("path.create", &json!({"d":"M0 0L50 0L50 50L0 50Z"})).unwrap()["id"].clone();
|
||||
s.execute("threeD.addGraphic", &json!({"name": "Mark", "ids": [other]})).unwrap();
|
||||
let (name, size) = pasted(&mut s);
|
||||
assert_ne!(name, "Mark");
|
||||
assert_eq!(size, Some((11.0, 21.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_only_a_graphic_style_uses_is_used() {
|
||||
let (mut s, id) = setup("threeD.extrude");
|
||||
|
||||
@@ -40,20 +40,26 @@ impl FaceMap {
|
||||
self.rows.map(|r| r[0] * p.x + r[1] * p.y + r[2])
|
||||
}
|
||||
|
||||
/// The homogeneous depth a mapped point must lie beyond: the eye's distance times [`NEAREST`]
|
||||
/// with perspective, 0 without.
|
||||
fn nearest(&self) -> f64 {
|
||||
self.eye.map_or(0.0, |e| e * NEAREST)
|
||||
}
|
||||
|
||||
/// Where art point `p` lands on the page. `None` when it lies at or behind the eye, or the
|
||||
/// result is not finite.
|
||||
pub fn to_page(&self, p: Point) -> Option<Point> {
|
||||
let [u, v, w] = self.homogeneous(p);
|
||||
let limit = self.eye.map_or(0.0, |e| e * NEAREST);
|
||||
if w.is_nan() || w <= limit {
|
||||
if w.is_nan() || w <= self.nearest() {
|
||||
return None;
|
||||
}
|
||||
let q = Point::new(self.origin.x + u / w, self.origin.y + v / w);
|
||||
(q.x.is_finite() && q.y.is_finite()).then_some(q)
|
||||
}
|
||||
|
||||
/// The art point that lands on page point `q`. `None` when the face is seen edge-on there or
|
||||
/// the point lies beyond the face's horizon.
|
||||
/// The art point that lands on page point `q`. `None` when the face is seen edge-on there, the
|
||||
/// point lies beyond the face's horizon, or it lies nearer the eye than [`FaceMap::to_page`]
|
||||
/// maps.
|
||||
pub fn to_art(&self, q: Point) -> Option<Point> {
|
||||
let (u, v) = (q.x - self.origin.x, q.y - self.origin.y);
|
||||
let [r0, r1, r2] = self.rows;
|
||||
@@ -65,8 +71,9 @@ impl FaceMap {
|
||||
return None;
|
||||
}
|
||||
let p = Point::new((c * e - b * f) / det, (a * f - c * d) / det);
|
||||
// A point of the plane behind the eye projects to the same page point as one in front.
|
||||
(p.x.is_finite() && p.y.is_finite() && self.homogeneous(p)[2] > 0.0).then_some(p)
|
||||
// A point of the plane behind the eye projects to the same page point as one in front, and
|
||||
// `to_page` maps no point nearer the eye than its limit.
|
||||
(p.x.is_finite() && p.y.is_finite() && self.homogeneous(p)[2] > self.nearest()).then_some(p)
|
||||
}
|
||||
|
||||
/// The part of rectangle `r` of the face's plane whose points [`FaceMap::to_page`] maps, kept
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect};
|
||||
|
||||
use super::super::{Extrude, Facet, PlanarFace, View, extrude, plane};
|
||||
use super::{FaceMap, extrude_face_map, plane_face_map};
|
||||
use super::{FaceMap, NEAREST, extrude_face_map, plane_face_map};
|
||||
|
||||
fn square() -> (PathData, Rect) {
|
||||
let mut b = BezPath::new();
|
||||
@@ -125,6 +125,24 @@ fn a_point_behind_the_eye_has_no_page_point() {
|
||||
assert!(map.to_page(Point::new(40.0, -1e6)).is_none() || map.to_page(Point::new(40.0, 1e6)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_point_nearer_the_eye_than_the_page_map_allows_has_no_art_point() {
|
||||
let (_, bounds) = square();
|
||||
let view = View { rotation_x: -89.0, perspective: 160.0, ..turned(0.0) };
|
||||
let map = plane_face_map(bounds, view).unwrap();
|
||||
let limit = map.eye.unwrap() * NEAREST;
|
||||
// An art point in front of the eye, but at half the nearest depth `to_page` maps.
|
||||
let [a, b, c] = map.rows[2];
|
||||
let p0 = bounds.center();
|
||||
let t = (limit / 2.0 - (a * p0.x + b * p0.y + c)) / (a * a + b * b);
|
||||
let p = Point::new(p0.x + t * a, p0.y + t * b);
|
||||
let [u, v, w] = map.homogeneous(p);
|
||||
assert!(w > 0.0 && w < limit, "{w}");
|
||||
assert_eq!(map.to_page(p), None);
|
||||
let q = Point::new(map.origin.x + u / w, map.origin.y + v / w);
|
||||
assert_eq!(map.to_art(q), None, "the inverse map takes {q:?} back to an art point the page map has no point for");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mapped_part_of_a_rectangle_stops_before_the_eye() {
|
||||
let (_, bounds) = square();
|
||||
|
||||
@@ -527,6 +527,83 @@ fn a_graphics_handle_drags_it_over_its_face_in_one_undo_step() {
|
||||
assert!(!frame(&mut app, vec![button(target, true)]));
|
||||
}
|
||||
|
||||
/// One frame of the artboard's graphic handles over an 800 × 600 canvas with `events`: whether the
|
||||
/// handles took the pointer.
|
||||
fn handles_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> bool {
|
||||
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0));
|
||||
let mut taken = false;
|
||||
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
crate::panels::set_pstate(ui.ctx(), super::DRAWN, Some(super::root_pass(ui.ctx())));
|
||||
let resp = ui.allocate_rect(rect, egui::Sense::click_and_drag());
|
||||
taken = super::graphic_handles_input(app, ui, &resp, rect);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
taken
|
||||
}
|
||||
|
||||
/// Where document point (`x`, `y`) is on the 800 × 600 canvas of [`handles_frame`].
|
||||
fn handles_screen(app: &VectorcraftApp, x: f64, y: f64) -> egui::Pos2 {
|
||||
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0));
|
||||
crate::canvas::Xf::new(rect, &app.view().cloned().unwrap_or_default()).to_screen(vectorcraft_geom::Point::new(x, y))
|
||||
}
|
||||
|
||||
fn primary(pos: egui::Pos2, pressed: bool) -> egui::Event {
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_gesture_on_a_graphics_handle_is_left_to_the_canvas() {
|
||||
let (mut app, ctx) = materials_app();
|
||||
app.run("threeD.mapGraphic", json!({"symbol": "Graphic"})).unwrap();
|
||||
let handle = handles_screen(&app, 150.0, 150.0);
|
||||
let space = |pressed| egui::Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
|
||||
handles_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(handle)]);
|
||||
// With Space held, the press pans the view.
|
||||
handles_frame(&mut app, &ctx, vec![space(true)]);
|
||||
assert!(!handles_frame(&mut app, &ctx, vec![primary(handle, true)]));
|
||||
handles_frame(&mut app, &ctx, vec![primary(handle, false), space(false)]);
|
||||
// With the Hand, Zoom or Rotate View tool, the press is the tool's.
|
||||
for tool in ["hand", "zoom", "rotateView"] {
|
||||
app.select_tool(tool);
|
||||
assert!(!handles_frame(&mut app, &ctx, vec![primary(handle, true)]), "{tool}");
|
||||
handles_frame(&mut app, &ctx, vec![primary(handle, false)]);
|
||||
}
|
||||
// With another tool and Space up, the press grabs the handle.
|
||||
app.select_tool("selection");
|
||||
assert!(handles_frame(&mut app, &ctx, vec![primary(handle, true)]));
|
||||
handles_frame(&mut app, &ctx, vec![primary(handle, false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_selection_change_during_a_handle_drag_ends_it() {
|
||||
let (mut app, ctx) = materials_app();
|
||||
app.run("threeD.mapGraphic", json!({"symbol": "Graphic"})).unwrap();
|
||||
let square = app.session.active().unwrap().selection.objects[0].0;
|
||||
// A second object with the same graphic.
|
||||
let other = app.run("shape.rectangle", json!({"x": 400, "y": 400, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"rotationX": 0.0, "rotationY": 0.0, "rotationZ": 0.0}}))
|
||||
.unwrap();
|
||||
app.run("threeD.mapGraphic", json!({"symbol": "Graphic"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [square]})).unwrap();
|
||||
let graphic_of = |app: &VectorcraftApp, id: u64| {
|
||||
let n = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap();
|
||||
n.appearance.effects[0].params["graphics"][0].clone()
|
||||
};
|
||||
let before = graphic_of(&app, other);
|
||||
let (handle, target, further) = (handles_screen(&app, 150.0, 150.0), handles_screen(&app, 170.0, 140.0), handles_screen(&app, 190.0, 130.0));
|
||||
handles_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(handle)]);
|
||||
assert!(handles_frame(&mut app, &ctx, vec![primary(handle, true)]));
|
||||
handles_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target)]);
|
||||
// The selection changes to the other object while the pointer is down: the drag ends there.
|
||||
app.run("select.set", json!({"ids": [other]})).unwrap();
|
||||
handles_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(further)]);
|
||||
handles_frame(&mut app, &ctx, vec![primary(target, false)]);
|
||||
assert_eq!(graphic_of(&app, other), before, "the other object's graphic moved");
|
||||
let moved = graphic_of(&app, square);
|
||||
assert!((moved["x"].as_f64().unwrap() - 20.0).abs() < 0.11, "{moved}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_settings_lists_only_new_values() {
|
||||
let before = json!({"depth": 50.0, "cap": true, "bevel": "none"});
|
||||
|
||||
@@ -327,13 +327,19 @@ fn handle_drag_id() -> egui::Id {
|
||||
egui::Id::unique("three-d-graphic-handle-drag")
|
||||
}
|
||||
|
||||
/// A drag of graphic `index`'s handle: its face, and the page offset from the handle to the point
|
||||
/// where it was grabbed.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// A drag of graphic `index`'s handle: its face, the page offset from the handle to the point
|
||||
/// where it was grabbed, and the document and selected objects it was grabbed for.
|
||||
#[derive(Clone, Debug)]
|
||||
struct HandleDrag {
|
||||
index: usize,
|
||||
face: vectorcraft_effects::PlanarFace,
|
||||
grab: vectorcraft_geom::Vec2,
|
||||
owner: (u64, Vec<u64>),
|
||||
}
|
||||
|
||||
/// The active document's uid and its selected objects' ids: what a handle drag moves graphics for.
|
||||
fn drag_owner(app: &VectorcraftApp) -> (u64, Vec<u64>) {
|
||||
app.session.active().map(|st| (st.uid, st.selection.objects.iter().map(|id| id.0).collect())).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// How near a press must be to a graphic's handle to grab it, in screen points.
|
||||
@@ -347,6 +353,13 @@ pub(crate) fn graphic_handles_input(app: &mut VectorcraftApp, ui: &egui::Ui, res
|
||||
let xf = &crate::canvas::Xf::at(rect, app.view().unwrap_or(&crate::state::View::default()), app.canvas_ui_zoom);
|
||||
let pointer = ui.input(|i| i.pointer.interact_pos());
|
||||
if let Some(drag) = ctx.data(|m| m.get_temp::<HandleDrag>(handle_drag_id())) {
|
||||
if drag_owner(app) != drag.owner {
|
||||
// The selection or the document changed during the drag: commit the preview and end
|
||||
// the drag, so its grab offset does not move another object's graphic.
|
||||
send(app, &ctx, Map::new(), false);
|
||||
ctx.data_mut(|m| m.remove::<HandleDrag>(handle_drag_id()));
|
||||
return true;
|
||||
}
|
||||
let down = ui.input(|i| i.pointer.primary_down());
|
||||
if let (Some(p), Some((frame, mut list, _))) = (pointer, graphic_handles(app, &ctx))
|
||||
&& let Some((x, y)) = frame.offset_at(drag.face, xf.to_doc(p) - drag.grab)
|
||||
@@ -369,6 +382,12 @@ pub(crate) fn graphic_handles_input(app: &mut VectorcraftApp, ui: &egui::Ui, res
|
||||
if !(resp.hovered() && ui.input(|i| i.pointer.primary_pressed())) {
|
||||
return false;
|
||||
}
|
||||
// A press with Space held, or with the Hand, Zoom or Rotate View tool, pans, zooms or turns the
|
||||
// view, as on any other part of the canvas.
|
||||
let space = ui.input(|i| i.key_down(egui::Key::Space));
|
||||
if space || matches!(app.session.tool_id(), "hand" | "zoom" | "rotateView") {
|
||||
return false;
|
||||
}
|
||||
let (Some(p), Some((_, list, handles))) = (pointer, graphic_handles(app, &ctx)) else { return false };
|
||||
let nearest = handles
|
||||
.iter()
|
||||
@@ -377,7 +396,8 @@ pub(crate) fn graphic_handles_input(app: &mut VectorcraftApp, ui: &egui::Ui, res
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2));
|
||||
let Some((index, at, _)) = nearest else { return false };
|
||||
let Some(face) = list.get(index).map(|g| g.face) else { return false };
|
||||
ctx.data_mut(|m| m.insert_temp(handle_drag_id(), HandleDrag { index, face, grab: xf.to_doc(p) - at }));
|
||||
let owner = drag_owner(app);
|
||||
ctx.data_mut(|m| m.insert_temp(handle_drag_id(), HandleDrag { index, face, grab: xf.to_doc(p) - at, owner }));
|
||||
set_selected_graphic(&ctx, index);
|
||||
true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user