Skip to content

Commit e630c21

Browse files
committed
Stamps are finally working like other strokes
1 parent 7c82892 commit e630c21

4 files changed

Lines changed: 420 additions & 13 deletions

File tree

frontend/src/components/Canvas.js

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ function Canvas({
115115
rotation: 0,
116116
opacity: 100,
117117
});
118+
const [backendStamps, setBackendStamps] = useState([]);
118119
const [activeFilter, setActiveFilter] = useState(null);
119120
const [filterParams, setFilterParams] = useState({});
120121
const [isFilterPreview, setIsFilterPreview] = useState(false);
@@ -434,6 +435,28 @@ function Canvas({
434435

435436
setPendingDrawings((prev) => [...prev, drawing]);
436437

438+
// If this is a custom stamp, add it to the stamp panel
439+
if (drawing.drawingType === "stamp" && drawing.stampData && drawing.stampData.image) {
440+
setBackendStamps((prevStamps) => {
441+
const imageKey = drawing.stampData.image.substring(0, 100);
442+
const alreadyExists = prevStamps.some(s =>
443+
s.image && s.image.substring(0, 100) === imageKey
444+
);
445+
446+
if (!alreadyExists) {
447+
console.log('Adding new custom stamp from Socket.IO:', drawing.stampData.name || 'Custom Stamp');
448+
return [...prevStamps, {
449+
id: `stamp-${Date.now()}-${prevStamps.length}`,
450+
name: drawing.stampData.name || 'Custom Stamp',
451+
category: drawing.stampData.category || 'custom',
452+
image: drawing.stampData.image,
453+
emoji: drawing.stampData.emoji
454+
}];
455+
}
456+
return prevStamps;
457+
});
458+
}
459+
437460
// Use requestAnimationFrame for smoother rendering
438461
requestAnimationFrame(() => {
439462
drawAllDrawings();
@@ -1454,7 +1477,6 @@ function Canvas({
14541477
}
14551478
}
14561479

1457-
// CRITICAL: Check for stamps FIRST before checking pathData as array
14581480
// Stamps have pathData as array but need special rendering
14591481
if (drawing.drawingType === "stamp" && drawing.stampData && drawing.stampSettings && Array.isArray(drawing.pathData) && drawing.pathData.length > 0) {
14601482
// Collect stamp for batch rendering (handled after loop)
@@ -1609,7 +1631,7 @@ function Canvas({
16091631
}
16101632
}
16111633
if (!selectedUser) {
1612-
// Group users by 5-minute intervals (periodStart in epoch ms).
1634+
// Group users by 5-minute intervals
16131635
// Use both committed drawings and pending drawings so the UI's
16141636
// user/time-group list reflects the strokes the user currently sees.
16151637
const groupMap = {};
@@ -1664,7 +1686,6 @@ function Canvas({
16641686
setUserList(groups);
16651687
}
16661688

1667-
// CRITICAL: Render all stamps synchronously to avoid async rendering issues
16681689
// Emoji stamps can be drawn immediately, image stamps need to be loaded first
16691690
console.log("[drawAllDrawings] Processing", stampsToRender.length, "stamps");
16701691

@@ -1743,7 +1764,7 @@ function Canvas({
17431764
}
17441765
}
17451766

1746-
// Copy offscreen canvas to visible canvas atomically (no flicker)
1767+
// Copy offscreen canvas to visible canvas atomically
17471768
console.log("[drawAllDrawings] Copying offscreen canvas to visible canvas. Total strokes rendered:", sortedDrawings.length);
17481769
context.imageSmoothingEnabled = false;
17491770
context.clearRect(0, 0, canvasWidth, canvasHeight);
@@ -2200,13 +2221,58 @@ function Canvas({
22002221
// Update pending drawings to only include those still not confirmed by backend
22012222
setPendingDrawings(stillPending);
22022223

2224+
// Extract custom stamps from all drawings and update stamp panel
2225+
extractCustomStamps();
2226+
22032227
// Use requestAnimationFrame for smoother rendering
22042228
requestAnimationFrame(() => {
22052229
drawAllDrawings();
22062230
setIsLoading(false);
22072231
});
22082232
};
22092233

2234+
// Extract custom stamps from backend drawings and update StampPanel
2235+
const extractCustomStamps = () => {
2236+
try {
2237+
const customStamps = [];
2238+
const seenStamps = new Map(); // Deduplicate by image content or emoji
2239+
2240+
(userData.drawings || []).forEach((drawing) => {
2241+
if (drawing.drawingType === "stamp" && drawing.stampData) {
2242+
const stamp = drawing.stampData;
2243+
2244+
// Skip default emoji stamps (they're already in StampPanel)
2245+
if (stamp.emoji && !stamp.image) {
2246+
return;
2247+
}
2248+
2249+
// For custom image stamps, create a unique key based on image content
2250+
if (stamp.image) {
2251+
const imageKey = stamp.image.substring(0, 100); // Use first 100 chars as key
2252+
2253+
if (!seenStamps.has(imageKey)) {
2254+
seenStamps.set(imageKey, true);
2255+
customStamps.push({
2256+
id: `stamp-${Date.now()}-${customStamps.length}`,
2257+
name: stamp.name || 'Custom Stamp',
2258+
category: stamp.category || 'custom',
2259+
image: stamp.image,
2260+
emoji: stamp.emoji
2261+
});
2262+
}
2263+
}
2264+
}
2265+
});
2266+
2267+
if (customStamps.length > 0) {
2268+
console.log('Extracted custom stamps from backend:', customStamps.length);
2269+
setBackendStamps(customStamps);
2270+
}
2271+
} catch (error) {
2272+
console.error('Error extracting custom stamps:', error);
2273+
}
2274+
};
2275+
22102276
const startDrawingHandler = (e) => {
22112277
const canvas = canvasRef.current;
22122278
const rect = canvas.getBoundingClientRect();
@@ -3278,6 +3344,7 @@ function Canvas({
32783344
selectedStamp={selectedStamp}
32793345
onStampSelect={handleStampSelect}
32803346
onStampChange={handleStampChange}
3347+
backendStamps={backendStamps}
32813348
onFilterApply={applyFilter}
32823349
onFilterPreview={previewFilter}
32833350
onFilterUndo={undoFilter}

frontend/src/components/Stamps/StampPanel.jsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const defaultStamps = [
3535
{ id: "rocket", emoji: "🚀", name: "Rocket", category: "objects" },
3636
];
3737

38-
export default function StampPanel({ onSelect, onStampChange }) {
38+
export default function StampPanel({ onSelect, onStampChange, backendStamps = [] }) {
3939
const [showEditor, setShowEditor] = useState(false);
4040
const [stamps, setStamps] = useState(defaultStamps);
4141
const [selectedStamp, setSelectedStamp] = useState(null);
@@ -46,20 +46,68 @@ export default function StampPanel({ onSelect, onStampChange }) {
4646
});
4747
const [filterCategory, setFilterCategory] = useState("all");
4848

49-
const categories = ["all", "nature", "shapes", "animals", "objects"];
49+
const categories = ["all", "nature", "shapes", "animals", "objects", "custom"];
5050

5151
useEffect(() => {
52-
// Load stamps from localStorage
52+
// Merge stamps from multiple sources:
53+
// 1. Default stamps (always present)
54+
// 2. localStorage stamps (user's local custom stamps)
55+
// 3. Backend stamps (custom stamps from all users in the room)
56+
5357
const savedStamps = localStorage.getItem('rescanvas-stamps');
58+
let localCustomStamps = [];
5459
if (savedStamps) {
5560
try {
56-
const parsed = JSON.parse(savedStamps);
57-
setStamps([...defaultStamps, ...parsed]);
61+
localCustomStamps = JSON.parse(savedStamps);
5862
} catch (e) {
5963
console.warn('Failed to load saved stamps:', e);
6064
}
6165
}
62-
}, []);
66+
67+
// Merge all stamps, avoiding duplicates
68+
// Use a Map to deduplicate by a combination of emoji/image content
69+
const stampMap = new Map();
70+
71+
// Add default stamps first
72+
defaultStamps.forEach(stamp => {
73+
const key = stamp.id;
74+
stampMap.set(key, stamp);
75+
});
76+
77+
// Add localStorage stamps
78+
localCustomStamps.forEach(stamp => {
79+
const key = stamp.id || (stamp.emoji ? `emoji-${stamp.emoji}` : `image-${stamp.image?.substring(0, 50)}`);
80+
if (!stampMap.has(key)) {
81+
stampMap.set(key, { ...stamp, id: stamp.id || key });
82+
}
83+
});
84+
85+
// Add backend stamps (highest priority for custom stamps)
86+
backendStamps.forEach(stamp => {
87+
// For backend stamps, use image content or emoji as key to avoid duplicates
88+
const key = stamp.emoji ? `emoji-${stamp.emoji}` : `image-${stamp.image?.substring(0, 50)}`;
89+
if (!stampMap.has(key)) {
90+
// Ensure backend stamp has proper structure
91+
stampMap.set(key, {
92+
id: stamp.id || key,
93+
name: stamp.name || 'Custom Stamp',
94+
category: stamp.category || 'custom',
95+
emoji: stamp.emoji,
96+
image: stamp.image
97+
});
98+
}
99+
});
100+
101+
const mergedStamps = Array.from(stampMap.values());
102+
setStamps(mergedStamps);
103+
104+
console.log('StampPanel: Merged stamps', {
105+
defaultCount: defaultStamps.length,
106+
localCount: localCustomStamps.length,
107+
backendCount: backendStamps.length,
108+
totalUnique: mergedStamps.length
109+
});
110+
}, [backendStamps]);
63111

64112
const saveStamps = (newStamps) => {
65113
const customStamps = newStamps.filter(s => !defaultStamps.find(d => d.id === s.id));

frontend/src/components/Toolbar.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const Toolbar = ({
6262
selectedStamp,
6363
onStampSelect,
6464
onStampChange,
65+
backendStamps,
6566
onFilterApply,
6667
onFilterPreview,
6768
onFilterUndo,
@@ -95,24 +96,25 @@ const Toolbar = ({
9596
PaperProps={{ sx: { p: 2, borderRadius: 2, boxShadow: 3 } }}
9697
>
9798
{tool === "brush" && (
98-
<BrushPanel
99+
<BrushPanel
99100
selectedBrush={currentBrushType}
100101
onSelect={onBrushSelect}
101102
onParamsChange={onBrushParamsChange}
102103
/>
103104
)}
104105
{tool === "mixer" && (
105-
<MixerPanel
106+
<MixerPanel
106107
onApply={onFilterApply}
107108
onPreview={onFilterPreview}
108109
onUndo={onFilterUndo}
109110
canUndo={canUndoFilter}
110111
/>
111112
)}
112113
{tool === "stamp" && (
113-
<StampPanel
114+
<StampPanel
114115
onSelect={onStampSelect}
115116
onStampChange={onStampChange}
117+
backendStamps={backendStamps}
116118
/>
117119
)}
118120
</Popover>

0 commit comments

Comments
 (0)