znlgis 博客

GIS开发与技术分享

第十章:二维绘图与视口管理

10.1 绘图模块概述

10.1.1 LightCAD.Drawing的定位

LightCAD.Drawing是CAD系统中负责二维绘图和视口管理的核心模块。它位于RenderUtils之上、Runtime之下,处理所有与视口显示、用户输入和对象交互相关的功能。

10.1.2 模块结构

LightCAD.Drawing/
├── InputSys/                # 输入系统
│   ├── Inputer.cs           # 统一输入处理器
│   ├── MouseInput.cs        # 鼠标输入处理
│   └── KeyInput.cs          # 键盘输入处理
├── Snap/                    # 捕捉系统
│   ├── SnapManager.cs       # 捕捉管理器
│   ├── SnapPoint.cs         # 捕捉点定义
│   ├── OSnapTypes.cs        # 对象捕捉类型
│   └── GridSnap.cs          # 栅格捕捉
├── PViewPort/               # 视口组件
│   ├── ViewportRenderer.cs  # 视口渲染器
│   ├── ViewportManager.cs   # 视口管理器
│   ├── PanZoomController.cs # 平移缩放控制
│   └── ViewportConfig.cs    # 视口配置
└── ViewPortRtAction/        # 视口运行时动作
    ├── DrawAction.cs        # 绘图动作
    ├── SelectAction.cs      # 选择动作
    ├── EditAction.cs        # 编辑动作
    └── MeasureAction.cs     # 测量动作

10.2 视口系统

10.2.1 视口管理器

public class ViewportManager
{
    private List<Viewport> viewports = new();
    private Viewport activeViewport;

    /// <summary>
    /// 创建新视口
    /// </summary>
    public Viewport CreateViewport(ViewportConfig config)
    {
        var viewport = new Viewport
        {
            Id = GenerateViewportId(),
            Bounds = config.Bounds,
            Camera = CreateCamera(config),
            BackgroundColor = config.BackgroundColor
        };

        viewports.Add(viewport);
        return viewport;
    }

    /// <summary>
    /// 设置活动视口
    /// </summary>
    public void SetActiveViewport(Viewport viewport)
    {
        activeViewport?.OnDeactivated();
        activeViewport = viewport;
        activeViewport.OnActivated();
    }

    /// <summary>
    /// 获取鼠标位置对应的视口
    /// </summary>
    public Viewport GetViewportAtPoint(int screenX, int screenY)
    {
        return viewports.FirstOrDefault(v =>
            v.ContainsScreenPoint(screenX, screenY));
    }
}

public class Viewport
{
    public int Id { get; set; }
    public Rectangle Bounds { get; set; }
    public Camera Camera { get; set; }
    public LcColor BackgroundColor { get; set; }

    // 坐标转换
    public Point2d ScreenToWorld(int screenX, int screenY)
    {
        var normalizedX = (double)(screenX - Bounds.X) / Bounds.Width;
        var normalizedY = 1.0 - (double)(screenY - Bounds.Y) / Bounds.Height;

        return new Point2d(
            Camera.Left + normalizedX * (Camera.Right - Camera.Left),
            Camera.Bottom + normalizedY * (Camera.Top - Camera.Bottom)
        );
    }

    public (int x, int y) WorldToScreen(Point2d worldPoint)
    {
        var normalizedX = (worldPoint.X - Camera.Left) /
            (Camera.Right - Camera.Left);
        var normalizedY = (worldPoint.Y - Camera.Bottom) /
            (Camera.Top - Camera.Bottom);

        return (
            Bounds.X + (int)(normalizedX * Bounds.Width),
            Bounds.Y + (int)((1 - normalizedY) * Bounds.Height)
        );
    }

    public bool ContainsScreenPoint(int x, int y)
    {
        return x >= Bounds.X && x < Bounds.X + Bounds.Width
            && y >= Bounds.Y && y < Bounds.Y + Bounds.Height;
    }
}

10.2.2 视口渲染器

public class ViewportRenderer
{
    private SceneManager sceneManager;
    private RenderMaterialManager materialManager;

    /// <summary>
    /// 渲染视口内容
    /// </summary>
    public void Render(Viewport viewport, LcDocument document)
    {
        // 1. 计算可见范围
        var visibleBounds = viewport.GetVisibleBounds();

        // 2. 收集可见实体
        var visibleEntities = document.Entities
            .FindInBounds(visibleBounds)
            .Where(e => e.IsVisible)
            .Where(e => IsLayerVisible(e.LayerName, document.Layers));

        // 3. 渲染网格(如果启用)
        if (viewport.ShowGrid)
        {
            RenderGrid(viewport);
        }

        // 4. 按图层排序渲染实体
        var sortedEntities = visibleEntities
            .OrderBy(e => GetLayerOrder(e.LayerName))
            .ThenBy(e => e.Handle);

        foreach (var entity in sortedEntities)
        {
            RenderEntity(viewport, entity);
        }

        // 5. 渲染选择集高亮
        RenderSelection(viewport);

        // 6. 渲染临时几何(橡皮筋等)
        RenderTemporaryGeometry(viewport);

        // 7. 渲染捕捉标记
        RenderSnapMarkers(viewport);

        // 8. 渲染光标
        RenderCursor(viewport);
    }

    /// <summary>
    /// 渲染单个实体
    /// </summary>
    private void RenderEntity(Viewport viewport, LcEntity entity)
    {
        var color = ResolveColor(entity);
        var lineType = ResolveLineType(entity);
        var lineWeight = ResolveLineWeight(entity);

        switch (entity)
        {
            case LcLine line:
                DrawLine(viewport, line, color, lineWeight);
                break;
            case LcCircle circle:
                DrawCircle(viewport, circle, color, lineWeight);
                break;
            case LcArc arc:
                DrawArc(viewport, arc, color, lineWeight);
                break;
            case LcPolyline polyline:
                DrawPolyline(viewport, polyline, color, lineWeight);
                break;
            case LcText text:
                DrawText(viewport, text, color);
                break;
            case LcSolid3d solid:
                DrawSolid(viewport, solid);
                break;
        }
    }

    /// <summary>
    /// 渲染网格
    /// </summary>
    private void RenderGrid(Viewport viewport)
    {
        var gridSpacing = CalculateGridSpacing(viewport.ZoomLevel);
        var bounds = viewport.GetVisibleBounds();

        var startX = Math.Floor(bounds.Min.X / gridSpacing) * gridSpacing;
        var startY = Math.Floor(bounds.Min.Y / gridSpacing) * gridSpacing;

        var gridColor = new LcColor(200, 200, 200);
        var axisColor = new LcColor(150, 150, 150);

        // 绘制网格线
        for (var x = startX; x <= bounds.Max.X; x += gridSpacing)
        {
            var color = Math.Abs(x) < 1e-10 ? axisColor : gridColor;
            DrawScreenLine(viewport,
                new Point2d(x, bounds.Min.Y),
                new Point2d(x, bounds.Max.Y),
                color, 1);
        }

        for (var y = startY; y <= bounds.Max.Y; y += gridSpacing)
        {
            var color = Math.Abs(y) < 1e-10 ? axisColor : gridColor;
            DrawScreenLine(viewport,
                new Point2d(bounds.Min.X, y),
                new Point2d(bounds.Max.X, y),
                color, 1);
        }
    }
}

10.3 平移与缩放

10.3.1 平移缩放控制器

public class PanZoomController
{
    private Viewport viewport;
    private bool isPanning = false;
    private Point2d panStartWorld;
    private int panStartScreenX, panStartScreenY;

    /// <summary>
    /// 缩放级别
    /// </summary>
    public double ZoomLevel { get; private set; } = 1.0;

    /// <summary>
    /// 鼠标滚轮缩放
    /// </summary>
    public void ZoomAtPoint(int screenX, int screenY, double zoomFactor)
    {
        // 获取缩放前鼠标位置的世界坐标
        var worldPoint = viewport.ScreenToWorld(screenX, screenY);

        // 计算新的缩放级别
        var newZoom = ZoomLevel * zoomFactor;
        newZoom = Math.Max(0.001, Math.Min(100000, newZoom));

        // 调整相机使得缩放中心点不移动
        var ratio = ZoomLevel / newZoom;
        var cameraCenter = viewport.Camera.Center;

        viewport.Camera.Center = new Point2d(
            worldPoint.X + (cameraCenter.X - worldPoint.X) * ratio,
            worldPoint.Y + (cameraCenter.Y - worldPoint.Y) * ratio
        );

        ZoomLevel = newZoom;
        viewport.Camera.UpdateExtents(newZoom);
    }

    /// <summary>
    /// 缩放到全部(显示所有实体)
    /// </summary>
    public void ZoomExtents(BoundingBox2d entityBounds)
    {
        var padding = 0.1; // 10%的边距
        var width = entityBounds.Width * (1 + padding);
        var height = entityBounds.Height * (1 + padding);

        var viewAspect = (double)viewport.Bounds.Width / viewport.Bounds.Height;
        var entityAspect = width / height;

        if (entityAspect > viewAspect)
        {
            // 以宽度为准
            ZoomLevel = viewport.Bounds.Width / width;
        }
        else
        {
            // 以高度为准
            ZoomLevel = viewport.Bounds.Height / height;
        }

        viewport.Camera.Center = entityBounds.Center;
        viewport.Camera.UpdateExtents(ZoomLevel);
    }

    /// <summary>
    /// 开始平移
    /// </summary>
    public void BeginPan(int screenX, int screenY)
    {
        isPanning = true;
        panStartScreenX = screenX;
        panStartScreenY = screenY;
        panStartWorld = viewport.ScreenToWorld(screenX, screenY);
    }

    /// <summary>
    /// 执行平移
    /// </summary>
    public void DoPan(int screenX, int screenY)
    {
        if (!isPanning) return;

        var currentWorld = viewport.ScreenToWorld(screenX, screenY);
        var delta = panStartWorld - currentWorld;

        viewport.Camera.Center = new Point2d(
            viewport.Camera.Center.X + delta.X,
            viewport.Camera.Center.Y + delta.Y
        );
    }

    /// <summary>
    /// 结束平移
    /// </summary>
    public void EndPan()
    {
        isPanning = false;
    }
}

10.4 选择系统

10.4.1 选择管理器

public class SelectionManager
{
    private HashSet<LcEntity> selectedEntities = new();

    /// <summary>
    /// 选择集变更事件
    /// </summary>
    public event EventHandler<SelectionChangedArgs> SelectionChanged;

    /// <summary>
    /// 单击选择
    /// </summary>
    public void PointSelect(Point2d worldPoint, double tolerance,
                            LcDocument document, bool addToSelection = false)
    {
        if (!addToSelection)
        {
            ClearSelection();
        }

        // 查找点击位置附近的实体
        var hitEntity = document.Entities
            .Where(e => e.IsVisible)
            .OrderBy(e => e.DistanceTo(worldPoint))
            .FirstOrDefault(e => e.DistanceTo(worldPoint) <= tolerance);

        if (hitEntity != null)
        {
            ToggleSelect(hitEntity);
        }
    }

    /// <summary>
    /// 窗口选择(从左到右拖动)
    /// </summary>
    public void WindowSelect(BoundingBox2d window, LcDocument document,
                             bool addToSelection = false)
    {
        if (!addToSelection)
        {
            ClearSelection();
        }

        foreach (var entity in document.Entities)
        {
            if (entity.IsVisible &&
                window.Contains(entity.Bounds.Min) &&
                window.Contains(entity.Bounds.Max))
            {
                Select(entity);
            }
        }
    }

    /// <summary>
    /// 交叉选择(从右到左拖动)
    /// </summary>
    public void CrossingSelect(BoundingBox2d window, LcDocument document,
                               bool addToSelection = false)
    {
        if (!addToSelection)
        {
            ClearSelection();
        }

        foreach (var entity in document.Entities)
        {
            if (entity.IsVisible &&
                entity.Bounds.Intersects(window))
            {
                Select(entity);
            }
        }
    }

    public void Select(LcEntity entity)
    {
        if (selectedEntities.Add(entity))
        {
            SelectionChanged?.Invoke(this,
                new SelectionChangedArgs(entity, true));
        }
    }

    public void Deselect(LcEntity entity)
    {
        if (selectedEntities.Remove(entity))
        {
            SelectionChanged?.Invoke(this,
                new SelectionChangedArgs(entity, false));
        }
    }

    public void ClearSelection()
    {
        var previous = selectedEntities.ToList();
        selectedEntities.Clear();
        foreach (var entity in previous)
        {
            SelectionChanged?.Invoke(this,
                new SelectionChangedArgs(entity, false));
        }
    }

    public IReadOnlyCollection<LcEntity> SelectedEntities => selectedEntities;
    public int Count => selectedEntities.Count;
    public bool IsSelected(LcEntity entity) => selectedEntities.Contains(entity);
}

10.5 夹点编辑

10.5.1 夹点系统

夹点(Grip)是实体上的可拖动控制点,允许用户通过直接拖动来编辑实体的几何形状:

public class GripPoint
{
    public Point2d Position { get; set; }
    public GripType Type { get; set; }
    public LcEntity Owner { get; set; }
    public int Index { get; set; }
    public Action<Point2d> OnDrag { get; set; }
}

public enum GripType
{
    Vertex,     // 顶点夹点
    MidPoint,   // 中点夹点
    Center,     // 中心夹点
    Quadrant,   // 象限点夹点
    Control     // 控制点夹点
}

public class GripManager
{
    private List<GripPoint> activeGrips = new();
    private GripPoint draggedGrip = null;

    /// <summary>
    /// 为实体生成夹点
    /// </summary>
    public List<GripPoint> GetGrips(LcEntity entity)
    {
        return entity switch
        {
            LcLine line => GetLineGrips(line),
            LcCircle circle => GetCircleGrips(circle),
            LcArc arc => GetArcGrips(arc),
            LcPolyline polyline => GetPolylineGrips(polyline),
            _ => new List<GripPoint>()
        };
    }

    private List<GripPoint> GetLineGrips(LcLine line)
    {
        return new List<GripPoint>
        {
            new GripPoint
            {
                Position = line.StartPoint,
                Type = GripType.Vertex,
                Owner = line,
                Index = 0,
                OnDrag = p => line.StartPoint = p
            },
            new GripPoint
            {
                Position = line.EndPoint,
                Type = GripType.Vertex,
                Owner = line,
                Index = 1,
                OnDrag = p => line.EndPoint = p
            },
            new GripPoint
            {
                Position = line.MidPoint,
                Type = GripType.MidPoint,
                Owner = line,
                Index = 2,
                OnDrag = p =>
                {
                    var offset = p - line.MidPoint;
                    line.StartPoint = line.StartPoint + offset;
                    line.EndPoint = line.EndPoint + offset;
                }
            }
        };
    }

    private List<GripPoint> GetCircleGrips(LcCircle circle)
    {
        return new List<GripPoint>
        {
            new GripPoint
            {
                Position = circle.Center,
                Type = GripType.Center,
                Owner = circle,
                OnDrag = p => circle.Center = p
            },
            new GripPoint
            {
                Position = circle.PointAt(0),
                Type = GripType.Quadrant,
                Owner = circle,
                OnDrag = p =>
                    circle.Radius = circle.Center.DistanceTo(p)
            },
            new GripPoint
            {
                Position = circle.PointAt(Math.PI / 2),
                Type = GripType.Quadrant,
                Owner = circle,
                OnDrag = p =>
                    circle.Radius = circle.Center.DistanceTo(p)
            },
            new GripPoint
            {
                Position = circle.PointAt(Math.PI),
                Type = GripType.Quadrant,
                Owner = circle,
                OnDrag = p =>
                    circle.Radius = circle.Center.DistanceTo(p)
            },
            new GripPoint
            {
                Position = circle.PointAt(3 * Math.PI / 2),
                Type = GripType.Quadrant,
                Owner = circle,
                OnDrag = p =>
                    circle.Radius = circle.Center.DistanceTo(p)
            }
        };
    }
}

10.6 线型系统

10.6.1 线型定义

public class LineTypeDefinition
{
    public string Name { get; set; }
    public string Description { get; set; }
    public double[] Pattern { get; set; }

    // 预定义线型
    public static readonly LineTypeDefinition Continuous =
        new LineTypeDefinition
        {
            Name = "Continuous",
            Description = "实线",
            Pattern = Array.Empty<double>()
        };

    public static readonly LineTypeDefinition Dashed =
        new LineTypeDefinition
        {
            Name = "Dashed",
            Description = "虚线",
            Pattern = new[] { 10.0, -5.0 } // 画10,空5
        };

    public static readonly LineTypeDefinition DashDot =
        new LineTypeDefinition
        {
            Name = "DashDot",
            Description = "点画线",
            Pattern = new[] { 10.0, -3.0, 1.0, -3.0 }
        };

    public static readonly LineTypeDefinition Center =
        new LineTypeDefinition
        {
            Name = "Center",
            Description = "中心线",
            Pattern = new[] { 20.0, -5.0, 5.0, -5.0 }
        };

    public static readonly LineTypeDefinition Hidden =
        new LineTypeDefinition
        {
            Name = "Hidden",
            Description = "隐藏线",
            Pattern = new[] { 5.0, -3.0 }
        };
}

10.7 橡皮筋绘制

10.7.1 临时图形系统

在绘制图元过程中,需要显示跟随鼠标移动的临时图形(橡皮筋效果):

public class RubberBand
{
    private List<TemporaryGraphic> temporaryGraphics = new();

    /// <summary>
    /// 添加临时直线
    /// </summary>
    public void AddLine(Point2d start, Point2d end)
    {
        temporaryGraphics.Add(new TemporaryGraphic
        {
            Type = TempGraphicType.Line,
            Points = new[] { start, end },
            Color = new LcColor(0, 150, 255)
        });
    }

    /// <summary>
    /// 添加临时圆
    /// </summary>
    public void AddCircle(Point2d center, double radius)
    {
        temporaryGraphics.Add(new TemporaryGraphic
        {
            Type = TempGraphicType.Circle,
            Center = center,
            Radius = radius,
            Color = new LcColor(0, 150, 255)
        });
    }

    /// <summary>
    /// 添加临时矩形
    /// </summary>
    public void AddRectangle(Point2d corner1, Point2d corner2)
    {
        temporaryGraphics.Add(new TemporaryGraphic
        {
            Type = TempGraphicType.Rectangle,
            Points = new[] { corner1, corner2 },
            Color = new LcColor(0, 150, 255)
        });
    }

    /// <summary>
    /// 清除所有临时图形
    /// </summary>
    public void Clear()
    {
        temporaryGraphics.Clear();
    }

    /// <summary>
    /// 渲染临时图形
    /// </summary>
    public void Render(ViewportRenderer renderer)
    {
        foreach (var graphic in temporaryGraphics)
        {
            renderer.DrawTemporary(graphic);
        }
    }
}

10.8 本章小结

本章详细介绍了LightCAD的二维绘图和视口管理系统。视口系统提供了坐标转换、平移缩放、选择管理等核心功能。夹点编辑系统允许用户通过拖拽控制点直接编辑图元。线型系统支持实线、虚线、点画线等多种线型。橡皮筋绘制为交互式绘图提供了实时视觉反馈。这些系统共同构成了LightCAD二维绘图交互的基础。


上一章第九章:渲染系统与Three.js集成

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