mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-16 16:58:33 +00:00
Compare commits
1 Commits
codex/rum-
...
feat/load3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16c357f0a |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
@@ -833,6 +833,44 @@ describe('useLoad3d', () => {
|
||||
expect(composable.sceneConfig.value.backgroundImage).toBe('')
|
||||
expect(mockLoad3d.setBackgroundImage).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should reset a leftover panorama mode to tiled when uploading a new image', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue('uploaded-image.jpg')
|
||||
|
||||
const composable = useLoad3d(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
composable.sceneConfig.value.backgroundRenderMode = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await composable.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(composable.sceneConfig.value.backgroundRenderMode).toBe('tiled')
|
||||
await nextTick()
|
||||
expect(mockLoad3d.setBackgroundRenderMode).toHaveBeenLastCalledWith(
|
||||
'tiled'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not clear the background or touch render mode when the upload fails', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue(undefined)
|
||||
|
||||
const composable = useLoad3d(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
composable.sceneConfig.value.backgroundImage = 'existing.jpg'
|
||||
composable.sceneConfig.value.backgroundRenderMode = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await composable.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(composable.sceneConfig.value.backgroundImage).toBe('existing.jpg')
|
||||
expect(composable.sceneConfig.value.backgroundRenderMode).toBe('panorama')
|
||||
})
|
||||
})
|
||||
|
||||
describe('model export', () => {
|
||||
|
||||
@@ -770,6 +770,9 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
: '3d'
|
||||
|
||||
const uploadedPath = await Load3dUtils.uploadFile(file, subfolder)
|
||||
if (!uploadedPath) return
|
||||
|
||||
sceneConfig.value.backgroundRenderMode = 'tiled'
|
||||
sceneConfig.value.backgroundImage = uploadedPath
|
||||
await load3d?.setBackgroundImage(uploadedPath)
|
||||
}
|
||||
|
||||
@@ -560,6 +560,44 @@ describe('useLoad3dViewer', () => {
|
||||
expect(viewer.hasBackgroundImage.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset render mode to tiled when uploading a new image', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValueOnce(
|
||||
'uploaded-image.jpg'
|
||||
)
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d)
|
||||
|
||||
viewer.backgroundRenderMode.value = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(viewer.backgroundRenderMode.value).toBe('tiled')
|
||||
})
|
||||
|
||||
it('should not clear the background or touch render mode when the upload fails', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValueOnce(undefined)
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d)
|
||||
|
||||
viewer.backgroundImage.value = 'existing.jpg'
|
||||
viewer.backgroundRenderMode.value = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(viewer.backgroundImage.value).toBe('existing.jpg')
|
||||
expect(viewer.backgroundRenderMode.value).toBe('panorama')
|
||||
})
|
||||
|
||||
it('should handle upload errors', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockRejectedValueOnce(
|
||||
new Error('Upload failed')
|
||||
|
||||
@@ -752,6 +752,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
)
|
||||
|
||||
if (uploadPath) {
|
||||
backgroundRenderMode.value = 'tiled'
|
||||
backgroundImage.value = uploadPath
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
|
||||
@@ -353,7 +353,10 @@ describe('Load3d', () => {
|
||||
expect(sceneResize).toHaveBeenCalledWith(800, 400)
|
||||
})
|
||||
|
||||
it('renderMainScene applies the letterboxed viewport and feeds aspect to the camera', () => {
|
||||
function makeRenderMainSceneContext(
|
||||
backgroundType: 'color' | 'image' = 'color',
|
||||
activeCamera: THREE.Camera = ctx.cameraManager.activeCamera
|
||||
) {
|
||||
const setViewport = vi.fn()
|
||||
const setScissor = vi.fn()
|
||||
const setScissorTest = vi.fn()
|
||||
@@ -362,6 +365,10 @@ describe('Load3d', () => {
|
||||
const render = vi.fn()
|
||||
const updateAspectRatio = vi.fn()
|
||||
const renderBackground = vi.fn()
|
||||
const getCurrentBackgroundInfo = vi.fn(() => ({
|
||||
type: backgroundType,
|
||||
value: ''
|
||||
}))
|
||||
|
||||
const scene = {} as THREE.Scene
|
||||
|
||||
@@ -383,19 +390,133 @@ describe('Load3d', () => {
|
||||
targetHeight: 200,
|
||||
targetAspectRatio: 2,
|
||||
isViewerMode: false,
|
||||
cameraManager: { ...ctx.cameraManager, updateAspectRatio },
|
||||
sceneManager: { ...ctx.sceneManager, renderBackground, scene }
|
||||
cameraManager: {
|
||||
...ctx.cameraManager,
|
||||
updateAspectRatio,
|
||||
activeCamera
|
||||
},
|
||||
sceneManager: {
|
||||
...ctx.sceneManager,
|
||||
renderBackground,
|
||||
getCurrentBackgroundInfo,
|
||||
scene
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
setViewport,
|
||||
setScissor,
|
||||
setScissorTest,
|
||||
clear,
|
||||
render,
|
||||
updateAspectRatio,
|
||||
renderBackground,
|
||||
scene
|
||||
}
|
||||
}
|
||||
|
||||
it('renderMainScene renders the full canvas with an extrapolated view offset so the letterboxed rect is unchanged', () => {
|
||||
const r = makeRenderMainSceneContext()
|
||||
const camera = ctx.cameraManager.activeCamera as THREE.PerspectiveCamera
|
||||
|
||||
let viewAtRender: THREE.PerspectiveCamera['view'] = null
|
||||
r.render.mockImplementationOnce(() => {
|
||||
viewAtRender = camera.view ? { ...camera.view } : null
|
||||
})
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(setScissor).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(updateAspectRatio).toHaveBeenCalledWith(2)
|
||||
expect(setScissorTest).toHaveBeenCalledWith(true)
|
||||
expect(render).toHaveBeenCalledWith(scene, ctx.cameraManager.activeCamera)
|
||||
// Container 800x600, target aspect 2:1 → letterboxed rect 800x400 at y=100
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setScissorTest).toHaveBeenCalledWith(true)
|
||||
expect(r.clear).toHaveBeenCalledOnce()
|
||||
expect(r.updateAspectRatio).toHaveBeenCalledWith(2)
|
||||
expect(r.render).toHaveBeenNthCalledWith(1, r.scene, camera)
|
||||
|
||||
expect(viewAtRender).not.toBeNull()
|
||||
expect(viewAtRender!.enabled).toBe(true)
|
||||
expect(viewAtRender!.fullWidth).toBe(800)
|
||||
expect(viewAtRender!.fullHeight).toBe(400)
|
||||
expect(viewAtRender!.offsetX).toBeCloseTo(0)
|
||||
expect(viewAtRender!.offsetY).toBe(-100)
|
||||
expect(viewAtRender!.width).toBe(800)
|
||||
expect(viewAtRender!.height).toBe(600)
|
||||
|
||||
expect(camera.view?.enabled ?? false).toBe(false)
|
||||
})
|
||||
|
||||
it('renderMainScene dims the letterbox bars after rendering the scene', () => {
|
||||
const r = makeRenderMainSceneContext()
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.render).toHaveBeenCalledTimes(3)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(3, 0, 500, 800, 100)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(3, 0, 500, 800, 100)
|
||||
})
|
||||
|
||||
it('renderMainScene keeps a color background covering the whole canvas', () => {
|
||||
const r = makeRenderMainSceneContext('color')
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.renderBackground).toHaveBeenCalledWith()
|
||||
// No viewport narrowing before the bars are dimmed.
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
})
|
||||
|
||||
it('renderMainScene confines an image background to the letterboxed rect', () => {
|
||||
const r = makeRenderMainSceneContext('image')
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
// Viewport/scissor narrow to the letterbox rect for the background
|
||||
// pass, then restore to the full canvas for the scene pass.
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(3, 0, 0, 800, 600)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(3, 0, 0, 800, 600)
|
||||
|
||||
const backgroundOrder = r.renderBackground.mock.invocationCallOrder[0]
|
||||
expect(backgroundOrder).toBeGreaterThan(
|
||||
r.setViewport.mock.invocationCallOrder[1]
|
||||
)
|
||||
expect(backgroundOrder).toBeLessThan(
|
||||
r.setViewport.mock.invocationCallOrder[2]
|
||||
)
|
||||
})
|
||||
|
||||
it('renderMainScene applies the view offset to orthographic cameras too', () => {
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1)
|
||||
const r = makeRenderMainSceneContext('color', camera)
|
||||
|
||||
let viewEnabledAtRender = false
|
||||
r.render.mockImplementationOnce(() => {
|
||||
viewEnabledAtRender = camera.view?.enabled ?? false
|
||||
})
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.render).toHaveBeenNthCalledWith(1, r.scene, camera)
|
||||
expect(viewEnabledAtRender).toBe(true)
|
||||
expect(camera.view?.enabled ?? false).toBe(false)
|
||||
})
|
||||
|
||||
it('renderMainScene falls back to the letterboxed viewport for cameras without view-offset support', () => {
|
||||
const camera = new THREE.Camera()
|
||||
const r = makeRenderMainSceneContext('color', camera)
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.render).toHaveBeenCalledTimes(1)
|
||||
expect(r.render).toHaveBeenCalledWith(r.scene, camera)
|
||||
expect(r.renderBackground).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('setBackgroundImage updates background size with letterbox dimensions when a texture is loaded', async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ type CameraStub = {
|
||||
getCurrentCameraType: ReturnType<typeof vi.fn>
|
||||
handleResize: ReturnType<typeof vi.fn>
|
||||
updateAspectRatio: ReturnType<typeof vi.fn>
|
||||
dispose: ReturnType<typeof vi.fn>
|
||||
activeCamera: THREE.Camera
|
||||
}
|
||||
|
||||
@@ -48,6 +49,7 @@ function makeViewportInstance() {
|
||||
getCurrentCameraType: vi.fn(() => 'perspective' as const),
|
||||
handleResize: vi.fn(),
|
||||
updateAspectRatio: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
activeCamera: new THREE.PerspectiveCamera()
|
||||
}
|
||||
const sceneManager: SceneStub = {
|
||||
@@ -447,6 +449,82 @@ describe('Viewport3d', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('letterbox dimmer lifecycle', () => {
|
||||
type DimmerInternals = {
|
||||
dimLetterboxBars(
|
||||
bars: { x: number; y: number; width: number; height: number }[]
|
||||
): void
|
||||
disposeManagers(): void
|
||||
letterboxDimmer: {
|
||||
scene: THREE.Scene
|
||||
camera: THREE.OrthographicCamera
|
||||
geometry: THREE.PlaneGeometry
|
||||
material: THREE.MeshBasicMaterial
|
||||
} | null
|
||||
}
|
||||
|
||||
it('does not create the dim overlay when there are no letterbox bars', () => {
|
||||
const render = vi.fn()
|
||||
Object.assign(ctx.viewport, {
|
||||
view: {
|
||||
renderer: { setViewport: vi.fn(), setScissor: vi.fn(), render }
|
||||
}
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([])
|
||||
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(internals.letterboxDimmer ?? null).toBeNull()
|
||||
})
|
||||
|
||||
it('renders each bar with a lazily created dim overlay', () => {
|
||||
const setViewport = vi.fn()
|
||||
const setScissor = vi.fn()
|
||||
const render = vi.fn()
|
||||
Object.assign(ctx.viewport, {
|
||||
view: { renderer: { setViewport, setScissor, render } }
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([
|
||||
{ x: 0, y: 0, width: 100, height: 20 },
|
||||
{ x: 0, y: 80, width: 100, height: 20 }
|
||||
])
|
||||
|
||||
const dimmer = internals.letterboxDimmer
|
||||
expect(dimmer).not.toBeNull()
|
||||
expect(setViewport).toHaveBeenNthCalledWith(1, 0, 0, 100, 20)
|
||||
expect(setViewport).toHaveBeenNthCalledWith(2, 0, 80, 100, 20)
|
||||
expect(render).toHaveBeenCalledTimes(2)
|
||||
expect(render).toHaveBeenCalledWith(dimmer!.scene, dimmer!.camera)
|
||||
})
|
||||
|
||||
it('disposeManagers disposes the dim overlay resources', () => {
|
||||
Object.assign(ctx.viewport, {
|
||||
view: {
|
||||
renderer: {
|
||||
setViewport: vi.fn(),
|
||||
setScissor: vi.fn(),
|
||||
render: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([{ x: 0, y: 0, width: 100, height: 20 }])
|
||||
const dimmer = internals.letterboxDimmer!
|
||||
const geometryDispose = vi.spyOn(dimmer.geometry, 'dispose')
|
||||
const materialDispose = vi.spyOn(dimmer.material, 'dispose')
|
||||
|
||||
internals.disposeManagers()
|
||||
|
||||
expect(geometryDispose).toHaveBeenCalledOnce()
|
||||
expect(materialDispose).toHaveBeenCalledOnce()
|
||||
expect(internals.letterboxDimmer).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('start / remove lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -18,13 +18,52 @@ import type {
|
||||
import { attachContextMenuGuard } from './load3dContextMenuGuard'
|
||||
import type { RenderLoopHandle } from './load3dRenderLoop'
|
||||
import { startRenderLoop } from './load3dRenderLoop'
|
||||
import type { LetterboxNdc } from './load3dViewport'
|
||||
import type {
|
||||
LetterboxedViewport,
|
||||
LetterboxNdc,
|
||||
ViewportRect
|
||||
} from './load3dViewport'
|
||||
import {
|
||||
clientPointToLetterboxNdc,
|
||||
computeLetterboxBars,
|
||||
computeLetterboxedViewport,
|
||||
isLoad3dActive
|
||||
} from './load3dViewport'
|
||||
|
||||
const LETTERBOX_CLEAR_COLOR = 0x0a0a0a
|
||||
const LETTERBOX_DIM_OPACITY = 0.5
|
||||
|
||||
type LetterboxDimmer = {
|
||||
scene: THREE.Scene
|
||||
camera: THREE.OrthographicCamera
|
||||
geometry: THREE.PlaneGeometry
|
||||
material: THREE.MeshBasicMaterial
|
||||
}
|
||||
|
||||
function supportsViewOffset(
|
||||
camera: THREE.Camera
|
||||
): camera is THREE.PerspectiveCamera | THREE.OrthographicCamera {
|
||||
return (
|
||||
camera instanceof THREE.PerspectiveCamera ||
|
||||
camera instanceof THREE.OrthographicCamera
|
||||
)
|
||||
}
|
||||
|
||||
function createLetterboxDimmer(): LetterboxDimmer {
|
||||
const geometry = new THREE.PlaneGeometry(2, 2)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x000000,
|
||||
transparent: true,
|
||||
opacity: LETTERBOX_DIM_OPACITY,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
})
|
||||
const scene = new THREE.Scene()
|
||||
scene.add(new THREE.Mesh(geometry, material))
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1)
|
||||
return { scene, camera, geometry, material }
|
||||
}
|
||||
|
||||
const VIEW_HELPER_SIZE = 128
|
||||
|
||||
export type Viewport3dDeps = {
|
||||
@@ -67,6 +106,7 @@ export class Viewport3d {
|
||||
private overlay: SceneOverlay | null = null
|
||||
private initialRenderTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private viewPixelScale = 1
|
||||
private letterboxDimmer: LetterboxDimmer | null = null
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@@ -257,29 +297,96 @@ export class Viewport3d {
|
||||
this.renderer.setScissor(0, 0, viewWidth, viewHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
|
||||
if (this.shouldMaintainAspectRatio()) {
|
||||
const { offsetX, offsetY, width, height } = computeLetterboxedViewport(
|
||||
{ width: viewWidth, height: viewHeight },
|
||||
this.targetAspectRatio
|
||||
)
|
||||
|
||||
this.renderer.setClearColor(0x0a0a0a)
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.setViewport(offsetX, offsetY, width, height)
|
||||
this.renderer.setScissor(offsetX, offsetY, width, height)
|
||||
|
||||
this.cameraManager.updateAspectRatio(width / height)
|
||||
} else {
|
||||
if (!this.shouldMaintainAspectRatio()) {
|
||||
this.renderer.setClearColor(
|
||||
this.view.state.clearColor,
|
||||
this.view.state.clearAlpha
|
||||
)
|
||||
this.renderer.clear()
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(this.sceneManager.scene, this.getRenderCamera())
|
||||
return
|
||||
}
|
||||
|
||||
const container = { width: viewWidth, height: viewHeight }
|
||||
const viewport = computeLetterboxedViewport(
|
||||
container,
|
||||
this.targetAspectRatio
|
||||
)
|
||||
|
||||
this.renderer.setClearColor(LETTERBOX_CLEAR_COLOR)
|
||||
this.renderer.clear()
|
||||
|
||||
this.cameraManager.updateAspectRatio(viewport.width / viewport.height)
|
||||
|
||||
const camera = this.getRenderCamera()
|
||||
|
||||
if (!supportsViewOffset(camera)) {
|
||||
this.renderer.setViewport(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.renderer.setScissor(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(this.sceneManager.scene, camera)
|
||||
return
|
||||
}
|
||||
|
||||
camera.setViewOffset(
|
||||
viewport.width,
|
||||
viewport.height,
|
||||
-viewport.offsetX,
|
||||
-viewport.offsetY,
|
||||
viewWidth,
|
||||
viewHeight
|
||||
)
|
||||
|
||||
this.renderLetterboxedBackground(viewport)
|
||||
this.renderer.render(this.sceneManager.scene, camera)
|
||||
camera.clearViewOffset()
|
||||
|
||||
this.dimLetterboxBars(computeLetterboxBars(container, viewport))
|
||||
}
|
||||
|
||||
private renderLetterboxedBackground(viewport: LetterboxedViewport): void {
|
||||
if (this.sceneManager.getCurrentBackgroundInfo().type !== 'image') {
|
||||
this.sceneManager.renderBackground()
|
||||
return
|
||||
}
|
||||
|
||||
this.renderer.setViewport(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.renderer.setScissor(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(this.sceneManager.scene, this.getRenderCamera())
|
||||
this.renderer.setViewport(0, 0, this.view.width, this.view.height)
|
||||
this.renderer.setScissor(0, 0, this.view.width, this.view.height)
|
||||
}
|
||||
|
||||
private dimLetterboxBars(bars: ViewportRect[]): void {
|
||||
if (bars.length === 0) return
|
||||
|
||||
const dimmer = (this.letterboxDimmer ??= createLetterboxDimmer())
|
||||
for (const bar of bars) {
|
||||
this.renderer.setViewport(bar.x, bar.y, bar.width, bar.height)
|
||||
this.renderer.setScissor(bar.x, bar.y, bar.width, bar.height)
|
||||
this.renderer.render(dimmer.scene, dimmer.camera)
|
||||
}
|
||||
}
|
||||
|
||||
clientPointToNdc(clientX: number, clientY: number): LetterboxNdc | null {
|
||||
@@ -435,6 +542,11 @@ export class Viewport3d {
|
||||
this.overlay.dispose()
|
||||
this.overlay = null
|
||||
}
|
||||
if (this.letterboxDimmer) {
|
||||
this.letterboxDimmer.geometry.dispose()
|
||||
this.letterboxDimmer.material.dispose()
|
||||
this.letterboxDimmer = null
|
||||
}
|
||||
this.sceneManager.dispose()
|
||||
this.cameraManager.dispose()
|
||||
this.controlsManager.dispose()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
clientPointToLetterboxNdc,
|
||||
computeLetterboxBars,
|
||||
computeLetterboxedViewport,
|
||||
isLoad3dActive
|
||||
} from './load3dViewport'
|
||||
@@ -82,6 +83,53 @@ describe('computeLetterboxedViewport', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeLetterboxBars', () => {
|
||||
it('returns left and right bars for a pillarboxed viewport', () => {
|
||||
const container = { width: 800, height: 400 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([
|
||||
{ x: 0, y: 0, width: 200, height: 400 },
|
||||
{ x: 600, y: 0, width: 200, height: 400 }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns bottom and top bars for a letterboxed viewport', () => {
|
||||
const container = { width: 400, height: 800 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([
|
||||
{ x: 0, y: 0, width: 400, height: 200 },
|
||||
{ x: 0, y: 600, width: 400, height: 200 }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns no bars when the viewport fills the container', () => {
|
||||
const container = { width: 1024, height: 768 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1024 / 768)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores sub-pixel offsets', () => {
|
||||
const bars = computeLetterboxBars(
|
||||
{ width: 800, height: 400.5 },
|
||||
{ offsetX: 0, offsetY: 0.25, width: 800, height: 400 }
|
||||
)
|
||||
|
||||
expect(bars).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLoad3dActive', () => {
|
||||
const idle: Load3dActivityFlags = {
|
||||
mouseOnNode: false,
|
||||
|
||||
@@ -2,13 +2,20 @@ import { denormalize, normalize } from '@/utils/mathUtil'
|
||||
|
||||
type Size = { width: number; height: number }
|
||||
|
||||
type LetterboxedViewport = {
|
||||
export type LetterboxedViewport = {
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type ViewportRect = {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function computeLetterboxedViewport(
|
||||
container: Size,
|
||||
targetAspectRatio: number
|
||||
@@ -69,6 +76,37 @@ export function clientPointToLetterboxNdc(
|
||||
)
|
||||
}
|
||||
|
||||
export function computeLetterboxBars(
|
||||
container: Size,
|
||||
viewport: LetterboxedViewport
|
||||
): ViewportRect[] {
|
||||
if (viewport.offsetX >= 1) {
|
||||
return [
|
||||
{ x: 0, y: 0, width: viewport.offsetX, height: container.height },
|
||||
{
|
||||
x: viewport.offsetX + viewport.width,
|
||||
y: 0,
|
||||
width: container.width - viewport.offsetX - viewport.width,
|
||||
height: container.height
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (viewport.offsetY >= 1) {
|
||||
return [
|
||||
{ x: 0, y: 0, width: container.width, height: viewport.offsetY },
|
||||
{
|
||||
x: 0,
|
||||
y: viewport.offsetY + viewport.height,
|
||||
width: container.width,
|
||||
height: container.height - viewport.offsetY - viewport.height
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export type Load3dActivityFlags = {
|
||||
mouseOnNode: boolean
|
||||
mouseOnScene: boolean
|
||||
|
||||
Reference in New Issue
Block a user