新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于Catmull-Rom的曲线编辑器Demo演示

发布时间:2026/9/28 20:12:54来源:尧图网络
基于Catmull-Rom的曲线编辑器Demo演示
一、整体架构MVC 式的 Delegate 模式这套代码的核心思想是数据与逻辑分离角色谁职责数据模型ModelCurveDelegate里的std::vectorImVec2 points只存数据不画、不处理鼠标视图控制器View/ControllerImCurveEdit::Edit(...)读数据、画曲线、处理鼠标、回调 Delegate委托DelegateCurveDelegate继承ImCurveEdit::Delegate把 ImCurveEdit 的请求翻译成对数据的操作ImCurveEdit 本身不拥有数据它每一帧都通过GetPointCount()、GetPoints()、GetMin()、GetMax()从 Delegate 现拉数据画完就忘。这叫即时模式Immediate Mode和你 3D 版本用的保留模式先建树、后遍历、改节点重绘完全不同。关键好处改数据不用通知任何人下一帧 ImCurveEdit 自动看到新数据。这就是为什么 Delete 逻辑能写在 main.cpp 里、完全不用和 ImCurveEdit 通信。二、数学层曲线是怎么画出来的2.1 数据的两种表达同一批点在代码里有两个坐标系坐标系存储范围逻辑坐标points[i]存的值[min, max]由delegate.min/max决定屏幕坐标每帧实时算出来的像素单位[0, editSize]我们在main.cpp里维护了每逻辑单位 500 像素kPixelsPerUnit的映射所以max (editSize.x / 500 - 1, editSize.y / 500)这样窗口缩放时同一个逻辑点的屏幕位置按比例守恒不会拉伸——这是你之前遇到的窗口变大点会动问题的解法。2.2 逻辑坐标 → 屏幕坐标ImCurveEdit 内部用的公式main.cpp里复刻了一份保证一致range.x max.x - min.x 1 // 注意 x 多了个 1 range.y max.y - min.y // 归一化到 [0, 1] nx (point.x - min.x) / range.x ny (point.y - min.y) / range.y // 映射到屏幕像素 offset (origin.x, origin.y editSize.y) // 编辑区左下角 viewSize (editSize.x, -editSize.y) // y 是负的因为屏幕 y 向下 screen.x nx * viewSize.x offset.x screen.y ny * viewSize.y offset.yviewSize.y为负是关键逻辑 y 向上增长屏幕 y 向下增长负号把它们对齐。2.3 Catmull-Rom 插值对每相邻两点(p1, p2)看它们的前一个点p0和后一个点p3P(t) 0.5 * [ (2*p1) (-p0 p2) * t (2*p0 - 5*p1 4*p2 - p3) * t² (-p0 3*p1 - 3*p2 p3) * t³ ]为什么这样就很光滑在 t0 处P(0) p1在 t1 处P(1) p2。曲线经过所有控制点。在 t0 处的切线正比于(p2 - p0)在 t1 处的切线也正比于(p2 - p0)。由于相邻段共享端点左边那段在 p2 处的切线和右边那段在 p2 处的切线用的是同一个p3 - p1自动连续。这就是 C1 连续。相比贝塞尔曲线不需要手柄点摆哪里曲线就过哪里代价是无法表达尖角和无法单独调某段的曲率。首尾段的处理第一段没有 p0用p1自己代替最后一段没有 p3用p2自己代替。这叫端点复制clamped end。副作用是两点时曲线会轻微鼓出而不是直线——这是设计取舍为了保证和 ImCurveEdit 原版视觉一致。三、交互层四种鼠标操作3.1 拖动控制点Move PointImCurveEdit 内部维护一个selection集合记录选中的(curveIndex, pointIndex)。鼠标在控制点附近按下 → 加入 selection → 拖动时对每个选中点调用delegate.EditPoint(curveIndex,pointIndex,newValue);EditPoint是 Delegate 的回调你在里面做两件事函数模式赋值 按 x 排序 返回新索引排序后索引可能变自由模式赋值 不排序 返回原索引ImCurveEdit 拿到返回值如果索引变了就更新 selection——这是 MVC 里典型的Model 改变通知 View机制只不过这里是通过返回值实现的。3.2 双击曲线加点Add PointImCurveEdit 检测到鼠标距曲线 8 像素内双击 → 调用delegate.AddPoint(curveIndex,logicalPos);你在里面做的不是简单追加而是遍历所有相邻点对对每对算点到线段的距离平方找距离最小的段points.insert(begin bestSegment 1, value)为什么不用排序排序只在函数模式有效按 x 排恰好等于按段插入自由模式下 x 不是单调的排序会破坏点击顺序。用找最近段 中间插入两种模式行为统一也不依赖 x 单调性。3.3 框选Quad SelectionImCurveEdit 内部维护一个selectingQuad状态鼠标在空白处按下 → 记下起点拖动时画红框把框内所有控制点加入 selection松手 → 结束这跟你的 3D 版本应该很像区别只是从3D 视锥剔除退化成了2D 矩形相交测试。3.4 Delete 删除这块 ImCurveEdit 内部不做全部在 main.cpp 里实现每帧检查selectedPoints由ImCurveEdit::Edit的第五个参数返回如果检测到 Delete 键 有选中点 → 收集索引 → 从大到小排序去重从后往前points.erase(...)从后往前是因为删前面的会导致后面索引失效清空 selectedPoints关键坑ImCurveEdit 内部有一个static std::setEditPoint selection它记录的是索引。你从外部删除点后这个 set 里的旧索引会在下一帧越界访问。所以必须在ImCurveEdit.cpp的Edit()函数开头加一段越界清理foreach(curveIndex,pointIndex)in selection:ifpointIndexGetPointCount(curveIndex):remove from selection这叫防御式编程——不改数据结构而是在入口处过滤脏数据。四、渲染层ImDrawList 的即时绘制整个窗口的绘制都通过ImDrawList完成每帧重建// 曲线每两相邻点之间细分成 20 段小线段foreachsegment(p1,p2):forsub in0..19:r1CatmullRom(...)r2CatmullRom(...)dl-AddLine(screen(r1),screen(r2),green,1.3f);// 控制点菱形foreach point:dl-AddConvexPolyFilled(diamond,4,black);dl-AddPolyline(diamond,4,blue,2.0f);为什么细分成 20 段GPU 不能直接画三次曲线。用折线逼近段数多了视觉上就光滑。20 段对 500 像素的段长来说已经足够。为什么用 4 顶点菱形而不是AddCircleFilled为了和 ImCurveEdit 原版完全一致。原版用的是 4 顶点凸多边形看起来像小菱形用圆形会不一致切模式时会闪一下。五、状态层三种模式的状态机整个程序有一个顶层状态机N键 双击 EDIT -------- DRAW -------- EDIT ^ | |___________ S 键切换 ___________| 正交于上面的切换gMode只管 EDIT/DRAWdelegate.sortPointsByX只管函数/自由。两者正交可以任意组合gModesortPointsByX行为EDITtrue编辑模式 函数排序EDITfalse编辑模式 自由顺序DRAWtrue绘制模式 每点按 x 排序DRAWfalse绘制模式 保持点击顺序为什么模式切换时不重排点用户在函数模式下画了个 S 型其实被排序成了单调曲线切到自由模式如果重排曲线会突然跳到未排序的样子视觉上很突兀。保持数组顺序不变用户自己决定要不要拖动整理。为什么绘制模式下的加点也遵循模式用户在自由模式下按 S 形点击点序就是 S 形在函数模式下点击点会自动按 x 排好——两种需求都满足。六、输入层ImGui 1.87 的事件队列这块是最容易踩坑的地方也是你的 3D 版本可能没遇到过的问题ImGui 1.87 之前io.MousePos是直接赋值io.MousePos.x ...1.87 之后改为事件队列io.AddMousePosEvent(x, y)事件在下一帧NewFrame()时统一处理ImApp.h 原始代码用的是旧 API需要改。为什么如果你直接赋值io.MousePosImGui 内部会在NewFrame()里用队列里的值覆盖你的赋值——结果就是鼠标坐标永远不动一切交互失效。另一个坑IsAnyMouseButtonDown()在WM_LBUTTONUP里调用读的是io.MouseDown[]但队列还没处理读到的是上一帧的值会误判为还有键按下导致ReleaseCapture()不被调用 → 窗口标题栏失效。解法用 Win32 的GetAsyncKeyState(VK_LBUTTON)直接查物理按键绕过 ImGui 队列。七、和你 3D 版本的核心差异维度你的 3D 版本这个 2D Demo数据表示3D 点 手柄 可能是 NURBS 或 Bezier2D 点 Catmull-Rom无手柄渲染顶点缓冲 投影矩阵 深度测试ImDrawList 2D 屏幕空间直接画坐标变换4x4 矩阵含旋转、投影2x2 缩放 平移无旋转交互命中射线投射 屏幕空间距离屏幕空间欧氏距离模式通常是保留模式UI即时模式ImGui 哲学选中/框选视锥剔除矩形重叠数据存储常驻内存对象树每帧通过 Delegate 现拉本质上数学不同3D 复杂得多但架构模式相似都是数据 交互 渲染三层分离。你那边可能是 MFC/Qt 的 MVC这边是 ImGui 的即时模式 Delegate。八、一句话总结每个功能功能实现核心曲线光滑Catmull-Rom 插值段间 C1 连续不需要手柄经过控制点Catmull-Rom 数学性质P(0)p1P(1)p2窗口缩放点不动逻辑范围随编辑区尺寸动态调整像素/逻辑单位恒定双击加点找最近线段 中间插入两种模式统一函数/自由模式Delegate 里加 boolEditPoint/AddPoint 分支处理Delete 删除main.cpp 里监听按键 ImCurveEdit.cpp 里防御式清理 selection绘制模式独立状态机 自己处理鼠标 自己用 ImDrawList 画曲线窗口缩放/关闭用 Win32 原生样式去掉冲突的 WS_POPUP鼠标输入正常改用 ImGui 1.87 事件 API
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Spark 选 join 策略靠的是估算值,估错了它只会悄悄降级。 2026/9/28 20:55:27

Spark 选 join 策略靠的是估算值,估错了它只会悄悄降级。

一条 join 落在集群上,可能走四种完全不同的算法,代价差出一个量级甚至更多。 选哪一种的不是你,是 Spark 自己,依据是它手里那份统计信息。 统计信息大概率是估的,估歪了计划不会报错,只会换一条更贵的路走…

阅读更多 →
音质上乘的K歌音响品牌功能配置解析 2026/9/28 20:55:27

音质上乘的K歌音响品牌功能配置解析

在选择K歌音响的时候,很多用户会混淆“音质好”和“音量大”的区别,也容易忽略不同使用场景对功能配置的要求:比如在家用小空间K歌,音量太大可能会造成共振失真,反而影响音质;户外使用没有电源支持&#xf…

阅读更多 →
侧入式搅拌器的搅拌效率怎么提升 2026/9/28 20:55:27

侧入式搅拌器的搅拌效率怎么提升

侧入式搅拌器的搅拌效率怎么提升侧入式搅拌器是工业生产中固液混合、均质反应的核心设备,广泛应用于污水处理、化工、新能源等领域,其效率直接关系到生产能耗、物料混合均匀度及工艺稳定性。如何通过技术优化和运维调整提升侧入式搅拌效率,成…

阅读更多 →
企业 Workflow 如何接入 ERP、CRM?从 API 调用到业务契约 2026/9/28 20:55:27

企业 Workflow 如何接入 ERP、CRM?从 API 调用到业务契约

企业 Workflow 如何接入 ERP、CRM?从 API 调用到业务契约 AcmeFlow 的运营审核通过后,系统要从 CRM 找到客户与套餐,再向 ERP 创建一条维保服务记录。开发者很容易把这件事写成两次 HTTP 调用:先 GET 客户,再 POST 服务…

阅读更多 →
阳光下屏幕看不清?户外屏合适亮度尼特数实测 2026/9/28 20:55:26

阳光下屏幕看不清?户外屏合适亮度尼特数实测

做户外自助终端、充电桩、露天查询设备的同行,大概率都遇到过同一个尴尬问题:室内调试屏幕清晰通透,一拿到户外太阳直射,屏幕就发灰、发白、看不清,用户根本无法操作。很多客户经常问我,户外屏幕到底多少尼…

阅读更多 →
SpringBoot自动配置不生效?我掉进了这个连环坑 2026/9/28 20:55:20

SpringBoot自动配置不生效?我掉进了这个连环坑

上周五凌晨,我在紧急修复一个生产环境问题时,发现本应自动生效的 DataSourceAutoConfiguration 居然罢工了——没有报错,没有警告,只是静静地跳过了数据源初始化。更诡异的是,同样的配置在测试环境一切正常。你有没有遇…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉