Dimensions snap; justified text snaps and grips at its alignment point; grid snap follows SNAPBASE/SNAPANG; OSNAPHATCH (fixes #334) (#337)
- A dimension snaps to what it draws: the objects of its block (read from a file), expanded like block references, or else the lines generated from it (Endpoint on their ends, Midpoint, Intersection, Nearest); Node on its definition points. - TEXT justified other than left-baseline (as read from DXF, group 11) snaps (Insertion) and grips at its alignment point; attributes too. - The snap grid starts at SNAPBASE and turns by SNAPANG (drawing variables, defaults 0,0 and 0 degrees). SETVAR takes `x,y` for point variables. - Hatches are left out of object snaps unless OSNAPHATCH is 1 (new session variable, default 0 as in AutoCAD). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b1a78fbb8f
commit
6a15ba2a7d
|
|
@ -216,6 +216,17 @@ pub struct Text {
|
|||
#[serde(default)]
|
||||
pub valign: VAlign,
|
||||
}
|
||||
impl Text {
|
||||
/// The point the text is placed by, as the Insertion snap and the grip show it: the alignment
|
||||
/// point (DXF group 11) of justified text, the start point (group 10) of left-baseline,
|
||||
/// aligned and fit text.
|
||||
pub fn justify_point(&self) -> Vec2 {
|
||||
match (self.halign, self.valign) {
|
||||
(HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _) => self.insert.xy(),
|
||||
_ => self.align_pt.unwrap_or(self.insert).xy(),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn one() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
|
@ -977,10 +988,10 @@ impl EntityKind {
|
|||
}
|
||||
}
|
||||
EntityKind::Ray(r) | EntityKind::XLine(r) => vec![r.base.xy(), r.base.xy() + r.dir.xy()],
|
||||
EntityKind::Text(t) => vec![t.insert.xy()],
|
||||
EntityKind::Text(t) => vec![t.justify_point()],
|
||||
EntityKind::MText(t) => vec![t.insert.xy()],
|
||||
EntityKind::AttDef(a) => vec![a.text.insert.xy()],
|
||||
EntityKind::Insert(i) => std::iter::once(i.insert.xy()).chain(i.attribs.iter().map(|a| a.text.insert.xy())).collect(),
|
||||
EntityKind::AttDef(a) => vec![a.text.justify_point()],
|
||||
EntityKind::Insert(i) => std::iter::once(i.insert.xy()).chain(i.attribs.iter().map(|a| a.text.justify_point())).collect(),
|
||||
EntityKind::Dimension(d) => vec![d.p13.xy(), d.p14.xy(), d.defpt.xy(), d.text_mid.xy()],
|
||||
EntityKind::Leader(l) => l.vertices.iter().map(|v| v.xy()).collect(),
|
||||
EntityKind::MLeader(m) => m.leaders.iter().flat_map(|l| l.iter().map(|v| v.xy())).chain(std::iter::once(m.landing.xy())).collect(),
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ impl Header {
|
|||
h.set_f64("ELEVATION", 0.0);
|
||||
h.set_i64("LWDISPLAY", 0);
|
||||
h.set("INSBASE", HVal::Point(Vec3::ZERO));
|
||||
// Snap grid origin and rotation (SNAPANG in degrees).
|
||||
h.set("SNAPBASE", HVal::Point(Vec3::ZERO));
|
||||
h.set_f64("SNAPANG", 0.0);
|
||||
h
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ pub struct Settings {
|
|||
pub gridmode: bool,
|
||||
pub snapmode: bool,
|
||||
pub snapunit: Vec2,
|
||||
/// OSNAPHATCH: object snaps find hatch objects (off by default, as in AutoCAD).
|
||||
pub osnaphatch: bool,
|
||||
pub gridunit: Vec2,
|
||||
/// Major grid line every N minor lines (GRIDMAJOR).
|
||||
pub gridmajor: u32,
|
||||
|
|
@ -260,6 +262,7 @@ impl Default for Settings {
|
|||
gridmode: true,
|
||||
snapmode: false,
|
||||
snapunit: Vec2::new(0.5, 0.5),
|
||||
osnaphatch: false,
|
||||
gridunit: Vec2::new(0.5, 0.5),
|
||||
gridmajor: 5,
|
||||
dynmode: true,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ pub mod mode {
|
|||
pub const PAR: u32 = 8192;
|
||||
/// Running snaps temporarily off (F3).
|
||||
pub const OFF: u32 = 16384;
|
||||
/// Not an OSMODE bit: snaps also find hatch objects (OSNAPHATCH = 1).
|
||||
pub const HATCH: u32 = 1 << 16;
|
||||
pub const ALL: [(u32, &str); 14] = [
|
||||
(END, "Endpoint"),
|
||||
(MID, "Midpoint"),
|
||||
|
|
@ -164,11 +166,11 @@ pub(crate) fn osnap_with(
|
|||
}
|
||||
if osmode & mode::INS != 0 {
|
||||
match &e.kind {
|
||||
EntityKind::Text(t) => cands.push((mode::INS, t.insert.xy())),
|
||||
EntityKind::Text(t) => cands.push((mode::INS, t.justify_point())),
|
||||
EntityKind::MText(t) => cands.push((mode::INS, t.insert.xy())),
|
||||
EntityKind::Insert(i) => {
|
||||
cands.push((mode::INS, i.insert.xy()));
|
||||
cands.extend(i.attribs.iter().filter(|a| !a.invisible).map(|a| (mode::INS, a.text.insert.xy())));
|
||||
cands.extend(i.attribs.iter().filter(|a| !a.invisible).map(|a| (mode::INS, a.text.justify_point())));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -179,6 +181,9 @@ pub(crate) fn osnap_with(
|
|||
if let EntityKind::Insert(ins) = &e.kind {
|
||||
block_leaves(d, ins, &Mat3::IDENTITY, 0, &mut budget, &mut |k, m| sink.object(k, Some(m)));
|
||||
}
|
||||
if let EntityKind::Dimension(dm) = &e.kind {
|
||||
sink.dimension(d, dm, &mut budget);
|
||||
}
|
||||
}
|
||||
// Intersections between nearby primitives.
|
||||
if osmode & (mode::INT | mode::APP) != 0 {
|
||||
|
|
@ -277,6 +282,10 @@ impl Sink<'_> {
|
|||
/// (whose primitives far from the cursor are skipped).
|
||||
fn object(&mut self, kind: &EntityKind, xf: Option<&Mat3>) {
|
||||
let osmode = self.osmode;
|
||||
// OSNAPHATCH = 0: hatches are left out.
|
||||
if osmode & mode::HATCH == 0 && matches!(kind, EntityKind::Hatch(_)) {
|
||||
return;
|
||||
}
|
||||
let at = |p: Vec2| xf.map_or(p, |m| m.apply(p));
|
||||
if osmode & mode::END != 0
|
||||
&& let EntityKind::Solid(s) | EntityKind::Trace(s) = kind
|
||||
|
|
@ -375,6 +384,67 @@ impl Sink<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Sink<'_> {
|
||||
/// A dimension snaps to what it draws, as in AutoCAD (into its block): the objects of its
|
||||
/// block when the drawing has one (read from a file), else the lines generated from it
|
||||
/// (Endpoint on their ends); Node on its definition points.
|
||||
fn dimension(&mut self, d: &Drawing, dm: &cadcraft_doc::Dimension, budget: &mut usize) {
|
||||
match dm.block.as_ref().and_then(|n| d.block(n)) {
|
||||
Some(blk) => {
|
||||
// The block holds world geometry: a reference at its own base point places it as is.
|
||||
let ins = Insert {
|
||||
block: blk.name.clone(),
|
||||
insert: blk.base,
|
||||
scale: cadcraft_geom::Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: Vec::new(),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
block_leaves(d, &ins, &Mat3::IDENTITY, 0, budget, &mut |k, m| self.object(k, Some(m)));
|
||||
}
|
||||
None => {
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
for line in &g.lines {
|
||||
let (Some(a), Some(b)) = (line.first(), line.last()) else { continue };
|
||||
if self.osmode & mode::END != 0 {
|
||||
self.cands.extend([(mode::END, *a), (mode::END, *b)]);
|
||||
}
|
||||
if self.osmode & mode::MID != 0 && line.len() == 2 {
|
||||
self.cands.push((mode::MID, (*a + *b) * 0.5));
|
||||
}
|
||||
for w in line.windows(2) {
|
||||
if let [p, q] = w {
|
||||
let s = Prim::Seg(Segment::Line(cadcraft_geom::Line::new(*p, *q)));
|
||||
if prim_near(&s, &self.probe, self.aperture) {
|
||||
self.near.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.osmode & mode::NOD != 0 {
|
||||
self.cands.extend(dim_def_points(dm).into_iter().map(|p| (mode::NOD, p)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The definition points of a dimension that its kind uses.
|
||||
fn dim_def_points(dm: &cadcraft_doc::Dimension) -> Vec<Vec2> {
|
||||
use cadcraft_doc::DimKind;
|
||||
let pts = match dm.kind {
|
||||
DimKind::Linear { .. } | DimKind::Aligned | DimKind::ArcLength => vec![dm.defpt, dm.p13, dm.p14],
|
||||
DimKind::Radius | DimKind::Diameter => vec![dm.defpt, dm.p15],
|
||||
DimKind::Angular3P => vec![dm.defpt, dm.p13, dm.p14, dm.p15],
|
||||
DimKind::Angular => vec![dm.defpt, dm.p13, dm.p14, dm.p15, dm.p16],
|
||||
DimKind::Ordinate { .. } => vec![dm.p13, dm.p14],
|
||||
};
|
||||
pts.into_iter().map(|p| p.xy()).filter(|p| p.is_finite()).collect()
|
||||
}
|
||||
|
||||
/// Calls `f` with each object drawn by a block reference and the transform placing it, nested
|
||||
/// references expanded (within [`MAX_BLOCK_DEPTH`] levels and `budget` objects); objects that
|
||||
/// aren't drawn (hidden, on a layer that is off or frozen, attribute definitions) are left out.
|
||||
|
|
@ -684,11 +754,21 @@ pub fn polar(base: Vec2, p: Vec2, inc: f64, tol: f64) -> Option<(Vec2, f64)> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Grid snap.
|
||||
pub fn grid_snap(p: Vec2, unit: Vec2, origin: Vec2) -> Vec2 {
|
||||
let sx = if unit.x > 1e-12 { ((p.x - origin.x) / unit.x).round() * unit.x + origin.x } else { p.x };
|
||||
let sy = if unit.y > 1e-12 { ((p.y - origin.y) / unit.y).round() * unit.y + origin.y } else { p.y };
|
||||
Vec2::new(sx, sy)
|
||||
/// Grid snap: the nearest point of the grid of `unit` spacing through `origin`, turned by
|
||||
/// `angle` (radians) about it.
|
||||
pub fn grid_snap(p: Vec2, unit: Vec2, origin: Vec2, angle: f64) -> Vec2 {
|
||||
let angle = if angle.is_finite() { angle } else { 0.0 };
|
||||
let q = (p - origin).rotate(-angle);
|
||||
let sx = if unit.x > 1e-12 { (q.x / unit.x).round() * unit.x } else { q.x };
|
||||
let sy = if unit.y > 1e-12 { (q.y / unit.y).round() * unit.y } else { q.y };
|
||||
let s = origin + Vec2::new(sx, sy).rotate(angle);
|
||||
if s.is_finite() { s } else { p }
|
||||
}
|
||||
|
||||
/// The snap grid's origin (SNAPBASE) and rotation in radians (SNAPANG, in degrees) of a drawing.
|
||||
pub fn grid_frame(d: &Drawing) -> (Vec2, f64) {
|
||||
let base = d.header.point("SNAPBASE").map(|p| p.xy()).filter(|p| p.is_finite()).unwrap_or(Vec2::ZERO);
|
||||
(base, d.header.f64("SNAPANG", 0.0).to_radians())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -746,7 +826,7 @@ mod tests {
|
|||
let (p, a) = polar(Vec2::ZERO, Vec2::new(5.0, 5.1), 45f64.to_radians(), 3f64.to_radians()).unwrap();
|
||||
assert!((a - PI / 4.0).abs() < 1e-12);
|
||||
assert!((p.x - p.y).abs() < 1e-9);
|
||||
assert_eq!(grid_snap(Vec2::new(0.74, 1.26), Vec2::new(0.5, 0.5), Vec2::ZERO), Vec2::new(0.5, 1.5));
|
||||
assert_eq!(grid_snap(Vec2::new(0.74, 1.26), Vec2::new(0.5, 0.5), Vec2::ZERO, 0.0), Vec2::new(0.5, 1.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::{EngineError, Result, Session};
|
|||
|
||||
const SESSION_VARS: &[&str] = &[
|
||||
"OSMODE",
|
||||
"OSNAPHATCH",
|
||||
"ORTHOMODE",
|
||||
"POLARMODE",
|
||||
"POLARANG",
|
||||
|
|
@ -36,6 +37,7 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
|
|||
let st = &s.settings;
|
||||
let v = match n.as_str() {
|
||||
"OSMODE" => json!(st.osmode),
|
||||
"OSNAPHATCH" => json!(i32::from(st.osnaphatch)),
|
||||
"ORTHOMODE" => json!(i32::from(st.orthomode)),
|
||||
"POLARMODE" => json!(i32::from(st.polarmode)),
|
||||
"POLARANG" => json!(st.polarang.to_degrees()),
|
||||
|
|
@ -130,6 +132,7 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
|
|||
let st = &mut s.settings;
|
||||
match n.as_str() {
|
||||
"OSMODE" => st.osmode = v.as_u64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())).ok_or_else(bad)? as u32 & 0x7fff,
|
||||
"OSNAPHATCH" => st.osnaphatch = as_bool(v).ok_or_else(bad)?,
|
||||
"ORTHOMODE" => st.orthomode = as_bool(v).ok_or_else(bad)?,
|
||||
"POLARMODE" => st.polarmode = as_bool(v).ok_or_else(bad)?,
|
||||
"POLARANG" => st.polarang = as_f64(v).filter(|a| *a > 0.0).ok_or_else(bad)?.to_radians(),
|
||||
|
|
@ -192,6 +195,11 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
|
|||
d.header.set_i64(&n, i);
|
||||
return Ok(());
|
||||
}
|
||||
// A point variable takes `x,y` typed at the command line too.
|
||||
Some(cadcraft_doc::HVal::Point(_)) => {
|
||||
d.header.set(&n, cadcraft_doc::HVal::Point(as_pt(v).ok_or_else(bad)?.to3(0.0)));
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
d.header.set(&n, val);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
//! Snaps on dimensions, justified text and hatches; the snap grid's base and angle (#334).
|
||||
|
||||
use cadcraft_doc::{Common, EntityKind, HAlign, Space, Text, VAlign};
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::snap::{self, mode};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use serde_json::json;
|
||||
|
||||
/// The snap of `modes` near `p` (a hair off it, inside a 0.1 aperture).
|
||||
fn snap_at(s: &Session, p: Vec2, modes: u32) -> Option<(Vec2, u32)> {
|
||||
snap::osnap(s.doc().unwrap(), &Space::Model, p + Vec2::new(0.02, -0.01), 0.1, modes, None, false).map(|h| (h.point, h.mode))
|
||||
}
|
||||
|
||||
fn handle(s: &mut Session, cmd: &str, p: serde_json::Value) -> String {
|
||||
let r = s.execute(cmd, &p).unwrap();
|
||||
r.get("handle").unwrap_or(&r["handles"][0]).as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_snap_to_their_lines_and_definition_points() {
|
||||
let mut s = Session::new();
|
||||
s.cmdline("dimlinear 0,0 10,0 5,3").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
// Endpoint: the dimension line's end; Node: the extension line origins.
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 3.0), mode::END), Some((Vec2::new(10.0, 3.0), mode::END)));
|
||||
assert_eq!(snap_at(&s, Vec2::new(0.0, 0.0), mode::NOD), Some((Vec2::new(0.0, 0.0), mode::NOD)));
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 0.0), mode::NOD), Some((Vec2::new(10.0, 0.0), mode::NOD)));
|
||||
|
||||
// A dimension with a block (as read from a file) snaps to the block's objects.
|
||||
let l = handle(&mut s, "line", json!({ "points": [[0, 7], [10, 7]] }));
|
||||
s.execute("block", &json!({ "name": "DIMGEOM", "base": [0, 0], "handles": [l], "keep": "delete" })).unwrap();
|
||||
let d = s.doc_mut().unwrap();
|
||||
let dim = d.model.iter().find(|e| matches!(e.kind, EntityKind::Dimension(_))).unwrap().handle;
|
||||
d.model.modify(dim, |e| {
|
||||
if let EntityKind::Dimension(dm) = &mut e.kind {
|
||||
dm.block = Some("DIMGEOM".into());
|
||||
}
|
||||
});
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 7.0), mode::END), Some((Vec2::new(10.0, 7.0), mode::END)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justified_text_snaps_and_grips_at_its_alignment_point() {
|
||||
let mut s = Session::new();
|
||||
// As read from DXF: a right-justified text keeps group 10 (start) and group 11 (alignment).
|
||||
let t = Text {
|
||||
insert: Vec3::new(0.0, 0.0, 0.0),
|
||||
align_pt: Some(Vec3::new(10.0, 0.0, 0.0)),
|
||||
height: 1.0,
|
||||
value: "ABCDEFGHIJ".into(),
|
||||
rotation: 0.0,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Standard".into(),
|
||||
halign: HAlign::Right,
|
||||
valign: VAlign::Baseline,
|
||||
};
|
||||
s.doc_mut().unwrap().add(&Space::Model, Common::default(), EntityKind::Text(t.clone())).unwrap();
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 0.0), mode::INS), Some((Vec2::new(10.0, 0.0), mode::INS)));
|
||||
assert_eq!(snap_at(&s, Vec2::new(0.0, 0.0), mode::INS), None);
|
||||
assert_eq!(EntityKind::Text(t.clone()).grips(), vec![Vec2::new(10.0, 0.0)]);
|
||||
// Left-baseline and fit text: the start point.
|
||||
for (h, v) in [(HAlign::Left, VAlign::Baseline), (HAlign::Fit, VAlign::Baseline)] {
|
||||
assert_eq!(Text { halign: h, valign: v, ..t.clone() }.justify_point(), Vec2::new(0.0, 0.0));
|
||||
}
|
||||
assert_eq!(Text { halign: HAlign::Left, valign: VAlign::Top, ..t.clone() }.justify_point(), Vec2::new(10.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatches_snap_only_with_osnaphatch() {
|
||||
let mut s = Session::new();
|
||||
let r = handle(&mut s, "rectang", json!({ "p1": [0, 0], "p2": [10, 5] }));
|
||||
s.execute("hatch", &json!({ "handles": [r] })).unwrap();
|
||||
s.execute("erase", &json!({ "handles": [r] })).unwrap();
|
||||
assert!(matches!(s.doc().unwrap().model.iter().last().map(|e| &e.kind), Some(EntityKind::Hatch(_))));
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 5.0), mode::END), None);
|
||||
assert_eq!(s.execute("getvar", &json!({ "name": "OSNAPHATCH" })).unwrap()["value"], json!(0));
|
||||
s.cmdline("setvar osnaphatch 1").unwrap();
|
||||
assert!(s.settings.osnaphatch);
|
||||
let modes = mode::END | mode::HATCH;
|
||||
assert_eq!(snap_at(&s, Vec2::new(10.0, 5.0), modes).map(|h| h.0), Some(Vec2::new(10.0, 5.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_snap_follows_snapbase_and_snapang() {
|
||||
let mut s = Session::new();
|
||||
assert_eq!(snap::grid_frame(s.doc().unwrap()), (Vec2::ZERO, 0.0));
|
||||
s.cmdline("setvar snapbase 0.5,0.25").unwrap();
|
||||
s.cmdline("setvar snapang 30").unwrap();
|
||||
let (origin, angle) = snap::grid_frame(s.doc().unwrap());
|
||||
assert_eq!(origin, Vec2::new(0.5, 0.25));
|
||||
assert!((angle - 30f64.to_radians()).abs() < 1e-12);
|
||||
// Two units along the turned x axis and one along its y axis, nudged off the grid.
|
||||
let (u, v) = (Vec2::from_angle(angle), Vec2::from_angle(angle).perp());
|
||||
let on = origin + u * 2.0 + v * 1.0;
|
||||
let p = snap::grid_snap(on + Vec2::new(0.1, -0.15), Vec2::new(1.0, 1.0), origin, angle);
|
||||
assert!(p.near(on, 1e-12), "{p:?} vs {on:?}");
|
||||
// Unturned, from SNAPBASE.
|
||||
assert_eq!(snap::grid_snap(Vec2::new(1.4, 1.1), Vec2::new(1.0, 1.0), origin, 0.0), Vec2::new(1.5, 1.25));
|
||||
}
|
||||
|
|
@ -476,14 +476,16 @@ fn effective_point(app: &mut CadApp, raw: Vec2, xf: &Xf) -> Vec2 {
|
|||
}
|
||||
let ap = s.aperture / xf.scale;
|
||||
if let Ok(st) = app.session.state()
|
||||
&& let Some(hit) = snap::osnap(&st.doc, &st.edit_space(), raw, ap, s.osmode, base, deferred)
|
||||
&& let Some(hit) =
|
||||
snap::osnap(&st.doc, &st.edit_space(), raw, ap, s.osmode | if s.osnaphatch { snap::mode::HATCH } else { 0 }, base, deferred)
|
||||
{
|
||||
app.canvas.snap = Some(hit);
|
||||
return hit.point;
|
||||
}
|
||||
let mut p = raw;
|
||||
if s.snapmode {
|
||||
p = snap::grid_snap(p, s.snapunit, Vec2::ZERO);
|
||||
let (origin, angle) = app.session.doc().map(snap::grid_frame).unwrap_or((Vec2::ZERO, 0.0));
|
||||
p = snap::grid_snap(p, s.snapunit, origin, angle);
|
||||
}
|
||||
if let Some(b) = base {
|
||||
if s.orthomode {
|
||||
|
|
|
|||
Loading…
Reference in New Issue