# 配置界面 
配置界面是基于[tweakpane](https://tweakpane.github.io/docs/v3/)封装的，用于在Web应用中创建交互式的参数调整面板。通过它，你可以轻松地将各种类型的数据和设置暴露给用户，让他们能够实时观察和调整，从而极大地提升了调试、演示或实验性质的工作效率。

## 使用

获取应用配置界面对象
```js
const ui = await app.getConfigPane({ title: "配置", style: { width: "250px"}});
 ```

增加配置项 [UiObjectConfig](https://vjmap.com/map3d/api/interfaces/UiObjectConfig.html)
```js
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 参数：
> ```ts
> 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`（每次调用清空面板）。

## 示例
```js
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
})
```

<mapFrame>
<pre>
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
})
</pre>
</mapFrame>

可通过[generateUiConfig](https://vjmap.com/map3d/api/functions/generateUiConfig.html)根据一个对象自动获取配置信息

可以直接赋值一个object对象获取配置属性
```js
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)
```

也可以指定一个对象中的特定属性获取配置属性
```js
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))
```
<mapFrame>
<pre>
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))
</pre>
</mapFrame>

为了兼容`threejs`中`lil-gui`的写法示例，对上面的写法进行了封装，封装了[UiPanelJsonConfig](https://vjmap.com/map3d/api/classes/UiPanelJsonConfig.html), 上面的代码，也可以用下面的代码代替
```js
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))
```

<mapFrame>
<pre>
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))
</pre>
</mapFrame>

## 内置配置
如果一个对象实现了 `setupUiConfig` 方法，那么可以通过读取它的 `uiConfig` **属性**（getter，首次访问时调用 `setupUiConfig()` 生成并缓存，不要写成 `uiConfig()`）获取此对象的配置信息。如应用对象的配置：
```js
const ui = await app.getConfigPane()
ui.appendChild(app.uiConfig)
```

对于一个`object3d`对象，可以通过`makeObject3DUiConfig`获取
```js
const ui = await app.getConfigPane()
let config = vjmap3d.makeObject3DUiConfig(object)
ui.appendChild(config)
```
<mapFrame>
<pre>
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)
</pre>
</mapFrame>