mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238750c025 | ||
|
|
6fdb1559aa | ||
|
|
fbe3db74d7 | ||
|
|
d1f4cb615b | ||
|
|
19f1086492 | ||
|
|
8ab3aeba7c | ||
|
|
6423b352d6 | ||
|
|
7b942b363d | ||
|
|
8b8d65b509 | ||
|
|
23187b0b6a | ||
|
|
f66cdbfeb2 | ||
|
|
99137fe493 | ||
|
|
79dcec8a2e | ||
|
|
97f8d1968f | ||
|
|
ae1292a14b | ||
|
|
6ea5a156b6 | ||
|
|
401cc35743 | ||
|
|
b8d08df244 | ||
|
|
2ee0a44735 | ||
|
|
a9b9cf6bf5 | ||
|
|
d6622dc6b3 |
+3
-1
@@ -23,4 +23,6 @@ bun.lockb
|
||||
|
||||
target/
|
||||
|
||||
.cargo-tools/random_gibberish.txt
|
||||
.cargo-tools/
|
||||
|
||||
rust-migration-notes.md
|
||||
@@ -1,7 +1,7 @@
|
||||
Copyright 2025 OpenCut
|
||||
Copyright 2025-2026 OpenCut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -1,140 +1,44 @@
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="left" width="120">
|
||||
<img src="https://assets.opencut.app/branding/symbol.svg" alt="OpenCut Logo" width="100" />
|
||||
</td>
|
||||
<td align="right">
|
||||
<h1>OpenCut</h1>
|
||||
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
| | |
|
||||
| --- | ---------------------------------------------------------------------- |
|
||||
| | OpenCutA free, open-source video editor for web, desktop, and mobile. |
|
||||
[](https://discord.gg/zmR9N35cjK)
|
||||
[](https://x.com/opencutapp)
|
||||
[](LICENSE)
|
||||
|
||||
## Status
|
||||
|
||||
**OpenCut is being rewritten from the ground up.** What's coming:
|
||||
|
||||
- An Editor API
|
||||
- First-class third party plugins (made possible by a plugin-first architecture)
|
||||
- Desktop, mobile, and browser from one codebase (Rust core)
|
||||
- MCP server (for AI agents)
|
||||
- Headless mode (automation, batch rendering)
|
||||
- A scripting tab directly in the editor
|
||||
|
||||
You can still find the previous version at [opencut-app/opencut-classic](https://github.com/opencut-app/opencut-classic), which is the one to reach for today. [opencut.app](https://opencut.app) still runs the classic version; the rewrite will live at [new.opencut.app](https://new.opencut.app) until it's ready to take over.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
|
||||
OpenCut is supported by companies that believe in open source creator tools.
|
||||
|
||||
## Why?
|
||||
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place.
|
||||
|
||||
- **Privacy**: Your videos stay on your device
|
||||
- **Free features**: Most basic CapCut features are now paywalled
|
||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `apps/web/`: Next.js web application
|
||||
- `apps/desktop/`: Native desktop app built with GPUI (in progress)
|
||||
- `rust/`: Platform-agnostic core: GPU compositor, effects, masks, and WASM bindings. We're actively migrating business logic here from TypeScript.
|
||||
- `docs/`: Architecture and subsystem documentation
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh/docs/installation)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
> **Note:** Docker is optional but recommended for running the local database and Redis. If you only want to work on frontend features, you can skip it.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Fork and clone the repository
|
||||
2. Copy the environment file:
|
||||
```bash
|
||||
# Unix/Linux/Mac
|
||||
cp apps/web/.env.example apps/web/.env.local
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item apps/web/.env.example apps/web/.env.local
|
||||
```
|
||||
3. Start the database and Redis:
|
||||
```bash
|
||||
docker compose up -d db redis serverless-redis-http
|
||||
```
|
||||
4. Install dependencies and start the dev server:
|
||||
```bash
|
||||
bun install
|
||||
bun dev:web
|
||||
```
|
||||
|
||||
The application will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
The `.env.example` has sensible defaults that match the Docker Compose config — it should work out of the box.
|
||||
|
||||
### Desktop setup
|
||||
|
||||
Desktop is opt-in. If you're only working on the web app, skip this entirely.
|
||||
|
||||
If you want to get ready for `apps/desktop`, see `[apps/desktop/README.md](apps/desktop/README.md)`. It's a two-step setup: Rust toolchain first, then desktop native dependencies.
|
||||
|
||||
### Local WASM development
|
||||
|
||||
Only needed if you're editing `rust/wasm` and want the web app to use your local build instead of the published package.
|
||||
|
||||
**Prerequisites** — install these once before anything else:
|
||||
|
||||
```bash
|
||||
# Rust toolchain
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# build the WASM package
|
||||
cargo install wasm-pack
|
||||
|
||||
# reruns the build on file changes, used by bun dev:wasm
|
||||
cargo install cargo-watch
|
||||
```
|
||||
|
||||
1. Build the package once from the repo root:
|
||||
```bash
|
||||
bun run build:wasm
|
||||
```
|
||||
2. Register the generated package for linking:
|
||||
```bash
|
||||
cd rust/wasm/pkg
|
||||
bun link
|
||||
```
|
||||
3. Link `apps/web` to the local package:
|
||||
```bash
|
||||
cd apps/web
|
||||
bun link opencut-wasm
|
||||
```
|
||||
4. Rebuild on changes while you work:
|
||||
```bash
|
||||
bun dev:wasm
|
||||
```
|
||||
|
||||
To switch `apps/web` back to the published package, run:
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
bun add opencut-wasm
|
||||
```
|
||||
|
||||
### Self-Hosting with Docker
|
||||
|
||||
To run everything (including a production build of the app) in Docker:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The app will be available at [http://localhost:3100](http://localhost:3100).
|
||||
Want your logo here? Reach out at [sponsor@opencut.app](mailto:sponsor@opencut.app).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
|
||||
|
||||
**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
|
||||
|
||||
**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
|
||||
|
||||
See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
|
||||
|
||||
**Quick start for contributors:**
|
||||
|
||||
- Fork the repo and clone locally
|
||||
- Follow the setup instructions in CONTRIBUTING.md
|
||||
- Working on `apps/desktop`? See `[apps/desktop/README.md](apps/desktop/README.md)` for setup
|
||||
- Create a feature branch and submit a PR
|
||||
We're not set up to take outside contributions yet while the architecture is being designed. If you want to follow along, ask questions, or just hang out, [join the Discord](https://discord.gg/zmR9N35cjK) or [open an issue](https://github.com/opencut-app/opencut/issues).
|
||||
|
||||
## License
|
||||
|
||||
[MIT LICENSE](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Star History Chart
|
||||
[MIT](LICENSE)
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="588" height="512" viewBox="0 0 588 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M294 0L588 512H0L294 0Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 159 B |
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||
import { useEditor } from "@/editor/use-editor";
|
||||
import { formatTimecode } from "opencut-wasm";
|
||||
import { invokeAction } from "@/actions";
|
||||
import { useKeyboardShortcutsHelp } from "@/actions/use-keyboard-shortcuts-help";
|
||||
import { EditableTimecode } from "@/components/editable-timecode";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -24,6 +25,11 @@ import { PREVIEW_ZOOM_PRESETS } from "@/preview/zoom";
|
||||
import { usePreviewViewport } from "./preview-viewport";
|
||||
import { GridPopover } from "./guide-popover";
|
||||
import { usePreviewStore } from "@/preview/preview-store";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { MediaTime } from "@/wasm";
|
||||
|
||||
export function PreviewToolbar({
|
||||
@@ -133,14 +139,24 @@ function ZoomSelect() {
|
||||
|
||||
function PlayPauseButton() {
|
||||
const isPlaying = useEditor((e) => e.playback.getIsPlaying());
|
||||
const { shortcuts } = useKeyboardShortcutsHelp();
|
||||
const shortcut = shortcuts.find((s) => s.action === "toggle-play");
|
||||
const tooltipText = shortcut
|
||||
? `Play/Pause (${shortcut.keys.join(" or ")})`
|
||||
: "Play/Pause";
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
onClick={() => invokeAction("toggle-play")}
|
||||
>
|
||||
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
|
||||
</Button>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
onClick={() => invokeAction("toggle-play")}
|
||||
>
|
||||
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Notes
|
||||
|
||||
## `Transform` is misplaced
|
||||
|
||||
`Transform` is currently defined in `apps/web/src/rendering/index.ts`. It's
|
||||
exported from the rendering domain because rendering happens to be one of its
|
||||
consumers — but every other domain that touches scene geometry consumes it too
|
||||
(`@/timeline`, `@/preview`, `@/animation/values`, `@/text`, etc.). It's not
|
||||
"of" rendering any more than `Vector2` would be "of" math.
|
||||
|
||||
`Transform` is closer to a primitive than a domain concept. It's infrastructure:
|
||||
a small value type with no behavior, no dependencies, no domain-specific
|
||||
invariants. Anything that wants to position something on a 2D canvas needs it.
|
||||
|
||||
The codebase should be refactored so it's defined lower-level, rather than
|
||||
sitting in a domain. Candidate homes:
|
||||
|
||||
- `apps/web/src/primitives/transform.ts` (new dir for these value types)
|
||||
- `apps/web/src/geometry/transform.ts` if `Vector2` and friends end up there
|
||||
- `apps/web/src/rendering/transform.ts` is acceptable only if `@/rendering`
|
||||
becomes a primitives folder rather than a domain (it's borderline today —
|
||||
it also exports `BlendMode`, which has the same problem).
|
||||
|
||||
Side effect of fixing this: `@/rendering/animation-values.ts` (which exists
|
||||
only because `Transform` lives next to it) can move into `@/animation/values.ts`
|
||||
alongside the other resolve-at-time helpers, since `Transform` would no longer
|
||||
pull in a domain dependency.
|
||||
@@ -14,11 +14,4 @@ export const SPONSORS: Sponsor[] = [
|
||||
description: "Generative image, video, and audio models all in one place.",
|
||||
invertOnDark: true,
|
||||
},
|
||||
{
|
||||
name: "Vercel",
|
||||
url: "https://vercel.com?utm_source=opencut",
|
||||
logo: "/logos/others/vercel.svg",
|
||||
description: "Platform where we deploy and host OpenCut.",
|
||||
invertOnDark: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TIMELINE_ZOOM_MAX } from "@/timeline/scale";
|
||||
import { sliderToZoom, zoomToSlider } from "@/timeline/zoom-utils";
|
||||
import { ScenesView } from "@/components/editor/scenes-view";
|
||||
import { type TActionWithOptionalArgs, invokeAction } from "@/actions";
|
||||
import { useKeyboardShortcutsHelp } from "@/actions/use-keyboard-shortcuts-help";
|
||||
import {
|
||||
canToggleSourceAudio,
|
||||
getSourceAudioActionLabel,
|
||||
@@ -143,18 +144,21 @@ function ToolbarLeftSection() {
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipProvider delayDuration={500}>
|
||||
<ToolbarButton
|
||||
action="split"
|
||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||
tooltip="Split element"
|
||||
onClick={({ event }) => handleAction({ action: "split", event })}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="split-left"
|
||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||
tooltip="Split left"
|
||||
onClick={({ event }) => handleAction({ action: "split-left", event })}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="split-right"
|
||||
icon={<HugeiconsIcon icon={AlignRightIcon} />}
|
||||
tooltip="Split right"
|
||||
onClick={({ event }) =>
|
||||
@@ -163,6 +167,7 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="toggle-source-audio"
|
||||
icon={
|
||||
<HugeiconsIcon
|
||||
icon={isSelectedSourceAudioSeparated ? Unlink02Icon : Link02Icon}
|
||||
@@ -176,6 +181,7 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="duplicate-selected"
|
||||
icon={<HugeiconsIcon icon={Copy01Icon} />}
|
||||
tooltip="Duplicate element"
|
||||
onClick={({ event }) =>
|
||||
@@ -191,6 +197,7 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="delete-selected"
|
||||
icon={<HugeiconsIcon icon={Delete02Icon} />}
|
||||
tooltip="Delete element"
|
||||
onClick={({ event }) =>
|
||||
@@ -200,16 +207,15 @@ function ToolbarLeftSection() {
|
||||
|
||||
<div className="bg-border mx-1 h-6 w-px" />
|
||||
|
||||
<Tooltip>
|
||||
<ToolbarButton
|
||||
icon={<HugeiconsIcon icon={Bookmark02Icon} />}
|
||||
isActive={isCurrentlyBookmarked}
|
||||
tooltip={isCurrentlyBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "toggle-bookmark", event })
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ToolbarButton
|
||||
action="toggle-bookmark"
|
||||
icon={<HugeiconsIcon icon={Bookmark02Icon} />}
|
||||
isActive={isCurrentlyBookmarked}
|
||||
tooltip={isCurrentlyBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "toggle-bookmark", event })
|
||||
}
|
||||
/>
|
||||
|
||||
<GraphEditorPopover
|
||||
open={graphEditor.open}
|
||||
@@ -284,6 +290,7 @@ function ToolbarRightSection({
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipProvider delayDuration={500}>
|
||||
<ToolbarButton
|
||||
action="toggle-snapping"
|
||||
icon={<HugeiconsIcon icon={MagnetIcon} />}
|
||||
isActive={snappingEnabled}
|
||||
tooltip="Auto snapping"
|
||||
@@ -291,6 +298,7 @@ function ToolbarRightSection({
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
action="toggle-ripple-editing"
|
||||
icon={<OcRippleIcon size={24} className="scale-110" />}
|
||||
isActive={rippleEditingEnabled}
|
||||
tooltip="Ripple editing"
|
||||
@@ -337,6 +345,7 @@ function ToolbarButton({
|
||||
disabled,
|
||||
isActive,
|
||||
buttonWrapper,
|
||||
action,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
tooltip: string;
|
||||
@@ -344,7 +353,17 @@ function ToolbarButton({
|
||||
disabled?: boolean;
|
||||
isActive?: boolean;
|
||||
buttonWrapper?: (button: React.ReactElement) => React.ReactElement;
|
||||
action?: TActionWithOptionalArgs;
|
||||
}) {
|
||||
const { shortcuts } = useKeyboardShortcutsHelp();
|
||||
const shortcut = action
|
||||
? shortcuts.find((s) => s.action === action)
|
||||
: null;
|
||||
|
||||
const tooltipContent = shortcut
|
||||
? `${tooltip} (${shortcut.keys.join(" or ")})`
|
||||
: tooltip;
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "text"}
|
||||
@@ -370,7 +389,7 @@ function ToolbarButton({
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Primitives vs domains
|
||||
|
||||
The codebase has a recurring smell: **primitive value types defined inside
|
||||
domain folders**. The clearest current example is `Transform`, which lives in
|
||||
`apps/web/src/rendering/index.ts`. Rendering happens to consume it — but so do
|
||||
`@/timeline`, `@/preview`, `@/animation`, `@/text`, and anything else that
|
||||
positions things on a 2D canvas. It's not "of" rendering; rendering just owns
|
||||
the file.
|
||||
|
||||
## The test
|
||||
|
||||
If a type can be described without mentioning clips, tracks, effects, layers,
|
||||
keyframes, or any other product concept — and it has no behavior beyond shape
|
||||
— it's a **primitive**. The moment a type needs to know what a clip is, it
|
||||
has crossed into domain territory.
|
||||
|
||||
Primitives have:
|
||||
|
||||
- No domain-specific invariants (a 2D position doesn't care that it's a clip's
|
||||
position; it's just `{ x, y }`).
|
||||
- No dependencies on other parts of the app — they're leaves.
|
||||
- Multiple unrelated consumers across domains.
|
||||
- A name that would make sense in any 2D editor / video tool / graphics lib.
|
||||
|
||||
Domains, in contrast, can name things that only make sense given the rest of
|
||||
the product (`TimelineElement`, `Effect`, `GraphicDefinition`, `MediaAsset`).
|
||||
|
||||
## Why it matters
|
||||
|
||||
When a primitive lives in a domain folder, every other domain that consumes it
|
||||
takes a misleading dependency — `@/timeline` ends up importing from
|
||||
`@/rendering` not because timeline needs rendering, but because that's where
|
||||
`Transform` happens to sit. The dependency graph lies, and pieces that should
|
||||
move freely become anchored to the wrong layer.
|
||||
|
||||
## The refactor
|
||||
|
||||
Move primitives out of domain folders into a primitives location (somewhere
|
||||
like `apps/web/src/primitives/`, or split by concern — `geometry/`, `time/`,
|
||||
`color/`, etc.). Whatever the bucket, the rule is "no product concepts, no
|
||||
behavior, no upward dependencies".
|
||||
|
||||
Don't bulk-move. Each move is deliberate — the right destination depends on
|
||||
what other primitives already exist and what naming convention has emerged.
|
||||
|
||||
## Side effects to watch for
|
||||
|
||||
Files often end up parked next to misplaced primitives because they had nowhere
|
||||
better to live. Example: `apps/web/src/rendering/animation-values.ts` exists
|
||||
only because `Transform` lives next door. Once `Transform` moves to a primitive
|
||||
location, that file collapses back into `apps/web/src/animation/values.ts`
|
||||
alongside the other resolve-at-time helpers — there's no remaining reason to
|
||||
split them.
|
||||
|
||||
When moving a primitive, look at what *else* in its current folder only exists
|
||||
because of that primitive. Those usually want to move too (or merge somewhere
|
||||
else once the anchor is gone).
|
||||
@@ -1,36 +0,0 @@
|
||||
# Rust Migration Notes
|
||||
|
||||
## DefinitionRegistry is a TS pattern; in Rust it's a static match
|
||||
|
||||
In TS, `MasksRegistry`, `EffectsRegistry`, and similar registries are runtime maps that erase per-key type information through their `get()` return type. Helpers like `MaskDefinitionForRegistration = { [T in MaskType]: MaskDefinition<T> }[MaskType]` preserve useful type information on the registration side, but not on retrieval.
|
||||
|
||||
In Rust, mask definitions should be a `match mask { Mask::Rectangle(m) => RECTANGLE_DEF, ... }` or a `static const` table indexed by `MaskType`. Per-variant typing is preserved by exhaustive matching, so the registry class disappears entirely.
|
||||
|
||||
Do not try to fully type-thread the TS registry. The current shape is good enough for TS, and the awkwardness goes away for free in Rust.
|
||||
|
||||
## Mask interaction state moves into the Interaction enum
|
||||
|
||||
Today, `use-mask-handles.ts` holds drag state, pending-segment state, capture state, and active-handle state in refs/useState. These are all transient interaction state.
|
||||
|
||||
In Rust, this should live as one variant on `EditorState.interaction`. After the builtin/freeform split, that likely means `BuiltinMaskHandleSession` and `FreeformMaskHandleSession`, or one mask handle session with a `MaskHandleSessionKind` discriminator.
|
||||
|
||||
The `dragStateRef`, `pendingSegmentInsertRef`, and `captureRef` mutual-exclusion juggling disappears. The TS hook becomes event binding plus pixel-to-logical translation, calling into WASM for state transitions.
|
||||
|
||||
## MaskBody and MaskStroke map naturally to Rust enums
|
||||
|
||||
The `MaskBody` and `MaskStroke` discriminated unions map directly to Rust enums:
|
||||
|
||||
```rust
|
||||
enum MaskBody {
|
||||
FillPath,
|
||||
DrawOpaque,
|
||||
DrawWithFeather { fast_path: Option<MaskPathBuilder> },
|
||||
}
|
||||
|
||||
enum MaskStroke {
|
||||
StrokeFromPath,
|
||||
RenderStroke,
|
||||
}
|
||||
```
|
||||
|
||||
This TS cleanup is still worth keeping before the port. It makes renderer behavior explicit today, and the compositor will be enum-matched in Rust anyway.
|
||||
Reference in New Issue
Block a user