@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.
Files changed (57) hide show
  1. package/package.json +1 -1
  2. package/src/PhotoEditor.tsx +43 -137
  3. package/src/application/hooks/useEditor.ts +4 -6
  4. package/src/application/hooks/useEditorUI.ts +8 -5
  5. package/src/application/stores/EditorStore.ts +17 -6
  6. package/src/domain/entities/Layer.entity.ts +86 -0
  7. package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
  8. package/src/domain/entities/StickerLayer.entity.ts +37 -0
  9. package/src/domain/entities/TextLayer.entity.ts +58 -0
  10. package/src/domain/entities/index.ts +9 -0
  11. package/src/domain/services/History.service.ts +69 -0
  12. package/src/domain/services/LayerFactory.service.ts +81 -0
  13. package/src/domain/services/LayerRepository.service.ts +85 -0
  14. package/src/domain/services/LayerService.ts +1 -1
  15. package/src/domain/types.ts +39 -0
  16. package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
  17. package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
  18. package/src/domain/value-objects/Transform.vo.ts +61 -0
  19. package/src/domain/value-objects/index.ts +13 -0
  20. package/src/index.ts +4 -4
  21. package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
  22. package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
  23. package/src/presentation/components/DraggableLayer.tsx +13 -13
  24. package/src/presentation/components/EditorCanvas.tsx +5 -5
  25. package/src/presentation/components/EditorContent.tsx +72 -0
  26. package/src/presentation/components/EditorHeader.tsx +48 -0
  27. package/src/presentation/components/EditorSheets.tsx +85 -0
  28. package/src/presentation/components/FontControls.tsx +2 -2
  29. package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
  30. package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
  31. package/src/presentation/components/sheets/LayerManager.tsx +3 -4
  32. package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
  33. package/src/types.ts +8 -18
  34. package/src/utils/constants.ts +84 -0
  35. package/src/utils/formatters.ts +29 -0
  36. package/src/utils/helpers.ts +51 -0
  37. package/src/utils/index.ts +9 -0
  38. package/src/utils/validators.ts +38 -0
  39. package/ARCHITECTURE.md +0 -104
  40. package/MIGRATION.md +0 -100
  41. package/src/components/AIMagicSheet.tsx +0 -107
  42. package/src/components/AdjustmentsSheet.tsx +0 -108
  43. package/src/components/ColorPicker.tsx +0 -77
  44. package/src/components/DraggableSticker.tsx +0 -161
  45. package/src/components/DraggableText.tsx +0 -181
  46. package/src/components/EditorCanvas.tsx +0 -106
  47. package/src/components/EditorToolbar.tsx +0 -155
  48. package/src/components/FilterPicker.tsx +0 -73
  49. package/src/components/FontControls.tsx +0 -132
  50. package/src/components/LayerManager.tsx +0 -164
  51. package/src/components/Slider.tsx +0 -112
  52. package/src/components/StickerPicker.tsx +0 -47
  53. package/src/components/TextEditorSheet.tsx +0 -160
  54. package/src/core/HistoryManager.ts +0 -53
  55. package/src/hooks/usePhotoEditor.ts +0 -172
  56. package/src/hooks/usePhotoEditorUI.ts +0 -162
  57. package/src/infrastructure/history/HistoryManager.ts +0 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-photo-editor",
3
- "version": "2.0.23",
3
+ "version": "2.0.25",
4
4
  "description": "A powerful, generic photo editor for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -3,31 +3,25 @@
3
3
  * Main entry point for the photo editor
4
4
  */
5
5
 
6
- import React, { useCallback, useMemo } from "react";
7
- import { View, ScrollView, TouchableOpacity } from "react-native";
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 { EditorCanvas } from "./presentation/components/EditorCanvas";
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: any[], filters: Record<string, number>) => void;
21
+ onSave?: (uri: string, layers: Layer[], filters: FilterSettings) => void;
28
22
  onClose: () => void;
29
23
  title?: string;
30
- customTools?: React.ReactNode | ((ui: ReturnType<typeof useEditorUI>) => React.ReactNode);
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
- container: {
54
- flex: 1,
55
- backgroundColor: tokens.colors.surface,
56
- paddingTop: insets.top,
57
- },
58
- header: {
59
- flexDirection: "row" as const,
60
- alignItems: "center" as const,
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
- {/* Header */}
85
- <View style={styles.header}>
86
- <TouchableOpacity onPress={onClose} accessibilityLabel="Close editor" accessibilityRole="button">
87
- <AtomicIcon name="close" size="md" color="textPrimary" />
88
- </TouchableOpacity>
89
- <AtomicText type="headlineSmall" style={styles.headerTitle}>
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
- <FontControls
112
- fontSize={ui.fontSize}
113
- selectedFont={ui.selectedFont}
114
- fonts={fonts}
115
- onFontSizeChange={ui.setFontSize}
116
- onFontSelect={ui.setSelectedFont}
117
- />
118
- </ScrollView>
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
- {/* Bottom Sheets */}
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 "../../domain/entities/Layer";
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 "../../domain/entities/Layer";
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 "../../domain/entities/Layer";
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?.type === "text") {
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.updateLayer(editor.activeLayerId, {
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, TextLayer, StickerLayer } from "../../domain/entities/Layer";
8
- import { FiltersVO, FilterValues, DEFAULT_FILTERS } from "../../domain/entities/Filters";
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(private readonly data: LayerData) {}
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,9 @@
1
+ /**
2
+ * Domain Entities Export
3
+ * Clean exports for all domain entities
4
+ */
5
+
6
+ export { Layer } from "./Layer.entity";
7
+ export { TextLayer } from "./TextLayer.entity";
8
+ export { StickerLayer } from "./StickerLayer.entity";
9
+
@@ -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
+ }