feat(scene): shared zoom/pan for control and presentation
Keep effects pinned to map coordinates when the viewport changes; materials/NPC overlays stay screen-fixed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
DEFAULT_SCENE_VIEW_CAMERA,
|
||||
type SceneViewCamera,
|
||||
} from '../../shared/types/sceneView';
|
||||
|
||||
import styles from './RotatedImage.module.css';
|
||||
|
||||
type Mode = 'cover' | 'contain';
|
||||
@@ -13,6 +18,8 @@ type RotatedImageProps = {
|
||||
decoding?: React.ImgHTMLAttributes<HTMLImageElement>['decoding'];
|
||||
/** Высота/ширина полностью контролируются родителем. */
|
||||
style?: React.CSSProperties;
|
||||
/** Зум/пан поверх contain/cover (только для mode=contain в сценах). */
|
||||
viewCamera?: SceneViewCamera | null;
|
||||
/** Прямоугольник видимого контента (contain/cover) внутри контейнера. */
|
||||
onContentRectChange?: ((rect: { x: number; y: number; w: number; h: number }) => void) | undefined;
|
||||
};
|
||||
@@ -47,12 +54,18 @@ export function RotatedImage({
|
||||
loading,
|
||||
decoding,
|
||||
style,
|
||||
viewCamera = null,
|
||||
onContentRectChange,
|
||||
}: RotatedImageProps) {
|
||||
const [ref, size] = useElementSize<HTMLDivElement>();
|
||||
const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null);
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
const cam = viewCamera ?? DEFAULT_SCENE_VIEW_CAMERA;
|
||||
const viewScale = mode === 'contain' ? Math.max(1, cam.scale) : 1;
|
||||
const viewOx = mode === 'contain' ? cam.ox : 0.5;
|
||||
const viewOy = mode === 'contain' ? cam.oy : 0.5;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// If the image is served from cache, onLoad may fire before listeners attach.
|
||||
// Reading from the <img> element itself is the most reliable source.
|
||||
@@ -66,7 +79,7 @@ export function RotatedImage({
|
||||
setImgSize((prev) => (prev && prev.w === w0 && prev.h === h0 ? prev : { w: w0, h: h0 }));
|
||||
}, [url]);
|
||||
|
||||
const scale = useMemo(() => {
|
||||
const fitScale = useMemo(() => {
|
||||
if (!imgSize) return 1;
|
||||
if (size.w <= 1 || size.h <= 1) return 1;
|
||||
const rotated = rotationDeg === 90 || rotationDeg === 270;
|
||||
@@ -77,21 +90,28 @@ export function RotatedImage({
|
||||
return mode === 'cover' ? Math.max(sx, sy) : Math.min(sx, sy);
|
||||
}, [imgSize, mode, rotationDeg, size.h, size.w]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onContentRectChange) return;
|
||||
if (!imgSize) return;
|
||||
if (size.w <= 1 || size.h <= 1) return;
|
||||
const scale = fitScale * viewScale;
|
||||
|
||||
const contentRect = useMemo(() => {
|
||||
if (!imgSize) return null;
|
||||
if (size.w <= 1 || size.h <= 1) return null;
|
||||
const rotated = rotationDeg === 90 || rotationDeg === 270;
|
||||
// Bounding-box размеров после rotate(): при 90/270 меняются местами.
|
||||
const bw = (rotated ? imgSize.h : imgSize.w) * scale;
|
||||
const bh = (rotated ? imgSize.w : imgSize.h) * scale;
|
||||
const x = (size.w - bw) / 2;
|
||||
const y = (size.h - bh) / 2;
|
||||
onContentRectChange({ x, y, w: bw, h: bh });
|
||||
}, [imgSize, mode, onContentRectChange, rotationDeg, scale, size.h, size.w]);
|
||||
const x = size.w / 2 - viewOx * bw;
|
||||
const y = size.h / 2 - viewOy * bh;
|
||||
return { x, y, w: bw, h: bh };
|
||||
}, [imgSize, rotationDeg, scale, size.h, size.w, viewOx, viewOy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onContentRectChange || !contentRect) return;
|
||||
onContentRectChange(contentRect);
|
||||
}, [contentRect, onContentRectChange]);
|
||||
|
||||
const w = imgSize ? imgSize.w * scale : undefined;
|
||||
const h = imgSize ? imgSize.h * scale : undefined;
|
||||
const leftPx = contentRect ? contentRect.x + contentRect.w / 2 : undefined;
|
||||
const topPx = contentRect ? contentRect.y + contentRect.h / 2 : undefined;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.root} style={style}>
|
||||
@@ -117,6 +137,8 @@ export function RotatedImage({
|
||||
style={{
|
||||
width: w ?? '100%',
|
||||
height: h ?? '100%',
|
||||
left: leftPx !== undefined ? `${String(leftPx)}px` : '50%',
|
||||
top: topPx !== undefined ? `${String(topPx)}px` : '50%',
|
||||
objectFit: imgSize ? undefined : mode,
|
||||
transform: `translate(-50%, -50%) rotate(${String(rotationDeg)}deg)`,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user