惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Security @ Cisco Blogs
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
B
Blog RSS Feed
小众软件
小众软件
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
Microsoft Azure Blog
Microsoft Azure Blog
V
Vulnerabilities – Threatpost
The Hacker News
The Hacker News
T
Tor Project blog
A
Arctic Wolf
Jina AI
Jina AI
Hacker News: Ask HN
Hacker News: Ask HN
F
Fortinet All Blogs
Cloudbric
Cloudbric
S
Secure Thoughts
L
LINUX DO - 热门话题
博客园 - 司徒正美
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
J
Java Code Geeks
P
Privacy International News Feed
AWS News Blog
AWS News Blog
S
Securelist
TaoSecurity Blog
TaoSecurity Blog
AI
AI
O
OpenAI News
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
Know Your Adversary
Know Your Adversary
P
Palo Alto Networks Blog
T
Tenable Blog
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
S
SegmentFault 最新的问题

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
输入与交互系统
我才是银古 · 2026-06-20 · via 博客园 - 我才是银古

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

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


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

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