Browse Source

Add TweakpaneUiPlugin, CustomContextMenu, add ui config and decorators and fixes, Several uiconfig examples.

master
Palash Bansal 3 years ago
parent
commit
5ffad1b714
No account linked to committer's email address
39 changed files with 1368 additions and 331 deletions
  1. 35
    0
      examples/camera-uiconfig/index.html
  2. 23
    0
      examples/camera-uiconfig/script.ts
  3. 6
    3
      examples/dropzone-plugin/script.ts
  4. 1
    0
      examples/examples-utils/simple-code-preview.mjs
  5. 26
    18
      examples/index.html
  6. 35
    0
      examples/material-uiconfig/index.html
  7. 29
    0
      examples/material-uiconfig/script.ts
  8. 35
    0
      examples/object-uiconfig/index.html
  9. 26
    0
      examples/object-uiconfig/script.ts
  10. 35
    0
      examples/scene-uiconfig/index.html
  11. 23
    0
      examples/scene-uiconfig/script.ts
  12. 1
    0
      examples/tsconfig.json
  13. 35
    0
      examples/viewer-uiconfig/index.html
  14. 22
    0
      examples/viewer-uiconfig/script.ts
  15. 178
    138
      package-lock.json
  16. 27
    14
      package.json
  17. 1
    0
      src/core/ITexture.ts
  18. 15
    12
      src/core/camera/PerspectiveCamera2.ts
  19. 66
    27
      src/core/geometry/iGeometryCommons.ts
  20. 5
    3
      src/core/object/IObjectUi.ts
  21. 52
    38
      src/core/object/RootScene.ts
  22. 1
    0
      src/plugins/index.ts
  23. 9
    7
      src/plugins/interaction/DropzonePlugin.ts
  24. 0
    20
      src/plugins/ui/RenderTargetPreviewPlugin.css
  25. 5
    25
      src/plugins/ui/RenderTargetPreviewPlugin.ts
  26. 138
    0
      src/plugins/ui/tweakpane/TweakpaneUiPlugin.ts
  27. 252
    0
      src/plugins/ui/tweakpane/tpImageInputGenerator.ts
  28. 140
    0
      src/plugins/ui/tweakpane/tpTheme.css
  29. 7
    1
      src/postprocessing/ExtendedRenderPass.ts
  30. 2
    0
      src/postprocessing/ScreenPass.ts
  31. 8
    3
      src/rendering/RenderManager.ts
  32. 1
    1
      src/three/utils/misc.ts
  33. 17
    17
      src/ui/image-ui.ts
  34. 55
    0
      src/utils/CustomContextMenu.css
  35. 47
    0
      src/utils/CustomContextMenu.ts
  36. 1
    0
      src/utils/index.ts
  37. 1
    2
      src/viewer/IViewerPlugin.ts
  38. 6
    2
      src/viewer/ThreeViewer.ts
  39. 2
    0
      src/viewer/ViewerRenderManager.ts

+ 35
- 0
examples/camera-uiconfig/index.html View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Camera UiConfig</title>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"uiconfig-tweakpane": "https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 23
- 0
examples/camera-uiconfig/script.ts View File

@@ -0,0 +1,23 @@
import {_testFinish, IObject3D, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

ui.appendChild(viewer.scene.mainCamera.uiConfig)

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})


}

init().then(_testFinish)

+ 6
- 3
examples/dropzone-plugin/script.ts View File

@@ -1,4 +1,4 @@
import {_testFinish, DropzonePlugin, ThreeViewer} from 'threepipe'
import {_testFinish, DropzonePlugin, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

@@ -21,14 +21,17 @@ async function init() {

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

const dropzone = viewer.getPlugin(DropzonePlugin)
dropzone?.addEventListener('drop', (e: any) => {
const dropzone = viewer.getPlugin(DropzonePlugin)!
dropzone.addEventListener('drop', (e: any) => {
if (!e.assets?.length) return // no assets imported
console.log('Dropped Event:', e)
const promptDiv = document.getElementById('prompt-div')!
promptDiv.style.display = 'none'
})

const ui = viewer.addPluginSync(TweakpaneUiPlugin, true)
ui.appendChild(dropzone.uiConfig)

}

init().then(_testFinish)

+ 1
- 0
examples/examples-utils/simple-code-preview.mjs View File

@@ -12,6 +12,7 @@ setupCodePreview(
scripts.map(s=>(typeof s === 'string' && s.endsWith('.js')) ? s : 'https://github.com/repalash/threepipe/tree/master/examples/'+ window.location.pathname.split('/examples/').pop().replace('index.html', '')+(s.textContent ? 'index.html' : s)), // todo: github link
(c)=>c
.replaceAll(" from 'threepipe'", " from 'https://threepipe.org/dist/index.mjs'")
.replaceAll(" from 'uiconfig-tweakpane'", " from 'https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs'")
.replaceAll(" from '../", " from 'https://threepipe.org/examples/"),
{
title: 'ThreePipe: ' + document.title,

+ 26
- 18
examples/index.html View File

@@ -215,39 +215,47 @@
<h1><a href="https://github.com/repalash/threepipe">ThreePipe</a> Examples</h1>
<h2 class="category">Plugins</h2>
<ul>
<li><a href="./depth-buffer-plugin">Depth Buffer Plugin </a></li>
<li><a href="./render-target-preview">Render Target Preview </a></li>
<li><a href="./dropzone-plugin">Dropzone (Drag & Drop) </a></li>
<li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li>
<li><a href="./render-target-preview/">Render Target Preview </a></li>
<li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) </a></li>
</ul>
<h2 class="category">Import/Export</h2>
<ul>
<li><a href="./fbx-load">FBX Load </a></li>
<li><a href="./hdr-load">HDR Load </a></li>
<li><a href="./obj-mtl-load">OBJ MTL Load </a></li>
<li><a class="selected" href="./gltf-load">GLTF Load </a></li>
<li><a href="./drc-load">DRACO(DRC) Load </a></li>
<li><a href="./glb-export">GLB Export </a></li>
<li><a href="./pmat-material-export">PMAT Material export </a></li>
<li><a href="./fbx-load/">FBX Load </a></li>
<li><a href="./hdr-load/">HDR Load </a></li>
<li><a href="./obj-mtl-load/">OBJ MTL Load </a></li>
<li><a class="selected" href="./gltf-load/">GLTF Load </a></li>
<li><a href="./drc-load/">DRACO(DRC) Load </a></li>
<li><a href="./glb-export/">GLB Export </a></li>
<li><a href="./pmat-material-export/">PMAT Material export </a></li>
</ul>
<h2 class="category">Rendering</h2>
<ul>
<li><a href="./custom-pipeline">Custom Pipeline specification </a></li>
<li><a href="./custom-pipeline/">Custom Pipeline specification </a></li>
</ul>
<h2 class="category">UI Config</h2>
<ul>
<li><a href="./material-uiconfig/">Material UI </a></li>
<li><a href="./object-uiconfig/">Object UI </a></li>
<li><a href="./camera-uiconfig/">Camera UI </a></li>
<li><a href="./scene-uiconfig/">Scene UI </a></li>
<li><a href="./viewer-uiconfig/">Viewer UI </a></li>
</ul>
<h2 class="category">Utils</h2>
<ul>
<li><a href="./obj-to-glb">Convert OBJ to GLB </a></li>
<li><a href="./obj-to-glb/">Convert OBJ to GLB </a></li>
</ul>
<h2 class="category">Tests</h2>
<ul>
<li><a href="./import-test">Import Test</a></li>
<li><a href="./sphere-rgbm-test">Sphere RGBM Test </a></li>
<li><a href="./sphere-half-float-test">Sphere Half Float Test </a></li>
<li><a href="./sphere-msaa-test">Sphere MSAA Test </a></li>
<li><a href="./z-prepass">Z-Prepass Test </a></li>
<li><a href="./import-test/">Import Test</a></li>
<li><a href="./sphere-rgbm-test/">Sphere RGBM Test </a></li>
<li><a href="./sphere-half-float-test/">Sphere Half Float Test </a></li>
<li><a href="./sphere-msaa-test/">Sphere MSAA Test </a></li>
<li><a href="./z-prepass/">Z-Prepass Test </a></li>
</ul>
</div>
<div class="iframe-container">
<iframe id="example-iframe" src="./gltf-load" frameborder="0" allowfullscreen="allowfullscreen"
<iframe id="example-iframe" src="./gltf-load/" frameborder="0" allowfullscreen="allowfullscreen"
allow="accelerometer *; ambient-light-sensor *; autoplay *; camera *; clipboard-read *; clipboard-write *; encrypted-media *; fullscreen *; geolocation *; gyroscope *; magnetometer *; microphone *; midi *; payment *; picture-in-picture *; screen-wake-lock *; speaker *; sync-xhr *; usb *; web-share *; vibrate *; vr *">
</iframe>
</div>

+ 35
- 0
examples/material-uiconfig/index.html View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Material UiConfig</title>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"uiconfig-tweakpane": "https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 29
- 0
examples/material-uiconfig/script.ts View File

@@ -0,0 +1,29 @@
import {_testFinish, IObject3D, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', {
setBackground: true,
})
const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})
const model = result?.getObjectByName('node_damagedHelmet_-6514')
const materials = model?.materials || []
for (const material of materials) {
const config = material.uiConfig
if (!config) continue
ui.appendChild(config)
}

}

init().then(_testFinish)

+ 35
- 0
examples/object-uiconfig/index.html View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Object UiConfig</title>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"uiconfig-tweakpane": "https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 26
- 0
examples/object-uiconfig/script.ts View File

@@ -0,0 +1,26 @@
import {_testFinish, IObject3D, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', {
setBackground: true,
})
const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})

const model = result?.getObjectByName('node_damagedHelmet_-6514')
const config = model?.uiConfig
if (config) ui.appendChild(config)

}

init().then(_testFinish)

+ 35
- 0
examples/scene-uiconfig/index.html View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Scene UiConfig</title>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"uiconfig-tweakpane": "https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 23
- 0
examples/scene-uiconfig/script.ts View File

@@ -0,0 +1,23 @@
import {_testFinish, IObject3D, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

ui.appendChild(viewer.scene.uiConfig)

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})


}

init().then(_testFinish)

+ 1
- 0
examples/tsconfig.json View File

@@ -3,6 +3,7 @@
"baseUrl": "./",
"allowJs": false,
"checkJs": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"isolatedModules": false,

+ 35
- 0
examples/viewer-uiconfig/index.html View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Viewer UiConfig</title>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"uiconfig-tweakpane": "https://unpkg.com/uiconfig-tweakpane@latest/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 22
- 0
examples/viewer-uiconfig/script.ts View File

@@ -0,0 +1,22 @@
import {_testFinish, IObject3D, ThreeViewer, TweakpaneUiPlugin} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
ui.appendChild(viewer.uiConfig)

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})


}

init().then(_testFinish)

+ 178
- 138
package-lock.json View File

@@ -1,12 +1,12 @@
{
"name": "threepipe",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "threepipe",
"version": "0.0.1",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.0004/package.tgz",
@@ -17,10 +17,11 @@
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.1",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.1.1",
"@tweakpane/core": "^1.1.8",
"@types/stats.js": "^0.17.0",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.5",
"clean-package": "^2.2.0",
"eslint": "^8.40.0",
@@ -31,44 +32,20 @@
"local-web-server": "^5.3.0",
"markdown-to-html-cli": "^3.7.0",
"rimraf": "^5.0.1",
"rollup": "^3.21.7",
"rollup": "^3.23.0",
"rollup-plugin-license": "^3.0.1",
"rollup-plugin-postcss": "^4.0.2",
"stats.js": "^0.17.0",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz",
"ts-browser-helpers": "^0.4.0",
"ts-browser-helpers": "^0.5.0",
"tslib": "^2.5.0",
"tweakpane": "^3.1.9",
"tweakpane-image-plugin": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz",
"typedoc": "^0.24.7",
"typescript": "^5.0.4",
"typescript-plugin-css-modules": "^5.0.1",
"uiconfig.js": "^0.0.3"
},
"optionalDependencies": {
"win-node-env": "^0.6.1"
}
},
"../ts-browser-helpers": {
"version": "0.4.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@types/wicg-file-system-access": "^2020.9.5"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.1.0",
"@rollup/plugin-typescript": "^9.0.2",
"clean-package": "^2.2.0",
"local-web-server": "^5.2.1",
"rimraf": "^5.0.1",
"rollup": "^3.4.0",
"rollup-plugin-license": "^3.0.1",
"tslib": "^2.4.1",
"typedoc": "^0.24.7",
"typedoc-plugin-markdown": "^3.15.3",
"typescript": "^4.9.3"
"uiconfig-tweakpane": "^0.0.3",
"uiconfig.js": "^0.0.4"
},
"optionalDependencies": {
"win-node-env": "^0.6.1"
@@ -212,9 +189,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -555,6 +532,12 @@
"node": ">=10.13.0"
}
},
"node_modules/@tweakpane/core": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-1.1.8.tgz",
"integrity": "sha512-psvBf6Cbm3YSZOTmDFWkcGzHYMnw7gVZM3jw+TfbzErIC+sMXPQb85h4ayW04w2u7AGg8jD0gHXSCg5wd+rafg==",
"dev": true
},
"node_modules/@tweenjs/tween.js": {
"version": "18.6.4",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz",
@@ -594,9 +577,9 @@
}
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
"node_modules/@types/json5": {
@@ -633,9 +616,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.4.tgz",
"integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==",
"version": "20.2.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
"integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==",
"dev": true
},
"node_modules/@types/parse5": {
@@ -1280,9 +1263,9 @@
}
},
"node_modules/browserslist": {
"version": "4.21.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
"integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
"version": "4.21.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.6.tgz",
"integrity": "sha512-PF07dKGXKR+/bljJzCB6rAYtHEu21TthLxmJagtQizx+rwiqdRDBO5971Xu1N7MgcMLi4+mr4Cnl76x7O3DHtA==",
"dev": true,
"funding": [
{
@@ -1292,13 +1275,17 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001449",
"electron-to-chromium": "^1.4.284",
"node-releases": "^2.0.8",
"update-browserslist-db": "^1.0.10"
"caniuse-lite": "^1.0.30001489",
"electron-to-chromium": "^1.4.411",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
},
"bin": {
"browserslist": "cli.js"
@@ -1736,12 +1723,12 @@
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"engines": {
"node": ">= 10"
"node": ">= 12"
}
},
"node_modules/commenting": {
@@ -2387,9 +2374,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.405",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.405.tgz",
"integrity": "sha512-JdDgnwU69FMZURoesf9gNOej2Cms1XJFfLk24y1IBtnAdhTcJY/mXnokmpmxHN59PcykBP4bgUU98vLY44Lhuw==",
"version": "1.4.411",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.411.tgz",
"integrity": "sha512-5VXLW4Qw89vM2WTICHua/y8v7fKGDRVa2VPOtBB9IpLvW316B+xd8yD1wTmLPY2ot/00P/qt87xdolj4aG/Lzg==",
"dev": true
},
"node_modules/emoji-regex": {
@@ -2408,9 +2395,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz",
"integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==",
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz",
"integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -4744,15 +4731,6 @@
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"engines": {
"node": ">= 12"
}
},
"node_modules/keygrip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@@ -7059,9 +7037,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
"dev": true,
"funding": [
{
@@ -8811,9 +8789,9 @@
}
},
"node_modules/smob": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.1.1.tgz",
"integrity": "sha512-i5aqEBPnDv9d77+NDxfjROtywxzNdAVNyaOr+RsLhM28Ts+Ar7luIp/Q+SBYa6wv/7BBcOpEkrhtDxsl2WA9Jg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.3.0.tgz",
"integrity": "sha512-/fIjILUZrqpd2v9GWqaULJ1Gjw4BnWmPnQcqx8aFrSt97C2/ALamtVl9q1XRkuUg9TDkrXzfTsppQnm9DCJH8w==",
"dev": true
},
"node_modules/source-map": {
@@ -9099,9 +9077,9 @@
}
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -9348,6 +9326,15 @@
"node": ">=10.13.0"
}
},
"node_modules/svgo/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/synckit": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
@@ -9524,9 +9511,9 @@
}
},
"node_modules/ts-browser-helpers": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.4.0.tgz",
"integrity": "sha512-9XoLdIL5f61ht3UvJLAonPAam4EkCfjosLoF6xayOGxc0HnT0y8EzIdk/jx+MuFc3hT28TCzOgP9Va4C8ky/9Q==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.5.0.tgz",
"integrity": "sha512-seKCLyEIzNfjaVSYMMhYvvQlj0OHfgHbtbeOJNrFufhalmQOJcj5NP/PSMCcIU1qrKgcXNYuAolgtTmsPG6Aaw==",
"dev": true,
"dependencies": {
"@types/wicg-file-system-access": "^2020.9.5"
@@ -9580,6 +9567,22 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/tweakpane": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.9.tgz",
"integrity": "sha512-vMzh3X8uHo9HDY+9S9V0bc+UBScs8VYmMeOEW+BvynczV0aiLHweYv4eKpyoqpcRrQlkLhUsx8Dvv/1/qiCESg==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/cocopon"
}
},
"node_modules/tweakpane-image-plugin": {
"version": "1.1.403",
"resolved": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz",
"integrity": "sha512-CR1f5HFXqs/wqibb6W+Wxfg6xDJsYLoTgHMPKHmLsjr3CyFMROYHH4kDnBEhRroudiMA6PYG7SLq5/9N8HS0sQ==",
"dev": true,
"license": "MIT"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9751,10 +9754,20 @@
"node": ">=12.17"
}
},
"node_modules/uiconfig.js": {
"node_modules/uiconfig-tweakpane": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.3.tgz",
"integrity": "sha512-RZidKh6+6SJKrQa2xXHkZb8iY5UjYMJC651eIzWl7RzSuxDKvH+QahJkjtVULj/C103LH5Y6DOcC93/6WM3+gQ==",
"resolved": "https://registry.npmjs.org/uiconfig-tweakpane/-/uiconfig-tweakpane-0.0.3.tgz",
"integrity": "sha512-QDWiXMh5+pwr4jLOv7GV5nI/9RJdNAQGEwD503ShIxW9oB7eFdq9bRDvq46ncXHelWxiq2j3gqxvCnLhwfMx5A==",
"dev": true,
"dependencies": {
"@types/three": "^0.152.1",
"uiconfig.js": "^0.0.4"
}
},
"node_modules/uiconfig.js": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.4.tgz",
"integrity": "sha512-uFrcLIey/vx2+bYJwf3VBinHMRaSlTKaYOxYLFFqvu1jLj57OBJiuUosNfXKMxliSNMx4ptuNOhePFRSswumiQ==",
"dev": true
},
"node_modules/unbox-primitive": {
@@ -10262,9 +10275,9 @@
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -10428,9 +10441,9 @@
"dev": true
},
"strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"requires": {
"ansi-regex": "^6.0.1"
@@ -10652,6 +10665,12 @@
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
"dev": true
},
"@tweakpane/core": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-1.1.8.tgz",
"integrity": "sha512-psvBf6Cbm3YSZOTmDFWkcGzHYMnw7gVZM3jw+TfbzErIC+sMXPQb85h4ayW04w2u7AGg8jD0gHXSCg5wd+rafg==",
"dev": true
},
"@tweenjs/tween.js": {
"version": "18.6.4",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz",
@@ -10691,9 +10710,9 @@
}
},
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
"@types/json5": {
@@ -10730,9 +10749,9 @@
"dev": true
},
"@types/node": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.4.tgz",
"integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==",
"version": "20.2.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
"integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==",
"dev": true
},
"@types/parse5": {
@@ -11197,15 +11216,15 @@
}
},
"browserslist": {
"version": "4.21.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
"integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
"version": "4.21.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.6.tgz",
"integrity": "sha512-PF07dKGXKR+/bljJzCB6rAYtHEu21TthLxmJagtQizx+rwiqdRDBO5971Xu1N7MgcMLi4+mr4Cnl76x7O3DHtA==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001449",
"electron-to-chromium": "^1.4.284",
"node-releases": "^2.0.8",
"update-browserslist-db": "^1.0.10"
"caniuse-lite": "^1.0.30001489",
"electron-to-chromium": "^1.4.411",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
}
},
"buffer-from": {
@@ -11515,9 +11534,9 @@
}
},
"commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true
},
"commenting": {
@@ -11981,9 +12000,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.4.405",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.405.tgz",
"integrity": "sha512-JdDgnwU69FMZURoesf9gNOej2Cms1XJFfLk24y1IBtnAdhTcJY/mXnokmpmxHN59PcykBP4bgUU98vLY44Lhuw==",
"version": "1.4.411",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.411.tgz",
"integrity": "sha512-5VXLW4Qw89vM2WTICHua/y8v7fKGDRVa2VPOtBB9IpLvW316B+xd8yD1wTmLPY2ot/00P/qt87xdolj4aG/Lzg==",
"dev": true
},
"emoji-regex": {
@@ -11999,9 +12018,9 @@
"dev": true
},
"enhanced-resolve": {
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz",
"integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==",
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz",
"integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@@ -13706,14 +13725,6 @@
"dev": true,
"requires": {
"commander": "^8.3.0"
},
"dependencies": {
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true
}
}
},
"keygrip": {
@@ -15364,9 +15375,9 @@
"dev": true
},
"postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",
@@ -16611,9 +16622,9 @@
"dev": true
},
"smob": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.1.1.tgz",
"integrity": "sha512-i5aqEBPnDv9d77+NDxfjROtywxzNdAVNyaOr+RsLhM28Ts+Ar7luIp/Q+SBYa6wv/7BBcOpEkrhtDxsl2WA9Jg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.3.0.tgz",
"integrity": "sha512-/fIjILUZrqpd2v9GWqaULJ1Gjw4BnWmPnQcqx8aFrSt97C2/ALamtVl9q1XRkuUg9TDkrXzfTsppQnm9DCJH8w==",
"dev": true
},
"source-map": {
@@ -16837,9 +16848,9 @@
"dev": true
},
"strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"requires": {
"ansi-regex": "^6.0.1"
@@ -17024,6 +17035,14 @@
"csso": "^4.2.0",
"picocolors": "^1.0.0",
"stable": "^0.1.8"
},
"dependencies": {
"commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true
}
}
},
"synckit": {
@@ -17157,9 +17176,9 @@
"dev": true
},
"ts-browser-helpers": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.4.0.tgz",
"integrity": "sha512-9XoLdIL5f61ht3UvJLAonPAam4EkCfjosLoF6xayOGxc0HnT0y8EzIdk/jx+MuFc3hT28TCzOgP9Va4C8ky/9Q==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/ts-browser-helpers/-/ts-browser-helpers-0.5.0.tgz",
"integrity": "sha512-seKCLyEIzNfjaVSYMMhYvvQlj0OHfgHbtbeOJNrFufhalmQOJcj5NP/PSMCcIU1qrKgcXNYuAolgtTmsPG6Aaw==",
"dev": true,
"requires": {
"@types/wicg-file-system-access": "^2020.9.5"
@@ -17206,6 +17225,17 @@
}
}
},
"tweakpane": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.9.tgz",
"integrity": "sha512-vMzh3X8uHo9HDY+9S9V0bc+UBScs8VYmMeOEW+BvynczV0aiLHweYv4eKpyoqpcRrQlkLhUsx8Dvv/1/qiCESg==",
"dev": true
},
"tweakpane-image-plugin": {
"version": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz",
"integrity": "sha512-CR1f5HFXqs/wqibb6W+Wxfg6xDJsYLoTgHMPKHmLsjr3CyFMROYHH4kDnBEhRroudiMA6PYG7SLq5/9N8HS0sQ==",
"dev": true
},
"type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -17329,10 +17359,20 @@
"integrity": "sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==",
"dev": true
},
"uiconfig.js": {
"uiconfig-tweakpane": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.3.tgz",
"integrity": "sha512-RZidKh6+6SJKrQa2xXHkZb8iY5UjYMJC651eIzWl7RzSuxDKvH+QahJkjtVULj/C103LH5Y6DOcC93/6WM3+gQ==",
"resolved": "https://registry.npmjs.org/uiconfig-tweakpane/-/uiconfig-tweakpane-0.0.3.tgz",
"integrity": "sha512-QDWiXMh5+pwr4jLOv7GV5nI/9RJdNAQGEwD503ShIxW9oB7eFdq9bRDvq46ncXHelWxiq2j3gqxvCnLhwfMx5A==",
"dev": true,
"requires": {
"@types/three": "^0.152.1",
"uiconfig.js": "^0.0.4"
}
},
"uiconfig.js": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uiconfig.js/-/uiconfig.js-0.0.4.tgz",
"integrity": "sha512-uFrcLIey/vx2+bYJwf3VBinHMRaSlTKaYOxYLFFqvu1jLj57OBJiuUosNfXKMxliSNMx4ptuNOhePFRSswumiQ==",
"dev": true
},
"unbox-primitive": {
@@ -17654,9 +17694,9 @@
"dev": true
},
"strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"requires": {
"ansi-regex": "^6.0.1"

+ 27
- 14
package.json View File

@@ -1,6 +1,6 @@
{
"name": "threepipe",
"version": "0.0.1",
"version": "0.0.2",
"description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.",
"main": "src/index.ts",
"module": "dist/index.mjs",
@@ -67,14 +67,13 @@
},
"homepage": "https://github.com/repalash/threepipe#readme",
"devDependencies": {
"rimraf": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.1",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.1.1",
"@types/stats.js": "^0.17.0",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.5",
"clean-package": "^2.2.0",
"eslint": "^8.40.0",
@@ -83,18 +82,23 @@
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.5",
"local-web-server": "^5.3.0",
"rollup": "^3.21.7",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz",
"markdown-to-html-cli": "^3.7.0",
"rimraf": "^5.0.1",
"rollup": "^3.23.0",
"rollup-plugin-license": "^3.0.1",
"ts-browser-helpers": "^0.4.0",
"rollup-plugin-postcss": "^4.0.2",
"stats.js": "^0.17.0",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz",
"ts-browser-helpers": "^0.5.0",
"tslib": "^2.5.0",
"tweakpane": "^3.1.9",
"@tweakpane/core": "^1.1.8",
"typedoc": "^0.24.7",
"typescript": "^5.0.4",
"uiconfig.js": "^0.0.3",
"typescript-plugin-css-modules": "^5.0.1",
"rollup-plugin-postcss": "^4.0.2",
"markdown-to-html-cli": "^3.7.0"
"uiconfig-tweakpane": "^0.0.3",
"uiconfig.js": "^0.0.4",
"tweakpane-image-plugin": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz"
},
"dependencies": {
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.0004/package.tgz",
@@ -103,14 +107,19 @@
},
"//": {
"dependencies": {
"ts-browser-helpers": "^0.4.0",
"uiconfig.js": "^0.0.4",
"ts-browser-helpers": "^0.5.0",
"uiconfig-tweakpane": "^0.0.3",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz",
"three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2005.tar.gz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.0004/package.tgz",
"@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.0004.tar.gz",
"@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three"
"@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three",
"tweakpane-image-plugin": "git+ssh://github.com/repalash/tweakpane-image-plugin.git#52d5542047fd07d2e7225b5b67c9f7620366f2c7"
},
"local_dependencies": {
"uiconfig-tweakpane": "^file:./../uiconfig-tweakpane",
"uiconfig.js": "^file:./../uiconfig",
"ts-browser-helpers": "file:./../ts-browser-helpers",
"three": "file:./../three.js",
"@types/three": "file:./../three-ts-types/types/three"
@@ -128,8 +137,12 @@
"description": "A new way to work with three.js, 3D models and rendering on the web.",
"style": "body { padding: 4rem; } @media (max-width: 768px) { body { padding: 2.5rem 1rem; } }",
"meta": [
{ "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility" },
{ "keywords": "3d,three.js,typescript,javascipt,browser,esm,rendering,viewer,webgl,webgi,canvas" }
{
"description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility"
},
{
"keywords": "3d,three.js,typescript,javascipt,browser,esm,rendering,viewer,webgl,webgi,canvas"
}
]
},
"favicon": "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌐</text></svg>",

+ 1
- 0
src/core/ITexture.ts View File

@@ -2,6 +2,7 @@ import {IMaterial} from './IMaterial'
import {Texture} from 'three'

export interface ITextureUserData{
mimeType?: string
disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material
__appliedMaterials?: Set<IMaterial>
}

+ 15
- 12
src/core/camera/PerspectiveCamera2.ts View File

@@ -1,6 +1,6 @@
import {Camera, Event, IUniform, Object3D, PerspectiveCamera, Vector3} from 'three'
import {generateUiConfig, UiObjectConfig, uiSlider, uiVector} from 'uiconfig.js'
import {onChange, serialize} from 'ts-browser-helpers'
import {generateUiConfig, uiInput, UiObjectConfig, uiSlider, uiVector} from 'uiconfig.js'
import {onChange, onChange2, onChange3, serialize} from 'ts-browser-helpers'
import type {ICamera, ICameraEvent, ICameraUserData, TCameraControlsMode} from '../ICamera'
import {ICameraSetDirtyOptions} from '../ICamera'
import type {ICameraControls, TControlsCtor} from './ICameraControls'
@@ -16,10 +16,12 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
return this._controls
}

@uiInput('Name') name: string

@serialize('camControls')
private _controls?: ICameraControls
private _currentControlsMode: TCameraControlsMode = ''
@onChange(PerspectiveCamera2.prototype.refreshCameraControls)
@onChange2(PerspectiveCamera2.prototype.refreshCameraControls)
controlsMode: TCameraControlsMode
/**
* It should be the canvas actually
@@ -33,38 +35,38 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
@serialize()
userData: ICameraUserData = {}

@onChange(PerspectiveCamera2.prototype.setDirty)
@onChange3(PerspectiveCamera2.prototype.setDirty)
@uiSlider('Field Of View', [1, 180], 0.001)
@serialize() fov: number

@onChange(PerspectiveCamera2.prototype.setDirty)
@onChange3(PerspectiveCamera2.prototype.setDirty)
@serialize() focus: number

@onChange(PerspectiveCamera2.prototype.setDirty)
@onChange3(PerspectiveCamera2.prototype.setDirty)
// @uiSlider('Zoom', [0.001, 20], 0.001)
@serialize() zoom: number

@uiVector('Position')
@serialize() readonly position: Vector3

@onChange(PerspectiveCamera2.prototype.setDirty)
@onChange3(PerspectiveCamera2.prototype.setDirty)
@uiVector('Target')
@serialize() readonly target: Vector3 = new Vector3(0, 0, 0)

@serialize()
@onChange(PerspectiveCamera2.prototype.refreshAspect)
@onChange2(PerspectiveCamera2.prototype.refreshAspect)
autoAspect: boolean

/**
* Near clipping plane. This is managed by RootScene for active cameras
*/
@onChange(PerspectiveCamera2.prototype._nearFarChanged)
@onChange2(PerspectiveCamera2.prototype._nearFarChanged)
near = 0.01

/**
* Far clipping plane. This is managed by RootScene for active cameras
*/
@onChange(PerspectiveCamera2.prototype._nearFarChanged)
@onChange2(PerspectiveCamera2.prototype._nearFarChanged)
far = 50

constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) {
@@ -129,6 +131,8 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
setDirty(options?: ICameraSetDirtyOptions|Event): void {
if (!this._positionWorld) return // class not initialized

if (options?.key === 'fov') this.updateProjectionMatrix()

this.getWorldPosition(this._positionWorld)

iCameraCommons.setDirty.call(this, options)
@@ -317,8 +321,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {

uiConfig: UiObjectConfig = {
type: 'folder',
label: 'Camera',
limitedUi: true,
label: ()=>this.name || 'Camera',
children: [
...this._camUi,
// todo hack for zoom in and out for now.

+ 66
- 27
src/core/geometry/iGeometryCommons.ts View File

@@ -1,5 +1,6 @@
import {UiObjectConfig} from 'uiconfig.js'
import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry'
import {toIndexedGeometry} from '../../three'

export const iGeometryCommons = {
setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void {
@@ -21,18 +22,74 @@ export const iGeometryCommons = {
property: [this, 'uuid'],
disabled: true,
},
// {
// type: 'input',
// property: [this, 'name'],
// },
{
type: 'input',
property: [this, 'name'],
},
{
type: 'button',
label: 'Center Geometry',
value: () => {
this.center()
this.setDirty()
},
},
{
type: 'button',
label: 'Compute vertex normals',
value: () => {
if (this.hasAttribute('normal') && !confirm('Normals already exist, replace with computed normals?')) return
this.computeVertexNormals()
this.setDirty()
},
},
{
type: 'button',
label: 'Compute vertex tangents',
value: () => {
if (this.hasAttribute('tangent') && !confirm('Tangents already exist, replace with computed tangents?')) return
this.computeTangents()
this.setDirty()
},
},
{
type: 'button',
label: 'Normalize normals',
value: () => {
this.normalizeNormals()
this.setDirty()
},
},
{
type: 'button',
label: 'Convert to indexed',
hidden: () => !!this.index,
value: () => {
if (this.attributes.index) return
const tolerance = parseFloat(prompt('Tolerance', '-1') ?? '-1')
toIndexedGeometry(this, tolerance)
this.setDirty()
},
},
{
type: 'button',
label: 'Convert to non-indexed',
hidden: () => !this.index,
value: () => {
if (!this.attributes.index) return
this.toNonIndexed()
this.setDirty()
},
},
{
type: 'button',
label: 'Create uv2 from uv',
label: 'Create uv1 from uv',
value: () => {
if (this.hasAttribute('uv2')) {
if (!confirm('uv2 already exists, replace with uv data?')) return
if (this.hasAttribute('uv1')) {
if (!confirm('uv1 already exists, replace with uv data?')) return
}
this.setAttribute('uv2', this.getAttribute('uv'))
this.setAttribute('uv1', this.getAttribute('uv'))
this.setDirty()
},
},
{
@@ -46,27 +103,9 @@ export const iGeometryCommons = {
}
if (!confirm('Remove color attribute?')) return
this.deleteAttribute('color')
this.setDirty()
},
},
// {
// type: 'button',
// label: 'Invert eigen vectors',
// value: () => {
// console.log(geometry)
// const offsets = geometry.userData.normalsCaptureOffsets
// if (!offsets) return
// const m = offsets.offsetMatrix as Matrix4
// console.log(offsets.offsetMatrix.toArray())
// console.log(m.determinant())
//
// const m1 = new Matrix4().makeRotationX(Math.PI / 2)
// m.multiply(m1)
//
// console.log(m.determinant())
// offsets.offsetMatrixInv.copy(m).invert()
// console.log(offsets.offsetMatrix.toArray())
// },
// },
{
type: 'input',
label: 'Mesh count',

+ 5
- 3
src/core/object/IObjectUi.ts View File

@@ -88,9 +88,11 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
label: 'Auto Scale',
hidden: ()=>!this.autoScale,
prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true],
value: (res: string|null)=>{
if (!res) return
const rad = parseFloat(res)
value: ()=>{
const def = (this.userData.autoScaleRadius || 2) + ''
const res = prompt('Auto Scale Radius: Object will be scaled to the given radius', def)
if (res === null) return
const rad = parseFloat(res || def)
if (Math.abs(rad) > 0) this.autoScale?.(rad)
},
},

+ 52
- 38
src/core/object/RootScene.ts View File

@@ -8,20 +8,24 @@ import {
UVMapping,
Vector3,
} from 'three'
import {IObject3D, IObjectProcessor} from '../IObject'
import type {IObject3D, IObjectProcessor} from '../IObject'
import {type ICamera} from '../ICamera'
import {Box3B} from '../../three/math/Box3B'
import {AnyOptions, onChange, serialize} from 'ts-browser-helpers'
import {Box3B} from '../../three'
import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers'
import {PerspectiveCamera2} from '../camera/PerspectiveCamera2'
import {ThreeSerialization} from '../../utils/serialization'
import {ThreeSerialization} from '../../utils'
import {ITexture} from '../ITexture'
import {AddObjectOptions, IScene, ISceneEvent, ISceneEventTypes, ISceneSetDirtyOptions} from '../IScene'
import {iObjectCommons} from './iObjectCommons'
import {RootSceneImportResult} from '../../assetmanager/IAssetImporter'
import {RootSceneImportResult} from '../../assetmanager'
import {uiColor, uiConfig, uiFolderContainer, uiImage, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js'

@uiFolderContainer('Root Scene')
export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements IScene<ISceneEvent, ISceneEventTypes> {
isRootScene = true
readonly isRootScene = true

assetType = 'model' as const
uiConfig!: UiObjectConfig

// private _processors = new ObjectProcessorMap<'environment' | 'background'>()
// private _sceneObjects: ISceneObject[] = []
@@ -31,26 +35,43 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
*/
readonly modelRoot: IObject3D

@uiColor<RootScene>('Background Color', (s)=>({
onChange: ()=>s?._onBackgroundChange(),
}))
@serialize() @onChange2(RootScene.prototype._onBackgroundChange)
backgroundColor: Color | null = null // read in three.js WebGLBackground

@onChange2(RootScene.prototype._onBackgroundChange)
@serialize() @uiImage('Background Image')
background: null | Color | ITexture | 'environment' = null
/**
* The default camera in the scene
* The intensity for the environment light.
*/
@serialize() readonly defaultCamera: ICamera
@serialize() @onChange3(RootScene.prototype.setDirty)
@uiSlider('Background Intensity', [0, 10], 0.01)
backgroundIntensity = 1

@uiImage('Environment')
@serialize() @onChange2(RootScene.prototype._onEnvironmentChange)
environment: ITexture | null = null

/**
* The intensity for the environment light.
*/
@onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty
@serialize() envMapIntensity = 1
@uiSlider('Environment Intensity', [0, 10], 0.01)
@serialize() @onChange3(RootScene.prototype.setDirty)
envMapIntensity = 1
/**
* Fixed direction environment reflections irrespective of camera position.
*/
@onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty
@serialize() fixedEnvMapDirection = false
@uiToggle('Fixed Env Direction')
@serialize() @onChange3(RootScene.prototype.setDirty)
fixedEnvMapDirection = false

/**
* The intensity for the environment light.
* The default camera in the scene
*/
@onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty
@serialize() backgroundIntensity = 1
@uiConfig() @serialize() readonly defaultCamera: ICamera

// private _environmentLight?: IEnvironmentLight

@@ -80,14 +101,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
this.setDirty()
}

addEventListener<T extends ISceneEventTypes>(type: T, listener: EventListener<ISceneEvent, T, this>): void {
if (type === 'activeCameraChange') console.error('activeCameraChange is deprecated. Use mainCameraChange instead.')
if (type === 'activeCameraUpdate') console.error('activeCameraUpdate is deprecated. Use mainCameraUpdate instead.')
if (type === 'sceneMaterialUpdate') console.error('sceneMaterialUpdate is deprecated. Use materialUpdate instead.')
if (type === 'update') console.error('update is deprecated. Use sceneUpdate instead.')
super.addEventListener(type, listener)
}

/**
* Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately.
* @param camera
@@ -102,6 +115,8 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
// this is called from parentDispatch since scene is a parent.
this.addEventListener('materialUpdate', ()=>this.dispatchEvent({type: 'sceneMaterialUpdate'}))
this.addEventListener('objectUpdate', this.refreshScene)
this.addEventListener('geometryUpdate', this.refreshScene)
this.addEventListener('geometryChanged', this.refreshScene)

this.defaultCamera = camera
this.modelRoot = new Object3D() as IObject3D
@@ -240,10 +255,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
if (setDirty) this.setDirty({refreshScene: true})
}

@serialize()
@onChange(RootScene.prototype._onEnvironmentChange.name) // cannot do this as updateShadow in shadowBaker resets it every frame temporarily, todo: why was that needed?
public environment: ITexture | null = null

private _onEnvironmentChange() {
// console.warn('environment changed')
if (this.environment?.mapping === UVMapping) {
@@ -252,20 +263,15 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
}
this.dispatchEvent({type: 'environmentChanged', environment: this.environment})
this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.()
}

private _onBackgroundChange() {
this.dispatchEvent({type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor})
this.setDirty({refreshScene: true, geometryChanged: false})
this.refreshUi?.()
}

@serialize()
@onChange(RootScene.prototype._onBackgroundChange)
public background: null | Color | ITexture | 'environment' = null
@serialize()
@onChange(RootScene.prototype._onBackgroundChange)
public backgroundColor: Color | null = null // read in three.js WebGLBackground

/**
* @deprecated Use {@link addObject}
*/
@@ -284,7 +290,8 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
* @param options - set sceneUpdate to true to to mark that any object transformations have changed. It might trigger effects like frame fade depening on plugins.
* @returns {this}
*/
setDirty(options?: ISceneSetDirtyOptions): this { // todo;;;
setDirty(options?: ISceneSetDirtyOptions): this {
// todo: for onChange calls -> check options.key for specific key that's changed and use it to determine refreshScene
if (options?.sceneUpdate) {
console.warn('sceneUpdate is deprecated, use refreshScene instead.')
options.refreshScene = true
@@ -302,7 +309,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
private _mainCameraUpdate = () => {
this.setDirty({refreshScene: false})
this.refreshActiveCameraNearFar()
this.dispatchEvent({type: 'mainCameraUpdate'}) // this sets dirty in the viewer
this.dispatchEvent({type: 'mainCameraUpdate'})
this.dispatchEvent({type: 'activeCameraUpdate'}) // deprecated
}

@@ -328,7 +335,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
return this
}

refreshUi = iObjectCommons.refreshUi
refreshUi = iObjectCommons.refreshUi.bind(this)

/**
* Dispose the scene and clear all resources.
@@ -461,6 +468,14 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
return this
}

addEventListener<T extends ISceneEventTypes>(type: T, listener: EventListener<ISceneEvent, T, this>): void {
if (type === 'activeCameraChange') console.error('activeCameraChange is deprecated. Use mainCameraChange instead.')
if (type === 'activeCameraUpdate') console.error('activeCameraUpdate is deprecated. Use mainCameraUpdate instead.')
if (type === 'sceneMaterialUpdate') console.error('sceneMaterialUpdate is deprecated. Use materialUpdate instead.')
if (type === 'update') console.error('update is deprecated. Use sceneUpdate instead.')
super.addEventListener(type, listener)
}

/**
* Minimum Camera near plane
* @deprecated - use camera.userData.minNearPlane instead
@@ -469,7 +484,6 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead')
return this.mainCamera.userData.minNearPlane ?? 0.02
}

/**
* @deprecated - use camera.userData.minNearPlane instead
*/

+ 1
- 0
src/plugins/index.ts View File

@@ -2,4 +2,5 @@ export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin'
export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin'
export {PipelinePassPlugin} from './base/PipelinePassPlugin'
export {RenderTargetPreviewPlugin} from './ui/RenderTargetPreviewPlugin'
export {TweakpaneUiPlugin} from './ui/tweakpane/TweakpaneUiPlugin'
export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin'

+ 9
- 7
src/plugins/interaction/DropzonePlugin.ts View File

@@ -1,8 +1,9 @@
import {AViewerPluginSync} from '../../viewer/AViewerPlugin'
import {type ThreeViewer} from '../../viewer/'
import {Dropzone} from '../../utils'
import {uiButton, uiConfig, uiFolderContainer, uiToggle} from 'uiconfig.js'
import {uiButton, uiConfig, uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
import type {AddAssetOptions, ImportFilesOptions, ImportResult} from '../../assetmanager'
import {serialize} from 'ts-browser-helpers'

export interface DropzonePluginOptions {
domElement?: HTMLElement
@@ -15,7 +16,8 @@ export interface DropzonePluginOptions {
@uiFolderContainer('Dropzone')
export class DropzonePlugin extends AViewerPluginSync<'drop'> {
static readonly PluginType = 'Dropzone'
@uiToggle() enabled = true
uiConfig!: UiObjectConfig
@uiToggle() @serialize() enabled = true
private _inputEl?: HTMLInputElement
private _dropzone?: Dropzone
private _allowedExtensions: string[]|undefined = undefined // undefined and empty array is different.
@@ -23,17 +25,17 @@ export class DropzonePlugin extends AViewerPluginSync<'drop'> {
/**
* Automatically import assets when dropped.
*/
autoImport = true
@serialize() autoImport = true
/**
* Automatically add dropped and imported assets to the scene.
* Works only if {@link autoImport} is true.
Works only if {@link autoImport} is true.
*/
@uiToggle() autoAdd = true
@uiToggle() @serialize() autoAdd = true

/**
* Import options for the {@link AssetImporter.importFiles}
*/
@uiConfig() importOptions: ImportFilesOptions = {
@uiConfig() @serialize() importOptions: ImportFilesOptions = {
autoImportZipContents: true,
forceImporterReprocess: false,
}
@@ -41,7 +43,7 @@ export class DropzonePlugin extends AViewerPluginSync<'drop'> {
/**
* Add options for the {@link RootScene.addObject}
*/
@uiConfig() addOptions: AddAssetOptions = {
@uiConfig() @serialize() addOptions: AddAssetOptions = {
autoCenter: true,
importConfig: true,
autoScale: true,

+ 0
- 20
src/plugins/ui/RenderTargetPreviewPlugin.css View File

@@ -47,23 +47,3 @@
content: '+';
line-height: 20px;
}
.RenderTargetPreviewPluginContextMenu{
position: absolute;
background: #333e;
min-width: 8rem;
color: white;
font-size: 0.8rem;
overflow: hidden;
border-radius: 8px;
box-shadow: 2px 2px 10px #6666;
z-index: 100000;
font-family: monospace;
}
.RenderTargetPreviewPluginContextMenu div{
cursor: pointer;
padding: 6px 10px;
transition: background-color 0.25s ease-in-out;
}
.RenderTargetPreviewPluginContextMenu div:hover{
background: #1a1a1c;
}

+ 5
- 25
src/plugins/ui/RenderTargetPreviewPlugin.ts View File

@@ -3,6 +3,7 @@ import {IRenderTarget} from '../../rendering'
import {createDiv, createStyles, getOrCall, onChange, ValOrFunc} from 'ts-browser-helpers'
import {Vector4, WebGLRenderTarget} from 'three'
import styles from './RenderTargetPreviewPlugin.css'
import {CustomContextMenu} from '../../utils'

export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPluginSync<TEvent> {
static readonly PluginType = 'RenderTargetPreviewPlugin'
@@ -87,31 +88,10 @@ export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPl
header.oncontextmenu = (e) => {
e.preventDefault()
e.stopPropagation()
const menu = document.createElement('div')
menu.classList.add('RenderTargetPreviewPluginContextMenu')
menu.style.left = e.clientX + 'px'
menu.style.top = e.clientY + 'px'
const download = document.createElement('div')
download.innerText = 'Download'
download.onclick = () => {
this.downloadTarget(target)
menu.remove()
}
const remove = document.createElement('div')
remove.innerText = 'Remove'
remove.onclick = () => {
this.removeTarget(target)
menu.remove()
}
const cancel = document.createElement('div')
cancel.innerText = 'Cancel'
cancel.onclick = () => {
menu.remove()
}
menu.appendChild(download)
menu.appendChild(remove)
menu.appendChild(cancel)
document.body.appendChild(menu)
CustomContextMenu.Create({
'Download': () => this.downloadTarget(target),
'Remove': () => this.removeTarget(target),
}, e.clientX, e.clientY)
}
div.appendChild(header)
this.mainDiv.appendChild(div)

+ 138
- 0
src/plugins/ui/tweakpane/TweakpaneUiPlugin.ts View File

@@ -0,0 +1,138 @@
import {FolderApi} from 'tweakpane'
import * as TweakpaneImagePlugin from 'tweakpane-image-plugin'
import {UiConfigRendererTweakpane} from 'uiconfig-tweakpane'
import {IViewerPlugin, IViewerPluginSync, ThreeViewer} from '../../../viewer'
import styles from './tpTheme.css'
import {Class, createDiv, createStyles, downloadBlob, getOrCall, IEvent, uploadFile} from 'ts-browser-helpers'
import {UiObjectConfig} from 'uiconfig.js'
import {CustomContextMenu} from '../../../utils'
import {Color, Vector2, Vector3, Vector4} from 'three'
import {tpImageInputGenerator} from './tpImageInputGenerator'

export class TweakpaneUiPlugin extends UiConfigRendererTweakpane implements IViewerPluginSync {
declare ['constructor']: typeof TweakpaneUiPlugin
static readonly PluginType = 'TweakpaneUi'
enabled = true

constructor(expanded = false, bigTheme = true, container: HTMLElement = document.body) {
super(container, {
expanded, autoPostFrame: false,
})
this.THREE = {Color, Vector4, Vector3, Vector2} as any
this._root!.registerPlugin(TweakpaneImagePlugin)
if (bigTheme) createStyles(styles, container)
}

protected _viewer?: ThreeViewer
onAdded(viewer: ThreeViewer): void {
this._viewer = viewer
this.typeGenerators.image = tpImageInputGenerator(this._viewer)
viewer.addEventListener('preRender', this._preRender)
viewer.addEventListener('postRender', this._postRender)
viewer.addEventListener('preFrame', this._preFrame)
viewer.addEventListener('postFrame', this._postFrame)
}
onRemove(viewer: ThreeViewer): void {
this._viewer = undefined
viewer.removeEventListener('preRender', this._preRender)
viewer.removeEventListener('postRender', this._postRender)
viewer.removeEventListener('preFrame', this._preFrame)
viewer.removeEventListener('postFrame', this._postFrame)
this.dispose()
}

private _plugins: IViewerPlugin[] = []

setupPlugins<T extends IViewerPlugin>(...plugins: Class<T>[]): void {
plugins.forEach(plugin => this.setupPluginUi(plugin))
}

setupPluginUi<T extends IViewerPlugin>(plugin: T|Class<T>): UiObjectConfig | undefined {
const p = (<Class<IViewerPlugin>>plugin).prototype ? this._viewer?.getPlugin<T>(<Class<T>>plugin) : <T>plugin
if (!p) {
console.warn('plugin not found:', plugin)
return undefined
}
this._plugins.push(p)
if (p.uiConfig && p.uiConfig.hidden === undefined) p.uiConfig.hidden = false // todo; this is a hack for now
this.appendChild(p)
const ui = p.uiConfig
this._setupPluginSerializationContext(ui, p)
return ui
}

private _setupPluginSerializationContext(ui: any, p: IViewerPlugin) {
// serialization
if (!(ui?.uiRef && p.toJSON)) return;

(p as any)._defaultState = typeof p.toJSON === 'function' ? p.toJSON() : null
;(p as any).resetDefaults = async() => {
if (!(p as any)._defaultState) return
await p.fromJSON?.((p as any)._defaultState)
ui.uiRefresh?.(true, 'postFrame')
}
const topBtn = (ui.uiRef as FolderApi).controller_.view.element
const opBtn = createDiv({
innerHTML: '&#8942;',
classList: ['pluginOptionsButton'],
elementTag: 'button',
})
opBtn.onclick = (ev) => {
const ops = {} as any
if (typeof p.toJSON === 'function') {
ops['download preset'] = async() => {
if (!this._viewer) return
const json = this._viewer.exportPluginConfig(p)
await downloadBlob(new Blob([JSON.stringify(json, null, 2)], {type: 'application/json'}), 'preset.' + (p.constructor as any).PluginType + '.json')
}
}
if (typeof p.fromJSON === 'function') {
ops['upload preset'] = async() => {
const files = await uploadFile(false, false)
if (files.length === 0) return
const file = files[0]
const text = await file.text()
const json = JSON.parse(text)
await this._viewer?.importPluginConfig(json, p)
ui.uiRefresh?.(true, 'postFrame')
}
if ((p as any)._defaultState) ops['reset defaults'] = () => (p as any).resetDefaults?.()
}
const menu = CustomContextMenu.Create(ops, topBtn.clientWidth - 120, 12)
topBtn.append(menu)
ev.preventDefault()
}
topBtn.appendChild(opBtn)
}

refreshPluginsEnabled() {
this._plugins.forEach(p=>{
const config = p.uiConfig
if (config) {
// const enabled = (p as any).enabled ?? true
// safeSetProperty(config, 'hidden', !enabled, true)
// if (config.expanded)
// safeSetProperty(config, 'expanded', config.expanded && enabled, true)
if (getOrCall(config.hidden) !== true)
config.uiRefresh?.(true, 'postFrame')
else if (config.uiRef) {
config.uiRef.hidden = true
}
}
})
}


private _preRender = () => this.refreshQueue('preRender')
private _postRender = () => this.refreshQueue('postRender')
private _postFrame = (e: IEvent<'postFrame'>) => {
this.dispatchEvent(e)
this.refreshQueue('postFrame')
}
private _preFrame = () => this.refreshQueue('preFrame')

alert = async(message?: string): Promise<void> =>this._viewer ? this._viewer.dialog.alert(message) : window?.alert(message)
confirm = async(message?: string): Promise<boolean> =>this._viewer ? this._viewer.dialog.confirm(message) : window?.confirm(message)
prompt = async(message?: string, _default?: string, cancel = true): Promise<string | null> =>this._viewer ? this._viewer.dialog.prompt(message, _default, cancel) : window?.prompt(message, _default)

}

+ 252
- 0
src/plugins/ui/tweakpane/tpImageInputGenerator.ts View File

@@ -0,0 +1,252 @@
import {ThreeViewer} from '../../../viewer'
import type {FolderApi} from 'tweakpane'
import {UiObjectConfig} from 'uiconfig.js'
import {imageBitmapToBase64, makeTextSvg} from 'ts-browser-helpers'
import {generateUUID} from '../../../three'
import {ITexture, upgradeTexture} from '../../../core'
import {LinearSRGBColorSpace, RepeatWrapping, SRGBColorSpace, Texture} from 'three'
import {CustomContextMenu} from '../../../utils'
import {TweakpaneUiPlugin} from './TweakpaneUiPlugin'

const staticData = {
placeholderVal: 'placeholder',
renderTarImage: makeTextSvg('Render Target'),
dataTexImage: makeTextSvg('Data Texture'),
lutCubeTexImage: makeTextSvg('CUBE Texture'),
compressedTexImage: makeTextSvg('Compressed Texture'),
imageMap: {} as any,
tempMap: {} as any,
}

function proxyGetValue(cc: any) {
if (cc?.get) cc = cc.get()
let ret: any = undefined
if (!cc) return staticData.placeholderVal
if (cc.isRenderTargetTexture && !cc.image.tp_src) {
cc.image.tp_src = staticData.renderTarImage
}
if (cc.isDataTexture && !cc.image.tp_src) {
cc.image.tp_src = staticData.dataTexImage
}
if (cc.isCompressedTexture && !cc.image.tp_src) {
cc.image.tp_src = staticData.compressedTexImage
}
// todo: video is not playing
// if (cc.isVideoTexture && !cc.image.tp_src) {
// cc.image.tp_src = dataTexImage
// }
if (cc.isTexture) {
// console.warn('here')
if (cc.image && (cc.image instanceof ImageBitmap || cc.image instanceof HTMLImageElement || cc.image instanceof HTMLVideoElement) && !cc.image.tp_src) {
cc.image.tp_src = imageBitmapToBase64(cc.image, 160)
}
if (cc.image) {
ret = cc.image.tp_src_uuid
ret = ret ? staticData.imageMap[ret] : undefined
if (!ret) ret = cc.image.tp_src || cc.image.src
}
} else if (typeof cc === 'string') {
ret = cc
} else if (cc.domainMin) { // for lut CUBE files.
ret = cc.texture
if (cc.texture.image && !cc.texture.image.tp_src) {
cc.texture.image.tp_src = staticData.lutCubeTexImage
}
if (cc.texture.image) {
ret = cc.texture.image.tp_src_uuid
ret = ret ? staticData.imageMap[ret] : undefined
if (!ret) ret = cc.texture.image.tp_src || cc.texture.image.src
}
} else if (cc) {
console.error('unknown value', cc)
}
if (!ret) ret = staticData.placeholderVal
if (cc.image && !cc.image.tp_src_uuid) {
const uuid = generateUUID()
cc.image.tp_src_uuid = uuid
staticData.tempMap[ret] = uuid
}
// console.log(ret, cc, tar, key)
if (typeof ret === 'string')
ret = staticData.imageMap[ret] ?? ret // Note: this will be a bottleneck if the length of src is too long.
return ret
}

const setterTex = (v1: any, config: UiObjectConfig, renderer: TweakpaneUiPlugin)=>{
if (v1?.isTexture) {
if (!v1.isDataTexture) {
const key = renderer.methods.getBinding(config)[1] + ''
const isLinear = ['normalMap', 'aoMap', 'emissiveMap', 'roughnessMap', 'metalnessMap', 'displacementMap', 'bumpMap', 'alphaMap'].includes(key)
v1.colorSpace = isLinear ? LinearSRGBColorSpace : SRGBColorSpace
v1.wrapS = RepeatWrapping
v1.wrapT = RepeatWrapping
v1.flipY = config.__proxy.value_?.flipY ?? true // todo: figure out flipY
} else {
v1.needsUpdate = true
}
}
config.__proxy.value_ = v1
renderer.methods.setValue(config, v1, {last: true}, false)
config.uiRefresh?.(false, 'postFrame')
}

function proxySetValue(v: any, cc: any, config: UiObjectConfig, viewer: ThreeViewer, renderer: TweakpaneUiPlugin) {
if (typeof v === 'string') {
if (typeof cc === 'string') setterTex(v, config, renderer)
return
}
v = v || staticData.placeholderVal
if ((v as any).isPlaceholder || v === staticData.placeholderVal) {
if (cc) setterTex(typeof cc === 'string' ? '' : null, config, renderer)
return
}
let iMapKey = v.tp_src_uuid
if (!iMapKey) {
iMapKey = v.src ?? v.tp_src
iMapKey = staticData.tempMap[iMapKey] ?? iMapKey
delete staticData.tempMap[iMapKey]
v.tp_src_uuid = iMapKey
}
if (iMapKey)
staticData.imageMap[iMapKey] = v
// todo: dispose textures if not used.
if (typeof cc === 'string') {
setterTex(iMapKey, config, renderer)
return
}
if (cc?.image === v
|| cc?.image?.src === v.src
|| cc?.image?.tp_src === v.tp_src && v.tp_src != null
|| cc?.image?.tp_src === v.src && v.src != null
|| cc?.image?.src === v.tp_src && v.tp_src != null
) return

if (v instanceof File) { // v.src must be from createObjectURL.
viewer.assetManager.importer.importSingle<ITexture>({file: v, path: (v as any).src}).then(texture => {
if (!texture) return
if (texture.isDataTexture) texture.needsUpdate = true
const ext = (v as any).src?.split('?')?.[0]?.split('.').pop()
if ((texture as any).userData) {
if (!(texture as any).userData.mimeType)
(texture as any).userData.mimeType = 'image/' + (['jpg', 'jpeg'].includes(ext) ? 'jpeg' : 'png')
}
setterTex(texture, config, renderer)
})
} else { // HTMLImageElement, ImageBitmap, HTMLVideoElement
const tex: ITexture = new Texture(v)
upgradeTexture.call(tex)
tex.assetType = 'texture'
tex.needsUpdate = true
// set userData.mimeType for GLTFExporter
const ext = v.src?.split('?')?.[0]?.split('.').pop()
if (!tex.userData.mimeType)
tex.userData.mimeType = 'image/' + (['jpg', 'jpeg'].includes(ext) ? 'jpeg' : 'png')
setterTex(tex, config, renderer)
// todo: make normal maps jpeg always? jpg is lossy
}
}

function removeImage(config: UiObjectConfig, renderer: TweakpaneUiPlugin) {
const vc = config.uiRef.controller_.valueController as any
vc.value.setRawValue('')
const isStr = typeof config.__proxy.value_ === 'string'
setterTex(isStr ? '' : null, config, renderer)
}

function downloadImage(config: UiObjectConfig) {
const cc = config.__proxy.value_
let vcv = cc?.image ?? config.uiRef.controller_.valueController.value.rawValue
if (vcv && (vcv instanceof ImageBitmap || vcv instanceof HTMLImageElement || vcv instanceof HTMLVideoElement) && !(vcv as any).src)
vcv = imageBitmapToBase64(vcv)
const link = document.createElement('a')
document.body.appendChild(link)
link.style.display = 'none'
link.href = vcv?.src ?? vcv
link.download = 'image.png'
// link.target = '_blank'
link.click()
document.body.removeChild(link)
}

async function imageFromUrl(renderer: TweakpaneUiPlugin, config: UiObjectConfig, viewer: ThreeViewer) {
// let url: string|null = navigator.clipboard ? await navigator.clipboard.readText() : ''
let url: string | null = ''
// if (!url || !url.startsWith('http') && !url.startsWith('data:image')) {
// url = ''
// }
url = await renderer.prompt('Load texture: Enter Image/Texture URL', url, true)
if (!url || !url.startsWith('http') && !url.startsWith('data:image')) {
if (url !== null) await renderer.alert('Loading Image: Invalid URL')
return
} else {
url = url.trim()
}
const cc = config.__proxy.value_
const isStr = typeof cc === 'string'
if (isStr) {
setterTex(url, config, renderer)
} else { // texture
viewer.assetManager.importer.importSingle<ITexture>(url).then(texture => {
if (!texture) {
console.warn('Failed to load texture', url)
return
}
setterTex(texture, config, renderer)
})
}
}

export const tpImageInputGenerator = (viewer: ThreeViewer) => (parent: FolderApi, config: UiObjectConfig, renderer: TweakpaneUiPlugin, params?: any) => {
// if (config.value !== undefined) throw 'Not supported yet'

if (!config.__proxy) {
config.__proxy = {
listedOnChange: false,
}

Object.defineProperty(config.__proxy, 'value', {
get: () => {
config.__proxy.value_ = renderer.methods.getValue(config)
return proxyGetValue(config.__proxy.value_)
},
set: (v: any) => {
config.__proxy.value_ = renderer.methods.getValue(config)
proxySetValue(v, config.__proxy.value_, config, viewer, renderer)
},
})
}
config.__proxy.value_ = renderer.methods.getValue(config)

params = params ?? {}
params.extensions = ['.jpg', '.png', '.svg', '.hdr',
'.exr', /* '.mp4', '.ogg', '.mov',*/ '.jpeg',
'.bmp', '.gif', '.webp', '.cube']
if (typeof params.imageFit === 'undefined') params.imageFit = 'contain'
if (typeof params.clickCallback === 'undefined') params.clickCallback = (ev: MouseEvent, inp: HTMLInputElement) => {
const target = ev?.target as HTMLElement
const rect = target?.getBoundingClientRect()
if (!rect) {
inp.click()
return
}
const cv = config.uiRef.controller_.valueController.value.rawValue
const isPlaceholder = cv === staticData.placeholderVal || cv?.isPlaceholder
const items: any = isPlaceholder ? {} : {
['remove image']: () => removeImage(config, renderer),
['download image']: () => downloadImage(config),
}
const menu = CustomContextMenu.Create({
...items,
['set/replace image']: () => inp.click(),
['from url']: async() => imageFromUrl(renderer, config, viewer),
'cancel': () => {return},
}, 2, rect.height + 8, false, true)
target.parentElement?.appendChild(menu)
if (rect.y > document.body.clientHeight * 0.7) {
menu.style.top = 'auto'
menu.style.bottom = rect.height + 8 + 'px'
}
}
params.view = 'input-image'
return renderer.typeGenerators.input(parent, config, renderer, params)
}

+ 140
- 0
src/plugins/ui/tweakpane/tpTheme.css View File

@@ -0,0 +1,140 @@
#tweakpaneUiContainer {
/*padding-right: 0.25rem;*/
}

:root {
--tp-blade-unit-size: 24px;

/*
--tp-base-shadow-color: #00000000;
--tp-base-background-color: #00000000;
--tp-container-background-color: rgba(32, 32, 32, 0.8);
--tp-container-background-color-hover: rgba(32, 32, 32, 0.9);
--tp-container-background-color-active: rgba(32, 32, 32, 1.0);
--tp-container-background-color-focus: rgba(32, 32, 32, 1.0);
--tp-base-border-radius: 0.4rem;

--tp-button-background-color: hsla(0, 0%, 70%, 1.00);
--tp-button-background-color-active: hsla(0, 0%, 85%, 1.00);
--tp-button-background-color-focus: hsla(0, 0%, 80%, 1.00);
--tp-button-background-color-hover: hsla(0, 0%, 75%, 1.00);
--tp-button-foreground-color: hsla(0, 0%, 5%, 1.00);
--tp-container-foreground-color: hsla(0, 0%, 95%, 1.00);
--tp-groove-foreground-color: hsla(0, 0%, 10%, 1.00);
--tp-input-background-color: hsla(0, 0%, 10%, 1.00);
--tp-input-background-color-active: hsla(0, 0%, 25%, 1.00);
--tp-input-background-color-focus: hsla(0, 0%, 20%, 1.00);
--tp-input-background-color-hover: hsla(0, 0%, 15%, 1.00);
--tp-input-foreground-color: hsla(0, 0%, 90%, 1.00);
--tp-label-foreground-color: hsla(0, 0%, 85%, 1.00);
--tp-monitor-background-color: hsla(0, 0%, 8%, 1.00);
--tp-monitor-foreground-color: hsla(0, 0%, 48%, 1.00);
*/
--tp-element-border-radius: 0.25rem;

--tp-base-background-color: #28223C;

/*--tp-base-background-color: hsla(235, 21%, 31%, 0.5);*/
--tp-base-shadow-color: hsla(0, 0%, 0%, 0.2);
--tp-button-background-color: hsla(230, 10%, 80%, 1.00);
--tp-button-background-color-active: hsla(230, 10%, 95%, 1.00);
--tp-button-background-color-focus: hsla(230, 10%, 90%, 1.00);
--tp-button-background-color-hover: hsla(230, 10%, 85%, 1.00);
--tp-button-foreground-color: hsla(230, 20%, 11%, 1.00);
--tp-container-background-color: hsla(230, 25%, 16%, 0.65);
--tp-container-background-color-active: hsla(230, 25%, 31%, 0.65);
--tp-container-background-color-focus: hsla(230, 25%, 26%, 0.65);
--tp-container-background-color-hover: hsla(230, 25%, 21%, 0.65);
--tp-container-foreground-color: hsl(240, 10%, 92%);
--tp-groove-foreground-color: hsla(230, 20%, 8%, 1.00);
--tp-input-background-color: hsla(230, 20%, 8%, 1.00);
--tp-input-background-color-active: hsla(230, 28%, 23%, 1.00);
--tp-input-background-color-focus: hsla(230, 28%, 18%, 1.00);
--tp-input-background-color-hover: hsla(230, 20%, 13%, 1.00);
--tp-input-foreground-color: hsla(230, 10%, 80%, 1.00);
/*--tp-label-foreground-color: hsl(229, 100%, 97%);*/
--tp-monitor-background-color: hsla(230, 20%, 8%, 1.00);
--tp-monitor-foreground-color: hsla(230, 12%, 48%, 1.00);

--tp-label-foreground-color: #E4E2ED;

/*--tp-font-family: 'Inter';*/
--tp-font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
}

.tp-fldv {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
background-blend-mode: luminosity;
position: relative;
}

.tp-fldv .tp-fldv {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}

.tp-fldv .tp-brkv {
background-color: rgba(32, 32, 50, 0.85);
}

.tp-fldv .tp-fldv .tp-brkv {
background-color: rgba(32, 32, 50, 0.25) !important;
}

.tp-fldv-expanded > .tp-fldv_b {
background-color: rgba(32, 32, 50, 0.80) !important;
}

.tp-fldv_b {
height: calc(var(--bld-us) * 1.5 + 4px) !important;
font-size: 0.85rem !important;
}

.tp-fldv_b + .tp-brkv .tp-fldv_b {
height: calc(var(--bld-us) * 1.1 + 4px) !important;
font-size: 0.65rem !important;
}

.tp-fldv_b + .tp-brkv .tp-fldv-expanded > .tp-fldv_b {
}

.tp-lblv_l {
font-size: 0.7rem !important;
font-weight: 400 !important;
flex-grow: 1 !important;
flex-basis: 20% !important;
}

.tp-lblv_v {
flex-grow: 1 !important;
flex-basis: 50% !important;
}

.tp-txtv_i {
font-size: 0.7rem !important;
font-weight: 400 !important;
}

.tp-fldv_t {
font-weight: 400 !important;
padding-left: 1.5rem !important;
}

.tp-fldv_m {
right: auto !important;
left: 0.75rem;
opacity: 1.0 !important;
}

.pluginOptionsButton{
position: absolute;
right: 0;
top: 0.75rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
height: min-content;
background: transparent;
color: #eeeeee;
border: none;
}

+ 7
- 1
src/postprocessing/ExtendedRenderPass.ts View File

@@ -12,7 +12,7 @@ import {
WebGLMultipleRenderTargets,
WebGLRenderTarget,
} from 'three'
import {uiToggle} from 'uiconfig.js'
import {generateUiConfig, UiObjectConfig, uiToggle} from 'uiconfig.js'
import {serialize} from 'ts-browser-helpers'
import {GenericBlendTexturePass} from './GenericBlendTexturePass'
import {IRenderTarget} from '../rendering'
@@ -296,6 +296,12 @@ export class ExtendedRenderPass extends RenderPass implements IPipelinePass<'ren
this.camera = camera
}

uiConfig: UiObjectConfig = {
label: 'Render Pass',
type: 'folder',
children: generateUiConfig(this),
}


// legacy


+ 2
- 0
src/postprocessing/ScreenPass.ts View File

@@ -3,10 +3,12 @@ import {Shader} from 'three'
import {ShaderMaterial2} from '../core'
import {CopyShader} from 'three/examples/jsm/shaders/CopyShader.js'
import {IPassID, IPipelinePass} from './Pass'
import {uiFolderContainer} from 'uiconfig.js'

export type TViewerScreenShaderFrag = string | [string, string] | {pars?: string, main: string}
export type TViewerScreenShader = TViewerScreenShaderFrag | Shader | ShaderMaterial2

@uiFolderContainer('Screen Pass')
export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'screen'> {
readonly passId = 'screen'
after: IPassID[] = ['render']

+ 8
- 3
src/rendering/RenderManager.ts View File

@@ -25,16 +25,20 @@ import {
IWebGLRenderer,
upgradeWebGLRenderer,
} from '../core'
import {onChange, serializable, serialize} from 'ts-browser-helpers'
import {onChange2, serializable, serialize} from 'ts-browser-helpers'
import {uiConfig, uiFolderContainer, uiMonitor, uiSlider, uiToggle} from 'uiconfig.js'

@serializable('RenderManager')
@uiFolderContainer('Render Manager')
export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRenderManagerEventTypes> implements IShaderPropertiesUpdater, IRenderManager {
private readonly _isWebGL2: boolean
private readonly _composer: EffectComposer2
private readonly _context: WebGLRenderingContext
@uiMonitor('Render Size')
private readonly _renderSize = new Vector2(512, 512) // this is updated automatically.
protected readonly _renderer: IWebGLRenderer<this>
private _renderScale = 1.
@uiConfig(undefined, {label: 'Passes'})
private _passes: IPipelinePass[] = []
private _pipeline: IPassID[] = []
private _passesNeedsUpdate = true
@@ -50,7 +54,7 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
* Use total frame count, if this is set to true, then frameCount won't be reset when the viewer is set to dirty.
* Which will generate different random numbers for each frame during postprocessing steps. With TAA set properly, this will give a smoother result.
*/
@serialize() stableNoise = false
@uiToggle() @serialize() stableNoise = false

public frameWaitTime = 0 // time to wait before next frame // used by canvas recorder //todo/

@@ -59,7 +63,7 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
/**
* Set autoBuildPipeline = false to be able to set the pipeline manually.
*/
@onChange(RenderManager.prototype.rebuildPipeline)
@onChange2(RenderManager.prototype.rebuildPipeline)
public autoBuildPipeline = true

rebuildPipeline(setDirty = true): void {
@@ -331,6 +335,7 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
get renderSize(): Vector2 {
return this._renderSize
}
@uiSlider('Render Scale', [0.1, 8], 0.05)
get renderScale(): number {
return this._renderScale
}

+ 1
- 1
src/three/utils/misc.ts View File

@@ -4,7 +4,7 @@ import {mergeVertices} from 'three/examples/jsm/utils/BufferGeometryUtils.js'
/**
* Convert geometry to BufferGeometry with indexed attributes.
*/
export function toIndexedGeometry(geometry: BufferGeometry, tolerance = -1) {
export function toIndexedGeometry(geometry: BufferGeometry<any, any, any>, tolerance = -1) {
return mergeVertices(geometry, tolerance)
}


+ 17
- 17
src/ui/image-ui.ts View File

@@ -24,42 +24,32 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
type: 'folder',
label: <string>map + ' Sampler',
hidden: ()=>!mat[map],
onChange: ()=>mat.setDirty(),
children: [
()=>({
type: 'vec2',
label: 'Repeat',
// hidden: ()=>!im && mat.map,
bounds: [-100, 100],
stepSize: 0.001,
property: [mat[map], 'repeat'],
onChange: mat?.setDirty,
}),
()=>({
type: 'vec2',
label: 'Offset',
// hidden: ()=>!im && mat.map,
bounds: [-2, 2],
stepSize: 0.001,
property: [mat[map], 'offset'],
onChange: mat?.setDirty,
}),
()=>({
type: 'vec2',
label: 'Center',
// hidden: ()=>!im && mat.map,
bounds: [-2, 2],
stepSize: 0.001,
property: [mat[map], 'center'],
onChange: mat?.setDirty,
}),
()=>({
type: 'input',
label: 'Rotation',
stepSize: 0.001,
// hidden: ()=>!im && mat.map,
bounds: [-Math.PI, Math.PI],
property: [mat[map], 'rotation'],
onChange: mat?.setDirty,
}),
()=>({
type: 'dropdown',
@@ -81,7 +71,17 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
// mat[map] = tex.clone() // it doesn't work with just setting needsUpdate = true
// ;(mat[map] as any).uuid = tex.uuid
// tex!.dispose()
}, mat?.setDirty],
}],
}),
()=>({
type: 'dropdown',
label: 'UV Channel',
property: [mat[map], 'channel'],
children: [0, 1, 2, 3].map(value => ({label: value.toString(), value})),
onChange: ()=>{
const tex = mat[map] as any
if (tex) tex.needsUpdate = true
},
}),
()=>({
type: 'checkbox',
@@ -134,7 +134,7 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
label: value[0],
value: value[1],
})),
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}, mat?.setDirty],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}],
}),
()=>({
type: 'dropdown',
@@ -148,7 +148,7 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
label: value[0],
value: value[1],
})),
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}, mat?.setDirty],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}],
}),
()=>({
type: 'input',
@@ -156,7 +156,7 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
bounds: [1, 6],
stepSize: 1,
property: [mat[map], 'anisotropy'],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true; mat.needsUpdate = true}, mat?.setDirty],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true; mat.needsUpdate = true}],
}),
()=>({
type: 'dropdown',
@@ -173,7 +173,7 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
label: value[0],
value: value[1],
})),
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}, mat?.setDirty],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}],
}),
()=>({
type: 'dropdown',
@@ -186,7 +186,7 @@ export function makeSamplerUi<T extends IMaterial>(mat: T, map: keyof T) {
label: value[0],
value: value[1],
})),
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}, mat?.setDirty],
onChange: [()=>{if (mat[map])(mat[map] as any)!.needsUpdate = true}],
}),

],

+ 55
- 0
src/utils/CustomContextMenu.css View File

@@ -0,0 +1,55 @@
.CustomContextMenu{
position: absolute;
background: #333e;
min-width: 8rem;
color: white;
font-size: 0.8rem;
overflow: hidden;
border-radius: 8px;
box-shadow: 2px 2px 10px #6666;
z-index: 100000;
}
.CustomContextMenu div{
cursor: pointer;
padding: 6px 10px;
transition: background-color 0.25s ease-in-out;
}
.CustomContextMenu div:hover{
background: #1a1a1c;
}

#customContextMenu {
background: #333e;
backdrop-filter: blur(8px);
border: 0.5px solid rgba(20, 20, 20, 0.3);
width: auto;
height: auto;
position: absolute;
display: flex;
flex-direction: column;
z-index: 9999;
padding: 0.4rem 0.25rem;
border-radius: 0.375rem;
min-width: 8rem;
pointer-events: auto;
box-shadow: 0 2px 10px rgba(12, 12, 12, 0.2);
}

.customContextMenuItems {
color: white;
font-size: 0.8rem;
font-family: monospace;
background-color: transparent;
cursor: pointer;
padding: 4px 8px;
border-radius: 0.25rem;
line-height: 1rem;
font-weight: 500;
transition: background-color 0.25s ease-in-out;
}

.customContextMenuItems:hover {
color: white;
background-color: #017AFF;
}


+ 47
- 0
src/utils/CustomContextMenu.ts View File

@@ -0,0 +1,47 @@
import styles from './CustomContextMenu.css'

export class CustomContextMenu {
public static Element: HTMLDivElement | undefined = undefined
private static _inited = false

private static _initialize(): void {
this._inited = true
document.addEventListener('pointerdown', (e) => {
if (this.Element && !this.Element.contains(e.target as any)) {
this.Remove()
}
})
}

public static Create(items: Record<string, () => void>, x: number, y: number, show = true, removeOnSelect = true): HTMLDivElement {
if (!this._inited) this._initialize()

if (this.Element) this.Remove()

const container = document.createElement('div')
container.id = 'customContextMenu'
container.style.top = y + 'px'
container.style.left = x + 'px'
container.innerHTML = '<style>' + styles + '</style>'

for (const [key, func] of Object.entries(items)) {
const d = document.createElement('div')
d.classList.add('customContextMenuItems')
d.innerHTML = key
container.appendChild(d)
d.onclick = async() => {
await func()
if (removeOnSelect) this.Remove()
}
}

this.Element = container
if (show) document.body.appendChild(container)
return container
}

public static Remove(): void {
this.Element?.remove()
this.Element = undefined
}
}

+ 1
- 0
src/utils/index.ts View File

@@ -1,6 +1,7 @@
export * from './browser-helpers'
export {windowDialogWrapper, type IDialogWrapper} from './DialogWrapper'
export {GLStatsJS} from './GLStatsJS'
export {CustomContextMenu} from './CustomContextMenu'
export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from './Dropzone'
export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter} from './serialization'
export {shaderReplaceString} from './shader-helpers'

+ 1
- 2
src/viewer/IViewerPlugin.ts View File

@@ -1,4 +1,3 @@
import {BaseEvent, EventDispatcher} from 'three'
import {IUiConfigContainer} from 'uiconfig.js'
import {Class, IDisposable, IJSONSerializable} from 'ts-browser-helpers'
import {SerializationMetaType} from '../utils'
@@ -9,7 +8,7 @@ import {ISerializedConfig, ThreeViewer} from './ThreeViewer'
* @category Viewer
*/
export interface IViewerPlugin<TViewer extends ThreeViewer = ThreeViewer, IsSync extends boolean = boolean>
extends EventDispatcher<BaseEvent, string>, IUiConfigContainer, Partial<IJSONSerializable<ISerializedConfig, SerializationMetaType>>, IDisposable {
extends IUiConfigContainer, Partial<IJSONSerializable<ISerializedConfig, SerializationMetaType>>, IDisposable {
// all classes must have this static property with a unique identifier value for this plugin
constructor: {
PluginType: string

+ 6
- 2
src/viewer/ThreeViewer.ts View File

@@ -35,6 +35,7 @@ import {
import {GLStatsJS, IDialogWrapper, windowDialogWrapper} from '../utils'
import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
import {uiConfig, uiFolderContainer, UiObjectConfig} from 'uiconfig.js'

export type IViewerEvent = BaseEvent & {
type: 'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'
@@ -125,9 +126,11 @@ const VIEWER_VERSION = '0.0.1'
* The ThreeViewer is the main class in the framework.
* @category Viewer
*/
@uiFolderContainer('Viewer')
export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes> {
public static readonly VERSION = VIEWER_VERSION
public static readonly ConfigTypeSlug = 'vjson'
uiConfig!: UiObjectConfig

static Console: IConsoleWrapper = console
static Dialog: IDialogWrapper = windowDialogWrapper
@@ -150,14 +153,14 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
// this can be used by other plugins to add ui elements alongside the canvas
private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...

@serialize('renderManager')
@uiConfig() @serialize('renderManager')
readonly renderManager: ViewerRenderManager

/**
* The Scene attached to the viewer, this cannot be changed.
* @type {RootScene}
*/
@serialize('scene')
@uiConfig() @serialize('scene')
private readonly _scene: RootScene

public readonly plugins: Record<string, IViewerPlugin> = {}
@@ -221,6 +224,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
// camera

const camera = new PerspectiveCamera2('orbit', this._canvas)
camera.name = 'Default Camera'
camera.position.set(0, 0, 5)
camera.userData.autoLookAtTarget = true
this.addEventListener('postFrame', () => { // todo: move inside RootScene.

+ 2
- 0
src/viewer/ViewerRenderManager.ts View File

@@ -2,6 +2,7 @@ import {IRenderTarget, RenderManager} from '../rendering'
import {HalfFloatType, NoColorSpace, RGBM16ColorSpace, UnsignedByteType} from 'three'
import {IRenderManagerOptions} from '../core'
import {ExtendedRenderPass, ScreenPass, TViewerScreenShader} from '../postprocessing'
import {uiFolderContainer} from 'uiconfig.js'

export interface ViewerRenderManagerOptions extends IRenderManagerOptions {
rgbm?: boolean,
@@ -11,6 +12,7 @@ export interface ViewerRenderManagerOptions extends IRenderManagerOptions {
screenShader?: TViewerScreenShader
}

@uiFolderContainer('Render Manager')
export class ViewerRenderManager extends RenderManager {
readonly rgbm: boolean
readonly msaa: boolean

Loading…
Cancel
Save