znlgis 博客

GIS开发与技术分享

第十二章:输入与交互系统

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)负责分发所有鼠标和键盘事件。对象捕捉系统提供了端点、中点、圆心、交点等多种捕捉模式,确保绘图精度。正交模式和极轴追踪为精确绘图提供了约束手段。视口运行时动作系统通过命令模式实现了可扩展的操作框架。命令行输入支持坐标输入、距离输入和命令执行,提供了键盘驱动的高效工作方式。


上一章第十一章:视图构建系统

下一章第十三章:用户界面框架