mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: major editor overhaul (assets, properties, timeline, fonts) (#709)
* feat: major editor overhaul (assets, properties, timeline, fonts) Refactor editor core systems to standardize UI architecture and improve performance. Assets & Properties: - Replace monolithic property items with composable `Section` architecture. - Add specialized sections for Transform, Blending, and Text. - Implement `NumberField` with scrubbing and math evaluation. - Add new ColorPicker with EyeDropper and multiple format support. - Standardize asset panels using new `PanelView` layout. Fonts & Stickers: - Implement custom font atlas/sprite system for high-performance previews. - Add virtualized FontPicker with search and favorites. - Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes). - Standardize sticker IDs to `provider:value` format. Timeline & Interaction: - Convert bookmarks to rich objects with notes, colors, and duration. - Refactor drag-and-drop to use Command pattern (enabling proper undo/redo). - Add Shift modifier to disable snapping during moves/resizes. - Add new overlays for layout guides and text editing. Renderer: - Add support for multi-line text, custom line-height, and letter-spacing. - Implement global composite operation (blend modes). - Update sticker node to resolve dynamic provider IDs. Infrastructure: - Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks. - Update global styles and core UI components (Button, Input, Popover). * add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors * fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling * deleted shadcn components with errors * formatting * fix linter issues * migrate from next middleware to proxy * add missing component back * add breadcrumb back * chore: add @radix-ui/react-primitive deps * chore: more deps * chore: add missing env vars to bun-ci * next env
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export { Command } from "./base-command";
|
||||
export { BatchCommand } from "./batch-command";
|
||||
export { PreviewTracker } from "./preview-tracker";
|
||||
|
||||
export * from "./timeline";
|
||||
export * from "./media";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export class PreviewTracker<T> {
|
||||
private snapshot: T | null = null;
|
||||
|
||||
begin({ state }: { state: T }): void {
|
||||
if (this.snapshot === null) {
|
||||
this.snapshot = structuredClone(state);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.snapshot !== null;
|
||||
}
|
||||
|
||||
getSnapshot(): T | null {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
end(): T | null {
|
||||
const snapshot = this.snapshot;
|
||||
this.snapshot = null;
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,5 @@ export { DeleteSceneCommand } from "./delete-scene";
|
||||
export { RenameSceneCommand } from "./rename-scene";
|
||||
export { ToggleBookmarkCommand } from "./toggle-bookmark";
|
||||
export { RemoveBookmarkCommand } from "./remove-bookmark";
|
||||
export { UpdateBookmarkCommand } from "./update-bookmark";
|
||||
export { MoveBookmarkCommand } from "./move-bookmark";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import type { TScene } from "@/types/timeline";
|
||||
import { updateSceneInArray } from "@/lib/scenes";
|
||||
import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks";
|
||||
|
||||
export class MoveBookmarkCommand extends Command {
|
||||
private savedScenes: TScene[] | null = null;
|
||||
|
||||
constructor(
|
||||
private fromTime: number,
|
||||
private toTime: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
const activeScene = editor.scenes.getActiveScene();
|
||||
const activeProject = editor.project.getActive();
|
||||
|
||||
if (!activeScene || !activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scenes = editor.scenes.getScenes();
|
||||
this.savedScenes = [...scenes];
|
||||
|
||||
const fromFrameTime = getFrameTime({
|
||||
time: this.fromTime,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
const toFrameTime = getFrameTime({
|
||||
time: this.toTime,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
|
||||
const updatedBookmarks = moveBookmarkInArray({
|
||||
bookmarks: activeScene.bookmarks,
|
||||
fromTime: fromFrameTime,
|
||||
toTime: toFrameTime,
|
||||
});
|
||||
|
||||
const updatedScenes = updateSceneInArray({
|
||||
scenes,
|
||||
sceneId: activeScene.id,
|
||||
updates: { bookmarks: updatedBookmarks },
|
||||
});
|
||||
|
||||
editor.scenes.setScenes({ scenes: updatedScenes });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedScenes) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.scenes.setScenes({ scenes: this.savedScenes });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import type { Bookmark, TScene } from "@/types/timeline";
|
||||
import { updateSceneInArray } from "@/lib/scenes";
|
||||
import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks";
|
||||
|
||||
export class UpdateBookmarkCommand extends Command {
|
||||
private savedScenes: TScene[] | null = null;
|
||||
|
||||
constructor(
|
||||
private time: number,
|
||||
private updates: Partial<Omit<Bookmark, "time">>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
const activeScene = editor.scenes.getActiveScene();
|
||||
const activeProject = editor.project.getActive();
|
||||
|
||||
if (!activeScene || !activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scenes = editor.scenes.getScenes();
|
||||
this.savedScenes = [...scenes];
|
||||
|
||||
const frameTime = getFrameTime({
|
||||
time: this.time,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
|
||||
const updatedBookmarks = updateBookmarkInArray({
|
||||
bookmarks: activeScene.bookmarks,
|
||||
frameTime,
|
||||
updates: this.updates,
|
||||
});
|
||||
|
||||
const updatedScenes = updateSceneInArray({
|
||||
scenes,
|
||||
sceneId: activeScene.id,
|
||||
updates: { bookmarks: updatedBookmarks },
|
||||
});
|
||||
|
||||
editor.scenes.setScenes({ scenes: updatedScenes });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedScenes) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.scenes.setScenes({ scenes: this.savedScenes });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,9 @@ export class PasteCommand extends Command {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
editor.selection.setSelectedElements({ elements: this.previousSelection });
|
||||
editor.selection.setSelectedElements({
|
||||
elements: this.previousSelection,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ export class DuplicateElementsCommand extends Command {
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
|
||||
if (this.duplicatedElements.length > 0) {
|
||||
editor.selection.setSelectedElements({ elements: this.duplicatedElements });
|
||||
editor.selection.setSelectedElements({
|
||||
elements: this.duplicatedElements,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +94,9 @@ export class DuplicateElementsCommand extends Command {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
editor.selection.setSelectedElements({ elements: this.previousSelection });
|
||||
editor.selection.setSelectedElements({
|
||||
elements: this.previousSelection,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +163,8 @@ export class InsertElementCommand extends Command {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.type === "sticker" && !element.iconName) {
|
||||
console.error("Sticker element must have iconName");
|
||||
if (element.type === "sticker" && !element.stickerId) {
|
||||
console.error("Sticker element must have stickerId");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,9 @@ export class SplitElementsCommand extends Command {
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
|
||||
if (this.rightSideElements.length > 0) {
|
||||
editor.selection.setSelectedElements({ elements: this.rightSideElements });
|
||||
editor.selection.setSelectedElements({
|
||||
elements: this.rightSideElements,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +129,9 @@ export class SplitElementsCommand extends Command {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
editor.selection.setSelectedElements({ elements: this.previousSelection });
|
||||
editor.selection.setSelectedElements({
|
||||
elements: this.previousSelection,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./track";
|
||||
export * from "./element";
|
||||
export * from "./clipboard";
|
||||
|
||||
export { TracksSnapshotCommand } from "./tracks-snapshot";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
import { EditorCore } from "@/core";
|
||||
|
||||
export class TracksSnapshotCommand extends Command {
|
||||
constructor(
|
||||
private before: TimelineTrack[],
|
||||
private after: TimelineTrack[],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
EditorCore.getInstance().timeline.updateTracks(this.after);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
EditorCore.getInstance().timeline.updateTracks(this.before);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user