feat(bpt): add reference-side row selection

This commit is contained in:
Karim Jerbi committed 2026-10-18 19:06:36 +01:00
1 parent d3e64800f6
commit 2797b5b9e1
11 files changed
+399 -23

No files matched your search

+1
View File
@@ -563,6 +563,7 @@ impl OpenCADStudio {
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.table_popup = None;
history.checkpoint = self.block_edit_checkpoint(i);
self.tabs[i].block_edit_history = Some(history);
self.tabs[i].scene.bump_geometry();
+1
View File
@@ -134,6 +134,7 @@ impl OpenCADStudio {
tab.scene.set_current_layout("Model".into());
self.discard_block_tests(source_id);
self.visibility_popup = None;
self.table_popup = None;
self.grip_popup = None;
self.grip_hover = None;
self.tabs.push(tab);
+1
View File
@@ -105,6 +105,7 @@ impl OpenCADStudio {
self.grip_popup = None;
self.grip_pending = None;
self.visibility_popup = None;
self.table_popup = None;
self.hover_dwell = None;
self.ucs_grip_drag = None;
self.ucs_icon_selected = false;
+2
View File
@@ -1064,6 +1064,7 @@ impl OpenCADStudio {
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.table_popup = None;
// Entering BEDIT changes which block is assembled, not the
// cached geometry of that block's entities.
self.tabs[i].scene.bump_geometry_no_blocks();
@@ -1098,6 +1099,7 @@ impl OpenCADStudio {
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.table_popup = None;
let parent_index = return_block.and_then(|parent_name| {
self.tabs[i]
+2
View File
@@ -128,6 +128,7 @@ pub(super) struct DocumentTab {
/// must never target a tab by its transient vector index.
pub(super) id: u64,
pub(super) scene: Scene,
pub(super) table_grip: Option<super::visibility::TableGrip>,
pub(super) current_path: Option<PathBuf>,
/// Persistent native edit lease for the drawing currently at
/// `current_path`. The sidecar portion survives atomic file replacement.
@@ -660,6 +661,7 @@ impl DocumentTab {
selected_handle: None,
selected_authoring: None,
visibility_grip: None,
table_grip: None,
wireframe: false,
render_mode: codec::entities::ViewportRenderMode::Wireframe2D,
visual_style: "Wireframe 2D".into(),
+8 -5
View File
@@ -8,21 +8,21 @@ pub(crate) mod config;
#[cfg(not(target_arch = "wasm32"))]
pub use automation::{export_headless, serve};
mod annotation_data;
mod block_actions;
mod block_authoring_edit;
mod block_editor;
#[cfg(test)]
mod block_editor_tests;
mod block_save_as;
mod block_visibility;
mod block_lookup;
mod block_properties_table;
mod block_actions;
mod block_authoring_edit;
mod block_properties;
mod block_properties_table;
#[cfg(test)]
mod block_properties_tests;
mod block_save_as;
mod block_test;
#[cfg(test)]
mod block_test_tests;
mod block_visibility;
mod command_driver;
pub(crate) use command_driver::{
copy_to_clipboard_kernel, define_block_raw_doc, paste_entities_kernel, PasteSpace,
@@ -904,6 +904,7 @@ pub(super) struct OpenCADStudio {
grip_pending: Option<GripPendingValue>,
/// Open dynamic-block visibility-state dropdown.
visibility_popup: Option<visibility::VisibilityPopup>,
table_popup: Option<visibility::TablePopup>,
/// A leader line just added via the "Add Leader" grip menu whose arrow is
/// being placed (follows the cursor). `(entity handle, new-arrow grip id)`.
/// Esc before the placement click removes it again.
@@ -3376,6 +3377,7 @@ pub enum Message {
/// User picked a dynamic-block visibility state — index into the
/// visibility dropdown's items.
VisibilityPick(usize),
TableRowPick(usize),
/// Timer pulse while the cursor is dwelling on a grip; drives the
/// dwell-to-popup transition without requiring further mouse motion.
GripDwellTick,
@@ -4894,6 +4896,7 @@ impl OpenCADStudio {
grip_popup: None,
grip_pending: None,
visibility_popup: None,
table_popup: None,
grip_add_provisional: None,
grip_preview_handles: Vec::new(),
hover_dwell: None,
+8
View File
@@ -194,6 +194,7 @@ impl OpenCADStudio {
self.cancel_active_grip_edit();
self.grip_pending = None;
self.visibility_popup = None;
self.table_popup = None;
self.grip_popup = None;
self.grip_hover = None;
}
@@ -1354,7 +1355,11 @@ impl OpenCADStudio {
self.grip_hover = None;
return Task::none();
}
if self.table_popup.take().is_some() {
return Task::none();
}
if self.visibility_popup.take().is_some() {
self.table_popup = None;
return Task::none();
}
if self.tabs[self.active_tab].active_grip.is_some() {
@@ -3939,6 +3944,9 @@ impl OpenCADStudio {
}
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
if field == "authoring_name" {
return self.commit_authoring_name();
}
if field.starts_with("bedit_") {
return self.commit_block_property(field);
}
+39 -7
View File
@@ -1,6 +1,6 @@
use crate::io::plot_style::PlotStyleTableExt as _;
use super::{ArrowKey, Message, OpenCADStudio, TextEntryMode};
use crate::command::CadCommand;
use crate::io::plot_style::PlotStyleTableExt as _;
use crate::scene::VIEWCUBE_DRAW_PX;
use crate::ui::PropertiesPanel;
use iced::time::Instant;
@@ -4575,7 +4575,9 @@ impl OpenCADStudio {
// CATIA navigation: the left button pressed during a middle drag
// turns the drag into an orbit instead of starting a pick.
Message::ViewportLeftPressAt(_) | Message::ViewportLeftPress | Message::PanePress(_)
Message::ViewportLeftPressAt(_)
| Message::ViewportLeftPress
| Message::PanePress(_)
if self.navigation_style == crate::app::settings::NavigationStyle::Catia
&& self.tabs[self.active_tab]
.scene
@@ -4999,6 +5001,12 @@ impl OpenCADStudio {
}
Task::none()
}
Message::TableRowPick(idx) => {
if let Some(popup) = self.table_popup.take() {
self.apply_table_row(popup.insert_handle, idx);
}
Task::none()
}
Message::GripMenuPick(idx) => self.on_grip_menu_pick(idx),
@@ -6782,6 +6790,30 @@ impl OpenCADStudio {
self.delete_parametric_constraint(id);
return Task::none();
}
if let (Some(handle), Some(session)) = (
self.tabs[i].selected_authoring,
self.tabs[i].active_block_edit_session(),
) {
let definition = session.br_handle;
let mut staged = self.tabs[i].scene.document.clone();
match crate::scene::block_authoring::edit::remove_parameter(
&mut staged,
definition,
handle,
) {
Ok(()) => {
self.push_undo_snapshot(i, "Delete block parameter");
self.tabs[i].scene.document = staged;
self.tabs[i].selected_authoring = None;
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.finish_pending_history(i);
self.refresh_properties();
}
Err(message) => self.command_line.push_error(crate::t!(message).as_ref()),
}
return Task::none();
}
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
if !handles.is_empty() {
// Erase is delta-safe unless a target is in a group (group
@@ -9392,12 +9424,14 @@ impl OpenCADStudio {
Message::ToggleDynamicUcs => {
self.dynamic_ucs = !self.dynamic_ucs;
self.command_line.push_output(if self.dynamic_ucs {
self.command_line.push_output(
if self.dynamic_ucs {
crate::t!("Dynamic UCS: on")
} else {
crate::t!("Dynamic UCS: off")
}
.as_ref());
.as_ref(),
);
self.persist_settings_if_changed();
Task::none()
}
@@ -9780,9 +9814,7 @@ impl OpenCADStudio {
// Closing the attribute manager is its Cancel: live changes
// are taken back.
if self.active_modal == Some(super::ModalKind::AttMan) {
return self.on_attman(
crate::ui::window::attman_dialog::AttManMsg::Cancel,
);
return self.on_attman(crate::ui::window::attman_dialog::AttManMsg::Cancel);
}
if matches!(
self.active_modal,
+49 -10
View File
@@ -66,26 +66,51 @@ mod selection_gesture_priority_tests {
fn fast_first_click_never_becomes_a_lasso_solely_from_cursor_speed() {
assert!(!selection_drag_should_start(25.0 * 25.0, 20, false));
assert!(!selection_drag_should_start(120.0 * 120.0, 80, false));
assert!(!selection_drag_should_start(500.0 * 500.0, POLY_START_DELAY_MS - 1, false));
assert!(selection_drag_should_start(25.0 * 25.0, POLY_START_DELAY_MS, false));
assert!(!selection_drag_should_start(11.0 * 11.0, POLY_START_DELAY_MS + 500, false));
assert!(!selection_drag_should_start(
500.0 * 500.0,
POLY_START_DELAY_MS - 1,
false
));
assert!(selection_drag_should_start(
25.0 * 25.0,
POLY_START_DELAY_MS,
false
));
assert!(!selection_drag_should_start(
11.0 * 11.0,
POLY_START_DELAY_MS + 500,
false
));
}
#[test]
fn pickdrag_rectangle_keeps_its_fast_drag_shortcut() {
assert!(selection_drag_should_start(25.0 * 25.0, 20, true));
assert!(!selection_drag_should_start(23.0 * 23.0, 20, true));
assert!(selection_drag_should_start(13.0 * 13.0, POLY_START_DELAY_MS, true));
assert!(selection_drag_should_start(
13.0 * 13.0,
POLY_START_DELAY_MS,
true
));
}
#[test]
fn quick_selection_click_anchors_where_the_button_went_down() {
let start = Point::new(100.0, 100.0);
let moved = Point::new(190.0, 135.0);
assert_eq!(selection_click_position(moved, Some(start), true, true), start);
assert_eq!(
selection_click_position(moved, Some(start), true, true),
start
);
// During a command or a genuine press-drag, use the moving cursor.
assert_eq!(selection_click_position(moved, Some(start), true, false), moved);
assert_eq!(selection_click_position(moved, Some(start), false, true), moved);
assert_eq!(
selection_click_position(moved, Some(start), true, false),
moved
);
assert_eq!(
selection_click_position(moved, Some(start), false, true),
moved
);
assert_eq!(selection_click_position(moved, None, true, true), moved);
}
}
@@ -993,6 +1018,7 @@ impl OpenCADStudio {
.filter(|(owner, grip)| self.tabs[i].hot_grips.contains(&(*owner, grip.id)))
.filter(|(_, grip)| {
grip.id != crate::app::visibility::VIS_GRIP_ID
&& grip.id != crate::app::visibility::TABLE_GRIP_ID
&& !crate::app::block_actions::is_test_grip(grip.id)
&& !crate::app::block_authoring_edit::is_editor_grip(grip.id)
})
@@ -1019,6 +1045,7 @@ impl OpenCADStudio {
.zip(self.tabs[i].selected_grips.iter())
.filter(|(_, grip)| {
grip.id != crate::app::visibility::VIS_GRIP_ID
&& grip.id != crate::app::visibility::TABLE_GRIP_ID
&& !crate::app::block_actions::is_test_grip(grip.id)
&& !crate::app::block_authoring_edit::is_editor_grip(grip.id)
})
@@ -1163,6 +1190,7 @@ impl OpenCADStudio {
// hover grip-menu.
if self.grip_popup.is_none()
&& grip_id != crate::app::visibility::VIS_GRIP_ID
&& grip_id != crate::app::visibility::TABLE_GRIP_ID
&& !crate::app::block_actions::is_test_grip(grip_id)
&& !crate::app::block_authoring_edit::is_editor_grip(grip_id)
&& self
@@ -2860,8 +2888,7 @@ impl OpenCADStudio {
!self.tabs[i].snap_result.is_some_and(|s| {
!matches!(
s.snap_type,
crate::snap::SnapType::Grid
| crate::snap::SnapType::Extension
crate::snap::SnapType::Grid | crate::snap::SnapType::Extension
)
})
}) {
@@ -3905,6 +3932,9 @@ impl OpenCADStudio {
if self.visibility_popup.take().is_some() {
return Task::none();
}
if self.table_popup.take().is_some() {
return Task::none();
}
// Same dismiss-on-outside-click for the right-click
// context menu: its panel is opaque, so a press that
// reaches here is outside the menu.
@@ -4151,6 +4181,12 @@ impl OpenCADStudio {
self.grip_popup = None;
return Task::none();
}
if grip_id == crate::app::visibility::TABLE_GRIP_ID {
self.open_table_popup(p);
self.grip_hover = None;
self.grip_popup = None;
return Task::none();
}
if self.start_test_action_grip(handle, grip_id) {
return Task::none();
}
@@ -4275,7 +4311,7 @@ impl OpenCADStudio {
}
}
if edit_cam.is_none() && self.pick_authoring_object(p, bounds) {
if self.pick_authoring_object(p, bounds) {
return Task::none();
}
@@ -5434,6 +5470,7 @@ impl OpenCADStudio {
self.tabs[i].scene.select_entities(&handles);
self.tabs[i].scene.expand_selection_for_groups(&handles);
}
self.pick_authoring_box(a, p, crossing);
let apply_ms = t_apply.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_props = crate::perf::enabled().then(Instant::now);
self.refresh_properties();
@@ -5777,6 +5814,7 @@ was_selected={}",
}
let apply_ms = t_apply.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_props = crate::perf::enabled().then(Instant::now);
self.pick_authoring_box(a, p, crossing);
self.refresh_properties();
if crate::perf::enabled() {
crate::perf_record!(
@@ -6897,6 +6935,7 @@ properties={:.1}ms picked={}",
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.table_popup = None;
self.tabs[i].refresh_active_ucs();
self.tabs[i].scene.bump_geometry_no_blocks();
self.refresh_properties();
+192 -1
View File
@@ -22,6 +22,7 @@ use super::OpenCADStudio;
/// Sentinel grip id for the visibility (lookup) grip, distinct from any
/// entity's own grip ids so the click handler routes it to the dropdown.
pub(super) const VIS_GRIP_ID: usize = usize::MAX;
pub(super) const TABLE_GRIP_ID: usize = usize::MAX - 1;
/// Resolved visibility info for the active tab's selected dynamic-block INSERT.
#[derive(Clone, Debug)]
@@ -40,6 +41,20 @@ pub struct VisibilityPopup {
pub current: Option<usize>,
}
#[derive(Clone, Debug)]
pub(super) struct TableGrip {
pub insert_handle: Handle,
pub current: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct TablePopup {
pub insert_handle: Handle,
pub anchor: iced::Point,
pub header: String,
pub rows: Vec<String>,
pub current: Option<usize>,
}
/// Block-local entity-handle lists for a dynamic definition and the anonymous
/// block actually inserted, plus the state's visible-by-index set.
struct StateMapping {
@@ -50,6 +65,180 @@ struct StateMapping {
}
impl OpenCADStudio {
pub(super) fn apply_table_row(&mut self, insert: Handle, row: usize) {
let i = self.active_tab;
if self.reject_locked_edit(i, insert) {
return;
}
let result = {
let doc = &self.tabs[i].scene.document;
let Some(def) = doc.dynamic_definition_for_insert(insert) else {
return;
};
let Ok(Some(view)) = crate::scene::block_properties_table::table(doc, def) else {
return;
};
crate::scene::block_actions::instance::evaluate_table_row(
doc,
insert,
&view.columns,
row,
)
};
match result {
Ok(Some(staged)) => {
self.push_undo_snapshot(i, "Block Properties Table row");
self.apply_action_document(i, staged);
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.finish_pending_history(i);
self.refresh_selected_grips();
}
Ok(None) => {}
Err(error) => self.command_line.push_error(crate::t!(error).as_ref()),
}
}
pub(super) fn open_table_popup(&mut self, anchor: iced::Point) {
if let Some(grip) = &self.tabs[self.active_tab].table_grip {
let tab = &self.tabs[self.active_tab];
let doc = &tab.scene.document;
let Some(def) = doc.dynamic_definition_for_insert(grip.insert_handle) else {
return;
};
let Ok(Some(view)) = crate::scene::block_properties_table::table(doc, def) else {
return;
};
let parameters = crate::scene::block_actions::all_parameters(doc, def);
let header = view
.columns
.iter()
.map(|c| {
let display = if c.connection_name.eq_ignore_ascii_case("UpdatedAngle") {
crate::scene::block_orientation_parameter::parameters(doc, def)
.into_iter()
.find(|p| p.handle == c.handle)
.map(|p| p.label)
} else {
crate::scene::block_distance_parameter::parameters(doc, def)
.into_iter()
.find(|p| p.handle == c.handle)
.and_then(|p| p.labels.first().cloned())
};
display
.filter(|n| !n.trim().is_empty())
.or_else(|| {
parameters
.iter()
.find(|p| p.handle == c.handle)
.map(|p| p.name.clone())
})
.filter(|n| !n.trim().is_empty())
.unwrap_or_else(|| c.connection_name.clone())
})
.collect::<Vec<_>>()
.join(", ");
let rows = view
.rows
.iter()
.map(|r| {
r.iter()
.map(|v| format!("{v:.4}"))
.collect::<Vec<_>>()
.join(", ")
})
.collect();
self.table_popup = Some(TablePopup {
insert_handle: grip.insert_handle,
anchor,
header,
rows,
current: grip.current,
});
}
}
pub(super) fn refresh_table_grip(&mut self, wo: [f64; 3]) {
let i = self.active_tab;
self.tabs[i].table_grip = None;
let Some(handle) = self.tabs[i].selected_handle else {
return;
};
if self.tabs[i].active_block_edit_session().is_some()
|| self.tabs[i].scene.selected.len() != 1
{
return;
}
let doc = &self.tabs[i].scene.document;
let Some(def) = doc.dynamic_definition_for_insert(handle) else {
return;
};
let Ok(Some(view)) = crate::scene::block_properties_table::table(doc, def) else {
return;
};
let Some(EntityType::Insert(ins)) = doc.get_entity(handle) else {
return;
};
let graph =
crate::scene::block_properties_table::graph_handle(doc, def).unwrap_or(Handle::NULL);
let point = doc
.objects
.values()
.find_map(|value| match value {
codec::objects::ObjectType::DynamicBlock(obj) if obj.owner == graph => match &obj
.data
{
codec::objects::DynamicBlockData::PropertiesTableGrip(g) => Some(g.location),
_ => None,
},
_ => None,
})
.unwrap_or(view.table.parameter.definition_point);
let wp = crate::scene::render_graph::insert_transform(doc, ins).apply(point);
let mut values = Vec::new();
let mut valid = true;
for column in &view.columns {
let params = crate::scene::block_actions::all_parameters(doc, def);
let Some(p) = params.iter().find(|p| p.handle == column.handle) else {
valid = false;
continue;
};
match crate::scene::block_actions::instance::value(doc, handle, p) {
Ok(crate::scene::block_actions::Value::Point(v)) => values.push(
if column.connection_name.eq_ignore_ascii_case("UpdatedAngle") {
(v.y - p.base.y)
.atan2(v.x - p.base.x)
.rem_euclid(std::f64::consts::TAU)
} else {
v.distance(p.base)
},
),
_ => {
values.push(f64::NAN);
valid = false;
}
}
}
let current = valid
.then(|| {
view.rows
.iter()
.position(|r| r.iter().zip(&values).all(|(a, b)| (a - b).abs() <= 1e-9))
})
.flatten();
self.tabs[i].selected_grips.push(GripDef {
id: TABLE_GRIP_ID,
world: glam::DVec3::new(wp.x - wo[0], wp.y - wo[1], wp.z - wo[2]),
is_midpoint: false,
shape: GripShape::Triangle,
dir: None,
axis: None,
});
self.tabs[i].selected_grip_handles.push(handle);
self.tabs[i].table_grip = Some(TableGrip {
insert_handle: handle,
current,
});
}
/// Compute the index sets needed to apply `state` of `param` (governing the
/// dynamic definition `def_block`) to the anonymous block `anon_name`.
///
@@ -139,7 +328,9 @@ impl OpenCADStudio {
label,
field: "dyn_visibility",
value: crate::scene::model::object::PropValue::Choice {
selected: current.and_then(|c| options.get(c).cloned()).unwrap_or_default(),
selected: current
.and_then(|c| options.get(c).cloned())
.unwrap_or_default(),
options,
},
})
+96
View File
@@ -306,6 +306,102 @@ pub(crate) fn evaluate_value(
Ok(Some(result))
}
pub(crate) fn evaluate_table_row(
doc: &CadDocument,
insert: Handle,
columns: &[crate::scene::block_properties_table::Column],
row: usize,
) -> Result<Option<CadDocument>, &'static str> {
let def =
definition(doc, insert).ok_or("Select a uniformly scaled dynamic block reference.")?;
let view = crate::scene::block_properties_table::table(doc, def)?
.ok_or("The Block Properties Table is unavailable.")?;
let values = view
.rows
.get(row)
.ok_or("The selected table row is unavailable.")?;
let mut staged = doc.clone();
for (column, value) in columns.iter().zip(values) {
let p = all_parameters(&staged, def)
.into_iter()
.find(|p| p.handle == column.handle)
.ok_or("The table parameter is unavailable.")?;
let input = if column.connection_name.eq_ignore_ascii_case("UpdatedAngle") {
Value::Point(
p.base + DVec3::new(value.cos(), value.sin(), 0.0) * p.end.distance(p.base),
)
} else {
let axis = (p.end - p.base).normalize();
Value::Point(p.base + axis * *value)
};
if let Some(next) = evaluate_value(&staged, insert, p.handle, input)? {
staged = next;
}
}
write_table_history(&mut staged, insert, &view, row)?;
Ok(Some(staged))
}
fn write_table_history(
doc: &mut CadDocument,
insert: Handle,
view: &crate::scene::block_properties_table::TableView,
row: usize,
) -> Result<(), &'static str> {
let mut owner = doc.ensure_extension_dictionary(insert);
for key in ["AcDbBlockRepresentation", "AppDataCache"] {
owner = ensure_dictionary(doc, owner, key)?;
}
let def = doc
.dynamic_definition_for_insert(insert)
.ok_or("The block definition is unavailable.")?;
let table_eval = view.table.parameter.parameter.element.eval.node_id;
let mut entries = vec![
XRecordEntry::int16(1070, 3),
XRecordEntry::int32(1071, table_eval),
XRecordEntry::new(300, XRecordValue::String("Block Table1".into())),
XRecordEntry::int16(70, row as i16),
XRecordEntry::int16(1070, -1),
];
let params = all_parameters(doc, def);
for (column, value) in view.columns.iter().zip(
view.rows
.get(row)
.ok_or("The selected table row is unavailable.")?,
) {
let p = params
.iter()
.find(|p| p.handle == column.handle)
.ok_or("The table parameter is unavailable.")?;
entries.push(XRecordEntry::int32(1071, p.id));
entries.push(XRecordEntry::new(300, XRecordValue::String(p.name.clone())));
entries.push(XRecordEntry::double(40, *value));
}
entries.push(XRecordEntry::int16(1070, -2));
let h = if let Some(h) = entry(doc, owner, "ACAD_ENHANCEDBLOCKHISTORY")? {
h
} else {
doc.allocate_handle()
};
let mut rec = XRecord::named("ACAD_ENHANCEDBLOCKHISTORY");
rec.handle = h;
rec.owner = owner;
rec.reactors.push(owner);
rec.cloning_flags = codec::objects::DictionaryCloningFlags::KeepExisting;
rec.entries = entries;
doc.objects.insert(h, ObjectType::XRecord(rec));
if let Some(ObjectType::Dictionary(d)) = doc.objects.get_mut(&owner) {
if !d
.entries
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("ACAD_ENHANCEDBLOCKHISTORY"))
{
d.add_entry("ACAD_ENHANCEDBLOCKHISTORY", h);
}
}
Ok(())
}
/// Refresh existing evaluated references after an authoring change.
pub(crate) fn refresh(doc: &CadDocument, def: Handle) -> Result<CadDocument, &'static str> {
refresh_after_edit(doc, doc, def)