93002f1bd2
- packages/model3d-viewer/src/status.ts:状态类型改名,与同文件的 Model3dViewerFailureReason / Model3dViewerStatusDetail 以及仓库既有 Model3d* 口径一致 - packages/model3d-viewer/src/Model3DViewer.tsx、packages/model3d-viewer/src/index.ts:跟随改名更新导入与引用 - src/components/image-editor/model3d-preview/Model3dViewerModal.tsx:跟随改名更新导入与状态类型引用 验证:npx tsc -p tsconfig.typecheck-guardrails.json --noEmit、npm run lint:eslint(改动文件)
177 lines
6.0 KiB
TypeScript
177 lines
6.0 KiB
TypeScript
import {
|
|
Model3DViewer,
|
|
type Model3DViewerHandle,
|
|
type Model3DViewerSource,
|
|
type Model3dViewerStatus,
|
|
type Model3dViewerStatusDetail,
|
|
} from '@genarrative/model3d-viewer';
|
|
import { useRef, useState } from 'react';
|
|
|
|
import { useResolvedAssetReadUrl } from '../../../hooks/useResolvedAssetReadUrl';
|
|
import { PlatformActionButton } from '../../common/PlatformActionButton';
|
|
import { UnifiedModal } from '../../common/UnifiedModal';
|
|
import type { CanvasLayer } from '../ImageCanvasEditorTypes';
|
|
import { resolveModel3dViewerFailureText } from './Model3dViewerModel';
|
|
|
|
export type Model3dViewerModalProps = {
|
|
layer: CanvasLayer | null;
|
|
onClose: () => void;
|
|
};
|
|
|
|
/**
|
|
* 面板该显示的提示文案;返回 null 表示可以进查看器。用早返回而不是嵌套三元,
|
|
* 每个分支对应一个前置条件,读起来是清单而不是括号树。
|
|
*/
|
|
function resolveModel3dViewerHint({
|
|
hasModelSource,
|
|
isResolving,
|
|
resolvedUrl,
|
|
}: {
|
|
hasModelSource: boolean;
|
|
isResolving: boolean;
|
|
resolvedUrl: string;
|
|
}) {
|
|
if (!hasModelSource) {
|
|
return '该资源没有可预览的 3D 模型文件。';
|
|
}
|
|
if (isResolving) {
|
|
return '正在准备模型地址…';
|
|
}
|
|
if (!resolvedUrl) {
|
|
return '模型地址暂不可用,请稍后重试。';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
type Model3dViewerUrlSource = Extract<Model3DViewerSource, { kind: 'url' }>;
|
|
|
|
/** 查看器来源:模型对象与它的签名地址。格式由查看器按读响应与字节判定。 */
|
|
function resolveModel3dViewerSource({
|
|
objectKey,
|
|
resolvedUrl,
|
|
}: {
|
|
objectKey: string;
|
|
resolvedUrl: string;
|
|
}): Model3dViewerUrlSource | null {
|
|
if (!objectKey || !resolvedUrl) {
|
|
return null;
|
|
}
|
|
return { kind: 'url', url: resolvedUrl };
|
|
}
|
|
|
|
const MODEL3D_VIEWER_FAILURE_FALLBACK_TEXT = '模型加载失败,请关闭后重试。';
|
|
|
|
/**
|
|
* 3D 模型预览模态:只做「取地址 + 交给查看器」,不解析模型、不产出缩略图。
|
|
*
|
|
* 模型本体只认 `objectKey`:投影后的 `imageSrc` 是预览图,拿它当模型地址会得到一个
|
|
* 图片文件。`objectKey` 缺失就直接判不可预览 —— 宁可少一次"也许能开",也不让同一份
|
|
* 字段在同一期承担两种语义。
|
|
*
|
|
* 格式不在这一层判定:查看器读模型时按「响应内容类型 → 字节魔数 → 地址扩展名」判定,
|
|
* 判不出来就报 unsupported-format,这里把失败原因翻译成用户能读的文案。
|
|
*/
|
|
export function Model3dViewerModal({
|
|
layer,
|
|
onClose,
|
|
}: Model3dViewerModalProps) {
|
|
const modelObjectKey = layer?.objectKey?.trim() ?? '';
|
|
const { resolvedUrl, isResolving } = useResolvedAssetReadUrl(modelObjectKey, {
|
|
objectKey: layer?.objectKey,
|
|
refreshKey: `${layer?.taskId ?? layer?.resourceId ?? ''}:model3d`,
|
|
});
|
|
const viewerRef = useRef<Model3DViewerHandle>(null);
|
|
const layerIdentity = layer?.id ?? null;
|
|
// 查看器状态连同"它属于哪个模型"一起存:换了模型,旧状态自动失效。
|
|
// 用 effect 在切换时重置不行 —— 它在挂载后运行,会盖掉查看器刚上报的 ready。
|
|
const [viewerStatus, setViewerStatus] = useState<{
|
|
identity: string;
|
|
status: Model3dViewerStatus;
|
|
detail: Model3dViewerStatusDetail | null;
|
|
} | null>(null);
|
|
|
|
const modelSource = resolveModel3dViewerSource({
|
|
objectKey: modelObjectKey,
|
|
resolvedUrl,
|
|
});
|
|
const viewerIdentity = modelSource
|
|
? `${layerIdentity ?? ''}:${modelSource.url}`
|
|
: null;
|
|
const canResetView =
|
|
Boolean(viewerIdentity) &&
|
|
viewerStatus?.identity === viewerIdentity &&
|
|
viewerStatus.status === 'ready';
|
|
const hintText = resolveModel3dViewerHint({
|
|
hasModelSource: Boolean(modelObjectKey),
|
|
isResolving,
|
|
resolvedUrl,
|
|
});
|
|
const errorText = resolveModel3dViewerFailureText(
|
|
viewerStatus?.identity === viewerIdentity ? viewerStatus.detail : null,
|
|
MODEL3D_VIEWER_FAILURE_FALLBACK_TEXT,
|
|
);
|
|
|
|
return (
|
|
<UnifiedModal
|
|
open={Boolean(layer)}
|
|
title="3D模型预览"
|
|
size="lg"
|
|
portalTheme="light"
|
|
closeLabel="关闭3D模型预览"
|
|
onClose={onClose}
|
|
// 面板强制浅色:`platform-modal-shell` 是层外手写规则,工具类必须带 `!` 才压得住。
|
|
panelClassName="!border-slate-200 !bg-white !text-slate-800 !shadow-[0_24px_70px_rgba(15,23,42,0.18)]"
|
|
bodyClassName="!p-0"
|
|
footer={
|
|
<PlatformActionButton
|
|
type="button"
|
|
tone="secondary"
|
|
size="sm"
|
|
disabled={!canResetView}
|
|
onClick={() => viewerRef.current?.resetView()}
|
|
>
|
|
重置视角
|
|
</PlatformActionButton>
|
|
}
|
|
>
|
|
{/* 查看器要一个非零尺寸的容器才能量出画布尺寸;画布编辑器只支持桌面端,按视口限高。 */}
|
|
<div className="flex h-[min(58vh,30rem)] min-h-56 items-center justify-center bg-slate-50">
|
|
{hintText ? (
|
|
<p
|
|
className="m-0 px-6 text-center text-sm leading-6 text-slate-500"
|
|
role="status"
|
|
>
|
|
{hintText}
|
|
</p>
|
|
) : null}
|
|
{modelSource ? (
|
|
<Model3DViewer
|
|
ref={viewerRef}
|
|
source={modelSource}
|
|
className="flex-auto self-stretch"
|
|
errorText={errorText}
|
|
// 幂等更新:同一个回调每次渲染都会换新引用,直接塞新对象会让"上报 → 重渲染
|
|
// → 再上报"循环下去;值没变时必须返回原状态对象。
|
|
onStatusChange={(status, detail) =>
|
|
setViewerStatus((current) => {
|
|
if (!viewerIdentity) {
|
|
return current;
|
|
}
|
|
if (
|
|
current?.identity === viewerIdentity &&
|
|
current.status === status &&
|
|
current.detail?.reason === detail?.reason &&
|
|
current.detail?.message === detail?.message
|
|
) {
|
|
return current;
|
|
}
|
|
return { identity: viewerIdentity, status, detail };
|
|
})
|
|
}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</UnifiedModal>
|
|
);
|
|
}
|