forked from 1b-agent/opencadstudio-upstream
1 parent
0038e386f7
commit
7a8da58d55
12 files changed
+352
-120
No files matched your search
Generated
+1
-1
@@ -891,7 +891,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8d05593#8d05593221cf6158076c38e98fef54ab767b81d9"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1fe426d#1fe426d1922dfc79685b8f6d34ce66ab15d97814"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8d05593", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1fe426d", features = ["acis", "offset"] }
|
||||
acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
|
||||
@@ -14,7 +14,15 @@ impl OpenCADStudio {
|
||||
true
|
||||
}
|
||||
|
||||
fn refresh_area_preview(&mut self, i: usize) {
|
||||
pub(super) fn refresh_area_preview(&mut self, i: usize) {
|
||||
let hatches = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.hatch_preview_models());
|
||||
if let Some(hatches) = hatches {
|
||||
self.tabs[i].scene.set_command_preview_hatches(hatches);
|
||||
return;
|
||||
}
|
||||
let regions = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
|
||||
@@ -619,9 +619,17 @@ impl OpenCADStudio {
|
||||
"HATCH" => {
|
||||
use crate::modules::draw::draw::hatch::HatchCommand;
|
||||
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
|
||||
let new_cmd = HatchCommand::new(outlines);
|
||||
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
|
||||
let selected = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(handle, _)| handle)
|
||||
.collect();
|
||||
let new_cmd = HatchCommand::new(outlines, boundary_sources, selected);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
self.refresh_area_preview(i);
|
||||
}
|
||||
|
||||
"HATCHEDIT" => {
|
||||
|
||||
@@ -1844,6 +1844,12 @@ pub trait CadCommand: Send {
|
||||
None
|
||||
}
|
||||
|
||||
fn hatch_preview_models(
|
||||
&self,
|
||||
) -> Option<Vec<crate::scene::model::hatch_model::HatchModel>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` when the current step picks a corner of a selection
|
||||
/// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a
|
||||
/// free point: applying the Ortho/Polar lock would pin the opposite corner to
|
||||
|
||||
+199
-28
@@ -13,6 +13,8 @@ use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use acadrust::Handle;
|
||||
use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
@@ -48,6 +50,13 @@ enum Mode {
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum HatchMode {
|
||||
PickInside,
|
||||
SelectObjects,
|
||||
Manual,
|
||||
}
|
||||
|
||||
// ── CPU point-in-polygon (ray casting) ────────────────────────────────────
|
||||
|
||||
fn point_in_polygon(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
|
||||
@@ -195,23 +204,91 @@ fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64;
|
||||
|
||||
pub struct HatchCommand {
|
||||
outlines: Vec<Vec<[f64; 2]>>,
|
||||
mode: Mode,
|
||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||
selected_objects: Vec<Handle>,
|
||||
mode: HatchMode,
|
||||
manual_pts: Vec<DVec3>,
|
||||
missed: bool,
|
||||
}
|
||||
|
||||
impl HatchCommand {
|
||||
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
||||
Self {
|
||||
pub fn new(
|
||||
outlines: Vec<Vec<[f64; 2]>>,
|
||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||
selected_objects: Vec<Handle>,
|
||||
) -> Self {
|
||||
let has_selection = !selected_objects.is_empty();
|
||||
let mut command = Self {
|
||||
outlines,
|
||||
mode: Mode::PickInside,
|
||||
boundary_sources,
|
||||
point_regions: Vec::new(),
|
||||
object_regions: Vec::new(),
|
||||
selected_objects: Vec::new(),
|
||||
mode: if has_selection {
|
||||
HatchMode::SelectObjects
|
||||
} else {
|
||||
HatchMode::PickInside
|
||||
},
|
||||
manual_pts: vec![],
|
||||
missed: false,
|
||||
};
|
||||
command.set_object_selection(selected_objects);
|
||||
command
|
||||
}
|
||||
|
||||
fn set_object_selection(&mut self, handles: Vec<Handle>) {
|
||||
let mut segments = Vec::new();
|
||||
for handle in &handles {
|
||||
if let Some(source) = self.boundary_sources.get(handle) {
|
||||
segments.extend(source.iter().copied());
|
||||
}
|
||||
}
|
||||
self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6))
|
||||
.into_iter()
|
||||
.map(|ring| vec![ring])
|
||||
.collect();
|
||||
self.missed = !handles.is_empty() && self.object_regions.is_empty();
|
||||
self.selected_objects = handles;
|
||||
}
|
||||
|
||||
fn add_point_region(&mut self, rings: Vec<Vec<[f64; 2]>>) {
|
||||
let duplicate = rings.first().is_some_and(|outer| {
|
||||
self.point_regions
|
||||
.iter()
|
||||
.any(|region| region.first() == Some(outer))
|
||||
});
|
||||
if !duplicate {
|
||||
self.point_regions.push(rings);
|
||||
}
|
||||
}
|
||||
|
||||
fn region_count(&self) -> usize {
|
||||
self.point_regions.len() + self.object_regions.len()
|
||||
}
|
||||
|
||||
fn combined_rings(&self) -> Vec<Vec<[f64; 2]>> {
|
||||
let mut rings = Vec::new();
|
||||
for ring in self
|
||||
.point_regions
|
||||
.iter()
|
||||
.chain(self.object_regions.iter())
|
||||
.flat_map(|region| region.iter())
|
||||
{
|
||||
if !rings.iter().any(|existing| existing == ring) {
|
||||
rings.push(ring.clone());
|
||||
}
|
||||
}
|
||||
rings
|
||||
}
|
||||
|
||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
||||
let (rel, origin, wcs) = pack_rings(&rings);
|
||||
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||
.into_iter()
|
||||
.map(|depth| depth % 2 == 0)
|
||||
.collect();
|
||||
// Default: ANSI31 from catalog; fallback to a single 45° family.
|
||||
let pat_name = "ANSI31";
|
||||
let families = crate::scene::model::hatch_patterns::find(pat_name)
|
||||
@@ -246,6 +323,7 @@ impl HatchCommand {
|
||||
scale: 1.0,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -258,15 +336,34 @@ impl CadCommand for HatchCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.mode {
|
||||
Mode::PickInside => {
|
||||
HatchMode::PickInside => {
|
||||
let miss = if self.missed {
|
||||
t!(" ⚠ No closed boundary found.").into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
t!("HATCH Pick internal point:%{miss}", miss = miss).into_owned()
|
||||
t!(
|
||||
"HATCH Pick internal point (%{count} regions selected, Enter to apply):%{miss}",
|
||||
count = self.region_count(),
|
||||
miss = miss
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
Mode::Manual => {
|
||||
HatchMode::SelectObjects => {
|
||||
let miss = if self.missed {
|
||||
t!(" ⚠ Selection has no closed boundary.").into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
t!(
|
||||
"HATCH Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}",
|
||||
objects = self.selected_objects.len(),
|
||||
count = self.region_count(),
|
||||
miss = miss
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
HatchMode::Manual => {
|
||||
if self.manual_pts.is_empty() {
|
||||
t!("HATCH Boundary point 1:").into_owned()
|
||||
} else {
|
||||
@@ -280,8 +377,27 @@ impl CadCommand for HatchCommand {
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
match &self.mode {
|
||||
Mode::PickInside => vec![CmdOption::new(t!("Draw manually").as_ref(), "S")],
|
||||
Mode::Manual => {
|
||||
HatchMode::PickInside => {
|
||||
let mut options = vec![
|
||||
CmdOption::new(t!("Select objects").as_ref(), "O"),
|
||||
CmdOption::new(t!("Draw manually").as_ref(), "S"),
|
||||
];
|
||||
if self.region_count() > 0 {
|
||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||
}
|
||||
options
|
||||
}
|
||||
HatchMode::SelectObjects => {
|
||||
let mut options = vec![
|
||||
CmdOption::new(t!("Pick internal points").as_ref(), "I"),
|
||||
CmdOption::new(t!("Draw manually").as_ref(), "S"),
|
||||
];
|
||||
if self.region_count() > 0 {
|
||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||
}
|
||||
options
|
||||
}
|
||||
HatchMode::Manual => {
|
||||
// Enter accepts the boundary once at least 3 points are picked.
|
||||
if self.manual_pts.len() >= 3 {
|
||||
vec![CmdOption::enter(t!("Accept").as_ref())]
|
||||
@@ -294,12 +410,13 @@ impl CadCommand for HatchCommand {
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match &self.mode {
|
||||
Mode::PickInside => {
|
||||
HatchMode::PickInside => {
|
||||
let xy = [pt.x, pt.y];
|
||||
match resolve_hatch_rings(&self.outlines, xy) {
|
||||
Some(rings) => {
|
||||
self.missed = false;
|
||||
return CmdResult::CommitHatch(self.make_hatch(rings));
|
||||
self.add_point_region(rings);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
None => {
|
||||
self.missed = true;
|
||||
@@ -307,7 +424,8 @@ impl CadCommand for HatchCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Manual => {
|
||||
HatchMode::SelectObjects => CmdResult::NeedPoint,
|
||||
HatchMode::Manual => {
|
||||
// Keep the typed/snapped point exact (issue #311).
|
||||
self.manual_pts.push(pt);
|
||||
CmdResult::NeedPoint
|
||||
@@ -316,16 +434,55 @@ impl CadCommand for HatchCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.mode {
|
||||
Mode::PickInside => CmdResult::Cancel,
|
||||
Mode::Manual => {
|
||||
if self.manual_pts.len() < 3 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let wcs = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
|
||||
CmdResult::CommitHatch(self.make_hatch(vec![wcs]))
|
||||
}
|
||||
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]);
|
||||
}
|
||||
let rings = self.combined_rings();
|
||||
if rings.is_empty() {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::CommitHatch(self.make_hatch(rings))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.mode, HatchMode::SelectObjects)
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
matches!(self.mode, HatchMode::SelectObjects)
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if matches!(self.mode, HatchMode::SelectObjects) {
|
||||
self.set_object_selection(handles);
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() {
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn hatch_preview_models(&self) -> Option<Vec<HatchModel>> {
|
||||
let mut rings = self.combined_rings();
|
||||
if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 {
|
||||
rings.push(self.manual_pts.iter().map(|point| [point.x, point.y]).collect());
|
||||
}
|
||||
Some(if rings.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut preview = self.make_hatch(rings);
|
||||
preview.color = [0.15, 0.55, 1.0, 0.75];
|
||||
vec![preview]
|
||||
})
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
@@ -333,20 +490,32 @@ impl CadCommand for HatchCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.mode, Mode::PickInside)
|
||||
!matches!(self.mode, HatchMode::Manual)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if text.trim().eq_ignore_ascii_case("s") {
|
||||
self.mode = Mode::Manual;
|
||||
self.missed = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"O" | "OBJECT" | "OBJECTS" => {
|
||||
self.mode = HatchMode::SelectObjects;
|
||||
self.missed = false;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"I" | "INTERNAL" => {
|
||||
self.mode = HatchMode::PickInside;
|
||||
self.missed = false;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"S" => {
|
||||
self.mode = HatchMode::Manual;
|
||||
self.missed = false;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
if let Mode::Manual = &self.mode {
|
||||
if let HatchMode::Manual = &self.mode {
|
||||
if self.manual_pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -416,6 +585,7 @@ impl GradientCommand {
|
||||
scale: 1.0,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: None,
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -627,6 +797,7 @@ impl CadCommand for BoundaryCommand {
|
||||
scale: 1.0,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: None,
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
CmdResult::CommitHatch(model)
|
||||
|
||||
+43
-33
@@ -10,6 +10,31 @@ use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
|
||||
/// enough not to weld genuinely separate corners together.
|
||||
const WELD_TOLERANCE: f64 = 1.0e-6;
|
||||
|
||||
fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
||||
let mut segments = Vec::new();
|
||||
let mut previous: Option<[f64; 2]> = None;
|
||||
|
||||
for (index, high) in wire.points.iter().copied().enumerate() {
|
||||
if !high[0].is_finite() || !high[1].is_finite() {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let current = [
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
];
|
||||
if let Some(start) = previous {
|
||||
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
|
||||
if dx.hypot(dy) > WELD_TOLERANCE {
|
||||
segments.push(Line { start, end: current });
|
||||
}
|
||||
}
|
||||
previous = Some(current);
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Build closed planar regions from the visible wire geometry.
|
||||
///
|
||||
@@ -30,41 +55,26 @@ impl Scene {
|
||||
let mut segments = Vec::<Line>::new();
|
||||
|
||||
for wire in self.entity_wires().iter() {
|
||||
let mut previous: Option<[f64; 2]> = None;
|
||||
|
||||
for (index, high) in wire.points.iter().copied().enumerate() {
|
||||
// NaNs delimit independent segments inside some WireModels,
|
||||
// notably polylines stored as A-B | B-C | C-D.
|
||||
if !high[0].is_finite() || !high[1].is_finite() {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The wire carries its coordinates as a double-single pair, so
|
||||
// both halves are needed to recover the f64 the tessellation
|
||||
// produced. Reading only the high half would put every vertex
|
||||
// of a survey-coordinate drawing on a grid coarser than the
|
||||
// weld tolerance.
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let current = [
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
];
|
||||
|
||||
if let Some(start) = previous {
|
||||
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
|
||||
if dx.hypot(dy) > WELD_TOLERANCE {
|
||||
segments.push(Line {
|
||||
start,
|
||||
end: current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(current);
|
||||
}
|
||||
segments.extend(wire_segments(wire));
|
||||
}
|
||||
|
||||
bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE))
|
||||
}
|
||||
|
||||
/// Tessellated boundary segments grouped by their selectable entity.
|
||||
pub fn hatch_boundary_sources(
|
||||
&self,
|
||||
) -> rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>> {
|
||||
let mut sources = rustc_hash::FxHashMap::default();
|
||||
for wire in self.entity_wires().iter() {
|
||||
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
||||
continue;
|
||||
};
|
||||
sources
|
||||
.entry(handle)
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(wire_segments(wire));
|
||||
}
|
||||
sources
|
||||
}
|
||||
}
|
||||
+75
-55
@@ -1362,6 +1362,7 @@ impl Scene {
|
||||
render_instance,
|
||||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color,
|
||||
@@ -1466,6 +1467,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
||||
@@ -1499,6 +1501,7 @@ impl Scene {
|
||||
boundary.truncate(before_path);
|
||||
continue;
|
||||
}
|
||||
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
|
||||
if boundary.len() >= path_start + 3 {
|
||||
let first = boundary[path_start];
|
||||
let last = *boundary.last().unwrap();
|
||||
@@ -1511,6 +1514,11 @@ impl Scene {
|
||||
if boundary.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !boundary_exterior.iter().any(|role| *role) {
|
||||
if let Some(first) = boundary_exterior.first_mut() {
|
||||
*first = true;
|
||||
}
|
||||
}
|
||||
// The batched hatch renderer keeps boundaries in a GPU storage
|
||||
// buffer (no fixed length), so a hatch with many island loops must
|
||||
// retain *every* loop or even-odd island detection breaks. The old
|
||||
@@ -1705,6 +1713,7 @@ impl Scene {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
@@ -2019,6 +2028,7 @@ impl Scene {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color,
|
||||
@@ -2037,64 +2047,74 @@ impl Scene {
|
||||
model.pattern,
|
||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
// Prefer the command-supplied exact f64 boundary so a typed vertex is
|
||||
// persisted without f32 quantization (issue #311). Falling back to the
|
||||
// render-side `boundary`, the points arrive in local render space
|
||||
// (world_offset already subtracted), so add `world_origin` back —
|
||||
// otherwise the boundary wire lands `world_offset` away from the fill.
|
||||
// Build one DXF boundary path per ring. The command layer separates
|
||||
// the outer boundary from its holes with NaN sentinels in
|
||||
// `boundary_wcs`; split on those so nested hatches (e.g. a small
|
||||
// rectangle inside a big one) persist with real holes instead of a
|
||||
// single self-intersecting polyline. Only the FIRST ring carries the
|
||||
// external / outermost flags — every later ring is a hole and must not
|
||||
// be flagged external, or DXF/DWG consumers treat the inner loop as
|
||||
// another outer island rather than a hole.
|
||||
if let Some(wcs) = &model.boundary_wcs {
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let mut path = if is_outer {
|
||||
let mut p = BoundaryPath::external();
|
||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
||||
p.flags.bits()
|
||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
||||
);
|
||||
p
|
||||
} else {
|
||||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
for &[x, y] in wcs.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = first;
|
||||
first = false;
|
||||
push_ring(&mut ring, is_outer);
|
||||
}
|
||||
}
|
||||
push_ring(&mut ring, first);
|
||||
}
|
||||
if dxf.paths.is_empty() {
|
||||
let wx = model.world_origin[0];
|
||||
let wy = model.world_origin[1];
|
||||
let verts: Vec<Vector2> = model
|
||||
// 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() {
|
||||
let [wx, wy] = model.world_origin;
|
||||
model
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
|
||||
.collect();
|
||||
let edge = PolylineEdge::new(verts, true);
|
||||
let mut path = BoundaryPath::external();
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
dxf.paths.push(path);
|
||||
.map(|&[x, y]| {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
[x as f64 + wx, y as f64 + wy]
|
||||
} else {
|
||||
[f64::NAN, f64::NAN]
|
||||
}
|
||||
})
|
||||
.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| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let mut path = if is_outer {
|
||||
let mut p = BoundaryPath::external();
|
||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
||||
p.flags.bits()
|
||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
||||
);
|
||||
p
|
||||
} else {
|
||||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
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);
|
||||
if !ring.is_empty() {
|
||||
ring_index += 1;
|
||||
}
|
||||
first = false;
|
||||
push_ring(&mut ring, is_outer);
|
||||
}
|
||||
}
|
||||
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);
|
||||
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
|
||||
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
||||
}
|
||||
|
||||
@@ -3181,6 +3181,7 @@ impl Scene {
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".to_string(),
|
||||
color: self.paper_bg_color,
|
||||
|
||||
@@ -180,6 +180,8 @@ 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]>>>,
|
||||
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
||||
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
||||
/// Fill pattern.
|
||||
pub pattern: HatchPattern,
|
||||
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
||||
|
||||
@@ -29,6 +29,10 @@ impl Scene {
|
||||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
pub fn set_command_preview_hatches(&mut self, models: Vec<HatchModel>) {
|
||||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
pub fn set_area_preview_regions(&mut self, regions: &[AreaPreviewRegion]) {
|
||||
let mut models = Vec::new();
|
||||
for region in regions {
|
||||
@@ -154,6 +158,7 @@ impl Scene {
|
||||
world_origin: origin,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "AREA_PREVIEW".into(),
|
||||
color: [0.0; 4],
|
||||
|
||||
@@ -130,6 +130,7 @@ impl canvas::Program<Message> for HatchPatternPreview {
|
||||
[pad, bounds.height - pad],
|
||||
]),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
pattern: self.pattern.clone(),
|
||||
name: String::new(),
|
||||
color: [1.0; 4],
|
||||
|
||||
Reference in new issue
Block a user