diff --git a/web/src/pages/app/modules/preview.ts b/web/src/pages/app/modules/preview.ts index 8ea3533..a29648d 100644 --- a/web/src/pages/app/modules/preview.ts +++ b/web/src/pages/app/modules/preview.ts @@ -96,10 +96,24 @@ type ParentImportScenePayload = { rawOperationTree: string; }; +type ParentBasePointMessage = { + type?: string; + message?: string; + payload?: { + modelId?: string; + modelName?: string; + basePoint?: PreviewBasePoint; + }; +}; + export type ModelPreviewOptions = { onImportScene?: (payload: PreviewImportScenePayload) => Promise | void; }; +const BASE_POINT_SUBSCRIBE_MESSAGE = "DMT_MODEL_LIBRARY_BASE_POINT_SUBSCRIBE"; +const BASE_POINT_UNSUBSCRIBE_MESSAGE = "DMT_MODEL_LIBRARY_BASE_POINT_UNSUBSCRIBE"; +const BASE_POINT_UPDATE_MESSAGE = "DMT_MODEL_LIBRARY_BASE_POINT_UPDATE"; +const BASE_POINT_ERROR_MESSAGE = "DMT_MODEL_LIBRARY_BASE_POINT_ERROR"; const zeroBasePoint: PreviewResolvedBasePoint = { x: 0, y: 0, @@ -117,6 +131,8 @@ let runtime: PreviewRuntime | null = null; let activePreviewContext: PreviewLoadContext | null = null; let previewEnvironmentTexturePromise: Promise | null = null; let previewPlayback: PreviewPlaybackState = createEmptyPlaybackState(); +let basePointBridgeBound = false; +let basePointSubscribed = false; export function openModelPreview( model: ModelItem, @@ -202,6 +218,7 @@ export function openModelPreview( .on("open:after", () => {popup console.log(popup) bindProcessPlaceholder(operations); + bindBasePointSelection(); bindImportScene(model, operations, options.onImportScene); if (canWrite) bindThumbnailCapture(model.id, onThumbnailSaved); requestAnimationFrame(() => { @@ -210,7 +227,10 @@ export function openModelPreview( }); }); }) - .on("close:after", () => disposePreview()); + .on("close:after", () => { + unsubscribeBasePointSelection(); + disposePreview(); + }); } export function setPreviewBasePoint(basePoint: PreviewBasePoint, enabled = true) { @@ -301,6 +321,68 @@ function resolveParentOrigin() { return "*"; } +function isAllowedParentOrigin(origin: string) { + const expectedOrigin = resolveParentOrigin(); + return expectedOrigin === "*" || origin === expectedOrigin || isLocalDmtParentOrigin(origin); +} + +function isLocalDmtParentOrigin(origin: string) { + try { + const url = new URL(origin); + const isLocalHost = url.hostname === "localhost" || url.hostname === "127.0.0.1"; + return isLocalHost && url.port === "3000"; + } catch { + return false; + } +} + +function bindBasePointSelection() { + bindBasePointBridge(); + const checkbox = document.querySelector("#previewUseBasePoint"); + checkbox?.addEventListener("change", () => { + if (checkbox.checked) { + subscribeBasePointSelection(); + } else { + unsubscribeBasePointSelection(); + } + }); + if (checkbox?.checked) subscribeBasePointSelection(); +} + +function bindBasePointBridge() { + if (basePointBridgeBound) return; + basePointBridgeBound = true; + window.addEventListener("message", (event) => { + const data = event.data as ParentBasePointMessage; + if (data?.type !== BASE_POINT_UPDATE_MESSAGE && data?.type !== BASE_POINT_ERROR_MESSAGE) return; + if (!isAllowedParentOrigin(event.origin)) return; + if (data.type === BASE_POINT_ERROR_MESSAGE) { + notify(data.message || "请先在主场景中选择模型或坐标"); + return; + } + if (data.payload?.basePoint) { + setPreviewBasePoint(data.payload.basePoint, true); + } + }); +} + +function subscribeBasePointSelection() { + if (!window.parent || window.parent === window) return; + basePointSubscribed = true; + window.parent.postMessage({ + type: BASE_POINT_SUBSCRIBE_MESSAGE + }, resolveParentPostMessageOrigin()); +} + +function unsubscribeBasePointSelection() { + if (!basePointSubscribed) return; + basePointSubscribed = false; + if (!window.parent || window.parent === window) return; + window.parent.postMessage({ + type: BASE_POINT_UNSUBSCRIBE_MESSAGE + }, resolveParentPostMessageOrigin()); +} + function parseModelOperations(value: string): PreviewOperation[] { try { const dbValue = JSON.parse(value || "{}") as Partial;