
SpacetimeDB 协作画板 Text Sticky Notes 功能深度解析从需求规格到服务端数据模型与实时编辑实现【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本文以 SpacetimeDB 仓库中 llm-oneshot Paint App 提示词系统的 Text Sticky Notes 功能规格15_text_sticky.md为核心结合同一仓库内完整的参考实现代码系统讲解文本标注与便签工具的需求拆解、服务端表结构与 reducer 设计、并发编辑互斥、正在编辑协作指示器以及元素级选中/移动/缩放能力。读完本文你将掌握如何在 SpacetimeDB 中以文本元素统一建模 text 与 sticky 两类对象、如何用editingBy字段实现多人编辑锁、以及如何把文本元素无缝接入选择、拖拽、undo/redo 等既有元素管线。一、功能规格总览Text Sticky Notes 在 Paint App 中的定位该规格文件是 Paint App 提示词体系prompts/README.md中 16 个特性积木之一的第 15 号Text Stickies。整个提示词系统按语言文件 组合提示词的 DRY 方式组织每个特性积木如本文件只描述功能行为与后端技术栈解耦在 12_full.md 的完整特性组合提示词中本特性被整体引用为「Text Sticky Notes」一节composed/12_full.md。在特性级别表中该功能被标注为Real-time sync能力的体现见 prompts/README.md 中特性 15 一行。规格原文共 8 条需求可归纳为三类能力类别规格要求快捷键文本工具拖拽定义文本区域大小直接内联输入无浏览器弹窗字体族选择器sans-serif / serif / monospace字号选择器small / medium / large / x-large文本颜色复用描边取色器T便签工具拖拽创建彩色便签内联输入S协作与编辑双击已有文本/便签内联编辑输入时向他人显示 User is editing... 指示器文本元素可像形状一样被选中、移动、缩放—规格虽短但每一行都对应了 Paint App 既有元素系统坐标、尺寸、旋转、选择、图层与实时同步机制的具体复用下面逐条结合参考实现展开。二、数据模型一个 TextElement 表同时承载文本与便签在参考实现的服务端 schemabackend/spacetimedb/src/schema.ts中规格中的文本与便签被统一建模为一张公开表text_elementexport const TextElement table( { name: text_element, public: true, indexes: [ { name: text_element_canvas_id, algorithm: btree, columns: [canvasId] }, { name: text_element_layer_id, algorithm: btree, columns: [layerId] }, ], }, { id: t.u64().primaryKey().autoInc(), canvasId: t.u64(), layerId: t.u64(), creatorIdentity: t.identity(), elementType: t.string(), // text | sticky x: t.f64(), y: t.f64(), width: t.f64(), height: t.f64(), rotation: t.f64(), content: t.string(), fontFamily: t.string(), // sans-serif | serif | monospace fontSize: t.string(), // small | medium | large | x-large textColor: t.string(), backgroundColor: t.string().optional(), // For sticky notes editingBy: t.identity().optional(), createdAt: t.timestamp(), } );从源码结构可以读出规格中几乎每一行的落点Text tool / Sticky note tool→ 由elementType字段区分注释明确取值范围为text | sticky拖拽定义文本区域大小→ 对应x / y / width / height四个f64几何字段与Shape表的布局完全一致保证后续能走同一套选中/移动/缩放逻辑字体族 / 字号选择器→ 对应fontFamily与fontSize两个字符串枚举字段枚举值在注释中与规格一一对应sans-serif/serif/monospace、small/medium/large/x-large文本颜色使用描边取色器→textColor字段参考实现中新增元素时直接将当前strokeColor传入客户端代码见下文彩色便签→backgroundColor可选字段仅 sticky 类型填写User is editing... 指示器→editingBy: t.identity().optional()记录当前正在编辑该元素的用户身份文本元素可像形状一样被选中、移动、缩放→ 表上有按canvasId、layerId的 btree 索引与Shape、Stroke表保持相同的外键结构天然融入图层与选择系统。在完整 schema 导出中TextElement与User、Shape、Selection、UndoEntry、Version等表一起注册进spacetimedbschema见 schema.ts意味着它的增删改会经由 SpacetimeDB 的订阅机制实时广播给画布上的所有协作者——这正是规格Real-time sync的基础。三、服务端 reducer文本元素的完整生命周期与并发编辑锁规格要求双击编辑输入时他人可见正在编辑这在服务端由一组围绕text_element表的 reducer 支撑backend/spacetimedb/src/index.ts 中 TEXT REDUCERS 一节3.1 创建add_text_element参数完整覆盖表字段index.ts#L772-L839spacetimedb.reducer( add_text_element, { canvasId: t.u64(), layerId: t.u64(), elementType: t.string(), // text | sticky x: t.f64(), y: t.f64(), width: t.f64(), height: t.f64(), content: t.string(), fontFamily: t.string(), fontSize: t.string(), textColor: t.string(), backgroundColor: t.string().optional(), }, (ctx, args) { requireEditor(ctx, args.canvasId); requireLayerEditable(ctx, args.layerId); const text ctx.db.textElement.insert({ id: 0n, /* ...各字段透传... */ rotation: 0, editingBy: undefined, createdAt: ctx.timestamp, }); // 写入 undo 快照 活动流 触碰画布活跃时间 } );reducer 中先执行requireEditor角色权限校验与requireLayerEditable图层锁定校验与 05 号特性Layers with Locking联动插入成功后还会写入一条actionType: add_text的UndoEntry以便 undo/redo通过logActivity记录added_text/added_sticky活动调用touchCanvas刷新画布lastActivityAt。可以推断文本元素从创建起就完整接入了 Paint App 的权限、图层、撤销、活动流与自动清理五条既有管线而非孤立功能。3.2 编辑锁与 User is editing... 指示器规格中输入时向他人显示 User is editing...在服务端通过两个 reducer 配合editingBy字段实现互斥编辑index.ts#L871-L901spacetimedb.reducer(start_editing_text, { textId: t.u64() }, (ctx, { textId }) { const text ctx.db.textElement.id.find(textId); if (!text) throw new SenderError(Text element not found); requireEditor(ctx, text.canvasId); if (text.editingBy text.editingBy.toHexString() ! ctx.sender.toHexString()) { throw new SenderError(Someone else is editing this); // 并发互斥 } ctx.db.textElement.id.update({ ...text, editingBy: ctx.sender }); }); spacetimedb.reducer(stop_editing_text, { textId: t.u64() }, (ctx, { textId }) { const text ctx.db.textElement.id.find(textId); if (!text) throw new SenderError(Text element not found); if (text.editingBy?.toHexString() ctx.sender.toHexString()) { ctx.db.textElement.id.update({ ...text, editingBy: undefined }); } });关键点在于start_editing_text在把editingBy置为当前用户前会拒绝已被他人编辑的元素从而保证同一时刻只有一个协作者拥有编辑权editingBy是公开表上的公开字段订阅该表的客户端都能收到变更前端据此渲染 User is editing... 指示器。此外还有一层兜底clientDisconnected生命周期钩子会遍历textElement把当前断开连接用户editingBy的所有元素重置为undefinedindex.ts#L71-L79避免编辑锁因异常断开而永久悬挂——这与图层锁定5 分钟自动解锁是同一类防悬挂设计思路。3.3 更新与删除update_text_elementindex.ts#L841-L869接收textId与x / y / width / height / rotation / content支持移动、缩放、旋转与内容修改四种操作合并提交delete_text_elementindex.ts#L903-L916删除元素并调用removeSelectionForElement清理相关选中记录。两个 reducer 都做requireEditorrequireLayerEditable双重校验与 3.1 的创建路径保持一致。四、客户端交互拖拽建框、内联编辑与像形状一样的操作规格中的交互要求在参考实现客户端client/src/App.tsx中有对应落地4.1 快捷键与工具栏工具栏中注册了sticky工具图标 快捷键ST键映射到文本工具快捷键映射逻辑见 App.tsx#L735 与 App.tsx#L778与 12_full.md 中T - Text tool、S - Sticky note tool的快捷键规格一致。4.2 拖拽定义区域与创建参数在handleMouseUp中当activeTool为text或sticky时会依据拖拽产生的shapePreview调用addTextElementApp.tsx#L1531-L1548conn.reducers.addTextElement({ canvasId, layerId, elementType: activeTool, // text | sticky x: shapePreview.x, y: shapePreview.y, width: Math.max(shapePreview.width, 100), // 拖拽框的最小宽高约束 height: Math.max(shapePreview.height, 40), content, fontFamily: sans-serif, fontSize: medium, textColor: strokeColor, // 复用描边取色器对应规格第 4 条 backgroundColor: activeTool sticky ? #fbdc8e : undefined, // 便签默认黄底 });可见文本颜色使用描边取色器直接体现为textColor: strokeColor拖拽定义文本区域大小体现为用拖拽矩形初始化x / y / width / height并对过小区域设置最小宽高兜底。需要说明的是规格要求type directly inline (no browser dialogs)而该参考实现中内容输入仍使用浏览器prompt()对话框App.tsx#L1532属于实现与规格之间存在差距的一个已知细节——服务端 schema 与 reducer 已为内联编辑editingBy锁 content频繁更新预留了完整支撑前端换用 DOM 内联输入即可闭环。4.3 选中、移动、缩放文本元素的选择与移动复用了形状的同一套逻辑handleMouseUp在拖拽位移非零时遍历selections对elementType text的分支调用updateTextElement提交位移后的新坐标App.tsx#L1475-L1488缩放通过更新width / height走同一 reducer。渲染层则按元素几何与文本属性绘制ctx.font \${sizes[text.fontSize] || 16}px ${text.fontFamily}[App.tsx#L1105](https://link.gitcode.com/i/325dd5dc0207f793943ddde67018c319#L1105)即用fontSize枚举映射像素值、用fontFamily选择字体并在text.y 处排版文本。五、与整体系统的协同文本元素不是孤岛从 schema 与 reducer 的交叉引用可以确认Text Sticky Notes 与 Paint App 的其他 15 个特性形成了完整的协作闭环选择系统Selection表的elementType枚举即包含textschema.ts#L267delete_selectedreducer 对text分支直接删除textElementindex.ts#L1060-L1066图层delete_layer会按text_element_layer_id索引级联清空该层的所有文本元素index.ts#L579-L583undo/redoadd_text动作可被undo/redoreducer 回放与还原重做时按elementDataJSON 快照重建整条TextElement记录index.ts#L1196-L1197、index.ts#L1263-L1282版本快照Version表的snapshotData是画布全部元素的 JSON 序列化createSnapshot/restoreSnapshot会把text_element一并纳入备份与恢复范围index.ts#L1396-L1470实时广播text_element表声明为public: true元素创建、编辑锁切换、移动缩放产生的表变更都会通过订阅推送给画布上的所有协作者满足Real-time sync。六、实战要点小结综合规格与参考实现在 SpacetimeDB 上落地文本/便签功能时可复用以下模式统一建模而非分表用elementType区分 text 与 sticky其余几何字段与形状共用换取选中/移动/缩放能力零成本复用编辑锁用identity字段editingBy存t.identity()配合start_editing_text的互斥校验与clientDisconnected的兜底清理实现协作画布上的安全内联编辑权限与图层校验前置所有文本 reducer 一律先requireEditor再requireLayerEditable保证 viewer 只读、被锁图层不可改枚举值约束即产品规格fontFamily/fontSize的取值注释与服务端校验、客户端渲染映射保持一致是字体族选择器字号选择器两项 UI 规格的最小数据契约复用全局管线文本元素的每次写操作都联动 undo 快照、活动流与画布活跃时间戳使功能天然获得撤销、审计与 30 天自动清理能力。若需在仓库中进一步研究可沿以下路径深入功能规格原文见 features/15_text_sticky.md组合到完整产品后的同款需求见 composed/12_full.md服务端表结构见 backend/spacetimedb/src/schema.ts服务端 reducer 见 backend/spacetimedb/src/index.ts客户端交互见 client/src/App.tsx。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考