@umituz/react-native-image 1.1.6 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -9
- package/src/domain/entities/EditorTypes.ts +180 -0
- package/src/index.ts +11 -0
- package/src/infrastructure/services/ImageEditorService.ts +274 -0
- package/src/infrastructure/utils/CropTool.ts +260 -0
- package/src/infrastructure/utils/DrawingEngine.ts +210 -0
- package/src/infrastructure/utils/FilterProcessor.ts +361 -0
- package/src/infrastructure/utils/LayerManager.ts +158 -0
- package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
- package/src/infrastructure/utils/TextEditor.ts +273 -0
- package/src/presentation/components/CropComponent.tsx +183 -0
- package/src/presentation/components/Editor.tsx +261 -0
- package/src/presentation/components/EditorCanvas.tsx +135 -0
- package/src/presentation/components/EditorPanel.tsx +321 -0
- package/src/presentation/components/EditorToolbar.tsx +180 -0
- package/src/presentation/components/FilterSlider.tsx +123 -0
- package/src/presentation/components/GalleryHeader.tsx +87 -25
- package/src/presentation/components/ImageGallery.tsx +97 -48
- package/src/presentation/hooks/useEditorTools.ts +188 -0
- package/src/presentation/hooks/useImageEditor.ts +165 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-image",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Image manipulation and viewing for React Native apps - resize, crop, rotate, flip, compress, gallery viewer",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -29,24 +29,39 @@
|
|
|
29
29
|
"url": "https://github.com/umituz/react-native-image"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
+
"@umituz/react-native-design-system-atoms": "latest",
|
|
33
|
+
"@umituz/react-native-design-system-theme": "latest",
|
|
32
34
|
"@umituz/react-native-filesystem": "latest",
|
|
35
|
+
"expo-image": ">=1.0.0",
|
|
33
36
|
"expo-image-manipulator": ">=11.0.0",
|
|
34
|
-
"react": ">=
|
|
35
|
-
"react-native": ">=0.
|
|
36
|
-
"react-native-image-viewing": ">=0.2.0"
|
|
37
|
+
"react": ">=19.1.0",
|
|
38
|
+
"react-native": ">=0.81.5",
|
|
39
|
+
"react-native-image-viewing": ">=0.2.0",
|
|
40
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
43
|
+
"@expo/vector-icons": "^15.0.3",
|
|
44
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
45
|
+
"@react-native-community/datetimepicker": "^8.5.1",
|
|
46
|
+
"@react-native-community/slider": "^4.5.5",
|
|
39
47
|
"@react-native/typescript-config": "^0.83.0",
|
|
40
48
|
"@types/react": "^18.2.45",
|
|
41
49
|
"@types/react-native": "^0.73.0",
|
|
42
|
-
"@umituz/react-native-design-system-
|
|
43
|
-
"@umituz/react-native-
|
|
50
|
+
"@umituz/react-native-design-system-atoms": "latest",
|
|
51
|
+
"@umituz/react-native-design-system-responsive": "^1.9.0",
|
|
52
|
+
"@umituz/react-native-design-system-theme": "latest",
|
|
53
|
+
"@umituz/react-native-design-system-typography": "^1.10.0",
|
|
54
|
+
"@umituz/react-native-filesystem": "latest",
|
|
55
|
+
"@umituz/react-native-icons": "^1.1.2",
|
|
44
56
|
"expo-file-system": "^19.0.21",
|
|
57
|
+
"expo-image": "~3.0.11",
|
|
45
58
|
"expo-image-manipulator": "^14.0.8",
|
|
46
|
-
"react": "
|
|
47
|
-
"react-native": "
|
|
59
|
+
"react": "19.1.0",
|
|
60
|
+
"react-native": "0.81.5",
|
|
48
61
|
"react-native-image-viewing": "^0.2.2",
|
|
49
|
-
"
|
|
62
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
63
|
+
"typescript": "^5.3.3",
|
|
64
|
+
"zustand": "^5.0.9"
|
|
50
65
|
},
|
|
51
66
|
"publishConfig": {
|
|
52
67
|
"access": "public"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain - Advanced Editor Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum EditorTool {
|
|
6
|
+
MOVE = 'move',
|
|
7
|
+
BRUSH = 'brush',
|
|
8
|
+
ERASER = 'eraser',
|
|
9
|
+
TEXT = 'text',
|
|
10
|
+
SHAPE = 'shape',
|
|
11
|
+
CROP = 'crop',
|
|
12
|
+
FILTER = 'filter',
|
|
13
|
+
STICKER = 'sticker',
|
|
14
|
+
SELECT = 'select',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum ShapeType {
|
|
18
|
+
RECTANGLE = 'rectangle',
|
|
19
|
+
CIRCLE = 'circle',
|
|
20
|
+
LINE = 'line',
|
|
21
|
+
ARROW = 'arrow',
|
|
22
|
+
TRIANGLE = 'triangle',
|
|
23
|
+
STAR = 'star',
|
|
24
|
+
HEART = 'heart',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum BrushStyle {
|
|
28
|
+
NORMAL = 'normal',
|
|
29
|
+
MARKER = 'marker',
|
|
30
|
+
SPRAY = 'spray',
|
|
31
|
+
PENCIL = 'pencil',
|
|
32
|
+
CALIGRAPHY = 'caligraphy',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EditorPoint {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EditorDimensions {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EditorStroke {
|
|
46
|
+
points: EditorPoint[];
|
|
47
|
+
color: string;
|
|
48
|
+
size: number;
|
|
49
|
+
style: BrushStyle;
|
|
50
|
+
opacity: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface EditorShape {
|
|
54
|
+
type: ShapeType;
|
|
55
|
+
startPoint: EditorPoint;
|
|
56
|
+
endPoint: EditorPoint;
|
|
57
|
+
color: string;
|
|
58
|
+
strokeWidth: number;
|
|
59
|
+
fillColor?: string;
|
|
60
|
+
opacity: number;
|
|
61
|
+
rotation?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface EditorText {
|
|
65
|
+
id: string;
|
|
66
|
+
text: string;
|
|
67
|
+
position: EditorPoint;
|
|
68
|
+
fontSize: number;
|
|
69
|
+
fontFamily: string;
|
|
70
|
+
color: string;
|
|
71
|
+
backgroundColor?: string;
|
|
72
|
+
rotation: number;
|
|
73
|
+
opacity: number;
|
|
74
|
+
maxWidth?: number;
|
|
75
|
+
textAlign: 'left' | 'center' | 'right';
|
|
76
|
+
fontWeight: 'normal' | 'bold';
|
|
77
|
+
fontStyle: 'normal' | 'italic';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EditorSticker {
|
|
81
|
+
id: string;
|
|
82
|
+
uri: string;
|
|
83
|
+
position: EditorPoint;
|
|
84
|
+
size: EditorDimensions;
|
|
85
|
+
rotation: number;
|
|
86
|
+
opacity: number;
|
|
87
|
+
scale: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface EditorLayer {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
visible: boolean;
|
|
94
|
+
opacity: number;
|
|
95
|
+
locked: boolean;
|
|
96
|
+
elements: Array<{
|
|
97
|
+
type: 'stroke' | 'shape' | 'text' | 'sticker';
|
|
98
|
+
data: EditorStroke | EditorShape | EditorText | EditorSticker;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface EditorSelection {
|
|
103
|
+
bounds: {
|
|
104
|
+
x: number;
|
|
105
|
+
y: number;
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
};
|
|
109
|
+
elements: string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface EditorHistory {
|
|
113
|
+
id: string;
|
|
114
|
+
timestamp: Date;
|
|
115
|
+
layers: EditorLayer[];
|
|
116
|
+
thumbnail?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface EditorCropArea {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
aspectRatio?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface EditorFilter {
|
|
128
|
+
type: string;
|
|
129
|
+
intensity: number;
|
|
130
|
+
preview?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface EditorState {
|
|
134
|
+
originalUri: string;
|
|
135
|
+
currentUri?: string;
|
|
136
|
+
tool: EditorTool;
|
|
137
|
+
selectedLayer?: string;
|
|
138
|
+
layers: EditorLayer[];
|
|
139
|
+
history: EditorHistory[];
|
|
140
|
+
historyIndex: number;
|
|
141
|
+
selection?: EditorSelection;
|
|
142
|
+
cropArea?: EditorCropArea;
|
|
143
|
+
activeFilter?: EditorFilter;
|
|
144
|
+
isDirty: boolean;
|
|
145
|
+
dimensions: EditorDimensions;
|
|
146
|
+
zoom: number;
|
|
147
|
+
pan: EditorPoint;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface EditorOptions {
|
|
151
|
+
maxLayers?: number;
|
|
152
|
+
maxHistory?: number;
|
|
153
|
+
enableUndo?: boolean;
|
|
154
|
+
enableRedo?: boolean;
|
|
155
|
+
enableFilters?: boolean;
|
|
156
|
+
enableShapes?: boolean;
|
|
157
|
+
enableText?: boolean;
|
|
158
|
+
enableStickers?: boolean;
|
|
159
|
+
enableCrop?: boolean;
|
|
160
|
+
brushSizeRange?: [number, number];
|
|
161
|
+
strokeWidthRange?: [number, number];
|
|
162
|
+
fontSizeRange?: [number, number];
|
|
163
|
+
defaultColors?: string[];
|
|
164
|
+
stickerPacks?: string[];
|
|
165
|
+
customFonts?: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface EditorEvent {
|
|
169
|
+
type: 'toolChange' | 'layerAdd' | 'layerRemove' | 'layerUpdate' | 'selectionChange' | 'historyChange';
|
|
170
|
+
data: any;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface EditorExportOptions {
|
|
174
|
+
format: 'jpeg' | 'png' | 'webp';
|
|
175
|
+
quality: number;
|
|
176
|
+
backgroundColor?: string;
|
|
177
|
+
includeHiddenLayers?: boolean;
|
|
178
|
+
flattenLayers?: boolean;
|
|
179
|
+
maxSize?: number;
|
|
180
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -89,7 +89,18 @@ export { ImageGallery, type ImageGalleryProps } from './presentation/components/
|
|
|
89
89
|
export { useImage } from './presentation/hooks/useImage';
|
|
90
90
|
export { useImageTransform } from './presentation/hooks/useImageTransform';
|
|
91
91
|
export { useImageConversion } from './presentation/hooks/useImageConversion';
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// PRESENTATION LAYER - Editor Components & Hooks
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
92
96
|
export { useImageEditor } from './presentation/hooks/useImageEditor';
|
|
97
|
+
export { useEditorTools } from './presentation/hooks/useEditorTools';
|
|
98
|
+
export { Editor } from './presentation/components/Editor';
|
|
99
|
+
export { EditorCanvas } from './presentation/components/EditorCanvas';
|
|
100
|
+
export { EditorToolbar } from './presentation/components/EditorToolbar';
|
|
101
|
+
export { EditorPanel } from './presentation/components/EditorPanel';
|
|
102
|
+
export { CropComponent } from './presentation/components/CropComponent';
|
|
103
|
+
export { FilterSlider } from './presentation/components/FilterSlider';
|
|
93
104
|
|
|
94
105
|
export {
|
|
95
106
|
useImageGallery,
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Advanced Editor Service
|
|
3
|
+
*
|
|
4
|
+
* Core editing functionality with history management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EditorTool, type EditorState, type EditorLayer, type EditorHistory, type EditorOptions } from '../../domain/entities/EditorTypes';
|
|
8
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
9
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
10
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
11
|
+
|
|
12
|
+
export class ImageEditorService {
|
|
13
|
+
private static generateId(): string {
|
|
14
|
+
return Math.random().toString(36).substr(2, 9);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static createInitialState(
|
|
18
|
+
uri: string,
|
|
19
|
+
dimensions: { width: number; height: number },
|
|
20
|
+
options: EditorOptions = {}
|
|
21
|
+
): EditorState {
|
|
22
|
+
const defaultLayer: EditorLayer = {
|
|
23
|
+
id: 'background',
|
|
24
|
+
name: 'Background',
|
|
25
|
+
visible: true,
|
|
26
|
+
opacity: 1,
|
|
27
|
+
locked: true,
|
|
28
|
+
elements: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
originalUri: uri,
|
|
33
|
+
tool: EditorTool.MOVE,
|
|
34
|
+
layers: [defaultLayer],
|
|
35
|
+
history: [{
|
|
36
|
+
id: ImageEditorService.generateId(),
|
|
37
|
+
timestamp: new Date(),
|
|
38
|
+
layers: [defaultLayer],
|
|
39
|
+
}],
|
|
40
|
+
historyIndex: 0,
|
|
41
|
+
isDirty: false,
|
|
42
|
+
dimensions,
|
|
43
|
+
zoom: 1,
|
|
44
|
+
pan: { x: 0, y: 0 },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static addLayer(
|
|
49
|
+
state: EditorState,
|
|
50
|
+
name?: string
|
|
51
|
+
): EditorState {
|
|
52
|
+
const newLayer: EditorLayer = {
|
|
53
|
+
id: ImageEditorService.generateId(),
|
|
54
|
+
name: name || `Layer ${state.layers.length}`,
|
|
55
|
+
visible: true,
|
|
56
|
+
opacity: 1,
|
|
57
|
+
locked: false,
|
|
58
|
+
elements: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const newHistory: EditorHistory = {
|
|
62
|
+
id: ImageEditorService.generateId(),
|
|
63
|
+
timestamp: new Date(),
|
|
64
|
+
layers: [...state.layers, newLayer],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...newHistoryState,
|
|
71
|
+
layers: [...state.layers, newLayer],
|
|
72
|
+
selectedLayer: newLayer.id,
|
|
73
|
+
isDirty: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static removeLayer(
|
|
78
|
+
state: EditorState,
|
|
79
|
+
layerId: string
|
|
80
|
+
): EditorState {
|
|
81
|
+
if (state.layers.length <= 1) {
|
|
82
|
+
throw ImageErrorHandler.createError(
|
|
83
|
+
'Cannot remove the last layer',
|
|
84
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
85
|
+
'removeLayer'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newLayers = state.layers.filter(layer => layer.id !== layerId);
|
|
90
|
+
const newHistory: EditorHistory = {
|
|
91
|
+
id: ImageEditorService.generateId(),
|
|
92
|
+
timestamp: new Date(),
|
|
93
|
+
layers: newLayers,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...newHistoryState,
|
|
100
|
+
layers: newLayers,
|
|
101
|
+
selectedLayer: newLayers[0].id,
|
|
102
|
+
isDirty: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static updateLayer(
|
|
107
|
+
state: EditorState,
|
|
108
|
+
layerId: string,
|
|
109
|
+
updates: Partial<EditorLayer>
|
|
110
|
+
): EditorState {
|
|
111
|
+
const newLayers = state.layers.map(layer =>
|
|
112
|
+
layer.id === layerId ? { ...layer, ...updates } : layer
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const newHistory: EditorHistory = {
|
|
116
|
+
id: ImageEditorService.generateId(),
|
|
117
|
+
timestamp: new Date(),
|
|
118
|
+
layers: newLayers,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...newHistoryState,
|
|
125
|
+
layers: newLayers,
|
|
126
|
+
isDirty: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static addElementToLayer(
|
|
131
|
+
state: EditorState,
|
|
132
|
+
layerId: string,
|
|
133
|
+
element: any
|
|
134
|
+
): EditorState {
|
|
135
|
+
const targetLayer = state.layers.find(layer => layer.id === layerId);
|
|
136
|
+
if (!targetLayer) {
|
|
137
|
+
throw ImageErrorHandler.createError(
|
|
138
|
+
'Layer not found',
|
|
139
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
140
|
+
'addElementToLayer'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (targetLayer.locked) {
|
|
145
|
+
throw ImageErrorHandler.createError(
|
|
146
|
+
'Cannot add element to locked layer',
|
|
147
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
148
|
+
'addElementToLayer'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const newLayers = state.layers.map(layer =>
|
|
153
|
+
layer.id === layerId
|
|
154
|
+
? { ...layer, elements: [...layer.elements, element] }
|
|
155
|
+
: layer
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const newHistory: EditorHistory = {
|
|
159
|
+
id: ImageEditorService.generateId(),
|
|
160
|
+
timestamp: new Date(),
|
|
161
|
+
layers: newLayers,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const newHistoryState = ImageEditorService.addToHistory(state, newHistory);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...newHistoryState,
|
|
168
|
+
layers: newLayers,
|
|
169
|
+
isDirty: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static undo(state: EditorState): EditorState {
|
|
174
|
+
if (state.historyIndex <= 0) {
|
|
175
|
+
return state;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const newIndex = state.historyIndex - 1;
|
|
179
|
+
const historyState = state.history[newIndex];
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...state,
|
|
183
|
+
layers: historyState.layers,
|
|
184
|
+
historyIndex: newIndex,
|
|
185
|
+
isDirty: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static redo(state: EditorState): EditorState {
|
|
190
|
+
if (state.historyIndex >= state.history.length - 1) {
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const newIndex = state.historyIndex + 1;
|
|
195
|
+
const historyState = state.history[newIndex];
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...state,
|
|
199
|
+
layers: historyState.layers,
|
|
200
|
+
historyIndex: newIndex,
|
|
201
|
+
isDirty: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static addToHistory(
|
|
206
|
+
state: EditorState,
|
|
207
|
+
newHistory: EditorHistory,
|
|
208
|
+
maxHistory: number = 50
|
|
209
|
+
): EditorState {
|
|
210
|
+
const newHistoryArray = [...state.history.slice(0, state.historyIndex + 1), newHistory];
|
|
211
|
+
|
|
212
|
+
// Keep only the last maxHistory states
|
|
213
|
+
if (newHistoryArray.length > maxHistory) {
|
|
214
|
+
newHistoryArray.shift();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...state,
|
|
219
|
+
history: newHistoryArray,
|
|
220
|
+
historyIndex: newHistoryArray.length - 1,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static setTool(state: EditorState, tool: EditorTool): EditorState {
|
|
225
|
+
return { ...state, tool };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static setSelectedLayer(state: EditorState, layerId?: string): EditorState {
|
|
229
|
+
return { ...state, selectedLayer: layerId };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static setZoom(state: EditorState, zoom: number): EditorState {
|
|
233
|
+
return { ...state, zoom: Math.max(0.1, Math.min(5, zoom)) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static setPan(state: EditorState, pan: { x: number; y: number }): EditorState {
|
|
237
|
+
return { ...state, pan };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static canUndo(state: EditorState): boolean {
|
|
241
|
+
return state.historyIndex > 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static canRedo(state: EditorState): boolean {
|
|
245
|
+
return state.historyIndex < state.history.length - 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static getVisibleLayers(state: EditorState): EditorLayer[] {
|
|
249
|
+
return state.layers.filter(layer => layer.visible);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static getActiveLayers(state: EditorState): EditorLayer[] {
|
|
253
|
+
return state.layers.filter(layer => layer.visible && !layer.locked);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static exportState(state: EditorState): EditorState {
|
|
257
|
+
return {
|
|
258
|
+
...state,
|
|
259
|
+
currentUri: undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static importState(data: any, uri: string): EditorState {
|
|
264
|
+
try {
|
|
265
|
+
return {
|
|
266
|
+
...data,
|
|
267
|
+
originalUri: uri,
|
|
268
|
+
currentUri: undefined,
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw ImageErrorHandler.handleUnknownError(error, 'importState');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|