Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 5ae80124cb refactor: split complex matchers, scanners and solver setup into helpers
Pure extraction, no behaviour change: same order of side effects, same
budget charges, same early returns and error values. Clears
cognitive-complexity findings on glob_inner, the PDF test ops scanner,
snap_ends, the loops inquiry run and build_budgeted (S3776).
2026-10-25 21:33:51 -04:00
7 changed files with 667 additions and 708 deletions
+51 -40
View File
@@ -358,6 +358,56 @@ fn components<'a>(cons: &[&'a Constraint]) -> Vec<Vec<&'a Constraint>> {
groups.into_values().collect()
}
/// The solver weight of each unknown of `model`: kept handles weigh more, preferred-to-move ones
/// less, angles and ratios by size², and contact parameters move freely.
fn solver_weights(model: &Model, opts: &SolveOptions) -> Vec<f64> {
let mut weights = vec![1.0; model.x0.len()];
for s in &model.slots {
let w = if opts.keep.contains(&s.handle) {
1e4
} else if opts.prefer_move.contains(&s.handle) {
1e-2
} else {
1.0
};
for j in s.off..s.off + s.len {
if let Some(slot) = weights.get_mut(j) {
*slot = w;
}
}
for (j, f) in slot_scales(model, s) {
if let Some(slot) = weights.get_mut(j) {
*slot *= f;
}
}
}
// Contact parameters move freely.
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
for w in weights.iter_mut().skip(geo) {
*w = 1e-3;
}
weights
}
/// Per-unknown factors for the angles and ratios of one slot: size², so that the movement of
/// points is what is minimised.
fn slot_scales(model: &Model, s: &model::Slot) -> Vec<(usize, f64)> {
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
match s.shape {
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
model::Shape::Ellipse { arc } => {
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
let mut v = vec![(s.off + 4, a2)];
if arc {
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
}
v
}
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
_ => Vec::new(),
}
}
/// Build the component systems. Constraints that cannot be interpreted are returned as skipped.
pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> Result<(Vec<Comp>, Vec<(u32, String)>), SolveError> {
let mut members_left = model::MAX_RIGID_WORK;
@@ -396,46 +446,7 @@ pub(crate) fn build_budgeted(
if built.is_empty() {
continue;
}
let mut weights = vec![1.0; model.x0.len()];
for s in &model.slots {
let w = if opts.keep.contains(&s.handle) {
1e4
} else if opts.prefer_move.contains(&s.handle) {
1e-2
} else {
1.0
};
for j in s.off..s.off + s.len {
if let Some(slot) = weights.get_mut(j) {
*slot = w;
}
}
// Angles and ratios: weight by size² so the movement of points is what is minimised.
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
let scaled: Vec<(usize, f64)> = match s.shape {
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
model::Shape::Ellipse { arc } => {
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
let mut v = vec![(s.off + 4, a2)];
if arc {
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
}
v
}
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
_ => Vec::new(),
};
for (j, f) in scaled {
if let Some(slot) = weights.get_mut(j) {
*slot *= f;
}
}
}
// Contact parameters move freely.
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
for w in weights.iter_mut().skip(geo) {
*w = 1e-3;
}
let weights = solver_weights(&model, opts);
let held = opts.hold.iter().filter_map(|r| model.lock_indices(r)).flatten().collect();
let tol = scale_tol(&model.x0);
comps.push(Comp { model, built, weights, held, tol });
+81 -63
View File
@@ -117,21 +117,7 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
// Endpoints closer than the tolerance meet. Transitive components of three or more ends are
// branch junctions; a component of exactly two is a simple joint.
let mut parent: Vec<usize> = (0..n).collect();
for i in 0..n {
let Some(a) = ends.get(i) else { continue };
for j in (i + 1)..n {
let Some(b) = ends.get(j) else { continue };
if a.at.dist(b.at) <= tol {
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
if ra != rb
&& let Some(slot) = parent.get_mut(ra)
{
*slot = rb;
}
}
}
}
let mut parent = merge_ends(&ends, tol);
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for i in 0..n {
let root = find(&mut parent, i);
@@ -140,54 +126,8 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
// Junctions are reported and their pieces left out of the chains: not resolved.
let mut excluded = vec![false; pieces.len()];
let mut junctions: Vec<Value> = Vec::new();
let mut joints: Vec<(usize, usize)> = Vec::new();
for group in groups.values() {
match group.as_slice() {
&[a, b] => joints.push((a, b)),
[_, _, ..] => {
let scale = group.len() as f64;
let mut at = Vec2::ZERO;
let mut members: Vec<Handle> = Vec::new();
for &e in group {
let Some(end) = ends.get(e) else { continue };
at += end.at / scale;
let Some(piece) = pieces.get(end.piece) else { continue };
if !members.contains(&piece.handle) {
members.push(piece.handle);
}
if let Some(slot) = excluded.get_mut(end.piece) {
*slot = true;
}
}
junctions.push(json!({
"at": [at.x, at.y],
"ends": group.len(),
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
}));
}
_ => {}
}
}
// A joint links the two ends for the walk. A joint into an excluded piece is not a link, and
// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
let mut link: Vec<Option<usize>> = vec![None; n];
for (a, b) in joints {
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
continue;
}
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
continue;
}
if let Some(slot) = link.get_mut(a) {
*slot = Some(b);
}
if let Some(slot) = link.get_mut(b) {
*slot = Some(a);
}
}
let (joints, junctions) = sort_groups(&groups, &ends, &pieces, &mut excluded);
let link = joint_links(joints, &ends, &pieces, &excluded);
let mut visited = vec![false; pieces.len()];
let mut loops: Vec<Value> = Vec::new();
@@ -225,6 +165,84 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
}
/// A report with nothing analyzed: an empty drawing, or one past the pair cap.
/// Union-find over the ends: ends closer than `tol` share a root. Returns each end's parent.
fn merge_ends(ends: &[End], tol: f64) -> Vec<usize> {
let n = ends.len();
let mut parent: Vec<usize> = (0..n).collect();
for i in 0..n {
let Some(a) = ends.get(i) else { continue };
for j in (i + 1)..n {
let Some(b) = ends.get(j) else { continue };
if a.at.dist(b.at) <= tol {
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
if ra != rb
&& let Some(slot) = parent.get_mut(ra)
{
*slot = rb;
}
}
}
}
parent
}
/// Each group of two ends is a joint (end indices); each of three or more is a junction, reported
/// with its pieces marked `excluded`.
fn sort_groups(groups: &BTreeMap<usize, Vec<usize>>, ends: &[End], pieces: &[Piece], excluded: &mut [bool]) -> (Vec<(usize, usize)>, Vec<Value>) {
let mut joints: Vec<(usize, usize)> = Vec::new();
let mut junctions: Vec<Value> = Vec::new();
for group in groups.values() {
match group.as_slice() {
&[a, b] => joints.push((a, b)),
[_, _, ..] => {
let scale = group.len() as f64;
let mut at = Vec2::ZERO;
let mut members: Vec<Handle> = Vec::new();
for &e in group {
let Some(end) = ends.get(e) else { continue };
at += end.at / scale;
let Some(piece) = pieces.get(end.piece) else { continue };
if !members.contains(&piece.handle) {
members.push(piece.handle);
}
if let Some(slot) = excluded.get_mut(end.piece) {
*slot = true;
}
}
junctions.push(json!({
"at": [at.x, at.y],
"ends": group.len(),
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
}));
}
_ => {}
}
}
(joints, junctions)
}
/// The link each end takes from the joints. A joint into an excluded piece is not a link, and
/// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
fn joint_links(joints: Vec<(usize, usize)>, ends: &[End], pieces: &[Piece], excluded: &[bool]) -> Vec<Option<usize>> {
let mut link: Vec<Option<usize>> = vec![None; ends.len()];
for (a, b) in joints {
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
continue;
}
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
continue;
}
if let Some(slot) = link.get_mut(a) {
*slot = Some(b);
}
if let Some(slot) = link.get_mut(b) {
*slot = Some(a);
}
}
link
}
fn empty_report(offset: usize, tol: f64, pair_tests: usize, pair_tests_needed: usize, pair_limit: usize, capped: bool, skipped: Skipped) -> Value {
json!({
"schema": 1,
+43 -36
View File
@@ -785,6 +785,48 @@ fn glob_cached(
Ok(matched)
}
/// For the bracket class opening at `pat[p]`, whether `text[t]` is in it: the index of its closing
/// `]` when it is, `None` when it is not or the class is never closed.
fn class_end(text: &[char], pat: &[char], p: usize, t: usize, budget: &mut u64) -> Result<Option<usize>, ()> {
let mut close = p;
while let Some(c) = pat.get(close) {
charge_match(budget)?;
if *c == ']' {
break;
}
close += 1;
}
if close == pat.len() {
return Ok(None);
}
let Some(&c) = text.get(t) else { return Ok(None) };
let set = &pat[p + 1..close];
let (neg, set) = match set.split_first() {
Some(('~', r)) if !r.is_empty() => (true, r),
_ => (false, set),
};
let mut hit = false;
let mut i = 0;
while i < set.len() {
charge_match(budget)?;
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
if set[i] <= c && c <= set[i + 2] {
hit = true;
}
i += 3;
} else {
if set[i] == c {
hit = true;
}
i += 1;
}
}
if hit == neg {
return Ok(None);
}
Ok(Some(close))
}
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
fn glob_inner(
text: &[char],
@@ -822,42 +864,7 @@ fn glob_inner(
p += 2;
}
'[' => {
let mut close = p;
while let Some(c) = pat.get(close) {
charge_match(budget)?;
if *c == ']' {
break;
}
close += 1;
}
if close == pat.len() {
return Ok(false);
}
let Some(&c) = text.get(t) else { return Ok(false) };
let set = &pat[p + 1..close];
let (neg, set) = match set.split_first() {
Some(('~', r)) if !r.is_empty() => (true, r),
_ => (false, set),
};
let mut hit = false;
let mut i = 0;
while i < set.len() {
charge_match(budget)?;
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
if set[i] <= c && c <= set[i + 2] {
hit = true;
}
i += 3;
} else {
if set[i] == c {
hit = true;
}
i += 1;
}
}
if hit == neg {
return Ok(false);
}
let Some(close) = class_end(text, pat, p, t, budget)? else { return Ok(false) };
t += 1;
p = close + 1;
}
+40 -32
View File
@@ -447,6 +447,44 @@ fn islands(g: &Graph, outer: &[usize], p: Vec2) -> Vec<Island> {
out
}
/// The point of the nearest other curve within `snap` of the end `e` of curve `i`, as (curve,
/// segment, parameter, point). `None` when the end touches a curve, or none is near. `Err` when
/// the budget of segment checks runs out.
fn nearest_curve(polys: &[Vec<Vec2>], i: usize, e: Vec2, snap: f64, tol: f64, budget: &mut usize) -> Result<Option<(usize, usize, f64, Vec2)>, ()> {
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
let mut touching = false;
for (j, other) in polys.iter().enumerate() {
if j == i {
continue;
}
for (k, w) in other.windows(2).enumerate() {
if *budget == 0 {
return Err(());
}
*budget -= 1;
let [a, b] = w else { continue };
if !(a.is_finite() && b.is_finite())
|| e.x < a.x.min(b.x) - snap
|| e.x > a.x.max(b.x) + snap
|| e.y < a.y.min(b.y) - snap
|| e.y > a.y.max(b.y) + snap
{
continue;
}
let line = Line::new(*a, *b);
let t = line.param_of(e).clamp(0.0, 1.0);
let q = line.at(t);
let d = q.dist(e);
if d <= tol {
touching = true;
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
best = Some((d, j, k, t, q));
}
}
}
Ok(best.filter(|_| !touching).map(|(_, j, k, t, q)| (j, k, t, q)))
}
/// The curves with each open end that stops within `snap` of another curve moved onto that
/// curve (which gains a vertex there), so the two meet exactly. A curve that is flattened
/// into chords (an ellipse, a spline) lies up to the flattening tolerance off the true curve,
@@ -474,38 +512,8 @@ pub fn snap_ends(polys: &[Vec<Vec2>], snap: f64, tol: f64) -> Vec<Vec<Vec2>> {
if !e.is_finite() {
continue;
}
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
let mut touching = false;
for (j, other) in polys.iter().enumerate() {
if j == i {
continue;
}
for (k, w) in other.windows(2).enumerate() {
if budget == 0 {
break 'ends;
}
budget -= 1;
let [a, b] = w else { continue };
if !(a.is_finite() && b.is_finite())
|| e.x < a.x.min(b.x) - snap
|| e.x > a.x.max(b.x) + snap
|| e.y < a.y.min(b.y) - snap
|| e.y > a.y.max(b.y) + snap
{
continue;
}
let line = Line::new(*a, *b);
let t = line.param_of(e).clamp(0.0, 1.0);
let q = line.at(t);
let d = q.dist(e);
if d <= tol {
touching = true;
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
best = Some((d, j, k, t, q));
}
}
}
let Some((_, j, k, t, q)) = best.filter(|_| !touching) else { continue };
let Ok(near) = nearest_curve(polys, i, e, snap, tol, &mut budget) else { break 'ends };
let Some((j, k, t, q)) = near else { continue };
let Some(slot) = out.get_mut(i).and_then(|v| if at_end { v.last_mut() } else { v.first_mut() }) else { continue };
*slot = q;
let ends = polys.get(j).and_then(|o| o.get(k).zip(o.get(k + 1)));
+326 -396
View File
@@ -249,130 +249,6 @@ 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) {
@@ -384,16 +260,138 @@ 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" => EntityKind::Arc(arc(&c, &t)),
"ELLIPSE" => EntityKind::Ellipse(ellipse(&c, &t)),
"LWPOLYLINE" => lwpolyline(tags, &c, &t)?,
"SPLINE" => EntityKind::Spline(spline(&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);
}
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)
}
"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" => EntityKind::AttDef(attdef(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),
})
}
"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
@@ -1111,91 +1109,6 @@ 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,
@@ -1221,17 +1134,92 @@ fn parse_entities(
i += 1;
match kind.as_str() {
"POLYLINE" => {
let Some((c, k, more)) = polyline_entity(recs, &mut i, &t) else {
*skipped = skipped.saturating_add(1);
continue;
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,
})
};
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();
let attribs = insert_attribs(recs, &mut i, &ins_layer);
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;
}
}
if let Some((_, EntityKind::Insert(ins))) = kindent.as_mut() {
ins.attribs = attribs;
}
@@ -1700,13 +1688,6 @@ 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()))?;
@@ -1719,8 +1700,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<BlockDef> = Vec::new();
let mut layout_objs: Vec<LayoutDef> = Vec::new();
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 entities = Vec::new();
let mut objs = Objects::default();
// Every handle another program's file already uses is taken before anything gets a new one:
@@ -1740,109 +1721,73 @@ 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" => blocks.extend(read_blocks(&s.tags, &mut d, &mut rx, &mut table_cells_left, &mut skipped)),
"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);
}
// 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" => read_objects(&s.tags, &mut objs, &mut layout_objs),
"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)));
}
}
}
_ => {}
}
}
// 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()));
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) {
// Layouts.
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.iter() {
for (name, order, br, page) in &layout_objs {
if name.eq_ignore_ascii_case("Model") {
continue;
}
@@ -1861,12 +1806,7 @@ fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (Ha
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());
(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) {
// Blocks: model/paper space blocks hold entities in newer files.
for (name, base, description, ents, xref) in blocks {
let up = name.to_ascii_uppercase();
if up == "*MODEL_SPACE" {
@@ -1877,7 +1817,7 @@ fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block
continue;
}
if up.starts_with("*PAPER_SPACE") {
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.to_string());
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.clone());
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
for (_, _, e) in ents {
l.entities.push(e);
@@ -1917,22 +1857,13 @@ fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block
}
d.blocks.insert(name, std::sync::Arc::new(b));
}
}
/// 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,
) {
// ENTITIES section: model space, or paper space when flagged (67=1).
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.to_string());
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.clone());
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
l.entities.push(e);
}
@@ -1940,6 +1871,21 @@ fn place_entities(
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) {
@@ -2108,7 +2054,13 @@ impl Objects {
Some(text)
}
fn apply_table_styles(&self, d: &mut Drawing) {
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);
}
// 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 };
@@ -2118,9 +2070,6 @@ 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
@@ -2141,61 +2090,6 @@ 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;
@@ -2219,14 +2113,50 @@ 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;
}
self.apply_mleader_styles(d, 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),
}
}
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);
}
}
self.apply_groups(d);
// 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),
}
}
// Standard associativity, for dimensions without CADCraft's exact links.
for tags in &self.dimassocs {
dimassoc(d, tags);
+84 -110
View File
@@ -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::{DPrim, Kind, Sheet, clip, paper};
use cadcraft_render::{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 = &p.list;
let (list, map) = (&p.list, &p.map);
let texts = fonts::texts(list);
let white = Rgb(255, 255, 255);
let mut batch: Option<(StrokeStyle, PathBuilder)> = None;
@@ -829,126 +829,100 @@ 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 { .. } => polyline(s, &mut batch, p, prim, raw, m),
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::Tris | Kind::Mask => {
flush(s, &mut batch);
// A wipeout's mask is paper white.
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),
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);
}
}
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) {
+42 -31
View File
@@ -235,6 +235,46 @@ pub struct Op {
pub names: Vec<String>,
}
/// The index just past the string literal opening at `b[i]`, honouring escapes and nesting.
fn skip_string(b: &[char], mut i: usize) -> usize {
let mut depth = 0;
while i < b.len() {
match b[i] {
'\\' => i += 1,
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
i + 1
}
/// The index just past the inline dictionary opening at `b[i]` (`<<`).
fn skip_dict(b: &[char], mut i: usize) -> usize {
let mut depth = 0;
while i < b.len() {
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
depth += 1;
i += 1;
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
depth -= 1;
i += 1;
if depth == 0 {
i += 1;
break;
}
}
i += 1;
}
i
}
/// The operators of content text in order, each with its numeric operands (other operands
/// are skipped).
pub fn ops(content: &str) -> Vec<Op> {
@@ -253,39 +293,10 @@ pub fn ops(content: &str) -> Vec<Op> {
i += 1;
}
} else if c == '(' {
let mut depth = 0;
while i < b.len() {
match b[i] {
'\\' => i += 1,
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
i += 1;
i = skip_string(&b, i);
} else if c == '<' && b.get(i + 1) == Some(&'<') {
// An inline dictionary (marked-content properties): skip to its end.
let mut depth = 0;
while i < b.len() {
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
depth += 1;
i += 1;
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
depth -= 1;
i += 1;
if depth == 0 {
i += 1;
break;
}
}
i += 1;
}
i = skip_dict(&b, i);
} else if c == '<' {
while i < b.len() && b[i] != '>' {
i += 1;