Files
OneCAD-Draft/src/app/command_driver/entity_commit.rs
T
Karim Jerbi 5dfa41c032 fix(blocks): one base point convention for creation, redefine, insert, explode, XCLIP and REFEDIT
world = insert_point + Rot * Scale * (P - base_point). Attribute placement, EXPLODE, XCLIP storage, REFEDIT's working copy, the UCS reframe of attributes and the dynamic-block visibility grip ignored the base point and were offset by Rot*Scale*base. They now share insert_transform_with_base. Creation stores a ZERO base and redefine resets it; a no-op redefine moves nothing; the Block Definition dialog prefills the base from the definition. XCLIP filters stored by older builds keep their old displacement.
2026-10-12 04:35:19 +01:00

1202 lines
48 KiB
Rust

use super::*;
impl OpenCADStudio {
pub(super) fn handle_commit_entity(
&mut self,
entity: codec::EntityType,
preserve_commit_layer: bool,
) -> Option<Task<Message>> {
let i = self.active_tab;
let source_handle = entity.common().handle;
if !source_handle.is_null()
&& self.tabs[i]
.scene
.document
.get_entity(source_handle)
.is_some()
&& self.reject_locked_edit(i, source_handle)
{
return Some(Task::none());
}
// A line/arc drawn by a repeating command advances the ARC_CONT
// continuation anchor, so ending one run and launching another
// keeps continuing from the last segment (mirrors the
// CommitAndExit arm). Non-continuable repeats (point/ray)
// leave the anchor untouched. (#327)
if matches!(
entity,
codec::EntityType::Line(_)
| codec::EntityType::Arc(_)
| codec::EntityType::LwPolyline(_)
| codec::EntityType::Polyline2D(_)
) {
self.update_cont_anchor(&entity);
}
let label = self.history_label_from_active_cmd(i, "ENTITY");
// Ordinary drawables, viewports and raster images use targeted
// entity/object deltas. Block sentinels and novel layers retain
// the structure snapshot fallback.
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
let is_associative_dimension = matches!(
entity,
codec::EntityType::Dimension(
codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_)
)
);
let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2;
let committed = if preserve_commit_layer {
self.commit_entity_handle_preserve_layer(entity)
} else {
self.commit_entity_handle(entity)
};
if is_associative_dimension && association_enabled {
if let Some(handle) = committed {
let sources = self.infer_dimension_sources_guarded(i, handle);
self.tabs[i]
.scene
.attach_dimension_association(handle, sources);
}
}
if let Some(handle) = committed {
self.apply_continuous_constraints(i, &[handle]);
self.notify_entities_committed(i, &[handle]);
}
self.tabs[i].dirty = true;
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
None
}
pub(super) fn handle_commit_entities(
&mut self,
mut entities: Vec<codec::EntityType>,
preserve_commit_style: bool,
preserve_commit_layer: bool,
) -> Option<Task<Message>> {
let i = self.active_tab;
let locked_source = entities.iter().find_map(|entity| {
let handle = entity.common().handle;
(!handle.is_null()
&& self.tabs[i].scene.document.get_entity(handle).is_some()
&& self.tabs[i].scene.is_layer_locked(handle))
.then_some(handle)
});
if let Some(handle) = locked_source {
self.reject_locked_edit(i, handle);
return Some(Task::none());
}
let label = self.history_label_from_active_cmd(i, "ENTITY");
let symbol_names = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.nested_copy_symbol_names())
.cloned();
let delta_safe =
symbol_names.is_none() && entities.iter().all(|entity| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
if let Some(names) = symbol_names {
let retained = self.tabs[i]
.scene
.document
.localize_nested_copy_symbols(&mut entities, &names);
if retained > 0 {
self.command_line.push_info(&format!("{} copied object(s) retain imported style references whose dependencies cannot be localized.", retained));
}
self.refresh_layer_panel();
}
let mut committed = Vec::new();
for entity in entities {
let handle = if preserve_commit_style {
self.commit_entity_handle_preserve_style(entity)
} else if preserve_commit_layer {
self.commit_entity_handle_preserve_layer(entity)
} else {
self.commit_entity_handle(entity)
};
committed.extend(handle);
}
self.apply_continuous_constraints(i, &committed);
self.notify_entities_committed(i, &committed);
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
None
}
/// Hand the stored copies of freshly committed entities to the command
/// that is still running.
fn notify_entities_committed(&mut self, i: usize, handles: &[codec::Handle]) {
let entities: Vec<codec::EntityType> = handles
.iter()
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
.collect();
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_entities_committed(&entities);
}
}
pub(super) fn handle_commit_entities_and_exit(
&mut self,
mut entities: Vec<codec::EntityType>,
preserve_commit_style: bool,
preserve_commit_layer: bool,
) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "ENTITY");
let symbol_names = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.nested_copy_symbol_names())
.cloned();
let delta_safe =
symbol_names.is_none() && entities.iter().all(|entity| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
if let Some(names) = symbol_names {
let retained = self.tabs[i]
.scene
.document
.localize_nested_copy_symbols(&mut entities, &names);
if retained > 0 {
self.command_line.push_info(&format!("{} copied object(s) retain imported style references whose dependencies cannot be localized.", retained));
}
self.refresh_layer_panel();
}
let mut committed = Vec::new();
for entity in entities {
let handle = if preserve_commit_style {
self.commit_entity_handle_preserve_style(entity)
} else if preserve_commit_layer {
self.commit_entity_handle_preserve_layer(entity)
} else {
self.commit_entity_handle(entity)
};
committed.extend(handle);
}
self.apply_continuous_constraints(i, &committed);
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_commit_and_exit(
&mut self,
entity: codec::EntityType,
preserve_commit_layer: bool,
) -> Option<Task<Message>> {
let i = self.active_tab;
// XATTACH: the definition and its INSERT are created together
// in one undo step.
let xattach_request = self.tabs[i]
.active_cmd
.as_ref()
.filter(|cmd| cmd.name() == "XATTACH")
.and_then(|cmd| cmd.xattach_request());
if let Some(request) = xattach_request {
self.commit_xref_attach(i, request, entity);
return Some(Task::none());
}
let insert_block_name = match &entity {
codec::EntityType::Insert(ins) => Some(ins.block_name.clone()),
_ => None,
};
// Record where this draw ended so ARC_CONT can continue from it
// (before `entity` is moved into commit_entity).
self.update_cont_anchor(&entity);
// ATTDEF's height becomes the next text height (TEXTSIZE), as TEXT's does.
if let codec::EntityType::AttributeDefinition(attribute) = &entity {
if self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|cmd| cmd.name().ends_with("ATTDEF"))
{
self.tabs[i].scene.document.header.text_height = attribute.height;
}
}
// A field the committed text shows (FIELD, an ATTDEF Default from the
// Field dialog) is attached once the entity has its handle; the field
// objects are not part of the add delta, so the step is a snapshot.
let text_field = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|cmd| cmd.text_field());
let label = self.history_label_from_active_cmd(i, "ENTITY");
// A block reference whose attribute definitions carry fields gets
// them copied onto its attributes (field objects: snapshot step).
let attribute_fields = match &entity {
codec::EntityType::Insert(insert) if !insert.attributes.is_empty() => {
crate::entities::field::block_has_attribute_fields(
&self.tabs[i].scene.document,
&insert.block_name,
)
}
_ => false,
};
let delta_safe =
text_field.is_none() && !attribute_fields && self.delta_add_safe(i, &entity);
let field_value = match &entity {
codec::EntityType::MText(m) => m.value.clone(),
codec::EntityType::AttributeDefinition(a) => a.default_value.clone(),
_ => String::new(),
};
let pending = self.begin_undo(i, label, 1, delta_safe);
let is_associative_dimension = matches!(
entity,
codec::EntityType::Dimension(
codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_)
)
);
let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2;
let committed = if preserve_commit_layer {
self.commit_entity_handle_preserve_layer(entity)
} else {
self.commit_entity_handle(entity)
};
if is_associative_dimension && association_enabled {
if let Some(handle) = committed {
let sources = self.infer_dimension_sources_guarded(i, handle);
self.tabs[i]
.scene
.attach_dimension_association(handle, sources);
}
}
if let (Some(handle), Some(field)) = (committed, text_field) {
crate::entities::field::set_text_field(
&mut self.tabs[i].scene.document,
handle,
Some(field),
&field_value,
);
}
if let (Some(handle), true) = (committed, attribute_fields) {
crate::entities::field::attach_attribute_fields(
&mut self.tabs[i].scene.document,
handle,
);
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
}
if let Some(handle) = committed {
self.apply_continuous_constraints(i, &[handle]);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
if let Some(name) = insert_block_name {
self.record_block_insert(&name);
}
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
None
}
pub(super) fn handle_commit_dimension(&mut self, result: CmdResult) -> Option<Task<Message>> {
let i = self.active_tab;
let CmdResult::CommitDimension {
mut entity,
association,
preserve_base_style,
continue_command,
} = result
else {
unreachable!("router only routes the matching variant");
};
let label = self.history_label_from_active_cmd(i, "DIMENSION");
let association_mode = self.tabs[i].scene.document.header.dimension_associativity;
let single_source_dimension = matches!(
&entity,
codec::EntityType::Dimension(codec::entities::Dimension::Ordinate(_))
);
// A dimension placed on the sheet but measuring model geometry
// through a viewport carries the compensation as a negative
// DIMLFAC override.
if !preserve_base_style {
crate::scene::creation_style::apply_current_creation_styles(
&self.tabs[i].scene.document,
&mut entity,
);
}
if !self.apply_viewport_dimension_measurement(i, &mut entity) {
return Some(Task::none());
}
// Projected points cannot use direct paper-space source inference.
let association_allowed = self.dimension_association_allowed(i);
let inherited_dimension = if preserve_base_style {
match &entity {
codec::EntityType::Dimension(dimension) => Some((
dimension.base().common.layer.clone(),
dimension.base().style_name.clone(),
)),
_ => None,
}
} else {
None
};
let pending =
if association_mode == 0 {
let layer = self.tabs[i].active_layer.clone();
if layer != "0" || entity.as_entity().layer().is_empty() {
entity.as_entity_mut().set_layer(layer);
}
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
&self.ribbon.active_linetype.clone(),
);
crate::scene::view::dispatch::apply_line_weight(
&mut entity,
self.ribbon.active_lineweight,
);
let celtscale = self.tabs[i]
.scene
.document
.header
.current_entity_linetype_scale;
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
entity.common_mut().linetype_scale = celtscale;
}
crate::scene::creation_style::apply_current_creation_styles(
&self.tabs[i].scene.document,
&mut entity,
);
if let (Some((layer, style_name)), codec::EntityType::Dimension(dimension)) =
(inherited_dimension, &mut entity)
{
dimension.base_mut().common.layer = layer;
dimension.base_mut().style_name = style_name;
}
let pieces = crate::modules::draw::modify::explode::explode_entity(
&entity,
&self.tabs[i].scene.document,
);
let delta_safe = pieces.iter().all(|piece| self.delta_add_safe(i, piece));
let pending = self.begin_undo(i, label, pieces.len(), delta_safe);
for piece in pieces {
self.tabs[i].scene.add_entity(piece);
}
pending
} else {
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
if let Some(handle) =
self.commit_entity_handle_with_dimension_policy(entity, preserve_base_style)
{
if association_mode == 2 && association_allowed {
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
match association {
crate::command::DimensionAssociationInput::Infer(source) => {
let sources: Vec<_> = source.map_or_else(
|| self.infer_dimension_sources_guarded(i, handle),
|source| {
if single_source_dimension {
vec![Some(source)]
} else {
vec![Some(source), Some(source)]
}
},
);
self.tabs[i]
.scene
.attach_dimension_association(handle, sources.clone());
changes.extend(
sources
.into_iter()
.flatten()
.map(|source| (source, crate::scene::ChangeKind::Modified)),
);
}
crate::command::DimensionAssociationInput::Explicit(sources) => {
self.tabs[i]
.scene
.attach_dimension_association_sources(handle, sources.clone());
changes.extend(sources.into_iter().flatten().map(|source| {
(source.handle, crate::scene::ChangeKind::Modified)
}));
}
}
changes.sort_by_key(|(handle, _)| handle.value());
changes.dedup_by_key(|(handle, _)| handle.value());
self.tabs[i].scene.bump_entities(&changes);
} else if association_mode == 2 {
// Preserve the acquired viewport and source paths.
self.attach_viewport_dimension_association(i, handle);
}
}
pending
};
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].snap_result = None;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if continue_command {
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|cmd| cmd.prompt()) {
self.command_line.push_info(&prompt);
}
} else {
self.tabs[i].active_cmd = None;
self.restore_pre_cmd_tangent();
}
None
}
pub(super) fn handle_commit_dimensions_and_exit(
&mut self,
dimensions: Vec<(codec::EntityType, crate::command::DimensionAssociationInput)>,
) {
let i = self.active_tab;
let association_mode = self.tabs[i].scene.document.header.dimension_associativity;
let mut made = 0;
let pending = if association_mode == 0 {
let mut pieces = Vec::new();
for (mut entity, _) in dimensions {
let layer = self.tabs[i].active_layer.clone();
if layer != "0" || entity.as_entity().layer().is_empty() {
entity.as_entity_mut().set_layer(layer);
}
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
&self.ribbon.active_linetype.clone(),
);
crate::scene::view::dispatch::apply_line_weight(
&mut entity,
self.ribbon.active_lineweight,
);
let celtscale = self.tabs[i]
.scene
.document
.header
.current_entity_linetype_scale;
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
entity.common_mut().linetype_scale = celtscale;
}
crate::scene::creation_style::apply_current_creation_styles(
&self.tabs[i].scene.document,
&mut entity,
);
let exploded = crate::modules::draw::modify::explode::explode_entity(
&entity,
&self.tabs[i].scene.document,
);
if !exploded.is_empty() {
made += 1;
}
pieces.extend(exploded);
}
let delta_safe = pieces.iter().all(|piece| self.delta_add_safe(i, piece));
let pending = self.begin_undo(i, "QDIM", pieces.len(), delta_safe);
for piece in pieces {
self.tabs[i].scene.add_entity(piece);
}
pending
} else {
let delta_safe = dimensions
.iter()
.all(|(entity, _)| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, "QDIM", dimensions.len(), delta_safe);
for (entity, association) in dimensions {
let Some(handle) = self.commit_entity_handle(entity) else {
continue;
};
made += 1;
if association_mode != 2 {
continue;
}
let sources = match association {
crate::command::DimensionAssociationInput::Infer(source) => source.map_or_else(
|| {
self.infer_dimension_sources_guarded(i, handle)
.into_iter()
.map(|source| {
source.map(crate::command::DimensionAssociationSource::inferred)
})
.collect()
},
|source| {
vec![Some(crate::command::DimensionAssociationSource::inferred(
source,
))]
},
),
crate::command::DimensionAssociationInput::Explicit(sources) => sources,
};
self.tabs[i]
.scene
.attach_dimension_association_sources(handle, sources.clone());
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
changes.extend(
sources
.into_iter()
.flatten()
.map(|source| (source.handle, crate::scene::ChangeKind::Modified)),
);
changes.sort_by_key(|(handle, _)| handle.value());
changes.dedup_by_key(|(handle, _)| handle.value());
self.tabs[i].scene.bump_entities(&changes);
}
pending
};
if made > 0 {
self.tabs[i].dirty = true;
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.command_line
.push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_commit_solid(&mut self, result: CmdResult) {
let i = self.active_tab;
let CmdResult::CommitSolid {
entity,
solid,
history,
erase_source,
} = result
else {
unreachable!("router only routes the matching variant");
};
let label = self.history_label_from_active_cmd(i, "SOLID");
let erase_source = erase_source.filter(|handle| {
self.delete_objects != 0 && !self.tabs[i].scene.is_layer_locked(*handle)
});
let pending = self.begin_undo(i, label, 1 + usize::from(erase_source.is_some()), true);
let handle = self.add_solid_model(entity, *solid, history);
if !handle.is_null() {
if let Some(source) = erase_source {
self.tabs[i].scene.erase_entities(&[source]);
}
self.tabs[i].dirty = true;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
}
pub(super) fn handle_commit_and_edit_text(
&mut self,
entity: codec::EntityType,
) -> Option<Task<Message>> {
let i = self.active_tab;
let annotative_mleader = matches!(
&entity,
codec::EntityType::MultiLeader(ml) if ml.enable_annotation_scale
);
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = self.delta_add_safe(i, &entity) && !annotative_mleader;
let pending = self.begin_undo(i, label, 1, delta_safe);
let handle = self.commit_entity_handle(entity);
if annotative_mleader {
let scale_handle = self.tabs[i].scene.creation_annotation_scale_handle();
if let (Some(handle), Some(scale_handle)) = (handle, scale_handle) {
crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
handle,
scale_handle,
);
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.ribbon.deactivate_tool();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if let Some(h) = handle {
return Some(self.begin_text_edit(h));
}
None
}
pub(super) fn handle_commit_many_and_edit_text(
&mut self,
entities: Vec<codec::EntityType>,
edit_index: usize,
open_editor: bool,
) -> Option<Task<Message>> {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = entities.iter().all(|entity| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
let mut edit_handle = None;
let mut leader_handle = None;
for (idx, entity) in entities.into_iter().enumerate() {
let is_leader = matches!(entity, codec::EntityType::Leader(_));
let h = self.commit_entity_handle(entity);
if idx == edit_index {
edit_handle = h;
}
if is_leader {
leader_handle = h;
}
}
// Link the leader to its annotation so the pair edits as a unit
// (double-click on the leader resolves to the text entity).
if let (Some(lh), Some(ah)) = (leader_handle, edit_handle) {
let linked = if let Some(codec::EntityType::Leader(l)) =
self.tabs[i].scene.document.get_entity_mut(lh)
{
l.annotation_handle = ah;
true
} else {
false
};
if linked {
// The LEADER may already have received its annotation context while
// annotation_handle was still NULL. Refresh it now that the MTEXT link
// is known so the context represents the finished leader.
self.tabs[i].scene.sync_displayed_annotation_context(lh);
self.tabs[i]
.scene
.bump_entities(&[(lh, crate::scene::ChangeKind::Modified)]);
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.ribbon.deactivate_tool();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if open_editor {
if let Some(h) = edit_handle {
return Some(self.begin_text_edit(h));
}
}
None
}
pub(super) fn handle_create_block(
&mut self,
mut handles: Vec<Handle>,
name: String,
base: glam::DVec3,
) -> Option<Task<Message>> {
let i = self.active_tab;
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "BLOCK");
let ucs = self.tabs[i].ucs_xform();
let world_to_block = ucs.to_ucs_transform_at(base);
let block_to_world = ucs.to_wcs_transform_at(base);
match self.tabs[i].scene.create_block_from_entities(
&handles,
&name,
&world_to_block,
&block_to_world,
) {
Ok(insert_handle) => {
self.tabs[i].dirty = true;
self.tabs[i].scene.deselect_all();
if !insert_handle.is_null() {
self.tabs[i].scene.select_entity(insert_handle, false);
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_output(crate::tf!("Block \"{name}\" created.").as_ref());
self.refresh_properties();
}
Err(err) => {
self.discard_last_undo_entry(i);
self.command_line.push_error(&err);
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
}
}
None
}
pub(super) fn handle_create_block_with_options(
&mut self,
mut options: Box<crate::scene::CreateBlockOptions>,
) -> Option<Task<Message>> {
let i = self.active_tab;
options
.handles
.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if options.handles.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "BLOCK");
let name = options.name.clone();
match self.tabs[i].scene.create_block_with_options(*options) {
Ok(insert_handle) => {
self.tabs[i].dirty = true;
self.tabs[i].scene.deselect_all();
if !insert_handle.is_null() {
self.tabs[i].scene.select_entity(insert_handle, false);
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_output(crate::tf!("Block \"{name}\" created.").as_ref());
self.refresh_properties();
self.note_recent_block(&name);
self.refresh_block_palette();
}
Err(err) => {
self.discard_last_undo_entry(i);
self.command_line.push_error(&err);
}
}
None
}
pub(super) fn handle_commit_hatch(&mut self, hatch: crate::scene::HatchModel) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, 1, true);
let layer = self.tabs[i].active_layer.clone();
let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_commit_styled_hatch(
&mut self,
hatch: crate::scene::HatchModel,
color: codec::types::Color,
transparency: codec::types::Transparency,
) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, 1, true);
let layer = self.tabs[i].active_layer.clone();
let new_handle =
self.tabs[i]
.scene
.add_hatch(hatch, Some(&layer), Some((color, transparency)));
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_commit_hatch_with_boundaries(
&mut self,
mut hatch: crate::scene::HatchModel,
boundaries: Vec<codec::EntityType>,
entity_style: Option<(codec::types::Color, codec::types::Transparency)>,
) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, boundaries.len() + 1, true);
let mut sources = Vec::with_capacity(boundaries.len());
for boundary in boundaries {
let handles = self
.commit_entity_handle(boundary)
.into_iter()
.collect::<Vec<_>>();
sources.push(handles);
}
if let Some(paths) = hatch.boundary_paths.as_mut() {
for (path, handles) in std::sync::Arc::make_mut(paths).iter_mut().zip(&sources) {
path.boundary_handles = handles.clone();
path.flags.set_external(!handles.is_empty());
}
}
hatch.boundary_sources = Some(std::sync::Arc::new(sources));
let layer = self.tabs[i].active_layer.clone();
let new_handle = self.tabs[i]
.scene
.add_hatch(hatch, Some(&layer), entity_style);
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_commit_hatches(
&mut self,
hatches: Vec<crate::scene::HatchModel>,
entity_style: Option<(codec::types::Color, codec::types::Transparency)>,
) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, hatches.len(), true);
let layer = self.tabs[i].active_layer.clone();
for hatch in hatches {
let new_handle =
self.tabs[i]
.scene
.add_hatch(hatch, Some(&layer), entity_style.clone());
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
pub(super) fn handle_update_entity_and_finish(
&mut self,
handle: Handle,
entity: codec::EntityType,
) -> Option<Task<Message>> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Some(Task::none());
}
let label = self.history_label_from_active_cmd(i, "EDIT");
self.push_undo_snapshot(i, label);
let is_dimension = matches!(entity, codec::EntityType::Dimension(_));
if let Some(current) = self.tabs[i].scene.document.get_entity_mut(handle) {
*current = entity;
if is_dimension {
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
}
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
} else {
self.command_line
.push_error(crate::t!("The selected object is no longer available.").as_ref());
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.refresh_properties();
None
}
pub(super) fn handle_reassociate_center_mark(
&mut self,
target: Handle,
source: Handle,
point: glam::DVec3,
) -> Option<Task<Message>> {
let i = self.active_tab;
if self.reject_locked_edit(i, target) {
return Some(Task::none());
}
self.push_undo_snapshot(i, "CENTERREASSOCIATE");
if self.tabs[i]
.scene
.reassociate_center_mark(target, source, point)
{
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("CENTERREASSOCIATE: center mark associated.").as_ref());
} else {
self.command_line.push_error(
crate::t!("CENTERREASSOCIATE: the selected source is not circular.").as_ref(),
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.refresh_properties();
None
}
pub(super) fn handle_attreq_needed(&mut self, block_name: String) -> Option<Task<Message>> {
let i = self.active_tab;
// Collect the full AttributeDefinitions owned by this block
// record so each created attribute keeps its geometry (#255).
let mut attdefs: Vec<codec::entities::AttributeDefinition> = {
let doc = &self.tabs[i].scene.document;
if let Some(br) = doc.block_records.get(&block_name) {
br.entity_handles
.iter()
.filter_map(|&h| {
if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(h)
{
Some(ad.clone())
} else {
None
}
})
.collect()
} else {
vec![]
}
};
// ATTREQ 0: attributes take their defaults without prompting.
if !self.tabs[i].scene.document.header.attribute_request {
for attdef in &mut attdefs {
attdef.flags.preset = true;
}
}
if attdefs.is_empty() {
// No attribute definitions — commit the INSERT directly.
let entity = self.tabs[i]
.active_cmd
.as_mut()
.and_then(|c| c.attreq_take_insert());
if let Some(entity) = entity {
let insert_name = match &entity {
codec::EntityType::Insert(ins) => Some(ins.block_name.clone()),
_ => None,
};
let label = self.history_label_from_active_cmd(i, "INSERT");
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
self.commit_entity(entity);
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
// A repeating insertion keeps its command for the next block.
let keep = self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|c| c.attreq_continue());
if keep {
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
} else {
self.tabs[i].active_cmd = None;
}
self.tabs[i].snap_result = None;
if let Some(n) = insert_name {
self.record_block_insert(&n);
}
self.restore_pre_cmd_tangent();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
} else {
// Attributes are placed through the same base-point convention as
// the render path, so hand the command the definition's base point.
let block_base = crate::scene::render_graph::block_base_point(
&self.tabs[i].scene.document,
&block_name,
);
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
cmd.attreq_set_block_base(block_base);
}
let completed = self.tabs[i]
.active_cmd
.as_mut()
.and_then(|cmd| cmd.attreq_set_attdefs(attdefs));
if let Some(entity) = completed {
if let codec::EntityType::Insert(ins) = &entity {
let name = ins.block_name.clone();
self.record_block_insert(&name);
}
let keep = self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|c| c.attreq_continue());
return Some(self.apply_cmd_result(if keep {
CmdResult::CommitEntity(entity)
} else {
CmdResult::CommitAndExit(entity)
}));
}
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
}
None
}
pub(super) fn handle_commit_live_entity(&mut self, entity: codec::EntityType) {
let i = self.active_tab;
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
let handle = self.commit_entity_handle(entity);
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if let Some(h) = handle {
// Keep the live document Arc unique while later vertices
// replace the geometry in place. The final Arc is captured
// by UpdateLiveEntity when the command completes.
self.defer_live_entity_history_after(i, h);
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
cmd.set_live_handle(h);
}
}
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
}
pub(super) fn handle_update_live_entity(
&mut self,
handle: Handle,
entity: codec::EntityType,
finish: bool,
) {
let i = self.active_tab;
let tracks_draw_anchor = matches!(
&entity,
codec::EntityType::Line(_)
| codec::EntityType::Arc(_)
| codec::EntityType::LwPolyline(_)
| codec::EntityType::Polyline(_)
| codec::EntityType::Polyline2D(_)
| codec::EntityType::Polyline3D(_)
);
// Replace the live entity's geometry in place, preserving its
// handle and layer (the fresh entity from the command carries
// defaults — a NULL handle would desync it from the document
// map key and drop it from rendering / hit-test). No undo
// snapshot — the create already pushed one, so the whole object
// reverts as a unit.
if let Some(old) = self.tabs[i].scene.document.get_entity_mut(handle) {
// The initial live commit assigns the handle, owning block,
// current layer and common display properties. Geometry
// refreshes must retain all of that identity; replacing only
// the handle and layer reset owner_handle to NULL and made
// the completed entity unavailable to scoped operations such
// as parametric constraints.
let common = old.common().clone();
let mut new = entity;
*new.common_mut() = common;
*old = new;
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
}
if tracks_draw_anchor {
self.tabs[i].last_draw_anchor = Some(handle);
}
if finish {
self.finish_live_entity_history(i, 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();
} else {
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
}
}
pub(super) fn handle_finalize_live_entity(&mut self, handle: Handle) {
let i = self.active_tab;
// The live geometry already matches the command's committed
// vertices. Close the deferred history image and UI state
// without publishing a redundant Modified delta.
self.finish_live_entity_history(i, 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();
}
pub(super) fn handle_remove_live_entity(&mut self, handle: Handle) {
let i = self.active_tab;
// The command backed off below a valid entity (PLINE Undo at
// one remaining vertex): take the live entity out of the
// document, drop its provisional history entry and keep
// prompting. A later second point creates one fresh entry.
self.tabs[i].scene.erase_entities(&[handle]);
if self.tabs[i]
.last_draw_anchor
.is_some_and(|anchor_handle| anchor_handle == handle)
{
self.tabs[i].last_draw_anchor = None;
}
self.discard_last_undo_entry(i);
self.tabs[i].dirty = true;
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
}
}