@umituz/react-native-photo-editor 2.0.23 → 2.0.25
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 +1 -1
- package/src/PhotoEditor.tsx +43 -137
- package/src/application/hooks/useEditor.ts +4 -6
- package/src/application/hooks/useEditorUI.ts +8 -5
- package/src/application/stores/EditorStore.ts +17 -6
- package/src/domain/entities/Layer.entity.ts +86 -0
- package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
- package/src/domain/entities/StickerLayer.entity.ts +37 -0
- package/src/domain/entities/TextLayer.entity.ts +58 -0
- package/src/domain/entities/index.ts +9 -0
- package/src/domain/services/History.service.ts +69 -0
- package/src/domain/services/LayerFactory.service.ts +81 -0
- package/src/domain/services/LayerRepository.service.ts +85 -0
- package/src/domain/services/LayerService.ts +1 -1
- package/src/domain/types.ts +39 -0
- package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
- package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
- package/src/domain/value-objects/Transform.vo.ts +61 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/index.ts +4 -4
- package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
- package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
- package/src/presentation/components/DraggableLayer.tsx +13 -13
- package/src/presentation/components/EditorCanvas.tsx +5 -5
- package/src/presentation/components/EditorContent.tsx +72 -0
- package/src/presentation/components/EditorHeader.tsx +48 -0
- package/src/presentation/components/EditorSheets.tsx +85 -0
- package/src/presentation/components/FontControls.tsx +2 -2
- package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
- package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
- package/src/presentation/components/sheets/LayerManager.tsx +3 -4
- package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
- package/src/types.ts +8 -18
- package/src/utils/constants.ts +84 -0
- package/src/utils/formatters.ts +29 -0
- package/src/utils/helpers.ts +51 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/validators.ts +38 -0
- package/ARCHITECTURE.md +0 -104
- package/MIGRATION.md +0 -100
- package/src/components/AIMagicSheet.tsx +0 -107
- package/src/components/AdjustmentsSheet.tsx +0 -108
- package/src/components/ColorPicker.tsx +0 -77
- package/src/components/DraggableSticker.tsx +0 -161
- package/src/components/DraggableText.tsx +0 -181
- package/src/components/EditorCanvas.tsx +0 -106
- package/src/components/EditorToolbar.tsx +0 -155
- package/src/components/FilterPicker.tsx +0 -73
- package/src/components/FontControls.tsx +0 -132
- package/src/components/LayerManager.tsx +0 -164
- package/src/components/Slider.tsx +0 -112
- package/src/components/StickerPicker.tsx +0 -47
- package/src/components/TextEditorSheet.tsx +0 -160
- package/src/core/HistoryManager.ts +0 -53
- package/src/hooks/usePhotoEditor.ts +0 -172
- package/src/hooks/usePhotoEditorUI.ts +0 -162
- package/src/infrastructure/history/HistoryManager.ts +0 -38
package/package.json
CHANGED
package/src/PhotoEditor.tsx
CHANGED
|
@@ -3,31 +3,25 @@
|
|
|
3
3
|
* Main entry point for the photo editor
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
7
|
-
import { View
|
|
8
|
-
import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
|
|
9
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
10
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View } from "react-native";
|
|
11
8
|
import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
|
|
12
|
-
|
|
13
|
-
import {
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
+
import { EditorHeader } from "./presentation/components/EditorHeader";
|
|
11
|
+
import { EditorContent } from "./presentation/components/EditorContent";
|
|
12
|
+
import { EditorSheets } from "./presentation/components/EditorSheets";
|
|
14
13
|
import { EditorToolbar } from "./presentation/components/EditorToolbar";
|
|
15
|
-
import { FontControls } from "./presentation/components/FontControls";
|
|
16
|
-
import { TextEditorSheet } from "./presentation/components/sheets/TextEditorSheet";
|
|
17
|
-
import { StickerPicker } from "./presentation/components/sheets/StickerPicker";
|
|
18
|
-
import { FilterSheet } from "./presentation/components/sheets/FilterSheet";
|
|
19
|
-
import { AdjustmentsSheet } from "./presentation/components/sheets/AdjustmentsSheet";
|
|
20
|
-
import { LayerManager } from "./presentation/components/sheets/LayerManager";
|
|
21
|
-
import { AIMagicSheet } from "./presentation/components/sheets/AIMagicSheet";
|
|
22
14
|
import { useEditorUI } from "./application/hooks/useEditorUI";
|
|
23
|
-
import { DEFAULT_FONTS } from "./constants";
|
|
15
|
+
import { DEFAULT_FONTS } from "./utils/constants";
|
|
16
|
+
import type { Layer } from "./domain/entities/Layer.entity";
|
|
17
|
+
import type { FilterSettings } from "./domain/value-objects/FilterSettings.vo";
|
|
24
18
|
|
|
25
19
|
export interface PhotoEditorProps {
|
|
26
20
|
imageUri: string;
|
|
27
|
-
onSave?: (uri: string, layers:
|
|
21
|
+
onSave?: (uri: string, layers: Layer[], filters: FilterSettings) => void;
|
|
28
22
|
onClose: () => void;
|
|
29
23
|
title?: string;
|
|
30
|
-
customTools?: React.ReactNode | ((ui:
|
|
24
|
+
customTools?: React.ReactNode | ((ui: any) => React.ReactNode);
|
|
31
25
|
initialCaption?: string;
|
|
32
26
|
t: (key: string) => string;
|
|
33
27
|
fonts?: readonly string[];
|
|
@@ -49,73 +43,42 @@ export function PhotoEditor({
|
|
|
49
43
|
const insets = useSafeAreaInsets();
|
|
50
44
|
const ui = useEditorUI(initialCaption);
|
|
51
45
|
|
|
52
|
-
const styles = useMemo(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
justifyContent: "space-between" as const,
|
|
62
|
-
paddingHorizontal: tokens.spacing.md,
|
|
63
|
-
paddingVertical: tokens.spacing.sm,
|
|
64
|
-
borderBottomWidth: 1,
|
|
65
|
-
borderBottomColor: tokens.colors.border,
|
|
66
|
-
},
|
|
67
|
-
headerTitle: {
|
|
68
|
-
flex: 1,
|
|
69
|
-
textAlign: "center",
|
|
70
|
-
},
|
|
71
|
-
scrollContent: {
|
|
72
|
-
padding: tokens.spacing.md,
|
|
73
|
-
gap: tokens.spacing.md,
|
|
74
|
-
},
|
|
75
|
-
}), [tokens, insets]);
|
|
76
|
-
|
|
77
|
-
const handleSave = useCallback(
|
|
78
|
-
() => onSave?.(imageUri, ui.layers.map(l => l.toJSON()), ui.filters),
|
|
79
|
-
[onSave, imageUri, ui.layers, ui.filters],
|
|
46
|
+
const styles = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
container: {
|
|
49
|
+
flex: 1,
|
|
50
|
+
backgroundColor: tokens.colors.surface,
|
|
51
|
+
paddingTop: insets.top,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
[tokens, insets]
|
|
80
55
|
);
|
|
81
56
|
|
|
57
|
+
const handleSave = () =>
|
|
58
|
+
onSave?.(imageUri, ui.layers, ui.filters);
|
|
59
|
+
|
|
82
60
|
return (
|
|
83
61
|
<View style={styles.container}>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{title}
|
|
91
|
-
</AtomicText>
|
|
92
|
-
<TouchableOpacity onPress={handleSave} accessibilityLabel="Save" accessibilityRole="button">
|
|
93
|
-
<AtomicText fontWeight="bold" color="primary">
|
|
94
|
-
{t("common.save") || "Save"}
|
|
95
|
-
</AtomicText>
|
|
96
|
-
</TouchableOpacity>
|
|
97
|
-
</View>
|
|
98
|
-
|
|
99
|
-
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
100
|
-
<EditorCanvas
|
|
101
|
-
imageUrl={imageUri}
|
|
102
|
-
layers={ui.layers}
|
|
103
|
-
activeLayerId={ui.activeLayerId}
|
|
104
|
-
filters={ui.filters}
|
|
105
|
-
onLayerTap={ui.handleTextLayerTap}
|
|
106
|
-
onLayerTransform={ui.handleLayerTransform}
|
|
107
|
-
/>
|
|
108
|
-
|
|
109
|
-
{typeof customTools === "function" ? customTools(ui) : customTools}
|
|
62
|
+
<EditorHeader
|
|
63
|
+
title={title}
|
|
64
|
+
saveLabel={t("common.save") || "Save"}
|
|
65
|
+
onClose={onClose}
|
|
66
|
+
onSave={handleSave}
|
|
67
|
+
/>
|
|
110
68
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
69
|
+
<EditorContent
|
|
70
|
+
imageUrl={imageUri}
|
|
71
|
+
layers={ui.layers}
|
|
72
|
+
filters={ui.filters}
|
|
73
|
+
activeLayerId={ui.activeLayerId}
|
|
74
|
+
selectedFont={ui.selectedFont}
|
|
75
|
+
fontSize={ui.fontSize}
|
|
76
|
+
fonts={fonts}
|
|
77
|
+
customTools={customTools}
|
|
78
|
+
ui={ui}
|
|
79
|
+
onFontSizeChange={ui.setFontSize}
|
|
80
|
+
onFontSelect={ui.setSelectedFont}
|
|
81
|
+
/>
|
|
119
82
|
|
|
120
83
|
<EditorToolbar
|
|
121
84
|
onAddText={ui.handleAddText}
|
|
@@ -131,64 +94,7 @@ export function PhotoEditor({
|
|
|
131
94
|
t={t}
|
|
132
95
|
/>
|
|
133
96
|
|
|
134
|
-
{
|
|
135
|
-
<BottomSheetModal ref={ui.textEditorSheetRef} snapPoints={["55%"]}>
|
|
136
|
-
<TextEditorSheet
|
|
137
|
-
value={ui.editingText}
|
|
138
|
-
onChange={ui.setEditingText}
|
|
139
|
-
onSave={ui.handleSaveText}
|
|
140
|
-
t={t}
|
|
141
|
-
color={ui.editingColor}
|
|
142
|
-
onColorChange={ui.setEditingColor}
|
|
143
|
-
textAlign={ui.editingAlign}
|
|
144
|
-
onTextAlignChange={ui.setEditingAlign}
|
|
145
|
-
isBold={ui.editingBold}
|
|
146
|
-
onBoldChange={ui.setEditingBold}
|
|
147
|
-
isItalic={ui.editingItalic}
|
|
148
|
-
onItalicChange={ui.setEditingItalic}
|
|
149
|
-
/>
|
|
150
|
-
</BottomSheetModal>
|
|
151
|
-
|
|
152
|
-
<BottomSheetModal ref={ui.stickerSheetRef} snapPoints={["50%"]}>
|
|
153
|
-
<StickerPicker onSelectSticker={ui.handleSelectSticker} />
|
|
154
|
-
</BottomSheetModal>
|
|
155
|
-
|
|
156
|
-
<BottomSheetModal ref={ui.filterSheetRef} snapPoints={["40%"]}>
|
|
157
|
-
<FilterSheet
|
|
158
|
-
selectedFilter={ui.selectedFilter}
|
|
159
|
-
onSelectFilter={(option) => {
|
|
160
|
-
ui.setSelectedFilter(option.id);
|
|
161
|
-
ui.updateFilters(option.filters);
|
|
162
|
-
ui.filterSheetRef.current?.dismiss();
|
|
163
|
-
}}
|
|
164
|
-
/>
|
|
165
|
-
</BottomSheetModal>
|
|
166
|
-
|
|
167
|
-
<BottomSheetModal ref={ui.adjustmentsSheetRef} snapPoints={["55%"]}>
|
|
168
|
-
<AdjustmentsSheet
|
|
169
|
-
filters={ui.filters}
|
|
170
|
-
onFiltersChange={ui.updateFilters}
|
|
171
|
-
/>
|
|
172
|
-
</BottomSheetModal>
|
|
173
|
-
|
|
174
|
-
<BottomSheetModal ref={ui.layerSheetRef} snapPoints={["55%"]}>
|
|
175
|
-
<LayerManager
|
|
176
|
-
layers={ui.layers}
|
|
177
|
-
activeLayerId={ui.activeLayerId}
|
|
178
|
-
onSelectLayer={ui.selectLayer}
|
|
179
|
-
onDeleteLayer={ui.deleteLayer}
|
|
180
|
-
onDuplicateLayer={ui.duplicateLayer}
|
|
181
|
-
onMoveLayerUp={ui.moveLayerUp}
|
|
182
|
-
onMoveLayerDown={ui.moveLayerDown}
|
|
183
|
-
t={t}
|
|
184
|
-
/>
|
|
185
|
-
</BottomSheetModal>
|
|
186
|
-
|
|
187
|
-
{onAICaption && (
|
|
188
|
-
<BottomSheetModal ref={ui.aiSheetRef} snapPoints={["60%"]}>
|
|
189
|
-
<AIMagicSheet onGenerateCaption={onAICaption} />
|
|
190
|
-
</BottomSheetModal>
|
|
191
|
-
)}
|
|
97
|
+
<EditorSheets ui={ui} filters={ui.filters} t={t} onAICaption={onAICaption} />
|
|
192
98
|
</View>
|
|
193
99
|
);
|
|
194
100
|
}
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import { useEditorStore } from "../stores/EditorStore";
|
|
8
|
-
import { Layer } from "../../domain/entities/Layer";
|
|
9
|
-
import type { TextLayerData } from "../../domain/entities/Layer";
|
|
10
|
-
import type { Transform } from "../../domain/entities/Transform";
|
|
11
|
-
import type { FilterValues } from "../../domain/entities/Filters";
|
|
12
8
|
|
|
13
9
|
export function useEditor() {
|
|
14
10
|
const store = useEditorStore();
|
|
@@ -30,6 +26,7 @@ export function useEditor() {
|
|
|
30
26
|
addTextLayer: store.addTextLayer,
|
|
31
27
|
addStickerLayer: store.addStickerLayer,
|
|
32
28
|
updateLayer: store.updateLayer,
|
|
29
|
+
updateTextLayerContent: store.updateTextLayerContent,
|
|
33
30
|
deleteLayer: store.deleteLayer,
|
|
34
31
|
duplicateLayer: store.duplicateLayer,
|
|
35
32
|
moveLayerUp: store.moveLayerUp,
|
|
@@ -51,6 +48,7 @@ export function useEditor() {
|
|
|
51
48
|
store.addTextLayer,
|
|
52
49
|
store.addStickerLayer,
|
|
53
50
|
store.updateLayer,
|
|
51
|
+
store.updateTextLayerContent,
|
|
54
52
|
store.deleteLayer,
|
|
55
53
|
store.duplicateLayer,
|
|
56
54
|
store.moveLayerUp,
|
|
@@ -61,7 +59,7 @@ export function useEditor() {
|
|
|
61
59
|
]);
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
export type { Layer } from "
|
|
62
|
+
export type { Layer } from "../entities/Layer.entity"";
|
|
65
63
|
export type { Transform } from "../../domain/entities/Transform";
|
|
66
64
|
export type { FilterValues } from "../../domain/entities/Filters";
|
|
67
|
-
export type { TextLayerData } from "
|
|
65
|
+
export type { TextLayerData } from "../entities/Layer.entity"";
|
|
@@ -7,7 +7,8 @@ import { useState, useRef, useCallback, useEffect } from "react";
|
|
|
7
7
|
import type { BottomSheetModalRef } from "@umituz/react-native-design-system/molecules";
|
|
8
8
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
9
9
|
import { useEditor } from "./useEditor";
|
|
10
|
-
import type { TextAlign } from "
|
|
10
|
+
import type { TextAlign } from "../entities/Layer.entity"";
|
|
11
|
+
import type { Transform } from "../../domain/entities/Transform";
|
|
11
12
|
|
|
12
13
|
export function useEditorUI(initialCaption?: string) {
|
|
13
14
|
const tokens = useAppDesignTokens();
|
|
@@ -50,8 +51,8 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
50
51
|
// Handle text layer tap
|
|
51
52
|
const handleTextLayerTap = useCallback((layerId: string) => {
|
|
52
53
|
editor.selectLayer(layerId);
|
|
53
|
-
const layer = editor.layers.find(l => l.id === layerId);
|
|
54
|
-
if (layer
|
|
54
|
+
const layer = editor.layers.find((l: { id: string }) => l.id === layerId);
|
|
55
|
+
if (layer && layer.isText()) {
|
|
55
56
|
setEditingText(layer.text ?? "");
|
|
56
57
|
setFontSize(layer.fontSize ?? 48);
|
|
57
58
|
setEditingColor(layer.color ?? tokens.colors.textPrimary);
|
|
@@ -64,8 +65,8 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
64
65
|
|
|
65
66
|
// Handle save text
|
|
66
67
|
const handleSaveText = useCallback(() => {
|
|
67
|
-
if (editor.activeLayerId) {
|
|
68
|
-
editor.
|
|
68
|
+
if (editor.activeLayerId && editor.activeLayer?.isText()) {
|
|
69
|
+
editor.updateTextLayerContent(editor.activeLayerId, {
|
|
69
70
|
text: editingText,
|
|
70
71
|
fontSize,
|
|
71
72
|
fontFamily: selectedFont,
|
|
@@ -143,3 +144,5 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
143
144
|
handleLayerTransform,
|
|
144
145
|
};
|
|
145
146
|
}
|
|
147
|
+
|
|
148
|
+
export type EditorUIState = ReturnType<typeof useEditorUI>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback, useMemo } from "react";
|
|
7
|
-
import { Layer,
|
|
8
|
-
import { FiltersVO, FilterValues
|
|
7
|
+
import { Layer, type TextLayerData } from "../entities/Layer.entity"";
|
|
8
|
+
import { FiltersVO, FilterValues } from "../../domain/entities/Filters";
|
|
9
9
|
import { HistoryService, HistoryState } from "../../domain/services/HistoryService";
|
|
10
10
|
import { LayerService } from "../../domain/services/LayerService";
|
|
11
11
|
import type { Transform } from "../../domain/entities/Transform";
|
|
@@ -23,15 +23,15 @@ export function useEditorStore() {
|
|
|
23
23
|
|
|
24
24
|
// History actions
|
|
25
25
|
const pushLayers = useCallback((layers: Layer[]) => {
|
|
26
|
-
setHistory((prev) => historyService.push(prev, layers));
|
|
26
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.push(prev, layers));
|
|
27
27
|
}, []);
|
|
28
28
|
|
|
29
29
|
const undo = useCallback(() => {
|
|
30
|
-
setHistory((prev) => historyService.undo(prev));
|
|
30
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.undo(prev));
|
|
31
31
|
}, []);
|
|
32
32
|
|
|
33
33
|
const redo = useCallback(() => {
|
|
34
|
-
setHistory((prev) => historyService.redo(prev));
|
|
34
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.redo(prev));
|
|
35
35
|
}, []);
|
|
36
36
|
|
|
37
37
|
// Layer actions
|
|
@@ -59,6 +59,16 @@ export function useEditorStore() {
|
|
|
59
59
|
pushLayers(layers);
|
|
60
60
|
}, [history.present, pushLayers]);
|
|
61
61
|
|
|
62
|
+
const updateTextLayerContent = useCallback((id: string, updates: Partial<Omit<TextLayerData, "id" | "type">>) => {
|
|
63
|
+
const layers = history.present.map(layer => {
|
|
64
|
+
if (layer.id === id && layer.isText()) {
|
|
65
|
+
return layer.withStyle(updates);
|
|
66
|
+
}
|
|
67
|
+
return layer;
|
|
68
|
+
});
|
|
69
|
+
pushLayers(layers);
|
|
70
|
+
}, [history.present, pushLayers]);
|
|
71
|
+
|
|
62
72
|
const deleteLayer = useCallback((id: string) => {
|
|
63
73
|
const layers = layerService.deleteLayer(history.present, id);
|
|
64
74
|
pushLayers(layers);
|
|
@@ -101,7 +111,7 @@ export function useEditorStore() {
|
|
|
101
111
|
// Getters
|
|
102
112
|
const layers = useMemo(() => layerService.sortByZIndex(history.present), [history.present]);
|
|
103
113
|
const activeLayer = useMemo(() =>
|
|
104
|
-
history.present.find(l => l.id === activeLayerId) ?? null,
|
|
114
|
+
history.present.find((l: Layer) => l.id === activeLayerId) ?? null,
|
|
105
115
|
[history.present, activeLayerId]
|
|
106
116
|
);
|
|
107
117
|
const canUndo = useMemo(() => historyService.canUndo(history), [history]);
|
|
@@ -124,6 +134,7 @@ export function useEditorStore() {
|
|
|
124
134
|
addTextLayer,
|
|
125
135
|
addStickerLayer,
|
|
126
136
|
updateLayer,
|
|
137
|
+
updateTextLayerContent,
|
|
127
138
|
deleteLayer,
|
|
128
139
|
duplicateLayer,
|
|
129
140
|
moveLayerUp,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Layer Entity
|
|
3
|
+
* Core layer abstraction with transform and appearance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LayerType, Position, Appearance, LayerContent } from "../types";
|
|
7
|
+
import type { TextLayer } from "./TextLayer.entity";
|
|
8
|
+
import type { StickerLayer } from "./StickerLayer.entity";
|
|
9
|
+
|
|
10
|
+
export class Layer {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly type: LayerType;
|
|
13
|
+
readonly position: Position;
|
|
14
|
+
readonly rotation: number;
|
|
15
|
+
readonly scale: number;
|
|
16
|
+
readonly appearance: Appearance;
|
|
17
|
+
readonly content: LayerContent;
|
|
18
|
+
|
|
19
|
+
constructor(data: {
|
|
20
|
+
id: string;
|
|
21
|
+
type: LayerType;
|
|
22
|
+
position: Position;
|
|
23
|
+
rotation: number;
|
|
24
|
+
scale: number;
|
|
25
|
+
appearance: Appearance;
|
|
26
|
+
content: LayerContent;
|
|
27
|
+
}) {
|
|
28
|
+
this.id = data.id;
|
|
29
|
+
this.type = data.type;
|
|
30
|
+
this.position = data.position;
|
|
31
|
+
this.rotation = data.rotation;
|
|
32
|
+
this.scale = data.scale;
|
|
33
|
+
this.appearance = data.appearance;
|
|
34
|
+
this.content = data.content;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Type guards
|
|
38
|
+
isText(): this is TextLayer {
|
|
39
|
+
return this.type === "text";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isSticker(): this is StickerLayer {
|
|
43
|
+
return this.type === "sticker";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Immutable updates
|
|
47
|
+
withPosition(position: Partial<Position>): Layer {
|
|
48
|
+
return new Layer({
|
|
49
|
+
...this,
|
|
50
|
+
position: { ...this.position, ...position },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
withTransform(transform: { rotation?: number; scale?: number }): Layer {
|
|
55
|
+
return new Layer({
|
|
56
|
+
...this,
|
|
57
|
+
...transform,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
withAppearance(appearance: Partial<Appearance>): Layer {
|
|
62
|
+
return new Layer({
|
|
63
|
+
...this,
|
|
64
|
+
appearance: { ...this.appearance, ...appearance },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toJSON() {
|
|
69
|
+
return {
|
|
70
|
+
id: this.id,
|
|
71
|
+
type: this.type,
|
|
72
|
+
x: this.position.x,
|
|
73
|
+
y: this.position.y,
|
|
74
|
+
rotation: this.rotation,
|
|
75
|
+
scale: this.scale,
|
|
76
|
+
opacity: this.appearance.opacity,
|
|
77
|
+
zIndex: this.appearance.zIndex,
|
|
78
|
+
...this.content,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Re-export for convenience
|
|
84
|
+
export type { TextLayer } from "./TextLayer.entity";
|
|
85
|
+
export type { StickerLayer } from "./StickerLayer.entity";
|
|
86
|
+
|
|
@@ -41,7 +41,7 @@ export interface StickerLayerData extends BaseLayerData {
|
|
|
41
41
|
export type LayerData = TextLayerData | StickerLayerData;
|
|
42
42
|
|
|
43
43
|
export class Layer {
|
|
44
|
-
constructor(
|
|
44
|
+
constructor(protected readonly data: LayerData) {}
|
|
45
45
|
|
|
46
46
|
get id(): string { return this.data.id; }
|
|
47
47
|
get type(): LayerType { return this.data.type; }
|
|
@@ -87,7 +87,7 @@ export class Layer {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export class TextLayer extends Layer {
|
|
90
|
-
declare readonly data: TextLayerData;
|
|
90
|
+
declare protected readonly data: TextLayerData;
|
|
91
91
|
|
|
92
92
|
get text(): string { return this.data.text; }
|
|
93
93
|
get fontSize(): number { return this.data.fontSize; }
|
|
@@ -108,7 +108,7 @@ export class TextLayer extends Layer {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export class StickerLayer extends Layer {
|
|
111
|
-
declare readonly data: StickerLayerData;
|
|
111
|
+
declare protected readonly data: StickerLayerData;
|
|
112
112
|
|
|
113
113
|
get uri(): string { return this.data.uri; }
|
|
114
114
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticker Layer Entity
|
|
3
|
+
* Specialized layer for sticker/image content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Layer } from "./Layer.entity";
|
|
7
|
+
import type { StickerContent, Position, Appearance } from "../types";
|
|
8
|
+
|
|
9
|
+
export class StickerLayer extends Layer {
|
|
10
|
+
readonly content: StickerContent;
|
|
11
|
+
|
|
12
|
+
constructor(data: {
|
|
13
|
+
id: string;
|
|
14
|
+
position: Position;
|
|
15
|
+
rotation: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
appearance: Appearance;
|
|
18
|
+
content: StickerContent;
|
|
19
|
+
}) {
|
|
20
|
+
super({
|
|
21
|
+
...data,
|
|
22
|
+
type: "sticker",
|
|
23
|
+
});
|
|
24
|
+
this.content = data.content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convenience getters
|
|
28
|
+
get uri(): string { return this.content.uri; }
|
|
29
|
+
|
|
30
|
+
// Immutable updates
|
|
31
|
+
withUri(uri: string): StickerLayer {
|
|
32
|
+
return new StickerLayer({
|
|
33
|
+
...this,
|
|
34
|
+
content: { ...this.content, uri },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Layer Entity
|
|
3
|
+
* Specialized layer for text content with font and style properties
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Layer } from "./Layer.entity";
|
|
7
|
+
import type { TextContent, Position, Appearance, TextAlign } from "../types";
|
|
8
|
+
|
|
9
|
+
export class TextLayer extends Layer {
|
|
10
|
+
readonly content: TextContent;
|
|
11
|
+
|
|
12
|
+
constructor(data: {
|
|
13
|
+
id: string;
|
|
14
|
+
position: Position;
|
|
15
|
+
rotation: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
appearance: Appearance;
|
|
18
|
+
content: TextContent;
|
|
19
|
+
}) {
|
|
20
|
+
super({
|
|
21
|
+
...data,
|
|
22
|
+
type: "text",
|
|
23
|
+
});
|
|
24
|
+
this.content = data.content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convenience getters
|
|
28
|
+
get text(): string { return this.content.text; }
|
|
29
|
+
get fontSize(): number { return this.content.fontSize; }
|
|
30
|
+
get fontFamily(): string { return this.content.fontFamily; }
|
|
31
|
+
get color(): string { return this.content.color; }
|
|
32
|
+
get backgroundColor(): string { return this.content.backgroundColor; }
|
|
33
|
+
get textAlign(): TextAlign { return this.content.textAlign; }
|
|
34
|
+
get isBold(): boolean { return this.content.isBold ?? false; }
|
|
35
|
+
get isItalic(): boolean { return this.content.isItalic ?? false; }
|
|
36
|
+
|
|
37
|
+
// Immutable updates
|
|
38
|
+
withText(text: string): TextLayer {
|
|
39
|
+
return new TextLayer({
|
|
40
|
+
...this,
|
|
41
|
+
content: { ...this.content, text },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
withStyle(styles: Partial<Omit<TextContent, "text">>): TextLayer {
|
|
46
|
+
return new TextLayer({
|
|
47
|
+
...this,
|
|
48
|
+
content: { ...this.content, ...styles },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
withContent(content: Partial<TextContent>): TextLayer {
|
|
53
|
+
return new TextLayer({
|
|
54
|
+
...this,
|
|
55
|
+
content: { ...this.content, ...content },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Service
|
|
3
|
+
* Undo/redo functionality for any state type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface HistoryState<T> {
|
|
7
|
+
past: T[][];
|
|
8
|
+
present: T[];
|
|
9
|
+
future: T[][];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HistoryService<T> {
|
|
13
|
+
constructor(private readonly maxSize: number = 20) {}
|
|
14
|
+
|
|
15
|
+
createInitialState(initialValue: T[]): HistoryState<T> {
|
|
16
|
+
return {
|
|
17
|
+
past: [],
|
|
18
|
+
present: initialValue,
|
|
19
|
+
future: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
push(history: HistoryState<T>, newValue: T[]): HistoryState<T> {
|
|
24
|
+
const past = [...history.past, history.present];
|
|
25
|
+
const trimmedPast = past.slice(-this.maxSize);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
past: trimmedPast,
|
|
29
|
+
present: newValue,
|
|
30
|
+
future: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
undo(history: HistoryState<T>): HistoryState<T> {
|
|
35
|
+
if (history.past.length === 0) return history;
|
|
36
|
+
|
|
37
|
+
const previous = history.past[history.past.length - 1];
|
|
38
|
+
const newPast = history.past.slice(0, -1);
|
|
39
|
+
const newFuture = [history.present, ...history.future];
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
past: newPast,
|
|
43
|
+
present: previous,
|
|
44
|
+
future: newFuture,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
redo(history: HistoryState<T>): HistoryState<T> {
|
|
49
|
+
if (history.future.length === 0) return history;
|
|
50
|
+
|
|
51
|
+
const next = history.future[0];
|
|
52
|
+
const newPast = [...history.past, history.present];
|
|
53
|
+
const newFuture = history.future.slice(1);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
past: newPast,
|
|
57
|
+
present: next,
|
|
58
|
+
future: newFuture,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
canUndo(history: HistoryState<T>): boolean {
|
|
63
|
+
return history.past.length > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
canRedo(history: HistoryState<T>): boolean {
|
|
67
|
+
return history.future.length > 0;
|
|
68
|
+
}
|
|
69
|
+
}
|