Svelte で簡易的に操作 UI パネルを追加できるコンポーネント svelte-tweakpane-ui

# はじめに

最近まとめている Threlte の実装を追っていくと、[サンプル](https://threlte.xyz/docs/reference/extras/sky)にある操作パネルの UI で「[svelte-tweakpane-ui](https://github.com/kitschpatrol/svelte-tweakpane-ui
)」なるものを使用しているものを見つけた。
元々は Tweakpane というもので、それの Svelte ラッパとなる様だ。

Threlte を使用した簡易ツールを実装する上では簡易 UI やデバッガ UI として使用できそうなので、これについての調査をまとめる。

https://github.com/kitschpatrol/svelte-tweakpane-ui

https://tweakpane.github.io/docs/

# インストール

**svelte-tweakpane-ui** を npm インストールする。

```bash
npm install svelte-tweakpane-ui
```

# 使い方

公式ドキュメントの方にはより基本的な作成の仕方が記載されいているが、今回行いたいことはあくまで調整用の UI を作成したい目的なので、UI 枠の作成を基本に操作メモを残しておく。

## ペインの作成

ペインの作成には `<Pane>` コンポーネントを使用する。

```vue
<script lang="ts">
  import { Pane } from 'svelte-tweakpane-ui';
</script>

<Pane title="Sky" position="fixed">

</Pane>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/9c3715aa-46cb-f227-3311-c3e2ba4f4eac.png)

## スライダーの追加

```vue
<script lang="ts">
	import { Slider, Pane } from 'svelte-tweakpane-ui';

	let elevation = 0.0;
</script>

<Pane title="Sky" position="fixed">
	<Slider bind:value={elevation} label="Elevation" min={-5} max={90} />
</Pane>
```

![output6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/3ed81335-5834-cd89-bc4f-856bc7498ae5.gif)

## チェックボックスの追加

```vue
<script lang="ts">
	import { Pane, Checkbox } from 'svelte-tweakpane-ui';

	let setEnvironment = true;
</script>

<Pane title="Sky" position="fixed">
	<Checkbox bind:value={setEnvironment} label="Set Environment" />
</Pane>
```

![output7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/103a371b-dabf-286b-c938-2bcd742f1250.gif)

## ポイントコントロール UI を追加する。

ポイントコントロールは、2D、3D、4D の各タイプが存在する。
それぞれに使用する際のタイプが異なるので、必要に応じてタイプをインポートしてきて当てる。

| 2D | 3D | 4D |
|:-----------:|:------------:|:------------:|
| PointValue2d | PointValue3d  | PointValue4d |
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/f0372ace-db1c-09b6-4010-725f8b77efbe.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/23c084be-15cb-1280-c000-eef22c0468dc.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/5725f6f7-3ddb-df5c-99ac-09d564b66ee3.png) |

```vue
<script lang="ts">
	import { Pane, Point, PointValue3d, type PointValue3d } from 'svelte-tweakpane-ui';

	let pos3d: PointValue3d = [0, 1.2, 0];
</script>

<Pane title="Control" position="fixed">
	<Point bind:value={pos3d} label="Sphere Position" />
</Pane>
```

![output8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/28758303-94dc-e370-d6e7-04773cbe52c4.gif)


:::note info
2D の場合でのみ、X-Y でのポインタコントローラーが使用できる。![output9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/3aad0941-2cb5-fddd-9155-b4fcacf9223c.gif)
:::



## フォルダで要素をくくる

フォルダでくくる場合は `<Folder>` コンポーネントを使用する。

```diff_vue
<script lang="ts">
+	import { Folder } from 'svelte-tweakpane-ui';

+	let skyFolderExpanded = true;
+	let sphereFolderExpanded = true;
</script>

<Pane title="Control" position="fixed">
+	<Folder bind:expanded={skyFolderExpanded} title="Sky">
		<Checkbox bind:value={setEnvironment} label="Set Environment" />
		<Slider bind:value={elevation} label="Elevation" min={-5} max={10} />
+	</Folder>
+	<Folder bind:expanded={sphereFolderExpanded} title="Sphere">
		<Point bind:value={pos3d} label="Sphere Position" />
+	</Folder>
</Pane>
```

![output10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/483027d1-008d-7c13-f11c-72beca82c298.gif)

## リスト

リストを適用する場合は `<List>` コンポーネントを使用する。

また以下の例では、値を変更した際に、リストのラベルから背景マップの URL を割り出して背景マップの URL として適用するための関数を、`change` イベントに対してバインドしている。

```diff_vue
<script lang="ts">
	import { T } from '@threlte/core';
	import { Environment, OrbitControls, Sky } from '@threlte/extras';
	import {
		Pane,
		Folder,
+		List,
+		type ListOptions,
	} from 'svelte-tweakpane-ui';

	let envMapFolderExpanded = true;
	let sphereFolderExpanded = true;

+   const envUrls = [
+	    '/textures/blouberg_sunrise_2_1k.hdr',
+		'/textures/pedestrian_overpass_1k.hdr',
+		'/textures/venice_sunset_1k.hdr'
+	];

+	const options: ListOptions<number> = {
+		blouberg_sunrise: 0,
+		pedestrian_overpass: 1,
+		venice_sunset: 2
+	};
+	let selection: number = 0;
+	let envMapUrl: string = envUrls[0];

+	const listSelectionChanged = (event: CustomEvent) => {
+		envMapUrl = envUrls[event.detail.value];
+	};
</script>

<Pane title="Control" position="fixed">
	<Folder bind:expanded={envMapFolderExpanded} title="Environment">
+		<List
+			bind:value={selection}
+			label="Environment Map"
+			{options}
+			on:change={listSelectionChanged}
+		/>
	</Folder>
	<Folder bind:expanded={sphereFolderExpanded} title="Sphere">
		<Point bind:value={pos3d} label="Sphere Position" />
	</Folder>
</Pane>

<Environment files={envMapUrl} isBackground={true} />
```

![output11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/5231b902-3b24-21ac-ec27-409afffbbff7.gif)



# トラブルシュート

## svelte-local-storage-store がエラーを起こす

コードを挿入して更新された場合は問題ないが、ページのリロード等が発生すると以下のエラーを起こす。

```log
1:46:12 [vite] Error when evaluating SSR module /src/routes/sky/+page.svelte: failed to import "/src/lib/components/sky/App.svelte"
|- Error [ERR_REQUIRE_ESM]: require() of ES Module F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte\src\runtime\store\index.js from F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js not supported.
Instead change the require of F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte\src\runtime\store\index.js in F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js:27:20)

Error [ERR_REQUIRE_ESM]: require() of ES Module F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte\src\runtime\store\index.js from F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js not supported.  
Instead change the require of F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte\src\runtime\store\index.js in F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (F:\Develop\Svelte\_learn\my-threlte-qiita\node_modules\svelte-local-storage-store\dist\index.js:27:20) {
  code: 'ERR_REQUIRE_ESM'
}
```

### 原因

svelte-local-storage-store に含まれるコードが ESM に対応しているようだが、これのコンバートに失敗している様子。
開発用サーバーを立ち上げた状態でインストールを試みていたため、このコンバートが走らなかったがゆえに起きたと推測される。

### 解決方法

とりあえず現状は開発用サーバーを立ち上げなおすことで直った。
開発用サーバーが立ち上がった際にコンバートが走った？


# まとめ

UI としての機能も結構豊富で、Three.js や Threlte との相性もとても良い様子。
現状では簡易実装として第一候補でいいと思う。