:从开发的视角看下CAD画出那些好看的图形们)))
OpenGL渲染与几何内核那点事-项目实践理论补充一-1-1从开发的视角看下CAD画出那些好看的图形们系列文章规划我们生活中见过很多“很专业也很好看”的图如果你是最早的 CAD 开发者你会怎么做初步思路用 C 的“类”来表示图形元素问题一我想同时控制一整批线换个视角看1. 层 —— 数据的分组与“开关”问题二图里有很多重复的零件换个视角看2. 块 —— 实例化与复用问题三每个螺栓还需要不同的“属性”换个视角看3. 属性 —— 数据的载体问题四画倾斜的零件时输入坐标太痛苦换个视角看4. 坐标系 (WCS/UCS) —— 数学基与变换矩阵问题五出图时比例和布局怎么弄5. 布局 与 视口 —— 模型与视图的分离回到你最初的疑惑但是问题又来了一切都要自己存太蠢了把“描述”和“图形”分开不光层块和线型也一样但有些东西既不是实体也不是符号表回到你的角色这对你开发插件意味着什么命令行时代一次只做一件事图形界面的诱惑让工具面板“浮”起来模态对话框简单的“弹窗锁”非模态对话框麻烦开始了多文档与上下文切换开发者噩梦模态 vs 非模态设计的取舍转换思维AutoCAD 是一个数据库服务给新手的建议代码仓库入口github源码地址。gitee源码地址。系列文章规划巨人的肩膀deepseek我们生活中见过很多“很专业也很好看”的图假设我们现在看到的是一张机械装配图里面有一个复杂的齿轮箱外壳粗实线带剖面线内部齿轮细实线齿形需要精确尺寸标注带箭头、文字标题栏表格、公司名称、比例还有几个局部放大图这张图用铅笔和尺规画在 A0 图纸上要花几天但现在我们想用电脑来画。如果你是最早的 CAD 开发者你会怎么做电脑只有 64KB 内存硬盘还没普及屏幕还是单色的。你不能像今天那样随便 new 几万个对象。你得设计一个极其紧凑的数据结构还要让用户能轻松修改。初步思路用 C 的“类”来表示图形元素你定义一个基类Entity派生出Line、Circle、Arc、Text等等。每个实体记录自己的几何参数起点、终点、圆心、半径、文字内容和一些样式颜色、线型。你写了一个Draw()函数遍历所有实体一个个画到屏幕上。这看起来很不错——至少能画图了。但是很快你就遇到了麻烦。问题一我想同时控制一整批线比如我想把所有剖面线都改成红色或者把所有尺寸标注都隐藏。如果按现在的设计我得遍历全部实体判断它是不是剖面线的一部分再改颜色。这在只有几十个实体时还行但图纸一复杂上千个实体速度慢得无法忍受而且用户操作极其繁琐。你灵机一动给每个实体加一个“分组标签”不就行了这个标签就是一个字符串比如HATCH、DIM、OUTLINE。修改时我只需要把属于这个标签的所有实体找出来一次性修改它们的属性。而且我还可以给这个标签增加“全局开关”——比如隐藏整个标签组所有实体瞬间消失。这个“标签”就是层 (Layer)。在代码里它其实就是一个std::mapstring, LayerProperties每个Entity里存一个layerId指向它。这样改一个层的颜色所有属于该层的实体自动改变不需要遍历所有实体。内存节省以前每个实体都存一遍颜色、线型现在这些信息只存在层表里实体只需存一个 4 字节的 ID。换个视角看1. 层 —— 数据的分组与“开关”是什么Layer 就像是 Photoshop 里的图层或者更像 C 里的namespace命名空间std::vector的索引。为什么存在在没有层的时候画图就像在一张纸上乱画想选中所有红色的线非常困难。AutoCAD引入了“层”它本质上是一个容器。你可以规定“第0层放轮廓线黑色”“第1层放标注绿色”。对开发者的意义层决定了实体的可见性Visible、颜色Color、线型Linetype和是否可以编辑Locked。当你开发插件时不需要去遍历整个数据库找某个特定的圆只需要去特定的层里找。问题二图里有很多重复的零件你画了一个螺栓它由六边形头部、圆柱杆、螺纹线组成一共 20 条线。在装配图里这个螺栓要出现 50 次。按照现在的设计你得在内存里存 50 × 20 1000 条线的数据。这太浪费了而且如果要修改螺栓的样式你得把这 1000 条线全部找到并更新。你又灵机一动我能不能只定义一次“螺栓”的图形组合然后在图中放置 50 个“引用”每个引用只记录它放在哪里位置、旋转角度就像 C 里定义一个类然后创建多个对象。这就是块 (Block)和块引用 (Block Reference)。块定义BlockTableRecord里存一份几何数据块引用BlockReference里只存一个指向块定义的指针 变换矩阵位置、旋转、缩放。内存从 1000 条线缩减到 1 份几何定义 50 个轻量级引用。而且修改螺栓形状时只要改块定义所有引用自动更新。这就是享元模式的经典应用。换个视角看2. 块 —— 实例化与复用是什么Block 是 AutoCAD 中最精华的部分。它相当于 C 中的class类而你在图中插入的那个图形叫做Block Reference块参照相当于instance实例。前世今生早期 CAD 画螺丝、门、窗户如果每次都要复制几百个一模一样的图形文件会巨大无比。块的出现解决了这个问题定义Block Definition在内存中只存储一份几何图形的“蓝图”。引用Block Reference在图纸中放几百个“指针”它们都指向同一个蓝图。优势如果你改了蓝图重定义块几百个实例会自动更新。这完全就是面向对象编程中的类与对象关系也是设计模式中Flyweight享元模式的完美体现。问题三每个螺栓还需要不同的“属性”螺栓有规格、材质、价格、生产厂家。这些信息放在哪里如果放在块引用里每个螺栓实例都可以不同如果放在块定义里所有螺栓都一样。显然我们需要一种“实例数据”附加在块引用上。这就是属性 (Attribute)。你可以把块定义想象成 C 的类属性就是类的成员变量。每个块引用实例都有自己的属性值比如M12×50、不锈钢。这样你可以轻松提取 BOM 表把图形和数据无缝连接起来。换个视角看3. 属性 —— 数据的载体是什么Attribute 是附着在块上的struct成员变量或者是数据库中的Field。为什么存在假设你要画一张建筑图里面有 100 个“窗户”块。你不仅要看到窗户的形状几何还要知道每个窗户的“型号”、“价格”、“生产厂家”。属性就是用来干这个的。当你选中一个块参照你可以通过属性提取出“C 结构体”里存储的具体值。属性是 AutoCAD 连接“图形”和“数据MIS系统、ERP系统”的桥梁。问题四画倾斜的零件时输入坐标太痛苦你想画一个倾斜 30° 的齿条上面的齿形相对于水平线是斜的。如果用世界坐标系每个齿的坐标都要经过旋转矩阵计算不仅容易算错代码也复杂。你想我能不能临时把“坐标系”旋转一下让我以为我还在画水平线用户说“我想在这个斜面上画一条水平线。”其实这条线在绝对世界里是斜的但在用户看来就是水平的。这就是用户坐标系 (UCS)。你给用户提供一个命令让他定义一个新的 UCS选原点、X 轴方向、Y 轴方向。之后用户输入的所有点都先通过这个 UCS 的变换矩阵转成世界坐标WCS然后存储。画图的人觉得“我在画水平线”实际上数据存储的是世界坐标系下的倾斜线。UCS 本质上是一个变换矩阵它把用户习惯的局部坐标系映射到绝对存储坐标系。换个视角看4. 坐标系 (WCS/UCS) —— 数学基与变换矩阵是什么WCS (World Coordinate System)世界坐标系。这是绝对的、唯一的。你可以理解为 C 数学库中的全局原点(0,0,0)是底层数据库存储数据的唯一标准。UCS (User Coordinate System)用户坐标系。这是一个Transform Matrix变换矩阵。为什么存在现实中你画图不可能都是横平竖直的。比如你要画一个倾斜 30 度的桌子上的零件。如果你用 WCS 算点要用一大堆三角函数。有了 UCS你只需要调用AcGeMatrix3d::setToRotation告诉 AutoCAD“把你的纸转 30 度让我以为我还是在画水平线。” 然后你输入(0,0)就代表在斜面上的那个点。UCS 的本质就是一个从用户习惯到数据库存储的坐标映射。问题五出图时比例和布局怎么弄图纸画好了1:1 的模型空间里齿轮箱实际尺寸是 2 米 × 1 米。但打印要用 A3 纸420mm×297mm必须缩小 100 倍。你不想缩小图形本身因为修改时还是要按实际尺寸来。你再次灵机一动把“绘图”和“打印”分开。模型空间里永远是 1:1 的真实尺寸。打印时我们创建一个“布局”Layout它代表一张虚拟的纸。在布局上我们开一个“视口”Viewport这是一个窗口可以设置缩放比例例如 1:100透过它去看模型空间里的内容。这样你可以在同一张图纸上放多个视口一个视口看整体1:100一个视口看局部放大图1:10还可以加标题栏、图框这些也放在布局里和模型空间无关。这就是MVC 模式的直观体现模型空间 数据模型布局 视图视口 摄像机 投影变换5. 布局 与 视口 —— 模型与视图的分离是什么这是经典的MVC 模式的图形化体现。模型空间 (Model Space)相当于 C 中的数据模型。你在这里 1:1 画图不管图纸多大就像数据类本身。布局 (Layout)相当于View。它模拟一张真实的物理纸张A4、A0。视口 (Viewport)相当于摄像机。它是一个窗口透过它去看模型空间里的内容而且可以设置不同的“缩放比例”。为什么存在早期 CAD 是在模型空间里画个框框住图形来打印修改非常麻烦。后来引入了布局。这意味着你可以把“画图”和“出图”完全解耦。在模型空间画一个房子1:1在布局里你可以开一个视口看整体平面图1:100开另一个视口看卫生间的大样图1:20两者互不干扰。这完美解决了工程制图中“同一数据多比例呈现”的痛点。也有人说AutoCAD本质上是一个拥有40年历史的、极其庞大的图形数据库操作系统回到你最初的疑惑这些名词 —— 层、块、属性、UCS、布局/视口 —— 不是凭空冒出来的。它们都是早期 CAD 开发者面对内存、性能、易用性等实际问题时一步步做出的优雅设计。当你用 C 去操作 AutoCAD 时你其实是在和这套经典的数据结构打交道。理解了它们的前世今生你就会明白层是分组与属性继承的容器块是复用与享元模式的实现属性是附着在块上的结构化数据UCS是坐标变换的便捷工具布局/视口是模型与视图分离的经典实践这些设计至今仍在几乎所有 CAD 软件中沿用因为它们确实解决了本质问题。现在你可以带着这些“历史原因”去重新打开 AutoCAD点开“图层特性管理器”插入一个带属性的块切换 UCS 试试 —— 你会发现这些按钮背后都是一段段为了节省内存、方便修改而设计的精巧代码。解决了层、块、UCS 和布局这些“看得见”的功能之后你作为早期 CAD 开发者终于能画出一张像模像样的图纸了。用户也渐渐从“能画线”变成“画复杂的装配图、建筑图”。但很快你又遇到了新的麻烦——这次不是内存不够而是数据管理开始失控。但是问题又来了一切都要自己存太蠢了现在你的程序里每个Line、Circle都存着自己的颜色、线型、所在层名。比如一条红色的中心线它存着几何起点 (10,20)终点 (100,200)颜色红色线型虚线层名CENTER图里有一千条中心线每条都存一遍“红色、虚线、CENTER”。如果你想把中心线改成蓝色就得遍历这一千条线挨个改。这跟当年手工改图纸有什么区别而且你刚实现了“层”——用户明明已经在图层管理器里把CENTER层的颜色从红改成蓝了为什么线还是红的你意识到问题实体自己存属性就失去了通过“层”统一控制的意义。把“描述”和“图形”分开你开始重构数据模型。实体Entity只保留几何信息起点、终点、半径等以及一个指向“描述”的 ID。所有关于“怎么显示”的信息——颜色、线型、是否可见、是否锁定——全部抽出来放进一个专门的表里叫做符号表 (Symbol Table)。对于层你建了一个Layer Table它是一个字典像std::mapstring, LayerRecord。每个LayerRecord存储层的名字、颜色、线型、开关状态。实体里只存一个整数layerId指向Layer Table中的某个记录。这样当用户把CENTER层的颜色从红改成蓝时你只需要修改Layer Table里那一条记录。所有layerId指向它的实体下次重绘时自然就变成蓝色了。你甚至不需要知道哪些实体用了这一层——它们都通过 ID 间接引用。不光层块和线型也一样块定义也类似。你之前用“块定义 块引用”的方式节省了内存现在你把所有块定义也放进一个Block Table。每个块定义是一个BlockTableRecord里面装着组成这个块的实体列表那些线、圆、属性定义。块引用只存一个blockId以及变换矩阵。线型比如虚线、点划线同样如此。每个实体的线型不是存字符串DASHED而是存一个linetypeId指向Linetype Table里的线型定义包含线型模式、缩放比例等。你发现这种模式的好处巨大统一修改改一处处处生效。内存节省颜色、线型、层状态这些信息只存一份。查询高效想知道某个层有哪些实体不用遍历所有实体只需遍历实体时检查layerId——虽然还是得遍历但至少数据不再冗余。但有些东西既不是实体也不是符号表有一天用户想保存一些额外信息比如这张图纸的作者是谁这个插件上次运行的参数是什么某个自定义对象的扩展数据。这些东西不属于图形不能画出来也不属于系统预定义的符号表层、块、线型、文字样式等。你怎么办你可以在图纸文件末尾偷偷塞一个二进制块但那样不透明也不易扩展。于是你设计了一个字典 (Dictionary)—— 一个更通用的键值对容器。它就像 C 的std::unordered_mapstd::string, ObjectId。你可以把任何“命名对象”放进去比如AUTHOR指向一个文字对象MyPluginSettings指向一个你自己定义的配置对象。在 AutoCAD 里这个顶级字典叫命名对象字典 (Named Objects Dictionary)。它给所有开发者留了一个“万能口袋”用来存储任何不属于标准符号表的东西。回到你的角色现在当你写 C 代码用 ObjectARX时你脑子里很清楚实体看得见、可编辑、有几何的玩意。它们都派生自AcDbEntity。非实体对象符号表系统级的容器存着层的定义、块的定义、线型的定义等。字典开发者级的容器存着各种自定义数据。每次你操作一个实体比如修改颜色你实际上不是直接改实体本身而是通过它的layerId找到Layer Table里的记录再改那个记录的颜色。你明白了为什么 AutoCAD 里“改层的颜色所有对象瞬间变” —— 因为本来就是这样设计的。这对你开发插件意味着什么当你想统计一张图里所有红色的线时你不会傻乎乎地遍历每条线判断它的颜色属性。你会打开Layer Table找到所有颜色为红色的层。记录下这些层的 ID。遍历所有实体检查它们的layerId是否在那组 ID 中。这样效率更高而且更符合 AutoCAD 的数据哲学。同样当你想插入一个块时你其实是在Block Table里查找或创建块定义然后创建BlockReference指向它。当你想要保存插件配置时你打开Named Objects Dictionary创建一个自定义对象放进去。这套设计诞生于内存以 KB 计量的年代却极其优雅地解决了“数据共享、统一控制、灵活扩展”的问题。直到今天它依然是 AutoCAD 的底层骨架。理解它你就不再是一个只会“调用 API”的开发者而是真正理解了 AutoCAD 的“操作系统内核”。你看着自己设计出的这套“实体符号表字典”的数据库结构长舒一口气。数据管理终于变得清晰了图纸不再臃肿修改也能瞬间生效。你开始觉得也许 AutoCAD 真的能成为设计师们的得力助手。但很快你又遇到了新的难题——这一次不是数据怎么存而是用户怎么用。命令行时代一次只做一件事早期的 AutoCAD 只有命令行。用户敲一个命令比如LINE程序就进入“画线模式”提示用户输入起点、终点。在命令执行期间用户不能干别的——不能画圆不能改层更不能打开另一个图纸。你作为开发者代码逻辑很简单一个命令函数从头跑到尾中间穿插几次用户输入函数返回后用户才能敲下一个命令。这种模式叫做模态交互—— 程序的控制流是线性的一次只做一件事。对开发者来说这很舒服。你不需要考虑用户会不会在画线中途跑去修改图层属性因为根本不允许。图形界面的诱惑让工具面板“浮”起来随着 Windows 普及用户开始习惯鼠标点一点、右边有个面板随时改属性。他们问你“能不能像 Word 一样我一边画图右边的属性面板随时显示当前选中对象的信息我改了颜色它就立刻变”你心动了。但实现这个意味着你的程序必须允许两个东西同时运行用户在图纸上画线、选对象一个对话框属性面板始终显示着用户可以在上面修改参数这和你熟悉的命令行模式完全不同。这种对话框不会阻塞用户对 AutoCAD 主窗口的操作它叫非模态对话框 (Modeless Dialog)。模态对话框简单的“弹窗锁”你首先尝试的是模态对话框。它就像 C 里的DoModal()弹出来时用户只能操作这个对话框点不了画图区也输不了命令。这很简单你在对话框里收集用户输入比如“新建文件时选模板”点击“确定”后关闭继续执行后续代码。在这期间AutoCAD 的图形数据库是安全的因为用户没法去修改它。你把这种用于“一次性参数输入”的对话框做得很顺手。非模态对话框麻烦开始了真正让你头疼的是非模态对话框。你做了一个“快速属性面板”里面有个下拉框可以选择颜色。用户选中一条线面板显示它的颜色用户改成红色那条线就应该立即变红。但你发现当面板开着的时候用户可能切换到另一张打开的图纸新建一张图纸关闭当前图纸在执行某个命令的过程中点你的面板上的按钮你的代码必须知道此时此刻用户正在操作哪张图纸如果面板还在但用户已经关掉了图纸你的代码再去修改那个图纸里的对象程序就会崩溃。如果用户激活了第二张图纸你的面板应该显示第二张图纸里当前选中对象的信息而不是第一张的。多文档与上下文切换开发者噩梦你意识到AutoCAD 支持同时打开多个图纸MDI多文档界面每个图纸对应一个文档 (Document)每个文档有自己的图形数据库、自己的当前选中集。你的非模态对话框就像一个独立的线程它必须在正确的时间操作正确的文档。在 C 开发ObjectARX中你得学会一个概念文档锁 (Document Lock)。当你的对话框要操作某个文档时必须先锁定那个文档防止用户在操作过程中把它关了或者防止另一个对话框同时修改它。你还要监听“文档激活”事件当用户切换到另一个文档时你的对话框要立即刷新数据。这比你预想的复杂得多。你终于理解了为什么 AutoCAD 早期几乎都是模态交互——非模态需要开发者处理异步、多文档、资源竞争一不小心就 crash。模态 vs 非模态设计的取舍回过头看你总结出两者的本质区别模态对话框程序执行流被阻塞用户必须完成当前交互才能继续。适合“一次性参数输入”例如新建文件、插入块时的参数设置。在这种模式下AutoCAD 的图形数据库是安全的你不需要担心用户在对话框开着时乱改图纸。非模态对话框程序执行流不阻塞用户可以在画图和对话框操作之间来回切换。适合“实时编辑”和“状态显示”例如属性面板、工具选项板。但开发者必须处理文档上下文切换、锁定、事件同步等复杂逻辑。这不仅是 UI 设计问题更是程序架构问题。在你的 C 代码里模态就像普通的函数调用非模态就像多线程编程——你得小心地管理资源确保对话框在正确的时机访问正确的数据。转换思维AutoCAD 是一个数据库服务经历了这么多你终于学会用全新的视角看 AutoCADAutoCAD 一个运行中的图形数据库服务它管理着所有图纸数据库的读写、事务、并发。一张图纸 (DWG) 一个数据库文件里面包含实体表、符号表、字典等。实体 数据库中的记录每一条都有唯一的 ID。层表、块表 数据库的系统表Schema存储元数据。UCS 一个临时的坐标变换矩阵方便用户输入但底层存储永远是 WCS。模态/非模态 程序执行流是否阻塞也决定了你是否需要处理文档上下文切换。当你开始写插件时你会发现想删除所有红色线你不会傻到遍历所有线判断颜色而是去层表找到红色的层 ID然后遍历所有指向该层的实体高效且符合数据库索引思维。想统计图纸里所有块的属性你不会去逐个选择图形而是去块表遍历块定义再读取每个块引用的属性。想自动出图你会去操作布局和视口来调整比例和位置而不是去缩放模型空间里的图形。给新手的建议你现在明白这些名词不是 AutoCAD 的“功能”而是它的核心数据结构和交互模型。它们不是凭空发明的而是为了应对早期资源限制和用户需求一步步演变出来的优雅设计。作为 C 开发者我建议你从面向对象的角度理解这些概念把它们对应到 C 的类、继承、多态、容器。如果你刚入门可以先看 AutoCAD 的.NET API文档即使你最后用 C 开发 ObjectARX。.NET 的 API 更现代、逻辑更清晰能帮你快速建立对Database、Transaction、ObjectId等核心概念的理解。记住AutoCAD 的二次开发本质上是在操作一个图形数据库而不是简单的绘图 API。一旦你掌握了这个视角写出的代码才会高效、稳定。现在你再打开 AutoCAD点击“图层特性管理器”插入一个带属性的块或者在 UCS 下画条线——你看到的已经不是按钮而是一段段为了节省内存、方便修改、保证安全而精心设计的代码逻辑。而这些正是 CAD 软件四十年来沉淀下来的智慧。如果想了解一些成像系统、图像、人眼、颜色等等的小知识快去看看视频吧 抖音数字图像哪些好玩的事咱就不照课本念轻轻松松谝闲传快手数字图像哪些好玩的事咱就不照课本念轻轻松松谝闲传B站数字图像哪些好玩的事咱就不照课本念轻轻松松谝闲传认准一个头像保你不迷路您要是也想站在文章开头的巨人的肩膀啦可以动动您发财的小指头然后把您的想要展现的名称和公开信息发我这些信息会跟随每篇文章屹立在文章的顶部哦