fix: correct hatch geometry workflow
This commit is contained in:
parent
f83037271c
commit
4bc02b6b4a
11 changed files with 401 additions and 255 deletions
|
|
@ -1375,6 +1375,13 @@ impl OpenCADStudio {
|
|||
.collect::<Vec<_>>();
|
||||
sources.push(handles);
|
||||
}
|
||||
if let Some(paths) = hatch.boundary_paths.as_mut() {
|
||||
for (path, handles) in std::sync::Arc::make_mut(paths).iter_mut().zip(&sources)
|
||||
{
|
||||
path.boundary_handles = handles.clone();
|
||||
path.flags.set_external(!handles.is_empty());
|
||||
}
|
||||
}
|
||||
hatch.boundary_sources = Some(std::sync::Arc::new(sources));
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
let new_handle =
|
||||
|
|
@ -1619,10 +1626,7 @@ impl OpenCADStudio {
|
|||
.into_iter()
|
||||
.map(|e| self.tabs[i].scene.add_entity(e))
|
||||
.collect();
|
||||
// A replaced dimension carries edited geometry/text but still
|
||||
// names its old *D block; drop that stale block so the next save
|
||||
// re-bakes it — otherwise BricsCAD/ODA draw the pre-edit
|
||||
// graphics while OCS shows the edit. (#181)
|
||||
// Rebuild replaced dimensions from edited data.
|
||||
for &nh in &new_handles {
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(nh),
|
||||
|
|
@ -1932,11 +1936,6 @@ impl OpenCADStudio {
|
|||
}
|
||||
// The command stays active after each apply so more targets
|
||||
// can keep being picked; Enter / Esc ends it (#362).
|
||||
// Special (type-specific) properties travel like AutoCAD's
|
||||
// Special Properties: each is captured from the source when it
|
||||
// carries it and applied only to destinations that support it
|
||||
// (#281). Text formatting crosses TEXT ↔ MTEXT (#361); the dim
|
||||
// style crosses Dimension / Leader / Tolerance.
|
||||
let src_clone = self.tabs[i].scene.document.get_entity(src).cloned();
|
||||
let src_common = src_clone.as_ref().map(|e| e.common().clone());
|
||||
let thickness = src_clone
|
||||
|
|
@ -3337,6 +3336,7 @@ impl OpenCADStudio {
|
|||
if disassociate {
|
||||
for path in &mut hatch.paths {
|
||||
path.boundary_handles.clear();
|
||||
path.flags.set_external(false);
|
||||
}
|
||||
hatch.is_associative = false;
|
||||
}
|
||||
|
|
@ -3378,21 +3378,19 @@ impl OpenCADStudio {
|
|||
.edit_hatch_boundary_handles(handle, &handles, false);
|
||||
}
|
||||
HatchEditOperation::RecreateBoundary => {
|
||||
let model = self.tabs[i].scene.hatches.get(&handle).cloned();
|
||||
if let Some(model) = model {
|
||||
let mut rings = vec![Vec::new()];
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
rings.last_mut().unwrap().push([
|
||||
model.world_origin[0] + x as f64,
|
||||
model.world_origin[1] + y as f64,
|
||||
]);
|
||||
} else if !rings.last().unwrap().is_empty() {
|
||||
rings.push(Vec::new());
|
||||
}
|
||||
}
|
||||
rings.retain(|ring| ring.len() >= 3);
|
||||
let entities = crate::scene::boundary_entities(&rings);
|
||||
let source = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||
if let Some(acadrust::EntityType::Hatch(source)) = source {
|
||||
let storage = crate::entities::curve::ocs_plane(
|
||||
source.normal,
|
||||
source.elevation,
|
||||
);
|
||||
let plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(storage.origin),
|
||||
glam::DVec3::from_array(storage.x_axis),
|
||||
glam::DVec3::from_array(storage.y_axis),
|
||||
);
|
||||
let rings = crate::scene::hatch_boundary_rings(&source);
|
||||
let entities = crate::scene::boundary_entities(&rings, plane);
|
||||
let mut handles = Vec::new();
|
||||
for entity in entities {
|
||||
if let Some(boundary) = self.commit_entity_handle(entity) {
|
||||
|
|
@ -3406,6 +3404,7 @@ impl OpenCADStudio {
|
|||
hatch.paths.iter_mut().zip(handles.iter().copied())
|
||||
{
|
||||
path.boundary_handles = vec![boundary];
|
||||
path.flags.set_external(true);
|
||||
}
|
||||
hatch.is_associative = !handles.is_empty();
|
||||
}
|
||||
|
|
@ -3418,18 +3417,22 @@ impl OpenCADStudio {
|
|||
HatchEditOperation::Separate => {
|
||||
let source = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) = source {
|
||||
for path in hatch.paths.iter().cloned() {
|
||||
let mut separated = hatch.clone();
|
||||
separated.common.handle = acadrust::Handle::NULL;
|
||||
separated.paths = vec![path];
|
||||
separated.is_associative = separated.paths.iter().any(|path| {
|
||||
!path.boundary_handles.is_empty()
|
||||
});
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.add_entity(acadrust::EntityType::Hatch(separated));
|
||||
let groups = crate::scene::separated_hatch_path_groups(&hatch);
|
||||
if groups.len() > 1 {
|
||||
for paths in groups {
|
||||
let mut separated = hatch.clone();
|
||||
separated.common.handle = acadrust::Handle::NULL;
|
||||
separated.paths = paths;
|
||||
separated.is_associative = separated
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.add_entity(acadrust::EntityType::Hatch(separated));
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
}
|
||||
}
|
||||
HatchEditOperation::DrawOrderFront
|
||||
|
|
|
|||
|
|
@ -635,9 +635,25 @@ impl OpenCADStudio {
|
|||
|
||||
"HATCH" => {
|
||||
use crate::modules::draw::draw::hatch::HatchCommand;
|
||||
let working_plane = if self.tabs[i].editing_model_space() {
|
||||
self.tabs[i].ucs_xform().working_plane()
|
||||
} else {
|
||||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
let normal = working_plane.z.normalize_or(glam::DVec3::Z);
|
||||
let elevation = working_plane.origin.dot(normal);
|
||||
let storage = crate::entities::curve::ocs_plane(
|
||||
acadrust::types::Vector3::new(normal.x, normal.y, normal.z),
|
||||
elevation,
|
||||
);
|
||||
let plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(storage.origin),
|
||||
glam::DVec3::from_array(storage.x_axis),
|
||||
glam::DVec3::from_array(storage.y_axis),
|
||||
);
|
||||
let boundary_sources = self.tabs[i]
|
||||
.scene
|
||||
.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6);
|
||||
.boundary_sources_on_plane(plane, 1.0e-6);
|
||||
let outlines = crate::scene::boundary_faces(&boundary_sources, 1.0e-6);
|
||||
let selected = self.tabs[i]
|
||||
.scene
|
||||
|
|
@ -652,8 +668,13 @@ impl OpenCADStudio {
|
|||
let common = self.tabs[i].scene.document.get_entity(*handle)?.common();
|
||||
Some((model, common.color.clone(), common.transparency))
|
||||
});
|
||||
let new_cmd =
|
||||
HatchCommand::new(outlines, boundary_sources, selected, inherited);
|
||||
let new_cmd = HatchCommand::new(
|
||||
outlines,
|
||||
boundary_sources,
|
||||
selected,
|
||||
inherited,
|
||||
plane,
|
||||
);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
self.refresh_area_preview(i);
|
||||
|
|
@ -666,21 +687,25 @@ impl OpenCADStudio {
|
|||
if sel.len() == 1 {
|
||||
let (h, _) = sel[0];
|
||||
if let Some(model) = self.tabs[i].scene.hatches.get(&h).cloned() {
|
||||
let annotative = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h)
|
||||
.is_some_and(|entity| {
|
||||
let entity = self.tabs[i].scene.document.get_entity(h);
|
||||
let annotative = entity.is_some_and(|entity| {
|
||||
crate::scene::annotative::is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
entity,
|
||||
)
|
||||
});
|
||||
let (scale, angle) = match entity {
|
||||
Some(acadrust::EntityType::Hatch(hatch)) => (
|
||||
hatch.pattern_scale as f32,
|
||||
hatch.pattern_angle.to_degrees() as f32,
|
||||
),
|
||||
_ => (model.scale, model.angle_offset.to_degrees()),
|
||||
};
|
||||
let cmd = HatcheditCommand::with_handle(
|
||||
h,
|
||||
model.name.clone(),
|
||||
model.scale,
|
||||
model.angle_offset.to_degrees(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
|
|
|
|||
|
|
@ -2057,9 +2057,7 @@ pub(super) fn aggregate_sections(
|
|||
for sections in all_sections {
|
||||
result = merge_sections(&result, §ions);
|
||||
}
|
||||
// Unlike ordinary common properties, cumulative area is an aggregate by
|
||||
// definition. Preserve the individual Area row's "varies" state while
|
||||
// summing every selected hatch's actual filled area (holes subtracted).
|
||||
// Sum the filled area while individual Area rows may still vary.
|
||||
if selected.len() > 1
|
||||
&& selected
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -3490,23 +3490,27 @@ impl OpenCADStudio {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(model) = self.tabs[i].scene.hatches.get(&handle).cloned() {
|
||||
let annotative = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| {
|
||||
let entity = self.tabs[i].scene.document.get_entity(handle);
|
||||
let annotative = entity.is_some_and(|entity| {
|
||||
crate::scene::annotative::is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
entity,
|
||||
)
|
||||
});
|
||||
let (scale, angle) = match entity {
|
||||
Some(acadrust::EntityType::Hatch(hatch)) => (
|
||||
hatch.pattern_scale as f32,
|
||||
hatch.pattern_angle.to_degrees() as f32,
|
||||
),
|
||||
_ => (model.scale, model.angle_offset.to_degrees()),
|
||||
};
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
||||
let cmd: Box<dyn CadCommand> = Box::new(HatcheditCommand::with_handle(
|
||||
handle,
|
||||
model.name.clone(),
|
||||
model.scale,
|
||||
model.angle_offset.to_degrees(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
));
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
|
|
|
|||
|
|
@ -14,42 +14,19 @@ use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Pr
|
|||
use crate::scene::convert::tess_util::FallbackGeometry;
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
|
||||
/// The area the hatch's boundary paths enclose.
|
||||
///
|
||||
/// Summed edge by edge through the kernel, which measures what each edge
|
||||
/// actually encloses rather than what a polygon through some of its points
|
||||
/// would. The version this replaced pushed an arc's *centre* into the ring
|
||||
/// and a spline's control points — neither of which is on the boundary — so
|
||||
/// the number it produced was not the area of anything.
|
||||
///
|
||||
/// Outer paths and their holes both contribute; the sign of a loop says
|
||||
/// which it is, so the magnitude of the sum is the region's own area.
|
||||
/// The area enclosed by the hatch boundary paths.
|
||||
pub(crate) fn boundary_area(h: &Hatch) -> f64 {
|
||||
let mut path_areas = Vec::new();
|
||||
let mut rings = Vec::new();
|
||||
for path in &h.paths {
|
||||
let mut path_area = 0.0;
|
||||
let mut ends: Vec<[f64; 2]> = Vec::new();
|
||||
let mut ring = Vec::new();
|
||||
for edge in &path.edges {
|
||||
let Some(curve) = edge_curve(edge) else {
|
||||
continue;
|
||||
};
|
||||
path_area += curve.enclosed_area();
|
||||
ends.push(curve.point_at(0.0));
|
||||
ends.push(curve.point_at(1.0));
|
||||
let tessellated = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||
let skip = usize::from(!ring.is_empty());
|
||||
ring.extend(tessellated.into_iter().skip(skip));
|
||||
}
|
||||
// Edges are stored as separate pieces, so the chain has to be closed
|
||||
// by the chord from the last end back to the first — the same closing
|
||||
// an open polyline gets.
|
||||
if let (Some(first), Some(last)) = (ends.first(), ends.last()) {
|
||||
path_area += 0.5 * (last[0] * first[1] - first[0] * last[1]);
|
||||
let directions = crate::scene::hatch_path_directions(path);
|
||||
let curves = path.edges.iter().filter_map(edge_curve);
|
||||
for (curve, direction) in curves.zip(directions) {
|
||||
path_area += direction * curve.enclosed_area();
|
||||
}
|
||||
path_areas.push(path_area.abs());
|
||||
rings.push(ring);
|
||||
rings.push(crate::scene::hatch_path_ring(path).unwrap_or_default());
|
||||
}
|
||||
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||
path_areas
|
||||
|
|
@ -70,7 +47,7 @@ pub(crate) fn boundary_area(h: &Hatch) -> f64 {
|
|||
.abs()
|
||||
}
|
||||
|
||||
/// A hatch boundary edge as a kernel curve, in the hatch's own OCS.
|
||||
/// A hatch boundary edge as a kernel curve in the hatch OCS.
|
||||
pub(crate) fn edge_curve(edge: &BoundaryEdge) -> Option<KernelCurve> {
|
||||
Some(match edge {
|
||||
BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine {
|
||||
|
|
@ -483,9 +460,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||
|
||||
|
||||
// ── Hatch (pattern / solid) ────────────────────────────────────────────
|
||||
// Pattern-specific rows are conditional: scale belongs to catalog/custom
|
||||
// definitions, while spacing and double belong to user-defined hatches.
|
||||
// A solid fill does not expose inert pattern controls.
|
||||
// Show only controls used by the selected fill type.
|
||||
let type_row = if h.is_solid {
|
||||
ro(t!("Type").as_ref(), "fill_kind", t!("Solid").into_owned())
|
||||
} else {
|
||||
|
|
@ -538,9 +513,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||
pattern_props.push(double_row);
|
||||
} else {
|
||||
pattern_props.push(edit(t!("Scale").as_ref(), "pattern_scale", h.pattern_scale));
|
||||
// Project convention: these fields are relative offsets, therefore
|
||||
// they read zero after every committed move instead of leaking the
|
||||
// absolute base point of the first stored pattern line.
|
||||
// Origin edits are relative offsets.
|
||||
pattern_props.push(edit(t!("Origin X").as_ref(), "origin_x", 0.0));
|
||||
pattern_props.push(edit(t!("Origin Y").as_ref(), "origin_y", 0.0));
|
||||
if h.pattern.name.to_ascii_uppercase().starts_with("ISO") {
|
||||
|
|
@ -645,9 +618,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||
h.is_solid = false;
|
||||
match requested {
|
||||
HatchPatternType::UserDefined => {
|
||||
// User-defined geometry is derived from angle, spacing
|
||||
// and the Double flag; stale catalog lines would make
|
||||
// the renderer treat it as a prebaked definition.
|
||||
// Rebuild user-defined geometry from its parameters.
|
||||
h.pattern = acadrust::entities::HatchPattern::new("_USER");
|
||||
}
|
||||
HatchPatternType::Predefined => {
|
||||
|
|
@ -776,13 +747,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||
|
||||
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
||||
crate::scene::view::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
||||
// Delegate the mirror to acadrust's transform_hatch (via the Entity
|
||||
// trait): it flips the boundary-arc direction flags, re-mirrors the
|
||||
// stored angles and preserves the stored sweep — including the
|
||||
// wrap-encoded end angles above 2π that AutoCAD writes. The old
|
||||
// hand-rolled angle-swap here was only valid for ccw boundary arcs on
|
||||
// an axis-aligned mirror line and went stale the moment those
|
||||
// conventions were fixed upstream.
|
||||
// Keep boundary directions, angles, and sweeps consistent.
|
||||
let t = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
|
||||
acadrust::entities::Entity::apply_transform(entity, &t);
|
||||
});
|
||||
|
|
@ -832,9 +797,7 @@ impl Grippable for Hatch {
|
|||
id += 1;
|
||||
}
|
||||
}
|
||||
// Associative boundaries are edited through their source objects. A
|
||||
// hatch therefore exposes only its circular pattern control instead
|
||||
// of a second, conflicting set of boundary vertices.
|
||||
// Edit associative boundaries through their source objects.
|
||||
if self.is_associative {
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
// Hatch/Gradient/Boundary commands — OpenCADStudio Home > Draw > Hatch dropdown.
|
||||
//
|
||||
// Commands:
|
||||
// HATCH — ANSI31: 45° hatch lines (pick inside or type S for manual)
|
||||
// GRADIENT — Linear gradient fill (pick inside or type S for manual)
|
||||
// BOUNDARY — Traces the enclosing boundary as a closed LwPolyline
|
||||
//
|
||||
// Primary workflow (matches OpenCADStudio):
|
||||
// Click a point INSIDE a closed region → boundary auto-detected.
|
||||
// Type "S" to switch to manual vertex-picking mode (HATCH/GRADIENT only).
|
||||
// Hatch, gradient, and boundary commands.
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::IconKind;
|
||||
|
|
@ -15,7 +6,7 @@ use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
|||
use crate::scene::model::wire_model::WireModel;
|
||||
use acadrust::Handle;
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, ring_nesting_depths, signed_area, Curve, Line, Tolerance,
|
||||
bounded_faces, contains, ring_nesting_depths, signed_area, Circle, Curve, Line, Tolerance,
|
||||
};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
|
@ -110,17 +101,7 @@ fn polygon_contains_polygon(outer: &[[f64; 2]], inner: &[[f64; 2]]) -> bool {
|
|||
inner.iter().all(|&v| point_in_polygon(v, outer))
|
||||
}
|
||||
|
||||
/// Resolve the hatch boundary for a "pick inside" click.
|
||||
///
|
||||
/// The outer ring is the *smallest* outline containing the click point — the
|
||||
/// innermost region the point belongs to. Its holes are that ring's **direct
|
||||
/// children**: outlines nested one level inside it with no other outline in
|
||||
/// between. Deeper (grandchild) outlines belong to those children's own fills,
|
||||
/// so they are left out — otherwise even-odd rasterisation would flip the
|
||||
/// innermost island back on for 3+ nesting levels. The result is intuitive and
|
||||
/// draw-order independent:
|
||||
/// * click inside the innermost shape → hatch just that shape,
|
||||
/// * click in a gap → hatch that ring, with the next level in as holes.
|
||||
/// Resolve the innermost clicked ring and its direct holes.
|
||||
fn resolve_hatch_rings(
|
||||
outlines: &[Vec<[f64; 2]>],
|
||||
p: [f64; 2],
|
||||
|
|
@ -184,12 +165,7 @@ fn pack_rings(rings: &[Vec<[f64; 2]>]) -> (Vec<[f32; 2]>, [f64; 2], Vec<[f64; 2]
|
|||
(rel, origin, wcs)
|
||||
}
|
||||
|
||||
/// Split an absolute boundary into the `(f32 offsets, f64 origin)` pair that
|
||||
/// `HatchModel` expects: the origin anchors on the first vertex in full f64 so a
|
||||
/// typed coordinate (issue #311) and large/UTM positions keep their precision,
|
||||
/// and `add_hatch` reconstructs each WCS vertex as `origin + offset`. A zero
|
||||
/// origin with absolute f32 offsets — the previous command output — quantized
|
||||
/// typed points and mis-placed the fill at large coordinates.
|
||||
/// Store boundary points as precise-origin-relative offsets.
|
||||
fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64; 2]) {
|
||||
let pts: Vec<(f64, f64)> = pts.collect();
|
||||
let Some(&(ox, oy)) = pts.first() else {
|
||||
|
|
@ -228,6 +204,7 @@ pub struct HatchCommand {
|
|||
acadrust::types::Color,
|
||||
acadrust::types::Transparency,
|
||||
)>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl HatchCommand {
|
||||
|
|
@ -240,6 +217,7 @@ impl HatchCommand {
|
|||
acadrust::types::Color,
|
||||
acadrust::types::Transparency,
|
||||
)>,
|
||||
plane: WorkingPlane,
|
||||
) -> Self {
|
||||
let selected_objects: Vec<_> = selected_objects
|
||||
.into_iter()
|
||||
|
|
@ -273,6 +251,7 @@ impl HatchCommand {
|
|||
.map(|(model, _, _)| model.style)
|
||||
.unwrap_or(acadrust::entities::HatchStyleType::Normal),
|
||||
inherited,
|
||||
plane,
|
||||
};
|
||||
command.set_object_selection(selected_objects);
|
||||
command
|
||||
|
|
@ -332,7 +311,30 @@ impl HatchCommand {
|
|||
}
|
||||
|
||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
||||
let (rel, origin, wcs) = pack_rings(&rings);
|
||||
let world_rings: Vec<Vec<[f64; 2]>> = rings
|
||||
.iter()
|
||||
.map(|ring| {
|
||||
ring.iter()
|
||||
.map(|&[x, y]| {
|
||||
let point = self.plane.to_world(DVec3::new(x, y, 0.0));
|
||||
[point.x, point.y]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let (rel, origin, wcs) = pack_rings(&world_rings);
|
||||
let mut local_boundary = Vec::new();
|
||||
for (index, ring) in rings.iter().enumerate() {
|
||||
if index != 0 {
|
||||
local_boundary.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
local_boundary.extend(ring.iter().map(|&[x, y]| [x as f32, y as f32]));
|
||||
}
|
||||
let fill_plane = crate::scene::model::hatch_model::FillPlane {
|
||||
origin: self.plane.origin.to_array(),
|
||||
x_axis: self.plane.x.to_array(),
|
||||
y_axis: self.plane.y.to_array(),
|
||||
};
|
||||
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||
.into_iter()
|
||||
.map(|depth| depth == 0)
|
||||
|
|
@ -353,6 +355,7 @@ impl HatchCommand {
|
|||
}
|
||||
for path in &mut boundary_paths {
|
||||
path.boundary_handles.clear();
|
||||
path.flags.set_external(false);
|
||||
}
|
||||
}
|
||||
if let Some((source, _, _)) = &self.inherited {
|
||||
|
|
@ -391,8 +394,8 @@ impl HatchCommand {
|
|||
scale,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
fill_plane: Some(fill_plane),
|
||||
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
|
||||
|
|
@ -438,8 +441,8 @@ impl HatchCommand {
|
|||
scale: self.scale_override.unwrap_or(1.0).max(1.0e-6),
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
fill_plane: Some(fill_plane),
|
||||
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
|
||||
|
|
@ -476,32 +479,27 @@ impl HatchCommand {
|
|||
}
|
||||
|
||||
fn arc_bulge(start: DVec3, middle: DVec3, end: DVec3) -> Option<f64> {
|
||||
let d = 2.0
|
||||
* (start.x * (middle.y - end.y)
|
||||
+ middle.x * (end.y - start.y)
|
||||
+ end.x * (start.y - middle.y));
|
||||
if d.abs() <= 1.0e-12 {
|
||||
let curvature = DVec3::from_array(cadkernel::space::curve::curvature_through(
|
||||
start.to_array(),
|
||||
middle.to_array(),
|
||||
end.to_array(),
|
||||
));
|
||||
let squared = curvature.length_squared();
|
||||
if squared <= f64::MIN_POSITIVE {
|
||||
return None;
|
||||
}
|
||||
let s2 = start.x * start.x + start.y * start.y;
|
||||
let m2 = middle.x * middle.x + middle.y * middle.y;
|
||||
let e2 = end.x * end.x + end.y * end.y;
|
||||
let center_x = (s2 * (middle.y - end.y)
|
||||
+ m2 * (end.y - start.y)
|
||||
+ e2 * (start.y - middle.y))
|
||||
/ d;
|
||||
let center_y = (s2 * (end.x - middle.x)
|
||||
+ m2 * (start.x - end.x)
|
||||
+ e2 * (middle.x - start.x))
|
||||
/ d;
|
||||
let angle = |point: DVec3| (point.y - center_y).atan2(point.x - center_x);
|
||||
let first = angle(start);
|
||||
let through = (angle(middle) - first).rem_euclid(std::f64::consts::TAU);
|
||||
let ccw = (angle(end) - first).rem_euclid(std::f64::consts::TAU);
|
||||
let centre = start + curvature / squared;
|
||||
let circle = Curve::Circle(Circle {
|
||||
centre: [centre.x, centre.y],
|
||||
radius: squared.sqrt().recip(),
|
||||
});
|
||||
let first = circle.parameter_at([start.x, start.y]);
|
||||
let through = (circle.parameter_at([middle.x, middle.y]) - first).rem_euclid(1.0);
|
||||
let ccw = (circle.parameter_at([end.x, end.y]) - first).rem_euclid(1.0);
|
||||
let sweep = if through <= ccw + 1.0e-12 {
|
||||
ccw
|
||||
ccw * std::f64::consts::TAU
|
||||
} else {
|
||||
ccw - std::f64::consts::TAU
|
||||
(ccw - 1.0) * std::f64::consts::TAU
|
||||
};
|
||||
Some((sweep * 0.25).tan())
|
||||
}
|
||||
|
|
@ -631,6 +629,7 @@ impl CadCommand for HatchCommand {
|
|||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
let pt = self.plane.to_local(pt);
|
||||
match &self.mode {
|
||||
HatchMode::PickInside => {
|
||||
let xy = [pt.x, pt.y];
|
||||
|
|
@ -667,6 +666,9 @@ impl CadCommand for HatchCommand {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if matches!(self.mode, HatchMode::Manual) && self.manual_arc_midpoint.is_some() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 {
|
||||
let ring = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
|
||||
self.add_point_region(vec![ring]);
|
||||
|
|
@ -706,7 +708,12 @@ impl CadCommand for HatchCommand {
|
|||
} else if self.retain_boundaries {
|
||||
CmdResult::CommitHatchWithBoundaries {
|
||||
hatch: self.make_hatch(rings.clone()),
|
||||
boundaries: crate::scene::boundary_entities(&rings),
|
||||
boundaries: crate::scene::boundary_entities_from_sources(
|
||||
&rings,
|
||||
self.plane,
|
||||
&self.boundary_sources,
|
||||
1.0e-6,
|
||||
),
|
||||
entity_style: self
|
||||
.inherited
|
||||
.as_ref()
|
||||
|
|
@ -798,6 +805,10 @@ impl CadCommand for HatchCommand {
|
|||
_ => None,
|
||||
};
|
||||
}
|
||||
if upper == "ASSOCIATIVE" {
|
||||
self.associative = !self.associative;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if let Some(rest) = upper.strip_prefix('P') {
|
||||
let name = rest.trim();
|
||||
if !name.is_empty() {
|
||||
|
|
@ -839,6 +850,9 @@ impl CadCommand for HatchCommand {
|
|||
}
|
||||
"B" | "BOUNDARY" | "BOUNDARIES" => {
|
||||
self.retain_boundaries = !self.retain_boundaries;
|
||||
if self.retain_boundaries {
|
||||
self.separate_hatches = false;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"N" | "ASSOCIATIVE" => {
|
||||
|
|
@ -847,6 +861,9 @@ impl CadCommand for HatchCommand {
|
|||
}
|
||||
"D" | "SEPARATE" => {
|
||||
self.separate_hatches = !self.separate_hatches;
|
||||
if self.separate_hatches {
|
||||
self.retain_boundaries = false;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"Y" | "ISLAND" => {
|
||||
|
|
@ -875,14 +892,10 @@ impl CadCommand for HatchCommand {
|
|||
let mut pts: Vec<[f32; 3]> = self
|
||||
.manual_pts
|
||||
.iter()
|
||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||
.map(|&p| self.plane.to_world(p).as_vec3().to_array())
|
||||
.collect();
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
pts.push([
|
||||
self.manual_pts[0].x as f32,
|
||||
self.manual_pts[0].y as f32,
|
||||
self.manual_pts[0].z as f32,
|
||||
]);
|
||||
pts.push(pt.to_array());
|
||||
pts.push(self.plane.to_world(self.manual_pts[0]).as_vec3().to_array());
|
||||
return Some(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
pts,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,14 @@ impl CadCommand for HatcheditCommand {
|
|||
return self.apply_result(self.update_operation());
|
||||
}
|
||||
|
||||
if text == "ANNOTATIVE" {
|
||||
self.annotative = Some(!self.annotative.unwrap_or(self.annotative_current));
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if text == "SEPARATE" {
|
||||
return self.apply_result(HatchEditOperation::Separate);
|
||||
}
|
||||
|
||||
// Parse option: P/S/A followed by value
|
||||
if let Some(rest) = text.strip_prefix('P') {
|
||||
let n = rest.trim().to_string();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use super::*;
|
||||
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, distance_to, intersect, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance, Transform as CurveTransform,
|
||||
bounded_faces, closest_point, contains, distance_to, intersect, ring_nesting_depths,
|
||||
segment_crossing, signed_area, triangulate, Curve, Line, SegmentCrossing, Tolerance,
|
||||
Transform as CurveTransform,
|
||||
};
|
||||
|
||||
use crate::command::WorkingPlane;
|
||||
|
|
@ -13,12 +14,7 @@ pub struct BoundarySource {
|
|||
pub curves: Vec<Curve>,
|
||||
}
|
||||
|
||||
/// How far apart two points may be and still be taken for the same one.
|
||||
///
|
||||
/// The boundary search runs on already-tessellated wire geometry, so the
|
||||
/// input is a chord approximation of the drawn curves to begin with; this
|
||||
/// only has to be coarse enough to close the gaps that leaves and fine
|
||||
/// enough not to weld genuinely separate corners together.
|
||||
/// Boundary welding tolerance for tessellated wires.
|
||||
const WELD_TOLERANCE: f64 = 1.0e-6;
|
||||
|
||||
fn wire_segments_on_plane(
|
||||
|
|
@ -188,6 +184,19 @@ fn curve_forward(curve: &Curve, start: [f64; 2], next: [f64; 2]) -> bool {
|
|||
delta >= 0.0
|
||||
}
|
||||
|
||||
fn stored_arc_angles(start: f64, end: f64, counter_clockwise: bool, whole: bool) -> (f64, f64) {
|
||||
let stored_start = if counter_clockwise { start } else { -start }
|
||||
.rem_euclid(std::f64::consts::TAU);
|
||||
let sweep = if whole {
|
||||
std::f64::consts::TAU
|
||||
} else if counter_clockwise {
|
||||
(end - start).rem_euclid(std::f64::consts::TAU)
|
||||
} else {
|
||||
(start - end).rem_euclid(std::f64::consts::TAU)
|
||||
};
|
||||
(stored_start, stored_start + sweep)
|
||||
}
|
||||
|
||||
fn exact_boundary_edge(
|
||||
curve: Option<&Curve>,
|
||||
start: [f64; 2],
|
||||
|
|
@ -213,11 +222,10 @@ fn exact_boundary_edge(
|
|||
end: Vector2::new(end[0], end[1]),
|
||||
}),
|
||||
Curve::Circle(circle) => {
|
||||
let start_angle = (start[1] - circle.centre[1]).atan2(start[0] - circle.centre[0]);
|
||||
let mut end_angle = (end[1] - circle.centre[1]).atan2(end[0] - circle.centre[0]);
|
||||
if whole_curve {
|
||||
end_angle = start_angle + std::f64::consts::TAU;
|
||||
}
|
||||
let true_start = (start[1] - circle.centre[1]).atan2(start[0] - circle.centre[0]);
|
||||
let true_end = (end[1] - circle.centre[1]).atan2(end[0] - circle.centre[0]);
|
||||
let (start_angle, end_angle) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::CircularArc(CircularArcEdge {
|
||||
center: Vector2::new(circle.centre[0], circle.centre[1]),
|
||||
radius: circle.radius,
|
||||
|
|
@ -227,8 +235,10 @@ fn exact_boundary_edge(
|
|||
})
|
||||
}
|
||||
Curve::Arc(arc) => {
|
||||
let start_angle = (start[1] - arc.centre[1]).atan2(start[0] - arc.centre[0]);
|
||||
let end_angle = (end[1] - arc.centre[1]).atan2(end[0] - arc.centre[0]);
|
||||
let true_start = (start[1] - arc.centre[1]).atan2(start[0] - arc.centre[0]);
|
||||
let true_end = (end[1] - arc.centre[1]).atan2(end[0] - arc.centre[0]);
|
||||
let (start_angle, end_angle) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::CircularArc(CircularArcEdge {
|
||||
center: Vector2::new(arc.centre[0], arc.centre[1]),
|
||||
radius: arc.radius,
|
||||
|
|
@ -239,14 +249,10 @@ fn exact_boundary_edge(
|
|||
}
|
||||
Curve::Ellipse(arc) => {
|
||||
let ellipse = arc.ellipse;
|
||||
let mut start_parameter = arc.start_parameter + curve.parameter_at(start) * arc.sweep();
|
||||
let mut end_parameter = arc.start_parameter + curve.parameter_at(end) * arc.sweep();
|
||||
if !forward {
|
||||
std::mem::swap(&mut start_parameter, &mut end_parameter);
|
||||
}
|
||||
if whole_curve {
|
||||
end_parameter = start_parameter + std::f64::consts::TAU;
|
||||
}
|
||||
let true_start = arc.start_parameter + curve.parameter_at(start) * arc.sweep();
|
||||
let true_end = arc.start_parameter + curve.parameter_at(end) * arc.sweep();
|
||||
let (start_parameter, end_parameter) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::EllipticArc(EllipticArcEdge {
|
||||
center: Vector2::new(ellipse.centre[0], ellipse.centre[1]),
|
||||
major_axis_endpoint: Vector2::new(
|
||||
|
|
@ -264,8 +270,13 @@ fn exact_boundary_edge(
|
|||
Some(if forward { source.clone() } else { source.reversed() })
|
||||
} else {
|
||||
source.trimmed(source.parameter_at(start), source.parameter_at(end))
|
||||
}
|
||||
.unwrap_or_else(|| source.clone());
|
||||
};
|
||||
let Some(trimmed) = trimmed else {
|
||||
return BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
});
|
||||
};
|
||||
let rational = trimmed.is_rational();
|
||||
BoundaryEdge::Spline(SplineEdge {
|
||||
degree: trimmed.degree() as i32,
|
||||
|
|
@ -294,9 +305,7 @@ fn exact_boundary_edge(
|
|||
}
|
||||
}
|
||||
|
||||
/// Rebuild detected tessellated rings as analytic hatch paths wherever their
|
||||
/// source entity exposes an exact curve. Intersections remain the graph's
|
||||
/// vertices, while the edge between them is stored as a trimmed source curve.
|
||||
/// Rebuild detected rings from exact source curves.
|
||||
pub(crate) fn exact_hatch_paths(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
exterior: &[bool],
|
||||
|
|
@ -371,7 +380,10 @@ pub(crate) fn exact_hatch_paths(
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
||||
pub(crate) fn boundary_entities(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
plane: WorkingPlane,
|
||||
) -> Vec<acadrust::EntityType> {
|
||||
rings
|
||||
.iter()
|
||||
.filter_map(|ring| {
|
||||
|
|
@ -397,7 +409,7 @@ pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::Entity
|
|||
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
||||
})
|
||||
.collect();
|
||||
Some(acadrust::EntityType::LwPolyline(polyline))
|
||||
Some(plane.place_entity(acadrust::EntityType::LwPolyline(polyline)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -458,7 +470,7 @@ fn nearest_crossing(a: &Curve, b: &Curve, point: [f64; 2], tolerance: f64) -> Op
|
|||
}
|
||||
|
||||
fn project_to_curve(curve: &Curve, point: [f64; 2]) -> [f64; 2] {
|
||||
curve.point_at(curve.parameter_at(point))
|
||||
closest_point(curve, point).point
|
||||
}
|
||||
|
||||
fn refined_boundary_ring(
|
||||
|
|
@ -571,6 +583,18 @@ pub(crate) fn boundary_polyline_entities(
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities_from_sources(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
plane: WorkingPlane,
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Vec<EntityType> {
|
||||
rings
|
||||
.iter()
|
||||
.filter_map(|ring| boundary_polyline(ring, plane, sources, tolerance))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub(crate) fn edit_hatch_boundary_handles(
|
||||
&mut self,
|
||||
|
|
@ -593,6 +617,11 @@ impl Scene {
|
|||
} else {
|
||||
path.boundary_handles.retain(|handle| !handles.contains(handle));
|
||||
}
|
||||
if path.boundary_handles.is_empty() {
|
||||
path.flags.set_external(false);
|
||||
} else {
|
||||
path.flags.set_external(true);
|
||||
}
|
||||
hatch.is_associative = hatch
|
||||
.paths
|
||||
.iter()
|
||||
|
|
@ -693,6 +722,9 @@ impl Scene {
|
|||
let old_count = path.boundary_handles.len();
|
||||
path.boundary_handles
|
||||
.retain(|source| self.document.get_entity(*source).is_some());
|
||||
if path.boundary_handles.is_empty() {
|
||||
path.flags.set_external(false);
|
||||
}
|
||||
association_changed |= path.boundary_handles.len() != old_count;
|
||||
modified |= association_changed;
|
||||
let sources: rustc_hash::FxHashMap<_, _> = path
|
||||
|
|
@ -801,6 +833,82 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
fn hatch_path_geometry(
|
||||
path: &acadrust::entities::BoundaryPath,
|
||||
) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||
let edges: Vec<_> = path
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(crate::entities::hatch::edge_curve)
|
||||
.map(|curve| curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE))
|
||||
.collect();
|
||||
super::entity::chain_path_edges_with_directions(edges)
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_path_ring(path: &acadrust::entities::BoundaryPath) -> Option<Vec<[f64; 2]>> {
|
||||
let (ring, _) = hatch_path_geometry(path);
|
||||
(ring.len() >= 3).then_some(ring)
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_path_directions(path: &acadrust::entities::BoundaryPath) -> Vec<f64> {
|
||||
hatch_path_geometry(path).1
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_boundary_rings(hatch: &acadrust::entities::Hatch) -> Vec<Vec<[f64; 2]>> {
|
||||
hatch.paths.iter().filter_map(hatch_path_ring).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn separated_hatch_path_groups(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
) -> Vec<Vec<acadrust::entities::BoundaryPath>> {
|
||||
let items: Vec<_> = hatch
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| hatch_path_ring(path).map(|ring| (path.clone(), ring)))
|
||||
.collect();
|
||||
let rings: Vec<_> = items.iter().map(|(_, ring)| ring.clone()).collect();
|
||||
let depths = ring_nesting_depths(&rings);
|
||||
let outer_indices: Vec<_> = depths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, depth)| (*depth == 0).then_some(index))
|
||||
.collect();
|
||||
let mut groups: Vec<_> = outer_indices
|
||||
.iter()
|
||||
.map(|index| vec![items[*index].0.clone()])
|
||||
.collect();
|
||||
for (index, (path, ring)) in items.iter().enumerate() {
|
||||
if depths.get(index) == Some(&0) {
|
||||
continue;
|
||||
}
|
||||
let Some(seed) = ring.first().copied() else {
|
||||
continue;
|
||||
};
|
||||
let owner = outer_indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, outer)| {
|
||||
contains(
|
||||
&face_curves(&rings[**outer]),
|
||||
seed,
|
||||
Tolerance::new(WELD_TOLERANCE),
|
||||
)
|
||||
})
|
||||
.min_by(|(_, left), (_, right)| {
|
||||
signed_area(&rings[**left])
|
||||
.abs()
|
||||
.total_cmp(&signed_area(&rings[**right]).abs())
|
||||
})
|
||||
.map(|(group, _)| group);
|
||||
if let Some(owner) = owner {
|
||||
groups[owner].push(path.clone());
|
||||
} else {
|
||||
groups.push(vec![path.clone()]);
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_faces(
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -1622,6 +1615,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 {
|
||||
|
|
@ -1636,26 +1630,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());
|
||||
}
|
||||
|
||||
|
|
@ -1665,9 +1660,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,
|
||||
|
|
@ -1678,8 +1679,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);
|
||||
}
|
||||
|
|
@ -1879,12 +1886,17 @@ 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())),
|
||||
|
|
@ -2229,18 +2241,22 @@ impl Scene {
|
|||
) -> 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
|
||||
);
|
||||
// Keep analytic command geometry when available. The tessellated model
|
||||
// remains the render representation only; it must not replace circles,
|
||||
// ellipse arcs or splines in the persisted entity.
|
||||
// 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 the render offsets without
|
||||
// dropping its separators.
|
||||
// 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
|
||||
|
|
@ -2328,6 +2344,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;
|
||||
|
|
@ -2345,10 +2371,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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@ mod scene_markers;
|
|||
mod selection;
|
||||
|
||||
pub(crate) use boundary::{
|
||||
boundary_entities, boundary_faces, boundary_polyline_entities, exact_hatch_paths,
|
||||
ring_source_handles, BoundarySource,
|
||||
boundary_entities, boundary_entities_from_sources, boundary_faces,
|
||||
boundary_polyline_entities, exact_hatch_paths, hatch_boundary_rings, hatch_path_directions,
|
||||
hatch_path_ring, ring_source_handles, separated_hatch_path_groups, BoundarySource,
|
||||
};
|
||||
|
||||
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||
|
|
|
|||
|
|
@ -203,17 +203,14 @@ pub struct HatchModel {
|
|||
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
||||
/// vertices from `boundary` + `world_origin` instead.
|
||||
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
|
||||
/// Optional 3-D placement used by wipeout fills.
|
||||
/// Optional 3-D placement for planar fills.
|
||||
pub fill_plane: Option<FillPlane>,
|
||||
pub fill_plane_boundary: Option<Arc<Vec<[f32; 2]>>>,
|
||||
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
||||
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
||||
/// Source entity handles for each boundary ring.
|
||||
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
||||
/// Exact persisted boundary paths for draw/edit workflows. Rendering keeps
|
||||
/// using the compact tessellated boundary above, while persistence can
|
||||
/// retain analytic arcs, ellipses and splines without rebuilding them as
|
||||
/// straight polyline chords.
|
||||
/// Exact boundary paths retained for persistence and editing.
|
||||
pub boundary_paths: Option<Arc<Vec<acadrust::entities::BoundaryPath>>>,
|
||||
/// Island handling used by the persisted hatch entity.
|
||||
pub style: acadrust::entities::HatchStyleType,
|
||||
|
|
|
|||
Loading…
Reference in a new issue