唯杰地图 logo唯杰地图
VJMAP
EN

配置界面

vjmap3d/references/ui/uiconfig.md 15.4 KB518 行
原文

配置界面是基于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)