feat: add FaceInferToolPlugin - single-click face normal inference tool
- New FaceInferToolPlugin: pixel-neighborhood method with {2,5,10}px offsets
- New FaceInferResult data class
- Test button in SystemManagement page
- Cursor: MarkupAutoPoly (arrow + dashed box)
- Uses project-standard tools: FindNamedParentContainer, viewpoint.Position
- Design doc at doc/design/2026/face-infer-tool-design.md
This commit is contained in:
parent
922864ce4f
commit
c7a1eeebb8
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,5 +1,22 @@
|
||||
# NavisworksTransport 变更日志
|
||||
|
||||
## [0.15.5] - 2026-06-02
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
- **快速取面工具 `FaceInferToolPlugin`**:新增独立 ToolPlugin,单点+像素邻域法快速推断点击面法向量。
|
||||
- 算法:一次点击内做三次 `PickItemFromPoint`(中心、右侧 δ px、下侧 δ px),叉积求法向量,δ 递进 {2, 5, 10} px 规避共线
|
||||
- 朝向统一:法向量自动翻转到指向相机一侧(`viewpoint.Position` 与命中点对比)
|
||||
- 构件识别:沿 `ModelItemAnalysisHelper.FindNamedParentContainer` 向上查找命名容器
|
||||
- 光标:`Cursor.MarkupAutoPoly`(箭头+虚线小方块),与取点工具的 `Measure` 十字光标区分
|
||||
- 与 `PathClickToolPlugin` 并列,互斥激活,结构完全平行
|
||||
- **`FaceInferResult`**:结果数据类,包含命中点(宿主坐标)、归一化法向量、ModelItem
|
||||
- **系统管理功能测试按钮**:页签底部新增「🧭 快速取面测试」,点击后激活工具,用户点击模型面 → 弹出结果对话框
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- 新增 `doc/design/2026/face-infer-tool-design.md` 设计文档
|
||||
|
||||
## [0.15.4] - 2026-06-01
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
@ -106,6 +106,8 @@
|
||||
<!-- Core - Main Plugin Files -->
|
||||
<Compile Include="src\Core\MainPlugin.cs" />
|
||||
<Compile Include="src\Core\PathClickToolPlugin.cs" />
|
||||
<Compile Include="src\Core\FaceInferToolPlugin.cs" />
|
||||
<Compile Include="src\Core\FaceInferResult.cs" />
|
||||
<Compile Include="src\Core\PathInputMonitor.cs" />
|
||||
<Compile Include="src\Core\PathPointRenderPlugin.cs" />
|
||||
<!-- Core - Business Logic -->
|
||||
|
||||
111
doc/design/2026/face-infer-tool-design.md
Normal file
111
doc/design/2026/face-infer-tool-design.md
Normal file
@ -0,0 +1,111 @@
|
||||
# 快速取面工具设计
|
||||
|
||||
创建时间:2026-06-02
|
||||
|
||||
## 目标
|
||||
|
||||
新增一个独立 ToolPlugin,通过单点+像素邻域法快速推断鼠标点击位置所在面的法向量。
|
||||
和取点工具 `PathClickToolPlugin` 并列,互斥激活。
|
||||
|
||||
## 核心算法
|
||||
|
||||
单点+像素邻域法 —— 一次点击内做三次 `PickItemFromPoint`:
|
||||
|
||||
```
|
||||
中心点: PickItemFromPoint(x, y) → p0
|
||||
右邻域: PickItemFromPoint(x+δ, y) → p1
|
||||
下邻域: PickItemFromPoint(x, y+δ) → p2
|
||||
|
||||
v1 = p1 - p0
|
||||
v2 = p2 - p0
|
||||
normal = normalize(cross(v1, v2))
|
||||
```
|
||||
|
||||
偏移量 δ 三级递进:2px → 5px → 10px,小偏移共线时自动退到更大偏移。
|
||||
|
||||
## 新增文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `src/Models/FaceInferResult.cs` | 结果数据类 |
|
||||
| `src/Core/FaceInferToolPlugin.cs` | ToolPlugin 主体 |
|
||||
|
||||
不修改任何现有文件。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
FaceInferToolPlugin : ToolPlugin
|
||||
│
|
||||
├── Plugin("FaceInferTool", "NavisworksTransport")
|
||||
│
|
||||
├── Static 成员
|
||||
│ ├── event FaceInferred # 外部订阅事件
|
||||
│ ├── AssemblyPath # 用于动态加载
|
||||
│ └── TriggerFaceInferred() # 允许外部手动触发
|
||||
│
|
||||
├── GetCursor → Cursor.MarkupQuickPick
|
||||
│
|
||||
├── MouseDown(x, y)
|
||||
│ ├── centerPick = PickItemFromPoint(x, y)
|
||||
│ ├── rightPick = PickItemFromPoint(x + δ, y)
|
||||
│ ├── downPick = PickItemFromPoint(x, y + δ)
|
||||
│ ├── 同构件校验 (ModelItem 比较)
|
||||
│ ├── 共线校验 (cross.Length < 0.001 → 退到下一级 δ)
|
||||
│ ├── 法向量归一化
|
||||
│ ├── 朝向统一 (dot with viewDir, 始终指向相机)
|
||||
│ └── 触发 FaceInferred 事件
|
||||
│
|
||||
└── MouseMove → 暂不处理(轻量原则)
|
||||
```
|
||||
|
||||
## FaceInferResult 数据结构
|
||||
|
||||
```csharp
|
||||
public class FaceInferResult
|
||||
{
|
||||
public Point3D HitPoint { get; } // 命中点(宿主坐标系)
|
||||
public Vector3D Normal { get; } // 面法向量(已归一化,朝向相机)
|
||||
public ModelItem ModelItem { get; } // 命中的 ModelItem
|
||||
public bool IsValid { get; } // 推断是否成功
|
||||
}
|
||||
```
|
||||
|
||||
所有坐标均为**宿主坐标系**(`PickItemFromPoint` 原始输出)。
|
||||
|
||||
## 激活方式
|
||||
|
||||
与 `PathClickToolPlugin` 完全相同的流程,仅插件名不同:
|
||||
|
||||
```csharp
|
||||
var assemblyPath = FaceInferToolPlugin.AssemblyPath;
|
||||
Application.Plugins.AddPluginAssembly(assemblyPath);
|
||||
var record = (ToolPluginRecord)Application.Plugins.FindPlugin("FaceInferTool.NavisworksTransport");
|
||||
var plugin = record.LoadPlugin();
|
||||
Application.MainDocument.Tool.SetCustomToolPlugin(plugin);
|
||||
```
|
||||
|
||||
## 光标
|
||||
|
||||
使用 `Cursor.MarkupQuickPick`。Navisworks `Cursor` 枚举无平行四边形,此为最接近的选项。
|
||||
|
||||
## 与 PathClickToolPlugin 的关系
|
||||
|
||||
- 两者都是 `ToolPlugin`,同一时刻只有一个激活
|
||||
- 互斥切换:取面时取点失活,取面结束切回取点
|
||||
- 切换流程与项目中已有的「导航工具 ↔ ClickTool」切换模式一致
|
||||
|
||||
## 暂不实现
|
||||
|
||||
| 项目 | 原因 |
|
||||
|---|---|
|
||||
| 透视近大远小动态 δ 调整 | 先验证精度,不够再加 |
|
||||
| 跨面距离检测 (maxDist) | 同构件判断已覆盖基本场景 |
|
||||
| 曲面曲率分析 | 不同业务需求 |
|
||||
| MouseMove 实时预览 | 保持轻量 |
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 实现两个新文件
|
||||
2. 在已知平面上验证法向量精度(误差应在 1° 以内)
|
||||
3. 后续由调用方(ViewModel)按需集成激活/切换逻辑
|
||||
35
src/Core/FaceInferResult.cs
Normal file
35
src/Core/FaceInferResult.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Autodesk.Navisworks.Api;
|
||||
|
||||
namespace NavisworksTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速取面结果。
|
||||
/// 所有坐标为宿主坐标系。
|
||||
/// </summary>
|
||||
public class FaceInferResult
|
||||
{
|
||||
/// <summary>点击命中点(宿主坐标系)</summary>
|
||||
public Point3D HitPoint { get; }
|
||||
|
||||
/// <summary>面法向量(已归一化,朝向相机)</summary>
|
||||
public Vector3D Normal { get; }
|
||||
|
||||
/// <summary>命中的 ModelItem</summary>
|
||||
public ModelItem ModelItem { get; }
|
||||
|
||||
/// <summary>推断是否成功</summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
public FaceInferResult(Point3D hitPoint, Vector3D normal, ModelItem modelItem)
|
||||
{
|
||||
HitPoint = hitPoint;
|
||||
Normal = normal;
|
||||
ModelItem = modelItem;
|
||||
IsValid = modelItem != null;
|
||||
}
|
||||
|
||||
/// <summary>创建失败结果</summary>
|
||||
public static FaceInferResult Invalid { get; } = new FaceInferResult(
|
||||
new Point3D(), new Vector3D(), null);
|
||||
}
|
||||
}
|
||||
171
src/Core/FaceInferToolPlugin.cs
Normal file
171
src/Core/FaceInferToolPlugin.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using Autodesk.Navisworks.Api;
|
||||
using Autodesk.Navisworks.Api.Plugins;
|
||||
using NavisworksTransport.Utils;
|
||||
|
||||
namespace NavisworksTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速取面工具。单点+像素邻域法推断点击位置所在面的法向量。
|
||||
/// 与 PathClickToolPlugin 并列,互斥激活。
|
||||
/// </summary>
|
||||
[Plugin("FaceInferTool", "NavisworksTransport")]
|
||||
public class FaceInferToolPlugin : ToolPlugin
|
||||
{
|
||||
/// <summary>像素偏移递进序列(共线时自动退到下一级)</summary>
|
||||
private static readonly int[] PixelOffsets = { 2, 5, 10 };
|
||||
|
||||
/// <summary>最小叉积长度阈值(低于此值视为三点共线)</summary>
|
||||
private const double MinCrossLength = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// 面推断结果事件。命中物体时传递 FaceInferResult,未命中时传递 Invalid。
|
||||
/// </summary>
|
||||
public static event EventHandler<FaceInferResult> FaceInferred;
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的程序集路径(用于动态加载)
|
||||
/// </summary>
|
||||
public static string AssemblyPath =>
|
||||
System.Reflection.Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
/// <summary>
|
||||
/// 允许外部代码手动触发 FaceInferred 事件
|
||||
/// </summary>
|
||||
public static void TriggerFaceInferred(object sender, FaceInferResult result)
|
||||
{
|
||||
FaceInferred?.Invoke(sender, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件构造函数
|
||||
/// </summary>
|
||||
public FaceInferToolPlugin()
|
||||
{
|
||||
LogManager.Debug("[FaceInferTool] FaceInferToolPlugin 实例已创建");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标按下事件 — 单次点击内做三次 PickItemFromPoint 推断面法向量
|
||||
/// </summary>
|
||||
public override bool MouseDown(View view, KeyModifiers modifiers, ushort button, int x, int y, double timeOffset)
|
||||
{
|
||||
if (button != 1) return false; // 只响应左键
|
||||
|
||||
LogManager.Debug("[FaceInferTool] ===== 鼠标点击 =====");
|
||||
LogManager.Debug($"[FaceInferTool] 屏幕坐标: ({x}, {y})");
|
||||
|
||||
var result = InferFace(view, x, y);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
LogManager.Debug($"[FaceInferTool] ✓ 面推断成功");
|
||||
LogManager.Debug($"[FaceInferTool] 命中点: ({result.HitPoint.X:F3}, {result.HitPoint.Y:F3}, {result.HitPoint.Z:F3})");
|
||||
LogManager.Debug($"[FaceInferTool] 法向量: ({result.Normal.X:F3}, {result.Normal.Y:F3}, {result.Normal.Z:F3})");
|
||||
LogManager.Debug($"[FaceInferTool] 构件: {ModelItemAnalysisHelper.GetSafeDisplayName(result.ModelItem)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Debug("[FaceInferTool] ✗ 面推断失败(未命中/跨构件/共线)");
|
||||
}
|
||||
|
||||
FaceInferred?.Invoke(this, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标移动事件 — 暂不处理(保持轻量)
|
||||
/// </summary>
|
||||
public override bool MouseMove(View view, KeyModifiers modifiers, int x, int y, double timeOffset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 光标样式 — 使用 MarkupQuickPick 与取点工具的 Measure 形成视觉区分
|
||||
/// </summary>
|
||||
public override Cursor GetCursor(View view, KeyModifiers modifier)
|
||||
{
|
||||
return Cursor.MarkupAutoPoly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心面推断逻辑。
|
||||
/// 在 (x, y) 处做三次 PickItemFromPoint(中心+右下邻域),
|
||||
/// 通过叉积计算法向量,γ 递进规避共线场景。
|
||||
/// </summary>
|
||||
private static FaceInferResult InferFace(View view, int x, int y)
|
||||
{
|
||||
// 拾取中心点
|
||||
var centerPick = view.PickItemFromPoint(x, y);
|
||||
if (centerPick == null)
|
||||
return FaceInferResult.Invalid;
|
||||
|
||||
var p0 = centerPick.Point;
|
||||
|
||||
foreach (int offset in PixelOffsets)
|
||||
{
|
||||
var rightPick = view.PickItemFromPoint(x + offset, y);
|
||||
if (rightPick == null) continue;
|
||||
|
||||
var downPick = view.PickItemFromPoint(x, y + offset);
|
||||
if (downPick == null) continue;
|
||||
|
||||
// 必须命中同一构件
|
||||
if (rightPick.ModelItem != centerPick.ModelItem ||
|
||||
downPick.ModelItem != centerPick.ModelItem)
|
||||
continue;
|
||||
|
||||
var p1 = rightPick.Point;
|
||||
var p2 = downPick.Point;
|
||||
|
||||
var v1 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
|
||||
var v2 = new Vector3D(p2.X - p0.X, p2.Y - p0.Y, p2.Z - p0.Z);
|
||||
|
||||
var normal = Cross(v1, v2);
|
||||
double len = Length(normal);
|
||||
if (len < MinCrossLength)
|
||||
{
|
||||
LogManager.Debug($"[FaceInferTool] 偏移{offset}px 三点近似共线,尝试更大偏移");
|
||||
continue; // 退到下一级偏移
|
||||
}
|
||||
|
||||
// 归一化
|
||||
normal = new Vector3D(normal.X / len, normal.Y / len, normal.Z / len);
|
||||
|
||||
// 统一法向量朝向:指向相机一侧
|
||||
var camPos = view.Document.CurrentViewpoint.Value.Position;
|
||||
var toCam = new Vector3D(camPos.X - p0.X, camPos.Y - p0.Y, camPos.Z - p0.Z);
|
||||
if (Dot(normal, toCam) < 0)
|
||||
{
|
||||
normal = new Vector3D(-normal.X, -normal.Y, -normal.Z);
|
||||
}
|
||||
|
||||
return new FaceInferResult(p0, normal, centerPick.ModelItem);
|
||||
}
|
||||
|
||||
LogManager.Debug("[FaceInferTool] 所有偏移量均无法推断平面");
|
||||
return FaceInferResult.Invalid;
|
||||
}
|
||||
|
||||
// ---- Vector3D 辅助方法(避免依赖 Navisworks Vector3D 静态方法在不同版本的可用性差异)----
|
||||
|
||||
private static Vector3D Cross(Vector3D a, Vector3D b)
|
||||
{
|
||||
return new Vector3D(
|
||||
a.Y * b.Z - a.Z * b.Y,
|
||||
a.Z * b.X - a.X * b.Z,
|
||||
a.X * b.Y - a.Y * b.X);
|
||||
}
|
||||
|
||||
private static double Length(Vector3D v)
|
||||
{
|
||||
return Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
|
||||
}
|
||||
|
||||
private static double Dot(Vector3D a, Vector3D b)
|
||||
{
|
||||
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Autodesk.Navisworks.Api;
|
||||
using Autodesk.Navisworks.Api.Plugins;
|
||||
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
|
||||
using NavisworksTransport.UI.WPF.Collections;
|
||||
using NavisworksTransport.Core;
|
||||
@ -184,6 +185,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
public ICommand CoordinateSystemExplorerCommand { get; private set; }
|
||||
public ICommand CaptureRealObjectTransformCommand { get; private set; }
|
||||
public ICommand DetectFragmentDefaultUpCommand { get; private set; }
|
||||
public ICommand TestFaceInferCommand { get; private set; }
|
||||
|
||||
|
||||
// 坐标系设置
|
||||
@ -339,6 +341,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
CoordinateSystemExplorerCommand = new RelayCommand(() => ExecuteCoordinateSystemExplorer());
|
||||
CaptureRealObjectTransformCommand = new RelayCommand(() => ExecuteCaptureRealObjectTransform());
|
||||
DetectFragmentDefaultUpCommand = new RelayCommand(() => ExecuteDetectFragmentDefaultUp());
|
||||
TestFaceInferCommand = new RelayCommand(() => ExecuteTestFaceInfer());
|
||||
|
||||
|
||||
// 初始化坐标系选项
|
||||
@ -2024,6 +2027,102 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
public Vector3 SampleTranslation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速取面测试 — 激活 FaceInferToolPlugin,用户在模型上点击后显示面法向量
|
||||
/// </summary>
|
||||
private void ExecuteTestFaceInfer()
|
||||
{
|
||||
SafeExecute(() =>
|
||||
{
|
||||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||||
if (doc == null || doc.IsClear)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
"没有活动的文档!请先打开一个模型。",
|
||||
"错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载并激活 FaceInferToolPlugin
|
||||
var assemblyPath = FaceInferToolPlugin.AssemblyPath;
|
||||
Application.Plugins.AddPluginAssembly(assemblyPath);
|
||||
var record = (ToolPluginRecord)Application.Plugins.FindPlugin("FaceInferTool.NavisworksTransport");
|
||||
if (record == null)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
"无法找到 FaceInferTool 插件!",
|
||||
"错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
var plugin = record.LoadPlugin();
|
||||
if (plugin == null)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
"FaceInferTool 插件加载失败!",
|
||||
"错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateMainStatus("请在模型上点击一个面…");
|
||||
|
||||
// 一次性事件处理器:取到面后自动取消订阅并恢复工具
|
||||
EventHandler<FaceInferResult> handler = null;
|
||||
handler = (sender, result) =>
|
||||
{
|
||||
FaceInferToolPlugin.FaceInferred -= handler;
|
||||
|
||||
// 恢复为选择工具
|
||||
try
|
||||
{
|
||||
Application.MainDocument.Tool.Value = Tool.Select;
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
UpdateMainStatus("面推断失败:未命中有效面");
|
||||
return;
|
||||
}
|
||||
|
||||
var info = new StringBuilder();
|
||||
info.AppendLine("=== 面推断结果 ===");
|
||||
info.AppendLine();
|
||||
var namedParent = ModelItemAnalysisHelper.FindNamedParentContainer(result.ModelItem);
|
||||
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(namedParent);
|
||||
info.AppendLine($"构件: {displayName}");
|
||||
info.AppendLine($"命中点: ({result.HitPoint.X:F3}, {result.HitPoint.Y:F3}, {result.HitPoint.Z:F3})");
|
||||
info.AppendLine($"法向量: ({result.Normal.X:F6}, {result.Normal.Y:F6}, {result.Normal.Z:F6})");
|
||||
|
||||
string message = info.ToString();
|
||||
LogManager.Info("[面推断测试]\n" + message);
|
||||
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var dialog = new NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog
|
||||
{
|
||||
Title = "面推断结果",
|
||||
ResultText = message
|
||||
};
|
||||
DialogHelper.SetOwnerSafely(dialog);
|
||||
dialog.ShowDialog();
|
||||
});
|
||||
|
||||
var statusName = ModelItemAnalysisHelper.GetSafeDisplayName(namedParent);
|
||||
UpdateMainStatus($"面推断完成 — {statusName}");
|
||||
};
|
||||
|
||||
FaceInferToolPlugin.FaceInferred += handler;
|
||||
|
||||
Application.MainDocument.Tool.SetCustomToolPlugin(plugin);
|
||||
}, "面推断测试");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取选中对象的Transform信息,并测试旋转
|
||||
/// </summary>
|
||||
|
||||
@ -288,6 +288,11 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav
|
||||
Command="{Binding CaptureRealObjectTransformCommand}"
|
||||
Style="{StaticResource ActionButtonStyle}"
|
||||
ToolTip="读取选中真实物体的原始Transform,并在3D视图中可视化X/Y/Z三轴"/>
|
||||
|
||||
<Button Content="🧭 快速取面测试"
|
||||
Command="{Binding TestFaceInferCommand}"
|
||||
Style="{StaticResource ActionButtonStyle}"
|
||||
ToolTip="单点+像素邻域法快速推断面法向量。点击后在模型面上点击。"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user