forked from 1b-agent/opencadstudio-upstream
fix(render): share repeated block meshes
Keep placement transforms and bounds separate from immutable mesh data to prevent large drawings from exhausting memory or stalling selection.
This commit is contained in:
1 parent
5e773fbcce
commit
7b4af0aa75
6 files changed
+408
-399
No files matched your search
+116
-220
@@ -89,6 +89,9 @@ fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64
|
||||
}
|
||||
|
||||
fn mesh_interaction_aabb(set: &model::mesh_model::MeshLodSet) -> Option<[f64; 6]> {
|
||||
if let Some(aabb) = set.instance_aabb {
|
||||
return Some(aabb);
|
||||
}
|
||||
let mut aabb = [
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
@@ -1213,95 +1216,63 @@ fn offset_mesh_lod_set(mut set: MeshLodSet) -> MeshLodSet {
|
||||
set
|
||||
}
|
||||
|
||||
/// Instance a block-local mesh into the render frame: apply the accumulated
|
||||
/// INSERT transform (block-local → world/DXF) then subtract world_offset, so a
|
||||
/// block scaled at the INSERT renders at the right size. Normals are rotated by
|
||||
/// the transform's linear part and re-normalized. (#123)
|
||||
/// Build a lightweight placement over immutable block-local geometry.
|
||||
fn transform_block_mesh_lod_set(
|
||||
set: &MeshLodSet,
|
||||
xform: &acadrust::types::Transform,
|
||||
) -> MeshLodSet {
|
||||
use acadrust::types::Vector3;
|
||||
let mut out = set.clone();
|
||||
out.instance_transform = Some(*xform);
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
for lod in &mut out.lods {
|
||||
let has_low = lod.verts_low.len() == lod.verts.len();
|
||||
if !has_low {
|
||||
lod.verts_low = vec![[0.0; 3]; lod.verts.len()];
|
||||
}
|
||||
for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) {
|
||||
// Reconstruct the block-local f64, apply the INSERT transform and
|
||||
// subtract world_offset in f64, then re-split into (high, low).
|
||||
let w = xform.apply(Vector3::new(
|
||||
v[0] as f64 + vl[0] as f64,
|
||||
v[1] as f64 + vl[1] as f64,
|
||||
v[2] as f64 + vl[2] as f64,
|
||||
));
|
||||
let ax = w.x;
|
||||
let ay = w.y;
|
||||
let az = w.z;
|
||||
let hx = ax as f32;
|
||||
let hy = ay as f32;
|
||||
let hz = az as f32;
|
||||
*v = [hx, hy, hz];
|
||||
*vl = [
|
||||
(ax - hx as f64) as f32,
|
||||
(ay - hy as f64) as f32,
|
||||
(az - hz as f64) as f32,
|
||||
];
|
||||
if hx < min_x {
|
||||
min_x = hx;
|
||||
}
|
||||
if hy < min_y {
|
||||
min_y = hy;
|
||||
}
|
||||
if hx > max_x {
|
||||
max_x = hx;
|
||||
}
|
||||
if hy > max_y {
|
||||
max_y = hy;
|
||||
}
|
||||
}
|
||||
for n in &mut lod.normals {
|
||||
let d = xform.apply_rotation(Vector3::new(n[0] as f64, n[1] as f64, n[2] as f64));
|
||||
let len = (d.x * d.x + d.y * d.y + d.z * d.z).sqrt();
|
||||
if len > 1e-12 {
|
||||
n[0] = (d.x / len) as f32;
|
||||
n[1] = (d.y / len) as f32;
|
||||
n[2] = (d.z / len) as f32;
|
||||
let source = set.instance_source.clone().unwrap_or_else(|| {
|
||||
std::sync::Arc::new(model::mesh_model::MeshInstanceSource {
|
||||
handle: set.entity_handle().unwrap_or(Handle::new(0)),
|
||||
lods: set.lods.clone(),
|
||||
edge_verts: set.edge_verts.clone(),
|
||||
edge_verts_low: set.edge_verts_low.clone(),
|
||||
curved_gens: set.curved_gens.clone(),
|
||||
})
|
||||
});
|
||||
let mut bounds = [
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
];
|
||||
for x in [set.world_aabb[0], set.world_aabb[2]] {
|
||||
for y in [set.world_aabb[1], set.world_aabb[3]] {
|
||||
for z in [set.z_aabb[0], set.z_aabb[1]] {
|
||||
let point = xform.apply(Vector3::new(x as f64, y as f64, z as f64));
|
||||
for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
|
||||
bounds[axis] = bounds[axis].min(value);
|
||||
bounds[axis + 3] = bounds[axis + 3].max(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply the same INSERT transform to the feature edges.
|
||||
{
|
||||
let n = out.edge_verts.len();
|
||||
if out.edge_verts_low.len() != n {
|
||||
out.edge_verts_low = vec![[0.0; 3]; n];
|
||||
}
|
||||
for (v, vl) in out.edge_verts.iter_mut().zip(out.edge_verts_low.iter_mut()) {
|
||||
let w = xform.apply(Vector3::new(
|
||||
v[0] as f64 + vl[0] as f64,
|
||||
v[1] as f64 + vl[1] as f64,
|
||||
v[2] as f64 + vl[2] as f64,
|
||||
));
|
||||
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32);
|
||||
*v = [hx, hy, hz];
|
||||
*vl = [
|
||||
(w.x - hx as f64) as f32,
|
||||
(w.y - hy as f64) as f32,
|
||||
(w.z - hz as f64) as f32,
|
||||
];
|
||||
}
|
||||
MeshLodSet {
|
||||
lods: Vec::new(),
|
||||
material: set.material.clone(),
|
||||
face_materials: set.face_materials.clone(),
|
||||
visual_style: set.visual_style.clone(),
|
||||
complete: set.complete,
|
||||
edge_verts: Vec::new(),
|
||||
edge_verts_low: Vec::new(),
|
||||
curved_gens: Vec::new(),
|
||||
metrics: set.metrics,
|
||||
world_aabb: [
|
||||
bounds[0] as f32,
|
||||
bounds[1] as f32,
|
||||
bounds[3] as f32,
|
||||
bounds[4] as f32,
|
||||
],
|
||||
z_aabb: [bounds[2] as f32, bounds[5] as f32],
|
||||
instance_source: Some(source),
|
||||
instance_transform: Some(*xform),
|
||||
instance_handle: None,
|
||||
instance_color: set.display_color(),
|
||||
instance_aabb: bounds.iter().all(|value| value.is_finite()).then_some(bounds),
|
||||
}
|
||||
if min_x.is_finite() {
|
||||
out.world_aabb = [min_x, min_y, max_x, max_y];
|
||||
}
|
||||
out.recompute_aabb();
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -1538,8 +1509,7 @@ pub struct Scene {
|
||||
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
|
||||
)>,
|
||||
>,
|
||||
/// Auxiliary broad phase for objects that have no wire representation:
|
||||
/// hatch-only and solid-only block instances.
|
||||
/// Auxiliary broad phase for hatch-only objects.
|
||||
interaction_handle_index_cache: RefCell<
|
||||
Option<(
|
||||
u64,
|
||||
@@ -6169,15 +6139,11 @@ impl Scene {
|
||||
}
|
||||
let mut lookup: HashMap<Handle, Vec<u32>> = HashMap::default();
|
||||
for (index, set) in meshes.iter().enumerate() {
|
||||
let Some(value) = set
|
||||
.lods
|
||||
.first()
|
||||
.and_then(|mesh| mesh.name.parse::<u64>().ok())
|
||||
else {
|
||||
let Some(handle) = set.entity_handle() else {
|
||||
continue;
|
||||
};
|
||||
lookup
|
||||
.entry(Handle::new(value))
|
||||
.entry(handle)
|
||||
.or_default()
|
||||
.push(index as u32);
|
||||
}
|
||||
@@ -6490,18 +6456,6 @@ impl Scene {
|
||||
true
|
||||
}
|
||||
|
||||
fn mesh_visible_for_interaction(&self, handle: Handle) -> bool {
|
||||
self.mesh_entity_visible(handle)
|
||||
&& self.document.get_entity(handle).is_some_and(|entity| {
|
||||
!self.interaction_layer_frozen(&entity.common().layer)
|
||||
&& self.belongs_to_visible_block(
|
||||
handle,
|
||||
entity.common().owner_handle,
|
||||
self.interaction_block_handle(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// One transformed mesh per block-definition solid instance reached from an
|
||||
/// INSERT owned by `layout_block`. Nested INSERTs accumulate their
|
||||
/// transform. Empty when no block solids exist. (#123)
|
||||
@@ -6541,8 +6495,7 @@ impl Scene {
|
||||
return;
|
||||
};
|
||||
let inherit = self.mesh_inherit_for_path(&context.insert_path);
|
||||
let own_alpha =
|
||||
set.lods.first().map(|mesh| mesh.color[3]).unwrap_or(1.0);
|
||||
let own_alpha = set.display_color().map_or(1.0, |color| color[3]);
|
||||
let mut transformed =
|
||||
transform_block_mesh_lod_set(set, &context.transform);
|
||||
if let Some(color) = self.block_mesh_override_color(
|
||||
@@ -6551,9 +6504,7 @@ impl Scene {
|
||||
inherit.as_ref(),
|
||||
own_alpha,
|
||||
) {
|
||||
for mesh in &mut transformed.lods {
|
||||
mesh.color = color;
|
||||
}
|
||||
transformed.instance_color = Some(color);
|
||||
if let Some(material) = transformed.material.as_mut() {
|
||||
if material.handle.is_none() {
|
||||
material.diffuse = color;
|
||||
@@ -6569,10 +6520,7 @@ impl Scene {
|
||||
self.material_base_dir.as_deref(),
|
||||
);
|
||||
}
|
||||
let root_name = context.root_handle.value().to_string();
|
||||
for mesh in &mut transformed.lods {
|
||||
mesh.name = root_name.clone();
|
||||
}
|
||||
transformed.instance_handle = Some(context.root_handle);
|
||||
out.push(transformed);
|
||||
},
|
||||
);
|
||||
@@ -7449,20 +7397,6 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
for set in self.interaction_meshes_arc().iter() {
|
||||
let Some(mesh) = set.lods.first() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(handle) = mesh.name.parse::<u64>() else {
|
||||
continue;
|
||||
};
|
||||
if !self.mesh_visible_for_interaction(Handle::new(handle)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(aabb) = mesh_interaction_aabb(set) {
|
||||
entries.push((handle, aabb));
|
||||
}
|
||||
}
|
||||
let index =
|
||||
Arc::new(crate::scene::pick::interaction_index::InteractionHandleIndex::build(entries));
|
||||
*self.interaction_handle_index_cache.borrow_mut() =
|
||||
@@ -7677,7 +7611,6 @@ impl Scene {
|
||||
.collect();
|
||||
let insert_hatches = self.insert_hatches_for_click();
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let mesh_lookup = self.mesh_pick_lookup(&meshes);
|
||||
handles.extend(
|
||||
self.interaction_handle_index()
|
||||
.query_xy(aabb)
|
||||
@@ -7685,12 +7618,19 @@ impl Scene {
|
||||
.filter_map(|value| {
|
||||
let handle = Handle::new(value);
|
||||
(self.hatch_visible_for_interaction(handle)
|
||||
|| insert_hatches.contains_key(&handle)
|
||||
|| (mesh_lookup.contains_key(&handle)
|
||||
&& self.mesh_visible_for_interaction(handle)))
|
||||
|| insert_hatches.contains_key(&handle))
|
||||
.then_some(handle)
|
||||
}),
|
||||
);
|
||||
handles.extend(meshes.iter().filter_map(|set| {
|
||||
let handle = set.entity_handle()?;
|
||||
let bounds = mesh_interaction_aabb(set)?;
|
||||
(bounds[3] >= aabb[0]
|
||||
&& bounds[0] <= aabb[2]
|
||||
&& bounds[4] >= aabb[1]
|
||||
&& bounds[1] <= aabb[3])
|
||||
.then_some(handle)
|
||||
}));
|
||||
handles
|
||||
}
|
||||
|
||||
@@ -7703,22 +7643,17 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
_candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Vec<Handle> {
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let lookup = self.mesh_pick_lookup(&meshes);
|
||||
let handles: Vec<Handle> = candidate_handles.map_or_else(
|
||||
|| lookup.keys().copied().collect(),
|
||||
|candidates| candidates.iter().copied().collect(),
|
||||
);
|
||||
let handles: Vec<Handle> = lookup.keys().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
for handle in handles {
|
||||
if !self.mesh_visible_for_interaction(handle)
|
||||
|| matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
)
|
||||
{
|
||||
if matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let Some(indices) = lookup.get(&handle) else {
|
||||
@@ -7728,12 +7663,12 @@ impl Scene {
|
||||
.iter()
|
||||
.filter_map(|&index| meshes.get(index as usize))
|
||||
.any(|set| {
|
||||
set.lods.first().is_some_and(|mesh| {
|
||||
set.geometry_lods().first().is_some_and(|mesh| {
|
||||
!pick::hit_test::mesh_box_hit(
|
||||
a,
|
||||
b,
|
||||
crossing,
|
||||
std::iter::once((handle, mesh)),
|
||||
std::iter::once((handle, mesh, set.instance_transform)),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
@@ -7877,22 +7812,17 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
_candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Vec<Handle> {
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let lookup = self.mesh_pick_lookup(&meshes);
|
||||
let handles: Vec<Handle> = candidate_handles.map_or_else(
|
||||
|| lookup.keys().copied().collect(),
|
||||
|candidates| candidates.iter().copied().collect(),
|
||||
);
|
||||
let handles: Vec<Handle> = lookup.keys().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
for handle in handles {
|
||||
if !self.mesh_visible_for_interaction(handle)
|
||||
|| matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
)
|
||||
{
|
||||
if matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let Some(indices) = lookup.get(&handle) else {
|
||||
@@ -7902,11 +7832,11 @@ impl Scene {
|
||||
.iter()
|
||||
.filter_map(|&index| meshes.get(index as usize))
|
||||
.any(|set| {
|
||||
set.lods.first().is_some_and(|mesh| {
|
||||
set.geometry_lods().first().is_some_and(|mesh| {
|
||||
!pick::hit_test::mesh_poly_hit(
|
||||
poly,
|
||||
crossing,
|
||||
std::iter::once((handle, mesh)),
|
||||
std::iter::once((handle, mesh, set.instance_transform)),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
@@ -7971,60 +7901,34 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
_candidate_handles: Option<&HashSet<Handle>>,
|
||||
coarse_lod: bool,
|
||||
) -> Option<Handle> {
|
||||
// Reuse the renderer's expanded mesh set (top-level solids + per-INSERT
|
||||
// block instances), cached per geometry epoch — so hover no longer
|
||||
// re-expands every block instance on each move. Every `MeshLodSet`
|
||||
// carries its handle (in `mesh.name`) and a 3D AABB.
|
||||
// Reuse the renderer's cached top-level and block-instance mesh set.
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let lookup = candidate_handles.map(|_| self.mesh_pick_lookup(&meshes));
|
||||
// Candidate handles already came from the precise f64 interaction BVH.
|
||||
// When no index is active the source is small/non-resident, so exact
|
||||
// triangles are safer than reintroducing the old f32 AABB precision loss.
|
||||
let mut sets: Vec<(Handle, &MeshLodSet)> = Vec::new();
|
||||
if let (Some(handles), Some(lookup)) = (candidate_handles, lookup.as_ref()) {
|
||||
for handle in handles {
|
||||
if !self.mesh_visible_for_interaction(*handle) {
|
||||
continue;
|
||||
}
|
||||
let Some(indices) = lookup.get(handle) else {
|
||||
continue;
|
||||
};
|
||||
sets.extend(
|
||||
indices
|
||||
.iter()
|
||||
.filter_map(|&index| meshes.get(index as usize).map(|set| (*handle, set))),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
sets.extend(meshes.iter().filter_map(|set| {
|
||||
let handle = set
|
||||
.lods
|
||||
.first()?
|
||||
.name
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.map(Handle::new)?;
|
||||
self.mesh_visible_for_interaction(handle)
|
||||
.then_some((handle, set))
|
||||
}));
|
||||
}
|
||||
if coarse_lod {
|
||||
return pick::hit_test::mesh_click_hit(
|
||||
cursor,
|
||||
sets.iter()
|
||||
.filter_map(|(handle, set)| set.lods.last().map(|mesh| (*handle, mesh))),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
let sets: Vec<(Handle, &MeshLodSet)> = meshes
|
||||
.iter()
|
||||
.filter_map(|set| {
|
||||
let handle = set.entity_handle()?;
|
||||
Some((handle, set))
|
||||
})
|
||||
.collect();
|
||||
pick::hit_test::mesh_click_hit(
|
||||
cursor,
|
||||
sets.iter()
|
||||
.filter_map(|(handle, set)| set.lods.first().map(|mesh| (*handle, mesh))),
|
||||
sets.iter().filter_map(|(handle, set)| {
|
||||
let lods = set.geometry_lods();
|
||||
let mesh = if coarse_lod {
|
||||
lods.last()?
|
||||
} else {
|
||||
lods.first()?
|
||||
};
|
||||
Some((
|
||||
*handle,
|
||||
mesh,
|
||||
set.instance_transform,
|
||||
mesh_interaction_aabb(set)?,
|
||||
))
|
||||
}),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
@@ -8043,24 +7947,20 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
_candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Vec<Handle> {
|
||||
if self.block_meshes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let lookup = self.mesh_pick_lookup(&meshes);
|
||||
let handles: Vec<Handle> = candidate_handles.map_or_else(
|
||||
|| lookup.keys().copied().collect(),
|
||||
|candidates| candidates.iter().copied().collect(),
|
||||
);
|
||||
let handles: Vec<Handle> = lookup.keys().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
for handle in handles {
|
||||
if !matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
) || !self.mesh_visible_for_interaction(handle)
|
||||
{
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let Some(indices) = lookup.get(&handle) else {
|
||||
@@ -8070,12 +7970,12 @@ impl Scene {
|
||||
.iter()
|
||||
.filter_map(|&index| meshes.get(index as usize))
|
||||
.any(|set| {
|
||||
set.lods.first().map_or(false, |m| {
|
||||
set.geometry_lods().first().is_some_and(|mesh| {
|
||||
!pick::hit_test::mesh_box_hit(
|
||||
a,
|
||||
b,
|
||||
crossing,
|
||||
std::iter::once((handle, m)),
|
||||
std::iter::once((handle, mesh, set.instance_transform)),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
@@ -8098,24 +7998,20 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
_candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Vec<Handle> {
|
||||
if self.block_meshes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let meshes = self.interaction_meshes_arc();
|
||||
let lookup = self.mesh_pick_lookup(&meshes);
|
||||
let handles: Vec<Handle> = candidate_handles.map_or_else(
|
||||
|| lookup.keys().copied().collect(),
|
||||
|candidates| candidates.iter().copied().collect(),
|
||||
);
|
||||
let handles: Vec<Handle> = lookup.keys().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
for handle in handles {
|
||||
if !matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Insert(_))
|
||||
) || !self.mesh_visible_for_interaction(handle)
|
||||
{
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let Some(indices) = lookup.get(&handle) else {
|
||||
@@ -8125,11 +8021,11 @@ impl Scene {
|
||||
.iter()
|
||||
.filter_map(|&index| meshes.get(index as usize))
|
||||
.any(|set| {
|
||||
set.lods.first().map_or(false, |m| {
|
||||
set.geometry_lods().first().is_some_and(|mesh| {
|
||||
!pick::hit_test::mesh_poly_hit(
|
||||
poly,
|
||||
crossing,
|
||||
std::iter::once((handle, m)),
|
||||
std::iter::once((handle, mesh, set.instance_transform)),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
|
||||
Reference in new issue
Block a user