mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-16 23:58:03 -05:00
Compare commits
55 Commits
controlnet
...
psyche/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
497859297f | ||
|
|
764fc6d5b4 | ||
|
|
61efbec862 | ||
|
|
903a2f88c7 | ||
|
|
bcc20cb332 | ||
|
|
405c2094b2 | ||
|
|
9ba71d5eef | ||
|
|
8365bc12b2 | ||
|
|
7f762f5c3a | ||
|
|
aee5b09541 | ||
|
|
4bd7de131c | ||
|
|
b5d74214aa | ||
|
|
19ada649c4 | ||
|
|
e84e371744 | ||
|
|
2c877e8d79 | ||
|
|
a730acf9ed | ||
|
|
aa7123cc22 | ||
|
|
2ee4fe1c7d | ||
|
|
80f02499de | ||
|
|
e0502d26d0 | ||
|
|
f8cab78906 | ||
|
|
461695ea14 | ||
|
|
f3f6049604 | ||
|
|
439969b698 | ||
|
|
77d7b22acd | ||
|
|
1e7f631552 | ||
|
|
318b090a52 | ||
|
|
e13028022f | ||
|
|
c9b7295da7 | ||
|
|
9a27a88efe | ||
|
|
de255819b7 | ||
|
|
21b1555c64 | ||
|
|
fb97dbaab7 | ||
|
|
324f5eedfa | ||
|
|
5c36afec5c | ||
|
|
e650fcfbc6 | ||
|
|
d52cea1261 | ||
|
|
2c8a0ea294 | ||
|
|
bca552fa26 | ||
|
|
a3723ce22a | ||
|
|
dd2b9a1c58 | ||
|
|
3419b49e4f | ||
|
|
d3c112b01e | ||
|
|
1d1a213de4 | ||
|
|
172a2c116e | ||
|
|
3aa37ab8f7 | ||
|
|
fbe19a9392 | ||
|
|
12ba2cd9fd | ||
|
|
024c25eca6 | ||
|
|
cfa057396c | ||
|
|
15bbbde803 | ||
|
|
86d25a019f | ||
|
|
4e1f8cdc0a | ||
|
|
ef02527c46 | ||
|
|
0e7bff2673 |
602
CANVAS_MULTI_INSTANCE.md
Normal file
602
CANVAS_MULTI_INSTANCE.md
Normal 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
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -13,6 +13,9 @@ export const RegionalGuidanceIPAdapters = memo(() => {
|
||||
const selectIPAdapterIds = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
if (!canvas) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
const ipAdapterIds = selectEntityOrThrow(
|
||||
canvas,
|
||||
entityIdentifier,
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user