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

@tmesoft/lab-scene

v2.1.0

Published

a lab scene

Readme

说明

基于 Three.js 的 3D 实验室场景编辑器。核心是 LabScene 类:加载实验室与设备 GLB 模型,提供拖拽/变换、后处理描边、模型动画控制,以及场景状态的 JSON 序列化。

扩展模块:ZoneManager(区域人数可视化) —— 在实验室地面创建监测区域、对接传感器实时显示人数分布。

一、基本使用

import LabScene from '@/js/LabScene'
import request from '@/utils/request'

const app = document.querySelector('#app')

// 构造函数:第一参为容器节点,第二参为可选配置
const labScene = new LabScene(app, {
  // 禁用后处理(描边、抗锯齿等),不需要时可禁用以提升性能。默认 false
  disablePostProcessing: false,
  // 自动更新(requestAnimationFrame 循环)。无模型动画时可关闭,自行控制更新时机。默认 true
  autoUpdate: true,
  // 禁用模型点击交互。不需要选中模型时可禁用。默认 false
  disableClickModel: false
})

// 请求数据
const v = Date.now()
Promise.all([
  request.get(`glb/config.json?v=${v}`),   // 模型列表数据
  request.get(`data.json?v=${v}`)          // 上一次保存的场景数据
]).then(([modelList, jsonData]) => {
  // 初始化列表(必须先调用,后续 initLab / setJSON / addModel 都依赖它)
  labScene.initList(modelList)
  // 有保存的数据则还原,否则加载第一个实验室
  if (jsonData) {
    labScene.setJSON(jsonData)
  } else {
    labScene.initLab(modelList.labs[0])
  }
})

// 监听焦点(选中模型)变化
labScene.addEventListener('focusChange', e => {
  console.log('焦点变化', e.model)
})

构造参数 options

| 字段 | 类型 | 默认值 | 说明 | |---|---|---|---| | disablePostProcessing | boolean | false | 禁用后处理(OutlinePass 描边、SMAA 抗锯齿) | | autoUpdate | boolean | true | 自动渲染循环。关闭后需手动调 update() 触发重绘 | | disableClickModel | boolean | false | 禁用模型点击/拖拽交互(仍可缩放相机) |

二、事件

基于 Three.js 的 EventDispatcher,用 addEventListener 监听:

| 事件 | 触发时机 | 回调参数 | |---|---|---| | focusChange | 焦点(选中模型)变化 | { model },取消选中时 modelundefined | | hoveron | 鼠标移入模型 | { model } | | hoveroff | 鼠标移出模型 | { model } | | pointerdown | 在模型上按下 | { model } | | pointerup | 在模型上抬起 | { model } | | click | 在模型上点击(按下到抬起 ≤ 200ms) | { model } |

// 监听选中模型变化(点击切换焦点,再次点击同一模型取消选中)
labScene.addEventListener('focusChange', e => {
  console.log('焦点变化', e.model)
})

三、属性

// 相机
labScene.camera

// 渲染器
labScene.renderer

// 轨道控制器(默认仅开启缩放)
labScene.orbitControl

// 变换控制器(平移、旋转)
labScene.transformControl

// 后处理合成器(未禁用后处理时存在)
labScene.composer

// 描边通道(未禁用后处理时存在)
labScene.outlinePass

// 实验室列表 object(按 id 索引),initList 后填充
labScene.labList
// 设备列表 object(按 id 索引),initList 后填充
labScene.equipmentList

// 当前实验室模型对象,未初始化时为 undefined
labScene.lab

// 当前可交互的设备模型数组(含实验室自带的门窗等)
labScene.equipments

// Boolean,默认 true,是否允许对设备进行平移、旋转
labScene.enabled

// 当前焦点设备模型,没有则为 undefined
labScene.focus
// 焦点模型绑定的业务数据(仅独立设备有;实验室设备的数据在 lab.equipments[key].data)
labScene.focus.data

// 绘制延迟时间,控制帧率(低性能设备用),单位 ms,默认 16(约 60fps)
// 性能差时可设为 33(约 30fps)或 66(约 15fps)
labScene.delayTime = 16

// 同时下载设备模型的最大并发数,默认 2
labScene.limitDownloadTasks

关于模型绑定数据:统一通过 model.data 读写。注意 data 是一个对象,不要直接替换 data 引用——实验室自带设备的 data 是引用数据,getJSON 时从 labEquipmentsData 读取,修改引用对象不会反映到导出结果。

// 控制场景灯光:0(全黑)~ 1(正常),开灯 1,关灯 0.4
labScene.renderer.toneMappingExposure = 1

四、方法

初始化与数据

// 初始化实验室/设备列表,必须在 initLab / setJSON / addModel 之前调用
// modelList 为 config.json 的内容
labScene.initList(modelList)

// 用 JSON 数据还原整个场景(实验室 + 设备)
// jsonData 为 getJSON 的返回值(字符串或对象均可)
// onLoad / onProgress(0~100) / onError 为可选回调
labScene.setJSON(jsonData, onLoad, onProgress, onError)

// 返回当前场景的 JSON 数据(字符串类型,可直接保存到后端)
const jsonStr = labScene.getJSON()

实验室与设备

// 单独初始化(切换)实验室模型,可传 id 或列表中的实验室数据
// 会自动清除当前实验室
labScene.initLab(typeId)                            // 传 id
labScene.initLab(labScene.labList[typeId])          // 传对象
// 完整签名:labScene.initLab(labConfig, onLoad, onProgress, onError)

// 单独导入设备模型到场景,可传 id 或列表中的设备数据
// 同一设备可多次添加(通过 config.list 指定多份)
labScene.addModel(typeId)
labScene.addModel(labScene.equipmentList[typeId])
// 完整签名:labScene.addModel(config, onLoad, onProgress, onError)
// config 为对象时支持 { id, list: [{ name, position, rotation, scale, data }] }

// 删除场景内的模型,可传单个模型或数组
// 删除实验室传 labScene.lab(其自带设备会一并删除)
// 删除独立设备传 labScene.equipments 中的某项(不影响实验室自带设备)
labScene.removeModels(model)
labScene.removeModels([model1, model2])

渲染与生命周期

// 手动触发一次绘制更新
// autoUpdate 关闭时,模型增删/动画/尺寸变化后需要手动调用
labScene.update()

// 销毁实例,释放资源(控制器、渲染器、事件、动画帧等)
labScene.destroy()

外部扩展点

// 注册逐帧更新钩子,fn 会收到 update 内部计算的 mixerDelta(单位秒)
// 供 ZoneManager 等伴随模块接入逐帧动画,不要在回调里做重活(处于渲染关键路径)
labScene.addUpdateHook(fn)

// 注销逐帧更新钩子
labScene.removeUpdateHook(fn)

五、模型动画控制

模型动画来自 GLB 文件内部,config.json 中通过 controls 声明每个动画的播放参数(name 对应 GLB 中 AnimationClip 的名称)。LabScene 会为每个声明的动画挂载 play 方法。

// play(immediate?, progress?)
//   immediate  Boolean,可选,立即跳到动画结果,无过渡动画
//   progress   Number 0~1,可选,设置动画播放到指定进度
// 生效条件:action.loop = LoopOnce 且 clampWhenFinished = true

实验室模型动画(门窗、窗帘)

实验室自带设备(窗户、窗帘、门等)挂在 lab.equipments 上,key 对应 config.json 中实验室 equipments 的字段名。动画控制名:on_window / off_window / on_curtain / off_curtain

// 实际使用需遍历 lab.equipments,根据 data 值匹配对应的设备再控制
labScene.lab.equipments['window1'].controls.on_window.play()    // 开窗
labScene.lab.equipments['window1'].controls.off_window.play()   // 关窗
labScene.lab.equipments['window1'].controls.on_curtain.play()   // 拉开窗帘
labScene.lab.equipments['window1'].controls.off_curtain.play()  // 关上窗帘

// 立即关窗(无过渡)
labScene.lab.equipments['window1'].controls.off_window.play(true)
// 窗户打开到 50% 进度
labScene.lab.equipments['window1'].controls.on_window.play(false, 0.5)

设备模型动画

独立设备动画挂在 equipmentList[typeId].models 数组的每个模型上。动画控制名:on / off

// models 是数组,需按 name 遍历查找目标模型
const models = labScene.equipmentList['KongTiao'].models
const target = models.find(m => m.name === 'KongTiao')
target.controls.on.play()    // 开启
target.controls.off.play()   // 关闭

六、数据格式

config.json(模型清单)

由后端返回,initList 时加载。结构示例:

{
  "labs": [
    {
      "id": "Lab1",
      "name": "实验室三窗两门",
      "url": "glb/lab/Lab1.glb",
      "config": {
        "wall":     { "xMin": 0.25, "xMax": 0.25, "yMin": 0.1, "yMax": 0, "zMin": 0.35, "zMax": 0.3 },
        "rotation": { "x": 0, "y": 0, "z": 0 },
        "scale":    { "x": 0.01, "y": 0.01, "z": 0.01 }
      },
      "equipments": {                       // 实验室自带的设备(窗户/窗帘/门等)
        "window1": {
          "type": "window",
          "name": "窗户窗帘",
          "enable": true,                   // 是否启用该设备
          "group": ["Lab1_ChuangLian01", "Lab1_Window01"],  // GLB 中对应的节点名
          "controls": {
            "on_window":  { "name": "ChuangHu1_DaKai", "only": false, "loop": false, "timeScale": 1,  "clampWhenFinished": true },
            "off_window": { "name": "ChuangHu1_DaKai", "only": false, "loop": false, "timeScale": -1, "clampWhenFinished": true }
          }
        }
      }
    }
  ],
  "equipments": [
    {
      "id": "KongTiao",
      "name": "空调",
      "url": "glb/equipment/KongTiao.glb",
      "config":   { "position": { "x": 0, "y": 0, "z": 0 }, "scale": { "x": 0.01, "y": 0.01, "z": 0.01 } },
      "controls": {
        "on":  { "name": "KaiQi", "loop": false, "timeScale": 1,  "clampWhenFinished": true },
        "off": { "name": "KaiQi", "loop": false, "timeScale": -1, "clampWhenFinished": true }
      }
    }
  ]
}

controls 中每项的字段含义:

| 字段 | 类型 | 说明 | |---|---|---| | name | string | GLB 中 AnimationClip 的名称 | | only | boolean | 播放时是否停止该模型其它动画 | | loop | boolean | 是否循环(LoopRepeat / LoopOnce) | | timeScale | number | 时间比例,默认 1;-1 倒放;0 暂停 | | clampWhenFinished | boolean | 单次播放后是否停在最后一帧(immediate / progress 依赖此选项生效) |

场景数据(getJSON / setJSON)

getJSON() 返回字符串setJSON() 接受字符串或对象。结构示例:

{
  "lab": {
    "id": "Lab2",
    "name": "实验室三窗两门",
    "equipments": { /* 实验室自带设备的绑定数据,key 与 config.json 一致 */ }
  },
  "equipments": [
    {
      "id": "KongTiao",
      "list": [
        {
          "name": "空调-1",
          "position": { "x": 0, "z": -3 },   // 只保存平移 x、z
          "rotation": { "x": 0, "y": 0, "z": 0 },
          "data": { /* 设备绑定的业务数据 */ }
        }
      ]
    }
  ]
}