npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@overworld-engine/editor

v1.5.0

Published

In-game scene editor overlay for Overworld games: place/move entities, export scene JSON

Readme

@overworld-engine/editor

游戏内场景编辑器雏形:在运行中的游戏里摆放/移动 NPC、建筑、装饰物, 调整属性,一键导出场景 JSON。由三部分组成:

  • useEditorStore —— 无头工作集(zustand 单例),含实体模板与 JSON 导入/导出,可脱离渲染层单测;
  • <EditorScene> —— 挂在 <Canvas> 内的 R3F 编辑层(地面拾取、占位网格 + GLTF 模型预览、拖拽移动);
  • <EditorPanel> / <EditorToggle> —— Canvas 外的 DOM 面板(含模板选择器)与悬浮开关按钮。

编辑器关闭时(enabled === false)三者都不渲染任何内容,可以常驻在开发构建里。

快速开始

import { Canvas } from '@react-three/fiber'
import { EditorScene, EditorPanel, EditorToggle } from '@overworld-engine/editor'

<Canvas>
  <MyScene />
  <EditorScene groundSize={120} y={0} />
</Canvas>
<EditorPanel />
<EditorToggle hotkey="F2" />   {/* 悬浮按钮 + 可选快捷键开关编辑器 */}

<EditorScene> 的 props:groundSize(可点击地面边长,默认 100)、 y(地面高度,默认 0)、snap(覆盖项:传入时优先于 store 里的 可调 snap;不传则跟随面板的「吸附」输入,初始 0.5,0 关闭吸附)。

交互

  • 放置模式:点击地面,在命中点新建实体(自动吸附网格)。有激活模板时 按模板预填字段(见下文「实体模板」),否则新建当前 placingKind 的空白实体。
  • 选择模式:点击实体(占位网格或模型)选中(高亮 + 地面光环); Shift+点击加入/移出多选(见下文「多选与对齐」);按住拖拽即可在 XZ 平面 移动(多选时整组移动);点击空地取消选中。整次拖拽只算一步撤销。
  • 占位形状:NPC = 胶囊体,建筑 = 立方体,装饰 = 圆柱体;有 modelPath 时 显示真实模型(见下文「模型预览」)。
  • 面板中可编辑坐标、旋转、缩放、名称、模型路径、碰撞半径,并删除实体。 注意:面板里的旋转以「度」显示,store 中的 rotationY 存的是弧度。

多选与对齐

  • 多选:Shift+点击实体(3D 视图或面板实体列表均可)加入/移出选择;普通 点击 = 单选;Ctrl/Cmd+A 全选(阻止浏览器默认行为);点击空地清空选择。 store 中选择集是 selectedIds: string[];selectedId 保留为派生兼容字段 (= 最后选中的 id,空选择时为 null),原有单选用法不受影响。
  • 整组拖拽:拖拽任意已选中的实体会移动整个选择集 —— 被拖拽实体照常 吸附网格,其余实体按相同增量平移(基于增量的 moveSelectedBy);拖拽未选中 的实体则先单选它(原有行为)。整次组拖拽仍合并为一步撤销。
  • 删除 / 复制:工具栏「删除」「复制」与快捷键 Delete/BackspaceCtrl/Cmd+D 作用于整个选择集(removeSelected() / duplicateSelected(), 各为一步撤销;复制把所有克隆体整体偏移 [+1, 0, +1] 并选中它们)。
  • 对齐(≥2 个选中时可用,一步撤销):工具栏「对齐X / 对齐Z」的 min / 中 / max 三键把所选实体在该轴上的坐标统一为选择集的最小值 / 包围范围中点((min + max) / 2)/ 最大值 —— 即 alignSelected(axis, mode),axis'x' | 'z',mode'min' | 'center' | 'max';另一轴坐标不变。
  • 均分(≥2 个选中时可用,一步撤销):「均分」的 X / Z 按钮把所选实体按 当前坐标排序(相同坐标保持工作集顺序,排序稳定)后,在最小值与最大值 之间等距分布(distributeSelected(axis));首尾实体不动。
  • 属性面板:恰好选中 1 个实体时照常编辑字段;多选时显示 「已选 N 个实体」提示(字段编辑仅支持单选)。
  • 无头 API:toggleSelect(id)selectMany(ids)clearSelection()moveSelectedBy(dx, dz, options?)(options.transient 与拖拽共用同一 burst 机制,commitTransient() 合并为一步)。撤销/重做后选择集自动剔除 已不存在的 id(selectedId 同步为剩余的最后一个)。

实体模板

游戏可以注册一份放置预设目录,启动时调用一次即可:

import { useEditorStore, type EditorTemplate } from '@overworld-engine/editor'

const templates: EditorTemplate[] = [
  { id: 'bank', label: '银行', kind: 'building',
    modelPath: '/models/bank.glb', scale: 2, collisionRadius: 5, name: '银行' },
  { id: 'guide', label: '向导 NPC', kind: 'npc', modelPath: '/models/guide.glb' },
]
useEditorStore.getState().setTemplates(templates)
  • setTemplates(templates) 整体替换目录(不进撤销历史;若激活模板已不存在 则自动回退到 null)。
  • setActiveTemplate(id | null) 激活模板(null = 空白放置;未知 id 无操作)。 激活的同时会把 placingKind 切换成模板的 kind
  • 放置模式下,面板的「模板」区(有模板时出现)列出所有 label 按钮,外加 一个「空白」按钮(= 不用模板)。
  • 放置点击走 addEntityFromTemplate(position):kind / modelPath / scale / collisionRadius / name 取自激活模板,位置取命中点;无激活模板时等价于 addEntity({ kind: placingKind, position })。与 addEntity 一样可撤销。
  • 模板只预填实体字段 —— 放下的实体是自包含的,导出 JSON 不引用模板 id, exportScene / importScene 行为不变。

模型预览

实体的 modelPath 非空时,<EditorScene>useLoader(GLTFLoader, url) (GLTFLoader 来自 three/examples,无新依赖)加载并渲染真实模型代替占位形状:

  • 加载中(Suspense)与加载失败(内部错误边界捕获)都回退到占位形状, 坏路径不会让编辑器崩溃;修改 modelPath 会重试加载。
  • 同一 URL 只加载一次(useLoader 按 URL 缓存),每个实体渲染缓存场景的 克隆体;克隆体与缓存共享几何/材质,卸载时交给 GC 即可(主动 dispose 反而会破坏其他克隆体共享的缓存)。
  • 选中高亮对模型实体依然有效:地面光环照常显示,点击模型即可选中 (指针事件挂在包裹 group 上,占位形状与模型走同一条路径)。

撤销 / 重做

  • 每个变更操作(addEntity / removeEntity / updateEntity / duplicate / loadEntities / importScene / clear)在执行前把当前实体快照压入撤销栈, 并清空重做栈;栈上限 100 条,超出丢最旧的。
  • updateEntity(id, patch, { transient: true })瞬时更新:只改实体、 不进历史(用于拖拽 / 输入过程中);burst 开始前的快照会被记住, commitTransient() 把整个 burst 合并成一步撤销。未提交 burst 时执行任何 非瞬时操作(或 undo/redo)会先自动提交它。
  • undo() / redo() 恢复快照;恢复后选择集(selectedIds)自动剔除已 不存在的 id,selectedId 同步为剩余的最后一个(全没了则为 null)。 canUndo / canRedo 是与栈同步的布尔状态,UI 可直接订阅。
  • id 计数器随撤销回退(与「删除后不复用 id」的约定一致),重做/新增 永远不会产生重复 id。
  • 面板工具栏提供「撤销 / 重做」按钮;快捷键(仅编辑器开启时,输入框内不生效): Ctrl/Cmd+Z 撤销,Ctrl/Cmd+Shift+ZCtrl/Cmd+Y 重做, Ctrl/Cmd+D 复制所选,Ctrl/Cmd+A 全选,Delete/Backspace 删除所选。

复制

duplicate(id) 以相同的 id 计数机制克隆单个实体(npc-1npc-4 之类), 位置偏移 [+1, 0, +1],克隆体自动选中,可撤销。duplicateSelected()整个选择集做同样的事:所有克隆体一次生成(一步撤销)并成为新的选择。 面板工具栏的「复制」按钮(有选中时可用)和 Ctrl/Cmd+D 都走 duplicateSelected()

吸附与网格

  • snap 现在存放在 store 里(默认 0.5,setSnap(v) 修改,0 = 关闭; 负数/非法值按 0 处理),面板工具栏的「吸附」数字输入(步长 0.1)直接改它。 <EditorScene snap={...}> 传了 prop 时以 prop 为准。
  • showGrid(默认 true,setShowGrid / 面板「网格」勾选框)控制 <EditorScene> 里的吸附网格(gridHelper):格距跟随生效的 snap, 分割数上限 200(极小 snap 不会撑爆渲染),略微抬高 0.02 避免与地面 z-fighting。

无头 store

import { useEditorStore } from '@overworld-engine/editor'

const store = useEditorStore.getState()
store.setEnabled(true)
store.setMode('place')                    // 'select' | 'place'
store.setPlacingKind('building')          // 'npc' | 'building' | 'decoration'
store.setTemplates(templates)             // 注册放置模板(启动时一次)
store.setActiveTemplate('bank')           // 激活模板(null = 空白;同步 placingKind)
store.addEntityFromTemplate([4, 0, -2])   // 放置:字段取自激活模板,位置取参数
const e = store.addEntity({ position: [4, 0, -2], name: '银行' })  // id 自动生成:building-2
store.updateEntity(e.id, { rotationY: Math.PI / 2 })
store.updateEntity(e.id, { scale: 2 }, { transient: true })  // 瞬时:不进历史
store.commitTransient()                   // 把瞬时 burst 合并成一步撤销
store.duplicate(e.id)                     // 克隆(偏移 [+1, 0, +1],自动选中)
store.undo(); store.redo()                // 撤销 / 重做(见上文)
store.setSnap(1)                          // 吸附步长(0 = 关)
store.setShowGrid(false)                  // 网格显隐
store.select(e.id)                        // 单选;select(null) 取消选中
store.toggleSelect(e.id)                  // 加入/移出多选
store.selectMany(['npc-1', 'npc-2'])      // 整体替换选择集(selectedId = 最后一个)
store.clearSelection()                    // 清空选择
store.moveSelectedBy(1, -2)               // 平移所选(支持 { transient: true })
store.alignSelected('x', 'center')        // 对齐:'x' | 'z' × 'min' | 'center' | 'max'
store.distributeSelected('z')             // 均分(按当前坐标排序,首尾不动)
store.duplicateSelected()                 // 复制所选(一步撤销,选中克隆体)
store.removeSelected()                    // 删除所选(一步撤销)
store.removeEntity(e.id)
store.loadEntities(entities)              // 整体替换(重置选中与 id 计数器)
store.clear()                             // 清空

id 按类型递增(npc-1npc-2building-1…),loadEntities 会从已有 id 重新播种计数器,不会产生重复 id。

导出 / 导入 JSON

const json = store.exportScene()   // 纯函数,也可直接用 exportEntities(entities)
store.importScene(json)            // 逆操作(尽力解析,跳过坏条目;根不是对象时抛错)

导出形状:

{
  "npcs":      [{ "id", "modelPath", "position", "rotation", "scale?", "name?" }],
  "buildings": [{ "id", "name", "modelPath", "position", "rotation", "scale", "collisionRadius" }],
  "decorations": { "tree": { "radius": 0.8, "instances": [{ "position", "rotation?", "scale?" }] } }
}

约定:rotation 一律为 [0, rotationY, 0];modelPath 缺省导出为 ''; 建筑 name 缺省取 id、collisionRadius 缺省为 2;装饰按 name 分组 (未命名归入 "decoration" 组),组内 radius 取该组最后一个显式的 collisionRadius(缺省 2)。导出 → 导入 → 再导出 结果稳定。

面板的「导出 JSON」按钮会同时复制到剪贴板并下载 .json 文件; 「导入 JSON」从文本框解析,结果(含错误)显示在面板内的状态行上。

可测性:稳定 data-testid

<EditorPanel> / <EditorToggle> 的关键交互元素带稳定的 data-testid, 前缀由 testIdPrefix prop 配置(默认 'ow-editor',常量 DEFAULT_EDITOR_TESTID_PREFIX):

| 元素 | testid | |---|---| | 面板根节点 | ow-editor-panel | | 模式按钮 | ow-editor-mode-select / ow-editor-mode-place | | 工具栏 | ow-editor-undo / -redo / -duplicate / -delete | | 实体列表行 | ow-editor-entity-<实体id>(如 ow-editor-entity-npc-1) | | 导出 / 导入按钮 | ow-editor-export / ow-editor-import | | 悬浮开关按钮 | ow-editor-toggle |

E2E 断言请认 testid 而不是按钮文案(文案可被覆写,见下节);逻辑断言 推荐直接读 useEditorStore(见测试指南)。

文案覆写(i18n)

面板与开关按钮的内置文案(默认中文)可整体或部分覆写 —— configureEditorLabels(partial) 在启动或切换语言时调用,已挂载的面板 立即重渲染;resetEditorLabels() 恢复默认:

import { configureEditorLabels } from '@overworld-engine/editor'

configureEditorLabels({
  undo: 'Undo',
  redo: 'Redo',
  modeSelect: 'Select',
  modePlace: 'Place',
  toggleLabel: (on) => `Editor ${on ? 'ON' : 'OFF'}`,
  multiSelectionHint: (n) => `${n} entities selected`,
})

字典类型为 EditorLabels(默认值常量 DEFAULT_EDITOR_LABELS):静态标签为 字符串字段(undo / redo / duplicate / delete / snap / grid / modeSelect / modePlace / kindNpc / kindBuilding / kindDecoration / templateBlank / exportJson / importJson / clearScene / 各分区标题与 属性字段名等),参数化消息为函数字段(multiSelectionHint(count) / exportedStatus(count) / importSuccessStatus(count) / importFailStatus(error) / toggleLabel(enabled))。React 侧也可用 useEditorLabels() 订阅当前字典。编辑器是唯一内置用户可见文案的 Overworld 包 —— 其余自渲染 UI(摇杆、小地图)无文字,系统包(通知/对话/ 任务等)文案全部来自游戏内容。

与 @overworld-engine/scene 的关系

按架构分层规则,本包不 import @overworld-engine/scene。导出的 npcs / buildings / decorations 分别是 NPCConfig / BuildingConfig / DecorationInstance结构化拷贝,可以直接喂给 <SceneShell>:

const scene = store.exportScene()
<SceneShell npcs={scene.npcs} buildings={scene.buildings}
            decorationCollisions={scene.decorations} />

已知边界(雏形)

  • 只支持 XZ 平面拖拽;旋转/缩放需在面板中输入,没有 3D gizmo。
  • 模型预览不做碰撞体可视化,collisionRadius 只是随 JSON 导出的数据。
  • 编辑器状态不持久化 —— 产物就是导出的 JSON,不是存档(模板目录同理, 由游戏每次启动时用 setTemplates 注册)。