Files
Genarrative/packages/model3d-viewer/src/Model3DViewer.tsx
T
k88936 104656aa99 去掉 Model3DViewer 加载 effect 里冗余的 sourceKind 依赖
- packages/model3d-viewer/src/Model3DViewer.tsx:删除只出现在依赖数组里的 sourceKind 变量与依赖项,effect 实际只分支 sourceData / sourceUrl,kind 变化必然带动这两者之一变化

验证:npx tsc(严格模式单文件检查)、npx vitest run packages/model3d-viewer
2026-09-22 13:17:27 +08:00

183 lines
5.1 KiB
TypeScript

import {
type Ref,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import {
describeModel3dViewerError,
Model3dViewerAbortedError,
Model3dViewerLoadError,
type Model3DViewerSource,
} from './loader';
import { createModel3dViewerScene, type Model3dViewerScene } from './scene';
import {
MODEL3D_VIEWER_LOADING_TEXT,
MODEL3D_VIEWER_STATUS_ATTRIBUTE,
type Model3dViewerStatus,
type Model3dViewerStatusDetail,
resolveModel3dViewerErrorText,
} from './status';
export type Model3DViewerHandle = {
/** 回到打开模型时的归一化取景。 */
resetView(): void;
};
export type Model3DViewerProps = {
/** 模型来源:宿主已解析好的签名地址,或已读入的字节。 */
source: Model3DViewerSource;
autoRotate?: boolean;
/** 失败时那一行状态文案;不传用内置文案。 */
errorText?: string;
onStatusChange?: (
status: Model3dViewerStatus,
detail: Model3dViewerStatusDetail,
) => void;
className?: string;
ref?: Ref<Model3DViewerHandle>;
};
/**
* WebGL2 模型查看器:只负责加载与交互查看,不取业务数据、不产出缩略图。
* 宿主要给容器一个非零尺寸,并在祖先上带 platform-theme 主题类才能取到平台色值。
*/
export function Model3DViewer({
source,
autoRotate = false,
errorText,
onStatusChange,
className,
ref,
}: Model3DViewerProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const sceneRef = useRef<Model3dViewerScene | null>(null);
const onStatusChangeRef = useRef(onStatusChange);
const autoRotateRef = useRef(autoRotate);
const [status, setStatus] = useState<Model3dViewerStatus>('idle');
useEffect(() => {
onStatusChangeRef.current = onStatusChange;
}, [onStatusChange]);
const reportStatus = useCallback(
(next: Model3dViewerStatus, detail: Model3dViewerStatusDetail) => {
setStatus(next);
onStatusChangeRef.current?.(next, detail);
},
[],
);
useImperativeHandle(
ref,
() => ({
resetView() {
sceneRef.current?.resetView();
},
}),
[],
);
const sourceFormat = source.format;
const sourceUrl = source.kind === 'url' ? source.url : null;
const sourceData = source.kind === 'bytes' ? source.data : null;
useEffect(() => {
const container = containerRef.current;
const canvas = canvasRef.current;
if (!container || !canvas) {
return;
}
let aborted = false;
let createdScene: Model3dViewerScene | null = null;
const activeSource: Model3DViewerSource = sourceData
? { kind: 'bytes', data: sourceData, format: sourceFormat }
: { kind: 'url', url: sourceUrl ?? '', format: sourceFormat };
reportStatus('loading', {});
createModel3dViewerScene({
canvas,
container,
source: activeSource,
autoRotate: autoRotateRef.current,
isAborted: () => aborted,
})
.then((scene) => {
if (aborted) {
scene.dispose();
return;
}
createdScene = scene;
sceneRef.current = scene;
// 场景创建期间 autoRotate 可能已经变过,而那时 sceneRef 还是空的,
// 只靠 setAutoRotate 的 effect 会丢掉这次更新,所以这里补一次最新值。
scene.setAutoRotate(autoRotateRef.current);
reportStatus('ready', {});
})
.catch((error: unknown) => {
if (aborted || error instanceof Model3dViewerAbortedError) {
return;
}
if (error instanceof Model3dViewerLoadError) {
reportStatus('failed', {
reason: error.reason,
message: error.message,
byteLength: error.byteLength,
});
return;
}
reportStatus('failed', {
reason: 'load-failed',
message: describeModel3dViewerError(error),
});
});
return () => {
aborted = true;
createdScene?.dispose();
if (sceneRef.current === createdScene) {
sceneRef.current = null;
}
};
}, [sourceFormat, sourceUrl, sourceData, reportStatus]);
useEffect(() => {
autoRotateRef.current = autoRotate;
sceneRef.current?.setAutoRotate(autoRotate);
}, [autoRotate]);
return (
<div
ref={containerRef}
className={['relative h-full w-full overflow-hidden', className]
.filter(Boolean)
.join(' ')}
{...{ [MODEL3D_VIEWER_STATUS_ATTRIBUTE]: status }}
>
<canvas
ref={canvasRef}
className={[
'absolute inset-0 block h-full w-full',
status === 'ready' ? 'opacity-100' : 'opacity-0',
].join(' ')}
/>
{status === 'loading' ? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-xs text-[var(--platform-text-soft)]">
{MODEL3D_VIEWER_LOADING_TEXT}
</div>
) : null}
{status === 'failed' ? (
<div className="absolute inset-0 flex items-center justify-center px-4 text-center text-xs text-rose-700">
{resolveModel3dViewerErrorText(errorText)}
</div>
) : null}
</div>
);
}