feat(hatch): add associative boundaries
This commit is contained in:
parent
3d2848021f
commit
1d2f0a09c1
10 changed files with 577 additions and 49 deletions
|
|
@ -793,6 +793,24 @@ impl OpenCADStudio {
|
|||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitEntitiesAndExit(entities) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
let delta_safe = entities
|
||||
.iter()
|
||||
.all(|entity| self.delta_add_safe(i, entity));
|
||||
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
|
||||
for entity in entities {
|
||||
self.commit_entity(entity);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::MviewCreate {
|
||||
viewport,
|
||||
preserve_view,
|
||||
|
|
@ -1271,7 +1289,67 @@ impl OpenCADStudio {
|
|||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer));
|
||||
let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
|
||||
if !new_handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(new_handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitStyledHatch {
|
||||
hatch,
|
||||
color,
|
||||
transparency,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
let new_handle = self.tabs[i].scene.add_hatch(
|
||||
hatch,
|
||||
Some(&layer),
|
||||
Some((color, transparency)),
|
||||
);
|
||||
if !new_handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(new_handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitHatchWithBoundaries {
|
||||
mut hatch,
|
||||
boundaries,
|
||||
entity_style,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||
let pending = self.begin_undo(i, label, boundaries.len() + 1, true);
|
||||
let mut sources = Vec::with_capacity(boundaries.len());
|
||||
for boundary in boundaries {
|
||||
let handles = self
|
||||
.commit_entity_handle(boundary)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
sources.push(handles);
|
||||
}
|
||||
hatch.boundary_sources = Some(std::sync::Arc::new(sources));
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
let new_handle =
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.add_hatch(hatch, Some(&layer), entity_style);
|
||||
if !new_handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(new_handle, true);
|
||||
}
|
||||
|
|
@ -3097,7 +3175,7 @@ impl OpenCADStudio {
|
|||
// Remove old hatch (entity + GPU model)
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
// Re-add with updated model
|
||||
self.tabs[i].scene.add_hatch(model, Some(&layer));
|
||||
self.tabs[i].scene.add_hatch(model, Some(&layer), None);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -625,8 +625,16 @@ impl OpenCADStudio {
|
|||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(handle, _)| handle)
|
||||
.collect();
|
||||
let new_cmd = HatchCommand::new(outlines, boundary_sources, selected);
|
||||
.collect::<Vec<_>>();
|
||||
let inherited = selected
|
||||
.iter()
|
||||
.find_map(|handle| {
|
||||
let model = self.tabs[i].scene.hatches.get(handle)?.clone();
|
||||
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);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
self.refresh_area_preview(i);
|
||||
|
|
|
|||
|
|
@ -1151,6 +1151,8 @@ pub enum CmdResult {
|
|||
CommitEntity(EntityType),
|
||||
/// Commit several acadrust entities in one undo step; keep the command active.
|
||||
CommitEntities(Vec<EntityType>),
|
||||
/// Commit several entities in one undo step and end the command.
|
||||
CommitEntitiesAndExit(Vec<EntityType>),
|
||||
/// Commit an acadrust entity to the document and end the command.
|
||||
CommitAndExit(EntityType),
|
||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||
|
|
@ -1183,6 +1185,18 @@ pub enum CmdResult {
|
|||
CopySelected(Vec<Handle>, EntityTransform),
|
||||
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
|
||||
CommitHatch(HatchModel),
|
||||
/// Commit a hatch with the selected hatch's entity colour and transparency.
|
||||
CommitStyledHatch {
|
||||
hatch: HatchModel,
|
||||
color: acadrust::types::Color,
|
||||
transparency: acadrust::types::Transparency,
|
||||
},
|
||||
/// Commit a hatch and retain each boundary ring as an entity.
|
||||
CommitHatchWithBoundaries {
|
||||
hatch: HatchModel,
|
||||
boundaries: Vec<EntityType>,
|
||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||
},
|
||||
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
|
||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||
/// Erase `handle` and replace with new entities; command stays active.
|
||||
|
|
|
|||
|
|
@ -211,6 +211,12 @@ pub struct HatchCommand {
|
|||
mode: HatchMode,
|
||||
manual_pts: Vec<DVec3>,
|
||||
missed: bool,
|
||||
retain_boundaries: bool,
|
||||
inherited: Option<(
|
||||
HatchModel,
|
||||
acadrust::types::Color,
|
||||
acadrust::types::Transparency,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl HatchCommand {
|
||||
|
|
@ -218,7 +224,16 @@ impl HatchCommand {
|
|||
outlines: Vec<Vec<[f64; 2]>>,
|
||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||
selected_objects: Vec<Handle>,
|
||||
inherited: Option<(
|
||||
HatchModel,
|
||||
acadrust::types::Color,
|
||||
acadrust::types::Transparency,
|
||||
)>,
|
||||
) -> Self {
|
||||
let selected_objects: Vec<_> = selected_objects
|
||||
.into_iter()
|
||||
.filter(|handle| boundary_sources.contains_key(handle))
|
||||
.collect();
|
||||
let has_selection = !selected_objects.is_empty();
|
||||
let mut command = Self {
|
||||
outlines,
|
||||
|
|
@ -233,6 +248,8 @@ impl HatchCommand {
|
|||
},
|
||||
manual_pts: vec![],
|
||||
missed: false,
|
||||
retain_boundaries: false,
|
||||
inherited,
|
||||
};
|
||||
command.set_object_selection(selected_objects);
|
||||
command
|
||||
|
|
@ -289,6 +306,51 @@ impl HatchCommand {
|
|||
.into_iter()
|
||||
.map(|depth| depth % 2 == 0)
|
||||
.collect();
|
||||
let boundary_sources = rings
|
||||
.iter()
|
||||
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
||||
.collect();
|
||||
if let Some((source, _, _)) = &self.inherited {
|
||||
let mut pattern = source.pattern.clone();
|
||||
if let HatchPattern::Pattern(families) = &mut pattern {
|
||||
let scale = if source.scale.abs() > 1.0e-6 {
|
||||
source.scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let (sin, cos) = source.angle_offset.sin_cos();
|
||||
for family in families {
|
||||
let base_x = source.world_origin[0]
|
||||
+ (family.x0 as f64 * cos as f64
|
||||
- family.y0 as f64 * sin as f64)
|
||||
* scale as f64;
|
||||
let base_y = source.world_origin[1]
|
||||
+ (family.x0 as f64 * sin as f64
|
||||
+ family.y0 as f64 * cos as f64)
|
||||
* scale as f64;
|
||||
let dx = base_x - origin[0];
|
||||
let dy = base_y - origin[1];
|
||||
family.x0 = ((dx * cos as f64 + dy * sin as f64) / scale as f64) as f32;
|
||||
family.y0 = ((-dx * sin as f64 + dy * cos as f64) / scale as f64) as f32;
|
||||
}
|
||||
}
|
||||
return HatchModel {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
pattern,
|
||||
name: source.name.clone(),
|
||||
color: source.color,
|
||||
aci: source.aci,
|
||||
line_weight_px: source.line_weight_px,
|
||||
angle_offset: source.angle_offset,
|
||||
scale: source.scale,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
draw_depth: source.draw_depth,
|
||||
};
|
||||
}
|
||||
// Default: ANSI31 from catalog; fallback to a single 45° family.
|
||||
let pat_name = "ANSI31";
|
||||
let families = crate::scene::model::hatch_patterns::find(pat_name)
|
||||
|
|
@ -324,6 +386,7 @@ impl HatchCommand {
|
|||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -381,6 +444,14 @@ impl CadCommand for HatchCommand {
|
|||
let mut options = vec![
|
||||
CmdOption::new(t!("Select objects").as_ref(), "O"),
|
||||
CmdOption::new(t!("Draw manually").as_ref(), "S"),
|
||||
CmdOption::new(
|
||||
if self.retain_boundaries {
|
||||
"Keep boundaries: on"
|
||||
} else {
|
||||
"Keep boundaries: off"
|
||||
},
|
||||
"B",
|
||||
),
|
||||
];
|
||||
if self.region_count() > 0 {
|
||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||
|
|
@ -391,6 +462,14 @@ impl CadCommand for HatchCommand {
|
|||
let mut options = vec![
|
||||
CmdOption::new(t!("Pick internal points").as_ref(), "I"),
|
||||
CmdOption::new(t!("Draw manually").as_ref(), "S"),
|
||||
CmdOption::new(
|
||||
if self.retain_boundaries {
|
||||
"Keep boundaries: on"
|
||||
} else {
|
||||
"Keep boundaries: off"
|
||||
},
|
||||
"B",
|
||||
),
|
||||
];
|
||||
if self.region_count() > 0 {
|
||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||
|
|
@ -441,6 +520,21 @@ impl CadCommand for HatchCommand {
|
|||
let rings = self.combined_rings();
|
||||
if rings.is_empty() {
|
||||
CmdResult::Cancel
|
||||
} else if self.retain_boundaries {
|
||||
CmdResult::CommitHatchWithBoundaries {
|
||||
hatch: self.make_hatch(rings.clone()),
|
||||
boundaries: crate::scene::boundary_entities(&rings),
|
||||
entity_style: self
|
||||
.inherited
|
||||
.as_ref()
|
||||
.map(|(_, color, transparency)| (color.clone(), *transparency)),
|
||||
}
|
||||
} else if let Some((_, color, transparency)) = &self.inherited {
|
||||
CmdResult::CommitStyledHatch {
|
||||
hatch: self.make_hatch(rings),
|
||||
color: color.clone(),
|
||||
transparency: *transparency,
|
||||
}
|
||||
} else {
|
||||
CmdResult::CommitHatch(self.make_hatch(rings))
|
||||
}
|
||||
|
|
@ -510,6 +604,10 @@ impl CadCommand for HatchCommand {
|
|||
self.missed = false;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"B" | "BOUNDARY" | "BOUNDARIES" => {
|
||||
self.retain_boundaries = !self.retain_boundaries;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -586,6 +684,7 @@ impl GradientCommand {
|
|||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -783,24 +882,7 @@ impl CadCommand for BoundaryCommand {
|
|||
match resolve_hatch_rings(&self.outlines, xy) {
|
||||
Some(rings) => {
|
||||
self.missed = false;
|
||||
// Store as a Hatch entity (solid fill) so it is selectable.
|
||||
let (rel, origin, wcs) = pack_rings(&rings);
|
||||
let model = HatchModel {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color: [0.45, 0.45, 0.45, 0.60],
|
||||
aci: 0,
|
||||
line_weight_px: 1.0,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: None,
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
CmdResult::CommitHatch(model)
|
||||
CmdResult::CommitEntitiesAndExit(crate::scene::boundary_entities(&rings))
|
||||
}
|
||||
None => {
|
||||
self.missed = true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use super::*;
|
||||
|
||||
use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance,
|
||||
};
|
||||
|
||||
/// How far apart two points may be and still be taken for the same one.
|
||||
///
|
||||
|
|
@ -35,7 +38,267 @@ fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
|||
segments
|
||||
}
|
||||
|
||||
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||
let mut ring = Vec::new();
|
||||
let mut index = 0usize;
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
if index == wanted {
|
||||
ring.push([
|
||||
model.world_origin[0] + x as f64,
|
||||
model.world_origin[1] + y as f64,
|
||||
]);
|
||||
}
|
||||
} else if index == wanted {
|
||||
break;
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if ring.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let (points, triangles) = triangulate(&ring, &[]);
|
||||
if let Some(triangle) = triangles.first() {
|
||||
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
||||
return Some([
|
||||
(a[0] + b[0] + c[0]) / 3.0,
|
||||
(a[1] + b[1] + c[1]) / 3.0,
|
||||
]);
|
||||
}
|
||||
Some([
|
||||
ring.iter().map(|point| point[0]).sum::<f64>() / ring.len() as f64,
|
||||
ring.iter().map(|point| point[1]).sum::<f64>() / ring.len() as f64,
|
||||
])
|
||||
}
|
||||
|
||||
fn face_curves(face: &[[f64; 2]]) -> Vec<Curve> {
|
||||
face.iter()
|
||||
.copied()
|
||||
.zip(face.iter().copied().cycle().skip(1))
|
||||
.take(face.len())
|
||||
.map(|(start, end)| Curve::Line(Line { start, end }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec<[f64; 2]>> {
|
||||
if faces.len() == 1 {
|
||||
return faces.first();
|
||||
}
|
||||
let seed = seed?;
|
||||
let tolerance = Tolerance::new(WELD_TOLERANCE);
|
||||
if let Some(face) = faces.iter().find(|face| {
|
||||
let curves = face_curves(face);
|
||||
contains(&curves, seed, tolerance)
|
||||
}) {
|
||||
return Some(face);
|
||||
}
|
||||
faces.iter().min_by(|a, b| {
|
||||
let nearest = |face: &Vec<[f64; 2]>| {
|
||||
face_curves(face)
|
||||
.iter()
|
||||
.map(|curve| distance_to(curve, seed))
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
};
|
||||
nearest(a).total_cmp(&nearest(b))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ring_source_handles(
|
||||
ring: &[[f64; 2]],
|
||||
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
|
||||
) -> Vec<acadrust::Handle> {
|
||||
let mut handles = rustc_hash::FxHashSet::default();
|
||||
for (&start, &end) in ring
|
||||
.iter()
|
||||
.zip(ring.iter().cycle().skip(1))
|
||||
.take(ring.len())
|
||||
{
|
||||
let edge = Line { start, end };
|
||||
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
|
||||
for (&handle, lines) in sources {
|
||||
if lines.iter().any(|line| {
|
||||
matches!(
|
||||
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
||||
SegmentCrossing::Overlap { a, .. }
|
||||
if (a[1] - a[0]).abs() * edge_length > WELD_TOLERANCE
|
||||
)
|
||||
}) {
|
||||
handles.insert(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut handles: Vec<_> = handles.into_iter().collect();
|
||||
handles.sort_by_key(|handle| handle.value());
|
||||
handles
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
||||
rings
|
||||
.iter()
|
||||
.filter_map(|ring| {
|
||||
let mut points = Vec::with_capacity(ring.len());
|
||||
for &point in ring {
|
||||
if point.iter().all(|value| value.is_finite())
|
||||
&& points.last() != Some(&point)
|
||||
{
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
if points.len() > 1 && points.first() == points.last() {
|
||||
points.pop();
|
||||
}
|
||||
if points.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let mut polyline = acadrust::entities::LwPolyline::new();
|
||||
polyline.is_closed = true;
|
||||
polyline.vertices = points
|
||||
.into_iter()
|
||||
.map(|[x, y]| {
|
||||
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
||||
})
|
||||
.collect();
|
||||
Some(acadrust::EntityType::LwPolyline(polyline))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
||||
self.wire_models_for(handles)
|
||||
.iter()
|
||||
.flat_map(wire_segments)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn associative_hatch_dependents(
|
||||
&self,
|
||||
changed: &rustc_hash::FxHashSet<Handle>,
|
||||
) -> Vec<Handle> {
|
||||
if self.associative_hatch_source_cache.borrow().is_none() {
|
||||
let mut index: HashMap<Handle, Vec<Handle>> = HashMap::default();
|
||||
for entity in self.document.entities() {
|
||||
let EntityType::Hatch(hatch) = entity else {
|
||||
continue;
|
||||
};
|
||||
if !hatch.is_associative {
|
||||
continue;
|
||||
}
|
||||
for source in hatch
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(|path| path.boundary_handles.iter().copied())
|
||||
{
|
||||
let dependents = index.entry(source).or_default();
|
||||
if !dependents.contains(&hatch.common.handle) {
|
||||
dependents.push(hatch.common.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
*self.associative_hatch_source_cache.borrow_mut() = Some(index);
|
||||
}
|
||||
let cache = self.associative_hatch_source_cache.borrow();
|
||||
let index = cache.as_ref().expect("associative hatch index");
|
||||
let mut handles = rustc_hash::FxHashSet::default();
|
||||
for source in changed {
|
||||
if let Some(dependents) = index.get(source) {
|
||||
handles.extend(dependents.iter().copied());
|
||||
}
|
||||
}
|
||||
handles.into_iter().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_associative_hatches(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
) -> Vec<(Handle, ChangeKind)> {
|
||||
if changes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let changed: rustc_hash::FxHashSet<_> =
|
||||
changes.iter().map(|(handle, _)| *handle).collect();
|
||||
let candidates: Vec<_> = self
|
||||
.associative_hatch_dependents(&changed)
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
let EntityType::Hatch(hatch) = self.document.get_entity(handle)? else {
|
||||
return None;
|
||||
};
|
||||
Some({
|
||||
let seeds = self
|
||||
.hatches
|
||||
.get(&hatch.common.handle)
|
||||
.map(|model| {
|
||||
(0..hatch.paths.len())
|
||||
.map(|index| ring_seed(model, index))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(handle, hatch.clone(), seeds)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut refreshed = Vec::new();
|
||||
for (handle, mut hatch, seeds) in candidates {
|
||||
let normal = hatch.normal;
|
||||
if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 {
|
||||
continue;
|
||||
}
|
||||
let mut modified = false;
|
||||
let mut association_changed = false;
|
||||
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
||||
if !path
|
||||
.boundary_handles
|
||||
.iter()
|
||||
.any(|source| changed.contains(source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let old_count = path.boundary_handles.len();
|
||||
path.boundary_handles
|
||||
.retain(|source| self.document.get_entity(*source).is_some());
|
||||
association_changed |= path.boundary_handles.len() != old_count;
|
||||
modified |= association_changed;
|
||||
let segments = self.associative_boundary_segments(&path.boundary_handles);
|
||||
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
|
||||
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
|
||||
continue;
|
||||
};
|
||||
path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline(
|
||||
acadrust::entities::hatch::PolylineEdge::new(
|
||||
face.iter()
|
||||
.map(|point| acadrust::types::Vector2::new(point[0], point[1]))
|
||||
.collect(),
|
||||
true,
|
||||
),
|
||||
)];
|
||||
modified = true;
|
||||
}
|
||||
hatch.is_associative = hatch
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
if !modified {
|
||||
continue;
|
||||
}
|
||||
if association_changed {
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
}
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity_arc(handle);
|
||||
self.record_undo_before(handle, before);
|
||||
}
|
||||
if let Some(slot) = self.document.get_entity_mut(handle) {
|
||||
*slot = EntityType::Hatch(hatch);
|
||||
self.refresh_fill_model(handle);
|
||||
refreshed.push((handle, ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
refreshed
|
||||
}
|
||||
|
||||
/// Build closed planar regions from the visible wire geometry.
|
||||
///
|
||||
/// Unlike `closed_outlines()`, the source entities do not need to be closed
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,7 @@ impl Scene {
|
|||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color,
|
||||
|
|
@ -1714,6 +1715,13 @@ impl Scene {
|
|||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(
|
||||
dxf.paths
|
||||
.iter()
|
||||
.filter(|path| path.flags.bits() & 8 == 0)
|
||||
.map(|path| path.boundary_handles.clone())
|
||||
.collect(),
|
||||
)),
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
|
|
@ -2029,6 +2037,7 @@ impl Scene {
|
|||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color,
|
||||
|
|
@ -2041,7 +2050,12 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_hatch(&mut self, model: HatchModel, layer: Option<&str>) -> Handle {
|
||||
pub fn add_hatch(
|
||||
&mut self,
|
||||
model: HatchModel,
|
||||
layer: Option<&str>,
|
||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||
) -> Handle {
|
||||
let mut dxf = DxfHatch::new();
|
||||
dxf.is_solid = matches!(
|
||||
model.pattern,
|
||||
|
|
@ -2074,7 +2088,7 @@ impl Scene {
|
|||
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| {
|
||||
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 mut path = if is_outer {
|
||||
|
|
@ -2088,6 +2102,17 @@ impl Scene {
|
|||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
if let Some(handles) = model
|
||||
.boundary_sources
|
||||
.as_deref()
|
||||
.and_then(|sources| sources.get(index))
|
||||
{
|
||||
for &handle in handles {
|
||||
if handle.is_valid() {
|
||||
path.add_boundary_handle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
|
|
@ -2101,11 +2126,11 @@ impl Scene {
|
|||
.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;
|
||||
}
|
||||
first = false;
|
||||
push_ring(&mut ring, is_outer);
|
||||
}
|
||||
}
|
||||
let is_outer = model
|
||||
|
|
@ -2114,29 +2139,56 @@ impl Scene {
|
|||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
dxf.is_associative = dxf
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
let pattern_scale = if model.scale.abs() > 1e-6 {
|
||||
model.scale as f64
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
|
||||
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
||||
if let Some(base) = dxf.pattern.lines.first().map(|line| line.base_point) {
|
||||
let angle = model.angle_offset as f64;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let target_x = model.world_origin[0]
|
||||
+ (base.x * cos - base.y * sin) * pattern_scale;
|
||||
let target_y = model.world_origin[1]
|
||||
+ (base.x * sin + base.y * cos) * pattern_scale;
|
||||
crate::entities::hatch::scale_pattern_geometry(&mut dxf.pattern, pattern_scale);
|
||||
crate::entities::hatch::rotate_pattern_geometry(&mut dxf.pattern, angle);
|
||||
crate::entities::hatch::translate_pattern_geometry(
|
||||
&mut dxf.pattern,
|
||||
target_x - base.x,
|
||||
target_y - base.y,
|
||||
);
|
||||
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;
|
||||
let (rotation_sin, rotation_cos) = rotation.sin_cos();
|
||||
for family in families {
|
||||
let family_angle = (family.angle_deg as f64).to_radians();
|
||||
let angle = family_angle + rotation;
|
||||
let (family_sin, family_cos) = family_angle.sin_cos();
|
||||
let local_offset_x = family.dx as f64 * family_cos
|
||||
- family.dy as f64 * family_sin;
|
||||
let local_offset_y = family.dx as f64 * family_sin
|
||||
+ family.dy as f64 * family_cos;
|
||||
let base_x = family.x0 as f64 * pattern_scale;
|
||||
let base_y = family.y0 as f64 * pattern_scale;
|
||||
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
||||
angle,
|
||||
base_point: Vector2::new(
|
||||
model.world_origin[0]
|
||||
+ base_x * rotation_cos
|
||||
- base_y * rotation_sin,
|
||||
model.world_origin[1]
|
||||
+ base_x * rotation_sin
|
||||
+ base_y * rotation_cos,
|
||||
),
|
||||
offset: Vector2::new(
|
||||
(local_offset_x * rotation_cos
|
||||
- local_offset_y * rotation_sin)
|
||||
* pattern_scale,
|
||||
(local_offset_x * rotation_sin
|
||||
+ local_offset_y * rotation_cos)
|
||||
* pattern_scale,
|
||||
),
|
||||
dash_lengths: family
|
||||
.dashes
|
||||
.iter()
|
||||
.map(|dash| *dash as f64 * pattern_scale)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
dxf.pattern = pattern;
|
||||
}
|
||||
dxf.pattern_angle = model.angle_offset as f64;
|
||||
dxf.pattern_scale = pattern_scale;
|
||||
|
|
@ -2195,6 +2247,10 @@ impl Scene {
|
|||
if let Some(layer) = layer {
|
||||
entity.as_entity_mut().set_layer(layer.to_string());
|
||||
}
|
||||
if let Some((color, transparency)) = entity_style {
|
||||
entity.common_mut().color = color;
|
||||
entity.common_mut().transparency = transparency;
|
||||
}
|
||||
|
||||
self.add_entity(entity)
|
||||
}
|
||||
|
|
@ -2207,6 +2263,7 @@ impl Scene {
|
|||
self.preview_text = vec![];
|
||||
self.current_layout = "Model".to_string();
|
||||
self.hatches = HashMap::default();
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
self.images = HashMap::default();
|
||||
self.meshes = HashMap::default();
|
||||
self.block_meshes = HashMap::default();
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ mod project;
|
|||
mod scene_markers;
|
||||
mod selection;
|
||||
|
||||
pub(crate) use boundary::{boundary_entities, ring_source_handles};
|
||||
|
||||
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||
// other tessellation code); re-exported here so this root and sibling topic
|
||||
// modules (each does `use super::*`) keep referencing them unqualified.
|
||||
|
|
@ -1677,6 +1679,9 @@ pub struct Scene {
|
|||
/// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate
|
||||
/// only its dependants, and avoid a whole-document scan on every toggle.
|
||||
dependency_index_cache: RefCell<Option<SceneDependencyIndex>>,
|
||||
/// Boundary source → associative hatch handles. Source edits reuse this
|
||||
/// index instead of scanning the document during every drag step.
|
||||
associative_hatch_source_cache: RefCell<Option<HashMap<Handle, Vec<Handle>>>>,
|
||||
/// Tessellated block definitions in block-local coords, keyed by render
|
||||
/// background and block epoch. Model and Paper adapt black/white colours
|
||||
/// differently; retaining both variants prevents a full block rebuild on
|
||||
|
|
@ -1893,6 +1898,7 @@ impl Scene {
|
|||
model_extents_cache: RefCell::new(None),
|
||||
entity_block_map_cache: RefCell::new(None),
|
||||
dependency_index_cache: RefCell::new(None),
|
||||
associative_hatch_source_cache: RefCell::new(None),
|
||||
block_defn_cache: RefCell::new(HashMap::default()),
|
||||
entity_index_cache: RefCell::new(None),
|
||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||
|
|
@ -2368,6 +2374,21 @@ impl Scene {
|
|||
}
|
||||
|
||||
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
|
||||
if changes.iter().any(|(handle, kind)| {
|
||||
matches!(kind, ChangeKind::Removed)
|
||||
|| self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Hatch(_)))
|
||||
}) {
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
}
|
||||
let mut changes = changes.to_vec();
|
||||
for change in self.refresh_associative_hatches(&changes) {
|
||||
if !changes.iter().any(|(handle, _)| *handle == change.0) {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
// A Modified delta can change layer/style/block references as well
|
||||
// as coordinates. Rebuild the reverse dependency map lazily on its
|
||||
|
|
@ -2386,14 +2407,14 @@ impl Scene {
|
|||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
|
||||
});
|
||||
if cached_light_changed || live_light_changed {
|
||||
for (handle, _) in changes {
|
||||
for &(handle, _) in &changes {
|
||||
let exists = self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)));
|
||||
crate::entities::object_data::update_light_entity(
|
||||
&mut self.object_data_cache,
|
||||
*handle,
|
||||
handle,
|
||||
exists,
|
||||
);
|
||||
}
|
||||
|
|
@ -2404,7 +2425,7 @@ impl Scene {
|
|||
{
|
||||
let mut tm = self.tess_memo.borrow_mut();
|
||||
let mut rm = self.resident_tess_memo.borrow_mut();
|
||||
for &(h, k) in changes {
|
||||
for &(h, k) in &changes {
|
||||
// A changed or removed entity must re-tessellate; a fresh Add has
|
||||
// no memo entry yet, so there is nothing to drop.
|
||||
if matches!(k, ChangeKind::Modified | ChangeKind::Removed) {
|
||||
|
|
@ -2413,7 +2434,7 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.push_geometry_delta(epoch, changes.to_vec(), false);
|
||||
self.push_geometry_delta(epoch, changes, false);
|
||||
}
|
||||
|
||||
/// True when any changed handle belongs to an ordinary block definition
|
||||
|
|
@ -3182,6 +3203,7 @@ impl Scene {
|
|||
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".to_string(),
|
||||
color: self.paper_bg_color,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ pub struct HatchModel {
|
|||
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>>>,
|
||||
/// Source entity handles for each boundary ring.
|
||||
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
||||
/// Fill pattern.
|
||||
pub pattern: HatchPattern,
|
||||
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ impl Scene {
|
|||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "AREA_PREVIEW".into(),
|
||||
color: [0.0; 4],
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ impl canvas::Program<Message> for HatchPatternPreview {
|
|||
]),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: self.pattern.clone(),
|
||||
name: String::new(),
|
||||
color: [1.0; 4],
|
||||
|
|
|
|||
Loading…
Reference in a new issue