From 2d6f1dbd0ef6f485fca5ee432c5a4f21dd433d63 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 17 Dec 2024 19:01:24 +0100 Subject: [PATCH 1/4] feat(Bounds): add component, demo, docs --- docs/.vitepress/config.ts | 1 + .../theme/components/BoundsDemo.vue | 32 ++ docs/component-list/components.ts | 1 + docs/guide/staging/bounds.md | 68 +++ docs/guide/staging/randomized-lights.md | 35 ++ .../vue/src/pages/staging/BoundsDemo.vue | 145 ++++++ playground/vue/src/router/routes/staging.ts | 5 + src/core/staging/Bounds/Bounds.ts | 437 ++++++++++++++++++ src/core/staging/Bounds/component.vue | 109 +++++ src/core/staging/index.ts | 2 + 10 files changed, 835 insertions(+) create mode 100644 docs/.vitepress/theme/components/BoundsDemo.vue create mode 100644 docs/guide/staging/bounds.md create mode 100644 docs/guide/staging/randomized-lights.md create mode 100644 playground/vue/src/pages/staging/BoundsDemo.vue create mode 100644 src/core/staging/Bounds/Bounds.ts create mode 100644 src/core/staging/Bounds/component.vue diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bc5c98df..8fb15de3 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -166,6 +166,7 @@ export default defineConfig({ { text: 'Align', link: '/guide/staging/align' }, { text: 'SoftShadows', link: '/guide/staging/soft-shadows' }, { text: 'Grid', link: '/guide/staging/grid' }, + { text: 'Bounds', link: '/guide/staging/bounds' }, ], }, { diff --git a/docs/.vitepress/theme/components/BoundsDemo.vue b/docs/.vitepress/theme/components/BoundsDemo.vue new file mode 100644 index 00000000..4c95a476 --- /dev/null +++ b/docs/.vitepress/theme/components/BoundsDemo.vue @@ -0,0 +1,32 @@ + + + diff --git a/docs/component-list/components.ts b/docs/component-list/components.ts index 71253f3c..b471b111 100644 --- a/docs/component-list/components.ts +++ b/docs/component-list/components.ts @@ -117,6 +117,7 @@ export default [ { text: 'Align', link: '/guide/staging/align' }, { text: 'SoftShadows', link: '/guide/staging/soft-shadows' }, { text: 'Grid', link: '/guide/staging/grid' }, + { text: 'Bounds', link: '/guide/staging/bounds' }, ], }, { diff --git a/docs/guide/staging/bounds.md b/docs/guide/staging/bounds.md new file mode 100644 index 00000000..2fde68fb --- /dev/null +++ b/docs/guide/staging/bounds.md @@ -0,0 +1,68 @@ +# Bounds + + + + + +Calculates a boundary box and centers the camera accordingly. Its `lookAt` method accepts a target to look at imperatively e.g., after a click. + +::: info +If you are using other camera controls, be sure to make them the 'default'. +```vue + +``` +::: + +## Usage + +<<< @/.vitepress/theme/components/BoundsDemo.vue + +## Props + +| Name | Description | Default | +| :--- | :--- | ---- | +| `duration` | Duration of the `lookAt` animation in seconds | `1.0` | +| `offset` | Additional distance from the target when using `lookAt` with a `Box3` or `Object3D` | `0.2` | +| `useScreenSize` | Whether to re`lookAt` the last target when the screen is resized | `false` | +| `useMounted` | Whether to `lookAt` the `Bounds` object when the component is mounts | `false` | +| `clip` | Whether to adjust the camera's `near` and `far` settings when using `lookAt` | `false` | +| `easing` | Animation's easing function. `t` and the returned value should be in the interval `[0, 1]` | Cubic ease out | + +## `lookAt` + +`` `lookAt` points the camera at its first argument: an `Object3D`, `Box3` or `Vector3`. + +``` + /** + * Calculates a boundary box around an `Object3D` and centers the camera accordingly. + */ + lookAt(object: Object3D): void + /** + * Calculates a boundary box around an `Object3D` and centers the camera accordingly and animates the camera's `up` vector. + */ + lookAt(object: Object3D, up: VectorFlexibleParams): void + /** + * Centers the camera's viewport on a `Box3`. + */ + lookAt(box3: Box3): void + /** + * Centers the camera's viewport on a `Box3` and animates the camera's `up` vector. + */ + lookAt(box3: Box3, up: VectorFlexibleParams): void + /** + * Look at a `Vector3`. + */ + lookAt(target: VectorFlexibleParams): void + /** + * Look at a `Vector3`, if provided. Move the camera to `position`. + */ + lookAt(target: VectorFlexibleParams | undefined | null, position: VectorFlexibleParams): void + /** + * Look at a `Vector3`, if provided. Move the camera to `position` and animate the camera's `up` vector. + */ + lookAt(target: VectorFlexibleParams | undefined | null, position: VectorFlexibleParams, up: VectorFlexibleParams): void + /** + * Rerun `lookAt` using the prior arguments. If `lookAt` has never been called, uses the `Bounds` object. + */ + lookAt(): void +``` diff --git a/docs/guide/staging/randomized-lights.md b/docs/guide/staging/randomized-lights.md new file mode 100644 index 00000000..7cf5fe43 --- /dev/null +++ b/docs/guide/staging/randomized-lights.md @@ -0,0 +1,35 @@ +# RandomizedLights + +`` internally creates multiple lights and jiggles them. You would normally add it as a child of ``. + +It is based on this [Drei component](http://drei.docs.pmnd.rs/staging/randomized-light). + +## Usage + +```vue + +``` + +## Props + +| Prop | Description | Default | +| - | - | - | +| `count` | Number of lights | `8`| +| `radius` | Radius of the jiggle, higher values make softer light | `1` | +| `intensity` | Light intensity | `Math.PI` | +| `ambient` | "Ambient occlusion" to directional light ratio, lower values mean less AO | `0.5` | +| `castShadow` | If the lights cast shadows | `true` | +| `bias` | Default shadow bias | `0` | +| `mapSize` | Size of the lights' shadow map | `512` | +| `size` | Size of the lights' shadow camera frustum | `10` | +| `near` | Lights' shadow camera near value | `0.5` | +| `far` | Lights' shadow camera far value | `500` | +| `position` | Position | `[5, 5, -10]` | diff --git a/playground/vue/src/pages/staging/BoundsDemo.vue b/playground/vue/src/pages/staging/BoundsDemo.vue new file mode 100644 index 00000000..20f558f9 --- /dev/null +++ b/playground/vue/src/pages/staging/BoundsDemo.vue @@ -0,0 +1,145 @@ + + + diff --git a/playground/vue/src/router/routes/staging.ts b/playground/vue/src/router/routes/staging.ts index de21d597..76a34d9f 100644 --- a/playground/vue/src/router/routes/staging.ts +++ b/playground/vue/src/router/routes/staging.ts @@ -44,6 +44,11 @@ export const stagingRoutes = [ name: 'Ocean', component: () => import('../../pages/staging/OceanDemo.vue'), }, + { + path: '/staging/bounds', + name: 'Bounds', + component: () => import('../../pages/staging/BoundsDemo.vue'), + }, { path: '/staging/fit', name: 'Fit', diff --git a/src/core/staging/Bounds/Bounds.ts b/src/core/staging/Bounds/Bounds.ts new file mode 100644 index 00000000..17c22b75 --- /dev/null +++ b/src/core/staging/Bounds/Bounds.ts @@ -0,0 +1,437 @@ +import type { VectorFlexibleParams } from '@tresjs/core' +import { normalizeVectorFlexibleParam } from '@tresjs/core' +import type { Camera, OrthographicCamera, PerspectiveCamera } from 'three' +import { Box3, Matrix4, Object3D, Quaternion, Vector3 } from 'three' +import { clamp } from 'three/src/math/MathUtils' + +interface SizeReturn { + box: Box3 + size: Vector3 + center: Vector3 + distance: number +} + +export interface BoundsControlsProto { + update: () => void + target: Vector3 + maxDistance: number + addEventListener: (event: string, callback: (event: any) => void) => void + removeEventListener: (event: string, callback: (event: any) => void) => void +} + +interface StartT { + position: Vector3 + quaternion: Quaternion + zoom: number +} + +interface GoalT { + position: Vector3 | undefined + quaternion: Quaternion | undefined + zoom: number | undefined + up: Vector3 | undefined + lookAt: Vector3 | undefined + box: Box3 | undefined + object: Box3 | Object3D | undefined +} + +export interface OnFitCallbackArg { + position: Vector3 + quaternion: Quaternion + zoom: number | undefined + up: Vector3 | undefined + lookAt: Vector3 + box: Box3 + object: Box3 | Object3D | undefined +} + +type CachedFitArgs = + [ Vector3 | null, Vector3, Vector3 ] + | [ Vector3 | null, Vector3] + | [ Vector3 | null] + | [ Object3D ] + | [ Object3D, Vector3 ] + | [ Box3 ] + | [ Box3, Vector3 ] + +enum AnimationState { + NONE = 0, + START = 1, + ACTIVE = 2, +} + +const isOrthographicCamera = (def: Camera): def is OrthographicCamera => + def && (def as OrthographicCamera).isOrthographicCamera +const isPerspectiveCamera = (def: Camera): def is PerspectiveCamera => + def && (def as PerspectiveCamera).isPerspectiveCamera +const isBox3 = (def: any): def is Box3 => def && (def as Box3).isBox3 + +const easingFnDefault = (t: number) => { return 1 - Math.exp(-5 * t) + 0.007 * t } + +export class Bounds extends Object3D { + camera: Camera + offset = 0.2 + duration = 1 + clip = true + + private _start: StartT = { + position: new Vector3(), + quaternion: new Quaternion(), + zoom: 1, + } + + private _goal: GoalT = { + position: undefined, + quaternion: undefined, + zoom: undefined, + up: undefined, + lookAt: undefined, + box: undefined, + object: undefined, + } + + private animationState = AnimationState.NONE + private t = 0.0 + private _controls: BoundsControlsProto | null = null + private _controlsRemoveEventListener = () => {} + + // NOTE: Overloaded functions and TS `Parameters` does not work. + // moz-extension://b37b5993-6262-452a-b49a-4f9e44f44989/confirm-page.html?url=https%3A%2F%2Fgithub.com%2Fmicrosoft%2FTypeScript%2Fissues%2F29732&cookieStoreId=firefox-container-21¤tCookieStoreId=firefox-container-8 + private _cachedFitArgs: CachedFitArgs = [this] + + constructor(camera: Camera) { + super() + this.camera = camera + } + + dispose() { + this.controls = null + } + + onFitStart(_: OnFitCallbackArg) {} + onFitCancel(_: OnFitCallbackArg) {} + onFitEnd(_: OnFitCallbackArg) {} + easing = easingFnDefault + + get controls() { + return this._controls + } + + set controls(controls: BoundsControlsProto | null) { + this._controlsRemoveEventListener() + this._controlsRemoveEventListener = () => {} + + if (controls) { + this._controls = controls + // NOTE: Try to prevent drag hijacking + // Attach an event to listen to `controls` "start". + // It is triggered when active controls are interacted with and + // should cancel animations here. + // https://threejs.org/docs/#examples/en/controls/OrbitControls + const callback = () => { + if (controls && this._goal.lookAt && this.animationState !== AnimationState.NONE) { + const front = new Vector3().setFromMatrixColumn(this.camera.matrix, 2) + const d0 = this._start.position.distanceTo(controls.target) + const d1 = (this._goal.position || this._start.position).distanceTo(this._goal.lookAt) + const d = (1 - this.t) * d0 + this.t * d1 + + controls.target.copy(this.camera.position).addScaledVector(front, -d) + controls.update() + this._stop() + } + + this.animationState = AnimationState.NONE + } + + controls.addEventListener('start', callback) + + this._controlsRemoveEventListener = () => controls.removeEventListener('start', callback) + } + } + + private _stop() { + if (this._goal.position) { + this.onFitCancel(this._goal as OnFitCallbackArg) + } + _resetGoal(this._goal) + } + + /** + * Calculates a boundary box around an `Object3D` and centers the camera accordingly. + */ + lookAt(object: Object3D): void + /** + * Calculates a boundary box around an `Object3D` and centers the camera accordingly and animates the camera's `up` vector. + */ + lookAt(object: Object3D, up: VectorFlexibleParams): void + /** + * Centers the camera's viewport on a `Box3`. + */ + lookAt(box3: Box3): void + /** + * Centers the camera's viewport on a `Box3` and animates the camera's `up` vector. + */ + lookAt(box3: Box3, up: VectorFlexibleParams): void + /** + * Look at a `Vector3`. + */ + lookAt(target: VectorFlexibleParams): void + /** + * Look at a `Vector3`, if provided. Move the camera to `position`. + */ + lookAt(target: VectorFlexibleParams | undefined | null, position: VectorFlexibleParams): void + /** + * Look at a `Vector3`, if provided. Move the camera to `position` and animate the camera's `up` vector. + */ + lookAt(target: VectorFlexibleParams | undefined | null, position: VectorFlexibleParams, up: VectorFlexibleParams): void + /** + * Rerun `lookAt` using the prior arguments. If `lookAt` has never been called, uses the `Bounds` object. + */ + lookAt(): void + lookAt( + arg0?: Object3D | Box3 | VectorFlexibleParams | undefined | null, + arg1?: VectorFlexibleParams, + arg2?: VectorFlexibleParams, + ) { + // NOTE: Normalize args + const size = arguments.length + + let args: CachedFitArgs = this._cachedFitArgs + const v1 = arg1 ? new Vector3().fromArray(normalizeVectorFlexibleParam(arg1)) : new Vector3() + const v2 = arg2 ? new Vector3().fromArray(normalizeVectorFlexibleParam(arg2)) : new Vector3() + + if (size === 0) { + // NOTE: We didn't get any args, use prior args. + args = this._cachedFitArgs + } + else if (!arg0 && arg0 !== 0) { + // NOTE: `fit(lookAt=undefined | null)` + if (size === 1) { args = [null] } + // NOTE: `fit(lookAt=undefined | null, lookAt: VectorFlexibleParams)` + else if (size === 2) { args = [null, v1] } + // NOTE: `fit(lookAt=undefined | null, lookAt: VectorFlexibleParams, up: VectorFlexibleParams)` + else if (size === 3) { args = [null, v1, v2] } + } + else if (typeof arg0 === 'number' || (arg0 as Vector3).isVector3 || Array.isArray(arg0)) { + const v0 = new Vector3().fromArray(normalizeVectorFlexibleParam(arg0 as VectorFlexibleParams)) + // NOTE: `fit(position: VectorFlexibleParams)` + if (size === 1) { args = [v0] } + // NOTE: `fit(position: VectorFlexibleParams, lookAt: VectorFlexibleParams)` + else if (size === 2) { args = [v0, v1] } + // NOTE: `fit(position: VectorFlexibleParams, lookAt: VectorFlexibleParams, up: VectorFlexibleParams)` + else if (size === 3) { args = [v0, v1, v2] } + } + else if ((arg0 as Box3).isBox3) { + // NOTE: `fit(box3: Box3)` + if (size === 1) { args = [arg0 as Box3] } + // NOTE: `fit(box3: Box3, up)` + else { args = [arg0 as Box3, arg1 as Vector3] } + } + else if ((arg0 as Object3D).isObject3D) { + // NOTE: `fit(object: Object3D)` + if (size === 1) { args = [arg0 as Object3D] } + // NOTE: `fit(object: Object3D, up)` + else { args = [arg0 as Object3D, arg1 as Vector3] } + } + + // NOTE: End normalization. + + this._cachedFitArgs = args + + this._stop() + _resetGoal(this._goal) + + if (args.length > 0 && (args[0] === null || args[0] === undefined || (args[0] as Vector3).isVector3)) { + // NOTE: The user sent specific numeric values, not an object. + const [lookAt, position, up] = args + this._start.position.copy(this.camera.position) + this._start.quaternion.copy(this.camera.quaternion) + isOrthographicCamera(this.camera) && (this._start.zoom = (this.camera as OrthographicCamera).zoom) + + if (position) { + this._goal.position = Array.isArray(position) ? new Vector3(...position) : (position as Vector3).clone() + } + else { + this._goal.position = this.camera.position + } + + if (lookAt) { + this._goal.lookAt = Array.isArray(lookAt) ? new Vector3(...lookAt) : (lookAt as Vector3).clone() + } + else { + this._goal.lookAt = new Vector3(0, 0, 1).applyQuaternion(this.camera.quaternion) + } + + if (up) { + this._goal.up = Array.isArray(up) ? new Vector3(...up) : up.clone() + } + + const mCamRot = new Matrix4().lookAt( + this._goal.position || this.camera.position, + this._goal.lookAt, + this._goal.up ?? this.camera.up, + ) + this._goal.quaternion = new Quaternion().setFromRotationMatrix(mCamRot) + } + else { + const box3OrObject = args[0] as Box3 | Object3D + const { center, distance, box } = _getSize(box3OrObject, this.camera, this.offset) + + this._start.position.copy(this.camera.position) + this._start.quaternion.copy(this.camera.quaternion) + isOrthographicCamera(this.camera) && (this._start.zoom = (this.camera as OrthographicCamera).zoom) + + const direction = this.camera.position.clone().sub(center).normalize() + this._goal.object = box3OrObject + this._goal.box = box + this._goal.position = center.clone().addScaledVector(direction, distance) + this._goal.lookAt = center.clone() + const mCamRot = new Matrix4().lookAt(this._goal.position, this._goal.lookAt, this.camera.up) + this._goal.quaternion = new Quaternion().setFromRotationMatrix(mCamRot) + + if (isOrthographicCamera(this.camera)) { + let maxHeight = 0 + let maxWidth = 0 + const vertices = [ + new Vector3(box.min.x, box.min.y, box.min.z), + new Vector3(box.min.x, box.max.y, box.min.z), + new Vector3(box.min.x, box.min.y, box.max.z), + new Vector3(box.min.x, box.max.y, box.max.z), + new Vector3(box.max.x, box.max.y, box.max.z), + new Vector3(box.max.x, box.max.y, box.min.z), + new Vector3(box.max.x, box.min.y, box.max.z), + new Vector3(box.max.x, box.min.y, box.min.z), + ] + + // NOTE: Transform the center and each corner to camera space + const goal = this._goal + const pos = goal.position || this.camera.position + const target = goal.lookAt || this._controls?.target + const up = goal.up || this.camera.up + const mCamWInv = target + ? new Matrix4().lookAt(pos, target, up).setPosition(pos).invert() + : this.camera.matrixWorldInverse + for (const v of vertices) { + v.applyMatrix4(mCamWInv) + maxHeight = Math.max(maxHeight, Math.abs(v.y)) + maxWidth = Math.max(maxWidth, Math.abs(v.x)) + } + maxHeight *= 2 + maxWidth *= 2 + const zoomForHeight = (this.camera.top - this.camera.bottom) / maxHeight + const zoomForWidth = (this.camera.right - this.camera.left) / maxWidth + + goal.zoom = Math.min(zoomForHeight, zoomForWidth) / (1 + this.offset) + // NOTE: Fix possible division by 0. + if (Number.isNaN(goal.zoom)) { goal.zoom = 0 } + } + + if (this.clip) { + if (isPerspectiveCamera(this.camera)) { + this.camera.near = Math.abs(distance) / 100 + this.camera.far = Math.abs(distance) * 100 + this.camera.updateProjectionMatrix() + } + + if (this._controls) { + this._controls.maxDistance = Math.abs(distance) * 100 + this._controls.update() + } + } + } + + this.animationState = AnimationState.START + this.t = 0 + + this.onFitStart && this.onFitStart(this._goal as OnFitCallbackArg) + } + + animate(delta: number) { + if (this.animationState === AnimationState.NONE) { + return false + } + + if (this.animationState === AnimationState.START) { + this.animationState = AnimationState.ACTIVE + } + else if (this.animationState === AnimationState.ACTIVE) { + this.t += delta / this.duration + this.t = clamp(this.t, 0, 1) + + if (this.t >= 1) { + this._goal.position && this.camera.position.copy(this._goal.position) + this._goal.quaternion && this.camera.quaternion.copy(this._goal.quaternion) + this._goal.up && this.camera.up.copy(this._goal.up) + this._goal.zoom && isOrthographicCamera(this.camera) && (this.camera.zoom = this._goal.zoom) + + this.camera.updateMatrixWorld() + if (isPerspectiveCamera(this.camera)) { + this.camera.updateProjectionMatrix() + } + + if (this._controls && this._goal.lookAt) { + this._controls.target.copy(this._goal.lookAt) + this._controls.update() + } + + this.animationState = AnimationState.NONE + this.onFitEnd && this.onFitEnd(this._goal as OnFitCallbackArg) + _resetGoal(this._goal) + } + else { + const k = this.easing && this.easing(this.t) + + this._goal.position && this.camera.position.lerpVectors(this._start.position, this._goal.position, k) + this._goal.quaternion && this.camera.quaternion.slerpQuaternions(this._start.quaternion, this._goal.quaternion, k) + this._goal.up && this.camera.up.set(0, 1, 0).applyQuaternion(this.camera.quaternion) + this._goal.zoom + && isOrthographicCamera(this.camera) + && (this.camera.zoom = (1 - k) * this._start.zoom + k * this._goal.zoom) + + this.camera.updateMatrixWorld() + if (isPerspectiveCamera(this.camera)) { + this.camera.updateProjectionMatrix() + } + } + } + + return true + } +} + +function _getSize(box3OrObject: Box3 | Object3D, camera: Camera, offset = 0): SizeReturn { + const box = new Box3() + if (isBox3(box3OrObject)) { + box.copy(box3OrObject) + } + else { + box3OrObject.updateWorldMatrix(true, true) + box.setFromObject(box3OrObject) + } + + if (box.isEmpty()) { + const max = camera.position.length() || 10 + box.setFromCenterAndSize(new Vector3(), new Vector3(max, max, max)) + } + + const boxSize = box.getSize(new Vector3()) + const center = box.getCenter(new Vector3()) + const maxSize = Math.max(boxSize.x, boxSize.y, boxSize.z) + const fitHeightDistance = isOrthographicCamera(camera) + ? maxSize * 4 + : maxSize / (2 * Math.atan((Math.PI * (camera as PerspectiveCamera).fov) / 360)) + const fitWidthDistance = isOrthographicCamera(camera) ? maxSize * 4 : fitHeightDistance / (camera as PerspectiveCamera).aspect + const distance = (1 + offset) * Math.max(fitHeightDistance, fitWidthDistance) + + return { box, size: boxSize, center, distance } +} + +function _resetGoal(goal: GoalT) { + goal.position = undefined + goal.quaternion = undefined + goal.zoom = undefined + goal.up = undefined + goal.lookAt = undefined + goal.box = undefined + goal.object = undefined +} diff --git a/src/core/staging/Bounds/component.vue b/src/core/staging/Bounds/component.vue new file mode 100644 index 00000000..940c2a59 --- /dev/null +++ b/src/core/staging/Bounds/component.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/core/staging/index.ts b/src/core/staging/index.ts index fc6ea028..6cd1d32e 100644 --- a/src/core/staging/index.ts +++ b/src/core/staging/index.ts @@ -1,5 +1,6 @@ import Align from './Align.vue' import Backdrop from './Backdrop.vue' +import Bounds from './Bounds/component.vue' import ContactShadows from './ContactShadows.vue' import Fit from './Fit.vue' import Grid from './Grid.vue' @@ -16,6 +17,7 @@ import Lightformer from './useEnvironment/lightformer/index.vue' export { Align, Backdrop, + Bounds, ContactShadows, Environment, Fit, From 0db841a834f5d35e972ac41ff8fd197547291988 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 17 Dec 2024 19:44:25 +0100 Subject: [PATCH 2/4] refactor(Bounds): useScreenSize -> useResize --- docs/guide/staging/bounds.md | 2 +- playground/vue/src/pages/staging/BoundsDemo.vue | 4 ++-- src/core/staging/Bounds/component.vue | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guide/staging/bounds.md b/docs/guide/staging/bounds.md index 2fde68fb..ecaeb559 100644 --- a/docs/guide/staging/bounds.md +++ b/docs/guide/staging/bounds.md @@ -23,7 +23,7 @@ If you are using other camera controls, be sure to make them the 'default'. | :--- | :--- | ---- | | `duration` | Duration of the `lookAt` animation in seconds | `1.0` | | `offset` | Additional distance from the target when using `lookAt` with a `Box3` or `Object3D` | `0.2` | -| `useScreenSize` | Whether to re`lookAt` the last target when the screen is resized | `false` | +| `useResize` | Whether to re`lookAt` the last target when the screen is resized | `false` | | `useMounted` | Whether to `lookAt` the `Bounds` object when the component is mounts | `false` | | `clip` | Whether to adjust the camera's `near` and `far` settings when using `lookAt` | `false` | | `easing` | Animation's easing function. `t` and the returned value should be in the interval `[0, 1]` | Cubic ease out | diff --git a/playground/vue/src/pages/staging/BoundsDemo.vue b/playground/vue/src/pages/staging/BoundsDemo.vue index 20f558f9..5a1bf3bc 100644 --- a/playground/vue/src/pages/staging/BoundsDemo.vue +++ b/playground/vue/src/pages/staging/BoundsDemo.vue @@ -12,7 +12,7 @@ const c = useControls({ clip: false, useMounted: true, useOrthographic: false, - useScreenSize: false, + useResize: false, isLinear: false, enabled: true, lookAtX: { value: 0, min: -20, max: 20, step: 0.10 }, @@ -122,7 +122,7 @@ const onEndFn = (v: any) => { endArg.value = v.object?.uuid; endCount.value++ } :clip="c.clip.value.value" :duration="c.duration.value.value" :offset="c.offset.value.value" - :use-screen-size="c.useScreenSize.value.value" + :use-resize="c.useResize.value.value" :use-mounted="c.useMounted.value.value" :easing="easingFn" @start="onStartFn" diff --git a/src/core/staging/Bounds/component.vue b/src/core/staging/Bounds/component.vue index 940c2a59..0cf82ec6 100644 --- a/src/core/staging/Bounds/component.vue +++ b/src/core/staging/Bounds/component.vue @@ -18,7 +18,7 @@ export interface BoundsProps { /** * Whether to re`lookAt` the last target when the screen is resized, false */ - useScreenSize?: boolean + useResize?: boolean /** * Whether to `lookAt` the `Bounds` object when the component is mounts, false */ @@ -36,7 +36,7 @@ export interface BoundsProps { const props = withDefaults(defineProps(), { duration: 1.0, offset: 0.2, - useScreenSize: false, + useResize: false, useMounted: false, clip: false, }) @@ -82,7 +82,7 @@ watch(shallowCam, () => { const onResize = useDebounceFn(refresh, 100) watch(() => [size.width.value, size.height.value], () => { - if (props.useScreenSize) { onResize() } + if (props.useResize) { onResize() } }) // NOTE: Tres core doesn't currently allow for most From 1b2169576dce4824ca6b946d0e321389bebe5ac6 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 20 Dec 2024 22:55:39 +0100 Subject: [PATCH 3/4] refactor(Bounds): rename variables, remove unneeded state --- src/core/staging/Bounds/Bounds.ts | 46 ++++++++++++--------------- src/core/staging/Bounds/component.vue | 20 ++++++------ 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/core/staging/Bounds/Bounds.ts b/src/core/staging/Bounds/Bounds.ts index 17c22b75..d753fc43 100644 --- a/src/core/staging/Bounds/Bounds.ts +++ b/src/core/staging/Bounds/Bounds.ts @@ -35,7 +35,7 @@ interface GoalT { object: Box3 | Object3D | undefined } -export interface OnFitCallbackArg { +export interface OnLookAtCallbackArg { position: Vector3 quaternion: Quaternion zoom: number | undefined @@ -56,7 +56,6 @@ type CachedFitArgs = enum AnimationState { NONE = 0, - START = 1, ACTIVE = 2, } @@ -90,8 +89,8 @@ export class Bounds extends Object3D { object: undefined, } - private animationState = AnimationState.NONE - private t = 0.0 + private _animationState = AnimationState.NONE + private _t = 0.0 private _controls: BoundsControlsProto | null = null private _controlsRemoveEventListener = () => {} @@ -108,9 +107,9 @@ export class Bounds extends Object3D { this.controls = null } - onFitStart(_: OnFitCallbackArg) {} - onFitCancel(_: OnFitCallbackArg) {} - onFitEnd(_: OnFitCallbackArg) {} + onStart(_: OnLookAtCallbackArg) {} + onCancel(_: OnLookAtCallbackArg) {} + onEnd(_: OnLookAtCallbackArg) {} easing = easingFnDefault get controls() { @@ -129,18 +128,18 @@ export class Bounds extends Object3D { // should cancel animations here. // https://threejs.org/docs/#examples/en/controls/OrbitControls const callback = () => { - if (controls && this._goal.lookAt && this.animationState !== AnimationState.NONE) { + if (controls && this._goal.lookAt && this._animationState !== AnimationState.NONE) { const front = new Vector3().setFromMatrixColumn(this.camera.matrix, 2) const d0 = this._start.position.distanceTo(controls.target) const d1 = (this._goal.position || this._start.position).distanceTo(this._goal.lookAt) - const d = (1 - this.t) * d0 + this.t * d1 + const d = (1 - this._t) * d0 + this._t * d1 controls.target.copy(this.camera.position).addScaledVector(front, -d) controls.update() this._stop() } - this.animationState = AnimationState.NONE + this._animationState = AnimationState.NONE } controls.addEventListener('start', callback) @@ -151,7 +150,7 @@ export class Bounds extends Object3D { private _stop() { if (this._goal.position) { - this.onFitCancel(this._goal as OnFitCallbackArg) + this.onCancel(this._goal as OnLookAtCallbackArg) } _resetGoal(this._goal) } @@ -340,25 +339,22 @@ export class Bounds extends Object3D { } } - this.animationState = AnimationState.START - this.t = 0 + this._t = 0 + this._animationState = AnimationState.ACTIVE - this.onFitStart && this.onFitStart(this._goal as OnFitCallbackArg) + this.onStart && this.onStart(this._goal as OnLookAtCallbackArg) } animate(delta: number) { - if (this.animationState === AnimationState.NONE) { + if (this._animationState === AnimationState.NONE) { return false } - if (this.animationState === AnimationState.START) { - this.animationState = AnimationState.ACTIVE - } - else if (this.animationState === AnimationState.ACTIVE) { - this.t += delta / this.duration - this.t = clamp(this.t, 0, 1) + if (this._animationState === AnimationState.ACTIVE) { + this._t += delta / this.duration + this._t = clamp(this._t, 0, 1) - if (this.t >= 1) { + if (this._t >= 1) { this._goal.position && this.camera.position.copy(this._goal.position) this._goal.quaternion && this.camera.quaternion.copy(this._goal.quaternion) this._goal.up && this.camera.up.copy(this._goal.up) @@ -374,12 +370,12 @@ export class Bounds extends Object3D { this._controls.update() } - this.animationState = AnimationState.NONE - this.onFitEnd && this.onFitEnd(this._goal as OnFitCallbackArg) + this._animationState = AnimationState.NONE + this.onEnd && this.onEnd(this._goal as OnLookAtCallbackArg) _resetGoal(this._goal) } else { - const k = this.easing && this.easing(this.t) + const k = this.easing && this.easing(this._t) this._goal.position && this.camera.position.lerpVectors(this._start.position, this._goal.position, k) this._goal.quaternion && this.camera.quaternion.slerpQuaternions(this._start.quaternion, this._goal.quaternion, k) diff --git a/src/core/staging/Bounds/component.vue b/src/core/staging/Bounds/component.vue index 0cf82ec6..900ce10f 100644 --- a/src/core/staging/Bounds/component.vue +++ b/src/core/staging/Bounds/component.vue @@ -1,6 +1,6 @@ From d5f3c218f3dba03d1d19246438bce085385fa325 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Thu, 2 Jan 2025 10:53:21 +0100 Subject: [PATCH 4/4] docs: fix material items merge issue --- docs/component-list/components.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/component-list/components.ts b/docs/component-list/components.ts index edf53c90..e92fdb11 100644 --- a/docs/component-list/components.ts +++ b/docs/component-list/components.ts @@ -71,6 +71,8 @@ export default [ { text: 'PointMaterial', link: '/guide/materials/point-material', + }, + { text: 'MeshDiscardMaterial', link: '/guide/materials/mesh-discard-material', },