forked from 1b-agent/opencadstudio-upstream
Merge main into Select-Color-Redesign
Keep both sides: main's ViewBackground colour target and PREVIEW icon next to this branch's Interface target and LIST/SEARCH icons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
231 files changed
+32217
-3423
No files matched your search
@@ -66,6 +66,46 @@ impl OpenCADStudio {
|
||||
None
|
||||
}
|
||||
|
||||
/// ARRAYRECT / ARRAYPOLAR with Associative on: the objects become the
|
||||
/// source of an associative array, in one undo step.
|
||||
pub(super) fn handle_assoc_array(
|
||||
&mut self,
|
||||
mut handles: Vec<Handle>,
|
||||
spec: crate::scene::assoc_array::AssocArraySpec,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "ARRAY");
|
||||
let source_point = crate::app::helpers::entities_lower_left_by_bbox(
|
||||
&self.tabs[i].scene.document,
|
||||
&handles,
|
||||
);
|
||||
self.push_undo_snapshot(i, label.clone());
|
||||
match crate::scene::assoc_array::create_from_spec(
|
||||
&mut self.tabs[i].scene,
|
||||
&handles,
|
||||
source_point,
|
||||
&spec,
|
||||
) {
|
||||
Ok(array) => {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(array, false);
|
||||
self.command_line
|
||||
.push_output(crate::tf!("{label}: associative array created.").as_ref());
|
||||
}
|
||||
Err(error) => self.command_line.push_error(&format!("{label}: {error}")),
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
pub(super) fn handle_replace_many(
|
||||
&mut self,
|
||||
replacements: Vec<(Handle, Vec<codec::EntityType>)>,
|
||||
|
||||
@@ -990,6 +990,35 @@ impl OpenCADStudio {
|
||||
|
||||
pub(super) fn handle_attreq_needed(&mut self, block_name: String) -> Option<Task<Message>> {
|
||||
let i = self.active_tab;
|
||||
// When active space is a block editor session (BEDIT), refuse inserting
|
||||
// any block that would create a circular nesting dependency.
|
||||
if let Some(edit_handle) = self.tabs[i].scene.block_edit_block {
|
||||
let container_name = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.handle == edit_handle)
|
||||
.map(|br| br.name.clone());
|
||||
if let Some(container) = container_name {
|
||||
if crate::scene::block_service::would_create_cycle(
|
||||
&self.tabs[i].scene.document,
|
||||
&container,
|
||||
&block_name,
|
||||
) {
|
||||
self.command_line.push_error(
|
||||
&crate::scene::block_service::circular_dependency_error(
|
||||
&container,
|
||||
&block_name,
|
||||
),
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Collect the full AttributeDefinitions owned by this block
|
||||
// record so each created attribute keeps its geometry (#255).
|
||||
let mut attdefs: Vec<codec::entities::AttributeDefinition> = {
|
||||
|
||||
@@ -80,7 +80,7 @@ use crate::command::{CmdResult, SelectionEntity, StepInput};
|
||||
use codec::Handle;
|
||||
use iced::Task;
|
||||
|
||||
mod clipboard;
|
||||
pub(super) mod clipboard;
|
||||
mod constraint;
|
||||
mod dim_edit;
|
||||
mod entity_commit;
|
||||
@@ -95,7 +95,8 @@ mod utilities;
|
||||
mod viewport;
|
||||
|
||||
pub(crate) use clipboard::{
|
||||
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace,
|
||||
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, remap_object,
|
||||
PasteSpace,
|
||||
};
|
||||
|
||||
impl OpenCADStudio {
|
||||
@@ -169,6 +170,7 @@ impl OpenCADStudio {
|
||||
&result,
|
||||
CmdResult::Relaunch(..)
|
||||
| CmdResult::Dispatch(..)
|
||||
| CmdResult::QSelectSample { .. }
|
||||
| CmdResult::SolidEdgeBlend { .. }
|
||||
| CmdResult::SolidSubtract { .. }
|
||||
| CmdResult::SliceEntities { .. }
|
||||
@@ -436,6 +438,13 @@ impl OpenCADStudio {
|
||||
// runtime or messages such as a chosen render mode are dropped.
|
||||
let mut dispatched = Task::none();
|
||||
match result {
|
||||
CmdResult::QSelectSample {
|
||||
state,
|
||||
tab_id,
|
||||
handle,
|
||||
} => {
|
||||
self.finish_qselect_sample(*state, tab_id, handle);
|
||||
}
|
||||
CmdResult::OpenAutoConstrainSettings => {
|
||||
self.handle_open_auto_constrain_settings();
|
||||
}
|
||||
@@ -598,6 +607,9 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
CmdResult::AssocArray { handles, spec } => {
|
||||
self.handle_assoc_array(handles, spec);
|
||||
}
|
||||
CmdResult::ReplaceMany(replacements, additions) => {
|
||||
if let Some(task) = self.handle_replace_many(replacements, additions) {
|
||||
return task;
|
||||
@@ -874,6 +886,21 @@ impl OpenCADStudio {
|
||||
CmdResult::ZoomToWindow { p1, p2 } => {
|
||||
self.handle_zoom_to_window(p1, p2);
|
||||
}
|
||||
CmdResult::Show { p1, p2 } => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
self.tabs[i].scene.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
||||
}
|
||||
CmdResult::CommitAndZoom { entity, p1, p2 } => {
|
||||
self.handle_commit_entities_and_exit(vec![entity], false, false);
|
||||
self.handle_zoom_to_window(p1, p2);
|
||||
}
|
||||
CmdResult::ScaleViewports { handles, factor } => {
|
||||
self.handle_scale_viewports(handles, factor);
|
||||
}
|
||||
CmdResult::TwistView(twist) => {
|
||||
self.handle_twist_view(twist);
|
||||
}
|
||||
CmdResult::Measurement(msg) => {
|
||||
self.handle_measurement(msg);
|
||||
}
|
||||
@@ -1284,7 +1311,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output(&msg);
|
||||
if !msg.is_empty() {
|
||||
self.command_line.push_output(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_report_measurement(&mut self, msg: String) {
|
||||
|
||||
@@ -1284,12 +1284,9 @@ mod bedit_characterization_tests {
|
||||
assert!(app.tabs[tab].block_edits.is_empty());
|
||||
}
|
||||
|
||||
/// Documenting: the REFEDIT guard lives only in the `BEDIT` picker arm.
|
||||
/// `BEDIT_BEGIN:<handle>` (used by the palette "Edit" action) has no
|
||||
/// REFEDIT check and opens a session while REFEDIT is active. Expected to
|
||||
/// flip in Phase 1 once the guard moves into the begin path.
|
||||
/// Every entry point must refuse while REFEDIT is active.
|
||||
#[test]
|
||||
fn documenting_bedit_begin_bypasses_refedit_guard() {
|
||||
fn locked_bedit_begin_refuses_active_refedit() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
@@ -1297,8 +1294,8 @@ mod bedit_characterization_tests {
|
||||
assert!(app.tabs[tab].refedit_session.is_some());
|
||||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value()));
|
||||
assert!(
|
||||
app.tabs[tab].active_block_edit.is_some(),
|
||||
"documenting: BEDIT_BEGIN opens a session despite an active REFEDIT"
|
||||
app.tabs[tab].active_block_edit.is_none(),
|
||||
"begin must refuse while REFEDIT is active"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1323,12 +1320,9 @@ mod bedit_characterization_tests {
|
||||
|
||||
// ---- documenting: today's behavior, expected to flip in Phase 1 ----
|
||||
|
||||
/// Documenting: modify an existing member, discard, then undo once. The
|
||||
/// undo target is the BEDIT_BEGIN entry. Records whatever the stack does
|
||||
/// with the stale before-image (ISS-02). The assertion pins the current
|
||||
/// result; Phase 1 must justify any change.
|
||||
/// Discard removes local before-images before drawing undo resumes.
|
||||
#[test]
|
||||
fn documenting_modify_discard_then_undo_probes_stale_before_image() {
|
||||
fn locked_modify_discard_then_undo_does_not_restore_local_edits() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, members) = setup_block_and_insert(&mut app);
|
||||
let line = members[0];
|
||||
@@ -1367,10 +1361,9 @@ mod bedit_characterization_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: undo during a session crosses the entry boundary. A
|
||||
/// single undo after an in-session edit may revert the BEDIT_BEGIN step.
|
||||
/// Local undo restores both the entity store and block membership.
|
||||
#[test]
|
||||
fn documenting_undo_inside_session_crosses_entry_boundary() {
|
||||
fn locked_session_undo_restores_membership_without_crossing_entry() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
begin_bedit(&mut app, insert);
|
||||
@@ -1381,38 +1374,33 @@ mod bedit_characterization_tests {
|
||||
assert_eq!(block_members(&app, "BLK").len(), 2);
|
||||
|
||||
app.undo_steps(1);
|
||||
// Documenting: undo removes the circle from the entity store but leaves
|
||||
// its handle in the block record, so the definition holds a dangling
|
||||
// member. Expected to flip in Phase 1 (members should match the store).
|
||||
let members = block_members(&app, "BLK");
|
||||
assert_eq!(
|
||||
members.len(),
|
||||
2,
|
||||
"documenting: block record keeps the undone handle"
|
||||
1,
|
||||
"block membership must match the undone entity store"
|
||||
);
|
||||
let dangling = members
|
||||
.iter()
|
||||
.filter(|h| app.tabs[tab].scene.document.get_entity(**h).is_none())
|
||||
.count();
|
||||
assert_eq!(dangling, 1, "documenting: one member handle has no entity");
|
||||
assert_eq!(dangling, 0, "every member must have an entity");
|
||||
assert!(
|
||||
app.tabs[tab].active_block_edit.is_some(),
|
||||
"documenting: undo does not close the session"
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: undoing the BEDIT_BEGIN step itself, while the session is
|
||||
/// still open, leaves the editor open. Records whether the undo reaches
|
||||
/// across the entry boundary.
|
||||
/// Opening starts an empty local history; undo cannot cross entry.
|
||||
#[test]
|
||||
fn documenting_undo_across_entry_leaves_session_open() {
|
||||
fn locked_session_entry_has_no_undo_step() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
begin_bedit(&mut app, insert);
|
||||
app.finish_pending_history(tab);
|
||||
let depth_after_begin = undo_depth(&app);
|
||||
assert!(depth_after_begin >= 1, "BEDIT_BEGIN recorded an undo step");
|
||||
assert_eq!(depth_after_begin, 0, "opening must not record an undo step");
|
||||
|
||||
app.undo_steps(1);
|
||||
assert!(
|
||||
@@ -1421,10 +1409,9 @@ mod bedit_characterization_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: BEDIT on a block while another block is open nests a
|
||||
/// second session. Records the resulting stack.
|
||||
/// Switching a clean block replaces the single session.
|
||||
#[test]
|
||||
fn documenting_nested_bedit_opens_second_session() {
|
||||
fn locked_clean_switch_replaces_the_session() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
let tab = app.active_tab;
|
||||
@@ -1448,15 +1435,14 @@ mod bedit_characterization_tests {
|
||||
begin_bedit(&mut app, other_insert);
|
||||
assert_eq!(
|
||||
app.tabs[tab].block_edits.len(),
|
||||
2,
|
||||
"documenting: a second BEDIT pushes a nested session"
|
||||
1,
|
||||
"a drawing has only one block-editor session"
|
||||
);
|
||||
}
|
||||
|
||||
/// Documenting: BEDIT_BEGIN pushes an undo entry, and BEDIT_SAVE and
|
||||
/// BEDIT_DISCARD push none. Phase 1 may change this.
|
||||
/// No-change Save and Discard leave the drawing history untouched.
|
||||
#[test]
|
||||
fn documenting_save_and_discard_push_no_undo_entry() {
|
||||
fn locked_clean_save_and_discard_leave_history_unchanged() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let (insert, _) = setup_block_and_insert(&mut app);
|
||||
begin_bedit(&mut app, insert);
|
||||
|
||||
@@ -24,6 +24,9 @@ impl OpenCADStudio {
|
||||
// component so each member keeps its own dimensions. Other
|
||||
// transforms retain their selected-object behavior.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
let handles = self.tabs[i]
|
||||
.scene
|
||||
.transform_picked_segments(&handles, &transform);
|
||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||
self.apply_inferred_constraints(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
|
||||
@@ -98,6 +98,7 @@ impl OpenCADStudio {
|
||||
self.otrack_cross = None;
|
||||
self.otrack_kind = None;
|
||||
self.axis_lock_dir = None;
|
||||
self.angle_override = None;
|
||||
self.dyn_user_reshaped = false;
|
||||
self.dyn_coord_absolute = false;
|
||||
self.grip_hover = None;
|
||||
@@ -506,6 +507,32 @@ impl OpenCADStudio {
|
||||
StepInput::SelectionComplete(kept)
|
||||
}
|
||||
|
||||
/// Keeps only the kinds of object the active command takes, dropping
|
||||
/// the rest of a completed selection silently.
|
||||
fn filter_command_entity_selection(&mut self, input: StepInput) -> StepInput {
|
||||
let StepInput::SelectionComplete(handles) = input else {
|
||||
return input;
|
||||
};
|
||||
let i = self.active_tab;
|
||||
let Some(keep) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.selection_entity_filter())
|
||||
else {
|
||||
return StepInput::SelectionComplete(handles);
|
||||
};
|
||||
let (kept, dropped): (Vec<Handle>, Vec<Handle>) = handles
|
||||
.into_iter()
|
||||
.partition(|h| self.tabs[i].scene.document.get_entity(*h).is_some_and(keep));
|
||||
for handle in &dropped {
|
||||
self.tabs[i].scene.deselect_entity(*handle);
|
||||
}
|
||||
if !dropped.is_empty() {
|
||||
self.refresh_properties();
|
||||
}
|
||||
StepInput::SelectionComplete(kept)
|
||||
}
|
||||
|
||||
/// Drops locked point clouds from a completed selection when the active
|
||||
/// command would move them, saying which were locked and how many went.
|
||||
fn filter_locked_point_cloud_selection(&mut self, input: StepInput) -> StepInput {
|
||||
@@ -561,6 +588,7 @@ impl OpenCADStudio {
|
||||
let input = self.filter_associative_dimension_selection(input);
|
||||
let input = self.filter_block_reference_selection(input);
|
||||
let input = self.filter_locked_point_cloud_selection(input);
|
||||
let input = self.filter_command_entity_selection(input);
|
||||
// XCLIP / CLIP learn which chosen references carry a clip already.
|
||||
{
|
||||
let i = self.active_tab;
|
||||
@@ -595,6 +623,7 @@ impl OpenCADStudio {
|
||||
if !self.command_point_allowed(i, *point) {
|
||||
return (Task::none(), true);
|
||||
}
|
||||
self.angle_override = None;
|
||||
// Typed / dynamic-input / headless points carry no snap, but the
|
||||
// accepted-snap list must stay index-parallel with the points the
|
||||
// command collects. Interactive picks record themselves in
|
||||
@@ -1130,6 +1159,9 @@ impl OpenCADStudio {
|
||||
self.apply_snap_override(kind);
|
||||
return Task::none();
|
||||
}
|
||||
if self.try_angle_override(token) {
|
||||
return Task::none();
|
||||
}
|
||||
if let Some((coord, kind)) = crate::app::helpers::parse_coord(token) {
|
||||
// Match the GUI command line: typed coordinates are in the active
|
||||
// UCS (relative offsets are rotated by the UCS axes), so a multi-
|
||||
@@ -1161,7 +1193,13 @@ impl OpenCADStudio {
|
||||
// bare number is the step's own value, not a distance — ROTATE's
|
||||
// angle, SCALE's factor, CIRCLE's radius. Only a token no step
|
||||
// claims becomes direct distance entry, measured along the cursor
|
||||
// direction from the command's anchor.
|
||||
// direction from the command's anchor. Under a typed `<angle`
|
||||
// the number is the distance along it first.
|
||||
if self.angle_override.is_some() {
|
||||
if let Some(distance) = self.try_direct_distance_entry(token) {
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
let (task, consumed) = self.feed_command_consumed(StepInput::Text(token.to_string()));
|
||||
if consumed {
|
||||
return task;
|
||||
|
||||
@@ -447,6 +447,53 @@ impl OpenCADStudio {
|
||||
.push_output(crate::t!("Zoom Window").as_ref());
|
||||
}
|
||||
|
||||
/// MVSETUP Scale viewports: each picked layout viewport shows the model
|
||||
/// at `factor` paper units per model unit. One undo step.
|
||||
pub(in crate::app) fn handle_scale_viewports(&mut self, handles: Vec<Handle>, factor: f64) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
let handles: Vec<Handle> = handles
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(codec::EntityType::Viewport(viewport)) if viewport.id > 1
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot(i, "MVSETUP");
|
||||
for handle in handles {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_viewport_scale_factor_for(handle, factor);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
|
||||
/// Turn the active view about its line of sight so the drawing shows
|
||||
/// `twist` radians counter-clockwise from its untwisted orientation.
|
||||
pub(in crate::app) fn handle_twist_view(&mut self, twist: f64) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
let set = |camera: &mut crate::scene::view::camera::Camera| {
|
||||
// The drawing turns one way when the camera rolls the other.
|
||||
camera.roll_by(-twist as f32 - camera.roll());
|
||||
};
|
||||
if self.tabs[i].scene.active_viewport.is_some() {
|
||||
self.tabs[i].scene.mutate_active_viewport_camera(set);
|
||||
} else {
|
||||
set(&mut self.tabs[i].scene.camera.borrow_mut());
|
||||
}
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_set_plot_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
|
||||
Reference in new issue
Block a user