From 8523c63cd2140f542c63511ae1f8113aa43a9fdd Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 11 Jul 2024 15:57:15 +0200 Subject: [PATCH 1/3] feat(is): add is.num and tests --- src/utils/is.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/utils/is.ts | 4 ++++ 2 files changed, 39 insertions(+) diff --git a/src/utils/is.test.ts b/src/utils/is.test.ts index c60a14a0d..ba078f905 100644 --- a/src/utils/is.test.ts +++ b/src/utils/is.test.ts @@ -2,6 +2,41 @@ import { BufferGeometry, Fog, MeshBasicMaterial, MeshNormalMaterial, Object3D, P import * as is from './is' describe('is', () => { + describe('is.num(a: any)', () => { + describe('true', () => { + it('number', () => { + assert(is.num(0)) + assert(is.num(-1)) + assert(is.num(Math.PI)) + assert(is.num(Number.POSITIVE_INFINITY)) + assert(is.num(Number.NEGATIVE_INFINITY)) + assert(is.num(42)) + assert(is.num(0b1111)) + assert(is.num(0o17)) + assert(is.num(0xF)) + }) + }) + describe('false', () => { + it('null', () => { + assert(!is.num(null)) + }) + it('undefined', () => { + assert(!is.num(undefined)) + }) + it('string', () => { + assert(!is.num('')) + assert(!is.num('1')) + }) + it('function', () => { + assert(!is.num(() => {})) + assert(!is.num(() => 1)) + }) + it('array', () => { + assert(!is.num([])) + assert(!is.num([1])) + }) + }) + }) describe('is.und(a: any)', () => { describe('true', () => { it('undefined', () => { diff --git a/src/utils/is.ts b/src/utils/is.ts index 67fa0f70c..baed2a358 100644 --- a/src/utils/is.ts +++ b/src/utils/is.ts @@ -9,6 +9,10 @@ export function arr(u: unknown) { return Array.isArray(u) } +export function num(u: unknown): u is number { + return typeof u === 'number' +} + export function str(u: unknown): u is string { return typeof u === 'string' } From ddb5dcb0e6f03b539377192db367af2fd17bfa8b Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 11 Jul 2024 15:58:14 +0200 Subject: [PATCH 2/3] feat(TresCanvas): add dpr prop --- src/components/TresCanvas.vue | 1 + src/composables/useRenderer/index.ts | 13 +-- src/utils/index.test.ts | 129 +++++++++++++++++++++++++++ src/utils/index.ts | 22 ++++- 4 files changed, 159 insertions(+), 6 deletions(-) diff --git a/src/components/TresCanvas.vue b/src/components/TresCanvas.vue index 1c88adf77..713467974 100644 --- a/src/components/TresCanvas.vue +++ b/src/components/TresCanvas.vue @@ -50,6 +50,7 @@ export interface TresCanvasProps outputColorSpace?: ColorSpace toneMappingExposure?: number renderMode?: 'always' | 'on-demand' | 'manual' + dpr?: number | [number, number] // required by useTresContextProvider camera?: TresCamera diff --git a/src/composables/useRenderer/index.ts b/src/composables/useRenderer/index.ts index da5590113..f80c468c0 100644 --- a/src/composables/useRenderer/index.ts +++ b/src/composables/useRenderer/index.ts @@ -14,7 +14,7 @@ import type { EmitEventFn, TresColor } from '../../types' import { normalizeColor } from '../../utils/normalize' import type { TresContext } from '../useTresContextProvider' -import { get, merge, set } from '../../utils' +import { get, merge, set, setPixelRatio } from '../../utils' // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts import { revision } from '../../core/revision' @@ -92,6 +92,11 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter preset?: MaybeRefOrGetter renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'> + /** + * A `number` sets the renderer's device pixel ratio. + * `[number, number]` clamp's the renderer's device pixel ratio. + */ + dpr?: MaybeRefOrGetter } export function useRenderer( @@ -151,10 +156,6 @@ export function useRenderer( const { pixelRatio } = useDevicePixelRatio() - watch(pixelRatio, () => { - renderer.value.setPixelRatio(pixelRatio.value) - }) - const { logError } = useLogger() const getThreeRendererDefaults = () => { @@ -199,6 +200,8 @@ export function useRenderer( merge(renderer.value, rendererPresets[rendererPreset]) } + setPixelRatio(renderer.value, pixelRatio.value, toValue(options.dpr)) + // Render mode if (renderMode === 'always') { diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 861e961c7..211e109c0 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -135,3 +135,132 @@ describe('resolve', () => { expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz') }) }) + +describe('setPixelRatio', () => { + const INITIAL_DPR = 1 + let dpr = INITIAL_DPR + const mockRenderer = { + setPixelRatio: (n: number) => { dpr = n }, + getPixelRatio: () => dpr, + } + const setPixelRatioSpy = vi.spyOn(mockRenderer, 'setPixelRatio') + + beforeEach(() => { + dpr = 1 + setPixelRatioSpy.mockClear() + }) + + describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number)', () => { + it('calls the renderer\'s setPixelRatio method with systemDpr', () => { + expect(setPixelRatioSpy).not.toBeCalled() + utils.setPixelRatio(mockRenderer, 2) + expect(setPixelRatioSpy).toBeCalledWith(2) + + utils.setPixelRatio(mockRenderer, 2.1) + expect(setPixelRatioSpy).toBeCalledWith(2.1) + + utils.setPixelRatio(mockRenderer, 1.44444) + expect(setPixelRatioSpy).toBeCalledWith(1.44444) + }) + it('does not set the renderer\'s pixelRatio if systemDpr === pixelRatio', () => { + utils.setPixelRatio(mockRenderer, 1) + expect(setPixelRatioSpy).not.toBeCalled() + + utils.setPixelRatio(mockRenderer, 2) + expect(setPixelRatioSpy).toBeCalledTimes(1) + + utils.setPixelRatio(mockRenderer, 2) + expect(setPixelRatioSpy).toBeCalledTimes(1) + + utils.setPixelRatio(mockRenderer, 1) + expect(setPixelRatioSpy).toBeCalledTimes(2) + + utils.setPixelRatio(mockRenderer, 1) + expect(setPixelRatioSpy).toBeCalledTimes(2) + }) + it('does not throw if passed a "renderer" without a `setPixelRatio` method', () => { + const mockSVGRenderer = {} + expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow() + }) + it('calls `setPixelRatio` even if passed a "renderer" without a `getPixelRatio` method', () => { + const mockSVGRenderer = { setPixelRatio: () => {} } + const setPixelRatioSpy = vi.spyOn(mockSVGRenderer, 'setPixelRatio') + expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow() + expect(setPixelRatioSpy).toBeCalledWith(2) + expect(setPixelRatioSpy).toBeCalledTimes(1) + + utils.setPixelRatio(mockSVGRenderer, 1.99) + expect(setPixelRatioSpy).toBeCalledWith(1.99) + expect(setPixelRatioSpy).toBeCalledTimes(2) + + utils.setPixelRatio(mockSVGRenderer, 2.1) + expect(setPixelRatioSpy).toBeCalledWith(2.1) + expect(setPixelRatioSpy).toBeCalledTimes(3) + }) + }) + + describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: number)', () => { + it('calls the renderer\'s setPixelRatio method with userDpr', () => { + expect(setPixelRatioSpy).not.toBeCalled() + utils.setPixelRatio(mockRenderer, 2, 100) + expect(setPixelRatioSpy).toBeCalledWith(100) + }) + it('does not call the renderer\'s setPixelRatio method if current dpr === new dpr', () => { + expect(setPixelRatioSpy).not.toBeCalled() + utils.setPixelRatio(mockRenderer, 2, 1) + expect(setPixelRatioSpy).not.toBeCalledWith() + + utils.setPixelRatio(mockRenderer, 3, 1.4) + expect(setPixelRatioSpy).toBeCalledTimes(1) + expect(setPixelRatioSpy).toBeCalledWith(1.4) + + utils.setPixelRatio(mockRenderer, 3, 1.4) + expect(setPixelRatioSpy).toBeCalledTimes(1) + expect(setPixelRatioSpy).toBeCalledWith(1.4) + + utils.setPixelRatio(mockRenderer, 2, 1.4) + expect(setPixelRatioSpy).toBeCalledTimes(1) + expect(setPixelRatioSpy).toBeCalledWith(1.4) + + utils.setPixelRatio(mockRenderer, 42, 0.1) + expect(setPixelRatioSpy).toBeCalledTimes(2) + expect(setPixelRatioSpy).toBeCalledWith(0.1) + + utils.setPixelRatio(mockRenderer, 4, 0.1) + expect(setPixelRatioSpy).toBeCalledTimes(2) + expect(setPixelRatioSpy).toBeCalledWith(0.1) + }) + }) + + describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: [number, number])', () => { + it('clamps systemDpr to userDpr', () => { + utils.setPixelRatio(mockRenderer, 2, [0, 4]) + expect(setPixelRatioSpy).toBeCalledTimes(1) + expect(setPixelRatioSpy).toBeCalledWith(2) + + utils.setPixelRatio(mockRenderer, 2, [3, 4]) + expect(setPixelRatioSpy).toBeCalledTimes(2) + expect(setPixelRatioSpy).toBeCalledWith(3) + + utils.setPixelRatio(mockRenderer, 5, [3, 4]) + expect(setPixelRatioSpy).toBeCalledTimes(3) + expect(setPixelRatioSpy).toBeCalledWith(4) + + utils.setPixelRatio(mockRenderer, 100, [3, 4]) + expect(setPixelRatioSpy).toBeCalledTimes(3) + expect(setPixelRatioSpy).toBeCalledWith(4) + + utils.setPixelRatio(mockRenderer, 100, [3.5, 4]) + expect(setPixelRatioSpy).toBeCalledTimes(3) + expect(setPixelRatioSpy).toBeCalledWith(4) + + utils.setPixelRatio(mockRenderer, 100, [3, 6.1]) + expect(setPixelRatioSpy).toBeCalledTimes(4) + expect(setPixelRatioSpy).toBeCalledWith(6.1) + + utils.setPixelRatio(mockRenderer, 1, [2.99, 6.1]) + expect(setPixelRatioSpy).toBeCalledTimes(5) + expect(setPixelRatioSpy).toBeCalledWith(2.99) + }) + }) +}) diff --git a/src/utils/index.ts b/src/utils/index.ts index bde0fbd4e..8748c1278 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ import type { Material, Mesh, Object3D, Texture } from 'three' -import { DoubleSide, MeshBasicMaterial, Scene, Vector3 } from 'three' +import { DoubleSide, MathUtils, MeshBasicMaterial, Scene, Vector3 } from 'three' import type { AttachType, LocalState, TresInstance, TresObject } from 'src/types' import { HightlightMesh } from '../devtools/highlight' import type { TresContext } from '../composables/useTresContextProvider' @@ -445,3 +445,23 @@ export function noop(fn: string): any { // eslint-disable-next-line no-unused-expressions fn } + +export function setPixelRatio(renderer: { setPixelRatio?: (dpr: number) => void, getPixelRatio?: () => number }, systemDpr: number, userDpr?: number | [number, number]) { + // NOTE: Optional `setPixelRatio` allows this function to accept + // THREE renderers like SVGRenderer. + if (!is.fun(renderer.setPixelRatio)) { return } + + let newDpr = 0 + + if (is.arr(userDpr) && userDpr.length >= 2) { + const [min, max] = userDpr + newDpr = MathUtils.clamp(systemDpr, min, max) + } + else if (is.num(userDpr)) { newDpr = userDpr } + else { newDpr = systemDpr } + + // NOTE: Don't call `setPixelRatio` unless both: + // - the dpr value has changed + // - the renderer has `setPixelRatio`; this check allows us to pass any THREE renderer + if (newDpr !== renderer.getPixelRatio?.()) { renderer.setPixelRatio(newDpr) } +} From 0de058dc14866bc2d0c4603dac0f0319efe8c0d6 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 11 Jul 2024 15:58:42 +0200 Subject: [PATCH 3/3] docs: add dpr playground demo --- .../pages/advanced/devicePixelRatio/index.vue | 62 +++++++++++++++++++ playground/src/router/routes/advanced.ts | 5 ++ 2 files changed, 67 insertions(+) create mode 100644 playground/src/pages/advanced/devicePixelRatio/index.vue diff --git a/playground/src/pages/advanced/devicePixelRatio/index.vue b/playground/src/pages/advanced/devicePixelRatio/index.vue new file mode 100644 index 000000000..ed2417d38 --- /dev/null +++ b/playground/src/pages/advanced/devicePixelRatio/index.vue @@ -0,0 +1,62 @@ + + + diff --git a/playground/src/router/routes/advanced.ts b/playground/src/router/routes/advanced.ts index 86a96f5ae..2210a1b61 100644 --- a/playground/src/router/routes/advanced.ts +++ b/playground/src/router/routes/advanced.ts @@ -29,4 +29,9 @@ export const advancedRoutes = [ name: 'Material array', component: () => import('../../pages/advanced/materialArray/index.vue'), }, + { + path: '/advanced/device-pixel-ratio', + name: 'Device Pixel Ratio', + component: () => import('../../pages/advanced/devicePixelRatio/index.vue'), + }, ]