Compare commits

...

17 Commits

Author SHA1 Message Date
Ayobami 34cb733ed7 Fix: issue 11: objects not snapping. and increase snap threshold 2025-07-06 22:06:31 +01:00
Ayobami 20618725d5 Fix: add download excel while downloading image 2025-07-06 21:27:02 +01:00
Ayobami d55b1cb9b7 Fix: copy and past to add support for multiple selected objects 2025-07-06 21:16:59 +01:00
Ayobami 12546d02c4 Fix: undo and redo state management 2025-07-06 21:06:47 +01:00
Ayobami 2100daadeb Refactor add selected dock image to appear in front of the sidebar 2025-07-02 20:58:55 +01:00
Ayobami 046280e25c ISSUE 11: add edgeDetectionAndSnap functionality 2025-07-02 20:57:31 +01:00
Ayobami 8bcd4a014c ISSUE 10: add upload file functionality 2025-07-02 18:44:52 +01:00
Ayobami 5843ffdc9b Refactor copy and paste to include dockData 2025-07-02 18:25:38 +01:00
Ayobami ab2f477b7f ISSUE 9: add download file functionality 2025-07-02 18:17:26 +01:00
Ayobami 0496a48623 ISSUE 8: add downloadImage function 2025-07-02 17:57:55 +01:00
Ayobami e089dfabec ISSUE 7: add delete functionality 2025-07-02 17:48:31 +01:00
Ayobami 4904f4d66a ISSUE 6: add print functionality 2025-07-02 17:23:00 +01:00
Ayobami 9936cd0ffc ISSUE 5: add undo functionality 2025-07-02 17:17:20 +01:00
Ayobami e3e95fd05f ISSUE 4: add redo functionality 2025-07-02 16:56:13 +01:00
Ayobami 3e3ea34e5f ISSUE 3: add paste functionality 2025-07-02 16:50:40 +01:00
Ayobami 00f9c602af ISSUE 2: fix copy selection 2025-07-02 16:02:47 +01:00
Ayobami 9ada6f005f ISSUE 1: add selected doc to canvas editor 2025-07-02 15:49:49 +01:00
2 changed files with 426 additions and 126 deletions
+94 -70
View File
@@ -5,13 +5,13 @@ import { fabric } from "fabric";
import { import {
GrayMaterial, GrayMaterial,
PerforatedMaterial, PerforatedMaterial,
WoodgrainMaterial WoodgrainMaterial,
} from "Assets/images"; } from "Assets/images";
import { import {
DockPanelCategories, DockPanelCategories,
MaterialType, MaterialType,
DockPanelCategoryMap, DockPanelCategoryMap,
CylinderType CylinderType,
} from "Utils/constants"; } from "Utils/constants";
import { Chevron } from "Assets/svgs"; import { Chevron } from "Assets/svgs";
import MkdSDK from "Utils/MkdSDK"; import MkdSDK from "Utils/MkdSDK";
@@ -21,7 +21,7 @@ import {
getMaterial, getMaterial,
getRampsCategory, getRampsCategory,
getWedgesAndRampsMaterial, getWedgesAndRampsMaterial,
getWedgesCategory getWedgesCategory,
} from "Utils/utils"; } from "Utils/utils";
const sdk = new MkdSDK(); const sdk = new MkdSDK();
@@ -32,7 +32,7 @@ export const Builder = ({ editor }) => {
DockPanelCategories.RollIn DockPanelCategories.RollIn
); );
const rampsInitialState = { const rampsInitialState = {
data: [] data: [],
}; };
// const [ramps, setRamps] = useState(null); // const [ramps, setRamps] = useState(null);
const [activeLiftRange, setActiveLiftRange] = useState(null); const [activeLiftRange, setActiveLiftRange] = useState(null);
@@ -44,7 +44,7 @@ export const Builder = ({ editor }) => {
wedges: [], wedges: [],
ramps: [], ramps: [],
selectedRamps: [], selectedRamps: [],
selectedWedges: [] selectedWedges: [],
}); });
const [boatLift, setBoatLift] = useState([]); const [boatLift, setBoatLift] = useState([]);
const [liftRanges, setLiftRanges] = useState([]); const [liftRanges, setLiftRanges] = useState([]);
@@ -60,56 +60,80 @@ export const Builder = ({ editor }) => {
[activeMaterial] [activeMaterial]
); );
const onDockSelect = useCallback((dock) => { const onDockSelect = useCallback(
if (!editor) { (dock) => {
return; if (!editor) {
} return;
const editorHeight = editor.getHeight(); }
const division = editorHeight / oneFeet - 4; const editorHeight = editor.getHeight();
const division = editorHeight / oneFeet - 4;
let imageTopViewURL; let imageTopViewURL;
let materials; let materials;
let category; let category;
if (["wedges", "ramps"].includes(dock?.type)) { if (["wedges", "ramps"].includes(dock?.type)) {
imageTopViewURL = (dock?.top_view).replace("%20", "+"); imageTopViewURL = (dock?.top_view).replace("%20", "+");
materials = getWedgesAndRampsMaterial(dock?.material);
} else {
imageTopViewURL = dock?.top_view;
materials = getMaterial(dock?.materials);
}
materials = getWedgesAndRampsMaterial(dock?.material); if (["ramps"].includes(dock?.type)) {
} else { category = getRampsCategory(dock?.category);
imageTopViewURL = dock?.top_view; } else if (["wedges"].includes(dock?.type)) {
materials = getMaterial(dock?.materials); category = getWedgesCategory(dock?.category);
} } else {
category = getCategory(dock?.category);
}
if (["ramps"].includes(dock?.type)) { const dockData = {
category = getRampsCategory(dock?.category); itemName: activeDockCategory,
} else if (["wedges"].includes(dock?.type)) { image: dock?.image,
category = getWedgesCategory(dock?.category); category: category,
} else { length: dock?.length,
category = getCategory(dock?.category); materials: materials,
} top_view: dock?.top_view,
width: dock?.width,
lift_range: dock?.lift_range,
model: dock?.model,
no_of_cylinders: dock?.no_of_cylinders,
name: dock?.name,
thumbnail: dock?.thumbnail,
weight_capacity: dock?.weight_capacity,
};
const dockData = { // Add dock to editor as an image
itemName: activeDockCategory, const imageUrl = imageTopViewURL || dock?.image || dock?.thumbnail;
image: dock?.image,
category: category,
length: dock?.length,
materials: materials,
top_view: dock?.top_view,
width: dock?.width,
lift_range: dock?.lift_range,
model: dock?.model,
no_of_cylinders: dock?.no_of_cylinders,
name: dock?.name,
thumbnail: dock?.thumbnail,
weight_capacity: dock?.weight_capacity
};
// TODO: Add dock to editor if (!imageUrl) return;
// TODO: object which is the image should have the dockData, snapAngle of 45, snapThreshold of 5
// TODO: image should be scaled down by scaleFactor // Add dock to editor
// TODO: image should be positioned at the top left of the editor fabric.Image.fromURL(
// TODO: image should be added to the editor imageUrl,
// TODO: render the editor function (img) {
}, []); img.set({
left: 300, //To prevent appearing behind the sidebar
top: 0,
scaleX: scaleFactor,
scaleY: scaleFactor,
snapAngle: 45, // TODO: snapAngle
snapThreshold: 5, // TODO: snapThreshold
dockData: dockData, // TODO: attach dockData
selectable: true,
hasControls: true,
});
// Add to editor and make active
editor.add(img); // TODO: add to editor
editor.setActiveObject(img);
editor.requestRenderAll(); // TODO: render editor
},
{ crossOrigin: "anonymous" }
);
},
[editor, activeDockCategory]
);
const getItems = useCallback((table) => { const getItems = useCallback((table) => {
// console.log( category, materials ); // console.log( category, materials );
@@ -145,8 +169,8 @@ export const Builder = ({ editor }) => {
? [ ? [
...result?.model.map((item) => ({ ...result?.model.map((item) => ({
...item, ...item,
type: "boatlifts" type: "boatlifts",
})) })),
] ]
: [] : []
); );
@@ -158,8 +182,8 @@ export const Builder = ({ editor }) => {
? [ ? [
...result?.model.map((item) => ({ ...result?.model.map((item) => ({
...item, ...item,
type: "accessories" type: "accessories",
})) })),
] ]
: [] : []
); );
@@ -171,10 +195,10 @@ export const Builder = ({ editor }) => {
? [ ? [
...result?.model.map((item) => ({ ...result?.model.map((item) => ({
...item, ...item,
type: "wedges" type: "wedges",
})) })),
] ]
: [] : [],
})); }));
case Tables.Ramps: case Tables.Ramps:
// return console.log( result ) // return console.log( result )
@@ -182,7 +206,7 @@ export const Builder = ({ editor }) => {
...prev, ...prev,
ramps: result?.model ramps: result?.model
? [...result?.model.map((item) => ({ ...item, type: "ramps" }))] ? [...result?.model.map((item) => ({ ...item, type: "ramps" }))]
: [] : [],
})); }));
// return setRamps(() => [...result?.model]); // return setRamps(() => [...result?.model]);
} }
@@ -231,7 +255,7 @@ export const Builder = ({ editor }) => {
[ [
DockPanelCategories.RollIn, DockPanelCategories.RollIn,
DockPanelCategories.Floating, DockPanelCategories.Floating,
DockPanelCategories.Sectional DockPanelCategories.Sectional,
].includes(activeDockCategory) ].includes(activeDockCategory)
) { ) {
// console.log( activeDockCategory, activeMaterial ) // console.log( activeDockCategory, activeMaterial )
@@ -296,7 +320,7 @@ export const Builder = ({ editor }) => {
: "bg-gray-200" : "bg-gray-200"
}`} }`}
> >
<img className={`rounded-md`} src={GrayMaterial} alt="GreyMaterial" /> <img className={`rounded-md`} src={GrayMaterial} alt='GreyMaterial' />
</div> </div>
<div <div
@@ -311,7 +335,7 @@ export const Builder = ({ editor }) => {
<img <img
className={`rounded-md`} className={`rounded-md`}
src={PerforatedMaterial} src={PerforatedMaterial}
alt="PerforatedMaterial" alt='PerforatedMaterial'
/> />
</div> </div>
@@ -327,7 +351,7 @@ export const Builder = ({ editor }) => {
<img <img
className={`rounded-md`} className={`rounded-md`}
src={WoodgrainMaterial} src={WoodgrainMaterial}
alt="WoodgrainMaterial" alt='WoodgrainMaterial'
/> />
</div> </div>
</div> </div>
@@ -353,7 +377,7 @@ export const Builder = ({ editor }) => {
> >
{dock.length ? ( {dock.length ? (
<> <>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} /> <img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => ( {dock?.map((dockItem, index) => (
<button <button
@@ -397,7 +421,7 @@ export const Builder = ({ editor }) => {
> >
{dock.length ? ( {dock.length ? (
<> <>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} /> <img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => ( {dock?.map((dockItem, index) => (
<button <button
@@ -441,7 +465,7 @@ export const Builder = ({ editor }) => {
> >
{dock.length ? ( {dock.length ? (
<> <>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} /> <img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => ( {dock?.map((dockItem, index) => (
<button <button
@@ -484,7 +508,7 @@ export const Builder = ({ editor }) => {
<> <>
<img <img
src={wedgesAndRamps?.wedges[0].image} src={wedgesAndRamps?.wedges[0].image}
alt="" alt=''
className={`rounded-md my-2`} className={`rounded-md my-2`}
/> />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
@@ -532,7 +556,7 @@ export const Builder = ({ editor }) => {
<> <>
<img <img
src={wedgesAndRamps.ramps[0].image} src={wedgesAndRamps.ramps[0].image}
alt="" alt=''
className={`rounded-md my-2`} className={`rounded-md my-2`}
/> />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
@@ -606,7 +630,7 @@ export const Builder = ({ editor }) => {
</div> </div>
<img <img
src={boatLift[0].image} src={boatLift[0].image}
alt="" alt=''
className={`rounded-md my-2`} className={`rounded-md my-2`}
/> />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
@@ -677,7 +701,7 @@ export const Builder = ({ editor }) => {
</div> </div>
<img <img
src={boatLift[0].image} src={boatLift[0].image}
alt="" alt=''
className={`rounded-md my-2`} className={`rounded-md my-2`}
/> />
<div className={`grid grid-cols-2`}> <div className={`grid grid-cols-2`}>
@@ -735,7 +759,7 @@ export const Builder = ({ editor }) => {
<span>{ dockItem.length }'</span> */} <span>{ dockItem.length }'</span> */}
<img <img
src={dockItem.thumbnail} src={dockItem.thumbnail}
alt="" alt=''
className={`rounded-md `} className={`rounded-md `}
/> />
{/* </div> */} {/* </div> */}
+332 -56
View File
@@ -4,7 +4,7 @@ import React, {
useCallback, useCallback,
useRef, useRef,
useState, useState,
useContext useContext,
} from "react"; } from "react";
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";
import { fabric } from "fabric"; import { fabric } from "fabric";
@@ -20,7 +20,7 @@ import { clearClone, clone } from "Utils/DockBuilderUtils/clone";
import { import {
edgeDetection, edgeDetection,
handleEdgeDetection, handleEdgeDetection,
handleIntersection handleIntersection,
} from "Utils/DockBuilderUtils"; } from "Utils/DockBuilderUtils";
import { import {
CanvasModes, CanvasModes,
@@ -32,12 +32,12 @@ import {
oneFeet, oneFeet,
rotateIcon, rotateIcon,
scaleFactor, scaleFactor,
twoSeventyDeg twoSeventyDeg,
} from "Utils/constants"; } from "Utils/constants";
import { import {
reScaleXY, reScaleXY,
resolveHeight, resolveHeight,
resolveWidth resolveWidth,
} from "Utils/DockBuilderUtils/edgeDetection"; } from "Utils/DockBuilderUtils/edgeDetection";
import { DeleteIcon, RotateIcon } from "Assets/svgs"; import { DeleteIcon, RotateIcon } from "Assets/svgs";
import { capitalize } from "Utils/helper"; import { capitalize } from "Utils/helper";
@@ -90,7 +90,7 @@ export const DockBuilder = () => {
const ext = "png"; const ext = "png";
const base64 = editorMemo.toDataURL({ const base64 = editorMemo.toDataURL({
format: ext, format: ext,
enableRetinaScaling: true enableRetinaScaling: true,
}); });
setDockImage(base64); setDockImage(base64);
setShowEstimateModal(true); setShowEstimateModal(true);
@@ -103,36 +103,63 @@ export const DockBuilder = () => {
// }, [ editor ] ); // }, [ editor ] );
const toJSON = () => { const toJSON = () => {
// TODO: download the json file // Get JSON of editor content, including dockData and snapClone
// TODO: get json of editor content const json = editorMemo.toJSON(["dockData", "snapClone"]);
// TODO: Ensure dockData is included in the json const data = JSON.stringify(json, null, 2);
// TODO: save the json to the local storage as dock // Save to localStorage
// TODO: name the file as paradise_dock_<timestamp here>.dock localStorage.setItem("dock", data);
const blob = new Blob([data], { type: "application/json" });
const anchor = document.createElement("a");
const timestamp = Date.now();
anchor.href = URL.createObjectURL(blob);
anchor.download = `paradise_dock_${timestamp}.dock`; // TODO: name the file as paradise_dock_<timestamp here>.dock
anchor.click();
}; };
const uploadFile = (e) => { const uploadFile = (e) => {
// TODO: Our own upload the file we must have downloaded previously const file = e.target.files[0];
// TODO: extract the json from the file if (!file) return;
// TODO: load the json to the editor const reader = new FileReader();
// TODO: render all reader.onload = function (event) {
try {
const json = JSON.parse(event.target.result);
console.log(json);
editorMemo.loadFromJSON(json, () => {
editorMemo.renderAll();
});
} catch (err) {
alert("Invalid file format. Please select a valid .dock file.");
}
};
reader.readAsText(file);
// Reset file input to allow reupload of the same file
e.target.value = "";
}; };
const downloadImage = useCallback(() => { const downloadImage = useCallback(() => {
// // console.log( 'Download' )
const ext = "png"; const ext = "png";
// Generate base64 image of the canvas
const base64 = editorMemo.toDataURL({
format: ext,
enableRetinaScaling: true,
});
// Download the image
const anchor = document.createElement("a");
const timestamp = Date.now();
anchor.href = base64;
anchor.download = `paradise_dock_snapshot_${timestamp}.${ext}`;
anchor.click();
// TODO: download the image // Extract dockData from all objects (excluding snapClone)
// TODO: get the json of the editor content const json = editorMemo.toJSON(["dockData", "snapClone"]);
// TODO: extract the dockData from the json const dockDataArr = (json.objects || [])
// TODO: filter the dockData to ensure it does not contain snapClone .filter((object) => object.dockData && !object.snapClone)
// TODO: generate the base64 image .map((object) => object.dockData);
// TODO: download the image and name it as paradise_dock_snapshot_<timestamp here>.${ext}
// TODO: download the excel file of the dockData you extracted
// TODO: check if dockData is empty if (dockDataArr.length > 0) {
// TODO: generate the base64 image // Download as Excel
downloadExcel(dockDataArr);
// TODO: trigger download of the dockData you extracted in excel format }
}, [editorMemo]); }, [editorMemo]);
const renderBg = useCallback(() => { const renderBg = useCallback(() => {
@@ -144,14 +171,14 @@ export const DockBuilder = () => {
function (img) { function (img) {
img.set({ img.set({
scaleX: editorMemo.width / img.width, scaleX: editorMemo.width / img.width,
scaleY: editorMemo.height / img.height scaleY: editorMemo.height / img.height,
}); });
editorMemo.setBackgroundImage(img); editorMemo.setBackgroundImage(img);
editorMemo.renderAll(); editorMemo.renderAll();
// updateModifications( true, ) // updateModifications( true, )
}, },
{ {
crossOrigin: "anonymous" crossOrigin: "anonymous",
} }
); );
}, [editorMemo]); }, [editorMemo]);
@@ -195,7 +222,7 @@ export const DockBuilder = () => {
onChange: function (value) { onChange: function (value) {
editorMemo.zoomToPoint({ x: pointer.x, y: pointer.y }, value); editorMemo.zoomToPoint({ x: pointer.x, y: pointer.y }, value);
}, },
fps: 1080 fps: 1080,
}); });
opt.e.preventDefault(); opt.e.preventDefault();
opt.e.stopPropagation(); opt.e.stopPropagation();
@@ -350,12 +377,12 @@ export const DockBuilder = () => {
let line = new fabric.Line([0, newTop, editorMemo.getWidth(), newTop], { let line = new fabric.Line([0, newTop, editorMemo.getWidth(), newTop], {
stroke: "#AAAAAA", stroke: "#AAAAAA",
testLine: true testLine: true,
// strokeDashArray: [ 5 ], // strokeDashArray: [ 5 ],
}); });
let line2 = new fabric.Line([0, objTop, editorMemo.getWidth(), objTop], { let line2 = new fabric.Line([0, objTop, editorMemo.getWidth(), objTop], {
stroke: "#AAAAAA", stroke: "#AAAAAA",
testLine: true testLine: true,
// strokeDashArray: [ 5 ], // strokeDashArray: [ 5 ],
}); });
editorMemo.add(line); editorMemo.add(line);
@@ -371,12 +398,12 @@ export const DockBuilder = () => {
let line = new fabric.Line([newLeft, 0, newLeft, editorMemo.getHeight()], { let line = new fabric.Line([newLeft, 0, newLeft, editorMemo.getHeight()], {
stroke: "#AAAAAA", stroke: "#AAAAAA",
testLine: true testLine: true,
// strokeDashArray: [ 5 ], // strokeDashArray: [ 5 ],
}); });
let line2 = new fabric.Line([objLeft, 0, objLeft, editorMemo.getWidth()], { let line2 = new fabric.Line([objLeft, 0, objLeft, editorMemo.getWidth()], {
stroke: "#AAAAAA", stroke: "#AAAAAA",
testLine: true testLine: true,
// strokeDashArray: [ 5 ], // strokeDashArray: [ 5 ],
}); });
editorMemo.add(line); editorMemo.add(line);
@@ -390,9 +417,144 @@ export const DockBuilder = () => {
} }
// TODO: Edge detection and snap to object within snap range // TODO: Edge detection and snap to object within snap range
// TODO: Detect if the object is within the snap range of the selected object if (options && options.target) {
// TODO: detect if the selected object contains dockData and dockData contains itemName of DockPanelCategories.Accessories || DockPanelCategories.BoatLift2 || DockPanelCategories.BoatLift4 const selectedObj = options.target.setCoords();
// TODO: handle rotation of 0, 90, 180, 270 degrees
// TODO: Detect if the object is within the snap range of the selected object
// This is handled by the edgeDetection function which checks if objects are within snap threshold
editorMemo.forEachObject((obj) => {
if (obj === selectedObj) return;
const detectedObjBound = obj.getBoundingRect();
const selectedObjBound = selectedObj.getBoundingRect();
// Only process image objects that are not snap clones
if (!obj.snapClone && obj.type === "image") {
// TODO: handle rotation of 0, 90, 180, 270 degrees
// Handle TOP edge snapping
if (edgeDetection(selectedObj, obj, "top")) {
let newTop = selectedObjBound.top;
// Handle different rotation combinations for top snapping
if ([0, nintyDeg].includes(obj.angle)) {
if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
newTop = obj.oCoords.tl.y;
} else {
newTop = obj.oCoords.tl.y - selectedObjBound.height;
}
} else {
if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
newTop = obj.oCoords.tl.y - detectedObjBound.height;
} else {
newTop =
obj.oCoords.tl.y -
(selectedObjBound.height + detectedObjBound.height);
}
}
// Apply the new position
selectedObj.setPositionByOrigin(
{ x: selectedObj.oCoords.tl.x, y: newTop },
"left",
"top"
);
selectedObj.setCoords();
editorMemo.renderAll();
}
// Handle BOTTOM edge snapping
else if (edgeDetection(selectedObj, obj, "bottom")) {
let newTop = selectedObjBound.top;
// Handle different rotation combinations for bottom snapping
if ([0, nintyDeg].includes(obj.angle)) {
if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
newTop =
obj.oCoords.tl.y +
(detectedObjBound.height + selectedObjBound.height);
} else {
newTop = obj.oCoords.tl.y + detectedObjBound.height;
}
} else {
if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
newTop = obj.oCoords.tl.y + selectedObjBound.height;
} else {
newTop = obj.oCoords.tl.y;
}
}
// Apply the new position
selectedObj.setPositionByOrigin(
{ x: selectedObj.oCoords.tl.x, y: newTop },
"left",
"top"
);
selectedObj.setCoords();
editorMemo.renderAll();
}
// Handle LEFT edge snapping
else if (edgeDetection(selectedObj, obj, "left")) {
let newLeft = selectedObjBound.left;
// Handle different rotation combinations for left snapping
if ([0, twoSeventyDeg].includes(obj.angle)) {
if ([oneEightyDeg, nintyDeg].includes(selectedObj.angle)) {
newLeft = obj.oCoords.tl.x;
} else {
newLeft = obj.oCoords.tl.x - selectedObjBound.width;
}
} else {
if ([oneEightyDeg, nintyDeg].includes(selectedObj.angle)) {
newLeft = obj.oCoords.tl.x - detectedObjBound.width;
} else {
newLeft =
obj.oCoords.tl.x -
(selectedObjBound.width + detectedObjBound.width);
}
}
// Apply the new position
selectedObj.setPositionByOrigin(
{ x: newLeft, y: selectedObj.oCoords.tl.y },
"left",
"top"
);
selectedObj.setCoords();
editorMemo.renderAll();
}
// Handle RIGHT edge snapping
else if (edgeDetection(selectedObj, obj, "right")) {
let newLeft = selectedObjBound.left;
// Handle different rotation combinations for right snapping
if ([0, twoSeventyDeg].includes(obj.angle)) {
if ([oneEightyDeg, nintyDeg].includes(selectedObj.angle)) {
newLeft =
obj.oCoords.tl.x +
(detectedObjBound.width + selectedObjBound.width);
} else {
newLeft = obj.oCoords.tl.x + detectedObjBound.width;
}
} else {
if ([oneEightyDeg, nintyDeg].includes(selectedObj.angle)) {
newLeft = obj.oCoords.tl.x + selectedObjBound.width;
} else {
newLeft = obj.oCoords.tl.x;
}
}
// Apply the new position
selectedObj.setPositionByOrigin(
{ x: newLeft, y: selectedObj.oCoords.tl.y },
"left",
"top"
);
selectedObj.setCoords();
editorMemo.renderAll();
}
}
});
}
editorMemo.defaultCursor = "default"; editorMemo.defaultCursor = "default";
prevPointer = null; prevPointer = null;
@@ -497,30 +659,144 @@ export const DockBuilder = () => {
}, [editor, selectedItems]); }, [editor, selectedItems]);
const onUndoClick = useCallback(() => { const onUndoClick = useCallback(() => {
// TODO: Undo const undoResult = stack.undo();
if (undoResult.currentCount === -1) {
return 0;
}
if (undoResult.currentCount === 0) {
editorMemo.getObjects().forEach((obj) => {
if (obj !== editorMemo.backgroundImage) {
if (obj.type == "image") {
editorMemo.remove(obj).renderAll();
}
}
const data = editorMemo.toJSON(["dockData"]);
localStorage.setItem("dock", JSON.stringify(data));
});
} else if (undoResult.currentCount > 0) {
const json = JSON.parse(undoResult.currentState);
editorMemo.loadFromJSON(
json,
() => {
editorMemo.renderAll();
},
(obj) => {}
);
}
}, [editorMemo]); }, [editorMemo]);
const onRedoClick = useCallback(() => { const onRedoClick = useCallback(() => {
// TODO: Redo const redoResult = stack.redo();
if (!redoResult.currentState) {
return 0;
}
localStorage.setItem("dock", redoResult.currentState);
editorMemo.loadFromJSON(
JSON.parse(redoResult.currentState),
() => {
editorMemo.renderAll();
},
(o) => {}
);
}, [editorMemo]); }, [editorMemo]);
const onPrintScreen = useCallback(() => { const onPrintScreen = useCallback(() => {
// convert the canvas to a data url and download it. // Save current background
// TODO: Print screen const originalBgColor = editorMemo.backgroundColor;
// TODO: print screen without background image and background color, the background image should be white const originalBgImage = editorMemo.backgroundImage;
// TODO: after printing, the background image and background color should be restored
// Set background to white for printing
editorMemo.setBackgroundColor(
"#fff",
editorMemo.renderAll.bind(editorMemo)
);
editorMemo.setBackgroundImage(null, editorMemo.renderAll.bind(editorMemo));
// Give time for background to update
setTimeout(() => {
const dataUrl = editorMemo.toDataURL({
format: "png",
enableRetinaScaling: true,
});
const printWindow = window.open("", "_blank");
if (printWindow) {
printWindow.document.write(
'<html><head><title>Print Canvas</title></head><body style="margin:0"><img src="' +
dataUrl +
'" style="width:100vw;max-width:100%"/></body></html>'
);
printWindow.document.close();
printWindow.focus();
printWindow.onload = function () {
printWindow.print();
printWindow.close();
};
}
// Restore original background
editorMemo.setBackgroundColor(
originalBgColor,
editorMemo.renderAll.bind(editorMemo)
);
if (originalBgImage) {
editorMemo.setBackgroundImage(
originalBgImage,
editorMemo.renderAll.bind(editorMemo)
);
}
}, 200);
}, [editorMemo]); }, [editorMemo]);
const CopySelection = () => { const CopySelection = () => {
// TODO: Copy selection const activeObject = editorMemo.getActiveObject();
if (activeObject) {
activeObject.clone(
function (cloned) {
clipboard = cloned;
},
// Ensure dockData is copied
["dockData"]
);
}
}; };
const PasteSelection = () => { const PasteSelection = () => {
// TODO: Paste selection if (clipboard) {
clipboard.clone(
function (clonedObj) {
// Offset the pasted object so it's visible
clonedObj.set({
left: clonedObj.left + 20,
top: clonedObj.top + 20,
evented: true,
});
// Add support for multiple selected objects
if (clonedObj.type === "activeSelection") {
clonedObj.canvas = editorMemo;
clonedObj.forEachObject(function (obj) {
editorMemo.add(obj);
});
clonedObj.setCoords();
} else {
editorMemo.add(clonedObj);
}
editorMemo.setActiveObject(clonedObj);
editorMemo.requestRenderAll();
},
// Ensure dockData is copied
["dockData"]
);
}
}; };
const onDeleteSelection = () => { const onDeleteSelection = () => {
// TODO: Delete selection const activeObject = editorMemo.getActiveObject();
if (activeObject) {
editorMemo.remove(activeObject);
editorMemo.discardActiveObject();
editorMemo.renderAll();
updateModifications(true, { target: activeObject });
}
}; };
const addLines = useCallback(() => { const addLines = useCallback(() => {
@@ -539,7 +815,7 @@ export const DockBuilder = () => {
[0, oneFeet * i, editorMemo.getWidth(), oneFeet * i], [0, oneFeet * i, editorMemo.getWidth(), oneFeet * i],
{ {
stroke: "#AAAAAA", stroke: "#AAAAAA",
strokeDashArray: [5] strokeDashArray: [5],
} }
); );
@@ -548,7 +824,7 @@ export const DockBuilder = () => {
fill: "#AAAAAA", fill: "#AAAAAA",
fontSize: 18, fontSize: 18,
left: editorMemo.getWidth() - 40, left: editorMemo.getWidth() - 40,
top: oneFeet * i top: oneFeet * i,
}); });
multiplier++; multiplier++;
@@ -576,7 +852,7 @@ export const DockBuilder = () => {
dispatch({ dispatch({
type: "DOCK_LOADING", type: "DOCK_LOADING",
payload: false payload: false,
}); });
setInitialLoad(false); setInitialLoad(false);
}, [editorMemo, showBuildCanvasFromLocalModal, dispatch, initialLoad]); }, [editorMemo, showBuildCanvasFromLocalModal, dispatch, initialLoad]);
@@ -609,13 +885,13 @@ export const DockBuilder = () => {
fabric.devicePixelRatio = 1; fabric.devicePixelRatio = 1;
fabric.Group.prototype.hasControls = true; fabric.Group.prototype.hasControls = true;
fabric.Group.prototype.snapAngle = 45; fabric.Group.prototype.snapAngle = 45;
fabric.Group.prototype.snapThreshold = 5; fabric.Group.prototype.snapThreshold = 10;
fabric.Group.prototype.stroke = "#0f75bc"; fabric.Group.prototype.stroke = "#0f75bc";
fabric.Object.prototype.snapAngle = 45; fabric.Object.prototype.snapAngle = 45;
// Optimize object rendering // Optimize object rendering
fabric.Object.prototype.objectCaching = true; fabric.Object.prototype.objectCaching = true;
fabric.Object.prototype.snapThreshold = 5; fabric.Object.prototype.snapThreshold = 10;
fabric.Object.prototype.setControlsVisibility({ fabric.Object.prototype.setControlsVisibility({
tl: false, //top-left tl: false, //top-left
mt: false, // middle-top mt: false, // middle-top
@@ -625,7 +901,7 @@ export const DockBuilder = () => {
bl: false, // bottom-left bl: false, // bottom-left
mb: false, //middle-bottom mb: false, //middle-bottom
br: false, //bottom-right br: false, //bottom-right
mtr: false // rotate icon mtr: false, // rotate icon
}); });
// fabric.Object.prototype.controls.deleteControl = new fabric.Control( { // fabric.Object.prototype.controls.deleteControl = new fabric.Control( {
// x: -0.8, // x: -0.8,
@@ -654,7 +930,7 @@ export const DockBuilder = () => {
sizeX: 40, sizeX: 40,
sizeY: 40, sizeY: 40,
touchSizeX: 40, touchSizeX: 40,
touchSizeY: 40 touchSizeY: 40,
}); });
fabric.Object.prototype.hasControls = true; fabric.Object.prototype.hasControls = true;
fabric.Object.prototype.lockScalingX = true; fabric.Object.prototype.lockScalingX = true;
@@ -718,7 +994,7 @@ export const DockBuilder = () => {
setInitialLoad(false); setInitialLoad(false);
dispatch({ dispatch({
type: "DOCK_LOADING", type: "DOCK_LOADING",
payload: false payload: false,
}); });
} }
} }
@@ -733,7 +1009,7 @@ export const DockBuilder = () => {
editorMemo.setBackgroundImage(editorMemo.backgroundImage, (bgImage) => { editorMemo.setBackgroundImage(editorMemo.backgroundImage, (bgImage) => {
bgImage.set({ bgImage.set({
scaleX: editorMemo.width / bgImage.width, scaleX: editorMemo.width / bgImage.width,
scaleY: editorMemo.height / bgImage.height scaleY: editorMemo.height / bgImage.height,
}); });
}); });
removeLines(); removeLines();
@@ -789,8 +1065,8 @@ export const DockBuilder = () => {
<img <img
ref={imageRef} ref={imageRef}
src="" src=''
alt="" alt=''
width={174} width={174}
height={116} height={116}
className={`rounded relative ${objHovered ? "" : "hidden"}`} className={`rounded relative ${objHovered ? "" : "hidden"}`}