@umituz/react-native-photo-editor 2.0.24 → 2.0.26

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 (55) 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 +42 -17
  6. package/src/domain/entities/Layer.entity.ts +138 -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 +90 -0
  14. package/src/domain/services/LayerService.ts +21 -23
  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 +6 -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 +9 -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/src/components/AIMagicSheet.tsx +0 -107
  40. package/src/components/AdjustmentsSheet.tsx +0 -108
  41. package/src/components/ColorPicker.tsx +0 -77
  42. package/src/components/DraggableSticker.tsx +0 -161
  43. package/src/components/DraggableText.tsx +0 -181
  44. package/src/components/EditorCanvas.tsx +0 -106
  45. package/src/components/EditorToolbar.tsx +0 -155
  46. package/src/components/FilterPicker.tsx +0 -73
  47. package/src/components/FontControls.tsx +0 -132
  48. package/src/components/LayerManager.tsx +0 -164
  49. package/src/components/Slider.tsx +0 -112
  50. package/src/components/StickerPicker.tsx +0 -47
  51. package/src/components/TextEditorSheet.tsx +0 -160
  52. package/src/core/HistoryManager.ts +0 -53
  53. package/src/hooks/usePhotoEditor.ts +0 -172
  54. package/src/hooks/usePhotoEditorUI.ts +0 -162
  55. 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.24",
3
+ "version": "2.0.26",
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: unknown) => 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 "../../domain/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 { TextContent as TextLayerData } from "../../domain/types";
@@ -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 "../../domain/types";
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,9 @@
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 } from "../../domain/entities/Layer.entity";
8
+ import type { TextContent as TextLayerData } from "../../domain/types";
9
+ import { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
9
10
  import { HistoryService, HistoryState } from "../../domain/services/HistoryService";
10
11
  import { LayerService } from "../../domain/services/LayerService";
11
12
  import type { Transform } from "../../domain/entities/Transform";
@@ -19,27 +20,24 @@ export function useEditorStore() {
19
20
  historyService.createInitialState([])
20
21
  );
21
22
  const [activeLayerId, setActiveLayerId] = useState<string | null>(null);
22
- const [filters, setFilters] = useState<FiltersVO>(FiltersVO.default());
23
+ const [filters, setFilters] = useState<FilterSettings>(FilterSettings.DEFAULT);
23
24
 
24
25
  // History actions
25
26
  const pushLayers = useCallback((layers: Layer[]) => {
26
- setHistory((prev) => historyService.push(prev, layers));
27
+ setHistory((prev: HistoryState<Layer[]>) => historyService.push(prev, layers));
27
28
  }, []);
28
29
 
29
30
  const undo = useCallback(() => {
30
- setHistory((prev) => historyService.undo(prev));
31
+ setHistory((prev: HistoryState<Layer[]>) => historyService.undo(prev));
31
32
  }, []);
32
33
 
33
34
  const redo = useCallback(() => {
34
- setHistory((prev) => historyService.redo(prev));
35
+ setHistory((prev: HistoryState<Layer[]>) => historyService.redo(prev));
35
36
  }, []);
36
37
 
37
38
  // Layer actions
38
- const addTextLayer = useCallback((overrides?: Partial<Omit<TextLayerData, "id" | "type">>) => {
39
- const layer = layerService.createTextLayer({
40
- ...overrides,
41
- zIndex: history.present.length,
42
- });
39
+ const addTextLayer = useCallback((overrides?: Partial<TextLayerData>) => {
40
+ const layer = layerService.createTextLayer(overrides || {});
43
41
  pushLayers([...history.present, layer]);
44
42
  setActiveLayerId(layer.id);
45
43
  return layer.id;
@@ -59,6 +57,16 @@ export function useEditorStore() {
59
57
  pushLayers(layers);
60
58
  }, [history.present, pushLayers]);
61
59
 
60
+ const updateTextLayerContent = useCallback((id: string, updates: Partial<Omit<TextLayerData, "id" | "type">>) => {
61
+ const layers = history.present.map(layer => {
62
+ if (layer.id === id && layer.isText()) {
63
+ return layer.withStyle(updates);
64
+ }
65
+ return layer;
66
+ });
67
+ pushLayers(layers);
68
+ }, [history.present, pushLayers]);
69
+
62
70
  const deleteLayer = useCallback((id: string) => {
63
71
  const layers = layerService.deleteLayer(history.present, id);
64
72
  pushLayers(layers);
@@ -89,19 +97,35 @@ export function useEditorStore() {
89
97
  }, []);
90
98
 
91
99
  // Filter actions
92
- const updateFilters = useCallback((updates: Partial<FilterValues>) => {
93
- const current = filters.toJSON();
94
- setFilters(FiltersVO.from({ ...current, ...updates }));
100
+ const updateFilters = useCallback((updates: Partial<{ brightness: number; contrast: number; saturation: number; sepia: number; grayscale: number; hueRotate?: number }>) => {
101
+ if (updates.brightness !== undefined) {
102
+ setFilters(filters.withBrightness(updates.brightness));
103
+ }
104
+ if (updates.contrast !== undefined) {
105
+ setFilters(filters.withContrast(updates.contrast));
106
+ }
107
+ if (updates.saturation !== undefined) {
108
+ setFilters(filters.withSaturation(updates.saturation));
109
+ }
110
+ if (updates.sepia !== undefined) {
111
+ setFilters(filters.withSepia(updates.sepia));
112
+ }
113
+ if (updates.grayscale !== undefined) {
114
+ setFilters(filters.withGrayscale(updates.grayscale));
115
+ }
116
+ if (updates.hueRotate !== undefined) {
117
+ setFilters(filters.withHueRotate(updates.hueRotate));
118
+ }
95
119
  }, [filters]);
96
120
 
97
121
  const resetFilters = useCallback(() => {
98
- setFilters(FiltersVO.default());
122
+ setFilters(FilterSettings.DEFAULT);
99
123
  }, []);
100
124
 
101
125
  // Getters
102
126
  const layers = useMemo(() => layerService.sortByZIndex(history.present), [history.present]);
103
127
  const activeLayer = useMemo(() =>
104
- history.present.find(l => l.id === activeLayerId) ?? null,
128
+ history.present.find((l: Layer) => l.id === activeLayerId) ?? null,
105
129
  [history.present, activeLayerId]
106
130
  );
107
131
  const canUndo = useMemo(() => historyService.canUndo(history), [history]);
@@ -112,7 +136,7 @@ export function useEditorStore() {
112
136
  layers,
113
137
  activeLayerId,
114
138
  activeLayer,
115
- filters: filters.toJSON(),
139
+ filters,
116
140
  canUndo,
117
141
  canRedo,
118
142
 
@@ -124,6 +148,7 @@ export function useEditorStore() {
124
148
  addTextLayer,
125
149
  addStickerLayer,
126
150
  updateLayer,
151
+ updateTextLayerContent,
127
152
  deleteLayer,
128
153
  duplicateLayer,
129
154
  moveLayerUp,
@@ -0,0 +1,138 @@
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
+ import { TextLayer as TextLayerClass } from "./TextLayer.entity";
10
+ import { StickerLayer as StickerLayerClass } from "./StickerLayer.entity";
11
+
12
+ export class Layer {
13
+ readonly id: string;
14
+ readonly type: LayerType;
15
+ readonly position: Position;
16
+ readonly rotation: number;
17
+ readonly scale: number;
18
+ readonly appearance: Appearance;
19
+ readonly content: LayerContent;
20
+
21
+ constructor(data: {
22
+ id: string;
23
+ type: LayerType;
24
+ position: Position;
25
+ rotation: number;
26
+ scale: number;
27
+ appearance: Appearance;
28
+ content: LayerContent;
29
+ }) {
30
+ this.id = data.id;
31
+ this.type = data.type;
32
+ this.position = data.position;
33
+ this.rotation = data.rotation;
34
+ this.scale = data.scale;
35
+ this.appearance = data.appearance;
36
+ this.content = data.content;
37
+ }
38
+
39
+ // Convenience getters for backward compatibility
40
+ get x(): number { return this.position.x; }
41
+ get y(): number { return this.position.y; }
42
+ get zIndex(): number { return this.appearance.zIndex; }
43
+ get opacity(): number { return this.appearance.opacity; }
44
+
45
+ // Type guards
46
+ isText(): this is TextLayer {
47
+ return this.type === "text";
48
+ }
49
+
50
+ isSticker(): this is StickerLayer {
51
+ return this.type === "sticker";
52
+ }
53
+
54
+ // Immutable updates
55
+ withPosition(position: Partial<Position>): Layer {
56
+ return new Layer({
57
+ ...this,
58
+ position: { ...this.position, ...position },
59
+ });
60
+ }
61
+
62
+ withTransform(transform: { rotation?: number; scale?: number }): Layer {
63
+ return new Layer({
64
+ ...this,
65
+ ...transform,
66
+ });
67
+ }
68
+
69
+ withAppearance(appearance: Partial<Appearance>): Layer {
70
+ return new Layer({
71
+ ...this,
72
+ appearance: { ...this.appearance, ...appearance },
73
+ });
74
+ }
75
+
76
+ withZIndex(zIndex: number): Layer {
77
+ return this.withAppearance({ zIndex });
78
+ }
79
+
80
+ toJSON() {
81
+ return {
82
+ id: this.id,
83
+ type: this.type,
84
+ x: this.position.x,
85
+ y: this.position.y,
86
+ rotation: this.rotation,
87
+ scale: this.scale,
88
+ opacity: this.appearance.opacity,
89
+ zIndex: this.appearance.zIndex,
90
+ ...this.content,
91
+ };
92
+ }
93
+
94
+ static from(data: ReturnType<Layer["toJSON"]>): Layer {
95
+ if (data.type === "text") {
96
+ return new TextLayerClass({
97
+ id: data.id,
98
+ position: { x: data.x, y: data.y },
99
+ rotation: data.rotation,
100
+ scale: data.scale,
101
+ appearance: { opacity: data.opacity, zIndex: data.zIndex },
102
+ content: {
103
+ text: (data as Record<string, unknown>).text as string || "",
104
+ fontSize: (data as Record<string, unknown>).fontSize as number || 32,
105
+ fontFamily: (data as Record<string, unknown>).fontFamily as string || "System",
106
+ color: (data as Record<string, unknown>).color as string || "#FFFFFF",
107
+ backgroundColor: (data as Record<string, unknown>).backgroundColor as string || "transparent",
108
+ textAlign: (data as Record<string, unknown>).textAlign as "left" | "center" | "right" || "center",
109
+ isBold: (data as Record<string, unknown>).isBold as boolean || undefined,
110
+ isItalic: (data as Record<string, unknown>).isItalic as boolean || undefined,
111
+ },
112
+ });
113
+ }
114
+ return new StickerLayerClass({
115
+ id: data.id,
116
+ position: { x: data.x, y: data.y },
117
+ rotation: data.rotation,
118
+ scale: data.scale,
119
+ appearance: { opacity: data.opacity, zIndex: data.zIndex },
120
+ content: { uri: (data as Record<string, unknown>).uri as string || "" },
121
+ });
122
+ }
123
+ }
124
+
125
+ // Type exports for backward compatibility
126
+ export type { TextLayer } from "./TextLayer.entity";
127
+ export type { StickerLayer } from "./StickerLayer.entity";
128
+ export type { TextAlign } from "../types";
129
+
130
+ // Type guard functions for backward compatibility
131
+ export function isTextLayer(layer: Layer): layer is TextLayer {
132
+ return layer.isText();
133
+ }
134
+
135
+ export function isStickerLayer(layer: Layer): layer is StickerLayer {
136
+ return layer.isSticker();
137
+ }
138
+
@@ -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
+ }