第十二章:输入与交互系统
12.1 输入系统概述
12.1.1 InputSys的定位
输入系统是LightCAD用户交互的核心,负责处理所有来自鼠标、键盘和其他输入设备的事件,并将它们转化为CAD操作命令。
12.1.2 输入处理流程
用户输入(鼠标/键盘)
↓
输入事件分发(Inputer)
↓
捕捉系统处理(SnapManager)
↓
当前动作处理(ViewPortRtAction)
↓
实体/视口更新
↓
视觉反馈渲染
12.2 统一输入处理器(Inputer)
12.2.1 输入处理器设计
public class Inputer
{
private ViewportManager viewportManager;
private SnapManager snapManager;
private IViewPortAction currentAction;
/// <summary>
/// 鼠标移动事件处理
/// </summary>
public void OnMouseMove(int screenX, int screenY)
{
var viewport = viewportManager.ActiveViewport;
if (viewport == null) return;
// 屏幕坐标转世界坐标
var worldPoint = viewport.ScreenToWorld(screenX, screenY);
// 应用对象捕捉
var snappedPoint = snapManager.FindSnapPoint(
worldPoint, viewport);
// 通知当前动作
currentAction?.OnMouseMove(new InputEventArgs
{
ScreenPoint = new System.Drawing.Point(screenX, screenY),
WorldPoint = snappedPoint ?? worldPoint,
IsSnapped = snappedPoint.HasValue,
SnapType = snapManager.LastSnapType,
Viewport = viewport
});
}
/// <summary>
/// 鼠标点击事件处理
/// </summary>
public void OnMouseClick(int screenX, int screenY,
MouseButton button)
{
var viewport = viewportManager.ActiveViewport;
if (viewport == null) return;
var worldPoint = viewport.ScreenToWorld(screenX, screenY);
var snappedPoint = snapManager.FindSnapPoint(worldPoint, viewport);
var args = new InputEventArgs
{
ScreenPoint = new System.Drawing.Point(screenX, screenY),
WorldPoint = snappedPoint ?? worldPoint,
IsSnapped = snappedPoint.HasValue,
Button = button,
Viewport = viewport
};
switch (button)
{
case MouseButton.Left:
currentAction?.OnLeftClick(args);
break;
case MouseButton.Right:
currentAction?.OnRightClick(args);
break;
case MouseButton.Middle:
// 中键启动平移
viewport.PanZoomController.BeginPan(screenX, screenY);
break;
}
}
/// <summary>
/// 鼠标滚轮事件处理
/// </summary>
public void OnMouseWheel(int screenX, int screenY, int delta)
{
var viewport = viewportManager.ActiveViewport;
if (viewport == null) return;
var zoomFactor = delta > 0 ? 1.1 : 1.0 / 1.1;
viewport.PanZoomController.ZoomAtPoint(
screenX, screenY, zoomFactor);
}
/// <summary>
/// 键盘按键事件处理
/// </summary>
public void OnKeyDown(Keys key)
{
switch (key)
{
case Keys.Escape:
CancelCurrentAction();
break;
case Keys.Enter:
currentAction?.OnConfirm();
break;
case Keys.Delete:
DeleteSelectedEntities();
break;
case Keys.F3:
ToggleObjectSnap();
break;
case Keys.F7:
ToggleGrid();
break;
case Keys.F8:
ToggleOrthoMode();
break;
}
// 传递给当前动作
currentAction?.OnKeyDown(key);
}
/// <summary>
/// 设置当前操作动作
/// </summary>
public void SetAction(IViewPortAction action)
{
currentAction?.OnDeactivate();
currentAction = action;
currentAction?.OnActivate();
}
}
12.3 对象捕捉系统(Snap)
12.3.1 捕捉管理器
public class SnapManager
{
private List<ISnapProvider> snapProviders = new();
private double snapTolerance = 10.0; // 像素容差
/// <summary>
/// 是否启用对象捕捉
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 启用的捕捉类型
/// </summary>
public OSnapTypes EnabledTypes { get; set; } = OSnapTypes.All;
/// <summary>
/// 上一次捕捉的类型
/// </summary>
public OSnapTypes LastSnapType { get; private set; }
public SnapManager()
{
// 注册所有捕捉提供者
snapProviders.Add(new EndPointSnapProvider());
snapProviders.Add(new MidPointSnapProvider());
snapProviders.Add(new CenterSnapProvider());
snapProviders.Add(new IntersectionSnapProvider());
snapProviders.Add(new PerpendicularSnapProvider());
snapProviders.Add(new TangentSnapProvider());
snapProviders.Add(new NearestSnapProvider());
snapProviders.Add(new GridSnapProvider());
}
/// <summary>
/// 在给定位置查找最近的捕捉点
/// </summary>
public Point2d? FindSnapPoint(Point2d worldPoint, Viewport viewport)
{
if (!IsEnabled) return null;
var worldTolerance = snapTolerance / viewport.ZoomLevel;
Point2d? bestSnap = null;
double bestDistance = worldTolerance;
OSnapTypes bestType = OSnapTypes.None;
foreach (var provider in snapProviders)
{
if (!EnabledTypes.HasFlag(provider.SnapType))
continue;
var snapPoints = provider.FindSnapPoints(
worldPoint, worldTolerance, viewport);
foreach (var snap in snapPoints)
{
var dist = worldPoint.DistanceTo(snap.Position);
if (dist < bestDistance)
{
bestDistance = dist;
bestSnap = snap.Position;
bestType = snap.Type;
}
}
}
LastSnapType = bestType;
return bestSnap;
}
}
[Flags]
public enum OSnapTypes
{
None = 0,
EndPoint = 1, // 端点
MidPoint = 2, // 中点
Center = 4, // 圆心
Quadrant = 8, // 象限点
Intersection = 16, // 交点
Perpendicular = 32, // 垂足
Tangent = 64, // 切点
Nearest = 128, // 最近点
Grid = 256, // 栅格点
All = 511
}
12.3.2 捕捉提供者
public interface ISnapProvider
{
OSnapTypes SnapType { get; }
List<SnapResult> FindSnapPoints(Point2d worldPoint,
double tolerance, Viewport viewport);
}
/// <summary>
/// 端点捕捉
/// </summary>
public class EndPointSnapProvider : ISnapProvider
{
public OSnapTypes SnapType => OSnapTypes.EndPoint;
public List<SnapResult> FindSnapPoints(Point2d worldPoint,
double tolerance, Viewport viewport)
{
var results = new List<SnapResult>();
var searchBounds = new BoundingBox2d
{
Min = new Point2d(worldPoint.X - tolerance,
worldPoint.Y - tolerance),
Max = new Point2d(worldPoint.X + tolerance,
worldPoint.Y + tolerance)
};
var nearbyEntities = viewport.Document.Entities
.FindInBounds(searchBounds);
foreach (var entity in nearbyEntities)
{
switch (entity)
{
case LcLine line:
TryAddSnap(results, line.StartPoint,
worldPoint, tolerance);
TryAddSnap(results, line.EndPoint,
worldPoint, tolerance);
break;
case LcArc arc:
TryAddSnap(results, arc.StartPoint,
worldPoint, tolerance);
TryAddSnap(results, arc.EndPoint,
worldPoint, tolerance);
break;
case LcPolyline polyline:
foreach (var vertex in polyline.Vertices)
{
TryAddSnap(results, vertex.Position,
worldPoint, tolerance);
}
break;
}
}
return results;
}
private void TryAddSnap(List<SnapResult> results,
Point2d snapPoint, Point2d cursorPoint, double tolerance)
{
if (snapPoint.DistanceTo(cursorPoint) <= tolerance)
{
results.Add(new SnapResult
{
Position = snapPoint,
Type = OSnapTypes.EndPoint
});
}
}
}
/// <summary>
/// 交点捕捉
/// </summary>
public class IntersectionSnapProvider : ISnapProvider
{
public OSnapTypes SnapType => OSnapTypes.Intersection;
public List<SnapResult> FindSnapPoints(Point2d worldPoint,
double tolerance, Viewport viewport)
{
var results = new List<SnapResult>();
var entities = viewport.Document.Entities
.FindInBounds(new BoundingBox2d
{
Min = worldPoint - new Vector2d(tolerance, tolerance),
Max = worldPoint + new Vector2d(tolerance, tolerance)
}).ToList();
// 计算所有实体对之间的交点
for (int i = 0; i < entities.Count; i++)
{
for (int j = i + 1; j < entities.Count; j++)
{
var intersections = CalculateIntersections(
entities[i], entities[j]);
foreach (var point in intersections)
{
if (point.DistanceTo(worldPoint) <= tolerance)
{
results.Add(new SnapResult
{
Position = point,
Type = OSnapTypes.Intersection
});
}
}
}
}
return results;
}
}
12.3.3 捕捉标记渲染
public class SnapMarkerRenderer
{
/// <summary>
/// 渲染捕捉标记
/// </summary>
public void RenderSnapMarker(Viewport viewport,
Point2d snapPoint, OSnapTypes snapType)
{
var markerSize = 8; // 像素大小
var (sx, sy) = viewport.WorldToScreen(snapPoint);
switch (snapType)
{
case OSnapTypes.EndPoint:
DrawSquare(sx, sy, markerSize, Color.Green);
break;
case OSnapTypes.MidPoint:
DrawTriangle(sx, sy, markerSize, Color.Green);
break;
case OSnapTypes.Center:
DrawCircle(sx, sy, markerSize, Color.Green);
break;
case OSnapTypes.Intersection:
DrawCross(sx, sy, markerSize, Color.Green);
break;
case OSnapTypes.Perpendicular:
DrawPerpendicularMark(sx, sy, markerSize, Color.Green);
break;
}
}
}
12.4 正交模式与极轴追踪
12.4.1 正交模式
public class OrthoMode
{
public bool IsEnabled { get; set; } = false;
/// <summary>
/// 将自由点约束到正交方向
/// </summary>
public Point2d ConstrainToOrtho(Point2d basePoint,
Point2d freePoint)
{
if (!IsEnabled) return freePoint;
var dx = Math.Abs(freePoint.X - basePoint.X);
var dy = Math.Abs(freePoint.Y - basePoint.Y);
if (dx > dy)
{
// 约束到水平
return new Point2d(freePoint.X, basePoint.Y);
}
else
{
// 约束到垂直
return new Point2d(basePoint.X, freePoint.Y);
}
}
}
12.4.2 极轴追踪
public class PolarTracking
{
public bool IsEnabled { get; set; } = false;
public double IncrementAngle { get; set; } = Math.PI / 4; // 45度
/// <summary>
/// 将点约束到最近的极轴方向
/// </summary>
public Point2d? ConstrainToPolar(Point2d basePoint,
Point2d freePoint,
double tolerance)
{
if (!IsEnabled) return null;
var dir = freePoint - basePoint;
var angle = Math.Atan2(dir.Y, dir.X);
var distance = dir.Length;
// 找到最近的极轴角度
var steps = (int)Math.Round(angle / IncrementAngle);
var nearestPolarAngle = steps * IncrementAngle;
// 检查是否足够接近极轴
var angleDiff = Math.Abs(
AngleUtils.NormalizeSigned(angle - nearestPolarAngle));
var pixelDeviation = distance * Math.Sin(angleDiff);
if (pixelDeviation < tolerance)
{
// 约束到极轴方向
return new Point2d(
basePoint.X + distance * Math.Cos(nearestPolarAngle),
basePoint.Y + distance * Math.Sin(nearestPolarAngle)
);
}
return null;
}
}
12.5 视口运行时动作
12.5.1 动作接口
public interface IViewPortAction
{
string Name { get; }
void OnActivate();
void OnDeactivate();
void OnLeftClick(InputEventArgs args);
void OnRightClick(InputEventArgs args);
void OnMouseMove(InputEventArgs args);
void OnKeyDown(Keys key);
void OnConfirm();
}
12.5.2 绘制直线动作
public class DrawLineAction : IViewPortAction
{
private Point2d? firstPoint;
private LcDocument document;
private RubberBand rubberBand;
public string Name => "DrawLine";
public void OnActivate()
{
firstPoint = null;
// 提示用户"指定第一个点"
StatusBar.SetPrompt("指定第一个点:");
}
public void OnLeftClick(InputEventArgs args)
{
if (firstPoint == null)
{
// 设置第一个点
firstPoint = args.WorldPoint;
StatusBar.SetPrompt("指定下一个点:");
}
else
{
// 创建直线
var line = new LcLine
{
StartPoint = firstPoint.Value,
EndPoint = args.WorldPoint,
LayerName = document.Layers.CurrentLayer.Name,
Color = LcColor.ByLayer
};
document.Entities.Add(line);
// 连续绘制:当前终点变为下一条线的起点
firstPoint = args.WorldPoint;
StatusBar.SetPrompt("指定下一个点(或按Enter结束):");
}
}
public void OnMouseMove(InputEventArgs args)
{
rubberBand.Clear();
if (firstPoint.HasValue)
{
rubberBand.AddLine(firstPoint.Value, args.WorldPoint);
}
}
public void OnRightClick(InputEventArgs args)
{
// 右键结束绘制
OnConfirm();
}
public void OnConfirm()
{
firstPoint = null;
rubberBand.Clear();
StatusBar.SetPrompt("就绪");
}
public void OnKeyDown(Keys key) { }
public void OnDeactivate() { rubberBand.Clear(); }
}
12.5.3 选择动作
public class SelectAction : IViewPortAction
{
private Point2d? selectionStart;
private SelectionManager selectionManager;
public string Name => "Select";
public void OnLeftClick(InputEventArgs args)
{
if (selectionStart == null)
{
// 先尝试单击选择
selectionManager.PointSelect(
args.WorldPoint,
GetPickTolerance(args.Viewport),
args.Viewport.Document,
IsShiftPressed());
if (selectionManager.Count == 0)
{
// 没有选中,开始框选
selectionStart = args.WorldPoint;
}
}
else
{
// 完成框选
var box = new BoundingBox2d
{
Min = new Point2d(
Math.Min(selectionStart.Value.X, args.WorldPoint.X),
Math.Min(selectionStart.Value.Y, args.WorldPoint.Y)),
Max = new Point2d(
Math.Max(selectionStart.Value.X, args.WorldPoint.X),
Math.Max(selectionStart.Value.Y, args.WorldPoint.Y))
};
if (args.WorldPoint.X > selectionStart.Value.X)
{
// 从左到右 → 窗口选择
selectionManager.WindowSelect(box, args.Viewport.Document);
}
else
{
// 从右到左 → 交叉选择
selectionManager.CrossingSelect(box, args.Viewport.Document);
}
selectionStart = null;
}
}
public void OnMouseMove(InputEventArgs args)
{
if (selectionStart.HasValue)
{
// 绘制选择框
var rubberBand = args.Viewport.RubberBand;
rubberBand.Clear();
rubberBand.AddRectangle(selectionStart.Value, args.WorldPoint);
}
}
public void OnActivate() { }
public void OnDeactivate() { }
public void OnRightClick(InputEventArgs args) { }
public void OnKeyDown(Keys key) { }
public void OnConfirm() { }
}
12.6 命令行输入
12.6.1 命令行处理器
public class CommandLineProcessor
{
private Dictionary<string, Action> commands = new();
/// <summary>
/// 注册命令
/// </summary>
public void RegisterCommand(string name, Action action)
{
commands[name.ToUpper()] = action;
}
/// <summary>
/// 执行命令
/// </summary>
public bool ExecuteCommand(string input)
{
var parts = input.Trim().Split(' ');
var commandName = parts[0].ToUpper();
if (commands.TryGetValue(commandName, out var action))
{
action.Invoke();
return true;
}
// 尝试解析为坐标输入
if (TryParseCoordinate(input, out var point))
{
OnCoordinateInput(point);
return true;
}
// 尝试解析为距离输入
if (double.TryParse(input, out var distance))
{
OnDistanceInput(distance);
return true;
}
return false;
}
/// <summary>
/// 解析坐标输入(支持绝对和相对坐标)
/// </summary>
private bool TryParseCoordinate(string input, out Point2d point)
{
point = Point2d.Origin;
if (input.Contains(','))
{
var parts = input.Split(',');
if (parts.Length >= 2)
{
bool isRelative = parts[0].StartsWith("@");
var xStr = isRelative ? parts[0].Substring(1) : parts[0];
if (double.TryParse(xStr, out var x) &&
double.TryParse(parts[1], out var y))
{
point = new Point2d(x, y);
if (isRelative && lastPoint.HasValue)
{
point = new Point2d(
lastPoint.Value.X + x,
lastPoint.Value.Y + y);
}
return true;
}
}
}
return false;
}
}
12.7 本章小结
本章详细介绍了LightCAD的输入与交互系统。统一输入处理器(Inputer)负责分发所有鼠标和键盘事件。对象捕捉系统提供了端点、中点、圆心、交点等多种捕捉模式,确保绘图精度。正交模式和极轴追踪为精确绘图提供了约束手段。视口运行时动作系统通过命令模式实现了可扩展的操作框架。命令行输入支持坐标输入、距离输入和命令执行,提供了键盘驱动的高效工作方式。
上一章:第十一章:视图构建系统
下一章:第十三章:用户界面框架