| @@ -1,12 +1,21 @@ | |||
| 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 {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() { | |||
| viewer.addPluginSync(LoadingScreenPlugin) | |||
| viewer.addPluginSync(AssetExporterPlugin) | |||
| viewer.addPluginSync(GLTFDracoExportPlugin) | |||
| // Note: see asset-exporter-plugin example as well | |||
| @@ -15,16 +15,32 @@ | |||
| } | |||
| :root { | |||
| --primary-color: #58a6ff; | |||
| //--primary-color: #58a6ff; | |||
| --primary-color: #6d7a8c; | |||
| //--primary-color: #ccdef5; | |||
| //--primary-color: #ec630a; | |||
| --secondary-color: #ec630a; | |||
| //--background-color: #1a1a1c; | |||
| --background-color: #1B1B1F; | |||
| --background-color: #E7EFF8; | |||
| --background-color-search: #dae1ea; | |||
| --background-color-search-hover: #c3d4e7; | |||
| //--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 { | |||
| @@ -43,6 +59,7 @@ | |||
| font-size: 1.75rem; | |||
| padding: 0.5rem; | |||
| color: var(--text-color); | |||
| cursor: pointer; | |||
| } | |||
| .hamburger:hover { | |||
| @@ -50,7 +67,7 @@ | |||
| } | |||
| .sidebar { | |||
| max-width: min(320px, 30%); | |||
| width: min(320px, 30%); | |||
| height: 100%; | |||
| background: var(--background-color); | |||
| color: var(--text-color); | |||
| @@ -61,10 +78,11 @@ | |||
| flex-direction: column; | |||
| gap: 0.5rem; | |||
| position: relative; | |||
| transition: width 0s ease-in-out; | |||
| } | |||
| .sidebar h1 { | |||
| color: #ddd; | |||
| color: var(--text-color); | |||
| font-size: 1.5rem; | |||
| margin: 0 3rem 1rem 0; | |||
| font-weight: normal; | |||
| @@ -72,7 +90,10 @@ | |||
| } | |||
| .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; | |||
| } | |||
| @@ -83,7 +104,7 @@ | |||
| .sidebar h2 { | |||
| font-size: 1.2rem; | |||
| margin: 0 0 0.5rem 0.25rem; | |||
| font-weight: normal; | |||
| font-weight: 500; | |||
| padding: 0; | |||
| } | |||
| @@ -102,7 +123,13 @@ | |||
| color: var(--primary-color); | |||
| text-underline-offset: 0.25rem; | |||
| 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 { | |||
| @@ -112,7 +139,8 @@ | |||
| .sidebar ul li a.selected { | |||
| color: var(--text-color-hover); | |||
| font-weight: bold; | |||
| text-decoration: underline; | |||
| background-size: 100% 0.1em; | |||
| //text-decoration: underline; | |||
| } | |||
| .iframe-container { | |||
| @@ -141,6 +169,8 @@ | |||
| .closed.sidebar{ | |||
| padding: 1.75rem; | |||
| width: 3.3rem; | |||
| transition: width 0.3s ease-in-out; | |||
| } | |||
| .closed:before{ | |||
| content: attr(data-selected-example); | |||
| @@ -156,6 +186,8 @@ | |||
| line-height: 3.25rem; | |||
| writing-mode: vertical-lr; | |||
| text-orientation: upright; | |||
| text-transform: uppercase; | |||
| font-family: "Source Code Pro", Menlo, Courier, monospace; | |||
| } | |||
| .closed > .search-bar { | |||
| @@ -172,29 +204,29 @@ | |||
| box-sizing: border-box; | |||
| width: 100%; | |||
| padding: 0.5rem; | |||
| color: var(--text-color); | |||
| background: #1e1e20; | |||
| color: var(--text-color-accent); | |||
| background: var(--background-color-search); | |||
| border: none; | |||
| border-radius: 4px; | |||
| transition: all 0.3s ease-in-out; | |||
| } | |||
| .search-bar input:hover { | |||
| background: #2a2a2c; | |||
| background: var(--background-color-search-hover); | |||
| } | |||
| .search-bar input:focus { | |||
| background: #2a2a2c; | |||
| background: var(--background-color-search-hover); | |||
| color: var(--text-color-hover); | |||
| outline: none; | |||
| } | |||
| .search-bar input::placeholder { | |||
| color: #888; | |||
| color: var(--primary-color); | |||
| } | |||
| .search-icon { | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 10px; | |||
| transform: translateY(-50%); | |||
| color: #888; | |||
| color: var(--primary-color); | |||
| width: 15px; | |||
| height: 15px; | |||
| } | |||
| @@ -208,7 +240,7 @@ | |||
| flex-direction: column; | |||
| } | |||
| .sidebar{ | |||
| max-width: 100%; | |||
| width: 100%; | |||
| height: auto; | |||
| max-height: 50%; | |||
| padding: 1rem; | |||
| @@ -125,7 +125,7 @@ async function init() { | |||
| BlendLoadPlugin, | |||
| HierarchyUiPlugin, | |||
| GeometryGeneratorPlugin, | |||
| Object3DWidgetsPlugin, | |||
| new Object3DWidgetsPlugin(false), | |||
| Object3DGeneratorPlugin, | |||
| GaussianSplattingPlugin, | |||
| ContactShadowGroundPlugin, | |||
| @@ -10,7 +10,7 @@ import { | |||
| } from 'three' | |||
| import type {IObject3D, IObjectProcessor} from '../IObject' | |||
| import {type ICamera} from '../ICamera' | |||
| import {Box3B} from '../../three' | |||
| import {bindToValue, Box3B} from '../../three' | |||
| import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers' | |||
| import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' | |||
| import {ThreeSerialization} from '../../utils' | |||
| @@ -54,21 +54,36 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| @uiSlider('Background Intensity', [0, 10], 0.01) | |||
| backgroundIntensity = 1 | |||
| /** | |||
| * The default environment map used when rendering materials in the scene | |||
| */ | |||
| @uiImage('Environment') | |||
| @serialize() @onChange2(RootScene.prototype._onEnvironmentChange) | |||
| 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. | |||
| */ | |||
| @uiSlider('Environment Intensity', [0, 10], 0.01) | |||
| @serialize() @onChange3(RootScene.prototype.setDirty) | |||
| 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. | |||
| */ | |||
| @@ -44,7 +44,7 @@ export class Object3DWidgetsPlugin extends AViewerPluginSync<''> { | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| 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) { | |||
| viewer.scene.removeEventListener('addSceneObject', this._addSceneObject) | |||
| @@ -48,7 +48,10 @@ export class TonemapPlugin extends AScreenPassExtensionPlugin<''> { | |||
| ['TONEMAP_BACKGROUND']: '1', | |||
| } as const | |||
| @serialize() @uiToggle('Enabled') enabled = true | |||
| @serialize() | |||
| @onChange(TonemapPlugin.prototype.setDirty) | |||
| @uiToggle('Enabled') | |||
| enabled = true | |||
| @uiDropdown('Mode', ([ | |||
| ['Linear', LinearToneMapping], | |||
| @@ -128,10 +128,11 @@ export function matDefineBool(key?: string|symbol, customDefines?: any, thisMat | |||
| * @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 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 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 | |||
| return (targetPrototype: any, propertyKey: string|symbol, descriptor?: TypedPropertyDescriptor<any>) => { | |||
| @@ -144,18 +145,20 @@ export function bindToValue({obj, key, onChange, processVal, invProcessVal}: {ob | |||
| const prop = { | |||
| get() { | |||
| const {t, p} = getTarget(this) | |||
| if (!t || typeof t !== 'object') return | |||
| let res = t[p] | |||
| if (invProcessVal) res = invProcessVal(res) | |||
| return res | |||
| }, | |||
| set(newVal: any) { | |||
| const {t, p} = getTarget(this) | |||
| if (!t || typeof t !== 'object') return | |||
| if (processVal) newVal = processVal(newVal) | |||
| safeSetProperty(t, p, newVal, true) | |||
| if (newVal === undefined) delete t[p] | |||
| let oc = onChange | |||
| 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, | |||
| // enumerable: true, | |||