配置界面
配置界面是基于tweakpane封装的,用于在Web应用中创建交互式的参数调整面板。通过它,你可以轻松地将各种类型的数据和设置暴露给用户,让他们能够实时观察和调整,从而极大地提升了调试、演示或实验性质的工作效率。
使用
获取应用配置界面对象
const ui = await app.getConfigPane({ title: "配置", style: { width: "250px"}});
增加配置项 UiObjectConfig
let cfg = {
type: 'checkbox',
label: '可见',
property: [mesh, 'visible'],
}
ui.appendChild(cfg)
TypeScript 注记:
onChange声明为(...args: ChangeArgs) => void,其中ChangeArgs = [ChangeEvent, ...any[]] | never[](可能 0 参),所以(e) => …、({value}) => …这类带参箭头函数在 strict 下编译不过;setValue同理(首参是新值)。TS 下可用的写法只有无参或 rest 参数:onChange: () => { /* 不取值 */ } onChange: (...args: any[]) => { const e = args[0]; const v = e?.value; /* e: ChangeEvent */ } setValue: (v: any, ...rest: any[]) => { /* v = 新值 */ }本文示例均为 JS 写法,运行时无此限制。
getConfigPane返回对象除声明的configPane/rootPane/element/isInit/setVisible/isVisible/reset/appendChild/renderUiConfig/renderUiObject外,运行时还存在uiRefresh()(示例中用于手动刷新 UI),但该方法未在 d.ts 声明——TS 项目里需(ui as any).uiRefresh()。isReset选项默认true(每次调用清空面板)。
示例
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
const cfg = [
{
type: 'input',
label: 'uuid',
property: [mesh, 'uuid'],
},
{
type: 'checkbox',
label: '可见',
property: [mesh, 'visible'],
},
{
type: 'slider',
label: '缩放',
value: 1.0,
bounds: [0.3, 5],
stepSize: 0.1, // 注意键名是 stepSize(写 step 会被忽略)
onChange: ({ value } )=> {
mesh.scale.set(value, value, value)
}
},
{
type: 'vec3',
label: '位置',
property: [mesh, 'position'],
},
() => {
// 下面旋转模型时要改变旋转,刷新下需要改变,所以这里用函数的形式获取
return {
type: 'vec3',
label: '旋转',
property: [mesh, 'rotation'],
}
},
{
type: 'color',
label: '颜色',
property: [mesh.material, 'color'],
},
{
type: 'dropdown',
label: 'side',
property: [mesh.material, 'side'],
children: [
{
label: "正面",
value: THREE.FrontSide,
},{
label: "反面",
value: THREE.BackSide,
},{
label: "双面",
value: THREE.DoubleSide,
}
]
},
{
type: 'folder',
label: '旋转模型',
expanded: true,
children: [
'X +', 'X -', 'Y +', 'Y -', 'Z +', 'Z -',
].map((l)=>{
return {
type: 'button',
label: 'Rotate ' + l + '10',
value: (e)=>{
mesh.rotateOnAxis(new THREE.Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 18 * (l.includes('-') ? -1 : 1))
// 刷新UI
ui.uiRefresh()
}
}
}),
},
]
ui.appendChild( {
type: 'folder',
label: 'Mesh',
expanded: true,
children: cfg
})
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
const mesh = new THREE.Mesh(
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.MeshStandardMaterial( { color: 0x00ffff } )
)
mesh.position.set(-2, 0, 0);
app.scene.add(mesh);
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
const cfg = [
{
type: 'input',
label: 'uuid',
property: [mesh, 'uuid'],
},
{
type: 'checkbox',
label: '可见',
property: [mesh, 'visible'],
},
{
type: 'slider',
label: '缩放',
value: 1.0,
bounds: [0.3, 5],
stepSize: 0.1, // 注意键名是 stepSize(写 step 会被忽略)
onChange: ({ value } )=> {
mesh.scale.set(value, value, value)
}
},
{
type: 'vec3',
label: '位置',
property: [mesh, 'position'],
},
() => {
// 下面旋转模型时要改变旋转,刷新下需要改变,所以这里用函数的形式获取
return {
type: 'vec3',
label: '旋转',
property: [mesh, 'rotation'],
}
},
{
type: 'color',
label: '颜色',
property: [mesh.material, 'color'],
},
{
type: 'dropdown',
label: 'side',
property: [mesh.material, 'side'],
children: [
{
label: "正面",
value: THREE.FrontSide,
},{
label: "反面",
value: THREE.BackSide,
},{
label: "双面",
value: THREE.DoubleSide,
}
]
},
{
type: 'folder',
label: '旋转模型',
expanded: true,
children: [
'X +', 'X -', 'Y +', 'Y -', 'Z +', 'Z -',
].map((l)=>{
return {
type: 'button',
label: 'Rotate ' + l + '10',
value: (e)=>{
mesh.rotateOnAxis(new THREE.Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 18 * (l.includes('-') ? -1 : 1))
// 刷新UI
ui.uiRefresh()
}
}
}),
},
]
ui.appendChild( {
type: 'folder',
label: 'Mesh',
expanded: true,
children: cfg
})
可通过generateUiConfig根据一个对象自动获取配置信息
可以直接赋值一个object对象获取配置属性
let options = {
duration: 5,
looping: true,
emissionBursts: {
time: 0,
count: {
// 如果某项指定了uiConfig属性,就用uiConfig中的配置
uiConfig: {
type: "slider",
label: "个数",
value: 10,
bounds: [0, 100],
stepSize: 1 // 注意键名是 stepSize(写 step 会被忽略)
}
}
}
}
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
let cfg = {
type: 'folder',
expanded: true,
app: app,
label: "参数",
children: vjmap3d.generateUiConfig(options),
onChange: v => {
updateParticle(options)
}
}
ui.appendChild(cfg)
也可以指定一个对象中的特定属性获取配置属性
const light = new THREE.SpotLight( "#ff4800", 110 );
light.angle = 0.3;
light.penumbra = 0.2;
light.decay = 2;
light.distance = 50;
light.position.set( -7, 2.6, 8.6 );
app.scene.add(light)
let config = [
{
prop: "position"
},
{
prop: "color"
},
{
prop: "intensity"
},
{
prop: "angle"
},
{
prop: "penumbra"
},
{
prop: "decay"
},
{
prop: "distance",
label: "距离",
bounds: [0, 200],
stepSize: 1, // 注意键名是 stepSize(写 step 会被忽略)
type: "slider",
value: 50 // 缺省值
}
];
const ui = await app.getConfigPane({ title: "SpotLight设置", style: { width: "250px"}});
let cfg = vjmap3d.generateUiConfig(light, config);
cfg.forEach(c => ui.appendChild(c))
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
defaultLights: false
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
toEntity: true,
scale: [10, 10, 10]
});
entity.position.set(-2, 0, 1);
entity.addTo(app);
const light = new THREE.SpotLight( "#ff4800", 110 );
light.angle = 0.3;
light.penumbra = 0.2;
light.decay = 2;
light.distance = 50;
light.position.set( -7, 2.6, 8.6 );
app.scene.add(light)
let config = [
{
prop: "position"
},
{
prop: "color"
},
{
prop: "intensity"
},
{
prop: "angle"
},
{
prop: "penumbra"
},
{
prop: "decay"
},
{
prop: "distance",
value: "50" // 缺省值
}
];
const ui = await app.getConfigPane({ title: "SpotLight设置", style: { width: "250px"}});
let cfg = vjmap3d.generateUiConfig(light, config);
cfg.forEach(c => ui.appendChild(c))
为了兼容threejs中lil-gui的写法示例,对上面的写法进行了封装,封装了UiPanelJsonConfig, 上面的代码,也可以用下面的代码代替
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
let folder = gui.addFolder("mesh");
folder.open();
folder.add(mesh, 'uuid');
folder.add(mesh, 'visible').label("可见");
folder.addBinding({scale: 1}, "scale", {type: "slider", min: 0.5, max: 5, step: 0.1}).onChange(v => mesh.scale.set(v, v, v))
folder.add(mesh, 'position').label("位置");
folder.addFunction(() => {
// 下面旋转模型时要改变旋转,刷新下需要改变,所以这里用函数的形式获取
return {
type: 'vec3',
label: '旋转',
property: [mesh, 'rotation'],
}
});
folder.add(mesh.material, 'color').label("颜色");
folder.add(mesh.material, 'side', [
{
label: "正面",
value: THREE.FrontSide,
},{
label: "反面",
value: THREE.BackSide,
},{
label: "双面",
value: THREE.DoubleSide,
}
]);
let subFolder = folder.addFolder("旋转模型");
subFolder.open();
[
'X +', 'X -', 'Y +', 'Y -', 'Z +', 'Z -',
].forEach((l)=>{
subFolder.addButton('Rotate ' + l + '10', (e)=>{
mesh.rotateOnAxis(new THREE.Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 18 * (l.includes('-') ? -1 : 1))
// 刷新UI
ui.uiRefresh()
})
});
gui.toJson().forEach(c => ui.appendChild(c))
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
const mesh = new THREE.Mesh(
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.MeshStandardMaterial( { color: 0x00ffff } )
)
mesh.position.set(-2, 0, 0);
app.scene.add(mesh);
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
let folder = gui.addFolder("mesh");
folder.open();
folder.add(mesh, 'uuid');
folder.add(mesh, 'visible').label("可见");
folder.addBinding({scale: 1}, "scale", {type: "slider", min: 0.5, max: 5, step: 0.1}).onChange(v => mesh.scale.set(v, v, v))
folder.add(mesh, 'position').label("位置");
folder.addFunction(() => {
// 下面旋转模型时要改变旋转,刷新下需要改变,所以这里用函数的形式获取
return {
type: 'vec3',
label: '旋转',
property: [mesh, 'rotation'],
}
});
folder.add(mesh.material, 'color').label("颜色");
folder.add(mesh.material, 'side', [
{
label: "正面",
value: THREE.FrontSide,
},{
label: "反面",
value: THREE.BackSide,
},{
label: "双面",
value: THREE.DoubleSide,
}
]);
let subFolder = folder.addFolder("旋转模型");
subFolder.open();
[
'X +', 'X -', 'Y +', 'Y -', 'Z +', 'Z -',
].forEach((l)=>{
subFolder.addButton('Rotate ' + l + '10', (e)=>{
mesh.rotateOnAxis(new THREE.Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 18 * (l.includes('-') ? -1 : 1))
// 刷新UI
ui.uiRefresh()
})
});
gui.toJson().forEach(c => ui.appendChild(c))
内置配置
如果一个对象实现了 setupUiConfig 方法,那么可以通过读取它的 uiConfig 属性(getter,首次访问时调用 setupUiConfig() 生成并缓存,不要写成 uiConfig())获取此对象的配置信息。如应用对象的配置:
const ui = await app.getConfigPane()
ui.appendChild(app.uiConfig)
对于一个object3d对象,可以通过makeObject3DUiConfig获取
const ui = await app.getConfigPane()
let config = vjmap3d.makeObject3DUiConfig(object)
ui.appendChild(config)
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
for(let i = 0; i < 10; i++) {
const mesh = new THREE.Mesh(
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.MeshStandardMaterial( { color: vjmap3d.randColor() } )
)
mesh.position.copy(vjmap3d.toVector3(vjmap3d.randPoint3D()));
let meshEntity = vjmap3d.Entity.fromObject3d(mesh);
meshEntity.addTo(app);
}
const ui = await app.getConfigPane()
ui.appendChild(app.uiConfig)
let opts = {
highlightUseBoxHelper: true,
highlightOpacity: 0,
pickCallBack: async (param) => {
ui.reset();
if (param.curPick) {
let config;
if (app.Input.getKeyPressed("ControlLeft") && param.curPick.intersection?.object) {
// 如果按往了ctrl键,则只选择中当前选择的
let obj = param.curPick.intersection?.object;
config = vjmap3d.makeObject3DUiConfig(obj)
} else {
config = param.curPick.target.uiConfig;
}
ui.appendChild(config)
} else {
const ui = await app.getConfigPane()
ui.appendChild(app.uiConfig)
}
}
}
app.setCurrentTool("pick", opts)