diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5c8681d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# Architecture Rules + +Follow these rules for the whole repository. More specific `AGENTS.md` files override/add rules for their folders. + +## Boundaries +- `core/`: pure domain models only. See `core/AGENTS.md`. +- `commands/`: the only write path for application state. +- `editor/`: transient editor/app state types, e.g. viewport/camera and selection. +- `renderer/`: renders the current `ImageDocument` using the graphics backend, e.g. WebGL. +- `view/`: React UI shell and controls only. + +## State Ownership +- All persistent document state is represented by `ImageDocument` and related `core/` models. +- Transient editor state is application state too: viewport/camera, selection, tools, active artboard/layer, drag state, etc. +- Commands are the only code allowed to create, replace, or mutate persistent or transient state. +- React must not own, manage, derive authoritative, or alter document/editor state. +- React may hold only local ephemeral UI details with no app meaning, e.g. open popover, hovered button, uncontrolled input draft before command submit. +- Do not update state directly from React event handlers, renderer callbacks, effects, stores, services, or keybind handlers. Dispatch a command instead. + +## Commands +- Every state change must be modeled as a command with an explicit id, payload, and context. +- Commands must be deterministic and testable; avoid DOM, React, WebGL, timers, network, and filesystem access inside command execution. +- Commands return/apply the next state; they should preserve domain invariants and validate payloads before changing state. +- UI actions, menus, toolbar buttons, keybinds, and renderer interactions all request changes by dispatching commands. + +## React / View +- React displays current state and exposes user intent. +- React components receive state snapshots/selectors and command dispatch functions; they do not contain business rules. +- Do not put rendering engine logic, document mutation logic, or editor workflow ownership in React components. +- Effects are for UI integration/subscription setup only, not for deriving or correcting application state. + +## Renderer +- The renderer draws the current `ImageDocument` plus read-only editor state overlays. +- Renderer code must not mutate document/editor state directly. +- Renderer interactions may emit intents/events that are translated into commands. +- Keep rendering backend details isolated behind renderer APIs; do not leak WebGL objects into `core/`, `commands/`, or React state. + +## Keybinds +- Keybind handling is ordered and explicit. +- First check the global keybind consumer/map. +- If the global consumer handles the keybind, stop. +- If not consumed globally, the keybind may be consumed by dispatching a command. +- Keybind handlers must not mutate state directly. +- Avoid ad-hoc component-local shortcuts unless they are purely local UI behavior and cannot affect app/editor/document state. + +## Imports +- `core/` imports nothing from app layers. +- `commands/` may import `core/` and `editor/`; avoid importing React or renderer backend APIs. +- `editor/` may import `core/` types; it must not import React, commands, renderer, storage, or backend APIs. +- `renderer/` may import `core/` and `editor/` types; it must not import React components. +- `view/` may import UI components and command dispatch interfaces; avoid importing renderer internals except through stable view-facing adapters. diff --git a/commands/command.ts b/commands/command.ts index a855412..ffc9bf3 100644 --- a/commands/command.ts +++ b/commands/command.ts @@ -1,11 +1,11 @@ -import type { ImageDocument } from "@core/document"; +import type { AppState } from "@editor/state"; export type CommandContext = { - document: ImageDocument; + state: AppState; }; export type Command = { id: string; name: string; - execute(context: CommandContext, payload: TPayload): ImageDocument; + execute(context: CommandContext, payload: TPayload): AppState; }; diff --git a/core/AGENTS.md b/core/AGENTS.md new file mode 100644 index 0000000..cf7ee0d --- /dev/null +++ b/core/AGENTS.md @@ -0,0 +1,34 @@ +# Core Domain Model Rules + +Follow these rules for all changes in `core/`. + +## Purpose +- `core/` contains pure domain types for the image editor: documents, artboards, layers, assets, ids, and geometry. +- Keep it framework-free, runtime-light, and reusable by renderer, commands, persistence, and tests. + +## Hard Rules +- Do not import React, DOM APIs, UI components, storage, networking, filesystem, command handlers, or renderer code. +- Do not add side effects, global state, caches, singletons, or environment-dependent behavior. +- Prefer exported TypeScript `type`s. Add runtime code only when it is pure, deterministic, and domain-generic. +- Keep domain files small and focused. One concept per file; re-export public types from `core/index.ts`. +- Use `import type` / `export type` for type-only dependencies. +- Preserve discriminated unions. Every `Layer` variant must have a stable `type` string. +- Do not weaken domain types with `any`, broad `string | number` unions, optional fields, or nullable values unless the domain truly allows absence. +- IDs are opaque aliases from `id.ts`; do not inline plain `string` ID fields in models. +- Do not duplicate geometry shapes outside `geometry.ts`. Use `Vec2D`, `Size`, `Rect`, `Bounds`, `Transform`, `Mat2D`, and `CoordinateSpace`. +- Avoid app/workflow concerns in names and fields. Domain models describe image-editing state, not UI state. + +## Model Invariants +- `ImageDocument` owns `artboards` and shared `assets`. +- `Artboard` owns top-level `layers` and has document-space `bounds`. +- `LayerGroup.children` owns nested layers; only groups have children. +- `ImageLayer.assetId` must reference an `Asset.id` in the same document. +- `BaseLayer.opacity` is normalized `0..1`; `visible` and `locked` are explicit booleans. +- `Transform` stores position, scale, and rotation only; derived matrices/bounds should not be persisted on models. +- `clippingMask.maskLayerId` references another layer by `LayerId`; do not embed mask layer objects. + +## Changing Models +- Before adding a field, decide whether it is core persisted state or derived/UI state. Derived/UI state does not belong here. +- When adding a new domain type, create a focused file and export it from `index.ts`. +- When adding a new layer kind, update the union in `layer.ts`, add a discriminant, and document its required relationships. +- Keep names stable and serialization-friendly; assume these types may be saved, loaded, diffed, and migrated. diff --git a/editor/index.ts b/editor/index.ts new file mode 100644 index 0000000..7b77577 --- /dev/null +++ b/editor/index.ts @@ -0,0 +1 @@ +export type { AppState, EditorState, SelectionState, ViewportState } from "./state"; diff --git a/editor/state.ts b/editor/state.ts new file mode 100644 index 0000000..f881bc8 --- /dev/null +++ b/editor/state.ts @@ -0,0 +1,25 @@ +import type { ImageDocument } from "@core/document"; +import type { Angle, Size, Vec2D } from "@core/geometry"; +import type { ArtboardId, LayerId } from "@core/id"; + +export type ViewportState = { + center: Vec2D; + zoom: number; + rotation: Angle; + size: Size; +}; + +export type SelectionState = { + artboardId?: ArtboardId; + layerIds: LayerId[]; +}; + +export type EditorState = { + viewport: ViewportState; + selection: SelectionState; +}; + +export type AppState = { + document: ImageDocument; + editor: EditorState; +}; diff --git a/tsconfig.json b/tsconfig.json index 7c34690..6088890 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "@view/*": ["./view/*"], "@core/*": ["./core/*"], "@renderer/*": ["./renderer/*"], - "@commands/*": ["./commands/*"] + "@commands/*": ["./commands/*"], + "@editor/*": ["./editor/*"] }, // Some stricter flags (disabled by default)