forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints
# Conflicts: # src/app/command_driver.rs # src/command.rs # src/ui/ribbon/mod.rs # src/ui/window/options.rs
This commit is contained in:
commit
ad718def3f
124 files changed
+9866
-692
No files matched your search
+220
-32
@@ -191,7 +191,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
c.is_ascii_digit()
|
||||
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
||||
});
|
||||
if dyn_field_char && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() {
|
||||
if dyn_field_char
|
||||
&& self.command_line.input.is_empty()
|
||||
&& self.dyn_input
|
||||
&& !self.tabs[i].dyn_fields.is_empty()
|
||||
{
|
||||
let a = self.tabs[i]
|
||||
.dyn_active
|
||||
.min(self.tabs[i].dyn_fields.len() - 1);
|
||||
@@ -199,6 +203,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
.buffer
|
||||
.get_or_insert_with(String::new)
|
||||
.push_str(&s);
|
||||
if self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||
matches!(
|
||||
grip.mode,
|
||||
GripEditMode::Lengthen
|
||||
| GripEditMode::Radius
|
||||
| GripEditMode::ArcLength
|
||||
)
|
||||
}) {
|
||||
self.command_line.input.push_str(&s);
|
||||
}
|
||||
} else {
|
||||
// Command-line entry is shown uppercase — except in
|
||||
// free-form text prompts, where the typed case is the
|
||||
@@ -242,6 +256,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
if buf.is_empty() {
|
||||
self.tabs[i].dyn_fields[a].buffer = None;
|
||||
}
|
||||
if self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||
matches!(
|
||||
grip.mode,
|
||||
GripEditMode::Lengthen
|
||||
| GripEditMode::Radius
|
||||
| GripEditMode::ArcLength
|
||||
)
|
||||
}) {
|
||||
self.command_line.input.pop();
|
||||
}
|
||||
return self.focus_cmd_input();
|
||||
}
|
||||
}
|
||||
@@ -277,13 +301,49 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
// Grip-menu value prompt — consume the typed number and
|
||||
// route it through `apply_grip_menu_value`.
|
||||
// Interactive grip prompts are valid only while their matching
|
||||
// grip edit is alive. Do not let a stale Radius / Lengthen /
|
||||
// Arc Length prompt consume Enter or coordinates from a later
|
||||
// drawing command.
|
||||
let stale_interactive_grip_prompt = self.grip_pending.as_ref().is_some_and(|pending| {
|
||||
let expected_mode = match pending.action {
|
||||
crate::scene::model::object::GripMenuAction::Lengthen => {
|
||||
Some(GripEditMode::Lengthen)
|
||||
}
|
||||
crate::scene::model::object::GripMenuAction::Radius => {
|
||||
Some(GripEditMode::Radius)
|
||||
}
|
||||
crate::scene::model::object::GripMenuAction::ArcLength => {
|
||||
Some(GripEditMode::ArcLength)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
expected_mode.is_some_and(|mode| {
|
||||
!self.tabs[self.active_tab].active_grip.as_ref().is_some_and(|grip| {
|
||||
grip.mode == mode
|
||||
&& grip.handle == pending.handle
|
||||
&& grip.grip_id == pending.grip_id
|
||||
})
|
||||
})
|
||||
});
|
||||
if stale_interactive_grip_prompt {
|
||||
self.grip_pending = None;
|
||||
}
|
||||
if let Some(pending) = self.grip_pending.take() {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, pending.handle) {
|
||||
self.cancel_active_grip_edit();
|
||||
return Task::none();
|
||||
}
|
||||
let raw = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
||||
// Dynamic Input keeps numeric typing in its focused field.
|
||||
// Fall back to the command-line buffer so the established
|
||||
// prompt workflow remains unchanged when DYN is disabled.
|
||||
let dyn_value = self.tabs[i]
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.find_map(|field| field.buffer.as_deref());
|
||||
let entered = dyn_value.unwrap_or(self.command_line.input.trim());
|
||||
let raw = crate::app::expr_eval::eval_to_string(entered);
|
||||
self.command_line.input.clear();
|
||||
let Ok(v) = raw.parse::<f64>() else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
@@ -293,15 +353,27 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.grip_pending = Some(pending);
|
||||
return self.focus_cmd_input();
|
||||
};
|
||||
let interactive_lengthen = self.tabs[i]
|
||||
let interactive_value_grip = self.tabs[i]
|
||||
.active_grip
|
||||
.as_ref()
|
||||
.is_some_and(|grip| {
|
||||
grip.mode == GripEditMode::Lengthen
|
||||
matches!(
|
||||
(grip.mode, pending.action),
|
||||
(
|
||||
GripEditMode::Lengthen,
|
||||
crate::scene::model::object::GripMenuAction::Lengthen,
|
||||
) | (
|
||||
GripEditMode::Radius,
|
||||
crate::scene::model::object::GripMenuAction::Radius,
|
||||
) | (
|
||||
GripEditMode::ArcLength,
|
||||
crate::scene::model::object::GripMenuAction::ArcLength,
|
||||
)
|
||||
)
|
||||
&& grip.handle == pending.handle
|
||||
&& grip.grip_id == pending.grip_id
|
||||
});
|
||||
if interactive_lengthen {
|
||||
if interactive_value_grip {
|
||||
self.cancel_active_grip_edit();
|
||||
}
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
@@ -566,6 +638,30 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
.map(|c| c.is_free_text_step())
|
||||
.unwrap_or(false);
|
||||
let raw = self.command_line.input.trim().to_string();
|
||||
let is_mtp = raw.trim_start_matches('_').eq_ignore_ascii_case("MTP")
|
||||
|| raw.trim_start_matches('_').eq_ignore_ascii_case("M2P");
|
||||
if is_mtp {
|
||||
let is_point_step = !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.input_kind().wants_text())
|
||||
.unwrap_or(true)
|
||||
|| self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.point_step_accepts_keywords())
|
||||
.unwrap_or(false);
|
||||
let not_entity_pick = !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.needs_entity_pick())
|
||||
.unwrap_or(false);
|
||||
if is_point_step && not_entity_pick {
|
||||
self.command_line.input.clear();
|
||||
self.start_mtp_modifier(i);
|
||||
return self.focus_cmd_input();
|
||||
}
|
||||
}
|
||||
let text = if free_text {
|
||||
raw
|
||||
} else {
|
||||
@@ -1306,7 +1402,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
action: item.action,
|
||||
label,
|
||||
});
|
||||
if matches!(item.action, GripMenuAction::Lengthen) {
|
||||
if matches!(
|
||||
item.action,
|
||||
GripMenuAction::Lengthen
|
||||
| GripMenuAction::Radius
|
||||
| GripMenuAction::ArcLength
|
||||
) {
|
||||
if let Some((_, grip)) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
@@ -1315,14 +1416,52 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
**owner == popup.handle && grip.id == popup.grip_id
|
||||
})
|
||||
{
|
||||
self.tabs[i].active_grip = Some(GripEdit::lengthen(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
grip.world,
|
||||
));
|
||||
if self.grip_originals.is_empty() {
|
||||
self.grip_originals = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(popup.handle)
|
||||
.cloned()
|
||||
.map(|entity| vec![(popup.handle, entity)])
|
||||
.unwrap_or_default();
|
||||
}
|
||||
self.tabs[i].active_grip = Some(match item.action {
|
||||
GripMenuAction::Radius => GripEdit::radius(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
grip.world,
|
||||
),
|
||||
GripMenuAction::ArcLength => GripEdit::arc_length(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
grip.world,
|
||||
),
|
||||
_ => GripEdit::lengthen(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
grip.world,
|
||||
),
|
||||
});
|
||||
}
|
||||
// Popup actions do not pass through the normal grip
|
||||
// press handler, which is where dynamic fields are
|
||||
// usually seeded. Build the value field
|
||||
// immediately so it is visible before the next mouse
|
||||
// move (and so keyboard input has a field to target).
|
||||
self.sync_dyn_fields();
|
||||
if matches!(item.action, GripMenuAction::Radius) {
|
||||
self.command_line.push_info(
|
||||
crate::t!("Specify point or enter radius:").as_ref(),
|
||||
);
|
||||
} else if matches!(item.action, GripMenuAction::ArcLength) {
|
||||
self.command_line.push_info(
|
||||
crate::t!("Specify point or enter arc length:").as_ref(),
|
||||
);
|
||||
} else {
|
||||
self.command_line.push_info(
|
||||
crate::t!("Specify point or enter distance:").as_ref(),
|
||||
);
|
||||
}
|
||||
self.command_line
|
||||
.push_info(crate::t!("Specify point or enter distance:").as_ref());
|
||||
} else {
|
||||
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
||||
}
|
||||
@@ -1443,24 +1582,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
// One-shot action — apply immediately.
|
||||
let unchanged = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(popup.handle)
|
||||
.is_some_and(|entity| match (item.action, entity) {
|
||||
(GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => {
|
||||
!spline.cv_frame_visible
|
||||
&& crate::entities::spline::uses_fit_method(spline)
|
||||
}
|
||||
(
|
||||
GripMenuAction::ShowControlVertices,
|
||||
acadrust::EntityType::Spline(spline),
|
||||
) => {
|
||||
spline.cv_frame_visible
|
||||
|| !crate::entities::spline::uses_fit_method(spline)
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
let unchanged = item.label.starts_with('✓')
|
||||
|| self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(popup.handle)
|
||||
.is_some_and(|entity| match (item.action, entity) {
|
||||
(GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => {
|
||||
!spline.cv_frame_visible
|
||||
&& crate::entities::spline::uses_fit_method(spline)
|
||||
}
|
||||
(
|
||||
GripMenuAction::ShowControlVertices,
|
||||
acadrust::EntityType::Spline(spline),
|
||||
) => {
|
||||
spline.cv_frame_visible
|
||||
|| !crate::entities::spline::uses_fit_method(spline)
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
if unchanged {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -2002,7 +2142,51 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
|
||||
if crate::scene::model::solid_history::is_loft_geometry_choice(field) {
|
||||
if crate::scene::model::solid_history::is_surface_property_choice(field) {
|
||||
use crate::scene::model::solid_history::{
|
||||
PROP_SURFACE_MAINTAIN_ASSOCIATIVITY, PROP_SURFACE_SHOW_ASSOCIATIVITY,
|
||||
PROP_SURFACE_WIREFRAME_TYPE,
|
||||
};
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
}
|
||||
let Some(mut state) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| match entity {
|
||||
acadrust::EntityType::Surface(surface) => Some(
|
||||
crate::entities::solid3d::surface_property_state(surface),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match field {
|
||||
PROP_SURFACE_WIREFRAME_TYPE => {
|
||||
state.isolines = !value.eq_ignore_ascii_case("Isoparms")
|
||||
}
|
||||
PROP_SURFACE_MAINTAIN_ASSOCIATIVITY => {
|
||||
state.maintain_associativity = value.eq_ignore_ascii_case("Yes")
|
||||
}
|
||||
PROP_SURFACE_SHOW_ASSOCIATIVITY => {
|
||||
state.show_associativity = value.eq_ignore_ascii_case("Yes")
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
crate::entities::solid3d::SURFACE_PROPERTIES_APP,
|
||||
Some(crate::entities::solid3d::surface_property_xdata_values(state)),
|
||||
);
|
||||
if field == PROP_SURFACE_WIREFRAME_TYPE {
|
||||
self.tabs[i].scene.reseed_derived_caches(handle);
|
||||
}
|
||||
}
|
||||
} else if crate::scene::model::solid_history::is_loft_geometry_choice(field) {
|
||||
// These choices change the generated body, not just history
|
||||
// flags. Use the same transactional rebuild as numeric edits.
|
||||
for &handle in &handles {
|
||||
@@ -2980,6 +3164,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(field, "srf_u_isolines" | "srf_v_isolines")
|
||||
{
|
||||
self.tabs[i].scene.reseed_derived_caches(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-14
@@ -65,34 +65,55 @@ impl OpenCADStudio {
|
||||
|
||||
// A normal 2D grip stretch behaves like a point-placement step:
|
||||
// distance and angle are measured from the grip's original position.
|
||||
// Lengthen uses the same overlay machinery, but it only needs the
|
||||
// distance field: the entity itself determines the endpoint direction
|
||||
// (or arc sweep), so an angle entry would be misleading.
|
||||
//
|
||||
// Grip editing is not an `active_cmd`, so handle it before the normal
|
||||
// command-only path below.
|
||||
let grip_origin = self.tabs[i]
|
||||
let grip_input = self.tabs[i]
|
||||
.active_grip
|
||||
.as_ref()
|
||||
.filter(|grip| {
|
||||
grip.mode == crate::scene::pick::grip::GripEditMode::Stretch
|
||||
})
|
||||
.map(|grip| grip.origin_world);
|
||||
.map(|grip| match grip.mode {
|
||||
crate::scene::pick::grip::GripEditMode::Stretch => {
|
||||
(grip.origin_world, None)
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::Lengthen => {
|
||||
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::Radius => {
|
||||
(grip.origin_world, Some(crate::command::DynRole::Radius))
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
||||
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(origin) = grip_origin {
|
||||
let wanted = [DynComponent::Distance, DynComponent::Angle];
|
||||
let current: Vec<DynComponent> = self.tabs[i]
|
||||
if let Some((origin, scalar_role)) = grip_input {
|
||||
let wanted_roles: Vec<crate::command::DynRole> = scalar_role.map_or_else(
|
||||
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|
||||
|role| vec![role],
|
||||
);
|
||||
let current: Vec<crate::command::DynRole> = self.tabs[i]
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.map(|field| field.component)
|
||||
.map(|field| field.role)
|
||||
.collect();
|
||||
|
||||
if current.as_slice() != wanted {
|
||||
self.tabs[i].dyn_fields = wanted
|
||||
.into_iter()
|
||||
.map(DynFieldEntry::new)
|
||||
if current != wanted_roles {
|
||||
self.tabs[i].dyn_fields = wanted_roles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DynFieldEntry::from_role)
|
||||
.collect();
|
||||
self.tabs[i].dyn_active = 0;
|
||||
}
|
||||
|
||||
self.tabs[i].dyn_guide = crate::command::DynGuide::Polar;
|
||||
self.tabs[i].dyn_guide = if scalar_role.is_some() {
|
||||
crate::command::DynGuide::Radius
|
||||
} else {
|
||||
crate::command::DynGuide::Polar
|
||||
};
|
||||
self.tabs[i].dyn_anchor = Some(origin);
|
||||
self.tabs[i].dyn_ref = None;
|
||||
return;
|
||||
|
||||
+49
-3
@@ -355,6 +355,37 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a header variable on the active drawing and mark it modified.
|
||||
pub(in crate::app) fn set_drawing_var(
|
||||
&mut self,
|
||||
write: impl FnOnce(&mut acadrust::document::HeaderVariables),
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
if let Some(tab) = self.tabs.get_mut(i) {
|
||||
write(&mut tab.scene.document.header);
|
||||
tab.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-tessellate active-drawing solids after an ISOLINES change.
|
||||
pub(in crate::app) fn regenerate_meshes(&mut self) {
|
||||
let i = self.active_tab;
|
||||
if let Some(tab) = self.tabs.get_mut(i) {
|
||||
tab.scene.populate_meshes_from_document();
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild text geometry in every open drawing.
|
||||
///
|
||||
/// `TEXTFILL` is a process-global, so a change to it invalidates the text
|
||||
/// in every tab. Doing only the active one leaves the others rendering the
|
||||
/// previous setting until some unrelated edit happens to rebuild them.
|
||||
pub(in crate::app) fn invalidate_text_everywhere(&mut self) {
|
||||
for tab in &mut self.tabs {
|
||||
tab.scene.invalidate_text_geometry_dependencies();
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the persisted UI preferences from live state.
|
||||
pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings {
|
||||
crate::app::settings::UserSettings {
|
||||
@@ -365,6 +396,11 @@ impl OpenCADStudio {
|
||||
zoom_factor: self.zoom_factor,
|
||||
cursor_size: self.cursor_size,
|
||||
pick_box: self.pick_box,
|
||||
options_tab: self.options_tab,
|
||||
show_viewcube: self.show_viewcube,
|
||||
show_ucs_icon: self.show_ucs_icon,
|
||||
ucs_icon_at_origin: self.ucs_icon_at_origin,
|
||||
selection_cycling: self.selection_cycling,
|
||||
double_click_block_refedit: self.double_click_block_refedit,
|
||||
double_click_block_attedit: self.double_click_block_attedit,
|
||||
grip_object_limit: self.grip_object_limit,
|
||||
@@ -393,6 +429,7 @@ impl OpenCADStudio {
|
||||
texteditmode: self.texteditmode,
|
||||
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
|
||||
dimension_continue_mode: self.dimension_continue_mode,
|
||||
delete_objects: self.delete_objects,
|
||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
@@ -403,8 +440,6 @@ impl OpenCADStudio {
|
||||
pick_add: self.pick_add,
|
||||
pick_drag_rect: self.pick_drag_rect,
|
||||
quick_properties: self.quick_properties,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
language: self.language,
|
||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||
commandline_fade_ms: crate::app::settings::clamp_commandline_fade_ms(
|
||||
@@ -424,6 +459,14 @@ impl OpenCADStudio {
|
||||
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
||||
self.cursor_size = s.cursor_size.clamp(1, 100);
|
||||
self.pick_box = s.pick_box.clamp(0, 50);
|
||||
self.options_tab = s.options_tab;
|
||||
// These four drive real features with commands and status-bar pills,
|
||||
// but they lived only on the app struct: turning the ViewCube off and
|
||||
// restarting brought it straight back.
|
||||
self.show_viewcube = s.show_viewcube;
|
||||
self.show_ucs_icon = s.show_ucs_icon;
|
||||
self.ucs_icon_at_origin = s.ucs_icon_at_origin;
|
||||
self.selection_cycling = s.selection_cycling;
|
||||
self.double_click_block_refedit = s.double_click_block_refedit;
|
||||
self.double_click_block_attedit = s.double_click_block_attedit;
|
||||
self.grip_object_limit = s.grip_object_limit.clamp(0, 32767);
|
||||
@@ -442,6 +485,9 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// The Options field edits a buffer rather than the value, so it has to
|
||||
// be reseeded whenever the value is restored from behind it.
|
||||
self.snap_angle_input = crate::app::settings::format_snap_angle(self.snap_angle_deg);
|
||||
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
||||
// open / tab switch), not app-global, so they are not applied here.
|
||||
self.snapper.otrack_enabled = s.otrack;
|
||||
@@ -464,6 +510,7 @@ impl OpenCADStudio {
|
||||
self.texteditmode = s.texteditmode;
|
||||
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
|
||||
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
|
||||
self.delete_objects = s.delete_objects.clamp(0, 3);
|
||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
@@ -475,7 +522,6 @@ impl OpenCADStudio {
|
||||
self.pick_add = s.pick_add;
|
||||
self.pick_drag_rect = s.pick_drag_rect;
|
||||
self.quick_properties = s.quick_properties;
|
||||
// Legacy settings.bg_color / paper_bg_color are superseded by model_space config.
|
||||
if crate::i18n::set_language(s.language).is_ok() {
|
||||
self.language = s.language;
|
||||
}
|
||||
|
||||
@@ -1583,6 +1583,24 @@ impl OpenCADStudio {
|
||||
}
|
||||
let live_input = s.clone();
|
||||
self.command_line.input = live_input.clone();
|
||||
let i = self.active_tab;
|
||||
if self.dyn_input
|
||||
&& self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||
matches!(
|
||||
grip.mode,
|
||||
crate::scene::pick::grip::GripEditMode::Lengthen
|
||||
| crate::scene::pick::grip::GripEditMode::Radius
|
||||
| crate::scene::pick::grip::GripEditMode::ArcLength
|
||||
)
|
||||
})
|
||||
&& !self.tabs[i].dyn_fields.is_empty()
|
||||
{
|
||||
let a = self.tabs[i]
|
||||
.dyn_active
|
||||
.min(self.tabs[i].dyn_fields.len() - 1);
|
||||
self.tabs[i].dyn_fields[a].buffer =
|
||||
(!live_input.is_empty()).then_some(live_input.clone());
|
||||
}
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT: update picker on each keystroke
|
||||
@@ -6269,6 +6287,188 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SaveTimeChanged(minutes) => {
|
||||
self.savetime_min = minutes.max(0);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::BackupOnSaveChanged(enabled) => {
|
||||
self.backup_on_save = enabled;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::TextFillChanged(filled) => {
|
||||
crate::scene::text::sdf_atlas::set_textfill(filled);
|
||||
self.invalidate_text_everywhere();
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ClipromptLinesChanged(lines) => {
|
||||
let lines = crate::app::settings::clamp_clipromptlines(lines);
|
||||
self.cliprompt_lines = lines;
|
||||
self.command_line.set_cliprompt_lines(lines.clamp(0, 50) as u8);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::CommandLineFadeChanged(ms) => {
|
||||
let ms = crate::app::settings::clamp_commandline_fade_ms(ms);
|
||||
self.commandline_fade_ms = ms;
|
||||
self.command_line.set_commandline_fade_ms(ms.max(0) as u32);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ZoomWheelReversedChanged(reversed) => {
|
||||
self.zoom_wheel_reversed = reversed;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ZoomFactorChanged(factor) => {
|
||||
self.zoom_factor = factor.clamp(3, 100);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::TextEditModeChanged(single) => {
|
||||
self.texteditmode = single;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::DimContinueModeChanged(inherit) => {
|
||||
self.dimension_continue_mode = i16::from(inherit);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QdimSnapPriorityChanged(priority) => {
|
||||
self.quick_dimension_snap_priority = priority.min(1);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// The sign carries on/off and the magnitude the mode, so switching
|
||||
// off and back on has to return to the mode that was chosen — the
|
||||
// same convention the status-bar pill uses when it negates.
|
||||
Message::AnnoAutoScaleChanged(mode) => {
|
||||
self.annotation_auto_scale = if mode == 0 {
|
||||
-self.annotation_auto_scale.abs().max(1)
|
||||
} else {
|
||||
mode.clamp(1, 4)
|
||||
};
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// Typed into freely; only committed when it parses, so clearing
|
||||
// the field to retype does not snap the crosshair back to zero.
|
||||
Message::SnapAngleInputChanged(value) => {
|
||||
self.snap_angle_input = value;
|
||||
if let Ok(angle) = self.snap_angle_input.trim().parse::<f32>() {
|
||||
if angle.is_finite() {
|
||||
self.snap_angle_deg = angle.rem_euclid(360.0);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PolarIncrementChanged(deg) => {
|
||||
if deg.is_finite() && deg > 0.0 {
|
||||
self.polar_increment_deg = deg;
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ShowViewCubeChanged(show) => {
|
||||
self.show_viewcube = show;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ShowUcsIconChanged(show) => {
|
||||
self.show_ucs_icon = show;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::UcsIconAtOriginChanged(at_origin) => {
|
||||
self.ucs_icon_at_origin = at_origin;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ISOLINES is baked into solid meshes, so rebuild once on release.
|
||||
Message::IsolinesChanged(value) => {
|
||||
let value = value.max(0);
|
||||
let changed = self
|
||||
.tabs
|
||||
.get(self.active_tab)
|
||||
.is_some_and(|tab| tab.scene.document.header.isolines != value);
|
||||
if changed {
|
||||
self.set_drawing_var(|header| header.isolines = value);
|
||||
self.isolines_awaiting_regen = true;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::IsolinesReleased => {
|
||||
if std::mem::take(&mut self.isolines_awaiting_regen) {
|
||||
self.regenerate_meshes();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::DispSilhChanged(on) => {
|
||||
self.set_drawing_var(|header| header.display_silhouette = on);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SurfaceUChanged(value) => {
|
||||
self.set_drawing_var(|header| header.surface_u_density = value.clamp(0, 200));
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SurfaceVChanged(value) => {
|
||||
self.set_drawing_var(|header| header.surface_v_density = value.clamp(0, 200));
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SurfaceTypeChanged(value) => {
|
||||
self.set_drawing_var(|header| header.surface_type = value);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SolidHistChanged(record) => {
|
||||
self.set_drawing_var(|header| header.record_solid_history = record);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ShowHistChanged(mode) => {
|
||||
self.set_drawing_var(|header| {
|
||||
header.show_solid_history = mode.clamp(0, 2)
|
||||
});
|
||||
let i = self.active_tab;
|
||||
if let Some(tab) = self.tabs.get_mut(i) {
|
||||
tab.scene.bump_geometry();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionCyclingChanged(on) => {
|
||||
self.selection_cycling = on;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::OpenFolder(path) => crate::sys::open_url(&path, None),
|
||||
|
||||
Message::PickDragRectToggled(rectangle) => {
|
||||
self.pick_drag_rect = rectangle;
|
||||
self.persist_settings_if_changed();
|
||||
|
||||
+67
-15
@@ -1535,15 +1535,30 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
if let Some(axis) = grip.axis {
|
||||
snapped = cursor_on_projected_axis(
|
||||
p,
|
||||
bounds,
|
||||
view_rot,
|
||||
eye,
|
||||
grip.origin_world,
|
||||
axis,
|
||||
)
|
||||
.unwrap_or(snapped);
|
||||
let acquired_point = otrack_hit.is_some()
|
||||
|| snap_hit.is_some_and(|hit| {
|
||||
hit.snap_type != crate::snap::SnapType::Grid
|
||||
});
|
||||
if acquired_point {
|
||||
if let Some(axis) = axis.try_normalize() {
|
||||
// An axis grip still moves on its construction axis, but
|
||||
// an acquired object point supplies the exact coordinate
|
||||
// along that axis. This lets height and draft arrows seat
|
||||
// back onto existing surface/solid geometry.
|
||||
snapped = grip.origin_world
|
||||
+ axis * (snapped - grip.origin_world).dot(axis);
|
||||
}
|
||||
} else {
|
||||
snapped = cursor_on_projected_axis(
|
||||
p,
|
||||
bounds,
|
||||
view_rot,
|
||||
eye,
|
||||
grip.origin_world,
|
||||
axis,
|
||||
)
|
||||
.unwrap_or(snapped);
|
||||
}
|
||||
}
|
||||
|
||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
@@ -1565,8 +1580,19 @@ impl OpenCADStudio {
|
||||
|
||||
let apply_started = Instant::now();
|
||||
let delta = snapped - grip.last_world;
|
||||
let lengthen = grip.mode == GripEditMode::Lengthen;
|
||||
let actions: Vec<_> = if lengthen {
|
||||
let menu_action = match grip.mode {
|
||||
GripEditMode::Lengthen => {
|
||||
Some(crate::scene::model::object::GripMenuAction::Lengthen)
|
||||
}
|
||||
GripEditMode::Radius => {
|
||||
Some(crate::scene::model::object::GripMenuAction::Radius)
|
||||
}
|
||||
GripEditMode::ArcLength => {
|
||||
Some(crate::scene::model::object::GripMenuAction::ArcLength)
|
||||
}
|
||||
GripEditMode::Stretch => None,
|
||||
};
|
||||
let actions: Vec<_> = if menu_action.is_some() {
|
||||
Vec::new()
|
||||
} else {
|
||||
grip.targets
|
||||
@@ -1581,14 +1607,13 @@ impl OpenCADStudio {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if lengthen {
|
||||
if let Some(action) = menu_action {
|
||||
let original = self
|
||||
.grip_originals
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == grip.handle)
|
||||
.map(|(_, entity)| entity.clone());
|
||||
if let Some(original) = original {
|
||||
let action = crate::scene::model::object::GripMenuAction::Lengthen;
|
||||
let value = crate::scene::view::dispatch::grip_menu_point_value(
|
||||
&original,
|
||||
grip.grip_id,
|
||||
@@ -1782,6 +1807,26 @@ impl OpenCADStudio {
|
||||
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||
self.refresh_selected_grips();
|
||||
// A history rebuild can clamp a requested value or move a derived
|
||||
// construction handle non-linearly. Rebase each active target onto
|
||||
// the freshly rebuilt grip so the hot arrow remains attached to the
|
||||
// live surface and reverses immediately from a geometric limit.
|
||||
let refreshed_targets: Vec<_> = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.map(|(handle, grip)| ((handle, grip.id), grip.world))
|
||||
.collect();
|
||||
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||
for target in &mut active.targets {
|
||||
if let Some((_, world)) = refreshed_targets.iter().find(|((handle, id), _)| {
|
||||
*handle == target.handle && *id == target.grip_id
|
||||
}) {
|
||||
target.last_world = *world;
|
||||
}
|
||||
}
|
||||
}
|
||||
let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms;
|
||||
// Properties are refreshed when the grip is committed or cancelled.
|
||||
// Rebuilding the inspector on every pointer event adds no drawing
|
||||
@@ -3080,7 +3125,11 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
};
|
||||
let grip_shape = self.tabs[i].selected_grips[grip_index].shape;
|
||||
if grip_shape == crate::scene::model::object::GripShape::Dropdown {
|
||||
if matches!(
|
||||
grip_shape,
|
||||
crate::scene::model::object::GripShape::Dropdown
|
||||
| crate::scene::model::object::GripShape::DropdownAdjacent
|
||||
) {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let items = self.tabs[i]
|
||||
.scene
|
||||
@@ -3224,7 +3273,10 @@ impl OpenCADStudio {
|
||||
// Engaging click — stay hot, wait for the placement click.
|
||||
return Task::none();
|
||||
}
|
||||
if grip.mode == GripEditMode::Lengthen {
|
||||
if matches!(
|
||||
grip.mode,
|
||||
GripEditMode::Lengthen | GripEditMode::Radius | GripEditMode::ArcLength
|
||||
) {
|
||||
self.grip_pending = None;
|
||||
self.command_line.input.clear();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user