104656aa99
- packages/model3d-viewer/src/Model3DViewer.tsx:删除只出现在依赖数组里的 sourceKind 变量与依赖项,effect 实际只分支 sourceData / sourceUrl,kind 变化必然带动这两者之一变化 验证:npx tsc(严格模式单文件检查)、npx vitest run packages/model3d-viewer
183 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|