Merge pull request #852 from ramox81/codex/complete-hatch-workflow
This commit is contained in:
commit
bd5f9436ea
18 changed files with 1535 additions and 479 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=90922c8#90922c8cf0b6c77f1db01edac56b37ad59f8fc76"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f36bdf6#f36bdf6c967dcdb8d0e740869527090a569dae1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
|
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "90922c8", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f36bdf6", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|
|
||||||
|
|
@ -1375,6 +1375,13 @@ impl OpenCADStudio {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sources.push(handles);
|
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));
|
hatch.boundary_sources = Some(std::sync::Arc::new(sources));
|
||||||
let layer = self.tabs[i].active_layer.clone();
|
let layer = self.tabs[i].active_layer.clone();
|
||||||
let new_handle =
|
let new_handle =
|
||||||
|
|
@ -1394,6 +1401,33 @@ impl OpenCADStudio {
|
||||||
self.commit_undo_delta(i, pd);
|
self.commit_undo_delta(i, pd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CmdResult::CommitHatches {
|
||||||
|
hatches,
|
||||||
|
entity_style,
|
||||||
|
} => {
|
||||||
|
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||||
|
let pending = self.begin_undo(i, label, hatches.len(), true);
|
||||||
|
let layer = self.tabs[i].active_layer.clone();
|
||||||
|
for hatch in hatches {
|
||||||
|
let new_handle = self.tabs[i].scene.add_hatch(
|
||||||
|
hatch,
|
||||||
|
Some(&layer),
|
||||||
|
entity_style.clone(),
|
||||||
|
);
|
||||||
|
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::BatchCopy(mut handles, transforms) => {
|
CmdResult::BatchCopy(mut handles, transforms) => {
|
||||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
|
|
@ -1612,10 +1646,7 @@ impl OpenCADStudio {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|e| self.tabs[i].scene.add_entity(e))
|
.map(|e| self.tabs[i].scene.add_entity(e))
|
||||||
.collect();
|
.collect();
|
||||||
// A replaced dimension carries edited geometry/text but still
|
// Rebuild replaced dimensions from edited data.
|
||||||
// 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)
|
|
||||||
for &nh in &new_handles {
|
for &nh in &new_handles {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.tabs[i].scene.document.get_entity(nh),
|
self.tabs[i].scene.document.get_entity(nh),
|
||||||
|
|
@ -1925,11 +1956,6 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
// The command stays active after each apply so more targets
|
// The command stays active after each apply so more targets
|
||||||
// can keep being picked; Enter / Esc ends it (#362).
|
// 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_clone = self.tabs[i].scene.document.get_entity(src).cloned();
|
||||||
let src_common = src_clone.as_ref().map(|e| e.common().clone());
|
let src_common = src_clone.as_ref().map(|e| e.common().clone());
|
||||||
let thickness = src_clone
|
let thickness = src_clone
|
||||||
|
|
@ -3223,42 +3249,218 @@ impl OpenCADStudio {
|
||||||
name,
|
name,
|
||||||
scale,
|
scale,
|
||||||
angle,
|
angle,
|
||||||
|
operation,
|
||||||
} => {
|
} => {
|
||||||
if self.reject_locked_edit(i, handle) {
|
if self.reject_locked_edit(i, handle) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if let Some(mut model) = self.tabs[i].scene.hatches.get(&handle).cloned() {
|
if !matches!(
|
||||||
let layer = self.tabs[i]
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
.scene
|
Some(acadrust::EntityType::Hatch(_))
|
||||||
.document
|
) {
|
||||||
.get_entity(handle)
|
|
||||||
.map(|entity| entity.as_entity().layer().to_string())
|
|
||||||
.unwrap_or_else(|| "0".to_string());
|
|
||||||
// Update model fields
|
|
||||||
if !name.is_empty() {
|
|
||||||
use crate::scene::model::hatch_model::HatchPattern;
|
|
||||||
use crate::scene::model::hatch_patterns;
|
|
||||||
model.name = name.clone();
|
|
||||||
if name.to_uppercase() == "SOLID" {
|
|
||||||
model.pattern = HatchPattern::Solid;
|
|
||||||
} else if let Some(entry) = hatch_patterns::find(&name) {
|
|
||||||
model.pattern = entry.gpu.clone();
|
|
||||||
}
|
|
||||||
// If not found in catalog, keep existing pattern type
|
|
||||||
}
|
|
||||||
model.scale = scale;
|
|
||||||
model.angle_offset = angle;
|
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "HATCHEDIT");
|
|
||||||
// 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), None);
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
|
|
||||||
} else {
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref());
|
.push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref());
|
||||||
|
} else {
|
||||||
|
use crate::command::HatchEditOperation;
|
||||||
|
if matches!(
|
||||||
|
&operation,
|
||||||
|
HatchEditOperation::DrawOrderFront | HatchEditOperation::DrawOrderBack
|
||||||
|
) {
|
||||||
|
let command = if matches!(&operation, HatchEditOperation::DrawOrderFront) {
|
||||||
|
"DRAWORDER FRONT"
|
||||||
|
} else {
|
||||||
|
"DRAWORDER BACK"
|
||||||
|
};
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
return self.dispatch_view(command, i).unwrap_or_else(Task::none);
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot(i, "HATCHEDIT");
|
||||||
|
match operation {
|
||||||
|
HatchEditOperation::Update {
|
||||||
|
origin,
|
||||||
|
disassociate,
|
||||||
|
style,
|
||||||
|
annotative,
|
||||||
|
} => {
|
||||||
|
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||||
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
if !name.is_empty() && name != hatch.pattern.name {
|
||||||
|
if let Some(entry) =
|
||||||
|
crate::scene::model::hatch_patterns::find(&name)
|
||||||
|
{
|
||||||
|
let old_origin = hatch
|
||||||
|
.pattern
|
||||||
|
.lines
|
||||||
|
.first()
|
||||||
|
.map(|line| line.base_point);
|
||||||
|
let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
||||||
|
crate::entities::hatch::scale_pattern_geometry(
|
||||||
|
&mut pattern,
|
||||||
|
scale.max(1.0e-6) as f64,
|
||||||
|
);
|
||||||
|
crate::entities::hatch::rotate_pattern_geometry(
|
||||||
|
&mut pattern,
|
||||||
|
(angle as f64).to_radians(),
|
||||||
|
);
|
||||||
|
if let (Some(old), Some(new)) = (
|
||||||
|
old_origin,
|
||||||
|
pattern.lines.first().map(|line| line.base_point),
|
||||||
|
) {
|
||||||
|
crate::entities::hatch::translate_pattern_geometry(
|
||||||
|
&mut pattern,
|
||||||
|
old.x - new.x,
|
||||||
|
old.y - new.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hatch.pattern = pattern;
|
||||||
|
hatch.is_solid = matches!(
|
||||||
|
entry.gpu,
|
||||||
|
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||||
|
);
|
||||||
|
hatch.pattern_type =
|
||||||
|
acadrust::entities::HatchPatternType::Predefined;
|
||||||
|
hatch.gradient_color.enabled = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let requested_scale = scale.max(1.0e-6) as f64;
|
||||||
|
if hatch.pattern_scale > 1.0e-12 {
|
||||||
|
let factor = requested_scale / hatch.pattern_scale;
|
||||||
|
crate::entities::hatch::scale_pattern_geometry(
|
||||||
|
&mut hatch.pattern,
|
||||||
|
factor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let requested_angle = (angle as f64).to_radians();
|
||||||
|
let delta = requested_angle - hatch.pattern_angle;
|
||||||
|
crate::entities::hatch::rotate_pattern_geometry(
|
||||||
|
&mut hatch.pattern,
|
||||||
|
delta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hatch.pattern_scale = scale.max(1.0e-6) as f64;
|
||||||
|
hatch.pattern_angle = (angle as f64).to_radians();
|
||||||
|
if let Some((x, y)) = origin {
|
||||||
|
if let Some(current) =
|
||||||
|
hatch.pattern.lines.first().map(|line| line.base_point)
|
||||||
|
{
|
||||||
|
crate::entities::hatch::translate_pattern_geometry(
|
||||||
|
&mut hatch.pattern,
|
||||||
|
x - current.x,
|
||||||
|
y - current.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disassociate {
|
||||||
|
for path in &mut hatch.paths {
|
||||||
|
path.boundary_handles.clear();
|
||||||
|
path.flags.set_external(false);
|
||||||
|
}
|
||||||
|
hatch.is_associative = false;
|
||||||
|
}
|
||||||
|
if let Some(style) = style {
|
||||||
|
hatch.style = style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(value) = annotative {
|
||||||
|
crate::scene::annotative::set_entity_annotative(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
handle,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
if value {
|
||||||
|
if let Some(scale_handle) =
|
||||||
|
self.tabs[i].scene.creation_annotation_scale_handle()
|
||||||
|
{
|
||||||
|
crate::scene::annotative::create_annotation_context(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
handle,
|
||||||
|
scale_handle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_entities(&[(
|
||||||
|
handle,
|
||||||
|
crate::scene::ChangeKind::Modified,
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
HatchEditOperation::AddBoundaries(handles) => {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.edit_hatch_boundary_handles(handle, &handles, true);
|
||||||
|
}
|
||||||
|
HatchEditOperation::RemoveBoundaries(handles) => {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.edit_hatch_boundary_handles(handle, &handles, false);
|
||||||
|
}
|
||||||
|
HatchEditOperation::RecreateBoundary => {
|
||||||
|
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) {
|
||||||
|
handles.push(boundary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||||
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
for (path, boundary) in
|
||||||
|
hatch.paths.iter_mut().zip(handles.iter().copied())
|
||||||
|
{
|
||||||
|
path.boundary_handles = vec![boundary];
|
||||||
|
path.flags.set_external(true);
|
||||||
|
}
|
||||||
|
hatch.is_associative = !handles.is_empty();
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_entities(&[(
|
||||||
|
handle,
|
||||||
|
crate::scene::ChangeKind::Modified,
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HatchEditOperation::Separate => {
|
||||||
|
let source = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||||
|
if let Some(acadrust::EntityType::Hatch(hatch)) = source {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HatchEditOperation::DrawOrderFront
|
||||||
|
| HatchEditOperation::DrawOrderBack => unreachable!(),
|
||||||
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
|
||||||
}
|
}
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
|
||||||
|
|
@ -691,8 +691,26 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
"HATCH" => {
|
"HATCH" => {
|
||||||
use crate::modules::draw::draw::hatch::HatchCommand;
|
use crate::modules::draw::draw::hatch::HatchCommand;
|
||||||
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
|
let working_plane = if self.tabs[i].editing_model_space() {
|
||||||
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
|
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(plane, 1.0e-6);
|
||||||
|
let outlines = crate::scene::boundary_faces(&boundary_sources, 1.0e-6);
|
||||||
let selected = self.tabs[i]
|
let selected = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.selected_entities()
|
.selected_entities()
|
||||||
|
|
@ -706,8 +724,13 @@ impl OpenCADStudio {
|
||||||
let common = self.tabs[i].scene.document.get_entity(*handle)?.common();
|
let common = self.tabs[i].scene.document.get_entity(*handle)?.common();
|
||||||
Some((model, common.color.clone(), common.transparency))
|
Some((model, common.color.clone(), common.transparency))
|
||||||
});
|
});
|
||||||
let new_cmd =
|
let new_cmd = HatchCommand::new(
|
||||||
HatchCommand::new(outlines, boundary_sources, selected, inherited);
|
outlines,
|
||||||
|
boundary_sources,
|
||||||
|
selected,
|
||||||
|
inherited,
|
||||||
|
plane,
|
||||||
|
);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
self.refresh_area_preview(i);
|
self.refresh_area_preview(i);
|
||||||
|
|
@ -720,11 +743,26 @@ impl OpenCADStudio {
|
||||||
if sel.len() == 1 {
|
if sel.len() == 1 {
|
||||||
let (h, _) = sel[0];
|
let (h, _) = sel[0];
|
||||||
if let Some(model) = self.tabs[i].scene.hatches.get(&h).cloned() {
|
if let Some(model) = self.tabs[i].scene.hatches.get(&h).cloned() {
|
||||||
|
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(
|
let cmd = HatcheditCommand::with_handle(
|
||||||
h,
|
h,
|
||||||
model.name.clone(),
|
model.name.clone(),
|
||||||
model.scale,
|
scale,
|
||||||
model.angle_offset,
|
angle,
|
||||||
|
annotative,
|
||||||
);
|
);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
|
|
@ -741,8 +779,10 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
"GRADIENT" => {
|
"GRADIENT" => {
|
||||||
use crate::modules::draw::draw::hatch::GradientCommand;
|
use crate::modules::draw::draw::hatch::GradientCommand;
|
||||||
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
|
let boundary_sources = self.tabs[i]
|
||||||
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
|
.scene
|
||||||
|
.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6);
|
||||||
|
let outlines = crate::scene::boundary_faces(&boundary_sources, 1.0e-6);
|
||||||
let new_cmd = GradientCommand::new(outlines, boundary_sources);
|
let new_cmd = GradientCommand::new(outlines, boundary_sources);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(crate) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
"DONATE" => {
|
"DONATE" => {
|
||||||
self.command_line.push_info(crate::t!("Opening Patreon page...").as_ref());
|
self.command_line.push_info(crate::t!("Opening Patreon page...").as_ref());
|
||||||
|
|
|
||||||
|
|
@ -2092,6 +2092,23 @@ pub(super) fn aggregate_sections(
|
||||||
for sections in all_sections {
|
for sections in all_sections {
|
||||||
result = merge_sections(&result, §ions);
|
result = merge_sections(&result, §ions);
|
||||||
}
|
}
|
||||||
|
// Sum the filled area while individual Area rows may still vary.
|
||||||
|
if selected.len() > 1
|
||||||
|
&& selected
|
||||||
|
.iter()
|
||||||
|
.all(|(_, entity)| matches!(entity, acadrust::EntityType::Hatch(_)))
|
||||||
|
{
|
||||||
|
let total = selected
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, entity)| match entity {
|
||||||
|
acadrust::EntityType::Hatch(hatch) => {
|
||||||
|
Some(crate::entities::hatch::boundary_area(hatch))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.sum::<f64>();
|
||||||
|
set_row(&mut result, "cumulative_area", format!("{total:.4}"));
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1726,6 +1726,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
entry.gpu,
|
entry.gpu,
|
||||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||||
);
|
);
|
||||||
|
dxf.pattern_type =
|
||||||
|
acadrust::entities::HatchPatternType::Predefined;
|
||||||
|
dxf.gradient_color.enabled = false;
|
||||||
}
|
}
|
||||||
if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) {
|
if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) {
|
||||||
model.pattern = entry.gpu.clone();
|
model.pattern = entry.gpu.clone();
|
||||||
|
|
|
||||||
|
|
@ -3490,13 +3490,28 @@ impl OpenCADStudio {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
if let Some(model) = self.tabs[i].scene.hatches.get(&handle).cloned() {
|
if let Some(model) = self.tabs[i].scene.hatches.get(&handle).cloned() {
|
||||||
|
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::command::CadCommand;
|
||||||
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
||||||
let cmd: Box<dyn CadCommand> = Box::new(HatcheditCommand::with_handle(
|
let cmd: Box<dyn CadCommand> = Box::new(HatcheditCommand::with_handle(
|
||||||
handle,
|
handle,
|
||||||
model.name.clone(),
|
model.name.clone(),
|
||||||
model.scale,
|
scale,
|
||||||
model.angle_offset,
|
angle,
|
||||||
|
annotative,
|
||||||
));
|
));
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(cmd);
|
self.tabs[i].active_cmd = Some(cmd);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,22 @@ use crate::scene::Scene;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum HatchEditOperation {
|
||||||
|
Update {
|
||||||
|
origin: Option<(f64, f64)>,
|
||||||
|
disassociate: bool,
|
||||||
|
style: Option<acadrust::entities::HatchStyleType>,
|
||||||
|
annotative: Option<bool>,
|
||||||
|
},
|
||||||
|
RecreateBoundary,
|
||||||
|
Separate,
|
||||||
|
AddBoundaries(Vec<Handle>),
|
||||||
|
RemoveBoundaries(Vec<Handle>),
|
||||||
|
DrawOrderFront,
|
||||||
|
DrawOrderBack,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Working plane ─────────────────────────────────────────────────────────
|
// ── Working plane ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Full-precision coordinate frame used by interactive commands.
|
/// Full-precision coordinate frame used by interactive commands.
|
||||||
|
|
@ -1197,6 +1213,11 @@ pub enum CmdResult {
|
||||||
boundaries: Vec<EntityType>,
|
boundaries: Vec<EntityType>,
|
||||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||||
},
|
},
|
||||||
|
/// Commit independently editable hatch entities for every selected region.
|
||||||
|
CommitHatches {
|
||||||
|
hatches: Vec<HatchModel>,
|
||||||
|
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||||
|
},
|
||||||
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
|
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
|
||||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||||
/// Erase `handle` and replace with new entities; command stays active.
|
/// Erase `handle` and replace with new entities; command stays active.
|
||||||
|
|
@ -1344,6 +1365,7 @@ pub enum CmdResult {
|
||||||
name: String,
|
name: String,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
angle: f32,
|
angle: f32,
|
||||||
|
operation: HatchEditOperation,
|
||||||
},
|
},
|
||||||
/// STRETCH crossing-window selection. The command can accumulate several
|
/// STRETCH crossing-window selection. The command can accumulate several
|
||||||
/// independent crossing windows before Enter ends the selection stage.
|
/// independent crossing windows before Enter ends the selection stage.
|
||||||
|
|
|
||||||
|
|
@ -14,41 +14,40 @@ use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Pr
|
||||||
use crate::scene::convert::tess_util::FallbackGeometry;
|
use crate::scene::convert::tess_util::FallbackGeometry;
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
|
|
||||||
/// The area the hatch's boundary paths enclose.
|
/// The area enclosed by the hatch boundary paths.
|
||||||
///
|
pub(crate) fn boundary_area(h: &Hatch) -> f64 {
|
||||||
/// Summed edge by edge through the kernel, which measures what each edge
|
let mut path_areas = Vec::new();
|
||||||
/// actually encloses rather than what a polygon through some of its points
|
let mut rings = Vec::new();
|
||||||
/// 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.
|
|
||||||
fn boundary_area(h: &Hatch) -> f64 {
|
|
||||||
let mut area = 0.0;
|
|
||||||
for path in &h.paths {
|
for path in &h.paths {
|
||||||
let mut path_area = 0.0;
|
let mut path_area = 0.0;
|
||||||
let mut ends: Vec<[f64; 2]> = Vec::new();
|
let directions = crate::scene::hatch_path_directions(path);
|
||||||
for edge in &path.edges {
|
let curves = path.edges.iter().filter_map(edge_curve);
|
||||||
let Some(curve) = edge_curve(edge) else {
|
for (curve, direction) in curves.zip(directions) {
|
||||||
continue;
|
path_area += direction * curve.enclosed_area();
|
||||||
};
|
|
||||||
path_area += curve.enclosed_area();
|
|
||||||
ends.push(curve.point_at(0.0));
|
|
||||||
ends.push(curve.point_at(1.0));
|
|
||||||
}
|
}
|
||||||
// Edges are stored as separate pieces, so the chain has to be closed
|
path_areas.push(path_area.abs());
|
||||||
// by the chord from the last end back to the first — the same closing
|
rings.push(crate::scene::hatch_path_ring(path).unwrap_or_default());
|
||||||
// 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]);
|
|
||||||
}
|
|
||||||
area += path_area.abs();
|
|
||||||
}
|
}
|
||||||
area
|
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||||
|
path_areas
|
||||||
|
.into_iter()
|
||||||
|
.zip(depths)
|
||||||
|
.filter_map(|(area, depth)| match h.style {
|
||||||
|
acadrust::entities::HatchStyleType::Normal => {
|
||||||
|
Some(if depth % 2 == 0 { area } else { -area })
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchStyleType::Outer if depth <= 1 => {
|
||||||
|
Some(if depth == 0 { area } else { -area })
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchStyleType::Outer => None,
|
||||||
|
acadrust::entities::HatchStyleType::Ignore if depth == 0 => Some(area),
|
||||||
|
acadrust::entities::HatchStyleType::Ignore => None,
|
||||||
|
})
|
||||||
|
.sum::<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> {
|
pub(crate) fn edge_curve(edge: &BoundaryEdge) -> Option<KernelCurve> {
|
||||||
Some(match edge {
|
Some(match edge {
|
||||||
BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine {
|
BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine {
|
||||||
|
|
@ -461,14 +460,18 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
||||||
|
|
||||||
|
|
||||||
// ── Hatch (pattern / solid) ────────────────────────────────────────────
|
// ── Hatch (pattern / solid) ────────────────────────────────────────────
|
||||||
// "Type" = pattern definition source (Predefined / User Defined / Custom).
|
// Show only controls used by the selected fill type.
|
||||||
let type_row = Property {
|
let type_row = if h.is_solid {
|
||||||
label: t!("Type").into_owned(),
|
ro(t!("Type").as_ref(), "fill_kind", t!("Solid").into_owned())
|
||||||
field: "pattern_type_label",
|
} else {
|
||||||
value: PropValue::Choice {
|
Property {
|
||||||
selected: pattern_type.to_string(),
|
label: t!("Type").into_owned(),
|
||||||
options: vec!["Predefined".into(), "User Defined".into(), "Custom".into()],
|
field: "pattern_type_label",
|
||||||
},
|
value: PropValue::Choice {
|
||||||
|
selected: pattern_type.to_string(),
|
||||||
|
options: vec!["Predefined".into(), "User Defined".into(), "Custom".into()],
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let pattern_name_row = Property {
|
let pattern_name_row = Property {
|
||||||
label: t!("Pattern name").into_owned(),
|
label: t!("Pattern name").into_owned(),
|
||||||
|
|
@ -492,47 +495,51 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
||||||
options: vec!["Normal".into(), "Outer".into(), "Ignore".into()],
|
options: vec!["Normal".into(), "Outer".into(), "Ignore".into()],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let spacing_row = edit(t!("Spacing").as_ref(),
|
let spacing_row = edit(t!("Spacing").as_ref(), "spacing", h.pattern_scale);
|
||||||
"spacing",
|
|
||||||
h.pattern
|
let mut pattern_props = vec![type_row, pattern_name_row];
|
||||||
.lines
|
pattern_props.push(ro(t!("Annotative").as_ref(), "annotative", String::new()));
|
||||||
.first()
|
if !h.is_solid {
|
||||||
.map(|l| l.offset.length())
|
pattern_props.push(edit_angle(
|
||||||
.unwrap_or_default(),
|
t!("Angle").as_ref(),
|
||||||
);
|
"pattern_angle",
|
||||||
// Pattern tiling origin: the base point the pattern lines are anchored to.
|
h.pattern_angle.to_degrees(),
|
||||||
let (origin_x, origin_y) = h
|
));
|
||||||
.pattern
|
if matches!(
|
||||||
.lines
|
h.pattern_type,
|
||||||
.first()
|
acadrust::entities::HatchPatternType::UserDefined
|
||||||
.map(|l| (l.base_point.x, l.base_point.y))
|
) {
|
||||||
.unwrap_or((0.0, 0.0));
|
pattern_props.push(spacing_row);
|
||||||
|
pattern_props.push(double_row);
|
||||||
|
} else {
|
||||||
|
pattern_props.push(edit(t!("Scale").as_ref(), "pattern_scale", h.pattern_scale));
|
||||||
|
// 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") {
|
||||||
|
pattern_props.push(edit(
|
||||||
|
t!("ISO pen width").as_ref(),
|
||||||
|
"iso_pen_width",
|
||||||
|
h.pattern_scale,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pattern_props.push(associative_row);
|
||||||
|
pattern_props.push(island_row);
|
||||||
|
pattern_props.push(Property {
|
||||||
|
label: t!("Background").into_owned(),
|
||||||
|
field: "bg_enabled",
|
||||||
|
value: PropValue::BoolToggle {
|
||||||
|
field: "bg_enabled",
|
||||||
|
value: bg_on,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let mut sections = vec![
|
let mut sections = vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Pattern").into_owned(),
|
title: t!("Pattern").into_owned(),
|
||||||
props: vec![
|
props: pattern_props,
|
||||||
type_row,
|
|
||||||
pattern_name_row,
|
|
||||||
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
|
||||||
edit_angle(t!("Angle").as_ref(), "pattern_angle", h.pattern_angle.to_degrees()),
|
|
||||||
edit(t!("Scale").as_ref(), "pattern_scale", h.pattern_scale),
|
|
||||||
edit(t!("Origin X").as_ref(), "origin_x", origin_x),
|
|
||||||
edit(t!("Origin Y").as_ref(), "origin_y", origin_y),
|
|
||||||
spacing_row,
|
|
||||||
ro(t!("ISO pen width").as_ref(), "iso_pen_width", String::new()),
|
|
||||||
double_row,
|
|
||||||
associative_row,
|
|
||||||
island_row,
|
|
||||||
Property {
|
|
||||||
label: t!("Background").into_owned(),
|
|
||||||
field: "bg_enabled",
|
|
||||||
value: PropValue::BoolToggle {
|
|
||||||
field: "bg_enabled",
|
|
||||||
value: bg_on,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
|
|
@ -599,12 +606,44 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"pattern_type_label" => {
|
"pattern_type_label" => {
|
||||||
h.pattern_type = match value {
|
let requested = match value {
|
||||||
"Predefined" => HatchPatternType::Predefined,
|
"Predefined" => HatchPatternType::Predefined,
|
||||||
"User Defined" => HatchPatternType::UserDefined,
|
"User Defined" => HatchPatternType::UserDefined,
|
||||||
"Custom" => HatchPatternType::Custom,
|
"Custom" => HatchPatternType::Custom,
|
||||||
_ => h.pattern_type,
|
_ => h.pattern_type,
|
||||||
};
|
};
|
||||||
|
if requested != h.pattern_type {
|
||||||
|
let old_origin = h.pattern.lines.first().map(|line| line.base_point);
|
||||||
|
h.pattern_type = requested;
|
||||||
|
h.is_solid = false;
|
||||||
|
match requested {
|
||||||
|
HatchPatternType::UserDefined => {
|
||||||
|
// Rebuild user-defined geometry from its parameters.
|
||||||
|
h.pattern = acadrust::entities::HatchPattern::new("_USER");
|
||||||
|
}
|
||||||
|
HatchPatternType::Predefined => {
|
||||||
|
if let Some(entry) =
|
||||||
|
crate::scene::model::hatch_patterns::find("ANSI31")
|
||||||
|
{
|
||||||
|
let mut pattern =
|
||||||
|
crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
||||||
|
scale_pattern_geometry(&mut pattern, h.pattern_scale);
|
||||||
|
rotate_pattern_geometry(&mut pattern, h.pattern_angle);
|
||||||
|
if let (Some(old), Some(new)) =
|
||||||
|
(old_origin, pattern.lines.first().map(|line| line.base_point))
|
||||||
|
{
|
||||||
|
translate_pattern_geometry(
|
||||||
|
&mut pattern,
|
||||||
|
old.x - new.x,
|
||||||
|
old.y - new.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
h.pattern = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HatchPatternType::Custom => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"style" => {
|
"style" => {
|
||||||
|
|
@ -674,38 +713,33 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||||
// Scale every pattern line's offset so the first line's spacing = v,
|
// Scale every pattern line's offset so the first line's spacing = v,
|
||||||
// preserving the relative spacing between lines.
|
// preserving the relative spacing between lines.
|
||||||
"spacing" if v > 0.0 => {
|
"spacing" if v > 0.0 => {
|
||||||
let cur = h
|
h.pattern_scale = v;
|
||||||
.pattern
|
if matches!(
|
||||||
.lines
|
h.pattern_type,
|
||||||
.first()
|
acadrust::entities::HatchPatternType::UserDefined
|
||||||
.map(|l| l.offset.length())
|
) {
|
||||||
.unwrap_or(0.0);
|
h.pattern.lines.clear();
|
||||||
if cur > 1e-9 {
|
h.pattern.name = "_USER".to_string();
|
||||||
let s = v / cur;
|
|
||||||
for line in h.pattern.lines.iter_mut() {
|
|
||||||
line.offset.x *= s;
|
|
||||||
line.offset.y *= s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Move the pattern origin: shift every line's base point by the delta
|
// Origin rows are relative offsets and return to zero after commit.
|
||||||
// from the current origin (first line), preserving their relative offsets.
|
|
||||||
"origin_x" => {
|
"origin_x" => {
|
||||||
if let Some(cur) = h.pattern.lines.first().map(|l| l.base_point.x) {
|
for line in h.pattern.lines.iter_mut() {
|
||||||
let d = v - cur;
|
line.base_point.x += v;
|
||||||
for line in h.pattern.lines.iter_mut() {
|
|
||||||
line.base_point.x += d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"origin_y" => {
|
"origin_y" => {
|
||||||
if let Some(cur) = h.pattern.lines.first().map(|l| l.base_point.y) {
|
for line in h.pattern.lines.iter_mut() {
|
||||||
let d = v - cur;
|
line.base_point.y += v;
|
||||||
for line in h.pattern.lines.iter_mut() {
|
|
||||||
line.base_point.y += d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"iso_pen_width" if v > 0.0 => {
|
||||||
|
let old = h.pattern_scale;
|
||||||
|
if old > 1e-12 {
|
||||||
|
scale_pattern_geometry(&mut h.pattern, v / old);
|
||||||
|
}
|
||||||
|
h.pattern_scale = v;
|
||||||
|
}
|
||||||
"elevation" => h.elevation = v,
|
"elevation" => h.elevation = v,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -713,13 +747,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||||
|
|
||||||
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
||||||
crate::scene::view::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
crate::scene::view::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
||||||
// Delegate the mirror to acadrust's transform_hatch (via the Entity
|
// Keep boundary directions, angles, and sweeps consistent.
|
||||||
// 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.
|
|
||||||
let t = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
|
let t = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
|
||||||
acadrust::entities::Entity::apply_transform(entity, &t);
|
acadrust::entities::Entity::apply_transform(entity, &t);
|
||||||
});
|
});
|
||||||
|
|
@ -763,6 +791,15 @@ impl Grippable for Hatch {
|
||||||
boundary_centroid(self).unwrap_or((l0.base_point.x, l0.base_point.y));
|
boundary_centroid(self).unwrap_or((l0.base_point.x, l0.base_point.y));
|
||||||
out.push(circle_grip(id, glam::DVec3::new(gx, gy, elev)));
|
out.push(circle_grip(id, glam::DVec3::new(gx, gy, elev)));
|
||||||
id += 1;
|
id += 1;
|
||||||
|
} else if self.is_associative {
|
||||||
|
if let Some((gx, gy)) = boundary_centroid(self) {
|
||||||
|
out.push(circle_grip(id, glam::DVec3::new(gx, gy, elev)));
|
||||||
|
id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Edit associative boundaries through their source objects.
|
||||||
|
if self.is_associative {
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
for path in &self.paths {
|
for path in &self.paths {
|
||||||
for edge in &path.edges {
|
for edge in &path.edges {
|
||||||
|
|
@ -834,8 +871,11 @@ impl Grippable for Hatch {
|
||||||
.map(|l| (l.base_point.x, l.base_point.y))
|
.map(|l| (l.base_point.x, l.base_point.y))
|
||||||
{
|
{
|
||||||
if grip_id == id {
|
if grip_id == id {
|
||||||
let (nx, ny) = resolve(&apply, Vec3::new(ox as f32, oy as f32, elev));
|
let (gx, gy) = boundary_centroid(self).unwrap_or((ox, oy));
|
||||||
let (dx, dy) = (nx - ox, ny - oy);
|
let (dx, dy) = match apply {
|
||||||
|
GripApply::Absolute(point) => (point.x - gx, point.y - gy),
|
||||||
|
GripApply::Translate(delta) => (delta.x, delta.y),
|
||||||
|
};
|
||||||
for line in self.pattern.lines.iter_mut() {
|
for line in self.pattern.lines.iter_mut() {
|
||||||
line.base_point.x += dx;
|
line.base_point.x += dx;
|
||||||
line.base_point.y += dy;
|
line.base_point.y += dy;
|
||||||
|
|
@ -934,8 +974,14 @@ impl Grippable for Hatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||||
|
if self.pattern.lines.is_empty() || grip_id != 0 {
|
||||||
|
return vec![GripMenuItem {
|
||||||
|
label: "Stretch",
|
||||||
|
action: GripMenuAction::Stretch,
|
||||||
|
}];
|
||||||
|
}
|
||||||
vec![
|
vec![
|
||||||
GripMenuItem {
|
GripMenuItem {
|
||||||
label: "Stretch",
|
label: "Stretch",
|
||||||
|
|
@ -956,9 +1002,16 @@ impl Grippable for Hatch {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||||
// Origin / Angle / Scale need a follow-up value — handled by
|
use crate::scene::model::object::GripMenuAction as A;
|
||||||
// `apply_grip_menu_value`.
|
if grip_id == 0 && matches!(action, A::OriginPoint) {
|
||||||
|
if let (Some((gx, gy)), Some((ox, oy))) = (
|
||||||
|
boundary_centroid(self),
|
||||||
|
self.pattern.lines.first().map(|line| (line.base_point.x, line.base_point.y)),
|
||||||
|
) {
|
||||||
|
translate_pattern_geometry(&mut self.pattern, gx - ox, gy - oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grip_menu_value_prompt(
|
fn grip_menu_value_prompt(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
// Hatch/Gradient/Boundary commands — OpenCADStudio Home > Draw > Hatch dropdown.
|
// Hatch, gradient, and boundary commands.
|
||||||
//
|
|
||||||
// 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).
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||||
use crate::modules::IconKind;
|
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 crate::scene::model::wire_model::WireModel;
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use cadkernel::geom2d::{
|
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 glam::DVec3;
|
||||||
use crate::t;
|
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))
|
inner.iter().all(|&v| point_in_polygon(v, outer))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the hatch boundary for a "pick inside" click.
|
/// Resolve the innermost clicked ring and its direct holes.
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
fn resolve_hatch_rings(
|
fn resolve_hatch_rings(
|
||||||
outlines: &[Vec<[f64; 2]>],
|
outlines: &[Vec<[f64; 2]>],
|
||||||
p: [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)
|
(rel, origin, wcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split an absolute boundary into the `(f32 offsets, f64 origin)` pair that
|
/// Store boundary points as precise-origin-relative offsets.
|
||||||
/// `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.
|
|
||||||
fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64; 2]) {
|
fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64; 2]) {
|
||||||
let pts: Vec<(f64, f64)> = pts.collect();
|
let pts: Vec<(f64, f64)> = pts.collect();
|
||||||
let Some(&(ox, oy)) = pts.first() else {
|
let Some(&(ox, oy)) = pts.first() else {
|
||||||
|
|
@ -206,31 +182,42 @@ fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64;
|
||||||
|
|
||||||
pub struct HatchCommand {
|
pub struct HatchCommand {
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||||
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||||
selected_objects: Vec<Handle>,
|
selected_objects: Vec<Handle>,
|
||||||
mode: HatchMode,
|
mode: HatchMode,
|
||||||
manual_pts: Vec<DVec3>,
|
manual_pts: Vec<DVec3>,
|
||||||
|
manual_bulges: Vec<f64>,
|
||||||
|
manual_arc_mode: bool,
|
||||||
|
manual_arc_midpoint: Option<DVec3>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
retain_boundaries: bool,
|
retain_boundaries: bool,
|
||||||
|
pattern_override: Option<(String, HatchPattern)>,
|
||||||
|
angle_override: Option<f32>,
|
||||||
|
scale_override: Option<f32>,
|
||||||
|
associative: bool,
|
||||||
|
separate_hatches: bool,
|
||||||
|
island_style: acadrust::entities::HatchStyleType,
|
||||||
inherited: Option<(
|
inherited: Option<(
|
||||||
HatchModel,
|
HatchModel,
|
||||||
acadrust::types::Color,
|
acadrust::types::Color,
|
||||||
acadrust::types::Transparency,
|
acadrust::types::Transparency,
|
||||||
)>,
|
)>,
|
||||||
|
plane: WorkingPlane,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchCommand {
|
impl HatchCommand {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
selected_objects: Vec<Handle>,
|
selected_objects: Vec<Handle>,
|
||||||
inherited: Option<(
|
inherited: Option<(
|
||||||
HatchModel,
|
HatchModel,
|
||||||
acadrust::types::Color,
|
acadrust::types::Color,
|
||||||
acadrust::types::Transparency,
|
acadrust::types::Transparency,
|
||||||
)>,
|
)>,
|
||||||
|
plane: WorkingPlane,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let selected_objects: Vec<_> = selected_objects
|
let selected_objects: Vec<_> = selected_objects
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -249,9 +236,22 @@ impl HatchCommand {
|
||||||
HatchMode::PickInside
|
HatchMode::PickInside
|
||||||
},
|
},
|
||||||
manual_pts: vec![],
|
manual_pts: vec![],
|
||||||
|
manual_bulges: vec![],
|
||||||
|
manual_arc_mode: false,
|
||||||
|
manual_arc_midpoint: None,
|
||||||
missed: false,
|
missed: false,
|
||||||
retain_boundaries: false,
|
retain_boundaries: false,
|
||||||
|
pattern_override: None,
|
||||||
|
angle_override: None,
|
||||||
|
scale_override: None,
|
||||||
|
associative: true,
|
||||||
|
separate_hatches: false,
|
||||||
|
island_style: inherited
|
||||||
|
.as_ref()
|
||||||
|
.map(|(model, _, _)| model.style)
|
||||||
|
.unwrap_or(acadrust::entities::HatchStyleType::Normal),
|
||||||
inherited,
|
inherited,
|
||||||
|
plane,
|
||||||
};
|
};
|
||||||
command.set_object_selection(selected_objects);
|
command.set_object_selection(selected_objects);
|
||||||
command
|
command
|
||||||
|
|
@ -261,7 +261,7 @@ impl HatchCommand {
|
||||||
let mut segments = Vec::new();
|
let mut segments = Vec::new();
|
||||||
for handle in &handles {
|
for handle in &handles {
|
||||||
if let Some(source) = self.boundary_sources.get(handle) {
|
if let Some(source) = self.boundary_sources.get(handle) {
|
||||||
segments.extend(source.iter().copied());
|
segments.extend(source.segments.iter().copied());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6))
|
self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6))
|
||||||
|
|
@ -287,6 +287,14 @@ impl HatchCommand {
|
||||||
self.point_regions.len() + self.object_regions.len()
|
self.point_regions.len() + self.object_regions.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn island_style_label(&self) -> &'static str {
|
||||||
|
match self.island_style {
|
||||||
|
acadrust::entities::HatchStyleType::Normal => "Normal",
|
||||||
|
acadrust::entities::HatchStyleType::Outer => "Outer",
|
||||||
|
acadrust::entities::HatchStyleType::Ignore => "Ignore",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn combined_rings(&self) -> Vec<Vec<[f64; 2]>> {
|
fn combined_rings(&self) -> Vec<Vec<[f64; 2]>> {
|
||||||
let mut rings = Vec::new();
|
let mut rings = Vec::new();
|
||||||
for ring in self
|
for ring in self
|
||||||
|
|
@ -303,24 +311,62 @@ impl HatchCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
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
|
||||||
let exterior = cadkernel::geom2d::ring_nesting_depths(&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()
|
.into_iter()
|
||||||
.map(|depth| depth == 0)
|
.map(|depth| depth == 0)
|
||||||
.collect();
|
.collect();
|
||||||
let boundary_sources = rings
|
let mut boundary_sources: Vec<Vec<Handle>> = rings
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
||||||
.collect();
|
.collect();
|
||||||
|
let mut boundary_paths = crate::scene::exact_hatch_paths(
|
||||||
|
&rings,
|
||||||
|
&exterior,
|
||||||
|
&self.boundary_sources,
|
||||||
|
1.0e-6,
|
||||||
|
);
|
||||||
|
if !self.associative {
|
||||||
|
for handles in &mut boundary_sources {
|
||||||
|
handles.clear();
|
||||||
|
}
|
||||||
|
for path in &mut boundary_paths {
|
||||||
|
path.boundary_handles.clear();
|
||||||
|
path.flags.set_external(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some((source, _, _)) = &self.inherited {
|
if let Some((source, _, _)) = &self.inherited {
|
||||||
let mut pattern = source.pattern.clone();
|
let (name, mut pattern) = self
|
||||||
|
.pattern_override
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| (source.name.clone(), source.pattern.clone()));
|
||||||
|
let angle = self.angle_override.unwrap_or(source.angle_offset);
|
||||||
|
let scale = self.scale_override.unwrap_or(source.scale).max(1.0e-6);
|
||||||
if let HatchPattern::Pattern(families) = &mut pattern {
|
if let HatchPattern::Pattern(families) = &mut pattern {
|
||||||
let scale = if source.scale.abs() > 1.0e-6 {
|
let (sin, cos) = angle.sin_cos();
|
||||||
source.scale
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let (sin, cos) = source.angle_offset.sin_cos();
|
|
||||||
for family in families {
|
for family in families {
|
||||||
let base_x = source.world_origin[0]
|
let base_x = source.world_origin[0]
|
||||||
+ (family.x0 as f64 * cos as f64
|
+ (family.x0 as f64 * cos as f64
|
||||||
|
|
@ -340,27 +386,29 @@ impl HatchCommand {
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
boundary: std::sync::Arc::new(rel),
|
boundary: std::sync::Arc::new(rel),
|
||||||
pattern,
|
pattern,
|
||||||
name: source.name.clone(),
|
name,
|
||||||
color: source.color,
|
color: source.color,
|
||||||
aci: source.aci,
|
aci: source.aci,
|
||||||
line_weight_px: source.line_weight_px,
|
line_weight_px: source.line_weight_px,
|
||||||
angle_offset: source.angle_offset,
|
angle_offset: angle,
|
||||||
scale: source.scale,
|
scale,
|
||||||
world_origin: origin,
|
world_origin: origin,
|
||||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||||
fill_plane: None,
|
fill_plane: Some(fill_plane),
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
|
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
|
||||||
|
style: self.island_style,
|
||||||
draw_depth: source.draw_depth,
|
draw_depth: source.draw_depth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Default: ANSI31 from catalog; fallback to a single 45° family.
|
// Default: ANSI31 from catalog; fallback to a single 45° family.
|
||||||
let pat_name = "ANSI31";
|
let pat_name = "ANSI31";
|
||||||
let families = crate::scene::model::hatch_patterns::find(pat_name)
|
let default_pattern = crate::scene::model::hatch_patterns::find(pat_name)
|
||||||
.and_then(|e| {
|
.and_then(|e| {
|
||||||
if let HatchPattern::Pattern(f) = &e.gpu {
|
if let HatchPattern::Pattern(f) = &e.gpu {
|
||||||
Some(f.clone())
|
Some(HatchPattern::Pattern(f.clone()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -368,34 +416,92 @@ impl HatchCommand {
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// 45° lines, perpendicular spacing ≈ 5 world units.
|
// 45° lines, perpendicular spacing ≈ 5 world units.
|
||||||
let dy = 5.0_f32 / (45.0_f32.to_radians().cos());
|
let dy = 5.0_f32 / (45.0_f32.to_radians().cos());
|
||||||
vec![PatFamily {
|
HatchPattern::Pattern(vec![PatFamily {
|
||||||
angle_deg: 45.0,
|
angle_deg: 45.0,
|
||||||
x0: 0.0,
|
x0: 0.0,
|
||||||
y0: 0.0,
|
y0: 0.0,
|
||||||
dx: 0.0,
|
dx: 0.0,
|
||||||
dy,
|
dy,
|
||||||
dashes: vec![],
|
dashes: vec![],
|
||||||
}]
|
}])
|
||||||
});
|
});
|
||||||
|
let (name, pattern) = self
|
||||||
|
.pattern_override
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| (pat_name.to_string(), default_pattern));
|
||||||
HatchModel {
|
HatchModel {
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
boundary: std::sync::Arc::new(rel),
|
boundary: std::sync::Arc::new(rel),
|
||||||
pattern: HatchPattern::Pattern(families),
|
pattern,
|
||||||
name: pat_name.into(),
|
name,
|
||||||
color: [0.75, 0.75, 0.75, 0.85],
|
color: [0.75, 0.75, 0.75, 0.85],
|
||||||
aci: 0,
|
aci: 0,
|
||||||
line_weight_px: 1.0,
|
line_weight_px: 1.0,
|
||||||
angle_offset: 0.0,
|
angle_offset: self.angle_override.unwrap_or(0.0),
|
||||||
scale: 1.0,
|
scale: self.scale_override.unwrap_or(1.0).max(1.0e-6),
|
||||||
world_origin: origin,
|
world_origin: origin,
|
||||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||||
fill_plane: None,
|
fill_plane: Some(fill_plane),
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
|
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
|
||||||
|
style: self.island_style,
|
||||||
draw_depth: 0.0,
|
draw_depth: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn manual_boundary_path(&self) -> Option<acadrust::entities::BoundaryPath> {
|
||||||
|
use acadrust::entities::{BoundaryEdge, BoundaryPath, PolylineEdge};
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
if self.manual_pts.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let vertices = self
|
||||||
|
.manual_pts
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, point)| {
|
||||||
|
Vector3::new(
|
||||||
|
point.x,
|
||||||
|
point.y,
|
||||||
|
self.manual_bulges.get(index).copied().unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut path = BoundaryPath::new();
|
||||||
|
path.add_edge(BoundaryEdge::Polyline(PolylineEdge {
|
||||||
|
vertices,
|
||||||
|
is_closed: true,
|
||||||
|
}));
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_bulge(start: DVec3, middle: DVec3, end: DVec3) -> Option<f64> {
|
||||||
|
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 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 * std::f64::consts::TAU
|
||||||
|
} else {
|
||||||
|
(ccw - 1.0) * std::f64::consts::TAU
|
||||||
|
};
|
||||||
|
Some((sweep * 0.25).tan())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for HatchCommand {
|
impl CadCommand for HatchCommand {
|
||||||
|
|
@ -412,7 +518,7 @@ impl CadCommand for HatchCommand {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
t!(
|
t!(
|
||||||
"HATCH Pick internal point (%{count} regions selected, Enter to apply):%{miss}",
|
"HATCH Pick internal point (%{count} regions selected; P <pattern> / A <angle> / L <scale>; Enter to apply):%{miss}",
|
||||||
count = self.region_count(),
|
count = self.region_count(),
|
||||||
miss = miss
|
miss = miss
|
||||||
)
|
)
|
||||||
|
|
@ -425,7 +531,7 @@ impl CadCommand for HatchCommand {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
t!(
|
t!(
|
||||||
"HATCH Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}",
|
"HATCH Select boundary objects (%{objects} objects, %{count} regions; P <pattern> / A <angle> / L <scale>; Enter to apply):%{miss}",
|
||||||
objects = self.selected_objects.len(),
|
objects = self.selected_objects.len(),
|
||||||
count = self.region_count(),
|
count = self.region_count(),
|
||||||
miss = miss
|
miss = miss
|
||||||
|
|
@ -458,6 +564,18 @@ impl CadCommand for HatchCommand {
|
||||||
},
|
},
|
||||||
"B",
|
"B",
|
||||||
),
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
if self.associative { "Associative: on" } else { "Associative: off" },
|
||||||
|
"N",
|
||||||
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
if self.separate_hatches { "Separate hatches: on" } else { "Separate hatches: off" },
|
||||||
|
"D",
|
||||||
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
&format!("Island style: {}", self.island_style_label()),
|
||||||
|
"Y",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if self.region_count() > 0 {
|
if self.region_count() > 0 {
|
||||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||||
|
|
@ -476,6 +594,18 @@ impl CadCommand for HatchCommand {
|
||||||
},
|
},
|
||||||
"B",
|
"B",
|
||||||
),
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
if self.associative { "Associative: on" } else { "Associative: off" },
|
||||||
|
"N",
|
||||||
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
if self.separate_hatches { "Separate hatches: on" } else { "Separate hatches: off" },
|
||||||
|
"D",
|
||||||
|
),
|
||||||
|
CmdOption::new(
|
||||||
|
&format!("Island style: {}", self.island_style_label()),
|
||||||
|
"Y",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if self.region_count() > 0 {
|
if self.region_count() > 0 {
|
||||||
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||||
|
|
@ -483,17 +613,23 @@ impl CadCommand for HatchCommand {
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
HatchMode::Manual => {
|
HatchMode::Manual => {
|
||||||
// Enter accepts the boundary once at least 3 points are picked.
|
let mut options = vec![
|
||||||
|
CmdOption::new(
|
||||||
|
if self.manual_arc_mode { "Line" } else { "Arc" },
|
||||||
|
if self.manual_arc_mode { "L" } else { "A" },
|
||||||
|
),
|
||||||
|
];
|
||||||
if self.manual_pts.len() >= 3 {
|
if self.manual_pts.len() >= 3 {
|
||||||
vec![CmdOption::enter(t!("Accept").as_ref())]
|
options.push(CmdOption::new("Close", "C"));
|
||||||
} else {
|
options.push(CmdOption::enter(t!("Accept").as_ref()));
|
||||||
vec![]
|
|
||||||
}
|
}
|
||||||
|
options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
let pt = self.plane.to_local(pt);
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
HatchMode::PickInside => {
|
HatchMode::PickInside => {
|
||||||
let xy = [pt.x, pt.y];
|
let xy = [pt.x, pt.y];
|
||||||
|
|
@ -511,14 +647,28 @@ impl CadCommand for HatchCommand {
|
||||||
}
|
}
|
||||||
HatchMode::SelectObjects => CmdResult::NeedPoint,
|
HatchMode::SelectObjects => CmdResult::NeedPoint,
|
||||||
HatchMode::Manual => {
|
HatchMode::Manual => {
|
||||||
// Keep the typed/snapped point exact (issue #311).
|
if self.manual_pts.is_empty() || !self.manual_arc_mode {
|
||||||
self.manual_pts.push(pt);
|
if !self.manual_pts.is_empty() {
|
||||||
|
self.manual_bulges.push(0.0);
|
||||||
|
}
|
||||||
|
self.manual_pts.push(pt);
|
||||||
|
} else if let Some(middle) = self.manual_arc_midpoint.take() {
|
||||||
|
let start = *self.manual_pts.last().unwrap();
|
||||||
|
self.manual_bulges
|
||||||
|
.push(arc_bulge(start, middle, pt).unwrap_or(0.0));
|
||||||
|
self.manual_pts.push(pt);
|
||||||
|
} else {
|
||||||
|
self.manual_arc_midpoint = Some(pt);
|
||||||
|
}
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
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 {
|
if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 {
|
||||||
let ring = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
|
let ring = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
|
||||||
self.add_point_region(vec![ring]);
|
self.add_point_region(vec![ring]);
|
||||||
|
|
@ -526,10 +676,44 @@ impl CadCommand for HatchCommand {
|
||||||
let rings = self.combined_rings();
|
let rings = self.combined_rings();
|
||||||
if rings.is_empty() {
|
if rings.is_empty() {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
|
} else if matches!(self.mode, HatchMode::Manual) {
|
||||||
|
let mut hatch = self.make_hatch(rings);
|
||||||
|
if let Some(path) = self.manual_boundary_path() {
|
||||||
|
hatch.boundary_paths = Some(std::sync::Arc::new(vec![path]));
|
||||||
|
}
|
||||||
|
if let Some((_, color, transparency)) = &self.inherited {
|
||||||
|
CmdResult::CommitStyledHatch {
|
||||||
|
hatch,
|
||||||
|
color: color.clone(),
|
||||||
|
transparency: *transparency,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CmdResult::CommitHatch(hatch)
|
||||||
|
}
|
||||||
|
} else if self.separate_hatches && !self.retain_boundaries {
|
||||||
|
let hatches = self
|
||||||
|
.point_regions
|
||||||
|
.iter()
|
||||||
|
.chain(self.object_regions.iter())
|
||||||
|
.cloned()
|
||||||
|
.map(|region| self.make_hatch(region))
|
||||||
|
.collect();
|
||||||
|
CmdResult::CommitHatches {
|
||||||
|
hatches,
|
||||||
|
entity_style: self
|
||||||
|
.inherited
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, color, transparency)| (color.clone(), *transparency)),
|
||||||
|
}
|
||||||
} else if self.retain_boundaries {
|
} else if self.retain_boundaries {
|
||||||
CmdResult::CommitHatchWithBoundaries {
|
CmdResult::CommitHatchWithBoundaries {
|
||||||
hatch: self.make_hatch(rings.clone()),
|
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
|
entity_style: self
|
||||||
.inherited
|
.inherited
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -564,6 +748,15 @@ impl CadCommand for HatchCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||||
|
if matches!(self.mode, HatchMode::Manual) {
|
||||||
|
if self.manual_arc_midpoint.take().is_some() {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if self.manual_pts.pop().is_some() {
|
||||||
|
self.manual_bulges.pop();
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() {
|
if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() {
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -590,11 +783,56 @@ impl CadCommand for HatchCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
!matches!(self.mode, HatchMode::Manual)
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match text.trim().to_ascii_uppercase().as_str() {
|
let input = text.trim();
|
||||||
|
let upper = input.to_ascii_uppercase();
|
||||||
|
if matches!(self.mode, HatchMode::Manual) {
|
||||||
|
return match upper.as_str() {
|
||||||
|
"A" | "ARC" => {
|
||||||
|
self.manual_arc_mode = true;
|
||||||
|
self.manual_arc_midpoint = None;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"L" | "LINE" => {
|
||||||
|
self.manual_arc_mode = false;
|
||||||
|
self.manual_arc_midpoint = None;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"C" | "CLOSE" if self.manual_pts.len() >= 3 => Some(self.on_enter()),
|
||||||
|
_ => 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() {
|
||||||
|
if let Some(entry) = crate::scene::model::hatch_patterns::find(name) {
|
||||||
|
self.pattern_override = Some((entry.name.clone(), entry.gpu.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if let Some(rest) = upper.strip_prefix('A') {
|
||||||
|
if let Ok(value) = rest.trim().replace(',', ".").parse::<f32>() {
|
||||||
|
self.angle_override = Some(value.to_radians());
|
||||||
|
}
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if let Some(rest) = upper.strip_prefix('L') {
|
||||||
|
if let Ok(value) = rest.trim().replace(',', ".").parse::<f32>() {
|
||||||
|
if value > 0.0 {
|
||||||
|
self.scale_override = Some(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
match upper.as_str() {
|
||||||
"O" | "OBJECT" | "OBJECTS" => {
|
"O" | "OBJECT" | "OBJECTS" => {
|
||||||
self.mode = HatchMode::SelectObjects;
|
self.mode = HatchMode::SelectObjects;
|
||||||
self.missed = false;
|
self.missed = false;
|
||||||
|
|
@ -612,6 +850,34 @@ impl CadCommand for HatchCommand {
|
||||||
}
|
}
|
||||||
"B" | "BOUNDARY" | "BOUNDARIES" => {
|
"B" | "BOUNDARY" | "BOUNDARIES" => {
|
||||||
self.retain_boundaries = !self.retain_boundaries;
|
self.retain_boundaries = !self.retain_boundaries;
|
||||||
|
if self.retain_boundaries {
|
||||||
|
self.separate_hatches = false;
|
||||||
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"N" | "ASSOCIATIVE" => {
|
||||||
|
self.associative = !self.associative;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"D" | "SEPARATE" => {
|
||||||
|
self.separate_hatches = !self.separate_hatches;
|
||||||
|
if self.separate_hatches {
|
||||||
|
self.retain_boundaries = false;
|
||||||
|
}
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"Y" | "ISLAND" => {
|
||||||
|
self.island_style = match self.island_style {
|
||||||
|
acadrust::entities::HatchStyleType::Normal => {
|
||||||
|
acadrust::entities::HatchStyleType::Outer
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchStyleType::Outer => {
|
||||||
|
acadrust::entities::HatchStyleType::Ignore
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchStyleType::Ignore => {
|
||||||
|
acadrust::entities::HatchStyleType::Normal
|
||||||
|
}
|
||||||
|
};
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -626,14 +892,10 @@ impl CadCommand for HatchCommand {
|
||||||
let mut pts: Vec<[f32; 3]> = self
|
let mut pts: Vec<[f32; 3]> = self
|
||||||
.manual_pts
|
.manual_pts
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
pts.push([pt.x, pt.y, pt.z]);
|
pts.push(pt.to_array());
|
||||||
pts.push([
|
pts.push(self.plane.to_world(self.manual_pts[0]).as_vec3().to_array());
|
||||||
self.manual_pts[0].x as f32,
|
|
||||||
self.manual_pts[0].y as f32,
|
|
||||||
self.manual_pts[0].z as f32,
|
|
||||||
]);
|
|
||||||
return Some(WireModel::solid(
|
return Some(WireModel::solid(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
pts,
|
pts,
|
||||||
|
|
@ -649,7 +911,7 @@ impl CadCommand for HatchCommand {
|
||||||
|
|
||||||
pub struct GradientCommand {
|
pub struct GradientCommand {
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
manual_pts: Vec<DVec3>,
|
manual_pts: Vec<DVec3>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
|
|
@ -662,7 +924,7 @@ pub struct GradientCommand {
|
||||||
impl GradientCommand {
|
impl GradientCommand {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
boundary_sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outlines,
|
outlines,
|
||||||
|
|
@ -677,7 +939,7 @@ impl GradientCommand {
|
||||||
|
|
||||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
||||||
let (rel, origin, wcs) = pack_rings(&rings);
|
let (rel, origin, wcs) = pack_rings(&rings);
|
||||||
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|depth| depth == 0)
|
.map(|depth| depth == 0)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -685,6 +947,12 @@ impl GradientCommand {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
||||||
.collect();
|
.collect();
|
||||||
|
let boundary_paths = crate::scene::exact_hatch_paths(
|
||||||
|
&rings,
|
||||||
|
&exterior,
|
||||||
|
&self.boundary_sources,
|
||||||
|
1.0e-6,
|
||||||
|
);
|
||||||
HatchModel {
|
HatchModel {
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
boundary: std::sync::Arc::new(rel),
|
boundary: std::sync::Arc::new(rel),
|
||||||
|
|
@ -707,6 +975,8 @@ impl GradientCommand {
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
|
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
draw_depth: 0.0,
|
draw_depth: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use acadrust::Handle;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult, HatchEditOperation};
|
||||||
|
|
||||||
enum HatcheditStep {
|
enum HatcheditStep {
|
||||||
PickHatch,
|
PickHatch,
|
||||||
|
|
@ -26,16 +26,32 @@ enum HatcheditStep {
|
||||||
|
|
||||||
pub struct HatcheditCommand {
|
pub struct HatcheditCommand {
|
||||||
step: HatcheditStep,
|
step: HatcheditStep,
|
||||||
|
origin: Option<(f64, f64)>,
|
||||||
|
disassociate: bool,
|
||||||
|
style: Option<acadrust::entities::HatchStyleType>,
|
||||||
|
annotative: Option<bool>,
|
||||||
|
annotative_current: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatcheditCommand {
|
impl HatcheditCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: HatcheditStep::PickHatch,
|
step: HatcheditStep::PickHatch,
|
||||||
|
origin: None,
|
||||||
|
disassociate: false,
|
||||||
|
style: None,
|
||||||
|
annotative: None,
|
||||||
|
annotative_current: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_handle(handle: Handle, name: String, scale: f32, angle: f32) -> Self {
|
pub fn with_handle(
|
||||||
|
handle: Handle,
|
||||||
|
name: String,
|
||||||
|
scale: f32,
|
||||||
|
angle: f32,
|
||||||
|
annotative: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: HatcheditStep::EditOptions {
|
step: HatcheditStep::EditOptions {
|
||||||
handle,
|
handle,
|
||||||
|
|
@ -43,6 +59,39 @@ impl HatcheditCommand {
|
||||||
scale,
|
scale,
|
||||||
angle,
|
angle,
|
||||||
},
|
},
|
||||||
|
origin: None,
|
||||||
|
disassociate: false,
|
||||||
|
style: None,
|
||||||
|
annotative: None,
|
||||||
|
annotative_current: annotative,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_result(&self, operation: HatchEditOperation) -> Option<CmdResult> {
|
||||||
|
let HatcheditStep::EditOptions {
|
||||||
|
handle,
|
||||||
|
name,
|
||||||
|
scale,
|
||||||
|
angle,
|
||||||
|
} = &self.step
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(CmdResult::HatcheditApply {
|
||||||
|
handle: *handle,
|
||||||
|
name: name.clone(),
|
||||||
|
scale: *scale,
|
||||||
|
angle: *angle,
|
||||||
|
operation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_operation(&self) -> HatchEditOperation {
|
||||||
|
HatchEditOperation::Update {
|
||||||
|
origin: self.origin,
|
||||||
|
disassociate: self.disassociate,
|
||||||
|
style: self.style,
|
||||||
|
annotative: self.annotative,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +110,7 @@ impl CadCommand for HatcheditCommand {
|
||||||
let scale = format!("{scale:.4}");
|
let scale = format!("{scale:.4}");
|
||||||
let angle = format!("{angle:.1}");
|
let angle = format!("{angle:.1}");
|
||||||
t!(
|
t!(
|
||||||
"HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [P <pat> / S <scale> / A <angle> | Enter to apply]:",
|
"HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [P pattern / S scale / A angle / O x,y / D disassociate / Y style / N annotative / R recreate / E separate / + handles / - handles | Enter]:",
|
||||||
name = name,
|
name = name,
|
||||||
scale = scale,
|
scale = scale,
|
||||||
angle = angle
|
angle = angle
|
||||||
|
|
@ -94,8 +143,23 @@ impl CadCommand for HatcheditCommand {
|
||||||
matches!(self.step, HatcheditStep::EditOptions { .. })
|
matches!(self.step, HatcheditStep::EditOptions { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||||
|
if !matches!(self.step, HatcheditStep::EditOptions { .. }) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
crate::command::CmdOption::new("Disassociate", "D"),
|
||||||
|
crate::command::CmdOption::new("Annotative", "N"),
|
||||||
|
crate::command::CmdOption::new("Recreate boundary", "R"),
|
||||||
|
crate::command::CmdOption::new("Separate hatches", "E"),
|
||||||
|
crate::command::CmdOption::new("Draw front", "F"),
|
||||||
|
crate::command::CmdOption::new("Draw back", "B"),
|
||||||
|
crate::command::CmdOption::enter("Apply"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let (handle, name, scale, angle) = match &mut self.step {
|
let (_handle, name, scale, angle) = match &mut self.step {
|
||||||
HatcheditStep::EditOptions {
|
HatcheditStep::EditOptions {
|
||||||
handle,
|
handle,
|
||||||
name,
|
name,
|
||||||
|
|
@ -108,13 +172,15 @@ impl CadCommand for HatcheditCommand {
|
||||||
let text = text.trim().to_uppercase();
|
let text = text.trim().to_uppercase();
|
||||||
|
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
// Apply and exit
|
return self.apply_result(self.update_operation());
|
||||||
return Some(CmdResult::HatcheditApply {
|
}
|
||||||
handle,
|
|
||||||
name: name.clone(),
|
if text == "ANNOTATIVE" {
|
||||||
scale: *scale,
|
self.annotative = Some(!self.annotative.unwrap_or(self.annotative_current));
|
||||||
angle: *angle,
|
return Some(CmdResult::NeedPoint);
|
||||||
});
|
}
|
||||||
|
if text == "SEPARATE" {
|
||||||
|
return self.apply_result(HatchEditOperation::Separate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse option: P/S/A followed by value
|
// Parse option: P/S/A followed by value
|
||||||
|
|
@ -140,6 +206,65 @@ impl CadCommand for HatcheditCommand {
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(rest) = text.strip_prefix('O') {
|
||||||
|
let values: Vec<_> = rest
|
||||||
|
.trim()
|
||||||
|
.split([',', ';', ' '])
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.filter_map(|part| part.replace(',', ".").parse::<f64>().ok())
|
||||||
|
.collect();
|
||||||
|
if values.len() >= 2 {
|
||||||
|
self.origin = Some((values[0], values[1]));
|
||||||
|
}
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if text == "D" || text == "DISASSOCIATE" {
|
||||||
|
self.disassociate = true;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix('Y') {
|
||||||
|
self.style = match rest.trim() {
|
||||||
|
"NORMAL" | "N" => Some(acadrust::entities::HatchStyleType::Normal),
|
||||||
|
"OUTER" | "O" => Some(acadrust::entities::HatchStyleType::Outer),
|
||||||
|
"IGNORE" | "I" => Some(acadrust::entities::HatchStyleType::Ignore),
|
||||||
|
_ => self.style,
|
||||||
|
};
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if text == "N" || text == "ANNOTATIVE" {
|
||||||
|
self.annotative = Some(!self.annotative.unwrap_or(self.annotative_current));
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
if text == "R" || text == "RECREATE" {
|
||||||
|
return self.apply_result(HatchEditOperation::RecreateBoundary);
|
||||||
|
}
|
||||||
|
if text == "E" || text == "SEPARATE" {
|
||||||
|
return self.apply_result(HatchEditOperation::Separate);
|
||||||
|
}
|
||||||
|
if text == "F" || text == "FRONT" {
|
||||||
|
return self.apply_result(HatchEditOperation::DrawOrderFront);
|
||||||
|
}
|
||||||
|
if text == "B" || text == "BACK" {
|
||||||
|
return self.apply_result(HatchEditOperation::DrawOrderBack);
|
||||||
|
}
|
||||||
|
let parse_handles = |source: &str| {
|
||||||
|
source
|
||||||
|
.split([',', ';', ' '])
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.filter_map(|part| {
|
||||||
|
u64::from_str_radix(part.trim_start_matches("0X"), 16)
|
||||||
|
.ok()
|
||||||
|
.map(Handle::new)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
if let Some(rest) = text.strip_prefix('+') {
|
||||||
|
return self.apply_result(HatchEditOperation::AddBoundaries(parse_handles(rest)));
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix('-') {
|
||||||
|
return self.apply_result(HatchEditOperation::RemoveBoundaries(parse_handles(rest)));
|
||||||
|
}
|
||||||
|
|
||||||
// Unrecognized — stay and re-prompt
|
// Unrecognized — stay and re-prompt
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
|
|
@ -149,21 +274,7 @@ impl CadCommand for HatcheditCommand {
|
||||||
}
|
}
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
// Enter without text → apply current settings
|
// Enter without text → apply current settings
|
||||||
let (handle, name, scale, angle) = match &self.step {
|
self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)
|
||||||
HatcheditStep::EditOptions {
|
|
||||||
handle,
|
|
||||||
name,
|
|
||||||
scale,
|
|
||||||
angle,
|
|
||||||
} => (*handle, name.clone(), *scale, *angle),
|
|
||||||
_ => return CmdResult::Cancel,
|
|
||||||
};
|
|
||||||
CmdResult::HatcheditApply {
|
|
||||||
handle,
|
|
||||||
name,
|
|
||||||
scale,
|
|
||||||
angle,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use cadkernel::geom2d::{
|
use cadkernel::geom2d::{
|
||||||
bounded_faces, contains, distance_to, intersect, segment_crossing, triangulate, Curve, Line,
|
bounded_faces, closest_point, contains, distance_to, intersect, ring_nesting_depths,
|
||||||
SegmentCrossing, Tolerance, Transform as CurveTransform,
|
segment_crossing, signed_area, triangulate, Curve, Line, SegmentCrossing, Tolerance,
|
||||||
|
Transform as CurveTransform,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::command::WorkingPlane;
|
use crate::command::WorkingPlane;
|
||||||
|
|
@ -13,39 +14,9 @@ pub struct BoundarySource {
|
||||||
pub curves: Vec<Curve>,
|
pub curves: Vec<Curve>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How far apart two points may be and still be taken for the same one.
|
/// Boundary welding tolerance for tessellated wires.
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
const WELD_TOLERANCE: f64 = 1.0e-6;
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wire_segments_on_plane(
|
fn wire_segments_on_plane(
|
||||||
wire: &WireModel,
|
wire: &WireModel,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
|
|
@ -113,22 +84,12 @@ fn entity_curves_on_plane(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> {
|
||||||
let mut ring = Vec::new();
|
let mut ring = Vec::new();
|
||||||
let mut index = 0usize;
|
for edge in &path.edges {
|
||||||
for &[x, y] in model.boundary.iter() {
|
let curve = crate::entities::hatch::edge_curve(edge)?;
|
||||||
if x.is_finite() && y.is_finite() {
|
let points = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||||
if index == wanted {
|
ring.extend(points.into_iter().skip(usize::from(!ring.is_empty())));
|
||||||
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 {
|
if ring.len() < 3 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -136,15 +97,16 @@ fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||||
let (points, triangles) = triangulate(&ring, &[]);
|
let (points, triangles) = triangulate(&ring, &[]);
|
||||||
if let Some(triangle) = triangles.first() {
|
if let Some(triangle) = triangles.first() {
|
||||||
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
||||||
return Some([
|
Some([
|
||||||
(a[0] + b[0] + c[0]) / 3.0,
|
(a[0] + b[0] + c[0]) / 3.0,
|
||||||
(a[1] + b[1] + c[1]) / 3.0,
|
(a[1] + b[1] + c[1]) / 3.0,
|
||||||
]);
|
])
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
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> {
|
fn face_curves(face: &[[f64; 2]]) -> Vec<Curve> {
|
||||||
|
|
@ -181,7 +143,7 @@ fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec
|
||||||
|
|
||||||
pub(crate) fn ring_source_handles(
|
pub(crate) fn ring_source_handles(
|
||||||
ring: &[[f64; 2]],
|
ring: &[[f64; 2]],
|
||||||
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
|
sources: &rustc_hash::FxHashMap<acadrust::Handle, BoundarySource>,
|
||||||
) -> Vec<acadrust::Handle> {
|
) -> Vec<acadrust::Handle> {
|
||||||
let mut handles = rustc_hash::FxHashSet::default();
|
let mut handles = rustc_hash::FxHashSet::default();
|
||||||
for (&start, &end) in ring
|
for (&start, &end) in ring
|
||||||
|
|
@ -191,8 +153,8 @@ pub(crate) fn ring_source_handles(
|
||||||
{
|
{
|
||||||
let edge = Line { start, end };
|
let edge = Line { start, end };
|
||||||
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
|
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
|
||||||
for (&handle, lines) in sources {
|
for (&handle, source) in sources {
|
||||||
if lines.iter().any(|line| {
|
if source.segments.iter().any(|line| {
|
||||||
matches!(
|
matches!(
|
||||||
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
||||||
SegmentCrossing::Overlap { a, .. }
|
SegmentCrossing::Overlap { a, .. }
|
||||||
|
|
@ -208,7 +170,220 @@ pub(crate) fn ring_source_handles(
|
||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
fn curve_forward(curve: &Curve, start: [f64; 2], next: [f64; 2]) -> bool {
|
||||||
|
let a = curve.parameter_at(start);
|
||||||
|
let b = curve.parameter_at(next);
|
||||||
|
let mut delta = b - a;
|
||||||
|
if curve.is_closed() {
|
||||||
|
if delta > 0.5 {
|
||||||
|
delta -= 1.0;
|
||||||
|
} else if delta < -0.5 {
|
||||||
|
delta += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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],
|
||||||
|
end: [f64; 2],
|
||||||
|
next: [f64; 2],
|
||||||
|
whole_curve: bool,
|
||||||
|
) -> acadrust::entities::BoundaryEdge {
|
||||||
|
use acadrust::entities::{
|
||||||
|
BoundaryEdge, CircularArcEdge, EllipticArcEdge, LineEdge, SplineEdge,
|
||||||
|
};
|
||||||
|
use acadrust::types::{Vector2, Vector3};
|
||||||
|
|
||||||
|
let Some(curve) = curve else {
|
||||||
|
return BoundaryEdge::Line(LineEdge {
|
||||||
|
start: Vector2::new(start[0], start[1]),
|
||||||
|
end: Vector2::new(end[0], end[1]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let forward = curve_forward(curve, start, next);
|
||||||
|
match curve {
|
||||||
|
Curve::Line(_) => BoundaryEdge::Line(LineEdge {
|
||||||
|
start: Vector2::new(start[0], start[1]),
|
||||||
|
end: Vector2::new(end[0], end[1]),
|
||||||
|
}),
|
||||||
|
Curve::Circle(circle) => {
|
||||||
|
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,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
counter_clockwise: forward,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Arc(arc) => {
|
||||||
|
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,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
counter_clockwise: forward,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Ellipse(arc) => {
|
||||||
|
let ellipse = arc.ellipse;
|
||||||
|
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(
|
||||||
|
ellipse.major_axis[0] * ellipse.major_radius,
|
||||||
|
ellipse.major_axis[1] * ellipse.major_radius,
|
||||||
|
),
|
||||||
|
minor_axis_ratio: ellipse.minor_radius / ellipse.major_radius,
|
||||||
|
start_angle: start_parameter,
|
||||||
|
end_angle: end_parameter,
|
||||||
|
counter_clockwise: forward,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Nurbs(source) => {
|
||||||
|
let trimmed = if whole_curve {
|
||||||
|
Some(if forward { source.clone() } else { source.reversed() })
|
||||||
|
} else {
|
||||||
|
source.trimmed(source.parameter_at(start), source.parameter_at(end))
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
rational,
|
||||||
|
periodic: trimmed.is_closed(),
|
||||||
|
knots: trimmed.knots().to_vec(),
|
||||||
|
control_points: trimmed
|
||||||
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.zip(trimmed.weights())
|
||||||
|
.map(|(point, weight)| {
|
||||||
|
Vector3::new(point[0], point[1], if rational { *weight } else { 1.0 })
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
fit_points: Vec::new(),
|
||||||
|
start_tangent: Vector2::new(0.0, 0.0),
|
||||||
|
end_tangent: Vector2::new(0.0, 0.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Polyline(_) | Curve::Ray(_) | Curve::XLine(_) => {
|
||||||
|
BoundaryEdge::Line(LineEdge {
|
||||||
|
start: Vector2::new(start[0], start[1]),
|
||||||
|
end: Vector2::new(end[0], end[1]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild detected rings from exact source curves.
|
||||||
|
pub(crate) fn exact_hatch_paths(
|
||||||
|
rings: &[Vec<[f64; 2]>],
|
||||||
|
exterior: &[bool],
|
||||||
|
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Vec<acadrust::entities::BoundaryPath> {
|
||||||
|
use acadrust::entities::{BoundaryPath, BoundaryPathFlags};
|
||||||
|
|
||||||
|
rings
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(ring_index, ring)| {
|
||||||
|
let (points, curves) = refined_boundary_ring(ring, sources, tolerance);
|
||||||
|
let count = points.len();
|
||||||
|
if count < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let handles = ring_source_handles(ring, sources);
|
||||||
|
let mut bits = 0;
|
||||||
|
if exterior.get(ring_index).copied().unwrap_or(ring_index == 0) {
|
||||||
|
bits |= BoundaryPathFlags::OUTERMOST.bits();
|
||||||
|
}
|
||||||
|
if !handles.is_empty() {
|
||||||
|
bits |= BoundaryPathFlags::EXTERNAL.bits();
|
||||||
|
}
|
||||||
|
let mut path = BoundaryPath::with_flags(BoundaryPathFlags::from_bits(bits));
|
||||||
|
|
||||||
|
let all_same = curves.first().is_some_and(|first| {
|
||||||
|
first.is_some() && curves.iter().all(|curve| curve == first)
|
||||||
|
});
|
||||||
|
if all_same {
|
||||||
|
let curve = curves[0].as_ref();
|
||||||
|
path.add_edge(exact_boundary_edge(
|
||||||
|
curve,
|
||||||
|
points[0],
|
||||||
|
points[0],
|
||||||
|
points[1],
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let start_index = (0..count)
|
||||||
|
.find(|index| curves[*index] != curves[(*index + count - 1) % count])
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut consumed = 0usize;
|
||||||
|
while consumed < count {
|
||||||
|
let edge_index = (start_index + consumed) % count;
|
||||||
|
let curve = curves.get(edge_index).and_then(Option::as_ref);
|
||||||
|
let mut length = 1usize;
|
||||||
|
if curve.is_some() {
|
||||||
|
while consumed + length < count
|
||||||
|
&& curves[(edge_index + length) % count].as_ref() == curve
|
||||||
|
{
|
||||||
|
length += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let end_index = (edge_index + length) % count;
|
||||||
|
path.add_edge(exact_boundary_edge(
|
||||||
|
curve,
|
||||||
|
points[edge_index],
|
||||||
|
points[end_index],
|
||||||
|
points[(edge_index + 1) % count],
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
consumed += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for handle in handles {
|
||||||
|
path.add_boundary_handle(handle);
|
||||||
|
}
|
||||||
|
Some(path)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn boundary_entities(
|
||||||
|
rings: &[Vec<[f64; 2]>],
|
||||||
|
plane: WorkingPlane,
|
||||||
|
) -> Vec<acadrust::EntityType> {
|
||||||
rings
|
rings
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|ring| {
|
.filter_map(|ring| {
|
||||||
|
|
@ -234,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))
|
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Some(acadrust::EntityType::LwPolyline(polyline))
|
Some(plane.place_entity(acadrust::EntityType::LwPolyline(polyline)))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -295,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] {
|
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(
|
fn refined_boundary_ring(
|
||||||
|
|
@ -408,12 +583,61 @@ pub(crate) fn boundary_polyline_entities(
|
||||||
.collect()
|
.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 {
|
impl Scene {
|
||||||
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
pub(crate) fn edit_hatch_boundary_handles(
|
||||||
self.wire_models_for(handles)
|
&mut self,
|
||||||
|
hatch_handle: Handle,
|
||||||
|
handles: &[Handle],
|
||||||
|
add: bool,
|
||||||
|
) -> bool {
|
||||||
|
let Some(EntityType::Hatch(hatch)) = self.document.get_entity_mut(hatch_handle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(path) = hatch.paths.first_mut() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if add {
|
||||||
|
for handle in handles.iter().copied().filter(|handle| handle.is_valid()) {
|
||||||
|
if !path.boundary_handles.contains(&handle) {
|
||||||
|
path.boundary_handles.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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()
|
.iter()
|
||||||
.flat_map(wire_segments)
|
.any(|candidate| !candidate.boundary_handles.is_empty());
|
||||||
.collect()
|
self.associative_hatch_source_cache.borrow_mut().take();
|
||||||
|
if add && !handles.is_empty() {
|
||||||
|
let changes: Vec<_> = handles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|handle| (handle, ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.refresh_associative_hatches(&changes);
|
||||||
|
} else {
|
||||||
|
self.refresh_fill_model(hatch_handle);
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn associative_hatch_dependents(
|
fn associative_hatch_dependents(
|
||||||
|
|
@ -470,15 +694,7 @@ impl Scene {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some({
|
Some({
|
||||||
let seeds = self
|
let seeds = hatch.paths.iter().map(hatch_path_seed).collect::<Vec<_>>();
|
||||||
.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)
|
(handle, hatch.clone(), seeds)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -486,10 +702,13 @@ impl Scene {
|
||||||
|
|
||||||
let mut refreshed = Vec::new();
|
let mut refreshed = Vec::new();
|
||||||
for (handle, mut hatch, seeds) in candidates {
|
for (handle, mut hatch, seeds) in candidates {
|
||||||
let normal = hatch.normal;
|
let storage = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation);
|
||||||
if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 {
|
let plane = WorkingPlane::new(
|
||||||
continue;
|
glam::DVec3::from_array(storage.origin),
|
||||||
}
|
glam::DVec3::from_array(storage.x_axis),
|
||||||
|
glam::DVec3::from_array(storage.y_axis),
|
||||||
|
);
|
||||||
|
let all_sources = self.boundary_sources_on_plane(plane, WELD_TOLERANCE);
|
||||||
let mut modified = false;
|
let mut modified = false;
|
||||||
let mut association_changed = false;
|
let mut association_changed = false;
|
||||||
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
||||||
|
|
@ -503,21 +722,41 @@ impl Scene {
|
||||||
let old_count = path.boundary_handles.len();
|
let old_count = path.boundary_handles.len();
|
||||||
path.boundary_handles
|
path.boundary_handles
|
||||||
.retain(|source| self.document.get_entity(*source).is_some());
|
.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;
|
association_changed |= path.boundary_handles.len() != old_count;
|
||||||
modified |= association_changed;
|
modified |= association_changed;
|
||||||
let segments = self.associative_boundary_segments(&path.boundary_handles);
|
let sources: rustc_hash::FxHashMap<_, _> = path
|
||||||
|
.boundary_handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|source| {
|
||||||
|
all_sources
|
||||||
|
.get(source)
|
||||||
|
.cloned()
|
||||||
|
.map(|geometry| (*source, geometry))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let segments: Vec<_> = sources
|
||||||
|
.values()
|
||||||
|
.flat_map(|source| source.segments.iter().copied())
|
||||||
|
.collect();
|
||||||
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
|
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
|
||||||
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
|
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline(
|
let exterior = [path.flags.is_outermost()];
|
||||||
acadrust::entities::hatch::PolylineEdge::new(
|
if let Some(exact) = exact_hatch_paths(
|
||||||
face.iter()
|
std::slice::from_ref(face),
|
||||||
.map(|point| acadrust::types::Vector2::new(point[0], point[1]))
|
&exterior,
|
||||||
.collect(),
|
&sources,
|
||||||
true,
|
WELD_TOLERANCE,
|
||||||
),
|
)
|
||||||
)];
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
path.edges = exact.edges;
|
||||||
|
}
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
hatch.is_associative = hatch
|
hatch.is_associative = hatch
|
||||||
|
|
@ -543,48 +782,6 @@ impl Scene {
|
||||||
refreshed
|
refreshed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build closed planar regions from the visible wire geometry.
|
|
||||||
///
|
|
||||||
/// Source entities do not need to be closed individually. Intersections are
|
|
||||||
/// inserted as temporary graph vertices and
|
|
||||||
/// the bounded faces of that planar graph are returned as hatch candidates.
|
|
||||||
///
|
|
||||||
/// Curved entities participate through their already-tessellated WireModel
|
|
||||||
/// geometry, so arcs, circles, ellipses and splines can take part in the
|
|
||||||
/// boundary search without modifying the source entities.
|
|
||||||
///
|
|
||||||
/// The arrangement itself is the kernel's: splitting at crossings, welding
|
|
||||||
/// coincident ends and tracing the bounded faces is the same problem a
|
|
||||||
/// B-rep boolean solves in a face's parameter space, and it is solved
|
|
||||||
/// once. What stays here is reading the wires — which is where the
|
|
||||||
/// drawing's own conventions live.
|
|
||||||
pub fn hatch_boundary_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
|
||||||
let mut segments = Vec::<Line>::new();
|
|
||||||
|
|
||||||
for wire in self.entity_wires().iter() {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Boundary candidates in the active working plane, with exact curves
|
/// Boundary candidates in the active working plane, with exact curves
|
||||||
/// where the source entity exposes them.
|
/// where the source entity exposes them.
|
||||||
pub fn boundary_sources_on_plane(
|
pub fn boundary_sources_on_plane(
|
||||||
|
|
@ -636,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(
|
pub(crate) fn boundary_faces(
|
||||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||||
tolerance: f64,
|
tolerance: f64,
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,33 @@
|
||||||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Convert a HATCH's own resolved pattern line (world-unit `offset` = step to
|
/// Order sampled boundary edges into one tip-to-tail loop.
|
||||||
/// the next parallel line, plus a base angle) into a render `PatFamily` whose
|
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||||
/// geometry is already final — the HatchModel that carries it uses scale 1 and
|
chain_path_edges_with_directions(polys).0
|
||||||
/// 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
|
pub(super) fn chain_path_edges_with_directions(
|
||||||
/// the exact stored step. `x0/y0` are filled in by the caller (from the stored
|
polys: Vec<Vec<[f64; 2]>>,
|
||||||
/// `base_point`, relative to `world_origin`) once the boundary anchor is known;
|
) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||||
/// 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]> {
|
|
||||||
let d2 = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2);
|
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() {
|
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() {
|
while !polys.is_empty() {
|
||||||
let head = *chain.front().unwrap();
|
let head = *chain.front().unwrap();
|
||||||
let tail = *chain.back().unwrap();
|
let tail = *chain.back().unwrap();
|
||||||
// (distance, index, reverse-points, attach-at-front)
|
// (distance, index, reverse-points, attach-at-front)
|
||||||
let mut best = (f64::MAX, 0usize, false, false);
|
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 s = p[0];
|
||||||
let e = *p.last().unwrap();
|
let e = *p.last().unwrap();
|
||||||
for c in [
|
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 (_, 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 {
|
if rev {
|
||||||
p.reverse();
|
p.reverse();
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +64,7 @@ fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||||
chain.extend(it);
|
chain.extend(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chain.into()
|
(chain.into(), directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn family_from_stored_line(
|
fn family_from_stored_line(
|
||||||
|
|
@ -1533,6 +1526,8 @@ impl Scene {
|
||||||
fill_plane_boundary,
|
fill_plane_boundary,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: model::hatch_model::HatchPattern::Solid,
|
pattern: model::hatch_model::HatchPattern::Solid,
|
||||||
name: "WIPEOUT_FILL".into(),
|
name: "WIPEOUT_FILL".into(),
|
||||||
color,
|
color,
|
||||||
|
|
@ -1684,6 +1679,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rings = Vec::new();
|
let mut rings = Vec::new();
|
||||||
|
let mut local_rings = Vec::new();
|
||||||
let mut ring_sources = Vec::new();
|
let mut ring_sources = Vec::new();
|
||||||
|
|
||||||
for path in &dxf.paths {
|
for path in &dxf.paths {
|
||||||
|
|
@ -1698,26 +1694,27 @@ impl Scene {
|
||||||
for edge in &path.edges {
|
for edge in &path.edges {
|
||||||
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
||||||
edge_polys.push(
|
edge_polys.push(
|
||||||
curve
|
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE),
|
||||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
|
||||||
.into_iter()
|
|
||||||
.map(|point| to_xy(point[0], point[1]))
|
|
||||||
.collect(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut ring = chain_path_edges(edge_polys);
|
let mut local_ring = chain_path_edges(edge_polys);
|
||||||
if ring.is_empty() {
|
if local_ring.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ring.len() >= 3 {
|
if local_ring.len() >= 3 {
|
||||||
let first = ring[0];
|
let first = local_ring[0];
|
||||||
let last = *ring.last().unwrap();
|
let last = *local_ring.last().unwrap();
|
||||||
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
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);
|
rings.push(ring);
|
||||||
|
local_rings.push(local_ring);
|
||||||
ring_sources.push(path.boundary_handles.clone());
|
ring_sources.push(path.boundary_handles.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1727,9 +1724,15 @@ impl Scene {
|
||||||
|
|
||||||
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||||
let mut boundary = Vec::new();
|
let mut boundary = Vec::new();
|
||||||
|
let mut local_boundary = Vec::new();
|
||||||
let mut boundary_exterior = Vec::new();
|
let mut boundary_exterior = Vec::new();
|
||||||
let mut boundary_sources = 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 {
|
let keep = match dxf.style {
|
||||||
acadrust::entities::HatchStyleType::Normal => true,
|
acadrust::entities::HatchStyleType::Normal => true,
|
||||||
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
||||||
|
|
@ -1740,8 +1743,14 @@ impl Scene {
|
||||||
}
|
}
|
||||||
if !boundary.is_empty() {
|
if !boundary.is_empty() {
|
||||||
boundary.push([f64::NAN, f64::NAN]);
|
boundary.push([f64::NAN, f64::NAN]);
|
||||||
|
local_boundary.push([f32::NAN, f32::NAN]);
|
||||||
}
|
}
|
||||||
boundary.extend(ring);
|
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_exterior.push(depth == 0);
|
||||||
boundary_sources.push(sources);
|
boundary_sources.push(sources);
|
||||||
}
|
}
|
||||||
|
|
@ -1941,14 +1950,21 @@ impl Scene {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation);
|
||||||
Some(HatchModel {
|
Some(HatchModel {
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
boundary: std::sync::Arc::new(boundary_f32),
|
boundary: std::sync::Arc::new(boundary_f32),
|
||||||
boundary_wcs: None,
|
boundary_wcs: None,
|
||||||
fill_plane: None,
|
fill_plane: Some(model::hatch_model::FillPlane {
|
||||||
fill_plane_boundary: None,
|
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_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
|
boundary_paths: Some(std::sync::Arc::new(dxf.paths.clone())),
|
||||||
|
style: dxf.style,
|
||||||
pattern,
|
pattern,
|
||||||
name,
|
name,
|
||||||
// A gradient starts from its first stop; other fills use the
|
// A gradient starts from its first stop; other fills use the
|
||||||
|
|
@ -2267,6 +2283,8 @@ impl Scene {
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: model::hatch_model::HatchPattern::Solid,
|
pattern: model::hatch_model::HatchPattern::Solid,
|
||||||
name: "SOLID".into(),
|
name: "SOLID".into(),
|
||||||
color,
|
color,
|
||||||
|
|
@ -2286,14 +2304,24 @@ impl Scene {
|
||||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||||
) -> Handle {
|
) -> Handle {
|
||||||
let mut dxf = DxfHatch::new();
|
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!(
|
dxf.is_solid = matches!(
|
||||||
model.pattern,
|
model.pattern,
|
||||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||||
);
|
);
|
||||||
// Prefer exact command geometry; otherwise reconstruct every ring from
|
// Persist analytic command geometry when available.
|
||||||
// the render offsets without dropping its separators.
|
if let Some(paths) = model.boundary_paths.as_deref() {
|
||||||
// Build one DXF path per NaN-separated ring and retain each outer/hole role.
|
dxf.paths = paths.clone();
|
||||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
} else {
|
||||||
|
// Otherwise reconstruct every ring from render offsets.
|
||||||
|
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||||
let [wx, wy] = model.world_origin;
|
let [wx, wy] = model.world_origin;
|
||||||
model
|
model
|
||||||
.boundary
|
.boundary
|
||||||
|
|
@ -2306,18 +2334,18 @@ impl Scene {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let wcs = model
|
let wcs = model
|
||||||
.boundary_wcs
|
.boundary_wcs
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|points| points.as_slice())
|
.map(|points| points.as_slice())
|
||||||
.unwrap_or(reconstructed_wcs.as_slice());
|
.unwrap_or(reconstructed_wcs.as_slice());
|
||||||
let mut ring: Vec<Vector2> = Vec::new();
|
let mut ring: Vec<Vector2> = Vec::new();
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
let mut ring_index = 0usize;
|
let mut ring_index = 0usize;
|
||||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||||
if !r.is_empty() {
|
if !r.is_empty() {
|
||||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||||
let handles: Vec<_> = model
|
let handles: Vec<_> = model
|
||||||
|
|
@ -2345,31 +2373,32 @@ impl Scene {
|
||||||
}
|
}
|
||||||
dxf.paths.push(path);
|
dxf.paths.push(path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for &[x, y] in wcs {
|
for &[x, y] in wcs {
|
||||||
if x.is_finite() && y.is_finite() {
|
if x.is_finite() && y.is_finite() {
|
||||||
ring.push(Vector2::new(x, y));
|
ring.push(Vector2::new(x, y));
|
||||||
} else {
|
} else {
|
||||||
let is_outer = model
|
let is_outer = model
|
||||||
.boundary_exterior
|
.boundary_exterior
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|roles| roles.get(ring_index))
|
.and_then(|roles| roles.get(ring_index))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(first);
|
.unwrap_or(first);
|
||||||
first = false;
|
first = false;
|
||||||
if !ring.is_empty() {
|
if !ring.is_empty() {
|
||||||
push_ring(&mut ring, is_outer, ring_index);
|
push_ring(&mut ring, is_outer, ring_index);
|
||||||
ring_index += 1;
|
ring_index += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let is_outer = model
|
||||||
|
.boundary_exterior
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|roles| roles.get(ring_index))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(first);
|
||||||
|
push_ring(&mut ring, is_outer, ring_index);
|
||||||
}
|
}
|
||||||
let is_outer = model
|
|
||||||
.boundary_exterior
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|roles| roles.get(ring_index))
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(first);
|
|
||||||
push_ring(&mut ring, is_outer, ring_index);
|
|
||||||
dxf.is_associative = dxf
|
dxf.is_associative = dxf
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2379,6 +2408,16 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
1.0
|
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 {
|
if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern {
|
||||||
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
|
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
|
||||||
let rotation = model.angle_offset as f64;
|
let rotation = model.angle_offset as f64;
|
||||||
|
|
@ -2396,10 +2435,10 @@ impl Scene {
|
||||||
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
||||||
angle,
|
angle,
|
||||||
base_point: Vector2::new(
|
base_point: Vector2::new(
|
||||||
model.world_origin[0]
|
pattern_origin[0]
|
||||||
+ base_x * rotation_cos
|
+ base_x * rotation_cos
|
||||||
- base_y * rotation_sin,
|
- base_y * rotation_sin,
|
||||||
model.world_origin[1]
|
pattern_origin[1]
|
||||||
+ base_x * rotation_sin
|
+ base_x * rotation_sin
|
||||||
+ base_y * rotation_cos,
|
+ base_y * rotation_cos,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ mod scene_markers;
|
||||||
mod selection;
|
mod selection;
|
||||||
|
|
||||||
pub(crate) use boundary::{
|
pub(crate) use boundary::{
|
||||||
boundary_entities, boundary_faces, boundary_polyline_entities, ring_source_handles,
|
boundary_entities, boundary_entities_from_sources, boundary_faces,
|
||||||
BoundarySource,
|
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
|
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||||
|
|
@ -3227,6 +3228,8 @@ impl Scene {
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
||||||
name: "SOLID".to_string(),
|
name: "SOLID".to_string(),
|
||||||
color: self.paper_bg_color,
|
color: self.paper_bg_color,
|
||||||
|
|
|
||||||
|
|
@ -203,13 +203,17 @@ pub struct HatchModel {
|
||||||
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
||||||
/// vertices from `boundary` + `world_origin` instead.
|
/// vertices from `boundary` + `world_origin` instead.
|
||||||
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
|
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: Option<FillPlane>,
|
||||||
pub fill_plane_boundary: Option<Arc<Vec<[f32; 2]>>>,
|
pub fill_plane_boundary: Option<Arc<Vec<[f32; 2]>>>,
|
||||||
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
||||||
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
||||||
/// Source entity handles for each boundary ring.
|
/// Source entity handles for each boundary ring.
|
||||||
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
||||||
|
/// 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,
|
||||||
/// Fill pattern.
|
/// Fill pattern.
|
||||||
pub pattern: HatchPattern,
|
pub pattern: HatchPattern,
|
||||||
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,8 @@ impl Scene {
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: HatchPattern::Solid,
|
pattern: HatchPattern::Solid,
|
||||||
name: "AREA_PREVIEW".into(),
|
name: "AREA_PREVIEW".into(),
|
||||||
color: [0.0; 4],
|
color: [0.0; 4],
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,8 @@ impl canvas::Program<Message> for HatchPatternPreview {
|
||||||
fill_plane_boundary: None,
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: self.pattern.clone(),
|
pattern: self.pattern.clone(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
color: [1.0; 4],
|
color: [1.0; 4],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue