mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 12:00:50 -06:00
HATCH and LWPOLYLINE in a tilted plane are read as seen from above (#1886)
Their boundary and vertices are in the object's plane (OCS). Only a mirrored plane (0,0,-1) was handled, by flipping x; a tilted plane whose normal points down was taken for a mirror too, and one pointing up kept its plane coordinates, so each face landed turned about the origin by its own angle. A tilted one is now mapped to the WCS and drawn as seen from above at its mean height: bulged edges are flattened (they are elliptical there), and a hatch's pattern direction and origin follow. Its extrusion becomes Z, since its points are plan coordinates now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5caaf73e3b
commit
d19df49fcf
@@ -1,4 +1,4 @@
|
||||
use super::{Color, Common, Lineweight, Rgb, Tag, Transparency, Vec2, Vec3};
|
||||
use super::{Color, Common, Lineweight, PolyVertex, Rgb, Tag, Transparency, Vec2, Vec3};
|
||||
|
||||
/// Value lookup helpers over an entity's tags.
|
||||
pub(super) struct T<'a>(pub(super) &'a [Tag]);
|
||||
@@ -105,3 +105,33 @@ pub(super) fn ocs(c: &Common, p: Vec3) -> Vec3 {
|
||||
pub(super) fn ocs_n(n: Vec3, p: Vec3) -> Vec3 {
|
||||
if (n.z - 1.0).abs() < 1e-12 && n.x.abs() < 1e-12 && n.y.abs() < 1e-12 { p } else { cadcraft_geom::Mat4::ocs(n).apply(p) }
|
||||
}
|
||||
|
||||
/// Whether an extrusion tilts an object's plane off the XY plane (neither the Z axis nor the Z
|
||||
/// axis turned over, as MIRROR leaves it, allowing for noise in x and y like INSERT does).
|
||||
pub(super) fn tilted(n: Vec3) -> bool {
|
||||
n.is_finite() && n.x.hypot(n.y) > 1e-3 * n.z.abs()
|
||||
}
|
||||
|
||||
/// A run of OCS vertices at `elevation` in the plane of `n` as seen from above in the WCS. A
|
||||
/// tilted arc is an elliptical arc there, so bulged edges are flattened (within a thousandth of
|
||||
/// the run's size); straight runs keep their vertices. Also gives the mean WCS Z, the elevation
|
||||
/// the flattened run takes.
|
||||
pub(super) fn plan_of_tilted(n: Vec3, elevation: f64, vertices: &[PolyVertex], closed: bool) -> (Vec<PolyVertex>, f64) {
|
||||
let m = cadcraft_geom::Mat4::ocs(n);
|
||||
let pts: Vec<Vec2> = if vertices.iter().all(|v| v.bulge == 0.0) {
|
||||
vertices.iter().map(|v| v.p).collect()
|
||||
} else {
|
||||
let pl = cadcraft_geom::Polyline { vertices: vertices.to_vec(), closed };
|
||||
let b = pl.bounds();
|
||||
let size = (b.max - b.min).len();
|
||||
let tol = if size.is_finite() && size > 0.0 { size * 1e-3 } else { 1e-6 };
|
||||
let mut pts = pl.tessellate(tol);
|
||||
if closed && pts.len() > 1 && pts.first() == pts.last() {
|
||||
pts.pop();
|
||||
}
|
||||
pts
|
||||
};
|
||||
let world: Vec<Vec3> = pts.iter().map(|p| m.apply(Vec3::new(p.x, p.y, elevation))).collect();
|
||||
let z = if world.is_empty() { elevation } else { world.iter().map(|p| p.z).sum::<f64>() / world.len() as f64 };
|
||||
(world.iter().map(|p| PolyVertex::new(p.xy())).collect(), z)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::common::{T, common, ocs};
|
||||
use super::common::{T, common, ocs, plan_of_tilted, tilted};
|
||||
use super::hatch::hatch;
|
||||
use super::text::{attdef, attrib_modes, attrib_props, mtext_from, multiline_attrib, split_embedded, text_from};
|
||||
use super::{
|
||||
@@ -81,13 +81,17 @@ fn lwpolyline(tags: &[Tag], c: &Common, t: &T) -> Option<EntityKind> {
|
||||
}
|
||||
verts.extend(cur);
|
||||
let flags = t.i(70).unwrap_or(0);
|
||||
if c.extrusion.z < 0.0 {
|
||||
let mut elevation = t.fd(38, 0.0);
|
||||
if tilted(c.extrusion) {
|
||||
// In a tilted plane: drawn as seen from above, at the vertices' mean height.
|
||||
(verts, elevation) = plan_of_tilted(c.extrusion, elevation, &verts, flags & 1 != 0);
|
||||
} else 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 };
|
||||
let pl = LwPolyline { vertices: verts, closed: flags & 1 != 0, const_width: t.fd(43, 0.0), elevation, plinegen: flags & 128 != 0 };
|
||||
// A region CADCraft wrote as polylines (`dxf_region`).
|
||||
crate::dxf_region::from_polyline(tags, pl)
|
||||
}
|
||||
@@ -121,14 +125,21 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef
|
||||
return None;
|
||||
}
|
||||
let t = T(tags);
|
||||
let c = common(&t);
|
||||
let mut c = common(&t);
|
||||
let k = match kind {
|
||||
"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)?,
|
||||
"LWPOLYLINE" => {
|
||||
let k = lwpolyline(tags, &c, &t)?;
|
||||
if tilted(c.extrusion) {
|
||||
// Its vertices are now WCS, seen from above (`lwpolyline`).
|
||||
c.extrusion = Vec3::Z;
|
||||
}
|
||||
k
|
||||
}
|
||||
"SPLINE" => EntityKind::Spline(spline(&t)),
|
||||
"HELIX" => EntityKind::Helix(helix(tags)),
|
||||
"RAY" => EntityKind::Ray(RayLine { base: t.p(10), dir: t.p(11) }),
|
||||
@@ -227,7 +238,30 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef
|
||||
"3DFACE" => EntityKind::Face3d(Face3d { corners: [t.p(10), t.p(11), t.p(12), t.p(13)], hidden_edges: t.i(70).unwrap_or(0) as u8 }),
|
||||
"HATCH" => {
|
||||
let mut k = hatch(tags, &t)?;
|
||||
if c.extrusion.z < 0.0
|
||||
if tilted(c.extrusion)
|
||||
&& let EntityKind::Hatch(h) = &mut k
|
||||
{
|
||||
// In a tilted plane (a terrain face, a slope): the boundary as seen from above,
|
||||
// at its mean height, and the pattern direction and origin projected the same way.
|
||||
let n = c.extrusion;
|
||||
let m = cadcraft_geom::Mat4::ocs(n);
|
||||
let (mut sum, mut count) = (0.0, 0usize);
|
||||
for l in &mut h.loops {
|
||||
let (v, z) = plan_of_tilted(n, h.elevation, &l.vertices, true);
|
||||
sum += z * v.len() as f64;
|
||||
count += v.len();
|
||||
l.vertices = v;
|
||||
}
|
||||
let dir = m.apply(Vec3::new(h.angle.cos(), h.angle.sin(), 0.0)).xy();
|
||||
if dir.len() > 1e-9 {
|
||||
h.angle = cadcraft_geom::norm_angle(dir.y.atan2(dir.x));
|
||||
}
|
||||
h.origin = m.apply(Vec3::new(h.origin.x, h.origin.y, h.elevation)).xy();
|
||||
if count > 0 {
|
||||
h.elevation = sum / count as f64;
|
||||
}
|
||||
c.extrusion = Vec3::Z;
|
||||
} else if c.extrusion.z < 0.0
|
||||
&& let EntityKind::Hatch(h) = &mut k
|
||||
{
|
||||
// Boundary in the mirrored OCS, like LWPOLYLINE; the pattern direction mirrors too.
|
||||
|
||||
@@ -2109,6 +2109,47 @@ fn mirrored_hatch_extrusion_flips_boundary() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #1886: a HATCH in a tilted plane (a terrain face) is drawn as seen from above, not turned
|
||||
/// about the origin. With extrusion (0, 0.5, -√3/2) the OCS axes are x = (-1,0,0) and
|
||||
/// y = (0, √3/2, 1/2): the square (1,0)..(3,2) lies over (-3,0)..(-1,√3), from z 0 up to z 1.
|
||||
/// Before, the negative Z counted as a plain mirror and only x was flipped.
|
||||
#[test]
|
||||
fn tilted_hatch_is_drawn_as_seen_from_above() {
|
||||
let text = "0\nSECTION\n2\nENTITIES\n0\nHATCH\n8\n0\n10\n0\n20\n0\n30\n0\n210\n0\n220\n0.5\n230\n-0.8660254037844386\n2\nSOLID\n70\n1\n71\n0\n91\n1\n\
|
||||
92\n2\n72\n0\n73\n1\n93\n4\n10\n1\n20\n0\n10\n3\n20\n0\n10\n3\n20\n2\n10\n1\n20\n2\n97\n0\n75\n0\n76\n1\n0\nENDSEC\n0\nEOF\n";
|
||||
let d = read(text.as_bytes(), "h.dxf").unwrap();
|
||||
let b = d.extents(&Space::Model);
|
||||
for (got, want) in [(b.min.x, -3.0), (b.min.y, 0.0), (b.max.x, -1.0), (b.max.y, 3f64.sqrt())] {
|
||||
assert!((got - want).abs() < 1e-9, "extents {b:?}");
|
||||
}
|
||||
let e = d.model.iter().next().unwrap();
|
||||
assert_eq!(e.common.extrusion, Vec3::Z, "its points are plan coordinates now");
|
||||
let EntityKind::Hatch(h) = &e.kind else { panic!("not a hatch") };
|
||||
assert!((h.elevation - 0.5).abs() < 1e-9, "the mean height of its corners: {}", h.elevation);
|
||||
}
|
||||
|
||||
/// Issue #1886: a LWPOLYLINE circle (two half-circle bulges) in the same tilted plane is the
|
||||
/// ellipse it looks like from above, flattened; a plain one keeps its bulges.
|
||||
#[test]
|
||||
fn tilted_lwpolyline_is_drawn_as_seen_from_above() {
|
||||
let circle = |n: &str| {
|
||||
format!("0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n8\n0\n90\n2\n70\n1\n{n}10\n1\n20\n0\n42\n1\n10\n-1\n20\n0\n42\n1\n0\nENDSEC\n0\nEOF\n")
|
||||
};
|
||||
let d = read(circle("210\n0\n220\n0.5\n230\n-0.8660254037844386\n").as_bytes(), "p.dxf").unwrap();
|
||||
let b = d.extents(&Space::Model);
|
||||
let ry = 3f64.sqrt() / 2.0;
|
||||
for (got, want) in [(b.min.x, -1.0), (b.min.y, -ry), (b.max.x, 1.0), (b.max.y, ry)] {
|
||||
assert!((got - want).abs() < 5e-3, "extents {b:?}");
|
||||
}
|
||||
let e = d.model.iter().next().unwrap();
|
||||
assert_eq!(e.common.extrusion, Vec3::Z);
|
||||
let p = first(&d, |k| if let EntityKind::LwPolyline(p) = k { Some(p.clone()) } else { None });
|
||||
assert!(p.vertices.len() > 8 && p.vertices.iter().all(|v| v.bulge == 0.0), "{} vertices", p.vertices.len());
|
||||
let plain = read(circle("").as_bytes(), "p.dxf").unwrap();
|
||||
let p = first(&plain, |k| if let EntityKind::LwPolyline(p) = k { Some(p.clone()) } else { None });
|
||||
assert_eq!(p.vertices.iter().map(|v| v.bulge).collect::<Vec<_>>(), vec![1.0, 1.0]);
|
||||
}
|
||||
|
||||
/// The model extents of a one-loop solid HATCH whose edge boundary is `edges` (group 72 onwards).
|
||||
fn hatch_edge_extents(edges: &str) -> Bounds2 {
|
||||
let n = edges.matches("72\n").count();
|
||||
|
||||
@@ -73,7 +73,7 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG
|
||||
|
||||
| Content | Read | Write | Notes |
|
||||
|---|---|---|---|
|
||||
| LINE, POINT, CIRCLE, ARC, ELLIPSE, LWPOLYLINE (widths, bulges, elevation), SPLINE, RAY, XLINE | yes | yes | round-trip tests |
|
||||
| LINE, POINT, CIRCLE, ARC, ELLIPSE, LWPOLYLINE (widths, bulges, elevation), SPLINE, RAY, XLINE | yes | yes; a LWPOLYLINE in a tilted plane is read as seen from above (arcs flattened, #1886) | round-trip tests |
|
||||
| POLYLINE 2D/3D | yes | yes (as LWPOLYLINE / 3D POLYLINE) | polyface and polygon meshes flattened: lossy |
|
||||
| TEXT, MTEXT, ATTDEF, INSERT + ATTRIB | yes; multiline attributes (embedded MTEXT) read and drawn | yes; multiline attributes written as embedded MTEXT (DXF 2007 and later; older versions: one line); changed attribute values refresh justified baselines, including small width changes | ATTDEF prompt kept (#137); unchanged values retain imported-font placement; a justified TEXT's insertion point (group 10, which AutoCAD takes from a DWG as it is) is computed as AutoCAD computes it: Middle (M) centred on the string's own extents, bottom justifications at a TrueType face's descent (#2229) |
|
||||
| DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written for every associated kind — linear/aligned, angular, radial/diametric, ordinate and arc length — and read back without CADCraft's xdata (#1651) |
|
||||
@@ -81,7 +81,7 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG
|
||||
| DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks; their text keeps the diameter sign as `%%c`, so SHX styles plot it (#1645) | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written |
|
||||
| LEADER | yes | yes | |
|
||||
| MULTILEADER | yes (#459) | yes, native MULTILEADER with MLEADERSTYLE (#459) | several landings per multileader are folded into one; per-object overrides and annotative scale contexts not kept |
|
||||
| HATCH pattern / solid | yes | yes; associative hatches keep their boundary objects in blocks too, and the boundary carries the hatch reactor (stretched dynamic blocks drag the fill in AutoCAD) | |
|
||||
| HATCH pattern / solid | yes | yes; associative hatches keep their boundary objects in blocks too, and the boundary carries the hatch reactor (stretched dynamic blocks drag the fill in AutoCAD); a hatch in a tilted plane is read as seen from above (#1886) | |
|
||||
| HATCH gradient | yes | yes (#162, fixes #80) | |
|
||||
| SOLID, TRACE, 3DFACE | yes | yes | |
|
||||
| ACAD_TABLE | yes | yes (entity + block) | |
|
||||
@@ -140,6 +140,7 @@ our output in AutoCAD black-box.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | HATCH and LWPOLYLINE in a tilted plane (extrusion off the Z axis) are read as seen from above at their mean height, instead of keeping plane coordinates that turned them about the origin (#1886) |
|
||||
| 2026-10-29 | minor | R2000 DXF: CLASS records written without group 91 (instance count), which came with the 2004 format; AutoCAD LT 2026 stopped responding on every R2000 DXF CADCraft saved (checked black-box in AutoCAD LT 2026) |
|
||||
| 2026-10-26 | minor | CC-046: ATTEDIT and -ATTEDIT value changes invalidate derived justified baselines before DXF/DWG export; preserve alignment anchors, unchanged values and Aligned/Fit endpoints. Review follow-up: baseline recomputation preserves insertion elevation independently of alignment elevation. The shared writer correction also applies to justified TEXT. Independent of attribute editor UI; reverting the follow-up restores elevation loss on recomputation, while reverting the value setter restores stale XY baselines. |
|
||||
| 2026-10-25 | minor | LAYOUT SAveas (`layout.saveas`) writes a layout as a drawing template: its page setup and paper space objects, no model space objects or other layouts, and only the named objects they use |
|
||||
|
||||
Reference in New Issue
Block a user