尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Pascal 编辑器插件开发指南:NodeDefinition 契约、宿主集成与地形落地实践

Pascal 编辑器插件开发指南:NodeDefinition 契约、宿主集成与地形落地实践 Pascal 编辑器插件开发指南NodeDefinition 契约、宿主集成与地形落地实践【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor本文面向为开源 3D 建筑编辑器 Pascaleditor编写第三方节点插件node pack的开发者与 AI Agent。文章以仓库中 插件开发契约文档 为骨架结合pascal-app/core的注册表源码registry.ts、types.ts与内置插件 pascal-app/nodes 的实际定义完整讲解 Plugin 清单结构、NodeDefinition 可贡献的全部能力、地形落地floorPlaced / levelBaseAt的正确姿势、宿主包引用约定、生命周期、插件发现setPluginDiscovery、编辑器宿主面板EditorHostPanel与项目级安装机制以及版本化与本地测试方法。读完本文你可以从一个可工作的示例插件出发编写出与内置pascal:core同等地位、能正确处理坡地地形、遵守宿主外观与性能约定的第三方节点插件。插件是什么一个对象、一个清单在 Pascal 的插件体系中插件只是一个导出一个符号的 JS 对象——manifest清单。它不包含任何特殊格式内置插件与第三方插件走的是同一条loadPlugin路径。import type { Plugin } from pascal-app/core export const myPlugin: Plugin { id: acme:furniture-pack, apiVersion: 1, nodes: [ couchDefinition, armchairDefinition, // ... ], }清单字段如下字段必填说明id是全局唯一。使用vendor:pack-name命名空间避免冲突。宿主将其视为不透明字符串。apiVersion是当前为1。宿主在不匹配时抛错——提升版本会故意破坏旧插件。nodes否AnyNodeDefinition数组。从源码看loadPlugin的 API 版本门槛是硬编码在注册表实现中的registry.ts 的HOST_API_VERSION 1不匹配时抛出形如plugin ... requires apiVersion ...; host supports 1的错误。plugin.nodes中的每个定义会经registerNode注册并记录该 kind 由哪个插件贡献pluginIdsByKind这是后面项目级安装isNodeKindEnabled的基础。值得注意的是Plugin类型在 types.ts 中还支持inspectorExtensions字段——它允许插件为指定 node kind 的浮动检查器inspector卡片贡献一个独立分区如Engineering分区。扩展只在对应插件被项目安装时才显示且与 kind 自身的控件是二选一关系详见后文宿主面板章节。内置插件的同构印证仓库中内置插件pascal:core定义于 packages/nodes/src/index.ts它把 shelf、wall、door、roof、duct-segment 等几十个内置 kind 打包进同一个Plugin形状。源码注释明确写道External plugins follow the exact same shape — samePlugintype, sameloadPlugincall path. 也就是说内置插件能做的第三方插件全都能做。文档还提到独立的pascalorg/plugin-trees仓库作为可工作的参考示例建议将其克隆作为起点。NodeDefinition 能贡献什么插件 v1 中nodes数组是唯一有意义的贡献点。每一项都是NodeDefinitionS extends ZodObject注册表会为其盖上kind、schemaVersion、schema三个必填字段并支持下列能力的任意组合defaults—— 新实例的初始字段值。capabilities——selectable/duplicable/deletable/surfaces/relations等框架消费的标志。parametrics—— 自动推导的检查器 UI 形态fields 可选的customPanel逃生舱。renderer—— 自定义 3D React 组件GLB、drei、TSL 材质可退出def.geometry。system—— 逐帧工作动画、dirty 级联、运行时状态。geometry—— 纯函数(node, ctx) Object3D供通用GeometrySystem使用。floorplan—— 纯函数(node, ctx) FloorplanGeometry用于 2D 平面图图层。floorplanAffordances/floorplanMoveTarget—— 2D 拖拽处理器。tool/affordanceTools—— 3D 放置 移动工具懒加载组件。presentation—— 面板/侧边栏元数据label、icon、paletteSection等。mcp—— 面向 AI 消费者的 MCP 工具描述。relations/computeLevelData—— 兄弟节点查找 层级批量预计算。从三选一组合模型看贡献边界这 13 类能力中geometry/renderer/system三者构成核心的三复选框组合模型详见 node-definitions.md三个字段相互独立、没有判别标签存在即参与。一个 kind 可以选择仅geometry如 shelf纯函数构建网格框架的ParametricNodeRenderer挂空 groupGeometrySystem在节点变脏时重建子对象。仅renderer如 GLB 物品需要 JSX-only 特性——Html、useGLTF、drei 辅助、实例化、TSL 着色器材质、R3F portal。renderersystem如 zone树中包含 React-only 原语Html同时需要逐帧命令式工作按名字修改 uniform / 透明度。geometrysystem如 door / window参数化几何 动画职责分离——geometry纯函数建网格system驱动operationState动画状态。文档推荐组合的完整清单与迁移步骤见 node-definitions.md这里不再展开。需要强调的是纯函数 builder 不得导入useScene场景读取必须经由GeometryContextresolve/children/siblings/parent这是保证 builder 可单元测试、可被通用系统重建的关键约束。一个真实的NodeDefinition示例shelfpackages/nodes/src/shelf/definition.ts 是内置 kind 中最教科书的注册方式几乎覆盖了文档列举的全部贡献面export const shelfDefinition: NodeDefinitiontypeof ShelfNode { kind: shelf, snapProfile: item, schemaVersion: 2, schema: ShelfNode, category: furnish, surfaceRole: joinery, defaults: () ({ object: node, parentId: null, visible: true, metadata: {}, children: [], position: [0, 0, 0], rotation: [0, 0, 0], width: 1, depth: 0.5, thickness: 0.05, height: 1.8, style: cubby, rows: 3, columns: 2, withBack: true, withSides: true, withBottom: true, bracketStyle: minimal, }), capabilities: { movable: { axes: [x, z], gridSnap: true }, rotatable: { axes: [y], snapAngles: [0, Math.PI / 4, Math.PI / 2, (3 * Math.PI) / 4, Math.PI] }, surfaces: { top: { height: (n) shelfRowSurfaceYs(n as ShelfNode).at(-1) ?? 0 }, custom: (n) shelfRowSurfaceYs(n as ShelfNode).map((y) ({ position: [0, y, 0] as const, normal: [0, 1, 0] as const, })), }, selectable: { hitVolume: bbox }, duplicable: true, deletable: true, paint: shelfPaint, slots: (n) shelfSlots(n as ShelfNode), floorPlaced: { footprint: (node) ({ dimensions: [...], rotation: ... }), collides: true, }, }, relations: { hosts: [item], cascadeDelete: descendants }, parametrics: shelfParametrics, handles: shelfHandles, geometry: buildShelfGeometry, geometryKey: (n) JSON.stringify([/* 几何相关字段 */]), floorplan: buildShelfFloorplan, floorplanMoveTarget: shelfFloorplanMoveTarget, floorplanAffordances: { shelf-resize: shelfResizeAffordance, shelf-rotate: shelfRotateAffordance }, preview: () import(./preview), tool: () import(./tool), toolHints: [ { key: Left click, label: Place shelf }, { key: Esc, label: Cancel }, ], presentation: { label: Shelf, description: A configurable shelving unit. Items host on each row., icon: { kind: url, src: /icons/shelf.webp }, paletteSection: furnish, paletteOrder: 30, }, mcp: { description: A parametric shelving unit. ... }, }这个真实定义展示了文档提到的几乎所有字段的取值形态surfaceRole: joinery决定无纹理表面解析出的主题角色色floorPlaced携带 footprint 让FloorElevationSystem每帧抬升relations.hosts: [item]声明可托管物品toolHints声明放置工具激活时的快捷键提示presentation.icon使用url类型的 IconRef指向 shelf.webp。写插件时可直接照此结构填充。站在地面上地形落地的三种姿势场地site携带雕刻的高度场所以地面不是y 0平面。一个把基准硬编码为0的插件 kind 在平坦地块上看起来正确在雕刻的山坡上则会把自己埋进坡里。文档明确没有能力需要声明、也没有东西需要注册——根据你的 kind 如何获得 Y 坐标选择下面三种方式之一地形会自动跟随节点站在某个表面上→ 声明capabilities.floorPlaced并带上footprint复合形体用footprints。FloorElevationSystem每帧抬升已注册的 mesh在每个 footprint 上于重叠的楼板与地面之间择优。这是完整契约一棵树、一条长凳、一个花盆只需要这一步。FloorPlacedConfig在 types.ts 中定义支持三个字段footprint/footprints—— 解析占地形状applies?: (node) boolean—— 按节点决定是否适用collides?: boolean—— 是否参与落地碰撞实心家具类 kinditem / shelf / column设置为true使其 footprint 阻挡其他放置且自身放置/移动拒绝重叠spawn、MEP、stair 等标记类与端口对接类 kind 留空既不阻挡也不被阻挡默认关闭。def.geometrybuilder 自行烘焙垂直原点→ 用ctx.levelBaseAt(x, z)替代硬编码的0。它返回该层级局部坐标点处的地面高度该 storey 下方没有地形时返回0。调用它还会把你的 kind 纳入地形失效机制——地面移动时 builder 会重新运行无需你手工接线 dirty 规则。但注意def.floorplan中不存在levelBaseAt——平面图没有高程——所以被 2D/3D 共享的 builder 必须写成ctx.levelBaseAt?.(x, z) ?? 0。你提供集体renderer一个组件绘制多个节点——实例化 mesh、合并缓冲→ 你拥有每个实例的 Y。FloorElevationSystem写入的是节点已注册的对象对集体 kind 而言那是不可见的选择代理selection proxy而非实例本身所以逐实例直接写node.position[1]会同时忽略楼板与地形。正确做法是在写每个实例矩阵时通过getFloorStackedPosition({ node, nodes, position })解析且放置工具提交基准位置[x, 0, z]——抬升只是表现层绝不持久化。最后一条铁律在几何体锚定的同一个 XZ 坐标上采样地面。一个手柄、一条吸附参考线、一个 mesh 若在坡面上采样不同位置会肉眼可见地不一致。背景机制见 vertical-model.md。导入宿主包peer 依赖不是普通依赖插件从已发布的pascal-app/*包导入——与内置节点使用的表面一致peer-dependency 风格// Schemas, types, registry types import { type AnyNode, type NodeDefinition, type Plugin, z, // re-exported from zod for schema authoring } from pascal-app/core // Viewer-side primitives (lazy: only inside renderers / systems) import { useNodeEvents, NodeRenderer } from pascal-app/viewer // Editor-side primitives (lazy: only inside tool / affordanceTools) import { useDragAction, EDITOR_LAYER } from pascal-app/editor这些包是peer dependencies 而非普通 dependencies——宿主应用拥有版本。如果插件钉死自己的一份pascal-app/core副本会创建两个注册表并静默失败npm 的 peer-dep 解析会在安装时捕获这一点。z由pascal-app/core从 zod 再导出供 schema 编写直接使用。遵循宿主的外观与性能偏好自定义renderer拥有自己的材质因此必须像内置节点一样遵循宿主的外观轴。以只读方式订阅useViewer的shading、textures、colorPreset、sceneTheme不要添加插件专属的画质开关也不要把这些值复制进场景数据。Colored Rendered保留导入模型自带的材质。Colored Solid使用createDefaultMaterial(..., solid)或另一种缓存的MeshLambertNodeMaterial变体。保留作者提供的 albedo 贴图、颜色、透明度与材质槽位但省略会破坏更廉价 Solid 路径的 PBR-only 贴图。Monochrome使用createSurfaceRoleMaterial(def.surfaceRole, colorPreset, side, sceneTheme)。导入的道具通常声明surfaceRole: furnishing。性能纪律模型加载时捕获一次作者材质、按源材质缓存变体、仅在偏好变化时交换、销毁前恢复。绝不逐帧克隆材质、绝不修改 loader 缓存的作者材质、绝不销毁来自宿主缓存的材质。完整材质生命周期模式见 materials-and-themes.md。shadows、edges、Solid/Rendered 后期处理开销都是宿主全局的。普通插件几何体保持在SCENE_LAYER因此光照 rig 与深度/法线管线自动包含它。仅编辑器放置预览应放在OVERLAY_LAYER/EDITOR_LAYER——这能让 ghost 预览避开阴影、SSGI 与墨线ink-edge通道。插件只需要为透明或 overlay mesh 管理castShadow/receiveShadow无需复制宿主设置。生命周期与注册表变更通知loadPlugin在 v1 中是**只增add-only**的。热移除一个 kind 需要拆除场景中每个已挂载实例——超出范围。插件只在启动时加载一次。registerNode在重复kind时抛错因此两个插件都提供kind: couch是启动期错误而非静默覆盖。实现细节registry.ts_register首先校验kind为非空字符串、schemaVersion为正整数重复 kind 时生产环境抛[registry] duplicate node kind: ...而开发模式HMR降级为警告并原地替换避免保存def.ts时崩溃或留下陈旧描述符。插件 kind 是异步注册的应用引导通过动态导入发现它们因此任何在挂载时快照注册表的消费者如选择管理器的getSelectableKinds()订阅列表都会在插件稍后加载时过期。为此_register/_reset会递增单调版本号并通知监听器useRegistryVersion()use-registry-version.ts通过useSyncExternalStore把它变成 React 重渲染让 effect 能重新推导 kind 列表。在 apps/editor/lib/bootstrap.ts 可以看到宿主应用的完整引导序列loadBuiltinsSync()以同步方式在模块导入时注册所有内置 kind保证 SSR / 水合首帧就看到完整注册表随后loadExternalPlugins()异步调用discoverPlugins()并逐个await loadPlugin(plugin)开发模式下控制台打印[pascal:registry] loaded pascal:core v1 (... kinds ...)这就是文档验证锚点的来源。发现机制setPluginDiscovery宿主在加载内置插件后调用discoverPlugins()。默认实现返回[]。需要携带外部插件的应用在bootstrap 模块求值之前替换它// In app boot, BEFORE import ./pascal-bootstrap import { setPluginDiscovery } from pascal-app/core import { myPlugin } from acme/furniture-pack setPluginDiscovery(async () { // Static import: bundled into the app. return [myPlugin] // Or fetch a manifest, dynamic-import each entry, etc. // const manifest await fetch(/plugins.json).then(r r.json()) // return Promise.all(manifest.map(m import(m.url).then(mod mod.default))) })setPluginDiscovery是全局的。调用两次会静默覆盖——与 bootstrap 导入的顺序至关重要。从 registry.ts 的源码看契约被刻意保持最小——只是返回要加载的插件列表。加载器可以是静态import.meta.glob、针对注册表端点的fetch、worker IPC 等每个返回的插件仍会走loadPlugin因此同样的 API 版本门槛与重复 kind 保护都适用。文档未提及的extendPluginDiscovery也在同一处实现它把新发现源追加到现有链上Promise.all合并用于宿主捆绑的一方可选插件如 bootstrap.ts 中treesPlugin、bonesPlugin、mintPlugin、streetscapePlugin的注册方式避免覆盖宿主提供的发现源。宿主面板与项目安装核心Plugin清单保持渲染器无关。一个同时提供编辑器 UI 的插件单独导出EditorHostPanelimport type { EditorHostPanel } from pascal-app/editor export const myHostPanel: EditorHostPanel { id: acme:furniture-pack:catalog, pluginId: acme:furniture-pack, label: Furniture pack, description: A curated furniture catalog., creator: { name: Acme, url: https://acme.example, }, pluginUrl: https://github.com/acme/pascal-furniture-pack, icon: { kind: iconify, name: lucide:armchair }, component: () import(./catalog-panel), }EditorHostPanel类型定义在 packages/editor/src/lib/plugin-panels.ts与文档示例一致且额外支持kinds按 kind 关联面板、workspaces、defaultInstalled等字段。宿主通过registerEditorHostPanel注册它。注册的插件出现在Plugins 侧边栏而场景图的installedPlugins: string[]控制该项目的图标栏显示哪些插件面板。defaultInstalled: true让第一方插件自动进入旧项目与新创建的项目Nature 当前就使用这一机制Bones 则显式defaultInstalled: false作为可选的专业视图见 bootstrap.ts。安装/卸载是项目级别的可见性操作。插件代码与节点定义在浏览器会话内保持加载因为loadPlugin只增但未安装插件的面板、放置 UI、渲染器、系统与平面图输出都会被禁用。场景图中已序列化的插件节点仍然保留插件重新安装后重新可见卸载从不删除项目数据。底层依据是isNodeKindEnabled(kind, installedPlugins)registry.ts宿主直接注册的 kind 与内置插件始终启用省略安装列表遗留场景为向后兼容保持插件可见否则按installedPlugins.includes(pluginId)门控。编辑器 2D 图层floorplan-registry-layer.tsx与面板挂载处panel-wrapper.tsx都调用它做过滤。creator与pluginUrl是可选的管理员元数据。在 Plugins 侧边栏选择插件会打开其详情页宿主在此展示元数据与项目的安装/卸载控件。宿主面板在错误边界error boundary内懒加载挂载。使用宿主的 CSS 变量、保持 CSS 作用域在插件内不要写全局样式。版本化apiVersion: 1覆盖上述全部表面。宿主的策略删除或改变既有字段的形态时提升 major新增可选字段不提升。计划是尽可能长期保持向后兼容的增量——bump 是逃生舱不是默认。插件自身的数据版本化是每个NodeDefinition上的schemaVersion。宿主不做迁移插件的migrate(node, fromVersion)未来实现负责处理自己的遗留持久化节点。哪些尚不属于插件贡献契约刻意保持窄边界以便可交付每个not yet都是计划而非永不材质—— 没有plugin.materials槽位。在def.renderer/def.system内使用pascal-app/viewer的createMaterial。平面图原语——FloorplanGeometry联合类型归宿主所有。要绘制联合类型无法表达的内容退回def.renderer通过另一个 2D 挂载点渲染或提交 issue。核心清单中的面板 / 侧边栏 UI—— 宿主相关。为使用pascal-app/editor的宿主单独导出EditorHostPanel。Stores—— 插件创建自己的 Zustand store它们不扩展useScene、useEditor或useViewer。渲染器可以只读订阅导出的宿主表现状态如useViewer外观轴但不得把宿主 store 当作插件自有状态。路由 / 页面—— 插件是可视化 交互代码不是完整应用表面。承载设置页属于应用。测试你的插件pascal-app/nodes是内置参考实现独立的pascalorg/plugin-trees是独立示例。本地测试步骤把插件构建为普通 npm 包pascal-app/*作为 peerDependencies。在消费你内置包的宿主应用apps/editor是最容易的目标中接线setPluginDiscovery返回你的插件。开发模式的[pascal:registry]控制台日志会显示加载的插件 id 节点数量——这就是验证锚点对应 bootstrap.ts 的 N discovered plugin(s)输出。宿主的对等测试packages/nodes/src/index.test.ts断言每个AnyNode判别符都有已注册的 kind。插件贡献的 kind不参与该测试它们不在AnyNode中如果你在别处维护手写类型联合请自行添加等价测试。小结插件在 Pascal 中的定位非常明确一个Plugin清单 若干NodeDefinition通过setPluginDiscovery交给宿主在启动时一次性加载并冻结注册表。地形落地的三条路径floorPlaced、ctx.levelBaseAt、集体 renderer 的getFloorStackedPosition覆盖了从简单家具到实例化批处理的全部场景peer 依赖与只读订阅useViewer保证了多版本宿主兼容与外观一致项目级installedPlugins让安装/卸载成为纯可见性操作。把 plugin-authoring.md 与 node-definitions.md 结合阅读再对照内置 shelfDefinition 这个完整样例即可动手编写自己的节点插件。延伸阅读node-definitions.md —— 三复选框组合模型、GeometryContext、迁移步骤与陷阱vertical-model.md —— 地形继承的通用接缝materials-and-themes.md —— 外部插件渲染器的材质生命周期registry.ts ——loadPlugin/discoverPlugins/isNodeKindEnabled的实现types.ts ——Plugin/NodeDefinition/Capabilities完整类型定义packages/nodes/src/index.ts —— 内置pascal:core插件清单apps/editor/lib/bootstrap.ts —— 宿主应用的实际引导与插件发现接线packages/editor/src/lib/plugin-panels.ts ——EditorHostPanel类型与宿主面板注册表【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表