mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(relationships): preserve orbit target with detail panel
This commit is contained in:
1 parent
de3494bc7f
commit
d54675cb3a
4 files changed
+114
-27
No files matched your search
@@ -17,6 +17,7 @@ import {
|
||||
import { buildRelationshipVisibleLabelKeys } from '../relationship-galaxy-connections'
|
||||
import {
|
||||
applyRelationshipGalaxy3DSafeArea,
|
||||
buildRelationshipGalaxy3DViewOffset,
|
||||
buildRelationshipGalaxy3DFitCameraPose,
|
||||
type RelationshipGalaxy3DCameraPose,
|
||||
} from '../relationship-galaxy-3d-camera'
|
||||
@@ -513,8 +514,8 @@ function resizeCanvas() {
|
||||
if (!renderer || !camera) return
|
||||
const size = getViewportSize()
|
||||
camera.aspect = size.width / size.height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(size.width, size.height)
|
||||
applyCameraSafeAreaProjection(size)
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
@@ -631,6 +632,7 @@ function fitView() {
|
||||
|
||||
function applySafeAreaToCameraPose(pose: RelationshipGalaxy3DCameraPose): RelationshipGalaxy3DCameraPose {
|
||||
const size = getViewportSize()
|
||||
applyCameraSafeAreaProjection(size)
|
||||
return applyRelationshipGalaxy3DSafeArea(pose, {
|
||||
viewportWidth: size.width,
|
||||
viewportHeight: size.height,
|
||||
@@ -639,6 +641,28 @@ function applySafeAreaToCameraPose(pose: RelationshipGalaxy3DCameraPose): Relati
|
||||
})
|
||||
}
|
||||
|
||||
function applyCameraSafeAreaProjection(size = getViewportSize()) {
|
||||
if (!camera) return
|
||||
const viewOffset = buildRelationshipGalaxy3DViewOffset({
|
||||
viewportWidth: size.width,
|
||||
viewportHeight: size.height,
|
||||
safeInsetRight: props.safeInsetRight,
|
||||
})
|
||||
if (!viewOffset) {
|
||||
camera.clearViewOffset()
|
||||
return
|
||||
}
|
||||
|
||||
camera.setViewOffset(
|
||||
viewOffset.fullWidth,
|
||||
viewOffset.fullHeight,
|
||||
viewOffset.offsetX,
|
||||
viewOffset.offsetY,
|
||||
viewOffset.width,
|
||||
viewOffset.height
|
||||
)
|
||||
}
|
||||
|
||||
function vectorToPose(vector: THREE.Vector3): RelationshipGalaxy3DCameraPose['position'] {
|
||||
return { x: vector.x, y: vector.y, z: vector.z }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
applyRelationshipGalaxy3DSafeArea,
|
||||
buildRelationshipGalaxy3DViewOffset,
|
||||
buildRelationshipGalaxy3DFitCameraPose,
|
||||
} from './relationship-galaxy-3d-camera'
|
||||
|
||||
@@ -30,7 +31,7 @@ test('fits the panorama with enough padding for a selected relationship network'
|
||||
assert.deepEqual(pose.target, { x: 0, y: 0, z: 0 })
|
||||
})
|
||||
|
||||
test('shifts and expands 3D camera pose so the focused node network fits left of a right-side panel', () => {
|
||||
test('keeps the 3D orbit target fixed while expanding camera distance for a right-side panel', () => {
|
||||
const pose = applyRelationshipGalaxy3DSafeArea(
|
||||
{
|
||||
position: { x: 0, y: 0, z: 1000 },
|
||||
@@ -44,10 +45,36 @@ test('shifts and expands 3D camera pose so the focused node network fits left of
|
||||
}
|
||||
)
|
||||
|
||||
assert.ok(pose.position.x > 769)
|
||||
assert.ok(pose.position.x < 771)
|
||||
assert.equal(pose.position.x, 0)
|
||||
assert.equal(pose.position.y, 0)
|
||||
assert.ok(pose.position.z > 1650)
|
||||
assert.ok(pose.position.z < 1670)
|
||||
assert.deepEqual(pose.target, { x: pose.position.x, y: 0, z: 0 })
|
||||
assert.deepEqual(pose.target, { x: 0, y: 0, z: 0 })
|
||||
})
|
||||
|
||||
test('builds a 3D camera view offset that moves the focus into the visible area', () => {
|
||||
assert.deepEqual(
|
||||
buildRelationshipGalaxy3DViewOffset({
|
||||
viewportWidth: 1000,
|
||||
viewportHeight: 500,
|
||||
safeInsetRight: 400,
|
||||
}),
|
||||
{
|
||||
fullWidth: 1400,
|
||||
fullHeight: 500,
|
||||
offsetX: 400,
|
||||
offsetY: 0,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
}
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
buildRelationshipGalaxy3DViewOffset({
|
||||
viewportWidth: 1000,
|
||||
viewportHeight: 500,
|
||||
safeInsetRight: 0,
|
||||
}),
|
||||
null
|
||||
)
|
||||
})
|
||||
@@ -19,6 +19,15 @@ export interface RelationshipGalaxy3DSafeAreaOptions {
|
||||
fovDegrees: number
|
||||
}
|
||||
|
||||
export interface RelationshipGalaxy3DViewOffset {
|
||||
fullWidth: number
|
||||
fullHeight: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const FIT_CAMERA_PADDING_SCALE = 1.3
|
||||
|
||||
export function buildRelationshipGalaxy3DFitCameraPose(
|
||||
@@ -42,7 +51,6 @@ export function applyRelationshipGalaxy3DSafeArea(
|
||||
): RelationshipGalaxy3DCameraPose {
|
||||
const inset = normalizeRelationshipGalaxySafeInsetRight(options)
|
||||
const viewportWidth = Math.max(1, options.viewportWidth)
|
||||
const viewportHeight = Math.max(1, options.viewportHeight)
|
||||
if (inset <= 0) return pose
|
||||
|
||||
const forward = normalizeVector({
|
||||
@@ -50,27 +58,40 @@ export function applyRelationshipGalaxy3DSafeArea(
|
||||
y: pose.target.y - pose.position.y,
|
||||
z: pose.target.z - pose.position.z,
|
||||
})
|
||||
const right = normalizeVector(crossVector(forward, { x: 0, y: 1, z: 0 }))
|
||||
const distance = Math.max(1, distanceBetween(pose.position, pose.target))
|
||||
// The side panel reduces usable width, so move back before shifting the target into the visible area.
|
||||
// The side panel reduces usable width. Move the camera back, but keep the orbit target fixed.
|
||||
const visibleWidthRatio = Math.max(0.001, (viewportWidth - inset) / viewportWidth)
|
||||
const expandedDistance = distance / visibleWidthRatio
|
||||
const visibleHeight = 2 * expandedDistance * Math.tan((Math.max(1, options.fovDegrees) * Math.PI) / 360)
|
||||
const visibleWidth = visibleHeight * (viewportWidth / viewportHeight)
|
||||
const offset = (inset / 2 / viewportWidth) * visibleWidth
|
||||
const shift = {
|
||||
x: right.x * offset,
|
||||
y: right.y * offset,
|
||||
z: right.z * offset,
|
||||
}
|
||||
const shiftedTarget = addVector(pose.target, shift)
|
||||
|
||||
return {
|
||||
position: addVector(shiftedTarget, multiplyVector(forward, -expandedDistance)),
|
||||
target: shiftedTarget,
|
||||
position: addVector(pose.target, multiplyVector(forward, -expandedDistance)),
|
||||
target: pose.target,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRelationshipGalaxy3DViewOffset(
|
||||
options: RelationshipGalaxyViewportViewOffsetOptions
|
||||
): RelationshipGalaxy3DViewOffset | null {
|
||||
const viewportWidth = Math.max(1, Math.floor(options.viewportWidth))
|
||||
const viewportHeight = Math.max(1, Math.floor(options.viewportHeight))
|
||||
const inset = Math.floor(normalizeRelationshipGalaxySafeInsetRight(options))
|
||||
if (inset <= 0) return null
|
||||
|
||||
return {
|
||||
fullWidth: viewportWidth + inset,
|
||||
fullHeight: viewportHeight,
|
||||
offsetX: inset,
|
||||
offsetY: 0,
|
||||
width: viewportWidth,
|
||||
height: viewportHeight,
|
||||
}
|
||||
}
|
||||
|
||||
type RelationshipGalaxyViewportViewOffsetOptions = Pick<
|
||||
RelationshipGalaxy3DSafeAreaOptions,
|
||||
'viewportWidth' | 'viewportHeight' | 'safeInsetRight'
|
||||
>
|
||||
|
||||
function addVector(a: RelationshipGalaxy3DVector, b: RelationshipGalaxy3DVector): RelationshipGalaxy3DVector {
|
||||
return {
|
||||
x: a.x + b.x,
|
||||
@@ -100,11 +121,3 @@ function normalizeVector(vector: RelationshipGalaxy3DVector): RelationshipGalaxy
|
||||
z: vector.z / length,
|
||||
}
|
||||
}
|
||||
|
||||
function crossVector(a: RelationshipGalaxy3DVector, b: RelationshipGalaxy3DVector): RelationshipGalaxy3DVector {
|
||||
return {
|
||||
x: a.y * b.z - a.z * b.y,
|
||||
y: a.z * b.x - a.x * b.z,
|
||||
z: a.x * b.y - a.y * b.x,
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,27 @@ describe('RelationshipGalaxyThreeCanvas scene wiring', () => {
|
||||
'selectedKey changes affect 3D topology and must not be handled as a label-only update'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses camera view offset for right panel safe area instead of moving the orbit target', () => {
|
||||
const source = readCanvasSource()
|
||||
const resizeCanvas = source.slice(
|
||||
source.indexOf('function resizeCanvas'),
|
||||
source.indexOf('function handlePointerMove')
|
||||
)
|
||||
const applySafeArea = source.slice(
|
||||
source.indexOf('function applySafeAreaToCameraPose'),
|
||||
source.indexOf('function vectorToPose')
|
||||
)
|
||||
|
||||
assert.ok(source.includes('camera.setViewOffset('), '3D safe area should use projection view offset')
|
||||
assert.ok(source.includes('camera.clearViewOffset()'), '3D safe area should clear projection offset when closed')
|
||||
assert.ok(
|
||||
resizeCanvas.includes('applyCameraSafeAreaProjection(size)'),
|
||||
'resizing the canvas must preserve the current safe-area projection'
|
||||
)
|
||||
assert.ok(
|
||||
applySafeArea.includes('applyCameraSafeAreaProjection(size)'),
|
||||
'focus and fit camera poses must apply the safe-area projection separately from the orbit target'
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user