mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Dimension edits: DIMEDIT Oblique/New/Rotate/Home, DIMJOGGED, DIMJOGLINE, DIMBREAK, DIMINSPECT (part of #507) (#522)
Squash of PR #522 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b7b37e29cd
commit
c334fedf31
21 files changed
+1504
-5
No files matched your search
@@ -412,6 +412,89 @@ pub struct Dimension {
|
||||
/// Associativity: which definition points follow which objects.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub assoc: Vec<DimAssoc>,
|
||||
/// Oblique extension lines, jogs, breaks and inspection frames.
|
||||
#[serde(default, skip_serializing_if = "DimMarks::is_empty")]
|
||||
pub marks: DimMarks,
|
||||
}
|
||||
|
||||
/// What dimension edits add to a dimension's drawing: DIMEDIT Oblique, DIMJOGLINE, DIMJOGGED,
|
||||
/// DIMBREAK and DIMINSPECT.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct DimMarks {
|
||||
/// Angle (radians) of the extension lines of a linear or aligned dimension; None =
|
||||
/// perpendicular to the dimension line.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub oblique: Option<f64>,
|
||||
/// A jog on the dimension line of a linear or aligned dimension, at this point (DIMJOGLINE).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jog_line: Option<Vec3>,
|
||||
/// A jogged radius dimension (DIMJOGGED).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jogged: Option<JoggedRadius>,
|
||||
/// Gaps in the dimension and extension lines (DIMBREAK).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub breaks: Vec<DimBreak>,
|
||||
/// Inspection frame, label and rate (DIMINSPECT).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inspect: Option<Inspection>,
|
||||
}
|
||||
|
||||
impl DimMarks {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
*self == DimMarks::default()
|
||||
}
|
||||
|
||||
/// Transform the points and angles (part of [`EntityKind::transform`]).
|
||||
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 });
|
||||
}
|
||||
if let Some(j) = &mut self.jog_line {
|
||||
t3(j);
|
||||
}
|
||||
if let Some(j) = &mut self.jogged {
|
||||
t3(&mut j.center);
|
||||
t3(&mut j.jog);
|
||||
}
|
||||
for b in &mut self.breaks {
|
||||
t3(&mut b.at);
|
||||
b.size *= s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The definition of a jogged radius dimension: the dimension line starts at an overriding
|
||||
/// centre and jogs on its way to the arc.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct JoggedRadius {
|
||||
/// Where the dimension line starts instead of the true centre.
|
||||
pub center: Vec3,
|
||||
/// The middle of the jog.
|
||||
pub jog: Vec3,
|
||||
}
|
||||
|
||||
/// A gap in a dimension's lines: everything within `size / 2` of `at` is left out.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct DimBreak {
|
||||
pub at: Vec3,
|
||||
pub size: f64,
|
||||
/// Made by DIMBREAK Auto (recomputed by the next Auto) rather than picked by hand.
|
||||
pub auto: bool,
|
||||
}
|
||||
|
||||
/// An inspection dimension (DIMINSPECT).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Inspection {
|
||||
/// Frame shape: 0 round ends, 1 angular ends, 2 none.
|
||||
pub shape: u8,
|
||||
/// Text left of the value ("" = none).
|
||||
pub label: String,
|
||||
/// Inspection rate right of the value ("" = none).
|
||||
pub rate: String,
|
||||
}
|
||||
|
||||
/// A dimension definition point that follows an object (associative dimensions).
|
||||
@@ -1025,6 +1108,7 @@ impl EntityKind {
|
||||
if let DimKind::Linear { rotation } = &mut d.kind {
|
||||
*rotation = cadcraft_geom::norm_angle(*rotation + rot);
|
||||
}
|
||||
d.marks.transform(&t3, rot, s, mirror);
|
||||
d.block = None;
|
||||
}
|
||||
EntityKind::MLine(ml) => {
|
||||
|
||||
@@ -442,6 +442,7 @@ fn invisible_dimblock_entity_is_ignored_in_dimension_extents() {
|
||||
block: Some("*D1".into()),
|
||||
overrides: Default::default(),
|
||||
assoc: vec![],
|
||||
marks: Default::default(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Dimensions and leaders: DIMLINEAR, DIMALIGNED, DIMRADIUS, DIMDIAMETER, DIMANGULAR, DIMARC,
|
||||
//! DIMORDINATE, DIMCONTINUE, DIMBASELINE, QDIM, DIM (smart), MLEADER, LEADER, DIMOVERRIDE,
|
||||
//! DIMREASSOCIATE / DIMDISASSOCIATE, DIMTEDIT, DIMSPACE.
|
||||
//! DIMREASSOCIATE / DIMDISASSOCIATE, DIMTEDIT, DIMSPACE; DIMEDIT, DIMJOGGED, DIMJOGLINE,
|
||||
//! DIMBREAK and DIMINSPECT in `annotate_dimedit.rs`.
|
||||
|
||||
use cadcraft_doc::{AssocSnap, DimAssoc, DimKind, Dimension, EntityKind, Handle, MLeader, MText, Prim};
|
||||
use cadcraft_geom::{Segment, Vec2, Vec3};
|
||||
@@ -106,6 +107,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.menu(&["Dimension", "Dimension Space"])
|
||||
.params("{base: hex, handles: [hex], spacing?: number (default DIMDLI)}"),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(dimedit::specs())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn style(s: &Session) -> String {
|
||||
@@ -129,6 +133,7 @@ fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2, p15: Vec2,
|
||||
block: None,
|
||||
overrides: s.doc().map(|d| d.dim_overrides()).unwrap_or_default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -371,6 +376,7 @@ fn chained(s: &Session, prev: &Dimension, next: Vec2, baseline: bool) -> EntityK
|
||||
text: String::new(),
|
||||
block: None,
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
user_text_pos: false,
|
||||
..prev.clone()
|
||||
})
|
||||
@@ -1778,6 +1784,9 @@ impl Interactive for MLeaderM {
|
||||
#[path = "annotate_tedit.rs"]
|
||||
mod tedit;
|
||||
|
||||
#[path = "annotate_dimedit.rs"]
|
||||
mod dimedit;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "annotate_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,778 @@
|
||||
//! Dimension edits: DIMEDIT (Home/New/Rotate/Oblique), DIMJOGGED, DIMJOGLINE, DIMBREAK and
|
||||
//! DIMINSPECT. Each has a JSON form; the prompt sequences run the same JSON form.
|
||||
|
||||
use cadcraft_doc::{DimBreak, DimKind, Dimension, EntityKind, Handle, Inspection, JoggedRadius, Prim};
|
||||
use cadcraft_geom::{Segment, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::machines::{SelOutcome, SelectPhase};
|
||||
use super::*;
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Most objects DIMBREAK Auto looks at, and most breaks it adds to one dimension.
|
||||
const MAX_BREAK_OBJECTS: usize = 20_000;
|
||||
const MAX_AUTO_BREAKS: usize = 1000;
|
||||
|
||||
pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("dimedit", "Dimension Edit", run_dimedit)
|
||||
.alias(&["ded", "dimed"])
|
||||
.params("{handles?, mode?: home|new|rotate|oblique (default home), text? (new: \"\" or \"<>\" = the measurement), angle?: degrees (rotate: text angle; oblique: extension line angle, absent = none)}")
|
||||
.interactive(|_| Ok(Box::new(DimEditM::new(None)))),
|
||||
CommandSpec::new("dimedit.oblique", "Oblique", |s, p| run_dimedit(s, &with_mode(p, "oblique")))
|
||||
.menu(&["Dimension", "Oblique"])
|
||||
.params("{handles?, angle?: degrees}")
|
||||
.interactive(|_| Ok(Box::new(DimEditM::new(Some(Mode::Oblique))))),
|
||||
CommandSpec::new("dimjogged", "Jogged", run_jogged)
|
||||
.menu(&["Dimension", "Jogged"])
|
||||
.alias(&["djo", "jog"])
|
||||
.params("{handle (arc or circle), centerOverride: [x,y], at: [x,y] (dimension line location), jog?: [x,y], text?}")
|
||||
.interactive(|_| Ok(Box::new(JoggedM::default()))),
|
||||
CommandSpec::new("dimjogline", "Jogged Linear", run_jogline)
|
||||
.menu(&["Dimension", "Jogged Linear"])
|
||||
.alias(&["djl"])
|
||||
.params("{handle (linear or aligned dimension), at?: [x,y] (jog location; default between the text and the first extension line), remove?: bool}")
|
||||
.interactive(|_| Ok(Box::new(JogLineM::default()))),
|
||||
CommandSpec::new("dimbreak", "Dimension Break", run_dimbreak)
|
||||
.menu(&["Dimension", "Dimension Break"])
|
||||
.params("{handles?, mode?: auto|manual|remove (default auto), objects?: [hex] (auto: the objects that break; default all crossing objects), first?/second?: [x,y] (manual), size?: number (default 0.125 × DIMSCALE)}")
|
||||
.interactive(|_| Ok(Box::new(DimBreakM::default()))),
|
||||
CommandSpec::new("diminspect", "Inspection", run_diminspect)
|
||||
.menu(&["Dimension", "Inspection"])
|
||||
.params("{handles?, shape?: round|angular|none (default round), label?: string, rate?: string (default \"100%\"), remove?: bool}")
|
||||
.interactive(|_| Ok(Box::new(InspectM::default()))),
|
||||
]
|
||||
}
|
||||
|
||||
fn with_mode(p: &Value, mode: &str) -> Value {
|
||||
let mut p = p.clone();
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.insert("mode".into(), json!(mode));
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn handles_json(hs: &[Handle]) -> Value {
|
||||
json!(hs.iter().map(|h| h.hex()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Change each dimension of `hs` with `f` (its block regenerated); returns how many changed.
|
||||
fn edit_dims(s: &mut Session, hs: &[Handle], f: &dyn Fn(&mut Dimension) -> bool) -> Result<usize> {
|
||||
let d = s.doc_mut()?;
|
||||
let mut n = 0;
|
||||
for h in hs {
|
||||
let mut changed = false;
|
||||
d.modify_entity(*h, |e| {
|
||||
if let EntityKind::Dimension(dm) = &mut e.kind
|
||||
&& f(dm)
|
||||
{
|
||||
dm.block = None;
|
||||
changed = true;
|
||||
}
|
||||
})?;
|
||||
n += usize::from(changed);
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn is_linear(dm: &Dimension) -> bool {
|
||||
matches!(dm.kind, DimKind::Linear { .. } | DimKind::Aligned)
|
||||
}
|
||||
|
||||
// ---------------- DIMEDIT ----------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Mode {
|
||||
Home,
|
||||
New,
|
||||
Rotate,
|
||||
Oblique,
|
||||
}
|
||||
|
||||
fn run_dimedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = dims_of(s, &targets(s, p)?);
|
||||
if hs.is_empty() {
|
||||
return Err(bad("dimedit", "select dimensions"));
|
||||
}
|
||||
let mode = str_param(p, "mode").unwrap_or("home").to_ascii_lowercase();
|
||||
let angle = match p.get("angle") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(v.as_f64().filter(|a| a.is_finite()).ok_or_else(|| bad("dimedit", "`angle` must be a number (degrees)"))?.to_radians()),
|
||||
};
|
||||
let n = match mode.as_str() {
|
||||
"home" => edit_dims(s, &hs, &|dm| {
|
||||
dm.user_text_pos = false;
|
||||
dm.text_rotation = 0.0;
|
||||
dm.overrides.remove("textJust");
|
||||
true
|
||||
})?,
|
||||
"new" => {
|
||||
let t = str_param(p, "text").unwrap_or("").to_string();
|
||||
// "<>" alone is the measurement, which is what no override text means.
|
||||
let t = if t.trim() == "<>" { String::new() } else { t };
|
||||
edit_dims(s, &hs, &|dm| {
|
||||
dm.text = t.clone();
|
||||
true
|
||||
})?
|
||||
}
|
||||
"rotate" => {
|
||||
let a = angle.ok_or_else(|| bad("dimedit", "`angle` (degrees) is required"))?;
|
||||
edit_dims(s, &hs, &|dm| {
|
||||
dm.text_rotation = cadcraft_geom::norm_angle(a);
|
||||
true
|
||||
})?
|
||||
}
|
||||
"oblique" => edit_dims(s, &hs, &|dm| {
|
||||
if !is_linear(dm) {
|
||||
return false;
|
||||
}
|
||||
dm.marks.oblique = angle.map(cadcraft_geom::norm_angle);
|
||||
true
|
||||
})?,
|
||||
other => return Err(bad("dimedit", format!("unknown mode `{other}` (home, new, rotate, oblique)"))),
|
||||
};
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
|
||||
/// DIMEDIT at the command line: the kind of edit, then (New) the text or (Rotate) the angle,
|
||||
/// the dimensions, and (Oblique) the extension line angle.
|
||||
struct DimEditM {
|
||||
mode: Option<Mode>,
|
||||
text: Option<String>,
|
||||
angle: Option<f64>,
|
||||
selecting: Option<SelectPhase>,
|
||||
picked: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl DimEditM {
|
||||
fn new(mode: Option<Mode>) -> Self {
|
||||
DimEditM { mode, text: None, angle: None, selecting: None, picked: Vec::new() }
|
||||
}
|
||||
/// "Select objects", or the pickfirst selection at once.
|
||||
fn select(&mut self, s: &mut Session) -> Result<Step> {
|
||||
let ph = SelectPhase::begin(s);
|
||||
if ph.done {
|
||||
return self.after_selection(s, ph.picked);
|
||||
}
|
||||
self.selecting = Some(ph);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn run(&self, s: &mut Session, hs: &[Handle], angle: Option<f64>) -> Result<Step> {
|
||||
let (mode, extra) = match self.mode {
|
||||
Some(Mode::New) => ("new", json!({ "text": self.text.clone().unwrap_or_default() })),
|
||||
Some(Mode::Rotate) => ("rotate", json!({ "angle": angle.unwrap_or(0.0).to_degrees() })),
|
||||
Some(Mode::Oblique) => ("oblique", json!({ "angle": angle.map(f64::to_degrees) })),
|
||||
_ => ("home", json!({})),
|
||||
};
|
||||
let mut p = extra;
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.insert("handles".into(), handles_json(hs));
|
||||
o.insert("mode".into(), json!(mode));
|
||||
}
|
||||
let r = run_dimedit(s, &p)?;
|
||||
s.echo(format!("{} dimension(s) changed.", r.get("changed").and_then(Value::as_u64).unwrap_or(0)));
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Done)
|
||||
}
|
||||
fn after_selection(&mut self, s: &mut Session, hs: Vec<Handle>) -> Result<Step> {
|
||||
let hs = dims_of(s, &hs);
|
||||
if hs.is_empty() {
|
||||
s.echo("No dimensions selected.");
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
if self.mode == Some(Mode::Oblique) {
|
||||
self.picked = hs;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
self.run(s, &hs, self.angle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for DimEditM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMEDIT"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
// Dimension ▸ Oblique asks for the dimensions first.
|
||||
if self.mode == Some(Mode::Oblique) {
|
||||
return self.select(s);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(sel) = &self.selecting {
|
||||
return sel.prompt();
|
||||
}
|
||||
match self.mode {
|
||||
None => Prompt::new("Enter type of dimension editing", Accept::TEXT).kw(&["Home", "New", "Rotate", "Oblique"]).default("Home"),
|
||||
Some(Mode::New) => Prompt::new("Enter new dimension text", Accept::TEXT).default("<>"),
|
||||
Some(Mode::Rotate) => Prompt::new("Specify angle for dimension text", Accept::NUMBER),
|
||||
_ => Prompt::new("Enter obliquing angle (press ENTER for none)", Accept::NUMBER),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(sel) = &mut self.selecting {
|
||||
return match sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => {
|
||||
self.selecting = None;
|
||||
self.after_selection(s, hs)
|
||||
}
|
||||
};
|
||||
}
|
||||
match (self.mode, i) {
|
||||
(None, Input::Keyword(k)) => {
|
||||
self.mode = match k.as_str() {
|
||||
"New" => Some(Mode::New),
|
||||
"Rotate" => Some(Mode::Rotate),
|
||||
"Oblique" => Some(Mode::Oblique),
|
||||
_ => Some(Mode::Home),
|
||||
};
|
||||
if matches!(self.mode, Some(Mode::Home | Mode::Oblique)) {
|
||||
return self.select(s);
|
||||
}
|
||||
}
|
||||
(None, Input::Enter) => {
|
||||
self.mode = Some(Mode::Home);
|
||||
return self.select(s);
|
||||
}
|
||||
(None, Input::Text(_)) => s.echo("Invalid option keyword."),
|
||||
(Some(Mode::New), Input::Text(t)) if self.text.is_none() => {
|
||||
self.text = Some(t);
|
||||
return self.select(s);
|
||||
}
|
||||
(Some(Mode::New), Input::Enter) if self.text.is_none() => {
|
||||
self.text = Some(String::new());
|
||||
return self.select(s);
|
||||
}
|
||||
(Some(Mode::Rotate), Input::Text(t)) if self.angle.is_none() => match s.angle_settings().direction(&t) {
|
||||
Some(a) => {
|
||||
self.angle = Some(a);
|
||||
return self.select(s);
|
||||
}
|
||||
None => s.echo("Requires a valid angle."),
|
||||
},
|
||||
(Some(Mode::Oblique), Input::Text(t)) if !self.picked.is_empty() => match s.angle_settings().direction(&t) {
|
||||
Some(a) => return self.run(s, &self.picked.clone(), Some(a)),
|
||||
None => s.echo("Requires a valid angle."),
|
||||
},
|
||||
(Some(Mode::Oblique), Input::Enter) if !self.picked.is_empty() => return self.run(s, &self.picked.clone(), None),
|
||||
(_, Input::Cancel) => return Ok(Step::Cancel),
|
||||
(Some(_), Input::Enter) => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, _s: &Session, _c: Vec2) -> Vec<EntityKind> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- DIMJOGGED ----------------
|
||||
|
||||
/// A jogged radius dimension of the circle (`c`, `r`): the arrow where the direction of `at`
|
||||
/// meets it, the line from `center` (the override) with a jog at `jog` (default halfway).
|
||||
fn jogged_kind(s: &Session, c: Vec2, r: f64, center: Vec2, at: Vec2, jog: Option<Vec2>, text: &str) -> Option<EntityKind> {
|
||||
let dir = (at - c).normalized();
|
||||
if dir == Vec2::ZERO || !dir.is_finite() || r.is_nan() || r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let tip = c + dir * r;
|
||||
let jog = jog.unwrap_or_else(|| center.mid(tip));
|
||||
let mut k = dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, tip, Vec2::ZERO, text);
|
||||
if let EntityKind::Dimension(dm) = &mut k {
|
||||
dm.marks.jogged = Some(JoggedRadius { center: v3(center), jog: v3(jog) });
|
||||
if at.dist(c) > r + 1e-9 {
|
||||
dm.text_mid = v3(at);
|
||||
dm.user_text_pos = true;
|
||||
}
|
||||
}
|
||||
Some(k)
|
||||
}
|
||||
|
||||
fn run_jogged(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("dimjogged", "`handle` of an arc or circle is required"))?;
|
||||
let (c, r) = circle_of(s, h).ok_or_else(|| bad("dimjogged", "object is not a circle or arc"))?;
|
||||
let center = point_req("dimjogged", p, "centerOverride")?;
|
||||
let at = point_req("dimjogged", p, "at")?;
|
||||
let k = jogged_kind(s, c, r, center, at, point_param(p, "jog"), str_param(p, "text").unwrap_or(""))
|
||||
.ok_or_else(|| bad("dimjogged", "`at` must differ from the centre"))?;
|
||||
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct JoggedM {
|
||||
circle: Option<(Vec2, f64)>,
|
||||
center: Option<Vec2>,
|
||||
at: Option<Vec2>,
|
||||
text: DimText,
|
||||
}
|
||||
|
||||
impl Interactive for JoggedM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMJOGGED"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
match (self.circle, self.center, self.at) {
|
||||
(None, ..) => Prompt::new("Select arc or circle", Accept::POINT),
|
||||
(Some((c, _)), None, _) => Prompt::new("Specify center location override", Accept::POINT).base(c),
|
||||
(Some(_), Some(o), None) => Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle"]).base(o),
|
||||
(Some(_), Some(o), Some(_)) => Prompt::new("Specify jog location", Accept::POINT).base(o),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match (self.circle, self.center, self.at, i) {
|
||||
(None, _, _, Input::Point(p)) => match pick_at(s, p).and_then(|h| circle_of(s, h)) {
|
||||
Some(t) => self.circle = Some(t),
|
||||
None => s.echo("Object selected is not a circle or arc."),
|
||||
},
|
||||
(Some(_), None, _, Input::Point(p)) => self.center = Some(p),
|
||||
(Some(_), Some(_), None, Input::Keyword(k)) => {
|
||||
self.text.keyword(&k);
|
||||
}
|
||||
(Some(_), Some(_), None, Input::Point(p)) => self.at = Some(p),
|
||||
(Some((c, r)), Some(o), Some(at), Input::Point(j)) => {
|
||||
if let Some(k) = jogged_kind(s, c, r, o, at, Some(j), "") {
|
||||
add_dim(s, self.text.apply(k))?;
|
||||
}
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(_, _, _, Input::Enter | Input::Cancel) => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, s: &Session, p: Vec2) -> Vec<EntityKind> {
|
||||
let Some((c, r)) = self.circle else { return Vec::new() };
|
||||
let (o, at, jog) = match (self.center, self.at) {
|
||||
(Some(o), None) => (o, p, None),
|
||||
(Some(o), Some(at)) => (o, at, Some(p)),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
jogged_kind(s, c, r, o, at, jog, "").map(|k| vec![self.text.apply(k)]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- DIMJOGLINE ----------------
|
||||
|
||||
/// Where a dimension line meets the first extension line, and the dimension line direction.
|
||||
fn first_foot(dm: &Dimension) -> Option<(Vec2, Vec2)> {
|
||||
let dir = match dm.kind {
|
||||
DimKind::Linear { rotation } => Vec2::from_angle(rotation),
|
||||
DimKind::Aligned => (dm.p14.xy() - dm.p13.xy()).normalized(),
|
||||
_ => return None,
|
||||
};
|
||||
if dir == Vec2::ZERO || !dir.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let on = dm.defpt.xy();
|
||||
Some((on + dir * (dm.p13.xy() - on).dot(dir), dir))
|
||||
}
|
||||
|
||||
fn run_jogline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = dims_of(s, &targets(s, p)?);
|
||||
let h = *hs.first().ok_or_else(|| bad("dimjogline", "`handle` of a linear or aligned dimension is required"))?;
|
||||
let d = s.doc()?;
|
||||
let Some(EntityKind::Dimension(dm)) = d.entity(h).map(|e| &e.kind) else { return Err(bad("dimjogline", "not a dimension")) };
|
||||
let (f1, dir) = first_foot(dm).ok_or_else(|| bad("dimjogline", "only linear and aligned dimensions take a jog"))?;
|
||||
let remove = bool_or(p, "remove", false);
|
||||
let at = match point_param(p, "at") {
|
||||
Some(at) => at,
|
||||
None => {
|
||||
// Halfway between the text and the first extension line.
|
||||
let t = cadcraft_render::dimension_in(d, dm).text_pos;
|
||||
f1.mid(f1 + dir * (t - f1).dot(dir))
|
||||
}
|
||||
};
|
||||
edit_dims(s, &[h], &|dm| {
|
||||
dm.marks.jog_line = if remove { None } else { Some(v3(at)) };
|
||||
true
|
||||
})?;
|
||||
Ok(json!({ "handle": h.hex() }))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct JogLineM {
|
||||
dim: Option<Handle>,
|
||||
removing: bool,
|
||||
}
|
||||
|
||||
impl Interactive for JogLineM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMJOGLINE"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match (self.dim, self.removing) {
|
||||
(None, false) => Prompt::new("Select dimension to add jog", Accept::SELECT).kw(&["Remove"]),
|
||||
(None, true) => Prompt::new("Select jog to remove", Accept::SELECT),
|
||||
(Some(_), _) => Prompt::new("Specify jog location (or press ENTER)", Accept::POINT),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match (self.dim, i) {
|
||||
(None, Input::Keyword(k)) if k == "Remove" => self.removing = true,
|
||||
(None, Input::Pick(hs)) => {
|
||||
let lin = |s: &Session, h: Handle| {
|
||||
s.doc().ok().and_then(|d| d.entity(h)).is_some_and(|e| matches!(&e.kind, EntityKind::Dimension(dm) if is_linear(dm)))
|
||||
};
|
||||
match dims_of(s, &hs).into_iter().find(|h| lin(s, *h)) {
|
||||
Some(h) if self.removing => {
|
||||
run_jogline(s, &json!({ "handle": h.hex(), "remove": true }))?;
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
Some(h) => self.dim = Some(h),
|
||||
None if hs.is_empty() => {}
|
||||
None => s.echo("Select a linear or aligned dimension."),
|
||||
}
|
||||
}
|
||||
(Some(h), Input::Point(at)) => {
|
||||
run_jogline(s, &json!({ "handle": h.hex(), "at": [at.x, at.y] }))?;
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(Some(h), Input::Enter) => {
|
||||
run_jogline(s, &json!({ "handle": h.hex() }))?;
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(_, Input::Enter | Input::Cancel) => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, _s: &Session, _c: Vec2) -> Vec<EntityKind> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- DIMBREAK ----------------
|
||||
|
||||
/// Where the dimension's drawn lines cross the objects: (point, direction of the line there).
|
||||
fn crossings(dm_lines: &[Vec<Vec2>], prims: &[Prim]) -> Vec<Vec2> {
|
||||
let mut out = Vec::new();
|
||||
for l in dm_lines {
|
||||
for w in l.windows(2) {
|
||||
let seg = Segment::Line(cadcraft_geom::Line::new(w[0], w[1]));
|
||||
for p in prims {
|
||||
let pts = match p {
|
||||
Prim::Seg(o) => cadcraft_geom::intersect_segments(&seg, o),
|
||||
Prim::Circle(c) => cadcraft_geom::line_circle(&cadcraft_geom::Line::new(w[0], w[1]), c)
|
||||
.into_iter()
|
||||
.filter(|(_, t)| (-1e-9..=1.0 + 1e-9).contains(t))
|
||||
.map(|(q, _)| q)
|
||||
.collect(),
|
||||
Prim::Infinite { base, dir, ray } => cadcraft_geom::line_line_infinite(w[0], w[1], *base, *base + *dir)
|
||||
.filter(|(_, t, u)| (-1e-9..=1.0 + 1e-9).contains(t) && (!ray || *u >= -1e-9))
|
||||
.map(|(q, _, _)| vec![q])
|
||||
.unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for q in pts {
|
||||
if out.len() >= MAX_AUTO_BREAKS {
|
||||
return out;
|
||||
}
|
||||
if !out.iter().any(|o: &Vec2| o.dist(q) < 1e-9) {
|
||||
out.push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn run_dimbreak(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = dims_of(s, &targets(s, p)?);
|
||||
if hs.is_empty() {
|
||||
return Err(bad("dimbreak", "select dimensions"));
|
||||
}
|
||||
let mode = str_param(p, "mode").unwrap_or("auto").to_ascii_lowercase();
|
||||
let d = s.doc()?;
|
||||
let size = match size_param("dimbreak", p, "size", false)? {
|
||||
Some(x) => x,
|
||||
None => 0.125 * Some(d.header.f64("DIMSCALE", 1.0)).filter(|k| *k > 0.0 && *k < 1e12).unwrap_or(1.0),
|
||||
};
|
||||
match mode.as_str() {
|
||||
"remove" => {
|
||||
let n = edit_dims(s, &hs, &|dm| {
|
||||
let had = !dm.marks.breaks.is_empty();
|
||||
dm.marks.breaks.clear();
|
||||
had
|
||||
})?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
"manual" => {
|
||||
let (a, b) = (point_req("dimbreak", p, "first")?, point_req("dimbreak", p, "second")?);
|
||||
if a.near(b, 1e-12) {
|
||||
return Err(bad("dimbreak", "`first` and `second` must differ"));
|
||||
}
|
||||
let br = DimBreak { at: v3(a.mid(b)), size: a.dist(b), auto: false };
|
||||
let n = edit_dims(s, &hs[..1], &|dm| {
|
||||
dm.marks.breaks.push(br.clone());
|
||||
true
|
||||
})?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
"auto" => {
|
||||
// The breaking objects: those given, else every object (not a dimension or text).
|
||||
let given: Option<Vec<Handle>> = match p.get("objects") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(_) => Some(targets(s, &json!({ "handles": p.get("objects") }))?),
|
||||
};
|
||||
let space = s.space();
|
||||
let store = d.space(&space).ok_or_else(|| bad("dimbreak", "no current space"))?;
|
||||
let others: Vec<(Handle, Vec<Prim>)> = store
|
||||
.iter()
|
||||
.filter(|e| match &given {
|
||||
Some(g) => g.contains(&e.handle),
|
||||
None => !matches!(e.kind, EntityKind::Dimension(_) | EntityKind::Text(_) | EntityKind::MText(_)),
|
||||
})
|
||||
.take(MAX_BREAK_OBJECTS)
|
||||
.map(|e| (e.handle, e.kind.prims()))
|
||||
.collect();
|
||||
let mut plan = Vec::new();
|
||||
for h in &hs {
|
||||
let Some(EntityKind::Dimension(dm)) = d.entity(*h).map(|e| &e.kind) else { continue };
|
||||
let mut plain = dm.clone();
|
||||
plain.marks.breaks.clear();
|
||||
let g = cadcraft_render::dimension_in(d, &plain);
|
||||
let prims: Vec<Prim> = others.iter().filter(|(o, _)| o != h).flat_map(|(_, p)| p.iter().cloned()).collect();
|
||||
plan.push((*h, crossings(&g.lines, &prims)));
|
||||
}
|
||||
let mut n = 0;
|
||||
for (h, pts) in plan {
|
||||
n += edit_dims(s, &[h], &|dm| {
|
||||
dm.marks.breaks.retain(|b| !b.auto);
|
||||
dm.marks.breaks.extend(pts.iter().map(|q| DimBreak { at: v3(*q), size, auto: true }));
|
||||
true
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
other => Err(bad("dimbreak", format!("unknown mode `{other}` (auto, manual, remove)"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DimBreakM {
|
||||
dims: Vec<Handle>,
|
||||
multiple: Option<SelectPhase>,
|
||||
objects: Vec<Handle>,
|
||||
manual: Vec<Vec2>,
|
||||
manual_mode: bool,
|
||||
}
|
||||
|
||||
impl DimBreakM {
|
||||
fn run(&self, s: &mut Session, mut p: Value) -> Result<Step> {
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.insert("handles".into(), handles_json(&self.dims));
|
||||
}
|
||||
let r = run_dimbreak(s, &p)?;
|
||||
s.echo(format!("{} dimension(s) changed.", r.get("changed").and_then(Value::as_u64).unwrap_or(0)));
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for DimBreakM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMBREAK"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(sel) = &self.multiple {
|
||||
return sel.prompt();
|
||||
}
|
||||
if self.dims.is_empty() {
|
||||
return Prompt::new("Select dimension to add/remove break", Accept::SELECT).kw(&["Multiple"]);
|
||||
}
|
||||
if self.manual_mode {
|
||||
return Prompt::new(if self.manual.is_empty() { "Specify first break point" } else { "Specify second break point" }, Accept::POINT)
|
||||
.base_opt(self.manual.first().copied());
|
||||
}
|
||||
if self.objects.is_empty() {
|
||||
let kws: &[&str] = if self.dims.len() == 1 { &["Auto", "Manual", "Remove"] } else { &["Auto", "Remove"] };
|
||||
Prompt::new("Select object to break dimension", Accept::SELECT).kw(kws).default("Auto")
|
||||
} else {
|
||||
Prompt::new("Select object to break dimension", Accept::SELECT)
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(sel) = &mut self.multiple {
|
||||
return match sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => {
|
||||
self.multiple = None;
|
||||
self.dims = dims_of(s, &hs);
|
||||
Ok(if self.dims.is_empty() { Step::Done } else { Step::Continue })
|
||||
}
|
||||
};
|
||||
}
|
||||
if self.dims.is_empty() {
|
||||
match i {
|
||||
Input::Keyword(k) if k == "Multiple" => self.multiple = Some(SelectPhase::default()),
|
||||
Input::Pick(hs) => match dims_of(s, &hs).first() {
|
||||
Some(h) => self.dims = vec![*h],
|
||||
None if hs.is_empty() => {}
|
||||
None => s.echo("Object selected is not a dimension."),
|
||||
},
|
||||
Input::Enter | Input::Cancel => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.manual_mode {
|
||||
return match i {
|
||||
Input::Point(p) if self.manual.is_empty() => {
|
||||
self.manual.push(p);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Input::Point(p) => {
|
||||
let a = self.manual.first().copied().unwrap_or(p);
|
||||
self.run(s, json!({ "mode": "manual", "first": [a.x, a.y], "second": [p.x, p.y] }))
|
||||
}
|
||||
Input::Enter | Input::Cancel => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
};
|
||||
}
|
||||
match i {
|
||||
Input::Keyword(k) if k == "Auto" => return self.run(s, json!({ "mode": "auto" })),
|
||||
Input::Keyword(k) if k == "Remove" => return self.run(s, json!({ "mode": "remove" })),
|
||||
Input::Keyword(k) if k == "Manual" => self.manual_mode = true,
|
||||
Input::Pick(hs) => {
|
||||
for h in hs {
|
||||
if !self.dims.contains(&h) && !self.objects.contains(&h) {
|
||||
self.objects.push(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::Enter if self.objects.is_empty() => return self.run(s, json!({ "mode": "auto" })),
|
||||
Input::Enter => return self.run(s, json!({ "mode": "auto", "objects": handles_json(&self.objects) })),
|
||||
Input::Cancel => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, _s: &Session, _c: Vec2) -> Vec<EntityKind> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- DIMINSPECT ----------------
|
||||
|
||||
fn run_diminspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = dims_of(s, &targets(s, p)?);
|
||||
if hs.is_empty() {
|
||||
return Err(bad("diminspect", "select dimensions"));
|
||||
}
|
||||
if bool_or(p, "remove", false) {
|
||||
let n = edit_dims(s, &hs, &|dm| dm.marks.inspect.take().is_some())?;
|
||||
return Ok(json!({ "changed": n }));
|
||||
}
|
||||
let shape = match str_param(p, "shape").unwrap_or("round").to_ascii_lowercase().as_str() {
|
||||
"round" => 0,
|
||||
"angular" => 1,
|
||||
"none" => 2,
|
||||
other => return Err(bad("diminspect", format!("unknown shape `{other}` (round, angular, none)"))),
|
||||
};
|
||||
let cap = |t: &str| t.chars().take(256).collect::<String>();
|
||||
let ins = Inspection { shape, label: cap(str_param(p, "label").unwrap_or("")), rate: cap(str_param(p, "rate").unwrap_or("100%")) };
|
||||
let n = edit_dims(s, &hs, &|dm| {
|
||||
dm.marks.inspect = Some(ins.clone());
|
||||
true
|
||||
})?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
|
||||
/// DIMINSPECT at the command line: the dimensions, then shape (or Remove), label and rate.
|
||||
#[derive(Default)]
|
||||
struct InspectM {
|
||||
selecting: Option<SelectPhase>,
|
||||
dims: Vec<Handle>,
|
||||
shape: Option<&'static str>,
|
||||
label: Option<String>,
|
||||
}
|
||||
|
||||
impl Interactive for InspectM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMINSPECT"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
let ph = SelectPhase::begin(s);
|
||||
if ph.done {
|
||||
self.dims = dims_of(s, &ph.picked);
|
||||
if self.dims.is_empty() {
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
} else {
|
||||
self.selecting = Some(ph);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(sel) = &self.selecting {
|
||||
return sel.prompt();
|
||||
}
|
||||
match (self.shape, &self.label) {
|
||||
(None, _) => Prompt::new("Enter inspection shape", Accept::TEXT).kw(&["Round", "Angular", "None", "Remove"]).default("Round"),
|
||||
(Some(_), None) => Prompt::new("Enter label (press ENTER for none)", Accept::TEXT),
|
||||
(Some(_), Some(_)) => Prompt::new("Enter inspection rate", Accept::TEXT).default("100%"),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(sel) = &mut self.selecting {
|
||||
return match sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => {
|
||||
self.selecting = None;
|
||||
self.dims = dims_of(s, &hs);
|
||||
if self.dims.is_empty() {
|
||||
s.echo("No dimensions selected.");
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
};
|
||||
}
|
||||
let handles = handles_json(&self.dims);
|
||||
match (self.shape, self.label.clone(), i) {
|
||||
(None, _, Input::Keyword(k)) if k == "Remove" => {
|
||||
run_diminspect(s, &json!({ "handles": handles, "remove": true }))?;
|
||||
s.set_selection(Vec::new());
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(None, _, Input::Keyword(k)) => {
|
||||
self.shape = Some(match k.as_str() {
|
||||
"Angular" => "angular",
|
||||
"None" => "none",
|
||||
_ => "round",
|
||||
})
|
||||
}
|
||||
(None, _, Input::Enter) => self.shape = Some("round"),
|
||||
(Some(_), None, Input::Text(t)) => self.label = Some(t),
|
||||
(Some(_), None, Input::Enter) => self.label = Some(String::new()),
|
||||
(Some(shape), Some(label), Input::Text(t)) => {
|
||||
run_diminspect(s, &json!({ "handles": handles, "shape": shape, "label": label, "rate": t }))?;
|
||||
s.set_selection(Vec::new());
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(Some(shape), Some(label), Input::Enter) => {
|
||||
run_diminspect(s, &json!({ "handles": handles, "shape": shape, "label": label }))?;
|
||||
s.set_selection(Vec::new());
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(_, _, Input::Cancel) => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, _s: &Session, _c: Vec2) -> Vec<EntityKind> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ fn dim(kind: DimKind, p13: Vec2, p14: Vec2, defpt: Vec2) -> EntityKind {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -180,6 +181,7 @@ pub fn bracket() -> Drawing {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}),
|
||||
);
|
||||
add(
|
||||
@@ -200,6 +202,7 @@ pub fn bracket() -> Drawing {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
//! DIMEDIT (Home/New/Rotate/Oblique), DIMJOGGED, DIMJOGLINE, DIMBREAK and DIMINSPECT: the JSON
|
||||
//! forms, the prompt sequences and what they draw.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Dimension, EntityKind, Handle};
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use cadcraft_engine::render::{DimGeometry, LineRole};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn dim_of(s: &Session, h: &str) -> (Dimension, DimGeometry) {
|
||||
let d = s.doc().unwrap();
|
||||
match &d.entity(Handle::parse_hex(h).unwrap()).unwrap().kind {
|
||||
EntityKind::Dimension(dm) => (dm.clone(), cadcraft_engine::render::dimension_in(d, dm)),
|
||||
k => panic!("not a dimension: {k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(r: &Value) -> String {
|
||||
r["handle"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn linear(s: &mut Session) -> String {
|
||||
handle(&s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [10, 0], "at": [5, 3] })).unwrap())
|
||||
}
|
||||
|
||||
/// Whether a line of the role on y = `y` covers `x`.
|
||||
fn covers(g: &DimGeometry, role: LineRole, y: f64, x: f64) -> bool {
|
||||
g.lines_of(role)
|
||||
.any(|l| l.windows(2).any(|w| (w[0].y - y).abs() < 1e-9 && (w[1].y - y).abs() < 1e-9 && w[0].x.min(w[1].x) < x && x < w[0].x.max(w[1].x)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimedit_modes() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "new", "text": "<> TYP" })).unwrap();
|
||||
assert_eq!(dim_of(&s, &h).1.value, "10.0000 TYP");
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate", "angle": 30 })).unwrap();
|
||||
assert!((dim_of(&s, &h).1.text_angle - 30f64.to_radians()).abs() < 1e-9);
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "home" })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert!(dm.text_rotation == 0.0 && !dm.user_text_pos && g.text_angle == 0.0);
|
||||
// Oblique: extension lines at 60°, the measurement unchanged.
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique", "angle": 60 })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert!((dm.marks.oblique.unwrap() - 60f64.to_radians()).abs() < 1e-9);
|
||||
assert_eq!(g.value, "10.0000 TYP");
|
||||
for l in g.lines_of(LineRole::Ext) {
|
||||
let d = l[1] - l[0];
|
||||
assert!((d.y.atan2(d.x).to_degrees() - 60.0).abs() < 1e-6, "{l:?}");
|
||||
}
|
||||
// The dimension line still runs along y = 3, between the oblique feet.
|
||||
let foot = 3.0 / 60f64.to_radians().tan();
|
||||
assert!(covers(&g, LineRole::Dim, 3.0, foot + 0.5) && covers(&g, LineRole::Dim, 3.0, 10.0 + foot - 0.5));
|
||||
// No angle: back to perpendicular. Other kinds are left alone.
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique" })).unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.oblique, None);
|
||||
s.execute("circle", &json!({ "center": [30, 0], "radius": 5 })).unwrap();
|
||||
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
let r = handle(&s.execute("dimradius", &json!({ "handle": c })).unwrap());
|
||||
let n = s.execute("dimedit", &json!({ "handles": [r], "mode": "oblique", "angle": 60 })).unwrap();
|
||||
assert_eq!(n["changed"], json!(0));
|
||||
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "bogus" })).is_err());
|
||||
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate" })).is_err());
|
||||
assert!(s.execute("dimedit", &json!({ "handles": [h], "mode": "rotate", "angle": "x" })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimedit_prompts() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
s.cmdline("dimedit").unwrap();
|
||||
assert!(s.prompt_text().contains("[Home/New/Rotate/Oblique] <Home>"), "{}", s.prompt_text());
|
||||
// Oblique: the dimensions first, then the angle.
|
||||
s.script("o\n4,3\n\n45\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
assert!((dim_of(&s, &h).0.marks.oblique.unwrap() - 45f64.to_radians()).abs() < 1e-9);
|
||||
// Rotate: the angle first.
|
||||
s.script("dimedit r 15\n4,3\n\n").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert!((dim_of(&s, &h).0.text_rotation - 15f64.to_radians()).abs() < 1e-9);
|
||||
// Home by default.
|
||||
s.script("dimedit\n\n4,3\n\n").unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.text_rotation, 0.0);
|
||||
// The Dimension ▸ Oblique menu item asks for the dimensions straight away.
|
||||
s.cmdline("dimedit.oblique").unwrap();
|
||||
assert!(s.prompt_text().contains("Select objects"), "{}", s.prompt_text());
|
||||
s.script("4,3\n\n\n").unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.oblique, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jogged_radius() {
|
||||
let mut s = Session::new();
|
||||
s.execute("circle", &json!({ "center": [0, 0], "radius": 50 })).unwrap();
|
||||
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
let r = s.execute("dimjogged", &json!({ "handle": c, "centerOverride": [20, -10], "at": [40, 40], "jog": [30, 15] })).unwrap();
|
||||
let h = handle(&r);
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
let j = dm.marks.jogged.clone().unwrap();
|
||||
assert_eq!(g.value, "R50.0000", "the true radius");
|
||||
assert!(j.center.xy().dist(Vec2::new(20.0, -10.0)) < 1e-9);
|
||||
// The line runs from the arrowhead on the circle to the override, with a 45° jog (DIMJOGANG).
|
||||
let tip = Vec2::new(1.0, 1.0).normalized() * 50.0;
|
||||
let path = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap().clone();
|
||||
assert!(path[0].dist(tip) < 1e-9 && path[3].dist(Vec2::new(20.0, -10.0)) < 1e-9, "{path:?}");
|
||||
let u = tip.normalized();
|
||||
let jog = path[2] - path[1];
|
||||
let angle = jog.normalized().dot(u).abs().acos().to_degrees();
|
||||
assert!((angle - 45.0).abs() < 1e-6, "{angle}");
|
||||
assert!((path[3] - path[2]).normalized().cross(u).abs() < 1e-9, "parallel to the first part");
|
||||
// DIMJOGANG changes the jog.
|
||||
s.execute("setvar", &json!({ "name": "DIMJOGANG", "value": 60 })).unwrap();
|
||||
let h2 = handle(&s.execute("dimjogged", &json!({ "handle": c, "centerOverride": [20, -10], "at": [40, 40] })).unwrap());
|
||||
let g = dim_of(&s, &h2).1;
|
||||
let path = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap().clone();
|
||||
assert!(((path[2] - path[1]).normalized().dot(u).abs().acos().to_degrees() - 60.0).abs() < 1e-6);
|
||||
// At the command line.
|
||||
s.script("dimjogged 0,50\n10,-20\n0,70\n5,20\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let last = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
let (dm, _) = dim_of(&s, &last);
|
||||
let j = dm.marks.jogged.unwrap();
|
||||
assert!(j.center.xy().dist(Vec2::new(10.0, -20.0)) < 1e-9 && j.jog.xy().dist(Vec2::new(5.0, 20.0)) < 1e-9);
|
||||
assert!(dm.p15.xy().dist(Vec2::new(0.0, 50.0)) < 1e-9);
|
||||
// Not a circle: refused.
|
||||
s.execute("line", &json!({ "points": [[100, 0], [110, 0]] })).unwrap();
|
||||
let l = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
assert!(s.execute("dimjogged", &json!({ "handle": l, "centerOverride": [0, 0], "at": [1, 1] })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jogged_linear() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
s.execute("dimjogline", &json!({ "handle": h, "at": [2, 3] })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert!(dm.marks.jog_line.is_some());
|
||||
// The line is cut around the jog and a zigzag drawn there (1.5 × the text height).
|
||||
assert!(!covers(&g, LineRole::Dim, 3.0, 2.0));
|
||||
let zig = g.lines_of(LineRole::Dim).find(|l| l.len() == 4).unwrap();
|
||||
let h_jog = zig.iter().map(|p| p.y).fold(f64::MIN, f64::max) - zig.iter().map(|p| p.y).fold(f64::MAX, f64::min);
|
||||
assert!((h_jog - 1.5 * 0.18).abs() < 1e-9, "{h_jog}");
|
||||
// Default place: halfway between the first extension line and the text.
|
||||
s.execute("dimjogline", &json!({ "handle": h })).unwrap();
|
||||
assert!(dim_of(&s, &h).0.marks.jog_line.unwrap().xy().dist(Vec2::new(2.5, 3.0)) < 1e-9);
|
||||
s.execute("dimjogline", &json!({ "handle": h, "remove": true })).unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.jog_line, None);
|
||||
// Prompts: pick the dimension, then the jog location; Remove picks the jog to remove.
|
||||
s.script("dimjogline 1,3\n8,3\n").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert!(dim_of(&s, &h).0.marks.jog_line.unwrap().xy().dist(Vec2::new(8.0, 3.0)) < 1e-9);
|
||||
s.script("dimjogline r\n1,3\n").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(dim_of(&s, &h).0.marks.jog_line, None);
|
||||
// Radial dimensions take no jog.
|
||||
s.execute("circle", &json!({ "center": [30, 0], "radius": 5 })).unwrap();
|
||||
let c = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
let r = handle(&s.execute("dimradius", &json!({ "handle": c })).unwrap());
|
||||
assert!(s.execute("dimjogline", &json!({ "handle": r })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breaks() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
// A line crossing the dimension line and the second extension line.
|
||||
s.execute("line", &json!({ "points": [[4, -1], [4, 6]] })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[8, 1], [12, 1]] })).unwrap();
|
||||
s.execute("dimbreak", &json!({ "handles": [h] })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert_eq!(dm.marks.breaks.len(), 2, "{:?}", dm.marks.breaks);
|
||||
assert!(dm.marks.breaks.iter().all(|b| b.auto && (b.size - 0.125).abs() < 1e-12));
|
||||
assert!(!covers(&g, LineRole::Dim, 3.0, 4.0) && covers(&g, LineRole::Dim, 3.0, 3.8));
|
||||
let ext_at = |g: &DimGeometry, y: f64| {
|
||||
g.lines_of(LineRole::Ext).any(|l| l.windows(2).any(|w| (w[0].x - 10.0).abs() < 1e-9 && w[0].y.min(w[1].y) < y && y < w[0].y.max(w[1].y)))
|
||||
};
|
||||
assert!(!ext_at(&g, 1.0) && ext_at(&g, 2.0), "extension line 2 broken at y = 1");
|
||||
// Auto again doesn't pile up; only the chosen objects; manual; remove.
|
||||
let first = s.doc().unwrap().model.iter().nth(1).unwrap().handle.hex();
|
||||
s.execute("dimbreak", &json!({ "handles": [h], "objects": [first], "size": 0.5 })).unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 1);
|
||||
s.execute("dimbreak", &json!({ "handles": [h], "mode": "manual", "first": [6, 3], "second": [7, 3] })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert_eq!(dm.marks.breaks.len(), 2);
|
||||
assert!(!covers(&g, LineRole::Dim, 3.0, 6.5) && covers(&g, LineRole::Dim, 3.0, 7.2));
|
||||
s.execute("dimbreak", &json!({ "handles": [h], "mode": "remove" })).unwrap();
|
||||
assert!(dim_of(&s, &h).0.marks.breaks.is_empty());
|
||||
// Prompts: the dimension, then objects (Enter ends) or Auto/Manual/Remove.
|
||||
s.script("dimbreak 1,3\n\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 2);
|
||||
s.script("dimbreak 1,3 r\n").unwrap();
|
||||
assert!(dim_of(&s, &h).0.marks.breaks.is_empty());
|
||||
s.script("dimbreak 1,3 m 1,3 2,3\n").unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.breaks.len(), 1);
|
||||
// Bad input.
|
||||
assert!(s.execute("dimbreak", &json!({ "handles": [h], "size": -1 })).is_err());
|
||||
assert!(s.execute("dimbreak", &json!({ "handles": [h], "mode": "manual", "first": [1, 1], "second": [1, 1] })).is_err());
|
||||
assert!(s.execute("dimbreak", &json!({ "handles": [h], "mode": "nope" })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspection_dimensions() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
s.execute("diminspect", &json!({ "handles": [h], "label": "A", "rate": "50%" })).unwrap();
|
||||
let (dm, g) = dim_of(&s, &h);
|
||||
assert_eq!(dm.marks.inspect.as_ref().unwrap().shape, 0);
|
||||
assert_eq!(g.value, "A 10.0000 50%");
|
||||
// The frame: two long sides, two separators, two round ends.
|
||||
let plain = {
|
||||
let mut p = dm.clone();
|
||||
p.marks.inspect = None;
|
||||
cadcraft_engine::render::dimension_in(s.doc().unwrap(), &p)
|
||||
};
|
||||
assert_eq!(g.lines_of(LineRole::Dim).count(), plain.lines_of(LineRole::Dim).count() + 6);
|
||||
s.execute("diminspect", &json!({ "handles": [h], "shape": "none", "label": "", "rate": "" })).unwrap();
|
||||
let (_, g) = dim_of(&s, &h);
|
||||
assert_eq!(g.value, "10.0000");
|
||||
assert_eq!(g.lines_of(LineRole::Dim).count(), plain.lines_of(LineRole::Dim).count());
|
||||
s.execute("diminspect", &json!({ "handles": [h], "remove": true })).unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.inspect, None);
|
||||
assert!(s.execute("diminspect", &json!({ "handles": [h], "shape": "oval" })).is_err());
|
||||
// Prompts: dimensions, shape, label, rate.
|
||||
s.script("diminspect 1,3\n\na\nQC\n25%\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let ins = dim_of(&s, &h).0.marks.inspect.unwrap();
|
||||
assert_eq!((ins.shape, ins.label.as_str(), ins.rate.as_str()), (1, "QC", "25%"));
|
||||
s.script("diminspect 1,3\n\nremove\n").unwrap();
|
||||
assert_eq!(dim_of(&s, &h).0.marks.inspect, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_follow_move_and_rotate_and_hostile_values_do_not_panic() {
|
||||
let mut s = Session::new();
|
||||
let h = linear(&mut s);
|
||||
s.execute("dimjogline", &json!({ "handle": h, "at": [2, 3] })).unwrap();
|
||||
s.execute("dimedit", &json!({ "handles": [h], "mode": "oblique", "angle": 60 })).unwrap();
|
||||
s.execute("move", &json!({ "handles": [h], "from": [0, 0], "to": [10, 5] })).unwrap();
|
||||
let (dm, _) = dim_of(&s, &h);
|
||||
assert!(dm.marks.jog_line.unwrap().xy().dist(Vec2::new(12.0, 8.0)) < 1e-9);
|
||||
s.execute("rotate", &json!({ "handles": [h], "base": [10, 5], "angle": 90 })).unwrap();
|
||||
let (dm, _) = dim_of(&s, &h);
|
||||
assert!((dm.marks.oblique.unwrap() - 150f64.to_radians()).abs() < 1e-9);
|
||||
// Damaged marks still draw.
|
||||
let d = s.doc_mut().unwrap();
|
||||
let hh = Handle::parse_hex(&h).unwrap();
|
||||
d.modify_entity(hh, |e| {
|
||||
if let EntityKind::Dimension(dm) = &mut e.kind {
|
||||
dm.marks.oblique = Some(f64::NAN);
|
||||
dm.marks.jog_line = Some(cadcraft_engine::geom::Vec3::new(f64::INFINITY, 0.0, 0.0));
|
||||
dm.marks.breaks.push(cadcraft_engine::doc::DimBreak { at: Default::default(), size: f64::NAN, auto: false });
|
||||
dm.marks.inspect = Some(cadcraft_engine::doc::Inspection { shape: 9, label: "x".into(), rate: String::new() });
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let _ = dim_of(&s, &h);
|
||||
}
|
||||
@@ -453,6 +453,78 @@ pub(crate) fn arclen_xdata(center: cadcraft_geom::Vec3) -> Vec<Tag> {
|
||||
vec![Tag::s(1000, "ARCLEN"), Tag::s(1002, "{"), Tag::f(1010, center.x), Tag::f(1020, center.y), Tag::f(1030, center.z), Tag::s(1002, "}")]
|
||||
}
|
||||
|
||||
/// Dimension marks as CADCraft xdata (`1000 MARKS`, `1002 {`, items, `1002 }`): `1000 JOGLINE`
|
||||
/// and a point; `1000 JOGGED`, the centre and the jog point; `1000 BREAK`, a point, `1040` size
|
||||
/// and `1070` auto; `1000 INSPECT`, `1070` shape, `1000` label and `1000` rate. Points are
|
||||
/// `1010/1020/1030`. The oblique angle is the standard group 52 instead.
|
||||
pub(crate) fn marks_xdata(m: &cadcraft_doc::DimMarks) -> Vec<Tag> {
|
||||
let pt = |v: &mut Vec<Tag>, p: cadcraft_geom::Vec3| v.extend([Tag::f(1010, p.x), Tag::f(1020, p.y), Tag::f(1030, p.z)]);
|
||||
let mut v = Vec::new();
|
||||
if let Some(j) = m.jog_line {
|
||||
v.push(Tag::s(1000, "JOGLINE"));
|
||||
pt(&mut v, j);
|
||||
}
|
||||
if let Some(j) = &m.jogged {
|
||||
v.push(Tag::s(1000, "JOGGED"));
|
||||
pt(&mut v, j.center);
|
||||
pt(&mut v, j.jog);
|
||||
}
|
||||
for b in m.breaks.iter().take(MAX_XDATA_ITEMS / 8) {
|
||||
v.push(Tag::s(1000, "BREAK"));
|
||||
pt(&mut v, b.at);
|
||||
v.extend([Tag::f(1040, b.size), Tag::i(1070, i64::from(b.auto))]);
|
||||
}
|
||||
if let Some(i) = &m.inspect {
|
||||
v.extend([Tag::s(1000, "INSPECT"), Tag::i(1070, i64::from(i.shape)), Tag::s(1000, i.label.clone()), Tag::s(1000, i.rate.clone())]);
|
||||
}
|
||||
if v.is_empty() {
|
||||
return v;
|
||||
}
|
||||
let mut out = vec![Tag::s(1000, "MARKS"), Tag::s(1002, "{")];
|
||||
out.extend(v);
|
||||
out.push(Tag::s(1002, "}"));
|
||||
out
|
||||
}
|
||||
|
||||
/// The marks written by [`marks_xdata`] (no oblique angle); empty for other writers. Damaged
|
||||
/// items are skipped.
|
||||
pub(crate) fn read_marks(tags: &[Tag]) -> cadcraft_doc::DimMarks {
|
||||
let list = xdata_list(xdata(tags, APP), "MARKS");
|
||||
let list = list.get(..list.len().min(MAX_XDATA_ITEMS)).unwrap_or(&[]);
|
||||
let mut m = cadcraft_doc::DimMarks::default();
|
||||
let num = |i: usize, code: i32| list.get(i).filter(|t| t.code == code).map(Tag::f64).filter(|x| x.is_finite());
|
||||
let point = |i: usize| Some(cadcraft_geom::Vec3::new(num(i, 1010)?, num(i + 1, 1020)?, num(i + 2, 1030)?));
|
||||
let text = |i: usize| list.get(i).filter(|t| t.code == 1000).map(Tag::str);
|
||||
for (i, t) in list.iter().enumerate() {
|
||||
if t.code != 1000 {
|
||||
continue;
|
||||
}
|
||||
match t.str().as_str() {
|
||||
"JOGLINE" => m.jog_line = point(i + 1).or(m.jog_line),
|
||||
"JOGGED" => {
|
||||
if let (Some(center), Some(jog)) = (point(i + 1), point(i + 4)) {
|
||||
m.jogged = Some(cadcraft_doc::JoggedRadius { center, jog });
|
||||
}
|
||||
}
|
||||
"BREAK" => {
|
||||
let size = list.get(i + 4).filter(|t| t.code == 1040).map(Tag::f64).filter(|x| x.is_finite() && *x > 0.0);
|
||||
let auto = list.get(i + 5).filter(|t| t.code == 1070).is_some_and(|t| t.i64() != 0);
|
||||
if let (Some(at), Some(size)) = (point(i + 1), size) {
|
||||
m.breaks.push(cadcraft_doc::DimBreak { at, size, auto });
|
||||
}
|
||||
}
|
||||
"INSPECT" => {
|
||||
let shape = list.get(i + 1).filter(|t| t.code == 1070).and_then(|t| u8::try_from(t.i64()).ok()).filter(|s| *s <= 2);
|
||||
if let (Some(shape), Some(label), Some(rate)) = (shape, text(i + 2), text(i + 3)) {
|
||||
m.inspect = Some(cadcraft_doc::Inspection { shape, label, rate });
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The arc centre written by [`arclen_xdata`]; None for other dimensions and other writers.
|
||||
pub(crate) fn read_arclen(tags: &[Tag]) -> Option<cadcraft_geom::Vec3> {
|
||||
let list = xdata_list(xdata(tags, APP), "ARCLEN");
|
||||
|
||||
@@ -393,6 +393,7 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
block: t.s(2).filter(|s| !s.is_empty()),
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
})
|
||||
}
|
||||
"LEADER" => EntityKind::Leader(Leader {
|
||||
@@ -928,6 +929,16 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m
|
||||
dm.kind = DimKind::ArcLength;
|
||||
dm.p15 = center;
|
||||
}
|
||||
dm.marks = crate::dxf_ext::read_marks(tags);
|
||||
// Group 52: the extension lines' angle relative to the dimension line (DIMEDIT Oblique).
|
||||
let base = match dm.kind {
|
||||
DimKind::Linear { rotation } => Some(rotation),
|
||||
DimKind::Aligned => Some((dm.p14.xy() - dm.p13.xy()).angle()),
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(base), Some(o)) = (base, T(tags).f(52).filter(|o| o.is_finite() && o.rem_euclid(180.0) != 0.0)) {
|
||||
dm.marks.oblique = Some(cadcraft_geom::norm_angle(base + o.to_radians()));
|
||||
}
|
||||
}
|
||||
("ACAD_TABLE", EntityKind::Table(_)) => {
|
||||
let t = T(tags);
|
||||
|
||||
@@ -190,12 +190,21 @@ fn dim_xdata(w: &mut W, dm: &Dimension, cx: &Ctx) {
|
||||
if !dm.assoc.is_empty() {
|
||||
ours.extend(dxf_ext::assoc_xdata(&dm.assoc));
|
||||
}
|
||||
ours.extend(dxf_ext::marks_xdata(&dm.marks));
|
||||
if !ours.is_empty() {
|
||||
w.s(1001, dxf_ext::APP);
|
||||
w.xdata(ours);
|
||||
}
|
||||
}
|
||||
|
||||
/// Group 52 of a linear or aligned dimension with oblique extension lines: their angle less the
|
||||
/// dimension line's (`base`), in degrees.
|
||||
fn oblique_52(w: &mut W, dm: &Dimension, base: f64) {
|
||||
if let Some(a) = dm.marks.oblique.filter(|a| a.is_finite()) {
|
||||
w.f(52, cadcraft_geom::norm_angle(a - base).to_degrees());
|
||||
}
|
||||
}
|
||||
|
||||
/// Header variables we write, with their group codes (DXF Reference, HEADER section).
|
||||
/// `P2`/`P3` = 2D/3D point.
|
||||
const HEADER_VARS: &[(&str, i32)] = &[
|
||||
@@ -695,12 +704,16 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
||||
w.p(13, dm.p13);
|
||||
w.p(14, dm.p14);
|
||||
w.f(50, rotation.to_degrees());
|
||||
oblique_52(w, dm, rotation);
|
||||
w.s(100, "AcDbRotatedDimension");
|
||||
}
|
||||
DimKind::Aligned | DimKind::ArcLength => {
|
||||
w.s(100, "AcDbAlignedDimension");
|
||||
w.p(13, dm.p13);
|
||||
w.p(14, dm.p14);
|
||||
if matches!(dm.kind, DimKind::Aligned) {
|
||||
oblique_52(w, dm, (dm.p14.xy() - dm.p13.xy()).angle());
|
||||
}
|
||||
}
|
||||
DimKind::Radius => {
|
||||
w.s(100, "AcDbRadialDimension");
|
||||
|
||||
@@ -93,6 +93,7 @@ fn sample() -> Drawing {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -576,6 +577,7 @@ fn dim(kind: DimKind, p13: Vec3, p14: Vec3) -> Dimension {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ fn linear(a: [f64; 2], b: [f64; 2], at: [f64; 2]) -> Dimension {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ fn height_written(style_scale: f64, overrides: serde_json::Value) -> f64 {
|
||||
block: None,
|
||||
overrides: overrides.as_object().cloned().unwrap_or_default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
|
||||
block_mtext_height(&cadcraft_io::write_dxf(&d))
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Dimension edits survive DXF: the oblique angle as group 52 (relative to the dimension line,
|
||||
//! DXF Reference), jogs, breaks and inspection frames as CADCraft xdata. Damaged xdata is
|
||||
//! skipped.
|
||||
|
||||
use cadcraft_doc::{Common, DimBreak, DimKind, DimMarks, Dimension, Drawing, EntityKind, Inspection, JoggedRadius, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{read_dxf, write_dxf};
|
||||
|
||||
fn dim(kind: DimKind, marks: DimMarks) -> EntityKind {
|
||||
EntityKind::Dimension(Dimension {
|
||||
kind,
|
||||
defpt: Vec3::new(0.0, 3.0, 0.0),
|
||||
text_mid: Vec3::ZERO,
|
||||
p13: Vec3::ZERO,
|
||||
p14: Vec3::new(10.0, 0.0, 0.0),
|
||||
p15: Vec3::new(3.0, 4.0, 0.0),
|
||||
p16: Vec3::ZERO,
|
||||
text: String::new(),
|
||||
style: "Standard".into(),
|
||||
measurement: 0.0,
|
||||
text_rotation: 0.0,
|
||||
user_text_pos: false,
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks,
|
||||
})
|
||||
}
|
||||
|
||||
fn marks_of(d: &Drawing) -> Vec<DimMarks> {
|
||||
d.model
|
||||
.iter()
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::Dimension(dm) => Some(dm.marks.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_marks_roundtrip() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let linear = DimMarks {
|
||||
oblique: Some(60f64.to_radians()),
|
||||
jog_line: Some(Vec3::new(2.0, 3.0, 0.0)),
|
||||
breaks: vec![
|
||||
DimBreak { at: Vec3::new(4.0, 3.0, 0.0), size: 0.125, auto: true },
|
||||
DimBreak { at: Vec3::new(7.0, 3.0, 0.0), size: 0.5, auto: false },
|
||||
],
|
||||
inspect: Some(Inspection { shape: 1, label: "QC".into(), rate: "25%".into() }),
|
||||
..Default::default()
|
||||
};
|
||||
let aligned = DimMarks { oblique: Some(100f64.to_radians()), ..Default::default() };
|
||||
let jogged = DimMarks { jogged: Some(JoggedRadius { center: Vec3::new(1.0, -2.0, 0.0), jog: Vec3::new(2.0, 1.0, 0.0) }), ..Default::default() };
|
||||
for (k, m) in [(DimKind::Linear { rotation: 0.0 }, &linear), (DimKind::Aligned, &aligned), (DimKind::Radius, &jogged)] {
|
||||
d.add(&Space::Model, Common::default(), dim(k, m.clone())).unwrap();
|
||||
}
|
||||
let text = write_dxf(&d);
|
||||
// The oblique angle is the standard group 52, not xdata.
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||||
assert!(lines.windows(2).any(|w| w[0] == "52" && (w[1].parse::<f64>().unwrap() - 60.0).abs() < 1e-9));
|
||||
let back = read_dxf(text.as_bytes()).unwrap();
|
||||
let got = marks_of(&back);
|
||||
assert_eq!(got.len(), 3);
|
||||
let close = |a: Option<f64>, b: Option<f64>| a.zip(b).is_some_and(|(a, b)| (a - b).abs() < 1e-9);
|
||||
assert!(close(got[0].oblique, linear.oblique) && close(got[1].oblique, aligned.oblique));
|
||||
assert_eq!(DimMarks { oblique: None, ..got[0].clone() }, DimMarks { oblique: None, ..linear.clone() });
|
||||
assert_eq!(got[2], jogged);
|
||||
// And again: nothing piles up.
|
||||
assert_eq!(marks_of(&read_dxf(write_dxf(&back).as_bytes()).unwrap()).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_marks_are_skipped() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let m = DimMarks {
|
||||
jog_line: Some(Vec3::new(2.0, 3.0, 0.0)),
|
||||
breaks: vec![DimBreak { at: Vec3::new(4.0, 3.0, 0.0), size: 0.125, auto: false }],
|
||||
inspect: Some(Inspection { shape: 0, label: "A".into(), rate: "B".into() }),
|
||||
..Default::default()
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), dim(DimKind::Linear { rotation: 0.0 }, m)).unwrap();
|
||||
let text = write_dxf(&d);
|
||||
// A break size of NaN, an inspection shape out of range, a jog point cut short.
|
||||
let bad = text.replacen("1040\r\n0.125\r\n", "1040\r\nnan\r\n", 1).replacen("INSPECT\r\n1070\r\n0\r\n", "INSPECT\r\n1070\r\n77\r\n", 1).replacen(
|
||||
"JOGLINE\r\n1010\r\n",
|
||||
"JOGLINE\r\n1000\r\n",
|
||||
1,
|
||||
);
|
||||
assert_ne!(bad, text);
|
||||
let back = read_dxf(bad.as_bytes()).unwrap();
|
||||
let got = marks_of(&back);
|
||||
assert_eq!(got.first(), Some(&DimMarks::default()), "{got:?}");
|
||||
}
|
||||
@@ -59,6 +59,7 @@ fn dimension(overrides: serde_json::Value) -> EntityKind {
|
||||
block: None,
|
||||
overrides: overrides.as_object().cloned().unwrap_or_default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+163
-4
@@ -12,6 +12,9 @@ use cadcraft_geom::{Arc, Vec2, ccw_sweep};
|
||||
|
||||
use crate::units::{NumFormat, format_angle, format_decimal, format_linear};
|
||||
|
||||
/// Most DIMBREAK gaps drawn per dimension.
|
||||
const MAX_BREAKS: usize = 1000;
|
||||
|
||||
/// What a dimension line primitive is (for DIMCLRD / DIMCLRE colours).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LineRole {
|
||||
@@ -58,6 +61,26 @@ impl DimGeometry {
|
||||
self.line_roles.push(LineRole::Ext);
|
||||
}
|
||||
}
|
||||
/// Leave out the parts of the lines (of `role`, or all) within `r` of `c`.
|
||||
fn cut(&mut self, c: Vec2, r: f64, role: Option<LineRole>) {
|
||||
if !(r > 0.0 && r.is_finite() && c.is_finite()) {
|
||||
return;
|
||||
}
|
||||
let mut lines = Vec::with_capacity(self.lines.len());
|
||||
let mut roles = Vec::with_capacity(self.lines.len());
|
||||
let mut ext2 = Vec::new();
|
||||
for (i, (l, rl)) in self.lines.iter().zip(&self.line_roles).enumerate() {
|
||||
let pieces = if role.is_none_or(|x| x == *rl) { cut_polyline(l, c, r) } else { vec![l.clone()] };
|
||||
for p in pieces {
|
||||
if self.ext2_lines.contains(&i) {
|
||||
ext2.push(lines.len());
|
||||
}
|
||||
lines.push(p);
|
||||
roles.push(*rl);
|
||||
}
|
||||
}
|
||||
(self.lines, self.line_roles, self.ext2_lines) = (lines, roles, ext2);
|
||||
}
|
||||
/// Extension line 1 or 2 (`second`).
|
||||
fn ext_n(&mut self, l: Option<Vec<Vec2>>, second: bool) {
|
||||
if let Some(l) = l.filter(|l| l.len() >= 2) {
|
||||
@@ -263,6 +286,44 @@ pub fn arrowhead(kind: Arrowhead, tip: Vec2, dir: Vec2, size: f64) -> ArrowGeom
|
||||
g
|
||||
}
|
||||
|
||||
/// The pieces of a polyline outside the circle (`c`, `r`).
|
||||
fn cut_polyline(l: &[Vec2], c: Vec2, r: f64) -> Vec<Vec<Vec2>> {
|
||||
let mut out: Vec<Vec<Vec2>> = Vec::new();
|
||||
let mut keep = |a: Vec2, b: Vec2| {
|
||||
if a.dist(b) <= 1e-12 {
|
||||
return;
|
||||
}
|
||||
match out.last_mut() {
|
||||
Some(last) if last.last() == Some(&a) => last.push(b),
|
||||
_ => out.push(vec![a, b]),
|
||||
}
|
||||
};
|
||||
for w in l.windows(2) {
|
||||
let (p, q) = (w[0], w[1]);
|
||||
let dv = q - p;
|
||||
let (qa, f) = (dv.dot(dv), p - c);
|
||||
let (qb, qc) = (2.0 * f.dot(dv), f.dot(f) - r * r);
|
||||
let disc = qb * qb - 4.0 * qa * qc;
|
||||
if qa < 1e-24 || disc.is_nan() || disc <= 0.0 {
|
||||
keep(p, q);
|
||||
continue;
|
||||
}
|
||||
let sq = disc.sqrt();
|
||||
let (t0, t1) = ((-qb - sq) / (2.0 * qa), (-qb + sq) / (2.0 * qa));
|
||||
if t1 <= 0.0 || t0 >= 1.0 {
|
||||
keep(p, q);
|
||||
continue;
|
||||
}
|
||||
if t0 > 0.0 {
|
||||
keep(p, p + dv * t0);
|
||||
}
|
||||
if t1 < 1.0 {
|
||||
keep(p + dv * t1, q);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A closed filled arrowhead at `tip` pointing along `dir` (unit), as one triangle.
|
||||
pub fn arrow(tip: Vec2, dir: Vec2, size: f64) -> Vec<Vec2> {
|
||||
let back = tip - dir * size;
|
||||
@@ -331,7 +392,15 @@ fn linear_text(meas: f64, d: &Dimension, st: &DimStyle, prefix: &str) -> String
|
||||
let alt = format_linear(v * st.alt_factor, &af).1;
|
||||
s += &format!(" [{}]", post(&st.alt_post, &alt));
|
||||
}
|
||||
apply_override(&s, d)
|
||||
inspected(apply_override(&s, d), d)
|
||||
}
|
||||
|
||||
/// DIMINSPECT: the label and the rate either side of the value, two spaces apart.
|
||||
fn inspected(value: String, d: &Dimension) -> String {
|
||||
match &d.marks.inspect {
|
||||
Some(ins) => [ins.label.as_str(), value.as_str(), ins.rate.as_str()].iter().filter(|p| !p.is_empty()).copied().collect::<Vec<_>>().join(" "),
|
||||
None => value,
|
||||
}
|
||||
}
|
||||
|
||||
/// `\H` code for stacked tolerance parts (DIMTFAC relative to the 70% stack scale).
|
||||
@@ -445,11 +514,19 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font
|
||||
_ => (p2 - p1).normalized(),
|
||||
};
|
||||
let dir = if dir == Vec2::ZERO || !dir.is_finite() { Vec2::X } else { dir };
|
||||
let foot = |p: Vec2| on + dir * (p - on).dot(dir);
|
||||
// DIMEDIT Oblique: extension lines at their own angle (not along the dimension line).
|
||||
let oblique = d.marks.oblique.map(Vec2::from_angle).filter(|e| e.is_finite() && e.cross(dir).abs() > 1e-6);
|
||||
let foot = |p: Vec2| match oblique {
|
||||
Some(e) => p + e * ((on - p).cross(dir) / e.cross(dir)),
|
||||
None => on + dir * (p - on).dot(dir),
|
||||
};
|
||||
let f1 = foot(p1);
|
||||
let f2 = foot(p2);
|
||||
let meas = f1.dist(f2);
|
||||
// The measurement is along the dimension direction whatever the extension lines do.
|
||||
let meas = (p2 - p1).dot(dir).abs();
|
||||
g.mtext = linear_text(meas, d, st, "");
|
||||
// From here on: the length drawn between the extension lines.
|
||||
let meas = f1.dist(f2);
|
||||
if !st.suppress_ext1 {
|
||||
g.ext_n(ext_line(p1, f1, st, k), false);
|
||||
}
|
||||
@@ -607,6 +684,47 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font
|
||||
g.dim(vec![base, text_mid + side * (tw / 2.0 + gap)]);
|
||||
}
|
||||
}
|
||||
// DIMJOGLINE: a zigzag in the dimension line, 1.5 text heights tall.
|
||||
if let Some(j) = d.marks.jog_line {
|
||||
let at = f1 + u * (j.xy() - f1).dot(u);
|
||||
let h = 1.5 * th;
|
||||
if at.is_finite() && h > 0.0 {
|
||||
let r = h / 2.0;
|
||||
g.cut(at, r, Some(LineRole::Dim));
|
||||
g.dim(vec![at - u * r, at - u * (r / 4.0) + n * (h / 2.0), at + u * (r / 4.0) - n * (h / 2.0), at + u * r]);
|
||||
}
|
||||
}
|
||||
}
|
||||
DimKind::Radius if d.marks.jogged.is_some() => {
|
||||
// DIMJOGGED: from the arrowhead on the arc toward the centre, a jog at DIMJOGANG,
|
||||
// then on to the overriding centre (parallel to the first part).
|
||||
let center = d.defpt.xy();
|
||||
let tip = d.p15.xy();
|
||||
let r = center.dist(tip);
|
||||
let j = d.marks.jogged.clone().unwrap_or_default();
|
||||
let (o, jp) = (j.center.xy(), j.jog.xy());
|
||||
g.mtext = linear_text(r, d, st, "R");
|
||||
let (tw, _) = measure(&g.mtext);
|
||||
let u = (tip - center).normalized();
|
||||
let u = if u == Vec2::ZERO || !u.is_finite() { Vec2::X } else { u };
|
||||
let n = u.perp();
|
||||
let h = (o - center).dot(n);
|
||||
let ang = if st.jog_angle.is_finite() { st.jog_angle.clamp(5.0, 90.0) } else { 45.0 }.to_radians();
|
||||
let run = h.abs() / ang.tan().max(1e-6);
|
||||
let tj = (jp - center).dot(u).clamp(0.0, r);
|
||||
let (j1, j2) = (center + u * (tj + run / 2.0), center + u * (tj - run / 2.0) + n * h);
|
||||
g.dim(vec![tip, j1, j2, o]);
|
||||
end(&mut g, blk2, tip, u);
|
||||
let tpos = if d.user_text_pos { text_mid } else { tip + u * (asz.max(tsz) * 3.0) };
|
||||
if tpos.dist(center) > r {
|
||||
g.dim(vec![tip, tpos]);
|
||||
}
|
||||
text_angle = if st.text_outside_horizontal { 0.0 } else { readable(u.angle()) };
|
||||
if !d.user_text_pos {
|
||||
let x = Vec2::from_angle(text_angle);
|
||||
let x = if x.dot(u) >= 0.0 { x } else { -x };
|
||||
text_mid = tpos + x * (tw / 2.0 + gap);
|
||||
}
|
||||
}
|
||||
DimKind::Radius | DimKind::Diameter => {
|
||||
let a = d.defpt.xy();
|
||||
@@ -698,7 +816,7 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font
|
||||
let azin = (st.angular_zero_suppression & 3) * 4;
|
||||
let txt = format_angle(sweep, st.angular_unit, st.angular_decimals, azin);
|
||||
// DIMPOST is a linear-dimension prefix/suffix; angles have their own format only.
|
||||
g.mtext = apply_override(&txt, d);
|
||||
g.mtext = inspected(apply_override(&txt, d), d);
|
||||
let mut pts = Vec::new();
|
||||
arc.tessellate(r * 1e-3, &mut pts);
|
||||
g.dim(pts);
|
||||
@@ -786,6 +904,46 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font
|
||||
if d.text_rotation != 0.0 && d.text_rotation.is_finite() {
|
||||
text_angle = d.text_rotation;
|
||||
}
|
||||
// DIMBREAK: gaps in the dimension and extension lines.
|
||||
for b in d.marks.breaks.iter().take(MAX_BREAKS) {
|
||||
g.cut(b.at.xy(), b.size / 2.0, None);
|
||||
}
|
||||
// DIMINSPECT: a frame round the text, with separators between label, value and rate.
|
||||
if let Some(ins) = &d.marks.inspect
|
||||
&& ins.shape < 2
|
||||
&& !g.mtext.is_empty()
|
||||
{
|
||||
let (w, h) = measure(&g.mtext);
|
||||
let (x, y) = (Vec2::from_angle(text_angle), Vec2::from_angle(text_angle).perp());
|
||||
let at = |px: f64, py: f64| text_mid + x * px + y * py;
|
||||
let (hw, hh) = (w / 2.0 + gap, h / 2.0 + gap);
|
||||
// Widths of the label, value and rate as laid out: the two spaces between parts measured
|
||||
// in "x x", the value is what the rest leaves.
|
||||
let gap2 = (measure("x x").0 - 2.0 * measure("x").0).max(0.0);
|
||||
let (wl, wr) = (measure(&ins.label).0, measure(&ins.rate).0);
|
||||
let seps = usize::from(!ins.label.is_empty()) + usize::from(!ins.rate.is_empty());
|
||||
let wv = (w - wl - wr - gap2 * seps as f64).max(0.0);
|
||||
let mut left = -w / 2.0;
|
||||
if !ins.label.is_empty() {
|
||||
left += wl;
|
||||
g.dim(vec![at(left + gap2 / 2.0, -hh), at(left + gap2 / 2.0, hh)]);
|
||||
left += gap2;
|
||||
}
|
||||
if !ins.rate.is_empty() {
|
||||
left += wv;
|
||||
g.dim(vec![at(left + gap2 / 2.0, -hh), at(left + gap2 / 2.0, hh)]);
|
||||
}
|
||||
g.dim(vec![at(-hw, hh), at(hw, hh)]);
|
||||
g.dim(vec![at(-hw, -hh), at(hw, -hh)]);
|
||||
for side in [-1.0, 1.0] {
|
||||
let ends: Vec<Vec2> = if ins.shape == 0 {
|
||||
(0..=12).map(|i| f64::from(i) * std::f64::consts::PI / 12.0).map(|a| at(side * (hw + hh * a.sin()), hh * a.cos())).collect()
|
||||
} else {
|
||||
vec![at(side * hw, hh), at(side * (hw + hh), 0.0), at(side * hw, -hh)]
|
||||
};
|
||||
g.dim(ends);
|
||||
}
|
||||
}
|
||||
g.value = decode_value(&cadcraft_fonts::plain_mtext(&g.mtext));
|
||||
if !g.mtext.is_empty() {
|
||||
let l = cadcraft_fonts::layout_mtext_with(&g.mtext, ¶ms(text_mid, text_angle));
|
||||
@@ -826,6 +984,7 @@ mod tests {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -317,6 +317,7 @@ fn lin_dim() -> cadcraft_doc::Dimension {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ fn dim(kind: DimKind) -> Dimension {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ fn text(vars: serde_json::Value) -> (String, String) {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
};
|
||||
let mut st = DimStyle::default();
|
||||
let rejected = st.apply_fields(vars.as_object().unwrap());
|
||||
|
||||
@@ -22,6 +22,7 @@ fn linear(len: f64) -> Dimension {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ fn dimensions_take_the_viewport_layer_colour() {
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: Vec::new(),
|
||||
marks: Default::default(),
|
||||
};
|
||||
d.add(&Space::Model, on("A"), EntityKind::Dimension(dim)).unwrap();
|
||||
d.add(&Space::Model, on("A"), EntityKind::Line(Line { a: Vec3::new(0.0, -1.0, 0.0), b: Vec3::new(10.0, -1.0, 0.0) })).unwrap();
|
||||
|
||||
@@ -183,6 +183,8 @@
|
||||
Deselect All Скасувати вибір
|
||||
Diameter Діаметр
|
||||
Dim style Розмірний стиль
|
||||
Dimension Break Розрив розміру
|
||||
Dimension Edit Редагувати розмір
|
||||
Dimension Розміри
|
||||
Dimension Objects Only Лише розміри
|
||||
Dimension Space Відстань між розмірами
|
||||
@@ -372,6 +374,7 @@
|
||||
Insertion Z Вставлення Z
|
||||
Inspect Constraints Переглянути обмеження
|
||||
Inspect Drawing Переглянути креслення
|
||||
Inspection Контрольний розмір
|
||||
Interface Language Мова інтерфейсу
|
||||
Interface Theme Тема інтерфейсу
|
||||
Intersect Перетин
|
||||
@@ -380,6 +383,8 @@
|
||||
Isolate Layer Ізолювати шар
|
||||
Isolate Objects Ізолювати об'єкти
|
||||
Isometric Drafting Ізометричне креслення
|
||||
Jogged Зі зламом
|
||||
Jogged Linear Лінійний зі зламом
|
||||
Join Об'єднати
|
||||
Join the ArtCraft Discord… Приєднатися до Discord ArtCraft…
|
||||
Join us on Discord Приєднуйтеся до нас у Discord
|
||||
@@ -512,6 +517,7 @@
|
||||
Object Snap Tracking (F11) Відстеження об'єктної прив'язки (F11)
|
||||
Object Snap Tracking Settings Параметри відстеження об'єктної прив'язки
|
||||
Object type: Тип об'єкта:
|
||||
Oblique Похилий
|
||||
Off Вимкнено
|
||||
Offset Зміщення
|
||||
On Увімкнено
|
||||
|
||||
|
Reference in new issue
Block a user