第十章:二维绘图与视口管理
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二维绘图交互的基础。
下一章:第十一章:视图构建系统