feat: remove default background scene and add scene management
This commit is contained in:
parent
b84e699e9c
commit
d8f446580c
|
|
@ -14,6 +14,7 @@ import { cn } from "@/lib/utils";
|
|||
import { formatTimeCode } from "@/lib/time";
|
||||
import { EditableTimecode } from "@/components/ui/editable-timecode";
|
||||
import { useFrameCache } from "@/hooks/use-frame-cache";
|
||||
import { useSceneStore } from "@/stores/scene-store";
|
||||
import {
|
||||
DEFAULT_CANVAS_SIZE,
|
||||
DEFAULT_FPS,
|
||||
|
|
@ -43,6 +44,7 @@ export function PreviewPanel() {
|
|||
const { currentTime, toggle, setCurrentTime } = usePlaybackStore();
|
||||
const { isPlaying, volume, muted } = usePlaybackStore();
|
||||
const { activeProject } = useProjectStore();
|
||||
const { currentScene } = useSceneStore();
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { getCachedFrame, cacheFrame, invalidateCache, preRenderNearbyFrames } =
|
||||
|
|
@ -219,11 +221,10 @@ export function PreviewPanel() {
|
|||
};
|
||||
}, [dragState, previewDimensions, canvasSize, updateTextElement]);
|
||||
|
||||
// Invalidate cache when timeline changes
|
||||
// Clear the frame cache when background settings change since they affect rendering
|
||||
useEffect(() => {
|
||||
invalidateCache();
|
||||
}, [
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject?.backgroundColor,
|
||||
activeProject?.backgroundType,
|
||||
|
|
@ -494,7 +495,8 @@ export function PreviewPanel() {
|
|||
currentTime,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject
|
||||
activeProject,
|
||||
currentScene?.id
|
||||
);
|
||||
if (cachedFrame) {
|
||||
mainCtx.putImageData(cachedFrame, 0, 0);
|
||||
|
|
@ -532,7 +534,9 @@ export function PreviewPanel() {
|
|||
});
|
||||
|
||||
return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
|
||||
}
|
||||
},
|
||||
currentScene?.id,
|
||||
3
|
||||
);
|
||||
} else {
|
||||
// Small lookahead while playing
|
||||
|
|
@ -567,6 +571,7 @@ export function PreviewPanel() {
|
|||
|
||||
return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
|
||||
},
|
||||
currentScene?.id,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
|
@ -644,7 +649,14 @@ export function PreviewPanel() {
|
|||
displayWidth,
|
||||
displayHeight
|
||||
);
|
||||
cacheFrame(currentTime, imageData, tracks, mediaFiles, activeProject);
|
||||
cacheFrame(
|
||||
currentTime,
|
||||
imageData,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject,
|
||||
currentScene?.id
|
||||
);
|
||||
|
||||
// Blit offscreen to visible canvas
|
||||
mainCtx.clearRect(0, 0, displayWidth, displayHeight);
|
||||
|
|
|
|||
|
|
@ -1,64 +1,198 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSceneStore } from "@/stores/scene-store";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
export function ScenesView({ children }: { children: React.ReactNode }) {
|
||||
const { scenes, currentScene, switchToScene } = useSceneStore();
|
||||
|
||||
const handleSceneSwitch = async (sceneId: string) => {
|
||||
try {
|
||||
await switchToScene({ sceneId });
|
||||
} catch (error) {
|
||||
console.error("Failed to switch scene:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Scenes</SheetTitle>
|
||||
<SheetDescription>
|
||||
Switch between scenes in your project
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="py-4">
|
||||
{scenes.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No scenes available
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{scenes.map((scene) => (
|
||||
<Button
|
||||
key={scene.id}
|
||||
variant={
|
||||
currentScene?.id === scene.id ? "default" : "outline"
|
||||
}
|
||||
className="w-full justify-between"
|
||||
onClick={() => handleSceneSwitch(scene.id)}
|
||||
>
|
||||
<span>{scene.name}</span>
|
||||
{currentScene?.id === scene.id && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSceneStore } from "@/stores/scene-store";
|
||||
import { Check, ListCheck, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function ScenesView({ children }: { children: React.ReactNode }) {
|
||||
const { scenes, currentScene, switchToScene, deleteScene } = useSceneStore();
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
const [selectedScenes, setSelectedScenes] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleSceneSwitch = async (sceneId: string) => {
|
||||
if (isSelectMode) {
|
||||
toggleSceneSelection(sceneId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await switchToScene({ sceneId });
|
||||
} catch (error) {
|
||||
console.error("Failed to switch scene:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSceneSelection = (sceneId: string) => {
|
||||
setSelectedScenes((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sceneId)) {
|
||||
newSet.delete(sceneId);
|
||||
} else {
|
||||
newSet.add(sceneId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectMode = () => {
|
||||
setIsSelectMode(!isSelectMode);
|
||||
setSelectedScenes(new Set());
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
for (const sceneId of selectedScenes) {
|
||||
const scene = scenes.find((s) => s.id === sceneId);
|
||||
if (scene && !scene.isMain) {
|
||||
try {
|
||||
await deleteScene({ sceneId });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete scene:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
setSelectedScenes(new Set());
|
||||
setIsSelectMode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{isSelectMode ? `Select scenes (${selectedScenes.size})` : "Scenes"}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{isSelectMode
|
||||
? "Select scenes to delete"
|
||||
: "Switch between scenes in your project"}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="py-4 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="rounded-md"
|
||||
variant={isSelectMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleSelectMode}
|
||||
>
|
||||
<ListCheck />
|
||||
{isSelectMode ? "Cancel" : "Select"}
|
||||
</Button>
|
||||
{isSelectMode && (
|
||||
<DeleteDialog
|
||||
count={selectedScenes.size}
|
||||
onDelete={handleDeleteSelected}
|
||||
disabled={Array.from(selectedScenes).some(
|
||||
(id) => scenes.find((s) => s.id === id)?.isMain
|
||||
)}
|
||||
>
|
||||
<Button className="rounded-md" variant="destructive" size="sm">
|
||||
<Trash2 />
|
||||
Delete ({selectedScenes.size})
|
||||
</Button>
|
||||
</DeleteDialog>
|
||||
)}
|
||||
</div>
|
||||
{scenes.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No scenes available
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{scenes.map((scene) => (
|
||||
<Button
|
||||
key={scene.id}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between font-normal",
|
||||
currentScene?.id === scene.id &&
|
||||
!isSelectMode &&
|
||||
"border-primary !text-primary",
|
||||
isSelectMode &&
|
||||
selectedScenes.has(scene.id) &&
|
||||
"bg-accent border-foreground/30"
|
||||
)}
|
||||
onClick={() => handleSceneSwitch(scene.id)}
|
||||
>
|
||||
<span>{scene.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{((isSelectMode && selectedScenes.has(scene.id)) ||
|
||||
(!isSelectMode && currentScene?.id === scene.id)) && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
count,
|
||||
onDelete,
|
||||
disabled,
|
||||
children,
|
||||
}: {
|
||||
count: number;
|
||||
onDelete: () => void;
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
onDelete();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Scenes</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete {count} scene
|
||||
{count === 1 ? "" : "s"}? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={disabled}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|||
import { TimelineTrack } from "@/types/timeline";
|
||||
import { MediaFile } from "@/types/media";
|
||||
import { TProject } from "@/types/project";
|
||||
import { useSceneStore } from "@/stores/scene-store";
|
||||
|
||||
interface CacheSegment {
|
||||
startTime: number;
|
||||
|
|
@ -22,7 +23,8 @@ interface TimelineCacheIndicatorProps {
|
|||
time: number,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
) => "cached" | "not-cached";
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,8 @@ export function TimelineCacheIndicator({
|
|||
activeProject,
|
||||
getRenderStatus,
|
||||
}: TimelineCacheIndicatorProps) {
|
||||
const { currentScene } = useSceneStore();
|
||||
|
||||
// Calculate cache segments by sampling the timeline
|
||||
const calculateCacheSegments = (): CacheSegment[] => {
|
||||
const segments: CacheSegment[] = [];
|
||||
|
|
@ -49,7 +53,13 @@ export function TimelineCacheIndicator({
|
|||
for (let i = 0; i <= totalSamples; i++) {
|
||||
const time = i / sampleRate;
|
||||
const cached =
|
||||
getRenderStatus(time, tracks, mediaFiles, activeProject) === "cached";
|
||||
getRenderStatus(
|
||||
time,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject,
|
||||
currentScene?.id
|
||||
) === "cached";
|
||||
|
||||
if (!currentSegment) {
|
||||
// Start first segment
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function TimelineToolbar({
|
|||
} = useTimelineStore();
|
||||
const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
|
||||
const { toggleBookmark, isBookmarked, activeProject } = useProjectStore();
|
||||
const { currentScene } = useSceneStore();
|
||||
const { scenes, currentScene } = useSceneStore();
|
||||
|
||||
const handleSplitSelected = () => {
|
||||
if (selectedElements.length === 0) return;
|
||||
|
|
@ -359,11 +359,11 @@ export function TimelineToolbar({
|
|||
</TooltipProvider>
|
||||
</div>
|
||||
<div>
|
||||
<SplitButton>
|
||||
<SplitButton className="border border-foreground/10">
|
||||
<SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
|
||||
<SplitButtonSeparator />
|
||||
<ScenesView>
|
||||
<SplitButtonRight onClick={() => {}}>
|
||||
<SplitButtonRight disabled={scenes.length === 1} onClick={() => {}}>
|
||||
<LayersIcon />
|
||||
</SplitButtonRight>
|
||||
</ScenesView>
|
||||
|
|
|
|||
|
|
@ -92,23 +92,13 @@ export function ScenesMigrator({ children }: { children: React.ReactNode }) {
|
|||
id: generateUUID(),
|
||||
name: "Main Scene",
|
||||
isMain: true,
|
||||
isBackground: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const backgroundScene: Scene = {
|
||||
id: generateUUID(),
|
||||
name: "Background",
|
||||
isMain: false,
|
||||
isBackground: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const migratedProject: TProject = {
|
||||
...project,
|
||||
scenes: [mainScene, backgroundScene],
|
||||
scenes: [mainScene],
|
||||
currentSceneId: mainScene.id,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const buttonVariants = cva(
|
|||
"primary-gradient":
|
||||
"bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
|
||||
"bg-destructive/0 border border-destructive/25 text-destructive shadow-xs hover:bg-destructive hover:text-destructive-foreground",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:opacity-75 transition-opacity hover:text-accent-foreground",
|
||||
secondary:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-150 bg-black/20 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-250 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] p-6 z-150 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg",
|
||||
"fixed left-[50%] top-[50%] p-6 z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg",
|
||||
className
|
||||
)}
|
||||
onCloseAutoFocus={(e) => {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const SplitButtonSide = forwardRef<
|
|||
ref={ref}
|
||||
variant="text"
|
||||
className={cn(
|
||||
"h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors",
|
||||
"h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors disabled:text-muted-foreground",
|
||||
onClick
|
||||
? "hover:bg-foreground/10 hover:opacity-100 cursor-pointer"
|
||||
: "cursor-default select-text",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
time: number,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
): string => {
|
||||
// Get elements that are active at this time
|
||||
const activeElements: Array<{
|
||||
|
|
@ -117,11 +118,13 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
canvasSize: activeProject?.canvasSize,
|
||||
};
|
||||
|
||||
return JSON.stringify({
|
||||
const hash = {
|
||||
activeElements,
|
||||
projectState,
|
||||
time: Math.floor(time * cacheResolution) / cacheResolution, // Quantize time
|
||||
});
|
||||
sceneId,
|
||||
time: Math.floor(time * cacheResolution) / cacheResolution,
|
||||
};
|
||||
return JSON.stringify(hash);
|
||||
},
|
||||
[cacheResolution]
|
||||
);
|
||||
|
|
@ -132,7 +135,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
time: number,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
): boolean => {
|
||||
const frameKey = Math.floor(time * cacheResolution);
|
||||
const cached = frameCacheRef.current.get(frameKey);
|
||||
|
|
@ -143,7 +147,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
time,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject
|
||||
activeProject,
|
||||
sceneId
|
||||
);
|
||||
return cached.timelineHash === currentHash;
|
||||
},
|
||||
|
|
@ -156,21 +161,33 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
time: number,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
): ImageData | null => {
|
||||
const frameKey = Math.floor(time * cacheResolution);
|
||||
const cached = frameCacheRef.current.get(frameKey);
|
||||
|
||||
if (!cached) return null;
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentHash = getTimelineHash(
|
||||
time,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject
|
||||
activeProject,
|
||||
sceneId
|
||||
);
|
||||
console.log(cached.timelineHash === currentHash);
|
||||
if (cached.timelineHash !== currentHash) {
|
||||
// Cache is stale, remove it
|
||||
console.log(
|
||||
"Cache miss - hash mismatch:",
|
||||
JSON.stringify({
|
||||
cachedHash: cached.timelineHash.slice(0, 100),
|
||||
currentHash: currentHash.slice(0, 100),
|
||||
})
|
||||
);
|
||||
frameCacheRef.current.delete(frameKey);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -187,14 +204,16 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
imageData: ImageData,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
): void => {
|
||||
const frameKey = Math.floor(time * cacheResolution);
|
||||
const timelineHash = getTimelineHash(
|
||||
time,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject
|
||||
activeProject,
|
||||
sceneId
|
||||
);
|
||||
|
||||
// Enforce cache size limit (LRU eviction)
|
||||
|
|
@ -230,9 +249,10 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
time: number,
|
||||
tracks: TimelineTrack[],
|
||||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null
|
||||
activeProject: TProject | null,
|
||||
sceneId?: string
|
||||
): "cached" | "not-cached" => {
|
||||
return isFrameCached(time, tracks, mediaFiles, activeProject)
|
||||
return isFrameCached(time, tracks, mediaFiles, activeProject, sceneId)
|
||||
? "cached"
|
||||
: "not-cached";
|
||||
},
|
||||
|
|
@ -247,6 +267,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
mediaFiles: MediaFile[],
|
||||
activeProject: TProject | null,
|
||||
renderFunction: (time: number) => Promise<ImageData>,
|
||||
sceneId?: string,
|
||||
range: number = 3 // seconds
|
||||
) => {
|
||||
const framesToPreRender: number[] = [];
|
||||
|
|
@ -260,7 +281,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
const time = currentTime + offset;
|
||||
if (time < 0) continue;
|
||||
|
||||
if (!isFrameCached(time, tracks, mediaFiles, activeProject)) {
|
||||
if (!isFrameCached(time, tracks, mediaFiles, activeProject, sceneId)) {
|
||||
framesToPreRender.push(time);
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +297,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
for (let k = 0; k < cacheResolution; k++) {
|
||||
const t = s + k / cacheResolution;
|
||||
if (t < 0) continue;
|
||||
if (!isFrameCached(t, tracks, mediaFiles, activeProject)) {
|
||||
if (!isFrameCached(t, tracks, mediaFiles, activeProject, sceneId)) {
|
||||
expandedTimes.push(t);
|
||||
}
|
||||
}
|
||||
|
|
@ -298,7 +319,14 @@ export function useFrameCache(options: FrameCacheOptions = {}) {
|
|||
requestIdleCallback(async () => {
|
||||
try {
|
||||
const imageData = await renderFunction(time);
|
||||
cacheFrame(time, imageData, tracks, mediaFiles, activeProject);
|
||||
cacheFrame(
|
||||
time,
|
||||
imageData,
|
||||
tracks,
|
||||
mediaFiles,
|
||||
activeProject,
|
||||
sceneId
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`Pre-render failed for time ${time}:`, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ class StorageService {
|
|||
id: scene.id,
|
||||
name: scene.name,
|
||||
isMain: scene.isMain,
|
||||
isBackground: scene.isBackground,
|
||||
createdAt: scene.createdAt.toISOString(),
|
||||
updatedAt: scene.updatedAt.toISOString(),
|
||||
}));
|
||||
|
|
@ -114,7 +113,6 @@ class StorageService {
|
|||
id: scene.id,
|
||||
name: scene.name,
|
||||
isMain: scene.isMain,
|
||||
isBackground: scene.isBackground || false, // Default for legacy scenes
|
||||
createdAt: new Date(scene.createdAt),
|
||||
updatedAt: new Date(scene.updatedAt),
|
||||
})) || [];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { storageService } from "@/lib/storage/storage-service";
|
|||
import { toast } from "sonner";
|
||||
import { useMediaStore } from "./media-store";
|
||||
import { useTimelineStore } from "./timeline-store";
|
||||
import { createBackgroundScene, useSceneStore } from "./scene-store";
|
||||
import { useSceneStore } from "./scene-store";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import { CanvasSize, CanvasMode } from "@/types/editor";
|
||||
|
||||
|
|
@ -14,9 +14,8 @@ export const DEFAULT_FPS = 30;
|
|||
export function createMainScene(): Scene {
|
||||
return {
|
||||
id: generateUUID(),
|
||||
name: "Main scene",
|
||||
name: "Main Scene",
|
||||
isMain: true,
|
||||
isBackground: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
|
@ -24,7 +23,6 @@ export function createMainScene(): Scene {
|
|||
|
||||
const createDefaultProject = (name: string): TProject => {
|
||||
const mainScene = createMainScene();
|
||||
const backgroundScene = createBackgroundScene();
|
||||
|
||||
return {
|
||||
id: generateUUID(),
|
||||
|
|
@ -32,7 +30,7 @@ const createDefaultProject = (name: string): TProject => {
|
|||
thumbnail: "",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
scenes: [mainScene, backgroundScene],
|
||||
scenes: [mainScene],
|
||||
currentSceneId: mainScene.id,
|
||||
backgroundColor: "#000000",
|
||||
backgroundType: "color",
|
||||
|
|
|
|||
|
|
@ -9,29 +9,17 @@ export function getMainScene({ scenes }: { scenes: Scene[] }): Scene | null {
|
|||
return scenes.find((scene) => scene.isMain) || null;
|
||||
}
|
||||
|
||||
export function getBackgroundScene({
|
||||
scenes,
|
||||
}: {
|
||||
scenes: Scene[];
|
||||
}): Scene | null {
|
||||
return scenes.find((scene) => scene.isBackground) || null;
|
||||
}
|
||||
|
||||
export function createBackgroundScene(): Scene {
|
||||
return {
|
||||
id: generateUUID(),
|
||||
name: "Background",
|
||||
isMain: false,
|
||||
isBackground: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureBackgroundScene({ scenes }: { scenes: Scene[] }): Scene[] {
|
||||
const hasBackground = scenes.some((scene) => scene.isBackground);
|
||||
if (!hasBackground) {
|
||||
return [...scenes, createBackgroundScene()];
|
||||
function ensureMainScene(scenes: Scene[]): Scene[] {
|
||||
const hasMain = scenes.some((scene) => scene.isMain);
|
||||
if (!hasMain) {
|
||||
const mainScene: Scene = {
|
||||
id: generateUUID(),
|
||||
name: "Main scene",
|
||||
isMain: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
return [mainScene, ...scenes];
|
||||
}
|
||||
return scenes;
|
||||
}
|
||||
|
|
@ -49,6 +37,7 @@ interface SceneStore {
|
|||
name: string;
|
||||
isMain: boolean;
|
||||
}) => Promise<string>;
|
||||
deleteScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
|
||||
renameScene: ({
|
||||
sceneId,
|
||||
name,
|
||||
|
|
@ -115,6 +104,62 @@ export const useSceneStore = create<SceneStore>((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
deleteScene: async ({ sceneId }: { sceneId: string }) => {
|
||||
const { scenes, currentScene } = get();
|
||||
const sceneToDelete = scenes.find((s) => s.id === sceneId);
|
||||
|
||||
if (!sceneToDelete) {
|
||||
throw new Error("Scene not found");
|
||||
}
|
||||
|
||||
if (sceneToDelete.isMain) {
|
||||
throw new Error("Cannot delete main scene");
|
||||
}
|
||||
|
||||
const updatedScenes = scenes.filter((s) => s.id !== sceneId);
|
||||
|
||||
// Determine new current scene if we're deleting the current one
|
||||
let newCurrentScene = currentScene;
|
||||
if (currentScene?.id === sceneId) {
|
||||
newCurrentScene = getMainScene({ scenes: updatedScenes });
|
||||
}
|
||||
|
||||
// Update project
|
||||
const projectStore = useProjectStore.getState();
|
||||
const { activeProject } = projectStore;
|
||||
|
||||
if (!activeProject) {
|
||||
throw new Error("No active project");
|
||||
}
|
||||
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
scenes: updatedScenes,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
try {
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
useProjectStore.setState({ activeProject: updatedProject });
|
||||
set({
|
||||
scenes: updatedScenes,
|
||||
currentScene: newCurrentScene,
|
||||
});
|
||||
|
||||
// If we switched scenes, load the new scene's timeline
|
||||
if (newCurrentScene && newCurrentScene.id !== currentScene?.id) {
|
||||
const timelineStore = useTimelineStore.getState();
|
||||
await timelineStore.loadProjectTimeline({
|
||||
projectId: activeProject.id,
|
||||
sceneId: newCurrentScene.id,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete scene:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
renameScene: async ({ sceneId, name }: { sceneId: string; name: string }) => {
|
||||
const { scenes } = get();
|
||||
const updatedScenes = scenes.map((scene) =>
|
||||
|
|
@ -226,7 +271,7 @@ export const useSceneStore = create<SceneStore>((set, get) => ({
|
|||
scenes: Scene[];
|
||||
currentSceneId?: string;
|
||||
}) => {
|
||||
const ensuredScenes = ensureBackgroundScene({ scenes });
|
||||
const ensuredScenes = ensureMainScene(scenes);
|
||||
const currentScene = currentSceneId
|
||||
? ensuredScenes.find((s) => s.id === currentSceneId)
|
||||
: null;
|
||||
|
|
@ -241,19 +286,25 @@ export const useSceneStore = create<SceneStore>((set, get) => ({
|
|||
if (ensuredScenes.length > scenes.length) {
|
||||
const projectStore = useProjectStore.getState();
|
||||
const { activeProject } = projectStore;
|
||||
|
||||
|
||||
if (activeProject) {
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
scenes: ensuredScenes,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
storageService.saveProject({ project: updatedProject }).then(() => {
|
||||
useProjectStore.setState({ activeProject: updatedProject });
|
||||
}).catch(error => {
|
||||
console.error("Failed to save project with background scene:", error);
|
||||
});
|
||||
|
||||
storageService
|
||||
.saveProject({ project: updatedProject })
|
||||
.then(() => {
|
||||
useProjectStore.setState({ activeProject: updatedProject });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Failed to save project with background scene:",
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export interface Scene {
|
|||
id: string;
|
||||
name: string;
|
||||
isMain: boolean;
|
||||
isBackground: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue