/** * Editor state store — a single mutable object that the gallery editor * and its tool modules read and write directly. * * Migration: galleryEditor.js used to own ~110 module-scope `let` * declarations and capture them via closure. Tool modules can't import * a `let` binding's mutations across module boundaries, so we move the * state into a single exported OBJECT whose properties are freely * mutated by anyone holding a reference. Read/write `state.transformW` * exactly the way the old code wrote `_transformW`. * * Slices land here one tool at a time; this file grows as more state * migrates out of galleryEditor.js. Defaults match the legacy * module-scope initializers verbatim — every `state.foo = …` reset * site in galleryEditor.js still works unchanged. */ export const state = { // ── Transform tool ── // Drag-resize / rotate session state. While `transformActive` is // false every field below should be considered stale. transformActive: false, transformLayer: null, transformOrigW: 0, transformOrigH: 0, // Which corner/edge handle the user is currently dragging. One of // 'tl' | 'tr' | 'bl' | 'br' | 'rot' | null. transformHandle: null, // Which handle is currently under the cursor (no drag). Drives the // hover cursor lookup; lives next to `transformHandle` because both // come from `_getTransformHandle`. hoveredHandle: null, // Snapshot of the layer canvas + offset at transform start so Cancel // can restore exactly without re-fetching from the layer. transformOrigCanvas: null, transformOrigOffset: null, // In-progress dimensions / rotation / flips committed on Apply. transformPendingW: 0, transformPendingH: 0, transformPendingRot: 0, transformPendingFlipH: false, transformPendingFlipV: false, transformAspectLock: true, // Floating Transform popup element + drag-start offsets. transformPopup: null, transformStartX: 0, transformStartY: 0, transformStartOffX: 0, transformStartOffY: 0, // Transform overlay canvas — separate canvas positioned over the // main canvas with extra slack for handle rendering. Created by // _buildEditor; the move/transform tools draw their handle layer // onto its 2D context. transformOverlay: null, transformOverlayCtx: null, // ── Magic Wand tool ── // Binary selection mask + the layer it was sampled from. `wandMask` // is a canvas the size of `wandLayer`'s pixels, white where selected, // transparent elsewhere. `wandLastSeed` remembers the last click so // tolerance retunes can re-run the flood-fill without re-prompting. wandMask: null, wandLayerId: null, wandTolerance: 24, wandMaskVisible: true, wandMode: 'replace', wandLiveRetune: false, wandLastSeed: null, // Cached layer pixel data (getImageData is O(pixels) — expensive for // 4K layers; invalidated when the active layer changes). wandSrcCache: null, // ── Brush / Eraser / Clone tools ── // Shared paint color (brush picks up the swatch; eraser and clone // ignore color but reuse the same picker control). color: '#e06c75', // Brush diameter in canvas pixels. Persisted across tool switches; // bumped to a mask-friendly default on first inpaint entry. brushSize: 8, // Per-tool stroke modifiers — opacity + flow + softness. Each tool // owns its own row so users can dial them in independently. brushOpacity: 100, brushFlow: 100, brushSoftness: 100, eraserOpacity: 100, eraserFlow: 100, eraserSoftness: 100, cloneOpacity: 100, cloneFlow: 100, cloneSoftness: 100, // Clone-stamp source point (set via Alt-click or double-tap). Null // means no source picked yet — clicking with the clone tool no-ops // until a source is set. cloneSourceX: null, cloneSourceY: null, // Stroke-start offsets so the source moves WITH the brush, keeping // the source→destination offset constant across the stroke. cloneStrokeStartX: null, cloneStrokeStartY: null, // Frozen snapshot of the source layer's pixels at stroke start so // moving the source over previously-painted pixels samples the // original, not the in-progress stamp ring. cloneSourceSnapshot: null, cloneSourceLayerId: null, // Mobile: double-tap detection for "set source" since Alt-click // isn't an option without a keyboard. cloneLastTapTime: 0, cloneLastTapX: 0, cloneLastTapY: 0, // ── Inpaint + mask ── // Active mask canvas + its 2D context. Re-pointed to the active // mask sub-layer whenever the user picks a different mask in the // layer panel. maskCanvas: null, maskCtx: null, maskVisible: true, // Reused canvas for the union-of-masks tint pass (saves repeated // allocation on every composite). compositeMaskUnion: null, // Visual tint applied to mask pixels in the composite — purely // cosmetic; the AI model still sees a hard binary mask. maskTintColor: 'rgba(255, 110, 110, 1)', maskTintOpacity: 0.28, // Inpaint-tool paint vs erase modes (Ctrl+Alt flips for a single // stroke; UI buttons toggle the persistent setting). inpaintEraseMode: false, inpaintEraseStroke: false, // First-entry guard: bump brush size to the mask-friendly default // the first time the user opens inpaint per session. inpaintBrushInitialised: false, // Last successful inpaint result layer — drives the live edge // feather / stroke sliders (those only apply to the most recent // result). lastInpaintLayerId: null, // Captured handlers so we can detach them on close without leaking. inpaintDismissHandlers: null, // Background-remove tool state — pristine snapshot so the edge // cleanup sliders can live-rebuild alpha without re-running rembg. rembgLiveLayer: null, rembgLiveSnap: null, // Memoised "is rembg installed on the server?" probe. rembgInstalledCache: null, // ── Stroke drag state ── // Generic in-progress-stroke flags shared by brush/eraser/clone/ // inpaint. `lastX/Y` are the last mouse position used to interpolate // a continuous line through fast-moving cursor samples. drawing: false, lastX: 0, lastY: 0, // ── Move tool ── moving: false, moveStartX: 0, moveStartY: 0, // Layer offset at drag start so we can compute the new offset by // (mouse - startMouse) + startOffset rather than accumulating delta. moveLayerOffsetX: 0, moveLayerOffsetY: 0, // Snap guides drawn during a move-tool drag (Ctrl held). Each entry // is a vertical / horizontal line in canvas space. activeSnapGuides: null, // ── Crop tool ── cropping: false, cropStart: null, cropEnd: null, cropRect: null, cropAspectLock: null, // True while the user drags the inside of an already-finished crop // rect to reposition it. cropMoving: false, cropMoveStart: null, // ── Lasso tool ── // Freehand selection polygon in canvas pixels. Empty when no lasso // is in progress or staged. lassoPoints: [], lassoActive: false, // In-editor copy/paste — separate from the OS clipboard so we can // round-trip layer alpha and metadata losslessly. internalClipboard: null, // ── Editor DOM refs ── // Root container that openEditor mounts into. container: null, // Main image canvas + its 2D context. Re-created on every openEditor // so the editor can reopen with fresh dimensions. mainCanvas: null, mainCtx: null, // ── Document + layers ── layers: [], activeLayerId: null, // Active tool ID — one of move/crop/transform/brush/eraser/clone/ // lasso/wand/inpaint/rembg/harmonize/sharpen/upscale/style. tool: 'move', // Display zoom (1 = 100%). pan{X,Y} translate the canvas inside the // viewport. zoom: 1, panX: 0, panY: 0, // Document dimensions in canvas pixels. imgWidth: 0, imgHeight: 0, // Gallery image id this editor session is editing, or null for // blank-canvas drafts. imageId: null, // Original file extension so save-over-original re-encodes in the // same format (JPEG vs PNG matters: JPEG cuts upload size 5-10× for // camera photos over remote tunnels). originalExt: 'png', // True between openEditor / closeEditor — guards async callbacks // that fire after the user closes the editor (don't draw onto a // dead canvas, don't re-mount the spinner). editorOpen: false, // Document-level click-away handlers registered for the current // session. Tracked so closeEditor can detach them all cleanly. // Mutated in place (push / length = 0); the reference never changes. editorDocClickHandlers: [], // ── Undo / redo ── undoStack: [], redoStack: [], // ── Layer offsets + id allocation ── // Map — kept in a Map so we can serialise it // separately from the layer's own canvas. Mutated in place. layerOffsets: new Map(), nextLayerId: 1, // ── Popup / panel handles ── fxPopupEl: null, fxPopupLayerId: null, fxMenuEl: null, adjPopupEl: null, // rAF-throttled live preview while sliders are dragged in adj popups. adjRafPending: false, historyPanelEl: null, // Custom brush-cursor overlay element (circle following the mouse). cursorEl: null, // Hover-preview thumbnail floating element (singleton, repositioned). layerThumbEl: null, // Loading-overlay element (whirlpool + label). editorLoadingEl: null, // ── Draft persistence ── draftId: null, draftName: '', persistTimer: null, // Current PUT/POST promise so concurrent saves can chain. persistInFlight: null, // True when an edit happened during an in-flight save — triggers a // follow-up persist after the current one finishes. persistDirty: false, };