Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve DOO-95 "Images live collab and export serialization dataurl for now" #62

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/components/lib/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export default function Canvas() {
p1: { x: clientX - width / 2, y: clientY - height / 2 },
p2: { x: clientX + width / 2, y: clientY + height / 2 },
});
setWebsocketAction(pendingImageElementId, 'addCanvasShape');
// Unselect current image and reset cursor
setPendingImageElement('');
setCursor('');
Expand Down
56 changes: 51 additions & 5 deletions client/src/components/lib/SaveOpenDropDownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
} from '@/stores/CanvasElementsStore';
import { createElement } from '@/lib/canvasElements/canvasElementUtils';
import saveAs from 'file-saver';
import { fileCache } from '@/lib/cache';
import { BinaryFileData } from '@/types';
import { commitImageToCache } from '@/lib/image';

/**
* Component that the save and load button with their functionality in the drop down menu in the canavas
Expand Down Expand Up @@ -35,6 +38,12 @@ export const SaveOpenDropDownMenu = () => {
freehandPoints,
p1,
p2,
editCanvasElement,
textStrings,
isImagePlaceds,
freehandBounds,
angles,
fileIds,
} = useCanvasElementStore([
'setCanvasElementState',
'allIds',
Expand All @@ -52,6 +61,12 @@ export const SaveOpenDropDownMenu = () => {
'freehandPoints',
'p1',
'p2',
'editCanvasElement',
'textStrings',
'isImagePlaceds',
'freehandBounds',
'angles',
'fileIds',
]);

/**
Expand All @@ -75,7 +90,10 @@ export const SaveOpenDropDownMenu = () => {
reader.readAsText(file[0]);

reader.onload = () => {
const state: CanvasElementState = JSON.parse(reader.result as string);
const { fileData, ...state } = JSON.parse(
reader.result as string,
) as CanvasElementState & { fileData: Record<string, BinaryFileData> };

const roughElements: Record<string, CanvasElement['roughElement']> = {};

//create the roughElements
Expand Down Expand Up @@ -107,15 +125,36 @@ export const SaveOpenDropDownMenu = () => {
).roughElement;
}
state.roughElements = roughElements;
// Reset all fileIds, they will get get set to show the imageswhen they resolve.
const fileIds = state.fileIds;
state.fileIds = {};
setCanvasElementState(state);

// Populate the cache and images asynchronously
Object.entries(fileIds).forEach(([elemId, fileId]) => {
const binary = fileId && fileData[fileId];
if (!binary) throw new Error('Failed to resolve saved binary images');

const imageElement = { id: elemId };
commitImageToCache(
{
...fileData[fileId],
lastRetrieved: Date.now(),
},
imageElement,
// Will set fileIds, triggering a rerender. A placeholder
// will be shown in the mean time.
editCanvasElement,
);
});
};
};

/**
* Serializes the data and saves it to local disk
*/
const handleSave = () => {
const serializedState = JSON.stringify({
const state = {
allIds,
types,
strokeColors,
Expand All @@ -131,7 +170,14 @@ export const SaveOpenDropDownMenu = () => {
freehandPoints,
p1,
p2,
});
textStrings,
isImagePlaceds,
freehandBounds,
angles,
fileIds,
fileData: fileCache.cache,
};
const serializedState = JSON.stringify(state);

const blob = new Blob([serializedState], {
type: 'text/plain;charset=utf-8',
Expand All @@ -140,7 +186,7 @@ export const SaveOpenDropDownMenu = () => {
};

return (
<React.Fragment>
<>
<input
type="file"
ref={fileInputRef}
Expand All @@ -163,6 +209,6 @@ export const SaveOpenDropDownMenu = () => {
>
<DownloadIcon /> Save
</DropdownMenu.Item>
</React.Fragment>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,10 @@ const StableDiffusionSheet = () => {
'addCanvasShape',
'editCanvasElement',
]);
const { isUsingStableDiffusion, setIsUsingStableDiffusion, zoom, appHeight } =
useAppStore([
'isUsingStableDiffusion',
'setIsUsingStableDiffusion',
'zoom',
'appHeight',
]);
const { isUsingStableDiffusion, setIsUsingStableDiffusion } = useAppStore([
'isUsingStableDiffusion',
'setIsUsingStableDiffusion',
]);
const { canvasColor, setTool } = useAppStore(['canvasColor', 'setTool']);
const {
selectedElementIds,
Expand Down Expand Up @@ -219,7 +216,6 @@ const StableDiffusionSheet = () => {
imageFile,
addCanvasShape,
editCanvasElement,
{ zoom, appHeight },
true,
);
// And let the user place the image
Expand Down
7 changes: 1 addition & 6 deletions client/src/components/lib/ToolBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ const ToolButton = ({
active: boolean;
children?: React.ReactNode;
}) => {
const { setTool, zoom, appHeight } = useAppStore([
'setTool',
'zoom',
'appHeight',
]);
const { setTool } = useAppStore(['setTool']);
const {
removeCanvasElements,
setSelectedElements,
Expand Down Expand Up @@ -119,7 +115,6 @@ const ToolButton = ({
imageFile,
addCanvasShape,
editCanvasElement,
{ zoom, appHeight },
true,
);
// And let the user place the image
Expand Down
1 change: 1 addition & 0 deletions client/src/components/lib/ToolButtonSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const ToolButton = ({
if (selectedElementId === undefined) {
// If no element is selected, then set the tool options
setToolOptions(customizabilityDict);
return;
}
const { roughElement, fillColor } = createElement(
selectedElementId,
Expand Down
10 changes: 8 additions & 2 deletions client/src/hooks/useMultiSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@ const getElementsWithinFrame = (
const { elementIds, p1, p2, angles } = appState;

// Destructure coordinates of the frame
const { x: frameX1, y: frameY1 } = frame.p1;
const { x: frameX2, y: frameY2 } = frame.p2;
let { x: frameX1, y: frameY1 } = frame.p1;
let { x: frameX2, y: frameY2 } = frame.p2;

// Adjust the coordinates so that x1 < x2 and y1 < y2
// this is needed in case we draw the frame from right to left or bottom to top and
// simplifies the logic of checking whether an element is within the frame
[frameX1, frameX2] = [frameX1, frameX2].sort((a, b) => a - b);
[frameY1, frameY2] = [frameY1, frameY2].sort((a, b) => a - b);

// Filter element IDs based on whether their positions fall within the frame
return elementIds.filter((id) => {
Expand Down
108 changes: 75 additions & 33 deletions client/src/hooks/useSocket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
import { createElement } from '@/lib/canvasElements/canvasElementUtils';
import { useEffect, useRef } from 'react';
import { useAuthStore } from '@/stores/AuthStore';
import { fileCache } from '@/lib/cache';
import { dataURLToFile } from '@/lib/bytes';
import { commitImageToCache, isSupportedImageFile } from '@/lib/image';

/**
* Defines a hook that controls all socket related activities
Expand Down Expand Up @@ -49,6 +52,7 @@ export const useSocket = () => {
p1,
p2,
textStrings,
fileIds,
} = useCanvasElementStore([
'addCanvasShape',
'addCanvasFreehand',
Expand All @@ -74,6 +78,7 @@ export const useSocket = () => {
'p1',
'p2',
'textStrings',
'fileIds',
]);

const socket = useRef<WebsocketClient>();
Expand Down Expand Up @@ -104,8 +109,31 @@ export const useSocket = () => {
angle: element.angle,
},
);
addCanvasShape(newElement);
pushCanvasHistory();

if (element.type === 'image' && element.imgDataURL) {
// Add the file to the cache, and set the image as placed
newElement.isImagePlaced = true;
const imageFile = dataURLToFile(element.imgDataURL);
if (!isSupportedImageFile(imageFile)) {
throw new Error('Unsupported image type.');
}
addCanvasShape(newElement);
commitImageToCache(
{
mimeType: imageFile.type,
id: element.id,
dataURL: element.imgDataURL,
created: Date.now(),
lastRetrieved: Date.now(),
},
newElement,
editCanvasElement,
false,
).then(pushCanvasHistory);
} else {
addCanvasShape(newElement);
pushCanvasHistory();
}
},
addCanvasFreehand: (element: CanvasElement) => {
const newElement = createElement(
Expand All @@ -132,6 +160,7 @@ export const useSocket = () => {
},
true,
);

addCanvasFreehand(newElement);
pushCanvasHistory();
},
Expand Down Expand Up @@ -202,7 +231,49 @@ export const useSocket = () => {
}
}, [roomID]);

// Send message once action gets set. Note: will be changed
const processWebsocketAction = async (id: string) => {
//Create element to send to other sockets in room
const element = createElement(
id,
p1[id].x,
p1[id].y,
p2[id].x,
p2[id].y,
types[id],
freehandPoints[id],
{
stroke: strokeColors[id],
fill: fillColors[id],
font: fontFamilies[id],
size: fontSizes[id],
bowing: bowings[id],
roughness: roughnesses[id],
strokeWidth: strokeWidths[id],
fillStyle: fillStyles[id],
strokeLineDash: strokeLineDashes[id],
opacity: opacities[id],
text: textStrings[id],
angle: angles[id],
},
true,
);

if (element.type === 'image') {
const imageFileId = fileIds[id];
const imageFile = fileCache.cache[imageFileId ?? ''];
if (imageFileId === undefined || imageFile === undefined) {
throw new Error('Image file not found for transmision');
}
const imgDataURL = imageFile.dataURL;
element.imgDataURL = imgDataURL;
}

delete element.roughElement;
socket.current?.sendMsgRoom(action, element);
setWebsocketAction('', '');
};

// Send message once action gets set.
useEffect(() => {
if (actionElementID === '') return;

Expand All @@ -218,36 +289,7 @@ export const useSocket = () => {
setWebsocketAction('', '');
return;
}

//Create element to send to other sockets in room
const element = createElement(
actionElementID,
p1[actionElementID].x,
p1[actionElementID].y,
p2[actionElementID].x,
p2[actionElementID].y,
types[actionElementID],
freehandPoints[actionElementID],
{
stroke: strokeColors[actionElementID],
fill: fillColors[actionElementID],
font: fontFamilies[actionElementID],
size: fontSizes[actionElementID],
bowing: bowings[actionElementID],
roughness: roughnesses[actionElementID],
strokeWidth: strokeWidths[actionElementID],
fillStyle: fillStyles[actionElementID],
strokeLineDash: strokeLineDashes[actionElementID],
opacity: opacities[actionElementID],
text: textStrings[actionElementID],
angle: angles[actionElementID],
},
true,
);

delete element.roughElement;
socket.current?.sendMsgRoom(action, element);
setWebsocketAction('', '');
processWebsocketAction(actionElementID);
}, [
actionElementID,
action,
Expand Down
4 changes: 4 additions & 0 deletions client/src/lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class FileCache {
return this.#cache;
}

public set cache(newCache: Record<string, BinaryFileData>) {
this.#cache = newCache;
}

/**
* Adds the provided file to the cache, corresponding
* to the specified id.
Expand Down
13 changes: 11 additions & 2 deletions client/src/lib/canvasElements/renderScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,23 @@ export const renderCanvasElements = (
}
} else if (type === 'image') {
if (isImagePlaceds[id]) {
const [width, height] = [Math.abs(x2 - x1), Math.abs(y2 - y1)];

const [width, height] = [x2 - x1, y2 - y1];
const imgFileId = fileIds[id];
const img = imgFileId
? imageCache.cache.get(imgFileId)?.image
: undefined;
if (img !== undefined && !(img instanceof Promise)) {
ctx.drawImage(img, x1, y1, width, height);
// const sX = Math.sign(width);
// const sY = Math.sign(height);
// ctx.scale(sX, sY);
// ctx.drawImage(
// img,
// sX * x1,
// sY * y1,
// Math.abs(width),
// Math.abs(height),
// );
} else {
drawImagePlaceholder(width, height, ctx);
}
Expand Down
Loading
Loading