├── .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 | 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 | 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 | [![NPM Package](https://img.shields.io/npm/v/uiconfig-tweakpane.svg)](https://www.npmjs.com/package/uiconfig-tweakpane) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | --------------------------------------------------------------------------------