chore: checkpoint before cross assistant work
This commit is contained in:
@@ -1,30 +1,57 @@
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { TIssueAttachment } from "@plane/types";
|
||||
|
||||
export type TBeamViewerAttachment = {
|
||||
backend: "beam-viewer-local" | "beam-viewer";
|
||||
conversion?: {
|
||||
artifactSrc?: string;
|
||||
artifactType?: string;
|
||||
componentTreeRequired: boolean;
|
||||
message?: string;
|
||||
metadataSrc?: string;
|
||||
sourceFormat: string;
|
||||
sourceSrc?: string;
|
||||
status: "conversion_required" | "processing" | "ready" | "failed";
|
||||
targetFormat: "glb" | "xkt";
|
||||
updatedAt?: string;
|
||||
};
|
||||
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;
|
||||
sha256?: string;
|
||||
size: number;
|
||||
src: string;
|
||||
type: string;
|
||||
uploadedBy?: string;
|
||||
uploadedAt: string;
|
||||
viewerUrl?: string;
|
||||
version: number;
|
||||
versionId?: string;
|
||||
viewerUrl?: string | null;
|
||||
};
|
||||
|
||||
export type TBeamViewerAttachment = {
|
||||
assetId?: string;
|
||||
backend: "beam-viewer-local" | "beam-viewer";
|
||||
conversion?: TBeamViewerConversion;
|
||||
downloadUrl: string;
|
||||
originalFilename: string;
|
||||
previewAvailable: boolean;
|
||||
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;
|
||||
@@ -34,11 +61,15 @@ export type TBeamConversionStatus = {
|
||||
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 const BEAM_VIEWER_OPEN_EVENT = "nodedc:beam-viewer-open";
|
||||
@@ -160,13 +191,120 @@ export const getBeamViewerAttachment = (attachment: TIssueAttachment | undefined
|
||||
return beamViewer;
|
||||
};
|
||||
|
||||
const getAttachmentVersionNumber = (attachment: TIssueAttachment | undefined): number => {
|
||||
const rawVersion = attachment?.attributes.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,
|
||||
sha256: beamViewer.sha256,
|
||||
size: options.size ?? beamViewer.conversion?.size ?? Number(options.attachment?.attributes.size ?? 0),
|
||||
src: beamViewer.src,
|
||||
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))
|
||||
.toSorted((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].toSorted(
|
||||
(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?.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,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachment): Promise<TBeamConversionStatus> => {
|
||||
const statusUrl = new URL("/api/conversions/status", `${getBeamApiBaseUrl()}/`);
|
||||
statusUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src));
|
||||
|
||||
const response = await fetch(statusUrl.toString());
|
||||
if (!response.ok) {
|
||||
throw new Error(`Beam conversion status failed: HTTP ${response.status}`);
|
||||
throw new Error(`BIM conversion status failed: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as TBeamConversionStatus;
|
||||
@@ -180,16 +318,19 @@ export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachmen
|
||||
export const uploadBeamModelFile = (
|
||||
file: File,
|
||||
options: {
|
||||
assetId?: string;
|
||||
issueId?: string;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
projectId?: string;
|
||||
uploadedBy?: string;
|
||||
version?: number;
|
||||
workspaceSlug?: string;
|
||||
} = {}
|
||||
): Promise<TBeamViewerAttachment> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const type = getBeamModelTypeFromName(file.name);
|
||||
if (!type) {
|
||||
reject(new Error("Формат модели не поддерживается Beam Viewer"));
|
||||
reject(new Error("Формат модели не поддерживается BIM Viewer"));
|
||||
return;
|
||||
}
|
||||
const directViewerType = getBeamDirectModelTypeFromName(file.name);
|
||||
@@ -201,64 +342,81 @@ export const uploadBeamModelFile = (
|
||||
"projectId",
|
||||
getUploadGroupId([options.workspaceSlug, options.projectId, options.issueId])
|
||||
);
|
||||
if (options.assetId) uploadUrl.searchParams.set("assetId", options.assetId);
|
||||
if (options.version) uploadUrl.searchParams.set("version", String(options.version));
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", uploadUrl.toString());
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
xhr.upload.onprogress = (event) => {
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (!event.lengthComputable || !options.onUploadProgress) return;
|
||||
options.onUploadProgress({
|
||||
loaded: event.loaded,
|
||||
progress: event.loaded / event.total,
|
||||
total: event.total,
|
||||
} as AxiosProgressEvent);
|
||||
};
|
||||
xhr.onerror = () => reject(new Error("Не удалось загрузить модель в Beam Viewer"));
|
||||
xhr.onload = () => {
|
||||
});
|
||||
xhr.addEventListener("error", () => reject(new Error("Не удалось загрузить модель в BIM Viewer")));
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
reject(new Error(xhr.responseText || `Beam Viewer upload failed: HTTP ${xhr.status}`));
|
||||
reject(new Error(xhr.responseText || `BIM Viewer upload failed: HTTP ${xhr.status}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText) as {
|
||||
assetId?: string;
|
||||
conversion?: TBeamViewerAttachment["conversion"];
|
||||
originalFilename?: string;
|
||||
sha256?: string;
|
||||
src?: string;
|
||||
uploadedAt?: string;
|
||||
version?: number;
|
||||
versionId?: string;
|
||||
};
|
||||
if (!response.src) {
|
||||
reject(new Error("Beam Viewer не вернул путь к загруженной модели"));
|
||||
reject(new Error("BIM Viewer не вернул путь к загруженной модели"));
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = new URL(response.src.startsWith("/") ? response.src : `/${response.src}`, `${apiBaseUrl}/`);
|
||||
const baseAttachment: TBeamViewerAttachment = {
|
||||
assetId: response.assetId ?? options.assetId,
|
||||
backend: "beam-viewer-local",
|
||||
downloadUrl: downloadUrl.toString(),
|
||||
originalFilename: file.name,
|
||||
originalFilename: response.originalFilename || file.name,
|
||||
previewAvailable: !!directViewerType,
|
||||
sha256: response.sha256,
|
||||
src: downloadUrl.toString(),
|
||||
type,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
uploadedBy: options.uploadedBy,
|
||||
uploadedAt: response.uploadedAt || new Date().toISOString(),
|
||||
version: response.version ?? options.version ?? 1,
|
||||
versionId: response.versionId,
|
||||
};
|
||||
|
||||
if (!directViewerType) {
|
||||
resolve({
|
||||
const nextAttachment: TBeamViewerAttachment = {
|
||||
...baseAttachment,
|
||||
conversion: {
|
||||
...(response.conversion ?? {}),
|
||||
...response.conversion,
|
||||
componentTreeRequired: true,
|
||||
message:
|
||||
response.conversion?.message ??
|
||||
"Оригинальный STEP загружен. Preview появится после подготовки GLB и дерева компонентов.",
|
||||
"Оригинальный STEP загружен. Просмотр появится после подготовки модели и дерева компонентов.",
|
||||
sourceFormat: type,
|
||||
status: response.conversion?.status ?? "conversion_required",
|
||||
targetFormat: response.conversion?.targetFormat ?? "glb",
|
||||
targetFormat: response.conversion?.targetFormat ?? "xkt",
|
||||
},
|
||||
};
|
||||
resolve({
|
||||
...nextAttachment,
|
||||
versions: [createBeamModelVersionRecord(nextAttachment, { size: file.size })],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
const nextAttachment: TBeamViewerAttachment = {
|
||||
...baseAttachment,
|
||||
viewerUrl: buildBeamViewerUrl({
|
||||
name: file.name,
|
||||
@@ -266,10 +424,38 @@ export const uploadBeamModelFile = (
|
||||
src: downloadUrl.toString(),
|
||||
type: directViewerType,
|
||||
}),
|
||||
};
|
||||
|
||||
resolve({
|
||||
...nextAttachment,
|
||||
versions: [createBeamModelVersionRecord(nextAttachment, { size: file.size })],
|
||||
});
|
||||
} catch (_error) {
|
||||
reject(new Error("Beam Viewer вернул некорректный ответ"));
|
||||
reject(new Error("BIM Viewer вернул некорректный ответ"));
|
||||
}
|
||||
};
|
||||
});
|
||||
xhr.send(file);
|
||||
});
|
||||
|
||||
const deleteBeamStorageResource = async (
|
||||
endpoint: "/api/uploads/asset" | "/api/uploads/version",
|
||||
payload: unknown
|
||||
): Promise<void> => {
|
||||
const response = await fetch(new URL(endpoint, `${getBeamApiBaseUrl()}/`).toString(), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error((await response.text()) || `BIM storage delete failed: HTTP ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBeamModelAsset = async (beamViewer: TBeamViewerAttachment): Promise<void> =>
|
||||
deleteBeamStorageResource("/api/uploads/asset", beamViewer);
|
||||
|
||||
export const deleteBeamModelVersion = async (version: TBeamModelVersionRecord): Promise<void> =>
|
||||
deleteBeamStorageResource("/api/uploads/version", version);
|
||||
|
||||
Reference in New Issue
Block a user