├── .npmignore
├── .eslintignore
├── src
├── tsconfig.json
├── index.ts
├── tpUtils.ts
├── tpGeneratorsThree.ts
├── UiConfigRendererTweakpane.ts
└── tpGenerators.ts
├── .idea
├── watcherTasks.xml
├── vcs.xml
├── jsLinters
│ └── eslint.xml
├── .gitignore
├── jsLibraryMappings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
└── uiconfig-tweakpane.iml
├── index.html
├── tsconfig.json
├── LICENSE
├── .gitignore
├── examples
├── threepipe.html
├── dynamic-children.html
├── threepipe-pick.html
└── path-syntax.html
├── .github
└── workflows
│ └── docs-pages.yml
├── vite.config.js
├── README.md
├── package.json
└── .eslintrc.cjs
/.npmignore:
--------------------------------------------------------------------------------
1 | *.backup
2 | temp
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | examples/dist
4 | public
5 | config
6 | libs
7 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "compilerOptions": {
4 | "outDir": "../lib/esm"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {UiConfigRendererTweakpane} from './UiConfigRendererTweakpane'
2 | import {THREE} from './tpGeneratorsThree'
3 |
4 | class UI extends UiConfigRendererTweakpane {
5 |
6 | }
7 |
8 | export {UiConfigRendererTweakpane, UI, type THREE}
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Threepipe example
6 |
7 |
14 |
15 |
16 |
17 |
18 | Examples
19 | Path Syntax
20 | Threepipe
21 | Threepipe Pick
22 | Dynamic Children
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/uiconfig-tweakpane.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": "./",
5 | "allowSyntheticDefaultImports": true,
6 | "experimentalDecorators": true,
7 | "isolatedModules": true,
8 | "module": "es2015",
9 | "noImplicitAny": true,
10 | "declaration": true,
11 | "skipLibCheck": true,
12 | "noImplicitThis": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "removeComments": true,
16 | "preserveConstEnums": true,
17 | "moduleResolution": "Bundler",
18 | "emitDecoratorMetadata": true,
19 | "sourceMap": true,
20 | "target": "es6",
21 | "strictNullChecks": true,
22 | "lib": [
23 | "es5",
24 | "es2015",
25 | "es2016",
26 | "es2017",
27 | "es2019",
28 | "esnext",
29 | "dom"
30 | ]
31 | },
32 | "include": [
33 | "src/**/*"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "dist",
38 | "**/*.spec.ts"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 repalash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | lib
64 | dist
65 | examples/dist
66 | docs
67 |
68 | temp
69 |
--------------------------------------------------------------------------------
/examples/threepipe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Threepipe example
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/.github/workflows/docs-pages.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Build docs and deploy to github pages.
5 |
6 | on:
7 | # Runs on pushes targeting the default branch
8 | push:
9 | branches: ["master"]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow one concurrent deployment
21 | concurrency:
22 | group: "pages"
23 | cancel-in-progress: true
24 |
25 | jobs:
26 | build-and-deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 |
31 | runs-on: ubuntu-latest
32 |
33 | strategy:
34 | matrix:
35 | node-version: [18.x]
36 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
37 |
38 | steps:
39 | - uses: actions/checkout@v4
40 | - name: Use Node.js ${{ matrix.node-version }}
41 | uses: actions/setup-node@v4
42 | with:
43 | node-version: ${{ matrix.node-version }}
44 | cache: 'npm'
45 | - run: npm ci
46 | - run: npm run docs
47 | - name: Upload artifact
48 | uses: actions/upload-pages-artifact@v3
49 | with:
50 | path: 'docs'
51 | - name: Deploy to GitHub Pages
52 | id: deployment
53 | uses: actions/deploy-pages@v4
54 | - run: rm -rf docs
55 |
56 |
--------------------------------------------------------------------------------
/src/tpUtils.ts:
--------------------------------------------------------------------------------
1 | import {BladeApi, FolderApi} from 'tweakpane'
2 | import {BladeController, View} from '@tweakpane/core'
3 | import {UiObjectConfig} from 'uiconfig.js'
4 |
5 | /**
6 | * Check if child.parent === parent
7 | * @param child
8 | * @param parent
9 | */
10 | export const tweakpaneCheckParent = (child: BladeApi>, parent: FolderApi) => {
11 | return parent.controller_.rackController.rack === child.controller_.parent
12 | }
13 | /**
14 | * Move to a new parent or change index in the same parent.
15 | * @param child
16 | * @param newParent
17 | * @param index
18 | */
19 | export const tweakPaneMoveToParentIndex = (child: BladeApi>, newParent: FolderApi, index: number): boolean => {
20 | // console.log('tweakpane moving', (child as any).title, newParent.title, index)
21 | const cont = child.controller_
22 | const ind = cont.parent?.children?.indexOf(cont)
23 | const sameParent = tweakpaneCheckParent(child, newParent)
24 | if (!sameParent) {
25 | if (ind !== undefined && ind >= 0) {
26 | cont.parent?.remove(cont)
27 | }
28 | newParent.add(child as any, index)
29 | return true
30 | } else {
31 | if (ind !== index) {
32 | const i = newParent.controller_.rackController.rack.children.indexOf(cont)
33 | if (i !== -1) {
34 | newParent.controller_.rackController.rack.children.splice(i, 1)
35 | newParent.controller_.rackController.rack.children.splice(index, 0, cont)
36 | return true
37 | }
38 | }
39 | }
40 | return false
41 | }
42 |
43 | /**
44 | * Set up property forwarding from parent config to child config
45 | * @param child - The child config that should inherit properties
46 | * @param parent - The parent config to inherit properties from
47 | */
48 | export const setupPropertyForwarding = (child: UiObjectConfig, parent: UiObjectConfig): void => {
49 | if (child.property === undefined &&
50 | child.value === undefined &&
51 | child.getValue === undefined &&
52 | child.setValue === undefined &&
53 | child.type !== 'button' &&
54 | (parent.property !== undefined || parent.value !== undefined)
55 | ) {
56 | child.property = parent.property !== undefined ? parent.property : [parent, 'value']
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/tpGeneratorsThree.ts:
--------------------------------------------------------------------------------
1 | import {FolderApi} from 'tweakpane'
2 | import {UiObjectConfig} from 'uiconfig.js'
3 | import {getOrCall} from 'ts-browser-helpers'
4 | import {UiConfigRendererTweakpane} from './UiConfigRendererTweakpane'
5 | // eslint-disable-next-line @typescript-eslint/naming-convention
6 | import type * as THREE_1 from 'three'
7 | import {tpInputGenerator} from './tpGenerators'
8 |
9 | export type THREE = Pick
10 |
11 | export const tpColorInputGenerator = (parent: FolderApi, config: UiObjectConfig, renderer: UiConfigRendererTweakpane, params?: any) => {
12 | if (!renderer.THREE) {
13 | console.error('tpColorInputGenerator requires THREE to be set in the renderer')
14 | return
15 | }
16 | if (!config.__proxy) {
17 | config.__proxy = {
18 | forceOnChange: false,
19 | }
20 | const uiColorSpace = 'srgb'
21 | const tempColor = new renderer.THREE.Color()
22 | Object.defineProperty(config.__proxy, 'value', {
23 | get: () => {
24 | // config.__proxy.value_ = renderer.methods.getValue(config) // this is done below so it will be triggered on ui refresh
25 | const cc: any = config.__proxy.value_
26 | if (cc)
27 | return tempColor.set(cc).getHex(uiColorSpace)
28 | return 0
29 | },
30 | set: (v: number) => {
31 | config.__proxy.value_ = renderer.methods.getValue(config, config.__proxy.value_ || undefined)
32 | const cc: any = config.__proxy.value_
33 | const tempC = tempColor.setHex(v, uiColorSpace)
34 | if (cc?.isColor) {
35 | (cc as THREE_1.Color).copy(tempC)
36 | } else if (typeof cc === 'number') config.__proxy.value_ = tempC.getHex()
37 | else if (typeof cc === 'string') config.__proxy.value_ = '#' + tempC.getHexString()
38 | },
39 | })
40 | }
41 | config.__proxy.value_ = renderer.methods.getValue(config, config.__proxy.value_ || undefined)
42 | // console.log(config.__proxy.value_)
43 | params = params ?? {}
44 | params.view = 'color'
45 | if (getOrCall(config.inlinePicker))
46 | params.picker = 'inline'
47 | return tpInputGenerator(parent, config, renderer, params)
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/examples/dynamic-children.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Path Syntax JS
6 |
7 |
8 |
9 |
10 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import json from '@rollup/plugin-json';
3 | import dts from 'vite-plugin-dts'
4 | import packageJson from './package.json';
5 | import license from 'rollup-plugin-license';
6 | import replace from '@rollup/plugin-replace';
7 | import path from 'node:path';
8 |
9 | const isProd = process.env.NODE_ENV === 'production'
10 | const { name, version, author } = packageJson
11 | const {main, module, browser} = packageJson
12 |
13 | export default defineConfig({
14 | optimizeDeps: {
15 | exclude: ['uiconfig.js', 'ts-browser-helpers'],
16 | },
17 | // define: {
18 | // 'process.env': process.env
19 | // },
20 | build: {
21 | sourcemap: true,
22 | minify: isProd,
23 | cssMinify: isProd,
24 | cssCodeSplit: false,
25 | watch: !isProd ? {
26 | buildDelay: 1000,
27 | } : null,
28 | lib: {
29 | entry: 'src/index.ts',
30 | formats: isProd ? ['es', 'umd'] : ['es'],
31 | name: name,
32 | fileName: (format) => (format === 'umd' ? browser : module).replace('dist/', ''),
33 | },
34 | outDir: 'dist',
35 | emptyOutDir: isProd,
36 | commonjsOptions: {
37 | exclude: [/uiconfig.js/, /ts-browser-helpers/],
38 | },
39 | rollupOptions: {
40 | output: {
41 | // inlineDynamicImports: false,
42 | },
43 | },
44 | },
45 | plugins: [
46 | isProd ? dts({tsconfigPath: './tsconfig.json'}) : null,
47 | replace({
48 | 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'),
49 | preventAssignment: true,
50 | }),
51 | json(),
52 | // postcss({
53 | // modules: false,
54 | // autoModules: true, // todo; issues with typescript import css, because inject is false
55 | // inject: false,
56 | // minimize: isProduction,
57 | // // Or with custom options for `postcss-modules`
58 | // }),
59 | license({
60 | banner: `
61 | @license
62 | ${name} v${version}
63 | Copyright 2022<%= moment().format('YYYY') > 2022 ? '-' + moment().format('YYYY') : null %> ${author}
64 | ${packageJson.license} License
65 | See ./dependencies.txt for bundled third-party dependencies and licenses.
66 | `,
67 | thirdParty: {
68 | output: path.join(__dirname, 'dist', 'dependencies.txt'),
69 | includePrivate: true, // Default is false.
70 | },
71 | }),
72 | ],
73 | })
74 |
--------------------------------------------------------------------------------
/examples/threepipe-pick.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Threepipe example
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UiConfig Tweakpane
2 |
3 | [](https://www.npmjs.com/package/uiconfig-tweakpane)
4 | [](https://opensource.org/licenses/MIT)
5 |
6 | Tweakpane theme/wrapper library for [uiconfig.js](https://github.com/repalash/uiconfig.js): A UI renderer framework to dynamically generate website/configuration UIs from a JSON-like configurations and/or typescript decorators.
7 |
8 | It includes several components for editor-like user interfaces like folders, sliders, pickers, inputs for string, number, file, vector, colors, etc.
9 |
10 | The UI components are bound to javascript/typescript objects and properties through a JSON configuration.
11 |
12 | ### Examples
13 |
14 | Basic Examples: https://repalash.com/uiconfig.js/examples/index.html
15 |
16 | Threepipe Basic UI: https://threepipe.org/examples/#tweakpane-ui-plugin/
17 |
18 | Threepipe Editor: https://threepipe.org/examples/#tweakpane-editor/
19 |
20 | ## Installation and Usage
21 |
22 | ### npm package
23 |
24 | Install the `uiconfig-tweakpane` package from npm.
25 | ```bash
26 | npm install uiconfig-tweakpane
27 | ```
28 |
29 | Use in any javascript/typescript file.
30 | ```typescript
31 | import { UI } from 'uiconfig-tweakpane';
32 |
33 | const config = {
34 | type: "slider",
35 | label: "slider",
36 | value: 0.5,
37 | bounds: [0, 1],
38 | onChange: () => {
39 | console.log("changed", config.value);
40 | },
41 | }
42 |
43 | const ui = new UI();
44 | ui.appendChild(config);
45 | ```
46 |
47 | ### CDN link
48 |
49 | The module can be imported to HTML/JS a CDN link using [unpkg](https://unpkg.com/) or [jsdelivr](https://www.jsdelivr.com/).
50 |
51 | ```html
52 |
53 |
54 |
55 | ```
56 |
57 | The module can be accessed with the short-form `tpui`
58 | ```html
59 |
71 | ```
72 |
73 | ## Configuration
74 |
75 | Check the documentation at [uiconfig.js](https://github.com/repalash/uiconfig.js) on how to create a configuration for the UI.
76 |
77 | ## Components
78 |
79 | 1. `folder/panel` - A folder that can be collapsed and expanded. It can have other components as children.
80 | 2. `input` - A text input field for any kind of primitive types. The type is determined automatically from initial value.
81 | 3. `number` - A number input field for numbers.
82 | 4. `slider` - A slider for numbers.
83 | 5. `dropdown` - A dropdown. Options can be specified in children with label and optional value properties.
84 | 6. `checkbox/toggle` - A checkbox for boolean values.
85 | 7. `button` - A button that can trigger a function, `onClick` or bound property/value function.
86 | 8. `color` - A color picker for colors.
87 | 9. `vector/vec2/vec3/vec4` - Multiple number input fields in a row for vectors.
88 |
89 | ## Three.js integration
90 |
91 | Set the three.js classes for Color, Vector2, Vector3, Vector4 in the renderer and the color and vector components will automatically use them.
92 |
93 | ```typescript
94 | import { UI } from 'uiconfig-tweakpane';
95 | import { Color, Vector4, Vector3, Vector2 } from 'three';
96 |
97 | const ui = new UI();
98 | ui.THREE = {Color, Vector4, Vector3, Vector2}
99 | ```
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uiconfig-tweakpane",
3 | "name:umd": "tpui",
4 | "version": "1.0.1",
5 | "description": "Tweakpane wrapper and custom components for web controls UI. To be used with uiconfig.js",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "browser": "dist/index.js",
9 | "types": "dist/index.d.ts",
10 | "source": "src/index.ts",
11 | "sideEffects": false,
12 | "scripts": {
13 | "compile:esm:watch": "tsc -p ./src --watch",
14 | "compile:esm": "tsc -p ./src",
15 |
16 | "build": "vite build && npm run compile:esm",
17 | "start": "vite",
18 | "dev": "NODE_ENV=development vite build --watch",
19 | "test": "npm run check",
20 |
21 | "prepare": "npm run build && npm run docs",
22 | "docs": "npx typedoc ./src/index.ts",
23 | "serve-docs": "ws -d docs -p 8080",
24 | "serve": "ws",
25 | "lint": "eslint src --fix",
26 | "check": "tsc -p tsconfig.json --noEmit && eslint src",
27 |
28 | "new:pack": "npm run prepare && clean-package && npm pack; clean-package restore",
29 | "new:publish": "npm run test && git diff --exit-code --name-only HEAD * && npm run prepare && clean-package && npm publish && clean-package restore && git tag v$npm_package_version"
30 | },
31 | "exports": {
32 | ".": {
33 | "types": "./dist/index.d.ts",
34 | "import": "./dist/index.mjs",
35 | "require": "./dist/index.js"
36 | },
37 | "./dist/": {
38 | "import": "./dist/",
39 | "require": "./dist/"
40 | },
41 | "./lib/esm": {
42 | "types": "./lib/esm/index.d.ts",
43 | "import": "./lib/esm/index.js"
44 | },
45 | "./lib/esm/*": {
46 | "types": "./lib/esm/*.d.ts",
47 | "import": "./lib/esm/*.js"
48 | }
49 | },
50 | "clean-package": {
51 | "remove": [
52 | "clean-package",
53 | "scripts",
54 | "devDependencies",
55 | "optionalDependencies"
56 | ]
57 | },
58 | "files": [
59 | "lib",
60 | "dist",
61 | "src",
62 | "docs",
63 | "tsconfig.json"
64 | ],
65 | "repository": {
66 | "type": "git",
67 | "url": "git+https://github.com/repalash/uiconfig-tweakpane.git"
68 | },
69 | "keywords": [
70 | "javascript",
71 | "typescript",
72 | "json",
73 | "ui",
74 | "three.js",
75 | "html",
76 | "tweakpane",
77 | "library"
78 | ],
79 | "author": "repalash ",
80 | "license": "MIT",
81 | "bugs": {
82 | "url": "https://github.com/repalash/uiconfig-tweakpane/issues"
83 | },
84 | "homepage": "https://github.com/repalash/uiconfig-tweakpane#readme",
85 | "devDependencies": {
86 | "@rollup/plugin-json": "^6.0.0",
87 | "@rollup/plugin-replace": "^6.0.2",
88 | "@tweakpane/core": "1.1.9",
89 | "@typescript-eslint/eslint-plugin": "^5.62.0",
90 | "clean-package": "^2.2.0",
91 | "eslint": "^8.57.1",
92 | "eslint-plugin-deprecation": "^3.0.0",
93 | "eslint-plugin-html": "^8.1.2",
94 | "eslint-plugin-import": "^2.31.0",
95 | "local-web-server": "^5.3.0",
96 | "rimraf": "^5.0.1",
97 | "rollup-plugin-license": "^3.0.1",
98 | "tslib": "^2.5.0",
99 | "tweakpane": "3.1.10",
100 | "typedoc": "^0.27.5",
101 | "typescript": "^5.7.2",
102 | "vite": "^7.0.4",
103 | "vite-plugin-dts": "^4.4.0",
104 | "@pangenerator/tweakpane-textarea-plugin": "^1.0.4"
105 | },
106 | "dependencies": {
107 | "uiconfig.js": ">=0.2.1",
108 | "ts-browser-helpers": ">=0.19.3"
109 | },
110 | "peerDependencies": {
111 | "@tweakpane/core": "1.1.9",
112 | "tweakpane": "3.1.10",
113 | "@types/three": ">=0.152.1",
114 | "@pangenerator/tweakpane-textarea-plugin": "^1.0.4"
115 | },
116 | "peerDependenciesMeta": {
117 | "@tweakpane/core": {
118 | "optional": true
119 | },
120 | "tweakpane": {
121 | "optional": true
122 | },
123 | "@types/three": {
124 | "optional": true
125 | },
126 | "@pangenerator/tweakpane-textarea-plugin": {
127 | "optional": true
128 | }
129 | },
130 | "//": {
131 | "dependencies": {
132 | "uiconfig.js": "^0.1.6"
133 | },
134 | "local_dependencies": {
135 | "uiconfig.js": "file:./../uiconfig.js/"
136 | }
137 | },
138 | "optionalDependencies": {
139 | "win-node-env": "^0.6.1"
140 | },
141 | "browserslist": [
142 | "defaults"
143 | ],
144 | "type": "module"
145 | }
146 |
--------------------------------------------------------------------------------
/examples/path-syntax.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Path Syntax JS
6 |
7 |
8 |
9 |
10 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/src/UiConfigRendererTweakpane.ts:
--------------------------------------------------------------------------------
1 | import {AnyOptions, createDiv, createStyles, css, getOrCall, JSUndoManager} from 'ts-browser-helpers'
2 | import {BladeApi, FolderApi, Pane, TabPageApi} from 'tweakpane'
3 | import {TUiRefreshModes, UiConfigRenderer, UiObjectConfig} from 'uiconfig.js'
4 | import {BladeController, View} from '@tweakpane/core'
5 | import {
6 | tpButtonInputGenerator,
7 | tpDropdownInputGenerator,
8 | tpFolderGenerator,
9 | tpInputGenerator, tpSeparatorGenerator,
10 | tpSliderInputGenerator,
11 | tpTabGenerator, tpTextAreaInputGenerator,
12 | tpVecInputGenerator,
13 | } from './tpGenerators'
14 | import {THREE, tpColorInputGenerator} from './tpGeneratorsThree'
15 | import * as TextareaPlugin from '@pangenerator/tweakpane-textarea-plugin'
16 |
17 | export class UiConfigRendererTweakpane extends UiConfigRenderer {
18 |
19 | protected _root?: Pane
20 |
21 | unmount() {
22 | this._root?.dispose()
23 | super.unmount()
24 | }
25 |
26 | constructor(container: HTMLElement = document.body, {expanded = true, autoPostFrame = true} = {}, undoManager?: JSUndoManager|false) {
27 | super(container, autoPostFrame, undefined, undoManager)
28 | if (this._root) this._root.expanded = expanded
29 | }
30 |
31 | protected _createUiContainer(): HTMLDivElement {
32 | const container = createDiv({id: 'tweakpaneUiContainer', addToBody: false})
33 | createStyles(css`
34 | :root{
35 | --tweakpane-ui-container-width: 300px;
36 | }
37 | @media only screen and (min-width: 1500px) {
38 | :root{
39 | --tweakpane-ui-container-width: 300px;
40 | }
41 | }
42 | @media only screen and (min-width: 2500px) {
43 | :root{
44 | --tweakpane-ui-container-width: 500px;
45 | }
46 | }
47 | #tweakpaneUiContainer {
48 | position: fixed;
49 | top: 1rem;
50 | padding-right: 4px;
51 | padding-bottom: 20px;
52 | right: 1rem;
53 | width: var(--tweakpane-ui-container-width);
54 | height: auto;
55 | overflow-y: scroll;
56 | z-index: 100;
57 | pointer-events: auto;
58 | max-height: calc(100% - 3rem);
59 | border-radius: 0.5rem;
60 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
61 | }
62 | `)
63 |
64 | this._root = new Pane({title: 'Configuration', container})
65 | this._root.registerPlugin(TextareaPlugin)
66 | return container
67 | }
68 |
69 |
70 | appendChild(config?: UiObjectConfig, params?: UiObjectConfig) {
71 | if (!config) return
72 | super.appendChild(config, params)
73 | this.renderUiConfig(config)
74 | }
75 |
76 | renderUiConfig(uiConfig: UiObjectConfig): void {
77 | this.renderUiObject(uiConfig)
78 | }
79 |
80 | renderUiObject(uiConfig: UiObjectConfig, parent?: FolderApi | TabPageApi) {
81 | parent = parent ?? this._root
82 | if (!uiConfig.type) return
83 | // console.log(`Render ui ${uiConfig.label} in ${parent?.title}`)
84 | this.methods.initUiConfig(uiConfig)
85 | if (uiConfig.uiRef && uiConfig.uiRefType !== uiConfig.type) {
86 | // console.log('Removing UI object because of type mismatch', uiConfig.uiRef)
87 | this.disposeUiConfig(uiConfig)
88 | }
89 | const ui = uiConfig.type ? this.typeGenerators[uiConfig.type]?.(parent, uiConfig, this) as BladeApi> | undefined : undefined
90 | if (ui) {
91 | ui.hidden = getOrCall(uiConfig.hidden) ?? false
92 | if (uiConfig.type !== 'button') ui.disabled = getOrCall(uiConfig.disabled) ?? false // todo: also see if property is writable?
93 | ;(ui as any).srcUiConfig = uiConfig
94 | }
95 | uiConfig.uiRef = ui
96 | uiConfig.uiRefType = ui ? uiConfig.type : undefined
97 | uiConfig.__uiParent = parent
98 | uiConfig.uiRefresh =
99 | (deep = false, mode: TUiRefreshModes | 'immediate' = 'postFrame', delay = 0) =>
100 | this.addToRefreshQueue(mode, uiConfig, deep, delay)
101 | ui?.controller_.viewProps.handleDispose(()=>{
102 | uiConfig.uiRef = undefined
103 | uiConfig.uiRefType = undefined
104 | uiConfig.uiRefresh = undefined
105 | })
106 | }
107 |
108 | protected _refreshUiConfigObject(config: UiObjectConfig) {
109 | if (!config.__uiParent) {
110 | console.error('No parent for ui object', config)
111 | }
112 | this.renderUiObject(config, config.__uiParent)
113 | }
114 |
115 | // eslint-disable-next-line @typescript-eslint/naming-convention
116 | THREE: THREE|undefined = (window as any).THREE
117 |
118 | readonly typeGenerators: typeof defaultGenerators & AnyOptions = {
119 | ...defaultGenerators,
120 | }
121 |
122 | }
123 |
124 | const defaultGenerators = {
125 | panel: tpFolderGenerator,
126 | folder: tpFolderGenerator,
127 | tab: tpTabGenerator,
128 | tabs: tpTabGenerator,
129 | input: tpInputGenerator,
130 | number: tpInputGenerator,
131 | slider: tpSliderInputGenerator,
132 | separator: tpSeparatorGenerator,
133 | divider: tpSeparatorGenerator,
134 | dropdown: tpDropdownInputGenerator,
135 | select: tpDropdownInputGenerator,
136 | checkbox: tpInputGenerator,
137 | toggle: tpInputGenerator,
138 | button: tpButtonInputGenerator,
139 | vec: tpVecInputGenerator,
140 | vector: tpVecInputGenerator,
141 | vec2: tpVecInputGenerator,
142 | vec3: tpVecInputGenerator,
143 | vec4: tpVecInputGenerator,
144 | textarea: tpTextAreaInputGenerator,
145 | textArea: tpTextAreaInputGenerator,
146 | multiline: tpTextAreaInputGenerator,
147 |
148 | // three
149 | color: tpColorInputGenerator,
150 |
151 | // others
152 | monitor: (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, params?: any) => {
153 | config.readOnly = true
154 | return tpInputGenerator(parent, config, plugin, params)
155 | },
156 | // dummy for creating new ones, do not remove.
157 | dummy: (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, params?: any) => {
158 | return tpInputGenerator(parent, config, plugin, params)
159 | },
160 | }
161 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'root': true,
3 | 'extends': [
4 | 'eslint:recommended',
5 | ],
6 | 'parserOptions': {
7 | 'ecmaVersion': 2018,
8 | },
9 | 'plugins': [
10 | 'html',
11 | ],
12 | 'settings': {
13 | 'html/indent': 4,
14 | },
15 | 'env': {
16 | 'es6': true,
17 | 'node': true,
18 | },
19 | 'rules': {
20 | 'array-bracket-spacing': 'error',
21 | 'comma-style': 'error',
22 | 'space-before-blocks': 'error',
23 | 'space-before-function-paren': 'error',
24 | 'space-in-parens': 'error',
25 | 'space-infix-ops': 'error',
26 | 'space-unary-ops': 'error',
27 | 'spaced-comment': 'error',
28 | 'no-spaced-func': 'error',
29 | 'no-multi-spaces': 'error',
30 | 'no-regex-spaces': 'error',
31 | 'no-trailing-spaces': ['warn', { 'skipBlankLines': true }],
32 | 'no-mixed-spaces-and-tabs': 'error',
33 | 'no-irregular-whitespace': 'error',
34 | 'no-whitespace-before-property': 'error',
35 | 'default-case': 'error',
36 | 'require-jsdoc': 'warn',
37 | 'camelcase': 'error',
38 | 'comma-dangle': ['error', 'always-multiline'],
39 | 'indent': ['error', 4],
40 | 'quotes': ['error', 'single'],
41 | 'linebreak-style': ['error', 'unix'],
42 | 'no-loss-of-precision': 'error',
43 | },
44 | 'overrides': [
45 | {
46 | 'files': ['**/*.ts', '**/*.tsx'],
47 |
48 | 'parser': '@typescript-eslint/parser', // Specifies the ESLint parser
49 | 'parserOptions': {
50 | 'ecmaVersion': 2021, // Allows for the parsing of modern ECMAScript features
51 | 'sourceType': 'module', // Allows for the use of imports
52 | 'project': ['tsconfig.json', 'examples/tsconfig.json'],
53 | 'tsconfigRootDir': './',
54 | },
55 | 'extends': [
56 | 'eslint:recommended',
57 | 'plugin:@typescript-eslint/eslint-recommended' ,
58 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
59 | ],
60 | 'env': {
61 | 'browser': true,
62 | 'es6': true,
63 | },
64 | 'plugins': [
65 | 'html',
66 | '@typescript-eslint',
67 | 'import',
68 | 'deprecation',
69 | ],
70 | 'settings': {
71 | 'html/indent': 4,
72 | 'import/resolver': {
73 | 'typescript': {},
74 | },
75 | },
76 | 'rules': {
77 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
78 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
79 | '@typescript-eslint/no-explicit-any': 'off',
80 | 'camelcase': 'off',
81 | '@typescript-eslint/naming-convention': [ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md
82 | 'error',
83 | {
84 | 'selector': 'default',
85 | 'format': ['camelCase'],
86 | },
87 | {
88 | 'selector': 'variable',
89 | 'format': ['camelCase', 'UPPER_CASE'],
90 | },
91 | {
92 | 'selector': 'parameter',
93 | 'format': ['camelCase'],
94 | 'leadingUnderscore': 'allow',
95 | },
96 | {
97 | 'selector': 'memberLike',
98 | 'modifiers': ['private'],
99 | 'format': ['camelCase'],
100 | 'leadingUnderscore': 'require',
101 | },
102 | {
103 | 'selector': 'memberLike',
104 | 'modifiers': ['protected'],
105 | 'format': ['camelCase'],
106 | 'leadingUnderscore': 'require',
107 | },
108 | {
109 | 'selector': ['typeLike'],
110 | 'format': ['PascalCase'],
111 | },
112 | {
113 | 'selector': ['enumMember'],
114 | 'format': ['PascalCase', 'UPPER_CASE'],
115 | },
116 | {
117 | 'selector': 'memberLike',
118 | 'modifiers': ['static'],
119 | 'format': ['PascalCase', 'UPPER_CASE'],
120 | },
121 | ],
122 | 'semi': 'off',
123 | '@typescript-eslint/semi': ['error','never', { 'beforeStatementContinuationChars': 'always' }],
124 | 'no-extra-semi': 'off',
125 | '@typescript-eslint/no-extra-semi': ['error'],
126 | '@typescript-eslint/adjacent-overload-signatures': 'error',
127 | 'comma-spacing': 'off',
128 | '@typescript-eslint/comma-spacing': ['error'],
129 | 'no-extra-parens': 'off',
130 | '@typescript-eslint/no-extra-parens': ['error'],
131 | 'brace-style': 'off',
132 | '@typescript-eslint/brace-style': ['warn', '1tbs', { 'allowSingleLine': true }],
133 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
134 | 'default-param-last': 'off',
135 | '@typescript-eslint/default-param-last': ['error'],
136 | 'func-call-spacing': 'off',
137 | '@typescript-eslint/func-call-spacing': ['error'],
138 | 'keyword-spacing': 'off',
139 | 'object-curly-spacing': 'off',
140 | '@typescript-eslint/object-curly-spacing': ['error'],
141 | '@typescript-eslint/keyword-spacing': ['error'],
142 | 'space-before-function-paren': 'off',
143 | '@typescript-eslint/space-before-function-paren': ['error','never'],
144 | 'no-shadow': 'off',
145 | 'no-magic-numbers': 'off',
146 | // '@typescript-eslint/no-magic-numbers': [
147 | // 'warn', {
148 | // 'ignoreEnums': true,
149 | // 'ignoreNumericLiteralTypes': true,
150 | // 'ignoreReadonlyClassProperties': true,
151 | // },
152 | // ],
153 | '@typescript-eslint/promise-function-async': [
154 | 'error',
155 | {
156 | 'allowedPromiseNames': ['Thenable'],
157 | 'checkArrowFunctions': true,
158 | 'checkFunctionDeclarations': true,
159 | 'checkFunctionExpressions': true,
160 | 'checkMethodDeclarations': true,
161 | },
162 | ],
163 | 'dot-notation': 'off', // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/dot-notation.md
164 | '@typescript-eslint/dot-notation': ['error'],
165 | '@typescript-eslint/prefer-string-starts-ends-with': 'error',
166 | '@typescript-eslint/prefer-includes': 'error',
167 | '@typescript-eslint/prefer-for-of': 'error',
168 | '@typescript-eslint/prefer-as-const': 'error',
169 | '@typescript-eslint/prefer-function-type': 'error',
170 | '@typescript-eslint/no-unsafe-call': 'warn',
171 | '@typescript-eslint/no-misused-new': 'error',
172 | '@typescript-eslint/no-namespace': 'error',
173 | '@typescript-eslint/non-nullable-type-assertion-style': 'error',
174 | 'no-invalid-this': 'off',
175 | '@typescript-eslint/no-invalid-this': ['error'],
176 | '@typescript-eslint/prefer-ts-expect-error': ['error'],
177 | 'no-loop-func': 'off',
178 | '@typescript-eslint/no-loop-func': ['error'],
179 | 'no-loss-of-precision': 'off',
180 | '@typescript-eslint/no-loss-of-precision': ['error'],
181 | '@typescript-eslint/no-shadow': ['error'],
182 | 'no-duplicate-imports': 'off',
183 | '@typescript-eslint/no-duplicate-imports': ['error', { 'includeExports': false }],
184 | // "@typescript-eslint/prefer-nullish-coalescing": ["error", {ignoreConditionalTests: false, ignoreMixedLogicalExpressions: false}],
185 | 'comma-dangle': 'off',
186 | '@typescript-eslint/comma-dangle': ['error', {
187 | 'arrays': 'always-multiline',
188 | 'objects': 'always-multiline',
189 | 'imports': 'always-multiline',
190 | 'exports': 'always-multiline',
191 | 'functions': 'only-multiline',
192 | }],
193 | 'deprecation/deprecation': 'warn',
194 | '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
195 |
196 | },
197 |
198 | 'globals': { 'Atomics': 'readonly', 'SharedArrayBuffer': 'readonly' },
199 | },
200 | ],
201 | }
202 |
--------------------------------------------------------------------------------
/src/tpGenerators.ts:
--------------------------------------------------------------------------------
1 | import {BladeApi, ButtonApi, FolderApi, InputBindingApi, MonitorBindingApi, TabApi} from 'tweakpane'
2 | import {BladeController, TextView, View} from '@tweakpane/core'
3 | import {setupPropertyForwarding, tweakPaneMoveToParentIndex} from './tpUtils'
4 | import {ChangeEvent, UiObjectConfig} from 'uiconfig.js'
5 | import {getOrCall, objectHasOwn, safeSetProperty} from 'ts-browser-helpers'
6 | import {UiConfigRendererTweakpane} from './UiConfigRendererTweakpane'
7 |
8 | export const tpFolderGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, _?: any) => {
9 | let folder = config.uiRef as FolderApi | undefined
10 | if (folder?.controller_.viewProps.get('disposed')) folder = undefined
11 | const lastExpanded = folder?.expanded
12 | if (!folder) {
13 | // console.log(`Adding ${config.label} to ${parent.title}`, parent)
14 | const folder1 = parent.addFolder({
15 | title: '',
16 | })
17 | folder = folder1
18 | folder.on('fold', _2 => {
19 | let expanded = folder1.expanded
20 | safeSetProperty(config, 'expanded', expanded, true)
21 | expanded = getOrCall(config.expanded) ?? expanded
22 | if (expanded !== folder1.expanded) folder1.expanded = expanded
23 | config.uiRefresh?.(expanded, 'postFrame')
24 | if (expanded) {
25 | config.onExpand?.(config)
26 | }
27 | })
28 | }
29 |
30 | if (!folder) return folder
31 | folder.expanded = getOrCall(config.expanded) ?? lastExpanded ?? false // todo;;
32 |
33 | const childObjects = plugin.methods.getChildren(config)
34 | // console.log(childObjects)
35 | // if (childObjects) {
36 | // const children = childObjects?.map(value => value.uiRef) as BladeApi>[]
37 | let i = 0
38 | const newChildren = []
39 | for (const child of childObjects) { // todo check comparison with uiConfig.uuid
40 | child.parentOnChange = (ev: ChangeEvent, ...args) => { // there will be an issue if this child is then added to the root pane in tweakpane
41 | plugin.methods.dispatchOnChangeSync(config, {...ev}, ...args)
42 | }
43 | setupPropertyForwarding(child, config)
44 |
45 | let ui = child.uiRef as BladeApi> | undefined
46 | if (ui) {
47 | // check if all correct or set child.uiRef to undefined.
48 |
49 | // if (!tweakpaneCheckParent(ui, folder)) { // different parent
50 | // }
51 | if (ui.controller_.viewProps.get('disposed')) {
52 | plugin.disposeUiConfig(child, false)
53 | }
54 | }
55 |
56 | ui = child.uiRef
57 | if (!ui) {
58 | plugin.renderUiObject(child, folder)
59 | ui = child.uiRef
60 | } else {
61 | // this._refreshUiObject({uiConfig: child}, folder) // this is done in add, see flattenUiConfig
62 | }
63 |
64 | if (ui) {
65 | newChildren.push(ui)
66 | const moved = tweakPaneMoveToParentIndex(ui, folder, i++)
67 | if (moved) {
68 | // console.log('Moved child', child.label, 'to folder', config.label, 'index', i - 1)
69 | // child.uiRefresh?.('postFrame', false)
70 | // plugin._refreshUiObject({uiConfig: child}, folder)
71 | plugin.renderUiObject(child, folder) // its the same as refresh
72 | }
73 | }
74 | }
75 | for (const uichild of [...folder.children]) {
76 |
77 | if (!newChildren.includes(uichild as any)) {
78 | folder.remove(uichild)
79 | const child = (uichild as any).srcUiConfig as UiObjectConfig
80 | if (child) {
81 | uichild.dispose()
82 | // child.uiRef = undefined
83 | child.parentOnChange = undefined
84 | if (Array.isArray(child.property) &&
85 | (child.property === config.property ||
86 | child.property[0] === config &&
87 | child.property[1] === 'value'
88 | )
89 | ) delete child.property
90 | }
91 | }
92 | }
93 | // let ch = folder.children
94 | // while (ch.length > i) {
95 | // const rm = ch[ch.length - 1]
96 | // folder.remove(rm) // todo; remove listeners etc
97 | // ch = folder.children
98 | // const child = (rm as any).srcUiConfig as UiObjectConfig
99 | // if (child) {
100 | // rm.dispose()
101 | // child.uiRef = undefined
102 | // child.parentOnChange = undefined
103 | // if (Array.isArray(child.property) &&
104 | // (child.property === config.property ||
105 | // child.property[0] === config &&
106 | // child.property[1] === 'value'
107 | // )
108 | // ) delete child.property
109 | // // todo: remove uiRef here?
110 | // }
111 | // // todo: do we need to do disposeUiConfig?
112 | // }
113 |
114 | folder.controller_.props.set('title', plugin.methods.getLabel(config))
115 |
116 | const container = folder.controller_.view.containerElement
117 | const domChildren = getOrCall(config.domChildren, [])
118 | if (domChildren?.length !== undefined) {
119 | const x: any = []
120 | // eslint-disable-next-line @typescript-eslint/prefer-for-of
121 | for (let j = 0; j < container.children.length; j++) {
122 | const child: any = container.children[j]
123 | if (!child.dataset?.tpCustomDOM) continue
124 | x.push(child)
125 | }
126 | for (const child of x) {
127 | container.removeChild(child)
128 | }
129 | for (const domChild of domChildren) {
130 | if (domChild.parentElement !== container) {
131 | container.appendChild(domChild)
132 | domChild.dataset.tpCustomDOM = 'true'
133 | }
134 | // check ordering maybe?
135 | }
136 | folder.controller_.foldable.cleanUpTransition()
137 | }
138 |
139 | return folder
140 | }
141 |
142 | export const tpButtonInputGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, _?: any) => {
143 | let input = config.uiRef as ButtonApi | undefined
144 | if (input?.controller_.viewProps.get('disposed')) input = undefined
145 | if (!input) {
146 | // Create button in parent and bind click to property
147 | input = parent.addButton({title: ''})
148 | input.on('click', async() => plugin.methods.clickButton(config))
149 | }
150 | if (input) {
151 | input.title = plugin.methods.getLabel(config) || 'click me'
152 | input.disabled = getOrCall(config.disabled) || getOrCall(config.readOnly) || false
153 | }
154 | return input
155 | }
156 |
157 | export const tpDropdownInputGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, params?: any) => {
158 | const children = plugin.methods.getChildren(config)
159 | const options = Object.fromEntries(children.map(value => {
160 | const label = plugin.methods.getLabel(value)
161 | return [label, value!.value ?? label]
162 | }))
163 | const i = tpInputGenerator(parent, config, plugin, {options, ...params ?? {}})
164 | return i
165 | }
166 |
167 | export const tpSliderInputGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, params?: any) => {
168 | const bounds = getOrCall(config.bounds)
169 | const max = (bounds?.length ?? 0) >= 2 ? bounds![1] : 1
170 | const min = (bounds?.length ?? 0) >= 1 ? bounds![0] : 0
171 | const step = getOrCall(config.stepSize) || undefined
172 | return tpInputGenerator(parent, config, plugin, {min, max, step, ...params ?? {}})
173 | }
174 |
175 | export const tpTextAreaInputGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, params?: any) => {
176 | const lineCount = getOrCall(config.rows) || getOrCall(config.lineCount) || 4
177 | const placeholder = getOrCall(config.placeholder) || 'Type here...'
178 | return tpInputGenerator(parent, config, plugin, {view: 'textarea',
179 | lineCount: lineCount,
180 | rows: lineCount, // its called rows since tweakpane 4
181 | placeholder,
182 | ...params ?? {}})
183 | }
184 |
185 | export const tpSeparatorGenerator = (parent: FolderApi, config: UiObjectConfig, _plugin: UiConfigRendererTweakpane, _params?: any) => {
186 | let separator = config.uiRef
187 | if (separator?.controller_?.viewProps.get('disposed')) separator = undefined
188 | if (!separator) {
189 | separator = parent.addSeparator()
190 | }
191 | return separator
192 | }
193 |
194 | export const tpInputGenerator = (parent: FolderApi, config: UiObjectConfig, renderer: UiConfigRendererTweakpane, params?: any) => {
195 | params = params ?? {}
196 | const inputParams = {
197 | label: renderer.methods.getLabel(config),
198 | ...params,
199 | }
200 |
201 | let input = config.uiRef as InputBindingApi | MonitorBindingApi | undefined
202 | if (input?.controller_.viewProps.get('disposed')) input = undefined
203 |
204 | let proxy = config.__proxy
205 | if (!proxy) proxy = config.__proxy = {} // see tpColorInputGenerator
206 | if (!objectHasOwn(proxy, 'value_')) {
207 | const n = renderer.methods.getValue(config, proxy.value || undefined)
208 | proxy.value = n
209 | }
210 | if (!input) {
211 | // Create input in parent and bind to property
212 | try {
213 | if (!inputParams.view && getOrCall(config.readOnly)) {
214 | const [tar, key] = renderer.methods.getBinding(config)
215 | input = tar ? parent.addMonitor(tar, key, inputParams) : undefined
216 | } else {
217 | input = parent.addInput(proxy, 'value', inputParams).on('change', ev => {
218 | if (proxy.listedOnChange === false) return // used in tpImageInputGenerator
219 | // console.log(ev.last ?? true)
220 | config.dispatchMode = 'immediate' // this is required so that the value is set before the next uiRefresh.
221 | renderer.methods.setValue(config, proxy.value_ ?? proxy.value, {last: ev.last ?? true}, proxy.forceOnChange || false)
222 | })
223 | }
224 | } catch (e: any) {
225 | if (e.message.startsWith('No matching controller for')) input = undefined
226 | else throw e
227 | }
228 | }
229 |
230 | if (input) {
231 | for (const [key, val] of Object.entries(inputParams)) {
232 | const cont = input.controller_
233 | const oVal = cont.props.value(key as any)
234 | if (oVal !== undefined) {
235 | if (oVal.rawValue !== val) input.controller_.props.set(key as any, val)
236 | } else {
237 | // todo: update??
238 | // const vCont: any = cont.valueController
239 | // oVal = vCont.props?.value?.(key as any)
240 | // if (oVal !== undefined && oVal.rawValue !== val) {
241 | // console.log(key, val, oVal)
242 | // vCont.props.set(key as any, val)
243 | // }
244 | }
245 | }
246 | // update min max value of slider manually because it has a separate controller.
247 | if (config.type === 'slider') {
248 | if (inputParams.min !== undefined) (input as any).controller_.valueController.sliderController.props.set(
249 | 'minValue',
250 | inputParams.min,
251 | )
252 | if (inputParams.max !== undefined) (input as any).controller_.valueController.sliderController.props.set(
253 | 'maxValue',
254 | inputParams.max,
255 | )
256 | }
257 |
258 | // placeholder for text input
259 | (input?.controller_.valueController.view as TextView).inputElement?.setAttribute('placeholder', getOrCall(config.placeholder) ?? '')
260 |
261 | try {
262 | input.refresh()
263 | } catch (e) {
264 | console.warn(e)
265 | }
266 |
267 | const domChildren = getOrCall(config.domChildren, [])
268 | const container = input.controller_.view.element
269 | if (domChildren?.length !== undefined) {
270 | const x: any = []
271 | // eslint-disable-next-line @typescript-eslint/prefer-for-of
272 | for (let j = 0; j < container.children.length; j++) {
273 | const child: any = container.children[j]
274 | if (!child.dataset?.tpCustomDOM) continue
275 | x.push(child)
276 | }
277 | for (const child of x) {
278 | container.removeChild(child)
279 | }
280 | for (const domChild of domChildren) {
281 | if (domChild.parentElement !== container) {
282 | container.appendChild(domChild)
283 | domChild.dataset.tpCustomDOM = 'true'
284 | }
285 | // check ordering maybe?
286 | }
287 | }
288 |
289 | }
290 | // console.log('refresh', input)
291 | // console.log(ev)
292 | return input
293 | }
294 |
295 | export const tpVecInputGenerator = (parent: FolderApi, config: UiObjectConfig, renderer: UiConfigRendererTweakpane, params?: any) => {
296 | if (!config.__proxy) config.__proxy = {}
297 | config.__proxy.forceOnChange = false
298 |
299 | // todo handle array type of values instead of {x,y,z,w}
300 |
301 | const bounds = getOrCall(config.bounds)
302 | if (!bounds || bounds.length < 1)
303 | return tpInputGenerator(parent, config, renderer, {...params ?? {}})
304 |
305 | // todo: bounds are not working properly
306 | const max = (bounds.length ?? 0) >= 2 ? bounds[1] : 1
307 | const min = (bounds.length ?? 0) >= 1 ? bounds[0] : 0
308 | const step = config.stepSize ?? (max - min) / 100
309 | const p = {min, max, step}
310 | const pp: any = {x: p, y: p}
311 | if (config.type === 'vec3' || config.type === 'vec4') pp.z = p
312 | if (config.type === 'vec4') pp.w = p
313 | return tpInputGenerator(parent, config, renderer, {...pp, ...params ?? {}})
314 | }
315 |
316 | export const tpTabGenerator = (parent: FolderApi, config: UiObjectConfig, plugin: UiConfigRendererTweakpane, _?: any) => {
317 | let tabs = config.uiRef as TabApi | undefined
318 | if (tabs?.controller_.viewProps.get('disposed')) tabs = undefined
319 |
320 | const childObjects = plugin.methods.getChildren(config)
321 |
322 | if (!tabs) {
323 | // Create pages configuration based on children (folders)
324 | const pages = childObjects.map((child, i) => ({
325 | title: plugin.methods.getLabel(child) || `Tab ${i + 1}`,
326 | }))
327 |
328 | tabs = parent.addTab({pages})
329 | }
330 |
331 | if (!tabs) return tabs
332 |
333 | // Always render/re-render each folder's children into their respective tab pages
334 | childObjects.forEach((folderConfig, index) => {
335 | const tabPage = tabs.pages[index]
336 | if (!tabPage) return
337 |
338 | // Set up parent change handler for the folder
339 | folderConfig.parentOnChange = (ev: ChangeEvent, ...args) => {
340 | plugin.methods.dispatchOnChangeSync(config, {...ev}, ...args)
341 | }
342 |
343 | // Set up property forwarding for the folder config from the parent tab config
344 | setupPropertyForwarding(folderConfig, config)
345 |
346 | // Get the children of this folder
347 | const folderChildren = plugin.methods.getChildren(folderConfig)
348 |
349 | // Clear existing children in the tab page first
350 | while (tabPage.children.length > 0) {
351 | const child = tabPage.children[0]
352 | tabPage.remove(child)
353 | }
354 |
355 | // Render each child of the folder into the tab page
356 | for (const child of folderChildren) {
357 | child.parentOnChange = (ev: ChangeEvent, ...args) => {
358 | plugin.methods.dispatchOnChangeSync(folderConfig, {...ev}, ...args)
359 | }
360 |
361 | setupPropertyForwarding(child, folderConfig)
362 |
363 | // todo this recreates the complete ui, ideally there should be a proper refresh logic for children.
364 |
365 | // Force clear uiRef to ensure fresh rendering during re-render
366 | child.uiRef = undefined
367 | child.uiRefType = undefined
368 |
369 | // Render the child into the tab page
370 | plugin.renderUiObject(child, tabPage)
371 | }
372 | })
373 |
374 | // Update tab titles based on folder labels (in case they changed)
375 | childObjects.forEach((folderConfig, index) => {
376 | const tabPage = tabs.pages[index]
377 | if (tabPage) {
378 | tabPage.title = plugin.methods.getLabel(folderConfig) || `Tab ${index + 1}`
379 | }
380 | })
381 |
382 | return tabs
383 | }
384 |
--------------------------------------------------------------------------------