forked from 1b-agent/opencadstudio-upstream
Merge pull request #1188 from Ramox81/pedit-enhancements
This commit is contained in:
commit
7d28cc5256
4 files changed
+910
-226
No files matched your search
+216
-59
@@ -1303,6 +1303,211 @@ impl OpenCADStudio {
|
|||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_pedit_result(
|
||||||
|
&mut self,
|
||||||
|
tab: usize,
|
||||||
|
handle: Handle,
|
||||||
|
op: crate::modules::draw::modify::pedit::PeditOp,
|
||||||
|
) -> Task<Message> {
|
||||||
|
use crate::modules::draw::modify::pedit::{apply_pedit, convert_to_polyline, PeditOp};
|
||||||
|
|
||||||
|
if !matches!(&op, PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_))
|
||||||
|
&& self.reject_locked_edit(tab, handle)
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
match &op {
|
||||||
|
PeditOp::JoinSelection(handles) => {
|
||||||
|
let mut available = handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|handle| {
|
||||||
|
if self.tabs[tab].scene.is_layer_locked(*handle) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(*handle)
|
||||||
|
.cloned()
|
||||||
|
.map(|entity| (*handle, entity))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
while !available.is_empty() {
|
||||||
|
let (source_handle, source) = available.remove(0);
|
||||||
|
let candidates = available
|
||||||
|
.iter()
|
||||||
|
.map(|(handle, entity)| (*handle, entity))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if let Some((mut result, consumed)) =
|
||||||
|
crate::modules::draw::modify::join::join_to_source(&source, &candidates)
|
||||||
|
{
|
||||||
|
*result.common_mut() = source.common().clone();
|
||||||
|
available.retain(|(handle, _)| !consumed.contains(handle));
|
||||||
|
changes.push((source_handle, result, consumed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changes.is_empty() {
|
||||||
|
self.push_undo_snapshot(tab, "PEDIT");
|
||||||
|
for (source, replacement, consumed) in changes {
|
||||||
|
self.tabs[tab].scene.erase_entities(&consumed);
|
||||||
|
self.tabs[tab].scene.update_entity(replacement.clone());
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
for old in consumed {
|
||||||
|
command.on_entity_replaced(old, &[source]);
|
||||||
|
}
|
||||||
|
command.inject_picked_entity(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
command.on_pedit_applied();
|
||||||
|
}
|
||||||
|
self.tabs[tab].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PeditOp::Multiple(handles, operation) => {
|
||||||
|
let replacements: Vec<_> = handles
|
||||||
|
.iter()
|
||||||
|
.filter(|handle| !self.tabs[tab].scene.is_layer_locked(**handle))
|
||||||
|
.filter_map(|handle| {
|
||||||
|
let original = self.tabs[tab].scene.document.get_entity(*handle)?;
|
||||||
|
if matches!(operation.as_ref(), PeditOp::ConvertToPolyline) {
|
||||||
|
convert_to_polyline(original)
|
||||||
|
.map(|replacement| (*handle, replacement, true))
|
||||||
|
} else {
|
||||||
|
let mut replacement = original.clone();
|
||||||
|
apply_pedit(&mut replacement, operation)
|
||||||
|
.then_some((*handle, replacement, false))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if replacements.is_empty() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot(tab, "PEDIT");
|
||||||
|
for (handle, replacement, converted) in replacements {
|
||||||
|
let updated_handle = if converted {
|
||||||
|
self.tabs[tab].scene.erase_entities(&[handle]);
|
||||||
|
let new_handle = self.tabs[tab].scene.add_entity(replacement);
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
command.on_entity_replaced(handle, &[new_handle]);
|
||||||
|
}
|
||||||
|
new_handle
|
||||||
|
} else {
|
||||||
|
if let Some(entity) = self.tabs[tab].scene.document.get_entity_mut(handle) {
|
||||||
|
*entity = replacement;
|
||||||
|
}
|
||||||
|
self.tabs[tab].scene.refresh_fill_model(handle);
|
||||||
|
self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
|
handle
|
||||||
|
};
|
||||||
|
let updated = self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(updated_handle)
|
||||||
|
.cloned();
|
||||||
|
if let (Some(command), Some(entity)) =
|
||||||
|
(self.tabs[tab].active_cmd.as_mut(), updated)
|
||||||
|
{
|
||||||
|
command.inject_picked_entity(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
command.on_pedit_applied();
|
||||||
|
}
|
||||||
|
self.tabs[tab].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
|
PeditOp::VertexRange { first, last, split } => {
|
||||||
|
let pieces = self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.and_then(|entity| {
|
||||||
|
crate::modules::draw::modify::pedit::edit_vertex_range(
|
||||||
|
entity, *first, *last, *split,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Some(mut pieces) = pieces {
|
||||||
|
self.push_undo_snapshot(tab, "PEDIT");
|
||||||
|
let source = pieces.remove(0);
|
||||||
|
self.tabs[tab].scene.update_entity(source);
|
||||||
|
for mut piece in pieces {
|
||||||
|
piece.common_mut().handle = Handle::NULL;
|
||||||
|
self.tabs[tab].scene.add_entity(piece);
|
||||||
|
}
|
||||||
|
let updated = self.tabs[tab].scene.document.get_entity(handle).cloned();
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
if let Some(entity) = updated {
|
||||||
|
command.inject_picked_entity(entity);
|
||||||
|
}
|
||||||
|
command.on_pedit_applied();
|
||||||
|
}
|
||||||
|
self.tabs[tab].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
} else {
|
||||||
|
self.command_line.push_error("PEDIT: invalid vertex range.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PeditOp::ConvertToPolyline => {
|
||||||
|
let converted = self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.and_then(convert_to_polyline);
|
||||||
|
match converted {
|
||||||
|
Some(polyline) => {
|
||||||
|
return self.apply_cmd_result(CmdResult::ReplaceEntity(
|
||||||
|
handle,
|
||||||
|
vec![polyline],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => self.command_line.push_error(
|
||||||
|
crate::t!("PEDIT: cannot convert this entity.").as_ref(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.push_undo_snapshot(tab, "PEDIT");
|
||||||
|
let changed = self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(handle)
|
||||||
|
.map(|entity| apply_pedit(entity, &op))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if changed {
|
||||||
|
let updated = self.tabs[tab].scene.document.get_entity(handle).cloned();
|
||||||
|
if let Some(command) = self.tabs[tab].active_cmd.as_mut() {
|
||||||
|
if let Some(entity) = updated {
|
||||||
|
command.inject_picked_entity(entity);
|
||||||
|
}
|
||||||
|
command.on_pedit_applied();
|
||||||
|
}
|
||||||
|
self.tabs[tab].dirty = true;
|
||||||
|
self.tabs[tab].scene.refresh_fill_model(handle);
|
||||||
|
self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("PEDIT: applied.").as_ref());
|
||||||
|
self.refresh_properties();
|
||||||
|
} else {
|
||||||
|
self.discard_last_undo_entry(tab);
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("PEDIT: operation not applicable to this entity.").as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(prompt) = self.tabs[tab].active_cmd.as_ref().map(|command| command.prompt()) {
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if let Some(bind) = self.tabs[i].active_cmd.as_ref().and_then(|command| command.nested_copy_bind_setting()) {
|
if let Some(bind) = self.tabs[i].active_cmd.as_ref().and_then(|command| command.nested_copy_bind_setting()) {
|
||||||
@@ -2844,8 +3049,12 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.invalidate_dim_block_recorded(nh);
|
self.tabs[i].scene.invalidate_dim_block_recorded(nh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let pedit_entities = if self.tabs[i].active_cmd.as_ref().is_some_and(|command| command.name() == "PEDIT") {
|
||||||
|
new_handles.iter().filter_map(|new| self.tabs[i].scene.document.get_entity(*new).cloned()).collect::<Vec<_>>()
|
||||||
|
} else { Vec::new() };
|
||||||
if let Some(cmd) = &mut self.tabs[i].active_cmd {
|
if let Some(cmd) = &mut self.tabs[i].active_cmd {
|
||||||
cmd.on_entity_replaced(handle, &new_handles);
|
cmd.on_entity_replaced(handle, &new_handles);
|
||||||
|
for entity in pedit_entities { cmd.inject_picked_entity(entity); }
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
@@ -3736,65 +3945,7 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
CmdResult::PeditOp { handle, op } => {
|
CmdResult::PeditOp { handle, op } => return self.apply_pedit_result(i, handle, op),
|
||||||
if self.reject_locked_edit(i, handle) {
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
use crate::modules::draw::modify::pedit::{
|
|
||||||
apply_pedit, convert_to_polyline, PeditOp,
|
|
||||||
};
|
|
||||||
match &op {
|
|
||||||
// The convert replaces the entity (new handle).
|
|
||||||
PeditOp::ConvertToPolyline => {
|
|
||||||
let converted = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.document
|
|
||||||
.get_entity(handle)
|
|
||||||
.and_then(convert_to_polyline);
|
|
||||||
match converted {
|
|
||||||
Some(pl) => {
|
|
||||||
return self
|
|
||||||
.apply_cmd_result(CmdResult::ReplaceEntity(handle, vec![pl]));
|
|
||||||
}
|
|
||||||
None => self
|
|
||||||
.command_line
|
|
||||||
.push_error(crate::t!("PEDIT: cannot convert this entity.").as_ref()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Snapshot BEFORE the mutation — an after-the-fact
|
|
||||||
// snapshot records the changed state and undo no-ops.
|
|
||||||
self.push_undo_snapshot(i, "PEDIT");
|
|
||||||
let changed = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.document
|
|
||||||
.get_entity_mut(handle)
|
|
||||||
.map(|e| apply_pedit(e, &op))
|
|
||||||
.unwrap_or(false);
|
|
||||||
if changed {
|
|
||||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
|
||||||
command.on_pedit_applied();
|
|
||||||
}
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
// Repaint immediately (wide-band fill included).
|
|
||||||
self.tabs[i].scene.refresh_fill_model(handle);
|
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
|
||||||
self.command_line.push_output(crate::t!("PEDIT: applied.").as_ref());
|
|
||||||
self.refresh_properties();
|
|
||||||
} else {
|
|
||||||
self.discard_last_undo_entry(i);
|
|
||||||
self.command_line
|
|
||||||
.push_error(crate::t!("PEDIT: operation not applicable to this entity.").as_ref());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|
||||||
if let Some(p) = prompt {
|
|
||||||
self.command_line.push_info(&p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CmdResult::JoinToSource { source, handles } => {
|
CmdResult::JoinToSource { source, handles } => {
|
||||||
if self.reject_locked_edit(i, source) { return Task::none(); }
|
if self.reject_locked_edit(i, source) { return Task::none(); }
|
||||||
let joined = self.tabs[i].scene.document.get_entity(source).and_then(|entity| {
|
let joined = self.tabs[i].scene.document.get_entity(source).and_then(|entity| {
|
||||||
@@ -5301,6 +5452,12 @@ impl OpenCADStudio {
|
|||||||
let active = self.tabs[i].active_cmd.take();
|
let active = self.tabs[i].active_cmd.take();
|
||||||
self.undo_active_tab();
|
self.undo_active_tab();
|
||||||
self.tabs[i].active_cmd = active;
|
self.tabs[i].active_cmd = active;
|
||||||
|
if self.tabs[i].active_cmd.as_ref().is_some_and(|command| command.name() == "PEDIT") {
|
||||||
|
let entities: Vec<_> = self.tabs[i].scene.document.entities().cloned().collect();
|
||||||
|
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||||
|
for entity in entities { command.inject_picked_entity(entity); }
|
||||||
|
}
|
||||||
|
}
|
||||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
if let Some(p) = prompt {
|
if let Some(p) = prompt {
|
||||||
self.command_line.push_info(&p);
|
self.command_line.push_info(&p);
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ impl OpenCADStudio {
|
|||||||
header.surface_u_density,
|
header.surface_u_density,
|
||||||
header.surface_v_density,
|
header.surface_v_density,
|
||||||
)
|
)
|
||||||
|
.with_entities(self.tabs[i].scene.document.entities().cloned())
|
||||||
.with_preselection(&preselected);
|
.with_preselection(&preselected);
|
||||||
self.command_line.push_info(&cmd_obj.prompt());
|
self.command_line.push_info(&cmd_obj.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||||
|
|||||||
+670
-157
File diff suppressed because it is too large.
Load diff
@@ -20,10 +20,10 @@
|
|||||||
// Any other entity type is left untouched: the command returns
|
// Any other entity type is left untouched: the command returns
|
||||||
// `CmdResult::NeedPoint` and keeps prompting so nothing is corrupted.
|
// `CmdResult::NeedPoint` and keeps prompting so nothing is corrupted.
|
||||||
|
|
||||||
|
use crate::t;
|
||||||
use acadrust::entities::Spline;
|
use acadrust::entities::Spline;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
@@ -62,9 +62,7 @@ impl ReverseCommand {
|
|||||||
Some(EntityType::Line(out))
|
Some(EntityType::Line(out))
|
||||||
}
|
}
|
||||||
EntityType::LwPolyline(pl) => Some(EntityType::LwPolyline(reverse_lwpolyline(pl))),
|
EntityType::LwPolyline(pl) => Some(EntityType::LwPolyline(reverse_lwpolyline(pl))),
|
||||||
EntityType::Polyline2D(pl) => {
|
EntityType::Polyline2D(pl) => Some(EntityType::Polyline2D(reverse_polyline2d(pl))),
|
||||||
Some(EntityType::Polyline2D(reverse_polyline2d(pl)))
|
|
||||||
}
|
|
||||||
EntityType::Polyline(pl) => {
|
EntityType::Polyline(pl) => {
|
||||||
let mut out = pl.clone();
|
let mut out = pl.clone();
|
||||||
out.vertices.reverse();
|
out.vertices.reverse();
|
||||||
@@ -76,7 +74,9 @@ impl ReverseCommand {
|
|||||||
Some(EntityType::Polyline3D(out))
|
Some(EntityType::Polyline3D(out))
|
||||||
}
|
}
|
||||||
EntityType::Spline(sp) => Some(EntityType::Spline(reverse_spline(sp))),
|
EntityType::Spline(sp) => Some(EntityType::Spline(reverse_spline(sp))),
|
||||||
EntityType::Helix(helix) => crate::entities::helix::reversed(helix).map(EntityType::Helix),
|
EntityType::Helix(helix) => {
|
||||||
|
crate::entities::helix::reversed(helix).map(EntityType::Helix)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,8 +154,8 @@ fn reverse_polyline2d(pl: &acadrust::entities::Polyline2D) -> acadrust::entities
|
|||||||
for vertex in &mut out.vertices {
|
for vertex in &mut out.vertices {
|
||||||
let tangent_flag = acadrust::entities::VertexFlags::CURVE_FIT_TANGENT.bits();
|
let tangent_flag = acadrust::entities::VertexFlags::CURVE_FIT_TANGENT.bits();
|
||||||
if vertex.flags.bits() & tangent_flag != 0 {
|
if vertex.flags.bits() & tangent_flag != 0 {
|
||||||
vertex.curve_tangent = (vertex.curve_tangent + std::f64::consts::PI)
|
vertex.curve_tangent =
|
||||||
.rem_euclid(std::f64::consts::TAU);
|
(vertex.curve_tangent + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pl.is_closed() {
|
if pl.is_closed() {
|
||||||
@@ -189,8 +189,13 @@ pub(super) fn reverse_spline(sp: &Spline) -> Spline {
|
|||||||
if let Some(curve) = super::spline_ops::spline_to_nurbs(sp) {
|
if let Some(curve) = super::spline_ops::spline_to_nurbs(sp) {
|
||||||
out.knots = curve.reversed().knots().to_vec();
|
out.knots = curve.reversed().knots().to_vec();
|
||||||
}
|
}
|
||||||
out.begin_tangent = acadrust::types::Vector3::new(-sp.end_tangent.x, -sp.end_tangent.y, -sp.end_tangent.z);
|
out.begin_tangent =
|
||||||
out.end_tangent = acadrust::types::Vector3::new(-sp.begin_tangent.x, -sp.begin_tangent.y, -sp.begin_tangent.z);
|
acadrust::types::Vector3::new(-sp.end_tangent.x, -sp.end_tangent.y, -sp.end_tangent.z);
|
||||||
|
out.end_tangent = acadrust::types::Vector3::new(
|
||||||
|
-sp.begin_tangent.x,
|
||||||
|
-sp.begin_tangent.y,
|
||||||
|
-sp.begin_tangent.z,
|
||||||
|
);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +270,15 @@ mod tests {
|
|||||||
spline.end_tangent = Vector3::new(4.0, 5.0, 6.0);
|
spline.end_tangent = Vector3::new(4.0, 5.0, 6.0);
|
||||||
|
|
||||||
let reversed = reverse_spline(&spline);
|
let reversed = reverse_spline(&spline);
|
||||||
assert_eq!(reversed.control_points, spline.control_points.iter().rev().cloned().collect::<Vec<_>>());
|
assert_eq!(
|
||||||
|
reversed.control_points,
|
||||||
|
spline
|
||||||
|
.control_points
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
assert_eq!(reversed.weights, vec![4.0, 3.0, 2.0, 1.0]);
|
assert_eq!(reversed.weights, vec![4.0, 3.0, 2.0, 1.0]);
|
||||||
assert_eq!(reversed.knots, vec![0.0, 0.0, 0.0, 0.75, 1.0, 1.0, 1.0]);
|
assert_eq!(reversed.knots, vec![0.0, 0.0, 0.0, 0.75, 1.0, 1.0, 1.0]);
|
||||||
assert_eq!(reversed.begin_tangent, Vector3::new(-4.0, -5.0, -6.0));
|
assert_eq!(reversed.begin_tangent, Vector3::new(-4.0, -5.0, -6.0));
|
||||||
|
|||||||
Reference in new issue
Block a user