forked from 1b-agent/opencadstudio-upstream
589 lines
23 KiB
Rust
589 lines
23 KiB
Rust
//! REFEDIT, -REFEDIT, REFSET and REFCLOSE: in-place editing of a block
|
|
//! reference or an external reference, with the reference's prompts and
|
|
//! messages. The session itself lives in `DocumentTab::refedit_session`.
|
|
|
|
use crate::app::{Message, OpenCADStudio};
|
|
use crate::modules::draw::modify::refedit::{
|
|
apply_insert_inverse_transform, apply_insert_transform, level_hits,
|
|
nesting_chain, RefCloseCommand, RefEditOptionsCommand, RefEditPickCommand, RefEditSession,
|
|
RefSetCommand, NOT_CHECKED_OUT,
|
|
};
|
|
use codec::{EntityType, Handle};
|
|
use iced::Task;
|
|
use std::collections::HashSet;
|
|
|
|
/// `** Command not allowed, NAME already checked out for editing **`
|
|
fn already_checked_out(name: &str) -> String {
|
|
crate::tf!("** Command not allowed, {} already checked out for editing **", name).into_owned()
|
|
}
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn dispatch_refedit(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
|
match cmd {
|
|
"REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")),
|
|
"REFSET" => {
|
|
if self.tabs[i].refedit_session.is_none() {
|
|
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
|
return Some(Task::none());
|
|
}
|
|
self.command_line.push_info(
|
|
crate::t!("Transfer objects between the RefEdit working set and host drawing...")
|
|
.as_ref(),
|
|
);
|
|
let command = RefSetCommand::new();
|
|
use crate::command::CadCommand;
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
Some(Task::none())
|
|
}
|
|
"REFCLOSE" => {
|
|
if self.tabs[i].refedit_session.is_none() {
|
|
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
|
return Some(Task::none());
|
|
}
|
|
let command = RefCloseCommand::new();
|
|
use crate::command::CadCommand;
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
Some(Task::none())
|
|
}
|
|
"REFCLOSE_SAVE" => {
|
|
self.close_refedit(i, true);
|
|
Some(Task::none())
|
|
}
|
|
"REFCLOSE_DISCARD" => {
|
|
self.close_refedit(i, false);
|
|
Some(Task::none())
|
|
}
|
|
_ if cmd.starts_with("REFEDIT_PICKED:") => {
|
|
self.refedit_picked(i, &cmd["REFEDIT_PICKED:".len()..]);
|
|
Some(Task::none())
|
|
}
|
|
_ if cmd.starts_with("REFEDIT_BEGIN:") => {
|
|
self.begin_refedit(i, &cmd["REFEDIT_BEGIN:".len()..]);
|
|
Some(Task::none())
|
|
}
|
|
_ if cmd.starts_with("REFSET_APPLY:") => {
|
|
self.apply_refset(i, cmd.ends_with("ADD"));
|
|
Some(Task::none())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Block references and xrefs of the current space.
|
|
fn refedit_references(&self, i: usize) -> HashSet<Handle> {
|
|
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| matches!(e, EntityType::Insert(_)) && e.common().owner_handle == space)
|
|
.map(|e| e.common().handle)
|
|
.collect()
|
|
}
|
|
|
|
fn start_refedit(&mut self, i: usize, command_line: bool) -> Task<Message> {
|
|
if let Some(session) = &self.tabs[i].refedit_session {
|
|
let message = already_checked_out(&session.block_name);
|
|
self.command_line.push_error(&message);
|
|
return Task::none();
|
|
}
|
|
let references = self.refedit_references(i);
|
|
// A single preselected reference is taken as the pick.
|
|
let selected: Vec<Handle> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if let [handle] = selected[..] {
|
|
if references.contains(&handle) {
|
|
self.tabs[i].scene.deselect_all();
|
|
self.refedit_picked(
|
|
i,
|
|
&format!("{}:{}", command_line as u8, handle.value()),
|
|
);
|
|
return Task::none();
|
|
}
|
|
}
|
|
use crate::command::CadCommand;
|
|
let command = RefEditPickCommand::new(command_line, references);
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
Task::none()
|
|
}
|
|
|
|
/// `cli:handle[:x:y:z]` — the reference was picked (at the point).
|
|
fn refedit_picked(&mut self, i: usize, args: &str) {
|
|
let parts: Vec<&str> = args.split(':').collect();
|
|
let command_line = parts.first() == Some(&"1");
|
|
let Some(top) = parts.get(1).and_then(|v| v.parse::<u64>().ok()).map(Handle::new) else {
|
|
return;
|
|
};
|
|
let point = (parts.len() >= 5)
|
|
.then(|| {
|
|
Some(glam::DVec3::new(
|
|
parts[2].parse().ok()?,
|
|
parts[3].parse().ok()?,
|
|
parts[4].parse().ok()?,
|
|
))
|
|
})
|
|
.flatten();
|
|
self.tabs[i].scene.deselect_all();
|
|
let chain = nesting_chain(&self.tabs[i].scene.document, top, point);
|
|
if chain.is_empty() {
|
|
self.command_line
|
|
.push_error(crate::t!("Object is not a valid reference").as_ref());
|
|
return;
|
|
}
|
|
if command_line {
|
|
let hits = chain
|
|
.iter()
|
|
.map(|level| level_hits(&self.tabs[i].scene.document, level))
|
|
.collect();
|
|
let command = RefEditOptionsCommand::new(top, chain, hits);
|
|
use crate::command::CadCommand;
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
} else {
|
|
// The dialog's defaults: the innermost reference, all objects,
|
|
// attribute definitions not displayed.
|
|
let path: Vec<String> = chain[1..]
|
|
.iter()
|
|
.map(|l| l.insert.value().to_string())
|
|
.collect();
|
|
let _ = self.dispatch_command(&format!(
|
|
"REFEDIT_BEGIN:{}:{}:0:",
|
|
top.value(),
|
|
path.join(",")
|
|
));
|
|
}
|
|
}
|
|
|
|
/// `top[:path[:attributes[:nested]]]` — open the session.
|
|
fn begin_refedit(&mut self, i: usize, args: &str) {
|
|
let mut parts = args.split(':');
|
|
let Some(top) = parts
|
|
.next()
|
|
.and_then(|v| v.parse::<u64>().ok())
|
|
.map(Handle::new)
|
|
else {
|
|
return;
|
|
};
|
|
let handles = |s: Option<&str>| -> Vec<Handle> {
|
|
s.unwrap_or("")
|
|
.split(',')
|
|
.filter_map(|v| v.parse::<u64>().ok())
|
|
.map(Handle::new)
|
|
.collect()
|
|
};
|
|
let path = handles(parts.next());
|
|
let attributes = parts.next() == Some("1");
|
|
let nested = handles(parts.next());
|
|
if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) {
|
|
return;
|
|
}
|
|
let document = &self.tabs[i].scene.document;
|
|
let mut chain = nesting_chain(document, top, None);
|
|
if chain.is_empty() {
|
|
self.command_line
|
|
.push_error(crate::t!("Object is not a valid reference").as_ref());
|
|
return;
|
|
}
|
|
// Follow the stored path of definition-owned references.
|
|
for handle in path {
|
|
let Some(EntityType::Insert(inner)) = document.get_entity(handle) else {
|
|
break;
|
|
};
|
|
let base = crate::scene::render_graph::block_base_point(document, &inner.block_name);
|
|
let local = crate::scene::render_graph::insert_transform_with_base(inner, base);
|
|
let outer = chain.last().expect("non-empty chain").forward.clone();
|
|
chain.push(crate::modules::draw::modify::refedit::ChainLevel {
|
|
insert: handle,
|
|
block_name: inner.block_name.clone(),
|
|
forward: local.then(&outer),
|
|
});
|
|
}
|
|
let level = chain.last().expect("non-empty chain").clone();
|
|
let Some(br) = document.block_records.get(&level.block_name) else {
|
|
return;
|
|
};
|
|
let br_handle = br.handle;
|
|
let xref_path = (br.flags.is_xref || br.flags.is_xref_overlay)
|
|
.then(|| br.xref_path.clone())
|
|
.filter(|p| !p.is_empty());
|
|
let extracted: Vec<Handle> = br
|
|
.entity_handles
|
|
.iter()
|
|
.copied()
|
|
.filter(|h| match document.get_entity(*h) {
|
|
Some(EntityType::Block(_) | EntityType::BlockEnd(_)) | None => false,
|
|
Some(EntityType::AttributeDefinition(_)) => attributes,
|
|
Some(_) => nested.is_empty() || nested.contains(h),
|
|
})
|
|
.collect();
|
|
let Some(inverse) =
|
|
crate::modules::draw::modify::refedit::inverse_placement(&level.forward)
|
|
else {
|
|
return;
|
|
};
|
|
let session = RefEditSession {
|
|
block_name: level.block_name.clone(),
|
|
br_handle,
|
|
temp_handles: Vec::new(),
|
|
extracted: extracted.clone(),
|
|
added: HashSet::new(),
|
|
removed: HashSet::new(),
|
|
forward: level.forward.clone(),
|
|
inverse,
|
|
// Everything allocated from here on is part of the session.
|
|
handle_watermark: document.next_handle().max(document.header.handle_seed),
|
|
xref_path,
|
|
};
|
|
let copies: Vec<EntityType> = extracted
|
|
.iter()
|
|
.filter_map(|h| document.get_entity(*h).cloned())
|
|
.collect();
|
|
|
|
self.push_undo_snapshot(i, "REFEDIT");
|
|
let mut temp_handles = Vec::new();
|
|
for mut entity in copies {
|
|
apply_insert_transform(&mut entity, &session);
|
|
entity.common_mut().handle = Handle::NULL;
|
|
entity.common_mut().owner_handle = Handle::NULL;
|
|
temp_handles.push(self.tabs[i].scene.add_entity(entity));
|
|
}
|
|
let mut session = session;
|
|
session.temp_handles = temp_handles.clone();
|
|
self.tabs[i].refedit_session = Some(session);
|
|
self.tabs[i].refedit_closed.clear();
|
|
// Fade everything outside the working set by XFADECTL. (#136)
|
|
self.tabs[i]
|
|
.scene
|
|
.set_refedit_keep(Some(temp_handles.iter().copied().collect::<rustc_hash::FxHashSet<_>>()));
|
|
self.tabs[i].scene.refedit_lock = true;
|
|
self.tabs[i].scene.deselect_all();
|
|
self.tabs[i].dirty = true;
|
|
// No active command — the definition's objects are edited freely
|
|
// (move, grips, draw, erase…) until REFCLOSE.
|
|
self.tabs[i].active_cmd = None;
|
|
}
|
|
|
|
/// REFSET Add / Remove on the current selection.
|
|
fn apply_refset(&mut self, i: usize, add: bool) {
|
|
let Some(mut session) = self.tabs[i].refedit_session.take() else {
|
|
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
|
return;
|
|
};
|
|
let picked: Vec<Handle> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
let mut moved = 0usize;
|
|
for h in picked {
|
|
let inside = session.in_working_set(h);
|
|
if add && !inside {
|
|
session.removed.remove(&h);
|
|
if !session.in_working_set(h) {
|
|
session.added.insert(h);
|
|
}
|
|
moved += 1;
|
|
} else if !add && inside {
|
|
session.added.remove(&h);
|
|
if session.in_working_set(h) {
|
|
session.removed.insert(h);
|
|
}
|
|
moved += 1;
|
|
}
|
|
}
|
|
let keep: rustc_hash::FxHashSet<Handle> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|e| e.common().handle)
|
|
.filter(|h| session.in_working_set(*h))
|
|
.collect();
|
|
self.tabs[i].refedit_session = Some(session);
|
|
self.tabs[i].scene.set_refedit_keep(Some(keep));
|
|
self.tabs[i].scene.deselect_all();
|
|
self.command_line.push_output(&if add {
|
|
crate::tf!("{} Added to working set", moved).into_owned()
|
|
} else {
|
|
crate::tf!("{} Removed from working set", moved).into_owned()
|
|
});
|
|
}
|
|
|
|
/// REFCLOSE Save (`save`) or Discard.
|
|
fn close_refedit(&mut self, i: usize, save: bool) {
|
|
let Some(session) = self.tabs[i].refedit_session.take() else {
|
|
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
|
return;
|
|
};
|
|
self.push_undo_snapshot(i, "REFCLOSE");
|
|
self.command_line
|
|
.push_output(crate::t!("Regenerating model.").as_ref());
|
|
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
|
// The working set: the copies still in it, everything created in this
|
|
// space during the session (#423) and the host objects added with
|
|
// REFSET — less what REFSET removed.
|
|
let created: Vec<Handle> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| {
|
|
let c = e.common();
|
|
c.owner_handle == space && c.handle.value() >= session.handle_watermark
|
|
})
|
|
.map(|e| e.common().handle)
|
|
.collect();
|
|
let working: Vec<Handle> = created
|
|
.iter()
|
|
.chain(session.added.iter())
|
|
.copied()
|
|
.filter(|h| session.in_working_set(*h))
|
|
.collect::<HashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
if !save {
|
|
// The copies and new objects go; host objects added with REFSET
|
|
// stay where they are, and copies moved out with REFSET Remove
|
|
// stay in the host.
|
|
let drop: Vec<Handle> = working
|
|
.iter()
|
|
.copied()
|
|
.filter(|h| !session.added.contains(h))
|
|
.collect();
|
|
self.tabs[i].scene.erase_entities(&drop);
|
|
self.finish_refedit_close(i, session);
|
|
return;
|
|
}
|
|
|
|
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
|
let originals: HashSet<Handle> = session.temp_handles.iter().copied().collect();
|
|
let added_count = working.iter().filter(|h| !originals.contains(h)).count();
|
|
let removed_count = session.extracted.len()
|
|
- working.iter().filter(|h| originals.contains(h)).count();
|
|
let edited: Vec<EntityType> = working
|
|
.iter()
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
.map(|mut entity| {
|
|
apply_insert_inverse_transform(&mut entity, &session);
|
|
let mut entity = normalize_entity_for_block(entity);
|
|
entity.common_mut().handle = Handle::NULL;
|
|
entity.common_mut().owner_handle = session.br_handle;
|
|
entity
|
|
})
|
|
.collect();
|
|
self.tabs[i].scene.erase_entities(&working);
|
|
// The extracted definition objects are replaced; the rest stay.
|
|
for h in &session.extracted {
|
|
self.tabs[i].scene.document.remove_entity(*h);
|
|
}
|
|
if let Some(br) = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get_mut(&session.block_name)
|
|
{
|
|
let extracted: HashSet<Handle> = session.extracted.iter().copied().collect();
|
|
br.entity_handles.retain(|h| !extracted.contains(h));
|
|
}
|
|
for entity in edited.iter().cloned() {
|
|
let _ = self.tabs[i].scene.document.add_entity(entity);
|
|
}
|
|
let instances = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| {
|
|
matches!(e, EntityType::Insert(ins)
|
|
if ins.block_name.eq_ignore_ascii_case(&session.block_name))
|
|
})
|
|
.count();
|
|
let name = session.block_name.clone();
|
|
match added_count {
|
|
0 => {}
|
|
1 => self
|
|
.command_line
|
|
.push_output(crate::tf!("1 object added to {}", name).as_ref()),
|
|
n => self
|
|
.command_line
|
|
.push_output(crate::tf!("{} objects added to {}", n, name).as_ref()),
|
|
}
|
|
match removed_count {
|
|
0 => {}
|
|
1 => self
|
|
.command_line
|
|
.push_output(crate::tf!("1 object removed from {}", name).as_ref()),
|
|
n => self
|
|
.command_line
|
|
.push_output(crate::tf!("{} objects removed from {}", n, name).as_ref()),
|
|
}
|
|
let reloaded = match &session.xref_path {
|
|
Some(raw) => match self.write_back_xref(i, &session, raw, edited) {
|
|
Ok(()) => true,
|
|
Err(error) => {
|
|
self.command_line.push_error(&error);
|
|
false
|
|
}
|
|
},
|
|
None => false,
|
|
};
|
|
let updated = match (session.xref_path.is_some(), instances) {
|
|
(true, 1) => crate::t!("1 xref instance updated").into_owned(),
|
|
(true, n) => crate::tf!("{} xref instances updated", n).into_owned(),
|
|
(false, 1) => crate::t!("1 block instance updated").into_owned(),
|
|
(false, n) => crate::tf!("{} block instances updated", n).into_owned(),
|
|
};
|
|
self.command_line.push_output(&updated);
|
|
self.command_line.push_output(&if reloaded {
|
|
crate::tf!("{} redefined and reloaded.", name).into_owned()
|
|
} else {
|
|
crate::tf!("{} redefined.", name).into_owned()
|
|
});
|
|
self.finish_refedit_close(i, session);
|
|
// Rebuild hatch/image/mesh caches since block content changed.
|
|
self.tabs[i].scene.rebuild_derived_caches();
|
|
}
|
|
|
|
fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) {
|
|
self.tabs[i].refedit_closed.push(session);
|
|
self.tabs[i].scene.refedit_lock = false;
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
self.tabs[i].scene.deselect_all();
|
|
self.tabs[i].dirty = true;
|
|
}
|
|
|
|
/// Write the edited definition into the referenced drawing and reload it.
|
|
fn write_back_xref(
|
|
&mut self,
|
|
i: usize,
|
|
session: &RefEditSession,
|
|
raw: &str,
|
|
edited: Vec<EntityType>,
|
|
) -> Result<(), String> {
|
|
let base_dir = self.tabs[i]
|
|
.current_path
|
|
.as_ref()
|
|
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
|
|
.unwrap_or_default();
|
|
let path = crate::io::xref::resolve_path(raw, &base_dir)
|
|
.ok_or_else(|| format!("\"{raw}\" cannot be found."))?;
|
|
let mut source = crate::io::load_file(&path)?;
|
|
let prefix = format!("{}|", session.block_name);
|
|
let strip = |name: &str| -> String {
|
|
if name.len() > prefix.len()
|
|
&& name.is_char_boundary(prefix.len())
|
|
&& name[..prefix.len()].eq_ignore_ascii_case(&prefix)
|
|
{
|
|
name[prefix.len()..].to_string()
|
|
} else {
|
|
name.to_string()
|
|
}
|
|
};
|
|
// Dependencies come from the host (where they carry the xref prefix)
|
|
// and go back under their own names.
|
|
let mut deps = crate::app::ClipboardDeps::capture(&self.tabs[i].scene.document, &edited);
|
|
{
|
|
use codec::TableEntry;
|
|
for layer in &mut deps.layers {
|
|
let name = strip(layer.name());
|
|
layer.name = name;
|
|
}
|
|
for linetype in &mut deps.linetypes {
|
|
let name = strip(linetype.name());
|
|
linetype.name = name;
|
|
}
|
|
for style in &mut deps.text_styles {
|
|
let name = strip(style.name());
|
|
style.name = name;
|
|
}
|
|
}
|
|
for block in &mut deps.blocks {
|
|
block.name = strip(&block.name);
|
|
}
|
|
let entities: Vec<EntityType> = edited
|
|
.into_iter()
|
|
.map(|mut entity| {
|
|
let common = entity.common_mut();
|
|
common.layer = strip(&common.layer);
|
|
common.linetype = strip(&common.linetype);
|
|
common.layer_handle = None;
|
|
common.linetype_handle = None;
|
|
common.owner_handle = Handle::NULL;
|
|
match &mut entity {
|
|
EntityType::Insert(insert) => insert.block_name = strip(&insert.block_name),
|
|
EntityType::Text(text) => text.style = strip(&text.style),
|
|
EntityType::MText(text) => text.style = strip(&text.style),
|
|
_ => {}
|
|
}
|
|
entity
|
|
})
|
|
.collect();
|
|
let model: Vec<Handle> = source
|
|
.model_space_entities()
|
|
.map(|e| e.common().handle)
|
|
.collect();
|
|
for h in model {
|
|
source.remove_entity(h);
|
|
}
|
|
crate::app::command_driver::clipboard::paste_entities_kernel(
|
|
&mut source,
|
|
&entities,
|
|
&deps,
|
|
None,
|
|
crate::app::command_driver::clipboard::PasteSpace::Model,
|
|
);
|
|
crate::io::save(&source, &path)?;
|
|
let keys: rustc_hash::FxHashSet<Handle> = [session.br_handle].into_iter().collect();
|
|
self.tabs[i].xref_stat_cache.0.remove(&session.br_handle.value());
|
|
let _ = crate::io::xref::resolve_xrefs_for_keys(
|
|
&mut self.tabs[i].scene.document,
|
|
&base_dir,
|
|
&keys,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Undo of REFCLOSE checks the reference out again; undo of REFEDIT ends
|
|
/// the session (their redo does the reverse).
|
|
pub(in crate::app) fn refedit_after_history_step(&mut self, i: usize, label: &str, undo: bool) {
|
|
match (label, undo) {
|
|
("REFCLOSE", true) | ("REFEDIT", false) => {
|
|
let reopened = if undo {
|
|
self.tabs[i].refedit_closed.pop()
|
|
} else {
|
|
self.tabs[i].refedit_reopen.pop()
|
|
};
|
|
if let Some(session) = reopened {
|
|
let keep: rustc_hash::FxHashSet<Handle> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|e| e.common().handle)
|
|
.filter(|h| session.in_working_set(*h))
|
|
.collect();
|
|
self.tabs[i].refedit_session = Some(session);
|
|
self.tabs[i].scene.refedit_lock = true;
|
|
self.tabs[i].scene.set_refedit_keep(Some(keep));
|
|
}
|
|
}
|
|
("REFCLOSE", false) | ("REFEDIT", true) => {
|
|
if let Some(session) = self.tabs[i].refedit_session.take() {
|
|
if undo {
|
|
self.tabs[i].refedit_reopen.push(session);
|
|
} else {
|
|
self.tabs[i].refedit_closed.push(session);
|
|
}
|
|
self.tabs[i].scene.refedit_lock = false;
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|