mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1101dd7d0 |
@@ -963,57 +963,6 @@ impl AreaM {
|
||||
AreaMode::Subtract => self.total - v,
|
||||
}
|
||||
}
|
||||
/// Input while a height is pending: typed, or given by two points (their distance).
|
||||
fn height_input(&mut self, s: &mut Session, i: Input) -> Step {
|
||||
match (i, self.height_from) {
|
||||
(Input::Point(p), None) => self.height_from = Some(p),
|
||||
(Input::Point(p), Some(from)) => {
|
||||
self.height_from = None;
|
||||
return self.height(s, p.dist(from));
|
||||
}
|
||||
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
|
||||
Some(h) => return self.height(s, h),
|
||||
None => s.echo("Requires a distance."),
|
||||
},
|
||||
(Input::Enter, None) => s.echo("Requires a height."),
|
||||
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
|
||||
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
|
||||
_ => {}
|
||||
}
|
||||
Step::Continue
|
||||
}
|
||||
|
||||
/// Input while the objects to measure are picked.
|
||||
fn object_input(&mut self, s: &mut Session, i: Input) -> Step {
|
||||
match i {
|
||||
Input::Pick(hs) => {
|
||||
for h in hs {
|
||||
match object_area(s, h) {
|
||||
Ok((a, per)) => {
|
||||
if self.measured(s, a, per) == Step::Done {
|
||||
return Step::Done;
|
||||
}
|
||||
// A single volume takes the first object with an area.
|
||||
if self.volume && self.mode == AreaMode::Single {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => s.echo("The selected object has no area."),
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::Enter => {
|
||||
self.object = false;
|
||||
if self.mode == AreaMode::Single {
|
||||
return Step::Done;
|
||||
}
|
||||
}
|
||||
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
|
||||
_ => {}
|
||||
}
|
||||
Step::Continue
|
||||
}
|
||||
|
||||
/// Measure the oldest pending area's volume with height `h`.
|
||||
fn height(&mut self, s: &mut Session, h: f64) -> Step {
|
||||
match self.pending.first().map(|a| a * h.abs()) {
|
||||
@@ -1093,10 +1042,52 @@ impl Interactive for AreaM {
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if !self.pending.is_empty() {
|
||||
return Ok(self.height_input(s, i));
|
||||
// A height is typed, or given by two points (their distance).
|
||||
match (i, self.height_from) {
|
||||
(Input::Point(p), None) => self.height_from = Some(p),
|
||||
(Input::Point(p), Some(from)) => {
|
||||
self.height_from = None;
|
||||
return Ok(self.height(s, p.dist(from)));
|
||||
}
|
||||
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
|
||||
Some(h) => return Ok(self.height(s, h)),
|
||||
None => s.echo("Requires a distance."),
|
||||
},
|
||||
(Input::Enter, None) => s.echo("Requires a height."),
|
||||
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
|
||||
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.object {
|
||||
return Ok(self.object_input(s, i));
|
||||
match i {
|
||||
Input::Pick(hs) => {
|
||||
for h in hs {
|
||||
match object_area(s, h) {
|
||||
Ok((a, per)) => {
|
||||
if self.measured(s, a, per) == Step::Done {
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
// A single volume takes the first object with an area.
|
||||
if self.volume && self.mode == AreaMode::Single {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => s.echo("The selected object has no area."),
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::Enter => {
|
||||
self.object = false;
|
||||
if self.mode == AreaMode::Single {
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
}
|
||||
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.length {
|
||||
if let Some(p) = length_input(&self.pts, i)? {
|
||||
|
||||
+130
-127
@@ -1296,128 +1296,6 @@ fn run_chamfer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// ---------------- explode / array / order / break / join ----------------
|
||||
|
||||
fn explode_block_ref(d: &cadcraft_doc::Drawing, e: &Entity, ins: &cadcraft_doc::Insert) -> Option<Vec<Entity>> {
|
||||
if let Some(items) = super::array::explode_array(d, e) {
|
||||
return Some(items);
|
||||
}
|
||||
let blk = d.block(&ins.block)?;
|
||||
let m = ins.transform(blk.base.xy());
|
||||
Some(
|
||||
blk.entities
|
||||
.iter()
|
||||
.map(|be| {
|
||||
let mut ne = (**be).clone();
|
||||
ne.kind.transform(&m);
|
||||
if ne.common.layer == "0" {
|
||||
ne.common.layer = e.common.layer.clone();
|
||||
}
|
||||
if ne.common.color == cadcraft_color::Color::ByBlock {
|
||||
ne.common.color = e.common.color;
|
||||
}
|
||||
ne
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn explode_mtext(e: &Entity, m: &cadcraft_doc::MText) -> Vec<Entity> {
|
||||
cadcraft_fonts::plain_mtext(&m.contents)
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
|
||||
Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Text(cadcraft_doc::Text {
|
||||
insert: v3(at),
|
||||
align_pt: None,
|
||||
height: m.height,
|
||||
value: l.into(),
|
||||
rotation: m.rotation,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: m.style.clone(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The entities of a boundary's lines and arcs, on the object's layer and style.
|
||||
fn segment_entities(e: &Entity, segs: impl IntoIterator<Item = Segment>) -> Vec<Entity> {
|
||||
segs.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn explode_dimension(d: &cadcraft_doc::Drawing, e: &Entity, dm: &cadcraft_doc::Dimension) -> Vec<Entity> {
|
||||
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
|
||||
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
let line_entity = |l: &Vec<Vec2>| {
|
||||
if l.len() == 2 {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
|
||||
} else {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
|
||||
}
|
||||
};
|
||||
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
|
||||
let text_marks = &g.text_marks;
|
||||
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
|
||||
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
|
||||
for t in &g.fills {
|
||||
if let [a, b, c] = t.as_slice() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
|
||||
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
|
||||
if style.text_fill == 2
|
||||
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
|
||||
{
|
||||
let fill = style.text_fill_color;
|
||||
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: cadcraft_doc::Common { color, ..e.common.clone() },
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
|
||||
});
|
||||
}
|
||||
v.extend(marks(true).filter_map(&line_entity));
|
||||
if !g.value.is_empty() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::MText(cadcraft_doc::MText {
|
||||
insert: v3(g.text_pos),
|
||||
height: g.text_height,
|
||||
width: 0.0,
|
||||
attach: 5,
|
||||
rotation: g.text_angle,
|
||||
style: style.text_style.clone(),
|
||||
contents: g.mtext_contents(),
|
||||
line_spacing: 1.0,
|
||||
line_spacing_exact: false,
|
||||
columns: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<Entity>> {
|
||||
// An annotative object explodes into plain objects as it draws at the current annotation scale.
|
||||
if cadcraft_doc::is_annotative(e) {
|
||||
@@ -1431,9 +1309,42 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
|
||||
match &e.kind {
|
||||
EntityKind::LwPolyline(p) => {
|
||||
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
|
||||
Some(segment_entities(e, pl.segments()))
|
||||
Some(
|
||||
pl.segments()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
EntityKind::Insert(ins) => {
|
||||
if let Some(items) = super::array::explode_array(d, e) {
|
||||
return Some(items);
|
||||
}
|
||||
let blk = d.block(&ins.block)?;
|
||||
let m = ins.transform(blk.base.xy());
|
||||
Some(
|
||||
blk.entities
|
||||
.iter()
|
||||
.map(|be| {
|
||||
let mut ne = (**be).clone();
|
||||
ne.kind.transform(&m);
|
||||
if ne.common.layer == "0" {
|
||||
ne.common.layer = e.common.layer.clone();
|
||||
}
|
||||
if ne.common.color == cadcraft_color::Color::ByBlock {
|
||||
ne.common.color = e.common.color;
|
||||
}
|
||||
ne
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
EntityKind::Insert(ins) => explode_block_ref(d, e, ins),
|
||||
// A helix explodes into its spline (seen from above).
|
||||
EntityKind::Helix(h) => Some(vec![Entity { handle: Handle(0), common: e.common.clone(), kind: EntityKind::Spline(h.spline()) }]),
|
||||
EntityKind::Polyline3d(p) => Some(
|
||||
@@ -1446,10 +1357,102 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
|
||||
.collect(),
|
||||
),
|
||||
EntityKind::MLine(m) => Some(super::draw2::mline::explode(e, m)),
|
||||
EntityKind::MText(m) => Some(explode_mtext(e, m)),
|
||||
EntityKind::MText(m) => Some(
|
||||
cadcraft_fonts::plain_mtext(&m.contents)
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
|
||||
Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Text(cadcraft_doc::Text {
|
||||
insert: v3(at),
|
||||
align_pt: None,
|
||||
height: m.height,
|
||||
value: l.into(),
|
||||
rotation: m.rotation,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: m.style.clone(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// A region explodes into its boundary's lines and arcs.
|
||||
EntityKind::Region(r) => Some(segment_entities(e, r.segments())),
|
||||
EntityKind::Dimension(dm) => Some(explode_dimension(d, e, dm)),
|
||||
EntityKind::Region(r) => Some(
|
||||
r.segments()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let k = match s {
|
||||
Segment::Line(l) => line(l.a, l.b),
|
||||
Segment::Arc { arc: a, .. } => arc(&a),
|
||||
};
|
||||
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
EntityKind::Dimension(dm) => {
|
||||
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
|
||||
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
let line_entity = |l: &Vec<Vec2>| {
|
||||
if l.len() == 2 {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
|
||||
} else {
|
||||
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
|
||||
}
|
||||
};
|
||||
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
|
||||
let text_marks = &g.text_marks;
|
||||
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
|
||||
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
|
||||
for t in &g.fills {
|
||||
if let [a, b, c] = t.as_slice() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
|
||||
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
|
||||
if style.text_fill == 2
|
||||
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
|
||||
{
|
||||
let fill = style.text_fill_color;
|
||||
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: cadcraft_doc::Common { color, ..e.common.clone() },
|
||||
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
|
||||
});
|
||||
}
|
||||
v.extend(marks(true).filter_map(&line_entity));
|
||||
if !g.value.is_empty() {
|
||||
v.push(Entity {
|
||||
handle: Handle(0),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::MText(cadcraft_doc::MText {
|
||||
insert: v3(g.text_pos),
|
||||
height: g.text_height,
|
||||
width: 0.0,
|
||||
attach: 5,
|
||||
rotation: g.text_angle,
|
||||
style: style.text_style.clone(),
|
||||
contents: g.mtext_contents(),
|
||||
line_spacing: 1.0,
|
||||
line_spacing_exact: false,
|
||||
columns: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
EntityKind::Tolerance(t) => Some(super::tolerance::explode(d, e, t)),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
+396
-326
@@ -249,6 +249,130 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
}
|
||||
|
||||
/// Parse one entity while charging materialized table cells to the caller's shared import budget.
|
||||
/// An ARC, with its angles run the other way in a mirrored OCS.
|
||||
fn arc(c: &Common, t: &T) -> Arc {
|
||||
let mut a = Arc { center: ocs(c, t.p(10)), radius: t.fd(40, 1.0).abs(), start: t.fd(50, 0.0).to_radians(), end: t.fd(51, 360.0).to_radians() };
|
||||
if c.extrusion.z < 0.0 {
|
||||
// Mirrored OCS: angles run the other way.
|
||||
let (s, e) = (std::f64::consts::PI - a.end, std::f64::consts::PI - a.start);
|
||||
a.start = cadcraft_geom::norm_angle(s);
|
||||
a.end = cadcraft_geom::norm_angle(e);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn ellipse(c: &Common, t: &T) -> Ellipse {
|
||||
let mut e = Ellipse {
|
||||
center: t.p(10),
|
||||
major: t.p(11),
|
||||
ratio: t.fd(40, 1.0).clamp(1e-9, 1.0),
|
||||
start: t.fd(41, 0.0),
|
||||
end: t.fd(42, std::f64::consts::TAU),
|
||||
};
|
||||
if c.extrusion.z < 0.0 && (cadcraft_geom::ccw_sweep(e.start, e.end) - std::f64::consts::TAU).abs() > 1e-9 {
|
||||
// Centre and major axis are WCS, but the minor axis is extrusion × major: with (0,0,-1)
|
||||
// it points the other way, so parameter t lands where -t does in the XY plane. A full
|
||||
// ellipse is symmetric and stays as it is.
|
||||
let (s, e2) = (-e.end, -e.start);
|
||||
e.start = cadcraft_geom::norm_angle(s);
|
||||
e.end = cadcraft_geom::norm_angle(e2);
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// A LWPOLYLINE, or the region CADCraft wrote as one.
|
||||
fn lwpolyline(tags: &[Tag], c: &Common, t: &T) -> Option<EntityKind> {
|
||||
let mut verts = Vec::new();
|
||||
let mut cur: Option<PolyVertex> = None;
|
||||
for tg in tags {
|
||||
match tg.code {
|
||||
10 => {
|
||||
if let Some(v) = cur.take() {
|
||||
verts.push(v);
|
||||
}
|
||||
cur = Some(PolyVertex { p: Vec2::new(tg.f64(), 0.0), ..Default::default() });
|
||||
}
|
||||
20 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.p.y = tg.f64();
|
||||
}
|
||||
}
|
||||
40 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.start_width = tg.f64();
|
||||
}
|
||||
}
|
||||
41 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.end_width = tg.f64();
|
||||
}
|
||||
}
|
||||
42 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.bulge = tg.f64();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
verts.extend(cur);
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
if c.extrusion.z < 0.0 {
|
||||
for v in &mut verts {
|
||||
v.p.x = -v.p.x;
|
||||
v.bulge = -v.bulge;
|
||||
}
|
||||
}
|
||||
let pl = LwPolyline { vertices: verts, closed: flags & 1 != 0, const_width: t.fd(43, 0.0), elevation: t.fd(38, 0.0), plinegen: flags & 128 != 0 };
|
||||
// A region CADCraft wrote as polylines (`dxf_region`).
|
||||
crate::dxf_region::from_polyline(tags, pl)
|
||||
}
|
||||
|
||||
fn spline(t: &T) -> Spline {
|
||||
let degree = t.i(71).unwrap_or(3).clamp(1, 10) as usize;
|
||||
let knots = t.all_f(40);
|
||||
let control = t.pts(10);
|
||||
let weights = t.all_f(41);
|
||||
let fit = t.pts(11);
|
||||
let tangent = |c: i32| t.f(c).map(|_| t.p(c).xy());
|
||||
let fit_opts =
|
||||
cadcraft_geom::FitOptions { knots: Default::default(), start_tangent: tangent(12), end_tangent: tangent(13), tolerance: t.fd(44, 0.0) }
|
||||
.sanitized();
|
||||
let closed = t.i(70).unwrap_or(0) & 1 != 0;
|
||||
let mut sp = Spline { degree, knots, control, weights, fit: fit.clone(), closed, fit_opts };
|
||||
if !sp.is_valid() && fit.len() >= 2 {
|
||||
sp = Spline::fit_with(&fit, sp.closed, fit_opts);
|
||||
} else if !sp.is_valid() && sp.control.len() >= 2 {
|
||||
sp = Spline::from_control(sp.control.clone(), degree);
|
||||
} else if fit.len() >= 3 {
|
||||
// DXF keeps the knots, not how they were spaced: recover it for later refits.
|
||||
sp.fit_opts.knots = sp.infer_knot_param();
|
||||
}
|
||||
sp
|
||||
}
|
||||
|
||||
/// An ATTDEF, whose multiline text is embedded MTEXT after its own groups.
|
||||
fn attdef(tags: &[Tag], c: &Common) -> Attrib {
|
||||
// A multiline definition has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(tags);
|
||||
let multiline = multiline_attrib(embedded, c);
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: own.i(70).unwrap_or(0) & 2 != 0,
|
||||
prompt: own.s(3).unwrap_or_default(),
|
||||
props: Default::default(),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usize) -> Option<(Common, EntityKind)> {
|
||||
// The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit.
|
||||
if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) {
|
||||
@@ -260,138 +384,16 @@ fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usi
|
||||
"LINE" => EntityKind::Line(Line { a: t.p(10), b: t.p(11) }),
|
||||
"POINT" => EntityKind::Point(Point { p: t.p(10), angle: t.fd(50, 0.0).to_radians() }),
|
||||
"CIRCLE" => EntityKind::Circle(Circle { center: ocs(&c, t.p(10)), radius: t.fd(40, 1.0).abs() }),
|
||||
"ARC" => {
|
||||
let mut a =
|
||||
Arc { center: ocs(&c, t.p(10)), radius: t.fd(40, 1.0).abs(), start: t.fd(50, 0.0).to_radians(), end: t.fd(51, 360.0).to_radians() };
|
||||
if c.extrusion.z < 0.0 {
|
||||
// Mirrored OCS: angles run the other way.
|
||||
let (s, e) = (std::f64::consts::PI - a.end, std::f64::consts::PI - a.start);
|
||||
a.start = cadcraft_geom::norm_angle(s);
|
||||
a.end = cadcraft_geom::norm_angle(e);
|
||||
}
|
||||
EntityKind::Arc(a)
|
||||
}
|
||||
"ELLIPSE" => {
|
||||
let mut e = Ellipse {
|
||||
center: t.p(10),
|
||||
major: t.p(11),
|
||||
ratio: t.fd(40, 1.0).clamp(1e-9, 1.0),
|
||||
start: t.fd(41, 0.0),
|
||||
end: t.fd(42, std::f64::consts::TAU),
|
||||
};
|
||||
if c.extrusion.z < 0.0 && (cadcraft_geom::ccw_sweep(e.start, e.end) - std::f64::consts::TAU).abs() > 1e-9 {
|
||||
// Centre and major axis are WCS, but the minor axis is extrusion × major: with (0,0,-1)
|
||||
// it points the other way, so parameter t lands where -t does in the XY plane. A full
|
||||
// ellipse is symmetric and stays as it is.
|
||||
let (s, e2) = (-e.end, -e.start);
|
||||
e.start = cadcraft_geom::norm_angle(s);
|
||||
e.end = cadcraft_geom::norm_angle(e2);
|
||||
}
|
||||
EntityKind::Ellipse(e)
|
||||
}
|
||||
"LWPOLYLINE" => {
|
||||
let mut verts = Vec::new();
|
||||
let mut cur: Option<PolyVertex> = None;
|
||||
for tg in tags {
|
||||
match tg.code {
|
||||
10 => {
|
||||
if let Some(v) = cur.take() {
|
||||
verts.push(v);
|
||||
}
|
||||
cur = Some(PolyVertex { p: Vec2::new(tg.f64(), 0.0), ..Default::default() });
|
||||
}
|
||||
20 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.p.y = tg.f64();
|
||||
}
|
||||
}
|
||||
40 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.start_width = tg.f64();
|
||||
}
|
||||
}
|
||||
41 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.end_width = tg.f64();
|
||||
}
|
||||
}
|
||||
42 => {
|
||||
if let Some(v) = cur.as_mut() {
|
||||
v.bulge = tg.f64();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
verts.extend(cur);
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
if c.extrusion.z < 0.0 {
|
||||
for v in &mut verts {
|
||||
v.p.x = -v.p.x;
|
||||
v.bulge = -v.bulge;
|
||||
}
|
||||
}
|
||||
let pl = LwPolyline {
|
||||
vertices: verts,
|
||||
closed: flags & 1 != 0,
|
||||
const_width: t.fd(43, 0.0),
|
||||
elevation: t.fd(38, 0.0),
|
||||
plinegen: flags & 128 != 0,
|
||||
};
|
||||
// A region CADCraft wrote as polylines (`dxf_region`).
|
||||
crate::dxf_region::from_polyline(tags, pl)?
|
||||
}
|
||||
"SPLINE" => {
|
||||
let degree = t.i(71).unwrap_or(3).clamp(1, 10) as usize;
|
||||
let knots = t.all_f(40);
|
||||
let control = t.pts(10);
|
||||
let weights = t.all_f(41);
|
||||
let fit = t.pts(11);
|
||||
let tangent = |c: i32| t.f(c).map(|_| t.p(c).xy());
|
||||
let fit_opts = cadcraft_geom::FitOptions {
|
||||
knots: Default::default(),
|
||||
start_tangent: tangent(12),
|
||||
end_tangent: tangent(13),
|
||||
tolerance: t.fd(44, 0.0),
|
||||
}
|
||||
.sanitized();
|
||||
let closed = t.i(70).unwrap_or(0) & 1 != 0;
|
||||
let mut sp = Spline { degree, knots, control, weights, fit: fit.clone(), closed, fit_opts };
|
||||
if !sp.is_valid() && fit.len() >= 2 {
|
||||
sp = Spline::fit_with(&fit, sp.closed, fit_opts);
|
||||
} else if !sp.is_valid() && sp.control.len() >= 2 {
|
||||
sp = Spline::from_control(sp.control.clone(), degree);
|
||||
} else if fit.len() >= 3 {
|
||||
// DXF keeps the knots, not how they were spaced: recover it for later refits.
|
||||
sp.fit_opts.knots = sp.infer_knot_param();
|
||||
}
|
||||
EntityKind::Spline(sp)
|
||||
}
|
||||
"ARC" => EntityKind::Arc(arc(&c, &t)),
|
||||
"ELLIPSE" => EntityKind::Ellipse(ellipse(&c, &t)),
|
||||
"LWPOLYLINE" => lwpolyline(tags, &c, &t)?,
|
||||
"SPLINE" => EntityKind::Spline(spline(&t)),
|
||||
"HELIX" => EntityKind::Helix(helix(tags)),
|
||||
"RAY" => EntityKind::Ray(RayLine { base: t.p(10), dir: t.p(11) }),
|
||||
"XLINE" => EntityKind::XLine(RayLine { base: t.p(10), dir: t.p(11) }),
|
||||
"TEXT" => EntityKind::Text(text_from(&t)),
|
||||
"MTEXT" => EntityKind::MText(mtext_from(tags, &c)),
|
||||
"ATTDEF" => {
|
||||
// A multiline definition has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(tags);
|
||||
let multiline = multiline_attrib(embedded, &c);
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
EntityKind::AttDef(Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: own.i(70).unwrap_or(0) & 2 != 0,
|
||||
prompt: own.s(3).unwrap_or_default(),
|
||||
props: Default::default(),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
})
|
||||
}
|
||||
"ATTDEF" => EntityKind::AttDef(attdef(tags, &c)),
|
||||
"INSERT" => {
|
||||
// A mirrored block reference comes with extrusion (0,0,-1): its insertion point, scale,
|
||||
// rotation and column spacing are in that OCS, where x runs the other way. In the WCS that
|
||||
@@ -1109,6 +1111,91 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m
|
||||
|
||||
/// Parse a run of entity records (ENTITIES section or a block's body) into entities. Records that
|
||||
/// can't be placed are counted in `skipped`.
|
||||
/// The entity of a POLYLINE, with its mesh faces (advancing `i` past its VERTEX records). `None`
|
||||
/// for a mesh without faces.
|
||||
fn polyline_entity(recs: &[(String, Vec<Tag>)], i: &mut usize, t: &T) -> Option<(Common, EntityKind, Vec<EntityKind>)> {
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
let mut pts: Vec<(Vec3, f64, f64, f64)> = Vec::new();
|
||||
// Polyface face records (VERTEX 70 bit 128 without 64): vertex indices 71–74.
|
||||
let mut faces: Vec<[i64; 4]> = Vec::new();
|
||||
while let Some((k2, t2)) = recs.get(*i) {
|
||||
if k2 == "VERTEX" {
|
||||
let tt = T(t2);
|
||||
let vflags = tt.i(70).unwrap_or(0);
|
||||
if vflags & 128 != 0 && vflags & 64 == 0 {
|
||||
faces.push([71, 72, 73, 74].map(|c| tt.i(c).unwrap_or(0)));
|
||||
} else if vflags & 16 == 0 {
|
||||
pts.push((tt.p(10), tt.fd(42, 0.0), tt.fd(40, 0.0), tt.fd(41, 0.0)));
|
||||
}
|
||||
*i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
*i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let c = common(t);
|
||||
let mut extra: Vec<EntityKind> = Vec::new();
|
||||
let k = if flags & (16 | 64) != 0 {
|
||||
// Polyface (64) and polygon (16) meshes become 3D faces; the first takes the
|
||||
// polyline's place.
|
||||
let verts: Vec<Vec3> = pts.iter().map(|p| p.0).collect();
|
||||
let mut fs = if flags & 64 != 0 { polyface_faces(&verts, &faces) } else { mesh_faces(&verts, t, flags) }.into_iter().map(EntityKind::Face3d);
|
||||
let first = fs.next()?;
|
||||
extra.extend(fs);
|
||||
first
|
||||
} else if flags & 8 != 0 {
|
||||
EntityKind::Polyline3d(Polyline3d { points: pts.iter().map(|p| p.0).collect(), closed: flags & 1 != 0 })
|
||||
} else {
|
||||
// A 2D polyline's vertices are in its OCS: mirrored like LWPOLYLINE.
|
||||
let m = if c.extrusion.z < 0.0 { -1.0 } else { 1.0 };
|
||||
EntityKind::LwPolyline(LwPolyline {
|
||||
vertices: pts.iter().map(|p| PolyVertex { p: Vec2::new(m * p.0.x, p.0.y), bulge: m * p.1, start_width: p.2, end_width: p.3 }).collect(),
|
||||
closed: flags & 1 != 0,
|
||||
const_width: t.fd(40, 0.0).min(t.fd(41, 0.0)).max(0.0),
|
||||
elevation: t.p(10).z,
|
||||
plinegen: flags & 128 != 0,
|
||||
})
|
||||
};
|
||||
Some((c, k, extra))
|
||||
}
|
||||
|
||||
/// The ATTRIB records after an INSERT, advancing `i` past them and their SEQEND.
|
||||
fn insert_attribs(recs: &[(String, Vec<Tag>)], i: &mut usize, ins_layer: &str) -> Vec<Attrib> {
|
||||
let mut attribs = Vec::new();
|
||||
while let Some((k2, t2)) = recs.get(*i) {
|
||||
if k2 == "ATTRIB" {
|
||||
let tt = T(t2);
|
||||
// A multiline attribute has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(t2);
|
||||
let multiline = multiline_attrib(embedded, &common(&tt));
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
attribs.push(Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: false,
|
||||
prompt: String::new(),
|
||||
props: attrib_props(&tt, ins_layer),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
});
|
||||
*i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
*i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
attribs
|
||||
}
|
||||
|
||||
fn parse_entities(
|
||||
recs: &[(String, Vec<Tag>)],
|
||||
d: &mut Drawing,
|
||||
@@ -1134,92 +1221,17 @@ fn parse_entities(
|
||||
i += 1;
|
||||
match kind.as_str() {
|
||||
"POLYLINE" => {
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
let mut pts: Vec<(Vec3, f64, f64, f64)> = Vec::new();
|
||||
// Polyface face records (VERTEX 70 bit 128 without 64): vertex indices 71–74.
|
||||
let mut faces: Vec<[i64; 4]> = Vec::new();
|
||||
while let Some((k2, t2)) = recs.get(i) {
|
||||
if k2 == "VERTEX" {
|
||||
let tt = T(t2);
|
||||
let vflags = tt.i(70).unwrap_or(0);
|
||||
if vflags & 128 != 0 && vflags & 64 == 0 {
|
||||
faces.push([71, 72, 73, 74].map(|c| tt.i(c).unwrap_or(0)));
|
||||
} else if vflags & 16 == 0 {
|
||||
pts.push((tt.p(10), tt.fd(42, 0.0), tt.fd(40, 0.0), tt.fd(41, 0.0)));
|
||||
}
|
||||
i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let c = common(&t);
|
||||
let k = if flags & (16 | 64) != 0 {
|
||||
// Polyface (64) and polygon (16) meshes become 3D faces; the first takes the
|
||||
// polyline's place.
|
||||
let verts: Vec<Vec3> = pts.iter().map(|p| p.0).collect();
|
||||
let mut fs = if flags & 64 != 0 { polyface_faces(&verts, &faces) } else { mesh_faces(&verts, &t, flags) }
|
||||
.into_iter()
|
||||
.map(EntityKind::Face3d);
|
||||
let Some(first) = fs.next() else {
|
||||
*skipped = skipped.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
extra.extend(fs);
|
||||
first
|
||||
} else if flags & 8 != 0 {
|
||||
EntityKind::Polyline3d(Polyline3d { points: pts.iter().map(|p| p.0).collect(), closed: flags & 1 != 0 })
|
||||
} else {
|
||||
// A 2D polyline's vertices are in its OCS: mirrored like LWPOLYLINE.
|
||||
let m = if c.extrusion.z < 0.0 { -1.0 } else { 1.0 };
|
||||
EntityKind::LwPolyline(LwPolyline {
|
||||
vertices: pts
|
||||
.iter()
|
||||
.map(|p| PolyVertex { p: Vec2::new(m * p.0.x, p.0.y), bulge: m * p.1, start_width: p.2, end_width: p.3 })
|
||||
.collect(),
|
||||
closed: flags & 1 != 0,
|
||||
const_width: t.fd(40, 0.0).min(t.fd(41, 0.0)).max(0.0),
|
||||
elevation: t.p(10).z,
|
||||
plinegen: flags & 128 != 0,
|
||||
})
|
||||
let Some((c, k, more)) = polyline_entity(recs, &mut i, &t) else {
|
||||
*skipped = skipped.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
extra = more;
|
||||
kindent = Some((c, k));
|
||||
}
|
||||
"INSERT" => {
|
||||
if t.i(66) == Some(1) {
|
||||
let mut attribs = Vec::new();
|
||||
let ins_layer = kindent.as_ref().map(|(c, _)| c.layer.clone()).unwrap_or_default();
|
||||
while let Some((k2, t2)) = recs.get(i) {
|
||||
if k2 == "ATTRIB" {
|
||||
let tt = T(t2);
|
||||
// A multiline attribute has its text as embedded MTEXT after its own groups.
|
||||
let (own, embedded) = split_embedded(t2);
|
||||
let multiline = multiline_attrib(embedded, &common(&tt));
|
||||
let own = T(own);
|
||||
let mut text = text_from(&own);
|
||||
if let Some(m) = &multiline {
|
||||
text.value.clone_from(&m.read_as);
|
||||
}
|
||||
attribs.push(Attrib {
|
||||
tag: own.s(2).unwrap_or_default(),
|
||||
text,
|
||||
invisible: own.i(70).unwrap_or(0) & 1 != 0,
|
||||
constant: false,
|
||||
prompt: String::new(),
|
||||
props: attrib_props(&tt, &ins_layer),
|
||||
multiline,
|
||||
modes: attrib_modes(own.0),
|
||||
});
|
||||
i += 1;
|
||||
} else {
|
||||
if k2 == "SEQEND" {
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let attribs = insert_attribs(recs, &mut i, &ins_layer);
|
||||
if let Some((_, EntityKind::Insert(ins))) = kindent.as_mut() {
|
||||
ins.attribs = attribs;
|
||||
}
|
||||
@@ -1688,6 +1700,13 @@ pub fn read(bytes: &[u8]) -> Result<Drawing> {
|
||||
read_budgeted(bytes, 4_000_000)
|
||||
}
|
||||
|
||||
/// A block definition read from BLOCKS: name, base point, description, entities (owner, paper
|
||||
/// flag, entity) and block flags.
|
||||
type BlockDef = (String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64);
|
||||
|
||||
/// A layout read from OBJECTS: name, tab order, paper block handle and page setup.
|
||||
type LayoutDef = (String, u32, String, PageSetup);
|
||||
|
||||
/// Import DXF sections while sharing one materialized-table-cell budget across blocks and entity sections.
|
||||
fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?;
|
||||
@@ -1700,8 +1719,8 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
// A file without $INSUNITS declares no units (#1209); only new drawings take the template's.
|
||||
d.header.set_i64("INSUNITS", 0);
|
||||
let mut rx = Rx { pstyles: plot_style_names(&secs), ..Rx::default() };
|
||||
let mut blocks: Vec<(String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64)> = Vec::new();
|
||||
let mut layout_objs: Vec<(String, u32, String, PageSetup)> = Vec::new();
|
||||
let mut blocks: Vec<BlockDef> = Vec::new();
|
||||
let mut layout_objs: Vec<LayoutDef> = Vec::new();
|
||||
let mut entities = Vec::new();
|
||||
let mut objs = Objects::default();
|
||||
// Every handle another program's file already uses is taken before anything gets a new one:
|
||||
@@ -1721,73 +1740,109 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
match s.name.as_str() {
|
||||
"HEADER" => header(&s.tags, &mut d),
|
||||
"TABLES" => tables(&s.tags, &mut d, &mut rx),
|
||||
"BLOCKS" => {
|
||||
let recs = records(&s.tags);
|
||||
for (k, tg) in &recs {
|
||||
let t = T(tg);
|
||||
if k == "BLOCK" && t.i(70).unwrap_or(0) & 4 != 0 {
|
||||
rx.xref_paths.insert(t.s(2).unwrap_or_default().to_ascii_uppercase(), t.s(1).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while let Some((k, tg)) = recs.get(i) {
|
||||
if k == "BLOCK" {
|
||||
let t = T(tg);
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
let base = t.p(10);
|
||||
let description = t.s(4).unwrap_or_default();
|
||||
// Block flags: 4 xref, 8 overlay.
|
||||
let xref = t.i(70).unwrap_or(0) & 12;
|
||||
let mut j = i + 1;
|
||||
while recs.get(j).is_some_and(|(k, _)| k != "ENDBLK") {
|
||||
j += 1;
|
||||
}
|
||||
let body = recs.get(i + 1..j).unwrap_or(&[]);
|
||||
let ents = parse_entities(body, &mut d, &mut rx, &mut n, &mut table_cells_left);
|
||||
blocks.push((name, base, description, ents, xref));
|
||||
i = j + 1;
|
||||
} else {
|
||||
// A record outside any BLOCK ... ENDBLK pair.
|
||||
if k != "ENDBLK" {
|
||||
n = n.saturating_add(1);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
count_skipped(&mut skipped, "BLOCKS", n);
|
||||
}
|
||||
"BLOCKS" => blocks.extend(read_blocks(&s.tags, &mut d, &mut rx, &mut table_cells_left, &mut skipped)),
|
||||
// A damaged file may split its entities over several ENTITIES sections (#865).
|
||||
"ENTITIES" => {
|
||||
let mut n = 0;
|
||||
entities.extend(parse_entities(&records(&s.tags), &mut d, &mut rx, &mut n, &mut table_cells_left));
|
||||
count_skipped(&mut skipped, "ENTITIES", n);
|
||||
}
|
||||
"OBJECTS" => {
|
||||
for (k, tg) in records(&s.tags) {
|
||||
objs.add(&k, &tg);
|
||||
if k == "LAYOUT" {
|
||||
// Fields after the AcDbLayout marker (AcDbPlotSettings also has a code 1).
|
||||
let start = tg.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
|
||||
let tail = tg.get(start..).unwrap_or(&[]);
|
||||
let t = T(tail);
|
||||
// LAYOUT has two 330s: owner dict and the block record; take the last.
|
||||
let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
|
||||
let plot = T(tg.get(..start).unwrap_or(&[]));
|
||||
layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
|
||||
}
|
||||
}
|
||||
}
|
||||
"OBJECTS" => read_objects(&s.tags, &mut objs, &mut layout_objs),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// A file CADCraft wrote names its own next handle (#495): its table records and objects
|
||||
// are numbered above it and rebuilt on save, so following $HANDSEED would drift every save.
|
||||
let own_seed = objs.xrecord_text(crate::dxf_ext::HANDSEED_KEY).and_then(|t| Handle::parse_hex(t.trim()));
|
||||
// Layouts.
|
||||
let (paper_block_for, first_layout) = setup_layouts(&mut d, &mut layout_objs, &rx);
|
||||
place_blocks(&mut d, blocks, &mut rx, &paper_block_for, &first_layout);
|
||||
place_entities(&mut d, entities, &rx, &paper_block_for, &first_layout);
|
||||
apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);
|
||||
objs.apply(&mut d, &rx);
|
||||
crate::dxf_image::resolve(&secs, &mut d);
|
||||
crate::dxf_mline::resolve(&secs, &mut d);
|
||||
// Keep handles unique against the header's seed.
|
||||
let seed = d.header.str("HANDSEED", "");
|
||||
if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) {
|
||||
d.bump_handseed(Handle(h.0.saturating_sub(1)));
|
||||
}
|
||||
if d.layer("0").is_none() {
|
||||
d.layers.insert(0, Layer::default());
|
||||
}
|
||||
crate::dxf_raw::read(&secs, &mut d);
|
||||
d.skipped_records = skipped;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Read the BLOCKS section: the xref paths of external blocks, then each block's entities.
|
||||
fn read_blocks(
|
||||
tags: &[Tag],
|
||||
d: &mut Drawing,
|
||||
rx: &mut Rx,
|
||||
table_cells_left: &mut usize,
|
||||
skipped: &mut std::collections::BTreeMap<String, usize>,
|
||||
) -> Vec<BlockDef> {
|
||||
let recs = records(tags);
|
||||
for (k, tg) in &recs {
|
||||
let t = T(tg);
|
||||
if k == "BLOCK" && t.i(70).unwrap_or(0) & 4 != 0 {
|
||||
rx.xref_paths.insert(t.s(2).unwrap_or_default().to_ascii_uppercase(), t.s(1).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
let mut blocks = Vec::new();
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while let Some((k, tg)) = recs.get(i) {
|
||||
if k == "BLOCK" {
|
||||
let t = T(tg);
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
let base = t.p(10);
|
||||
let description = t.s(4).unwrap_or_default();
|
||||
// Block flags: 4 xref, 8 overlay.
|
||||
let xref = t.i(70).unwrap_or(0) & 12;
|
||||
let mut j = i + 1;
|
||||
while recs.get(j).is_some_and(|(k, _)| k != "ENDBLK") {
|
||||
j += 1;
|
||||
}
|
||||
let body = recs.get(i + 1..j).unwrap_or(&[]);
|
||||
let ents = parse_entities(body, d, rx, &mut n, table_cells_left);
|
||||
blocks.push((name, base, description, ents, xref));
|
||||
i = j + 1;
|
||||
} else {
|
||||
// A record outside any BLOCK ... ENDBLK pair.
|
||||
if k != "ENDBLK" {
|
||||
n = n.saturating_add(1);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
count_skipped(skipped, "BLOCKS", n);
|
||||
blocks
|
||||
}
|
||||
|
||||
/// Read the OBJECTS section into `objs`, and each LAYOUT into `layout_objs`.
|
||||
fn read_objects(tags: &[Tag], objs: &mut Objects, layout_objs: &mut Vec<LayoutDef>) {
|
||||
for (k, tg) in records(tags) {
|
||||
objs.add(&k, &tg);
|
||||
if k == "LAYOUT" {
|
||||
// Fields after the AcDbLayout marker (AcDbPlotSettings also has a code 1).
|
||||
let start = tg.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
|
||||
let tail = tg.get(start..).unwrap_or(&[]);
|
||||
let t = T(tail);
|
||||
// LAYOUT has two 330s: owner dict and the block record; take the last.
|
||||
let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
|
||||
let plot = T(tg.get(..start).unwrap_or(&[]));
|
||||
layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the drawing's layouts, and return the layout each paper-space block belongs to, and the
|
||||
/// layout that takes paper-space entities without an owner.
|
||||
fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (HashMap<String, String>, String) {
|
||||
let mut paper_block_for: HashMap<String, String> = HashMap::new();
|
||||
layout_objs.sort_by_key(|l| l.1);
|
||||
for (name, order, br, page) in &layout_objs {
|
||||
for (name, order, br, page) in layout_objs.iter() {
|
||||
if name.eq_ignore_ascii_case("Model") {
|
||||
continue;
|
||||
}
|
||||
@@ -1806,7 +1861,12 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
let first_tab = d.layouts.iter().min_by_key(|l| l.tab_order).map(|l| l.name.clone());
|
||||
d.active_layout = active.clone().filter(|a| Some(a) != first_tab.as_ref());
|
||||
let first_layout = active.or_else(|| d.layouts.first().map(|l| l.name.clone())).unwrap_or_else(|| "Layout1".into());
|
||||
// Blocks: model/paper space blocks hold entities in newer files.
|
||||
(paper_block_for, first_layout)
|
||||
}
|
||||
|
||||
/// Place the BLOCKS entities: model and paper space blocks go to their space, the rest become
|
||||
/// block definitions.
|
||||
fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block_for: &HashMap<String, String>, first_layout: &str) {
|
||||
for (name, base, description, ents, xref) in blocks {
|
||||
let up = name.to_ascii_uppercase();
|
||||
if up == "*MODEL_SPACE" {
|
||||
@@ -1817,7 +1877,7 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
continue;
|
||||
}
|
||||
if up.starts_with("*PAPER_SPACE") {
|
||||
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.clone());
|
||||
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.to_string());
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
for (_, _, e) in ents {
|
||||
l.entities.push(e);
|
||||
@@ -1857,13 +1917,22 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
}
|
||||
d.blocks.insert(name, std::sync::Arc::new(b));
|
||||
}
|
||||
// ENTITIES section: model space, or paper space when flagged (67=1).
|
||||
}
|
||||
|
||||
/// Place the ENTITIES section: model space, or paper space when flagged (67=1).
|
||||
fn place_entities(
|
||||
d: &mut Drawing,
|
||||
entities: Vec<(Option<String>, bool, Entity)>,
|
||||
rx: &Rx,
|
||||
paper_block_for: &HashMap<String, String>,
|
||||
first_layout: &str,
|
||||
) {
|
||||
for (owner, paper, e) in entities {
|
||||
d.ensure_layer(&e.common.layer);
|
||||
let owner_name = owner.and_then(|o| rx.brs.get(&o.trim().to_ascii_uppercase()).cloned()).map(|n| n.to_ascii_uppercase());
|
||||
let to_paper = paper || owner_name.as_deref().is_some_and(|n| n.starts_with("*PAPER_SPACE"));
|
||||
if to_paper {
|
||||
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.clone());
|
||||
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.to_string());
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
l.entities.push(e);
|
||||
}
|
||||
@@ -1871,21 +1940,6 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
d.model.push(e);
|
||||
}
|
||||
}
|
||||
apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);
|
||||
objs.apply(&mut d, &rx);
|
||||
crate::dxf_image::resolve(&secs, &mut d);
|
||||
crate::dxf_mline::resolve(&secs, &mut d);
|
||||
// Keep handles unique against the header's seed.
|
||||
let seed = d.header.str("HANDSEED", "");
|
||||
if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) {
|
||||
d.bump_handseed(Handle(h.0.saturating_sub(1)));
|
||||
}
|
||||
if d.layer("0").is_none() {
|
||||
d.layers.insert(0, Layer::default());
|
||||
}
|
||||
crate::dxf_raw::read(&secs, &mut d);
|
||||
d.skipped_records = skipped;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
fn count_skipped(skipped: &mut std::collections::BTreeMap<String, usize>, section: &str, n: usize) {
|
||||
@@ -2054,13 +2108,7 @@ impl Objects {
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn apply(self, d: &mut Drawing, rx: &Rx) {
|
||||
if let Some(v) = self.wipeout_frame {
|
||||
d.header.set_i64("WIPEOUTFRAME", v);
|
||||
}
|
||||
if let Some(style) = self.xrecord_text(crate::dxf_ext::MLEADER_STYLE_KEY).filter(|s| !s.trim().is_empty()) {
|
||||
d.header.set_str("CMLEADERSTYLE", &style);
|
||||
}
|
||||
fn apply_table_styles(&self, d: &mut Drawing) {
|
||||
// Table styles (named by their ACAD_TABLESTYLE dictionary entries).
|
||||
for (h, tags) in &self.table_styles {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
@@ -2070,6 +2118,9 @@ impl Objects {
|
||||
None => d.table_styles.push(st),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_table_style_fixes(&self, d: &mut Drawing, rx: &Rx) {
|
||||
for (h, sh) in &rx.table_style_fix {
|
||||
let name = self.names.get(sh).cloned();
|
||||
// A table from another writer has no scale of its own: its text height is its
|
||||
@@ -2090,6 +2141,61 @@ impl Objects {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mleader_styles(&self, d: &mut Drawing, rx: &Rx) {
|
||||
// Multileader styles (named by their ACAD_MLEADERSTYLE dictionary entries).
|
||||
for (h, tags) in &self.mleader_styles {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let def = MLeaderStyle::default();
|
||||
let num = |code: i32, d: f64, min: f64| t.f(code).filter(|v| v.is_finite() && *v >= min).unwrap_or(d);
|
||||
let text_style = t.s(342).and_then(|sh| rx.styles.get(&sh.trim().to_ascii_uppercase()).cloned()).filter(|n| !n.is_empty());
|
||||
let st = MLeaderStyle {
|
||||
name: name.clone(),
|
||||
arrow_size: num(44, def.arrow_size, 0.0),
|
||||
text_height: t.f(45).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(def.text_height),
|
||||
landing_gap: num(42, def.landing_gap, 0.0),
|
||||
dogleg: num(43, def.dogleg, 0.0),
|
||||
text_style: text_style.unwrap_or(def.text_style),
|
||||
annotative: t.i(296).unwrap_or(0) != 0,
|
||||
};
|
||||
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.mleader_styles.push(st),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_groups(&self, d: &mut Drawing) {
|
||||
// Groups (named by their ACAD_GROUP dictionary entries); members that exist.
|
||||
for (h, tags) in &self.groups {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let members: Vec<Handle> = tags
|
||||
.iter()
|
||||
.filter(|x| x.code == 340)
|
||||
.filter_map(|x| Handle::parse_hex(&x.str()))
|
||||
.filter(|m| d.entity(*m).is_some())
|
||||
.take(MAX_OBJECTS)
|
||||
.collect();
|
||||
let g = Group { name: name.clone(), description: t.s(300).unwrap_or_default(), selectable: t.i(71).unwrap_or(1) != 0, members };
|
||||
match d.groups.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = g,
|
||||
None => d.groups.push(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(self, d: &mut Drawing, rx: &Rx) {
|
||||
if let Some(v) = self.wipeout_frame {
|
||||
d.header.set_i64("WIPEOUTFRAME", v);
|
||||
}
|
||||
if let Some(style) = self.xrecord_text(crate::dxf_ext::MLEADER_STYLE_KEY).filter(|s| !s.trim().is_empty()) {
|
||||
d.header.set_str("CMLEADERSTYLE", &style);
|
||||
}
|
||||
self.apply_table_styles(d);
|
||||
self.apply_table_style_fixes(d, rx);
|
||||
// Parametric constraints and parameters.
|
||||
if let Some((c, p)) = self.xrecord_text(crate::dxf_ext::CONSTRAINTS_KEY).and_then(|t| crate::dxf_ext::parse_constraints(&t)) {
|
||||
d.constraints = c;
|
||||
@@ -2113,50 +2219,14 @@ impl Objects {
|
||||
if let Some(tables) = self.xrecord_text(crate::dxf_ext::PLOT_STYLES_KEY).and_then(|t| crate::dxf_ext::parse_plot_styles(&t)) {
|
||||
d.plot_style_tables = tables;
|
||||
}
|
||||
// Multileader styles (named by their ACAD_MLEADERSTYLE dictionary entries).
|
||||
for (h, tags) in &self.mleader_styles {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let def = MLeaderStyle::default();
|
||||
let num = |code: i32, d: f64, min: f64| t.f(code).filter(|v| v.is_finite() && *v >= min).unwrap_or(d);
|
||||
let text_style = t.s(342).and_then(|sh| rx.styles.get(&sh.trim().to_ascii_uppercase()).cloned()).filter(|n| !n.is_empty());
|
||||
let st = MLeaderStyle {
|
||||
name: name.clone(),
|
||||
arrow_size: num(44, def.arrow_size, 0.0),
|
||||
text_height: t.f(45).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(def.text_height),
|
||||
landing_gap: num(42, def.landing_gap, 0.0),
|
||||
dogleg: num(43, def.dogleg, 0.0),
|
||||
text_style: text_style.unwrap_or(def.text_style),
|
||||
annotative: t.i(296).unwrap_or(0) != 0,
|
||||
};
|
||||
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = st,
|
||||
None => d.mleader_styles.push(st),
|
||||
}
|
||||
}
|
||||
self.apply_mleader_styles(d, rx);
|
||||
crate::dxf_mleader::apply(d, &rx.mleader_fix, &self.names);
|
||||
for (name, view) in &self.layout_views {
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == *name) {
|
||||
l.view = Some(*view);
|
||||
}
|
||||
}
|
||||
// Groups (named by their ACAD_GROUP dictionary entries); members that exist.
|
||||
for (h, tags) in &self.groups {
|
||||
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
|
||||
let t = T(tags);
|
||||
let members: Vec<Handle> = tags
|
||||
.iter()
|
||||
.filter(|x| x.code == 340)
|
||||
.filter_map(|x| Handle::parse_hex(&x.str()))
|
||||
.filter(|m| d.entity(*m).is_some())
|
||||
.take(MAX_OBJECTS)
|
||||
.collect();
|
||||
let g = Group { name: name.clone(), description: t.s(300).unwrap_or_default(), selectable: t.i(71).unwrap_or(1) != 0, members };
|
||||
match d.groups.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
|
||||
Some(x) => *x = g,
|
||||
None => d.groups.push(g),
|
||||
}
|
||||
}
|
||||
self.apply_groups(d);
|
||||
// Standard associativity, for dimensions without CADCraft's exact links.
|
||||
for tags in &self.dimassocs {
|
||||
dimassoc(d, tags);
|
||||
|
||||
+110
-84
@@ -17,7 +17,7 @@ use std::fmt::Write as _;
|
||||
use cadcraft_color::Rgb;
|
||||
use cadcraft_doc::{Drawing, Handle, PageSetup, Space};
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
use cadcraft_render::{Kind, Sheet, clip, paper};
|
||||
use cadcraft_render::{DPrim, Kind, Sheet, clip, paper};
|
||||
use serde_json::Value;
|
||||
|
||||
use craft_pdf::write::backend as kr;
|
||||
@@ -808,7 +808,7 @@ fn krilla_failure(e: KrillaError) -> Failure {
|
||||
/// as text instead: the glyph at font size 1 under its glyph matrix (the matrix axes are one
|
||||
/// em).
|
||||
fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts::Fonts) -> std::result::Result<(), Failure> {
|
||||
let (list, map) = (&p.list, &p.map);
|
||||
let list = &p.list;
|
||||
let texts = fonts::texts(list);
|
||||
let white = Rgb(255, 255, 255);
|
||||
let mut batch: Option<(StrokeStyle, PathBuilder)> = None;
|
||||
@@ -829,100 +829,126 @@ fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts:
|
||||
let t = if prim.kind == Kind::Mask { 0 } else { prim.transparency.min(cadcraft_render::MAX_TRANSPARENCY) };
|
||||
let w = f64::from(prim.lw) * PT_PER_MM;
|
||||
let w = if w.is_finite() && w > 0.0 { w.min(100.0) } else { 0.0 };
|
||||
let m = Mark { rgb, t, w };
|
||||
match prim.kind {
|
||||
Kind::Polyline | Kind::Infinite { .. } => {
|
||||
let pts: Vec<Vec2> = match prim.kind {
|
||||
Kind::Infinite { ray } => match (raw.first(), raw.get(1)) {
|
||||
(Some(base), Some(dir)) => {
|
||||
let a = map.pt(*base);
|
||||
let dir_pt = map.pt(*base + *dir) - a;
|
||||
match clip::clip_infinite(a, dir_pt, ray, &p.clip_pt) {
|
||||
Some((x, y)) => vec![x, y],
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
_ => raw.iter().map(|q| map.pt(*q)).collect(),
|
||||
};
|
||||
if pts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
// With a vertex out of range, keep the pieces inside the plot area, widened so
|
||||
// a wide stroke just outside it still shows its edge.
|
||||
let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(w + 1.0)) };
|
||||
let style = StrokeStyle { rgb, width: w as f32, transparency: t };
|
||||
if t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) {
|
||||
flush(s, &mut batch);
|
||||
}
|
||||
for run in runs {
|
||||
let Some(pts) = run.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
|
||||
// The writer leaves out a path without extent, so a zero-length line is
|
||||
// painted as the round-cap dot its stroke would make.
|
||||
if let Some(&(x, y)) = pts.first()
|
||||
&& pts.iter().all(|q| *q == (x, y))
|
||||
{
|
||||
flush(s, &mut batch);
|
||||
let d = if w > 0.0 { w as f32 } else { HAIRLINE_DOT_PT };
|
||||
if let Some(path) = circle_path(x, y, d / 2.0) {
|
||||
fill_path(s, &path, rgb, t);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new()));
|
||||
for (j, (x, y)) in pts.iter().enumerate() {
|
||||
if j == 0 {
|
||||
pb.move_to(*x, *y);
|
||||
} else {
|
||||
pb.line_to(*x, *y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Polyline | Kind::Infinite { .. } => polyline(s, &mut batch, p, prim, raw, m),
|
||||
Kind::Tris | Kind::Mask => {
|
||||
flush(s, &mut batch);
|
||||
// A wipeout's mask is paper white.
|
||||
let rgb = if prim.kind == Kind::Mask { white } else { rgb };
|
||||
if let Some(text) = u32::try_from(i).ok().and_then(|i| texts.get(&i))
|
||||
&& let Some(font) = fonts.font(&text.glyph.face)
|
||||
{
|
||||
glyph(s, font, text, map, rgb, t);
|
||||
continue;
|
||||
}
|
||||
let mapped: Vec<Vec2> = raw.iter().map(|q| map.pt(*q)).collect();
|
||||
// With a vertex out of range, keep the parts inside the plot area.
|
||||
let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) };
|
||||
let mut pb = PathBuilder::new();
|
||||
for tri in tris.as_chunks::<3>().0 {
|
||||
let Some(v) = tri.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
|
||||
for (j, (x, y)) in v.iter().enumerate() {
|
||||
if j == 0 {
|
||||
pb.move_to(*x, *y);
|
||||
} else {
|
||||
pb.line_to(*x, *y);
|
||||
}
|
||||
}
|
||||
pb.close();
|
||||
}
|
||||
if let Some(path) = pb.finish() {
|
||||
fill_path(s, &path, rgb, t);
|
||||
}
|
||||
}
|
||||
Kind::Point => {
|
||||
let Some(q) = raw.first() else { continue };
|
||||
flush(s, &mut batch);
|
||||
let half = (w.max(0.5)) / 2.0;
|
||||
let c = map.pt(*q);
|
||||
if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) {
|
||||
fill_path(s, &path, rgb, t);
|
||||
let m = if prim.kind == Kind::Mask { Mark { rgb: white, ..m } } else { m };
|
||||
let text_font =
|
||||
u32::try_from(i).ok().and_then(|i| texts.get(&i).copied()).and_then(|text| fonts.font(&text.glyph.face).map(|font| (text, font)));
|
||||
match text_font {
|
||||
Some((text, font)) => glyph(s, font, text, &p.map, m.rgb, m.t),
|
||||
None => fill_tris(s, p, raw, m.rgb, m.t),
|
||||
}
|
||||
}
|
||||
Kind::Point => point(s, &mut batch, &p.map, raw, m),
|
||||
}
|
||||
}
|
||||
flush(s, &mut batch);
|
||||
layer_changed(s, layer, None)
|
||||
}
|
||||
|
||||
/// How a primitive is painted: colour, transparency (percent) and lineweight (points).
|
||||
#[derive(Clone, Copy)]
|
||||
struct Mark {
|
||||
rgb: Rgb,
|
||||
t: u8,
|
||||
w: f64,
|
||||
}
|
||||
|
||||
/// Stroke a polyline or infinite line, clipped to the plot area, into the open batch.
|
||||
fn polyline(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, p: &SheetPage, prim: &DPrim, raw: &[Vec2], m: Mark) {
|
||||
let Some(pts) = polyline_points(prim, raw, &p.map, &p.clip_pt) else { return };
|
||||
if pts.len() < 2 {
|
||||
return;
|
||||
}
|
||||
// With a vertex out of range, keep the pieces inside the plot area, widened so
|
||||
// a wide stroke just outside it still shows its edge.
|
||||
let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(m.w + 1.0)) };
|
||||
let style = StrokeStyle { rgb: m.rgb, width: m.w as f32, transparency: m.t };
|
||||
if m.t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) {
|
||||
flush(s, batch);
|
||||
}
|
||||
for run in runs {
|
||||
stroke_run(s, batch, &run, style, m);
|
||||
}
|
||||
}
|
||||
|
||||
/// The page points of a polyline, or of an infinite line clipped to the plot area; `None` when
|
||||
/// there is nothing to draw.
|
||||
fn polyline_points(prim: &DPrim, raw: &[Vec2], map: &Map, clip_pt: &Bounds2) -> Option<Vec<Vec2>> {
|
||||
match prim.kind {
|
||||
Kind::Infinite { ray } => {
|
||||
let (Some(base), Some(dir)) = (raw.first(), raw.get(1)) else { return None };
|
||||
let a = map.pt(*base);
|
||||
let dir_pt = map.pt(*base + *dir) - a;
|
||||
let (x, y) = clip::clip_infinite(a, dir_pt, ray, clip_pt)?;
|
||||
Some(vec![x, y])
|
||||
}
|
||||
_ => Some(raw.iter().map(|q| map.pt(*q)).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add one run of a polyline to the batch, or paint it as a dot when it has no extent.
|
||||
fn stroke_run(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, run: &[Vec2], style: StrokeStyle, m: Mark) {
|
||||
let Some(pts) = run.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { return };
|
||||
// The writer leaves out a path without extent, so a zero-length line is
|
||||
// painted as the round-cap dot its stroke would make.
|
||||
if let Some(&(x, y)) = pts.first()
|
||||
&& pts.iter().all(|q| *q == (x, y))
|
||||
{
|
||||
flush(s, batch);
|
||||
let d = if m.w > 0.0 { m.w as f32 } else { HAIRLINE_DOT_PT };
|
||||
if let Some(path) = circle_path(x, y, d / 2.0) {
|
||||
fill_path(s, &path, m.rgb, m.t);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new()));
|
||||
for (j, (x, y)) in pts.iter().enumerate() {
|
||||
if j == 0 {
|
||||
pb.move_to(*x, *y);
|
||||
} else {
|
||||
pb.line_to(*x, *y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a set of triangles, clipped to the plot area.
|
||||
fn fill_tris(s: &mut Surface<'_>, p: &SheetPage, raw: &[Vec2], rgb: Rgb, t: u8) {
|
||||
let mapped: Vec<Vec2> = raw.iter().map(|q| p.map.pt(*q)).collect();
|
||||
// With a vertex out of range, keep the parts inside the plot area.
|
||||
let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) };
|
||||
let mut pb = PathBuilder::new();
|
||||
for tri in tris.as_chunks::<3>().0 {
|
||||
let Some(v) = tri.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
|
||||
for (j, (x, y)) in v.iter().enumerate() {
|
||||
if j == 0 {
|
||||
pb.move_to(*x, *y);
|
||||
} else {
|
||||
pb.line_to(*x, *y);
|
||||
}
|
||||
}
|
||||
pb.close();
|
||||
}
|
||||
if let Some(path) = pb.finish() {
|
||||
fill_path(s, &path, rgb, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint a point marker: a small square at the point.
|
||||
fn point(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, map: &Map, raw: &[Vec2], m: Mark) {
|
||||
let Some(q) = raw.first() else { return };
|
||||
flush(s, batch);
|
||||
let half = (m.w.max(0.5)) / 2.0;
|
||||
let c = map.pt(*q);
|
||||
if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) {
|
||||
fill_path(s, &path, m.rgb, m.t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write one glyph as text in its embedded font: font size 1, placed by the glyph's matrix (one
|
||||
/// em along each axis). A glyph whose matrix is out of range or collapses is left out.
|
||||
fn glyph(s: &mut Surface<'_>, font: kr::text::Font, t: &cadcraft_render::PlotText, map: &Map, rgb: Rgb, transparency: u8) {
|
||||
|
||||
+26
-38
@@ -465,26 +465,6 @@ impl Family<'_> {
|
||||
self.origin + p.rotate(self.ang)
|
||||
}
|
||||
|
||||
/// The range of the lines swept, in this frame: the boundary's extent, cut to the view's
|
||||
/// when there is one, and the view's x extent that spans are cut to. Spans are cut to the
|
||||
/// view's extent only: they already lie within the boundary, and cutting them to its extent
|
||||
/// too would move an end that sits on a boundary corner by a rounding error, so a view
|
||||
/// covering the hatch would change its lines. `None` when the view misses the boundary.
|
||||
fn lines_range(&self, bb: &Bounds2, view: Option<&Bounds2>) -> Option<(Vec2, Vec2, Option<(f64, f64)>)> {
|
||||
let (mut lo, mut hi) = (bb.min, bb.max);
|
||||
let mut span_cut = None;
|
||||
if let Some(v) = view {
|
||||
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
|
||||
if vb.is_empty() || !vb.intersects(bb) {
|
||||
return None;
|
||||
}
|
||||
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
|
||||
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
|
||||
span_cut = Some((vb.min.x, vb.max.x));
|
||||
}
|
||||
Some((lo, hi, span_cut))
|
||||
}
|
||||
|
||||
/// Intersect a pattern family with boundary loops, respecting view, output, cancellation, and shared dash-work limits.
|
||||
fn sweep(&self, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>, cancel: Option<&AtomicBool>, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
|
||||
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
|
||||
@@ -492,7 +472,21 @@ impl Family<'_> {
|
||||
if bb.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some((lo, hi, span_cut)) = self.lines_range(&bb, view) else { return };
|
||||
// The lines swept: the boundary's extent, cut to the view's (its corners in this frame)
|
||||
// when there is one. Spans are cut to the view's extent only: they already lie within the
|
||||
// boundary, and cutting them to its extent too would move an end that sits on a boundary
|
||||
// corner by a rounding error, so a view covering the hatch would change its lines.
|
||||
let (mut lo, mut hi) = (bb.min, bb.max);
|
||||
let mut span_cut = None;
|
||||
if let Some(v) = view {
|
||||
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
|
||||
if vb.is_empty() || !vb.intersects(&bb) {
|
||||
return;
|
||||
}
|
||||
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
|
||||
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
|
||||
span_cut = Some((vb.min.x, vb.max.x));
|
||||
}
|
||||
let shift_per_line = self.delta.rotate(-self.ang).x;
|
||||
let k0 = (lo.y / self.spacing).floor() as i64;
|
||||
let k1 = (hi.y / self.spacing).ceil() as i64;
|
||||
@@ -510,7 +504,17 @@ impl Family<'_> {
|
||||
return;
|
||||
}
|
||||
let y = k as f64 * self.spacing;
|
||||
let xs = crossings(&local, y);
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in &local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
let offset = shift_per_line * k as f64;
|
||||
for span in xs.chunks(2) {
|
||||
let [x0, x1] = span else { continue };
|
||||
@@ -561,22 +565,6 @@ impl Family<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The x positions where the boundary loops cross the horizontal line at `y`, in order.
|
||||
fn crossings(local: &[Vec<Vec2>], y: f64) -> Vec<f64> {
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in local {
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
xs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_engine::cmd::file::compare::Comparison;
|
||||
use cadcraft_engine::doc::{Drawing, Handle};
|
||||
use cadcraft_engine::doc::Handle;
|
||||
use cadcraft_render::DisplayList;
|
||||
use egui::{Rect, RichText, Stroke, vec2};
|
||||
use serde_json::json;
|
||||
@@ -30,23 +29,6 @@ fn id() -> egui::Id {
|
||||
egui::Id::unique("cadcraft-compare-overlay")
|
||||
}
|
||||
|
||||
/// The display lists of the differences for this frame: the stored ones when they still match
|
||||
/// `key`, otherwise rebuilt at zoom `scale` and stored.
|
||||
fn cache_for(ctx: &egui::Context, key: (u64, u64, usize, usize, i64), doc: &Drawing, c: &Comparison, scale: f64) -> Cache {
|
||||
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
|
||||
if cache.key != key || cache.ours.is_none() {
|
||||
let opts = cadcraft_render::Options { tolerance: 0.5 / scale.max(1e-12), ..Default::default() };
|
||||
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
|
||||
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
|
||||
let list = |d: &Drawing, hs: &[Handle]| {
|
||||
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
|
||||
};
|
||||
cache = Cache { key, ours: Some(list(doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
|
||||
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
|
||||
}
|
||||
cache
|
||||
}
|
||||
|
||||
/// Draw the comparison over the canvas when it was made for the drawing and tab shown.
|
||||
pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf: &Xf, rect: Rect) {
|
||||
let t = Tokens::get();
|
||||
@@ -56,7 +38,17 @@ pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf
|
||||
p.rect_filled(rect, 0.0, t.canvas.gamma_multiply(0.75));
|
||||
let band = xf.scale.max(1e-300).log2().round() as i64;
|
||||
let key = (st.uid, st.revision, Arc::as_ptr(&c.other) as usize, c.differences(), band);
|
||||
let cache = cache_for(ctx, key, &st.doc, c, xf.scale);
|
||||
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
|
||||
if cache.key != key || cache.ours.is_none() {
|
||||
let opts = cadcraft_render::Options { tolerance: 0.5 / xf.scale.max(1e-12), ..Default::default() };
|
||||
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
|
||||
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
|
||||
let list = |d: &cadcraft_engine::doc::Drawing, hs: &[Handle]| {
|
||||
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
|
||||
};
|
||||
cache = Cache { key, ours: Some(list(&st.doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
|
||||
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
|
||||
}
|
||||
for (list, color) in [(&cache.theirs, t.compare_other), (&cache.ours, t.compare_this)] {
|
||||
if let Some(l) = list {
|
||||
let all: Vec<usize> = (0..l.prims.len()).collect();
|
||||
|
||||
@@ -217,15 +217,73 @@ fn display(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
/// Files tab: save format and default version, new-drawing units, autosave interval and autosave file location; edits accumulate in `changes`.
|
||||
fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
grid(ui, "options_files", |ui| {
|
||||
save_format_row(ui, app, changes);
|
||||
ui.label(crate::tl!("Save new drawings as"));
|
||||
ui.horizontal(|ui| {
|
||||
for f in crate::SaveFormat::ALL {
|
||||
let label = match f {
|
||||
crate::SaveFormat::Dxf => crate::tl!("DXF"),
|
||||
crate::SaveFormat::Dwg => crate::tl!("DWG"),
|
||||
};
|
||||
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
|
||||
changes.insert("saveFormat".into(), json!(f.as_str()));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
save_version_row(ui, app, changes);
|
||||
|
||||
let version_caption = ui.label(crate::tl!("Default save version"));
|
||||
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
|
||||
let mut version = current;
|
||||
let version_combo = egui::ComboBox::from_id_salt("options_save_version").selected_text(version.year()).show_ui(ui, |ui| {
|
||||
for v in crate::SaveVersion::ALL {
|
||||
ui.selectable_value(&mut version, v, v.year());
|
||||
}
|
||||
});
|
||||
version_combo.response.labelled_by(version_caption.id);
|
||||
if version != current {
|
||||
changes.insert("saveVersion".into(), json!(version.year()));
|
||||
}
|
||||
ui.end_row();
|
||||
units_row(ui, app, changes);
|
||||
|
||||
ui.label(crate::tl!("Units of new drawings"));
|
||||
ui.horizontal(|ui| {
|
||||
let metric = app.session.settings.measureinit;
|
||||
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(0));
|
||||
}
|
||||
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(1));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
autosave_row(ui, changes);
|
||||
|
||||
let minutes_caption = ui.label(crate::tl!("Automatic save every"));
|
||||
let mut minutes = cadcraft_engine::autosave::savetime();
|
||||
let max = cadcraft_engine::autosave::MAX_SAVETIME;
|
||||
if ui
|
||||
.add(egui::DragValue::new(&mut minutes).range(0..=max).suffix(format!(" {}", crate::tl!("min"))))
|
||||
.labelled_by(minutes_caption.id)
|
||||
.changed()
|
||||
{
|
||||
changes.insert("SAVETIME".into(), json!(minutes));
|
||||
}
|
||||
ui.end_row();
|
||||
savefile_row(ui, changes);
|
||||
|
||||
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
|
||||
// Edited as text; the folder changes when the field loses focus.
|
||||
let id = egui::Id::unique("cc_options_savefilepath");
|
||||
let current = cadcraft_engine::autosave::savefilepath();
|
||||
let mut path = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.clone());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut path).desired_width(330.0)).labelled_by(savefile_label.id);
|
||||
if r.changed() {
|
||||
ui.data_mut(|d| d.insert_temp(id, path.clone()));
|
||||
}
|
||||
if r.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<String>(id));
|
||||
if path.trim() != current {
|
||||
changes.insert("SAVEFILEPATH".into(), json!(path.trim()));
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
hint(ui, crate::tl!("New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE)."));
|
||||
@@ -233,76 +291,6 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
hint(ui, crate::tl!("Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off."));
|
||||
}
|
||||
|
||||
fn save_format_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
ui.label(crate::tl!("Save new drawings as"));
|
||||
ui.horizontal(|ui| {
|
||||
for f in crate::SaveFormat::ALL {
|
||||
let label = match f {
|
||||
crate::SaveFormat::Dxf => crate::tl!("DXF"),
|
||||
crate::SaveFormat::Dwg => crate::tl!("DWG"),
|
||||
};
|
||||
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
|
||||
changes.insert("saveFormat".into(), json!(f.as_str()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn save_version_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
let version_caption = ui.label(crate::tl!("Default save version"));
|
||||
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
|
||||
let mut version = current;
|
||||
let version_combo = egui::ComboBox::from_id_salt("options_save_version").selected_text(version.year()).show_ui(ui, |ui| {
|
||||
for v in crate::SaveVersion::ALL {
|
||||
ui.selectable_value(&mut version, v, v.year());
|
||||
}
|
||||
});
|
||||
version_combo.response.labelled_by(version_caption.id);
|
||||
if version != current {
|
||||
changes.insert("saveVersion".into(), json!(version.year()));
|
||||
}
|
||||
}
|
||||
|
||||
fn units_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
|
||||
ui.label(crate::tl!("Units of new drawings"));
|
||||
ui.horizontal(|ui| {
|
||||
let metric = app.session.settings.measureinit;
|
||||
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(0));
|
||||
}
|
||||
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
|
||||
changes.insert("MEASUREINIT".into(), json!(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn autosave_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
let minutes_caption = ui.label(crate::tl!("Automatic save every"));
|
||||
let mut minutes = cadcraft_engine::autosave::savetime();
|
||||
let max = cadcraft_engine::autosave::MAX_SAVETIME;
|
||||
if ui.add(egui::DragValue::new(&mut minutes).range(0..=max).suffix(format!(" {}", crate::tl!("min")))).labelled_by(minutes_caption.id).changed() {
|
||||
changes.insert("SAVETIME".into(), json!(minutes));
|
||||
}
|
||||
}
|
||||
|
||||
fn savefile_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
|
||||
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
|
||||
// Edited as text; the folder changes when the field loses focus.
|
||||
let id = egui::Id::unique("cc_options_savefilepath");
|
||||
let current = cadcraft_engine::autosave::savefilepath();
|
||||
let mut path = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.clone());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut path).desired_width(330.0)).labelled_by(savefile_label.id);
|
||||
if r.changed() {
|
||||
ui.data_mut(|d| d.insert_temp(id, path.clone()));
|
||||
}
|
||||
if r.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<String>(id));
|
||||
if path.trim() != current {
|
||||
changes.insert("SAVEFILEPATH".into(), json!(path.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drafting tab: object-snap marker size and aperture previews plus the Drafting Settings entry; edits accumulate in `changes` and the entry may request a command through `start`.
|
||||
fn drafting(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>, start: &mut Option<&'static str>) {
|
||||
let t = Tokens::get();
|
||||
|
||||
Reference in New Issue
Block a user