300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import type { TIssueAttachment } from "@plane/types";
|
|
import {
|
|
DEFAULT_BEAM_VIEWER_BASE_URL,
|
|
getBeamDirectModelTypeFromFileName,
|
|
getBeamFileExtension,
|
|
getBeamModelTypeFromFileName,
|
|
} from "./beam-viewer-config";
|
|
|
|
export type TBeamViewerConversion = {
|
|
artifactSrc?: string;
|
|
artifactType?: string;
|
|
componentTreeRequired: boolean;
|
|
message?: string;
|
|
metadataSrc?: string;
|
|
size?: number;
|
|
sourceFormat: string;
|
|
sourceSrc?: string;
|
|
status: "conversion_required" | "processing" | "ready" | "failed";
|
|
targetFormat: "glb" | "xkt";
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type TBeamModelVersionRecord = {
|
|
assetId?: string;
|
|
conversion?: TBeamViewerConversion;
|
|
downloadUrl: string;
|
|
originalFilename: string;
|
|
previewAvailable: boolean;
|
|
projectId?: string;
|
|
sha256?: string;
|
|
size: number;
|
|
sourceSrc?: string;
|
|
src: string;
|
|
status?: string;
|
|
type: string;
|
|
uploadedBy?: string;
|
|
uploadedAt: string;
|
|
version: number;
|
|
versionId?: string;
|
|
viewerUrl?: string | null;
|
|
};
|
|
|
|
export type TBeamViewerAttachment = {
|
|
assetId?: string;
|
|
backend: "beam-viewer-local" | "beam-viewer-ops" | "beam-viewer";
|
|
conversion?: TBeamViewerConversion;
|
|
downloadUrl: string;
|
|
originalFilename: string;
|
|
previewAvailable: boolean;
|
|
projectId?: string;
|
|
registryOwnerId?: string;
|
|
sha256?: string;
|
|
src: string;
|
|
type: string;
|
|
uploadedBy?: string;
|
|
uploadedAt: string;
|
|
version?: number;
|
|
versionId?: string;
|
|
versions?: TBeamModelVersionRecord[];
|
|
viewerUrl?: string | null;
|
|
};
|
|
|
|
export type TBeamConversionStatus = {
|
|
assetId?: string;
|
|
artifactSrc?: string;
|
|
artifactType?: string;
|
|
artifactUrl?: string;
|
|
componentCount?: number;
|
|
componentTreeRequired?: boolean;
|
|
error?: string;
|
|
message?: string;
|
|
metadataSrc?: string;
|
|
metadataUrl?: string;
|
|
sha256?: string;
|
|
sourceFormat?: string;
|
|
sourceSrc?: string;
|
|
size?: number;
|
|
status: "conversion_required" | "processing" | "ready" | "failed";
|
|
targetFormat?: "glb" | "xkt";
|
|
updatedAt?: string;
|
|
version?: number;
|
|
versionId?: string;
|
|
};
|
|
|
|
export type TBeamVersionHistoryResponse = {
|
|
assetId?: string;
|
|
currentVersion?: number;
|
|
currentVersionId?: string;
|
|
ok: boolean;
|
|
projectId?: string;
|
|
updatedAt?: string;
|
|
versions: TBeamModelVersionRecord[];
|
|
};
|
|
|
|
export const BEAM_VIEWER_OPEN_EVENT = "nodedc:beam-viewer-open";
|
|
|
|
export type TBeamViewerOpenEventDetail = {
|
|
downloadUrl?: string;
|
|
fileExtension?: string;
|
|
fileName: string;
|
|
fileSize: number;
|
|
issueId: string;
|
|
viewerUrl: string;
|
|
};
|
|
|
|
export const dispatchBeamViewerOpenEvent = (detail: TBeamViewerOpenEventDetail) => {
|
|
window.dispatchEvent(new CustomEvent<TBeamViewerOpenEventDetail>(BEAM_VIEWER_OPEN_EVENT, { detail }));
|
|
};
|
|
|
|
const MIME_TYPE_BY_EXTENSION: Record<string, string> = {
|
|
glb: "model/gltf-binary",
|
|
gltf: "model/gltf+json",
|
|
};
|
|
|
|
const normalizeBaseUrl = (url: string | undefined): string =>
|
|
(url && url.trim() ? url.trim() : DEFAULT_BEAM_VIEWER_BASE_URL).replace(/\/+$/, "");
|
|
|
|
const toBeamAbsoluteUrl = (src: string | undefined, cacheKey?: string): string | undefined => {
|
|
if (!src) return undefined;
|
|
let url: URL;
|
|
try {
|
|
url = new URL(src);
|
|
} catch (_error) {
|
|
url = new URL(src.startsWith("/") ? src : `/${src}`, `${getBeamViewerBaseUrl()}/`);
|
|
}
|
|
if (cacheKey) url.searchParams.set("v", cacheKey);
|
|
return url.toString();
|
|
};
|
|
|
|
export const getBeamViewerBaseUrl = (): string => normalizeBaseUrl(process.env.VITE_BEAM_VIEWER_BASE_URL);
|
|
|
|
export const getBeamModelTypeFromName = (name: string | undefined): string | null => {
|
|
return getBeamModelTypeFromFileName(name);
|
|
};
|
|
|
|
export const getBeamDirectModelTypeFromName = (name: string | undefined): string | null => {
|
|
return getBeamDirectModelTypeFromFileName(name);
|
|
};
|
|
|
|
export const getBeamModelMimeType = (file: File): string =>
|
|
MIME_TYPE_BY_EXTENSION[getBeamFileExtension(file.name)] || "application/octet-stream";
|
|
|
|
export const isBeamModelFile = (name: string | undefined): boolean => !!getBeamModelTypeFromName(name);
|
|
|
|
export const isBeamDirectViewerFile = (name: string | undefined): boolean => !!getBeamDirectModelTypeFromName(name);
|
|
|
|
export const buildBeamViewerUrl = (params: {
|
|
name?: string;
|
|
settingsSrc?: string;
|
|
src: string;
|
|
type?: string | null;
|
|
}): string => {
|
|
const viewerUrl = new URL(getBeamViewerBaseUrl());
|
|
viewerUrl.searchParams.set("url", params.src);
|
|
if (params.type) viewerUrl.searchParams.set("type", params.type);
|
|
if (params.type === "gltf" || params.type === "glb") viewerUrl.searchParams.set("edges", "false");
|
|
if (params.name) viewerUrl.searchParams.set("name", params.name);
|
|
if (params.settingsSrc) viewerUrl.searchParams.set("settingsSrc", params.settingsSrc);
|
|
return viewerUrl.toString();
|
|
};
|
|
|
|
export const getBeamViewerAttachment = (attachment: TIssueAttachment | undefined): TBeamViewerAttachment | null => {
|
|
const beamViewer = (attachment?.attributes as { beamViewer?: TBeamViewerAttachment } | undefined)?.beamViewer;
|
|
if (!beamViewer || typeof beamViewer !== "object") return null;
|
|
return beamViewer;
|
|
};
|
|
|
|
const getAttachmentVersionNumber = (attachment: TIssueAttachment | undefined): number => {
|
|
const rawVersion = (attachment?.attributes as { version?: number | string } | undefined)?.version;
|
|
if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) return rawVersion;
|
|
if (typeof rawVersion === "string") {
|
|
const parsed = Number.parseInt(rawVersion.replace(/^v/i, ""), 10);
|
|
if (Number.isFinite(parsed)) return parsed;
|
|
}
|
|
return 1;
|
|
};
|
|
|
|
export const getBeamViewerVersionNumber = (
|
|
beamViewer: TBeamViewerAttachment | null | undefined,
|
|
attachment?: TIssueAttachment
|
|
): number => {
|
|
if (typeof beamViewer?.version === "number" && Number.isFinite(beamViewer.version)) return beamViewer.version;
|
|
return getAttachmentVersionNumber(attachment);
|
|
};
|
|
|
|
export const createBeamModelVersionRecord = (
|
|
beamViewer: TBeamViewerAttachment,
|
|
options: {
|
|
attachment?: TIssueAttachment;
|
|
size?: number;
|
|
uploadedBy?: string;
|
|
} = {}
|
|
): TBeamModelVersionRecord => ({
|
|
assetId: beamViewer.assetId,
|
|
conversion: beamViewer.conversion,
|
|
downloadUrl: beamViewer.downloadUrl,
|
|
originalFilename: beamViewer.originalFilename,
|
|
previewAvailable: beamViewer.previewAvailable,
|
|
projectId: beamViewer.projectId,
|
|
sha256: beamViewer.sha256,
|
|
size: options.size ?? beamViewer.conversion?.size ?? Number(options.attachment?.attributes.size ?? 0),
|
|
sourceSrc: beamViewer.conversion?.sourceSrc || beamViewer.src,
|
|
src: beamViewer.src,
|
|
status: beamViewer.conversion?.status || "ready",
|
|
type: beamViewer.type,
|
|
uploadedBy: beamViewer.uploadedBy || options.uploadedBy || options.attachment?.created_by,
|
|
uploadedAt: beamViewer.uploadedAt,
|
|
version: getBeamViewerVersionNumber(beamViewer, options.attachment),
|
|
versionId: beamViewer.versionId,
|
|
viewerUrl: beamViewer.viewerUrl,
|
|
});
|
|
|
|
const isSameBeamVersionRecord = (first: TBeamModelVersionRecord, second: TBeamModelVersionRecord): boolean => {
|
|
if (first.versionId && second.versionId) return first.versionId === second.versionId;
|
|
return first.version === second.version;
|
|
};
|
|
|
|
export const getBeamModelVersionRecords = (
|
|
beamViewer: TBeamViewerAttachment | null | undefined,
|
|
attachment?: TIssueAttachment
|
|
): TBeamModelVersionRecord[] => {
|
|
if (!beamViewer) return [];
|
|
const currentRecord = createBeamModelVersionRecord(beamViewer, { attachment });
|
|
const storedVersions = Array.isArray(beamViewer.versions) ? beamViewer.versions : [];
|
|
const versions = storedVersions.some((version) => isSameBeamVersionRecord(version, currentRecord))
|
|
? storedVersions
|
|
: [...storedVersions, currentRecord];
|
|
|
|
return versions
|
|
.filter((version) => typeof version?.version === "number" && Number.isFinite(version.version))
|
|
.sort((first, second) => first.version - second.version);
|
|
};
|
|
|
|
export const mergeBeamModelVersionRecords = (
|
|
previousBeamViewer: TBeamViewerAttachment,
|
|
nextBeamViewer: TBeamViewerAttachment,
|
|
attachment?: TIssueAttachment
|
|
): TBeamModelVersionRecord[] => {
|
|
const previousVersions = getBeamModelVersionRecords(previousBeamViewer, attachment);
|
|
const nextRecord =
|
|
nextBeamViewer.versions?.find((version) =>
|
|
nextBeamViewer.versionId
|
|
? version.versionId === nextBeamViewer.versionId
|
|
: version.version === nextBeamViewer.version
|
|
) ||
|
|
nextBeamViewer.versions?.[0] ||
|
|
createBeamModelVersionRecord(nextBeamViewer);
|
|
return [...previousVersions.filter((version) => !isSameBeamVersionRecord(version, nextRecord)), nextRecord].sort(
|
|
(first, second) => first.version - second.version
|
|
);
|
|
};
|
|
|
|
export const mergeBeamModelVersionRecordLists = (
|
|
firstVersions: TBeamModelVersionRecord[] = [],
|
|
secondVersions: TBeamModelVersionRecord[] = []
|
|
): TBeamModelVersionRecord[] => {
|
|
const byKey = new Map<string, TBeamModelVersionRecord>();
|
|
[...firstVersions, ...secondVersions].forEach((version) => {
|
|
const key = version.versionId || `version-${version.version}`;
|
|
const previous = byKey.get(key);
|
|
byKey.set(key, { ...(previous || {}), ...version });
|
|
});
|
|
return [...byKey.values()]
|
|
.filter((version) => typeof version?.version === "number" && Number.isFinite(version.version))
|
|
.sort((first, second) => first.version - second.version);
|
|
};
|
|
|
|
export const syncCurrentBeamVersionRecord = (
|
|
beamViewer: TBeamViewerAttachment,
|
|
attachment?: TIssueAttachment
|
|
): TBeamViewerAttachment => {
|
|
const currentRecord = createBeamModelVersionRecord(beamViewer, { attachment });
|
|
const versions = getBeamModelVersionRecords(beamViewer, attachment).map((version) => {
|
|
if (!isSameBeamVersionRecord(version, currentRecord)) return version;
|
|
return Object.assign({}, version, currentRecord);
|
|
});
|
|
return { ...beamViewer, versions };
|
|
};
|
|
|
|
export const getBeamVersionViewerUrl = (version: TBeamModelVersionRecord): string | undefined => {
|
|
if (version.viewerUrl) return version.viewerUrl;
|
|
if (!version.conversion && version.previewAvailable && version.src) {
|
|
return buildBeamViewerUrl({
|
|
name: version.originalFilename,
|
|
settingsSrc: version.sourceSrc || version.src,
|
|
src: version.src,
|
|
type: version.type,
|
|
});
|
|
}
|
|
if (version.conversion?.status !== "ready" || !version.conversion.artifactSrc) return undefined;
|
|
const artifactType = version.conversion.artifactType || version.conversion.targetFormat || "gltf";
|
|
return buildBeamViewerUrl({
|
|
name: version.originalFilename,
|
|
settingsSrc: version.conversion.sourceSrc || version.src,
|
|
src:
|
|
toBeamAbsoluteUrl(version.conversion.artifactSrc, version.conversion.updatedAt) || version.conversion.artifactSrc,
|
|
type: artifactType,
|
|
});
|
|
};
|