forked from 1b-agent/opencadstudio-upstream
Merge pull request #1184 from Ramox81/codex/hatchedit-command-parity
This commit is contained in:
commit
753fa17b68
24 files changed
+926
-132
No files matched your search
+140
-48
@@ -5165,7 +5165,41 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref());
|
||||
} else {
|
||||
use crate::command::HatchEditOperation;
|
||||
use crate::command::{CadCommand, HatchEditOperation};
|
||||
if matches!(&operation,HatchEditOperation::BeginAssociate) {
|
||||
let associative=matches!(self.tabs[i].scene.document.get_entity(handle),Some(acadrust::EntityType::Hatch(h)) if h.is_associative);
|
||||
if associative {
|
||||
self.command_line.push_info("HATCHEDIT: hatch is already associative.");
|
||||
self.tabs[i].active_cmd=None;
|
||||
}else{
|
||||
let plane=match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Hatch(h))=>{
|
||||
let storage=crate::entities::curve::ocs_plane(h.normal,h.elevation);
|
||||
crate::command::WorkingPlane::new(glam::DVec3::from_array(storage.origin),glam::DVec3::from_array(storage.x_axis),glam::DVec3::from_array(storage.y_axis))
|
||||
},
|
||||
_=>self.tabs[i].ucs_xform().working_plane(),
|
||||
};
|
||||
let mut sources=self.tabs[i].scene.boundary_sources_on_plane(plane,1e-6);
|
||||
sources.remove(&handle);
|
||||
let command=crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association(handle,name,scale,angle,plane,sources);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd=Some(Box::new(command));
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if let HatchEditOperation::DrawOrderBoundary{above}=&operation {
|
||||
let references:Vec<_>=match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Hatch(h))=>h.paths.iter().flat_map(|p|p.boundary_handles.iter())
|
||||
.filter(|h|self.tabs[i].scene.document.get_entity(**h).is_some()).map(|h|format!("{:X}",h.value())).collect(),
|
||||
_=>Vec::new(),
|
||||
};
|
||||
self.tabs[i].active_cmd=None;
|
||||
if references.is_empty(){self.command_line.push_info("HATCHEDIT: no associated boundary objects.");return Task::none();}
|
||||
self.tabs[i].scene.deselect_all();self.tabs[i].scene.select_entity(handle,false);
|
||||
let command=format!("DRAWORDER {} {}",if *above{"ABOVE"}else{"UNDER"},references.join(" "));
|
||||
return self.dispatch_view(&command,i).unwrap_or_else(Task::none);
|
||||
}
|
||||
if matches!(
|
||||
&operation,
|
||||
HatchEditOperation::DrawOrderFront | HatchEditOperation::DrawOrderBack
|
||||
@@ -5182,12 +5216,38 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.push_undo_snapshot(i, "HATCHEDIT");
|
||||
match operation {
|
||||
HatchEditOperation::Appearance { color, layer, transparency } => {
|
||||
let layer=layer.map(|name|if name=="."{self.tabs[i].active_layer.clone()}else{name});
|
||||
if layer.as_ref().is_some_and(|name|self.tabs[i].scene.document.layers.get(name).is_none()) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: layer not found.");
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(entity)=self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
let common=entity.common_mut();
|
||||
if let Some(value)=color {common.color=value;common.color_name=None;common.color_book_handle=None;}
|
||||
if let Some(value)=layer {common.layer=value;}
|
||||
if let Some(value)=transparency {common.transparency=value;}
|
||||
}
|
||||
self.tabs[i].scene.bump_entities(&[(handle,crate::scene::ChangeKind::Modified)]);
|
||||
self.refresh_properties();
|
||||
}
|
||||
HatchEditOperation::Update {
|
||||
origin,
|
||||
store_origin,
|
||||
disassociate,
|
||||
style,
|
||||
annotative,
|
||||
} => {
|
||||
if store_origin {
|
||||
if let Some((x, y)) = origin {
|
||||
if !self.tabs[i].scene.document.set_hatch_origin([x, y]) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: cannot store default origin.");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
@@ -5195,11 +5255,6 @@ impl OpenCADStudio {
|
||||
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,
|
||||
@@ -5209,16 +5264,8 @@ impl OpenCADStudio {
|
||||
&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,
|
||||
);
|
||||
}
|
||||
let origin = hatch.pattern_origin();
|
||||
crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y);
|
||||
hatch.pattern = pattern;
|
||||
hatch.is_solid = matches!(
|
||||
entry.gpu,
|
||||
@@ -5232,30 +5279,16 @@ impl OpenCADStudio {
|
||||
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,
|
||||
);
|
||||
hatch.scale_pattern_about_origin(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.rotate_pattern_about_origin(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,
|
||||
);
|
||||
}
|
||||
hatch.set_pattern_origin(acadrust::types::Vector2::new(x, y));
|
||||
}
|
||||
if disassociate {
|
||||
for path in &mut hatch.paths {
|
||||
@@ -5301,7 +5334,15 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.edit_hatch_boundary_handles(handle, &handles, false);
|
||||
}
|
||||
HatchEditOperation::RecreateBoundary => {
|
||||
HatchEditOperation::AssociatePaths(paths) => {
|
||||
if paths.is_empty()||paths.iter().any(|path|path.boundary_handles.is_empty()||path.boundary_handles.iter().any(|source|self.tabs[i].scene.document.get_entity(*source).is_none())) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: associated boundary is no longer available.");
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].scene.replace_hatch_association(handle,paths);
|
||||
}
|
||||
HatchEditOperation::RecreateBoundary { associate, region } => {
|
||||
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(
|
||||
@@ -5313,25 +5354,74 @@ impl OpenCADStudio {
|
||||
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 {
|
||||
let (entities, path_groups) = if region {
|
||||
let loops = source.paths.iter().map(|path| {
|
||||
path.edges.iter().map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
|
||||
}).collect::<Option<Vec<_>>>();
|
||||
let Some(loops) = loops.filter(|loops| !loops.is_empty() && loops.iter().all(|ring| !ring.is_empty())) else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: boundary cannot form a region.");
|
||||
return Task::none();
|
||||
};
|
||||
let contained = loops.iter().enumerate().map(|(inner, ring)| {
|
||||
let seed = ring[0].point_at(0.0);
|
||||
loops.iter().enumerate().map(|(outer, boundary)| {
|
||||
inner != outer && cadkernel::geom2d::contains(boundary, seed, cadkernel::geom2d::Tolerance::new(1e-6))
|
||||
}).collect::<Vec<_>>()
|
||||
}).collect::<Vec<_>>();
|
||||
let depths = contained.iter().map(|row| row.iter().filter(|inside| **inside).count()).collect::<Vec<_>>();
|
||||
let groups = depths.iter().enumerate().filter(|(_, depth)| **depth % 2 == 0).map(|(outer, depth)| {
|
||||
let mut indices = vec![outer];
|
||||
indices.extend((0..loops.len()).filter(|inner| depths[*inner] == *depth + 1 && contained[*inner][outer]));
|
||||
indices
|
||||
}).collect::<Vec<_>>();
|
||||
let entities = groups.iter().map(|indices| {
|
||||
let profiles = indices.iter().map(|index| loops[*index].clone()).collect::<Vec<_>>();
|
||||
crate::scene::model::presspull_model::region_from_loops(&profiles, plane)
|
||||
}).collect::<Option<Vec<_>>>();
|
||||
let Some(entities) = entities else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("HATCHEDIT: boundary cannot form a region.");
|
||||
return Task::none();
|
||||
};
|
||||
(entities, groups)
|
||||
} else {
|
||||
let rings = crate::scene::hatch_boundary_rings(&source);
|
||||
if rings.len() != source.paths.len() {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a polyline.",
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let entities = crate::scene::boundary_entities(&rings, plane);
|
||||
if entities.len() != source.paths.len() {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"HATCHEDIT: boundary cannot form a polyline.",
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let groups = (0..entities.len()).map(|index| vec![index]).collect();
|
||||
(entities, groups)
|
||||
};
|
||||
let mut path_handles = vec![None; source.paths.len()];
|
||||
for (entity, paths) in entities.into_iter().zip(path_groups) {
|
||||
if let Some(boundary) = self.commit_entity_handle(entity) {
|
||||
handles.push(boundary);
|
||||
for path in paths { if let Some(slot) = path_handles.get_mut(path) { *slot = Some(boundary); } }
|
||||
}
|
||||
}
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) =
|
||||
if associate { 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);
|
||||
for (path, boundary) in hatch.paths.iter_mut().zip(path_handles.iter().copied()) {
|
||||
if let Some(boundary) = boundary {
|
||||
path.boundary_handles = vec![boundary];
|
||||
path.flags.set_external(true);
|
||||
}
|
||||
}
|
||||
hatch.is_associative = !handles.is_empty();
|
||||
}
|
||||
hatch.is_associative = path_handles.iter().all(Option::is_some);
|
||||
} }
|
||||
self.tabs[i].scene.bump_entities(&[(
|
||||
handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
@@ -5360,7 +5450,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
HatchEditOperation::DrawOrderFront
|
||||
| HatchEditOperation::DrawOrderBack => unreachable!(),
|
||||
| HatchEditOperation::DrawOrderBack
|
||||
| HatchEditOperation::DrawOrderBoundary {..}
|
||||
| HatchEditOperation::BeginAssociate => unreachable!(),
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
|
||||
@@ -789,7 +789,7 @@ impl OpenCADStudio {
|
||||
selected,
|
||||
inherited,
|
||||
plane,
|
||||
);
|
||||
).with_origin(self.tabs[i].scene.document.hatch_origin());
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
self.refresh_area_preview(i);
|
||||
@@ -822,7 +822,7 @@ impl OpenCADStudio {
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
);
|
||||
).with_appearance(entity,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()).with_origin(self.tabs[i].scene.document.hatch_origin());
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
@@ -830,7 +830,7 @@ impl OpenCADStudio {
|
||||
.push_error(crate::t!("HATCHEDIT: selected entity is not a hatch.").as_ref());
|
||||
}
|
||||
} else {
|
||||
let cmd = HatcheditCommand::new();
|
||||
let cmd = HatcheditCommand::new().with_appearance(None,self.tabs[i].scene.document.header.current_entity_color,self.tabs[i].scene.document.current_entity_transparency()).with_origin(self.tabs[i].scene.document.hatch_origin());
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
@@ -1387,6 +1387,7 @@ impl OpenCADStudio {
|
||||
crate::entities::names::dxf_name(e).to_string(),
|
||||
c.layer.clone(),
|
||||
c.color,
|
||||
c.transparency,
|
||||
c.linetype.clone(),
|
||||
c.linetype_scale,
|
||||
c.line_weight,
|
||||
@@ -1398,7 +1399,7 @@ impl OpenCADStudio {
|
||||
},
|
||||
)
|
||||
});
|
||||
let Some((verb, kind, layer, color, linetype, lt_scale, lw, template_dimstyle)) = info
|
||||
let Some((verb, kind, layer, color, transparency, linetype, lt_scale, lw, template_dimstyle)) = info
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("ADDSELECTED: selected object not found.").as_ref());
|
||||
@@ -1422,6 +1423,7 @@ impl OpenCADStudio {
|
||||
layer_name: self.tabs[i].scene.document.header.current_layer_name.clone(),
|
||||
layer_handle: self.tabs[i].scene.document.header.current_layer_handle,
|
||||
color: self.tabs[i].scene.document.header.current_entity_color,
|
||||
transparency: self.tabs[i].scene.document.current_entity_transparency(),
|
||||
linetype_name: self.tabs[i].scene.document.header.current_linetype_name.clone(),
|
||||
linetype_handle: self.tabs[i].scene.document.header.current_linetype_handle,
|
||||
line_weight: self.tabs[i].scene.document.header.current_line_weight,
|
||||
@@ -1436,6 +1438,11 @@ impl OpenCADStudio {
|
||||
ribbon_lineweight: self.ribbon.active_lineweight,
|
||||
};
|
||||
self.add_selected_restore = Some(restore);
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) {
|
||||
self.add_selected_restore = None;
|
||||
self.command_line.push_error("ADDSELECTED: template transparency cannot be adopted.");
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Adopt the template's general properties as the current defaults. The
|
||||
// entity-creation path stamps new objects from the tab's active layer
|
||||
@@ -1518,6 +1525,9 @@ impl OpenCADStudio {
|
||||
h.current_dimstyle_name = r.dimstyle_name;
|
||||
h.current_dimstyle_handle = r.dimstyle_handle;
|
||||
}
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(r.transparency) {
|
||||
self.command_line.push_error("ADDSELECTED: current transparency could not be restored.");
|
||||
}
|
||||
self.tabs[i].active_layer = r.tab_active_layer;
|
||||
self.tabs[i].layers.current_layer = r.tab_layers_current;
|
||||
self.ribbon.active_layer = r.ribbon_layer;
|
||||
|
||||
@@ -369,6 +369,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"COLOR",
|
||||
"COLOUR",
|
||||
"CECOLOR",
|
||||
"CETRANSPARENCY",
|
||||
"DDCOLOR",
|
||||
"BYLAYER",
|
||||
// Synchronise block attributes.
|
||||
|
||||
@@ -3,6 +3,10 @@ use super::*;
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
"CETRANSPARENCY" => return self.dispatch_styleprops("SETVAR CETRANSPARENCY", i),
|
||||
cmd if cmd.starts_with("CETRANSPARENCY ") => {
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
}
|
||||
"FRAMES0" => return self.dispatch_styleprops("SETVAR FRAME 0", i),
|
||||
"FRAMES1" => return self.dispatch_styleprops("SETVAR FRAME 1", i),
|
||||
"FRAMES2" => return self.dispatch_styleprops("SETVAR FRAME 2", i),
|
||||
@@ -1064,9 +1068,34 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
crate::t!("SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
);
|
||||
} else {
|
||||
if name == "CETRANSPARENCY" {
|
||||
let current = self.tabs[i].scene.document.current_entity_transparency();
|
||||
if let Some(value) = &value {
|
||||
match crate::scene::creation_style::parse_current_transparency(value) {
|
||||
Some(transparency) => {
|
||||
if current != transparency {
|
||||
self.push_undo_snapshot(i, &name);
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("CETRANSPARENCY: drawing variable dictionary is invalid.");
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.command_line.push_output(&format!("CETRANSPARENCY = {}", crate::scene::creation_style::current_transparency_label(transparency)));
|
||||
}
|
||||
None => self.command_line.push_error("CETRANSPARENCY: expected ByLayer (-1), ByBlock (-2), or an integer from 0 to 90."),
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_output(&format!("Enter new value for CETRANSPARENCY <{}>:", crate::scene::creation_style::current_transparency_label(current)));
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") {
|
||||
let current = if name == "SHOWHIST" {
|
||||
self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2)
|
||||
|
||||
@@ -305,6 +305,7 @@ struct AddSelectedRestore {
|
||||
layer_name: String,
|
||||
layer_handle: acadrust::types::Handle,
|
||||
color: AcadColor,
|
||||
transparency: acadrust::types::Transparency,
|
||||
linetype_name: String,
|
||||
linetype_handle: acadrust::types::Handle,
|
||||
line_weight: i16,
|
||||
|
||||
@@ -306,7 +306,7 @@ impl OpenCADStudio {
|
||||
label: t!("Transparency").into_owned(),
|
||||
field: "transparency",
|
||||
value: PropValue::EditChoice {
|
||||
value: "ByLayer".to_string(),
|
||||
value: crate::scene::creation_style::current_transparency_label(self.tabs[i].scene.document.current_entity_transparency()),
|
||||
options: vec!["ByLayer".to_string(), "ByBlock".to_string()],
|
||||
},
|
||||
},
|
||||
|
||||
+24
-15
@@ -2000,9 +2000,6 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
if let Some(acadrust::EntityType::Hatch(dxf)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
let old_origin = dxf.pattern.lines.first().map(|line| {
|
||||
(line.base_point.x, line.base_point.y)
|
||||
});
|
||||
let mut pattern = hatch_patterns::build_dxf_pattern(entry);
|
||||
// Stored pattern lines are final world-space
|
||||
// geometry. Preserve the selected hatch's scale,
|
||||
@@ -2016,17 +2013,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
&mut pattern,
|
||||
dxf.pattern_angle,
|
||||
);
|
||||
if let (Some((old_x, old_y)), Some(new_origin)) =
|
||||
(old_origin, pattern.lines.first())
|
||||
{
|
||||
let dx = old_x - new_origin.base_point.x;
|
||||
let dy = old_y - new_origin.base_point.y;
|
||||
crate::entities::hatch::translate_pattern_geometry(
|
||||
&mut pattern,
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
}
|
||||
let origin = dxf.pattern_origin();
|
||||
crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y);
|
||||
dxf.pattern = pattern;
|
||||
dxf.is_solid = matches!(
|
||||
entry.gpu,
|
||||
@@ -2811,7 +2799,28 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
} else {
|
||||
match field {
|
||||
"transparency" => {
|
||||
self.tabs[i].dirty = true;
|
||||
let Some(transparency) = crate::scene::creation_style::parse_current_transparency(&value) else {
|
||||
self.command_line.push_error("Transparency: expected ByLayer, ByBlock, or an integer from 0 to 90.");
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
};
|
||||
if self.tabs[i].scene.document.current_entity_transparency()
|
||||
!= transparency
|
||||
{
|
||||
self.push_undo_snapshot(i, "CETRANSPARENCY");
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.set_current_entity_transparency(transparency)
|
||||
{
|
||||
self.tabs[i].dirty = true;
|
||||
} else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"CETRANSPARENCY: drawing variable dictionary is invalid.",
|
||||
);
|
||||
}
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
"material" => {
|
||||
|
||||
@@ -3870,13 +3870,22 @@ impl OpenCADStudio {
|
||||
};
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
||||
let cmd: Box<dyn CadCommand> = Box::new(HatcheditCommand::with_handle(
|
||||
handle,
|
||||
model.name.clone(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
));
|
||||
let current_color =
|
||||
self.tabs[i].scene.document.header.current_entity_color;
|
||||
let current_transparency =
|
||||
self.tabs[i].scene.document.current_entity_transparency();
|
||||
let current_origin = self.tabs[i].scene.document.hatch_origin();
|
||||
let cmd: Box<dyn CadCommand> = Box::new(
|
||||
HatcheditCommand::with_handle(
|
||||
handle,
|
||||
model.name.clone(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
)
|
||||
.with_appearance(entity, current_color, current_transparency)
|
||||
.with_origin(current_origin),
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(cmd);
|
||||
} else {
|
||||
|
||||
+10
-1
@@ -13,13 +13,22 @@ use glam::DVec3;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HatchEditOperation {
|
||||
Appearance {
|
||||
color: Option<acadrust::types::Color>,
|
||||
layer: Option<String>,
|
||||
transparency: Option<acadrust::types::Transparency>,
|
||||
},
|
||||
Update {
|
||||
origin: Option<(f64, f64)>,
|
||||
store_origin: bool,
|
||||
disassociate: bool,
|
||||
style: Option<acadrust::entities::HatchStyleType>,
|
||||
annotative: Option<bool>,
|
||||
},
|
||||
RecreateBoundary,
|
||||
RecreateBoundary { associate: bool, region: bool },
|
||||
BeginAssociate,
|
||||
AssociatePaths(Vec<acadrust::entities::BoundaryPath>),
|
||||
DrawOrderBoundary { above: bool },
|
||||
Separate,
|
||||
AddBoundaries(Vec<Handle>),
|
||||
RemoveBoundaries(Vec<Handle>),
|
||||
|
||||
+19
-39
@@ -182,18 +182,14 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> {
|
||||
(n > 0.0).then(|| (sx / n, sy / n))
|
||||
}
|
||||
|
||||
/// Scale the stored, final hatch-pattern geometry around the pattern origin.
|
||||
/// Scale a catalog pattern about its intrinsic coordinate origin.
|
||||
/// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata
|
||||
/// scale is not applied again by the renderer.
|
||||
pub(crate) fn scale_pattern_geometry(
|
||||
pattern: &mut acadrust::entities::HatchPattern,
|
||||
factor: f64,
|
||||
) {
|
||||
let (origin_x, origin_y) = pattern
|
||||
.lines
|
||||
.first()
|
||||
.map(|line| (line.base_point.x, line.base_point.y))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
let (origin_x, origin_y) = (0.0, 0.0);
|
||||
for line in pattern.lines.iter_mut() {
|
||||
line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor;
|
||||
line.base_point.y = origin_y + (line.base_point.y - origin_y) * factor;
|
||||
@@ -205,18 +201,14 @@ pub(crate) fn scale_pattern_geometry(
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the stored, final hatch-pattern geometry around the pattern origin.
|
||||
/// Rotate a catalog pattern about its intrinsic coordinate origin.
|
||||
/// This keeps the line angle, base point and world-space offset in sync.
|
||||
pub(crate) fn rotate_pattern_geometry(
|
||||
pattern: &mut acadrust::entities::HatchPattern,
|
||||
angle: f64,
|
||||
) {
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let (origin_x, origin_y) = pattern
|
||||
.lines
|
||||
.first()
|
||||
.map(|line| (line.base_point.x, line.base_point.y))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
let (origin_x, origin_y) = (0.0, 0.0);
|
||||
for line in pattern.lines.iter_mut() {
|
||||
line.angle += angle;
|
||||
let (x, y) = (
|
||||
@@ -613,7 +605,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
_ => h.pattern_type,
|
||||
};
|
||||
if requested != h.pattern_type {
|
||||
let old_origin = h.pattern.lines.first().map(|line| line.base_point);
|
||||
let old_origin = h.pattern_origin();
|
||||
h.pattern_type = requested;
|
||||
h.is_solid = false;
|
||||
match requested {
|
||||
@@ -629,15 +621,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
translate_pattern_geometry(&mut pattern, old_origin.x, old_origin.y);
|
||||
h.pattern = pattern;
|
||||
}
|
||||
}
|
||||
@@ -695,7 +679,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
"pattern_angle" => {
|
||||
let angle = v.to_radians();
|
||||
let delta = angle - h.pattern_angle;
|
||||
rotate_pattern_geometry(&mut h.pattern, delta);
|
||||
h.rotate_pattern_about_origin(delta);
|
||||
h.pattern_angle = angle;
|
||||
}
|
||||
// The stored pattern lines are the FINAL rendered geometry (offsets,
|
||||
@@ -706,7 +690,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
let old = h.pattern_scale;
|
||||
if old > 1e-12 {
|
||||
let k = v / old;
|
||||
scale_pattern_geometry(&mut h.pattern, k);
|
||||
h.scale_pattern_about_origin(k);
|
||||
}
|
||||
h.pattern_scale = v;
|
||||
}
|
||||
@@ -724,19 +708,17 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
}
|
||||
// Origin rows are relative offsets and return to zero after commit.
|
||||
"origin_x" => {
|
||||
for line in h.pattern.lines.iter_mut() {
|
||||
line.base_point.x += v;
|
||||
}
|
||||
let origin = h.pattern_origin();
|
||||
h.set_pattern_origin(acadrust::types::Vector2::new(origin.x + v, origin.y));
|
||||
}
|
||||
"origin_y" => {
|
||||
for line in h.pattern.lines.iter_mut() {
|
||||
line.base_point.y += v;
|
||||
}
|
||||
let origin = h.pattern_origin();
|
||||
h.set_pattern_origin(acadrust::types::Vector2::new(origin.x, origin.y + v));
|
||||
}
|
||||
"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.scale_pattern_about_origin(v / old);
|
||||
}
|
||||
h.pattern_scale = v;
|
||||
}
|
||||
@@ -876,10 +858,8 @@ impl Grippable for Hatch {
|
||||
GripApply::Absolute(point) => (point.x - gx, point.y - gy),
|
||||
GripApply::Translate(delta) => (delta.x, delta.y),
|
||||
};
|
||||
for line in self.pattern.lines.iter_mut() {
|
||||
line.base_point.x += dx;
|
||||
line.base_point.y += dy;
|
||||
}
|
||||
let origin = self.pattern_origin();
|
||||
self.set_pattern_origin(acadrust::types::Vector2::new(origin.x + dx, origin.y + dy));
|
||||
return;
|
||||
}
|
||||
id += 1;
|
||||
@@ -1005,11 +985,11 @@ impl Grippable for Hatch {
|
||||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if grip_id == 0 && matches!(action, A::OriginPoint) {
|
||||
if let (Some((gx, gy)), Some((ox, oy))) = (
|
||||
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);
|
||||
self.set_pattern_origin(acadrust::types::Vector2::new(gx, gy));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1038,14 +1018,14 @@ impl Grippable for Hatch {
|
||||
A::HatchAngle => {
|
||||
let angle = value.to_radians();
|
||||
let delta = angle - self.pattern_angle;
|
||||
rotate_pattern_geometry(&mut self.pattern, delta);
|
||||
self.rotate_pattern_about_origin(delta);
|
||||
self.pattern_angle = angle;
|
||||
}
|
||||
A::HatchScale => {
|
||||
if value > 0.0 {
|
||||
if self.pattern_scale > 1e-12 {
|
||||
let factor = value / self.pattern_scale;
|
||||
scale_pattern_geometry(&mut self.pattern, factor);
|
||||
self.scale_pattern_about_origin(factor);
|
||||
}
|
||||
self.pattern_scale = value;
|
||||
}
|
||||
|
||||
@@ -927,6 +927,7 @@ fn emit_wire_fills(
|
||||
}
|
||||
boundary.push(boundary[0]);
|
||||
let hatch = HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: wire.render_instance.clone(),
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
|
||||
@@ -196,6 +196,7 @@ pub struct HatchCommand {
|
||||
pattern_override: Option<(String, HatchPattern)>,
|
||||
angle_override: Option<f32>,
|
||||
scale_override: Option<f32>,
|
||||
default_origin: [f64; 2],
|
||||
associative: bool,
|
||||
separate_hatches: bool,
|
||||
island_style: acadrust::entities::HatchStyleType,
|
||||
@@ -244,6 +245,7 @@ impl HatchCommand {
|
||||
pattern_override: None,
|
||||
angle_override: None,
|
||||
scale_override: None,
|
||||
default_origin: [0.0, 0.0],
|
||||
associative: true,
|
||||
separate_hatches: false,
|
||||
island_style: inherited
|
||||
@@ -257,6 +259,8 @@ impl HatchCommand {
|
||||
command
|
||||
}
|
||||
|
||||
pub fn with_origin(mut self, origin: [f64; 2]) -> Self { self.default_origin = origin; self }
|
||||
|
||||
fn set_object_selection(&mut self, handles: Vec<Handle>) {
|
||||
let mut segments = Vec::new();
|
||||
for handle in &handles {
|
||||
@@ -383,6 +387,7 @@ impl HatchCommand {
|
||||
}
|
||||
}
|
||||
return HatchModel {
|
||||
pattern_origin: source.pattern_origin,
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
pattern,
|
||||
@@ -425,11 +430,23 @@ impl HatchCommand {
|
||||
dashes: vec![],
|
||||
}])
|
||||
});
|
||||
let (name, pattern) = self
|
||||
let (name, mut pattern) = self
|
||||
.pattern_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| (pat_name.to_string(), default_pattern));
|
||||
let angle = self.angle_override.unwrap_or(0.0);
|
||||
let scale = self.scale_override.unwrap_or(1.0).max(1.0e-6);
|
||||
if let (HatchPattern::Pattern(families), Some(anchor)) = (&mut pattern, local_boundary.first()) {
|
||||
let (sin, cos) = (angle as f64).sin_cos();
|
||||
let dx = self.default_origin[0] - anchor[0] as f64;
|
||||
let dy = self.default_origin[1] - anchor[1] as f64;
|
||||
for family in families {
|
||||
family.x0 += ((dx * cos + dy * sin) / scale as f64) as f32;
|
||||
family.y0 += ((-dx * sin + dy * cos) / scale as f64) as f32;
|
||||
}
|
||||
}
|
||||
HatchModel {
|
||||
pattern_origin: Some(self.default_origin),
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
pattern,
|
||||
@@ -954,6 +971,7 @@ impl GradientCommand {
|
||||
1.0e-6,
|
||||
);
|
||||
HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
pattern: HatchPattern::Gradient {
|
||||
|
||||
@@ -28,9 +28,23 @@ pub struct HatcheditCommand {
|
||||
step: HatcheditStep,
|
||||
origin: Option<(f64, f64)>,
|
||||
disassociate: bool,
|
||||
store_origin: bool,
|
||||
current_origin: [f64; 2],
|
||||
origin_plane: cadkernel::space::Plane,
|
||||
origin_bounds: Option<([f64;2],[f64;2])>,
|
||||
origin_bounds_unavailable: bool,
|
||||
style: Option<acadrust::entities::HatchStyleType>,
|
||||
annotative: Option<bool>,
|
||||
annotative_current: bool,
|
||||
style_current: acadrust::entities::HatchStyleType,
|
||||
input: Option<&'static str>,
|
||||
source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>,
|
||||
current_color: acadrust::types::Color,
|
||||
current_transparency: acadrust::types::Transparency,
|
||||
boundary_region: bool,
|
||||
association_sources: Option<(crate::command::WorkingPlane,rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>)>,
|
||||
association_paths: Vec<acadrust::entities::BoundaryPath>,
|
||||
association_missed: bool,
|
||||
}
|
||||
|
||||
impl HatcheditCommand {
|
||||
@@ -39,9 +53,23 @@ impl HatcheditCommand {
|
||||
step: HatcheditStep::PickHatch,
|
||||
origin: None,
|
||||
disassociate: false,
|
||||
store_origin: false,
|
||||
origin_bounds: None,
|
||||
origin_bounds_unavailable: false,
|
||||
current_origin: [0.0, 0.0],
|
||||
origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
|
||||
style: None,
|
||||
annotative: None,
|
||||
annotative_current: false,
|
||||
style_current: acadrust::entities::HatchStyleType::Normal,
|
||||
input: None,
|
||||
source_appearance: None,
|
||||
current_color: acadrust::types::Color::ByLayer,
|
||||
current_transparency: acadrust::types::Transparency::ByLayer,
|
||||
boundary_region: false,
|
||||
association_sources: None,
|
||||
association_paths: Vec::new(),
|
||||
association_missed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +89,23 @@ impl HatcheditCommand {
|
||||
},
|
||||
origin: None,
|
||||
disassociate: false,
|
||||
store_origin: false,
|
||||
origin_bounds: None,
|
||||
origin_bounds_unavailable: false,
|
||||
current_origin: [0.0, 0.0],
|
||||
origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
|
||||
style: None,
|
||||
annotative: None,
|
||||
annotative_current: annotative,
|
||||
style_current: acadrust::entities::HatchStyleType::Normal,
|
||||
input: None,
|
||||
source_appearance: None,
|
||||
current_color: acadrust::types::Color::ByLayer,
|
||||
current_transparency: acadrust::types::Transparency::ByLayer,
|
||||
boundary_region: false,
|
||||
association_sources: None,
|
||||
association_paths: Vec::new(),
|
||||
association_missed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +128,57 @@ impl HatcheditCommand {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn for_association(handle:Handle,name:String,scale:f32,angle:f32,plane:crate::command::WorkingPlane,sources:rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>)->Self {
|
||||
let mut command=Self::with_handle(handle,name,scale,angle,false);
|
||||
command.input=Some("associate-point");command.association_sources=Some((plane,sources));command
|
||||
}
|
||||
fn add_association_rings(&mut self,rings:Vec<Vec<[f64;2]>>,sources:&rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>) {
|
||||
let exterior=cadkernel::geom2d::ring_nesting_depths(&rings).into_iter().map(|depth|depth==0).collect::<Vec<_>>();
|
||||
let paths=crate::scene::exact_hatch_paths(&rings,&exterior,sources,1e-6);
|
||||
self.association_missed=paths.is_empty()||paths.len()!=rings.len()||paths.iter().any(|path|path.boundary_handles.is_empty());
|
||||
if !self.association_missed {
|
||||
for path in paths {if !self.association_paths.contains(&path){self.association_paths.push(path);}}
|
||||
}
|
||||
}
|
||||
pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color,current_transparency:acadrust::types::Transparency)->Self {
|
||||
if let Some(acadrust::EntityType::Hatch(hatch)) = entity {
|
||||
self.style_current = hatch.style;
|
||||
self.origin_plane = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation);
|
||||
self.origin_bounds = hatch.paths.iter().flat_map(|path| &path.edges)
|
||||
.map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
|
||||
.and_then(|curves| {
|
||||
// Full ellipses use the four axis endpoints as origin anchors,
|
||||
// not their geometric extrema. Keep the general bounds contract
|
||||
// unchanged and aggregate these command-specific anchor segments.
|
||||
let mut anchors = Vec::new();
|
||||
for curve in curves {
|
||||
match curve {
|
||||
cadkernel::geom2d::Curve::Ellipse(arc) => {
|
||||
if (arc.end_parameter - arc.start_parameter).abs() < std::f64::consts::TAU { return None; }
|
||||
for angle in [0.0, std::f64::consts::FRAC_PI_2] {
|
||||
anchors.push(cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
|
||||
start: arc.ellipse.point_at(angle),
|
||||
end: arc.ellipse.point_at(angle + std::f64::consts::PI),
|
||||
}));
|
||||
}
|
||||
}
|
||||
cadkernel::geom2d::Curve::Nurbs(_) => return None,
|
||||
other => anchors.push(other),
|
||||
}
|
||||
}
|
||||
cadkernel::geom2d::analytic_curve_bounds(&anchors)
|
||||
});
|
||||
}
|
||||
self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)});
|
||||
self.current_color=current_color;self.current_transparency=current_transparency;self
|
||||
}
|
||||
|
||||
pub fn with_origin(mut self, origin: [f64; 2]) -> Self { self.current_origin = origin; self }
|
||||
|
||||
fn update_operation(&self) -> HatchEditOperation {
|
||||
HatchEditOperation::Update {
|
||||
origin: self.origin,
|
||||
store_origin: self.store_origin,
|
||||
disassociate: self.disassociate,
|
||||
style: self.style,
|
||||
annotative: self.annotative,
|
||||
@@ -102,6 +192,58 @@ impl CadCommand for HatcheditCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if let Some(input)=self.input {
|
||||
if input == "style" {
|
||||
let current = match self.style_current {
|
||||
acadrust::entities::HatchStyleType::Normal => "Normal",
|
||||
acadrust::entities::HatchStyleType::Outer => "Outer",
|
||||
acadrust::entities::HatchStyleType::Ignore => "Ignore",
|
||||
};
|
||||
return format!("Enter hatching style [Ignore/Outer/Normal] <{current}>:");
|
||||
}
|
||||
if let Some((color,layer,transparency))=&self.source_appearance {
|
||||
match input {
|
||||
"color"=>return format!("New color [Truecolor/. (for use current)] <{color:?}>:"),
|
||||
"layer"=>return format!("Specify layer or [. (for use current)] <{layer}>:"),
|
||||
"transparency"=>return format!("Specify transparency (0-90) or ByLayer/ByBlock <{}>:",match transparency {
|
||||
acadrust::types::Transparency::ByLayer=>"ByLayer".into(),
|
||||
acadrust::types::Transparency::ByBlock=>"ByBlock".into(),
|
||||
value=>format!("{:.0}",value.as_percent()*100.0),
|
||||
}),
|
||||
_=>{},
|
||||
}
|
||||
}
|
||||
if let HatcheditStep::EditOptions{name,scale,angle,..}=&self.step {
|
||||
match input {
|
||||
"pattern"=>return format!("Enter a pattern name or [Solid] <{name}>:"),
|
||||
"scale"=>return format!("Specify a scale for the pattern <{scale:.4}>:"),
|
||||
"angle"=>return format!("Specify an angle for the pattern <{angle:.4}>:"),
|
||||
_=>{},
|
||||
}
|
||||
}
|
||||
return match input {
|
||||
"annotative"=>if self.annotative_current {"Make hatch annotative [Yes/No] <Y>:"} else {"Make hatch annotative [Yes/No] <N>:"},
|
||||
"origin" if self.origin_bounds_unavailable=>"Boundary extents unavailable for this geometry. [Use current origin/Set new origin/Default to boundary extents] <Use current origin>:",
|
||||
"origin"=>"[Use current origin/Set new origin/Default to boundary extents] <Use current origin>:",
|
||||
"origin-extents"=>"[bottom Left/bottom Right/top rIght/top lEft/Center] <bottom Left>:",
|
||||
"origin-point"=>"Select point:",
|
||||
"origin-store"=>"Store as default origin? [Yes/No] <N>:",
|
||||
"pattern"=>"Enter a pattern name or [Solid]:",
|
||||
"scale"=>"Specify a scale for the pattern:",
|
||||
"angle"=>"Specify an angle for the pattern:",
|
||||
"color"=>"New color [Truecolor] <ByLayer>:",
|
||||
"truecolor"=>"Specify RGB color (red,green,blue):",
|
||||
"layer"=>"Specify layer or [. (for use current)]:",
|
||||
"transparency"=>"Specify transparency value (0-90) or ByLayer/ByBlock:",
|
||||
"draworder"=>"Enter draw order [do Not change/send to Back/bring to Front/send beHind boundary/bring in front of bounDary] <do Not change>:",
|
||||
"boundary-type"=>"Enter type of boundary object [Region/Polyline] <Polyline>:",
|
||||
"boundary-associate"=>"Reassociate hatch with new boundary? [Yes/No] <No>:",
|
||||
"associate-select"=>"Select boundary objects:",
|
||||
"associate-point" if self.association_missed=>"No closed boundary found. Specify internal point or [Select objects]:",
|
||||
"associate-point"=>"Specify internal point or [Select objects]:",
|
||||
_=>"Specify value:",
|
||||
}.into();
|
||||
}
|
||||
match &self.step {
|
||||
HatcheditStep::PickHatch => t!("HATCHEDIT Select hatch:").into_owned(),
|
||||
HatcheditStep::EditOptions {
|
||||
@@ -110,7 +252,7 @@ impl CadCommand for HatcheditCommand {
|
||||
let scale = format!("{scale:.4}");
|
||||
let angle = format!("{angle:.1}");
|
||||
t!(
|
||||
"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]:",
|
||||
"HATCHEDIT Pattern:%{name} Scale:%{scale} Angle:%{angle} [Style/Origin/Properties/COlor/LAyer/Transparency/DRaw order/ASsociate/DIsassociate/ANnotative/recreate Boundary/separate Hatches] <Properties>:",
|
||||
name = name,
|
||||
scale = scale,
|
||||
angle = angle
|
||||
@@ -123,6 +265,15 @@ impl CadCommand for HatcheditCommand {
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, HatcheditStep::PickHatch)
|
||||
}
|
||||
fn is_selection_gathering(&self)->bool {self.input==Some("associate-select")}
|
||||
fn on_selection_complete(&mut self,handles:Vec<Handle>)->CmdResult {
|
||||
if let Some((_,sources))=&self.association_sources {
|
||||
let sources=sources.iter().filter(|(handle,_)|handles.contains(handle)).map(|(handle,source)|(*handle,source.clone())).collect();
|
||||
let rings=crate::scene::boundary_faces(&sources,1e-6);
|
||||
self.add_association_rings(rings,&sources);
|
||||
}
|
||||
self.input=Some("associate-point");CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
@@ -140,25 +291,159 @@ impl CadCommand for HatcheditCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, HatcheditStep::EditOptions { .. })
|
||||
matches!(self.step, HatcheditStep::EditOptions { .. })&&!self.is_selection_gathering()
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
if self.input==Some("associate-point") {return vec![crate::command::CmdOption::new("Select objects","S")];}
|
||||
if let Some(input) = self.input {
|
||||
use crate::command::CmdOption;
|
||||
return match input {
|
||||
"origin" => vec![CmdOption::new("Use current origin", "U"), CmdOption::new("Set new origin", "S"), CmdOption::new("Default to boundary extents", "D")],
|
||||
"origin-extents" => vec![CmdOption::new("Bottom left", "L"), CmdOption::new("Bottom right", "R"), CmdOption::new("Top right", "I"), CmdOption::new("Top left", "E"), CmdOption::new("Center", "C")],
|
||||
"origin-store" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")],
|
||||
"style" => vec![CmdOption::new("Ignore", "I"), CmdOption::new("Outer", "O"), CmdOption::new("Normal", "N")],
|
||||
"draworder" => vec![CmdOption::new("Do not change", "N"), CmdOption::new("Send to back", "B"), CmdOption::new("Bring to front", "F"), CmdOption::new("Behind boundary", "H"), CmdOption::new("In front of boundary", "D")],
|
||||
"annotative" | "boundary-associate" => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")],
|
||||
"boundary-type" => vec![CmdOption::new("Region", "R"), CmdOption::new("Polyline", "P")],
|
||||
"color" => vec![CmdOption::new("Truecolor", "T")],
|
||||
"pattern" => vec![CmdOption::new("Solid", "SOLID")],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
}
|
||||
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"),
|
||||
crate::command::CmdOption::new("Style", "S"),
|
||||
crate::command::CmdOption::new("Origin", "O"),
|
||||
crate::command::CmdOption::new("Properties", "P"),
|
||||
crate::command::CmdOption::new("Color", "CO"),
|
||||
crate::command::CmdOption::new("Layer", "LA"),
|
||||
crate::command::CmdOption::new("Transparency", "T"),
|
||||
crate::command::CmdOption::new("Draw order", "DR"),
|
||||
crate::command::CmdOption::new("Associate", "AS"),
|
||||
crate::command::CmdOption::new("Disassociate", "DI"),
|
||||
crate::command::CmdOption::new("Annotative", "AN"),
|
||||
crate::command::CmdOption::new("Recreate boundary", "B"),
|
||||
crate::command::CmdOption::new("Separate hatches", "H"),
|
||||
crate::command::CmdOption::enter("Properties"),
|
||||
]
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword=text.trim().to_ascii_uppercase();
|
||||
if let Some(input)=self.input {
|
||||
use acadrust::types::{Color,Transparency};
|
||||
let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency};
|
||||
match input {
|
||||
"origin" => match keyword.as_str() {
|
||||
"U" | "USE" => { self.origin = Some((self.current_origin[0], self.current_origin[1])); return self.apply_result(self.update_operation()); }
|
||||
"S" | "SET" => { self.input = Some("origin-point"); }
|
||||
"D" | "DEFAULT" => {
|
||||
self.origin_bounds_unavailable = self.origin_bounds.is_none();
|
||||
if !self.origin_bounds_unavailable { self.input = Some("origin-extents"); }
|
||||
}
|
||||
_ => {},
|
||||
},
|
||||
"origin-extents" => {
|
||||
if let Some((min,max)) = self.origin_bounds {
|
||||
let origin = match keyword.as_str() {
|
||||
"L" | "LEFT" => min,
|
||||
"R" | "RIGHT" => [max[0],min[1]],
|
||||
"I" | "TOP RIGHT" => max,
|
||||
"E" | "TOP LEFT" => [min[0],max[1]],
|
||||
"C" | "CENTER" => [min[0]*0.5+max[0]*0.5,min[1]*0.5+max[1]*0.5],
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
};
|
||||
self.origin = Some((origin[0],origin[1]));
|
||||
self.input = Some("origin-store");
|
||||
}
|
||||
}
|
||||
"origin-point" => {
|
||||
let values: Option<Vec<f64>> = text.split(',').map(|s| s.trim().parse().ok()).collect();
|
||||
if let Some(values) = values.filter(|v| (2..=3).contains(&v.len()) && v.iter().all(|n| n.is_finite())) {
|
||||
return Some(self.on_point(DVec3::new(values[0], values[1], values.get(2).copied().unwrap_or(0.0))));
|
||||
}
|
||||
}
|
||||
"origin-store" => {
|
||||
self.store_origin = match keyword.as_str() { "Y" | "YES" => true, "N" | "NO" => false, _ => return Some(CmdResult::NeedPoint) };
|
||||
return self.apply_result(self.update_operation());
|
||||
}
|
||||
"style" => {
|
||||
self.style = match keyword.as_str() {
|
||||
"I" | "IGNORE" => Some(acadrust::entities::HatchStyleType::Ignore),
|
||||
"O" | "OUTER" => Some(acadrust::entities::HatchStyleType::Outer),
|
||||
"N" | "NORMAL" => Some(acadrust::entities::HatchStyleType::Normal),
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
};
|
||||
return self.apply_result(self.update_operation());
|
||||
}
|
||||
"annotative" => {
|
||||
let value = match keyword.as_str() { "Y" | "YES" => true, "N" | "NO" => false, _ => return Some(CmdResult::NeedPoint) };
|
||||
self.annotative = Some(value);
|
||||
return self.apply_result(self.update_operation());
|
||||
}
|
||||
"associate-point"=>{
|
||||
if matches!(keyword.as_str(),"S"|"SELECT"|"SELECT OBJECTS") {self.input=Some("associate-select");}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
},
|
||||
"color"=>{
|
||||
if matches!(keyword.as_str(),"T"|"TRUECOLOR") {self.input=Some("truecolor");return Some(CmdResult::NeedPoint);}
|
||||
let color=match keyword.as_str(){"."=>Some(self.current_color),"BYLAYER"=>Some(Color::ByLayer),"BYBLOCK"=>Some(Color::ByBlock),
|
||||
"RED"=>Some(Color::Index(1)),"YELLOW"=>Some(Color::Index(2)),"GREEN"=>Some(Color::Index(3)),
|
||||
"CYAN"=>Some(Color::Index(4)),"BLUE"=>Some(Color::Index(5)),"MAGENTA"=>Some(Color::Index(6)),"WHITE"=>Some(Color::Index(7)),
|
||||
n=>n.parse::<i16>().ok().filter(|v|(0..=256).contains(v)).map(Color::from_index)};
|
||||
return Some(color.and_then(|v|self.apply_result(appearance(Some(v),None,None))).unwrap_or(CmdResult::NeedPoint));
|
||||
}
|
||||
"truecolor"=>{
|
||||
let rgb:Option<Vec<u8>>=keyword.split(',').map(|s|s.trim().parse().ok()).collect();
|
||||
if let Some(rgb)=rgb.filter(|rgb|rgb.len()==3) {return self.apply_result(appearance(Some(Color::from_rgb(rgb[0],rgb[1],rgb[2])),None,None));}
|
||||
}
|
||||
"layer"=>if !text.trim().is_empty(){return self.apply_result(appearance(None,Some(text.trim().to_owned()),None));},
|
||||
"transparency"=>{
|
||||
let value=match keyword.as_str(){"."=>Some(self.current_transparency),"BYLAYER"=>Some(Transparency::BY_LAYER),"BYBLOCK"=>Some(Transparency::BY_BLOCK),
|
||||
n=>n.parse::<u8>().ok().filter(|v|*v<=90).map(|v|Transparency::from_percent(v as f64 / 100.0))};
|
||||
if let Some(value)=value{return self.apply_result(appearance(None,None,Some(value)));}
|
||||
}
|
||||
"draworder"=>return match keyword.as_str(){"F"|"FRONT"=>self.apply_result(HatchEditOperation::DrawOrderFront),
|
||||
"H"|"BEHIND"=>self.apply_result(HatchEditOperation::DrawOrderBoundary{above:false}),
|
||||
"D"=>self.apply_result(HatchEditOperation::DrawOrderBoundary{above:true}),
|
||||
"B"|"BACK"=>self.apply_result(HatchEditOperation::DrawOrderBack),"N"|"NOT"=>Some(CmdResult::Cancel),_=>Some(CmdResult::NeedPoint)},
|
||||
"boundary-type"=>match keyword.as_str(){
|
||||
"P"|"POLYLINE"=>{self.boundary_region=false;self.input=Some("boundary-associate");},
|
||||
"R"|"REGION"=>{self.boundary_region=true;self.input=Some("boundary-associate");},
|
||||
_=>{},
|
||||
},
|
||||
"boundary-associate"=>return match keyword.as_str(){
|
||||
"Y"|"YES"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:true,region:self.boundary_region}),
|
||||
"N"|"NO"=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false,region:self.boundary_region}),
|
||||
_=>Some(CmdResult::NeedPoint),
|
||||
},
|
||||
"pattern"=>{
|
||||
if crate::scene::model::hatch_patterns::find(&keyword).is_some() {
|
||||
if let HatcheditStep::EditOptions{name,..}=&mut self.step{*name=keyword.clone();}
|
||||
if keyword=="SOLID" {return self.apply_result(self.update_operation());}
|
||||
self.input=Some("scale");
|
||||
}
|
||||
}
|
||||
"scale"|"angle"=>if let Ok(value)=keyword.parse::<f32>() {if value.is_finite()&&(input=="angle"||value>0.0){
|
||||
if let HatcheditStep::EditOptions{scale,angle,..}=&mut self.step{if input=="scale"{*scale=value;}else{*angle=value;}}
|
||||
if input=="scale"{self.input=Some("angle");}else{return self.apply_result(self.update_operation());}
|
||||
}},
|
||||
_=>{},
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
let next=match keyword.as_str(){"O"|"ORIGIN"=>Some("origin"),"S"|"STYLE"=>Some("style"),"P"|"PROPERTIES"=>Some("pattern"),"CO"|"COLOR"=>Some("color"),"LA"|"LAYER"=>Some("layer"),
|
||||
"AN"|"ANNOTATIVE"=>Some("annotative"),"T"|"TRANSPARENCY"=>Some("transparency"),"DR"|"DRAW"|"DRAW ORDER"=>Some("draworder"),
|
||||
"B"|"BOUNDARY"|"R"|"RECREATE"=>Some("boundary-type"),_=>None};
|
||||
if let Some(input)=next {self.input=Some(input);return Some(CmdResult::NeedPoint);}
|
||||
if matches!(keyword.as_str(),"H"|"HATCHES"|"SEPARATE") {return self.apply_result(HatchEditOperation::Separate);}
|
||||
if matches!(keyword.as_str(),"AS"|"ASSOCIATE"){return self.apply_result(HatchEditOperation::BeginAssociate);}
|
||||
if matches!(keyword.as_str(),"DI"|"DISASSOCIATE"){
|
||||
self.disassociate=true;return self.apply_result(self.update_operation());
|
||||
}
|
||||
let (_handle, name, scale, angle) = match &mut self.step {
|
||||
HatcheditStep::EditOptions {
|
||||
handle,
|
||||
@@ -236,7 +521,7 @@ impl CadCommand for HatcheditCommand {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if text == "R" || text == "RECREATE" {
|
||||
return self.apply_result(HatchEditOperation::RecreateBoundary);
|
||||
self.input=Some("boundary-type");return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if text == "E" || text == "SEPARATE" {
|
||||
return self.apply_result(HatchEditOperation::Separate);
|
||||
@@ -269,18 +554,295 @@ impl CadCommand for HatcheditCommand {
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if self.input == Some("origin-point") {
|
||||
if let Some(point) = self.origin_plane.project(pt.to_array()) {
|
||||
if point.iter().all(|value| value.is_finite()) {
|
||||
self.origin = Some((point[0], point[1]));
|
||||
self.input = Some("origin-store");
|
||||
}
|
||||
}
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.input==Some("associate-point") {
|
||||
if let Some((plane,sources))=&self.association_sources {
|
||||
let point=plane.to_local(pt);
|
||||
let sources=sources.clone();
|
||||
if let Some(rings)=crate::scene::model::presspull_model::selected_rings(&sources,[point.x,point.y]) {
|
||||
self.add_association_rings(rings,&sources);
|
||||
}else{self.association_missed=true;}
|
||||
}
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Enter without text → apply current settings
|
||||
self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)
|
||||
match self.input {
|
||||
None if matches!(self.step,HatcheditStep::EditOptions{..})=>{
|
||||
self.input=Some("pattern");CmdResult::NeedPoint
|
||||
}
|
||||
Some("origin")=>{self.origin=Some((self.current_origin[0],self.current_origin[1]));self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)},
|
||||
Some("origin-extents")=>{
|
||||
if let Some((min,_)) = self.origin_bounds { self.origin=Some((min[0],min[1])); self.input=Some("origin-store"); }
|
||||
CmdResult::NeedPoint
|
||||
},
|
||||
Some("origin-point")=>CmdResult::NeedPoint,
|
||||
Some("origin-store")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel),
|
||||
Some("style")=>{self.style=Some(self.style_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)},
|
||||
Some("annotative")=>{self.annotative=Some(self.annotative_current);self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)},
|
||||
Some("pattern")=>{
|
||||
if matches!(&self.step,HatcheditStep::EditOptions{name,..} if name.eq_ignore_ascii_case("SOLID")) {
|
||||
self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel)
|
||||
}else{self.input=Some("scale");CmdResult::NeedPoint}
|
||||
}
|
||||
Some("scale")=>{self.input=Some("angle");CmdResult::NeedPoint}
|
||||
Some("angle")=>self.apply_result(self.update_operation()).unwrap_or(CmdResult::Cancel),
|
||||
Some("boundary-type")=>{self.boundary_region=false;self.input=Some("boundary-associate");CmdResult::NeedPoint},
|
||||
Some("boundary-associate")=>self.apply_result(HatchEditOperation::RecreateBoundary{associate:false,region:self.boundary_region}).unwrap_or(CmdResult::Cancel),
|
||||
Some("associate-select")=>{self.input=Some("associate-point");CmdResult::NeedPoint},
|
||||
Some("associate-point")=>if self.association_paths.is_empty(){CmdResult::Cancel}else{
|
||||
self.apply_result(HatchEditOperation::AssociatePaths(self.association_paths.clone())).unwrap_or(CmdResult::Cancel)
|
||||
},
|
||||
_=>CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::entities::hatch::{
|
||||
BoundaryEdge, BoundaryPath, EllipticArcEdge, LineEdge,
|
||||
};
|
||||
use acadrust::entities::{Hatch, HatchStyleType};
|
||||
use acadrust::types::{Color, Transparency, Vector2};
|
||||
use acadrust::EntityType;
|
||||
|
||||
fn hatch_command() -> HatcheditCommand {
|
||||
let mut hatch = Hatch::new();
|
||||
hatch.common.handle = Handle::new(7);
|
||||
hatch.common.layer = "Boundary".into();
|
||||
hatch.common.color = Color::Index(3);
|
||||
hatch.common.transparency = Transparency::T_20;
|
||||
hatch.style = HatchStyleType::Outer;
|
||||
let mut path = BoundaryPath::new();
|
||||
for (start, end) in [
|
||||
([1.0, 2.0], [5.0, 2.0]),
|
||||
([5.0, 2.0], [5.0, 6.0]),
|
||||
([5.0, 6.0], [1.0, 6.0]),
|
||||
([1.0, 6.0], [1.0, 2.0]),
|
||||
] {
|
||||
path.edges.push(BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
}));
|
||||
}
|
||||
hatch.paths.push(path);
|
||||
let entity = EntityType::Hatch(hatch);
|
||||
HatcheditCommand::with_handle(Handle::new(7), "ANSI31".into(), 1.0, 0.0, false)
|
||||
.with_appearance(Some(&entity), Color::Index(6), Transparency::T_40)
|
||||
.with_origin([9.0, 8.0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_stages_validate_and_apply_pattern_values() {
|
||||
let mut command = hatch_command();
|
||||
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(command.prompt().contains("ANSI31"));
|
||||
assert!(matches!(
|
||||
command.on_text_input("missing"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_text_input("ANSI31"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_text_input("0"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_text_input("2.5"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
let Some(CmdResult::HatcheditApply {
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
operation: HatchEditOperation::Update { .. },
|
||||
..
|
||||
}) = command.on_text_input("30")
|
||||
else {
|
||||
panic!("expected a validated hatch update");
|
||||
};
|
||||
assert_eq!(name, "ANSI31");
|
||||
assert_eq!(scale, 2.5);
|
||||
assert_eq!(angle, 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_stages_keep_exact_color_layer_and_transparency_choices() {
|
||||
let mut color = hatch_command();
|
||||
assert!(matches!(
|
||||
color.on_text_input("CO"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
color.on_text_input("red"),
|
||||
Some(CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Appearance {
|
||||
color: Some(Color::Index(1)),
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
let mut layer = hatch_command();
|
||||
assert!(matches!(
|
||||
layer.on_text_input("LA"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
layer.on_text_input("CaseSensitiveLayer"),
|
||||
Some(CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Appearance {
|
||||
layer: Some(value),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) if value == "CaseSensitiveLayer"
|
||||
));
|
||||
|
||||
let mut transparency = hatch_command();
|
||||
assert!(matches!(
|
||||
transparency.on_text_input("T"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
transparency.on_text_input("91"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
transparency.on_text_input("25"),
|
||||
Some(CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Appearance {
|
||||
transparency: Some(Transparency::Explicit(64)),
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_choices_use_drawing_default_or_analytic_boundary_extents() {
|
||||
let mut drawing_default = hatch_command();
|
||||
assert!(matches!(
|
||||
drawing_default.on_text_input("O"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
drawing_default.on_text_input("U"),
|
||||
Some(CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Update {
|
||||
origin: Some((9.0, 8.0)),
|
||||
store_origin: false,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
let mut extents = hatch_command();
|
||||
assert!(matches!(
|
||||
extents.on_text_input("O"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
extents.on_text_input("D"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(extents.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(matches!(
|
||||
extents.on_text_input("Y"),
|
||||
Some(CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Update {
|
||||
origin: Some((1.0, 2.0)),
|
||||
store_origin: true,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_enter_keeps_the_selected_hatch_default() {
|
||||
let mut command = hatch_command();
|
||||
assert!(matches!(
|
||||
command.on_text_input("S"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_enter(),
|
||||
CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Update {
|
||||
style: Some(HatchStyleType::Outer),
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_ellipse_origin_uses_axis_anchor_bounds() {
|
||||
let mut hatch = Hatch::new();
|
||||
let mut path = BoundaryPath::new();
|
||||
path.edges.push(BoundaryEdge::EllipticArc(EllipticArcEdge {
|
||||
center: Vector2::new(10.0, 20.0),
|
||||
major_axis_endpoint: Vector2::new(3.0, 4.0),
|
||||
minor_axis_ratio: 0.4,
|
||||
start_angle: 0.0,
|
||||
end_angle: std::f64::consts::TAU,
|
||||
counter_clockwise: true,
|
||||
}));
|
||||
hatch.paths.push(path);
|
||||
let entity = EntityType::Hatch(hatch);
|
||||
let mut command = HatcheditCommand::with_handle(
|
||||
Handle::new(7),
|
||||
"ANSI31".into(),
|
||||
1.0,
|
||||
0.0,
|
||||
false,
|
||||
)
|
||||
.with_appearance(Some(&entity), Color::ByLayer, Transparency::ByLayer);
|
||||
|
||||
assert!(matches!(
|
||||
command.on_text_input("O"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_text_input("D"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(matches!(
|
||||
command.on_enter(),
|
||||
CmdResult::HatcheditApply {
|
||||
operation: HatchEditOperation::Update {
|
||||
origin: Some((7.0, 16.0)),
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["HATCHEDIT"] }); // HatcheditCommand
|
||||
@@ -628,6 +628,14 @@ pub(crate) fn boundary_entities_from_sources(
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub(crate) fn replace_hatch_association(&mut self,handle:Handle,paths:Vec<acadrust::entities::BoundaryPath>) {
|
||||
let Some(EntityType::Hatch(hatch))=self.document.get_entity_mut(handle) else{return;};
|
||||
hatch.paths=paths;
|
||||
hatch.is_associative=true;
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
self.refresh_fill_model(handle);
|
||||
}
|
||||
|
||||
pub(crate) fn edit_hatch_boundary_handles(
|
||||
&mut self,
|
||||
hatch_handle: Handle,
|
||||
|
||||
@@ -252,7 +252,52 @@ fn apply_object_defaults(doc: &CadDocument, entity: &mut EntityType) {
|
||||
}
|
||||
|
||||
pub fn apply_current_creation_styles(doc: &CadDocument, entity: &mut EntityType) {
|
||||
entity.common_mut().transparency = doc.current_entity_transparency();
|
||||
apply_text_defaults(doc, entity);
|
||||
apply_dimension_defaults(doc, entity);
|
||||
apply_object_defaults(doc, entity);
|
||||
}
|
||||
|
||||
pub(crate) fn parse_current_transparency(value: &str) -> Option<acadrust::types::Transparency> {
|
||||
use acadrust::types::Transparency;
|
||||
match value.trim().to_ascii_uppercase().as_str() {
|
||||
"BYLAYER" | "-1" => Some(Transparency::ByLayer),
|
||||
"BYBLOCK" | "-2" => Some(Transparency::ByBlock),
|
||||
value => value.parse::<u8>().ok().filter(|value| *value <= 90)
|
||||
.map(|value| Transparency::from_percent(value as f64 / 100.0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_transparency_label(value: acadrust::types::Transparency) -> String {
|
||||
use acadrust::types::Transparency;
|
||||
match value {
|
||||
Transparency::ByLayer => "ByLayer".into(),
|
||||
Transparency::ByBlock => "ByBlock".into(),
|
||||
Transparency::Explicit(_) => format!("{:.0}", value.as_percent() * 100.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod transparency_tests {
|
||||
use super::*;
|
||||
use acadrust::types::Transparency;
|
||||
|
||||
#[test]
|
||||
fn parses_supported_current_transparency_values() {
|
||||
assert_eq!(parse_current_transparency("ByLayer"), Some(Transparency::ByLayer));
|
||||
assert_eq!(parse_current_transparency("-1"), Some(Transparency::ByLayer));
|
||||
assert_eq!(parse_current_transparency("ByBlock"), Some(Transparency::ByBlock));
|
||||
assert_eq!(parse_current_transparency("-2"), Some(Transparency::ByBlock));
|
||||
assert_eq!(parse_current_transparency("25"), Some(Transparency::Explicit(64)));
|
||||
assert!(parse_current_transparency("91").is_none());
|
||||
assert!(parse_current_transparency("25.5").is_none());
|
||||
assert!(parse_current_transparency("invalid").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_transparency_for_command_and_properties_inputs() {
|
||||
assert_eq!(current_transparency_label(Transparency::ByLayer), "ByLayer");
|
||||
assert_eq!(current_transparency_label(Transparency::ByBlock), "ByBlock");
|
||||
assert_eq!(current_transparency_label(Transparency::Explicit(64)), "25");
|
||||
}
|
||||
}
|
||||
@@ -1674,6 +1674,7 @@ impl Scene {
|
||||
let fill_plane_boundary = Some(Arc::new(fill_plane.1));
|
||||
let fill_plane = Some(fill_plane.0);
|
||||
models.push(HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance,
|
||||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
@@ -2107,6 +2108,7 @@ impl Scene {
|
||||
|
||||
let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation);
|
||||
Some(HatchModel {
|
||||
pattern_origin: Some([dxf.pattern_origin().x, dxf.pattern_origin().y]),
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
@@ -2429,6 +2431,7 @@ impl Scene {
|
||||
})
|
||||
.collect();
|
||||
HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
@@ -2663,6 +2666,9 @@ impl Scene {
|
||||
// carries `world_origin: [0, 0]`, which after the world_offset removal
|
||||
// leaves the fill mis-placed and effectively invisible until a later
|
||||
// edit rebuilds it from the DXF — so keep the seed, don't overwrite it.
|
||||
if let Some(origin) = model.pattern_origin {
|
||||
dxf.record_pattern_origin(Vector2::new(origin[0], origin[1]));
|
||||
}
|
||||
let mut entity = EntityType::Hatch(dxf);
|
||||
|
||||
if let Some(layer) = layer {
|
||||
@@ -2671,6 +2677,8 @@ impl Scene {
|
||||
if let Some((color, transparency)) = entity_style {
|
||||
entity.common_mut().color = color;
|
||||
entity.common_mut().transparency = transparency;
|
||||
} else {
|
||||
entity.common_mut().transparency = self.document.current_entity_transparency();
|
||||
}
|
||||
|
||||
self.add_entity(entity)
|
||||
|
||||
@@ -3856,6 +3856,7 @@ impl Scene {
|
||||
let ((x0, y0), (x1, y1)) = self.paper_limits()?;
|
||||
let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
||||
Some(HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
|
||||
|
||||
@@ -208,6 +208,8 @@ pub struct FillPlane {
|
||||
/// A hatched region defined by a closed polygon boundary.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HatchModel {
|
||||
/// Recorded hatch origin in its fill plane, independent of intrinsic PAT line bases.
|
||||
pub pattern_origin: Option<[f64; 2]>,
|
||||
pub render_instance: Option<super::instance_model::RenderInstance>,
|
||||
/// World XY anchor (in the same offset-relative coordinate space as
|
||||
/// the rest of the scene — `world_offset` already subtracted, but
|
||||
|
||||
@@ -141,7 +141,7 @@ fn boundary_sources(scene: &Scene, plane: WorkingPlane) -> FxHashMap<Handle, Bou
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point: [f64; 2]) -> Option<Vec<Vec<[f64; 2]>>> {
|
||||
pub(crate) fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point: [f64; 2]) -> Option<Vec<Vec<[f64; 2]>>> {
|
||||
let rings = boundary_faces(sources, BOUNDARY_TOLERANCE);
|
||||
let loops = boundary_loops(sources, &rings)?;
|
||||
let tolerance = Tolerance::new(BOUNDARY_TOLERANCE);
|
||||
@@ -179,8 +179,12 @@ fn boundary_region(
|
||||
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>], plane: WorkingPlane,
|
||||
) -> Option<EntityType> {
|
||||
let loops = boundary_loops(sources, rings)?;
|
||||
region_from_loops(&loops, plane)
|
||||
}
|
||||
|
||||
pub(crate) fn region_from_loops(loops: &[Vec<Curve>], plane: WorkingPlane) -> Option<EntityType> {
|
||||
let kernel_plane = Plane::from_axes(plane.origin.to_array(), plane.x.to_array(), plane.y.to_array());
|
||||
let sheet = brep::planar_region(kernel_plane, &loops)?;
|
||||
let sheet = brep::planar_region(kernel_plane, loops)?;
|
||||
let document = crate::scene::convert::acis_export::solid_to_sat(&sheet)?;
|
||||
let mut region = Region::new();
|
||||
region.acis_data = AcisData::from_sat(&document.to_sat_string());
|
||||
|
||||
@@ -154,6 +154,7 @@ impl Scene {
|
||||
return None;
|
||||
}
|
||||
let mut model = HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
world_origin: origin,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
|
||||
@@ -2886,6 +2886,7 @@ impl Scene {
|
||||
}
|
||||
boundary.push(boundary[0]);
|
||||
hatches.push(HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: wire.render_instance.clone(),
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(boundary),
|
||||
|
||||
@@ -131,6 +131,7 @@ impl canvas::Program<Message> for HatchPatternPreview {
|
||||
}
|
||||
HatchPattern::Pattern(_) => {
|
||||
let model = HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(vec![
|
||||
|
||||
@@ -240,6 +240,7 @@ fn app_created_hatch_roundtrips_catalog_spacing() {
|
||||
let mut scene = Scene::new();
|
||||
let boundary: Vec<[f32; 2]> = vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
|
||||
let model = HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(boundary),
|
||||
@@ -298,6 +299,7 @@ fn nested_hatch_serializes_only_outer_as_external() {
|
||||
let boundary_f32: Vec<[f32; 2]> = wcs.iter().map(|&[x, y]| [x as f32, y as f32]).collect();
|
||||
|
||||
let model = HatchModel {
|
||||
pattern_origin: None,
|
||||
render_instance: None,
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(boundary_f32),
|
||||
|
||||
Reference in new issue
Block a user