news 2026/9/26 8:49:52

undo_manager源码解析:从命令模式到多步撤销的编辑器架构设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
undo_manager源码解析:从命令模式到多步撤销的编辑器架构设计

简介:撤销/重做管理器源码包是一套面向桌面文本编辑器和富文本控件开发者的功能实现参考,适合需要在自定义编辑器或文档应用中集成 Undo/Redo 机制的中级程序员。压缩包共 61 个文件、70KB,以 C++ 头文件和实现文件为主(31 个 .h、28 个 .cpp),另含 1 个变更日志和 1 个资源文件,覆盖操作封装、编辑视图、菜单交互等模块。核心撤销管理器利用操作历史栈统一管理键入、替换、拖放与段落样式等动作,编辑器视图负责捕获用户输入并与撤销管理器联动,具体操作则被封装为可回滚对象,从而支持多步撤销和重做。已有 154 人学习下载。通过阅读这套轻量源码,可以直观理解命令模式在文本编辑中的应用,掌握撤销栈的构建思路,并进一步扩展到复杂编辑操作的事务化回滚设计。

1. 给编辑器加 Ctrl+Z 不是挂个栈就行:undo_manager 这套源码到底管到哪一层

做过文本编辑器的人应该都有这种经历:刚接上 Undo/Redo 那会儿觉得特别简单,不就是 push/pop 外加一个下标吗。等真正把打字、替换、拖放、样式修改全部灌进去,你会发现一次操作被拆成了好几段,Undo 一步退回去的状态根本不对,Redo 到一半还会把界面弄花。这个 undo_manager 压缩包提供的就是一套从底层操作封装到上层菜单联动的完整撤销/重做框架,核心在 UndoManager、Action 子类、以及 RicherEditView / BetterEditView 两个编辑视图的协作。

它适合正在做文本编辑器、RichEdit 增强、或者想在现有 MFC 工程里补撤销功能的开发者。压缩包里 Action、REAction、BEAction 三个文件组表明它对"普通编辑"和"增强编辑"两套视图做了统一的动作抽象,REReplaceAction、REDropAction、REStyleAction 这些文件又告诉我们替换、拖放、样式变更这类非文本操作也是用它来记录的。这套源码不是教学 demo,是从真实编辑工程里抽出来的骨架,值得你花一个晚上把它跑通,然后照着改。

2. UndoManager 核心:操作栈、游标与命令模式的落地

2.1 栈不是唯一的正确结构:为什么需要一个 undo 游标

直觉上做撤销就是维护一个后进先出的栈,但真在编辑器里试过就会发现一个关键问题:用户先撤销了三次,然后从那个中间状态开始输入了新内容,此时之前撤销掉的三步还留在栈顶。如果只是简单弹栈,Redo 会把已经被新操作"覆盖"掉的旧内容恢复出来,文档状态就乱了。

所以 UndoManager 内部维护的是"栈 + 游标"结构。

// UndoManager.h 核心结构(按常见做法整理) class UndoManager { public: bool CanUndo() const { return m_undoIndex > 0; } bool CanRedo() const { return m_undoIndex < m_actions.size(); } void AddAction(std::shared_ptr<Action> action); void Undo(); void Redo(); void ClearHistory(); private: std::vector<std::shared_ptr<Action>> m_actions; size_t m_undoIndex = 0; // 指向下一个可撤销的位置 size_t m_maxDepth = 1000; // 历史上限 };

这个结构中m_actions是总的历史记录,m_undoIndex是当前状态与历史记录的"分界线"。CanUndo()判断游标是否大于 0,CanRedo()判断游标是否没到数组末尾。关键点在AddAction的逻辑:新动作进来时,要先丢弃游标右侧那些已撤销的动作,否则 Redo 的和新动作会掺杂在一起。

void UndoManager::AddAction(std::shared_ptr<Action> action) { // 丢弃被撤销掉的分支 if (m_undoIndex < m_actions.size()) { m_actions.erase(m_actions.begin() + m_undoIndex, m_actions.end()); } m_actions.push_back(action); // 限制历史深度,超过上限时丢弃最旧的动作 if (m_actions.size() > m_maxDepth) { m_actions.erase(m_actions.begin()); } m_undoIndex = m_actions.size(); }

这里要说明两个参数的实际作用。m_maxDepth设为 1000 不是拍脑袋,编辑场景下用户很少会连续撤销几百步,但 Undo 栈里的每个Action都可能持有大段文本快照,不做上限的话,长文档编辑半小时内存就上去了。m_undoIndex的更新时机也很重要:它必须始终指向"下一个可撤销的位置",也就是当前文档状态对应的历史边界。只有当Undo和Redo方法里同步移动游标,AddAction才能保证新动作落在正确的位置。

2.2 命令模式:把编辑动作封装成 Action,而不是直接改控件

UndoManager 本身不关心你编辑的是文本框、表格还是段落样式,它只认统一的 Action 接口。这就是命令模式的核心价值:每个用户操作被封装成一个对象,对象内部记录"怎么做"和"怎么还原"。

// Action.h:动作基类(按压缩包中 Action.h 的角色设计) class Action { public: virtual ~Action() {} // 执行一遍,内部完成实际修改 virtual void Execute() = 0; // 撤销,把文档状态恢复到执行前 virtual void Undo() = 0; // 重做,再次执行,但要保证幂等 virtual void Redo() = 0; // 合并判定:新的输入是否能并入当前动作,减少栈帧 virtual bool CanMerge(const ActionPtr& next) const = 0; // 返回菜单/日志里显示的动作描述 virtual std::wstring GetDescription() const = 0; };

这里最容易被忽略的是CanMerge。想象用户按住键盘输入一行字,如果每个WM_CHAR都生成一个独立的 Action,按下 Ctrl+Z 想删掉整行就得敲几十次。这个压缩包里 RETypingAction.cpp 和 BETypingAction.cpp 的职责就是解决这个问题,它们通过CanMerge判断相邻两次输入是否属于同一段连续键入,如果是就合并到同一个 Action 里,撤销时一次退回整段。

2.3 注册、执行与回滚:UndoManager 的接口设计

以 RETypingAction 为例,看一个具体动作在 UndoManager 里怎么流转。

void RicherEditView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // 构造动作对象,记录插入位置和文本 auto action = std::make_shared<RETypingAction>( this, m_caretPos, m_textBuffer, CString((TCHAR)nChar)); // 尝试与栈顶动作合并,失败则入新栈帧 if (!m_undoManager->TryMergeWithTop(action)) { m_undoManager->AddAction(action); } action->Execute(); // 更新界面光标与滚动位置 UpdateCaretPos(); }

这里TryMergeWithTop做的事情是:拿到m_actions[m_undoIndex - 1]指向的动作对象,调用它的CanMerge(action),如果返回 true 就把这次输入的文本累加到旧 Action 里,而不是新建一个。这种"先合并、后入栈"的时序是保证撤销粒度合理的关键。如果先执行再合并,动作对象里已经保存了修改前后两份文本,合并时还得处理快照冲突,麻烦得多。

Execute()被调用的时机也有讲究:动作对象先入栈、后执行,这样即使执行过程中抛出异常,UndoManager 里已经存在该动作,后续Undo()仍然可以把它回滚掉,不会出现"文档改了但历史里没有记录"的脏状态。

3. Action 子类怎么拆:打字、替换、段落样式、拖放各管一摊

3.1 RETypingAction / BETypingAction:合并同源输入,避免一次击键一个栈帧

两个文件成对出现不是冗余。RE 前缀对应 RicherEditView 的普通输入路径,BE 前缀对应 BetterEditView 的增强输入路径。两种视图的输入来源不同,但动作封装逻辑几乎一样。

// RETypingAction.cpp:简化后的合并逻辑 bool RETypingAction::CanMerge(const ActionPtr& next) const { auto pNext = std::dynamic_pointer_cast<RETypingAction>(next); if (!pNext) return false; // 连续输入判定:新动作的插入起点必须紧贴当前末尾 if (pNext->m_start != m_start + m_insertedText.GetLength()) return false; // 时间相近才合并,防止"插入-等待-插入"被粘成一坨 if (pNext->m_tick - m_tick > MERGE_INTERVAL_MS) return false; return true; } void RETypingAction::Merge(const ActionPtr& next) { auto pNext = std::dynamic_pointer_cast<RETypingAction>(next); m_insertedText += pNext->m_insertedText; m_tick = pNext->m_tick; }

两个参数值得注意。MERGE_INTERVAL_MS是合并窗口,常见做法是 1000 毫秒到 1500 毫秒。窗口太短,用户输入稍微停顿一下就被拆帧,撤销次数爆炸;窗口太长,用户在文本框里先打几个字、停几秒再打几个字,想只撤销第二次输入都不行,只能全退。我自己一般用 1200 毫秒,配合"字符间隔"判断,能覆盖大部分人的输入节奏。m_start记录的是插入起点,合并时必须校验新动作起点是否紧贴当前末尾,这个校验能防止把用户在某段文本中间补插的字符错误合并。

3.2 REReplaceAction:替换不是删除再插入,是一对原子操作

很多人实现"查找替换"的撤销时,把替换拆成"删除旧文本 + 插入新文本"两个动作推进 UndoManager,结果 Redo 的时候界面会先闪一下删除状态,再闪一下插入结果,视觉上就像文档跳了两下。真正可控的做法是让 REReplaceAction 自己携带替换前后的完整文本,一次动作完成所有事。

// REReplaceAction.cpp:替换动作的构造与执行 REReplaceAction::REReplaceAction(REView* view, const TextRange& range, const CString& newText) : m_view(view) , m_range(range) , m_newText(newText) { m_oldText = view->GetText(range); // 构造时读取旧文本 } void REReplaceAction::Undo() { // 还原旧文本,替换掉新文本 m_view->SetText(m_range, m_oldText); } void REReplaceAction::Redo() { // 再次写入新文本,位置不变 m_view->SetText(m_range, m_newText); }

构造时把m_oldText从视图中取出来缓存,这是最重要的一步。如果等到 Undo 执行时才去读旧文本,那时文档已经被新文本覆盖,原始内容已经找不回来了。TextRange在这里充当"位置定锚",它记录了起始位置和长度,前提是替换操作没有改变其前方的文本长度。这也是为什么替换操作必须被当做一个原子动作——只要中间插入一次回车或删除,TextRange 记录的偏移量可能整体失效。

3.3 REParagraphAction / REStyleAction:对段落和样式的修改怎么回滚

这类动作和文本输入最大的不同在于,它们的操作对象不是连续的字符区间,而是样式对象和段落对象。压缩包里 ParagraphTable、ParagraphStyle、ObjectTable 这些文件就是为这类操作服务的。

操作类型记录的关键数据Undo 恢复策略
段落属性修改段落索引 + 旧ParagraphStyle + 新ParagraphStyle把旧样式写回段落索引处
字符样式修改字符范围 + 旧TextStyle + 新TextStyle在范围内恢复旧 TextStyle
插入文本对象对象指针 + 插入位置从容器中移除该对象
删除文本对象对象指针 + 原始位置 + 前后关联按关联关系插回原位

段落索引的引用要格外小心。如果用户先删掉了第 5 段,再对第 10 段做样式操作,之前记录的"段落索引"已经指向了错误的段。所以 REParagraphAction 里通常还会保存一个段落的唯一标识(比如段落对象的指针),Undo 时先按指针找回段落,再验证索引有效性。这在你接手这套源码时会看到相关的ParagraphTable文件,它就是用来维护段落与样式的映射关系的。

3.4 参数记录与 TextRange:为什么用 TextRange 比裸 start+length 更安全

TextRange.cpp 单独存在是有原因的。真实编辑过程里,一个函数从接收输入到落盘,中间要经过键盘处理、IME 组合、命令分发好几层。如果每层都用int start和int length两个参数传递,某个中间层把 start 加了 1,后面的替换逻辑就全错了。

// TextRange.h 简化定义 struct TextRange { size_t start; size_t length; TextRange() : start(0), length(0) {} TextRange(size_t s, size_t l) : start(s), length(l) {} // 区间合法性 bool IsValid() const { return length > 0; } // 转为字符串位置 LPCTSTR GetTextStart(Buffer* buf) const { return buf->GetText() + start; } };

把 start 和 length 打包成一个对象后,编译器会强制你在修改位置时明确意识:我改的是整个 TextRange,而不是顺手调一下数字。这套源码里 TextRange 还会在 Undo/Redo 时参与"偏移量修正"——删除操作发生后,位于其后的所有 TextRange 都要整体减少对应长度,这个修正逻辑维护起来比散落的两个 int 清晰得多。

4. 视图层与菜单接入:把动作流装进 RicherEditView 和多步撤销菜单

4.1 RicherEditView / BetterEditView:键盘事件、IME 组合输入和焦点管理的接入点

压缩包里RicherEditView.cpp与BetterEditView.cpp是两个编辑视图的实现,它们不自己存文本,而是通过调用 UndoManager 来记录一切修改动作。视图层最重要的职责是拦截输入事件,把它翻译成 Action 对象。

// RicherEditView.cpp:键盘输入转换为动作 BOOL RicherEditView::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_BACK) { TextRange range = GetSelectionRange(); if (!range.IsValid()) { // 光标前一个字符 range.start = m_caretPos - 1; range.length = 1; } auto action = std::make_shared<RETypingAction>( this, range, CString(_T(""))); m_undoManager->AddAction(action); action->Execute(); return TRUE; } return CView::PreTranslateMessage(pMsg); }

这里有两个细节值得琢磨。第一,退格键在 PreTranslateMessage 里拦截,而不是等 EN_CHANGE 通知,因为 EN_CHANGE 只在控件内容变化后触发,那时已经拿不到"删除前的文本内容",无法为 Undo 记录旧值。第二,RETypingAction的构造参数里既传了range又传了空字符串,这代表"删除"在底层被统一看作"插入空文本的替换操作",这样 RetingAction 就能同时覆盖字符插入和字符删除,UndoManager 侧不需要另写一套删除逻辑。

4.2 SimpleUndoRedoMenu 与 MultipleUndoRedoMenu:两级菜单实现思路对比

压缩包里同时出现SimpleUndoRedoMenu和MultipleUndoRedoMenu,这是两种不同粒度的用户交互方案。Simple 版只提供"撤销一步 / 重做一步"两个按钮,实现最直接;Multiple 版则要在菜单里列出最近 N 条操作,用户可以直接选择回退到任意一步。

// MultipleUndoRedoMenu.cpp:构建下拉列表的简化逻辑 void MultipleUndoRedoMenu::BuildMenu(CMenu* pMenu, UndoManager* mgr) { pMenu->AppendMenu(MF_STRING, ID_UNDO_LAST, _T("撤销")); int count = 0; std::vector<std::shared_ptr<Action>> items; // 从栈顶往下遍历,最多取 10 条 // 注意:这里要复制出来,不能直接持引用 size_t i = mgr->GetUndoIndex(); while (i > 0 && count < 10) { auto action = mgr->GetAction(i - 1); items.push_back(action); i--; count++; } for (size_t j = 0; j < items.size(); j++) { CString desc = items[j]->GetDescription(); // 超长描述截断为"…"形式 if (desc.GetLength() > 32) desc = desc.Left(32) + _T("..."); pMenu->AppendMenu(MF_STRING, ID_UNDO_TO + j + 1, CString(_T("撤销到:")) + desc); } }

这段代码的要点在于取栈顶动作时必须用GetUndoIndex()来确定范围,而不是简单地读m_actions的 size。因为用户撤销过几步之后,栈尾有些动作属于"已撤销分支",直接遍历会把它们也显示出来。items用复制而非引用的原因是菜单构建是异步的,菜单项点击时 Undo 可能已经执行过,引用会被后续入栈操作重分配而失效。

4.3 编辑视图切换时撤销栈的归属

Bundle 里 RE 与 BE 两套视图共存,还有一个容易踩的问题:切换视图时 UndoManager 要不要清空。正确做法是两套视图共享同一个 UndoManager 实例,这样用户用 RE 视图输入内容,切换到 BE 视图后仍可以撤销之前的操作。但共享的前提是两套视图的内部状态能对上号——BE 视图如果对文档做了额外的格式化处理,RE 视图记录的 TextRange 在 BE 视图里就会错位。所以压缩包里的 RE 和 BE 动作类都是成对文件,本质上它们通过同一组TextRange和数据表来协调偏移量,而不是各自为政。

5. 避坑手册:undo_manager 实战里最常见的五类问题

5.1 合并逻辑失控导致撤销粒度崩坏

现象:用户连续输入一长段文字,按下一次 Ctrl+Z 只退掉一个字符,想删掉整句话要按几十次。

原因:合并判断条件过严,CanMerge里要求两个 Action 的m_tick完全一致,或者MERGE_INTERVAL_MS设置得太小,使合并几乎从不生效。

解决:把时间窗口放宽到 1000 毫秒以上,并在CanMerge里同时校验输入字符是否属于"普通可见字符"。如果合并条件里加了不相关字段(比如只允许英文输入合并,不允许中文),趁早去掉。真正的连续中文输入同样需要合并成一个大 Action,否则撤销体验会非常差。

5.2 替换操作拆成两步导致重做界面闪烁

现象:用户执行一次全文替换,然后连续撤销/重做,重做时界面先短暂变为"旧文本已删、新文本未加入"的空状态,再闪出结果。

原因:把替换拆成了"删除旧文本"和"插入新文本"两个独立 Action 推入 UndoManager,Redo 时会依次执行两个动作。

解决:用 REReplaceAction 这种单 Action 原子对象。构造时一次性缓存旧文本和新文本,Undo/Redo 各只做一次 SetText 调用。如果已经拆成两步了,在替换逻辑入口处加一个聚合层,先暂存两步操作,等内部改造完成后再合并为一个REReplaceAction。

5.3 REDropAction 拖放操作后 Undo 目标错位

现象:把一段文本从文档中部拖到结尾,执行撤销,文本回到了中间的位置,但光标和选区状态完全乱了,再次撤销直接把相邻段落也吞掉。

原因:拖放动作在 RE 视图里通常被实现为"选中源文本 -> 删除 -> 在目标位置插入"。目标位置的 TextRange 是在删除源文本之前记录的,删除操作导致目标位置的实际偏移量整体前移,插入时错位。

解决:拖放动作构造时不要直接用目标位置索引,而是先在同一 UndoManager 里创建一个占位标记,执行完删除后再把目标的 TextRange 重新换算一次。这部分逻辑在REDropAction.cpp里就是关键,它需要在 Undo 时先删掉目标位置的文本,再把源位置的旧文本放回。

5.4 撤销后做新输入,Redo 分支没有按预期清空

现象:用户撤销了三步,输入了新内容,之后打开 Redo 菜单发现先前撤销的三步还能选,选中后文档直接跳回到一个完全不相关的状态。

原因:AddAction里没有做分支截断,m_undoIndex之后的旧动作还留在m_actions中。

解决:严格在AddAction开头执行erase(begin() + undoIndex, end())。这里要顺手检查一遍所有调用AddAction的代码路径,确认没有绕开 UndoManager 直接操作m_actions数组的地方。有一个隐蔽路径值得注意:某些刷新控件状态的代码会间接调用视图的 SetWindowText,从而触发编辑器自身的 EN_CHANGE,导致一个虚假的 Action 被创建。解决方案是在视图层维护m_bInUndoRedo标志,Undo/Redo 执行期间忽略 EN_CHANGE 回调。

5.5 Undo 栈内存泄漏与视图析构顺序

现象:关闭编辑器时程序崩溃,断点指向未定义行为,日志显示 stack 栈里某个 Action 的 m_view 已经是野指针。

原因:UndoManager 的析构顺序晚于视图对象,栈里还有未执行的 Action 引用已销毁的视图。尤其当视图类持有 UndoManager 指针时,视图的析构函数没有先调用ClearHistory()。

解决:在视图的析构函数里显式清理:

BetterEditView::~BetterEditView() { if (m_undoManager) { // 先清空历史,避免后续 Action 析构时访问 m_view m_undoManager->ClearHistory(); } // 再释放其他资源 }

同时建议 UndoManager 持有 View 的裸指针时,在 Action 类里保存std::weak_ptr<REView>而不是裸指针,Undo 执行前先lock()检查视图是否存活。这套压缩包里的 UndoManager 没有用智能指针管理视图引用,接手的代码要自己补上这层防护。

6. 验证与进阶:用动作回放压测撤销链,再把 UndoManager 抽到纯 C++ 层

验证这套撤销框架是否可靠,单靠手动点按钮不够。我常用的方法是写一个回放测试脚本,模拟一串操作序列并记录每一步文档内容的哈希值,然后做多轮 Undo/Redo,最后比对哈希值是否回到原位。

// 回放验证:执行 N 步后全部撤销,再全部重做 void VerifyUndoRedo(UndoManager* mgr, RicherEditView* view) { std::vector<size_t> snapshots; // 记录每步后的内容哈希 snapshots.push_back(HashView(view)); // 注入预定义操作序列(模拟实际输入路径) for (int i = 0; i < 100; i++) { auto action = BuildDummyAction(view, i); mgr->AddAction(action); action->Execute(); snapshots.push_back(HashView(view)); } // 全部撤销 while (mgr->CanUndo()) { mgr->Undo(); size_t h = HashView(view); ASSERT(h == snapshots[mgr->GetUndoIndex()]); } // 全部重做 while (mgr->CanRedo()) { mgr->Redo(); size_t h = HashView(view); ASSERT(h == snapshots[mgr->GetUndoIndex()]); } }

这个测试跑一遍能暴露大部分问题:合并逻辑错误导致快照不匹配、分支未截断导致 Redo 内容错位、以及 TextRange 修正算法在边界条件下的失败。跑测试时注意把m_maxDepth调小到 50 左右,这样可以验证"历史上限截断"逻辑是否会影响哈希比对。

说到进阶用法,这套 UndoManager 里唯一的工程耦合点就是UMString.h和UMResources.h这两个文件——前者封装了字符串操作,后者集中定义了资源 ID。如果你想把它移植到 Qt 或纯 C++ 控制台程序,第一步是把这两个文件里的 MFC 依赖干掉:CString换成std::wstring,CView指针换成抽象接口IEditView。动作类里的GetDescription可以直接复用,菜单那部分逻辑就别带过去了,Qt 的 QUndoStack 有自己的界面组件,用不到。

从那次回放测试抓出四五个崩溃点之后,我每接一个编辑类项目都会先做一遍这个流程:先建动作回放脚本,再写快照校验,最后才碰 UI。把那套流程跑通了,Undo/Redo 的内存泄漏和状态错乱基本都能在开发阶段暴露出来,不用等用户按几下 Ctrl+Z 再翻车。这套 undo_manager 源码的好处是核心框架和视图逻辑拆得干净,按上述步骤验证一轮,能省下不少排查功夫。希望这份拆解对你有用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 8:49:32

Open-Code-Review:基于LLM Agent的智能代码审查范式

1. 这不是传统Code Review&#xff0c;而是一次开发协作范式的迁移“open-code-review”这个词最近在GitHub趋势榜和开发者社区里频繁出现&#xff0c;但它绝不是把Git提交记录公开那么简单。我从去年底开始在三个中型项目里落地这套机制&#xff0c;核心目标很明确&#xff1a…

作者头像 李华
网站建设 2026/9/26 8:49:28

ORDL在线词典学习实战:EMR文本向量化与临床概念提取

简介&#xff1a;本资源是面向机器学习与信号处理方向研究者及MATLAB开发者的在线词典学习&#xff08;ORDL&#xff09;算法实践代码包&#xff0c;聚焦大规模流式数据下的稀疏表示建模问题&#xff0c;适用于文本分类、图像去噪、高维信号压缩等场景。压缩包为RAR格式&#x…

作者头像 李华
网站建设 2026/9/26 8:49:16

从CVE到在野利用:漏洞披露与应急响应的完整生命周期

2. 漏洞披露背后的时间线&#xff1a;从发现到在野利用有多远2.1 CVE编号的诞生与披露机制一说到CVE&#xff0c;很多刚入门的朋友以为是某个安全公司发明的&#xff0c;其实这是MITRE组织维护的一套公开漏洞编号体系。CVE编号的作用很简单&#xff0c;就是把全世界安全研究员发…

作者头像 李华
网站建设 2026/9/26 8:48:58

Atlas 300V 24G实战:YOLO推理加速卡部署全流程

1. 先回答那个热词&#xff1a;Atlas 300V 24G到底算不算运算加速卡 先给结论&#xff1a;算&#xff0c;但这个"加速卡"跟很多人脑子里的"运算加速卡"并不是一回事。它是一张 专用AI推理加速卡 &#xff0c;不是一张通用GPU&#xff0c;更不是用来做图形…

作者头像 李华
网站建设 2026/9/26 8:48:43

FAST-LIO2工程化落地:ikd-tree增量地图与重定位实战

1. FAST-LIO2工程化落地的两个关键拼图搞过激光SLAM的朋友应该都有体会&#xff0c;FAST-LIO2的论文和开源代码本身已经足够优雅&#xff0c;iEKF框架把IMU和LiDAR的紧耦合做到了极致&#xff0c;跑公开数据集的效果也确实能打。但真正把它往实际项目里塞的时候&#xff0c;你会…

作者头像 李华