diff options
Diffstat (limited to 'apps/web/src/services/storage/migrations/transformers')
6 files changed, 357 insertions, 368 deletions
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts index c4b64e50..065a6b34 100644 --- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts @@ -1,17 +1,24 @@ -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; -const DEFAULT_FPS = 30; -import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; -import type { MediaAssetData } from "@/services/storage/types"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -interface LegacyTimelineData { - tracks: unknown[]; - lastModified: string; +// Frozen snapshots of v2-era defaults. See ./README.md. +const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10; +const DEFAULT_BACKGROUND_COLOR = "#000000"; +const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 }; +const DEFAULT_FPS = 30; + +type LegacyMediaType = "image" | "video" | "audio"; + +export interface V1ToV2Context { + legacyTracksBySceneId: Record<string, unknown[]>; + mediaTypesById: Record<string, LegacyMediaType>; } +const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = { + legacyTracksBySceneId: {}, + mediaTypesById: {}, +}; + interface LegacyMediaElement { type: "media"; mediaId: string; @@ -146,21 +153,13 @@ interface V2AudioTrack { type V2TimelineTrack = V2VideoTrack | V2TextTrack | V2AudioTrack; -export interface TransformV1ToV2Options { - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -} - -export async function transformProjectV1ToV2({ +export function transformProjectV1ToV2({ project, - options = {}, + context = EMPTY_V1_TO_V2_CONTEXT, }: { project: ProjectRecord; - options?: TransformV1ToV2Options; -}): Promise<MigrationResult<ProjectRecord>> { + context?: V1ToV2Context; +}): MigrationResult<ProjectRecord> { const projectId = getProjectId({ project }); if (!projectId) { return { project, skipped: true, reason: "no project id" }; @@ -170,27 +169,23 @@ export async function transformProjectV1ToV2({ return { project, skipped: true, reason: "already v2" }; } - const migratedProject = await migrateProject({ + const migratedProject = migrateProject({ project, projectId, - loadMediaAsset: options.loadMediaAsset, + context, }); return { project: migratedProject, skipped: false }; } -async function migrateProject({ +function migrateProject({ project, projectId, - loadMediaAsset, + context, }: { project: ProjectRecord; projectId: string; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -}): Promise<ProjectRecord> { + context: V1ToV2Context; +}): ProjectRecord { const createdAt = normalizeDateString({ value: project.createdAt }); const updatedAt = normalizeDateString({ value: project.updatedAt }); const metadataValue = project.metadata; @@ -217,42 +212,35 @@ async function migrateProject({ ? project.bookmarks : null; - const migratedScenes = await Promise.all( - scenes.map(async (scene) => { - if (!isRecord(scene)) { - return scene; - } - - const sceneId = getStringValue({ value: scene.id }); - if (!sceneId) { - return scene; - } + const migratedScenes = scenes.map((scene) => { + if (!isRecord(scene)) { + return scene; + } - const existingTracks = scene.tracks; - const shouldLoadTracks = - !Array.isArray(existingTracks) || existingTracks.length === 0; + const sceneId = getStringValue({ value: scene.id }); + if (!sceneId) { + return scene; + } - if (!shouldLoadTracks) { - return scene; - } + const existingTracks = scene.tracks; + const shouldLoadTracks = + !Array.isArray(existingTracks) || existingTracks.length === 0; - const tracks = await loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain: scene.isMain === true, - }); + if (!shouldLoadTracks) { + return scene; + } - const transformedTracks = await transformTracks({ - tracks, - loadMediaAsset, - }); + const tracks = context.legacyTracksBySceneId[sceneId] ?? []; + const transformedTracks = transformTracks({ + tracks, + context, + }); - return { - ...scene, - tracks: transformedTracks, - }; - }), - ); + return { + ...scene, + tracks: transformedTracks, + }; + }); const normalizedScenes = applyLegacyBookmarks({ scenes: migratedScenes, @@ -305,183 +293,126 @@ async function migrateProject({ }; } -async function loadTracksFromLegacyDB({ - projectId, - sceneId, - isMain, -}: { - projectId: string; - sceneId: string; - isMain: boolean; -}): Promise<unknown[]> { - if (typeof indexedDB === "undefined") { - return []; - } - - const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`; - const projectDbName = `video-editor-timelines-${projectId}`; - - const adapter = new IndexedDBAdapter<LegacyTimelineData>( - sceneDbName, - "timeline", - 1, - ); - - let data = await adapter.get("timeline"); - - if (!data && isMain) { - const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>( - projectDbName, - "timeline", - 1, - ); - data = await projectAdapter.get("timeline"); - } - - if (!data || !Array.isArray(data.tracks)) { - return []; - } - - return data.tracks; -} - -async function transformTracks({ +function transformTracks({ tracks, - loadMediaAsset, + context, }: { tracks: unknown[]; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; -}): Promise<V2TimelineTrack[]> { + context: V1ToV2Context; +}): V2TimelineTrack[] { if (!Array.isArray(tracks)) { return []; } let isFirstVideoTrackFound = false; - const transformedTracks: (V2TimelineTrack | null)[] = []; - - for (const track of tracks) { + const transformedTracks = tracks.map((track): V2TimelineTrack | null => { if (!isRecord(track)) { - transformedTracks.push(null); - continue; + return null; } const trackType = track.type; if (trackType === "media") { - const videoTrack = await transformMediaTrack({ + const isMain = !isFirstVideoTrackFound; + isFirstVideoTrackFound = true; + const videoTrack = transformMediaTrack({ track: track as LegacyMediaTrack, - loadMediaAsset, - isMain: !isFirstVideoTrackFound, + context, + isMain, }); - isFirstVideoTrackFound = true; - transformedTracks.push(videoTrack); - continue; + return videoTrack; } if (trackType === "text") { - transformedTracks.push(transformTextTrack({ track })); - continue; + return transformTextTrack({ track }); } if (trackType === "audio") { - transformedTracks.push(transformAudioTrack({ track })); - continue; + return transformAudioTrack({ track }); } - transformedTracks.push(null); - } + return null; + }); return transformedTracks.filter( (track): track is V2TimelineTrack => track !== null, ); } -async function transformMediaTrack({ +function transformMediaTrack({ track, - loadMediaAsset, + context, isMain, }: { track: LegacyMediaTrack; - loadMediaAsset?: ({ - mediaId, - }: { - mediaId: string; - }) => Promise<MediaAssetData | null>; + context: V1ToV2Context; isMain: boolean; -}): Promise<V2VideoTrack> { +}): V2VideoTrack { const elements = Array.isArray(track.elements) ? track.elements : []; - const transformedElements = await Promise.all( - elements.map(async (element) => { - if (!isRecord(element) || element.type !== "media") { - return null; - } - - const mediaElement = element as LegacyMediaElement; - const mediaId = getStringValue({ value: mediaElement.mediaId }); - if (!mediaId) { - return null; - } + const transformedElements = elements.map((element) => { + if (!isRecord(element) || element.type !== "media") { + return null; + } - let mediaType: "video" | "image" = "video"; - if (loadMediaAsset) { - const mediaAsset = await loadMediaAsset({ mediaId }); - if (mediaAsset) { - mediaType = mediaAsset.type === "image" ? "image" : "video"; - } - } + const mediaElement = element as LegacyMediaElement; + const mediaId = getStringValue({ value: mediaElement.mediaId }); + if (!mediaId) { + return null; + } - const defaultTransform: V2Transform = { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }; + let mediaType: "video" | "image" = "video"; + const storedMediaType = context.mediaTypesById[mediaId]; + if (storedMediaType) { + mediaType = storedMediaType === "image" ? "image" : "video"; + } - const muted = mediaElement.muted === true; + const defaultTransform: V2Transform = { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }; - if (mediaType === "image") { - const imageElement: V2ImageElement = { - id: getStringValue({ value: element.id, fallback: "" }), - name: getStringValue({ value: element.name, fallback: "" }), - type: "image", - mediaId, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ - value: element.startTime, - fallback: 0, - }), - trimStart: getNumberValue({ - value: element.trimStart, - fallback: 0, - }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), - hidden: false, - transform: defaultTransform, - opacity: 1, - }; - return imageElement; - } + const muted = mediaElement.muted === true; - const videoElement: V2VideoElement = { + if (mediaType === "image") { + const imageElement: V2ImageElement = { id: getStringValue({ value: element.id, fallback: "" }), name: getStringValue({ value: element.name, fallback: "" }), - type: "video", + type: "image", mediaId, - muted, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ + value: element.startTime, + fallback: 0, + }), + trimStart: getNumberValue({ + value: element.trimStart, + fallback: 0, + }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), hidden: false, transform: defaultTransform, opacity: 1, - duration: getNumberValue({ value: element.duration, fallback: 0 }), - startTime: getNumberValue({ value: element.startTime, fallback: 0 }), - trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), - trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), }; - return videoElement; - }), - ); + return imageElement; + } + + const videoElement: V2VideoElement = { + id: getStringValue({ value: element.id, fallback: "" }), + name: getStringValue({ value: element.name, fallback: "" }), + type: "video", + mediaId, + muted, + hidden: false, + transform: defaultTransform, + opacity: 1, + duration: getNumberValue({ value: element.duration, fallback: 0 }), + startTime: getNumberValue({ value: element.startTime, fallback: 0 }), + trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }), + trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }), + }; + return videoElement; + }); const validElements = transformedElements.filter( (element): element is V2VideoElement | V2ImageElement => element !== null, diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts index 5f3c01e9..84b2704a 100644 --- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts @@ -1,67 +1,69 @@ -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
-import type { MigrationResult, ProjectRecord } from "./types";
-import { getProjectId, isRecord } from "./utils";
-
-export function transformProjectV14ToV15({
- project,
-}: {
- project: ProjectRecord;
-}): MigrationResult<ProjectRecord> {
- if (!getProjectId({ project })) {
- return { project, skipped: true, reason: "no project id" };
- }
-
- if (typeof project.version === "number" && project.version >= 15) {
- return { project, skipped: true, reason: "already v15" };
- }
-
- const migratedProject = backfillStickerIntrinsicDimensions({ project });
-
- return {
- project: { ...migratedProject, version: 15 },
- skipped: false,
- };
-}
-
-function backfillStickerIntrinsicDimensions({
- project,
-}: {
- project: ProjectRecord;
-}): ProjectRecord {
- const scenesValue = project.scenes;
- if (!Array.isArray(scenesValue)) return project;
-
- const migratedScenes = scenesValue.map((scene) => {
- if (!isRecord(scene)) return scene;
- const tracksValue = scene.tracks;
- if (!Array.isArray(tracksValue)) return scene;
-
- const migratedTracks = tracksValue.map((track) => {
- if (!isRecord(track)) return track;
- const elementsValue = track.elements;
- if (!Array.isArray(elementsValue)) return track;
-
- const migratedElements = elementsValue.map((element) => {
- if (!isRecord(element)) return element;
- if (element.type !== "sticker") return element;
- if (
- typeof element.intrinsicWidth === "number" &&
- typeof element.intrinsicHeight === "number"
- ) {
- return element;
- }
- return {
- ...element,
- intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK,
- intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK,
- };
- });
-
- return { ...track, elements: migratedElements };
- });
-
- return { ...scene, tracks: migratedTracks };
- });
-
- return { ...project, scenes: migratedScenes };
-}
+import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +// Frozen snapshot of the v15-era fallback. See ./README.md. +const STICKER_INTRINSIC_SIZE_FALLBACK = 200; + +export function transformProjectV14ToV15({ + project, +}: { + project: ProjectRecord; +}): MigrationResult<ProjectRecord> { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 15) { + return { project, skipped: true, reason: "already v15" }; + } + + const migratedProject = backfillStickerIntrinsicDimensions({ project }); + + return { + project: { ...migratedProject, version: 15 }, + skipped: false, + }; +} + +function backfillStickerIntrinsicDimensions({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) return scene; + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) return track; + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) return element; + if (element.type !== "sticker") return element; + if ( + typeof element.intrinsicWidth === "number" && + typeof element.intrinsicHeight === "number" + ) { + return element; + } + return { + ...element, + intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK, + intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK, + }; + }); + + return { ...track, elements: migratedElements }; + }); + + return { ...scene, tracks: migratedTracks }; + }); + + return { ...project, scenes: migratedScenes }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts index 3ef6cde9..660d91ee 100644 --- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts @@ -1,105 +1,113 @@ -import type { MigrationResult, ProjectRecord } from "./types";
-import { VOLUME_DB_MIN } from "@/timeline/audio-constants";
-import { clampDb } from "@/timeline/audio-state";
-import { getProjectId, isRecord } from "./utils";
-
-export function transformProjectV17ToV18({
- project,
-}: {
- project: ProjectRecord;
-}): MigrationResult<ProjectRecord> {
- if (!getProjectId({ project })) {
- return { project, skipped: true, reason: "no project id" };
- }
-
- if (typeof project.version === "number" && project.version >= 18) {
- return { project, skipped: true, reason: "already v18" };
- }
-
- return {
- project: {
- ...migrateElementVolumes({ project }),
- version: 18,
- },
- skipped: false,
- };
-}
-
-function migrateElementVolumes({
- project,
-}: {
- project: ProjectRecord;
-}): ProjectRecord {
- const scenesValue = project.scenes;
- if (!Array.isArray(scenesValue)) {
- return project;
- }
-
- const migratedScenes = scenesValue.map((scene) => {
- if (!isRecord(scene)) {
- return scene;
- }
-
- const tracksValue = scene.tracks;
- if (!Array.isArray(tracksValue)) {
- return scene;
- }
-
- const migratedTracks = tracksValue.map((track) => {
- if (!isRecord(track)) {
- return track;
- }
-
- const elementsValue = track.elements;
- if (!Array.isArray(elementsValue)) {
- return track;
- }
-
- const migratedElements = elementsValue.map((element) => {
- if (!isRecord(element)) {
- return element;
- }
-
- if (element.type !== "audio" && element.type !== "video") {
- return element;
- }
-
- const legacyVolume = element.volume;
- return {
- ...element,
- volume:
- typeof legacyVolume === "number"
- ? linearGainToDb(legacyVolume)
- : 0,
- };
- });
-
- return {
- ...track,
- elements: migratedElements,
- };
- });
-
- return {
- ...scene,
- tracks: migratedTracks,
- };
- });
-
- return {
- ...project,
- scenes: migratedScenes,
- };
-}
-
-function linearGainToDb(gain: number): number {
- if (!Number.isFinite(gain)) {
- return 0;
- }
-
- if (gain <= 0) {
- return VOLUME_DB_MIN;
- }
-
- return clampDb(20 * Math.log10(gain));
-}
+import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +// Frozen snapshots of v18-era volume bounds. See ./README.md. +const VOLUME_DB_MIN = -60; +const VOLUME_DB_MAX = 20; + +export function transformProjectV17ToV18({ + project, +}: { + project: ProjectRecord; +}): MigrationResult<ProjectRecord> { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 18) { + return { project, skipped: true, reason: "already v18" }; + } + + return { + project: { + ...migrateElementVolumes({ project }), + version: 18, + }, + skipped: false, + }; +} + +function migrateElementVolumes({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) { + return element; + } + + if (element.type !== "audio" && element.type !== "video") { + return element; + } + + const legacyVolume = element.volume; + return { + ...element, + volume: + typeof legacyVolume === "number" ? linearGainToDb(legacyVolume) : 0, + }; + }); + + return { + ...track, + elements: migratedElements, + }; + }); + + return { + ...scene, + tracks: migratedTracks, + }; + }); + + return { + ...project, + scenes: migratedScenes, + }; +} + +function linearGainToDb(gain: number): number { + if (!Number.isFinite(gain)) { + return 0; + } + + if (gain <= 0) { + return VOLUME_DB_MIN; + } + + return clampDb(20 * Math.log10(gain)); +} + +function clampDb(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(VOLUME_DB_MAX, Math.max(VOLUME_DB_MIN, value)); +} diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts index 6c30e5c8..14b7a092 100644 --- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts +++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts @@ -1,7 +1,8 @@ -import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; +// Frozen snapshot of the v21-era divisor. See ./README.md. +const INTENSITY_TO_SIGMA_DIVISOR = 5; const LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY = 50; export function transformProjectV20ToV21({ diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts index 5e3adc33..24cc86e2 100644 --- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts +++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts @@ -1,7 +1,8 @@ import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -import { TICKS_PER_SECOND } from "@/wasm"; +// Frozen snapshot of the v23-era tick rate. See ./README.md. +const TICKS_PER_SECOND = 120_000; const ARBITRARY_FPS_DENOMINATOR = 1_000_000; const STANDARD_FRAME_RATES = [ { value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 }, @@ -193,7 +194,9 @@ function migrateAnimationChannel({ channel }: { channel: unknown }): unknown { return { ...channel, - keys: channel.keys.map((keyframe) => migrateAnimationKeyframe({ keyframe })), + keys: channel.keys.map((keyframe) => + migrateAnimationKeyframe({ keyframe }), + ), }; } @@ -290,7 +293,8 @@ function migrateFrameRate({ fps }: { fps: unknown }): unknown { } const standardFrameRate = STANDARD_FRAME_RATES.find( - (candidate) => Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE, + (candidate) => + Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE, ); if (standardFrameRate) { return { diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts index 820b0087..3b118ee7 100644 --- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts +++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts @@ -1,7 +1,50 @@ -import { parseCustomMaskPath } from "@/masks/custom-path"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; +interface CustomMaskPathPoint { + id: string; + x: number; + y: number; + inX: number; + inY: number; + outX: number; + outY: number; +} + +function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint { + if (!value || typeof value !== "object") { + return false; + } + + const candidate = value as Record<string, unknown>; + return ( + typeof candidate.id === "string" && + typeof candidate.x === "number" && + typeof candidate.y === "number" && + typeof candidate.inX === "number" && + typeof candidate.inY === "number" && + typeof candidate.outX === "number" && + typeof candidate.outY === "number" + ); +} + +function parseCustomMaskPath({ + path, +}: { + path: string; +}): CustomMaskPathPoint[] { + if (!path) { + return []; + } + + try { + const parsed = JSON.parse(path); + return Array.isArray(parsed) ? parsed.filter(isCustomMaskPathPoint) : []; + } catch { + return []; + } +} + export function transformProjectV26ToV27({ project, }: { |
