From 2721aa8982478bc459b203f5a1d2c7d7d154f5ce Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 27 Oct 2026 15:55:09 -0400 Subject: [PATCH] Enforce Rust coding standards via Clippy (#2239) * chore(clippy): enforce workspace Rust quality gates * refactor(doc): use explicit entity imports * refactor(constraints): split residual evaluation * refactor(fonts): split mtext layout * refactor(render): use explicit entity imports * fix(fonts): retain height for empty paragraphs * fix(doc): repair dimension and dynamic block links * refactor(render): split entity drawing helpers * docs: document workspace lint inheritance * refactor(render): isolate dimension geometry * refactor(engine): use explicit imports * refactor(engine): split complex edit paths into helpers * refactor(io): split DXF read and write helpers * refactor(ui): split canvas interaction and drawing * refactor(ui): split command and drawing helpers * test(i18n): split locale preference checks * fix(ui): use current egui scope id API * fix(dim): preserve text offset and ellipse grip behavior --- AGENTS.md | 2 +- CLAUDE.md | 2 +- Cargo.toml | 4 +- clippy.toml | 1 + crates/constraints/src/residual.rs | 526 ++++-- crates/doc/src/dynblock.rs | 2 +- crates/doc/src/entity/block.rs | 2 +- crates/doc/src/entity/common.rs | 2 +- crates/doc/src/entity/curves.rs | 2 +- crates/doc/src/entity/dim.rs | 4 +- crates/doc/src/entity/fill.rs | 2 +- crates/doc/src/entity/layout.rs | 2 +- crates/doc/src/entity/text.rs | 2 +- crates/engine/src/autosave.rs | 5 +- crates/engine/src/cmd/annotate/angular.rs | 5 +- crates/engine/src/cmd/annotate/common.rs | 2 +- crates/engine/src/cmd/annotate/edit.rs | 5 +- crates/engine/src/cmd/annotate/linear.rs | 5 +- crates/engine/src/cmd/annotate/mleader.rs | 5 +- crates/engine/src/cmd/annotate/radial.rs | 2 +- crates/engine/src/cmd/draw/circle.rs | 5 +- crates/engine/src/cmd/draw/ellipse.rs | 5 +- crates/engine/src/cmd/draw/line_pline.rs | 5 +- crates/engine/src/cmd/draw/point.rs | 5 +- crates/engine/src/cmd/draw/polygon.rs | 5 +- crates/engine/src/cmd/draw/spline.rs | 5 +- crates/engine/src/cmd/draw/text.rs | 5 +- crates/engine/src/cmd/draw2/arc.rs | 5 +- crates/engine/src/cmd/draw2/centermark.rs | 4 +- crates/engine/src/cmd/draw2/curves3d.rs | 5 +- crates/engine/src/cmd/draw2/divide.rs | 5 +- crates/engine/src/cmd/draw2/ellipse.rs | 2 +- crates/engine/src/cmd/draw2/rect.rs | 5 +- crates/engine/src/cmd/draw2/revcloud.rs | 5 +- crates/engine/src/cmd/draw2/tancircle.rs | 5 +- crates/engine/src/cmd/draw2/wipeout.rs | 5 +- crates/engine/src/cmd/draw2/xline.rs | 5 +- crates/engine/src/cmd/hatch.rs | 2 +- crates/engine/src/cmd/layout.rs | 2 +- crates/engine/src/cmd/modify/break_join.rs | 5 +- crates/engine/src/cmd/modify/explode.rs | 2 +- crates/engine/src/cmd/modify/fillet.rs | 5 +- crates/engine/src/cmd/modify/offset.rs | 5 +- crates/engine/src/cmd/modify/select_then.rs | 4 +- crates/engine/src/cmd/modify/stretch.rs | 5 +- crates/engine/src/cmd/modify/transform.rs | 2 +- crates/engine/src/cmd/modify/trim.rs | 5 +- crates/engine/src/cmd/modify2/align.rs | 5 +- crates/engine/src/cmd/modify2/blend.rs | 5 +- crates/engine/src/cmd/modify2/fillet.rs | 5 +- crates/engine/src/cmd/modify2/lengthen.rs | 5 +- crates/engine/src/cmd/modify2/ncopy.rs | 5 +- crates/engine/src/cmd/modify2/patharray.rs | 5 +- crates/engine/src/cmd/modify2/pedit.rs | 6 +- crates/engine/src/cmd/modify2/selop.rs | 5 +- crates/engine/src/cmd/modify2/splinedit.rs | 169 +- crates/engine/src/cmd/modify2/tests.rs | 7 + crates/engine/src/cmd/modify2/textedit.rs | 4 +- crates/engine/src/cmd/modify2/trimext.rs | 2 +- crates/engine/src/cmd/options.rs | 2 +- crates/engine/src/cmd/props.rs | 435 ++--- crates/engine/src/doc_state.rs | 2 +- crates/engine/src/grips.rs | 147 +- crates/engine/src/grips/tests.rs | 12 + crates/engine/src/last_used.rs | 2 +- crates/engine/src/lisp/builtins/args.rs | 2 +- crates/engine/src/lisp/builtins/files.rs | 2 +- crates/engine/src/lisp/builtins/functions.rs | 2 +- crates/engine/src/lisp/builtins/geometry.rs | 2 +- crates/engine/src/lisp/builtins/lists.rs | 2 +- crates/engine/src/lisp/builtins/numbers.rs | 2 +- crates/engine/src/lisp/builtins/output.rs | 2 +- crates/engine/src/lisp/builtins/strings.rs | 2 +- crates/engine/src/lisp/builtins/wcmatch.rs | 2 +- crates/engine/src/lisp/tests.rs | 84 +- crates/engine/src/running.rs | 2 +- crates/engine/src/session.rs | 5 +- crates/engine/src/session/cmdline.rs | 2 +- crates/engine/src/session/execute.rs | 2 +- crates/engine/src/session/files.rs | 2 +- crates/engine/src/session/interactive.rs | 5 +- crates/engine/src/session/picking.rs | 2 +- crates/engine/src/session/zoom.rs | 2 +- crates/engine/src/settings.rs | 2 +- crates/engine/src/snap.rs | 191 ++- crates/engine/src/view.rs | 2 +- crates/fonts/src/mtext.rs | 120 +- crates/fonts/src/mtext/para.rs | 238 ++- crates/io/src/dxf_raw.rs | 145 +- crates/io/src/dxf_read.rs | 7 +- crates/io/src/dxf_read/common.rs | 2 +- crates/io/src/dxf_read/dimassoc.rs | 2 +- crates/io/src/dxf_read/document.rs | 2 +- crates/io/src/dxf_read/entities.rs | 6 +- crates/io/src/dxf_read/hatch.rs | 361 ++-- crates/io/src/dxf_read/objects.rs | 2 +- crates/io/src/dxf_read/tables.rs | 57 +- crates/io/src/dxf_read/text.rs | 2 +- crates/io/src/dxf_write.rs | 6 +- crates/io/src/dxf_write/assoc.rs | 2 +- crates/io/src/dxf_write/blocks.rs | 4 +- crates/io/src/dxf_write/common.rs | 5 +- crates/io/src/dxf_write/ctx.rs | 2 +- crates/io/src/dxf_write/entity.rs | 672 ++++---- crates/io/src/dxf_write/header.rs | 2 +- crates/io/src/dxf_write/layout.rs | 2 +- crates/io/src/dxf_write/objects.rs | 2 +- crates/io/src/dxf_write/sections.rs | 1604 ++++++++++-------- crates/render/src/blocks.rs | 2 +- crates/render/src/dim.rs | 548 +----- crates/render/src/dim/arrowheads.rs | 2 +- crates/render/src/dim/linear.rs | 268 +++ crates/render/src/dim/tests.rs | 13 + crates/render/src/dim/text.rs | 2 +- crates/render/src/dim/variants.rs | 291 ++++ crates/render/src/dim_draw.rs | 2 +- crates/render/src/display.rs | 2 +- crates/render/src/draw.rs | 190 ++- crates/render/src/pipeline.rs | 2 +- crates/render/src/props.rs | 2 +- crates/render/src/text_entity.rs | 2 +- crates/render/src/viewports.rs | 2 +- crates/ui-egui/src/canvas.rs | 1040 +++++++----- crates/ui-egui/src/canvas/cursor.rs | 2 +- crates/ui-egui/src/canvas/cycle.rs | 2 +- crates/ui-egui/src/canvas/draw.rs | 3 +- crates/ui-egui/src/canvas/dyn_boxes.rs | 2 +- crates/ui-egui/src/canvas/grid.rs | 2 +- crates/ui-egui/src/canvas/grips.rs | 2 +- crates/ui-egui/src/canvas/list.rs | 2 +- crates/ui-egui/src/canvas/pick.rs | 2 +- crates/ui-egui/src/canvas/viewport.rs | 2 +- crates/ui-egui/src/canvas/widgets.rs | 2 +- crates/ui-egui/src/chrome.rs | 66 +- crates/ui-egui/src/i18n/tests.rs | 78 +- crates/ui-egui/src/icons.rs | 84 +- crates/ui-egui/src/layers.rs | 196 ++- crates/ui-egui/src/menus.rs | 325 ++-- crates/ui-egui/src/parametric.rs | 48 +- xtask/Cargo.toml | 3 + 140 files changed, 4859 insertions(+), 3417 deletions(-) create mode 100644 crates/render/src/dim/linear.rs create mode 100644 crates/render/src/dim/variants.rs diff --git a/AGENTS.md b/AGENTS.md index cd6de23e3..e436894b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ People trust CADCraft with their drawings; a crash loses their work. **This outr - **Bound recursion** (nested/cyclic block references: `MAX_BLOCK_DEPTH`). - **Last-resort guard:** `Session::execute` and every step of an interactive command (its factory, `begin`, input, `prompt` and `preview`; `crates/engine/src/guard.rs`) run under `catch_unwind`; an escaped panic restores the drawing and reports an error. - **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`, `hostile_dxf_does_not_panic`). -- Every production crate root carries `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`. +- Every workspace crate manifest carries `[lints] workspace = true` to inherit `[workspace.lints]`; every production crate root carries `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`. ## Non-negotiables - **Clean-room.** AutoCAD is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic drawings, take screenshots (by window id) stored only under `plan/autocad/screenshots/` (never committed). **Never** read, disassemble or copy anything inside the AutoCAD bundle (names/listings only); never copy Autodesk fonts (SHX), hatch patterns (`.pat`), linetypes (`.lin`), templates, CUI/PGP files, icons, artwork or help text; never commit files produced by AutoCAD. File formats come from public specifications (Autodesk's published DXF Reference, the Open Design Alliance's public DWG specification) and our own synthetic tests. Never copy or link GPL/LGPL/AGPL code (LibreDWG, LibreCAD, QCAD, FreeCAD's planegcs, SolveSpace, OpenCASCADE) — don't even read their sources. diff --git a/CLAUDE.md b/CLAUDE.md index f15d4522b..33e6a2121 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ People trust CADCraft with their drawings; a crash loses their work. **This outr - **Bound recursion** (nested/cyclic block references: `MAX_BLOCK_DEPTH`). - **Last-resort guard:** `Session::execute` and every step of an interactive command (its factory, `begin`, input, `prompt` and `preview`; `crates/engine/src/guard.rs`) run under `catch_unwind`; an escaped panic restores the drawing and reports an error. - **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`, `hostile_dxf_does_not_panic`). -- Every production crate root carries `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`. +- Every workspace crate manifest carries `[lints] workspace = true` to inherit `[workspace.lints]`; every production crate root carries `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`. ## Non-negotiables - **Clean-room.** AutoCAD is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic drawings, take screenshots (by window id) stored only under `plan/autocad/screenshots/` (never committed). **Never** read, disassemble or copy anything inside the AutoCAD bundle (names/listings only); never copy Autodesk fonts (SHX), hatch patterns (`.pat`), linetypes (`.lin`), templates, CUI/PGP files, icons, artwork or help text; never commit files produced by AutoCAD. File formats come from public specifications (Autodesk's published DXF Reference, the Open Design Alliance's public DWG specification) and our own synthetic tests. Never copy or link GPL/LGPL/AGPL code (LibreDWG, LibreCAD, QCAD, FreeCAD's planegcs, SolveSpace, OpenCASCADE) — don't even read their sources. diff --git a/Cargo.toml b/Cargo.toml index 8c4230e22..3265938f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,9 +69,11 @@ arboard = { version = "3.6", default-features = false } unsafe_code = "forbid" [workspace.lints.clippy] +cognitive_complexity = "deny" +too_many_arguments = "deny" +wildcard_imports = "deny" dbg_macro = "warn" todo = "warn" -too_many_arguments = "allow" type_complexity = "allow" [profile.dev.package."*"] diff --git a/clippy.toml b/clippy.toml index 287e14edc..3522a15b6 100644 --- a/clippy.toml +++ b/clippy.toml @@ -5,3 +5,4 @@ allow-indexing-slicing-in-tests = true disallowed-types = [ { path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" }, ] +cognitive-complexity-threshold = 30 diff --git a/crates/constraints/src/residual.rs b/crates/constraints/src/residual.rs index be5723fec..05794b720 100644 --- a/crates/constraints/src/residual.rs +++ b/crates/constraints/src/residual.rs @@ -3,7 +3,7 @@ use cadcraft_doc::{Constraint, ConstraintKind, DistAxis, GeomRef}; use cadcraft_geom::Vec2; -use crate::curves::circle_at; +use crate::curves::{Ell, circle_at}; use crate::model::Model; /// Contact parameter samples per curve when guessing where two curves touch. @@ -205,42 +205,7 @@ impl Built { }; b.rows = 1; } - K::Tangent | K::Smooth => { - if n != 2 { - return need("two objects"); - } - if c.refs.iter().any(|r| m.is_spline(r)) { - return Self::spline_form(b, m, x); - } - b.rows = 1; - if let (Some((a, bb)), Some((cc, _))) = (ln(0), ci(1)) { - b.form = Form::LC { swap: false }; - b.sign = sgn(dir_n(a, bb).cross(cc - a)); - } else if let (Some((a, bb)), Some((cc, _))) = (ln(1), ci(0)) { - b.form = Form::LC { swap: true }; - b.sign = sgn(dir_n(a, bb).cross(cc - a)); - } else if let (Some((c1, r1)), Some((c2, r2))) = (ci(0), ci(1)) { - b.form = Form::CC; - // sign > 0: external tangency; < 0: internal. - let d = c1.dist(c2); - b.sign = if d >= r1.abs().max(r2.abs()) { 1.0 } else { -1.0 }; - b.target = sgn(r1 - r2); - } else if let (Some((a, bb)), Some(e)) = (ln(0), el(1)) { - b.form = Form::LE { swap: false }; - b.sign = sgn(dir_n(a, bb).cross(e.c - a)); - } else if let (Some((a, bb)), Some(e)) = (ln(1), el(0)) { - b.form = Form::LE { swap: true }; - b.sign = sgn(dir_n(a, bb).cross(e.c - a)); - } else if (ci(0).is_some() || el(0).is_some()) && (ci(1).is_some() || el(1).is_some()) { - b.form = Form::Contact; - b.rows = 3; - b.aux_init = b.contact_guess(m, x).ok_or_else(|| format!("{} could not find where the curves touch", c.kind.name()))?; - } else if ln(0).is_some() && ln(1).is_some() && c.kind == K::Smooth { - b.form = Form::LL; - } else { - return need("a line and a circle/arc/ellipse, two curves, or a spline end and a curve"); - } - } + K::Tangent | K::Smooth => return Self::tangent_form(b, c, m, x), K::Symmetric => { if n != 3 || ln(2).is_none() { return need("two objects and a symmetry line"); @@ -347,154 +312,8 @@ impl Built { out.resize(start + self.rows, 0.0); } - fn eval_inner(&self, m: &Model, x: &[f64], out: &mut Vec) -> Option<()> { - use ConstraintKind as K; - let r = |i: usize| self.refs.get(i); - let pt = |i: usize| m.point(x, r(i)?); - let ln = |i: usize| m.line(x, r(i)?); - let ci = |i: usize| m.circle(x, r(i)?); - let el = |i: usize| m.ellipse(x, r(i)?); - match (self.kind, self.form) { - (_, Form::Lock) => {} - (_, Form::PE { swap }) => { - let (p, e) = if swap { (pt(1)?, el(0)?) } else { (pt(0)?, el(1)?) }; - out.push(e.on(p)); - } - (_, Form::LE { swap }) => { - let ((a, b), e) = if swap { (ln(1)?, el(0)?) } else { (ln(0)?, el(1)?) }; - let d = dir_n(a, b); - out.push(self.sign * d.cross(e.c - a) - e.support(d.perp())); - } - (_, Form::Contact) => { - let (p0, d0) = self.curve_at(m, x, 0, *x.get(self.aux)?)?; - let (p1, d1) = self.curve_at(m, x, 1, *x.get(self.aux + 1)?)?; - out.extend([p0.x - p1.x, p0.y - p1.y, d0.normalized().cross(d1.normalized())]); - } - (_, Form::EndOn { spline, at_end }) => { - let f = m.end_frame(x, r(spline)?, at_end)?; - let o = 1 - spline.min(1); - if let Some((a, b)) = ln(o) { - let d = dir_n(a, b); - out.extend([d.cross(f.p - a), f.t.cross(d)]); - } else if let Some((c, rad)) = ci(o) { - out.extend([f.p.dist(c) - rad, f.t.dot(dir_n(c, f.p))]); - } else { - let e = el(o)?; - out.extend([e.on(f.p), f.t.dot(e.normal(f.p))]); - } - } - (_, Form::Join { a_end, b_end, g2 }) => { - // Orient both so the first curve runs into the joint and the second out of it. - let fa = m.end_frame(x, r(0)?, a_end)?; - let fb = m.end_frame(x, r(1)?, b_end)?; - let fa = if a_end { fa } else { fa.reversed() }; - let fb = if b_end { fb.reversed() } else { fb }; - out.extend([fa.p.x - fb.p.x, fa.p.y - fb.p.y, fa.t.cross(fb.t)]); - if g2 { - out.push(fa.k - fb.k); - } - } - (_, Form::Anchor(a)) => { - let p = pt(0)?; - out.extend([p.x - a.x, p.y - a.y]); - } - (K::Coincident, Form::PP) => { - let (p, q) = (pt(0)?, pt(1)?); - out.extend([p.x - q.x, p.y - q.y]); - } - (K::Coincident, Form::PL { swap }) => { - let (p, (a, b)) = if swap { (pt(1)?, ln(0)?) } else { (pt(0)?, ln(1)?) }; - out.push(dir_n(a, b).cross(p - a)); - } - (K::Coincident, Form::PC { swap }) => { - let (p, (c, rad)) = if swap { (pt(1)?, ci(0)?) } else { (pt(0)?, ci(1)?) }; - out.push(p.dist(c) - rad); - } - (K::Horizontal | K::Vertical, f) => { - let (a, b) = if f == Form::L1 { ln(0).or_else(|| el(0).map(|e| (e.c - e.mj, e.c + e.mj)))? } else { (pt(0)?, pt(1)?) }; - out.push(if self.kind == K::Horizontal { a.y - b.y } else { a.x - b.x }); - } - (K::Parallel | K::Smooth, Form::LL) => { - let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?); - out.push(dir_n(a1, b1).cross(dir_n(a2, b2))); - } - (K::Perpendicular, _) => { - let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?); - out.push(dir_n(a1, b1).dot(dir_n(a2, b2))); - } - (K::Collinear, _) => { - let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?); - let d = dir_n(a1, b1); - out.extend([d.cross(a2 - a1), d.cross(b2 - a1)]); - } - (K::Concentric, _) => { - let center = |i: usize| ci(i).map(|(c, _)| c).or_else(|| el(i).map(|e| e.c)).or_else(|| pt(i)); - let (p, q) = (center(0)?, center(1)?); - out.extend([p.x - q.x, p.y - q.y]); - } - (K::Equal, Form::LL) => { - let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?); - out.push(a1.dist(b1) - a2.dist(b2)); - } - (K::Equal, _) => { - let ((_, r1), (_, r2)) = (ci(0)?, ci(1)?); - out.push(r1 - r2); - } - (K::Tangent | K::Smooth, Form::LC { swap }) => { - let ((a, b), (c, rad)) = if swap { (ln(1)?, ci(0)?) } else { (ln(0)?, ci(1)?) }; - out.push(self.sign * dir_n(a, b).cross(c - a) - rad); - } - (K::Tangent | K::Smooth, _) => { - let ((c1, r1), (c2, r2)) = (ci(0)?, ci(1)?); - let d = c1.dist(c2); - out.push(if self.sign > 0.0 { d - (r1 + r2) } else { d - self.target * (r1 - r2) }); - } - (K::Symmetric, Form::PP) => { - let (p, q, (a, b)) = (pt(0)?, pt(1)?, ln(2)?); - let d = dir_n(a, b); - out.extend([d.cross(p.mid(q) - a), d.dot(q - p)]); - } - (K::Symmetric, Form::SymLines { crossed }) => { - let ((a1, b1), (a2, b2), (a, b)) = (ln(0)?, ln(1)?, ln(2)?); - let (a2, b2) = if crossed { (b2, a2) } else { (a2, b2) }; - let d = dir_n(a, b); - out.extend([d.cross(a1.mid(a2) - a), d.dot(a2 - a1), d.cross(b1.mid(b2) - a), d.dot(b2 - b1)]); - } - (K::Symmetric, _) => { - let ((p, r1), (q, r2), (a, b)) = (ci(0)?, ci(1)?, ln(2)?); - let d = dir_n(a, b); - out.extend([d.cross(p.mid(q) - a), d.dot(q - p), r1 - r2]); - } - (K::Distance(axis), f) => { - let v = match (axis, f) { - (_, Form::PL { swap }) => { - let (p, (a, b)) = if swap { (pt(1)?, ln(0)?) } else { (pt(0)?, ln(1)?) }; - dir_n(a, b).cross(p - a) - } - _ => { - let (a, b) = if f == Form::L1 { ln(0)? } else { (pt(0)?, pt(1)?) }; - match axis { - DistAxis::Aligned => a.dist(b), - DistAxis::Horizontal => b.x - a.x, - DistAxis::Vertical => b.y - a.y, - } - } - }; - out.push(self.sign * v - self.target); - } - (K::Angular, Form::ArcSweep) => { - out.push(m.arc_sweep(x, r(0)?)? - self.target); - } - (K::Angular, Form::Rays { rev }) => { - let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?); - let (d1, d2) = (ray(a1, b1, rev[0]), ray(a2, b2, rev[1])); - out.push(wrap(d1.cross(d2).atan2(d1.dot(d2)) - self.sign * self.target)); - } - (K::Radius, _) => out.push(ci(0)?.1 - self.target), - (K::Diameter, _) => out.push(2.0 * ci(0)?.1 - self.target), - _ => {} - } - Some(()) + fn eval_inner(&self, model: &Model, values: &[f64], out: &mut Vec) -> Option<()> { + ResidualEval { built: self, model, values, out }.evaluate() } /// Point and derivative of curve reference `i` (circle, arc or ellipse) at parameter `t`. @@ -527,6 +346,54 @@ impl Built { /// Tangent or Smooth with a spline: the spline end and the other curve's nearest end are /// joined (Smooth: G2; Tangent between splines: G1), or the spline's end touches a line, /// circle, arc or ellipse tangentially. A `start`/`end` reference picks the end. + fn tangent_form(mut built: Built, constraint: &Constraint, model: &Model, values: &[f64]) -> Result { + use ConstraintKind as K; + + let need = |what: &str| Err(format!("{} needs {what}", constraint.kind.name())); + if built.refs.len() != 2 { + return need("two objects"); + } + if constraint.refs.iter().any(|reference| model.is_spline(reference)) { + return Self::spline_form(built, model, values); + } + + let line = |index| r_at(constraint, index).and_then(|reference| model.line(values, reference)); + let circle = |index| r_at(constraint, index).and_then(|reference| model.circle(values, reference)); + let ellipse = |index| r_at(constraint, index).and_then(|reference| model.ellipse(values, reference)); + built.rows = 1; + + if let (Some((start, end)), Some((center, _))) = (line(0), circle(1)) { + built.form = Form::LC { swap: false }; + built.sign = sgn(dir_n(start, end).cross(center - start)); + } else if let (Some((start, end)), Some((center, _))) = (line(1), circle(0)) { + built.form = Form::LC { swap: true }; + built.sign = sgn(dir_n(start, end).cross(center - start)); + } else if let (Some((first_center, first_radius)), Some((second_center, second_radius))) = (circle(0), circle(1)) { + built.form = Form::CC; + // sign > 0: external tangency; < 0: internal. + let distance = first_center.dist(second_center); + built.sign = if distance >= first_radius.abs().max(second_radius.abs()) { 1.0 } else { -1.0 }; + built.target = sgn(first_radius - second_radius); + } else if let (Some((start, end)), Some(ellipse)) = (line(0), ellipse(1)) { + built.form = Form::LE { swap: false }; + built.sign = sgn(dir_n(start, end).cross(ellipse.c - start)); + } else if let (Some((start, end)), Some(ellipse)) = (line(1), ellipse(0)) { + built.form = Form::LE { swap: true }; + built.sign = sgn(dir_n(start, end).cross(ellipse.c - start)); + } else if (circle(0).is_some() || ellipse(0).is_some()) && (circle(1).is_some() || ellipse(1).is_some()) { + built.form = Form::Contact; + built.rows = 3; + built.aux_init = + built.contact_guess(model, values).ok_or_else(|| format!("{} could not find where the curves touch", constraint.kind.name()))?; + } else if line(0).is_some() && line(1).is_some() && constraint.kind == K::Smooth { + built.form = Form::LL; + } else { + return need("a line and a circle/arc/ellipse, two curves, or a spline end and a curve"); + } + + Ok(built) + } + fn spline_form(mut b: Built, m: &Model, x: &[f64]) -> Result { let (Some(r0), Some(r1)) = (b.refs.first().copied(), b.refs.get(1).copied()) else { return Err(format!("{} needs two objects", b.kind.name())); @@ -596,3 +463,292 @@ impl Built { if self.kind == ConstraintKind::Angular { v.to_degrees() } else { v } } } + +/// Shared access to a constraint's geometry, values, and output while evaluating one residual. +struct ResidualEval<'a> { + built: &'a Built, + model: &'a Model, + values: &'a [f64], + out: &'a mut Vec, +} + +impl ResidualEval<'_> { + fn evaluate(&mut self) -> Option<()> { + use ConstraintKind as K; + + match (self.built.kind, self.built.form) { + (_, Form::Lock) => Some(()), + (_, Form::PE { swap }) => self.eval_point_ellipse(swap), + (_, Form::LE { swap }) => self.eval_line_ellipse(swap), + (_, Form::Contact) => self.eval_contact(), + (_, Form::EndOn { spline, at_end }) => self.eval_end_on(spline, at_end), + (_, Form::Join { a_end, b_end, g2 }) => self.eval_join(a_end, b_end, g2), + (_, Form::Anchor(anchor)) => self.eval_anchor(anchor), + (K::Coincident, Form::PP) => self.eval_coincident_points(), + (K::Coincident, Form::PL { swap }) => self.eval_point_line(swap), + (K::Coincident, Form::PC { swap }) => self.eval_point_circle(swap), + (K::Horizontal | K::Vertical, form) => self.eval_horizontal_vertical(form), + (K::Parallel | K::Smooth, Form::LL) => self.eval_parallel(), + (K::Perpendicular, _) => self.eval_perpendicular(), + (K::Collinear, _) => self.eval_collinear(), + (K::Concentric, _) => self.eval_concentric(), + (K::Equal, Form::LL) => self.eval_equal_lines(), + (K::Equal, _) => self.eval_equal_circles(), + (K::Tangent | K::Smooth, Form::LC { swap }) => self.eval_tangent_line_circle(swap), + (K::Tangent | K::Smooth, _) => self.eval_tangent_circles(), + (K::Symmetric, Form::PP) => self.eval_symmetric_points(), + (K::Symmetric, Form::SymLines { crossed }) => self.eval_symmetric_lines(crossed), + (K::Symmetric, _) => self.eval_symmetric_circles(), + (K::Distance(axis), form) => self.eval_distance(axis, form), + (K::Angular, Form::ArcSweep) => self.eval_angular_arc(), + (K::Angular, Form::Rays { rev }) => self.eval_angular_rays(rev), + (K::Radius, _) => self.eval_radius(), + (K::Diameter, _) => self.eval_diameter(), + _ => Some(()), + } + } + + fn reference(&self, index: usize) -> Option<&GeomRef> { + self.built.refs.get(index) + } + + fn point(&self, index: usize) -> Option { + self.model.point(self.values, self.reference(index)?) + } + + fn line(&self, index: usize) -> Option<(Vec2, Vec2)> { + self.model.line(self.values, self.reference(index)?) + } + + fn circle(&self, index: usize) -> Option<(Vec2, f64)> { + self.model.circle(self.values, self.reference(index)?) + } + + fn ellipse(&self, index: usize) -> Option { + self.model.ellipse(self.values, self.reference(index)?) + } + + fn eval_point_ellipse(&mut self, swap: bool) -> Option<()> { + let (point, ellipse) = if swap { (self.point(1)?, self.ellipse(0)?) } else { (self.point(0)?, self.ellipse(1)?) }; + self.out.push(ellipse.on(point)); + Some(()) + } + + fn eval_line_ellipse(&mut self, swap: bool) -> Option<()> { + let (line, ellipse) = if swap { (self.line(1)?, self.ellipse(0)?) } else { (self.line(0)?, self.ellipse(1)?) }; + let (start, end) = line; + let direction = dir_n(start, end); + self.out.push(self.built.sign * direction.cross(ellipse.c - start) - ellipse.support(direction.perp())); + Some(()) + } + + fn eval_contact(&mut self) -> Option<()> { + let first_parameter = *self.values.get(self.built.aux)?; + let second_parameter = *self.values.get(self.built.aux + 1)?; + let (first_point, first_tangent) = self.built.curve_at(self.model, self.values, 0, first_parameter)?; + let (second_point, second_tangent) = self.built.curve_at(self.model, self.values, 1, second_parameter)?; + self.out.extend([ + first_point.x - second_point.x, + first_point.y - second_point.y, + first_tangent.normalized().cross(second_tangent.normalized()), + ]); + Some(()) + } + + fn eval_end_on(&mut self, spline: usize, at_end: bool) -> Option<()> { + let frame = self.model.end_frame(self.values, self.reference(spline)?, at_end)?; + let other = 1 - spline.min(1); + if let Some((start, end)) = self.line(other) { + let direction = dir_n(start, end); + self.out.extend([direction.cross(frame.p - start), frame.t.cross(direction)]); + } else if let Some((center, radius)) = self.circle(other) { + self.out.extend([frame.p.dist(center) - radius, frame.t.dot(dir_n(center, frame.p))]); + } else { + let ellipse = self.ellipse(other)?; + self.out.extend([ellipse.on(frame.p), frame.t.dot(ellipse.normal(frame.p))]); + } + Some(()) + } + + fn eval_join(&mut self, a_end: bool, b_end: bool, g2: bool) -> Option<()> { + let first = self.model.end_frame(self.values, self.reference(0)?, a_end)?; + let second = self.model.end_frame(self.values, self.reference(1)?, b_end)?; + let first = if a_end { first } else { first.reversed() }; + let second = if b_end { second.reversed() } else { second }; + self.out.extend([first.p.x - second.p.x, first.p.y - second.p.y, first.t.cross(second.t)]); + if g2 { + self.out.push(first.k - second.k); + } + Some(()) + } + + fn eval_anchor(&mut self, anchor: Vec2) -> Option<()> { + let point = self.point(0)?; + self.out.extend([point.x - anchor.x, point.y - anchor.y]); + Some(()) + } + + fn eval_coincident_points(&mut self) -> Option<()> { + let (first, second) = (self.point(0)?, self.point(1)?); + self.out.extend([first.x - second.x, first.y - second.y]); + Some(()) + } + + fn eval_point_line(&mut self, swap: bool) -> Option<()> { + let (point, (start, end)) = if swap { (self.point(1)?, self.line(0)?) } else { (self.point(0)?, self.line(1)?) }; + self.out.push(dir_n(start, end).cross(point - start)); + Some(()) + } + + fn eval_point_circle(&mut self, swap: bool) -> Option<()> { + let (point, (center, radius)) = if swap { (self.point(1)?, self.circle(0)?) } else { (self.point(0)?, self.circle(1)?) }; + self.out.push(point.dist(center) - radius); + Some(()) + } + + fn eval_horizontal_vertical(&mut self, form: Form) -> Option<()> { + use ConstraintKind as K; + + let (first, second) = if form == Form::L1 { + self.line(0).or_else(|| self.ellipse(0).map(|ellipse| (ellipse.c - ellipse.mj, ellipse.c + ellipse.mj)))? + } else { + (self.point(0)?, self.point(1)?) + }; + let value = if self.built.kind == K::Horizontal { first.y - second.y } else { first.x - second.x }; + self.out.push(value); + Some(()) + } + + fn eval_parallel(&mut self) -> Option<()> { + let ((first_start, first_end), (second_start, second_end)) = (self.line(0)?, self.line(1)?); + self.out.push(dir_n(first_start, first_end).cross(dir_n(second_start, second_end))); + Some(()) + } + + fn eval_perpendicular(&mut self) -> Option<()> { + let ((first_start, first_end), (second_start, second_end)) = (self.line(0)?, self.line(1)?); + self.out.push(dir_n(first_start, first_end).dot(dir_n(second_start, second_end))); + Some(()) + } + + fn eval_collinear(&mut self) -> Option<()> { + let ((first_start, first_end), (second_start, second_end)) = (self.line(0)?, self.line(1)?); + let direction = dir_n(first_start, first_end); + self.out.extend([direction.cross(second_start - first_start), direction.cross(second_end - first_start)]); + Some(()) + } + + fn eval_concentric(&mut self) -> Option<()> { + let center = |index| { + self.circle(index).map(|(center, _)| center).or_else(|| self.ellipse(index).map(|ellipse| ellipse.c)).or_else(|| self.point(index)) + }; + let (first, second) = (center(0)?, center(1)?); + self.out.extend([first.x - second.x, first.y - second.y]); + Some(()) + } + + fn eval_equal_lines(&mut self) -> Option<()> { + let ((first_start, first_end), (second_start, second_end)) = (self.line(0)?, self.line(1)?); + self.out.push(first_start.dist(first_end) - second_start.dist(second_end)); + Some(()) + } + + fn eval_equal_circles(&mut self) -> Option<()> { + let ((_, first_radius), (_, second_radius)) = (self.circle(0)?, self.circle(1)?); + self.out.push(first_radius - second_radius); + Some(()) + } + + fn eval_tangent_line_circle(&mut self, swap: bool) -> Option<()> { + let ((start, end), (center, radius)) = if swap { (self.line(1)?, self.circle(0)?) } else { (self.line(0)?, self.circle(1)?) }; + self.out.push(self.built.sign * dir_n(start, end).cross(center - start) - radius); + Some(()) + } + + fn eval_tangent_circles(&mut self) -> Option<()> { + let ((first_center, first_radius), (second_center, second_radius)) = (self.circle(0)?, self.circle(1)?); + let distance = first_center.dist(second_center); + let residual = if self.built.sign > 0.0 { + distance - (first_radius + second_radius) + } else { + distance - self.built.target * (first_radius - second_radius) + }; + self.out.push(residual); + Some(()) + } + + fn eval_symmetric_points(&mut self) -> Option<()> { + let (first, second, (line_start, line_end)) = (self.point(0)?, self.point(1)?, self.line(2)?); + let direction = dir_n(line_start, line_end); + self.out.extend([direction.cross(first.mid(second) - line_start), direction.dot(second - first)]); + Some(()) + } + + fn eval_symmetric_lines(&mut self, crossed: bool) -> Option<()> { + let ((first_start, first_end), (second_start, second_end), (line_start, line_end)) = (self.line(0)?, self.line(1)?, self.line(2)?); + let (second_start, second_end) = if crossed { (second_end, second_start) } else { (second_start, second_end) }; + let direction = dir_n(line_start, line_end); + self.out.extend([ + direction.cross(first_start.mid(second_start) - line_start), + direction.dot(second_start - first_start), + direction.cross(first_end.mid(second_end) - line_start), + direction.dot(second_end - first_end), + ]); + Some(()) + } + + fn eval_symmetric_circles(&mut self) -> Option<()> { + let ((first_center, first_radius), (second_center, second_radius), (line_start, line_end)) = + (self.circle(0)?, self.circle(1)?, self.line(2)?); + let direction = dir_n(line_start, line_end); + self.out.extend([ + direction.cross(first_center.mid(second_center) - line_start), + direction.dot(second_center - first_center), + first_radius - second_radius, + ]); + Some(()) + } + + fn eval_distance(&mut self, axis: DistAxis, form: Form) -> Option<()> { + let value = match (axis, form) { + (_, Form::PL { swap }) => { + let (point, (start, end)) = if swap { (self.point(1)?, self.line(0)?) } else { (self.point(0)?, self.line(1)?) }; + dir_n(start, end).cross(point - start) + } + _ => { + let (first, second) = if form == Form::L1 { self.line(0)? } else { (self.point(0)?, self.point(1)?) }; + match axis { + DistAxis::Aligned => first.dist(second), + DistAxis::Horizontal => second.x - first.x, + DistAxis::Vertical => second.y - first.y, + } + } + }; + self.out.push(self.built.sign * value - self.built.target); + Some(()) + } + + fn eval_angular_arc(&mut self) -> Option<()> { + let reference = self.reference(0)?; + self.out.push(self.model.arc_sweep(self.values, reference)? - self.built.target); + Some(()) + } + + fn eval_angular_rays(&mut self, reversed: [bool; 2]) -> Option<()> { + let ((first_start, first_end), (second_start, second_end)) = (self.line(0)?, self.line(1)?); + let (first, second) = (ray(first_start, first_end, reversed[0]), ray(second_start, second_end, reversed[1])); + let residual = wrap(first.cross(second).atan2(first.dot(second)) - self.built.sign * self.built.target); + self.out.push(residual); + Some(()) + } + + fn eval_radius(&mut self) -> Option<()> { + self.out.push(self.circle(0)?.1 - self.built.target); + Some(()) + } + + fn eval_diameter(&mut self) -> Option<()> { + self.out.push(2.0 * self.circle(0)?.1 - self.built.target); + Some(()) + } +} diff --git a/crates/doc/src/dynblock.rs b/crates/doc/src/dynblock.rs index 6887459a4..ef0a88be2 100644 --- a/crates/doc/src/dynblock.rs +++ b/crates/doc/src/dynblock.rs @@ -5,7 +5,7 @@ //! whose `ACAD_ENHANCEDBLOCK` entry is an evaluation graph (`ACAD_EVALUATION_GRAPH`), and the //! graph lists the block's *parameters* (a distance, a point, visibility states, a lookup…) and //! *actions* (what changing a parameter does to the block's objects: stretch, move, look up…). -//! [`dynamic_block`] reads them into the model below without changing the kept data, so a drawing +//! [`dynamic_block`](crate::dynblock::dynamic_block) reads them into the model below without changing the kept data, so a drawing //! saved again still carries them exactly as read. //! //! Every count and index here comes from the file: they are capped by the data actually present diff --git a/crates/doc/src/entity/block.rs b/crates/doc/src/entity/block.rs index 166afee27..3d6665e0b 100644 --- a/crates/doc/src/entity/block.rs +++ b/crates/doc/src/entity/block.rs @@ -1,6 +1,6 @@ use super::common::{Common, Lineweight}; use super::text::{MText, Text}; -use super::*; +use super::{Color, Deserialize, EntityKind, Mat3, Serialize, Vec2, Vec3}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/doc/src/entity/common.rs b/crates/doc/src/entity/common.rs index 6f7867ab3..17fc43c75 100644 --- a/crates/doc/src/entity/common.rs +++ b/crates/doc/src/entity/common.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Color, Deserialize, Serialize, Vec3}; /// A database handle (DXF group 5), unique per drawing. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] diff --git a/crates/doc/src/entity/curves.rs b/crates/doc/src/entity/curves.rs index 770b22559..8eeb556c4 100644 --- a/crates/doc/src/entity/curves.rs +++ b/crates/doc/src/entity/curves.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Deserialize, PolyVertex, Serialize, Vec3}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Line { diff --git a/crates/doc/src/entity/dim.rs b/crates/doc/src/entity/dim.rs index 396e74c6f..d10237b26 100644 --- a/crates/doc/src/entity/dim.rs +++ b/crates/doc/src/entity/dim.rs @@ -1,7 +1,7 @@ use super::block::Insert; use super::common::Handle; use super::text::MText; -use super::*; +use super::{Deserialize, GSpline, Serialize, Vec2, Vec3, one, standard, yes}; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -92,7 +92,7 @@ impl DimMarks { *self == DimMarks::default() } - /// Transform the points and angles (part of [`EntityKind::transform`]). + /// Transform the points and angles (part of [`super::EntityKind::transform`]). pub(super) fn transform(&mut self, t3: &dyn Fn(&mut Vec3), rot: f64, s: f64, mirror: bool) { if let Some(a) = &mut self.oblique { *a = cadcraft_geom::norm_angle(if mirror { rot - (*a - rot) } else { *a + rot }); diff --git a/crates/doc/src/entity/fill.rs b/crates/doc/src/entity/fill.rs index 1b69108a0..6790b0a7c 100644 --- a/crates/doc/src/entity/fill.rs +++ b/crates/doc/src/entity/fill.rs @@ -1,5 +1,5 @@ use super::common::Handle; -use super::*; +use super::{Color, Deserialize, PolyVertex, Serialize, Vec2, one}; /// A hatch boundary loop: closed polyline with bulges. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/crates/doc/src/entity/layout.rs b/crates/doc/src/entity/layout.rs index 7ebe49eee..be1b9584d 100644 --- a/crates/doc/src/entity/layout.rs +++ b/crates/doc/src/entity/layout.rs @@ -1,5 +1,5 @@ use super::common::Handle; -use super::*; +use super::{Color, Deserialize, Serialize, Vec2, Vec3, standard, yes}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/doc/src/entity/text.rs b/crates/doc/src/entity/text.rs index a4eff0427..75857eb30 100644 --- a/crates/doc/src/entity/text.rs +++ b/crates/doc/src/entity/text.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Deserialize, Serialize, Vec2, Vec3, one, standard}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/engine/src/autosave.rs b/crates/engine/src/autosave.rs index 4da107449..14b363996 100644 --- a/crates/engine/src/autosave.rs +++ b/crates/engine/src/autosave.rs @@ -256,7 +256,10 @@ mod native { use std::path::Path; use std::time::{Duration, SystemTime}; - use super::*; + use super::{ + Autosaver, Drawing, HEARTBEAT_SECS, HashMap, Job, Recoverable, STALE_LOCK_SECS, Session, is_instance_id, parse_file_name, parse_lock_name, + savefilepath, savetime, + }; /// A new instance id: the process id and the start time. pub fn instance_id() -> String { diff --git a/crates/engine/src/cmd/annotate/angular.rs b/crates/engine/src/cmd/annotate/angular.rs index e576798ca..98ee88c79 100644 --- a/crates/engine/src/cmd/annotate/angular.rs +++ b/crates/engine/src/cmd/annotate/angular.rs @@ -1,5 +1,8 @@ use super::common::{DimText, add_dim, assoc_enabled, dim, hex_param, pick_at, segment_at, with_assoc}; -use super::*; +use super::{ + Accept, AssocSnap, DimAssoc, DimKind, EntityKind, Handle, Input, Interactive, Prompt, Result, Segment, Session, Step, Value, Vec2, bad, json, + point_req, targets, v3, +}; /// Two-line angular dimension (type 2) between two lines, attached to them. fn angular_from_lines(s: &Session, h1: Handle, h2: Handle, at: Vec2) -> Option { diff --git a/crates/engine/src/cmd/annotate/common.rs b/crates/engine/src/cmd/annotate/common.rs index 2dec75558..2267f16bf 100644 --- a/crates/engine/src/cmd/annotate/common.rs +++ b/crates/engine/src/cmd/annotate/common.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Accept, DimAssoc, DimKind, Dimension, EntityKind, Handle, Input, Prompt, Result, Segment, Session, Value, Vec2, Vec3, v3}; pub(super) fn style(s: &Session) -> String { s.doc().map(|d| d.header.str("DIMSTYLE", "Standard")).unwrap_or_else(|_| "Standard".into()) diff --git a/crates/engine/src/cmd/annotate/edit.rs b/crates/engine/src/cmd/annotate/edit.rs index 74292bc81..38f02005f 100644 --- a/crates/engine/src/cmd/annotate/edit.rs +++ b/crates/engine/src/cmd/annotate/edit.rs @@ -1,5 +1,8 @@ use super::common::{hex_param, style}; -use super::*; +use super::{ + Accept, DimKind, EntityKind, Handle, Input, Interactive, Prompt, Result, SelOutcome, SelectPhase, Session, Step, Value, Vec2, bad, json, + point_param, str_param, targets, v3, +}; pub(super) fn run_update(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; diff --git a/crates/engine/src/cmd/annotate/linear.rs b/crates/engine/src/cmd/annotate/linear.rs index ab0a14214..6bcf8fbdb 100644 --- a/crates/engine/src/cmd/annotate/linear.rs +++ b/crates/engine/src/cmd/annotate/linear.rs @@ -1,5 +1,8 @@ use super::common::{DimText, add_dim, assoc_enabled, circle_of, dim, linear_kind, pick_at}; -use super::*; +use super::{ + Accept, DimKind, Dimension, EngineError, EntityKind, Handle, Input, Interactive, Prim, Prompt, Result, Segment, Session, Step, Value, Vec2, bad, + json, point_req, points_param, str_param, v3, +}; pub(super) fn run_linear(s: &mut Session, p: &Value) -> Result { let (a, b) = match p.get("object").and_then(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle))) { diff --git a/crates/engine/src/cmd/annotate/mleader.rs b/crates/engine/src/cmd/annotate/mleader.rs index c2e0a63df..536e83b8c 100644 --- a/crates/engine/src/cmd/annotate/mleader.rs +++ b/crates/engine/src/cmd/annotate/mleader.rs @@ -1,5 +1,8 @@ use super::common::style; -use super::*; +use super::{ + Accept, EngineError, EntityKind, Input, Interactive, LeaderOrder, MLeader, MLeaderOptions, MText, Prompt, Result, Session, Step, Value, Vec2, + bad, json, points_param, str_param, v3, +}; /// A multileader from arrowhead to landing; `landing` false leaves out the landing line (dogleg). fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option { diff --git a/crates/engine/src/cmd/annotate/radial.rs b/crates/engine/src/cmd/annotate/radial.rs index a83f7dfd2..cee5dcc01 100644 --- a/crates/engine/src/cmd/annotate/radial.rs +++ b/crates/engine/src/cmd/annotate/radial.rs @@ -1,5 +1,5 @@ use super::common::{DimText, add_dim, circle_of, dim, pick_at, text_at}; -use super::*; +use super::{Accept, DimKind, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, bad, json, point_param, targets}; fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result { let id = if diameter { "dimdiameter" } else { "dimradius" }; diff --git a/crates/engine/src/cmd/draw/circle.rs b/crates/engine/src/cmd/draw/circle.rs index 146b4de50..789cb5366 100644 --- a/crates/engine/src/cmd/draw/circle.rs +++ b/crates/engine/src/cmd/draw/circle.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Arc, Circle, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, added, arc, bad, circle, f64_req, line, number, + point_param, size_param, +}; pub(super) fn run_circle(s: &mut Session, p: &Value) -> Result { let c = if let Some(center) = point_param(p, "center") { diff --git a/crates/engine/src/cmd/draw/ellipse.rs b/crates/engine/src/cmd/draw/ellipse.rs index 87391c263..c1e7967d9 100644 --- a/crates/engine/src/cmd/draw/ellipse.rs +++ b/crates/engine/src/cmd/draw/ellipse.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Ellipse, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, added, bad, f64_or, f64_req, line, number, + point_req, v3, +}; pub(super) fn run_ellipse(s: &mut Session, p: &Value) -> Result { if p.get("isocircle").is_some() { diff --git a/crates/engine/src/cmd/draw/line_pline.rs b/crates/engine/src/cmd/draw/line_pline.rs index b7ad2455c..9f6df5fa6 100644 --- a/crates/engine/src/cmd/draw/line_pline.rs +++ b/crates/engine/src/cmd/draw/line_pline.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Input, Interactive, Outcome, PlineAsk, PolyVertex, Prompt, Result, Session, Step, Value, Vec2, added, arc_to_bulge, bad, + bool_or, bulge_to_arc, distinct_points, f64_or, json, line, lwpoly, number, point_value, points_param, size_param, snap, +}; // ---------------- JSON forms ---------------- diff --git a/crates/engine/src/cmd/draw/point.rs b/crates/engine/src/cmd/draw/point.rs index 507b0b10d..17f486b76 100644 --- a/crates/engine/src/cmd/draw/point.rs +++ b/crates/engine/src/cmd/draw/point.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Input, Interactive, Point, PolyVertex, Prompt, RayLine, Result, Session, Step, Value, Vec2, added, bad, json, lwpoly, number, + point_param, point_req, points_param, size_param, v3, +}; pub(super) fn run_point(s: &mut Session, p: &Value) -> Result { let pts = match point_param(p, "at") { diff --git a/crates/engine/src/cmd/draw/polygon.rs b/crates/engine/src/cmd/draw/polygon.rs index d67aa62e6..d13618683 100644 --- a/crates/engine/src/cmd/draw/polygon.rs +++ b/crates/engine/src/cmd/draw/polygon.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, added, bad, bool_or, lwpoly, number, point_req, points_param, + polygon_from_edge, polygon_typed_radius_point, polygon_vertices, size_param, +}; pub(super) fn run_polygon(s: &mut Session, p: &Value) -> Result { // A `sides` that is not a whole number (e.g. -3) is refused, not replaced by the default 4. diff --git a/crates/engine/src/cmd/draw/spline.rs b/crates/engine/src/cmd/draw/spline.rs index f10a0dd80..498f1a9f2 100644 --- a/crates/engine/src/cmd/draw/spline.rs +++ b/crates/engine/src/cmd/draw/spline.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Input, Interactive, Prompt, Result, Session, Spline, Step, Value, Vec2, added, bad, bool_or, distinct_points, find_command, + points_param, +}; pub(super) fn run_spline(s: &mut Session, p: &Value) -> Result { if p.get("object").is_some() { diff --git a/crates/engine/src/cmd/draw/text.rs b/crates/engine/src/cmd/draw/text.rs index 2ca220a9b..21962c469 100644 --- a/crates/engine/src/cmd/draw/text.rs +++ b/crates/engine/src/cmd/draw/text.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, HAlign, Input, Interactive, MText, PolyVertex, Prompt, Result, Session, Step, Text, VAlign, Value, Vec2, added, bad, bool_or, + f64_or, lwpoly, number, point_req, str_param, v3, +}; /// Justification code → (halign, valign). pub(crate) fn justify(j: &str) -> Option<(HAlign, VAlign)> { diff --git a/crates/engine/src/cmd/draw2/arc.rs b/crates/engine/src/cmd/draw2/arc.rs index f219348df..332f22653 100644 --- a/crates/engine/src/cmd/draw2/arc.rs +++ b/crates/engine/src/cmd/draw2/arc.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Arc, Circle, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, added, arc, bad, circle, curves, line, + norm_angle, number, other, point_param, point_req, point_value, +}; // ===================================================================================== // Generic point/value sequences (arc and circle variants) diff --git a/crates/engine/src/cmd/draw2/centermark.rs b/crates/engine/src/cmd/draw2/centermark.rs index 00edfbcc3..2a13a8e02 100644 --- a/crates/engine/src/cmd/draw2/centermark.rs +++ b/crates/engine/src/cmd/draw2/centermark.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Input, Interactive, Prompt, Result, Session, Step, Value, Vec2, added, bad, curves, json, line, other, targets, +}; // ===================================================================================== // CENTERMARK / CENTERLINE diff --git a/crates/engine/src/cmd/draw2/curves3d.rs b/crates/engine/src/cmd/draw2/curves3d.rs index 70af51c53..005c49f07 100644 --- a/crates/engine/src/cmd/draw2/curves3d.rs +++ b/crates/engine/src/cmd/draw2/curves3d.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Helix, HelixConstrain, Input, Interactive, MAX_GEN, Polyline3d, Prompt, Result, Session, Spline, Step, Value, Vec2, + Vec3, added, bad, bool_or, circle, curves, f64_or, f64_req, line, number, other, point_req, points_param, v3, +}; // ===================================================================================== // 3DPOLY diff --git a/crates/engine/src/cmd/draw2/divide.rs b/crates/engine/src/cmd/draw2/divide.rs index 1ba624c15..6b6ea28e3 100644 --- a/crates/engine/src/cmd/draw2/divide.rs +++ b/crates/engine/src/cmd/draw2/divide.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Chain, EntityKind, Handle, Input, Interactive, MAX_GEN, Point, Prompt, Result, Session, Step, Value, Vec2, bad, bool_or, curves, f64_req, + json, number, other, point_param, str_param, targets, v3, +}; // ===================================================================================== // DIVIDE / MEASURE diff --git a/crates/engine/src/cmd/draw2/ellipse.rs b/crates/engine/src/cmd/draw2/ellipse.rs index ca3f9c6e2..e699eac58 100644 --- a/crates/engine/src/cmd/draw2/ellipse.rs +++ b/crates/engine/src/cmd/draw2/ellipse.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Ellipse, EntityKind, Line, Result, Session, TAU, Value, Vec2, added, bad, f64_req, point_param, point_req, v3}; // ---------------- ellipse entries ---------------- diff --git a/crates/engine/src/cmd/draw2/rect.rs b/crates/engine/src/cmd/draw2/rect.rs index 84db5093a..4b19dc703 100644 --- a/crates/engine/src/cmd/draw2/rect.rs +++ b/crates/engine/src/cmd/draw2/rect.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Input, Interactive, PolyVertex, Prompt, Result, Session, Step, Value, Vec2, added, bad, bool_or, curves, f64_or, lwpoly, + number, other, point_param, point_req, rect_with_corners, size_param, +}; // ===================================================================================== // RECTANG with Area / Dimensions / Rotation / Width / Elevation / Thickness diff --git a/crates/engine/src/cmd/draw2/revcloud.rs b/crates/engine/src/cmd/draw2/revcloud.rs index 83d8cbb75..634456b05 100644 --- a/crates/engine/src/cmd/draw2/revcloud.rs +++ b/crates/engine/src/cmd/draw2/revcloud.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Chain, Circle, Ellipse, EntityKind, Handle, Input, Interactive, Line, MAX_GEN, PolyVertex, Polyline, Prompt, Result, Session, Step, + Value, Vec2, added, bad, bool_or, curves, lwpoly, number, other, point_param, points_param, rect_vertices, shoelace, str_param, +}; // ===================================================================================== // REVCLOUD diff --git a/crates/engine/src/cmd/draw2/tancircle.rs b/crates/engine/src/cmd/draw2/tancircle.rs index 48268987a..0d4cd9ca7 100644 --- a/crates/engine/src/cmd/draw2/tancircle.rs +++ b/crates/engine/src/cmd/draw2/tancircle.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Circle, Handle, Input, Interactive, Prompt, Result, Session, Step, TanObj, Value, Vec2, added, bad, circle, curves, f64_req, number, + other, point_param, point_req, +}; pub(super) fn run_circle_variant(s: &mut Session, p: &Value) -> Result { let c = if let (Some(c), Some(d)) = (point_param(p, "center"), p.get("diameter").and_then(Value::as_f64)) { diff --git a/crates/engine/src/cmd/draw2/wipeout.rs b/crates/engine/src/cmd/draw2/wipeout.rs index 212b645f3..e5ec3387f 100644 --- a/crates/engine/src/cmd/draw2/wipeout.rs +++ b/crates/engine/src/cmd/draw2/wipeout.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Input, Interactive, MAX_GEN, PolyVertex, Prompt, Result, Session, Step, Value, Vec2, Wipeout, added, bad, bool_or, + curves, json, lwpoly, other, points_param, shoelace, str_param, +}; // ===================================================================================== // WIPEOUT diff --git a/crates/engine/src/cmd/draw2/xline.rs b/crates/engine/src/cmd/draw2/xline.rs index 1e4b6cdae..31f152d16 100644 --- a/crates/engine/src/cmd/draw2/xline.rs +++ b/crates/engine/src/cmd/draw2/xline.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Input, Interactive, Polyline, Prompt, RayLine, Result, Session, Step, Value, Vec2, added, bad, bool_or, curves, + f64_req, number, other, point_param, point_req, v3, +}; // ===================================================================================== // XLINE with Hor/Ver/Ang/Bisect/Offset diff --git a/crates/engine/src/cmd/hatch.rs b/crates/engine/src/cmd/hatch.rs index d0221a00a..df0fef550 100644 --- a/crates/engine/src/cmd/hatch.rs +++ b/crates/engine/src/cmd/hatch.rs @@ -10,7 +10,7 @@ use cadcraft_geom::{Bounds2, PolyVertex, Polyline, Vec2, point_in_polygon}; use serde_json::{Value, json}; use super::helpers::lwpoly; -use super::*; +use super::{CommandSpec, bad, bool_or, f64_or, point_param, points_param, size_param, str_param, targets}; use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Session, Step}; diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 9821055e9..a538d73b5 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -9,7 +9,7 @@ use cadcraft_geom::{Bounds2, Vec2}; use serde_json::{Value, json}; use super::file::{base64_encode, io}; -use super::*; +use super::{CommandSpec, bad, bool_or, find_command, point_param, point_value, str_param, targets}; use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step}; use viewports::MviewM; diff --git a/crates/engine/src/cmd/modify/break_join.rs b/crates/engine/src/cmd/modify/break_join.rs index 16892c199..c7432e223 100644 --- a/crates/engine/src/cmd/modify/break_join.rs +++ b/crates/engine/src/cmd/modify/break_join.rs @@ -1,5 +1,8 @@ use super::trim::{param_on, sub_polyline}; -use super::*; +use super::{ + Accept, Arc, EngineError, Entity, EntityKind, Handle, Input, Interactive, Line, PolyVertex, Polyline, Prompt, Result, Segment, Session, Step, + Value, Vec2, arc, bad, json, lwpoly, norm_angle, point_param, point_req, targets, +}; /// Gives a rebuilt arc piece its source centre Z, or a rebuilt polyline piece its source elevation. fn with_z(mut k: EntityKind, z: f64) -> EntityKind { diff --git a/crates/engine/src/cmd/modify/explode.rs b/crates/engine/src/cmd/modify/explode.rs index 3b25ccfee..46ada655d 100644 --- a/crates/engine/src/cmd/modify/explode.rs +++ b/crates/engine/src/cmd/modify/explode.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Entity, EntityKind, Handle, PolyVertex, Polyline, Result, Segment, Session, Value, Vec2, arc, json, line, lwpoly, targets, v3}; // ---------------- explode / array / order / break / join ---------------- diff --git a/crates/engine/src/cmd/modify/fillet.rs b/crates/engine/src/cmd/modify/fillet.rs index 01d701817..963de2607 100644 --- a/crates/engine/src/cmd/modify/fillet.rs +++ b/crates/engine/src/cmd/modify/fillet.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Arc, EngineError, Entity, EntityKind, Handle, Input, Interactive, Line, PolyVertex, Prompt, Result, Session, Step, Value, Vec2, arc, bad, + bool_or, ccw_sweep, f64_or, json, line, line_line_infinite, number, point_param, +}; // ---------------- fillet / chamfer ---------------- diff --git a/crates/engine/src/cmd/modify/offset.rs b/crates/engine/src/cmd/modify/offset.rs index 67bdd7ec7..01ca46c04 100644 --- a/crates/engine/src/cmd/modify/offset.rs +++ b/crates/engine/src/cmd/modify/offset.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Arc, EngineError, Entity, EntityKind, Handle, Input, Interactive, Line, PolyVertex, Polyline, Prompt, Result, Segment, Session, Step, + Value, Vec2, arc, bad, bool_or, ccw_sweep, circle, f64_req, intersect_ext, json, line, lwpoly, number, point_req, str_param, targets, v3, +}; /// Whether OFFSET can work on this kind of object at all. fn offsettable(k: &EntityKind) -> bool { diff --git a/crates/engine/src/cmd/modify/select_then.rs b/crates/engine/src/cmd/modify/select_then.rs index 75264990c..10bf08399 100644 --- a/crates/engine/src/cmd/modify/select_then.rs +++ b/crates/engine/src/cmd/modify/select_then.rs @@ -1,7 +1,9 @@ use super::break_join::join; use super::explode::explode; use super::transform::{erase, transform_entities}; -use super::*; +use super::{ + Accept, EngineError, EntityKind, Handle, Input, Interactive, Mat3, Prompt, Result, SelOutcome, SelectPhase, Session, Step, Vec2, line, number, +}; // ---------------- interactive ---------------- diff --git a/crates/engine/src/cmd/modify/stretch.rs b/crates/engine/src/cmd/modify/stretch.rs index 5aa91bbef..a277f9774 100644 --- a/crates/engine/src/cmd/modify/stretch.rs +++ b/crates/engine/src/cmd/modify/stretch.rs @@ -1,5 +1,8 @@ use super::transform::{delta_of, transform_entities}; -use super::*; +use super::{ + Accept, Entity, EntityKind, Handle, Input, Interactive, Mat3, Prompt, Result, SelectKind, SelectMode, Session, Step, Value, Vec2, bad, json, + line, points_param, +}; /// An area whose vertices STRETCH moves: a window's box, or a polygon closed back to its first /// point. diff --git a/crates/engine/src/cmd/modify/transform.rs b/crates/engine/src/cmd/modify/transform.rs index 760e37cbc..5474dde16 100644 --- a/crates/engine/src/cmd/modify/transform.rs +++ b/crates/engine/src/cmd/modify/transform.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Entity, Handle, Mat3, Result, Session, Value, Vec2, bad, bool_or, f64_req, json, point_param, point_req, targets}; // ---------------- core operations ---------------- diff --git a/crates/engine/src/cmd/modify/trim.rs b/crates/engine/src/cmd/modify/trim.rs index 4caaadfb6..66eab0be8 100644 --- a/crates/engine/src/cmd/modify/trim.rs +++ b/crates/engine/src/cmd/modify/trim.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Arc, EngineError, Entity, EntityKind, Handle, Line, PolyVertex, Polyline, Prim, Result, Segment, Session, TAU, Vec2, angle_in_sweep, arc, + ccw_sweep, intersect_ext, line, lwpoly, norm_angle, +}; // ---------------- trim / extend ---------------- diff --git a/crates/engine/src/cmd/modify2/align.rs b/crates/engine/src/cmd/modify2/align.rs index 90e13887d..edb7ef22c 100644 --- a/crates/engine/src/cmd/modify2/align.rs +++ b/crates/engine/src/cmd/modify2/align.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Input, Interactive, Mat3, Prompt, Result, SelOutcome, SelectPhase, Session, Step, Value, Vec2, bool_or, curves, json, + line, point_param, point_req, targets, +}; // ===================================================================================== // ALIGN diff --git a/crates/engine/src/cmd/modify2/blend.rs b/crates/engine/src/cmd/modify2/blend.rs index 72dae0f43..32b477eea 100644 --- a/crates/engine/src/cmd/modify2/blend.rs +++ b/crates/engine/src/cmd/modify2/blend.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Chain, EntityKind, Handle, Input, Interactive, Prompt, Result, Session, Spline, Step, Value, Vec2, bad, curves, json, other, point_req, + str_param, +}; // ===================================================================================== // BLEND diff --git a/crates/engine/src/cmd/modify2/fillet.rs b/crates/engine/src/cmd/modify2/fillet.rs index bdc5dded6..20062a87f 100644 --- a/crates/engine/src/cmd/modify2/fillet.rs +++ b/crates/engine/src/cmd/modify2/fillet.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Arc, EntityKind, Handle, LwPolyline, PolyVertex, Result, Session, TanObj, Vec2, arc, ccw_sweep, curve_edits, curves, line, line_circle, + line_line_infinite, other, push_entity, set_kind, +}; // ===================================================================================== // FILLET extensions (arcs, circles, polylines) diff --git a/crates/engine/src/cmd/modify2/lengthen.rs b/crates/engine/src/cmd/modify2/lengthen.rs index d3021cb42..0c86c2c9c 100644 --- a/crates/engine/src/cmd/modify2/lengthen.rs +++ b/crates/engine/src/cmd/modify2/lengthen.rs @@ -1,5 +1,8 @@ use super::trimext::ellipse_of; -use super::*; +use super::{ + Accept, Arc, Chain, EntityKind, Handle, Input, Interactive, LwPolyline, Polyline, Prompt, Result, Session, Step, TAU, Value, Vec2, bad, + ccw_sweep, curve_edits, curves, json, number, other, point_param, set_kind, targets, +}; // ===================================================================================== // LENGTHEN diff --git a/crates/engine/src/cmd/modify2/ncopy.rs b/crates/engine/src/cmd/modify2/ncopy.rs index 237110a5e..475256e84 100644 --- a/crates/engine/src/cmd/modify2/ncopy.rs +++ b/crates/engine/src/cmd/modify2/ncopy.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Common, EntityKind, Handle, Input, Interactive, MAX_BLOCK_DEPTH, MAX_GEN, Mat3, Prompt, Result, Session, Step, Value, Vec2, bad, curves, + json, other, point_param, push_entity, targets, +}; // ===================================================================================== // NCOPY diff --git a/crates/engine/src/cmd/modify2/patharray.rs b/crates/engine/src/cmd/modify2/patharray.rs index e8f03fc6b..3f9445249 100644 --- a/crates/engine/src/cmd/modify2/patharray.rs +++ b/crates/engine/src/cmd/modify2/patharray.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Accept, Chain, Handle, Input, Interactive, Mat3, Prompt, Result, SelOutcome, SelectPhase, Session, Step, Value, bad, bool_or, curves, json, + number, other, targets, +}; // ===================================================================================== // ARRAYPATH (non-associative copies along a curve) diff --git a/crates/engine/src/cmd/modify2/pedit.rs b/crates/engine/src/cmd/modify2/pedit.rs index 399759cf4..ae9110e48 100644 --- a/crates/engine/src/cmd/modify2/pedit.rs +++ b/crates/engine/src/cmd/modify2/pedit.rs @@ -1,4 +1,8 @@ -use super::*; +use super::{ + Accept, Chain, EntityKind, Handle, Input, Interactive, LwPolyline, MAX_GEN, PolyVertex, Prompt, Result, SelOutcome, SelectPhase, Session, Spline, + Step, Value, Vec2, as_lwpoly, bad, bool_or, curves, f64_or, f64_req, json, line, number, other, pedit_vertex, point_req, set_kind, str_param, + targets, +}; /// Fit curve: a tangent-continuous pair of arcs (biarc) per segment through every vertex, /// following the tangent directions given per vertex (Edit vertex Tangent). diff --git a/crates/engine/src/cmd/modify2/selop.rs b/crates/engine/src/cmd/modify2/selop.rs index 90330bbbf..f1b63e3ce 100644 --- a/crates/engine/src/cmd/modify2/selop.rs +++ b/crates/engine/src/cmd/modify2/selop.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + EntityKind, Handle, Input, Interactive, LwPolyline, Mat3, PolyVertex, Prompt, Result, SelOutcome, SelectPhase, Session, Space, Step, Value, + curves, json, lwpoly, other, set_kind, str_param, targets, +}; // ===================================================================================== // REVERSE / CHSPACE / FLATTEN (select, then act) diff --git a/crates/engine/src/cmd/modify2/splinedit.rs b/crates/engine/src/cmd/modify2/splinedit.rs index f5ba767fb..ae01e89c6 100644 --- a/crates/engine/src/cmd/modify2/splinedit.rs +++ b/crates/engine/src/cmd/modify2/splinedit.rs @@ -499,6 +499,94 @@ impl SplineditM { } } +impl SplineditM { + fn input_main(&mut self, s: &mut Session, h: Handle, sp: &Spline, enter: bool, kw: &str) -> Result> { + match (enter, kw) { + (true, _) | (_, "exit" | "x") => { + s.set_selection(Vec::new()); + return Ok(Some(Step::Done)); + } + (_, "close" | "c") => self.apply(s, json!({ "option": "close" }))?, + (_, "open" | "o") => self.apply(s, json!({ "option": "open" }))?, + (_, "reverse" | "r") => self.apply(s, json!({ "option": "reverse" }))?, + (_, "convert to polyline" | "p") => self.sub = Sub::Precision, + (_, "fit data" | "f") if sp.fit.is_empty() => s.echo("The spline has no fit data."), + (_, "fit data" | "f") => self.sub = Sub::Fit, + (_, "edit vertex" | "e") => { + self.mark = 0; + self.sub = Sub::Vertex; + } + (_, "join" | "j") if sp.closed => s.echo("Cannot join to a closed spline."), + (_, "join" | "j") => { + self.join = SelectPhase::default(); + self.sub = Sub::Join; + } + (_, "undo" | "u") => { + if let Some((at, ents)) = self.joined.last() + && *at == self.history.len() + { + let d = s.doc_mut()?; + for (space, e) in ents { + if let Some(st) = d.space_mut(space) { + st.push(e.clone()); + } + } + self.joined.pop(); + } + match self.history.pop() { + Some(k) => set_kind(s, h, k)?, + None => s.echo("All edits have been undone."), + } + } + (_, "") => {} + _ => return Err(other("Invalid option keyword.")), + } + Ok(None) + } + + fn input_fit(&mut self, s: &mut Session, sp: &Spline, enter: bool, kw: &str) -> Result<()> { + match (enter, kw) { + (true, _) | (_, "exit" | "x") => self.sub = Sub::Main, + (_, "add" | "a") => self.sub = Sub::FitAddPick, + (_, "close" | "c") => self.apply(s, json!({ "option": "close" }))?, + (_, "open" | "o") => self.apply(s, json!({ "option": "open" }))?, + (_, "delete" | "d") => self.sub = Sub::FitDelete, + (_, "kink" | "k") => self.sub = Sub::FitKink, + (_, "move" | "m") => { + self.mark = 0; + self.sub = Sub::FitMove; + } + (_, "purge" | "p") => { + self.apply(s, json!({ "option": "purge" }))?; + self.sub = Sub::Main; + } + (_, "tangents" | "t") if sp.closed => s.echo("A closed spline has no end tangents."), + (_, "tangents" | "t") => self.sub = Sub::FitStartTangent, + (_, "tolerance" | "l") => self.sub = Sub::FitTolerance, + (_, "") => {} + _ => return Err(other("Invalid option keyword.")), + } + Ok(()) + } + + fn input_join(&mut self, s: &mut Session, h: Handle, sp: &Spline, input: &Input) -> Result<()> { + match self.join.feed(s, input)? { + SelOutcome::More => {} + SelOutcome::Empty => self.sub = Sub::Main, + SelOutcome::Done(handles) => { + self.sub = Sub::Main; + let before = curves::entity(s, h)?.kind; + let removed = join(s, h, sp, &handles)?; + self.history.push(before); + s.echo(format!("{} object(s) joined into the spline", removed.len())); + self.joined.push((self.history.len(), removed)); + s.set_selection(vec![h]); + } + } + Ok(()) + } +} + impl Interactive for SplineditM { fn name(&self) -> &'static str { "SPLINEDIT" @@ -578,60 +666,12 @@ impl Interactive for SplineditM { let enter = i == Input::Enter; let sp = self.spline(s).ok_or_else(|| other("The spline was erased."))?; match self.sub { - Sub::Main => match (enter, kw) { - (true, _) | (_, "exit" | "x") => { - s.set_selection(Vec::new()); - return Ok(Step::Done); + Sub::Main => { + if let Some(step) = self.input_main(s, h, &sp, enter, kw)? { + return Ok(step); } - (_, "close" | "c") => self.apply(s, json!({ "option": "close" }))?, - (_, "open" | "o") => self.apply(s, json!({ "option": "open" }))?, - (_, "reverse" | "r") => self.apply(s, json!({ "option": "reverse" }))?, - (_, "convert to polyline" | "p") => self.sub = Sub::Precision, - (_, "fit data" | "f") if sp.fit.is_empty() => s.echo("The spline has no fit data."), - (_, "fit data" | "f") => self.sub = Sub::Fit, - (_, "edit vertex" | "e") => { - self.mark = 0; - self.sub = Sub::Vertex; - } - (_, "join" | "j") if sp.closed => s.echo("Cannot join to a closed spline."), - (_, "join" | "j") => { - self.join = SelectPhase::default(); - self.sub = Sub::Join; - } - (_, "undo" | "u") => { - // Undoing a join brings the joined objects back. - if let Some((at, ents)) = self.joined.last() - && *at == self.history.len() - { - let d = s.doc_mut()?; - for (space, e) in ents { - if let Some(st) = d.space_mut(space) { - st.push(e.clone()); - } - } - self.joined.pop(); - } - match self.history.pop() { - Some(k) => set_kind(s, h, k)?, - None => s.echo("All edits have been undone."), - } - } - (_, "") => {} - _ => return Err(other("Invalid option keyword.")), - }, - Sub::Join => match self.join.feed(s, &i)? { - SelOutcome::More => {} - SelOutcome::Empty => self.sub = Sub::Main, - SelOutcome::Done(hs) => { - self.sub = Sub::Main; - let before = curves::entity(s, h)?.kind; - let removed = join(s, h, &sp, &hs)?; - self.history.push(before); - s.echo(format!("{} object(s) joined into the spline", removed.len())); - self.joined.push((self.history.len(), removed)); - s.set_selection(vec![h]); - } - }, + } + Sub::Join => self.input_join(s, h, &sp, &i)?, Sub::Precision => { let precision = match &i { Input::Text(t) => number(t).filter(|n| (0.0..=99.0).contains(n)).ok_or_else(|| other("Requires an integer between 0 and 99."))?, @@ -641,27 +681,7 @@ impl Interactive for SplineditM { s.set_selection(Vec::new()); return Ok(Step::Done); } - Sub::Fit => match (enter, kw) { - (true, _) | (_, "exit" | "x") => self.sub = Sub::Main, - (_, "add" | "a") => self.sub = Sub::FitAddPick, - (_, "close" | "c") => self.apply(s, json!({ "option": "close" }))?, - (_, "open" | "o") => self.apply(s, json!({ "option": "open" }))?, - (_, "delete" | "d") => self.sub = Sub::FitDelete, - (_, "kink" | "k") => self.sub = Sub::FitKink, - (_, "move" | "m") => { - self.mark = 0; - self.sub = Sub::FitMove; - } - (_, "purge" | "p") => { - self.apply(s, json!({ "option": "purge" }))?; - self.sub = Sub::Main; - } - (_, "tangents" | "t") if sp.closed => s.echo("A closed spline has no end tangents."), - (_, "tangents" | "t") => self.sub = Sub::FitStartTangent, - (_, "tolerance" | "l") => self.sub = Sub::FitTolerance, - (_, "") => {} - _ => return Err(other("Invalid option keyword.")), - }, + Sub::Fit => self.input_fit(s, &sp, enter, kw)?, Sub::FitAddPick => match point { Some(p) => { self.mark = nearest(&sp.fit, p).ok_or_else(|| other("The spline has no fit data."))?; @@ -806,6 +826,7 @@ impl Interactive for SplineditM { } Ok(Step::Continue) } + fn preview(&self, s: &Session, c: Vec2) -> Vec { let marked = matches!(self.sub, Sub::FitMove | Sub::VertexMove | Sub::VertexWeight | Sub::FitAddNew); let Some(v) = marked.then(|| self.marks(s).get(self.mark).copied()).flatten() else { return Vec::new() }; diff --git a/crates/engine/src/cmd/modify2/tests.rs b/crates/engine/src/cmd/modify2/tests.rs index 7c8f92625..98b2f40d9 100644 --- a/crates/engine/src/cmd/modify2/tests.rs +++ b/crates/engine/src/cmd/modify2/tests.rs @@ -148,6 +148,11 @@ fn pedit_command_line() { #[test] fn splinedit_json_and_command_line() { + splinedit_json_edits(); + splinedit_command_line_edits(); +} + +fn splinedit_json_edits() { let mut s = Session::new(); let sp = h(&s.execute("spline", &json!({"fit": [[0, 0], [2, 2], [4, 0], [6, 2]]})).unwrap()); s.execute("splinedit", &json!({"handle": sp, "option": "close"})).unwrap(); @@ -167,7 +172,9 @@ fn splinedit_json_and_command_line() { s.execute("splinedit", &json!({"handle": sp, "option": "polyline", "precision": 4})).unwrap(); assert!(matches!(kind(&s, &sp), EntityKind::LwPolyline(p) if p.vertices.len() > 4)); assert!(s.execute("splinedit", &json!({"handle": sp, "option": "close"})).is_err()); +} +fn splinedit_command_line_edits() { let mut s = Session::new(); s.execute("spline", &json!({"control": [[0, 0], [2, 3], [4, 0], [6, 3]]})).unwrap(); s.cmdline("splinedit 0,0 c").unwrap(); diff --git a/crates/engine/src/cmd/modify2/textedit.rs b/crates/engine/src/cmd/modify2/textedit.rs index d160d992f..10ca46245 100644 --- a/crates/engine/src/cmd/modify2/textedit.rs +++ b/crates/engine/src/cmd/modify2/textedit.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + Accept, EntityKind, Handle, Input, Interactive, Prompt, Result, Session, Step, Value, bad, curves, json, other, set_kind, str_param, targets, +}; // ===================================================================================== // TEXTEDIT diff --git a/crates/engine/src/cmd/modify2/trimext.rs b/crates/engine/src/cmd/modify2/trimext.rs index f5258dc43..8a18e4a85 100644 --- a/crates/engine/src/cmd/modify2/trimext.rs +++ b/crates/engine/src/cmd/modify2/trimext.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Ellipse, EntityKind, Line, Result, Segment, TAU, Vec2, ccw_sweep, curve_edits, curves, intersect_ext, norm_angle, other, v3}; // ===================================================================================== // TRIM / EXTEND for ellipses and splines diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs index 9dbeeddbf..b9a24580b 100644 --- a/crates/engine/src/cmd/options.rs +++ b/crates/engine/src/cmd/options.rs @@ -8,7 +8,7 @@ use serde_json::{Map, Value, json}; -use super::*; +use super::{CommandSpec, always, bad}; use crate::{Result, Session, Settings}; /// The profile variables, in the order [`profile`] lists them. diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index f9fbf2969..817ff77a2 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -216,212 +216,249 @@ fn run_set(s: &mut Session, p: &Value) -> Result { .transpose()?; let dim_over = dim_overrides(p)?; let anno_scale = d.current_anno_scale(); + let common = CommonEntityProperties { anno_scale: &anno_scale, color, linetype: linetype.as_deref(), lineweight: lw, transparency, thickness }; for h in &hs { d.modify_entity(*h, |e| { - // Annotative: Yes supports the current annotation scale; No drops every scale. - if let Some(a) = p.get("annotative").and_then(Value::as_bool) { - cadcraft_doc::set_annotative(e, a, &anno_scale); - } - if let Some(l) = str_param(p, "layer") { - e.common.layer = l.to_string(); - } - if let Some(c) = color { - e.common.color = c; - } - if let Some(lt) = &linetype { - e.common.linetype.clone_from(lt); - } - if let Some(w) = lw { - e.common.lineweight = w; - } - if let Some(x) = p.get("ltscale").and_then(Value::as_f64).filter(|x| *x > 0.0) { - e.common.ltscale = x; - } - if let Some(t) = transparency { - e.common.transparency = t; - } - if let Some(th) = thickness { - e.common.thickness = th; - } - if let Some(v) = p.get("visible").and_then(Value::as_bool) { - e.common.visible = v; - } - let num = |k: &str| p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite()); - match &mut e.kind { - EntityKind::Line(l) => { - if let Some(v) = p.get("start") { - set_xy(&mut l.a, v); - } - if let Some(v) = p.get("end") { - set_xy(&mut l.b, v); - } - // Length / angle keep the start point and move the end point. - let (a, b) = (l.a.xy(), l.b.xy()); - let len = num("length").filter(|x| *x > 0.0 && *x < 1e15).unwrap_or_else(|| a.dist(b)); - let ang = num("angle").map(f64::to_radians).unwrap_or_else(|| a.angle_to(b)); - if num("length").is_some() || num("angle").is_some() { - let e = cadcraft_geom::Vec2::polar(a, len, ang); - if e.is_finite() { - l.b.x = e.x; - l.b.y = e.y; - } - } - } - EntityKind::Circle(c) => { - if let Some(v) = p.get("center") { - set_xy(&mut c.center, v); - } - if let Some(r) = num("radius").filter(|r| *r > 0.0) { - c.radius = r; - } - if let Some(dm) = num("diameter").filter(|r| *r > 0.0) { - c.radius = dm / 2.0; - } - if let Some(cf) = num("circumference").filter(|x| *x > 0.0) { - c.radius = cf / cadcraft_geom::TAU; - } - if let Some(a) = num("area").filter(|x| *x > 0.0) { - c.radius = (a / cadcraft_geom::PI).sqrt(); - } - } - EntityKind::Arc(a) => { - if let Some(v) = p.get("center") { - set_xy(&mut a.center, v); - } - if let Some(r) = num("radius").filter(|r| *r > 0.0) { - a.radius = r; - } - if let Some(x) = num("startAngle") { - a.start = cadcraft_geom::norm_angle(x.to_radians()); - } - if let Some(x) = num("endAngle") { - a.end = cadcraft_geom::norm_angle(x.to_radians()); - } - } - EntityKind::Text(t) => { - if let Some(v) = str_param(p, "text") { - t.value = v.to_string(); - } - if let Some(x) = num("height").filter(|x| *x > 0.0) { - t.height = x; - } - if let Some(x) = num("rotation") { - t.rotation = x.to_radians(); - } - if let Some(x) = num("widthFactor").filter(|x| *x > 0.0) { - t.width_factor = x; - } - // The position is the point the text is placed by: moving it moves the text. - if let Some(to) = p.get("position").and_then(point_value) { - let d = to - t.justify_point(); - for q in std::iter::once(&mut t.insert).chain(t.align_pt.as_mut()) { - q.x += d.x; - q.y += d.y; - } - } - } - EntityKind::MText(t) => { - if let Some(v) = str_param(p, "text") { - t.contents = v.replace('\n', "\\P"); - } - if let Some(x) = num("height").filter(|x| *x > 0.0) { - t.height = x; - } - if let Some(x) = num("width").filter(|x| *x >= 0.0) { - t.width = x; - } - if let Some(x) = num("rotation") { - t.rotation = x.to_radians(); - } - } - EntityKind::LwPolyline(pl) => { - if let Some(x) = num("width").filter(|x| *x >= 0.0) { - pl.const_width = x; - } - if let Some(c) = p.get("closed").and_then(Value::as_bool) { - pl.closed = c; - } - } - EntityKind::Insert(i) => { - if let Some(x) = num("rotation") { - let turn = x.to_radians() - i.rotation; - i.rotation = x.to_radians(); - // The attached text turns with the reference, around its insertion point (as ROTATE does). - let base = i.insert.xy(); - for a in &mut i.attribs { - let t = &mut a.text; - for q in std::iter::once(&mut t.insert).chain(t.align_pt.as_mut()) { - let r = q.xy().rotate_about(base, turn); - q.x = r.x; - q.y = r.y; - } - t.rotation = cadcraft_geom::norm_angle(t.rotation + turn); - if let Some(ml) = &mut a.multiline { - let r = ml.mtext.insert.xy().rotate_about(base, turn); - ml.mtext.insert.x = r.x; - ml.mtext.insert.y = r.y; - ml.mtext.rotation = cadcraft_geom::norm_angle(ml.mtext.rotation + turn); - } - } - } - if let Some(x) = num("scale").filter(|x| *x != 0.0) { - // A new positive scale carries the attached attribute text with it, like SCALE about the insertion point. - if x > 0.0 && i.scale.x > 0.0 { - let m = cadcraft_geom::Mat3::scale_about(i.insert.xy(), x / i.scale.x); - for a in &mut i.attribs { - let mut k = EntityKind::Text(a.text.clone()); - k.transform(&m); - if let EntityKind::Text(t) = k { - a.text = t; - } - a.transform_multiline(&m); - } - } - i.scale = Vec3::new(x, x, x); - } - } - EntityKind::Dimension(dm) => { - if let Some(v) = str_param(p, "textOverride") { - dm.text = v.to_string(); - dm.block = None; - } - if let Some(st) = &dim_style { - dm.style.clone_from(st); - dm.block = None; - } - if !dim_over.is_empty() { - dm.overrides.extend(dim_over.clone()); - dm.block = None; - } - } - EntityKind::Hatch(h) => { - if let Some(v) = str_param(p, "pattern") { - if !h.pattern.eq_ignore_ascii_case(v) { - h.pattern_lines = super::hatch::lines_for(v); - } - h.pattern = v.to_ascii_uppercase(); - // The old pattern's definition lines no longer apply. - h.pattern_lines.clear(); - h.solid = h.pattern == "SOLID"; - if !h.solid { - // A named pattern replaces the gradient fill; the renderer draws the gradient first when it is set. - h.gradient = None; - } - } - if let Some(x) = num("scale").filter(|x| *x > 0.0) { - h.scale = x; - } - if let Some(x) = num("angle") { - h.angle = x.to_radians(); - } - } - _ => {} - } + set_common_properties(e, p, &common); + apply_kind_properties(e, p, dim_style.as_ref(), &dim_over); })?; } Ok(json!({ "changed": hs.len() })) } +fn apply_kind_properties( + entity: &mut cadcraft_doc::Entity, + params: &Value, + dim_style: Option<&String>, + dim_overrides: &serde_json::Map, +) { + let num = |key: &str| params.get(key).and_then(Value::as_f64).filter(|value| value.is_finite()); + match &mut entity.kind { + EntityKind::Line(l) => { + if let Some(v) = params.get("start") { + set_xy(&mut l.a, v); + } + if let Some(v) = params.get("end") { + set_xy(&mut l.b, v); + } + // Length / angle keep the start point and move the end point. + let (a, b) = (l.a.xy(), l.b.xy()); + let len = num("length").filter(|x| *x > 0.0 && *x < 1e15).unwrap_or_else(|| a.dist(b)); + let ang = num("angle").map(f64::to_radians).unwrap_or_else(|| a.angle_to(b)); + if num("length").is_some() || num("angle").is_some() { + let e = cadcraft_geom::Vec2::polar(a, len, ang); + if e.is_finite() { + l.b.x = e.x; + l.b.y = e.y; + } + } + } + EntityKind::Circle(c) => { + if let Some(v) = params.get("center") { + set_xy(&mut c.center, v); + } + if let Some(r) = num("radius").filter(|r| *r > 0.0) { + c.radius = r; + } + if let Some(dm) = num("diameter").filter(|r| *r > 0.0) { + c.radius = dm / 2.0; + } + if let Some(cf) = num("circumference").filter(|x| *x > 0.0) { + c.radius = cf / cadcraft_geom::TAU; + } + if let Some(a) = num("area").filter(|x| *x > 0.0) { + c.radius = (a / cadcraft_geom::PI).sqrt(); + } + } + EntityKind::Arc(a) => { + if let Some(v) = params.get("center") { + set_xy(&mut a.center, v); + } + if let Some(r) = num("radius").filter(|r| *r > 0.0) { + a.radius = r; + } + if let Some(x) = num("startAngle") { + a.start = cadcraft_geom::norm_angle(x.to_radians()); + } + if let Some(x) = num("endAngle") { + a.end = cadcraft_geom::norm_angle(x.to_radians()); + } + } + EntityKind::Text(t) => { + if let Some(v) = str_param(params, "text") { + t.value = v.to_string(); + } + if let Some(x) = num("height").filter(|x| *x > 0.0) { + t.height = x; + } + if let Some(x) = num("rotation") { + t.rotation = x.to_radians(); + } + if let Some(x) = num("widthFactor").filter(|x| *x > 0.0) { + t.width_factor = x; + } + // The position is the point the text is placed by: moving it moves the text. + if let Some(to) = params.get("position").and_then(point_value) { + let d = to - t.justify_point(); + for q in std::iter::once(&mut t.insert).chain(t.align_pt.as_mut()) { + q.x += d.x; + q.y += d.y; + } + } + } + EntityKind::MText(t) => { + if let Some(v) = str_param(params, "text") { + t.contents = v.replace('\n', "\\P"); + } + if let Some(x) = num("height").filter(|x| *x > 0.0) { + t.height = x; + } + if let Some(x) = num("width").filter(|x| *x >= 0.0) { + t.width = x; + } + if let Some(x) = num("rotation") { + t.rotation = x.to_radians(); + } + } + EntityKind::LwPolyline(pl) => { + if let Some(x) = num("width").filter(|x| *x >= 0.0) { + pl.const_width = x; + } + if let Some(c) = params.get("closed").and_then(Value::as_bool) { + pl.closed = c; + } + } + EntityKind::Insert(insert) => set_insert_properties(insert, params), + EntityKind::Dimension(dimension) => set_dimension_properties(dimension, params, dim_style, dim_overrides), + EntityKind::Hatch(hatch) => set_hatch_properties(hatch, params), + _ => {} + } +} + +fn set_insert_properties(insert: &mut cadcraft_doc::Insert, params: &Value) { + let number = |key: &str| params.get(key).and_then(Value::as_f64).filter(|value| value.is_finite()); + + if let Some(x) = number("rotation") { + let turn = x.to_radians() - insert.rotation; + insert.rotation = x.to_radians(); + // The attached text turns with the reference, around its insertion point (as ROTATE does). + let base = insert.insert.xy(); + for a in &mut insert.attribs { + let t = &mut a.text; + for q in std::iter::once(&mut t.insert).chain(t.align_pt.as_mut()) { + let r = q.xy().rotate_about(base, turn); + q.x = r.x; + q.y = r.y; + } + t.rotation = cadcraft_geom::norm_angle(t.rotation + turn); + if let Some(ml) = &mut a.multiline { + let r = ml.mtext.insert.xy().rotate_about(base, turn); + ml.mtext.insert.x = r.x; + ml.mtext.insert.y = r.y; + ml.mtext.rotation = cadcraft_geom::norm_angle(ml.mtext.rotation + turn); + } + } + } + if let Some(x) = number("scale").filter(|x| *x != 0.0) { + // A new positive scale carries the attached attribute text with it, like SCALE about the insertion point. + if x > 0.0 && insert.scale.x > 0.0 { + let m = cadcraft_geom::Mat3::scale_about(insert.insert.xy(), x / insert.scale.x); + for a in &mut insert.attribs { + let mut k = EntityKind::Text(a.text.clone()); + k.transform(&m); + if let EntityKind::Text(t) = k { + a.text = t; + } + a.transform_multiline(&m); + } + } + insert.scale = Vec3::new(x, x, x); + } +} + +fn set_dimension_properties( + dimension: &mut cadcraft_doc::Dimension, + params: &Value, + style: Option<&String>, + overrides: &serde_json::Map, +) { + if let Some(v) = str_param(params, "textOverride") { + dimension.text = v.to_string(); + dimension.block = None; + } + if let Some(st) = style { + dimension.style.clone_from(st); + dimension.block = None; + } + if !overrides.is_empty() { + dimension.overrides.extend(overrides.clone()); + dimension.block = None; + } +} + +fn set_hatch_properties(hatch: &mut cadcraft_doc::Hatch, params: &Value) { + let number = |key: &str| params.get(key).and_then(Value::as_f64).filter(|value| value.is_finite()); + + if let Some(v) = str_param(params, "pattern") { + if !hatch.pattern.eq_ignore_ascii_case(v) { + hatch.pattern_lines = super::hatch::lines_for(v); + } + hatch.pattern = v.to_ascii_uppercase(); + // The old pattern's definition lines no longer apply. + hatch.pattern_lines.clear(); + hatch.solid = hatch.pattern == "SOLID"; + if !hatch.solid { + // A named pattern replaces the gradient fill; the renderer draws the gradient first when it is set. + hatch.gradient = None; + } + } + if let Some(x) = number("scale").filter(|x| *x > 0.0) { + hatch.scale = x; + } + if let Some(x) = number("angle") { + hatch.angle = x.to_radians(); + } +} + +struct CommonEntityProperties<'a> { + anno_scale: &'a cadcraft_doc::AnnoScale, + color: Option, + linetype: Option<&'a str>, + lineweight: Option, + transparency: Option, + thickness: Option, +} + +fn set_common_properties(entity: &mut cadcraft_doc::Entity, params: &Value, common: &CommonEntityProperties<'_>) { + if let Some(annotative) = params.get("annotative").and_then(Value::as_bool) { + cadcraft_doc::set_annotative(entity, annotative, common.anno_scale); + } + if let Some(layer) = str_param(params, "layer") { + entity.common.layer = layer.to_string(); + } + if let Some(color) = common.color { + entity.common.color = color; + } + if let Some(linetype) = common.linetype { + entity.common.linetype = linetype.to_string(); + } + if let Some(lineweight) = common.lineweight { + entity.common.lineweight = lineweight; + } + if let Some(scale) = params.get("ltscale").and_then(Value::as_f64).filter(|scale| *scale > 0.0) { + entity.common.ltscale = scale; + } + if let Some(transparency) = common.transparency { + entity.common.transparency = transparency; + } + if let Some(thickness) = common.thickness { + entity.common.thickness = thickness; + } + if let Some(visible) = params.get("visible").and_then(Value::as_bool) { + entity.common.visible = visible; + } +} + fn run_color(s: &mut Session, p: &Value) -> Result { let c = p.get("color").and_then(parse_color).ok_or_else(|| bad("color", "`color` is required"))?; set_current_color(s.doc_mut()?, c); diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs index 8934df872..c70989a58 100644 --- a/crates/engine/src/doc_state.rs +++ b/crates/engine/src/doc_state.rs @@ -1,5 +1,5 @@ use super::view::{ModelTiles, View, usable_view}; -use super::*; +use super::{Arc, Drawing, Handle, Space, blockedit, undoctl}; #[derive(Clone, Debug)] pub struct Snapshot { diff --git a/crates/engine/src/grips.rs b/crates/engine/src/grips.rs index a7dee5b0b..a3b8921aa 100644 --- a/crates/engine/src/grips.rs +++ b/crates/engine/src/grips.rs @@ -119,74 +119,19 @@ pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option { - let g = Arc::new(a.center.xy(), a.radius, a.start, a.end); - let (mut s, mut m, mut e) = (g.start_point(), g.mid_point(), g.end_point()); - match index { - 0 => s = to, - 1 => m = to, - 2 => e = to, - _ => return Some(translated(kind, delta)), - } - let na = Arc::from_3_points(s, m, e)?; - return Some(crate::cmd::helpers::arc(&na)); + return stretch_arc(kind, a, index, to, delta); } EntityKind::Ellipse(e) => { - let c = e.center.xy(); - let mj = e.major.xy(); - let a = mj.len(); - if a <= EPS { + if e.major.xy().len() <= EPS { return None; } - let b = a * e.ratio; - match index { - 0 => return Some(translated(kind, delta)), - 1 | 3 => { - let v = if index == 1 { to - c } else { c - to }; - let l = v.len(); - if l <= EPS { - return None; - } - if l >= b { - e.major = v.to3(0.0); - e.ratio = (b / l).clamp(1e-9, 1.0); - } else { - e.major = (v.normalized().perp() * b).to3(0.0); - e.ratio = (l / b).clamp(1e-9, 1.0); - shift_params(e, -std::f64::consts::FRAC_PI_2); - } - } - _ => { - let u = mj / a; - let l = (to - c).dot(u.perp()).abs(); - if l <= EPS { - return None; - } - if l <= a { - e.ratio = (l / a).clamp(1e-9, 1.0); - } else { - e.major = (u.perp() * l).to3(0.0); - e.ratio = (a / l).clamp(1e-9, 1.0); - shift_params(e, -std::f64::consts::FRAC_PI_2); - } - } + if index == 0 { + return Some(translated(kind, delta)); } + stretch_ellipse(e, index, to)?; } EntityKind::LwPolyline(p) => { - let n = p.vertices.len(); - match poly_grip(p, index)? { - PolyGrip::Vertex(i) => p.vertices.get_mut(i)?.p = to, - PolyGrip::Mid(i) => { - let j = (i + 1) % n.max(1); - let (a, b) = (p.vertices.get(i)?.p, p.vertices.get(j)?.p); - if p.vertices.get(i)?.bulge.abs() > 1e-12 { - // Arc segment: reshape through the dragged midpoint. - p.vertices.get_mut(i)?.bulge = crate::cmd::curves::bulge_through(a, to, b); - } else { - p.vertices.get_mut(i)?.p = a + delta; - p.vertices.get_mut(j)?.p = b + delta; - } - } - } + stretch_polyline(p, index, to, delta)?; } EntityKind::Polyline3d(p) => set_xy(p.points.get_mut(index)?, to), EntityKind::Spline(sp) => { @@ -300,6 +245,86 @@ pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option Option { + let geometry = Arc::new(arc.center.xy(), arc.radius, arc.start, arc.end); + let (mut start, mut mid, mut end) = (geometry.start_point(), geometry.mid_point(), geometry.end_point()); + match index { + 0 => start = to, + 1 => mid = to, + 2 => end = to, + _ => return Some(translated(kind, delta)), + } + let adjusted = Arc::from_3_points(start, mid, end)?; + Some(crate::cmd::helpers::arc(&adjusted)) +} + +fn stretch_ellipse(ellipse: &mut cadcraft_doc::Ellipse, index: usize, to: Vec2) -> Option<()> { + let center = ellipse.center.xy(); + let major = ellipse.major.xy(); + let major_len = major.len(); + if major_len <= EPS { + return None; + } + let minor_len = major_len * ellipse.ratio; + match index { + 1 | 3 => stretch_ellipse_major(ellipse, index, to, center, minor_len)?, + _ => stretch_ellipse_minor(ellipse, to, center, major, major_len)?, + } + Some(()) +} + +fn stretch_ellipse_major(ellipse: &mut cadcraft_doc::Ellipse, index: usize, to: Vec2, center: Vec2, minor_len: f64) -> Option<()> { + let axis = if index == 1 { to - center } else { center - to }; + let len = axis.len(); + if len <= EPS { + return None; + } + if len >= minor_len { + ellipse.major = axis.to3(0.0); + ellipse.ratio = (minor_len / len).clamp(1e-9, 1.0); + } else { + ellipse.major = (axis.normalized().perp() * minor_len).to3(0.0); + ellipse.ratio = (len / minor_len).clamp(1e-9, 1.0); + shift_params(ellipse, -std::f64::consts::FRAC_PI_2); + } + Some(()) +} + +fn stretch_ellipse_minor(ellipse: &mut cadcraft_doc::Ellipse, to: Vec2, center: Vec2, major: Vec2, major_len: f64) -> Option<()> { + let unit = major / major_len; + let minor_len = (to - center).dot(unit.perp()).abs(); + if minor_len <= EPS { + return None; + } + if minor_len <= major_len { + ellipse.ratio = (minor_len / major_len).clamp(1e-9, 1.0); + } else { + ellipse.major = (unit.perp() * minor_len).to3(0.0); + ellipse.ratio = (major_len / minor_len).clamp(1e-9, 1.0); + shift_params(ellipse, -std::f64::consts::FRAC_PI_2); + } + Some(()) +} + +fn stretch_polyline(polyline: &mut LwPolyline, index: usize, to: Vec2, delta: Vec2) -> Option<()> { + let count = polyline.vertices.len(); + match poly_grip(polyline, index)? { + PolyGrip::Vertex(i) => polyline.vertices.get_mut(i)?.p = to, + PolyGrip::Mid(i) => { + let next = (i + 1) % count.max(1); + let (start, end) = (polyline.vertices.get(i)?.p, polyline.vertices.get(next)?.p); + if polyline.vertices.get(i)?.bulge.abs() > 1e-12 { + // Arc segment: reshape through the dragged midpoint. + polyline.vertices.get_mut(i)?.bulge = crate::cmd::curves::bulge_through(start, to, end); + } else { + polyline.vertices.get_mut(i)?.p = start + delta; + polyline.vertices.get_mut(next)?.p = end + delta; + } + } + } + Some(()) +} + fn shift_params(e: &mut cadcraft_doc::Ellipse, by: f64) { let full = (cadcraft_geom::ccw_sweep(e.start, e.end) - cadcraft_geom::TAU).abs() < 1e-9; if !full { diff --git a/crates/engine/src/grips/tests.rs b/crates/engine/src/grips/tests.rs index 39c43cd01..971937704 100644 --- a/crates/engine/src/grips/tests.rs +++ b/crates/engine/src/grips/tests.rs @@ -75,6 +75,18 @@ fn arc_and_ellipse_grips() { assert!(matches!(kind(&s, e), EntityKind::Ellipse(x) if (x.major.xy().len() - 8.0).abs() < 1e-9 && (x.ratio - 0.75).abs() < 1e-9)); } +#[test] +fn degenerate_ellipse_cannot_move_from_its_center_grip() { + let ellipse = EntityKind::Ellipse(cadcraft_doc::Ellipse { + center: cadcraft_geom::Vec3::ZERO, + major: cadcraft_geom::Vec3::ZERO, + ratio: 0.5, + start: 0.0, + end: cadcraft_geom::TAU, + }); + assert!(super::stretch_grip(&ellipse, 0, v(1.0, 1.0)).is_none()); +} + #[test] fn polyline_grips() { let mut s = Session::new(); diff --git a/crates/engine/src/last_used.rs b/crates/engine/src/last_used.rs index e984d9feb..73c610329 100644 --- a/crates/engine/src/last_used.rs +++ b/crates/engine/src/last_used.rs @@ -1,4 +1,4 @@ -use super::*; +use super::cmd; /// Options and values the interactive commands remember for the rest of the session, offered as /// the `` the next time they ask (as AutoCAD does). Values AutoCAD keeps in a drawing diff --git a/crates/engine/src/lisp/builtins/args.rs b/crates/engine/src/lisp/builtins/args.rs index 9ac9c3427..1bb986cb3 100644 --- a/crates/engine/src/lisp/builtins/args.rs +++ b/crates/engine/src/lisp/builtins/args.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, LispError, Value}; pub(super) type R = Result; diff --git a/crates/engine/src/lisp/builtins/files.rs b/crates/engine/src/lisp/builtins/files.rs index 6a1f90957..f97045a2b 100644 --- a/crates/engine/src/lisp/builtins/files.rs +++ b/crates/engine/src/lisp/builtins/files.rs @@ -1,6 +1,6 @@ use super::args::{R, arity, string}; use super::output::emit; -use super::*; +use super::{Ctx, Flow, LispError, Value, charge_bytes}; // ---------------- files ---------------- diff --git a/crates/engine/src/lisp/builtins/functions.rs b/crates/engine/src/lisp/builtins/functions.rs index 2f53d1651..2f94218f3 100644 --- a/crates/engine/src/lisp/builtins/functions.rs +++ b/crates/engine/src/lisp/builtins/functions.rs @@ -1,5 +1,5 @@ use super::args::{R, arity, list_arg, string}; -use super::*; +use super::{Arc, Ctx, Flow, LispError, Sym, Value, read, to_text}; // ---------------- functions of functions ---------------- diff --git a/crates/engine/src/lisp/builtins/geometry.rs b/crates/engine/src/lisp/builtins/geometry.rs index 7de673666..059d64d3c 100644 --- a/crates/engine/src/lisp/builtins/geometry.rs +++ b/crates/engine/src/lisp/builtins/geometry.rs @@ -1,5 +1,5 @@ +use super::Value; use super::args::{R, arity, num, point}; -use super::*; // ---------------- geometry ---------------- diff --git a/crates/engine/src/lisp/builtins/lists.rs b/crates/engine/src/lisp/builtins/lists.rs index ea5a4fb2c..6975ed5e0 100644 --- a/crates/engine/src/lisp/builtins/lists.rs +++ b/crates/engine/src/lisp/builtins/lists.rs @@ -1,5 +1,5 @@ use super::args::{R, arity, int, list_arg, num, string}; -use super::*; +use super::{LispError, Value, eq, equal}; // ---------------- predicates and lists ---------------- diff --git a/crates/engine/src/lisp/builtins/numbers.rs b/crates/engine/src/lisp/builtins/numbers.rs index e1baa86f8..2dfb6911c 100644 --- a/crates/engine/src/lisp/builtins/numbers.rs +++ b/crates/engine/src/lisp/builtins/numbers.rs @@ -1,5 +1,5 @@ use super::args::{R, arity, int, num, real_or_int}; -use super::*; +use super::{LispError, Ordering, Value, eq}; // ---------------- numbers ---------------- diff --git a/crates/engine/src/lisp/builtins/output.rs b/crates/engine/src/lisp/builtins/output.rs index 21db3f87d..d64fd84e3 100644 --- a/crates/engine/src/lisp/builtins/output.rs +++ b/crates/engine/src/lisp/builtins/output.rs @@ -1,5 +1,5 @@ use super::args::{arity, string}; -use super::*; +use super::{Arc, Ctx, Flow, LispError, Sym, Value, charge_bytes, real_text, to_text}; // ---------------- output ---------------- diff --git a/crates/engine/src/lisp/builtins/strings.rs b/crates/engine/src/lisp/builtins/strings.rs index 08456d3ca..46d985bf1 100644 --- a/crates/engine/src/lisp/builtins/strings.rs +++ b/crates/engine/src/lisp/builtins/strings.rs @@ -1,5 +1,5 @@ use super::args::{R, arity, int, list_arg, string}; -use super::*; +use super::{LispError, OsStr, Value, check_string_len, read, to_text}; // ---------------- strings ---------------- diff --git a/crates/engine/src/lisp/builtins/wcmatch.rs b/crates/engine/src/lisp/builtins/wcmatch.rs index 1f171b253..441086b6e 100644 --- a/crates/engine/src/lisp/builtins/wcmatch.rs +++ b/crates/engine/src/lisp/builtins/wcmatch.rs @@ -1,5 +1,5 @@ use super::args::{R, arity, string}; -use super::*; +use super::{LispError, Value}; /// `wcmatch`: `#` a digit, `@` a letter, `.` neither, `*` any run, `?` any character, `~` (first) /// not, `[…]` a set (`[~…]` not in it, ranges `a-z`), `` ` `` the next character as is, `,` diff --git a/crates/engine/src/lisp/tests.rs b/crates/engine/src/lisp/tests.rs index a48c04eba..d65795631 100644 --- a/crates/engine/src/lisp/tests.rs +++ b/crates/engine/src/lisp/tests.rs @@ -106,46 +106,54 @@ fn equality_compares_symbols_as_autolisp_does() { #[test] fn lists_and_strings() { let mut s = session(); - assert_eq!(ok(&mut s, "(cons 1 '(2 3))"), "(1 2 3)"); - assert_eq!(ok(&mut s, "(cons 8 \"0\")"), "(8 . \"0\")"); - assert_eq!(ok(&mut s, "(cadr '(1 2 3))"), "2"); - assert_eq!(ok(&mut s, "(cdr (assoc 40 '((10 1.0 2.0 0.0) (40 . 2.5))))"), "2.5"); - assert_eq!(ok(&mut s, "(subst '(40 . 5.0) '(40 . 2.5) '((8 . \"0\") (40 . 2.5)))"), "((8 . \"0\") (40 . 5.0))"); - assert_eq!(ok(&mut s, "(append '(1) '(2 3) nil '(4))"), "(1 2 3 4)"); - assert_eq!(ok(&mut s, "(reverse '(1 2 3))"), "(3 2 1)"); - assert_eq!(ok(&mut s, "(nth 1 '(a b c))"), "B"); - assert_eq!(ok(&mut s, "(member 2 '(1 2 3))"), "(2 3)"); - assert_eq!(ok(&mut s, "(length '(1 2 3))"), "3"); - assert_eq!(ok(&mut s, "(last '(1 2 3))"), "3"); - assert_eq!(ok(&mut s, "(mapcar '+ '(1 2 3) '(10 20 30))"), "(11 22 33)"); - assert_eq!(ok(&mut s, "(mapcar '(lambda (x) (* x x)) '(1 2 3))"), "(1 4 9)"); - assert_eq!(ok(&mut s, "(apply 'max '(3 9 2))"), "9"); - assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) '<)"), "(1 2 3)"); - assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) (function (lambda (a b) (> a b))))"), "(3 2 1)"); - assert_eq!(ok(&mut s, "(vl-remove-if 'minusp '(1 -2 3))"), "(1 3)"); - assert_eq!(ok(&mut s, "(strcat \"a\" \"b\" \"c\")"), "\"abc\""); - assert_eq!(ok(&mut s, "(substr \"Hello\" 2 3)"), "\"ell\""); - assert_eq!(ok(&mut s, "(strcase \"abc\")"), "\"ABC\""); - assert_eq!(ok(&mut s, "(strlen \"abc\" \"de\")"), "5"); - assert_eq!(ok(&mut s, "(itoa 42)"), "\"42\""); - assert_eq!(ok(&mut s, "(atoi \"12abc\")"), "12"); - assert_eq!(ok(&mut s, "(atof \"2.5mm\")"), "2.5"); - assert_eq!(ok(&mut s, "(atof \"x\")"), "0.0"); - assert_eq!(ok(&mut s, "(wcmatch \"Name\" \"N*\")"), "T"); - assert_eq!(ok(&mut s, "(wcmatch \"A12\" \"@##\")"), "T"); - assert_eq!(ok(&mut s, "(wcmatch \"abc\" \"x*,a?c\")"), "T"); - assert_eq!(ok(&mut s, "(wcmatch \"abc\" \"~a*\")"), "nil"); - assert_eq!(ok(&mut s, "(wcmatch \"b\" \"[a-c]\")"), "T"); + list_builtins(&mut s); + string_builtins(&mut s); +} + +fn list_builtins(s: &mut Session) { + assert_eq!(ok(s, "(cons 1 '(2 3))"), "(1 2 3)"); + assert_eq!(ok(s, "(cons 8 \"0\")"), "(8 . \"0\")"); + assert_eq!(ok(s, "(cadr '(1 2 3))"), "2"); + assert_eq!(ok(s, "(cdr (assoc 40 '((10 1.0 2.0 0.0) (40 . 2.5))))"), "2.5"); + assert_eq!(ok(s, "(subst '(40 . 5.0) '(40 . 2.5) '((8 . \"0\") (40 . 2.5)))"), "((8 . \"0\") (40 . 5.0))"); + assert_eq!(ok(s, "(append '(1) '(2 3) nil '(4))"), "(1 2 3 4)"); + assert_eq!(ok(s, "(reverse '(1 2 3))"), "(3 2 1)"); + assert_eq!(ok(s, "(nth 1 '(a b c))"), "B"); + assert_eq!(ok(s, "(member 2 '(1 2 3))"), "(2 3)"); + assert_eq!(ok(s, "(length '(1 2 3))"), "3"); + assert_eq!(ok(s, "(last '(1 2 3))"), "3"); + assert_eq!(ok(s, "(mapcar '+ '(1 2 3) '(10 20 30))"), "(11 22 33)"); + assert_eq!(ok(s, "(mapcar '(lambda (x) (* x x)) '(1 2 3))"), "(1 4 9)"); + assert_eq!(ok(s, "(apply 'max '(3 9 2))"), "9"); + assert_eq!(ok(s, "(vl-sort '(3 1 2) '<)"), "(1 2 3)"); + assert_eq!(ok(s, "(vl-sort '(3 1 2) (function (lambda (a b) (> a b))))"), "(3 2 1)"); + assert_eq!(ok(s, "(vl-remove-if 'minusp '(1 -2 3))"), "(1 3)"); +} + +fn string_builtins(s: &mut Session) { + assert_eq!(ok(s, "(strcat \"a\" \"b\" \"c\")"), "\"abc\""); + assert_eq!(ok(s, "(substr \"Hello\" 2 3)"), "\"ell\""); + assert_eq!(ok(s, "(strcase \"abc\")"), "\"ABC\""); + assert_eq!(ok(s, "(strlen \"abc\" \"de\")"), "5"); + assert_eq!(ok(s, "(itoa 42)"), "\"42\""); + assert_eq!(ok(s, "(atoi \"12abc\")"), "12"); + assert_eq!(ok(s, "(atof \"2.5mm\")"), "2.5"); + assert_eq!(ok(s, "(atof \"x\")"), "0.0"); + assert_eq!(ok(s, "(wcmatch \"Name\" \"N*\")"), "T"); + assert_eq!(ok(s, "(wcmatch \"A12\" \"@##\")"), "T"); + assert_eq!(ok(s, "(wcmatch \"abc\" \"x*,a?c\")"), "T"); + assert_eq!(ok(s, "(wcmatch \"abc\" \"~a*\")"), "nil"); + assert_eq!(ok(s, "(wcmatch \"b\" \"[a-c]\")"), "T"); let many = format!("(wcmatch \"{}\" \"{}\")", "a".repeat(80), "*a".repeat(80)); - assert_eq!(ok(&mut s, &many), "T"); + assert_eq!(ok(s, &many), "T"); let too_deep = format!("(wcmatch \"{}\" \"{}\")", "a".repeat(400), "*a".repeat(400)); - assert!(eval(&mut s, &too_deep).is_err()); - assert_eq!(ok(&mut s, "(vl-string-subst \"X\" \"b\" \"abcb\")"), "\"aXcb\""); - assert_eq!(ok(&mut s, "(vl-string-search \"c\" \"abc\")"), "2"); - assert_eq!(ok(&mut s, "(read \"(1 2)\")"), "(1 2)"); - assert_eq!(ok(&mut s, "(type \"x\")"), "STR"); - assert_eq!(ok(&mut s, "(type 'x)"), "SYM"); - assert_eq!(ok(&mut s, "(equal '(1 2.0) '(1 2.0000001) 1e-3)"), "T"); + assert!(eval(s, &too_deep).is_err()); + assert_eq!(ok(s, "(vl-string-subst \"X\" \"b\" \"abcb\")"), "\"aXcb\""); + assert_eq!(ok(s, "(vl-string-search \"c\" \"abc\")"), "2"); + assert_eq!(ok(s, "(read \"(1 2)\")"), "(1 2)"); + assert_eq!(ok(s, "(type \"x\")"), "STR"); + assert_eq!(ok(s, "(type 'x)"), "SYM"); + assert_eq!(ok(s, "(equal '(1 2.0) '(1 2.0000001) 1e-3)"), "T"); } #[test] diff --git a/crates/engine/src/running.rs b/crates/engine/src/running.rs index afcd0b38f..4ff8ce429 100644 --- a/crates/engine/src/running.rs +++ b/crates/engine/src/running.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, Drawing, Handle, Interactive, Serialize, Vec2}; /// An interactive command in progress. pub struct Running { diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index ef9bf8d73..d1ae67ee1 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -2,7 +2,10 @@ use super::doc_state::{DocState, Snapshot}; use super::error::{EngineError, Result}; use super::running::{PendingWindow, Running}; use super::view::View; -use super::*; +use super::{ + Arc, Bounds2, Drag, Drawing, Entity, Handle, Input, Prompt, Session, Space, Step, Value, Vec2, assoc, cmd, find_command, guard, lisp, pointmod, + prompt, select, snap, sysvars, units, +}; mod cmdline; mod execute; diff --git a/crates/engine/src/session/cmdline.rs b/crates/engine/src/session/cmdline.rs index 5a57e329e..7b9bd53a2 100644 --- a/crates/engine/src/session/cmdline.rs +++ b/crates/engine/src/session/cmdline.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{EngineError, Input, Result, Session, Value, Vec2, cmd, lisp, prompt, snap, units}; impl Session { /// Process one line typed at the command line (Enter pressed). Spaces separate inputs as in a diff --git a/crates/engine/src/session/execute.rs b/crates/engine/src/session/execute.rs index 165ec0863..7b0547404 100644 --- a/crates/engine/src/session/execute.rs +++ b/crates/engine/src/session/execute.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, EngineError, Result, Session, Snapshot, Value, cmd, find_command}; impl Session { // ---------------- programmatic execution ---------------- diff --git a/crates/engine/src/session/files.rs b/crates/engine/src/session/files.rs index f0270c798..95d439f8c 100644 --- a/crates/engine/src/session/files.rs +++ b/crates/engine/src/session/files.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DocState, Drawing, EngineError, Result, Session, Space, Value, Vec2, View, cmd}; impl Session { /// Append a new untitled drawing (`DrawingN`, metric or imperial default view), end a running command, make it active, bump the document epoch and return its index. diff --git a/crates/engine/src/session/interactive.rs b/crates/engine/src/session/interactive.rs index 335004349..e7cb07450 100644 --- a/crates/engine/src/session/interactive.rs +++ b/crates/engine/src/session/interactive.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Arc, Bounds2, Drag, Drawing, EngineError, Entity, Handle, Input, PendingWindow, Prompt, Result, Running, Session, Snapshot, Step, Value, Vec2, + assoc, cmd, find_command, guard, pointmod, select, snap, sysvars, +}; impl Session { // ---------------- interactive commands ---------------- diff --git a/crates/engine/src/session/picking.rs b/crates/engine/src/session/picking.rs index 2c7a61108..5646e8569 100644 --- a/crates/engine/src/session/picking.rs +++ b/crates/engine/src/session/picking.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Bounds2, Handle, Input, PendingWindow, Result, Session, Vec2, cmd, select}; impl Session { // ---------------- picking without a command ---------------- diff --git a/crates/engine/src/session/zoom.rs b/crates/engine/src/session/zoom.rs index 68be05a38..3936f3d14 100644 --- a/crates/engine/src/session/zoom.rs +++ b/crates/engine/src/session/zoom.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Bounds2, Result, Session, Space, Vec2, View}; impl Session { // ---------------- views ---------------- diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs index de936b0c7..32507162f 100644 --- a/crates/engine/src/settings.rs +++ b/crates/engine/src/settings.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Deserialize, Serialize, Vec2, snap}; /// Session-level drafting settings. [`Session::drafting_settings`] supplies the active drawing's /// snap/grid spacing in the corresponding fields for UI and JSON consumers. diff --git a/crates/engine/src/snap.rs b/crates/engine/src/snap.rs index 956d919c7..8def404d2 100644 --- a/crates/engine/src/snap.rs +++ b/crates/engine/src/snap.rs @@ -588,101 +588,110 @@ impl Sink<'_> { } None => p, }; - let cands = &mut *self.cands; - match &p { - Prim::Seg(s) => { - if osmode & mode::END != 0 { - cands.push((mode::END, s.start())); - cands.push((mode::END, s.end())); - } - if osmode & mode::MID != 0 { - cands.push((mode::MID, s.mid())); - } - if let Segment::Arc { arc, .. } = s { - if osmode & mode::CEN != 0 { - cands.push((mode::CEN, arc.center)); - } - if osmode & mode::QUA != 0 { - for k in 0..4 { - let a = k as f64 * PI / 2.0; - if arc.contains_angle(a) { - cands.push((mode::QUA, Vec2::polar(arc.center, arc.radius, a))); - } - } - } - } - } - Prim::Circle(c) => { - if osmode & mode::CEN != 0 { - cands.push((mode::CEN, c.center)); - } - if osmode & mode::QUA != 0 { - for k in 0..4 { - cands.push((mode::QUA, Vec2::polar(c.center, c.radius, k as f64 * PI / 2.0))); - } - } - } - Prim::Ellipse(el) => { - if osmode & mode::CEN != 0 { - cands.push((mode::CEN, el.center)); - } - if osmode & mode::QUA != 0 { - // Only the quadrant points the arc passes through. - for k in 0..4 { - let t = k as f64 * PI / 2.0; - if cadcraft_geom::angle_in_sweep(t, el.start, el.end) { - cands.push((mode::QUA, el.at_param(t))); - } - } - } - if osmode & mode::END != 0 && !el.is_full() { - cands.push((mode::END, el.at_param(el.start))); - cands.push((mode::END, el.at_param(el.end))); - } - if osmode & mode::MID != 0 - && let Some(m) = curves::ellipse_mid(el) - { - cands.push((mode::MID, m)); - } - } - Prim::Spline(s) => { - if osmode & mode::END != 0 && !s.closed { - let (lo, hi) = s.domain(); - cands.push((mode::END, s.eval(lo))); - cands.push((mode::END, s.eval(hi))); - } - if osmode & mode::MID != 0 - && let Some(m) = curves::spline_mid(s) - { - cands.push((mode::MID, m)); - } - } - Prim::Point(pt) => { - if osmode & mode::NOD != 0 && matches!(kind, EntityKind::Point(_)) { - cands.push((mode::NOD, *pt)); - } - } - Prim::Infinite { base, .. } => { - if osmode & mode::END != 0 && matches!(kind, EntityKind::Ray(_)) { - cands.push((mode::END, *base)); - } - } - Prim::Fill(_) => {} - } - self.near.push(p); + self.primitive(kind, p); } if osmode & mode::GCEN != 0 { - // The centroid of the area a closed polyline or spline encloses. - let pts = match kind { - EntityKind::LwPolyline(pl) if pl.closed => { - cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: true }.tessellate(self.aperture / 10.0) + self.closed_centroid(kind, &at); + } + } + + fn primitive(&mut self, kind: &EntityKind, p: Prim) { + let osmode = self.osmode; + let cands = &mut *self.cands; + match &p { + Prim::Seg(s) => { + if osmode & mode::END != 0 { + cands.push((mode::END, s.start())); + cands.push((mode::END, s.end())); + } + if osmode & mode::MID != 0 { + cands.push((mode::MID, s.mid())); + } + if let Segment::Arc { arc, .. } = s { + if osmode & mode::CEN != 0 { + cands.push((mode::CEN, arc.center)); + } + if osmode & mode::QUA != 0 { + for k in 0..4 { + let a = k as f64 * PI / 2.0; + if arc.contains_angle(a) { + cands.push((mode::QUA, Vec2::polar(arc.center, arc.radius, a))); + } + } + } } - EntityKind::Spline(s) if s.closed => s.tessellate(1e-3), - _ => Vec::new(), - }; - if let Some(c) = centroid(&pts) { - self.cands.push((mode::GCEN, at(c))); } + Prim::Circle(c) => { + if osmode & mode::CEN != 0 { + cands.push((mode::CEN, c.center)); + } + if osmode & mode::QUA != 0 { + for k in 0..4 { + cands.push((mode::QUA, Vec2::polar(c.center, c.radius, k as f64 * PI / 2.0))); + } + } + } + Prim::Ellipse(el) => { + if osmode & mode::CEN != 0 { + cands.push((mode::CEN, el.center)); + } + if osmode & mode::QUA != 0 { + // Only the quadrant points the arc passes through. + for k in 0..4 { + let t = k as f64 * PI / 2.0; + if cadcraft_geom::angle_in_sweep(t, el.start, el.end) { + cands.push((mode::QUA, el.at_param(t))); + } + } + } + if osmode & mode::END != 0 && !el.is_full() { + cands.push((mode::END, el.at_param(el.start))); + cands.push((mode::END, el.at_param(el.end))); + } + if osmode & mode::MID != 0 + && let Some(m) = curves::ellipse_mid(el) + { + cands.push((mode::MID, m)); + } + } + Prim::Spline(s) => { + if osmode & mode::END != 0 && !s.closed { + let (lo, hi) = s.domain(); + cands.push((mode::END, s.eval(lo))); + cands.push((mode::END, s.eval(hi))); + } + if osmode & mode::MID != 0 + && let Some(m) = curves::spline_mid(s) + { + cands.push((mode::MID, m)); + } + } + Prim::Point(pt) => { + if osmode & mode::NOD != 0 && matches!(kind, EntityKind::Point(_)) { + cands.push((mode::NOD, *pt)); + } + } + Prim::Infinite { base, .. } => { + if osmode & mode::END != 0 && matches!(kind, EntityKind::Ray(_)) { + cands.push((mode::END, *base)); + } + } + Prim::Fill(_) => {} + } + self.near.push(p); + } + + fn closed_centroid(&mut self, kind: &EntityKind, at: &impl Fn(Vec2) -> Vec2) { + // The centroid of the area a closed polyline or spline encloses. + let pts = match kind { + EntityKind::LwPolyline(pl) if pl.closed => { + cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: true }.tessellate(self.aperture / 10.0) + } + EntityKind::Spline(s) if s.closed => s.tessellate(1e-3), + _ => Vec::new(), + }; + if let Some(c) = centroid(&pts) { + self.cands.push((mode::GCEN, at(c))); } } } diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index 9fd1e3f79..9f08b4fc5 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Deserialize, Serialize, Vec2}; /// A 2D view of a space: world centre and visible height in drawing units. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] diff --git a/crates/fonts/src/mtext.rs b/crates/fonts/src/mtext.rs index 0c6cb074c..23736d017 100644 --- a/crates/fonts/src/mtext.rs +++ b/crates/fonts/src/mtext.rs @@ -537,6 +537,69 @@ pub fn layout_mtext(contents: &str, insert: Vec2, height: f64, width: f64, attac layout_mtext_with(contents, &MTextParams { insert, height, width, attach, rotation, line_spacing, ..MTextParams::new(insert, height) }) } +fn plain_lines(atoms: &[Atom], fmts: &[Fmt], base: &Fmt, width: f64) -> Vec { + let fmt = |index: usize| fmts.get(index).unwrap_or(base); + let mut lines = Vec::new(); + let mut line = Line::default(); + let mut pending_space = None; + let mut last_fmt = 0usize; + let mut paragraph = 0usize; + let finish = |lines: &mut Vec, line: &mut Line, format: &Fmt, paragraph: usize| { + if line.items.is_empty() { + line.max_h = format.height; + } + line.para = paragraph; + lines.push(std::mem::take(line)); + }; + + for atom in atoms { + match atom { + Atom::Space(format) => { + if !line.items.is_empty() || pending_space.is_some() { + pending_space = Some(*format); + } + last_fmt = *format; + } + Atom::Para(format) => { + pending_space = None; + finish(&mut lines, &mut line, fmt(*format), paragraph); + paragraph += 1; + last_fmt = *format; + } + Atom::Word(segments) => { + let word_width: f64 = segments.iter().map(|(segment, format)| seg_width(segment, fmt(*format))).sum(); + let space_width = + pending_space.map(|format| crate::space_width(&fmt(format).font, fmt(format).height, fmt(format).wf)).unwrap_or(0.0); + if width > 0.0 && !line.items.is_empty() && line.width + space_width + word_width > width + 1e-9 { + finish(&mut lines, &mut line, fmt(last_fmt), paragraph); + } else if !line.items.is_empty() || pending_space.is_some() { + if let Some(format) = pending_space { + let at = line.chars; + line.items.push(Item { seg: None, fmt: format, x: line.width, w: space_width, at }); + line.text.push(' '); + line.chars += 1; + } + line.width += space_width; + } + pending_space = None; + for (segment, format) in segments { + let segment_width = seg_width(segment, fmt(*format)); + line.max_h = line.max_h.max(fmt(*format).height); + let at = line.chars; + let plain = seg_plain(segment); + line.chars += plain.chars().count(); + line.text.push_str(&plain); + line.items.push(Item { seg: Some(segment.clone()), fmt: *format, x: line.width, w: segment_width, at }); + line.width += segment_width; + last_fmt = *format; + } + } + } + } + finish(&mut lines, &mut line, fmt(last_fmt), paragraph); + lines +} + /// Lay out MTEXT: inline formatting runs, stacked fractions, paragraphs, word wrap to the /// column width and the attachment point. pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout { @@ -550,62 +613,7 @@ pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout { return para::layout(contents, p, &base); } let fmt = |i: usize| fmts.get(i).unwrap_or(&base); - let mut lines: Vec = Vec::new(); - let mut line = Line::default(); - let mut pending_space: Option = None; - let mut last_fmt = 0usize; - let mut para = 0usize; - let finish = |lines: &mut Vec, line: &mut Line, f: &Fmt, para: usize| { - if line.items.is_empty() { - line.max_h = f.height; - } - line.para = para; - lines.push(std::mem::take(line)); - }; - for a in &atoms { - match a { - Atom::Space(f) => { - if !line.items.is_empty() || pending_space.is_some() { - pending_space = Some(*f); - } - last_fmt = *f; - } - Atom::Para(f) => { - pending_space = None; - finish(&mut lines, &mut line, fmt(*f), para); - para += 1; - last_fmt = *f; - } - Atom::Word(segs) => { - let ww: f64 = segs.iter().map(|(sg, f)| seg_width(sg, fmt(*f))).sum(); - let sw = pending_space.map(|f| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf)).unwrap_or(0.0); - if width > 0.0 && !line.items.is_empty() && line.width + sw + ww > width + 1e-9 { - finish(&mut lines, &mut line, fmt(last_fmt), para); - } else if !line.items.is_empty() || pending_space.is_some() { - if let Some(f) = pending_space { - let at = line.chars; - line.items.push(Item { seg: None, fmt: f, x: line.width, w: sw, at }); - line.text.push(' '); - line.chars += 1; - } - line.width += sw; - } - pending_space = None; - for (sg, f) in segs { - let fw = seg_width(sg, fmt(*f)); - line.max_h = line.max_h.max(fmt(*f).height); - let at = line.chars; - let plain = seg_plain(sg); - line.chars += plain.chars().count(); - line.text.push_str(&plain); - line.items.push(Item { seg: Some(sg.clone()), fmt: *f, x: line.width, w: fw, at }); - line.width += fw; - last_fmt = *f; - } - } - } - } - finish(&mut lines, &mut line, fmt(last_fmt), para); + let mut lines = plain_lines(&atoms, &fmts, &base, width); // Right-to-left text: each line in display order, its paragraph's direction from the // paragraph's first strong character. let mut bases: Vec> = Vec::new(); diff --git a/crates/fonts/src/mtext/para.rs b/crates/fonts/src/mtext/para.rs index d31f6a79d..064e62d02 100644 --- a/crates/fonts/src/mtext/para.rs +++ b/crates/fonts/src/mtext/para.rs @@ -402,6 +402,154 @@ fn split_tabs(segs: &[(Seg, usize)]) -> Vec { out } +struct LineBuilder<'a> { + paragraphs: &'a Paragraphs, + formats: &'a [Fmt], + base: &'a Fmt, + width: f64, + height: f64, + lines: Vec, + line: PLine, + paragraph: usize, + pending_space: Option, + last_format: usize, +} + +impl<'a> LineBuilder<'a> { + fn new(paragraphs: &'a Paragraphs, formats: &'a [Fmt], base: &'a Fmt, width: f64, height: f64) -> Self { + let first = paragraphs.props(0); + Self { + paragraphs, + formats, + base, + width, + height, + lines: Vec::new(), + line: PLine { width: (first.left + first.first).max(0.0), ..Default::default() }, + paragraph: 0, + pending_space: None, + last_format: 0, + } + } + + fn format(&self, index: usize) -> &Fmt { + self.formats.get(index).unwrap_or(self.base) + } + + fn build(mut self, atoms: &[Atom]) -> Vec { + for atom in atoms { + self.push_atom(atom); + } + self.finish(true, 0.0); + self.lines + } + + fn push_atom(&mut self, atom: &Atom) { + match atom { + Atom::Space(format) => { + if !self.line.items.is_empty() || self.pending_space.is_some() { + self.pending_space = Some(*format); + } + self.last_format = *format; + } + Atom::Para(format) => { + self.pending_space = None; + let next_start = self.first_start(self.paragraph + 1); + self.last_format = *format; + self.finish(true, next_start); + self.paragraph += 1; + } + Atom::Word(segments) => self.push_word(segments), + } + } + + fn first_start(&self, paragraph: usize) -> f64 { + let props = self.paragraphs.props(paragraph); + (props.left + props.first).max(0.0) + } + + fn finish(&mut self, paragraph_end: bool, next_start: f64) { + if self.line.items.is_empty() { + self.line.max_h = self.format(self.last_format).height; + } + self.line.para = self.paragraph; + self.line.para_end = paragraph_end; + let next = PLine { width: next_start, ..Default::default() }; + self.lines.push(std::mem::replace(&mut self.line, next)); + } + + fn push_word(&mut self, segments: &[(Seg, usize)]) { + let parts = split_tabs(segments); + for (index, part) in parts.iter().enumerate() { + match part { + Part::Tab => self.push_tab(&parts, index), + Part::Text(text) => self.push_text(index, text), + } + } + } + + fn push_tab(&mut self, parts: &[Part], index: usize) { + self.pending_space = None; + let (available, left) = { + let props = self.paragraphs.props(self.paragraph); + (self.available_width(props), props.left.max(0.0)) + }; + let (stop, kind) = next_tab(self.line.width, self.paragraphs.props(self.paragraph), self.height); + let next_width: f64 = match parts.get(index + 1) { + Some(Part::Text(segments)) => segments.iter().map(|(segment, format)| seg_width(segment, self.format(*format))).sum(), + _ => 0.0, + }; + let x = match kind { + TabKind::Left => stop, + TabKind::Center => (stop - next_width / 2.0).max(self.line.width), + TabKind::Right | TabKind::Decimal => (stop - next_width).max(self.line.width), + }; + if x > available + 1e-9 && !self.line.items.is_empty() { + self.finish(false, left); + } else { + self.line.width = x; + self.line.text.push(' '); + } + } + + fn push_text(&mut self, index: usize, segments: &[(Seg, usize)]) { + let word_width: f64 = segments.iter().map(|(segment, format)| seg_width(segment, self.format(*format))).sum(); + let space_width = if index == 0 { + self.pending_space + .map(|format| crate::space_width(&self.format(format).font, self.format(format).height, self.format(format).wf)) + .unwrap_or(0.0) + } else { + 0.0 + }; + let (available, left) = { + let props = self.paragraphs.props(self.paragraph); + (self.available_width(props), props.left.max(0.0)) + }; + if self.width > 0.0 && !self.line.items.is_empty() && self.line.width + space_width + word_width > available + 1e-9 { + self.finish(false, left); + } else if index == 0 && (!self.line.items.is_empty() || self.pending_space.is_some()) { + self.line.width += space_width; + if self.pending_space.is_some() { + self.line.text.push(' '); + self.line.gaps += 1; + } + } + self.pending_space = None; + for (segment, format) in segments { + let segment_width = seg_width(segment, self.format(*format)); + self.line.max_h = self.line.max_h.max(self.format(*format).height); + self.line.text.push_str(&seg_plain(segment)); + self.line.items.push(Item { seg: segment.clone(), fmt: *format, x: self.line.width, gap: self.line.gaps }); + self.line.width += segment_width; + self.last_format = *format; + } + } + + fn available_width(&self, props: &ParaProps) -> f64 { + if self.width > 0.0 { (self.width - props.right).max(0.0) } else { f64::INFINITY } + } +} + /// The next tab stop right of `x`: the paragraph's stops, the left indent of a hanging indent, /// then every [`DEFAULT_TAB`] text heights. fn next_tab(x: f64, pp: &ParaProps, h: f64) -> (f64, TabKind) { @@ -427,94 +575,7 @@ pub(super) fn layout(contents: &str, p: &MTextParams, base: &Fmt) -> MTextLayout let paras = Paragraphs::scan(contents); let (atoms, fmts) = parse(contents, base.clone()); let fmt = |i: usize| fmts.get(i).unwrap_or(base); - let space_w = |f: usize| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf); - - let mut lines: Vec = Vec::new(); - let mut k = 0usize; - let first_start = |k: usize| { - let pp = paras.props(k); - (pp.left + pp.first).max(0.0) - }; - let mut line = PLine { width: first_start(0), ..Default::default() }; - let mut pending_space: Option = None; - let mut last_fmt = 0usize; - let finish = |lines: &mut Vec, line: &mut PLine, f: &Fmt, k: usize, para_end: bool, next_start: f64| { - if line.items.is_empty() { - line.max_h = f.height; - } - line.para = k; - line.para_end = para_end; - let next = PLine { width: next_start, ..Default::default() }; - lines.push(std::mem::replace(line, next)); - }; - for a in &atoms { - let pp = paras.props(k); - let avail = if width > 0.0 { (width - pp.right).max(0.0) } else { f64::INFINITY }; - let left = pp.left.max(0.0); - match a { - Atom::Space(f) => { - if !line.items.is_empty() || pending_space.is_some() { - pending_space = Some(*f); - } - last_fmt = *f; - } - Atom::Para(f) => { - pending_space = None; - finish(&mut lines, &mut line, fmt(*f), k, true, first_start(k + 1)); - k += 1; - last_fmt = *f; - } - Atom::Word(segs) => { - let parts = split_tabs(segs); - for (pi, part) in parts.iter().enumerate() { - match part { - Part::Tab => { - pending_space = None; - let (stop, kind) = next_tab(line.width, pp, h); - let next_w: f64 = match parts.get(pi + 1) { - Some(Part::Text(s)) => s.iter().map(|(sg, f)| seg_width(sg, fmt(*f))).sum(), - _ => 0.0, - }; - let x = match kind { - TabKind::Left => stop, - TabKind::Center => (stop - next_w / 2.0).max(line.width), - TabKind::Right | TabKind::Decimal => (stop - next_w).max(line.width), - }; - if x > avail + 1e-9 && !line.items.is_empty() { - finish(&mut lines, &mut line, fmt(last_fmt), k, false, left); - } else { - line.width = x; - line.text.push(' '); - } - } - Part::Text(segs) => { - let ww: f64 = segs.iter().map(|(sg, f)| seg_width(sg, fmt(*f))).sum(); - let sw = if pi == 0 { pending_space.map(space_w).unwrap_or(0.0) } else { 0.0 }; - if width > 0.0 && !line.items.is_empty() && line.width + sw + ww > avail + 1e-9 { - finish(&mut lines, &mut line, fmt(last_fmt), k, false, left); - } else if pi == 0 && (!line.items.is_empty() || pending_space.is_some()) { - line.width += sw; - if pending_space.is_some() { - line.text.push(' '); - line.gaps += 1; - } - } - pending_space = None; - for (sg, f) in segs { - let fw = seg_width(sg, fmt(*f)); - line.max_h = line.max_h.max(fmt(*f).height); - line.text.push_str(&seg_plain(sg)); - line.items.push(Item { seg: sg.clone(), fmt: *f, x: line.width, gap: line.gaps }); - line.width += fw; - last_fmt = *f; - } - } - } - } - } - } - } - finish(&mut lines, &mut line, fmt(last_fmt), k, true, 0.0); + let lines = LineBuilder::new(¶s, &fmts, base, width, h).build(&atoms); // Vertical placement: baselines below the top of each line's column. let ls = if p.line_spacing > 0.0 && p.line_spacing.is_finite() { p.line_spacing } else { 1.0 }; @@ -634,6 +695,7 @@ mod tests { for (s, w, a) in [ ("the quick brown fox jumps over the lazy dog", 8.0, 1u8), ("one\\Ptwo\\P\\Pfour", 0.0, 5), + ("\\H3x;\\Psmall", 0.0, 1), ("alpha {\\H2x;beta} gamma delta", 6.0, 9), ("A{\\C1;B}C \\S1/2; x", 0.0, 2), (" lead spaces and\\P more", 5.0, 3), diff --git a/crates/io/src/dxf_raw.rs b/crates/io/src/dxf_raw.rs index a1103056f..0154b01d3 100644 --- a/crates/io/src/dxf_raw.rs +++ b/crates/io/src/dxf_raw.rs @@ -328,22 +328,10 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) { let get = |name: &str| secs.iter().find(|s| s.name == name).map(|s| split(&s.tags)).unwrap_or_default(); let (classes, tables, blocks, entities, objects) = (get("CLASSES"), get("TABLES"), get("BLOCKS"), get("ENTITIES"), get("OBJECTS")); // The drawing's entities by handle: whether each is of a type CADCraft doesn't model. - let mut ents: HashMap = HashMap::new(); - for e in all_entities(d) { - ents.entry(e.handle.hex()).or_insert(matches!(e.kind, EntityKind::Unknown(_))); - } + let ents = entity_types(d); let ids = identities(&tables, &blocks, &objects); // Extension dictionaries of entities and records: owner → dictionary. - let mut xdict_of: HashMap = HashMap::new(); - for (kind, _, t) in tables.iter().chain(&blocks).chain(&entities) { - if kind == "ENDBLK" { - continue; - } - let h = head(t); - if let (Some(o), Some(x)) = (h.handle, h.xdict) { - xdict_of.entry(o).or_insert(x); - } - } + let xdict_of = extension_dictionaries(&tables, &blocks, &entities); // Objects by handle: the first of each, and none that has an entity's handle. let mut by_handle: HashMap = HashMap::new(); for (i, (_, _, t)) in objects.iter().enumerate() { @@ -361,48 +349,7 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) { let (Some(h), Some(o)) = (hd.handle, hd.owner) else { return false }; !by_handle.contains_key(&o) && xdict_of.get(&o) == Some(&h) && (ents.contains_key(&o) || ids.contains_key(&o)) }; - let modelled_entry = |t: &[Tag], h: &str| entries(t).iter().any(|(k, eh)| eh == h && MODELLED_XDICT_KEYS.contains(&up(k).as_str())); - // Which objects to keep: those under a named object dictionary entry CADCraft doesn't model, - // under the extension dictionary of an entity or record (minus modelled entries; one left - // empty goes), or owned by an entity of a type CADCraft doesn't model. Memoized along each - // ownership chain; cycles and over-long chains are not kept. - let mut memo: HashMap = HashMap::new(); - for i in 0..objects.len() { - let mut path: Vec = Vec::new(); - let mut cur = i; - let verdict = loop { - if let Some(&v) = memo.get(&cur) { - break v; - } - if path.contains(&cur) || path.len() >= MAX_DEPTH { - break false; - } - path.push(cur); - let Some((_, _, t)) = objects.get(cur) else { break false }; - let hd = head(t); - let (Some(h), Some(o)) = (hd.handle, hd.owner) else { break false }; - if by_handle.get(&h) != Some(&cur) { - break false; - } - if Some(&o) == root.as_ref() { - break root_key(&h).is_some_and(|k| !modelled_key(&k)); - } - if let Some(&j) = by_handle.get(&o) { - if is_xdict(j) && objects.get(j).is_some_and(|p| modelled_entry(p.2, &h)) { - break false; - } - cur = j; - continue; - } - if is_xdict(cur) { - break entries(t).iter().any(|(k, _)| !MODELLED_XDICT_KEYS.contains(&up(k).as_str())); - } - break ents.get(&o) == Some(&true); - }; - for p in path { - memo.insert(p, verdict); - } - } + let memo = retained_objects(&objects, &ents, &ids, &by_handle, root.as_deref(), &root_entries, &xdict_of); let mut f = Foreign::default(); let mut kept: HashSet = HashSet::new(); for (i, (kind, _, t)) in objects.iter().enumerate() { @@ -511,6 +458,92 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) { } } +fn entity_types(drawing: &Drawing) -> HashMap { + let mut types = HashMap::new(); + for entity in all_entities(drawing) { + types.entry(entity.handle.hex()).or_insert(matches!(entity.kind, EntityKind::Unknown(_))); + } + types +} + +fn extension_dictionaries( + tables: &[(String, usize, &[Tag])], + blocks: &[(String, usize, &[Tag])], + entities: &[(String, usize, &[Tag])], +) -> HashMap { + let mut dictionaries = HashMap::new(); + for (kind, _, tags) in tables.iter().chain(blocks).chain(entities) { + if kind == "ENDBLK" { + continue; + } + let header = head(tags); + if let (Some(owner), Some(dictionary)) = (header.handle, header.xdict) { + dictionaries.entry(owner).or_insert(dictionary); + } + } + dictionaries +} + +/// Determine which objects belong to unmodelled dictionary entries or owners. +fn retained_objects( + objects: &[(String, usize, &[Tag])], + entities: &HashMap, + records: &HashMap, + by_handle: &HashMap, + root: Option<&str>, + root_entries: &[(String, String)], + xdict_of: &HashMap, +) -> HashMap { + let root_key = |handle: &str| root_entries.iter().find(|(_, entry_handle)| entry_handle == handle).map(|(key, _)| key); + let is_xdict = |index: usize| { + let Some((_, _, tags)) = objects.get(index) else { return false }; + let header = head(tags); + let (Some(handle), Some(owner)) = (header.handle, header.owner) else { return false }; + !by_handle.contains_key(&owner) && xdict_of.get(&owner) == Some(&handle) && (entities.contains_key(&owner) || records.contains_key(&owner)) + }; + let modelled_entry = |tags: &[Tag], handle: &str| { + entries(tags).iter().any(|(key, entry_handle)| entry_handle == handle && MODELLED_XDICT_KEYS.contains(&up(key).as_str())) + }; + let mut retained = HashMap::new(); + for index in 0..objects.len() { + let mut path = Vec::new(); + let mut current = index; + let keep = loop { + if let Some(&known) = retained.get(¤t) { + break known; + } + if path.contains(¤t) || path.len() >= MAX_DEPTH { + break false; + } + path.push(current); + let Some((_, _, tags)) = objects.get(current) else { break false }; + let header = head(tags); + let (Some(handle), Some(owner)) = (header.handle, header.owner) else { break false }; + if by_handle.get(&handle) != Some(¤t) { + break false; + } + if root == Some(owner.as_str()) { + break root_key(&handle).is_some_and(|key| !modelled_key(key)); + } + if let Some(&parent) = by_handle.get(&owner) { + if is_xdict(parent) && objects.get(parent).is_some_and(|object| modelled_entry(object.2, &handle)) { + break false; + } + current = parent; + continue; + } + if is_xdict(current) { + break entries(tags).iter().any(|(key, _)| !MODELLED_XDICT_KEYS.contains(&up(key).as_str())); + } + break entities.get(&owner) == Some(&true); + }; + for visited in path { + retained.insert(visited, keep); + } + } + retained +} + /// Proxy records (DXF Reference, ACAD_PROXY_OBJECT and ACAD_PROXY_ENTITY): another application's /// objects and entities, whose class is named by number in group 91. fn is_proxy(kind: &str) -> bool { diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index 88d1801c3..1489f4def 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -3,7 +3,12 @@ use std::collections::HashMap; use cadcraft_color::{Color, Rgb}; -use cadcraft_doc::*; +use cadcraft_doc::{ + Arc, AssocSnap, Attrib, AttribModes, AttribProps, Block, Circle, Common, DashElement, DimAssoc, DimKind, DimStyle, Dimension, Drawing, Ellipse, + Entity, EntityKind, Face3d, Gradient, Group, HAlign, HVal, Handle, Hatch, HatchLoop, Helix, HelixConstrain, Insert, Layer, Layout, Leader, Line, + Linetype, Lineweight, LwPolyline, MAX_FRAME_CHARS, MLeaderStyle, MText, MultilineAttrib, NamedView, PageSetup, Point, Polyline3d, RawTag, + RayLine, Solid, Table, TableCell, Text, TextStyle, Tolerance, Transparency, Ucs, Unknown, VAlign, Viewport, VportConfig, VportTile, Wipeout, +}; use cadcraft_dxf::{Tag, records, sections}; use cadcraft_geom::{PolyVertex, Spline, Vec2, Vec3}; diff --git a/crates/io/src/dxf_read/common.rs b/crates/io/src/dxf_read/common.rs index 18e8edd8e..9cba18171 100644 --- a/crates/io/src/dxf_read/common.rs +++ b/crates/io/src/dxf_read/common.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Color, Common, Lineweight, Rgb, Tag, Transparency, Vec2, Vec3}; /// Value lookup helpers over an entity's tags. pub(super) struct T<'a>(pub(super) &'a [Tag]); diff --git a/crates/io/src/dxf_read/dimassoc.rs b/crates/io/src/dxf_read/dimassoc.rs index a6219c1d4..fb4fc5aa1 100644 --- a/crates/io/src/dxf_read/dimassoc.rs +++ b/crates/io/src/dxf_read/dimassoc.rs @@ -1,5 +1,5 @@ use super::common::T; -use super::*; +use super::{Arc, AssocSnap, Circle, DimAssoc, DimKind, Dimension, Drawing, EntityKind, Handle, Tag, Vec2, Vec3}; /// Associativity from a DIMASSOC object (files from other writers). The dimension kind decides /// which definition points the references name, in the order AutoCAD writes them: `p13`/`p14` diff --git a/crates/io/src/dxf_read/document.rs b/crates/io/src/dxf_read/document.rs index 245bda331..2e16afbf3 100644 --- a/crates/io/src/dxf_read/document.rs +++ b/crates/io/src/dxf_read/document.rs @@ -1,7 +1,7 @@ use super::common::T; use super::entities::{entity_extras, entity_plot_style, entity_with_table_budget, insert_attribs, polyline_entity}; use super::objects::{MAX_OBJECTS, Objects, page_setup}; -use super::*; +use super::{Block, BlockDef, Drawing, Entity, EntityKind, Handle, HashMap, Layout, LayoutDef, Rx, Tag, records}; pub(super) fn parse_entities( recs: &[(String, Vec)], diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs index 9f0816d2a..aba1bdc4e 100644 --- a/crates/io/src/dxf_read/entities.rs +++ b/crates/io/src/dxf_read/entities.rs @@ -1,7 +1,11 @@ use super::common::{T, common, ocs}; use super::hatch::hatch; use super::text::{attdef, attrib_modes, attrib_props, mtext_from, multiline_attrib, split_embedded, text_from}; -use super::*; +use super::{ + Arc, Attrib, Circle, Common, DimKind, Dimension, Ellipse, EntityKind, Face3d, Handle, HashMap, Helix, HelixConstrain, Insert, Leader, Line, + LwPolyline, MAX_FRAME_CHARS, Point, PolyVertex, Polyline3d, RawTag, RayLine, Rx, Solid, Spline, Table, TableCell, Tag, Tolerance, Unknown, Vec2, + Vec3, Viewport, Wipeout, records, +}; /// Parse one entity record. POLYLINE/INSERT followers are handled by the caller. pub(super) fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> { diff --git a/crates/io/src/dxf_read/hatch.rs b/crates/io/src/dxf_read/hatch.rs index 18fe81531..13013a85d 100644 --- a/crates/io/src/dxf_read/hatch.rs +++ b/crates/io/src/dxf_read/hatch.rs @@ -1,5 +1,5 @@ use super::common::T; -use super::*; +use super::{Color, EntityKind, Gradient, Handle, Hatch, HatchLoop, PolyVertex, Rgb, Spline, Tag, Vec2}; pub(super) fn hatch(tags: &[Tag], t: &T) -> Option { let (origin, background) = hatch_xdata(tags); @@ -19,181 +19,11 @@ pub(super) fn hatch(tags: &[Tag], t: &T) -> Option { let flags = get(i)?.i64(); i += 1; let outer = flags & 1 != 0; - let mut verts: Vec = Vec::new(); - if flags & 2 != 0 { - // Polyline boundary: 72 has-bulge, 73 closed, 93 count, then 10/20/42. - let has_bulge = get(i).filter(|x| x.code == 72).map(Tag::i64).unwrap_or(0) != 0; - while get(i).is_some_and(|x| x.code != 93) { - i += 1; - } - let n = get(i)?.i64().clamp(0, 1_000_000) as usize; - i += 1; - for _ in 0..n { - let x = get(i).filter(|t| t.code == 10)?.f64(); - let y = get(i + 1).filter(|t| t.code == 20)?.f64(); - i += 2; - let mut b = 0.0; - if has_bulge && get(i).is_some_and(|t| t.code == 42) { - b = get(i)?.f64(); - i += 1; - } - verts.push(PolyVertex::with_bulge(Vec2::new(x, y), b)); - } - } else { - // Edge boundary: 93 edge count, each 72 edge type. - while get(i).is_some_and(|x| x.code != 93) { - i += 1; - } - let n = get(i)?.i64().clamp(0, 1_000_000) as usize; - i += 1; - for _ in 0..n { - while get(i).is_some_and(|x| x.code != 72) { - i += 1; - } - let ty = get(i)?.i64(); - i += 1; - let f = |i: usize| get(i).map(Tag::f64).unwrap_or(0.0); - match ty { - 1 => { - let a = Vec2::new(f(i), f(i + 1)); - let b = Vec2::new(f(i + 2), f(i + 3)); - i += 4; - if verts.last().is_none_or(|l| !l.p.near(a, 1e-9)) { - verts.push(PolyVertex::new(a)); - } - verts.push(PolyVertex::new(b)); - } - 2 => { - let c = Vec2::new(f(i), f(i + 1)); - let r = f(i + 2); - let sa = f(i + 3).to_radians(); - let ea = f(i + 4).to_radians(); - let ccw = get(i + 5).map(Tag::i64).unwrap_or(1) != 0; - i += 6; - let (s, e) = if ccw { (sa, ea) } else { (-sa, -ea) }; - let p0 = Vec2::polar(c, r, s); - let p1 = Vec2::polar(c, r, e); - let sweep = if ccw { cadcraft_geom::ccw_sweep(s, e) } else { -cadcraft_geom::ccw_sweep(e, s) }; - if verts.last().is_none_or(|l| !l.p.near(p0, 1e-9)) { - verts.push(PolyVertex::new(p0)); - } - // A full circle (0 to 360) starts and ends on the same point, which one - // bulge can't span: it goes in as two half circles. - let full = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9; - let bulge = cadcraft_geom::arc_to_bulge(if full { sweep / 2.0 } else { sweep }); - if let Some(l) = verts.last_mut() { - l.bulge = bulge; - } - if full { - verts.push(PolyVertex::with_bulge(Vec2::polar(c, r, s + sweep / 2.0), bulge)); - } - verts.push(PolyVertex::new(p1)); - } - 3 => { - // Elliptic arc: approximate with points. - let c = Vec2::new(f(i), f(i + 1)); - let m = Vec2::new(f(i + 2), f(i + 3)); - let ratio = f(i + 4); - let sa = f(i + 5).to_radians(); - let ea = f(i + 6).to_radians(); - let ccw = get(i + 7).filter(|t| t.code == 73).map(Tag::i64).unwrap_or(1) != 0; - i += 8; - // A clockwise edge stores its parameters mirrored, as an arc edge does: it - // runs from -start clockwise to -end, the ccw sweep from -end to -start - // walked backwards. - let (s, e) = if ccw { (sa, ea) } else { (-ea, -sa) }; - let e = cadcraft_geom::Ellipse { center: c, major: m, ratio, start: s, end: e }; - let mut pts = Vec::new(); - e.tessellate(m.len() * 1e-3, &mut pts); - if !ccw { - pts.reverse(); - } - verts.extend(pts.into_iter().map(PolyVertex::new)); - } - 4 => { - // Spline edge: 94 degree, 73 rational, 74 periodic, 95 knot count, 96 - // control point count, then the 40 knots, the 10/20 control points (each - // with a 42 weight when rational), and 97 fit point count with 11/21 fit - // points and 12/22, 13/23 end tangents (R2010+). - let (mut degree, mut nk, mut nc) = (3, 0, 0); - while let Some(x) = get(i).filter(|x| matches!(x.code, 94 | 73 | 74 | 95 | 96)) { - let n = x.i64().clamp(0, 100_000) as usize; - match x.code { - 94 => degree = n.clamp(1, 10), // same cap as the SPLINE reader - 95 => nk = n, - 96 => nc = n, - _ => {} - } - i += 1; - } - let mut knots = Vec::new(); - while knots.len() < nk && get(i).is_some_and(|x| x.code == 40) { - knots.push(f(i)); - i += 1; - } - let (mut control, mut weights) = (Vec::new(), Vec::new()); - while control.len() < nc && get(i).is_some_and(|x| x.code == 10) { - control.push(Vec2::new(f(i), f(i + 1))); - i += 2; - if get(i).is_some_and(|t| t.code == 42) { - weights.push(f(i)); - i += 1; - } - } - let mut fit = Vec::new(); - if get(i).is_some_and(|x| x.code == 97) { - let n = get(i).map(Tag::i64).unwrap_or(0).clamp(0, 100_000) as usize; - i += 1; - while fit.len() < n && get(i).is_some_and(|x| x.code == 11) { - fit.push(Vec2::new(f(i), f(i + 1))); - i += 2; - } - } - if weights.len() != control.len() { - weights.clear(); - } - let sp = Spline { degree, knots, control, weights, ..Spline::default() }; - let sp = if sp.is_valid() { - sp - } else if sp.control.is_empty() && fit.len() > 1 { - Spline::from_fit_points(&fit) - } else { - Spline::from_control(sp.control, degree) - }; - let pts = sp.tessellate(1e-3); - let skip = usize::from(verts.last().zip(pts.first()).is_some_and(|(l, p)| l.p.near(*p, 1e-9))); - verts.extend(pts.into_iter().skip(skip).map(PolyVertex::new)); - } - _ => {} - } - } - } + let mut verts = if flags & 2 != 0 { polyline_boundary(tags, &mut i)? } else { edge_boundary(tags, &mut i)? }; if verts.len() > 1 && verts.first().map(|f| f.p).zip(verts.last().map(|l| l.p)).is_some_and(|(a, b)| a.near(b, 1e-9)) { verts.pop(); } - // 97: number of source boundary objects, then their handles (330). A spline edge's fit - // data count is a 97 too, followed by its fit points instead. - let mut sources = Vec::new(); - for _ in 0..2 { - while get(i).is_some_and(|x| !matches!(x.code, 97 | 92 | 75)) { - i += 1; - } - if get(i).is_none_or(|x| x.code != 97) { - break; - } - let n = get(i).map(Tag::i64).unwrap_or(0).clamp(0, MAX_HATCH_SOURCES as i64) as usize; - i += 1; - if get(i).is_some_and(|x| !matches!(x.code, 330 | 92 | 75)) { - continue; - } - while sources.len() < n - && let Some(x) = get(i).filter(|x| x.code == 330) - { - sources.extend(Handle::parse_hex(&x.str()).filter(|h| h.0 != 0)); - i += 1; - } - break; - } + let sources = boundary_sources(tags, &mut i); loops.push(HatchLoop { vertices: verts, outer, sources }); } let solid = solid || pattern.eq_ignore_ascii_case("SOLID"); @@ -219,6 +49,191 @@ pub(super) fn hatch(tags: &[Tag], t: &T) -> Option { })) } +fn seek_code(tags: &[Tag], index: &mut usize, code: i32) { + while tags.get(*index).is_some_and(|tag| tag.code != code) { + *index += 1; + } +} + +fn polyline_boundary(tags: &[Tag], index: &mut usize) -> Option> { + let get = |i: usize| tags.get(i); + let has_bulge = get(*index).filter(|tag| tag.code == 72).map(Tag::i64).unwrap_or(0) != 0; + seek_code(tags, index, 93); + let count = get(*index)?.i64().clamp(0, 1_000_000) as usize; + *index += 1; + let mut vertices = Vec::new(); + for _ in 0..count { + let x = get(*index).filter(|tag| tag.code == 10)?.f64(); + let y = get(*index + 1).filter(|tag| tag.code == 20)?.f64(); + *index += 2; + let mut bulge = 0.0; + if has_bulge && get(*index).is_some_and(|tag| tag.code == 42) { + bulge = get(*index)?.f64(); + *index += 1; + } + vertices.push(PolyVertex::with_bulge(Vec2::new(x, y), bulge)); + } + Some(vertices) +} + +fn edge_boundary(tags: &[Tag], index: &mut usize) -> Option> { + let get = |i: usize| tags.get(i); + seek_code(tags, index, 93); + let count = get(*index)?.i64().clamp(0, 1_000_000) as usize; + *index += 1; + let mut vertices = Vec::new(); + for _ in 0..count { + seek_code(tags, index, 72); + let kind = get(*index)?.i64(); + *index += 1; + match kind { + 1 => line_edge(tags, index, &mut vertices), + 2 => circular_edge(tags, index, &mut vertices), + 3 => elliptic_edge(tags, index, &mut vertices), + 4 => spline_edge(tags, index, &mut vertices), + _ => {} + } + } + Some(vertices) +} + +fn edge_value(tags: &[Tag], index: usize) -> f64 { + tags.get(index).map(Tag::f64).unwrap_or(0.0) +} + +fn line_edge(tags: &[Tag], index: &mut usize, vertices: &mut Vec) { + let start = Vec2::new(edge_value(tags, *index), edge_value(tags, *index + 1)); + let end = Vec2::new(edge_value(tags, *index + 2), edge_value(tags, *index + 3)); + *index += 4; + if vertices.last().is_none_or(|last| !last.p.near(start, 1e-9)) { + vertices.push(PolyVertex::new(start)); + } + vertices.push(PolyVertex::new(end)); +} + +fn circular_edge(tags: &[Tag], index: &mut usize, vertices: &mut Vec) { + let center = Vec2::new(edge_value(tags, *index), edge_value(tags, *index + 1)); + let radius = edge_value(tags, *index + 2); + let start_angle = edge_value(tags, *index + 3).to_radians(); + let end_angle = edge_value(tags, *index + 4).to_radians(); + let ccw = tags.get(*index + 5).map(Tag::i64).unwrap_or(1) != 0; + *index += 6; + let (start, end) = if ccw { (start_angle, end_angle) } else { (-start_angle, -end_angle) }; + let start_point = Vec2::polar(center, radius, start); + let end_point = Vec2::polar(center, radius, end); + let sweep = if ccw { cadcraft_geom::ccw_sweep(start, end) } else { -cadcraft_geom::ccw_sweep(end, start) }; + if vertices.last().is_none_or(|last| !last.p.near(start_point, 1e-9)) { + vertices.push(PolyVertex::new(start_point)); + } + // A full circle needs two bulges because one bulge cannot span coincident endpoints. + let full = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9; + let bulge = cadcraft_geom::arc_to_bulge(if full { sweep / 2.0 } else { sweep }); + if let Some(last) = vertices.last_mut() { + last.bulge = bulge; + } + if full { + vertices.push(PolyVertex::with_bulge(Vec2::polar(center, radius, start + sweep / 2.0), bulge)); + } + vertices.push(PolyVertex::new(end_point)); +} + +fn elliptic_edge(tags: &[Tag], index: &mut usize, vertices: &mut Vec) { + let center = Vec2::new(edge_value(tags, *index), edge_value(tags, *index + 1)); + let major = Vec2::new(edge_value(tags, *index + 2), edge_value(tags, *index + 3)); + let ratio = edge_value(tags, *index + 4); + let start_angle = edge_value(tags, *index + 5).to_radians(); + let end_angle = edge_value(tags, *index + 6).to_radians(); + let ccw = tags.get(*index + 7).filter(|tag| tag.code == 73).map(Tag::i64).unwrap_or(1) != 0; + *index += 8; + let (start, end) = if ccw { (start_angle, end_angle) } else { (-end_angle, -start_angle) }; + let ellipse = cadcraft_geom::Ellipse { center, major, ratio, start, end }; + let mut points = Vec::new(); + ellipse.tessellate(major.len() * 1e-3, &mut points); + if !ccw { + points.reverse(); + } + vertices.extend(points.into_iter().map(PolyVertex::new)); +} + +fn spline_edge(tags: &[Tag], index: &mut usize, vertices: &mut Vec) { + let get = |i: usize| tags.get(i); + let mut degree = 3; + let (mut knot_count, mut control_count) = (0, 0); + while let Some(tag) = get(*index).filter(|tag| matches!(tag.code, 94 | 73 | 74 | 95 | 96)) { + let count = tag.i64().clamp(0, 100_000) as usize; + match tag.code { + 94 => degree = count.clamp(1, 10), + 95 => knot_count = count, + 96 => control_count = count, + _ => {} + } + *index += 1; + } + let mut knots = Vec::new(); + while knots.len() < knot_count && get(*index).is_some_and(|tag| tag.code == 40) { + knots.push(edge_value(tags, *index)); + *index += 1; + } + let (mut control, mut weights) = (Vec::new(), Vec::new()); + while control.len() < control_count && get(*index).is_some_and(|tag| tag.code == 10) { + control.push(Vec2::new(edge_value(tags, *index), edge_value(tags, *index + 1))); + *index += 2; + if get(*index).is_some_and(|tag| tag.code == 42) { + weights.push(edge_value(tags, *index)); + *index += 1; + } + } + let mut fit = Vec::new(); + if get(*index).is_some_and(|tag| tag.code == 97) { + let count = get(*index).map(Tag::i64).unwrap_or(0).clamp(0, 100_000) as usize; + *index += 1; + while fit.len() < count && get(*index).is_some_and(|tag| tag.code == 11) { + fit.push(Vec2::new(edge_value(tags, *index), edge_value(tags, *index + 1))); + *index += 2; + } + } + if weights.len() != control.len() { + weights.clear(); + } + let spline = Spline { degree, knots, control, weights, ..Spline::default() }; + let spline = if spline.is_valid() { + spline + } else if spline.control.is_empty() && fit.len() > 1 { + Spline::from_fit_points(&fit) + } else { + Spline::from_control(spline.control, degree) + }; + let points = spline.tessellate(1e-3); + let skip = usize::from(vertices.last().zip(points.first()).is_some_and(|(last, point)| last.p.near(*point, 1e-9))); + vertices.extend(points.into_iter().skip(skip).map(PolyVertex::new)); +} + +fn boundary_sources(tags: &[Tag], index: &mut usize) -> Vec { + let get = |i: usize| tags.get(i); + let mut sources = Vec::new(); + for _ in 0..2 { + while get(*index).is_some_and(|tag| !matches!(tag.code, 97 | 92 | 75)) { + *index += 1; + } + if get(*index).is_none_or(|tag| tag.code != 97) { + break; + } + let count = get(*index).map(Tag::i64).unwrap_or(0).clamp(0, MAX_HATCH_SOURCES as i64) as usize; + *index += 1; + if get(*index).is_some_and(|tag| !matches!(tag.code, 330 | 92 | 75)) { + continue; + } + while sources.len() < count + && let Some(tag) = get(*index).filter(|tag| tag.code == 330) + { + sources.extend(Handle::parse_hex(&tag.str()).filter(|handle| handle.0 != 0)); + *index += 1; + } + break; + } + sources +} + /// Upper bound on the source boundary objects of one HATCH loop. const MAX_HATCH_SOURCES: usize = 4096; diff --git a/crates/io/src/dxf_read/objects.rs b/crates/io/src/dxf_read/objects.rs index 97b30c786..4d692ccee 100644 --- a/crates/io/src/dxf_read/objects.rs +++ b/crates/io/src/dxf_read/objects.rs @@ -1,7 +1,7 @@ use super::common::T; use super::dimassoc::dimassoc; use super::document::sortents_table; -use super::*; +use super::{Drawing, EntityKind, Group, Handle, HashMap, MLeaderStyle, PageSetup, Rx, Tag, Vec2}; /// Non-graphical objects applied once every entity is placed. #[derive(Default)] diff --git a/crates/io/src/dxf_read/tables.rs b/crates/io/src/dxf_read/tables.rs index 9419a081e..8a57f7f9e 100644 --- a/crates/io/src/dxf_read/tables.rs +++ b/crates/io/src/dxf_read/tables.rs @@ -1,6 +1,9 @@ use super::common::T; use super::objects::MAX_OBJECTS; -use super::*; +use super::{ + Color, DashElement, DimStyle, Drawing, HVal, HashMap, Layer, Linetype, Lineweight, NamedView, Rgb, Rx, Tag, TextStyle, Ucs, Vec2, Vec3, + VportConfig, VportTile, records, +}; pub(super) fn header(tags: &[Tag], d: &mut Drawing) { let mut i = 0; @@ -99,30 +102,7 @@ pub(super) fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) { }) .collect(); // Handle maps first: DIMSTYLE records refer to text styles and arrow blocks by handle. - for (kind, tg) in &recs { - let t = T(tg); - let (Some(h), Some(n)) = (t.s(5), t.s(2)) else { continue }; - match kind.as_str() { - "STYLE" => { - rx.styles.insert(h.trim().to_ascii_uppercase(), n); - } - "BLOCK_RECORD" => { - let units = t.i(70).and_then(|u| u8::try_from(u).ok()).unwrap_or(0); - rx.block_props.insert(n.to_ascii_uppercase(), (units, t.i(280).unwrap_or(1) != 0)); - if let Some(a) = crate::dxf_ext::read_array(tg) { - rx.block_arrays.insert(n.to_ascii_uppercase(), a); - } - rx.brs.insert(h.trim().to_ascii_uppercase(), n); - } - "LTYPE" => { - rx.ltypes.insert(h.trim().to_ascii_uppercase(), n); - } - "LAYER" => { - rx.layers.insert(h.trim().to_ascii_uppercase(), n); - } - _ => {} - } - } + index_table_handles(&recs, rx); let mut active_vport = false; // Index of layers already appended, by lower-cased name, so each LAYER record updates or // appends in O(1) instead of scanning the growing layer list — otherwise many distinct layer @@ -300,6 +280,33 @@ pub(super) fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) { d.vports.retain(|c| c.tiles.len() > usize::from(c.is_active())); } +fn index_table_handles(recs: &[(String, Vec)], rx: &mut Rx) { + for (kind, tags) in recs { + let table = T(tags); + let (Some(handle), Some(name)) = (table.s(5), table.s(2)) else { continue }; + match kind.as_str() { + "STYLE" => { + rx.styles.insert(handle.trim().to_ascii_uppercase(), name); + } + "BLOCK_RECORD" => { + let units = table.i(70).and_then(|units| u8::try_from(units).ok()).unwrap_or(0); + rx.block_props.insert(name.to_ascii_uppercase(), (units, table.i(280).unwrap_or(1) != 0)); + if let Some(array) = crate::dxf_ext::read_array(tags) { + rx.block_arrays.insert(name.to_ascii_uppercase(), array); + } + rx.brs.insert(handle.trim().to_ascii_uppercase(), name); + } + "LTYPE" => { + rx.ltypes.insert(handle.trim().to_ascii_uppercase(), name); + } + "LAYER" => { + rx.layers.insert(handle.trim().to_ascii_uppercase(), name); + } + _ => {} + } + } +} + /// Most tiles a viewport configuration keeps. const MAX_VPORT_TILES: usize = 64; diff --git a/crates/io/src/dxf_read/text.rs b/crates/io/src/dxf_read/text.rs index 45ee2f003..10d770c0e 100644 --- a/crates/io/src/dxf_read/text.rs +++ b/crates/io/src/dxf_read/text.rs @@ -1,5 +1,5 @@ use super::common::{T, common, ocs_n}; -use super::*; +use super::{Attrib, AttribModes, AttribProps, Common, HAlign, MText, MultilineAttrib, Tag, Text, VAlign, Vec2, Vec3, mtext_dxf}; /// The properties an ATTRIB carries itself: those it has groups for. Its layer counts only when /// it differs from its block reference's (an attribute defined on layer 0 goes on the reference's diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index 75360ebae..4ec9fc636 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -6,7 +6,11 @@ use std::collections::HashMap; use cadcraft_color::Color; -use cadcraft_doc::*; +use cadcraft_doc::{ + AssocSnap, Attrib, Block, Common, DimAssoc, DimKind, DimStyle, Dimension, Drawing, Entity, EntityKind, EntityStore, Gradient, Group, HAlign, + HVal, Handle, Hatch, Insert, Layout, Leader, Line, Lineweight, LwPolyline, MLeader, MLeaderStyle, MText, PageSetup, Point, Solid, Space, Table, + TableStyle, Text, Transparency, VAlign, VportConfig, VportTile, +}; use cadcraft_dxf::{Tag, Version}; use cadcraft_geom::{Bounds2, Vec2, Vec3}; diff --git a/crates/io/src/dxf_write/assoc.rs b/crates/io/src/dxf_write/assoc.rs index 9d9a607aa..45a14df22 100644 --- a/crates/io/src/dxf_write/assoc.rs +++ b/crates/io/src/dxf_write/assoc.rs @@ -1,5 +1,5 @@ use super::ctx::OsnapRef; -use super::*; +use super::{AssocSnap, DimAssoc, DimKind, Dimension, Drawing, EntityKind, LwPolyline}; /// Whether a DIMASSOC point reference to `k` names a point of it without naming a segment: /// true for objects made of one curve. Polylines are referenced by segment ([`polyline_vertex_ref`]); diff --git a/crates/io/src/dxf_write/blocks.rs b/crates/io/src/dxf_write/blocks.rs index 6514d49bc..2de1400cf 100644 --- a/crates/io/src/dxf_write/blocks.rs +++ b/crates/io/src/dxf_write/blocks.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + Block, Color, Common, Dimension, Drawing, Entity, EntityKind, EntityStore, Handle, HashMap, Line, MText, Solid, Table, Vec2, Vec3, dxf_ext, +}; /// What happens to the anonymous `*D` blocks read from a file (#495): a dimension that still /// names its block regenerates its geometry under that name, keeping the block's entity diff --git a/crates/io/src/dxf_write/common.rs b/crates/io/src/dxf_write/common.rs index 91e00d888..bf8fad449 100644 --- a/crates/io/src/dxf_write/common.rs +++ b/crates/io/src/dxf_write/common.rs @@ -1,5 +1,8 @@ use super::ctx::{AssocObj, Ctx}; -use super::*; +use super::{ + Attrib, Color, Common, DimKind, DimStyle, Dimension, Drawing, Entity, EntityKind, HAlign, HashMap, Insert, Lineweight, Text, Transparency, + VAlign, Vec3, Version, W, dxf_ext, +}; /// `AcadAnnotative` xdata marking an annotative style. pub(super) fn annotative_xdata(w: &mut W) { diff --git a/crates/io/src/dxf_write/ctx.rs b/crates/io/src/dxf_write/ctx.rs index 11c679a95..ce9c38b7a 100644 --- a/crates/io/src/dxf_write/ctx.rs +++ b/crates/io/src/dxf_write/ctx.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DimVal, Handle, HashMap, K, Vec3, Version, dxf_ext}; /// The `DIMASSOC` object of an associative dimension (stored in the dimension's extension /// dictionary under `ACAD_DIMASSOC`). diff --git a/crates/io/src/dxf_write/entity.rs b/crates/io/src/dxf_write/entity.rs index 6c58cdfac..cd41cdf91 100644 --- a/crates/io/src/dxf_write/entity.rs +++ b/crates/io/src/dxf_write/entity.rs @@ -2,7 +2,9 @@ use super::blocks::table_size; use super::common::{attrib_common, common, common_x, dim_xdata, layer_and_props, multiline_tags, oblique_52, text_tags}; use super::ctx::Ctx; use super::layout::paper_view; -use super::*; +use super::{ + Bounds2, Color, DimKind, Drawing, Entity, EntityKind, Gradient, Handle, HashMap, Hatch, Leader, MLeader, Point, Vec2, Vec3, Version, W, dxf_ext, +}; /// The persistent reactors of boundary objects: each source object of an associative hatch in /// the same space or block → the hatches it bounds (AutoCAD links the two both ways; without the @@ -31,6 +33,19 @@ pub(super) fn hatch_reactors(d: &Drawing) -> HashMap> { } pub(super) fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + write_basic_entity(w, d, e, owner, paper, cx); + write_text_entity(w, d, e, owner, paper, cx); + write_insert_entity(w, d, e, owner, paper, cx); + write_dimension_entity(w, d, e, owner, paper, cx); + write_table_entity(w, d, e, owner, paper, cx); + write_annotations_entity(w, d, e, owner, paper, cx); + write_hatch_entity(w, d, e, owner, paper, cx); + write_viewport_entity(w, d, e, owner, paper, cx); + write_wipeout_entity(w, d, e, owner, paper, cx); + write_other_entity(w, d, e, owner, paper, cx); +} + +fn write_basic_entity(w: &mut W, _d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { match &e.kind { EntityKind::Line(l) => { w.s(0, "LINE"); @@ -199,6 +214,12 @@ pub(super) fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: boo w.p(10, r.base); w.p(11, r.dir); } + _ => {} + } +} + +fn write_text_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + match &e.kind { EntityKind::Text(t) => { w.s(0, "TEXT"); common(w, e, owner, paper, "AcDbText"); @@ -255,196 +276,211 @@ pub(super) fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: boo w.f(44, t.line_spacing); w.t.extend(crate::dxf_read::mtext_dxf::column_tags(t)); } - EntityKind::Insert(i) => { - w.s(0, "INSERT"); - common(w, e, owner, paper, "AcDbBlockReference"); - if !i.attribs.is_empty() { - w.i(66, 1); - } - w.s(2, &i.block); - w.p(10, i.insert); - w.f(41, i.scale.x); - w.f(42, i.scale.y); - w.f(43, i.scale.z); - w.f(50, i.rotation.to_degrees()); - if i.cols > 1 || i.rows > 1 { - w.i(70, i64::from(i.cols)); - w.i(71, i64::from(i.rows)); - w.f(44, i.col_spacing); - w.f(45, i.row_spacing); - } - if !i.attribs.is_empty() { - for a in &i.attribs { - let ah = w.h(); - w.s(0, "ATTRIB"); - w.s(5, ah); - w.s(330, e.handle.hex()); - w.s(100, "AcDbEntity"); - let (c, base) = attrib_common(d, e, i, a); - layer_and_props(w, &c, Some(&base)); - // Hidden with its definition (a dynamic block's visibility state hides it). - if !base.visible { - w.i(60, 1); - } - w.s(100, "AcDbText"); - text_tags(w, d, &a.text, Some((a, cx.target)), None); - if cx.target >= Version::R2007 { - multiline_tags(w, a, false); - } - } - let sh = w.h(); - w.s(0, "SEQEND"); - w.s(5, sh); + _ => {} + } +} + +fn write_insert_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + if let EntityKind::Insert(i) = &e.kind { + w.s(0, "INSERT"); + common(w, e, owner, paper, "AcDbBlockReference"); + if !i.attribs.is_empty() { + w.i(66, 1); + } + w.s(2, &i.block); + w.p(10, i.insert); + w.f(41, i.scale.x); + w.f(42, i.scale.y); + w.f(43, i.scale.z); + w.f(50, i.rotation.to_degrees()); + if i.cols > 1 || i.rows > 1 { + w.i(70, i64::from(i.cols)); + w.i(71, i64::from(i.rows)); + w.f(44, i.col_spacing); + w.f(45, i.row_spacing); + } + if !i.attribs.is_empty() { + for a in &i.attribs { + let ah = w.h(); + w.s(0, "ATTRIB"); + w.s(5, ah); w.s(330, e.handle.hex()); w.s(100, "AcDbEntity"); - w.s(8, &e.common.layer); + let (c, base) = attrib_common(d, e, i, a); + layer_and_props(w, &c, Some(&base)); + // Hidden with its definition (a dynamic block's visibility state hides it). + if !base.visible { + w.i(60, 1); + } + w.s(100, "AcDbText"); + text_tags(w, d, &a.text, Some((a, cx.target)), None); + if cx.target >= Version::R2007 { + multiline_tags(w, a, false); + } + } + let sh = w.h(); + w.s(0, "SEQEND"); + w.s(5, sh); + w.s(330, e.handle.hex()); + w.s(100, "AcDbEntity"); + w.s(8, &e.common.layer); + } + } +} + +fn write_dimension_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + if let EntityKind::Dimension(dm) = &e.kind { + w.s(0, "DIMENSION"); + common_x(w, e, owner, paper, "AcDbDimension", cx.assoc.get(&e.handle)); + let bname = cx.dim_blocks.get(&e.handle).cloned().or_else(|| dm.block.clone()).unwrap_or_default(); + w.s(2, bname); + w.p(10, dm.defpt); + let style = d.dim_style(&dm.style).cloned().unwrap_or_default(); + let g = cadcraft_render::dimension_geometry(dm, &style, d.header.f64("DIMSCALE", 1.0)); + w.p(11, if dm.user_text_pos { dm.text_mid } else { g.text_pos.to3(0.0) }); + let ty: i64 = match dm.kind { + DimKind::Linear { .. } => 0, + DimKind::Aligned => 1, + DimKind::Angular => 2, + DimKind::Diameter => 3, + DimKind::Radius => 4, + DimKind::Angular3P => 5, + DimKind::Ordinate { x_type } => 6 | if x_type { 64 } else { 0 }, + // Written as aligned, marked by CADCraft xdata (see `dim_xdata`). + DimKind::ArcLength => 1, + }; + w.i(70, ty | 32 | if dm.user_text_pos { 128 } else { 0 }); + // The actual measurement (read-only in DXF): AutoLISP's entget shows it. + w.f(42, g.measurement); + if !dm.text.is_empty() { + w.s(1, &dm.text); + } + w.s(3, &dm.style); + if dm.text_rotation != 0.0 { + w.f(53, dm.text_rotation.to_degrees()); + } + match dm.kind { + DimKind::Linear { rotation } => { + w.s(100, "AcDbAlignedDimension"); + w.p(13, dm.p13); + w.p(14, dm.p14); + w.f(50, rotation.to_degrees()); + oblique_52(w, dm, rotation); + w.s(100, "AcDbRotatedDimension"); + } + DimKind::Aligned | DimKind::ArcLength => { + w.s(100, "AcDbAlignedDimension"); + w.p(13, dm.p13); + w.p(14, dm.p14); + if matches!(dm.kind, DimKind::Aligned) { + oblique_52(w, dm, (dm.p14.xy() - dm.p13.xy()).angle()); + } + } + DimKind::Radius => { + w.s(100, "AcDbRadialDimension"); + w.p(15, dm.p15); + w.f(40, 0.0); + } + DimKind::Diameter => { + w.s(100, "AcDbDiametricDimension"); + w.p(15, dm.p15); + w.f(40, 0.0); + } + DimKind::Angular => { + w.s(100, "AcDb2LineAngularDimension"); + w.p(13, dm.p13); + w.p(14, dm.p14); + w.p(15, dm.p15); + w.p(16, dm.p16); + } + DimKind::Angular3P => { + w.s(100, "AcDb3PointAngularDimension"); + w.p(13, dm.p13); + w.p(14, dm.p14); + w.p(15, dm.p15); + } + DimKind::Ordinate { .. } => { + w.s(100, "AcDbOrdinateDimension"); + w.p(13, dm.p13); + w.p(14, dm.p14); } } - EntityKind::Dimension(dm) => { - w.s(0, "DIMENSION"); - common_x(w, e, owner, paper, "AcDbDimension", cx.assoc.get(&e.handle)); - let bname = cx.dim_blocks.get(&e.handle).cloned().or_else(|| dm.block.clone()).unwrap_or_default(); - w.s(2, bname); - w.p(10, dm.defpt); - let style = d.dim_style(&dm.style).cloned().unwrap_or_default(); - let g = cadcraft_render::dimension_geometry(dm, &style, d.header.f64("DIMSCALE", 1.0)); - w.p(11, if dm.user_text_pos { dm.text_mid } else { g.text_pos.to3(0.0) }); - let ty: i64 = match dm.kind { - DimKind::Linear { .. } => 0, - DimKind::Aligned => 1, - DimKind::Angular => 2, - DimKind::Diameter => 3, - DimKind::Radius => 4, - DimKind::Angular3P => 5, - DimKind::Ordinate { x_type } => 6 | if x_type { 64 } else { 0 }, - // Written as aligned, marked by CADCraft xdata (see `dim_xdata`). - DimKind::ArcLength => 1, - }; - w.i(70, ty | 32 | if dm.user_text_pos { 128 } else { 0 }); - // The actual measurement (read-only in DXF): AutoLISP's entget shows it. - w.f(42, g.measurement); - if !dm.text.is_empty() { - w.s(1, &dm.text); - } - w.s(3, &dm.style); - if dm.text_rotation != 0.0 { - w.f(53, dm.text_rotation.to_degrees()); - } - match dm.kind { - DimKind::Linear { rotation } => { - w.s(100, "AcDbAlignedDimension"); - w.p(13, dm.p13); - w.p(14, dm.p14); - w.f(50, rotation.to_degrees()); - oblique_52(w, dm, rotation); - w.s(100, "AcDbRotatedDimension"); - } - DimKind::Aligned | DimKind::ArcLength => { - w.s(100, "AcDbAlignedDimension"); - w.p(13, dm.p13); - w.p(14, dm.p14); - if matches!(dm.kind, DimKind::Aligned) { - oblique_52(w, dm, (dm.p14.xy() - dm.p13.xy()).angle()); - } - } - DimKind::Radius => { - w.s(100, "AcDbRadialDimension"); - w.p(15, dm.p15); - w.f(40, 0.0); - } - DimKind::Diameter => { - w.s(100, "AcDbDiametricDimension"); - w.p(15, dm.p15); - w.f(40, 0.0); - } - DimKind::Angular => { - w.s(100, "AcDb2LineAngularDimension"); - w.p(13, dm.p13); - w.p(14, dm.p14); - w.p(15, dm.p15); - w.p(16, dm.p16); - } - DimKind::Angular3P => { - w.s(100, "AcDb3PointAngularDimension"); - w.p(13, dm.p13); - w.p(14, dm.p14); - w.p(15, dm.p15); - } - DimKind::Ordinate { .. } => { - w.s(100, "AcDbOrdinateDimension"); - w.p(13, dm.p13); - w.p(14, dm.p14); - } - } - dim_xdata(w, dm, cx); + dim_xdata(w, dm, cx); + } +} + +fn write_table_entity(w: &mut W, _d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + if let EntityKind::Table(t) = &e.kind { + // Tables without a generated block (none expected) are not written. + let Some((bname, brh)) = cx.tables.get(&e.handle) else { return }; + w.s(0, "ACAD_TABLE"); + common(w, e, owner, paper, "AcDbBlockReference"); + w.s(2, bname); + w.p(10, t.insert); + w.s(100, "AcDbTable"); + w.i(280, 0); + w.s(342, cx.table_style(&t.style)); + w.s(343, brh); + w.p(11, Vec3::new(1.0, 0.0, 0.0)); + w.i(90, 0); + let (rows, cols) = table_size(t); + w.i(91, rows as i64); + w.i(92, cols as i64); + for c in [93, 94, 95, 96] { + w.i(c, 0); } - EntityKind::Table(t) => { - // Tables without a generated block (none expected) are not written. - let Some((bname, brh)) = cx.tables.get(&e.handle) else { return }; - w.s(0, "ACAD_TABLE"); - common(w, e, owner, paper, "AcDbBlockReference"); - w.s(2, bname); - w.p(10, t.insert); - w.s(100, "AcDbTable"); - w.i(280, 0); - w.s(342, cx.table_style(&t.style)); - w.s(343, brh); - w.p(11, Vec3::new(1.0, 0.0, 0.0)); - w.i(90, 0); - let (rows, cols) = table_size(t); - w.i(91, rows as i64); - w.i(92, cols as i64); - for c in [93, 94, 95, 96] { - w.i(c, 0); - } - for h in t.row_heights.iter().take(rows) { - w.f(141, *h); - } - for c in t.col_widths.iter().take(cols) { - w.f(142, *c); - } - let cov = cadcraft_render::table_covered(t); - let span = |v: u32| i64::from(v.min(i16::MAX as u32)); - for r in 0..rows { - for c in 0..cols { - let cell = t.cells.get(r).and_then(|x| x.get(c)); - let covered = cov.get(r).and_then(|x| x.get(c)).copied().unwrap_or(false); - let merge = cell.and_then(|x| x.merged).filter(|&(a, b)| a >= 1 && b >= 1 && (a, b) != (1, 1)); - w.i(171, 1); - w.i(172, 0); - w.i(173, i64::from(covered || merge.is_some())); - w.i(174, 0); - let (rs, cs) = if covered { (0, 0) } else { merge.map(|(a, b)| (span(a), span(b))).unwrap_or((1, 1)) }; - w.i(175, cs); - w.i(176, rs); - w.i(91, 0); - w.i(178, 0); - w.f(145, 0.0); - if let Some(h) = cell.and_then(|x| x.text_height).filter(|h| h.is_finite() && *h > 0.0) { - w.f(140, h); - } - // Long text: 250-character code-2 chunks, the rest in code 1. - let chars: Vec = cell.map(|x| x.text.chars().collect()).unwrap_or_default(); - let chunks: Vec = chars.chunks(250).map(|c| c.iter().collect()).collect(); - let n = chunks.len(); - for (i, ch) in chunks.into_iter().enumerate() { - w.s(if i + 1 == n { 1 } else { 2 }, ch); - } - if n == 0 { - w.s(1, ""); - } + for h in t.row_heights.iter().take(rows) { + w.f(141, *h); + } + for c in t.col_widths.iter().take(cols) { + w.f(142, *c); + } + let cov = cadcraft_render::table_covered(t); + let span = |v: u32| i64::from(v.min(i16::MAX as u32)); + for r in 0..rows { + for c in 0..cols { + let cell = t.cells.get(r).and_then(|x| x.get(c)); + let covered = cov.get(r).and_then(|x| x.get(c)).copied().unwrap_or(false); + let merge = cell.and_then(|x| x.merged).filter(|&(a, b)| a >= 1 && b >= 1 && (a, b) != (1, 1)); + w.i(171, 1); + w.i(172, 0); + w.i(173, i64::from(covered || merge.is_some())); + w.i(174, 0); + let (rs, cs) = if covered { (0, 0) } else { merge.map(|(a, b)| (span(a), span(b))).unwrap_or((1, 1)) }; + w.i(175, cs); + w.i(176, rs); + w.i(91, 0); + w.i(178, 0); + w.f(145, 0.0); + if let Some(h) = cell.and_then(|x| x.text_height).filter(|h| h.is_finite() && *h > 0.0) { + w.f(140, h); + } + // Long text: 250-character code-2 chunks, the rest in code 1. + let chars: Vec = cell.map(|x| x.text.chars().collect()).unwrap_or_default(); + let chunks: Vec = chars.chunks(250).map(|c| c.iter().collect()).collect(); + let n = chunks.len(); + for (i, ch) in chunks.into_iter().enumerate() { + w.s(if i + 1 == n { 1 } else { 2 }, ch); + } + if n == 0 { + w.s(1, ""); } - } - w.s(1001, dxf_ext::APP); - w.s(1000, "TABLE"); - w.i(1070, i64::from(t.title)); - w.i(1070, i64::from(t.header)); - w.f(1040, t.text_height); - w.s(1000, &t.style); - for c in crate::dxf_table::table_xdata(t) { - w.s(1000, c); } } + w.s(1001, dxf_ext::APP); + w.s(1000, "TABLE"); + w.i(1070, i64::from(t.title)); + w.i(1070, i64::from(t.header)); + w.f(1040, t.text_height); + w.s(1000, &t.style); + for c in crate::dxf_table::table_xdata(t) { + w.s(1000, c); + } + } +} + +fn write_annotations_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + match &e.kind { EntityKind::MLeader(m) if !cx.native_mleader() => mleader_fallback(w, d, e, m, owner, paper, cx), EntityKind::MLeader(m) => { w.s(0, "MULTILEADER"); @@ -493,142 +529,157 @@ pub(super) fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: boo w.p(13, f.corners[3]); w.i(70, i64::from(f.hidden_edges)); } - EntityKind::Hatch(h) => { - w.s(0, "HATCH"); - common(w, e, owner, paper, "AcDbHatch"); - w.p(10, Vec3::new(0.0, 0.0, h.elevation)); - w.p(210, Vec3::Z); - w.s(2, &h.pattern); - w.i(70, i64::from(h.solid)); - w.i(71, i64::from(h.associative)); - w.i(91, h.loops.len() as i64); - for l in &h.loops { - w.i(92, 2 | if l.outer { 1 } else { 0 }); - let has_bulge = l.vertices.iter().any(|v| v.bulge != 0.0); - w.i(72, i64::from(has_bulge)); - w.i(73, 1); - w.i(93, l.vertices.len() as i64); - for v in &l.vertices { - w.p2(10, v.p); - if has_bulge { - w.f(42, v.bulge); - } - } - // Source boundary objects of an associative hatch (the ones still beside it, in - // its space or block, which carry the hatch as a reactor). - let own = e.handle.hex(); - let sources: Vec = - l.sources.iter().filter(|s| h.associative && w.reactors.get(*s).is_some_and(|r| r.contains(&own))).map(|s| s.hex()).collect(); - w.i(97, sources.len() as i64); - for s in &sources { - w.s(330, s); + _ => {} + } +} + +fn write_hatch_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, _cx: &Ctx) { + if let EntityKind::Hatch(h) = &e.kind { + w.s(0, "HATCH"); + common(w, e, owner, paper, "AcDbHatch"); + w.p(10, Vec3::new(0.0, 0.0, h.elevation)); + w.p(210, Vec3::Z); + w.s(2, &h.pattern); + w.i(70, i64::from(h.solid)); + w.i(71, i64::from(h.associative)); + w.i(91, h.loops.len() as i64); + for l in &h.loops { + w.i(92, 2 | if l.outer { 1 } else { 0 }); + let has_bulge = l.vertices.iter().any(|v| v.bulge != 0.0); + w.i(72, i64::from(has_bulge)); + w.i(73, 1); + w.i(93, l.vertices.len() as i64); + for v in &l.vertices { + w.p2(10, v.p); + if has_bulge { + w.f(42, v.bulge); } } - w.i(75, i64::from(h.style)); - w.i(76, 1); - if !h.solid { - w.f(52, h.angle.to_degrees()); - w.f(41, h.scale); - w.i(77, 0); - // The lines read from the file, else the library's pattern of that name. - let lines = if h.pattern_lines.is_empty() { - cadcraft_doc::library::pattern_for(d, &h.pattern).map(|p| p.lines).unwrap_or_default() - } else { - h.pattern_lines.clone() - }; - w.i(78, lines.len() as i64); - for pl in lines { - let ang = pl.angle.to_radians() + h.angle; - w.f(53, ang.to_degrees()); - let o = h.origin + Vec2::new(pl.origin.0, pl.origin.1).rotate(h.angle) * h.scale; - w.f(43, o.x); - w.f(44, o.y); - let dl = Vec2::new(pl.delta.0, pl.delta.1).rotate(ang) * h.scale; - w.f(45, dl.x); - w.f(46, dl.y); - w.i(79, pl.dashes.len() as i64); - for dsh in pl.dashes { - w.f(49, dsh * h.scale); - } - } + // Source boundary objects of an associative hatch (the ones still beside it, in + // its space or block, which carry the hatch as a reactor). + let own = e.handle.hex(); + let sources: Vec = + l.sources.iter().filter(|s| h.associative && w.reactors.get(*s).is_some_and(|r| r.contains(&own))).map(|s| s.hex()).collect(); + w.i(97, sources.len() as i64); + for s in &sources { + w.s(330, s); } - w.i(98, 0); - if let Some(g) = &h.gradient { - gradient(w, g); - } - hatch_xdata(w, h); } - EntityKind::Viewport(v) => { - paper_view(w, d, e, owner, cx); - w.s(0, "VIEWPORT"); - common(w, e, owner, paper, "AcDbViewport"); - w.p(10, v.center); - w.f(40, v.width); - w.f(41, v.height); - // Status 68: a positive value is "on" (the stacking order; 1 is the active viewport), 0 off. - w.i(68, if v.on { i64::from(v.id.clamp(1, 32767)) } else { 0 }); - w.i(69, i64::from(v.id)); - w.p2(12, v.view_center); - w.f(45, v.view_height); - // Status flags: 16384 = display locked, 32768 = always set, 65536 = clipped by the - // 340 entity, 131072 = off. - let clip = v.clip.filter(|h| *h != e.handle && d.entity(*h).is_some()); - let flags = 32768 | if v.locked { 16384 } else { 0 } | if clip.is_some() { 65536 } else { 0 } | if v.on { 0 } else { 131072 }; - w.i(90, flags); - if let Some(h) = clip { - w.s(340, h.hex()); - } - // AutoCAD's frozen-layer list: one soft pointer per layer, to its LAYER record. Other - // programs read only this; CADCraft prefers its own xdata below when present. - for name in &v.frozen_layers { - if let Some(h) = w.layers.get(&name.to_ascii_uppercase()).cloned() { - w.s(341, h); - } - } - // One CADCraft xdata group holds both lists: readers stop at the first group of an app. - if !v.frozen_layers.is_empty() || !v.layer_colors.is_empty() { - w.s(1001, dxf_ext::APP); - if !v.frozen_layers.is_empty() { - w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers)); - } - if !v.layer_colors.is_empty() { - w.xdata(dxf_ext::layer_colors_xdata(&v.layer_colors)); + w.i(75, i64::from(h.style)); + w.i(76, 1); + if !h.solid { + w.f(52, h.angle.to_degrees()); + w.f(41, h.scale); + w.i(77, 0); + // The lines read from the file, else the library's pattern of that name. + let lines = if h.pattern_lines.is_empty() { + cadcraft_doc::library::pattern_for(d, &h.pattern).map(|p| p.lines).unwrap_or_default() + } else { + h.pattern_lines.clone() + }; + w.i(78, lines.len() as i64); + for pl in lines { + let ang = pl.angle.to_radians() + h.angle; + w.f(53, ang.to_degrees()); + let o = h.origin + Vec2::new(pl.origin.0, pl.origin.1).rotate(h.angle) * h.scale; + w.f(43, o.x); + w.f(44, o.y); + let dl = Vec2::new(pl.delta.0, pl.delta.1).rotate(ang) * h.scale; + w.f(45, dl.x); + w.f(46, dl.y); + w.i(79, pl.dashes.len() as i64); + for dsh in pl.dashes { + w.f(49, dsh * h.scale); } } } - EntityKind::Wipeout(wo) => { - // A 1x1 image spanning the boundary's extents. Clip vertices are in pixel space, whose origin is - // the image's top-left corner with y pointing down: the reader maps each vertex back through - // insert + u * (x + 0.5) + v * (0.5 - y). - // Polygonal clip boundaries are closed, so the first vertex is repeated when needed. - let b = Bounds2::from_points(wo.boundary.iter().copied()); - let o = if b.is_empty() { Vec2::ZERO } else { b.min }; - let (sx, sy) = (if b.width() > 0.0 { b.width() } else { 1.0 }, if b.height() > 0.0 { b.height() } else { 1.0 }); - w.s(0, "WIPEOUT"); - common(w, e, owner, paper, "AcDbWipeout"); - w.i(90, 0); - w.p(10, Vec3::new(o.x, o.y, 0.0)); - w.p(11, Vec3::new(sx, 0.0, 0.0)); - w.p(12, Vec3::new(0.0, sy, 0.0)); - w.p2(13, Vec2::new(1.0, 1.0)); - // Display flags 7 = show image, show unaligned, use clipping boundary; clipping on; default brightness/contrast/fade. - w.i(70, 7); - w.i(280, 1); - w.i(281, 50); - w.i(282, 50); - w.i(283, 0); - w.i(71, 2); - let mut pts = wo.boundary.clone(); - if let (Some(first), Some(last)) = (pts.first().copied(), pts.last().copied()) - && first != last - { - pts.push(first); - } - w.i(91, pts.len() as i64); - for q in pts { - w.p2(14, Vec2::new((q.x - o.x) / sx - 0.5, 0.5 - (q.y - o.y) / sy)); + w.i(98, 0); + if let Some(g) = &h.gradient { + gradient(w, g); + } + hatch_xdata(w, h); + } +} + +fn write_viewport_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx) { + if let EntityKind::Viewport(v) = &e.kind { + paper_view(w, d, e, owner, cx); + w.s(0, "VIEWPORT"); + common(w, e, owner, paper, "AcDbViewport"); + w.p(10, v.center); + w.f(40, v.width); + w.f(41, v.height); + // Status 68: a positive value is "on" (the stacking order; 1 is the active viewport), 0 off. + w.i(68, if v.on { i64::from(v.id.clamp(1, 32767)) } else { 0 }); + w.i(69, i64::from(v.id)); + w.p2(12, v.view_center); + w.f(45, v.view_height); + // Status flags: 16384 = display locked, 32768 = always set, 65536 = clipped by the + // 340 entity, 131072 = off. + let clip = v.clip.filter(|h| *h != e.handle && d.entity(*h).is_some()); + let flags = 32768 | if v.locked { 16384 } else { 0 } | if clip.is_some() { 65536 } else { 0 } | if v.on { 0 } else { 131072 }; + w.i(90, flags); + if let Some(h) = clip { + w.s(340, h.hex()); + } + // AutoCAD's frozen-layer list: one soft pointer per layer, to its LAYER record. Other + // programs read only this; CADCraft prefers its own xdata below when present. + for name in &v.frozen_layers { + if let Some(h) = w.layers.get(&name.to_ascii_uppercase()).cloned() { + w.s(341, h); } } + // One CADCraft xdata group holds both lists: readers stop at the first group of an app. + if !v.frozen_layers.is_empty() || !v.layer_colors.is_empty() { + w.s(1001, dxf_ext::APP); + if !v.frozen_layers.is_empty() { + w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers)); + } + if !v.layer_colors.is_empty() { + w.xdata(dxf_ext::layer_colors_xdata(&v.layer_colors)); + } + } + } +} + +fn write_wipeout_entity(w: &mut W, _d: &Drawing, e: &Entity, owner: &str, paper: bool, _cx: &Ctx) { + if let EntityKind::Wipeout(wo) = &e.kind { + // A 1x1 image spanning the boundary's extents. Clip vertices are in pixel space, whose origin is + // the image's top-left corner with y pointing down: the reader maps each vertex back through + // insert + u * (x + 0.5) + v * (0.5 - y). + // Polygonal clip boundaries are closed, so the first vertex is repeated when needed. + let b = Bounds2::from_points(wo.boundary.iter().copied()); + let o = if b.is_empty() { Vec2::ZERO } else { b.min }; + let (sx, sy) = (if b.width() > 0.0 { b.width() } else { 1.0 }, if b.height() > 0.0 { b.height() } else { 1.0 }); + w.s(0, "WIPEOUT"); + common(w, e, owner, paper, "AcDbWipeout"); + w.i(90, 0); + w.p(10, Vec3::new(o.x, o.y, 0.0)); + w.p(11, Vec3::new(sx, 0.0, 0.0)); + w.p(12, Vec3::new(0.0, sy, 0.0)); + w.p2(13, Vec2::new(1.0, 1.0)); + // Display flags 7 = show image, show unaligned, use clipping boundary; clipping on; default brightness/contrast/fade. + w.i(70, 7); + w.i(280, 1); + w.i(281, 50); + w.i(282, 50); + w.i(283, 0); + w.i(71, 2); + let mut pts = wo.boundary.clone(); + if let (Some(first), Some(last)) = (pts.first().copied(), pts.last().copied()) + && first != last + { + pts.push(first); + } + w.i(91, pts.len() as i64); + for q in pts { + w.p2(14, Vec2::new((q.x - o.x) / sx - 0.5, 0.5 - (q.y - o.y) / sy)); + } + } +} + +fn write_other_entity(w: &mut W, _d: &Drawing, e: &Entity, owner: &str, paper: bool, _cx: &Ctx) { + match &e.kind { EntityKind::Tolerance(t) => { w.s(0, "TOLERANCE"); common(w, e, owner, paper, "AcDbFcf"); @@ -639,6 +690,7 @@ pub(super) fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: boo } // Entity types CADCraft doesn't model aren't written; the save command reports them. EntityKind::Unknown(_) => {} + _ => {} } } diff --git a/crates/io/src/dxf_write/header.rs b/crates/io/src/dxf_write/header.rs index edd62eb13..b628a5c3d 100644 --- a/crates/io/src/dxf_write/header.rs +++ b/crates/io/src/dxf_write/header.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DimVal, Drawing, HVal, K, Version, W, dxf_ext}; /// Header variables we write, with their group codes (DXF Reference, HEADER section). /// `P2`/`P3` = 2D/3D point. diff --git a/crates/io/src/dxf_write/layout.rs b/crates/io/src/dxf_write/layout.rs index ccffa2929..4ad19ffbb 100644 --- a/crates/io/src/dxf_write/layout.rs +++ b/crates/io/src/dxf_write/layout.rs @@ -1,5 +1,5 @@ use super::ctx::Ctx; -use super::*; +use super::{Bounds2, Drawing, Entity, EntityKind, Handle, HashMap, Layout, Vec2, Vec3, VportConfig, VportTile, W, dxf_ext}; /// Layouts with viewports but without the paper-space viewport (id 1, the sheet itself), keyed /// by their first viewport. AutoCAD and DWG take a layout's first viewport as its paper-space diff --git a/crates/io/src/dxf_write/objects.rs b/crates/io/src/dxf_write/objects.rs index 2227ae096..83fe3c7ca 100644 --- a/crates/io/src/dxf_write/objects.rs +++ b/crates/io/src/dxf_write/objects.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Drawing, Group, MLeaderStyle, TableStyle, W, dxf_ext}; /// A TABLESTYLE object (DXF Reference, OBJECTS: TABLESTYLE): margins, title/header /// suppression and per row type (data, column header, title) text and border settings. diff --git a/crates/io/src/dxf_write/sections.rs b/crates/io/src/dxf_write/sections.rs index 7198eeee8..fe865515b 100644 --- a/crates/io/src/dxf_write/sections.rs +++ b/crates/io/src/dxf_write/sections.rs @@ -6,7 +6,879 @@ use super::entity::{entity, hatch_reactors}; use super::header::{code_page, header_vars}; use super::layout::{paper_views, record_head, sheet_first, table_head, vport_table}; use super::objects::{cadcraft_fonts_name, group_obj, mleader_style_obj, scale_list, table_style_obj}; -use super::*; +use super::{ + Block, Color, DimStyle, DimVal, Drawing, Entity, EntityKind, EntityStore, Group, HVal, Handle, HashMap, Layout, MLeaderStyle, PageSetup, Space, + TableStyle, Tag, Vec2, Vec3, Version, W, dxf_ext, +}; + +struct TableWritePlan<'a> { + d: &'a Drawing, + version: Version, + cx: &'a mut Ctx, + ext: cadcraft_geom::Bounds2, +} + +fn write_tables(w: &mut W, plan: TableWritePlan<'_>) -> HashMap { + let d = plan.d; + let version = plan.version; + let cx = plan.cx; + let ext = plan.ext; + // ---------------- TABLES ---------------- + w.s(0, "SECTION"); + w.s(2, "TABLES"); + vport_table(w, d, ext); + // LTYPE. Embedded text and shapes point at STYLE records (340), which get their handles + // in the STYLE table below: (tag position, style name, is a shape) are filled in there. + let mut ltype_handles: HashMap = HashMap::new(); + let mut ltype_style_refs: Vec<(usize, Option, bool)> = Vec::new(); + let th = table_head(w, "LTYPE", d.linetypes.len()); + for lt in &d.linetypes { + let h = record_head(w, "LTYPE", &th, "AcDbLinetypeTableRecord"); + ltype_handles.entry(lt.name.to_ascii_uppercase()).or_insert(h.clone()); + cx.ltypes.entry(lt.name.to_ascii_uppercase()).or_insert(h); + w.s(2, <.name); + w.i(70, if crate::dxf_xref::dependent(<.name) { 48 } else { 0 }); + w.s(3, <.description); + w.i(72, 65); + w.i(73, lt.pattern.len() as i64); + w.f(40, lt.pattern_length()); + for el in <.pattern { + w.f(49, el.length); + // 74: 2 = text, 4 = shape, + 1 when the rotation is absolute (DXF Reference, LTYPE). + let (kind, shape) = match (&el.text, el.shape) { + (Some(_), _) => (2, 0), + (None, Some(n)) => (4, n), + (None, None) => (0, 0), + }; + if kind == 0 { + w.i(74, 0); + continue; + } + w.i(74, kind | i64::from(el.absolute)); + w.i(75, i64::from(shape)); + ltype_style_refs.push((w.t.len(), el.style.clone(), kind == 4)); + w.s(340, "0"); + w.f(46, el.scale); + w.f(50, el.rotation); + w.f(44, el.offset.x); + w.f(45, el.offset.y); + if let Some(t) = &el.text { + w.s(9, t.clone()); + } + } + } + w.s(0, "ENDTAB"); + write_layer_table(w, d); + // STYLE. Linetype shapes from a shape file that isn't a text style get an unnamed shape + // style record (70 bit 1) naming the file. + let mut shape_files: Vec = Vec::new(); + for (_, style, shape) in <ype_style_refs { + let Some(f) = style.as_deref().map(str::trim).filter(|f| *shape && !f.is_empty() && d.text_style(f).is_none()) else { continue }; + if !shape_files.iter().any(|x| x.eq_ignore_ascii_case(f)) { + shape_files.push(f.to_string()); + } + } + let th = table_head(w, "STYLE", d.text_styles.len() + shape_files.len()); + for s in &d.text_styles { + let h = record_head(w, "STYLE", &th, "AcDbTextStyleTableRecord"); + cx.styles.entry(s.name.to_ascii_uppercase()).or_insert(h); + w.s(2, &s.name); + // 70 bit 4: vertical; 71 bits 2/4: backwards / upside down. + w.i(70, if s.vertical { 4 } else { 0 }); + w.f(40, s.height); + w.f(41, s.width_factor); + w.f(50, s.oblique.to_degrees()); + w.i(71, if s.backwards { 2 } else { 0 } | if s.upside_down { 4 } else { 0 }); + w.f(42, 0.2); + // The built-in font goes out as `txt`, which every reader has; CADCraft's own name + // travels in `CADCRAFT` xdata (`1000 FONT`, name) so the style reopens with it. + let file = if s.font == cadcraft_fonts_name() { "txt" } else { s.font.as_str() }; + w.s(3, file); + w.s(4, &s.big_font); + if s.annotative { + annotative_xdata(w); + } + if file != s.font { + w.s(1001, dxf_ext::APP); + w.s(1000, "FONT"); + w.s(1000, dxf_ext::xdata_str(&s.font)); + } + } + let mut shape_handles: HashMap = HashMap::new(); + for f in &shape_files { + let h = record_head(w, "STYLE", &th, "AcDbTextStyleTableRecord"); + shape_handles.insert(f.to_ascii_uppercase(), h); + w.s(2, ""); + w.i(70, 1); + w.f(40, 0.0); + w.f(41, 1.0); + w.f(50, 0.0); + w.i(71, 0); + w.f(42, 0.2); + w.s(3, f.clone()); + w.s(4, ""); + } + w.s(0, "ENDTAB"); + // Linetype text and shapes without a known style use Standard (else the first style). + let fallback_style = cx.style("Standard").or_else(|| d.text_styles.first().and_then(|s| cx.style(&s.name))).unwrap_or_else(|| "0".into()); + for (pos, style, shape) in ltype_style_refs { + let name = style.as_deref().map(str::trim).unwrap_or_default(); + let h = shape.then(|| shape_handles.get(&name.to_ascii_uppercase()).cloned()).flatten().or_else(|| cx.style(name)); + if let Some(t) = w.t.get_mut(pos) { + *t = Tag::s(340, h.unwrap_or_else(|| fallback_style.clone())); + } + } + // VIEW: named views (DXF Reference, VIEW). A view's layer state is CADCraft xdata. + let th = table_head(w, "VIEW", d.views.len()); + for v in &d.views { + record_head(w, "VIEW", &th, "AcDbViewTableRecord"); + w.s(2, &v.name); + w.i(70, i64::from(v.layout.is_some())); + w.f(40, v.height); + w.p2(10, v.center); + w.f(41, v.width); + w.p(11, Vec3::Z); + w.p(12, Vec3::ZERO); + w.f(42, 50.0); + w.f(43, 0.0); + w.f(44, 0.0); + w.f(50, if v.twist.is_finite() { v.twist.to_degrees() } else { 0.0 }); + w.i(71, 0); + w.i(281, 0); + w.i(72, 0); + let xdata = [("LAYERSTATE", v.layer_state.as_deref()), ("LAYOUT", v.layout.as_deref().filter(|l| !l.is_empty()))]; + if xdata.iter().any(|(_, x)| x.is_some()) { + w.s(1001, dxf_ext::APP); + for (key, value) in xdata { + if let Some(value) = value { + w.s(1000, key); + w.s(1000, dxf_ext::xdata_str(value)); + } + } + } + } + w.s(0, "ENDTAB"); + // UCS: named user coordinate systems (DXF Reference, UCS). + let th = table_head(w, "UCS", d.ucss.len()); + for u in &d.ucss { + record_head(w, "UCS", &th, "AcDbUCSTableRecord"); + w.s(2, &u.name); + w.i(70, 0); + w.p(10, u.origin); + w.p(11, u.x_axis); + w.p(12, u.y_axis); + w.i(79, 0); + w.f(146, 0.0); + } + w.s(0, "ENDTAB"); + let apps = ["ACAD", dxf_ext::APP, "AcadAnnotative", dxf_ext::LAYER_TRANSPARENCY_APP, dxf_ext::LAYER_DESCRIPTION_APP]; + let th = table_head(w, "APPID", apps.len()); + for app in apps { + record_head(w, "APPID", &th, "AcDbRegAppTableRecord"); + w.s(2, app); + w.i(70, 0); + } + w.s(0, "ENDTAB"); + write_dimstyles(w, d, cx, version); + ltype_handles +} + +fn write_layer_table(w: &mut W, d: &Drawing) { + let th = table_head(w, "LAYER", d.layers.len()); + for l in &d.layers { + let lh = record_head(w, "LAYER", &th, "AcDbLayerTableRecord"); + w.layers.insert(l.name.to_ascii_uppercase(), lh); + w.s(2, &l.name); + // 16 | 32: an xref's layer (`xref|name`), externally dependent and resolved. + let xref = if crate::dxf_xref::dependent(&l.name) { 48 } else { 0 }; + w.i(70, i64::from(l.frozen) | if l.vp_freeze_new { 2 } else { 0 } | if l.locked { 4 } else { 0 } | xref); + let aci = match l.color { + Color::True(rgb) => i64::from(cadcraft_color::nearest_aci(rgb)), + c => i64::from(c.to_aci()).clamp(1, 255), + }; + w.i(62, if l.on { aci } else { -aci }); + if let Color::True(rgb) = l.color { + w.i(420, i64::from(rgb.to_u32())); + } + w.s(6, &l.linetype); + if !l.plot { + w.i(290, 0); + } + w.i(370, i64::from(l.lineweight.to_dxf())); + let normal = w.pstyles.get(&cadcraft_doc::NORMAL_STYLE.to_ascii_uppercase()).cloned(); + if let Some(h) = w.pstyles.get(&l.plot_style.trim().to_ascii_uppercase()).cloned().or(normal) { + w.s(390, h); + } + if l.transparency > 0 { + w.s(1001, dxf_ext::LAYER_TRANSPARENCY_APP); + w.i(1071, dxf_ext::transparency_to_dxf(l.transparency)); + } + if !l.description.is_empty() { + // The first string is the layer standard's name, unused here. + w.s(1001, dxf_ext::LAYER_DESCRIPTION_APP); + w.s(1000, ""); + w.s(1000, dxf_ext::xdata_str(&l.description)); + } + } + w.s(0, "ENDTAB"); +} + +fn write_dimstyles(w: &mut W, d: &Drawing, cx: &mut Ctx, version: Version) { + // DIMSTYLE + let th = { + let h = w.h(); + w.s(0, "TABLE"); + w.s(2, "DIMSTYLE"); + w.s(5, h.clone()); + w.s(330, "0"); + w.s(100, "AcDbSymbolTable"); + w.i(70, d.dim_styles.len() as i64); + w.s(100, "AcDbDimStyleTable"); + w.i(71, 0); + h + }; + for s in &d.dim_styles { + record_head(w, "DIMSTYLE", &th, "AcDbDimStyleTableRecord"); + w.s(2, &s.name); + w.i(70, 0); + let v = serde_json::to_value(s).unwrap_or_default(); + let defaults = serde_json::to_value(DimStyle::default()).unwrap_or_default(); + let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() }; + for (field, code, kind) in dxf_ext::DIM_CODES { + let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue }; + // DIMPOST is written even when empty: a reader that defaults a missing one to "<>" + // (the DWG bridge's) would otherwise give radius and diameter dimensions no "R" or + // "Ø" in AutoCAD (#1053). + if *code == dxf_ext::DIMPOST + && let DimVal::Str(t) = &enc + && t.is_empty() + { + w.s(*code, ""); + continue; + } + if !dxf_ext::NEWER_CODES.contains(code) { + w.dimval(*code, enc, false); + continue; + } + // Table groups where the file version has them, else xdata. DIMTFILL and DIMTFILLCLR + // are written only when set, as other writers do, so a style without a text + // background has a single 70 (its flags). + if version >= dxf_ext::newer_since(*code) { + if matches!(*code, 69 | 70) && v.get(*field) == defaults.get(*field) { + continue; + } + w.dimval(*code, enc, false); + } else if v.get(*field) != defaults.get(*field) { + newer.i(1070, i64::from(*code)); + newer.dimval(*code, enc, true); + } + } + if !s.arrow_block1.trim().is_empty() || !s.arrow_block2.trim().is_empty() { + w.i(dxf_ext::DIMSAH, 1); + } + if s.annotative { + annotative_xdata(w); + } + if !newer.t.is_empty() { + w.s(1001, dxf_ext::APP); + w.s(1000, dxf_ext::DIMVARS_LIST); + w.s(1002, "{"); + w.xdata(newer.t); + w.s(1002, "}"); + } + } + w.s(0, "ENDTAB"); +} + +fn write_header(w: &mut W, d: &Drawing, version: Version) -> usize { + w.s(0, "SECTION"); + w.s(2, "HEADER"); + w.s(9, "$ACADVER"); + w.s(1, version.code()); + w.s(9, "$DWGCODEPAGE"); + w.s(3, code_page(d, version)); + let ext = d.extents(&Space::Model); + if !ext.is_empty() { + w.s(9, "$EXTMIN"); + w.p(10, ext.min.to3(0.0)); + w.s(9, "$EXTMAX"); + w.p(10, ext.max.to3(0.0)); + } + header_vars(w, d); + let seed_pos = w.t.len(); + w.s(9, "$HANDSEED"); + w.s(5, "0"); + w.s(0, "ENDSEC"); + + seed_pos +} + +fn write_classes(w: &mut W, cx: &Ctx, every: &[&Entity]) { + w.s(0, "SECTION"); + w.s(2, "CLASSES"); + let mut classes = vec![ + ("TABLESTYLE", "AcDbTableStyle", 4095, false), + ("MLEADERSTYLE", "AcDbMLeaderStyle", 4095, false), + ("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", 0, false), + ("ACDBPLACEHOLDER", "AcDbPlaceHolder", 0, false), + ]; + if !cx.tables.is_empty() { + classes.push(("ACAD_TABLE", "AcDbTable", 1025, true)); + } + if every.iter().any(|e| matches!(e.kind, EntityKind::Helix(_))) { + classes.push(("HELIX", "AcDbHelix", 4095, true)); + } + if !cx.assoc.is_empty() { + classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false)); + } + if cx.native_mleader() && every.iter().any(|e| matches!(e.kind, EntityKind::MLeader(_))) { + classes.push(("MULTILEADER", "AcDbMLeader", 1025, true)); + } + if every.iter().any(|e| matches!(e.kind, EntityKind::Wipeout(_))) { + classes.push(("WIPEOUT", "AcDbWipeout", 127, true)); + } + if !cx.images.is_empty() { + classes.push(("IMAGE", "AcDbRasterImage", 127, true)); + classes.push(("IMAGEDEF", "AcDbRasterImageDef", 0, false)); + classes.push(("IMAGEDEF_REACTOR", "AcDbRasterImageDefReactor", 1, false)); + classes.push(("RASTERVARIABLES", "AcDbRasterVariables", 0, false)); + } + classes.push(("SCALE", "AcDbScale", 0, false)); + for (dxf_name, cpp, proxy, is_entity) in classes { + w.s(0, "CLASS"); + w.s(1, dxf_name); + w.s(2, cpp); + w.s(3, "ObjectDBX Classes"); + w.i(90, proxy); + w.i(280, 0); + w.i(281, i64::from(is_entity)); + } + w.s(0, "ENDSEC"); +} + +fn write_entities(w: &mut W, d: &Drawing, ms_br: &str, ps_brs: &[(String, String, &Layout)], cx: &Ctx) { + w.s(0, "SECTION"); + w.s(2, "ENTITIES"); + for e in d.model.iter() { + entity(w, d, e, ms_br, false, cx); + } + if let Some((_, h, l)) = ps_brs.first() { + for e in sheet_first(l) { + entity(w, d, e, h, true, cx); + } + } + w.s(0, "ENDSEC"); +} + +struct LayoutWriteContext<'a> { + drawing: &'a Drawing, + dictionary: &'a str, +} + +struct LayoutWriteData<'a> { + handle: &'a str, + name: &'a str, + tab: i64, + block_record: &'a str, + flags: i64, + page: &'a PageSetup, + view: Option<(Vec2, f64)>, +} + +fn layout_obj(w: &mut W, context: LayoutWriteContext<'_>, data: LayoutWriteData<'_>) { + let d = context.drawing; + let layout_dict = context.dictionary; + let h = data.handle; + let name = data.name; + let tab = data.tab; + let brh = data.block_record; + let flags = data.flags; + let page = data.page; + let view = data.view; + // Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units + // per 143 drawing units). + let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 }; + let standard = page.scale_to_fit || scale == 1.0; + let plot_flags = 512 + | 32 + | if standard { 16 } else { 0 } + | if page.lineweights { 128 } else { 0 } + | if page.center { 4 } else { 0 } + | if page.show_plot_styles { 2 } else { 0 }; + let plot_type = match page.plot_area.as_str() { + "display" => 0, + "extents" => 1, + "limits" => 2, + "window" => 4, + _ => 5, + }; + w.s(0, "LAYOUT"); + w.s(5, h); + w.s(330, layout_dict.to_string()); + w.s(100, "AcDbPlotSettings"); + w.s(1, ""); + let device = page.device.trim(); + w.s(2, if device.is_empty() || device.eq_ignore_ascii_case("None") { "none_device" } else { device }); + w.s(4, page.paper.replace(' ', "_")); + w.s(6, ""); + for (c, v) in [ + (40, page.margins_mm[0]), + (41, page.margins_mm[1]), + (42, page.margins_mm[2]), + (43, page.margins_mm[3]), + (44, page.width_mm), + (45, page.height_mm), + (46, 0.0), + (47, 0.0), + (48, page.plot_window[0]), + (49, page.plot_window[1]), + (140, page.plot_window[2]), + (141, page.plot_window[3]), + (142, scale), + (143, 1.0), + ] { + w.f(c, v); + } + w.i(70, plot_flags); + // Plot paper units: 1 millimetres, 0 inches (#1058). + w.i(72, i64::from(cadcraft_render::paper::page_unit_mm(d, page) == 1.0)); + w.i(73, i64::from(page.landscape)); + w.i(74, plot_type); + w.s(7, &page.plot_style_table); + w.i(75, if page.scale_to_fit { 0 } else { 16 }); + w.f(147, scale); + w.f(148, 0.0); + w.f(149, 0.0); + w.s(100, "AcDbLayout"); + w.s(1, name); + w.i(70, flags); + w.i(71, tab); + w.p2(10, Vec2::ZERO); + w.p2(11, Vec2::new(12.0, 9.0)); + w.p(12, Vec3::ZERO); + w.p(14, Vec3::ZERO); + w.p(15, Vec3::ZERO); + w.f(146, 0.0); + w.p(13, Vec3::ZERO); + w.p(16, Vec3::new(1.0, 0.0, 0.0)); + w.p(17, Vec3::new(0.0, 1.0, 0.0)); + w.i(76, 0); + w.s(330, brh); + // The saved paper-space view (centre, height) as CADCraft xdata. + if let Some((c, height)) = view.filter(|(c, v)| c.x.is_finite() && c.y.is_finite() && v.is_finite()) { + w.s(1001, dxf_ext::APP); + w.s(1000, "PSVIEW"); + w.s(1002, "{"); + w.p(1010, c.to3(0.0)); + w.f(1040, height); + w.s(1002, "}"); + } +} + +struct ObjectWritePlan<'a> { + d: &'a Drawing, + cx: &'a Ctx, + root_dict: &'a String, + group_dict: &'a String, + layout_dict: &'a String, + table_style_dict: &'a String, + pstyle_dict: &'a String, + scale_dict: &'a String, + model_layout: &'a String, + ms_br: &'a String, + ps_brs: &'a [(String, String, &'a Layout)], + layout_handles: &'a [String], + xrecords: &'a [(&'static str, String, Vec)], + scales: &'a [(&'a cadcraft_doc::AnnoScale, String)], + pstyles: &'a [(String, String)], + table_styles: &'a [TableStyle], + mleader_styles: &'a [MLeaderStyle], + ltype_handles: &'a HashMap, +} + +fn write_layout_objects(w: &mut W, plan: &ObjectWritePlan<'_>) { + let d = plan.d; + let root_dict = plan.root_dict; + let layout_dict = plan.layout_dict; + let model_layout = plan.model_layout; + let ms_br = plan.ms_br; + let ps_brs = plan.ps_brs; + let layout_handles = plan.layout_handles; + w.s(0, "DICTIONARY"); + w.s(5, layout_dict.clone()); + w.s(330, root_dict.clone()); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + w.s(3, "Model"); + w.s(350, model_layout.clone()); + for (i, (_, _, l)) in ps_brs.iter().enumerate() { + w.s(3, &l.name); + w.s(350, layout_handles.get(i).cloned().unwrap_or_default()); + } + // Model space has no page setup of its own: write what PLOT does for it, the drawing extents + // fitted to the paper (plot type 1, scaled to fit). The default page's "layout" area is + // plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means + // nothing for model space: another program's Plot dialog then shows no plot area or scale. + let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() }; + layout_obj( + w, + LayoutWriteContext { drawing: d, dictionary: layout_dict }, + LayoutWriteData { handle: model_layout, name: "Model", tab: 0, block_record: ms_br, flags: 1, page: &model_page, view: None }, + ); + for (i, (_, brh, l)) in ps_brs.iter().enumerate() { + let h = layout_handles.get(i).cloned().unwrap_or_default(); + layout_obj( + w, + LayoutWriteContext { drawing: d, dictionary: layout_dict }, + LayoutWriteData { + handle: &h, + name: &l.name, + tab: i64::from(l.tab_order.max(1)), + block_record: brh, + flags: 1, + page: &l.page, + view: l.view, + }, + ); + } +} + +fn write_root_group_objects( + w: &mut W, + plan: &ObjectWritePlan<'_>, + groups: &[(&Group, String)], + group_owner: &str, + written: &std::collections::HashSet, + mleader_dict: &str, +) { + let cx = plan.cx; + let root_dict = plan.root_dict; + let group_dict = plan.group_dict; + let layout_dict = plan.layout_dict; + let table_style_dict = plan.table_style_dict; + let pstyle_dict = plan.pstyle_dict; + let scale_dict = plan.scale_dict; + let xrecords = plan.xrecords; + w.s(0, "SECTION"); + w.s(2, "OBJECTS"); + w.s(0, "DICTIONARY"); + w.s(5, root_dict.clone()); + w.s(330, "0"); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + w.s(3, "ACAD_GROUP"); + w.s(350, group_dict.clone()); + if !cx.images.is_empty() { + w.s(3, "ACAD_IMAGE_DICT"); + w.s(350, cx.images.dict.clone()); + w.s(3, "ACAD_IMAGE_VARS"); + w.s(350, cx.images.vars.clone()); + } + w.s(3, "ACAD_LAYOUT"); + w.s(350, layout_dict.clone()); + w.s(3, "ACAD_MLEADERSTYLE"); + w.s(350, mleader_dict.to_string()); + w.s(3, "ACAD_MLINESTYLE"); + w.s(350, cx.mlines.dict.clone()); + w.s(3, dxf_ext::PLOT_STYLE_NAMES_KEY); + w.s(350, pstyle_dict.clone()); + w.s(3, "ACAD_TABLESTYLE"); + w.s(350, table_style_dict.clone()); + for (key, h, _) in xrecords { + w.s(3, *key); + w.s(350, h); + } + w.s(3, "ACAD_SCALELIST"); + w.s(350, scale_dict.clone()); + w.s(0, "DICTIONARY"); + w.s(5, group_dict); + w.s(330, root_dict.clone()); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + for (g, h) in groups { + w.s(3, &g.name); + w.s(350, h); + } + for (g, h) in groups { + group_obj(w, g, h, group_owner, written); + } +} + +fn write_style_objects(w: &mut W, plan: &ObjectWritePlan<'_>, mleader_dict: &str, mleader_handles: &[String]) { + let d = plan.d; + let cx = plan.cx; + let root_dict = plan.root_dict; + let table_style_dict = plan.table_style_dict; + let mleader_styles = plan.mleader_styles; + let table_styles = plan.table_styles; + let ltype_handles = plan.ltype_handles; + // Multileader styles. + w.s(0, "DICTIONARY"); + w.s(5, mleader_dict.to_string()); + w.s(330, root_dict.clone()); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + for (s, h) in mleader_styles.iter().zip(mleader_handles) { + w.s(3, &s.name); + w.s(350, h); + } + let byblock_ltype = ltype_handles.get("BYBLOCK").cloned(); + for (s, h) in mleader_styles.iter().zip(mleader_handles) { + mleader_style_obj(w, s, h, mleader_dict, byblock_ltype.as_deref(), cx.style(&s.text_style)); + } + // Multiline styles. + w.t.extend(crate::dxf_mline::objects(&cx.mlines, root_dict)); + // Table styles. + w.s(0, "DICTIONARY"); + w.s(5, table_style_dict.clone()); + w.s(330, root_dict.clone()); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + for (name, h) in &cx.table_styles { + w.s(3, name); + w.s(350, h); + } + for (s, (_, h)) in table_styles.iter().zip(&cx.table_styles) { + table_style_obj(w, d, s, h, table_style_dict); + } +} + +fn write_named_objects(w: &mut W, plan: &ObjectWritePlan<'_>) { + let root_dict = plan.root_dict; + let pstyle_dict = plan.pstyle_dict; + let xrecords = plan.xrecords; + let pstyles = plan.pstyles; + let scales = plan.scales; + let scale_dict = plan.scale_dict; + // Plot style names: a dictionary with a default (Normal) of placeholder objects. + w.s(0, "ACDBDICTIONARYWDFLT"); + w.s(5, pstyle_dict.clone()); + w.group("ACAD_REACTORS", 330, &[root_dict]); + w.s(330, root_dict.clone()); + w.s(100, "AcDbDictionary"); + w.i(281, 1); + for (n, h) in pstyles { + w.s(3, n); + w.s(350, h); + } + w.s(100, "AcDbDictionaryWithDefault"); + w.s(340, pstyles.first().map(|(_, h)| h.clone()).unwrap_or_default()); + for (_, h) in pstyles { + w.s(0, "ACDBPLACEHOLDER"); + w.s(5, h); + w.group("ACAD_REACTORS", 330, &[pstyle_dict]); + w.s(330, pstyle_dict.clone()); + } + // Parametric constraints and parameters, saved layer states, plot style tables (CADCraft + // data). + for (_, h, chunks) in xrecords { + w.s(0, "XRECORD"); + w.s(5, h); + w.group("ACAD_REACTORS", 330, &[root_dict]); + w.s(330, root_dict.clone()); + w.s(100, "AcDbXrecord"); + w.i(280, 1); + for c in chunks { + w.s(1, c.clone()); + } + } + scale_list(w, scales, scale_dict, root_dict); +} + +fn write_associative_objects(w: &mut W, cx: &Ctx) { + // Dimension associativity: extension dictionary + DIMASSOC per dimension. + let mut assoc: Vec<(&Handle, &AssocObj)> = cx.assoc.iter().collect(); + assoc.sort_by_key(|(h, _)| **h); + for (dim, a) in assoc { + let dh = dim.hex(); + w.s(0, "DICTIONARY"); + w.s(5, a.xdict.clone()); + w.group("ACAD_REACTORS", 330, &[&dh]); + w.s(330, dh.clone()); + w.s(100, "AcDbDictionary"); + w.i(280, 1); + w.i(281, 1); + w.s(3, "ACAD_DIMASSOC"); + w.s(360, a.handle.clone()); + w.s(0, "DIMASSOC"); + w.s(5, a.handle.clone()); + w.group("ACAD_REACTORS", 330, &[&a.xdict]); + w.s(330, a.xdict.clone()); + w.s(100, "AcDbDimAssoc"); + w.s(330, dh); + w.i(90, a.flags); + w.i(70, 0); + w.i(71, 0); + for r in &a.refs { + w.s(1, "AcDbOsnapPointRef"); + w.i(72, r.osnap); + w.s(331, r.main.hex()); + w.i(73, if r.segment > 0 { 2 } else { 0 }); + w.i(91, r.segment); + w.s(301, ""); + w.f(40, r.param); + w.p(10, r.point); + if let Some(o) = r.other { + w.s(332, o.hex()); + w.i(74, 0); + w.i(92, 0); + w.s(302, ""); + } + w.i(75, 0); + } + } +} + +fn write_objects(w: &mut W, plan: ObjectWritePlan<'_>) { + // Groups (members that still exist) and multileader styles. + let group_owner = plan.group_dict.clone(); + let written: std::collections::HashSet = w.t.iter().filter(|t| t.code == 5).map(Tag::str).collect(); + let groups: Vec<(&Group, String)> = plan.d.groups.iter().filter(|g| !g.name.is_empty()).map(|g| (g, w.h())).collect(); + let mleader_dict = w.h(); + let mleader_handles: Vec = plan.cx.mleader_styles.iter().map(|(_, h)| h.clone()).collect(); + write_root_group_objects(w, &plan, &groups, &group_owner, &written, &mleader_dict); + write_style_objects(w, &plan, &mleader_dict, &mleader_handles); + write_named_objects(w, &plan); + write_associative_objects(w, plan.cx); + write_layout_objects(w, &plan); + // Raster image definitions. + w.t.extend(crate::dxf_image::objects(&plan.cx.images, plan.root_dict)); + w.s(0, "ENDSEC"); +} + +fn dimension_block_definitions( + d: &Drawing, + all_spaces: &[&EntityStore], + dim_plan: &super::blocks::DimBlockPlan, + w: &mut W, + cx: &mut Ctx, +) -> Vec<(String, String, Vec)> { + let mut dim_defs = Vec::new(); + let mut n = 1; + let mut reused_handles = std::collections::HashSet::new(); + for st in all_spaces { + for e in st.iter() { + if let EntityKind::Dimension(dm) = &e.kind { + let reused = dim_plan.reuse.get(&e.handle); + let ents: Vec; + let name = match reused { + // A dimension still linked to the block it was read with hasn't changed since + // (edits drop the link, and the screen draws that block): the block is written + // as read, so other programs show it as they drew it. + Some(b) if !b.entities.is_empty() => { + ents = b.entities.iter().map(|x| x.as_ref().clone()).collect(); + reused_handles.extend(ents.iter().map(|x| x.handle).filter(|h| h.0 != 0)); + b.name.clone() + } + Some(b) => { + let mut fresh = dim_block_entities(d, dm, &e.common.layer); + reuse_handles(&mut fresh, b, &mut reused_handles); + ents = fresh; + b.name.clone() + } + None => { + ents = dim_block_entities(d, dm, &e.common.layer); + let mut name = format!("*D{n}"); + while d.block(&name).is_some() { + n += 1; + name = format!("*D{n}"); + } + n += 1; + name + } + }; + let brh = w.h(); + cx.dim_blocks.insert(e.handle, name.clone()); + dim_defs.push((name, brh, ents)); + } + } + } + dim_defs +} + +fn arrow_block_definitions( + d: &Drawing, + every: &[&Entity], + user_blocks: &[(String, String, &Block)], + w: &mut W, + cx: &mut Ctx, +) -> Vec<(String, String, Vec)> { + let mut arrow_names: Vec = Vec::new(); + for s in &d.dim_styles { + arrow_names.extend([s.arrow_block.clone(), s.arrow_block1.clone(), s.arrow_block2.clone(), s.leader_arrow_block.clone()]); + } + for e in every { + if let EntityKind::Dimension(dm) = &e.kind { + for (k, v) in &dm.overrides { + let Some(f) = DimStyle::field_name(k).filter(|f| f.starts_with("arrowBlock") || *f == "leaderArrowBlock") else { continue }; + if let Some(s) = dxf_ext::canonical(f, v).as_ref().and_then(|v| v.as_str()) { + arrow_names.push(s.to_string()); + } + } + } + } + let mut arrow_defs: Vec<(String, String, Vec)> = Vec::new(); + for name in arrow_names { + let key = name.trim().to_ascii_uppercase(); + if key.is_empty() || cx.arrows.contains_key(&key) { + continue; + } + if let Some((_, h, _)) = user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())) { + cx.arrows.insert(key, h.clone()); + continue; + } + let Some((bname, kind)) = dxf_ext::arrow_block_name(&name) else { continue }; + let existing = arrow_defs.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone()); + let existing = existing.or_else(|| user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone())); + let h = match existing { + Some(h) => h, + None => { + let h = w.h(); + arrow_defs.push((bname, h.clone(), arrow_block_entities(kind))); + h + } + }; + cx.arrows.insert(key, h); + } + + arrow_defs +} + +fn table_block_definitions(d: &Drawing, every: &[&Entity], w: &mut W, cx: &mut Ctx) -> Vec<(String, String, Vec)> { + let mut table_defs = Vec::new(); + let mut tn = 1; + for e in every { + if let EntityKind::Table(t) = &e.kind { + if cx.tables.contains_key(&e.handle) { + continue; + } + let mut name = format!("*T{tn}"); + while d.block(&name).is_some() { + tn += 1; + name = format!("*T{tn}"); + } + tn += 1; + let brh = w.h(); + cx.tables.insert(e.handle, (name.clone(), brh.clone())); + table_defs.push((name, brh, table_block_entities(d, t, &e.common.layer))); + } + } + + table_defs +} + +fn plan_dimension_associations(d: &Drawing, all_spaces: &[&EntityStore], w: &mut W, cx: &mut Ctx) { + for st in all_spaces { + for e in st.iter() { + if let EntityKind::Dimension(dm) = &e.kind { + let (flags, refs) = std_assoc_refs(d, dm); + if !refs.is_empty() { + cx.assoc.insert(e.handle, AssocObj { xdict: w.h(), handle: w.h(), flags, refs }); + } + } + } + } +} /// The drawing's DXF tags, declaring `version` in `$ACADVER`, with what `target` (the version the /// file ends up in: `version`, or the DWG version the bridge writes) has. @@ -45,125 +917,20 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec = user_blocks.into_iter().map(|(n, _, b)| (n, w.h(), b)).collect(); let mut cx = Ctx { paper_views: paper_views(d), target, ..Ctx::default() }; cx.block_records = user_blocks.iter().map(|(n, h, _)| (n.to_ascii_uppercase(), h.clone())).collect(); - // Dimension blocks. - let mut dim_defs: Vec<(String, String, Vec)> = Vec::new(); // filled below - let mut n = 1; - let mut reused_handles = std::collections::HashSet::new(); let all_spaces: Vec<&EntityStore> = std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).collect(); - for st in &all_spaces { - for e in st.iter() { - if let EntityKind::Dimension(dm) = &e.kind { - let reused = dim_plan.reuse.get(&e.handle); - let ents: Vec; - let name = match reused { - // A dimension still linked to the block it was read with hasn't changed since - // (edits drop the link, and the screen draws that block): the block is written - // as read, so other programs show it as they drew it. - Some(b) if !b.entities.is_empty() => { - ents = b.entities.iter().map(|x| x.as_ref().clone()).collect(); - reused_handles.extend(ents.iter().map(|x| x.handle).filter(|h| h.0 != 0)); - b.name.clone() - } - Some(b) => { - let mut fresh = dim_block_entities(d, dm, &e.common.layer); - reuse_handles(&mut fresh, b, &mut reused_handles); - ents = fresh; - b.name.clone() - } - None => { - ents = dim_block_entities(d, dm, &e.common.layer); - let mut name = format!("*D{n}"); - while d.block(&name).is_some() { - n += 1; - name = format!("*D{n}"); - } - n += 1; - name - } - }; - let brh = w.h(); - cx.dim_blocks.insert(e.handle, name.clone()); - dim_defs.push((name, brh, ents)); - } - } - } + let mut dim_defs = dimension_block_definitions(d, &all_spaces, &dim_plan, &mut w, &mut cx); // Every entity, including block contents (for arrows, tables and overrides). let every: Vec<&Entity> = all_spaces .iter() .flat_map(|s| s.iter().map(|e| e.as_ref())) .chain(d.blocks.values().flat_map(|b| b.entities.iter().map(|e| e.as_ref()))) .collect(); - // Arrowhead blocks: user blocks by name, otherwise generated from our own geometry. - let mut arrow_names: Vec = Vec::new(); - for s in &d.dim_styles { - arrow_names.extend([s.arrow_block.clone(), s.arrow_block1.clone(), s.arrow_block2.clone(), s.leader_arrow_block.clone()]); - } - for e in &every { - if let EntityKind::Dimension(dm) = &e.kind { - for (k, v) in &dm.overrides { - let Some(f) = DimStyle::field_name(k).filter(|f| f.starts_with("arrowBlock") || *f == "leaderArrowBlock") else { continue }; - if let Some(s) = dxf_ext::canonical(f, v).as_ref().and_then(|v| v.as_str()) { - arrow_names.push(s.to_string()); - } - } - } - } - let mut arrow_defs: Vec<(String, String, Vec)> = Vec::new(); - for name in arrow_names { - let key = name.trim().to_ascii_uppercase(); - if key.is_empty() || cx.arrows.contains_key(&key) { - continue; - } - if let Some((_, h, _)) = user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())) { - cx.arrows.insert(key, h.clone()); - continue; - } - let Some((bname, kind)) = dxf_ext::arrow_block_name(&name) else { continue }; - let existing = arrow_defs.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone()); - let existing = existing.or_else(|| user_blocks.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(&bname)).map(|(_, h, _)| h.clone())); - let h = match existing { - Some(h) => h, - None => { - let h = w.h(); - arrow_defs.push((bname, h.clone(), arrow_block_entities(kind))); - h - } - }; - cx.arrows.insert(key, h); - } - // Table blocks (`*T`). - let mut table_defs: Vec<(String, String, Vec)> = Vec::new(); - let mut tn = 1; - for e in &every { - if let EntityKind::Table(t) = &e.kind { - if cx.tables.contains_key(&e.handle) { - continue; - } - let mut name = format!("*T{tn}"); - while d.block(&name).is_some() { - tn += 1; - name = format!("*T{tn}"); - } - tn += 1; - let brh = w.h(); - cx.tables.insert(e.handle, (name.clone(), brh.clone())); - table_defs.push((name, brh, table_block_entities(d, t, &e.common.layer))); - } - } + let mut arrow_defs = arrow_block_definitions(d, &every, &user_blocks, &mut w, &mut cx); + let mut table_defs = table_block_definitions(d, &every, &mut w, &mut cx); // Image definitions (IMAGEDEF objects) of raster images anywhere in the drawing. cx.images = crate::dxf_image::plan(every.iter().copied(), || w.h()); cx.mlines = crate::dxf_mline::plan(d, every.iter().copied(), || w.h()); - // DIMASSOC objects of associative dimensions in model and paper space. - for st in &all_spaces { - for e in st.iter() { - if let EntityKind::Dimension(dm) = &e.kind { - let (flags, refs) = std_assoc_refs(d, dm); - if !refs.is_empty() { - cx.assoc.insert(e.handle, AssocObj { xdict: w.h(), handle: w.h(), flags, refs }); - } - } - } - } + plan_dimension_associations(d, &all_spaces, &mut w, &mut cx); let default_table_style = [TableStyle::default()]; let table_styles: &[TableStyle] = if d.table_styles.is_empty() { &default_table_style } else { &d.table_styles }; cx.table_styles = table_styles.iter().map(|s| (s.name.clone(), w.h())).collect(); @@ -213,325 +980,12 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec = ps_brs.iter().map(|_| w.h()).collect(); - // ---------------- HEADER ---------------- - w.s(0, "SECTION"); - w.s(2, "HEADER"); - w.s(9, "$ACADVER"); - w.s(1, version.code()); - w.s(9, "$DWGCODEPAGE"); - w.s(3, code_page(d, version)); - let ext = d.extents(&Space::Model); - if !ext.is_empty() { - w.s(9, "$EXTMIN"); - w.p(10, ext.min.to3(0.0)); - w.s(9, "$EXTMAX"); - w.p(10, ext.max.to3(0.0)); - } - header_vars(&mut w, d); - let seed_pos = w.t.len(); - w.s(9, "$HANDSEED"); - w.s(5, "0"); - w.s(0, "ENDSEC"); + let seed_pos = write_header(&mut w, d, version); - // ---------------- CLASSES ---------------- - w.s(0, "SECTION"); - w.s(2, "CLASSES"); - let mut classes = vec![ - ("TABLESTYLE", "AcDbTableStyle", 4095, false), - ("MLEADERSTYLE", "AcDbMLeaderStyle", 4095, false), - ("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", 0, false), - ("ACDBPLACEHOLDER", "AcDbPlaceHolder", 0, false), - ]; - if !cx.tables.is_empty() { - classes.push(("ACAD_TABLE", "AcDbTable", 1025, true)); - } - if every.iter().any(|e| matches!(e.kind, EntityKind::Helix(_))) { - classes.push(("HELIX", "AcDbHelix", 4095, true)); - } - if !cx.assoc.is_empty() { - classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false)); - } - if cx.native_mleader() && every.iter().any(|e| matches!(e.kind, EntityKind::MLeader(_))) { - classes.push(("MULTILEADER", "AcDbMLeader", 1025, true)); - } - if every.iter().any(|e| matches!(e.kind, EntityKind::Wipeout(_))) { - classes.push(("WIPEOUT", "AcDbWipeout", 127, true)); - } - if !cx.images.is_empty() { - classes.push(("IMAGE", "AcDbRasterImage", 127, true)); - classes.push(("IMAGEDEF", "AcDbRasterImageDef", 0, false)); - classes.push(("IMAGEDEF_REACTOR", "AcDbRasterImageDefReactor", 1, false)); - classes.push(("RASTERVARIABLES", "AcDbRasterVariables", 0, false)); - } - classes.push(("SCALE", "AcDbScale", 0, false)); - for (dxf_name, cpp, proxy, is_entity) in classes { - w.s(0, "CLASS"); - w.s(1, dxf_name); - w.s(2, cpp); - w.s(3, "ObjectDBX Classes"); - w.i(90, proxy); - w.i(280, 0); - w.i(281, i64::from(is_entity)); - } - w.s(0, "ENDSEC"); + write_classes(&mut w, &cx, &every); + + let ltype_handles = write_tables(&mut w, TableWritePlan { d, version, cx: &mut cx, ext: d.extents(&Space::Model) }); - // ---------------- TABLES ---------------- - w.s(0, "SECTION"); - w.s(2, "TABLES"); - vport_table(&mut w, d, ext); - // LTYPE. Embedded text and shapes point at STYLE records (340), which get their handles - // in the STYLE table below: (tag position, style name, is a shape) are filled in there. - let mut ltype_handles: HashMap = HashMap::new(); - let mut ltype_style_refs: Vec<(usize, Option, bool)> = Vec::new(); - let th = table_head(&mut w, "LTYPE", d.linetypes.len()); - for lt in &d.linetypes { - let h = record_head(&mut w, "LTYPE", &th, "AcDbLinetypeTableRecord"); - ltype_handles.entry(lt.name.to_ascii_uppercase()).or_insert(h.clone()); - cx.ltypes.entry(lt.name.to_ascii_uppercase()).or_insert(h); - w.s(2, <.name); - w.i(70, if crate::dxf_xref::dependent(<.name) { 48 } else { 0 }); - w.s(3, <.description); - w.i(72, 65); - w.i(73, lt.pattern.len() as i64); - w.f(40, lt.pattern_length()); - for el in <.pattern { - w.f(49, el.length); - // 74: 2 = text, 4 = shape, + 1 when the rotation is absolute (DXF Reference, LTYPE). - let (kind, shape) = match (&el.text, el.shape) { - (Some(_), _) => (2, 0), - (None, Some(n)) => (4, n), - (None, None) => (0, 0), - }; - if kind == 0 { - w.i(74, 0); - continue; - } - w.i(74, kind | i64::from(el.absolute)); - w.i(75, i64::from(shape)); - ltype_style_refs.push((w.t.len(), el.style.clone(), kind == 4)); - w.s(340, "0"); - w.f(46, el.scale); - w.f(50, el.rotation); - w.f(44, el.offset.x); - w.f(45, el.offset.y); - if let Some(t) = &el.text { - w.s(9, t.clone()); - } - } - } - w.s(0, "ENDTAB"); - // LAYER - let th = table_head(&mut w, "LAYER", d.layers.len()); - for l in &d.layers { - let lh = record_head(&mut w, "LAYER", &th, "AcDbLayerTableRecord"); - w.layers.insert(l.name.to_ascii_uppercase(), lh); - w.s(2, &l.name); - // 16 | 32: an xref's layer (`xref|name`), externally dependent and resolved. - let xref = if crate::dxf_xref::dependent(&l.name) { 48 } else { 0 }; - w.i(70, i64::from(l.frozen) | if l.vp_freeze_new { 2 } else { 0 } | if l.locked { 4 } else { 0 } | xref); - let aci = match l.color { - Color::True(rgb) => i64::from(cadcraft_color::nearest_aci(rgb)), - c => i64::from(c.to_aci()).clamp(1, 255), - }; - w.i(62, if l.on { aci } else { -aci }); - if let Color::True(rgb) = l.color { - w.i(420, i64::from(rgb.to_u32())); - } - w.s(6, &l.linetype); - if !l.plot { - w.i(290, 0); - } - w.i(370, i64::from(l.lineweight.to_dxf())); - let normal = w.pstyles.get(&cadcraft_doc::NORMAL_STYLE.to_ascii_uppercase()).cloned(); - if let Some(h) = w.pstyles.get(&l.plot_style.trim().to_ascii_uppercase()).cloned().or(normal) { - w.s(390, h); - } - if l.transparency > 0 { - w.s(1001, dxf_ext::LAYER_TRANSPARENCY_APP); - w.i(1071, dxf_ext::transparency_to_dxf(l.transparency)); - } - if !l.description.is_empty() { - // The first string is the layer standard's name, unused here. - w.s(1001, dxf_ext::LAYER_DESCRIPTION_APP); - w.s(1000, ""); - w.s(1000, dxf_ext::xdata_str(&l.description)); - } - } - w.s(0, "ENDTAB"); - // STYLE. Linetype shapes from a shape file that isn't a text style get an unnamed shape - // style record (70 bit 1) naming the file. - let mut shape_files: Vec = Vec::new(); - for (_, style, shape) in <ype_style_refs { - let Some(f) = style.as_deref().map(str::trim).filter(|f| *shape && !f.is_empty() && d.text_style(f).is_none()) else { continue }; - if !shape_files.iter().any(|x| x.eq_ignore_ascii_case(f)) { - shape_files.push(f.to_string()); - } - } - let th = table_head(&mut w, "STYLE", d.text_styles.len() + shape_files.len()); - for s in &d.text_styles { - let h = record_head(&mut w, "STYLE", &th, "AcDbTextStyleTableRecord"); - cx.styles.entry(s.name.to_ascii_uppercase()).or_insert(h); - w.s(2, &s.name); - // 70 bit 4: vertical; 71 bits 2/4: backwards / upside down. - w.i(70, if s.vertical { 4 } else { 0 }); - w.f(40, s.height); - w.f(41, s.width_factor); - w.f(50, s.oblique.to_degrees()); - w.i(71, if s.backwards { 2 } else { 0 } | if s.upside_down { 4 } else { 0 }); - w.f(42, 0.2); - // The built-in font goes out as `txt`, which every reader has; CADCraft's own name - // travels in `CADCRAFT` xdata (`1000 FONT`, name) so the style reopens with it. - let file = if s.font == cadcraft_fonts_name() { "txt" } else { s.font.as_str() }; - w.s(3, file); - w.s(4, &s.big_font); - if s.annotative { - annotative_xdata(&mut w); - } - if file != s.font { - w.s(1001, dxf_ext::APP); - w.s(1000, "FONT"); - w.s(1000, dxf_ext::xdata_str(&s.font)); - } - } - let mut shape_handles: HashMap = HashMap::new(); - for f in &shape_files { - let h = record_head(&mut w, "STYLE", &th, "AcDbTextStyleTableRecord"); - shape_handles.insert(f.to_ascii_uppercase(), h); - w.s(2, ""); - w.i(70, 1); - w.f(40, 0.0); - w.f(41, 1.0); - w.f(50, 0.0); - w.i(71, 0); - w.f(42, 0.2); - w.s(3, f.clone()); - w.s(4, ""); - } - w.s(0, "ENDTAB"); - // Linetype text and shapes without a known style use Standard (else the first style). - let fallback_style = cx.style("Standard").or_else(|| d.text_styles.first().and_then(|s| cx.style(&s.name))).unwrap_or_else(|| "0".into()); - for (pos, style, shape) in ltype_style_refs { - let name = style.as_deref().map(str::trim).unwrap_or_default(); - let h = shape.then(|| shape_handles.get(&name.to_ascii_uppercase()).cloned()).flatten().or_else(|| cx.style(name)); - if let Some(t) = w.t.get_mut(pos) { - *t = Tag::s(340, h.unwrap_or_else(|| fallback_style.clone())); - } - } - // VIEW: named views (DXF Reference, VIEW). A view's layer state is CADCraft xdata. - let th = table_head(&mut w, "VIEW", d.views.len()); - for v in &d.views { - record_head(&mut w, "VIEW", &th, "AcDbViewTableRecord"); - w.s(2, &v.name); - w.i(70, i64::from(v.layout.is_some())); - w.f(40, v.height); - w.p2(10, v.center); - w.f(41, v.width); - w.p(11, Vec3::Z); - w.p(12, Vec3::ZERO); - w.f(42, 50.0); - w.f(43, 0.0); - w.f(44, 0.0); - w.f(50, if v.twist.is_finite() { v.twist.to_degrees() } else { 0.0 }); - w.i(71, 0); - w.i(281, 0); - w.i(72, 0); - let xdata = [("LAYERSTATE", v.layer_state.as_deref()), ("LAYOUT", v.layout.as_deref().filter(|l| !l.is_empty()))]; - if xdata.iter().any(|(_, x)| x.is_some()) { - w.s(1001, dxf_ext::APP); - for (key, value) in xdata { - if let Some(value) = value { - w.s(1000, key); - w.s(1000, dxf_ext::xdata_str(value)); - } - } - } - } - w.s(0, "ENDTAB"); - // UCS: named user coordinate systems (DXF Reference, UCS). - let th = table_head(&mut w, "UCS", d.ucss.len()); - for u in &d.ucss { - record_head(&mut w, "UCS", &th, "AcDbUCSTableRecord"); - w.s(2, &u.name); - w.i(70, 0); - w.p(10, u.origin); - w.p(11, u.x_axis); - w.p(12, u.y_axis); - w.i(79, 0); - w.f(146, 0.0); - } - w.s(0, "ENDTAB"); - let apps = ["ACAD", dxf_ext::APP, "AcadAnnotative", dxf_ext::LAYER_TRANSPARENCY_APP, dxf_ext::LAYER_DESCRIPTION_APP]; - let th = table_head(&mut w, "APPID", apps.len()); - for app in apps { - record_head(&mut w, "APPID", &th, "AcDbRegAppTableRecord"); - w.s(2, app); - w.i(70, 0); - } - w.s(0, "ENDTAB"); - // DIMSTYLE - let th = { - let h = w.h(); - w.s(0, "TABLE"); - w.s(2, "DIMSTYLE"); - w.s(5, h.clone()); - w.s(330, "0"); - w.s(100, "AcDbSymbolTable"); - w.i(70, d.dim_styles.len() as i64); - w.s(100, "AcDbDimStyleTable"); - w.i(71, 0); - h - }; - for s in &d.dim_styles { - record_head(&mut w, "DIMSTYLE", &th, "AcDbDimStyleTableRecord"); - w.s(2, &s.name); - w.i(70, 0); - let v = serde_json::to_value(s).unwrap_or_default(); - let defaults = serde_json::to_value(DimStyle::default()).unwrap_or_default(); - let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() }; - for (field, code, kind) in dxf_ext::DIM_CODES { - let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue }; - // DIMPOST is written even when empty: a reader that defaults a missing one to "<>" - // (the DWG bridge's) would otherwise give radius and diameter dimensions no "R" or - // "Ø" in AutoCAD (#1053). - if *code == dxf_ext::DIMPOST - && let DimVal::Str(t) = &enc - && t.is_empty() - { - w.s(*code, ""); - continue; - } - if !dxf_ext::NEWER_CODES.contains(code) { - w.dimval(*code, enc, false); - continue; - } - // Table groups where the file version has them, else xdata. DIMTFILL and DIMTFILLCLR - // are written only when set, as other writers do, so a style without a text - // background has a single 70 (its flags). - if version >= dxf_ext::newer_since(*code) { - if matches!(*code, 69 | 70) && v.get(*field) == defaults.get(*field) { - continue; - } - w.dimval(*code, enc, false); - } else if v.get(*field) != defaults.get(*field) { - newer.i(1070, i64::from(*code)); - newer.dimval(*code, enc, true); - } - } - if !s.arrow_block1.trim().is_empty() || !s.arrow_block2.trim().is_empty() { - w.i(dxf_ext::DIMSAH, 1); - } - if s.annotative { - annotative_xdata(&mut w); - } - if !newer.t.is_empty() { - w.s(1001, dxf_ext::APP); - w.s(1000, dxf_ext::DIMVARS_LIST); - w.s(1002, "{"); - w.xdata(newer.t); - w.s(1002, "}"); - } - } - w.s(0, "ENDTAB"); // BLOCK_RECORD let th = table_head(&mut w, "BLOCK_RECORD", 1 + ps_brs.len() + user_blocks.len() + dim_defs.len() + arrow_defs.len() + table_defs.len()); let br_rec = |w: &mut W, h: &str, name: &str, layout: Option<&str>, blk: Option<&Block>| { @@ -559,7 +1013,7 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec Vec = w.t.iter().filter(|t| t.code == 5).map(Tag::str).collect(); - let groups: Vec<(&Group, String)> = d.groups.iter().filter(|g| !g.name.is_empty()).map(|g| (g, w.h())).collect(); - let mleader_dict = w.h(); - let mleader_handles: Vec = cx.mleader_styles.iter().map(|(_, h)| h.clone()).collect(); - w.s(0, "SECTION"); - w.s(2, "OBJECTS"); - w.s(0, "DICTIONARY"); - w.s(5, root_dict.clone()); - w.s(330, "0"); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - w.s(3, "ACAD_GROUP"); - w.s(350, group_dict.clone()); - if !cx.images.is_empty() { - w.s(3, "ACAD_IMAGE_DICT"); - w.s(350, cx.images.dict.clone()); - w.s(3, "ACAD_IMAGE_VARS"); - w.s(350, cx.images.vars.clone()); - } - w.s(3, "ACAD_LAYOUT"); - w.s(350, layout_dict.clone()); - w.s(3, "ACAD_MLEADERSTYLE"); - w.s(350, mleader_dict.clone()); - w.s(3, "ACAD_MLINESTYLE"); - w.s(350, cx.mlines.dict.clone()); - w.s(3, dxf_ext::PLOT_STYLE_NAMES_KEY); - w.s(350, pstyle_dict.clone()); - w.s(3, "ACAD_TABLESTYLE"); - w.s(350, table_style_dict.clone()); - for (key, h, _) in &xrecords { - w.s(3, *key); - w.s(350, h); - } - w.s(3, "ACAD_SCALELIST"); - w.s(350, scale_dict.clone()); - w.s(0, "DICTIONARY"); - w.s(5, group_dict); - w.s(330, root_dict.clone()); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - for (g, h) in &groups { - w.s(3, &g.name); - w.s(350, h); - } - for (g, h) in &groups { - group_obj(&mut w, g, h, &group_owner, &written); - } - // Multileader styles. - w.s(0, "DICTIONARY"); - w.s(5, mleader_dict.clone()); - w.s(330, root_dict.clone()); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - for (s, h) in mleader_styles.iter().zip(&mleader_handles) { - w.s(3, &s.name); - w.s(350, h); - } - let byblock_ltype = ltype_handles.get("BYBLOCK").cloned(); - for (s, h) in mleader_styles.iter().zip(&mleader_handles) { - mleader_style_obj(&mut w, s, h, &mleader_dict, byblock_ltype.as_deref(), cx.style(&s.text_style)); - } - // Multiline styles. - w.t.extend(crate::dxf_mline::objects(&cx.mlines, &root_dict)); - // Table styles. - w.s(0, "DICTIONARY"); - w.s(5, table_style_dict.clone()); - w.s(330, root_dict.clone()); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - for (name, h) in &cx.table_styles { - w.s(3, name); - w.s(350, h); - } - for (s, (_, h)) in table_styles.iter().zip(&cx.table_styles) { - table_style_obj(&mut w, d, s, h, &table_style_dict); - } - // Plot style names: a dictionary with a default (Normal) of placeholder objects. - w.s(0, "ACDBDICTIONARYWDFLT"); - w.s(5, pstyle_dict.clone()); - w.group("ACAD_REACTORS", 330, &[&root_dict]); - w.s(330, root_dict.clone()); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - for (n, h) in &pstyles { - w.s(3, n); - w.s(350, h); - } - w.s(100, "AcDbDictionaryWithDefault"); - w.s(340, pstyles.first().map(|(_, h)| h.clone()).unwrap_or_default()); - for (_, h) in &pstyles { - w.s(0, "ACDBPLACEHOLDER"); - w.s(5, h); - w.group("ACAD_REACTORS", 330, &[&pstyle_dict]); - w.s(330, pstyle_dict.clone()); - } - // Parametric constraints and parameters, saved layer states, plot style tables (CADCraft - // data). - for (_, h, chunks) in &xrecords { - w.s(0, "XRECORD"); - w.s(5, h); - w.group("ACAD_REACTORS", 330, &[&root_dict]); - w.s(330, root_dict.clone()); - w.s(100, "AcDbXrecord"); - w.i(280, 1); - for c in chunks { - w.s(1, c.clone()); - } - } - scale_list(&mut w, &scales, &scale_dict, &root_dict); - // Dimension associativity: extension dictionary + DIMASSOC per dimension. - let mut assoc: Vec<(&Handle, &AssocObj)> = cx.assoc.iter().collect(); - assoc.sort_by_key(|(h, _)| **h); - for (dim, a) in assoc { - let dh = dim.hex(); - w.s(0, "DICTIONARY"); - w.s(5, a.xdict.clone()); - w.group("ACAD_REACTORS", 330, &[&dh]); - w.s(330, dh.clone()); - w.s(100, "AcDbDictionary"); - w.i(280, 1); - w.i(281, 1); - w.s(3, "ACAD_DIMASSOC"); - w.s(360, a.handle.clone()); - w.s(0, "DIMASSOC"); - w.s(5, a.handle.clone()); - w.group("ACAD_REACTORS", 330, &[&a.xdict]); - w.s(330, a.xdict.clone()); - w.s(100, "AcDbDimAssoc"); - w.s(330, dh); - w.i(90, a.flags); - w.i(70, 0); - w.i(71, 0); - for r in &a.refs { - w.s(1, "AcDbOsnapPointRef"); - w.i(72, r.osnap); - w.s(331, r.main.hex()); - w.i(73, if r.segment > 0 { 2 } else { 0 }); - w.i(91, r.segment); - w.s(301, ""); - w.f(40, r.param); - w.p(10, r.point); - if let Some(o) = r.other { - w.s(332, o.hex()); - w.i(74, 0); - w.i(92, 0); - w.s(302, ""); - } - w.i(75, 0); - } - } - w.s(0, "DICTIONARY"); - w.s(5, layout_dict.clone()); - w.s(330, root_dict.clone()); - w.s(100, "AcDbDictionary"); - w.i(281, 1); - w.s(3, "Model"); - w.s(350, model_layout.clone()); - for (i, (_, _, l)) in ps_brs.iter().enumerate() { - w.s(3, &l.name); - w.s(350, layout_handles.get(i).cloned().unwrap_or_default()); - } - let layout_obj = |w: &mut W, h: &str, (name, tab): (&str, i64), brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| { - // Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units - // per 143 drawing units). - let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 }; - let standard = page.scale_to_fit || scale == 1.0; - let plot_flags = 512 - | 32 - | if standard { 16 } else { 0 } - | if page.lineweights { 128 } else { 0 } - | if page.center { 4 } else { 0 } - | if page.show_plot_styles { 2 } else { 0 }; - let plot_type = match page.plot_area.as_str() { - "display" => 0, - "extents" => 1, - "limits" => 2, - "window" => 4, - _ => 5, - }; - w.s(0, "LAYOUT"); - w.s(5, h); - w.s(330, layout_dict.clone()); - w.s(100, "AcDbPlotSettings"); - w.s(1, ""); - let device = page.device.trim(); - w.s(2, if device.is_empty() || device.eq_ignore_ascii_case("None") { "none_device" } else { device }); - w.s(4, page.paper.replace(' ', "_")); - w.s(6, ""); - for (c, v) in [ - (40, page.margins_mm[0]), - (41, page.margins_mm[1]), - (42, page.margins_mm[2]), - (43, page.margins_mm[3]), - (44, page.width_mm), - (45, page.height_mm), - (46, 0.0), - (47, 0.0), - (48, page.plot_window[0]), - (49, page.plot_window[1]), - (140, page.plot_window[2]), - (141, page.plot_window[3]), - (142, scale), - (143, 1.0), - ] { - w.f(c, v); - } - w.i(70, plot_flags); - // Plot paper units: 1 millimetres, 0 inches (#1058). - w.i(72, i64::from(cadcraft_render::paper::page_unit_mm(d, page) == 1.0)); - w.i(73, i64::from(page.landscape)); - w.i(74, plot_type); - w.s(7, &page.plot_style_table); - w.i(75, if page.scale_to_fit { 0 } else { 16 }); - w.f(147, scale); - w.f(148, 0.0); - w.f(149, 0.0); - w.s(100, "AcDbLayout"); - w.s(1, name); - w.i(70, flags); - w.i(71, tab); - w.p2(10, Vec2::ZERO); - w.p2(11, Vec2::new(12.0, 9.0)); - w.p(12, Vec3::ZERO); - w.p(14, Vec3::ZERO); - w.p(15, Vec3::ZERO); - w.f(146, 0.0); - w.p(13, Vec3::ZERO); - w.p(16, Vec3::new(1.0, 0.0, 0.0)); - w.p(17, Vec3::new(0.0, 1.0, 0.0)); - w.i(76, 0); - w.s(330, brh); - // The saved paper-space view (centre, height) as CADCraft xdata. - if let Some((c, height)) = view.filter(|(c, v)| c.x.is_finite() && c.y.is_finite() && v.is_finite()) { - w.s(1001, dxf_ext::APP); - w.s(1000, "PSVIEW"); - w.s(1002, "{"); - w.p(1010, c.to3(0.0)); - w.f(1040, height); - w.s(1002, "}"); - } - }; - // Model space has no page setup of its own: write what PLOT does for it, the drawing extents - // fitted to the paper (plot type 1, scaled to fit). The default page's "layout" area is - // plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means - // nothing for model space: another program's Plot dialog then shows no plot area or scale. - let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() }; - layout_obj(&mut w, &model_layout, ("Model", 0), &ms_br, 1, &model_page, None); - for (i, (_, brh, l)) in ps_brs.iter().enumerate() { - let h = layout_handles.get(i).cloned().unwrap_or_default(); - layout_obj(&mut w, &h, (&l.name, i64::from(l.tab_order.max(1))), brh, 1, &l.page, l.view); - } - // Raster image definitions. - w.t.extend(crate::dxf_image::objects(&cx.images, &root_dict)); - w.s(0, "ENDSEC"); w.s(0, "EOF"); let seed = format!("{:X}", w.next); if let Some(t) = w.t.get_mut(seed_pos + 1) { diff --git a/crates/render/src/blocks.rs b/crates/render/src/blocks.rs index d8a62fddc..ad4689682 100644 --- a/crates/render/src/blocks.rs +++ b/crates/render/src/blocks.rs @@ -1,7 +1,7 @@ use super::draw::entity; use super::props::{resolve, resolve_common, transparency}; use super::text_entity::{mtext, place_text_entity}; -use super::*; +use super::{Builder, Color, Ctx, Entity, EntityKind, Ink, Lineweight, Mat3, Vec2}; pub(crate) fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> { let layer = if e.common.layer == "0" { ctx.block_layer.clone() } else { Some(e.common.layer.clone()) }; diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs index a2f0021a2..967a2962c 100644 --- a/crates/render/src/dim.rs +++ b/crates/render/src/dim.rs @@ -8,9 +8,9 @@ use cadcraft_doc::{DimKind, DimStyle, Dimension}; use cadcraft_fonts::{MTextParams, TextFont}; -use cadcraft_geom::{Arc, Vec2, ccw_sweep}; +use cadcraft_geom::Vec2; -use crate::units::{NumFormat, format_angle_with_separator, format_decimal, format_linear}; +use crate::units::{NumFormat, format_decimal, format_linear}; mod arrowheads; mod text; @@ -18,7 +18,6 @@ mod text; use arrowheads::tick; pub use arrowheads::{ArrowGeom, Arrowhead, arrow, arrowhead}; pub use text::{DimText, format_linear_value}; -use text::{apply_override, inspected, linear_text}; /// Most DIMBREAK gaps drawn per dimension. const MAX_BREAKS: usize = 1000; @@ -216,6 +215,75 @@ fn readable(a: f64) -> f64 { a } +struct DimConfig<'a> { + dimension: &'a Dimension, + style: &'a DimStyle, + font: &'a DimText, + scale: f64, + arrow_size: f64, + text_height: f64, + gap: f64, + tick_size: f64, + ticks: bool, + first_arrow: Arrowhead, + second_arrow: Arrowhead, + width_factor: f64, +} + +impl<'a> DimConfig<'a> { + fn new(dimension: &'a Dimension, style: &'a DimStyle, dimscale: f64, font: &'a DimText) -> Self { + let scale = style.effective_scale(dimscale); + let arrow_size = style.arrow_size * scale; + let text_height = if font.fixed_height > 0.0 { font.fixed_height } else { style.text_height * scale }; + let tick_size = style.tick_size * scale; + let first_arrow = if style.arrow_block1.is_empty() { &style.arrow_block } else { &style.arrow_block1 }; + let second_arrow = if style.arrow_block2.is_empty() { &style.arrow_block } else { &style.arrow_block2 }; + Self { + dimension, + style, + font, + scale, + arrow_size, + text_height, + gap: style.text_gap.abs() * scale, + tick_size, + ticks: tick_size > 0.0, + first_arrow: Arrowhead::parse(first_arrow), + second_arrow: Arrowhead::parse(second_arrow), + width_factor: if font.width_factor > 0.0 && font.width_factor.is_finite() { font.width_factor } else { 1.0 }, + } + } + + fn text_params(&self, at: Vec2, rotation: f64) -> MTextParams { + MTextParams { + attach: 5, + rotation, + font: self.font.font.clone(), + width_factor: self.width_factor, + oblique: self.font.oblique, + ..MTextParams::new(at, self.text_height) + } + } + + fn measure(&self, text: &str) -> (f64, f64) { + if text.is_empty() { + return (0.0, 0.0); + } + let layout = cadcraft_fonts::layout_mtext_with(text, &self.text_params(Vec2::ZERO, 0.0)); + (layout.bounds.width(), layout.bounds.height()) + } + + fn end(&self, geometry: &mut DimGeometry, kind: Arrowhead, tip: Vec2, direction: Vec2) -> f64 { + if self.ticks { + geometry.dim(tick(tip, direction, self.tick_size)); + 0.0 + } else { + geometry.add_arrow(arrowhead(kind, tip, direction, self.arrow_size)); + kind.inset(self.arrow_size) + } + } +} + /// Generate the geometry of a dimension with the stroke font. pub fn dimension_geometry(d: &Dimension, st: &DimStyle, dimscale: f64) -> DimGeometry { dimension_geometry_with(d, st, dimscale, &DimText { width_factor: 1.0, ..Default::default() }) @@ -223,470 +291,24 @@ pub fn dimension_geometry(d: &Dimension, st: &DimStyle, dimscale: f64) -> DimGeo /// Generate the geometry of a dimension. The dimension's own overrides are applied to `st`. pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font: &DimText) -> DimGeometry { - let st = &st.with_overrides(&d.overrides); - let k = st.effective_scale(dimscale); - let asz = st.arrow_size * k; - let th = if font.fixed_height > 0.0 { font.fixed_height } else { st.text_height * k }; - let gap = st.text_gap.abs() * k; - let tsz = st.tick_size * k; - let ticks = tsz > 0.0; - let blk1 = Arrowhead::parse(if st.arrow_block1.is_empty() { &st.arrow_block } else { &st.arrow_block1 }); - let blk2 = Arrowhead::parse(if st.arrow_block2.is_empty() { &st.arrow_block } else { &st.arrow_block2 }); + let overridden = st.with_overrides(&d.overrides); + let config = DimConfig::new(d, &overridden, dimscale, font); + let st = config.style; + let th = config.text_height; + let gap = config.gap; let mut g = DimGeometry { text_height: th, ..Default::default() }; - // Measure the text: lay it out once at the origin, centred. - let wf = if font.width_factor > 0.0 && font.width_factor.is_finite() { font.width_factor } else { 1.0 }; - let params = |mt: Vec2, rot: f64| MTextParams { - attach: 5, - rotation: rot, - font: font.font.clone(), - width_factor: wf, - oblique: font.oblique, - ..MTextParams::new(mt, th) - }; - let measure = |text: &str| { - if text.is_empty() { - return (0.0, 0.0); - } - let l = cadcraft_fonts::layout_mtext_with(text, ¶ms(Vec2::ZERO, 0.0)); - (l.bounds.width(), l.bounds.height()) - }; + let params = |at: Vec2, rotation: f64| config.text_params(at, rotation); + let measure = |text: &str| config.measure(text); let mut text_angle = 0.0; let mut text_mid = d.text_mid.xy(); // Place an end marker: tick or arrowhead. Returns how far the dimension line stops short. - let end = |g: &mut DimGeometry, kind: Arrowhead, tip: Vec2, dir: Vec2| -> f64 { - if ticks { - g.dim(tick(tip, dir, tsz)); - 0.0 - } else { - g.add_arrow(arrowhead(kind, tip, dir, asz)); - kind.inset(asz) - } - }; match d.kind { - DimKind::Linear { .. } | DimKind::Aligned => { - let p1 = d.p13.xy(); - let p2 = d.p14.xy(); - let on = d.defpt.xy(); - let dir = match d.kind { - DimKind::Linear { rotation } => Vec2::from_angle(rotation), - _ => (p2 - p1).normalized(), - }; - let dir = if dir == Vec2::ZERO || !dir.is_finite() { Vec2::X } else { dir }; - // DIMEDIT Oblique: extension lines at their own angle (not along the dimension line). - let oblique = d.marks.oblique.map(Vec2::from_angle).filter(|e| e.is_finite() && e.cross(dir).abs() > 1e-6); - let foot = |p: Vec2| match oblique { - Some(e) => p + e * ((on - p).cross(dir) / e.cross(dir)), - None => on + dir * (p - on).dot(dir), - }; - let f1 = foot(p1); - let f2 = foot(p2); - // The measurement is along the dimension direction whatever the extension lines do. - let meas = (p2 - p1).dot(dir).abs(); - g.mtext = linear_text(meas, d, st, ""); - g.measurement = meas; - // From here on: the length drawn between the extension lines. - let meas = f1.dist(f2); - if !st.suppress_ext1 { - g.ext_n(ext_line(p1, f1, st, k), false); - } - if !st.suppress_ext2 { - g.ext_n(ext_line(p2, f2, st, k), true); - } - let u = (f2 - f1).normalized(); - let u = if u == Vec2::ZERO || !u.is_finite() { dir } else { u }; - let n = u.perp(); - let (tw, tht) = measure(&g.mtext); - let has_text = tw > 0.0; - let tick_like = ticks || (blk1.is_tick() && blk2.is_tick()); - let aw = if tick_like { 0.0 } else { asz }; - let arrows_fit = tick_like || meas > 2.0 * asz + 1e-9; - // Text orientation: inside / outside horizontal. - let along_ang = readable(u.angle()); - let extent = |ang: f64| { - // Half-extent of the text box projected on the dimension line. - let r = Vec2::from_angle(ang); - (tw * r.dot(u).abs() + tht * r.perp().dot(u).abs()) / 2.0 - }; - let inside_ang = if st.text_inside_horizontal { 0.0 } else { along_ang }; - // DIMTVP (with DIMTAD 0) lifts the text off the line once it is 0.7 text heights or more. - let tvp = if st.text_above == 0 && st.text_vertical_pos.is_finite() { st.text_vertical_pos } else { 0.0 }; - let centred = st.text_above == 0 && tvp.abs() < 0.7; - let text_need = if centred { 2.0 * (extent(inside_ang) + gap) } else { 2.0 * extent(inside_ang) }; - let fits = !has_text || meas >= text_need + 2.0 * aw; - let text_fits = !has_text || meas >= text_need; - let just = st.text_just.min(4); - // DIMATFIT / DIMTIX: what goes outside the extension lines when text and arrows - // don't both fit between them. Text the user placed, or over an extension line, is - // not part of the fit. - let (text_in, arrows_inside) = if d.user_text_pos || !has_text || just >= 3 { - (true, arrows_fit) - } else if fits { - (true, true) - } else if st.text_inside { - (true, false) - } else { - match st.fit { - 0 => (false, false), - 1 => (text_fits, false), - 2 => (false, arrows_fit), - _ if arrows_fit => (false, true), - _ => (text_fits, false), - } - }; - let text_outside = !text_in; - text_angle = if text_outside { if st.text_outside_horizontal { 0.0 } else { along_ang } } else { inside_ang }; - let half = extent(text_angle); - // DIMTMOVE 1 / 2: text that doesn't fit is moved off the dimension line, which stays put. - let moved = text_outside && st.text_move != 0; - // DIMSOXD: arrowheads that don't fit inside are left out. - let outside_arrows = !arrows_inside && !st.suppress_outside_arrows; - // Text centre along the dimension line. - let along_c = if text_outside { - f2 + u * (2.0 * asz.max(tsz) + gap + half) - } else { - match just { - 1 => f1 + u * (aw + gap + half + asz), - 2 => f2 - u * (aw + gap + half + asz), - _ => f1.mid(f2), - } - }; - // Perpendicular offset per DIMTAD. - let side_out = if (on - p1).dot(n) >= 0.0 { n } else { -n }; - let tr = Vec2::from_angle(text_angle); - let off_along = |m: Vec2| { - let glyph_extent = tw * tr.dot(m).abs() / 2.0 + tht * tr.perp().dot(m).abs() * if st.text_above == 1 { 0.6 } else { 0.5 }; - // Empirical #2004 match: 0.595× height with SHX and 0.601× with TrueType. - gap + glyph_extent - }; - let above = if text_angle.abs() < 1e-12 && n.y.abs() > 1e-9 { - if n.y > 0.0 { n } else { -n } - } else if text_angle.abs() < 1e-12 { - if n.x < 0.0 { n } else { -n } - } else if tr.perp().dot(n) >= 0.0 { - n - } else { - -n - }; - let offset = match st.text_above { - 0 => above * (tvp * th), - 2 => side_out * off_along(side_out), - 4 => -above * off_along(-above), - _ => above * off_along(above), - }; - if just >= 3 && !d.user_text_pos && has_text { - // Over an extension line, aligned with it. - let (p, f) = if just == 3 { (p1, f1) } else { (p2, f2) }; - let e = (f - p).normalized(); - let e = if e == Vec2::ZERO { side_out } else { e }; - text_angle = readable(e.angle()); - let outward = if just == 3 { -u } else { u }; - text_mid = f + e * (st.ext_extend * k + gap + tw / 2.0) + outward * (gap + tht / 2.0); - } else if moved { - // Over the middle of the line, clear of the arrowheads, on the side away from the - // measured points; with DIMTMOVE 1 it starts there and a leader joins it. - let mid = f1.mid(f2); - let lift = off_along(side_out) + 2.0 * asz.max(tsz); - text_mid = if st.text_move == 1 { mid + side_out * lift + u * half } else { mid + side_out * (lift + off_along(side_out)) }; - } else if !d.user_text_pos { - text_mid = along_c + offset; - } - // Dimension line. DIMSD1 / DIMSD2 leave out its first / second half (with the arrow). - let dle = if tick_like { st.dim_line_extend * k } else { 0.0 }; - let lo = if st.suppress_dim1 { meas / 2.0 } else { f64::NEG_INFINITY }; - let hi = if st.suppress_dim2 { meas / 2.0 } else { f64::INFINITY }; - let seg = |g: &mut DimGeometry, a: Vec2, b: Vec2| { - let (ta, tb) = ((a - f1).dot(u), (b - f1).dot(u)); - let (t0, t1) = (ta.min(tb).max(lo), ta.max(tb).min(hi)); - if t1 > t0 + 1e-12 { - g.dim(vec![f1 + u * t0, f1 + u * t1]); - } - }; - // The line between `a` and `b`, broken around centred text between the extension lines. - let inner = |g: &mut DimGeometry, a: Vec2, b: Vec2| { - if centred && has_text && !text_outside && just <= 2 && !d.user_text_pos { - let h = extent(text_angle) + gap; - let (t0, t1) = ((along_c - u * h - f1).dot(u), (along_c + u * h - f1).dot(u)); - if t0 > (a - f1).dot(u) + 1e-9 { - seg(g, a, f1 + u * t0); - } - if t1 < (b - f1).dot(u) - 1e-9 { - seg(g, f1 + u * t1, b); - } - } else { - seg(g, a, b); - } - }; - // Out to text outside the extension lines, from `from` along the line. - let to_text = |g: &mut DimGeometry, from: Vec2| { - let to = text_mid - offset - u * (half + gap); - if text_outside && (to - from).dot(u) > 0.0 { - seg(g, from, from + u * (to - from).dot(u)); - } - }; - if arrows_inside { - let i1 = if st.suppress_dim1 { 0.0 } else { end(&mut g, blk1, f1, -u) }; - let i2 = if st.suppress_dim2 { 0.0 } else { end(&mut g, blk2, f2, u) }; - inner(&mut g, f1 - u * dle + u * i1, f2 + u * dle - u * i2); - to_text(&mut g, f2); - } else { - let stub = asz * 2.0; - if outside_arrows { - if !st.suppress_dim1 { - end(&mut g, blk1, f1, u); - } - if !st.suppress_dim2 { - end(&mut g, blk2, f2, -u); - } - seg(&mut g, f1 - u * stub, f1); - seg(&mut g, f2, f2 + u * stub); - } - // DIMTOFL: the line between the extension lines as well. - if st.force_line_inside { - inner(&mut g, f1, f2); - } - to_text(&mut g, if outside_arrows { f2 + u * stub } else { f2 }); - } - // DIMTMOVE 1: a leader from the dimension line to text moved off it. - if (d.user_text_pos || moved) && st.text_move == 1 && has_text { - let base = f1.mid(f2); - let off = (text_mid - base).dot(n).abs(); - if off > tht / 2.0 + gap + 1e-9 { - let side = if (base - text_mid).dot(tr) >= 0.0 { tr } else { -tr }; - g.dim(vec![base, text_mid + side * (tw / 2.0 + gap)]); - } - } - // DIMJOGLINE: a zigzag in the dimension line, 1.5 text heights tall. - if let Some(j) = d.marks.jog_line { - let at = f1 + u * (j.xy() - f1).dot(u); - let h = 1.5 * th; - if at.is_finite() && h > 0.0 { - let r = h / 2.0; - g.cut(at, r, Some(LineRole::Dim)); - g.dim(vec![at - u * r, at - u * (r / 4.0) + n * (h / 2.0), at + u * (r / 4.0) - n * (h / 2.0), at + u * r]); - } - } - } - DimKind::Radius if d.marks.jogged.is_some() => 'jogged: { - // DIMJOGGED: from the arrowhead on the arc toward the centre, a jog at DIMJOGANG, - // then on to the overriding centre (parallel to the first part). - let center = d.defpt.xy(); - let tip = d.p15.xy(); - let r = center.dist(tip); - let j = d.marks.jogged.clone().unwrap_or_default(); - let (o, jp) = (j.center.xy(), j.jog.xy()); - g.mtext = linear_text(r, d, st, "R"); - // A NaN or infinite radius leaves no line to draw (and NaN is no bound for `clamp`). - if !r.is_finite() { - break 'jogged; - } - let (tw, _) = measure(&g.mtext); - let u = (tip - center).normalized(); - let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u }; - let n = u.perp(); - let h = (o - center).dot(n); - let ang = if st.jog_angle.is_finite() { st.jog_angle.clamp(5.0, 90.0) } else { 45.0 }.to_radians(); - let run = h.abs() / ang.tan().max(1e-6); - let tj = (jp - center).dot(u).clamp(0.0, r); - let (j1, j2) = (center + u * (tj + run / 2.0), center + u * (tj - run / 2.0) + n * h); - g.dim(vec![tip, j1, j2, o]); - end(&mut g, blk2, tip, u); - let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) }; - if tpos.dist(center) > r { - g.dim(vec![tip, tpos]); - } - text_angle = if st.text_outside_horizontal { 0.0 } else { readable(u.angle()) }; - if !d.user_text_pos { - let x = Vec2::from_angle(text_angle); - let x = if x.dot(u) >= 0.0 { x } else { -x }; - text_mid = tpos + x * (tw / 2.0 + gap); - } - } - DimKind::Radius | DimKind::Diameter => { - let a = d.defpt.xy(); - let b = d.p15.xy(); - let radius = matches!(d.kind, DimKind::Radius); - let (center, r) = if radius { (a, a.dist(b)) } else { (a.mid(b), a.dist(b) / 2.0) }; - let meas = if radius { r } else { 2.0 * r }; - g.mtext = linear_text(meas, d, st, if radius { "R" } else { "%%c" }); - g.measurement = meas; - let (tw, tht) = measure(&g.mtext); - let u = (b - center).normalized(); - let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u }; - let tip = center + u * r; - let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) }; - // Text placed inside the circle (a dimension line location picked inside): the text - // sits on the dimension line, which runs from the text out to the arrowhead (for a - // diameter it spans the circle, broken around the text). DIMTIH turns it horizontal. - let inside = d.user_text_pos && text_mid.dist(center) < r; - let horizontal = if inside { st.text_inside_horizontal } else { st.text_outside_horizontal }; - if inside { - let tr = Vec2::from_angle(if horizontal { 0.0 } else { readable(u.angle()) }); - let half = if tw > 0.0 { (tw * tr.dot(u).abs() + tht * tr.perp().dot(u).abs()) / 2.0 + gap } else { 0.0 }; - let start = if radius { center } else { center - u * r }; - let (t0, t1) = ((text_mid - start).dot(u) - half, (text_mid - start).dot(u) + half); - if !radius { - if t0 > 0.0 { - g.dim(vec![start, start + u * t0]); - } - end(&mut g, blk1, start, -u); - } - if t1 < tip.dist(start) { - g.dim(vec![start + u * t1.max(0.0), tip]); - } - } else { - // Text outside: a diameter line spans the circle and a radius line runs in to the - // centre only with DIMTOFL; otherwise just the leader to the text. - if st.force_line_inside { - let start = if radius { center } else { center - u * r }; - g.dim(vec![start, tip]); - if !radius { - end(&mut g, blk1, start, -u); - } - } - // Text the user placed: the leader stops short of it by the offset the default - // placement leaves between leader end and text. - let lead = if !d.user_text_pos { - tpos - } else if horizontal { - text_mid - Vec2::new(tw / 2.0 + gap, 0.0) * if u.x >= 0.0 { 1.0 } else { -1.0 } - } else { - text_mid - u * (tw / 2.0 + gap) - }; - g.dim(vec![tip, lead]); - } - end(&mut g, blk2, tip, u); - if horizontal { - text_angle = 0.0; - let sx = if u.x >= 0.0 { 1.0 } else { -1.0 }; - if !d.user_text_pos { - text_mid = tpos + Vec2::new(tw / 2.0 + gap, 0.0) * sx; - } - } else { - text_angle = readable(u.angle()); - if !d.user_text_pos { - text_mid = tpos + u * (tw / 2.0 + gap); - } - } - // The DIMCEN mark is drawn only when no dimension line runs inside the circle (text - // outside without DIMTOFL), never with a line through the centre. - let line_outside = !st.force_line_inside && tpos.dist(center) >= r; - if line_outside && st.center_mark != 0.0 && st.center_mark.is_finite() { - let c = st.center_mark.abs() * k; - g.ext(vec![center - Vec2::X * c, center + Vec2::X * c]); - g.ext(vec![center - Vec2::Y * c, center + Vec2::Y * c]); - if st.center_mark < 0.0 && r > 2.0 * c { - // Centre lines out past the circle. - for dv in [Vec2::X, -Vec2::X, Vec2::Y, -Vec2::Y] { - g.ext(vec![center + dv * (2.0 * c), center + dv * (r + c)]); - } - } - } - } - DimKind::Angular | DimKind::Angular3P => { - // Angular3P: p15 vertex, p13/p14 on the legs, defpt on the arc. - // Angular (2 lines): lines p13-p14 and defpt-p15; p16 on the arc. - let (vertex, a1, a2, arc_pt) = if matches!(d.kind, DimKind::Angular3P) { - (d.p15.xy(), d.p13.xy(), d.p14.xy(), d.defpt.xy()) - } else { - let v = cadcraft_geom::line_line_infinite(d.p13.xy(), d.p14.xy(), d.defpt.xy(), d.p15.xy()).map(|x| x.0).unwrap_or(d.p14.xy()); - (v, d.p14.xy(), d.p15.xy(), d.p16.xy()) - }; - let r = vertex.dist(arc_pt).max(1e-9); - let s = vertex.angle_to(a1); - let e = vertex.angle_to(a2); - let arc = - if cadcraft_geom::angle_in_sweep(vertex.angle_to(arc_pt), s, e) { Arc::new(vertex, r, s, e) } else { Arc::new(vertex, r, e, s) }; - let sweep = ccw_sweep(arc.start, arc.end); - g.measurement = sweep; - // DIMAZIN 1 / 2 suppress leading / trailing zeros (DIMZIN's bits 4 / 8). - let azin = (st.angular_zero_suppression & 3) * 4; - let txt = format_angle_with_separator(sweep, st.angular_unit, st.angular_decimals, azin, &st.decimal_separator); - // DIMPOST is a linear-dimension prefix/suffix; angles have their own format only. - g.mtext = inspected(apply_override(&txt, d), d); - let mut pts = Vec::new(); - arc.tessellate(r * 1e-3, &mut pts); - g.dim(pts); - let sp = arc.start_point(); - let ep = arc.end_point(); - // The arc's ends with the outward tangents there, paired with leg 1 and leg 2: when - // the arc location is outside the leg 1 → leg 2 sweep the arc runs from leg 2 to - // leg 1, and each leg keeps its own extension line, DIMSE1/2 and DIMBLK1/2. - let (start, finish) = ((sp, -(sp - vertex).perp().normalized()), (ep, (ep - vertex).perp().normalized())); - let ((p1, dir1), (p2, dir2)) = - if cadcraft_geom::angle_in_sweep(vertex.angle_to(arc_pt), s, e) { (start, finish) } else { (finish, start) }; - if !st.suppress_ext1 { - g.ext_n(ext_line(vertex + (p1 - vertex).normalized() * vertex.dist(a1).min(r), p1, st, k), false); - } - if !st.suppress_ext2 { - g.ext_n(ext_line(vertex + (p2 - vertex).normalized() * vertex.dist(a2).min(r), p2, st, k), true); - } - end(&mut g, blk1, p1, dir1); - end(&mut g, blk2, p2, dir2); - let (tw, tht) = measure(&g.mtext); - let mid = arc.mid_point(); - let radial = (mid - vertex).normalized(); - text_angle = if st.text_inside_horizontal { 0.0 } else { readable(radial.perp().angle()) }; - if !d.user_text_pos { - let tr = Vec2::from_angle(text_angle); - let off = gap + (tw * tr.dot(radial).abs() + tht * tr.perp().dot(radial).abs()) / 2.0; - text_mid = mid + radial * off; - } - } - DimKind::Ordinate { x_type } => { - let feature = d.p13.xy(); - let leader_end = d.p14.xy(); - let origin = d.defpt.xy(); - let meas = if x_type { (feature.x - origin.x).abs() } else { (feature.y - origin.y).abs() }; - g.mtext = linear_text(meas, d, st, ""); - g.measurement = meas; - let (tw, _) = measure(&g.mtext); - g.dim(vec![feature, leader_end]); - if x_type { - text_angle = std::f64::consts::FRAC_PI_2; - } - if !d.user_text_pos { - let dirv = (leader_end - feature).normalized(); - let dirv = if dirv == Vec2::ZERO { if x_type { Vec2::Y } else { Vec2::X } } else { dirv }; - text_mid = leader_end + dirv * (tw / 2.0 + gap); - } - } - DimKind::ArcLength => { - let c = d.p15.xy(); - let r = c.dist(d.p13.xy()); - let arc = Arc::new(c, c.dist(d.defpt.xy()).max(1e-9), c.angle_to(d.p13.xy()), c.angle_to(d.p14.xy())); - let meas = r * arc.sweep(); - // DIMARCSYM: the arc symbol before the text (0), over it (1) or none (2). - g.mtext = linear_text(meas, d, st, if st.arc_symbol == 0 { "⌒" } else { "" }); - g.measurement = meas; - let mut pts = Vec::new(); - arc.tessellate(arc.radius * 1e-3, &mut pts); - g.dim(pts); - for (i, (p, q)) in [(d.p13.xy(), arc.start_point()), (d.p14.xy(), arc.end_point())].into_iter().enumerate() { - g.ext_n(ext_line(p, q, st, k), i == 1); - } - end(&mut g, blk1, arc.start_point(), -(arc.start_point() - c).perp().normalized()); - end(&mut g, blk2, arc.end_point(), (arc.end_point() - c).perp().normalized()); - let m = arc.mid_point(); - let radial = (m - c).normalized(); - text_angle = if st.text_inside_horizontal { 0.0 } else { readable(radial.perp().angle()) }; - if !d.user_text_pos { - text_mid = m + radial * (gap + th / 2.0); - } - if st.arc_symbol == 1 { - // A small arc over the text. - let (tw, tht) = measure(&g.mtext); - let (x, y) = (Vec2::from_angle(text_angle), Vec2::from_angle(text_angle).perp()); - let half = (tw / 2.0).max(th / 2.0); - let base = text_mid + y * (tht / 2.0 + gap / 2.0); - let sym: Vec = (0..=12) - .map(|i| { - let t = f64::from(i) / 6.0 - 1.0; - base + x * (t * half) + y * ((1.0 - t * t).max(0.0).sqrt() * th * 0.35) - }) - .collect(); - g.text_mark(sym); - } - } + DimKind::Linear { .. } | DimKind::Aligned => linear::draw_linear(&config, &mut g, &mut text_angle, &mut text_mid), + DimKind::Radius if d.marks.jogged.is_some() => variants::draw_jogged_radius(&config, &mut g, &mut text_angle, &mut text_mid), + DimKind::Radius | DimKind::Diameter => variants::draw_radius_or_diameter(&config, &mut g, &mut text_angle, &mut text_mid), + DimKind::Angular | DimKind::Angular3P => variants::draw_angular(&config, &mut g, &mut text_angle, &mut text_mid), + DimKind::Ordinate { x_type } => variants::draw_ordinate(&config, &mut g, &mut text_angle, &mut text_mid, x_type), + DimKind::ArcLength => variants::draw_arc_length(&config, &mut g, &mut text_angle, &mut text_mid), } // DIMTEDIT Angle: an explicit text rotation wins. if d.text_rotation != 0.0 && d.text_rotation.is_finite() { @@ -754,6 +376,8 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font g } +mod linear; +mod variants; /// `%%` codes in the plain value become their characters (`%%c` → ⌀, `%%p` → ±, `%%d` → °). fn decode_value(s: &str) -> String { cadcraft_fonts::decode_controls(s).into_iter().map(|(c, _, _)| c).collect() diff --git a/crates/render/src/dim/arrowheads.rs b/crates/render/src/dim/arrowheads.rs index 5940a27e9..c9c607a48 100644 --- a/crates/render/src/dim/arrowheads.rs +++ b/crates/render/src/dim/arrowheads.rs @@ -1,4 +1,4 @@ -use super::*; +use super::Vec2; /// The arrowheads drawn in code (DIMBLK names; our own geometry). #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/render/src/dim/linear.rs b/crates/render/src/dim/linear.rs new file mode 100644 index 000000000..40b684832 --- /dev/null +++ b/crates/render/src/dim/linear.rs @@ -0,0 +1,268 @@ +use super::text::linear_text; +use super::{DimConfig, DimGeometry, DimKind, LineRole, Vec2, ext_line, readable}; + +struct LinearDimState { + p1: Vec2, + p2: Vec2, + f1: Vec2, + f2: Vec2, + u: Vec2, + n: Vec2, + measurement: f64, + text_width: f64, + text_height: f64, + has_text: bool, + tick_like: bool, + arrow_width: f64, + arrows_inside: bool, + text_outside: bool, + just: u8, + centred: bool, + moved: bool, + outside_arrows: bool, + angle: f64, + half_extent: f64, + along_center: Vec2, + side_out: Vec2, + text_axis: Vec2, + offset: Vec2, + text_mid: Vec2, +} + +pub(super) fn draw_linear(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2) { + let mut state = prepare_linear(config, g, *text_mid_out); + position_linear_text(config, &mut state); + draw_linear_lines(config, g, &state); + *text_angle_out = state.angle; + *text_mid_out = state.text_mid; +} + +fn prepare_linear(config: &DimConfig<'_>, g: &mut DimGeometry, text_mid: Vec2) -> LinearDimState { + let d = config.dimension; + let st = config.style; + let p1 = d.p13.xy(); + let p2 = d.p14.xy(); + let on = d.defpt.xy(); + let direction = match d.kind { + DimKind::Linear { rotation } => Vec2::from_angle(rotation), + _ => (p2 - p1).normalized(), + }; + let direction = if direction == Vec2::ZERO || !direction.is_finite() { Vec2::X } else { direction }; + let oblique = d.marks.oblique.map(Vec2::from_angle).filter(|axis| axis.is_finite() && axis.cross(direction).abs() > 1e-6); + let foot = |point: Vec2| match oblique { + Some(axis) => point + axis * ((on - point).cross(direction) / axis.cross(direction)), + None => on + direction * (point - on).dot(direction), + }; + let (f1, f2) = (foot(p1), foot(p2)); + let raw_measurement = (p2 - p1).dot(direction).abs(); + g.mtext = linear_text(raw_measurement, d, st, ""); + g.measurement = raw_measurement; + let measurement = f1.dist(f2); + if !st.suppress_ext1 { + g.ext_n(ext_line(p1, f1, st, config.scale), false); + } + if !st.suppress_ext2 { + g.ext_n(ext_line(p2, f2, st, config.scale), true); + } + let dimension_axis = (f2 - f1).normalized(); + let u = if dimension_axis == Vec2::ZERO || !dimension_axis.is_finite() { direction } else { dimension_axis }; + let n = u.perp(); + let (text_width, text_height) = config.measure(&g.mtext); + let has_text = text_width > 0.0; + let tick_like = config.ticks || (config.first_arrow.is_tick() && config.second_arrow.is_tick()); + let arrow_width = if tick_like { 0.0 } else { config.arrow_size }; + let arrows_fit = tick_like || measurement > 2.0 * config.arrow_size + 1e-9; + let along_angle = readable(u.angle()); + let extent = |angle: f64| { + let axis = Vec2::from_angle(angle); + (text_width * axis.dot(u).abs() + text_height * axis.perp().dot(u).abs()) / 2.0 + }; + let inside_angle = if st.text_inside_horizontal { 0.0 } else { along_angle }; + let tvp = if st.text_above == 0 && st.text_vertical_pos.is_finite() { st.text_vertical_pos } else { 0.0 }; + let centred = st.text_above == 0 && tvp.abs() < 0.7; + let text_need = if centred { 2.0 * (extent(inside_angle) + config.gap) } else { 2.0 * extent(inside_angle) }; + let fits = !has_text || measurement >= text_need + 2.0 * arrow_width; + let text_fits = !has_text || measurement >= text_need; + let just = st.text_just.min(4); + let (text_inside, arrows_inside) = if d.user_text_pos || !has_text || just >= 3 { + (true, arrows_fit) + } else if fits { + (true, true) + } else if st.text_inside { + (true, false) + } else { + match st.fit { + 0 => (false, false), + 1 => (text_fits, false), + 2 => (false, arrows_fit), + _ if arrows_fit => (false, true), + _ => (text_fits, false), + } + }; + let text_outside = !text_inside; + LinearDimState { + p1, + p2, + f1, + f2, + u, + n, + measurement, + text_width, + text_height, + has_text, + tick_like, + arrow_width, + arrows_inside, + text_outside, + just, + centred, + moved: text_outside && st.text_move != 0, + outside_arrows: !arrows_inside && !st.suppress_outside_arrows, + angle: 0.0, + half_extent: 0.0, + along_center: f1.mid(f2), + side_out: n, + text_axis: Vec2::X, + offset: Vec2::ZERO, + text_mid, + } +} + +fn position_linear_text(config: &DimConfig<'_>, state: &mut LinearDimState) { + let st = config.style; + let along_angle = readable(state.u.angle()); + let extent = |angle: f64| { + let axis = Vec2::from_angle(angle); + (state.text_width * axis.dot(state.u).abs() + state.text_height * axis.perp().dot(state.u).abs()) / 2.0 + }; + let inside_angle = if st.text_inside_horizontal { 0.0 } else { along_angle }; + state.angle = if state.text_outside { if st.text_outside_horizontal { 0.0 } else { along_angle } } else { inside_angle }; + state.half_extent = extent(state.angle); + state.along_center = if state.text_outside { + state.f2 + state.u * (2.0 * config.arrow_size.max(config.tick_size) + config.gap + state.half_extent) + } else { + match state.just { + 1 => state.f1 + state.u * (state.arrow_width + config.gap + state.half_extent + config.arrow_size), + 2 => state.f2 - state.u * (state.arrow_width + config.gap + state.half_extent + config.arrow_size), + _ => state.f1.mid(state.f2), + } + }; + state.side_out = if (config.dimension.defpt.xy() - state.p1).dot(state.n) >= 0.0 { state.n } else { -state.n }; + state.text_axis = Vec2::from_angle(state.angle); + let off_along = |axis: Vec2| { + let glyph_extent = state.text_width * state.text_axis.dot(axis).abs() / 2.0 + + state.text_height * state.text_axis.perp().dot(axis).abs() * if st.text_above == 1 { 0.6 } else { 0.5 }; + config.gap + glyph_extent + }; + let above = if state.angle.abs() < 1e-12 && state.n.y.abs() > 1e-9 { + if state.n.y > 0.0 { state.n } else { -state.n } + } else if state.angle.abs() < 1e-12 { + if state.n.x < 0.0 { state.n } else { -state.n } + } else if state.text_axis.perp().dot(state.n) >= 0.0 { + state.n + } else { + -state.n + }; + state.offset = match st.text_above { + 0 => above * (if st.text_vertical_pos.is_finite() { st.text_vertical_pos } else { 0.0 } * config.text_height), + 2 => state.side_out * off_along(state.side_out), + 4 => -above * off_along(-above), + _ => above * off_along(above), + }; + if state.just >= 3 && !config.dimension.user_text_pos && state.has_text { + let (point, foot) = if state.just == 3 { (state.p1, state.f1) } else { (state.p2, state.f2) }; + let extension_axis = (foot - point).normalized(); + let extension_axis = if extension_axis == Vec2::ZERO { state.side_out } else { extension_axis }; + state.angle = readable(extension_axis.angle()); + let outward = if state.just == 3 { -state.u } else { state.u }; + state.text_mid = foot + + extension_axis * (st.ext_extend * config.scale + config.gap + state.text_width / 2.0) + + outward * (config.gap + state.text_height / 2.0); + } else if state.moved { + let mid = state.f1.mid(state.f2); + let lift = off_along(state.side_out) + 2.0 * config.arrow_size.max(config.tick_size); + state.text_mid = if st.text_move == 1 { + mid + state.side_out * lift + state.u * state.half_extent + } else { + mid + state.side_out * (lift + off_along(state.side_out)) + }; + } else if !config.dimension.user_text_pos { + state.text_mid = state.along_center + state.offset; + } +} + +fn draw_linear_lines(config: &DimConfig<'_>, g: &mut DimGeometry, state: &LinearDimState) { + let d = config.dimension; + let st = config.style; + let (u, n) = (state.u, state.n); + let dle = if state.tick_like { st.dim_line_extend * config.scale } else { 0.0 }; + let lo = if st.suppress_dim1 { state.measurement / 2.0 } else { f64::NEG_INFINITY }; + let hi = if st.suppress_dim2 { state.measurement / 2.0 } else { f64::INFINITY }; + let seg = |geometry: &mut DimGeometry, a: Vec2, b: Vec2| { + let (ta, tb) = ((a - state.f1).dot(u), (b - state.f1).dot(u)); + let (t0, t1) = (ta.min(tb).max(lo), ta.max(tb).min(hi)); + if t1 > t0 + 1e-12 { + geometry.dim(vec![state.f1 + u * t0, state.f1 + u * t1]); + } + }; + let inner = |geometry: &mut DimGeometry, a: Vec2, b: Vec2| { + if state.centred && state.has_text && !state.text_outside && state.just <= 2 && !d.user_text_pos { + let half = state.half_extent + config.gap; + let (t0, t1) = ((state.along_center - u * half - state.f1).dot(u), (state.along_center + u * half - state.f1).dot(u)); + if t0 > (a - state.f1).dot(u) + 1e-9 { + seg(geometry, a, state.f1 + u * t0); + } + if t1 < (b - state.f1).dot(u) - 1e-9 { + seg(geometry, state.f1 + u * t1, b); + } + } else { + seg(geometry, a, b); + } + }; + let to_text = |geometry: &mut DimGeometry, from: Vec2| { + let to = state.text_mid - state.offset - u * (state.half_extent + config.gap); + if state.text_outside && (to - from).dot(u) > 0.0 { + seg(geometry, from, from + u * (to - from).dot(u)); + } + }; + if state.arrows_inside { + let first_inset = if st.suppress_dim1 { 0.0 } else { config.end(g, config.first_arrow, state.f1, -u) }; + let second_inset = if st.suppress_dim2 { 0.0 } else { config.end(g, config.second_arrow, state.f2, u) }; + inner(g, state.f1 - u * dle + u * first_inset, state.f2 + u * dle - u * second_inset); + to_text(g, state.f2); + } else { + let stub = config.arrow_size * 2.0; + if state.outside_arrows { + if !st.suppress_dim1 { + config.end(g, config.first_arrow, state.f1, u); + } + if !st.suppress_dim2 { + config.end(g, config.second_arrow, state.f2, -u); + } + seg(g, state.f1 - u * stub, state.f1); + seg(g, state.f2, state.f2 + u * stub); + } + if st.force_line_inside { + inner(g, state.f1, state.f2); + } + to_text(g, if state.outside_arrows { state.f2 + u * stub } else { state.f2 }); + } + if (d.user_text_pos || state.moved) && st.text_move == 1 && state.has_text { + let base = state.f1.mid(state.f2); + let offset = (state.text_mid - base).dot(n).abs(); + if offset > state.text_height / 2.0 + config.gap + 1e-9 { + let side = if (base - state.text_mid).dot(state.text_axis) >= 0.0 { state.text_axis } else { -state.text_axis }; + g.dim(vec![base, state.text_mid + side * (state.text_width / 2.0 + config.gap)]); + } + } + if let Some(jog) = d.marks.jog_line { + let at = state.f1 + u * (jog.xy() - state.f1).dot(u); + let height = 1.5 * config.text_height; + if at.is_finite() && height > 0.0 { + let radius = height / 2.0; + g.cut(at, radius, Some(LineRole::Dim)); + g.dim(vec![at - u * radius, at - u * (radius / 4.0) + n * (height / 2.0), at + u * (radius / 4.0) - n * (height / 2.0), at + u * radius]); + } + } +} diff --git a/crates/render/src/dim/tests.rs b/crates/render/src/dim/tests.rs index 4f9143874..fd439097c 100644 --- a/crates/render/src/dim/tests.rs +++ b/crates/render/src/dim/tests.rs @@ -153,6 +153,19 @@ fn text_above_and_centred() { assert!(outside2.text_pos.y < 5.0); } +#[test] +fn dimtvp_uses_the_dimension_text_height() { + let d = lin((0.0, 0.0), (10.0, 0.0), (0.0, 5.0), 0.0); + let style = st_with(|s| { + s.text_above = 0; + s.text_vertical_pos = 2.0; + s.text_height = 0.18; + }); + let font = DimText { fixed_height: 0.8, width_factor: 1.0, ..Default::default() }; + let g = dimension_geometry_with(&d, &style, 1.0, &font); + assert!((g.text_pos.y - 6.6).abs() < 1e-6, "text y={} height={}", g.text_pos.y, g.text_height); +} + #[test] fn dimtad_one_uses_autocad_text_clearance() { let d = lin((0.0, 0.10), (0.0, 0.70), (0.30, 0.40), std::f64::consts::FRAC_PI_2); diff --git a/crates/render/src/dim/text.rs b/crates/render/src/dim/text.rs index fb3d6eda7..91e6b568c 100644 --- a/crates/render/src/dim/text.rs +++ b/crates/render/src/dim/text.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DimStyle, Dimension, NumFormat, TextFont, format_decimal, format_linear}; /// Format a linear measurement per the style (plain text). pub fn format_linear_value(v: f64, st: &DimStyle) -> String { diff --git a/crates/render/src/dim/variants.rs b/crates/render/src/dim/variants.rs new file mode 100644 index 000000000..240b5e88d --- /dev/null +++ b/crates/render/src/dim/variants.rs @@ -0,0 +1,291 @@ +use super::text::{apply_override, inspected, linear_text}; +use super::{Arrowhead, DimConfig, DimGeometry, DimKind, Vec2, ext_line, readable}; +use crate::units::format_angle_with_separator; +use cadcraft_geom::{Arc, ccw_sweep}; + +pub(super) fn draw_jogged_radius(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2) { + let d = config.dimension; + let st = config.style; + let asz = config.arrow_size; + let gap = config.gap; + let tsz = config.tick_size; + let blk2 = config.second_arrow; + let measure = |text: &str| config.measure(text); + let end = |geometry: &mut DimGeometry, kind: Arrowhead, tip: Vec2, direction: Vec2| config.end(geometry, kind, tip, direction); + let mut text_angle = 0.0; + let mut text_mid = *text_mid_out; + 'jogged: { + // DIMJOGGED: from the arrowhead on the arc toward the centre, a jog at DIMJOGANG, + // then on to the overriding centre (parallel to the first part). + let center = d.defpt.xy(); + let tip = d.p15.xy(); + let r = center.dist(tip); + let j = d.marks.jogged.clone().unwrap_or_default(); + let (o, jp) = (j.center.xy(), j.jog.xy()); + g.mtext = linear_text(r, d, st, "R"); + // A NaN or infinite radius leaves no line to draw (and NaN is no bound for `clamp`). + if !r.is_finite() { + break 'jogged; + } + let (tw, _) = measure(&g.mtext); + let u = (tip - center).normalized(); + let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u }; + let n = u.perp(); + let h = (o - center).dot(n); + let ang = if st.jog_angle.is_finite() { st.jog_angle.clamp(5.0, 90.0) } else { 45.0 }.to_radians(); + let run = h.abs() / ang.tan().max(1e-6); + let tj = (jp - center).dot(u).clamp(0.0, r); + let (j1, j2) = (center + u * (tj + run / 2.0), center + u * (tj - run / 2.0) + n * h); + g.dim(vec![tip, j1, j2, o]); + end(g, blk2, tip, u); + let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) }; + if tpos.dist(center) > r { + g.dim(vec![tip, tpos]); + } + text_angle = if st.text_outside_horizontal { 0.0 } else { readable(u.angle()) }; + if !d.user_text_pos { + let x = Vec2::from_angle(text_angle); + let x = if x.dot(u) >= 0.0 { x } else { -x }; + text_mid = tpos + x * (tw / 2.0 + gap); + } + } + *text_angle_out = text_angle; + *text_mid_out = text_mid; +} + +pub(super) fn draw_radius_or_diameter(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2) { + let d = config.dimension; + let st = config.style; + let k = config.scale; + let asz = config.arrow_size; + let gap = config.gap; + let tsz = config.tick_size; + let blk1 = config.first_arrow; + let blk2 = config.second_arrow; + let measure = |text: &str| config.measure(text); + let end = |geometry: &mut DimGeometry, kind: Arrowhead, tip: Vec2, direction: Vec2| config.end(geometry, kind, tip, direction); + let text_angle: f64; + let mut text_mid = *text_mid_out; + + let a = d.defpt.xy(); + let b = d.p15.xy(); + let radius = matches!(d.kind, DimKind::Radius); + let (center, r) = if radius { (a, a.dist(b)) } else { (a.mid(b), a.dist(b) / 2.0) }; + let meas = if radius { r } else { 2.0 * r }; + g.mtext = linear_text(meas, d, st, if radius { "R" } else { "%%c" }); + g.measurement = meas; + let (tw, tht) = measure(&g.mtext); + let u = (b - center).normalized(); + let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u }; + let tip = center + u * r; + let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) }; + // Text placed inside the circle (a dimension line location picked inside): the text + // sits on the dimension line, which runs from the text out to the arrowhead (for a + // diameter it spans the circle, broken around the text). DIMTIH turns it horizontal. + let inside = d.user_text_pos && text_mid.dist(center) < r; + let horizontal = if inside { st.text_inside_horizontal } else { st.text_outside_horizontal }; + if inside { + let tr = Vec2::from_angle(if horizontal { 0.0 } else { readable(u.angle()) }); + let half = if tw > 0.0 { (tw * tr.dot(u).abs() + tht * tr.perp().dot(u).abs()) / 2.0 + gap } else { 0.0 }; + let start = if radius { center } else { center - u * r }; + let (t0, t1) = ((text_mid - start).dot(u) - half, (text_mid - start).dot(u) + half); + if !radius { + if t0 > 0.0 { + g.dim(vec![start, start + u * t0]); + } + end(g, blk1, start, -u); + } + if t1 < tip.dist(start) { + g.dim(vec![start + u * t1.max(0.0), tip]); + } + } else { + // Text outside: a diameter line spans the circle and a radius line runs in to the + // centre only with DIMTOFL; otherwise just the leader to the text. + if st.force_line_inside { + let start = if radius { center } else { center - u * r }; + g.dim(vec![start, tip]); + if !radius { + end(g, blk1, start, -u); + } + } + // Text the user placed: the leader stops short of it by the offset the default + // placement leaves between leader end and text. + let lead = if !d.user_text_pos { + tpos + } else if horizontal { + text_mid - Vec2::new(tw / 2.0 + gap, 0.0) * if u.x >= 0.0 { 1.0 } else { -1.0 } + } else { + text_mid - u * (tw / 2.0 + gap) + }; + g.dim(vec![tip, lead]); + } + end(g, blk2, tip, u); + if horizontal { + text_angle = 0.0; + let sx = if u.x >= 0.0 { 1.0 } else { -1.0 }; + if !d.user_text_pos { + text_mid = tpos + Vec2::new(tw / 2.0 + gap, 0.0) * sx; + } + } else { + text_angle = readable(u.angle()); + if !d.user_text_pos { + text_mid = tpos + u * (tw / 2.0 + gap); + } + } + // The DIMCEN mark is drawn only when no dimension line runs inside the circle (text + // outside without DIMTOFL), never with a line through the centre. + let line_outside = !st.force_line_inside && tpos.dist(center) >= r; + if line_outside && st.center_mark != 0.0 && st.center_mark.is_finite() { + let c = st.center_mark.abs() * k; + g.ext(vec![center - Vec2::X * c, center + Vec2::X * c]); + g.ext(vec![center - Vec2::Y * c, center + Vec2::Y * c]); + if st.center_mark < 0.0 && r > 2.0 * c { + // Centre lines out past the circle. + for dv in [Vec2::X, -Vec2::X, Vec2::Y, -Vec2::Y] { + g.ext(vec![center + dv * (2.0 * c), center + dv * (r + c)]); + } + } + } + *text_angle_out = text_angle; + *text_mid_out = text_mid; +} + +pub(super) fn draw_angular(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2) { + let d = config.dimension; + let st = config.style; + let k = config.scale; + let gap = config.gap; + let blk1 = config.first_arrow; + let blk2 = config.second_arrow; + let measure = |text: &str| config.measure(text); + let end = |geometry: &mut DimGeometry, kind: Arrowhead, tip: Vec2, direction: Vec2| config.end(geometry, kind, tip, direction); + let mut text_mid = *text_mid_out; + + // Angular3P: p15 vertex, p13/p14 on the legs, defpt on the arc. + // Angular (2 lines): lines p13-p14 and defpt-p15; p16 on the arc. + let (vertex, a1, a2, arc_pt) = if matches!(d.kind, DimKind::Angular3P) { + (d.p15.xy(), d.p13.xy(), d.p14.xy(), d.defpt.xy()) + } else { + let v = cadcraft_geom::line_line_infinite(d.p13.xy(), d.p14.xy(), d.defpt.xy(), d.p15.xy()).map(|x| x.0).unwrap_or(d.p14.xy()); + (v, d.p14.xy(), d.p15.xy(), d.p16.xy()) + }; + let r = vertex.dist(arc_pt).max(1e-9); + let s = vertex.angle_to(a1); + let e = vertex.angle_to(a2); + let arc = if cadcraft_geom::angle_in_sweep(vertex.angle_to(arc_pt), s, e) { Arc::new(vertex, r, s, e) } else { Arc::new(vertex, r, e, s) }; + let sweep = ccw_sweep(arc.start, arc.end); + g.measurement = sweep; + // DIMAZIN 1 / 2 suppress leading / trailing zeros (DIMZIN's bits 4 / 8). + let azin = (st.angular_zero_suppression & 3) * 4; + let txt = format_angle_with_separator(sweep, st.angular_unit, st.angular_decimals, azin, &st.decimal_separator); + // DIMPOST is a linear-dimension prefix/suffix; angles have their own format only. + g.mtext = inspected(apply_override(&txt, d), d); + let mut pts = Vec::new(); + arc.tessellate(r * 1e-3, &mut pts); + g.dim(pts); + let sp = arc.start_point(); + let ep = arc.end_point(); + // The arc's ends with the outward tangents there, paired with leg 1 and leg 2: when + // the arc location is outside the leg 1 → leg 2 sweep the arc runs from leg 2 to + // leg 1, and each leg keeps its own extension line, DIMSE1/2 and DIMBLK1/2. + let (start, finish) = ((sp, -(sp - vertex).perp().normalized()), (ep, (ep - vertex).perp().normalized())); + let ((p1, dir1), (p2, dir2)) = if cadcraft_geom::angle_in_sweep(vertex.angle_to(arc_pt), s, e) { (start, finish) } else { (finish, start) }; + if !st.suppress_ext1 { + g.ext_n(ext_line(vertex + (p1 - vertex).normalized() * vertex.dist(a1).min(r), p1, st, k), false); + } + if !st.suppress_ext2 { + g.ext_n(ext_line(vertex + (p2 - vertex).normalized() * vertex.dist(a2).min(r), p2, st, k), true); + } + end(g, blk1, p1, dir1); + end(g, blk2, p2, dir2); + let (tw, tht) = measure(&g.mtext); + let mid = arc.mid_point(); + let radial = (mid - vertex).normalized(); + let text_angle = if st.text_inside_horizontal { 0.0 } else { readable(radial.perp().angle()) }; + if !d.user_text_pos { + let tr = Vec2::from_angle(text_angle); + let off = gap + (tw * tr.dot(radial).abs() + tht * tr.perp().dot(radial).abs()) / 2.0; + text_mid = mid + radial * off; + } + *text_angle_out = text_angle; + *text_mid_out = text_mid; +} + +pub(super) fn draw_ordinate(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2, x_type: bool) { + let d = config.dimension; + let st = config.style; + let gap = config.gap; + let measure = |text: &str| config.measure(text); + let mut text_angle = 0.0; + let mut text_mid = *text_mid_out; + + let feature = d.p13.xy(); + let leader_end = d.p14.xy(); + let origin = d.defpt.xy(); + let meas = if x_type { (feature.x - origin.x).abs() } else { (feature.y - origin.y).abs() }; + g.mtext = linear_text(meas, d, st, ""); + g.measurement = meas; + let (tw, _) = measure(&g.mtext); + g.dim(vec![feature, leader_end]); + if x_type { + text_angle = std::f64::consts::FRAC_PI_2; + } + if !d.user_text_pos { + let dirv = (leader_end - feature).normalized(); + let dirv = if dirv == Vec2::ZERO { if x_type { Vec2::Y } else { Vec2::X } } else { dirv }; + text_mid = leader_end + dirv * (tw / 2.0 + gap); + } + *text_angle_out = text_angle; + *text_mid_out = text_mid; +} + +pub(super) fn draw_arc_length(config: &DimConfig<'_>, g: &mut DimGeometry, text_angle_out: &mut f64, text_mid_out: &mut Vec2) { + let d = config.dimension; + let st = config.style; + let k = config.scale; + let th = config.text_height; + let gap = config.gap; + let blk1 = config.first_arrow; + let blk2 = config.second_arrow; + let measure = |text: &str| config.measure(text); + let end = |geometry: &mut DimGeometry, kind: Arrowhead, tip: Vec2, direction: Vec2| config.end(geometry, kind, tip, direction); + let mut text_mid = *text_mid_out; + + let c = d.p15.xy(); + let r = c.dist(d.p13.xy()); + let arc = Arc::new(c, c.dist(d.defpt.xy()).max(1e-9), c.angle_to(d.p13.xy()), c.angle_to(d.p14.xy())); + let meas = r * arc.sweep(); + // DIMARCSYM: the arc symbol before the text (0), over it (1) or none (2). + g.mtext = linear_text(meas, d, st, if st.arc_symbol == 0 { "⌒" } else { "" }); + g.measurement = meas; + let mut pts = Vec::new(); + arc.tessellate(arc.radius * 1e-3, &mut pts); + g.dim(pts); + for (i, (p, q)) in [(d.p13.xy(), arc.start_point()), (d.p14.xy(), arc.end_point())].into_iter().enumerate() { + g.ext_n(ext_line(p, q, st, k), i == 1); + } + end(g, blk1, arc.start_point(), -(arc.start_point() - c).perp().normalized()); + end(g, blk2, arc.end_point(), (arc.end_point() - c).perp().normalized()); + let m = arc.mid_point(); + let radial = (m - c).normalized(); + let text_angle = if st.text_inside_horizontal { 0.0 } else { readable(radial.perp().angle()) }; + if !d.user_text_pos { + text_mid = m + radial * (gap + th / 2.0); + } + if st.arc_symbol == 1 { + // A small arc over the text. + let (tw, tht) = measure(&g.mtext); + let (x, y) = (Vec2::from_angle(text_angle), Vec2::from_angle(text_angle).perp()); + let half = (tw / 2.0).max(th / 2.0); + let base = text_mid + y * (tht / 2.0 + gap / 2.0); + let sym: Vec = (0..=12) + .map(|i| { + let t = f64::from(i) / 6.0 - 1.0; + base + x * (t * half) + y * ((1.0 - t * t).max(0.0).sqrt() * th * 0.35) + }) + .collect(); + g.text_mark(sym); + } + *text_angle_out = text_angle; + *text_mid_out = text_mid; +} diff --git a/crates/render/src/dim_draw.rs b/crates/render/src/dim_draw.rs index b31ff13b4..b83a33128 100644 --- a/crates/render/src/dim_draw.rs +++ b/crates/render/src/dim_draw.rs @@ -2,7 +2,7 @@ use super::display::Kind; use super::draw::stroke_with; use super::props::default_lineweight; use super::text_entity::{layout_mtext_entity, place_text_entity, text_style}; -use super::*; +use super::{Builder, Color, Ctx, DimGeometry, DimText, Drawing, Entity, EntityKind, LineRole, Lineweight, Vec2, dim, ink}; /// The font settings of a dimension style's text style. pub fn dim_text(d: &Drawing, st: &cadcraft_doc::DimStyle) -> DimText { diff --git a/crates/render/src/display.rs b/crates/render/src/display.rs index b260daf8a..e6ba6b7e9 100644 --- a/crates/render/src/display.rs +++ b/crates/render/src/display.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Bounds2, Handle, Pen, Rgb, Sheet, Vec2, outline}; /// Handle carried by model-space geometry drawn inside a paper-space viewport (it belongs to no /// single paper-space entity, so selection highlighting never matches it). diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index 5ce0bd6c0..04e2651c5 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -3,7 +3,10 @@ use super::dim_draw::{MAX_FILL_SOLIDS, MAX_FILL_TEXTS, block_text_box, dim_text_ use super::display::{Kind, NO_LAYER, VIEWPORT_CONTENT}; use super::props::{resolve, transparency}; use super::text_entity::{mtext, place_text_entity}; -use super::*; +use super::{ + Builder, Ctx, DimGeometry, Drawing, Entity, EntityKind, Handle, Ink, Mat3, Polyline, Prim, Vec2, dim, fill, hatch, linetype, linetype_text, + mline, point, table, tolerance, wide, +}; /// The layer `e` is drawn on, as an index in the layer table: layer 0 inside a block is the /// block reference's layer. @@ -114,50 +117,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) { mtext(b, ctx, t, rgb, lw); } EntityKind::Insert(ins) => insert(b, ctx, e, ins, rgb), - EntityKind::Dimension(dm) => { - if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n)) - && ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH - { - let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::IDENTITY) }; - // DIMTFILL: the block's MTEXT carries no background mask here, so a background - // around each of the block's texts is drawn before the block's first text or first - // line of the text's marks, except for a text that a SOLID of the block covers. - let style = ctx.d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides); - let g = if style.text_fill != 0 { dimension_in(ctx.d, dm) } else { DimGeometry::default() }; - let gap = style.text_gap.abs() * style.effective_scale(ctx.d.header.f64("DIMSCALE", 1.0)); - let boxes: Vec> = if g.text_fill.is_empty() { - Vec::new() - } else { - let solids: Vec<&Entity> = - blk.entities.iter().map(|be| &**be).filter(|be| matches!(be.kind, EntityKind::Solid(_))).take(MAX_FILL_SOLIDS).collect(); - // A text's own box (no gap): a SOLID holding all its corners is its background. - let covered = - |be: &Entity| block_text_box(ctx.d, be, 0.0).is_some_and(|t| solids.iter().any(|s| t.iter().all(|p| solid_covers(s, *p)))); - blk.entities - .iter() - .filter(|be| matches!(be.kind, EntityKind::MText(_) | EntityKind::Text(_))) - .take(MAX_FILL_TEXTS) - .filter(|be| !covered(be)) - .filter_map(|be| block_text_box(ctx.d, be, gap)) - .collect() - }; - let mut fill_pending = !boxes.is_empty(); - for be in blk.entities.iter() { - if !b.expand() { - break; - } - if fill_pending && starts_text_marks(be, &g) { - for corners in &boxes { - dim_text_fill(b, ctx, e, &style, corners); - } - fill_pending = false; - } - entity(b, &sub, be); - } - } else { - dimension(b, ctx, e, dm, lw, lt.as_ref(), ltscale); - } - } + EntityKind::Dimension(dm) => draw_dimension(b, ctx, e, dm, lw, lt.as_ref(), ltscale), EntityKind::Hatch(h) => hatch::draw(b, ctx, e, h, rgb, lw, tol), EntityKind::MLine(m) => mline::draw(b, ctx, e, m, rgb, lw, tol), EntityKind::LwPolyline(p) if p.const_width > 0.0 || p.vertices.iter().any(|v| v.start_width > 0.0 || v.end_width > 0.0) => { @@ -221,29 +181,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) { b.polyline(ctx, rgb, lw, &[q[0], q[1], q[2], q[3], q[0]]); } } - EntityKind::Wipeout(w) => { - // Hide what was drawn before it (draw order), then the frame per WIPEOUTFRAME: - // 0 = none, 1 = shown and plotted, 2 = shown but not plotted. - if b.opts.fill { - let n = b.list.prims.len(); - b.tris(ctx, rgb, &fill::triangulate_evenodd(std::slice::from_ref(&w.boundary))); - if let Some(p) = b.list.prims.get_mut(n) { - p.kind = Kind::Mask; - } - } - let frame = match ctx.d.header.i64("WIPEOUTFRAME", 1) { - 0 => false, - 2 => !b.plotting, - _ => true, - }; - if frame { - let mut pts = w.boundary.clone(); - if let Some(f) = pts.first().copied() { - pts.push(f); - } - b.polyline(ctx, rgb, lw, &pts); - } - } + EntityKind::Wipeout(w) => draw_wipeout(b, ctx, w, rgb, lw), EntityKind::Image(i) => { let o = i.insert.xy(); let u = i.u.xy() * i.size.x; @@ -272,29 +210,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) { b.tris(ctx, rgb, &g.tris); } } - EntityKind::MLeader(m) => { - // Straight or spline leader lines; block content draws like a block reference. - for pts in m.leader_paths() { - b.polyline(ctx, rgb, lw, &pts); - if let (Some(a), Some(n)) = (pts.first(), pts.get(1)) { - b.tris(ctx, rgb, &dim::arrow(*a, (*a - *n).normalized(), m.arrow_size)); - } - } - if let Some(t) = &m.text { - let land = m.landing.xy(); - let dir = if t.insert.x >= land.x { 1.0 } else { -1.0 }; - b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]); - mtext(b, ctx, t, rgb, lw); - } - if let Some(ins) = &m.block { - if m.dogleg > 0.0 && !m.leaders.is_empty() { - let land = m.landing.xy(); - let dir = if ins.insert.x >= land.x { 1.0 } else { -1.0 }; - b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]); - } - insert(b, ctx, e, ins, rgb); - } - } + EntityKind::MLeader(m) => draw_mleader(b, ctx, e, m, rgb, lw), EntityKind::Point(p) => { // Relative sizes follow the view: undo the block / viewport scale. let view = b.opts.view_height / ctx.xf.scale_factor().max(1e-12); @@ -338,6 +254,98 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) { } } +fn draw_mleader(b: &mut Builder, ctx: &Ctx, e: &Entity, m: &cadcraft_doc::MLeader, rgb: Ink, lw: f32) { + // Straight or spline leader lines; block content draws like a block reference. + for pts in m.leader_paths() { + b.polyline(ctx, rgb, lw, &pts); + if let (Some(a), Some(n)) = (pts.first(), pts.get(1)) { + b.tris(ctx, rgb, &dim::arrow(*a, (*a - *n).normalized(), m.arrow_size)); + } + } + if let Some(t) = &m.text { + let land = m.landing.xy(); + let dir = if t.insert.x >= land.x { 1.0 } else { -1.0 }; + b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]); + mtext(b, ctx, t, rgb, lw); + } + if let Some(ins) = &m.block { + if m.dogleg > 0.0 && !m.leaders.is_empty() { + let land = m.landing.xy(); + let dir = if ins.insert.x >= land.x { 1.0 } else { -1.0 }; + b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]); + } + insert(b, ctx, e, ins, rgb); + } +} + +fn draw_wipeout(b: &mut Builder, ctx: &Ctx, w: &cadcraft_doc::Wipeout, rgb: Ink, lw: f32) { + // Hide what was drawn before it (draw order), then the frame per WIPEOUTFRAME: + // 0 = none, 1 = shown and plotted, 2 = shown but not plotted. + if b.opts.fill { + let n = b.list.prims.len(); + b.tris(ctx, rgb, &fill::triangulate_evenodd(std::slice::from_ref(&w.boundary))); + if let Some(p) = b.list.prims.get_mut(n) { + p.kind = Kind::Mask; + } + } + let frame = match ctx.d.header.i64("WIPEOUTFRAME", 1) { + 0 => false, + 2 => !b.plotting, + _ => true, + }; + if frame { + let mut pts = w.boundary.clone(); + if let Some(f) = pts.first().copied() { + pts.push(f); + } + b.polyline(ctx, rgb, lw, &pts); + } +} + +fn draw_dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimension, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64) { + if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n)) + && ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH + { + let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::IDENTITY) }; + // DIMTFILL: the block's MTEXT carries no background mask here, so a background + // around each of the block's texts is drawn before the block's first text or first + // line of the text's marks, except for a text that a SOLID of the block covers. + let style = ctx.d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides); + let g = if style.text_fill != 0 { dimension_in(ctx.d, dm) } else { DimGeometry::default() }; + let gap = style.text_gap.abs() * style.effective_scale(ctx.d.header.f64("DIMSCALE", 1.0)); + let boxes: Vec> = if g.text_fill.is_empty() { + Vec::new() + } else { + let solids: Vec<&Entity> = + blk.entities.iter().map(|be| &**be).filter(|be| matches!(be.kind, EntityKind::Solid(_))).take(MAX_FILL_SOLIDS).collect(); + // A text's own box (no gap): a SOLID holding all its corners is its background. + let covered = |be: &Entity| block_text_box(ctx.d, be, 0.0).is_some_and(|t| solids.iter().any(|s| t.iter().all(|p| solid_covers(s, *p)))); + blk.entities + .iter() + .filter(|be| matches!(be.kind, EntityKind::MText(_) | EntityKind::Text(_))) + .take(MAX_FILL_TEXTS) + .filter(|be| !covered(be)) + .filter_map(|be| block_text_box(ctx.d, be, gap)) + .collect() + }; + let mut fill_pending = !boxes.is_empty(); + for be in blk.entities.iter() { + if !b.expand() { + break; + } + if fill_pending && starts_text_marks(be, &g) { + for corners in &boxes { + dim_text_fill(b, ctx, e, &style, corners); + } + fill_pending = false; + } + entity(b, &sub, be); + } + } else { + dimension(b, ctx, e, dm, lw, lt, ltscale); + } +} + /// Draw a polyline with a linetype (None: continuous). pub(crate) fn stroke_with(b: &mut Builder, ctx: &Ctx, rgb: Ink, lw: f32, lt: Option<&cadcraft_doc::Linetype>, ltscale: f64, pts: &[Vec2]) { match lt { diff --git a/crates/render/src/pipeline.rs b/crates/render/src/pipeline.rs index 3427c61a4..8cbcb95b9 100644 --- a/crates/render/src/pipeline.rs +++ b/crates/render/src/pipeline.rs @@ -3,7 +3,7 @@ use super::display::append; use super::display::{DisplayList, Kind, NO_LAYER, PlotText, VIEWPORT_CONTENT}; use super::draw::entity; use super::viewports::{MAX_VIEWPORTS, viewport}; -use super::*; +use super::{Bounds2, Builder, Color, Ctx, Drawing, Entity, EntityKind, Handle, Lineweight, Mat3, Space, Vec2, paper, pen, plotstyle, point}; /// Build options. #[derive(Clone, Debug)] diff --git a/crates/render/src/props.rs b/crates/render/src/props.rs index 91419b0cb..ad001b40c 100644 --- a/crates/render/src/props.rs +++ b/crates/render/src/props.rs @@ -1,5 +1,5 @@ use super::display::MAX_TRANSPARENCY; -use super::*; +use super::{Color, Ctx, Drawing, Entity, Ink, Lineweight, ink}; /// An object's colour, lineweight (mm), linetype, linetype scale, visibility and the transparency /// of the layer it is drawn on (for ByLayer, see [`transparency`]). diff --git a/crates/render/src/text_entity.rs b/crates/render/src/text_entity.rs index 54b556139..f7989e2c8 100644 --- a/crates/render/src/text_entity.rs +++ b/crates/render/src/text_entity.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Bounds2, Builder, Color, Ctx, Drawing, Ink, Rgb, ink}; /// The text style of an entity (Standard defaults when missing). pub(crate) fn text_style(d: &Drawing, name: &str) -> cadcraft_doc::TextStyle { diff --git a/crates/render/src/viewports.rs b/crates/render/src/viewports.rs index e78106fa1..6f1e57517 100644 --- a/crates/render/src/viewports.rs +++ b/crates/render/src/viewports.rs @@ -5,7 +5,7 @@ use super::draw::entity; #[cfg(not(target_arch = "wasm32"))] use super::pipeline::PARALLEL_CHUNK; use super::pipeline::{Options, style_table, top_ctx, with_point_view}; -use super::*; +use super::{Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, MAX_BLOCK_EXPANSION, Mat3, Space, Vec2, clip, paper, pen, plotstyle}; /// Upper bound on viewports drawn per layout (hostile files). pub(crate) const MAX_VIEWPORTS: usize = 256; diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 787aa1a2f..fcc464be3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -217,6 +217,507 @@ pub struct SnapCycle { index: usize, } +#[derive(Clone, Copy)] +struct PointerInput { + hover_pos: Option, + scroll: egui::Vec2, + mods: egui::Modifiers, + middle_down: bool, + pressed_primary: bool, + pressed_secondary: bool, + dbl_middle: bool, + dbl_primary: bool, +} + +struct Navigation<'a> { + rect: Rect, + tiles: &'a [(Rect, cadcraft_engine::ModelTile)], + response: &'a egui::Response, + xf: Xf, + pointer: PointerInput, +} + +fn navigate_canvas(app: &mut CadApp, ui: &mut egui::Ui, nav: &Navigation<'_>) -> Option<(bool, bool)> { + let p = nav.pointer; + let inside = p.hover_pos.is_some_and(|at| nav.rect.contains(at) && ui.ctx().layer_id_at(at) == Some(ui.layer_id()) && nav.response.hovered()); + let on_menu = p.hover_pos.is_some_and(|at| over_canvas_menu(app, at)); + app.session.ctrl_down = p.mods.ctrl; + ui.input(|i| { + for event in &i.raw.events { + if let egui::Event::MouseWheel { unit, .. } = event { + app.canvas.scroll_pans = cfg!(target_os = "macos") && *unit == egui::MouseWheelUnit::Point; + } + } + }); + handle_scroll_zoom(app, ui, nav, inside); + handle_button_navigation(app, nav, inside); + if nav.response.dragged_by(egui::PointerButton::Primary) && app.session.realtime_zoom_active() { + let _ = app.session.realtime_zoom(f64::from(nav.response.drag_delta().y)); + } + if p.pressed_primary + && let Some(at) = p.hover_pos + && !nav.rect.contains(at) + && let Some(i) = nav.tiles.iter().position(|(r, _)| r.contains(at)) + { + if let Err(e) = app.session.click_tile(i) { + app.session.echo(e.to_string()); + } + ui.ctx().request_repaint(); + return None; + } + Some((inside, on_menu)) +} + +fn handle_scroll_zoom(app: &mut CadApp, ui: &mut egui::Ui, nav: &Navigation<'_>, inside: bool) { + let p = nav.pointer; + if inside + && app.canvas.scroll_pans + && p.scroll != egui::Vec2::ZERO + && let Ok(st) = app.session.state_mut() + { + let view = st.view(); + st.set_view_quiet(cadcraft_engine::View { + center: view.center - Vec2::new(f64::from(p.scroll.x) / nav.xf.scale, -f64::from(p.scroll.y) / nav.xf.scale), + height: view.height, + }); + } + let opts = ui.ctx().options(|o| o.input_options); + let wheel = ui.input(|i| wheel_zoom_points(&i.raw.events, &opts, nav.rect.height())); + if inside + && !app.canvas.scroll_pans + && wheel != 0.0 + && let Some(at) = p.hover_pos + { + let factor = app.session.wheel_zoom_factor(f64::from(wheel)); + let about = nav.xf.to_world(at); + let _ = app.session.zoom_about(factor, about); + } + let pinch = ui.input(|i| crate::animation::zoom_delta(i, &opts, app.shows_animations())); + if inside + && (pinch - 1.0).abs() > 1e-4 + && let Some(at) = p.hover_pos + { + let _ = app.session.zoom_about(f64::from(pinch), nav.xf.to_world(at)); + } +} + +fn handle_button_navigation(app: &mut CadApp, nav: &Navigation<'_>, inside: bool) { + let p = nav.pointer; + let middle_pressed = nav.response.ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Middle)); + let focused = nav.response.ctx.input(|i| i.focused); + if p.middle_down + && focused + && (app.canvas.pan_last.is_some() || (middle_pressed && inside)) + && let Some(at) = p.hover_pos + { + if let Some(last) = app.canvas.pan_last { + let delta = at - last; + if let Ok(st) = app.session.state_mut() { + let view = st.view(); + st.set_view_quiet(cadcraft_engine::View { + center: view.center - Vec2::new(f64::from(delta.x) / nav.xf.scale, -f64::from(delta.y) / nav.xf.scale), + height: view.height, + }); + } + } + app.canvas.pan_last = Some(at); + } else { + app.canvas.pan_last = None; + } + if p.dbl_middle && inside { + let _ = app.session.zoom_extents(); + } +} + +struct CanvasPaint<'a> { + app: &'a mut CadApp, + ui: &'a mut egui::Ui, + painter: &'a egui::Painter, + tokens: Tokens, + full: Rect, + tiles: &'a [(Rect, cadcraft_engine::ModelTile)], + rect: Rect, + xf: Xf, + xf_paper: Xf, + view: cadcraft_engine::View, + active_vp: Option, + active_clip: Option, + inside: bool, + on_menu: bool, + hover_pos: Option, + raw_world: Option, +} + +fn paint_content_highlights(frame: &mut CanvasPaint<'_>, bg: Rgb, sel: &[Handle], preview_look: Look, selection_look: Option) { + let app = &mut *frame.app; + let painter = frame.painter; + let xf_paper = frame.xf_paper; + let rect = frame.rect; + let xf = frame.xf; + let t = frame.tokens; + let ui = &mut *frame.ui; + let active_vp = &frame.active_vp; + let active_clip = &frame.active_clip; + let scale = frame.xf.scale; + // The active viewport's contents, drawn apart from the layout's list, under its own objects. + // `masked`: its outline is in the stencil buffer this frame. + let mut masked = false; + if let Some(f) = ensure_vp_content(app, &xf_paper, ui.ctx()) { + let gpu = app.canvas.gpu.is_some(); + let clipped = painter.with_clip_rect(f.on_screen.intersect(rect)); + draw_vp_content(&mut app.canvas.vp, gpu, &clipped, &f, &xf_paper, bg, app.session.settings.lwdisplay); + masked = gpu && f.outline.is_some(); + } + if app.canvas.gpu.is_some() { + draw_list_gpu(&mut app.canvas, painter, &xf_paper, bg, app.session.settings.lwdisplay); + } else if let Some(list) = &app.canvas.list { + draw_list(painter, &xf_paper, list, bg, app.session.settings.lwdisplay); + } + if let Some(vp) = &active_vp { + // The active viewport: a heavy border, highlights in model units clipped to it. + let r = Rect::from_two_pos( + xf_paper.to_screen(Vec2::new(vp.center.x - vp.width / 2.0, vp.center.y + vp.height / 2.0)), + xf_paper.to_screen(Vec2::new(vp.center.x + vp.width / 2.0, vp.center.y - vp.height / 2.0)), + ); + let heavy = Stroke::new(3.0, Color32::from_rgb(0x2a, 0x2a, 0x2a)); + match &active_clip { + Some(c) => { + let mut pts: Vec = c.polygon().iter().map(|q| xf_paper.to_screen(*q)).collect(); + pts.extend(pts.first().copied()); + painter.add(egui::Shape::line(pts, heavy)); + } + None => { + painter.rect_stroke(r, 0.0, heavy, egui::StrokeKind::Middle); + } + } + if let Ok(st) = app.session.state() { + // A clipped viewport's outline bounds (its outline is masked when `masked`). + let bounds = active_clip.as_ref().map_or(r, |c| { + let b = c.bounds(); + Rect::from_two_pos(xf_paper.to_screen(Vec2::new(b.min.x, b.max.y)), xf_paper.to_screen(Vec2::new(b.max.x, b.min.y))) + }); + let clipped = painter.with_clip_rect(bounds.intersect(rect)); + let c = &mut app.canvas; + if let Some(h) = c.hover + && !sel.contains(&h) + { + c.vp_hover.update(st, &[h], scale); + let list = &c.vp_hover.list; + draw_highlight(&clipped, &xf, list, &(0..list.prims.len()).collect::>(), t.hover, preview_look); + } + if let Some(look) = selection_look + && !sel.is_empty() + { + c.vp_sel.update(st, sel, scale); + let list = &c.vp_sel.list; + let all = || (0..list.prims.len()).collect::>(); + if c.gpu.is_some() { + // Shares the GPU highlight layer with the layout's own selection; the high bit + // keeps this list's generations apart from the layout list's. + let list_id = c.vp_sel.list_gen | 1 << 63; + let stencil = if masked { crate::gpu::Stencil::Inside } else { crate::gpu::Stencil::Off }; + let hl = (&mut c.hl_key, &c.hl_slot, &mut c.hl_origin, &mut c.hl_infinite); + draw_highlight_gpu(&clipped, &xf, list, list_id, all, sel, t.selection, look, hl, stencil); + draw_highlight(&clipped, &xf, list, &c.hl_infinite, t.selection, look); + } else { + draw_highlight(&clipped, &xf, list, &all(), t.selection, look); + } + } + } + } else if let CanvasState { list: Some(list), list_gen, prim_index, hover, gpu, hl_key, hl_slot, hl_origin, hl_infinite, .. } = &mut app.canvas { + if let Some(h) = *hover + && !sel.contains(&h) + { + draw_highlight(painter, &xf, list, &prim_index.prims(*list_gen, list, &[h]), t.hover, preview_look); + } + if let Some(look) = selection_look { + if gpu.is_some() && !sel.is_empty() { + let list_id = *list_gen; + let hl = (hl_key, &*hl_slot, hl_origin, &mut *hl_infinite); + draw_highlight_gpu( + painter, + &xf, + list, + list_id, + || prim_index.prims(list_id, list, sel), + sel, + t.selection, + look, + hl, + crate::gpu::Stencil::Off, + ); + draw_highlight(painter, &xf, list, hl_infinite, t.selection, look); + } else { + draw_highlight(painter, &xf, list, &prim_index.prims(*list_gen, list, sel), t.selection, look); + } + } + } +} + +fn paint_pointer_overlay( + frame: &mut CanvasPaint<'_>, + paper_rect: Option, + surround: Color32, + selecting: bool, + sheet: Option, +) { + let app = &mut *frame.app; + let ui = &mut *frame.ui; + let painter = frame.painter; + let t = frame.tokens; + let rect = frame.rect; + let full = frame.full; + let tiles = frame.tiles; + let xf = frame.xf; + let inside = frame.inside; + let on_menu = frame.on_menu; + let hover_pos = frame.hover_pos; + let view = frame.view; + // Acquired tracking points and the polar / tracking paths under the cursor. + if app.canvas.cursor.is_some() { + draw_tracking(painter, &xf, &app.session.tracking.acquired, app.canvas.track.as_ref(), view.height * 4.0); + } + if let (Some(hit), Some(_)) = (app.canvas.snap, app.canvas.cursor) { + draw_snap_marker(painter, xf.to_screen(hit.point), &hit); + } + draw_tiles(app, ui, tiles); + // UCS icon, ViewCube and viewport label. + if app.ui.show_ucs_icon { + draw_ucs_icon(painter, rect); + crate::viewcube::ucs_icon(app, ui, rect); + } + if app.ui.show_viewcube && sheet.is_none() { + draw_viewcube(app, ui, rect); + } + viewport_label(painter, rect); + // Typing and Escape reach the command line even when its overlay is hidden. + crate::cmdline::keyboard(app, ui.ctx()); + // Command line overlay. + if app.ui.show_command_line { + crate::cmdline::show(app, ui, full); + } + // Crosshair cursor. + if inside && !on_menu { + // Hide the OS cursor under the drawn crosshair, unless assistive tech needs it (#39). + ui.ctx().set_cursor_icon(if app.ui.system_cursor { egui::CursorIcon::Crosshair } else { egui::CursorIcon::None }); + if let Some(hp) = hover_pos { + // Use the painted sheet even in MSPACE, where the editing transform follows a + // model viewport. The whole cursor takes one colour from its centre. + let ink = paper_rect.map_or(t.crosshair, |paper| crate::theme::crosshair_color(if paper.contains(hp) { t.paper } else { surround })); + draw_crosshair(app, painter, rect, hp, selecting, ink); + if app.canvas.cycle_badge && app.canvas.cycle.is_none() { + cycle_badge(painter, hp); + } + } + } + if inside + && app.session.settings.dynmode + && let Some(hp) = hover_pos + { + // A stretched grip shows its dimensions instead of the pointer boxes; a prompt asking for + // a distance shows its one field and keeps the prompt and its options. + let fields = crate::grip_dyn::draw(app, painter); + if !fields || app.session.current_prompt().is_some_and(|p| p.distance) { + dynamic_input(app, painter, hp, fields); + } + } + // Over the crosshair, so the row under the pointer reads clearly. + cycle_list(app, painter); + // No continuous repaint while hovering: input events repaint by themselves. Repainting every + // frame kept a vsync swapchain's queue full (every frame waited behind queued ones) and would + // spin the CPU with the low-latency present mode (`gpu::surface_config`). +} +struct DoubleClickContext<'a> { + inside: bool, + double: bool, + hover: Option, + xf: Xf, + xf_paper: Xf, + scale: f64, + active_viewport: &'a Option, + active_clip: &'a Option, +} + +fn handle_double_click(app: &mut CadApp, ctx: DoubleClickContext<'_>) -> Option { + let DoubleClickContext { inside, double, hover, xf, xf_paper, scale, active_viewport, active_clip } = ctx; + if inside + && double + && !app.session.prompting() + && matches!(app.session.layout_space(), cadcraft_doc::Space::Paper(_)) + && let Some(hp) = hover + { + let pp = xf_paper.to_world(hp); + let in_rect = |v: &cadcraft_doc::Viewport| (pp.x - v.center.x).abs() <= v.width / 2.0 && (pp.y - v.center.y).abs() <= v.height / 2.0; + let in_active = active_viewport.as_ref().is_some_and(|v| active_clip.as_ref().map_or(in_rect(v), |c| c.contains(pp))); + if !in_active { + app.canvas.hot_grips.clear(); + app.session.pending_window = None; + app.canvas.drag_window = false; + if app.run("mspace", serde_json::json!({"at": [pp.x, pp.y]})).is_err() && active_viewport.is_some() { + let _ = app.run("pspace", serde_json::json!({})); + } + app.session.pending_window = None; + app.canvas.drag_window = false; + app.canvas.list = None; + return None; + } + } + // Double-click text to edit it, as in AutoCAD: TEXTEDIT with that object already picked. + if inside + && double + && app.session.running.is_none() + && let Some(hp) = hover + && edit_text_at(app, xf.to_world(hp)) + { + return None; + } + + let mut hatch_dialog = false; + // Double-click a hatch (no command running) to edit it in the Hatch Edit dialog. The click is + // used up: it must not also start a grip edit on the hatch's centre grip below. + if inside + && double + && app.session.running.is_none() + && let Some(hp) = hover + && let Some(h) = hatch_at(app, xf.to_world(hp), scale) + { + app.session.set_selection(vec![h]); + crate::hatchdlg::open_edit(app); + hatch_dialog = true; + } + + Some(hatch_dialog) +} + +fn update_canvas_cursor( + app: &mut CadApp, + ui: &egui::Ui, + xf: &Xf, + inside: bool, + hover: Option, +) -> (Option, Option) { + let raw_world = hover.filter(|_| inside).map(|p| xf.to_world(p)); + let dyn_frame = crate::cmdline::dyn_frame(app); + if let Some(w) = raw_world { + let mut eff = effective_point(app, w, xf, ui.ctx()); + // A locked Dynamic Input value (`40,` or `40<` typed) holds the cursor and overrides snaps. + if let Some(f) = dyn_frame + && let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f) + && e.first_locked().is_some() + { + eff = crate::dyninput::constrain(&e, &f, eff, app.cmd.second_locked()); + app.canvas.snap = None; + app.canvas.track = None; + } + // Grip dimensional input: typed lengths and angles hold the grip and override snaps. + let grip_held = crate::grip_dyn::constrain(app, eff); + if let Some(q) = grip_held { + eff = q; + app.canvas.snap = None; + app.canvas.track = None; + } + app.canvas.cursor = Some(eff); + app.session.cursor = eff; + app.session.cursor_deferred = app.canvas.snap.and_then(|h| h.deferred); + // Typed or menu snap overrides (END, NON…) and Tab cycling; a locked Dynamic Input value wins. + if dyn_frame.and_then(|f| crate::dyninput::parse(&app.cmd.buffer, &f)).is_none_or(|e| e.first_locked().is_none()) { + override_cursor(app, w, xf); + } + // Typed grip dimensions win over snap overrides too. + if let Some(q) = grip_held { + app.canvas.cursor = Some(q); + app.session.cursor = q; + app.canvas.snap = None; + app.session.cursor_deferred = None; + } + } else { + app.canvas.cursor = None; + app.canvas.snap = None; + app.session.cursor_deferred = None; + } + + (raw_world, dyn_frame) +} + +fn update_hover(app: &mut CadApp, ui: &egui::Ui, inside: bool, hover_pos: Option, raw_world: Option, scale: f64) { + // The cursor picks objects (pick box, rollover highlight): "Select objects" and single-object + // prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those it shows. + let selecting = app.session.previews_selection(); + if inside && selecting && app.session.pending_window.is_none() { + if hover_pos != app.canvas.hover_at + && let (Some(w), Ok(st)) = (raw_world, app.session.state()) + { + let filter = app.session.settings.previewfilter; + app.canvas.hover = cadcraft_engine::select::pick(&st.doc, &st.edit_space(), w, app.session.settings.pickbox.max(1.0) * 1.5 / scale) + .filter(|h| st.doc.entity(*h).is_none_or(|e| !cadcraft_engine::select::preview_filtered(&st.doc, e, filter))); + app.canvas.hover_at = hover_pos; + } + } else { + app.canvas.hover = None; + app.canvas.hover_at = None; + } + // The open selection cycling list highlights the object of the row under the pointer. + if app.canvas.cycle.is_some() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { + app.canvas.cycle = None; + } + if let Some(c) = &app.canvas.cycle { + app.canvas.hover = hover_pos.and_then(|hp| c.row_at(hp)); + app.canvas.hover_at = None; + } + shift_space_cycle(app, ui, hover_pos.filter(|_| inside), raw_world); + if inside && hover_pos != app.canvas.cycle_badge_at { + app.canvas.cycle_badge = raw_world.is_some_and(|w| app.session.cycling_badge(w)); + app.canvas.cycle_badge_at = hover_pos; + } else if !inside { + app.canvas.cycle_badge = false; + app.canvas.cycle_badge_at = None; + } +} + +struct PromptClickContext<'a> { + rect: Rect, + xf: Xf, + inside: bool, + middle_down: bool, + primary_clicked: bool, + response: &'a egui::Response, +} + +fn collect_prompt_clicks(app: &mut CadApp, ui: &egui::Ui, context: PromptClickContext<'_>) -> bool { + // egui drops a click whose button comes up a few pixels from where it went down (a drag to it), + // and eframe hands a slow frame's clicks over together, which egui reports as one. + let point_prompt = app.session.current_prompt().is_some_and(|p| cadcraft_engine::pointmod::accepts(&p)) + && app.session.pending_window.is_none() + && !app.session.realtime_zoom_active(); + let pressed_primary = context.primary_clicked || (point_prompt && context.response.drag_stopped_by(egui::PointerButton::Primary)); + if point_prompt && context.inside && pressed_primary && !context.middle_down && app.cmd.options.is_none() && app.cmd.buffer.is_empty() { + let releases: Vec = ui.input(|i| { + i.events + .iter() + .filter_map(|e| match e { + egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } => Some(*pos), + _ => None, + }) + .collect() + }); + // The last one is placed below, at the cursor, like any click. + if let Some((_, earlier)) = releases.split_last() { + for pos in earlier.iter().filter(|p| context.rect.contains(**p)) { + let w = effective_point(app, context.xf.to_world(*pos), &context.xf, ui.ctx()); + let result = match app.canvas.snap { + Some(hit) => app.session.input_snapped(hit), + None => app.session.input(Input::Point(w)), + }; + if let Err(e) = result { + app.session.echo(e.to_string()); + } + } + } + } + + pressed_primary +} + /// Draw the drawing area and handle its input (zoom and pan, picking, the crosshair) for this /// frame. /// @@ -247,116 +748,19 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { app.canvas.xf = Some(xf); // ---------- input ---------- - let (hover_pos, scroll, mods, middle_down, pressed_primary, pressed_secondary, dbl_middle, dbl_primary) = ui.input(|i| { - ( - i.pointer.hover_pos(), - i.smooth_scroll_delta, - i.modifiers, - i.pointer.middle_down(), - i.pointer.primary_clicked(), - i.pointer.secondary_clicked(), - i.pointer.button_double_clicked(egui::PointerButton::Middle), - i.pointer.button_double_clicked(egui::PointerButton::Primary), - ) + let pointer = ui.input(|i| PointerInput { + hover_pos: i.pointer.hover_pos(), + scroll: i.smooth_scroll_delta, + mods: i.modifiers, + middle_down: i.pointer.middle_down(), + pressed_primary: i.pointer.primary_clicked(), + pressed_secondary: i.pointer.secondary_clicked(), + dbl_middle: i.pointer.button_double_clicked(egui::PointerButton::Middle), + dbl_primary: i.pointer.button_double_clicked(egui::PointerButton::Primary), }); - // The drawing area alone: a menu popup's frame (between its items and its border), a tooltip - // or a floating palette lives on a layer above the canvas, where `hovered()` alone still - // counts the canvas. Over those the OS cursor stays visible and the crosshair stays put, as - // over the menu bar (#1701). - let inside = hover_pos.is_some_and(|p| rect.contains(p) && ui.ctx().layer_id_at(p) == Some(ui.layer_id()) && resp.hovered()); - // The menus the canvas paints itself (selection cycling's rows, Dynamic Input's options) are - // menus too, but they share the canvas's layer: `layer_id_at` reports the drawing under them, - // so only their rectangles tell them apart from it ([`over_canvas_menu`]). They are excluded - // from the cursor contract below, though not from the canvas's input: a click on a row is - // taken by the row. - let on_menu = hover_pos.is_some_and(|p| over_canvas_menu(app, p)); - // Whether Ctrl is held right now. Written every frame (not just while a command prompts), so - // previews that read `Session::ctrl_down` always see the current state. The engine - // deliberately does not carry modifiers through `Input`, which is UI-agnostic (see PR - // #407's Limits note: modifiers used to be dropped on the floor at this point). - app.session.ctrl_down = mods.ctrl; - // Which device is scrolling? On macOS, trackpads (and Magic Mouse) report precise point - // deltas; notched mouse wheels report lines. The answer is sticky because egui spreads a wheel - // notch over several frames. Elsewhere (browsers, Wayland smooth-scroll mice) ordinary wheels - // also report points, so the wheel keeps zooming there. - ui.input(|i| { - for e in &i.raw.events { - if let egui::Event::MouseWheel { unit, .. } = e { - app.canvas.scroll_pans = cfg!(target_os = "macos") && *unit == egui::MouseWheelUnit::Point; - } - } - }); - if inside && app.canvas.scroll_pans && scroll != egui::Vec2::ZERO { - // Two-finger swipe pans: the drawing follows the fingers, like a middle-drag. - // Pinch (and Cmd+scroll) still zoom via `zoom_delta` below. - if let Ok(st) = app.session.state_mut() { - let v = st.view(); - st.set_view_quiet(cadcraft_engine::View { - center: v.center - Vec2::new(f64::from(scroll.x) / scale, -f64::from(scroll.y) / scale), - height: v.height, - }); - } - } - let opts = ui.ctx().options(|o| o.input_options); - let wheel = ui.input(|i| wheel_zoom_points(&i.raw.events, &opts, rect.height())); - if inside - && !app.canvas.scroll_pans - && wheel != 0.0 - && let Some(hp) = hover_pos - { - // Zoom with the mouse wheel about the cursor, each notch at once. - let f = app.session.wheel_zoom_factor(f64::from(wheel)); - let about = xf.to_world(hp); - let _ = app.session.zoom_about(f, about); - } - let zoom_pinch = ui.input(|i| crate::animation::zoom_delta(i, &opts, app.shows_animations())); - if inside - && (zoom_pinch - 1.0).abs() > 1e-4 - && let Some(hp) = hover_pos - { - let _ = app.session.zoom_about(f64::from(zoom_pinch), xf.to_world(hp)); - } - // Pan with the middle button (or Shift+right-drag / two-finger drag on trackpads is scroll). - let middle_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Middle)); - let focused = ui.input(|i| i.focused); - if middle_down - && focused - && (app.canvas.pan_last.is_some() || (middle_pressed && inside)) - && let Some(hp) = hover_pos - { - if let Some(last) = app.canvas.pan_last { - let d = hp - last; - if let Ok(st) = app.session.state_mut() { - let v = st.view(); - st.set_view_quiet(cadcraft_engine::View { - center: v.center - Vec2::new(f64::from(d.x) / scale, -f64::from(d.y) / scale), - height: v.height, - }); - } - } - app.canvas.pan_last = Some(hp); - } else { - app.canvas.pan_last = None; - } - if dbl_middle && inside { - let _ = app.session.zoom_extents(); - } - // ZOOM Realtime: dragging up zooms in, down zooms out. - if resp.dragged_by(egui::PointerButton::Primary) && app.session.realtime_zoom_active() { - let _ = app.session.realtime_zoom(f64::from(resp.drag_delta().y)); - } - // A click in another tile makes it current (or answers -VPORTS Join's prompts). - if pressed_primary - && let Some(hp) = hover_pos - && !rect.contains(hp) - && let Some(i) = tiles.iter().position(|(r, _)| r.contains(hp)) - { - if let Err(e) = app.session.click_tile(i) { - app.session.echo(e.to_string()); - } - ui.ctx().request_repaint(); - return; - } + let navigation = Navigation { rect, tiles: &tiles, response: &resp, xf, pointer }; + let Some((inside, on_menu)) = navigate_canvas(app, ui, &navigation) else { return }; + let PointerInput { hover_pos, mods, middle_down, pressed_primary, pressed_secondary, dbl_primary, .. } = pointer; // Re-read the view after navigation. let view = app.session.state().map(|s| s.view()).unwrap_or(view); let scale = f64::from(rect.height()) / view.height.max(1e-12); @@ -373,124 +777,26 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { }; // A clipped active viewport's outline (picks and its heavy border follow it). let active_clip = app.session.state().ok().and_then(|st| st.active_viewport().and_then(|(h, v)| cadcraft_render::viewport_clip(&st.doc, h, &v))); - // Double-click a viewport to work inside it; double-click the sheet outside it to return. - if inside - && dbl_primary - && !app.session.prompting() - && matches!(app.session.layout_space(), cadcraft_doc::Space::Paper(_)) - && let Some(hp) = hover_pos - { - let pp = xf_paper.to_world(hp); - let in_rect = |v: &cadcraft_doc::Viewport| (pp.x - v.center.x).abs() <= v.width / 2.0 && (pp.y - v.center.y).abs() <= v.height / 2.0; - let in_active = active_vp.as_ref().is_some_and(|v| active_clip.as_ref().map_or(in_rect(v), |c| c.contains(pp))); - if !in_active { - app.canvas.hot_grips.clear(); - app.session.pending_window = None; - app.canvas.drag_window = false; - if app.run("mspace", serde_json::json!({"at": [pp.x, pp.y]})).is_err() && active_vp.is_some() { - let _ = app.run("pspace", serde_json::json!({})); - } - app.session.pending_window = None; - app.canvas.drag_window = false; - app.canvas.list = None; - return; - } - } - // Double-click text to edit it, as in AutoCAD: TEXTEDIT with that object already picked. - if inside - && dbl_primary - && app.session.running.is_none() - && let Some(hp) = hover_pos - && edit_text_at(app, xf.to_world(hp)) - { + let Some(hatch_dialog) = handle_double_click( + app, + DoubleClickContext { + inside, + double: dbl_primary, + hover: hover_pos, + xf, + xf_paper, + scale, + active_viewport: &active_vp, + active_clip: &active_clip, + }, + ) else { return; - } + }; - // Double-click a hatch (no command running) to edit it in the Hatch Edit dialog. The click is - // used up: it must not also start a grip edit on the hatch's centre grip below. - let mut hatch_dialog = false; - if inside - && dbl_primary - && app.session.running.is_none() - && let Some(hp) = hover_pos - && let Some(h) = hatch_at(app, xf.to_world(hp), scale) - { - app.session.set_selection(vec![h]); - crate::hatchdlg::open_edit(app); - hatch_dialog = true; - } + let pressed_primary = + collect_prompt_clicks(app, ui, PromptClickContext { rect, xf, inside, middle_down, primary_clicked: pressed_primary, response: &resp }); - // While a command asks for a point, every click places one, however fast the drafting (#815). - // egui drops a click whose button comes up a few pixels from where it went down (a drag to it), - // and eframe hands a slow frame's clicks over together, which egui reports as one. - let point_prompt = app.session.current_prompt().is_some_and(|p| cadcraft_engine::pointmod::accepts(&p)) - && app.session.pending_window.is_none() - && !app.session.realtime_zoom_active(); - let pressed_primary = pressed_primary || (point_prompt && resp.drag_stopped_by(egui::PointerButton::Primary)); - if point_prompt && inside && pressed_primary && !middle_down && app.cmd.options.is_none() && app.cmd.buffer.is_empty() { - let releases: Vec = ui.input(|i| { - i.events - .iter() - .filter_map(|e| match e { - egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } => Some(*pos), - _ => None, - }) - .collect() - }); - // The last one is placed below, at the cursor, like any click. - if let Some((_, earlier)) = releases.split_last() { - for pos in earlier.iter().filter(|p| rect.contains(**p)) { - let w = effective_point(app, xf.to_world(*pos), &xf, ui.ctx()); - let result = match app.canvas.snap { - Some(hit) => app.session.input_snapped(hit), - None => app.session.input(Input::Point(w)), - }; - if let Err(e) = result { - app.session.echo(e.to_string()); - } - } - } - } - - let raw_world = hover_pos.filter(|_| inside).map(|p| xf.to_world(p)); - let dyn_frame = crate::cmdline::dyn_frame(app); - if let Some(w) = raw_world { - let mut eff = effective_point(app, w, &xf, ui.ctx()); - // A locked Dynamic Input value (`40,` or `40<` typed) holds the cursor and overrides snaps. - if let Some(f) = dyn_frame - && let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f) - && e.first_locked().is_some() - { - eff = crate::dyninput::constrain(&e, &f, eff, app.cmd.second_locked()); - app.canvas.snap = None; - app.canvas.track = None; - } - // Grip dimensional input: typed lengths and angles hold the grip and override snaps. - let grip_held = crate::grip_dyn::constrain(app, eff); - if let Some(q) = grip_held { - eff = q; - app.canvas.snap = None; - app.canvas.track = None; - } - app.canvas.cursor = Some(eff); - app.session.cursor = eff; - app.session.cursor_deferred = app.canvas.snap.and_then(|h| h.deferred); - // Typed or menu snap overrides (END, NON…) and Tab cycling; a locked Dynamic Input value wins. - if dyn_frame.and_then(|f| crate::dyninput::parse(&app.cmd.buffer, &f)).is_none_or(|e| e.first_locked().is_none()) { - override_cursor(app, w, &xf); - } - // Typed grip dimensions win over snap overrides too. - if let Some(q) = grip_held { - app.canvas.cursor = Some(q); - app.session.cursor = q; - app.canvas.snap = None; - app.session.cursor_deferred = None; - } - } else { - app.canvas.cursor = None; - app.canvas.snap = None; - app.session.cursor_deferred = None; - } + let (raw_world, dyn_frame) = update_canvas_cursor(app, ui, &xf, inside, hover_pos); // A click on the Dynamic Input options menu picks the keyword; elsewhere it closes the menu. let mut picked = false; @@ -610,41 +916,49 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { crate::context_menu::show(app, &resp); } - // Hover highlight (throttled to cursor movement). - // The cursor picks objects (pick box, rollover highlight): "Select objects" and single-object - // prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those it shows. - let selecting = app.session.previews_selection(); - if inside && selecting && app.session.pending_window.is_none() { - if hover_pos != app.canvas.hover_at - && let (Some(w), Ok(st)) = (raw_world, app.session.state()) - { - let filter = app.session.settings.previewfilter; - app.canvas.hover = cadcraft_engine::select::pick(&st.doc, &st.edit_space(), w, app.session.settings.pickbox.max(1.0) * 1.5 / scale) - .filter(|h| st.doc.entity(*h).is_none_or(|e| !cadcraft_engine::select::preview_filtered(&st.doc, e, filter))); - app.canvas.hover_at = hover_pos; - } - } else { - app.canvas.hover = None; - app.canvas.hover_at = None; - } - // The open selection cycling list highlights the object of the row under the pointer. - if app.canvas.cycle.is_some() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - app.canvas.cycle = None; - } - if let Some(c) = &app.canvas.cycle { - app.canvas.hover = hover_pos.and_then(|hp| c.row_at(hp)); - app.canvas.hover_at = None; - } - shift_space_cycle(app, ui, hover_pos.filter(|_| inside), raw_world); - if inside && hover_pos != app.canvas.cycle_badge_at { - app.canvas.cycle_badge = raw_world.is_some_and(|w| app.session.cycling_badge(w)); - app.canvas.cycle_badge_at = hover_pos; - } else if !inside { - app.canvas.cycle_badge = false; - app.canvas.cycle_badge_at = None; - } + update_hover(app, ui, inside, hover_pos, raw_world, scale); - // ---------- paint ---------- + paint_canvas(CanvasPaint { + app, + ui, + painter: &painter, + tokens: t, + full, + tiles: &tiles, + rect, + xf, + xf_paper, + view, + active_vp, + active_clip, + inside, + on_menu, + hover_pos, + raw_world, + }); +} + +fn paint_canvas(frame: CanvasPaint<'_>) { + let CanvasPaint { + app, + ui, + painter, + tokens: t, + full, + tiles, + rect, + xf, + xf_paper, + view, + active_vp, + active_clip, + inside, + on_menu, + hover_pos, + raw_world, + } = frame; + let scale = xf.scale; + let selecting = app.session.previews_selection(); painter.rect_filled(rect, 0.0, t.canvas); let t0 = crate::now_ms(); let mut views = vec![(xf_paper.world_bounds(), 1.0 / xf_paper.scale)]; @@ -673,8 +987,8 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { (Rgb(t.paper.r(), t.paper.g(), t.paper.b()), Some(paper)) } None => { - blockedit::wash(app, &painter, rect); - draw_grid(app, &painter, &xf); + blockedit::wash(app, painter, rect); + draw_grid(app, painter, &xf); (Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b()), None) } }; @@ -682,102 +996,33 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { // How the object under the cursor and the selected ones are highlighted (PREVIEWEFFECT, // HIGHLIGHT, SELECTIONEFFECT); with HIGHLIGHT off only their grips show. let (preview_look, selection_look) = (Look::preview(&app.session.settings), Look::selection(&app.session.settings)); - // The active viewport's contents, drawn apart from the layout's list, under its own objects. - // `masked`: its outline is in the stencil buffer this frame. - let mut masked = false; - if let Some(f) = ensure_vp_content(app, &xf_paper, ui.ctx()) { - let gpu = app.canvas.gpu.is_some(); - let clipped = painter.with_clip_rect(f.on_screen.intersect(rect)); - draw_vp_content(&mut app.canvas.vp, gpu, &clipped, &f, &xf_paper, bg, app.session.settings.lwdisplay); - masked = gpu && f.outline.is_some(); - } - if app.canvas.gpu.is_some() { - draw_list_gpu(&mut app.canvas, &painter, &xf_paper, bg, app.session.settings.lwdisplay); - } else if let Some(list) = &app.canvas.list { - draw_list(&painter, &xf_paper, list, bg, app.session.settings.lwdisplay); - } - if let Some(vp) = &active_vp { - // The active viewport: a heavy border, highlights in model units clipped to it. - let r = Rect::from_two_pos( - xf_paper.to_screen(Vec2::new(vp.center.x - vp.width / 2.0, vp.center.y + vp.height / 2.0)), - xf_paper.to_screen(Vec2::new(vp.center.x + vp.width / 2.0, vp.center.y - vp.height / 2.0)), - ); - let heavy = Stroke::new(3.0, Color32::from_rgb(0x2a, 0x2a, 0x2a)); - match &active_clip { - Some(c) => { - let mut pts: Vec = c.polygon().iter().map(|q| xf_paper.to_screen(*q)).collect(); - pts.extend(pts.first().copied()); - painter.add(egui::Shape::line(pts, heavy)); - } - None => { - painter.rect_stroke(r, 0.0, heavy, egui::StrokeKind::Middle); - } - } - if let Ok(st) = app.session.state() { - // A clipped viewport's outline bounds (its outline is masked when `masked`). - let bounds = active_clip.as_ref().map_or(r, |c| { - let b = c.bounds(); - Rect::from_two_pos(xf_paper.to_screen(Vec2::new(b.min.x, b.max.y)), xf_paper.to_screen(Vec2::new(b.max.x, b.min.y))) - }); - let clipped = painter.with_clip_rect(bounds.intersect(rect)); - let c = &mut app.canvas; - if let Some(h) = c.hover - && !sel.contains(&h) - { - c.vp_hover.update(st, &[h], scale); - let list = &c.vp_hover.list; - draw_highlight(&clipped, &xf, list, &(0..list.prims.len()).collect::>(), t.hover, preview_look); - } - if let Some(look) = selection_look - && !sel.is_empty() - { - c.vp_sel.update(st, &sel, scale); - let list = &c.vp_sel.list; - let all = || (0..list.prims.len()).collect::>(); - if c.gpu.is_some() { - // Shares the GPU highlight layer with the layout's own selection; the high bit - // keeps this list's generations apart from the layout list's. - let list_id = c.vp_sel.list_gen | 1 << 63; - let stencil = if masked { crate::gpu::Stencil::Inside } else { crate::gpu::Stencil::Off }; - let hl = (&mut c.hl_key, &c.hl_slot, &mut c.hl_origin, &mut c.hl_infinite); - draw_highlight_gpu(&clipped, &xf, list, list_id, all, &sel, t.selection, look, hl, stencil); - draw_highlight(&clipped, &xf, list, &c.hl_infinite, t.selection, look); - } else { - draw_highlight(&clipped, &xf, list, &all(), t.selection, look); - } - } - } - } else if let CanvasState { list: Some(list), list_gen, prim_index, hover, gpu, hl_key, hl_slot, hl_origin, hl_infinite, .. } = &mut app.canvas { - if let Some(h) = *hover - && !sel.contains(&h) - { - draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &[h]), t.hover, preview_look); - } - if let Some(look) = selection_look { - if gpu.is_some() && !sel.is_empty() { - let list_id = *list_gen; - let hl = (hl_key, &*hl_slot, hl_origin, &mut *hl_infinite); - draw_highlight_gpu( - &painter, - &xf, - list, - list_id, - || prim_index.prims(list_id, list, &sel), - &sel, - t.selection, - look, - hl, - crate::gpu::Stencil::Off, - ); - draw_highlight(&painter, &xf, list, hl_infinite, t.selection, look); - } else { - draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &sel), t.selection, look); - } - } - } + paint_content_highlights( + &mut CanvasPaint { + app, + ui, + painter, + tokens: t, + full, + tiles, + rect, + xf, + xf_paper, + view, + active_vp: active_vp.clone(), + active_clip: active_clip.clone(), + inside, + on_menu, + hover_pos, + raw_world, + }, + bg, + &sel, + preview_look, + selection_look, + ); app.canvas.draw_ms = crate::now_ms() - t0; - compare::paint(app, ui.ctx(), &painter, &xf_paper, rect); - blockedit::paint(app, ui.ctx(), &painter, &xf_paper, rect); + compare::paint(app, ui.ctx(), painter, &xf_paper, rect); + blockedit::paint(app, ui.ctx(), painter, &xf_paper, rect); // Constraint bars and dynamic dimensional constraints (model space, or inside a viewport). if sheet.is_none() || active_vp.is_some() { let clip = match &active_vp { @@ -831,7 +1076,7 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { } } } - crate::dyngrips::draw(app, &painter, &xf, rect, hover_pos); + crate::dyngrips::draw(app, painter, &xf, rect, hover_pos); } // Rubber band preview of the active command. // `xf` is the space being edited: paper space, or model space inside the active viewport. @@ -843,9 +1088,9 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { if let Ok(st) = app.session.state() { let serial = app.session.clipboard_source.as_ref().map_or(0, |c| c.serial); let transparency = app.session.settings.transparency_display; - draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg, transparency); + draw_drag_gpu(&mut app.canvas, painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg, transparency); let list = rebuilt_preview(&st.doc, extra.iter(), scale); - draw_list(&painter, &xf, &list, bg, false); + draw_list(painter, &xf, &list, bg, false); } } else if let (Some(c), true) = (app.canvas.cursor, app.session.prompting()) { let ents = app.session.preview(c); @@ -853,7 +1098,7 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { && let Ok(st) = app.session.state() { let list = rebuilt_preview(&st.doc, ents.iter(), scale); - draw_list(&painter, &xf, &list, bg, false); + draw_list(painter, &xf, &list, bg, false); } } // Pending selection window. @@ -873,59 +1118,30 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { painter.rect_stroke(r, 0.0, st, egui::StrokeKind::Middle); } } - // Acquired tracking points and the polar / tracking paths under the cursor. - if app.canvas.cursor.is_some() { - draw_tracking(&painter, &xf, &app.session.tracking.acquired, app.canvas.track.as_ref(), view.height * 4.0); - } - if let (Some(hit), Some(_)) = (app.canvas.snap, app.canvas.cursor) { - draw_snap_marker(&painter, xf.to_screen(hit.point), &hit); - } - draw_tiles(app, ui, &tiles); - // UCS icon, ViewCube and viewport label. - if app.ui.show_ucs_icon { - draw_ucs_icon(&painter, rect); - crate::viewcube::ucs_icon(app, ui, rect); - } - if app.ui.show_viewcube && sheet.is_none() { - draw_viewcube(app, ui, rect); - } - viewport_label(&painter, rect); - // Typing and Escape reach the command line even when its overlay is hidden. - crate::cmdline::keyboard(app, ui.ctx()); - // Command line overlay. - if app.ui.show_command_line { - crate::cmdline::show(app, ui, full); - } - // Crosshair cursor. - if inside && !on_menu { - // Hide the OS cursor under the drawn crosshair, unless assistive tech needs it (#39). - ui.ctx().set_cursor_icon(if app.ui.system_cursor { egui::CursorIcon::Crosshair } else { egui::CursorIcon::None }); - if let Some(hp) = hover_pos { - // Use the painted sheet even in MSPACE, where the editing transform follows a - // model viewport. The whole cursor takes one colour from its centre. - let ink = paper_rect.map_or(t.crosshair, |paper| crate::theme::crosshair_color(if paper.contains(hp) { t.paper } else { surround })); - draw_crosshair(app, &painter, rect, hp, selecting, ink); - if app.canvas.cycle_badge && app.canvas.cycle.is_none() { - cycle_badge(&painter, hp); - } - } - } - if inside - && app.session.settings.dynmode - && let Some(hp) = hover_pos - { - // A stretched grip shows its dimensions instead of the pointer boxes; a prompt asking for - // a distance shows its one field and keeps the prompt and its options. - let fields = crate::grip_dyn::draw(app, &painter); - if !fields || app.session.current_prompt().is_some_and(|p| p.distance) { - dynamic_input(app, &painter, hp, fields); - } - } - // Over the crosshair, so the row under the pointer reads clearly. - cycle_list(app, &painter); - // No continuous repaint while hovering: input events repaint by themselves. Repainting every - // frame kept a vsync swapchain's queue full (every frame waited behind queued ones) and would - // spin the CPU with the low-latency present mode (`gpu::surface_config`). + paint_pointer_overlay( + &mut CanvasPaint { + app, + ui, + painter, + tokens: t, + full, + tiles, + rect, + xf, + xf_paper, + view, + active_vp: active_vp.clone(), + active_clip: active_clip.clone(), + inside, + on_menu, + hover_pos, + raw_world, + }, + paper_rect, + surround, + selecting, + sheet, + ); } #[cfg(test)] diff --git a/crates/ui-egui/src/canvas/cursor.rs b/crates/ui-egui/src/canvas/cursor.rs index 427445561..02f1cf4c2 100644 --- a/crates/ui-egui/src/canvas/cursor.rs +++ b/crates/ui-egui/src/canvas/cursor.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CadApp, Color32, Pos2, Rect, Shape, SnapCycle, SnapHit, Stroke, Tokens, Vec2, Xf, pos2, snap, vec2}; pub(super) fn draw_snap_marker(p: &egui::Painter, at: Pos2, hit: &SnapHit) { let t = Tokens::get(); diff --git a/crates/ui-egui/src/canvas/cycle.rs b/crates/ui-egui/src/canvas/cycle.rs index f32123c8d..e7dae44bc 100644 --- a/crates/ui-egui/src/canvas/cycle.rs +++ b/crates/ui-egui/src/canvas/cycle.rs @@ -1,5 +1,5 @@ use super::grips::grip_at; -use super::*; +use super::{CadApp, Handle, Pos2, Rect, Stroke, Tokens, Vec2, Xf, pos2, vec2}; /// The objects under a click that selection cycling lets the user choose between: where the click /// was, each object with its row's label (type and layer), Shift held, and the rows as drawn. diff --git a/crates/ui-egui/src/canvas/draw.rs b/crates/ui-egui/src/canvas/draw.rs index 0c11eb635..9ca1d9314 100644 --- a/crates/ui-egui/src/canvas/draw.rs +++ b/crates/ui-egui/src/canvas/draw.rs @@ -1,5 +1,5 @@ use super::list::preview_options; -use super::*; +use super::{Bounds2, CanvasState, Color32, DisplayList, Handle, Kind, ListKey, Pos2, Rect, Rgb, Shape, Stroke, Vec2, Xf, vec2}; fn color32(c: Rgb) -> Color32 { Color32::from_rgb(c.0, c.1, c.2) @@ -336,6 +336,7 @@ pub(super) fn selection_area_fill(s: &cadcraft_engine::Settings, crossing: bool) #[cfg(test)] mod highlight_stability_tests { use super::*; + use egui::pos2; fn frame(list: &DisplayList, look: Look, pointer: Pos2) -> Vec { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/canvas/dyn_boxes.rs b/crates/ui-egui/src/canvas/dyn_boxes.rs index 686f09110..b0bcfb9e2 100644 --- a/crates/ui-egui/src/canvas/dyn_boxes.rs +++ b/crates/ui-egui/src/canvas/dyn_boxes.rs @@ -1,5 +1,5 @@ use super::widgets::draw_lock; -use super::*; +use super::{CadApp, Pos2, Rect, Shape, Stroke, Tokens, Vec2, pos2, vec2}; /// The Dynamic Input tooltip beside the cursor, styled like the command line. At a point prompt /// it shows the pair of value boxes, a view of the command-line text (see [`crate::dyninput`]); diff --git a/crates/ui-egui/src/canvas/grid.rs b/crates/ui-egui/src/canvas/grid.rs index c2e616aaf..a07f4e092 100644 --- a/crates/ui-egui/src/canvas/grid.rs +++ b/crates/ui-egui/src/canvas/grid.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Bounds2, CadApp, Shape, Stroke, Tokens, Vec2, Xf, pos2}; /// Grid spacing adapted to the zoom: multiply the unit until lines are at least `min_px` apart. fn adaptive(unit: f64, scale: f64, min_px: f64) -> f64 { diff --git a/crates/ui-egui/src/canvas/grips.rs b/crates/ui-egui/src/canvas/grips.rs index 65e1cbc16..8bd0e6467 100644 --- a/crates/ui-egui/src/canvas/grips.rs +++ b/crates/ui-egui/src/canvas/grips.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CadApp, Handle, Input, Pos2, Rect, Xf, vec2}; /// A click on grip `g` (idle): Shift+click makes it hot or cold again; a plain click starts /// editing it, with the other hot grips when it is one of them. diff --git a/crates/ui-egui/src/canvas/list.rs b/crates/ui-egui/src/canvas/list.rs index 0e15fc01e..2d548264e 100644 --- a/crates/ui-egui/src/canvas/list.rs +++ b/crates/ui-egui/src/canvas/list.rs @@ -1,5 +1,5 @@ use super::viewport::{split_viewport, stencil_clip}; -use super::*; +use super::{Bounds2, CadApp, DisplayList, Handle, ListKey, Rgb, Tokens, Xf, rebuild, same_content}; /// Primitives of `list` sorted by entity handle, built on the first highlight after each list /// rebuild. Highlighting the hovered entity used to scan every primitive on every frame: ~2.5 ms diff --git a/crates/ui-egui/src/canvas/pick.rs b/crates/ui-egui/src/canvas/pick.rs index ea970a8d0..3bd28b017 100644 --- a/crates/ui-egui/src/canvas/pick.rs +++ b/crates/ui-egui/src/canvas/pick.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CadApp, Handle, Input, Vec2}; /// The vertical wheel scroll this frame that zooms, in points, read from the raw wheel events so /// each notch zooms by its whole step at once, as in standard CAD (#660). egui's smoothed delta diff --git a/crates/ui-egui/src/canvas/viewport.rs b/crates/ui-egui/src/canvas/viewport.rs index c96a2bc83..c79354b6c 100644 --- a/crates/ui-egui/src/canvas/viewport.rs +++ b/crates/ui-egui/src/canvas/viewport.rs @@ -1,5 +1,5 @@ use super::draw::draw_list; -use super::*; +use super::{Bounds2, CadApp, DisplayList, Handle, Kind, Rect, Rgb, Vec2, Xf}; /// The active MSPACE viewport when its contents are drawn apart from the layout's list (see /// [`VpContent`]), with its clip outline when it is clipped. A clipped viewport is clipped with diff --git a/crates/ui-egui/src/canvas/widgets.rs b/crates/ui-egui/src/canvas/widgets.rs index cd55a7f67..d10e40af3 100644 --- a/crates/ui-egui/src/canvas/widgets.rs +++ b/crates/ui-egui/src/canvas/widgets.rs @@ -1,6 +1,6 @@ use super::draw::draw_list; use super::grid::draw_grid; -use super::*; +use super::{CadApp, Color32, Pos2, Rect, Rgb, Shape, Stroke, Tokens, Xf, pos2, vec2}; pub(super) fn tile_rects(app: &CadApp, full: Rect) -> Vec<(Rect, cadcraft_engine::ModelTile)> { let x = |f: f64| full.left() + full.width() * f.clamp(0.0, 1.0) as f32; diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index f3a935d79..b8d9f09a1 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -290,6 +290,43 @@ fn visible_tabs(widths: &[f32], active: Option, room: f32) -> std::ops::R start..active + 1 } +fn draw_status_toggles( + app: &mut CadApp, + ui: &mut egui::Ui, + painter: &egui::Painter, + t: Tokens, + toggles: &[(Icon, bool, &str, &str)], + layout: (Rect, f32, f32), +) { + let (rect, mut rx, size) = layout; + let mut toggle_cmd = None; + for &(icon, on, cmd, tip) in toggles { + let br = Rect::from_min_size(pos2(rx, rect.center().y - size / 2.0), vec2(size, size)); + let resp = ui.interact(br, ui.scope_id().with(("tg", tip)), Sense::click()); + if on { + painter.rect_filled(br, 3.0, t.toggle_on.gamma_multiply(0.35)); + } else if resp.hovered() { + painter.rect_filled(br, 3.0, t.control_hover.gamma_multiply(0.5)); + } + icons::paint(painter, br.shrink(2.5), icon, false); + if on { + // Re-tint the icon blue-ish by an underline. + painter.hline(br.x_range().shrink(4.0), br.bottom() - 1.0, Stroke::new(1.5, t.toggle_on)); + } + icons::describe_control(ui, &resp, tip, Some(on), false); + if resp.on_hover_text(crate::i18n::t(tip)).clicked() { + toggle_cmd = Some(cmd); + } + rx += size + 4.0; + } + // The toggles don't cancel the active command (#1108); the dialog buttons start theirs. + match toggle_cmd { + Some(c) if c.starts_with("ui.") => app.start(c), + Some(c) => crate::menus::run_transparent(app, c), + None => {} + } +} + /// Status bar: layout tabs follow the left-hand layout actions, independent of palette docking. /// Coordinates and drafting toggles remain on the right. pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) { @@ -343,7 +380,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) { (Icon::Gear, false, "ui.dialog.dsettings", "Drafting Settings"), ]; let size = 20.0; - let mut rx = r.right() - 8.0 - toggles.len() as f32 * (size + 4.0); + let rx = r.right() - 8.0 - toggles.len() as f32 * (size + 4.0); // Coordinates. let coord = app.canvas.cursor.map(|c| { let (lu, lp) = app.session.doc().map(|d| (d.header.i64("LUNITS", 2), d.header.i64("LUPREC", 4))).unwrap_or((2, 4)); @@ -486,32 +523,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) { ui.interact(rect, ui.scope_id().with("status_readout"), Sense::hover()).on_hover_text(text); } } - let mut toggle_cmd = None; - for (icon, on, cmd, tip) in toggles { - let br = Rect::from_min_size(pos2(rx, r.center().y - size / 2.0), vec2(size, size)); - let resp = ui.interact(br, ui.scope_id().with(("tg", tip)), Sense::click()); - if on { - p.rect_filled(br, 3.0, t.toggle_on.gamma_multiply(0.35)); - } else if resp.hovered() { - p.rect_filled(br, 3.0, t.control_hover.gamma_multiply(0.5)); - } - icons::paint(&p, br.shrink(2.5), icon, false); - if on { - // Re-tint the icon blue-ish by an underline. - p.hline(br.x_range().shrink(4.0), br.bottom() - 1.0, Stroke::new(1.5, t.toggle_on)); - } - icons::describe_control(ui, &resp, tip, Some(on), false); - if resp.on_hover_text(crate::i18n::t(tip)).clicked() { - toggle_cmd = Some(cmd); - } - rx += size + 4.0; - } - // The toggles don't cancel the active command (#1108); the dialog buttons start theirs. - match toggle_cmd { - Some(c) if c.starts_with("ui.") => app.start(c), - Some(c) => crate::menus::run_transparent(app, c), - None => {} - } + draw_status_toggles(app, ui, &p, t, &toggles, (r, rx, size)); }); } diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 9524b2e9f..0e1e23e19 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -157,8 +157,7 @@ fn module_files(dir: &std::path::Path, files: &mut Vec) { } } -#[test] -fn locale_and_saved_preference_resolution() { +fn assert_primary_locale_aliases() { for tag in ["uk", "uk-UA", "uk_UA.UTF-8", "UK-ua", " uk-UA ", "uk_UA@euro"] { assert_eq!(locale_language(tag), Some("uk"), "{tag}"); } @@ -168,12 +167,18 @@ fn locale_and_saved_preference_resolution() { for (tag, code) in [("ru", "ru"), ("ru-RU", "ru"), ("ru_RU.UTF-8", "ru"), ("fr", "fr"), ("fr-FR", "fr"), ("fr_CA.UTF-8", "fr")] { assert_eq!(locale_language(tag), Some(code), "{tag}"); } +} + +fn assert_portuguese_and_spanish_aliases() { for tag in ["pt", "pt-BR", "pt_BR.UTF-8", "PT-br", "pt-PT"] { assert_eq!(locale_language(tag), Some("pt-BR"), "{tag}"); } for tag in ["es", "es-ES", "es_MX.UTF-8", "ES-ar"] { assert_eq!(locale_language(tag), Some("es"), "{tag}"); } +} + +fn assert_additional_locale_aliases() { for (code, tags) in [ ("cs", &["cs", "cs-CZ", "cs_CZ.UTF-8", "CS-cz", " cs-CZ ", "cs_CZ@euro"][..]), ("it", &["it", "it-IT", "it_IT.UTF-8", "IT-ch", " it-CH ", "it_IT@euro"]), @@ -188,12 +193,18 @@ fn locale_and_saved_preference_resolution() { assert_eq!(locale_language(tag), Some(code), "{tag}"); } } +} + +fn assert_unrecognized_locale_tags() { // Look-alike codes, three-letter codes and endonyms pick nothing: `se` is Northern Sami. let look_alikes = ["ua", "ukulele", "pl-PL", "", "garbage", "💙", "ptolemy", "cz", "csharp", "csomething", "italic"]; let others = ["kr", "korean", "kotlin", "se", "swe", "svenska", "hun", "magyar", "tur", "türkçe", "elvish"]; for tag in look_alikes.into_iter().chain(others) { assert_eq!(locale_language(tag), None, "{tag}"); } +} + +fn assert_auto_locale_precedence() { assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), "pt-BR".into()]), "pt-BR"); assert_eq!(Preference::Auto.resolve(&["fr-FR".into(), "pt-BR".into()]), "fr"); assert_eq!(Preference::Auto.resolve(&["es-ES".into(), "pt-BR".into()]), "es"); @@ -202,30 +213,51 @@ fn locale_and_saved_preference_resolution() { assert_eq!(Preference::English.resolve(&locales), "en"); assert_eq!(Preference::Auto.resolve(&["en-US".into(), "uk-UA".into()]), "en"); assert_eq!(Preference::Auto.resolve(&[]), "en"); +} + +fn assert_saved_preference_restoration(preference: Preference) { + assert_eq!(Preference::restore(Some(preference.code())), preference); + let saved = serde_json::to_string(&preference).unwrap(); + assert_eq!(saved, format!("\"{}\"", preference.code())); + assert_eq!(serde_json::from_str::(&saved).unwrap(), preference); + assert_eq!(serde_json::from_value::(json!(preference.code())).unwrap(), preference); +} + +fn assert_preference_resolution(preference: Preference) { + if preference == Preference::Auto { + return; + } + assert_eq!(Preference::parse(preference.code()), Some(preference)); + // A chosen language wins over the system's; Auto takes the first system language there is. + assert_eq!(preference.resolve(&["uk-UA".into()]), preference.code()); + if preference != Preference::English { + assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), preference.code().into(), "en-US".into()]), preference.code()); + } +} + +fn assert_saved_preference_persistence(preference: Preference) { + let mut app = CadApp::new(Session::new(), Services::default()); + app.ui.interface_language = preference; + let mut restored = CadApp::new(Session::new(), Services::default()); + restored.load_prefs(&app.prefs_json()); + assert_eq!(restored.ui.interface_language, preference); + restored.load_prefs(r#"{"interfaceLanguage":"klingon"}"#); + assert_eq!(restored.ui.interface_language, preference); +} + +#[test] +fn locale_and_saved_preference_resolution() { + assert_primary_locale_aliases(); + assert_portuguese_and_spanish_aliases(); + assert_additional_locale_aliases(); + assert_unrecognized_locale_tags(); + assert_auto_locale_precedence(); assert_eq!(Preference::restore(None), Preference::Auto); assert_eq!(Preference::restore(Some("obsolete")), Preference::Auto); for preference in Preference::ALL { - assert_eq!(Preference::restore(Some(preference.code())), preference); - let saved = serde_json::to_string(&preference).unwrap(); - assert_eq!(saved, format!("\"{}\"", preference.code())); - assert_eq!(serde_json::from_str::(&saved).unwrap(), preference); - if preference == Preference::Auto { - continue; - } - assert_eq!(Preference::parse(preference.code()), Some(preference)); - // A chosen language wins over the system's; Auto takes the first system language there is. - assert_eq!(preference.resolve(&["uk-UA".into()]), preference.code()); - if preference != Preference::English { - assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), preference.code().into(), "en-US".into()]), preference.code()); - } - // Saved with the host's other preferences; a bad saved value keeps the current choice. - let mut a = CadApp::new(Session::new(), Services::default()); - a.ui.interface_language = preference; - let mut b = CadApp::new(Session::new(), Services::default()); - b.load_prefs(&a.prefs_json()); - assert_eq!(b.ui.interface_language, preference); - b.load_prefs(r#"{"interfaceLanguage":"klingon"}"#); - assert_eq!(b.ui.interface_language, preference); + assert_saved_preference_restoration(preference); + assert_preference_resolution(preference); + assert_saved_preference_persistence(preference); } assert!(serde_json::from_str::("\"unsupported\"").is_err()); assert_eq!(serde_json::from_value::(json!({})).unwrap().interface_language, Preference::Auto); diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 1f018df2e..8e0f89963 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -230,6 +230,19 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { let alpha = |c: Color32| if dim { c.gamma_multiply(0.4) } else { c }; let pen = Pen { p, r: rect, w: (rect.width() / 16.0).clamp(1.0, 1.6), base: alpha(t.icon), acc: alpha(t.icon_accent), pt: alpha(t.icon_point) }; let (b, a) = (pen.base, pen.acc); + paint_file_view(&pen, icon, b, a, dim); + paint_document_tools(&pen, icon, b, a, dim); + paint_draw_tools(&pen, icon, b, a, dim); + paint_edit_tools(&pen, icon, b, a, dim); + paint_dimensions(&pen, icon, b, a, dim); + paint_block_tools(&pen, icon, b, a, dim); + paint_layer_tools(&pen, icon, b, a, dim); + paint_display_toggles(&pen, icon, b, a, dim); + paint_navigation_controls(&pen, icon, b, a, dim); + paint_constraints(&pen, icon, b, a, dim); +} + +fn paint_file_view(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { use Icon::*; match icon { New => { @@ -331,6 +344,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { b, ); } + _ => {} + } +} + +fn paint_document_tools(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { + use Icon::*; + match icon { Orbit => { pen.c(12.0, 12.0, 4.0, b); pen.arc(12.0, 12.0, 9.0, 20.0, 160.0, a); @@ -355,6 +375,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.poly(&[(3.0, y + 2.0), (12.0, *y - 2.0), (21.0, y + 2.0), (12.0, y + 6.0)], true, c); } } + _ => {} + } +} + +fn paint_draw_tools(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, dim: bool) { + use Icon::*; + match icon { Help => { pen.c(12.0, 12.0, 9.0, b); pen.text(12.0, 12.5, "?", 12.0, b); @@ -485,6 +512,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.la(14.0, 18.0, 21.0, 18.0); pen.la(14.0, 21.0, 19.0, 21.0); } + _ => {} + } +} + +fn paint_edit_tools(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { + use Icon::*; + match icon { Move => { pen.la(12.0, 3.0, 12.0, 21.0); pen.la(3.0, 12.0, 21.0, 12.0); @@ -604,6 +638,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.poly(&[(20.0, 6.0), (20.0, 11.0), (11.0, 11.0), (11.0, 14.0)], false, b); pen.fill_rect(9.5, 14.0, 3.0, 8.0, a); } + _ => {} + } +} + +fn paint_dimensions(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { + use Icon::*; + match icon { DimLinear | DimAligned | DimBaseline | DimContinue | QDim => { let (y0, y1) = (8.0, 18.0); pen.l(4.0, y1, 4.0, 5.0); @@ -673,6 +714,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { _ => pen.text(19.0, 19.0, "A", 9.0, a), } } + _ => {} + } +} + +fn paint_block_tools(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { + use Icon::*; + match icon { LayerOff | LayerIso | LayerFreeze | LayerLock | LayerMatch | MakeCurrent | LayerPrev => { pen.poly(&[(2.0, 9.0), (10.0, 5.0), (18.0, 9.0), (10.0, 13.0)], true, b); pen.poly(&[(2.0, 13.0), (10.0, 17.0), (18.0, 13.0)], false, b); @@ -691,11 +739,27 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { } } } + _ => {} + } +} + +fn paint_layer_tools(pen: &Pen<'_>, icon: Icon, _b: Color32, _a: Color32, dim: bool) { + let alpha = |c: Color32| if dim { c.gamma_multiply(0.4) } else { c }; + use Icon::*; + match icon { Bulb | BulbOff => { let c = if icon == Bulb { Color32::from_rgb(0xf5, 0xd0, 0x3b) } else { Color32::from_rgb(0x70, 0x78, 0x86) }; pen.fill_c(12.0, 10.0, 6.0, alpha(c)); pen.fill_rect(9.5, 15.0, 5.0, 4.0, alpha(Color32::from_rgb(0xb0, 0xb4, 0xba))); } + _ => {} + } +} + +fn paint_display_toggles(pen: &Pen<'_>, icon: Icon, b: Color32, _a: Color32, dim: bool) { + let alpha = |c: Color32| if dim { c.gamma_multiply(0.4) } else { c }; + use Icon::*; + match icon { Lock | Unlock => { let c = alpha(Color32::from_rgb(0xe6, 0xb4, 0x3c)); pen.fill_rect(6.0, 11.0, 12.0, 9.0, c); @@ -762,7 +826,7 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { Lineweight => { for (k, w) in [1.0f32, 2.0, 3.5].iter().enumerate() { let y = 6.0 + k as f32 * 6.0; - p.line_segment([pen.at(3.0, y), pen.at(21.0, y)], Stroke::new(w * pen.sc(), b)); + pen.p.line_segment([pen.at(3.0, y), pen.at(21.0, y)], Stroke::new(w * pen.sc(), b)); } } Transparency => { @@ -784,7 +848,7 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.c(12.0, 12.0, 4.0, b); for i in 0..8 { let ang = (i as f32 * 45.0).to_radians(); - p.line_segment( + pen.p.line_segment( [pen.at(12.0 + 6.0 * ang.cos(), 12.0 + 6.0 * ang.sin()), pen.at(12.0 + 9.0 * ang.cos(), 12.0 + 9.0 * ang.sin())], Stroke::new(2.2 * pen.sc(), b), ); @@ -801,6 +865,13 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.l(9.0, 4.0, 9.0, 20.0); pen.l(9.0, 9.0, 21.0, 9.0); } + _ => {} + } +} + +fn paint_navigation_controls(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, _dim: bool) { + use Icon::*; + match icon { ChevronDown => pen.poly(&[(6.0, 9.0), (12.0, 15.0), (18.0, 9.0)], false, b), ChevronUp => pen.poly(&[(6.0, 15.0), (12.0, 9.0), (18.0, 15.0)], false, b), ChevronLeft => pen.poly(&[(15.0, 6.0), (9.0, 12.0), (15.0, 18.0)], false, b), @@ -827,6 +898,14 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.l(4.0, y, 20.0, y); } } + _ => {} + } +} + +fn paint_constraints(pen: &Pen<'_>, icon: Icon, b: Color32, a: Color32, dim: bool) { + let alpha = |c: Color32| if dim { c.gamma_multiply(0.4) } else { c }; + use Icon::*; + match icon { Constraint | Fix => { pen.fill_rect(7.0, 11.0, 10.0, 9.0, Color32::from_rgb(0xe6, 0xb4, 0x3c)); pen.arc(12.0, 11.0, 4.0, 0.0, 180.0, Color32::from_rgb(0xe6, 0xb4, 0x3c)); @@ -940,6 +1019,7 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.poly(&[(3.0, 4.0), (19.0, 4.0), (13.0, 11.0), (13.0, 19.0), (9.0, 17.0), (9.0, 11.0)], true, b); pen.poly(&[(20.0, 10.0), (16.5, 15.5), (20.0, 15.5), (17.0, 21.0)], false, a); } + _ => {} } } diff --git a/crates/ui-egui/src/layers.rs b/crates/ui-egui/src/layers.rs index 8a95138eb..1801265fd 100644 --- a/crates/ui-egui/src/layers.rs +++ b/crates/ui-egui/src/layers.rs @@ -377,7 +377,19 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) { } ui.end_row(); for l in &shown { - row(ui, l, &cur, &used, vp.as_ref(), &linetypes, &library, &mut st, &mut actions); + row( + ui, + LayerRow { + layer: l, + current: &cur, + used: &used, + viewport: vp.as_ref(), + linetypes: &linetypes, + library: &library, + state: &mut st, + actions: &mut actions, + }, + ); ui.end_row(); } }, @@ -405,91 +417,22 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) { } } -/// One Layer Properties Manager row: status/name, on/freeze/lock/plot, colour, linetype, lineweight, transparency, viewport overrides, new-viewport freeze and description. -/// Edits update `st` and push commands into `actions`. -#[allow(clippy::too_many_arguments)] -fn row( - ui: &mut egui::Ui, - l: &Layer, - cur: &str, - used: &std::collections::HashSet, - vp: Option<&(Handle, cadcraft_doc::Viewport)>, - linetypes: &[cadcraft_doc::Linetype], - library: &[String], - st: &mut LpmState, - actions: &mut Vec<(&'static str, Value)>, -) { +/// One Layer Properties Manager row. +struct LayerRow<'a> { + layer: &'a Layer, + current: &'a str, + used: &'a std::collections::HashSet, + viewport: Option<&'a (Handle, cadcraft_doc::Viewport)>, + linetypes: &'a [cadcraft_doc::Linetype], + library: &'a [String], + state: &'a mut LpmState, + actions: &'a mut Vec<(&'static str, Value)>, +} + +fn row(ui: &mut egui::Ui, args: LayerRow<'_>) { + let LayerRow { layer: l, current: cur, used, viewport: vp, linetypes, library, state: st, actions } = args; let t = Tokens::get(); - let name = l.name.clone(); - let current = name.eq_ignore_ascii_case(cur); - let is_used = used.contains(&name.to_ascii_lowercase()); - let selected = st.selected.as_deref().is_some_and(|s| s.eq_ignore_ascii_case(&name)); - // Status. - let (sr, sresp) = ui.allocate_exact_size(vec2(22.0, 20.0), Sense::click()); - if selected { - ui.painter().rect_filled(sr, 2.0, t.accent.gamma_multiply(0.35)); - } - status_icon(ui.painter(), sr.shrink(2.0), current, is_used); - let tip = if current { - "Current layer" - } else if is_used { - "Layer contains objects (double-click to make current)" - } else { - "Layer is empty (double-click to make current)" - }; - if sresp.on_hover_text(crate::i18n::t(tip)).double_clicked() { - actions.push(("layer.current", json!({ "name": name }))); - } - // Name (double-click or F2-style rename). - match st.renaming.clone() { - Some((n, mut buf)) if n.eq_ignore_ascii_case(&name) => { - let r = ui - .add(egui::TextEdit::singleline(&mut buf).desired_width(150.0).id(egui::Id::unique(("lpm_rename", &name)))) - .accessible_name(crate::tl!("Name")); - if !r.has_focus() && !r.lost_focus() { - r.request_focus(); - } - if r.lost_focus() { - let cancel = ui.input(|i| i.key_pressed(egui::Key::Escape)); - if !cancel && buf.trim() != name && !buf.trim().is_empty() { - actions.push(("layer.set", json!({ "name": name, "newName": buf.trim() }))); - st.selected = Some(buf.trim().to_string()); - } - st.renaming = None; - } else { - st.renaming = Some((n, buf)); - } - } - _ => { - let (nr, r) = ui.allocate_exact_size(vec2(150.0, 20.0), Sense::click()); - if selected { - ui.painter().rect_filled(nr, 2.0, t.accent.gamma_multiply(0.45)); - } else if r.hovered() { - ui.painter().rect_filled(nr, 2.0, t.control_hover.gamma_multiply(0.5)); - } - ui.painter().text(pos2(nr.left() + 4.0, nr.center().y), egui::Align2::LEFT_CENTER, &name, crate::theme::body(), t.text); - if r.clicked() { - st.selected = Some(name.clone()); - } - if r.double_clicked() && name != "0" && !name.eq_ignore_ascii_case("Defpoints") { - st.renaming = Some((name.clone(), name.clone())); - } - r.context_menu(|ui| { - if ui.button(crate::tl!("Set Current")).clicked() { - actions.push(("layer.current", json!({ "name": name }))); - ui.close(); - } - if ui.button(crate::tl!("Rename Layer")).clicked() { - st.renaming = Some((name.clone(), name.clone())); - ui.close(); - } - if ui.button(crate::tl!("Delete Layer")).clicked() { - actions.push(("layer.delete", json!({ "name": name }))); - ui.close(); - } - }); - } - } + let name = layer_identity(ui, l, cur, used, st, actions); // On / Freeze / Lock / Plot. if icons::button(ui, if l.on { Icon::Bulb } else { Icon::BulbOff }, 20.0, "On/Off", false).clicked() { actions.push(("layer.set", json!({ "name": name, "on": !l.on }))); @@ -684,7 +627,88 @@ fn row( } } -/// Lower-case names of layers with objects on them. +fn layer_identity( + ui: &mut egui::Ui, + l: &Layer, + cur: &str, + used: &std::collections::HashSet, + st: &mut LpmState, + actions: &mut Vec<(&'static str, Value)>, +) -> String { + let t = Tokens::get(); + let name = l.name.clone(); + let current = name.eq_ignore_ascii_case(cur); + let is_used = used.contains(&name.to_ascii_lowercase()); + let selected = st.selected.as_deref().is_some_and(|s| s.eq_ignore_ascii_case(&name)); + // Status. + let (sr, sresp) = ui.allocate_exact_size(vec2(22.0, 20.0), Sense::click()); + if selected { + ui.painter().rect_filled(sr, 2.0, t.accent.gamma_multiply(0.35)); + } + status_icon(ui.painter(), sr.shrink(2.0), current, is_used); + let tip = if current { + "Current layer" + } else if is_used { + "Layer contains objects (double-click to make current)" + } else { + "Layer is empty (double-click to make current)" + }; + if sresp.on_hover_text(crate::i18n::t(tip)).double_clicked() { + actions.push(("layer.current", json!({ "name": name }))); + } + // Name (double-click or F2-style rename). + match st.renaming.clone() { + Some((n, mut buf)) if n.eq_ignore_ascii_case(&name) => { + let r = ui + .add(egui::TextEdit::singleline(&mut buf).desired_width(150.0).id(egui::Id::unique(("lpm_rename", &name)))) + .accessible_name(crate::tl!("Name")); + if !r.has_focus() && !r.lost_focus() { + r.request_focus(); + } + if r.lost_focus() { + let cancel = ui.input(|i| i.key_pressed(egui::Key::Escape)); + if !cancel && buf.trim() != name && !buf.trim().is_empty() { + actions.push(("layer.set", json!({ "name": name, "newName": buf.trim() }))); + st.selected = Some(buf.trim().to_string()); + } + st.renaming = None; + } else { + st.renaming = Some((n, buf)); + } + } + _ => { + let (nr, r) = ui.allocate_exact_size(vec2(150.0, 20.0), Sense::click()); + if selected { + ui.painter().rect_filled(nr, 2.0, t.accent.gamma_multiply(0.45)); + } else if r.hovered() { + ui.painter().rect_filled(nr, 2.0, t.control_hover.gamma_multiply(0.5)); + } + ui.painter().text(pos2(nr.left() + 4.0, nr.center().y), egui::Align2::LEFT_CENTER, &name, crate::theme::body(), t.text); + if r.clicked() { + st.selected = Some(name.clone()); + } + if r.double_clicked() && name != "0" && !name.eq_ignore_ascii_case("Defpoints") { + st.renaming = Some((name.clone(), name.clone())); + } + r.context_menu(|ui| { + if ui.button(crate::tl!("Set Current")).clicked() { + actions.push(("layer.current", json!({ "name": name }))); + ui.close(); + } + if ui.button(crate::tl!("Rename Layer")).clicked() { + st.renaming = Some((name.clone(), name.clone())); + ui.close(); + } + if ui.button(crate::tl!("Delete Layer")).clicked() { + actions.push(("layer.delete", json!({ "name": name }))); + ui.close(); + } + }); + } + } + name +} + fn used_layers(d: &cadcraft_doc::Drawing) -> std::collections::HashSet { let mut set = std::collections::HashSet::new(); let ents = d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter())); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 43b135d19..ca287f982 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -85,7 +85,7 @@ pub fn run_ui_command(app: &mut CadApp, id: &str, params: &Value) -> Option Option> { +fn route_ui_subsystems(app: &mut CadApp, id: &str, params: &Value) -> Option> { if let Some(result) = crate::spacemouse::route(app, id, params) { return Some(result); } @@ -118,14 +118,10 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option Option> { if id == RECENT_CLEAR { app.ui.recent_files.clear(); return Some(Ok(Value::Null)); @@ -135,22 +131,191 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option Option> { let r = match id { - "ui.language" => { - let Some(preference) = params.get("lang").and_then(Value::as_str).and_then(crate::i18n::Preference::parse) else { - return Some(Err(format!("language must be {}", crate::i18n::Preference::codes()))); - }; - app.ui.interface_language = preference; - let shown = app.language(); - if shown != preference.resolve(&app.system_languages) { - app.set_status(if preference.resolve(&app.system_languages) == "ko" { - "No font covers the Korean interface, so it stays in English. Install a Hangul font (Malgun Gothic or Nanum Gothic), or build with a Hang/Kore craft-fonts face." - } else { - "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese." - }); - } - Ok(json!({"interfaceLanguage": preference.code(), "shown": shown})) + "ui.theme.system" | "ui.theme.light" | "ui.theme.dark" => { + let pref = crate::theme::ThemePref::parse(id.trim_start_matches("ui.theme.")).unwrap_or_default(); + set_theme(app, pref) } + // `{"theme": "system"|"light"|"dark"}` sets the choice; without it, reports it. + "ui.theme" => match params.get("theme").and_then(Value::as_str) { + Some(name) => match crate::theme::ThemePref::parse(name) { + Some(pref) => set_theme(app, pref), + None => Err(format!("unknown theme \"{name}\" (system, light or dark)")), + }, + None => Ok(theme_json(app)), + }, + "ui.animations.system" | "ui.animations.on" | "ui.animations.off" => { + let pref = crate::animation::AnimationPref::parse(id.trim_start_matches("ui.animations.")).unwrap_or_default(); + Ok(set_animations(app, pref)) + } + // `{"animations": "system"|"on"|"off"}` sets the choice; without it, reports it. + "ui.animations" => match params.get("animations").and_then(Value::as_str) { + Some(name) => match crate::animation::AnimationPref::parse(name) { + Some(pref) => Ok(set_animations(app, pref)), + None => Err(format!("unknown animations choice \"{}\" (system, on or off)", name.chars().take(40).collect::())), + }, + None => Ok(animations_json(app)), + }, + "ui.saveformat.dxf" | "ui.saveformat.dwg" => { + let format = crate::SaveFormat::parse(id.trim_start_matches("ui.saveformat.")).unwrap_or_default(); + Ok(set_save_format(app, format)) + } + // `{"format": "dxf"|"dwg"}` sets the default save format for new drawings; without it, + // reports it. + "ui.saveformat" => match params.get("format").and_then(Value::as_str) { + Some(name) => match crate::SaveFormat::parse(name) { + Some(format) => Ok(set_save_format(app, format)), + None => Err(format!("unknown save format \"{name}\" (dxf or dwg)")), + }, + None => Ok(json!({ "saveFormat": app.ui.save_format.as_str() })), + }, + // `{"version": "2000"…"2018"}` sets the version new drawings are saved in; without it, + // reports it. + id if id.starts_with("ui.saveversion") => { + let name = id + .strip_prefix("ui.saveversion.") + .map(str::to_string) + .or_else(|| params.get("version").map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string))); + match name { + Some(name) => match crate::SaveVersion::parse(&name) { + Some(v) => { + app.ui.save_version = v.year().to_string(); + app.session.echo(format!("New drawings will be saved as version {v}")); + Ok(json!({ "saveVersion": app.ui.save_version })) + } + None => Err(format!("unknown version \"{}\" (2000, 2004, 2007, 2010, 2013 or 2018)", name.chars().take(40).collect::())), + }, + None => Ok(json!({ "saveVersion": app.ui.save_version })), + } + } + _ => return None, + }; + Some(r) +} + +fn run_ui_toggle_command(app: &mut CadApp, id: &str, params: &Value) -> Option> { + let toggle = |b: &mut bool, p: &Value| { + *b = p.get("on").and_then(Value::as_bool).unwrap_or(!*b); + }; + let result = match id { + "ui.toggle.toolsets" => { + if params.get("on").and_then(Value::as_bool) == Some(true) { + return Some(crate::docking::command(app, false, &json!({"operation":"open", "panel":"toolSets"}))); + } + toggle(&mut app.ui.show_toolsets, params); + Ok(Value::Null) + } + "ui.toggle.palettes" => { + toggle(&mut app.ui.show_palettes, params); + Ok(Value::Null) + } + "ui.toggle.toolbar" => { + toggle(&mut app.ui.show_toolbar, params); + Ok(Value::Null) + } + "ui.toggle.filetabs" => { + toggle(&mut app.ui.show_file_tabs, params); + Ok(Value::Null) + } + "ui.toggle.statusbar" => { + toggle(&mut app.ui.show_status_bar, params); + Ok(Value::Null) + } + "ui.toggle.cmdline" => { + toggle(&mut app.ui.show_command_line, params); + Ok(Value::Null) + } + "ui.toggle.systemcursor" => { + toggle(&mut app.ui.system_cursor, params); + Ok(Value::Null) + } + "ui.toggle.viewcube" => { + toggle(&mut app.ui.show_viewcube, params); + Ok(Value::Null) + } + "ui.toggle.ucsicon" => { + toggle(&mut app.ui.show_ucs_icon, params); + Ok(Value::Null) + } + "ui.toggle.menubar" => { + toggle(&mut app.ui.in_window_menu, params); + Ok(Value::Null) + } + "ui.toggle.history" | "textscr" => { + let open = json!({"on": true}); + toggle(&mut app.cmd.expanded, if id == "textscr" { &open } else { params }); + Ok(json!({ "on": app.cmd.expanded })) + } + _ => return None, + }; + Some(result) +} + +fn run_language_command(app: &mut CadApp, id: &str, params: &Value) -> Option> { + if id != "ui.language" { + return None; + } + let Some(preference) = params.get("lang").and_then(Value::as_str).and_then(crate::i18n::Preference::parse) else { + return Some(Err(format!("language must be {}", crate::i18n::Preference::codes()))); + }; + app.ui.interface_language = preference; + let shown = app.language(); + if shown != preference.resolve(&app.system_languages) { + app.set_status(if preference.resolve(&app.system_languages) == "ko" { + "No font covers the Korean interface, so it stays in English. Install a Hangul font (Malgun Gothic or Nanum Gothic), or build with a Hang/Kore craft-fonts face." + } else { + "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese." + }); + } + Some(Ok(json!({"interfaceLanguage": preference.code(), "shown": shown}))) +} + +fn run_close_command(app: &mut CadApp, id: &str, params: &Value) -> Option> { + match id { + "close" if params.is_null() => { + app.request_close(None); + Some(Ok(Value::Null)) + } + "closeall" if params.is_null() => { + app.request_close_all(); + Some(Ok(Value::Null)) + } + _ => None, + } +} + +fn should_pick_file_path(id: &str, params: &Value) -> bool { + if id.starts_with("ui.") { params.is_null() || (params.get("path").is_none() && params.get("data").is_none()) } else { params.is_null() } +} + +fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option> { + if let Some(result) = route_ui_subsystems(app, id, params) { + return Some(result); + } + // When a file command should ask for a file. The `ui.*` commands are the pickers; the engine's + // OPEN/SAVEAS/QSAVE ask only when typed or chosen from a menu (no parameters), so JSON calls + // never open a picker. + let no_path = should_pick_file_path(id, params); + if let Some(result) = run_recent_file_command(app, id) { + return Some(result); + } + if let Some(result) = run_preference_command(app, id, params) { + return Some(result); + } + if let Some(result) = run_ui_toggle_command(app, id, params) { + return Some(result); + } + if let Some(result) = run_language_command(app, id, params) { + return Some(result); + } + if let Some(result) = run_close_command(app, id, params) { + return Some(result); + } + let r = match id { "ui.open" | "open" if no_path => { let picked = app.services.pick_open.as_ref().and_then(|f| f()); if let Some(p) = picked { @@ -228,109 +393,9 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option { - if params.get("on").and_then(Value::as_bool) == Some(true) { - return Some(crate::docking::command(app, false, &json!({"operation":"open", "panel":"toolSets"}))); - } - toggle(&mut app.ui.show_toolsets, params); - Ok(Value::Null) - } - "ui.toggle.palettes" => { - toggle(&mut app.ui.show_palettes, params); - Ok(Value::Null) - } - "ui.toggle.toolbar" => { - toggle(&mut app.ui.show_toolbar, params); - Ok(Value::Null) - } - "ui.toggle.filetabs" => { - toggle(&mut app.ui.show_file_tabs, params); - Ok(Value::Null) - } - // Typed or chosen from a menu or the tool bar: ask for the file (or show the preview). "export" | "exp" | "import" | "imp" | "publish" | "preview" | "pre" if params.is_null() && app.session.state().is_ok() => { return Some(crate::exchange::route(app, id)); } - "ui.toggle.statusbar" => { - toggle(&mut app.ui.show_status_bar, params); - Ok(Value::Null) - } - "ui.toggle.cmdline" => { - toggle(&mut app.ui.show_command_line, params); - Ok(Value::Null) - } - "ui.toggle.systemcursor" => { - toggle(&mut app.ui.system_cursor, params); - Ok(Value::Null) - } - "ui.toggle.viewcube" => { - toggle(&mut app.ui.show_viewcube, params); - Ok(Value::Null) - } - "ui.toggle.ucsicon" => { - toggle(&mut app.ui.show_ucs_icon, params); - Ok(Value::Null) - } - "ui.toggle.menubar" => { - toggle(&mut app.ui.in_window_menu, params); - Ok(Value::Null) - } - "ui.theme.system" | "ui.theme.light" | "ui.theme.dark" => { - let pref = crate::theme::ThemePref::parse(id.trim_start_matches("ui.theme.")).unwrap_or_default(); - set_theme(app, pref) - } - // `{"theme": "system"|"light"|"dark"}` sets the choice; without it, reports it. - "ui.theme" => match params.get("theme").and_then(Value::as_str) { - Some(name) => match crate::theme::ThemePref::parse(name) { - Some(pref) => set_theme(app, pref), - None => Err(format!("unknown theme \"{name}\" (system, light or dark)")), - }, - None => Ok(theme_json(app)), - }, - "ui.animations.system" | "ui.animations.on" | "ui.animations.off" => { - let pref = crate::animation::AnimationPref::parse(id.trim_start_matches("ui.animations.")).unwrap_or_default(); - Ok(set_animations(app, pref)) - } - // `{"animations": "system"|"on"|"off"}` sets the choice; without it, reports it. - "ui.animations" => match params.get("animations").and_then(Value::as_str) { - Some(name) => match crate::animation::AnimationPref::parse(name) { - Some(pref) => Ok(set_animations(app, pref)), - None => Err(format!("unknown animations choice \"{}\" (system, on or off)", name.chars().take(40).collect::())), - }, - None => Ok(animations_json(app)), - }, - "ui.saveformat.dxf" | "ui.saveformat.dwg" => { - let format = crate::SaveFormat::parse(id.trim_start_matches("ui.saveformat.")).unwrap_or_default(); - Ok(set_save_format(app, format)) - } - // `{"format": "dxf"|"dwg"}` sets the default save format for new drawings; without it, - // reports it. - "ui.saveformat" => match params.get("format").and_then(Value::as_str) { - Some(name) => match crate::SaveFormat::parse(name) { - Some(format) => Ok(set_save_format(app, format)), - None => Err(format!("unknown save format \"{name}\" (dxf or dwg)")), - }, - None => Ok(json!({ "saveFormat": app.ui.save_format.as_str() })), - }, - // `{"version": "2000"…"2018"}` sets the version new drawings are saved in; without it, - // reports it. - id if id.starts_with("ui.saveversion") => { - let name = id - .strip_prefix("ui.saveversion.") - .map(str::to_string) - .or_else(|| params.get("version").map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string))); - match name { - Some(name) => match crate::SaveVersion::parse(&name) { - Some(v) => { - app.ui.save_version = v.year().to_string(); - app.session.echo(format!("New drawings will be saved as version {v}")); - Ok(json!({ "saveVersion": app.ui.save_version })) - } - None => Err(format!("unknown version \"{}\" (2000, 2004, 2007, 2010, 2013 or 2018)", name.chars().take(40).collect::())), - }, - None => Ok(json!({ "saveVersion": app.ui.save_version })), - } - } "ui.hidepalettes" => { app.ui.show_palettes = false; app.ui.show_toolsets = false; @@ -347,12 +412,6 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option { - let open = json!({"on": true}); - toggle(&mut app.cmd.expanded, if id == "textscr" { &open } else { params }); - Ok(json!({ "on": app.cmd.expanded })) - } - // `{"lines": n}`: how many history lines show above the command line (0–12); without it, reports it. "ui.cmdline.lines" => { if let Some(n) = params.get("lines").and_then(Value::as_u64) { app.ui.history_lines = n.min(crate::cmdline::MAX_LINES as u64) as usize; @@ -389,14 +448,6 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option Ok(Value::Null), // From a menu or typed, closing asks about unsaved changes; JSON calls close without asking. - "close" if params.is_null() => { - app.request_close(None); - Ok(Value::Null) - } - "closeall" if params.is_null() => { - app.request_close_all(); - Ok(Value::Null) - } "layer" | "la" | "layers" if params.is_null() => { app.ui.dialog = Some("layers".into()); Ok(Value::Null) diff --git a/crates/ui-egui/src/parametric.rs b/crates/ui-egui/src/parametric.rs index ade7a39fa..fb9edec5b 100644 --- a/crates/ui-egui/src/parametric.rs +++ b/crates/ui-egui/src/parametric.rs @@ -259,18 +259,20 @@ enum Act { } /// Draw constraint bars and dynamic constraints. `xf` maps model space to the screen. -pub fn draw_overlay(app: &mut CadApp, ui: &mut egui::Ui, painter: &egui::Painter, xf: &Xf) { - refresh(app); - let Ok(d) = app.session.doc() else { return }; - if d.constraints.is_empty() { - app.canvas.param.editing = None; - return; - } - let t = Tokens::get(); - let set = &d.parametric.settings; - let mut act: Option = None; - let glyphs = &app.canvas.param.glyphs; - let (lu, lp) = (d.header.i64("LUNITS", 2), d.header.i64("LUPREC", 4)); +struct DimensionalContext<'a> { + doc: &'a Drawing, + settings: &'a cadcraft_doc::ParametricSettings, + glyphs: &'a [Glyph], + xf: &'a Xf, + painter: &'a egui::Painter, + ui: &'a mut egui::Ui, + tokens: Tokens, + units: (i64, i64), +} + +fn draw_dimensional_constraints(frame: DimensionalContext<'_>) -> Option { + let DimensionalContext { doc: d, settings: set, glyphs, xf, painter, ui, tokens: t, units: (lu, lp) } = frame; + let mut act = None; // ----- dynamic (dimensional) constraints ----- let dim_c = Color32::from_rgb(0x9f, 0xb6, 0xd6); let off_px: f32 = 26.0; @@ -403,6 +405,13 @@ pub fn draw_overlay(app: &mut CadApp, ui: &mut egui::Ui, painter: &egui::Painter expression = c.expr )); } + act +} + +fn draw_geometric_constraints(app: &CadApp, ui: &mut egui::Ui, painter: &egui::Painter, xf: &Xf, t: Tokens) { + let Ok(d) = app.session.doc() else { return }; + let set = &d.parametric.settings; + let glyphs = &app.canvas.param.glyphs; // ----- geometric constraint bars ----- let mut bars: Vec<(Handle, Vec2, Vec<(&str, u32)>)> = Vec::new(); let mut dots: Vec = Vec::new(); @@ -488,6 +497,21 @@ pub fn draw_overlay(app: &mut CadApp, ui: &mut egui::Ui, painter: &egui::Painter painter.rect_filled(r, 2.0, t.canvas.gamma_multiply(0.85)); painter.galley(at + vec2(4.0, 2.0), galley, col); } +} + +pub fn draw_overlay(app: &mut CadApp, ui: &mut egui::Ui, painter: &egui::Painter, xf: &Xf) { + refresh(app); + let Ok(d) = app.session.doc() else { return }; + if d.constraints.is_empty() { + app.canvas.param.editing = None; + return; + } + let t = Tokens::get(); + let set = &d.parametric.settings; + let glyphs = &app.canvas.param.glyphs; + let (lu, lp) = (d.header.i64("LUNITS", 2), d.header.i64("LUPREC", 4)); + let act = draw_dimensional_constraints(DimensionalContext { doc: d, settings: set, glyphs, xf, painter, ui, tokens: t, units: (lu, lp) }); + draw_geometric_constraints(app, ui, painter, xf, t); let _ = act_take(app, act, ui); } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 701013cdc..aaa577fa7 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,6 +5,9 @@ edition.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] serde_json = { workspace = true } chrono = { workspace = true }