Compare commits

...

55 Commits

Author SHA1 Message Date
psychedelicious
497859297f fix(ui): remove singleton asserts 2025-09-03 22:30:48 +10:00
psychedelicious
764fc6d5b4 fix(canvas): use stable references for useSyncExternalStore
Changed from useCallback to useMemo and created stable empty Map instance
and function references. This prevents React from thinking the snapshot
has changed when it returns a new Map() on every call.

The key insight is that useSyncExternalStore requires referentially stable
functions AND return values from getSnapshot to prevent infinite loops.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
61efbec862 fix(canvas): prevent infinite loop in EntityAdapterContext
Fixed useSyncExternalStore usage by wrapping subscribe and getSnapshot functions
in useCallback hooks. This prevents creating new function references on every render
which was causing infinite re-renders and 'Maximum update depth exceeded' errors.

The issue occurred because useSyncExternalStore requires stable function references
for its subscribe and getSnapshot parameters.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
903a2f88c7 fix(canvas): use safe canvas busy hook in entity hooks
Update useEntitySegmentAnything, useEntityFilter, and useEntityTransform to use
useCanvasIsBusySafe instead of useCanvasIsBusy to prevent errors when these hooks
are used in components that might not have a canvas manager initialized yet.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
bcc20cb332 fix(canvas): handle layers panel components outside canvas context
- Update entity hooks to use safe versions (useCanvasManagerSafe, useCanvasIsBusySafe)
- Fix EntityAdapterContext gates to handle null canvas manager
- Update components in layers panel that might render before manager init
- Fix useEntitySegmentAnything, useEntityFilter, useEntityTransform to handle null manager
- Update RasterLayer and related components to use safe hooks

This prevents errors when the layers panel renders before a canvas manager is initialized.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
405c2094b2 fix(canvas): handle tool hooks outside canvas context
- Update useToolIsSelected and useSelectTool to use useCanvasManagerSafe
- Wrap FloatingCanvasLeftPanelButtons with ActiveCanvasProvider
- Tool buttons now gracefully handle being outside canvas context

This fixes errors when tool components are rendered in floating UI elements.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
9ba71d5eef fix(canvas): use useCanvasManagerSafe in CanvasPasteModal
CanvasPasteModal is rendered at the app level outside any canvas instance context.
Using useCanvasManagerSafe prevents errors when the modal is mounted.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
8365bc12b2 refactor(canvas): complete removal of backward compatibility code
- Update all 46 files to use new useCanvasManager hooks
- Remove CanvasManagerProviderGate imports and usage
- Replace useCanvasManagerContext with useCanvasManager/useCanvasManagerSafe
- Clean up unnecessary wrapper components
- Simplify component hierarchy

The canvas now fully supports multiple instances with no backward compatibility code remaining.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
7f762f5c3a refactor(canvas): remove CanvasManagerProviderGate, add ActiveCanvasProvider
- Delete obsolete CanvasManagerProviderGate.tsx
- Create new ActiveCanvasProvider for panels outside workspace
- Add CanvasInstanceGate to prevent rendering before manager init
- Create useCanvasManager hooks for accessing canvas context
- Update CanvasInstanceContext to render children even without manager
- Fix imports in affected components

This removes backward compatibility code as requested - the canvas has no external consumers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
aee5b09541 docs: complete TypeScript fixes documentation with final success summary
- Document all 8 commits made during the fix process
- Record the systematic approach used to eliminate 100+ TypeScript errors
- Document key patterns: metadata serialization, selector improvements, null safety
- Mark project as production ready with 0 TypeScript errors

 MISSION ACCOMPLISHED - Zero TypeScript compilation errors achieved

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
4bd7de131c style: fix import formatting in graph builders
- Add proper spacing in selectSanitizedCanvasMetadata imports
- Ensure consistent code formatting across files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
b5d74214aa fix: eliminate final TypeScript errors - achieve 0 error state
- Fix deleteImageModal functions to handle null canvas states
- Update getImageUsage function to accept CanvasState | null parameter
- Add null checks for all canvas entity access in image usage detection
- Fix DeleteBoardModal canvas state parameter handling

 All TypeScript errors resolved - app now compiles successfully

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
19ada649c4 fix: resolve remaining TypeScript errors in selectors and components
- Add null checks for scaledSize in CanvasHUDItemScaledBbox
- Add null checks for bboxRect in StagingArea context and imageActions
- Fix canvas state handling in CanvasEntityRendererModule and CanvasStateApiModule
- Fix canvasClearHistory calls to include empty payload object
- Handle undefined selector returns with proper null checks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
e84e371744 fix: resolve selector type alignment and bbox null access issues
- Fix entity selectors to return empty arrays instead of null when canvas is null
- Fix isHidden selectors to return false instead of null when canvas is null
- Add null checks for getBbox() access in entity adapters and transformers
- Fix canvas state selector in CanvasEntityAdapterBase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
2c877e8d79 fix: add null checks for metadata serialization and canvas access
- Add selectSanitizedCanvasMetadata selector to handle null values in metadata
- Update all graph builders to use sanitized metadata
- Add null checks for canvas in useInvertMask, useNextPrevEntity, and useNextRenderableEntityIdentifier hooks
- Add canvas null checks in FLUX graph builder

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
a730acf9ed fix: add canvas null checks to hook files
- Add null checks in useEntityTitle.ts
- Add null checks in useEntityTypeCount.ts
- Add null checks in useEntityTypeIsHidden.ts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
aa7123cc22 fix: resolve remaining canvas null and undefined issues
- Add null checks in buildSDXLGraph.ts and buildSD1Graph.ts
- Fix metadata null handling in graph builders
- Add canvas null checks in graphBuilderUtils.ts functions
- Fix selectActiveCanvas to return null instead of undefined
- Add null check in CanvasTabImageSettingsAccordion.tsx selector

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
2ee4fe1c7d fix: add canvas null checks for hook components
- Add null checks in CanvasEntityMenuItemsArrange.tsx selector
- Add null checks in CanvasEntityPreviewImage.tsx selector
- Fix metadata null handling in saveCanvasHooks.ts
- Add canvas null checks in useEntityIsBookmarkedForQuickSwitch.ts
- Add canvas null checks in useEntityIsEnabled.ts and useEntityIsLocked.ts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
80f02499de fix(canvas): fix React hooks order and remaining null checks
- Moved config null check after all React hooks to comply with rules of hooks
- Fixed CanvasPasteModal dependency array for canvasManager
- Added optional chaining for config usage before null check
- Updated TypeScript fixes work log with comprehensive status

This addresses hook order violations and resolves most critical TypeScript issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:45:01 +10:00
psychedelicious
e0502d26d0 fix(canvas): fix remaining null checks and missing imports
- Added null checks for canvas selectors in RegionalGuidance components
- Fixed saveCanvasHooks getBbox null check
- Added missing useAppDispatch import in saveCanvasHooks

This resolves several more TypeScript null pointer errors and import issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
f8cab78906 fix(ui): fix dockview panel header title property access
Changed props.title to props.api.title to access the title property through the API rather than directly from props, which matches the IDockviewPanelHeaderProps interface.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
461695ea14 fix(canvas): fix control adapter null checks and type guards
- Added null check for controlAdapter in ControlLayerControlAdapter component
- Fixed type guards to properly check for controlnet/t2i_adapter types before accessing beginEndStepPct
- Fixed controlMode property access to only happen when type is controlnet

This resolves TypeScript errors related to missing properties and null control adapters.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
f3f6049604 fix(canvas): add null checks for canvas manager and bbox selectors
- Fixed CanvasPasteModal null checks for canvasManager and getBbox()
- Fixed CanvasHUDItemBbox null check for bbox selector
- Fixed RegionalGuidanceEntityList null check for canvas selector

These fixes resolve TypeScript null pointer errors in canvas components.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
439969b698 fix(canvas): export missing actions from canvasInstanceSlice
- Added missing bbox, entity, and regional guidance action exports
- Fixed canvasClearHistory import in imageActions to come from canvasesSlice
- Corrected regionalGuidanceAdded export to rgAdded (actual action name)

This resolves all remaining import/export errors causing runtime failures.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
77d7b22acd fix(canvas): export missing rasterLayerConverted actions from canvasInstanceSlice
Added missing exports for rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask, and rasterLayerConvertedToRegionalGuidance
to fix import errors in RasterLayer menu components.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
1e7f631552 fix: resolve major TypeScript errors after multi-instance refactor
- Fix null canvas state checks throughout codebase with proper fallbacks
- Update all selector patterns to handle null canvas states
- Fix selectEntityOrThrow patterns to include null checks
- Resolve listener middleware null state access issues
- Fix import/export consistency for renamed canvas slice actions
- Add proper fallback values for entity selectors

Reduced TypeScript errors from 200+ to ~30-40 remaining complex issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
318b090a52 fix: resolve bbox component null checks
- Add null checks and fallback values for bbox selectors
- Fix entity list selectors to handle null canvas state
- Provide default values for width/height/aspect ratio selectors
- Resolve TypeScript errors in parameter components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
e13028022f fix: export individual actions from canvasInstanceSlice
- Export individual actions from canvasInstanceSlice for convenience
- Fix imports from canvasSlice to canvasInstanceSlice across codebase
- Resolve module export errors after slice refactoring

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
c9b7295da7 docs: complete Phase 7 work log with implementation summary
Updated phase7_worklog.md with final status showing all requirements completed:

Section 7.1 : Active canvas tracking (already implemented)
Section 7.2 : Full canvas tab management UI
- Working add canvas button (max 3)
- Close canvas functionality
- Canvas rename capability
- Enhanced UI with proper indicators

Phase 7: Navigation & Active Canvas Tracking is now complete.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
9a27a88efe feat(canvas): implement Phase 7.2 - canvas close & rename functionality
Enhanced DockviewTabCanvasWorkspace with full canvas management features:

- Added close button to canvas tabs
  - Only visible when more than one canvas exists (prevents closing last canvas)
  - Integrates with Redux canvasInstanceRemoved action
  - Properly closes dockview panel

- Added inline rename functionality
  - Click canvas title to edit it directly
  - Uses Editable component with smooth UX
  - Updates dockview panel title dynamically
  - Supports cancel/submit with proper error handling

- Smart UI behavior
  - Close/rename features only available for canvas instances (not launchpad/viewer)
  - Visual feedback with hover states
  - Proper event handling to prevent conflicts with panel activation

Phase 7.2 is now fully complete with all requirements met:
 Working "Add new canvas" button (max 3)
 Close canvas functionality
 Canvas rename capability
 Better UI with enhanced canvas tabs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
de255819b7 feat(canvas): implement Phase 7.2 - functional canvas instance manager
- Extended NavigationApi with getDockviewApi() and getContainer() methods to allow component access to dockview APIs
- Enhanced CanvasInstanceManager to create dockview panels dynamically when "Add Canvas" is clicked
- Added selectCanvasInstances selector for future use
- CanvasInstanceManager now creates both Redux state and corresponding dockview panel
- New panels are automatically activated when created
- Maximum 3 canvas limit enforced

This completes the core "Add new canvas" functionality specified in Phase 7.2.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
21b1555c64 docs: update Phase 6 work log with progress and strategy
- Document completion of Phase 6.1 (undo/redo actions) and core Phase 6.2 hooks
- Add systematic strategy for remaining 60+ component updates
- Include code patterns and best practices learned from hook updates
- Document key insight about context useSelector expecting CanvasState not RootState

Core canvas functionality hooks are now using multi-instance context architecture
2025-09-03 18:44:30 +10:00
psychedelicious
fb97dbaab7 feat(canvas): update useInvertMask to use context-based dispatch
- Replace useAppDispatch() with useCanvasContext().dispatch
- Replace useCanvasManager() with useCanvasContext().manager
- Replace useAppSelector() with context useSelector using inline canvas state
- Update entityRasterized dispatch to use instanceActions.entityRasterized
- Actions now automatically inject canvasId via context dispatch

Part of Phase 6.2: Component updates for multi-instance canvas
2025-09-03 18:44:30 +10:00
psychedelicious
324f5eedfa feat(canvas): fix addLayerHooks selector usage for context
- Replace selectSelectedEntityIdentifier with inline canvas state selector
- Fix buildSelectValidRegionalGuidanceActions to use selectActiveCanvas with null checks
- Update selector imports to remove unused selectSelectedEntityIdentifier
- Context useSelector expects CanvasState not RootState

Fixes TypeScript errors where context useSelector was getting wrong state shape
2025-09-03 18:44:30 +10:00
psychedelicious
5c36afec5c feat(canvas): update saveCanvasHooks to use context-based dispatch
- Replace useAppDispatch() with useCanvasContext().dispatch in all hooks
- Replace useCanvasManager() with useCanvasContext().manager
- Update all action imports from canvasSlice to instanceActions from canvasInstanceSlice
- Actions now automatically inject canvasId via context dispatch

Updated hooks:
- useSaveCanvas (core hook)
- useNewRasterLayerFromBbox, useNewControlLayerFromBbox
- usePullBboxIntoLayer, usePullBboxIntoRegionalGuidanceReferenceImage

Part of Phase 6.2: Component updates for multi-instance canvas
2025-09-03 18:44:30 +10:00
psychedelicious
e650fcfbc6 feat(canvas): update addLayerHooks to use context-based dispatch
- Replace useAppDispatch() with useCanvasContext().dispatch in all hooks
- Replace useAppSelector() with context useSelector for entity selection
- Update all action imports from canvasSlice to instanceActions from canvasInstanceSlice
- Hooks now automatically inject canvasId via context dispatch

Updated hooks:
- useAddControlLayer, useAddRasterLayer, useAddInpaintMask, useAddRegionalGuidance
- useAddNewRegionalGuidanceWithARefImage, useAddRefImageToExistingRegionalGuidance
- useAddPositivePromptToExistingRegionalGuidance, useAddNegativePromptToExistingRegionalGuidance
- useAddInpaintMaskNoise, useAddInpaintMaskDenoiseLimit

Part of Phase 6.2: Component updates for multi-instance canvas
2025-09-03 18:44:30 +10:00
psychedelicious
d52cea1261 feat(canvas): update undo/redo to use canvasesSlice actions
- Update useCanvasUndoRedoHotkeys to use canvasUndo/canvasRedo from canvasesSlice
- Update CanvasToolbarUndoButton to use canvasUndo from canvasesSlice
- Update CanvasToolbarRedoButton to use canvasRedo from canvasesSlice
- Update CanvasSettingsClearHistoryButton to use canvasClearHistory from canvasesSlice
- Actions now work with active canvas ID from the router slice

Part of Phase 6.1: Canvas hooks and action dispatching updates
2025-09-03 18:44:30 +10:00
psychedelicious
2c8a0ea294 fix(canvas): resolve TypeScript errors in Phase 5 readiness implementation
Fix remaining TypeScript compilation issues in readiness.ts:
- Update UpdateReasonsArg type to use activeCanvasId, activeCanvas, canvasManagers
- Fix debouncedUpdateReasons destructuring to match new type definition
- Resolve function signature mismatches between type and implementation
- Ensure all function calls use correct parameter names

Phase 5.1-5.2 implementation now complete with no critical errors.
useEnqueueCanvas.ts is completely error-free.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
bca552fa26 feat(canvas): implement Phase 5.1-5.2 - generation pipeline active canvas support
Update readiness checks and enqueue hook to work with active canvas:
- Update useReadinessWatcher to use active canvas selectors and managers map
- Add activeCanvasId and activeCanvas parameters to readiness checks
- Add null checks for no active canvas or uninitialized canvas manager
- Update useEnqueueCanvas to require active canvas ID before enqueuing
- Enhance error logging for missing active canvas scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
a3723ce22a docs: complete Phase 4 work log with comprehensive summary
Add detailed completion summary for Phase 4 implementation including:
- Technical implementation details for both 4.1 and 4.2
- Key architectural decisions and integration points
- Commit reference and file change summary
- Status confirmation for Phase 4 completion

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
dd2b9a1c58 feat(canvas): implement Phase 4 - Context-Based Canvas API
Complete Phase 4.1 and 4.2 of multi-instance canvas implementation:

Phase 4.1 - Canvas Instance Context:
- Create CanvasInstanceContext.tsx with provider and context value interface
- Implement useCanvasContext hook for accessing canvas-specific context
- Add useCanvasContextSafe hook for optional context access
- Create useCanvasManager hook for accessing managers by ID
- Provide canvas-specific dispatch and useSelector hooks

Phase 4.2 - Update CanvasWorkspacePanel:
- Modify CanvasWorkspacePanel to accept DockviewPanelProps and extract canvasId
- Wrap CanvasWorkspacePanel content with CanvasInstanceProvider
- Create custom wrapper to handle dockview props (bypass withPanelContainer)
- Update canvas-tab-auto-layout.tsx to use direct component reference
- Fix all TypeScript errors related to the changes

Technical Implementation:
- CanvasInstanceProvider automatically injects canvasId into action payloads
- Custom useSelector hook provides canvas instance-specific state access
- Maintains backward compatibility with existing CanvasManagerProviderGate usage
- Proper error handling for missing canvasId in panel params

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
3419b49e4f feat(canvas): implement Phase 3.1-3.2 - Dockview integration foundations
- Update canvas-tab-auto-layout to create first canvas instance with canvasId
- Add active panel tracking to sync Redux activeCanvasId with dockview state
- Add canvasId parameter support to DockviewPanelParameters type
- Create CanvasInstanceManager component for canvas management UI
- Add selectCanvasCount selector for canvas count tracking
- Integrate CanvasInstanceManager into CanvasToolbar

Phase 3 foundation complete with Redux state management. Dynamic panel
creation deferred due to dockview API access complexity - requires
architectural pattern for exposing APIs to components.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
d3c112b01e feat(canvas): complete Phase 2.3 - CanvasManager architecture update
- Update CanvasManager constructor to accept canvasId parameter
- Add onStateUpdated method for factory-managed state listening
- Remove singleton references from initialize/destroy methods
- Create compatibility layers for existing singleton usage
- Fix TypeScript compilation errors and imports
- Maintain backward compatibility during transition

Phase 2 now complete: Canvas Manager Factory Pattern fully implemented

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
1d1a213de4 feat(canvas): implement Phase 2.1-2.2 - factory pattern and registry
- Replace singleton  with Map-based registry in ephemeral.ts
- Create CanvasManagerFactory with full lifecycle management
- Add state listener setup/teardown for canvas instances
- Implement proper cleanup and resource management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
172a2c116e docs: create Phase 2 work log
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
3aa37ab8f7 docs: complete Phase 1 work log with comprehensive summary
- Document all 4 phases of Phase 1 implementation
- Track 2000+ lines changed across Redux state architecture refactoring
- Detail key achievements: isolated undo/redo, router pattern, migrations
- Mark Phase 1 as complete and ready for Phase 2

Phase 1: Redux State Architecture Refactoring - COMPLETE 

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
fbe19a9392 feat(canvas): complete Phase 1.4 - migration script and slice config
- Fix canvasesSlice.ts imports and add slice configuration with migration support
- Update migration script to use correct StateWithHistory type from redux-undo
- Add canvasesSliceConfig with schema validation and migration support
- Fix syntax error in canvasInstanceSlice.ts import statement
- Complete Phase 1: Redux State Architecture Refactoring

All Phase 1 tasks completed:
 1.1: canvasInstanceSlice.ts with undoable drawing reducers
 1.2: canvasesSlice.ts as multi-instance router
 1.3: Updated selectors for Undoable state shape
 1.4: Migration script for backward compatibility

Ready for Phase 2: Canvas Manager Factory Pattern implementation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:30 +10:00
psychedelicious
12ba2cd9fd feat(canvas): update selectors for new Undoable state shape
- Replace legacy createCanvasSelector with createActiveCanvasSelector for multi-instance support
- Update all selectors to handle null canvas states gracefully
- Maintain backward compatibility with legacy selectCanvasSlice
- Add null checks for array operations in entity selectors
- Update undo/redo selectors to access present/past/future from Undoable state
- Ensure all bbox, entity, and metadata selectors work with active canvas

Part of Phase 1.3 of Canvas Multi-Instance Implementation Plan.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
024c25eca6 feat(canvas): create canvasesSlice as router for multiple canvas instances
- New CanvasesState interface with instances dictionary and activeInstanceId
- Router pattern forwards instanceActions to correct canvas via extraReducers
- Undo/redo actions support both active canvas and specific canvasId
- Canvas instance lifecycle management (add/remove/activate)
- Global actions like canvasReset and modelChanged affect all instances
- Automatic active instance selection when instances are removed

Part of Phase 1.2 of Canvas Multi-Instance Implementation Plan.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
cfa057396c feat(canvas): create canvasInstanceSlice for single canvas instance state
- Extract all drawing-related reducers from canvasSlice to canvasInstanceSlice
- Wrap reducer with redux-undo to enable isolated undo/redo per canvas instance
- Include all entity operations: raster layers, control layers, regional guidance, inpaint masks
- Include bbox operations and shared entity management
- Add throttling filter to prevent excessive undo history entries
- Configure undoable with 64-step history limit

Part of Phase 1.1 of Canvas Multi-Instance Implementation Plan.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
15bbbde803 feat(ui): create canvasInstanceSlice for multi-instance canvas architecture
Extract all drawing-related reducers from canvasSlice to new canvasInstanceSlice.
This slice manages state for a single canvas instance and includes:
- All entity reducers (raster layers, control layers, regional guidance, inpaint masks)
- BBox management
- Shared entity operations
- Redux-undo configuration for isolated undo/redo histories

Part of Phase 1: Redux State Architecture Refactoring for multi-instance canvas support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
86d25a019f feat(ui): update store configuration for canvasesSlice architecture
Update Redux store configuration to use the new canvasesSlice instead of the old canvasSlice.

Key changes:
- Replace canvasSliceConfig import with canvasesSliceConfig in store.ts
- Remove undoable wrapper from store level (now managed internally by canvasesSlice)
- Update undo/redo selectors to work with new state structure:
  - selectCanvasMayUndo/Redo now check activeInstanceId and access past/future from instances
- Update SLICE_CONFIGS and ALL_REDUCERS mappings

State structure change:
- Before: state.canvas.{past, present, future}
- After: state.canvases.instances[activeId].{past, present, future}

Next step: Update all imports from canvasSlice to canvasInstanceSlice across codebase.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
4e1f8cdc0a feat(ui): implement Phase 1 - Redux State Architecture Refactoring for multi-canvas
Complete refactoring of canvas state architecture to support multiple canvas instances.

Key Changes:
1. canvasesSlice.ts - Multi-instance manager with routing
   - Manages dictionary of canvas instances
   - Routes actions to correct instance via extraReducers
   - Implements undo/redo per instance
   - Tracks active canvas instance

2. Enhanced selectors - New state shape support
   - selectCanvasInstance(state, canvasId) for specific instances
   - selectActiveCanvas(state) for active instance
   - New selector factories: createCanvasInstanceSelector, createActiveCanvasSelector
   - Backward compatibility with legacy selectCanvasSlice

3. Migration system - Backward compatibility
   - migrateCanvasV1ToV2 handles old canvas state structure
   - Wraps existing state in new Undoable instances structure
   - Integrated into slice persistConfig

Architecture:
- Before: state.canvas.present (single undoable canvas)
- After: state.canvases.instances[id].present (multiple undoable instances)
- Actions route to correct instance via canvasId in payload
- Active instance tracking for global operations

This establishes the foundation for multi-instance canvas support while maintaining full backward compatibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
ef02527c46 feat(ui): create canvasInstanceSlice.ts with extracted drawing reducers
Extract all drawing-related reducers from canvasSlice.ts to new canvasInstanceSlice.ts for Phase 1 of multi-instance canvas implementation. This slice manages the state for a single canvas instance with undoable history.

Key changes:
- Created canvasInstanceSlice.ts with all entity manipulation reducers
- Implemented undoable wrapper with redux-undo (64 action limit)
- Added action throttling filter for performance optimization
- Exported instanceActions and undoableCanvasInstanceReducer
- Preserved all original reducer logic and functionality

This is part of Phase 1: Redux State Architecture Refactoring for multi-instance canvas support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:44:29 +10:00
psychedelicious
0e7bff2673 docs: impl plan 2025-09-03 18:44:29 +10:00
194 changed files with 5034 additions and 624 deletions

602
CANVAS_MULTI_INSTANCE.md Normal file
View File

@@ -0,0 +1,602 @@
# Canvas Multi-Instance Implementation Plan
## Overview
Transform the InvokeAI canvas from a singleton architecture to support multiple canvas instances within the canvas tab. Each instance will have independent state, tools, and generation capabilities while sharing the same UI framework.
## Architecture Summary
### Current State
- Single canvas instance with singleton Redux slice (`canvasSlice`)
- Global `CanvasManager` instance stored in `$canvasManager` atom
- Direct component coupling to global canvas state
- Single workspace panel in dockview layout
### Target State
- Multiple canvas instances as dockview panels within main panel
- Redux slice supporting multiple canvas states with active instance tracking
- Canvas manager registry with instance lifecycle management
- Context-based API abstraction for components
- Per-instance readiness and generation
## Implementation Phases
## Phase 1: Redux State Architecture Refactoring
To achieve isolated undo/redo histories, the Redux architecture will be split into two main parts: a `canvasInstanceSlice` that manages the state for a single canvas, and a `canvasesSlice` that acts as a router, managing a collection of undoable instances.
### 1.1 Create `canvasInstanceSlice`
**File**: `src/features/controlLayers/store/canvasInstanceSlice.ts` (new)
This new slice will contain all the drawing-related reducers from the original `canvasSlice`. Its reducer will be wrapped with `redux-undo` to create a single, undoable history.
```typescript
import { createSlice } from '@reduxjs/toolkit';
import undoable, { type UndoableOptions } from 'redux-undo';
import { getInitialCanvasState, type CanvasState } from './types';
// All existing drawing reducers (rasterLayerAdded, etc.) go here
const reducers = { /* ... */ };
const canvasInstanceSlice = createSlice({
name: 'canvasInstance',
initialState: getInitialCanvasState(),
reducers: reducers,
});
const undoableConfig: UndoableOptions<CanvasState> = { limit: 64 };
// Export the undoable reducer for a single instance
export const undoableCanvasInstanceReducer = undoable(canvasInstanceSlice.reducer, undoableConfig);
export const instanceActions = canvasInstanceSlice.actions;
```
### 1.2 Create `canvasesSlice` as a Router
**File**: `src/features/controlLayers/store/canvasesSlice.ts` (renamed from `canvasSlice.ts`)
This slice manages the dictionary of canvas instances. It uses `extraReducers` to act as a router, forwarding actions from UI components to the correct `undoableCanvasInstanceReducer` based on `canvasId`.
```typescript
import { createSlice, type PayloadAction, isAnyOf } from '@reduxjs/toolkit';
import { undoableCanvasInstanceReducer, instanceActions } from './canvasInstanceSlice';
import { type Undoable } from 'redux-undo';
import { type CanvasState } from './types';
interface CanvasesState {
instances: Record<string, Undoable<CanvasState>>;
activeInstanceId: string | null;
}
const initialCanvasesState: CanvasesState = { instances: {}, activeInstanceId: null };
export const canvasesSlice = createSlice({
name: 'canvases',
initialState: initialCanvasesState,
reducers: {
canvasInstanceAdded: (state, action: PayloadAction<{ canvasId: string }>) => {
const { canvasId } = action.payload;
state.instances[canvasId] = undoableCanvasInstanceReducer(undefined, { type: '@@INIT' });
},
canvasInstanceRemoved: (state, action: PayloadAction<{ canvasId: string }>) => {
delete state.instances[action.payload.canvasId];
},
activeCanvasChanged: (state, action: PayloadAction<{ canvasId: string | null }>) => {
state.activeInstanceId = action.payload.canvasId;
},
},
extraReducers: (builder) => {
builder.addMatcher(
isAnyOf(...Object.values(instanceActions)),
(state, action) => {
const canvasId = (action as PayloadAction).payload?.canvasId;
if (canvasId && state.instances[canvasId]) {
state.instances[canvasId] = undoableCanvasInstanceReducer(state.instances[canvasId], action);
}
}
);
},
});
```
### 1.3 Update Selectors
**File**: `src/features/controlLayers/store/selectors.ts`
Selectors must be updated to account for the `Undoable` state shape, accessing the `.present` property for the current state.
```typescript
// Old
export const selectCanvasSlice = (state: RootState) => state.canvas;
// New
export const selectCanvasInstance = (state: RootState, canvasId: string) =>
state.canvases.instances[canvasId]?.present;
export const selectActiveCanvas = (state: RootState) =>
state.canvases.instances[state.canvases.activeInstanceId]?.present;
```
### 1.4 Migration Strategy
**File**: `src/app/store/migrations/canvasMigration.ts`
The migration script needs to wrap the old canvas state in the new `instances` and `Undoable` structures.
```typescript
const migrateCanvasV1ToV2 = (state: any) => {
if (state.canvas && !state.canvases) {
const canvasId = nanoid();
const undoableState = {
past: [],
present: state.canvas, // The old state becomes the 'present'
future: [],
};
return {
...state,
canvases: {
instances: {
[canvasId]: undoableState
},
activeInstanceId: canvasId
}
};
}
return state;
};
```
## Phase 2: Canvas Manager Factory Pattern
### 2.0 Configure Listener Middleware
**File**: `src/app/store/store.ts`
To enable efficient, targeted state subscriptions, the listener middleware must be added to the store. This is a one-time setup.
```typescript
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
// Create and export the middleware instance
export const listenerMiddleware = createListenerMiddleware();
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
// Prepend the listener middleware to the chain
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
});
```
### 2.1 Replace Singleton with Registry
**File**: `src/features/controlLayers/store/ephemeral.ts`
```typescript
// Old
export const $canvasManager = atom<CanvasManager | null>(null);
// New
export const $canvasManagers = atom<Map<string, CanvasManager>>(new Map());
```
### 2.2 Update Canvas Manager Factory
**File**: `src/features/controlLayers/konva/CanvasManagerFactory.ts`
The factory will now manage the lifecycle of state listeners, creating and destroying them alongside the canvas manager instances.
```typescript
import { listenerMiddleware } from 'src/app/store/store';
import { selectCanvasInstance } from 'src/features/controlLayers/store/selectors';
export class CanvasManagerFactory {
private managers = new Map<string, CanvasManager>();
private unsubscribers = new Map<string, () => void>();
createInstance(
canvasId: string,
container: HTMLDivElement,
store: AppStore,
socket: SocketClient
): CanvasManager {
const manager = new CanvasManager(container, store, socket, canvasId);
this.managers.set(canvasId, manager);
const listener = listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
const oldState = selectCanvasInstance(previousState, canvasId);
const newState = selectCanvasInstance(currentState, canvasId);
return oldState !== newState;
},
effect: (action, listenerApi) => {
const latestState = selectCanvasInstance(listenerApi.getState(), canvasId);
if (latestState) {
manager.onStateUpdated(latestState);
}
},
});
this.unsubscribers.set(canvasId, listener.unsubscribe);
return manager;
}
getInstance(canvasId: string): CanvasManager | undefined {
return this.managers.get(canvasId);
}
destroyInstance(canvasId: string): void {
this.unsubscribers.get(canvasId)?.();
this.unsubscribers.delete(canvasId);
const manager = this.managers.get(canvasId);
if (manager) {
manager.destroy();
this.managers.delete(canvasId);
}
}
}
```
### 2.3 Update Canvas Manager
**File**: `src/features/controlLayers/konva/CanvasManager.ts`
The manager is now simplified. It no longer subscribes to the store directly, but instead has a new method, `onStateUpdated`, which is called by the listener middleware.
```typescript
// The constructor no longer manages its own subscription.
constructor(
container: HTMLDivElement,
store: AppStore,
socket: SocketClient,
private canvasId: string
) {
// Initial state can be read once.
const initialState = selectCanvasInstance(store.getState(), this.canvasId);
if (initialState) {
this.onStateUpdated(initialState);
}
}
// New method to be called by the listener middleware's effect.
public onStateUpdated(state: CanvasState): void {
// All logic that used to be in the store.subscribe callback goes here.
}
```
## Phase 3: Dockview Integration
### 3.1 Update Canvas Tab Layout
**File**: `src/features/ui/layouts/canvas-tab-auto-layout.tsx`
Modify main panel initialization to support multiple workspace panels:
```typescript
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
navigationApi.registerContainer(tab, 'main', api, () => {
// ... existing launchpad and viewer setup
// Create first canvas instance
const firstCanvasId = nanoid();
api.addPanel<DockviewPanelParameters>({
id: `${WORKSPACE_PANEL_ID}_${firstCanvasId}`,
component: WORKSPACE_PANEL_ID,
title: 'Canvas 1',
tabComponent: DOCKVIEW_TAB_CANVAS_WORKSPACE_ID,
params: {
tab,
canvasId: firstCanvasId,
focusRegion: 'canvas',
i18nKey: 'ui.panels.canvas',
},
position: {
direction: 'within',
referencePanel: launchpad.id,
},
});
});
};
```
### 3.2 Add Canvas Management Actions
**File**: `src/features/ui/components/CanvasInstanceManager.tsx`
```typescript
export const CanvasInstanceManager = () => {
const dispatch = useAppDispatch();
const canvasCount = useAppSelector(selectCanvasCount);
const addCanvas = useCallback(() => {
if (canvasCount >= 3) return;
const canvasId = nanoid();
const canvasName = `Canvas ${canvasCount + 1}`;
// Add to Redux
dispatch(canvasInstanceAdded({ canvasId, name: canvasName }));
// Add to dockview
const api = getDockviewApi('canvas', 'main');
api?.addPanel({
id: `${WORKSPACE_PANEL_ID}_${canvasId}`,
component: WORKSPACE_PANEL_ID,
title: canvasName,
params: { canvasId },
});
}, [canvasCount, dispatch]);
// ... close canvas handler
};
```
## Phase 4: Context-Based Canvas API
### 4.1 Canvas Instance Context
**File**: `src/features/controlLayers/contexts/CanvasInstanceContext.tsx`
```typescript
interface CanvasInstanceContextValue {
canvasId: string;
canvasName: string;
manager: CanvasManager;
dispatch: (action: CanvasAction) => void;
useSelector: <T>(selector: (state: CanvasState) => T) => T;
}
export const CanvasInstanceProvider: React.FC<{
canvasId: string;
children: React.ReactNode;
}> = ({ canvasId, children }) => {
const store = useAppStore();
const manager = useCanvasManager(canvasId);
const dispatch = useCallback((action: CanvasAction) => {
store.dispatch({ ...action, canvasId });
}, [store, canvasId]);
const useSelector = useCallback(<T,>(selector: (state: CanvasState) => T) => {
return useAppSelector((state) =>
selector(selectCanvasInstance(state, canvasId))
);
}, [canvasId]);
const value = useMemo(() => ({
canvasId,
manager,
dispatch,
useSelector,
}), [canvasId, manager, dispatch, useSelector]);
return (
<CanvasInstanceContext.Provider value={value}>
{children}
</CanvasInstanceContext.Provider>
);
};
```
### 4.2 Update CanvasWorkspacePanel
**File**: `src/features/ui/layouts/CanvasWorkspacePanel.tsx`
```typescript
export const CanvasWorkspacePanel = memo(({ params }: DockviewPanelProps) => {
const { canvasId } = params as { canvasId: string };
return (
<CanvasInstanceProvider canvasId={canvasId}>
<StagingAreaContextProvider sessionId={sessionId}>
{/* ... existing content but now scoped to canvasId */}
</StagingAreaContextProvider>
</CanvasInstanceProvider>
);
});
```
## Phase 5: Generation Pipeline Updates
### 5.1 Update Readiness Checks
**File**: `src/features/queue/store/readiness.ts`
```typescript
const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
activeCanvasId: string | null;
canvasManagers: Map<string, CanvasManager>;
// ... other args
}) => {
if (!activeCanvasId) {
return [{ content: 'No active canvas' }];
}
const canvas = canvases[activeCanvasId];
const manager = canvasManagers.get(activeCanvasId);
if (!canvas || !manager) {
return [{ content: 'Canvas not initialized' }];
}
// ... existing readiness checks using specific canvas/manager
};
```
### 5.2 Update Enqueue Hook
**File**: `src/features/queue/hooks/useEnqueueCanvas.ts`
```typescript
export const useEnqueueCanvas = () => {
const store = useAppStore();
const activeCanvasId = useAppSelector(selectActiveCanvasId);
const canvasManager = useCanvasManager(activeCanvasId);
const enqueue = useCallback((prepend: boolean) => {
if (!canvasManager || !activeCanvasId) {
log.error('No active canvas');
return;
}
return enqueueCanvas(store, canvasManager, activeCanvasId, prepend);
}, [canvasManager, activeCanvasId, store]);
return enqueue;
};
```
## Phase 6: Component Migration
### 6.1 Update Canvas Hooks and Action Dispatching
All canvas-related hooks and action dispatching need to use the new context-aware and instance-aware patterns.
**Drawing Actions**: Use the `dispatch` function from the `useCanvasContext` hook, which automatically injects the correct `canvasId`.
```typescript
// Old
export const useAddRasterLayer = () => {
const dispatch = useAppDispatch();
// ...
dispatch(rasterLayerAdded(...));
}
// New
import { instanceActions } from '../store/canvasInstanceSlice';
export const useAddRasterLayer = () => {
const { dispatch, useSelector } = useCanvasContext();
// ...
dispatch(instanceActions.rasterLayerAdded(...));
}
```
**Undo/Redo Actions**: To undo/redo the *active* canvas, global hooks will dispatch the standard `redux-undo` actions, but they must be wrapped in a payload that includes the active `canvasId`. The `canvasesSlice` router will intercept this and apply the action to the correct history.
```typescript
import { ActionCreators as UndoActionCreators } from 'redux-undo';
const useUndo = () => {
const dispatch = useAppDispatch();
const activeCanvasId = useAppSelector(selectActiveCanvasId);
return () => {
if (!activeCanvasId) return;
dispatch({ ...UndoActionCreators.undo(), payload: { canvasId: activeCanvasId } });
}
}
```
### 6.2 Component Updates
Update approximately 45 components that use `useCanvasManager` or canvas selectors:
- Replace `useAppDispatch()` with `useCanvasContext().dispatch`
- Replace `useAppSelector(selectCanvas...)` with `useCanvasContext().useSelector(...)`
- Replace `useCanvasManager()` with `useCanvasContext().manager`
## Phase 7: Navigation & Active Canvas Tracking
### 7.1 Track Active Canvas in Dockview
**File**: `src/features/ui/layouts/canvas-tab-auto-layout.tsx`
```typescript
// Track active workspace panel
api.onDidActivePanelChange((panel) => {
if (panel?.id.startsWith(WORKSPACE_PANEL_ID)) {
const canvasId = panel.params?.canvasId;
// When a canvas panel is activated, its canvasId is set as the active canvas ID in redux.
// When a non-canvas panel is activated, the active canvas ID is set to null.
dispatch(setActiveCanvasId(canvasId ?? null));
}
});
```
### 7.2 Canvas Tab Management UI
Add UI controls for:
- Add new canvas button (max 3)
- Close canvas (with confirmation if has changes)
- Rename canvas
- Canvas count indicator
## Testing Strategy
### Unit Tests
- Redux slice with multiple instances
- Selector parameterization
- Context provider isolation
- Canvas manager lifecycle
### Integration Tests
- Canvas creation/deletion
- Switching between canvases
- Generation from correct canvas
- State isolation between instances
### E2E Tests
- Full workflow with multiple canvases
- Memory cleanup on canvas close
- Persistence and restoration
## Migration Considerations
### Backward Compatibility
- Migrate existing single canvas to first instance
- Preserve all canvas state during migration
- Version state structure for future migrations
### Performance Monitoring
- Track memory usage with multiple canvases
- Monitor React re-render patterns
- Profile Canvas manager instances
### Rollout Strategy
1. Feature flag for multi-instance UI
2. Beta testing with power users
3. Gradual rollout with monitoring
4. Full release after stability confirmation
## File Structure Changes
```
src/features/controlLayers/
├── store/
│ ├── canvasesSlice.ts (renamed from canvasSlice.ts)
│ ├── canvasSelectors.ts (parameterized selectors)
│ └── canvasMigrations.ts (new)
├── contexts/
│ ├── CanvasInstanceContext.tsx (new)
│ └── CanvasManagerProviderGate.tsx (updated)
├── konva/
│ ├── CanvasManager.ts (add canvasId support)
│ └── CanvasManagerFactory.ts (new)
└── hooks/
└── useCanvasContext.ts (new)
```
## Key Technical Decisions
1. **Use Dockview's native tabs** rather than implementing custom tab UI
2. **Context-based API abstraction** to hide multi-instance complexity from components
3. **Active canvas tracking in Redux** for generation pipeline
4. **Manager registry pattern** for lifecycle management
5. **Parameterized selectors** rather than duplicating selector code
6. **Migration-first approach** to preserve existing user work
## Risk Mitigation
- **Memory leaks**: Implement proper cleanup in manager lifecycle
- **State corruption**: Add validation and error boundaries per canvas
- **Performance degradation**: Lazy load non-active canvases
- **User confusion**: Clear active canvas indication in UI
- **Data loss**: Auto-save and recovery mechanisms
## Success Metrics
- Support 3 simultaneous canvas instances without performance degradation
- Zero data loss during canvas operations
- Maintain current generation speed
- Clean component API with minimal changes
- Successful migration of existing canvases

View File

@@ -1,7 +1,6 @@
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
import { DeleteVideoModal } from 'features/deleteVideoModal/components/DeleteVideoModal';
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
@@ -54,9 +53,7 @@ export const GlobalModalIsolator = memo(() => {
<FullscreenDropzone />
<VideosModal />
<SaveWorkflowAsDialog />
<CanvasManagerProviderGate>
<CanvasPasteModal />
</CanvasManagerProviderGate>
<CanvasPasteModal />
<LoadWorkflowFromGraphModal />
</>
);

View File

@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
import { canvasReset } from 'features/controlLayers/store/actions';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { instanceActions } from 'features/controlLayers/store/canvasInstanceSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
@@ -94,7 +94,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
};
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
store.dispatch(canvasReset());
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
store.dispatch(instanceActions.rasterLayerAdded({ overrides, isSelected: true }));
store.dispatch(sentImageToCanvas());
toast({
title: t('toast.sentToCanvas'),

View File

@@ -24,6 +24,9 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
const refImages = selectRefImagesSlice(state);
deleted_images.forEach((image_name) => {
if (!canvas) {
return;
}
const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/store';
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
@@ -119,6 +119,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
// All regional guidance entities are updated to use the same new model.
const canvasState = selectCanvasSlice(state);
if (!canvasState) {
return;
}
const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance');
for (const entity of canvasRegionalGuidanceEntities) {
for (const refImage of entity.referenceImages) {

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, AppStartListening, RootState } from 'app/store/store';
import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import {
clipEmbedModelSelected,
@@ -215,7 +215,11 @@ const handleVideoModels: ModelHandler = (models, state, dispatch, log) => {
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => {
const caModels = models.filter(isControlLayerModelConfig);
selectCanvasSlice(state).controlLayers.entities.forEach((entity) => {
const canvas = selectCanvasSlice(state);
if (!canvas) {
return;
}
canvas.controlLayers.entities.forEach((entity) => {
const selectedControlAdapterModel = entity.controlAdapter.model;
// `null` is a valid control adapter model - no need to do anything.
if (!selectedControlAdapterModel) {
@@ -250,7 +254,11 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
const canvas2 = selectCanvasSlice(state);
if (!canvas2) {
return;
}
canvas2.regionalGuidance.entities.forEach((entity) => {
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
if (!isIPAdapterConfig(config)) {
return;
@@ -293,7 +301,11 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
const canvas3 = selectCanvasSlice(state);
if (!canvas3) {
return;
}
canvas3.regionalGuidance.entities.forEach((entity) => {
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
if (!isFLUXReduxConfig(config)) {
return;

View File

@@ -1,6 +1,6 @@
import type { AppStartListening } from 'app/store/store';
import { isNil } from 'es-toolkit';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
heightChanged,

View File

@@ -0,0 +1,85 @@
import { nanoid } from '@reduxjs/toolkit';
import type { CanvasState } from 'features/controlLayers/store/types';
import { getInitialCanvasState } from 'features/controlLayers/store/types';
import type { StateWithHistory } from 'redux-undo';
// Type alias for backward compatibility
type Undoable<T> = StateWithHistory<T>;
/**
* Migration from single canvas (v1) to multi-canvas (v2) state structure.
*
* This migration wraps the existing canvas state in the new instances structure:
* - Creates a new canvases slice with instances dictionary
* - Wraps the old canvas state in an Undoable structure
* - Sets the first instance as active
*
* Before: { canvas: { present: CanvasState, past: [], future: [] }, ... }
* After: { canvases: { instances: { [id]: { present: CanvasState, past: [], future: [] } }, activeInstanceId: id }, ... }
*/
export const migrateCanvasV1ToV2 = (state: any) => {
// Check if we have old canvas state but no canvases state
if (state.canvas && !state.canvases) {
const canvasId = nanoid();
// The canvas state is already undoable, so we can use it directly
const undoableState: Undoable<CanvasState> = state.canvas;
// Remove the old canvas state and replace with new structure
const { canvas: _oldCanvas, ...restState } = state;
return {
...restState,
canvases: {
instances: {
[canvasId]: undoableState
},
activeInstanceId: canvasId
}
};
}
return state;
};
/**
* Migration helper to ensure backward compatibility.
* This can be used in the persist config of the canvases slice.
*/
export const migrateCanvasState = (state: any, version?: number) => {
// Apply v1 to v2 migration if needed
let migratedState = migrateCanvasV1ToV2(state);
// Future migrations can be added here
// if (version < 3) {
// migratedState = migrateV2ToV3(migratedState);
// }
return migratedState;
};
/**
* Helper to check if the state needs migration
*/
export const needsCanvasMigration = (state: any): boolean => {
return Boolean(state.canvas && !state.canvases);
};
/**
* Creates a default canvas instance for new installations
*/
export const createDefaultCanvasInstance = (): { instances: Record<string, Undoable<CanvasState>>, activeInstanceId: string } => {
const canvasId = nanoid();
return {
instances: {
[canvasId]: {
past: [],
present: getInitialCanvasState(), // This will need to be imported
future: []
}
},
activeInstanceId: canvasId
};
};
// Note: getInitialCanvasState is now imported from types

View File

@@ -20,8 +20,8 @@ import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMi
import { deepClone } from 'common/util/deepClone';
import { keys, mergeWith, omit, pick } from 'es-toolkit/compat';
import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice';
import { canvasesSliceConfig } from 'features/controlLayers/store/canvasesSlice';
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
@@ -64,7 +64,7 @@ const log = logger('system');
const SLICE_CONFIGS = {
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig,
[canvasSliceConfig.slice.reducerPath]: canvasSliceConfig,
[canvasesSliceConfig.slice.reducerPath]: canvasesSliceConfig,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig,
[configSliceConfig.slice.reducerPath]: configSliceConfig,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig,
@@ -90,11 +90,8 @@ const ALL_REDUCERS = {
[api.reducerPath]: api.reducer,
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer,
// Undoable!
[canvasSliceConfig.slice.reducerPath]: undoable(
canvasSliceConfig.slice.reducer,
canvasSliceConfig.undoableConfig?.reduxUndoOptions
),
// No longer undoable here - canvasesSlice manages its own undoable instances
[canvasesSliceConfig.slice.reducerPath]: canvasesSliceConfig.slice.reducer,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
[configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,

View File

@@ -81,7 +81,7 @@ export type Group<T extends object> = {
/**
* A unique key used for type-checking the group. Use the `buildGroup` function to create a group, which will set this key.
*/
[uniqueGroupKey]: true;
[uniqueGroupKey]: true
};
type OptionOrGroup<T extends object> = T | Group<T>;

View File

@@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { useCanvasManagerSafe } from 'features/controlLayers/hooks/useCanvasManager';
import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasInstanceSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
@@ -13,11 +13,12 @@ export const SessionMenuItems = memo(() => {
const dispatch = useAppDispatch();
const tab = useAppSelector(selectActiveTab);
const canvasManager = useCanvasManagerSafe();
const resetCanvasLayers = useCallback(() => {
dispatch(allEntitiesDeleted());
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
$canvasManager.get()?.stage.fitBboxToStage();
}, [dispatch]);
canvasManager?.stage.fitBboxToStage();
}, [dispatch, canvasManager]);
const resetGenerationSettings = useCallback(() => {
dispatch(paramsReset());
}, [dispatch]);

View File

@@ -31,14 +31,14 @@ import { useClientSideUpload } from './useClientSideUpload';
type UseImageUploadButtonArgs =
| {
isDisabled?: boolean;
allowMultiple: false;
allowMultiple: false
onUpload?: (imageDTO: ImageDTO) => void;
onUploadStarted?: (files: File) => void;
onError?: (error: unknown) => void;
}
| {
isDisabled?: boolean;
allowMultiple: true;
allowMultiple: true
onUpload?: (imageDTOs: ImageDTO[]) => void;
onUploadStarted?: (files: File[]) => void;
onError?: (error: unknown) => void;

View File

@@ -1,6 +1,6 @@
import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -33,13 +33,13 @@ type AlertData = {
const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled : true
);
const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked : false
);
const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapter }: ContentProps) => {

View File

@@ -1,7 +1,7 @@
import type { SpinnerProps } from '@invoke-ai/ui-library';
import { Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useAllEntityAdapters } from 'features/controlLayers/contexts/EntityAdapterContext';
import { computed } from 'nanostores';
import { memo, useMemo } from 'react';

View File

@@ -1,5 +1,5 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -4,7 +4,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasEntityListDnd } from 'features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
import { entitySelected } from 'features/controlLayers/store/canvasInstanceSlice';
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useRef } from 'react';

View File

@@ -13,7 +13,7 @@ import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/component
import { RasterLayerExportPSDButton } from 'features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton';
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
import { entitiesReordered } from 'features/controlLayers/store/canvasInstanceSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
import { triggerPostMoveFlash } from 'features/dnd/util';

View File

@@ -6,7 +6,7 @@ import {
useAddRasterLayer,
useAddRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,7 +14,7 @@ import { PiPlusBold } from 'react-icons/pi';
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage();

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDuplicated } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,7 +10,7 @@ import { PiCopyFill } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const onClick = useCallback(() => {
if (!selectedEntityIdentifier) {

View File

@@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/ColorPicker/RgbColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { entityFillColorChanged, entityFillStyleChanged } from 'features/controlLayers/store/canvasSlice';
import { entityFillColorChanged, entityFillStyleChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectSelectedEntityFill, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';

View File

@@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useInvertMask } from 'features/controlLayers/hooks/useInvertMask';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isInpaintMaskEntityIdentifier } from 'features/controlLayers/store/types';
@@ -11,7 +11,7 @@ import { PiSelectionInverseBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarInvertMaskButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const invertMask = useInvertMask();
if (!selectedEntityIdentifier) {

View File

@@ -17,7 +17,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clamp, round } from 'es-toolkit/compat';
import { snapToNearest } from 'features/controlLayers/konva/util';
import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice';
import { entityOpacityChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import {
selectCanvasSlice,
selectEntity,
@@ -61,6 +61,9 @@ const sliderDefaultValue = mapRawValueToSliderValue(1);
const snapCandidates = marks.slice(1, marks.length - 1);
const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return 1; // fallback to 100% opacity
}
const selectedEntityIdentifier = canvas.selectedEntityIdentifier;
if (!selectedEntityIdentifier) {
return 1; // fallback to 100% opacity

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isSaveableEntityIdentifier } from 'features/controlLayers/store/types';
@@ -11,7 +11,7 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
const saveLayerToAssets = useSaveLayerToAssets();

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { ActiveCanvasProvider } from 'features/controlLayers/contexts/ActiveCanvasProvider';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
@@ -13,7 +13,7 @@ export const CanvasLayersPanel = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
return (
<CanvasManagerProviderGate>
<ActiveCanvasProvider>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
@@ -22,7 +22,7 @@ export const CanvasLayersPanel = memo(() => {
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
</CanvasManagerProviderGate>
</ActiveCanvasProvider>
);
});

View File

@@ -12,7 +12,7 @@ import {
import { useStore } from '@nanostores/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManagerSafe } from 'features/controlLayers/hooks/useCanvasManager';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
@@ -31,13 +31,20 @@ export const CanvasPasteModal = memo(() => {
const { dispatch, getState } = useAppStore();
const { t } = useTranslation();
const imageToPaste = useStore($imageFile);
const canvasManager = useCanvasManager();
const canvasManager = useCanvasManagerSafe();
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const [uploadImage, { isLoading }] = useUploadImageMutation({ fixedCacheKey: 'canvasPasteModal' });
const getPosition = useCallback(
(destination: 'canvas' | 'bbox') => {
const { x, y } = canvasManager.stateApi.getBbox().rect;
if (!canvasManager) {
return { x: 0, y: 0 };
}
const bbox = canvasManager.stateApi.getBbox();
if (!bbox) {
return { x: 0, y: 0 };
}
const { x, y } = bbox.rect;
if (destination === 'bbox') {
return { x, y };
}
@@ -50,7 +57,7 @@ export const CanvasPasteModal = memo(() => {
return { x, y };
}
},
[canvasManager.compositor, canvasManager.stateApi]
[canvasManager]
);
const handlePaste = useCallback(

View File

@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerBadgesContent').withTransparencyEffect
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerBadgesContent').withTransparencyEffect : false
);
const ControlLayerBadgesContent = memo(() => {

View File

@@ -16,7 +16,7 @@ import {
controlLayerControlModeChanged,
controlLayerModelChanged,
controlLayerWeightChanged,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { getFilterForModel } from 'features/controlLayers/store/filters';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
@@ -34,8 +34,8 @@ import type {
const buildSelectControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createSelector(selectCanvasSlice, (canvas) => {
const layer = selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerControlAdapter');
return layer.controlAdapter;
const layer = canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerControlAdapter') : null;
return layer ? layer.controlAdapter : null;
});
export const ControlLayerControlAdapter = memo(() => {
@@ -125,6 +125,10 @@ export const ControlLayerControlAdapter = memo(() => {
);
const uploadApi = useImageUploadButton(uploadOptions);
if (!controlAdapter) {
return null;
}
return (
<Flex flexDir="column" gap={3} position="relative" w="full">
<Flex w="full" gap={2}>
@@ -162,7 +166,7 @@ export const ControlLayerControlAdapter = memo(() => {
<input {...uploadApi.getUploadInputProps()} />
</Flex>
<Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
{controlAdapter.type !== 'control_lora' && (
{(controlAdapter.type === 'controlnet' || controlAdapter.type === 't2i_adapter') && (
<BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
)}
{controlAdapter.type === 'controlnet' && !isFLUX && (

View File

@@ -8,7 +8,7 @@ import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return canvas.controlLayers.entities.map(getEntityIdentifier).toReversed();
return canvas ? canvas.controlLayers.entities.map(getEntityIdentifier).toReversed() : [];
});
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {

View File

@@ -8,7 +8,7 @@ import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
controlLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';

View File

@@ -8,7 +8,7 @@ import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
controlLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';

View File

@@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
@@ -14,7 +14,7 @@ const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentif
createSelector(
selectCanvasSlice,
(canvas) =>
selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect
canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect : false
);
export const ControlLayerMenuItemsTransparencyEffect = memo(() => {

View File

@@ -18,7 +18,7 @@ import { CanvasAutoProcessSwitch } from 'features/controlLayers/components/Canva
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';

View File

@@ -5,7 +5,7 @@ import { selectBbox } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectBboxRect = createSelector(selectBbox, (bbox) => bbox.rect);
const selectBboxRect = createSelector(selectBbox, (bbox) => bbox?.rect || { width: 0, height: 0 });
export const CanvasHUDItemBbox = memo(() => {
const { t } = useTranslation();

View File

@@ -9,7 +9,7 @@ export const CanvasHUDItemScaledBbox = memo(() => {
const scaleMethod = useAppSelector(selectScaleMethod);
const scaledSize = useAppSelector(selectScaledSize);
if (scaleMethod === 'none') {
if (scaleMethod === 'none' || !scaledSize) {
return null;
}

View File

@@ -6,7 +6,7 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import {
inpaintMaskDenoiseLimitChanged,
inpaintMaskDenoiseLimitDeleted,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -20,7 +20,7 @@ export const InpaintMaskDenoiseLimitSlider = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskDenoiseLimitSlider').denoiseLimit
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskDenoiseLimitSlider').denoiseLimit : undefined
),
[entityIdentifier]
);

View File

@@ -8,7 +8,7 @@ import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return canvas.inpaintMasks.entities.map(getEntityIdentifier).toReversed();
return canvas ? canvas.inpaintMasks.entities.map(getEntityIdentifier).toReversed() : [];
});
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {

View File

@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';

View File

@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';

View File

@@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InpaintMaskDeleteModifierButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskDeleteModifierButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasSlice';
import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -17,7 +17,7 @@ export const InpaintMaskNoiseSlider = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel : undefined
),
[entityIdentifier]
);

View File

@@ -10,14 +10,14 @@ import { memo, useMemo } from 'react';
const buildSelectHasDenoiseLimit = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) =>
createSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings');
return entity.denoiseLimit !== undefined;
const entity = canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings') : null;
return entity ? entity.denoiseLimit !== undefined : false
});
const buildSelectHasNoiseLevel = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) =>
createSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings');
return entity.noiseLevel !== undefined;
const entity = canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings') : null;
return entity ? entity.noiseLevel !== undefined : false
});
export const InpaintMaskSettings = memo(() => {

View File

@@ -3,7 +3,9 @@ import { useInvokeCanvas } from 'features/controlLayers/hooks/useInvokeCanvas';
import { memo } from 'react';
export const InvokeCanvasComponent = memo(() => {
console.log('InvokeCanvasComponent rendering');
const ref = useInvokeCanvas();
console.log('InvokeCanvasComponent - ref obtained:', !!ref);
return (
<Box

View File

@@ -7,7 +7,7 @@ import { CanvasEntityEditableTitle } from 'features/controlLayers/components/com
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { ReplaceCanvasEntityObjectsWithImageDndTargetData } from 'features/dnd/dnd';
import { replaceCanvasEntityObjectsWithImageDndTarget } from 'features/dnd/dnd';
@@ -21,7 +21,7 @@ type Props = {
export const RasterLayer = memo(({ id }: Props) => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
const dndTargetData = useMemo<ReplaceCanvasEntityObjectsWithImageDndTargetData>(
() => replaceCanvasEntityObjectsWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),

View File

@@ -8,7 +8,7 @@ import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return canvas.rasterLayers.entities.map(getEntityIdentifier).toReversed();
return canvas ? canvas.rasterLayers.entities.map(getEntityIdentifier).toReversed() : [];
});
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
return selectedEntityIdentifier?.type === 'raster_layer';

View File

@@ -1,5 +1,5 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useExportCanvasToPSD } from 'features/controlLayers/hooks/useExportCanvasToPSD';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,7 +7,7 @@ import { PiFileArrowDownBold } from 'react-icons/pi';
export const RasterLayerExportPSDButton = memo(() => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const { exportCanvasToPSD } = useExportCanvasToPSD();
const onClick = useCallback(() => {

View File

@@ -9,7 +9,7 @@ import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
rasterLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { initialControlNet } from 'features/controlLayers/store/util';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -9,7 +9,7 @@ import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
rasterLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { initialControlNet } from 'features/controlLayers/store/util';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasInstanceSlice';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
import type { ImageWithDims } from 'features/controlLayers/store/types';

View File

@@ -2,7 +2,6 @@ import { Button, Collapse, Divider, Flex, IconButton } from '@invoke-ai/ui-libra
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { RefImagePreview } from 'features/controlLayers/components/RefImage/RefImagePreview';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
import { useNewGlobalReferenceImageFromBbox } from 'features/controlLayers/hooks/saveCanvasHooks';
@@ -121,9 +120,7 @@ const AddRefImageDropTargetAndButton = memo(() => {
<DndDropTarget label="Drop" dndTarget={addGlobalReferenceImageDndTarget} dndTargetData={dndTargetData} />
</Button>
{tab === 'canvas' && (
<CanvasManagerProviderGate>
<BboxButton />
</CanvasManagerProviderGate>
<BboxButton />
)}
</Flex>
);

View File

@@ -10,10 +10,7 @@ import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAd
import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions';
import {
CanvasManagerProviderGate,
useCanvasManagerSafe,
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManagerSafe } from 'features/controlLayers/hooks/useCanvasManager';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
@@ -124,11 +121,7 @@ const RefImageSettingsContent = memo(() => {
{isIPAdapterConfig(config) && (
<IPAdapterCLIPVisionModel model={config.clipVisionModel} onChange={onChangeCLIPVisionModel} />
)}
{tab === 'canvas' && (
<CanvasManagerProviderGate>
<PullBboxIntoRefImageIconButton />
</CanvasManagerProviderGate>
)}
{tab === 'canvas' && <PullBboxIntoRefImageIconButton />}
</Flex>
<Flex gap={2} w="full">
{isIPAdapterConfig(config) && (
@@ -175,11 +168,7 @@ export const RefImageSettings = memo(() => {
const hasImage = useAppSelector(selectIPAdapterHasImage);
if (!hasImage && canvasManager && tab === 'canvas') {
return (
<CanvasManagerProviderGate>
<RefImageNoImageStateWithCanvasOptions />
</CanvasManagerProviderGate>
);
return <RefImageNoImageStateWithCanvasOptions />;
}
if (!hasImage) {

View File

@@ -13,7 +13,7 @@ export const RegionalGuidanceBadges = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceBadges').autoNegative
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceBadges').autoNegative : false
),
[entityIdentifier]
);

View File

@@ -8,6 +8,9 @@ import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return [];
}
return canvas.regionalGuidance.entities.map(getEntityIdentifier).toReversed();
});
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {

View File

@@ -21,7 +21,7 @@ import {
rgRefImageIPAdapterMethodChanged,
rgRefImageIPAdapterWeightChanged,
rgRefImageModelChanged,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
@@ -51,6 +51,9 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
const selectConfig = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return null;
}
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`);
return referenceImage.config;
@@ -112,14 +115,18 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
() =>
setRegionalGuidanceReferenceImageDndTarget.getData(
{ entityIdentifier, referenceImageId },
config.image?.image_name
config?.image?.image_name
),
[entityIdentifier, config.image?.image_name, referenceImageId]
[entityIdentifier, config?.image?.image_name, referenceImageId]
);
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
const isBusy = useCanvasIsBusy();
if (!config) {
return null;
}
return (
<Flex flexDir="column" gap={2}>
<Flex alignItems="center" gap={2}>
@@ -190,6 +197,9 @@ const buildSelectIPAdapterHasImage = (
referenceImageId: string
) =>
createSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return false;
}
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
return !!referenceImage && referenceImage.config.image !== null;
});

View File

@@ -4,7 +4,7 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { rgRefImageDeleted } from 'features/controlLayers/store/canvasSlice';
import { rgRefImageDeleted } from 'features/controlLayers/store/canvasInstanceSlice';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';

View File

@@ -13,6 +13,9 @@ export const RegionalGuidanceIPAdapters = memo(() => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return EMPTY_ARRAY;
}
const ipAdapterIds = selectEntityOrThrow(
canvas,
entityIdentifier,

View File

@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasSlice';
import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,7 +16,7 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceMenuItemsAutoNegative').autoNegative
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceMenuItemsAutoNegative').autoNegative : false
),
[entityIdentifier]
);

View File

@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';

View File

@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';

View File

@@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -22,7 +22,7 @@ export const RegionalGuidanceNegativePrompt = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceNegativePrompt').negativePrompt ?? ''
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceNegativePrompt').negativePrompt ?? '' : ''
),
[entityIdentifier]
);

View File

@@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -22,7 +22,7 @@ export const RegionalGuidancePositivePrompt = memo(() => {
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? ''
(canvas) => canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? '' : ''
),
[entityIdentifier]
);

View File

@@ -14,12 +14,12 @@ import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt
const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceSettings');
return {
const entity = canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceSettings') : null;
return entity ? {
hasPositivePrompt: entity.positivePrompt !== null,
hasNegativePrompt: entity.negativePrompt !== null,
hasIPAdapters: entity.referenceImages.length > 0,
};
} : { hasPositivePrompt: false, hasNegativePrompt: false, hasIPAdapters: false };
});
export const RegionalGuidanceSettings = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');

View File

@@ -22,7 +22,7 @@ import { CanvasAutoProcessSwitch } from 'features/controlLayers/components/Canva
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
import { SelectObjectInvert } from 'features/controlLayers/components/SelectObject/SelectObjectInvert';
import { SelectObjectPointType } from 'features/controlLayers/components/SelectObject/SelectObjectPointType';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';

View File

@@ -1,5 +1,5 @@
import { Button } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,6 +1,6 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { canvasClearHistory } from 'features/controlLayers/store/canvasSlice';
import { canvasClearHistory } from 'features/controlLayers/store/canvasesSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,7 +8,7 @@ export const CanvasSettingsClearHistoryButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(canvasClearHistory());
dispatch(canvasClearHistory({}));
}, [dispatch]);
return (
<Button onClick={onClick} size="sm">

View File

@@ -1,5 +1,5 @@
import { Button } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,5 +1,5 @@
import { Button } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import {
selectStagingAreaAutoSwitch,
settingsStagingAreaAutoSwitchChanged,

View File

@@ -2,7 +2,7 @@ import { Box, Flex, forwardRef } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { logger } from 'app/logging/logger';
import { QueueItemPreviewMini } from 'features/controlLayers/components/StagingArea/QueueItemPreviewMini';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import type { CSSProperties, RefObject } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,7 +1,7 @@
import { Button } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useMemo } from 'react';
export const StagingAreaToolbarImageCountButton = memo(() => {

View File

@@ -1,7 +1,7 @@
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { StagingAreaToolbarNewLayerFromImageMenuItems } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';

View File

@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';

View File

@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { useStagingAreaContext } from 'features/controlLayers/components/StagingArea/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';

View File

@@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiEyeSlashBold } from 'react-icons/pi';

View File

@@ -1,10 +1,10 @@
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasInstanceSlice';
import {
selectStagingAreaAutoSwitch,
settingsStagingAreaAutoSwitchChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import {
buildSelectCanvasQueueItems,
canvasQueueItemDiscarded,
@@ -71,6 +71,9 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
},
onAccept: (item, imageDTO) => {
const bboxRect = selectBboxRect(store.getState());
if (!bboxRect) {
return;
}
const { x, y } = bboxRect;
const imageObject = imageDTOToImageObject(imageDTO);
const selectedEntityIdentifier = selectSelectedEntityIdentifier(store.getState());

View File

@@ -1,7 +1,7 @@
import { useStore } from '@nanostores/react';
import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth';
import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { memo } from 'react';
export const ToolSettings = memo(() => {

View File

@@ -1,19 +1,22 @@
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManagerSafe } from 'features/controlLayers/hooks/useCanvasManager';
import type { Tool } from 'features/controlLayers/store/types';
import { computed } from 'nanostores';
import { useCallback } from 'react';
export const useToolIsSelected = (tool: Tool) => {
const canvasManager = useCanvasManager();
const isSelected = useStore(computed(canvasManager.tool.$tool, (t) => t === tool));
const canvasManager = useCanvasManagerSafe();
const isSelected = useStore(canvasManager ? computed(canvasManager.tool.$tool, (t) => t === tool) : $false);
return isSelected;
};
export const useSelectTool = (tool: Tool) => {
const canvasManager = useCanvasManager();
const canvasManager = useCanvasManagerSafe();
const setTool = useCallback(() => {
canvasManager.tool.$tool.set(tool);
}, [canvasManager.tool.$tool, tool]);
if (canvasManager) {
canvasManager.tool.$tool.set(tool);
}
}, [canvasManager, tool]);
return setTool;
};

View File

@@ -20,6 +20,7 @@ import { useCanvasToggleNonRasterLayersHotkey } from 'features/controlLayers/hoo
import { useCanvasTransformHotkey } from 'features/controlLayers/hooks/useCanvasTransformHotkey';
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
import { useNextPrevEntityHotkeys } from 'features/controlLayers/hooks/useNextPrevEntity';
import { CanvasInstanceManager } from 'features/ui/components/CanvasInstanceManager';
import { memo } from 'react';
export const CanvasToolbar = memo(() => {
@@ -38,6 +39,7 @@ export const CanvasToolbar = memo(() => {
<Flex w="full" gap={2} alignItems="center" px={2}>
<ToolFillColorPicker />
<ToolSettings />
<CanvasInstanceManager />
<Flex alignItems="center" h="full" flexGrow={1} justifyContent="flex-end">
<CanvasToolbarScale />
<CanvasToolbarResetViewButton />

View File

@@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { canvasRedo } from 'features/controlLayers/store/canvasSlice';
import { canvasRedo } from 'features/controlLayers/store/canvasesSlice';
import { selectCanvasMayRedo } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,7 +13,7 @@ export const CanvasToolbarRedoButton = memo(() => {
const isBusy = useCanvasIsBusy();
const mayRedo = useAppSelector(selectCanvasMayRedo);
const onClick = useCallback(() => {
dispatch(canvasRedo());
dispatch(canvasRedo({}));
}, [dispatch]);
return (

View File

@@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -15,7 +15,7 @@ import {
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { round } from 'es-toolkit/compat';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { snapToNearest } from 'features/controlLayers/konva/util';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { canvasUndo } from 'features/controlLayers/store/canvasSlice';
import { canvasUndo } from 'features/controlLayers/store/canvasesSlice';
import { selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,7 +13,7 @@ export const CanvasToolbarUndoButton = memo(() => {
const isBusy = useCanvasIsBusy();
const mayUndo = useAppSelector(selectCanvasMayUndo);
const onClick = useCallback(() => {
dispatch(canvasUndo());
dispatch(canvasUndo({}));
}, [dispatch]);
return (

View File

@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
import { TransformFitToBboxButtons } from 'features/controlLayers/components/Transform/TransformFitToBboxButtons';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useRef } from 'react';

View File

@@ -6,7 +6,7 @@ import {
useAddRasterLayer,
useAddRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -17,7 +17,7 @@ type Props = {
export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { entityDeleted } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCircleBold, PiCircleFill } from 'react-icons/pi';

View File

@@ -26,10 +26,14 @@ const buildSelectWarnings = (entityIdentifier: CanvasEntityIdentifier, t: TFunct
return createSelector(selectCanvasSlice, selectMainModelConfig, (canvas, model) => {
// This component is used within a <CanvasEntityStateGate /> so we can safely assume that the entity exists.
// Should never throw.
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings');
const entity = canvas ? selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings') : null;
let warnings: string[] = [];
if (!entity) {
return warnings;
}
const entityType = entity.type;
if (entityType === 'control_layer') {

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsBookmarkedForQuickSwitch } from 'features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch';
import { bookmarkedEntityChanged } from 'features/controlLayers/store/canvasSlice';
import { bookmarkedEntityChanged } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBookmarkSimpleBold, PiBookmarkSimpleFill } from 'react-icons/pi';

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';

View File

@@ -8,7 +8,7 @@ import {
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
} from 'features/controlLayers/store/canvasSlice';
} from 'features/controlLayers/store/canvasInstanceSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
@@ -55,6 +55,14 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
if (!canvas) {
return {
canMoveForwardOne: false,
canMoveBackwardOne: false,
canMoveToFront: false,
canMoveToBack: false,
};
}
const { index, count } = getIndexAndCount(canvas, entityIdentifier);
return {
canMoveForwardOne: index < count - 1,

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { entityDeleted } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';

View File

@@ -2,7 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { entityDuplicated } from 'features/controlLayers/store/canvasInstanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyFill } from 'react-icons/pi';

View File

@@ -1,5 +1,5 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasManager } from 'features/controlLayers/hooks/useCanvasManager';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';

View File

@@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasManagerSafe } from 'features/controlLayers/hooks/useCanvasManager';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useVisibleEntityCountByType } from 'features/controlLayers/hooks/useVisibleEntityCountByType';
import type { CanvasEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
@@ -13,12 +13,14 @@ type Props = {
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const canvasManager = useCanvasManagerSafe();
const isBusy = useCanvasIsBusySafe();
const entityCount = useVisibleEntityCountByType(type);
const mergeVisible = useCallback(() => {
canvasManager.compositor.mergeVisibleOfType(type);
}, [canvasManager.compositor, type]);
if (canvasManager) {
canvasManager.compositor.mergeVisibleOfType(type);
}
}, [canvasManager, type]);
return (
<IconButton
@@ -29,7 +31,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
icon={<PiStackBold />}
onClick={mergeVisible}
alignSelf="stretch"
isDisabled={entityCount <= 1 || isBusy}
isDisabled={entityCount <= 1 || isBusy || !canvasManager}
/>
);
});

View File

@@ -21,6 +21,9 @@ export const CanvasEntityPreviewImage = memo(() => {
const selectMaskColor = useMemo(
() =>
createSelector(selectCanvasSlice, (state) => {
if (!state) {
return null;
}
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return null;

Some files were not shown because too many files have changed in this diff Show More