feat(hatch): add associative boundaries

This commit is contained in:
Hakan Seven committed 2026-08-13 23:47:50 +03:00
1 parent 3d2848021f
commit 1d2f0a09c1
10 files changed
+577 -49

No files matched your search

+80 -2
View File
@@ -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 {
+10 -2
View File
@@ -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);
+14
View File
@@ -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.
+100 -18
View File
@@ -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;
+264 -1
View File
@@ -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
+78 -21
View File
@@ -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();
+27 -5
View File
@@ -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,
+2
View File
@@ -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").
+1
View File
@@ -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],
+1
View File
@@ -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],