forked from 1b-agent/opencadstudio-upstream
Merge pull request #852 from ramox81/codex/complete-hatch-workflow
This commit is contained in:
commit
bd5f9436ea
18 files changed
+1537
-481
No files matched your search
+122
-83
@@ -1,41 +1,33 @@
|
||||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
||||
use super::*;
|
||||
|
||||
/// Convert a HATCH's own resolved pattern line (world-unit `offset` = step to
|
||||
/// the next parallel line, plus a base angle) into a render `PatFamily` whose
|
||||
/// geometry is already final — the HatchModel that carries it uses scale 1 and
|
||||
/// angle_offset 0 (see `prebaked` in `hatch_model_from_dxf`). The world-space
|
||||
/// offset is rotated into the line's local frame so `pattern_segments` and the
|
||||
/// GPU shader, which rotate `(dx, dy)` back out by the family angle, reproduce
|
||||
/// the exact stored step. `x0/y0` are filled in by the caller (from the stored
|
||||
/// `base_point`, relative to `world_origin`) once the boundary anchor is known;
|
||||
/// they set the pattern origin, observable for dashed / offset patterns.
|
||||
/// Order a hatch boundary path's sampled edges into one tip-to-tail loop.
|
||||
///
|
||||
/// Real files do not store boundary edges as a sequential walk: associative
|
||||
/// hatches list them in boundary-source-entity order, with arbitrary
|
||||
/// direction — the next edge in the list may attach to either end of the
|
||||
/// chain built so far, or belong to the far side of the loop entirely.
|
||||
/// Concatenating them verbatim draws a self-crossing "bowtie" outline and
|
||||
/// flips the even-odd fill over the wrong region.
|
||||
///
|
||||
/// Greedy nearest-endpoint assembly: keep the chain open at both ends and, at
|
||||
/// each step, attach the unused edge whose endpoint lies closest to either
|
||||
/// end (reversing / prepending as needed). Distance comparison, no tolerance:
|
||||
/// a correctly-ordered file matches at distance 0 and reproduces exactly.
|
||||
fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
/// Order sampled boundary edges into one tip-to-tail loop.
|
||||
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
chain_path_edges_with_directions(polys).0
|
||||
}
|
||||
|
||||
pub(super) fn chain_path_edges_with_directions(
|
||||
polys: Vec<Vec<[f64; 2]>>,
|
||||
) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||
let d2 = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2);
|
||||
polys.retain(|p| !p.is_empty());
|
||||
let mut directions = vec![0.0; polys.len()];
|
||||
let mut polys: Vec<_> = polys
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, points)| !points.is_empty())
|
||||
.collect();
|
||||
if polys.is_empty() {
|
||||
return Vec::new();
|
||||
return (Vec::new(), directions);
|
||||
}
|
||||
let mut chain: std::collections::VecDeque<[f64; 2]> = polys.swap_remove(0).into();
|
||||
let (first_index, first) = polys.swap_remove(0);
|
||||
directions[first_index] = 1.0;
|
||||
let mut chain: std::collections::VecDeque<[f64; 2]> = first.into();
|
||||
while !polys.is_empty() {
|
||||
let head = *chain.front().unwrap();
|
||||
let tail = *chain.back().unwrap();
|
||||
// (distance, index, reverse-points, attach-at-front)
|
||||
let mut best = (f64::MAX, 0usize, false, false);
|
||||
for (i, p) in polys.iter().enumerate() {
|
||||
for (i, (_, p)) in polys.iter().enumerate() {
|
||||
let s = p[0];
|
||||
let e = *p.last().unwrap();
|
||||
for c in [
|
||||
@@ -50,7 +42,8 @@ fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
}
|
||||
}
|
||||
let (_, idx, rev, at_front) = best;
|
||||
let mut p = polys.swap_remove(idx);
|
||||
let (original_index, mut p) = polys.swap_remove(idx);
|
||||
directions[original_index] = if rev { -1.0 } else { 1.0 };
|
||||
if rev {
|
||||
p.reverse();
|
||||
}
|
||||
@@ -71,7 +64,7 @@ fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
chain.extend(it);
|
||||
}
|
||||
}
|
||||
chain.into()
|
||||
(chain.into(), directions)
|
||||
}
|
||||
|
||||
fn family_from_stored_line(
|
||||
@@ -1533,6 +1526,8 @@ impl Scene {
|
||||
fill_plane_boundary,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color,
|
||||
@@ -1684,6 +1679,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
let mut rings = Vec::new();
|
||||
let mut local_rings = Vec::new();
|
||||
let mut ring_sources = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
@@ -1698,26 +1694,27 @@ impl Scene {
|
||||
for edge in &path.edges {
|
||||
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
||||
edge_polys.push(
|
||||
curve
|
||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||
.into_iter()
|
||||
.map(|point| to_xy(point[0], point[1]))
|
||||
.collect(),
|
||||
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut ring = chain_path_edges(edge_polys);
|
||||
if ring.is_empty() {
|
||||
let mut local_ring = chain_path_edges(edge_polys);
|
||||
if local_ring.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if ring.len() >= 3 {
|
||||
let first = ring[0];
|
||||
let last = *ring.last().unwrap();
|
||||
if local_ring.len() >= 3 {
|
||||
let first = local_ring[0];
|
||||
let last = *local_ring.last().unwrap();
|
||||
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
||||
ring.push(first);
|
||||
local_ring.push(first);
|
||||
}
|
||||
}
|
||||
let ring = local_ring
|
||||
.iter()
|
||||
.map(|point| to_xy(point[0], point[1]))
|
||||
.collect();
|
||||
rings.push(ring);
|
||||
local_rings.push(local_ring);
|
||||
ring_sources.push(path.boundary_handles.clone());
|
||||
}
|
||||
|
||||
@@ -1727,9 +1724,15 @@ impl Scene {
|
||||
|
||||
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||
let mut boundary = Vec::new();
|
||||
let mut local_boundary = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
let mut boundary_sources = Vec::new();
|
||||
for ((ring, sources), depth) in rings.into_iter().zip(ring_sources).zip(depths) {
|
||||
for (((ring, local_ring), sources), depth) in rings
|
||||
.into_iter()
|
||||
.zip(local_rings)
|
||||
.zip(ring_sources)
|
||||
.zip(depths)
|
||||
{
|
||||
let keep = match dxf.style {
|
||||
acadrust::entities::HatchStyleType::Normal => true,
|
||||
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
||||
@@ -1740,8 +1743,14 @@ impl Scene {
|
||||
}
|
||||
if !boundary.is_empty() {
|
||||
boundary.push([f64::NAN, f64::NAN]);
|
||||
local_boundary.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
boundary.extend(ring);
|
||||
local_boundary.extend(
|
||||
local_ring
|
||||
.into_iter()
|
||||
.map(|[x, y]| [x as f32, y as f32]),
|
||||
);
|
||||
boundary_exterior.push(depth == 0);
|
||||
boundary_sources.push(sources);
|
||||
}
|
||||
@@ -1941,14 +1950,21 @@ impl Scene {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation);
|
||||
Some(HatchModel {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
fill_plane: Some(model::hatch_model::FillPlane {
|
||||
origin: storage.origin,
|
||||
x_axis: storage.x_axis,
|
||||
y_axis: storage.y_axis,
|
||||
}),
|
||||
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
boundary_paths: Some(std::sync::Arc::new(dxf.paths.clone())),
|
||||
style: dxf.style,
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
@@ -2267,6 +2283,8 @@ impl Scene {
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color,
|
||||
@@ -2286,14 +2304,24 @@ impl Scene {
|
||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||
) -> Handle {
|
||||
let mut dxf = DxfHatch::new();
|
||||
dxf.style = model.style;
|
||||
if let Some(plane) = model.fill_plane {
|
||||
let x = glam::DVec3::from_array(plane.x_axis);
|
||||
let y = glam::DVec3::from_array(plane.y_axis);
|
||||
let normal = x.cross(y).normalize_or(glam::DVec3::Z);
|
||||
dxf.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
dxf.elevation = glam::DVec3::from_array(plane.origin).dot(normal);
|
||||
}
|
||||
dxf.is_solid = matches!(
|
||||
model.pattern,
|
||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
// Prefer exact command geometry; otherwise reconstruct every ring from
|
||||
// the render offsets without dropping its separators.
|
||||
// Build one DXF path per NaN-separated ring and retain each outer/hole role.
|
||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||
// Persist analytic command geometry when available.
|
||||
if let Some(paths) = model.boundary_paths.as_deref() {
|
||||
dxf.paths = paths.clone();
|
||||
} else {
|
||||
// Otherwise reconstruct every ring from render offsets.
|
||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||
let [wx, wy] = model.world_origin;
|
||||
model
|
||||
.boundary
|
||||
@@ -2306,18 +2334,18 @@ impl Scene {
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let wcs = model
|
||||
.boundary_wcs
|
||||
.as_deref()
|
||||
.map(|points| points.as_slice())
|
||||
.unwrap_or(reconstructed_wcs.as_slice());
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut ring_index = 0usize;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let wcs = model
|
||||
.boundary_wcs
|
||||
.as_deref()
|
||||
.map(|points| points.as_slice())
|
||||
.unwrap_or(reconstructed_wcs.as_slice());
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut ring_index = 0usize;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let handles: Vec<_> = model
|
||||
@@ -2345,31 +2373,32 @@ impl Scene {
|
||||
}
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
for &[x, y] in wcs {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
first = false;
|
||||
if !ring.is_empty() {
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
ring_index += 1;
|
||||
};
|
||||
for &[x, y] in wcs {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
first = false;
|
||||
if !ring.is_empty() {
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
ring_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
}
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
dxf.is_associative = dxf
|
||||
.paths
|
||||
.iter()
|
||||
@@ -2379,6 +2408,16 @@ impl Scene {
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let pattern_origin = model
|
||||
.fill_plane_boundary
|
||||
.as_deref()
|
||||
.and_then(|points| {
|
||||
points
|
||||
.iter()
|
||||
.find(|point| point[0].is_finite() && point[1].is_finite())
|
||||
})
|
||||
.map(|point| [point[0] as f64, point[1] as f64])
|
||||
.unwrap_or(model.world_origin);
|
||||
if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern {
|
||||
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
|
||||
let rotation = model.angle_offset as f64;
|
||||
@@ -2396,10 +2435,10 @@ impl Scene {
|
||||
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
||||
angle,
|
||||
base_point: Vector2::new(
|
||||
model.world_origin[0]
|
||||
pattern_origin[0]
|
||||
+ base_x * rotation_cos
|
||||
- base_y * rotation_sin,
|
||||
model.world_origin[1]
|
||||
pattern_origin[1]
|
||||
+ base_x * rotation_sin
|
||||
+ base_y * rotation_cos,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user