mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-06 02:19:49 -06:00
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
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+3
-1
@@ -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."*"]
|
||||
|
||||
@@ -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
|
||||
|
||||
+341
-185
@@ -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<f64>) -> 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<f64>) -> 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<Built, String> {
|
||||
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<Built, String> {
|
||||
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<f64>,
|
||||
}
|
||||
|
||||
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<Vec2> {
|
||||
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<Ell> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Deserialize, PolyVertex, Serialize, Vec3};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Line {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<EntityKind> {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
|
||||
@@ -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<Value> {
|
||||
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))) {
|
||||
|
||||
@@ -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<EntityKind> {
|
||||
|
||||
@@ -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<Value> {
|
||||
let id = if diameter { "dimdiameter" } else { "dimradius" };
|
||||
|
||||
@@ -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<Value> {
|
||||
let c = if let Some(center) = point_param(p, "center") {
|
||||
|
||||
@@ -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<Value> {
|
||||
if p.get("isocircle").is_some() {
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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<Value> {
|
||||
let pts = match point_param(p, "at") {
|
||||
|
||||
@@ -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<Value> {
|
||||
// A `sides` that is not a whole number (e.g. -3) is refused, not replaced by the default 4.
|
||||
|
||||
@@ -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<Value> {
|
||||
if p.get("object").is_some() {
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Value> {
|
||||
let c = if let (Some(c), Some(d)) = (point_param(p, "center"), p.get("diameter").and_then(Value::as_f64)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Option<Step>> {
|
||||
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<EntityKind> {
|
||||
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() };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+236
-199
@@ -216,212 +216,249 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.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<String, Value>,
|
||||
) {
|
||||
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<String, Value>,
|
||||
) {
|
||||
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<Color>,
|
||||
linetype: Option<&'a str>,
|
||||
lineweight: Option<Lineweight>,
|
||||
transparency: Option<Transparency>,
|
||||
thickness: Option<f64>,
|
||||
}
|
||||
|
||||
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<Value> {
|
||||
let c = p.get("color").and_then(parse_color).ok_or_else(|| bad("color", "`color` is required"))?;
|
||||
set_current_color(s.doc_mut()?, c);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+86
-61
@@ -119,74 +119,19 @@ pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option<EntityK
|
||||
c.radius = r;
|
||||
}
|
||||
EntityKind::Arc(a) => {
|
||||
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<EntityK
|
||||
Some(k)
|
||||
}
|
||||
|
||||
fn stretch_arc(kind: &EntityKind, arc: &cadcraft_doc::Arc, index: usize, to: Vec2, delta: Vec2) -> Option<EntityKind> {
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 `<default>` the next time they ask (as AutoCAD does). Values AutoCAD keeps in a drawing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Arc, LispError, Value};
|
||||
|
||||
pub(super) type R = Result<Value, LispError>;
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Value;
|
||||
use super::args::{R, arity, num, point};
|
||||
use super::*;
|
||||
|
||||
// ---------------- geometry ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::args::{R, arity, int, num, real_or_int};
|
||||
use super::*;
|
||||
use super::{LispError, Ordering, Value, eq};
|
||||
|
||||
// ---------------- numbers ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
|
||||
@@ -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, `,`
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Arc, Drawing, Handle, Interactive, Serialize, Vec2};
|
||||
|
||||
/// An interactive command in progress.
|
||||
pub struct Running {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Arc, EngineError, Result, Session, Snapshot, Value, cmd, find_command};
|
||||
|
||||
impl Session {
|
||||
// ---------------- programmatic execution ----------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ----------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Bounds2, Handle, Input, PendingWindow, Result, Session, Vec2, cmd, select};
|
||||
|
||||
impl Session {
|
||||
// ---------------- picking without a command ----------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Bounds2, Result, Session, Space, Vec2, View};
|
||||
|
||||
impl Session {
|
||||
// ---------------- views ----------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
+100
-91
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
+64
-56
@@ -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<Line> {
|
||||
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>, 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<Line> = Vec::new();
|
||||
let mut line = Line::default();
|
||||
let mut pending_space: Option<usize> = None;
|
||||
let mut last_fmt = 0usize;
|
||||
let mut para = 0usize;
|
||||
let finish = |lines: &mut Vec<Line>, 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<Option<bool>> = Vec::new();
|
||||
|
||||
+150
-88
@@ -402,6 +402,154 @@ fn split_tabs(segs: &[(Seg, usize)]) -> Vec<Part> {
|
||||
out
|
||||
}
|
||||
|
||||
struct LineBuilder<'a> {
|
||||
paragraphs: &'a Paragraphs,
|
||||
formats: &'a [Fmt],
|
||||
base: &'a Fmt,
|
||||
width: f64,
|
||||
height: f64,
|
||||
lines: Vec<PLine>,
|
||||
line: PLine,
|
||||
paragraph: usize,
|
||||
pending_space: Option<usize>,
|
||||
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<PLine> {
|
||||
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<PLine> = 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<usize> = None;
|
||||
let mut last_fmt = 0usize;
|
||||
let finish = |lines: &mut Vec<PLine>, 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),
|
||||
|
||||
+89
-56
@@ -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<String, bool> = 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<String, String> = 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<String, usize> = 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<usize, bool> = HashMap::new();
|
||||
for i in 0..objects.len() {
|
||||
let mut path: Vec<usize> = 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<String> = 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<String, bool> {
|
||||
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<String, String> {
|
||||
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<String, bool>,
|
||||
records: &HashMap<String, ForeignKey>,
|
||||
by_handle: &HashMap<String, usize>,
|
||||
root: Option<&str>,
|
||||
root_entries: &[(String, String)],
|
||||
xdict_of: &HashMap<String, String>,
|
||||
) -> HashMap<usize, bool> {
|
||||
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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<Tag>)],
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
+188
-173
@@ -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<EntityKind> {
|
||||
let (origin, background) = hatch_xdata(tags);
|
||||
@@ -19,181 +19,11 @@ pub(super) fn hatch(tags: &[Tag], t: &T) -> Option<EntityKind> {
|
||||
let flags = get(i)?.i64();
|
||||
i += 1;
|
||||
let outer = flags & 1 != 0;
|
||||
let mut verts: Vec<PolyVertex> = 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<EntityKind> {
|
||||
}))
|
||||
}
|
||||
|
||||
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<Vec<PolyVertex>> {
|
||||
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<Vec<PolyVertex>> {
|
||||
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<PolyVertex>) {
|
||||
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<PolyVertex>) {
|
||||
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<PolyVertex>) {
|
||||
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<PolyVertex>) {
|
||||
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<Handle> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<Tag>)], 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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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`]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user