| import {_testFinish, downloadBlob, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||||
| import {_testFinish, AssetExporterPlugin, downloadBlob, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | ||||
| import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform' | import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform' | ||||
| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas') as HTMLCanvasElement, msaa: true}) | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| dropzone: { | |||||
| addOptions: { | |||||
| disposeSceneObjects: true, | |||||
| }, | |||||
| }, | |||||
| msaa: true, | |||||
| }) | |||||
| async function init() { | async function init() { | ||||
| viewer.addPluginSync(LoadingScreenPlugin) | viewer.addPluginSync(LoadingScreenPlugin) | ||||
| viewer.addPluginSync(AssetExporterPlugin) | |||||
| viewer.addPluginSync(GLTFDracoExportPlugin) | viewer.addPluginSync(GLTFDracoExportPlugin) | ||||
| // Note: see asset-exporter-plugin example as well | // Note: see asset-exporter-plugin example as well |
| } | } | ||||
| :root { | :root { | ||||
| --primary-color: #58a6ff; | |||||
| //--primary-color: #58a6ff; | |||||
| --primary-color: #6d7a8c; | |||||
| //--primary-color: #ccdef5; | //--primary-color: #ccdef5; | ||||
| //--primary-color: #ec630a; | //--primary-color: #ec630a; | ||||
| --secondary-color: #ec630a; | --secondary-color: #ec630a; | ||||
| //--background-color: #1a1a1c; | //--background-color: #1a1a1c; | ||||
| --background-color: #1B1B1F; | |||||
| --background-color: #E7EFF8; | |||||
| --background-color-search: #dae1ea; | |||||
| --background-color-search-hover: #c3d4e7; | |||||
| //--text-color: #bbb; | //--text-color: #bbb; | ||||
| --text-color: #DFDFD6; | |||||
| --text-color-accent: #eee; | |||||
| --text-color-hover: #fff; | |||||
| --text-color: #1C2026; | |||||
| --text-color-accent: #2b313a; | |||||
| --text-color-hover: #2b313a; | |||||
| } | |||||
| @media (prefers-color-scheme: light) { | |||||
| :root { | |||||
| --primary-color: #b6bfcb; | |||||
| --secondary-color: #ec630a; | |||||
| --background-color: #1C2026; | |||||
| --background-color-search: #3e4550; | |||||
| --background-color-search-hover: #555e6b; | |||||
| --text-color: #F6F7F9; | |||||
| --text-color-accent: #d2d3d5; | |||||
| --text-color-hover: #d2d3d5; | |||||
| } | |||||
| } | } | ||||
| .root-container { | .root-container { | ||||
| font-size: 1.75rem; | font-size: 1.75rem; | ||||
| padding: 0.5rem; | padding: 0.5rem; | ||||
| color: var(--text-color); | color: var(--text-color); | ||||
| cursor: pointer; | |||||
| } | } | ||||
| .hamburger:hover { | .hamburger:hover { | ||||
| } | } | ||||
| .sidebar { | .sidebar { | ||||
| max-width: min(320px, 30%); | |||||
| width: min(320px, 30%); | |||||
| height: 100%; | height: 100%; | ||||
| background: var(--background-color); | background: var(--background-color); | ||||
| color: var(--text-color); | color: var(--text-color); | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 0.5rem; | gap: 0.5rem; | ||||
| position: relative; | position: relative; | ||||
| transition: width 0s ease-in-out; | |||||
| } | } | ||||
| .sidebar h1 { | .sidebar h1 { | ||||
| color: #ddd; | |||||
| color: var(--text-color); | |||||
| font-size: 1.5rem; | font-size: 1.5rem; | ||||
| margin: 0 3rem 1rem 0; | margin: 0 3rem 1rem 0; | ||||
| font-weight: normal; | font-weight: normal; | ||||
| } | } | ||||
| .sidebar h1 a { | .sidebar h1 a { | ||||
| color: var(--secondary-color); | |||||
| //background: linear-gradient(to right, rgb(231, 7, 7), rgb(91, 34, 203), rgb(7, 108, 211)); | |||||
| background: linear-gradient(to right, var(--secondary-color), var(--secondary-color), rgb(231, 7, 7)); | |||||
| -webkit-background-clip: text; | |||||
| -webkit-text-fill-color: transparent; | |||||
| text-decoration: none; | text-decoration: none; | ||||
| } | } | ||||
| .sidebar h2 { | .sidebar h2 { | ||||
| font-size: 1.2rem; | font-size: 1.2rem; | ||||
| margin: 0 0 0.5rem 0.25rem; | margin: 0 0 0.5rem 0.25rem; | ||||
| font-weight: normal; | |||||
| font-weight: 500; | |||||
| padding: 0; | padding: 0; | ||||
| } | } | ||||
| color: var(--primary-color); | color: var(--primary-color); | ||||
| text-underline-offset: 0.25rem; | text-underline-offset: 0.25rem; | ||||
| text-decoration: none; | text-decoration: none; | ||||
| transition: color 0.25s ease-in-out; | |||||
| font-weight: 500; | |||||
| transition: color 0.3s ease-in-out, font-weight 0.3s ease-in-out, background-size 0.75s; | |||||
| background: linear-gradient(to right, rgb(231, 7, 7), rgb(91, 34, 203), rgb(7, 108, 211)); | |||||
| background-position: 0 100%; | |||||
| background-repeat: no-repeat; | |||||
| background-size: 0 0; | |||||
| padding-bottom: 2px; | |||||
| } | } | ||||
| .sidebar ul li a:hover { | .sidebar ul li a:hover { | ||||
| .sidebar ul li a.selected { | .sidebar ul li a.selected { | ||||
| color: var(--text-color-hover); | color: var(--text-color-hover); | ||||
| font-weight: bold; | font-weight: bold; | ||||
| text-decoration: underline; | |||||
| background-size: 100% 0.1em; | |||||
| //text-decoration: underline; | |||||
| } | } | ||||
| .iframe-container { | .iframe-container { | ||||
| .closed.sidebar{ | .closed.sidebar{ | ||||
| padding: 1.75rem; | padding: 1.75rem; | ||||
| width: 3.3rem; | |||||
| transition: width 0.3s ease-in-out; | |||||
| } | } | ||||
| .closed:before{ | .closed:before{ | ||||
| content: attr(data-selected-example); | content: attr(data-selected-example); | ||||
| line-height: 3.25rem; | line-height: 3.25rem; | ||||
| writing-mode: vertical-lr; | writing-mode: vertical-lr; | ||||
| text-orientation: upright; | text-orientation: upright; | ||||
| text-transform: uppercase; | |||||
| font-family: "Source Code Pro", Menlo, Courier, monospace; | |||||
| } | } | ||||
| .closed > .search-bar { | .closed > .search-bar { | ||||
| box-sizing: border-box; | box-sizing: border-box; | ||||
| width: 100%; | width: 100%; | ||||
| padding: 0.5rem; | padding: 0.5rem; | ||||
| color: var(--text-color); | |||||
| background: #1e1e20; | |||||
| color: var(--text-color-accent); | |||||
| background: var(--background-color-search); | |||||
| border: none; | border: none; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| transition: all 0.3s ease-in-out; | transition: all 0.3s ease-in-out; | ||||
| } | } | ||||
| .search-bar input:hover { | .search-bar input:hover { | ||||
| background: #2a2a2c; | |||||
| background: var(--background-color-search-hover); | |||||
| } | } | ||||
| .search-bar input:focus { | .search-bar input:focus { | ||||
| background: #2a2a2c; | |||||
| background: var(--background-color-search-hover); | |||||
| color: var(--text-color-hover); | color: var(--text-color-hover); | ||||
| outline: none; | outline: none; | ||||
| } | } | ||||
| .search-bar input::placeholder { | .search-bar input::placeholder { | ||||
| color: #888; | |||||
| color: var(--primary-color); | |||||
| } | } | ||||
| .search-icon { | .search-icon { | ||||
| position: absolute; | position: absolute; | ||||
| top: 50%; | top: 50%; | ||||
| left: 10px; | left: 10px; | ||||
| transform: translateY(-50%); | transform: translateY(-50%); | ||||
| color: #888; | |||||
| color: var(--primary-color); | |||||
| width: 15px; | width: 15px; | ||||
| height: 15px; | height: 15px; | ||||
| } | } | ||||
| flex-direction: column; | flex-direction: column; | ||||
| } | } | ||||
| .sidebar{ | .sidebar{ | ||||
| max-width: 100%; | |||||
| width: 100%; | |||||
| height: auto; | height: auto; | ||||
| max-height: 50%; | max-height: 50%; | ||||
| padding: 1rem; | padding: 1rem; |
| BlendLoadPlugin, | BlendLoadPlugin, | ||||
| HierarchyUiPlugin, | HierarchyUiPlugin, | ||||
| GeometryGeneratorPlugin, | GeometryGeneratorPlugin, | ||||
| Object3DWidgetsPlugin, | |||||
| new Object3DWidgetsPlugin(false), | |||||
| Object3DGeneratorPlugin, | Object3DGeneratorPlugin, | ||||
| GaussianSplattingPlugin, | GaussianSplattingPlugin, | ||||
| ContactShadowGroundPlugin, | ContactShadowGroundPlugin, |
| } from 'three' | } from 'three' | ||||
| import type {IObject3D, IObjectProcessor} from '../IObject' | import type {IObject3D, IObjectProcessor} from '../IObject' | ||||
| import {type ICamera} from '../ICamera' | import {type ICamera} from '../ICamera' | ||||
| import {Box3B} from '../../three' | |||||
| import {bindToValue, Box3B} from '../../three' | |||||
| import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers' | import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers' | ||||
| import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' | import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' | ||||
| import {ThreeSerialization} from '../../utils' | import {ThreeSerialization} from '../../utils' | ||||
| @uiSlider('Background Intensity', [0, 10], 0.01) | @uiSlider('Background Intensity', [0, 10], 0.01) | ||||
| backgroundIntensity = 1 | backgroundIntensity = 1 | ||||
| /** | |||||
| * The default environment map used when rendering materials in the scene | |||||
| */ | |||||
| @uiImage('Environment') | @uiImage('Environment') | ||||
| @serialize() @onChange2(RootScene.prototype._onEnvironmentChange) | @serialize() @onChange2(RootScene.prototype._onEnvironmentChange) | ||||
| environment: ITexture | null = null | environment: ITexture | null = null | ||||
| /** | |||||
| * Extra textures/envmaps that can be used by objects/materials/plugins and will be serialized. | |||||
| */ | |||||
| @serialize() | |||||
| public textureSlots: Record<string, ITexture> = {} | |||||
| /** | /** | ||||
| * The intensity for the environment light. | * The intensity for the environment light. | ||||
| */ | */ | ||||
| @uiSlider('Environment Intensity', [0, 10], 0.01) | @uiSlider('Environment Intensity', [0, 10], 0.01) | ||||
| @serialize() @onChange3(RootScene.prototype.setDirty) | @serialize() @onChange3(RootScene.prototype.setDirty) | ||||
| envMapIntensity = 1 | envMapIntensity = 1 | ||||
| /** | |||||
| * Rotation in radians of the default environment map. | |||||
| * Same as {@link environment}.rotation. | |||||
| * | |||||
| * Note - this is not serialized here, but inside the texture. | |||||
| */ | |||||
| @uiSlider('Environment Rotation', [-Math.PI, Math.PI], 0.01) | |||||
| @bindToValue({obj: 'environment', key: 'rotation', onChange: RootScene.prototype.setDirty, onChangeParams: false}) | |||||
| envMapRotation = 0 | |||||
| /** | |||||
| * Extra textures/envmaps that can be used by objects/materials/plugins and will be serialized. | |||||
| */ | |||||
| @serialize() | |||||
| public textureSlots: Record<string, ITexture> = {} | |||||
| /** | /** | ||||
| * Fixed direction environment reflections irrespective of camera position. | * Fixed direction environment reflections irrespective of camera position. | ||||
| */ | */ |
| onAdded(viewer: ThreeViewer) { | onAdded(viewer: ThreeViewer) { | ||||
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| viewer.scene.addEventListener('addSceneObject', this._addSceneObject) | viewer.scene.addEventListener('addSceneObject', this._addSceneObject) | ||||
| viewer.scene.addObject(this._widgetRoot) | |||||
| viewer.scene.addObject(this._widgetRoot, {addToRoot: true, autoScale: false, autoCenter: false}) | |||||
| } | } | ||||
| onRemove(viewer: ThreeViewer) { | onRemove(viewer: ThreeViewer) { | ||||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) |
| ['TONEMAP_BACKGROUND']: '1', | ['TONEMAP_BACKGROUND']: '1', | ||||
| } as const | } as const | ||||
| @serialize() @uiToggle('Enabled') enabled = true | |||||
| @serialize() | |||||
| @onChange(TonemapPlugin.prototype.setDirty) | |||||
| @uiToggle('Enabled') | |||||
| enabled = true | |||||
| @uiDropdown('Mode', ([ | @uiDropdown('Mode', ([ | ||||
| ['Linear', LinearToneMapping], | ['Linear', LinearToneMapping], |
| * @param obj - object to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the object/string. | * @param obj - object to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the object/string. | ||||
| * @param key - key to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the key/string. | * @param key - key to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the key/string. | ||||
| * @param onChange - function to call when the value changes. If a string, it is used as a property name in `this` and called. If a function, it is called. The function is called with the following parameters: key, newVal | * @param onChange - function to call when the value changes. If a string, it is used as a property name in `this` and called. If a function, it is called. The function is called with the following parameters: key, newVal | ||||
| * @param onChangeParams - if true, the parameters passed to the onChange function are [key, newVal]. If false, no parameters are passed. Default = `true` | |||||
| * @param processVal - function that processes the value before setting it. | * @param processVal - function that processes the value before setting it. | ||||
| * @param invProcessVal - function that processes the value before returning it. | * @param invProcessVal - function that processes the value before returning it. | ||||
| */ | */ | ||||
| export function bindToValue({obj, key, onChange, processVal, invProcessVal}: {obj?: ValOrFunc<any>, key?: ValOrFunc<string | symbol>, onChange?: ((...args: any[]) => any)|string, processVal?: (newVal: any) => any, invProcessVal?: (val: any) => any}): PropertyDecorator { | |||||
| export function bindToValue({obj, key, processVal, invProcessVal, onChange, onChangeParams = true}: {obj?: ValOrFunc<any>, key?: ValOrFunc<string | symbol>, onChange?: ((...args: any[]) => any)|string, processVal?: (newVal: any) => any, invProcessVal?: (val: any) => any, onChangeParams?: boolean}): PropertyDecorator { | |||||
| const cPropKey = !!key | const cPropKey = !!key | ||||
| return (targetPrototype: any, propertyKey: string|symbol, descriptor?: TypedPropertyDescriptor<any>) => { | return (targetPrototype: any, propertyKey: string|symbol, descriptor?: TypedPropertyDescriptor<any>) => { | ||||
| const prop = { | const prop = { | ||||
| get() { | get() { | ||||
| const {t, p} = getTarget(this) | const {t, p} = getTarget(this) | ||||
| if (!t || typeof t !== 'object') return | |||||
| let res = t[p] | let res = t[p] | ||||
| if (invProcessVal) res = invProcessVal(res) | if (invProcessVal) res = invProcessVal(res) | ||||
| return res | return res | ||||
| }, | }, | ||||
| set(newVal: any) { | set(newVal: any) { | ||||
| const {t, p} = getTarget(this) | const {t, p} = getTarget(this) | ||||
| if (!t || typeof t !== 'object') return | |||||
| if (processVal) newVal = processVal(newVal) | if (processVal) newVal = processVal(newVal) | ||||
| safeSetProperty(t, p, newVal, true) | safeSetProperty(t, p, newVal, true) | ||||
| if (newVal === undefined) delete t[p] | if (newVal === undefined) delete t[p] | ||||
| let oc = onChange | let oc = onChange | ||||
| if (oc && (typeof oc === 'string' || typeof oc === 'symbol')) oc = this[oc] // todo just call it here directly | if (oc && (typeof oc === 'string' || typeof oc === 'symbol')) oc = this[oc] // todo just call it here directly | ||||
| if (oc && typeof oc === 'function') FnCaller.callFunction(oc, this, [p, newVal]) | |||||
| if (oc && typeof oc === 'function') FnCaller.callFunction(oc, this, onChangeParams ? [p, newVal] : []) | |||||
| }, | }, | ||||
| // configurable: true, | // configurable: true, | ||||
| // enumerable: true, | // enumerable: true, |