Compare commits

...

4 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
+185 -106
View File
@@ -155,12 +155,10 @@ export const DockBuilder = () => {
const dockDataArr = (json.objects || []) const dockDataArr = (json.objects || [])
.filter((object) => object.dockData && !object.snapClone) .filter((object) => object.dockData && !object.snapClone)
.map((object) => object.dockData); .map((object) => object.dockData);
if (dockDataArr.length > 0) { if (dockDataArr.length > 0) {
// Download as Excel // Download as Excel
const worksheet = XLSX.utils.json_to_sheet(dockDataArr); downloadExcel(dockDataArr);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Dock Data");
XLSX.writeFile(workbook, `paradise_dock_data_${timestamp}.xlsx`);
} }
}, [editorMemo]); }, [editorMemo]);
@@ -414,96 +412,149 @@ export const DockBuilder = () => {
}; };
function edgeDetectionAndSnap(options) { function edgeDetectionAndSnap(options) {
if (editorMemo.getZoom() !== 1) { if (this.isZoomed) {
return; return;
} }
// TODO: Edge detection and snap to object within snap range // TODO: Edge detection and snap to object within snap range
const selectedObj = options.target; if (options && options.target) {
if (!selectedObj.dockData) return; const selectedObj = options.target.setCoords();
// TODO: Detect if the selected object is in the allowed categories
const snapCategories = [
DockPanelCategories.Accessories,
DockPanelCategories.BoatLift2,
DockPanelCategories.BoatLift4,
];
if (!snapCategories.includes(selectedObj.dockData.itemName)) return;
let closestSnapPoint = null;
let minDistance = edgeSnapThreshold;
// 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) => { editorMemo.forEachObject((obj) => {
if (obj === selectedObj || !obj.dockData) return; if (obj === selectedObj) return;
// TODO: Only consider objects in the allowed categories const detectedObjBound = obj.getBoundingRect();
if (!snapCategories.includes(obj.dockData.itemName)) return; 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 // TODO: handle rotation of 0, 90, 180, 270 degrees
const rotation = obj.angle % 360;
const isRotated = rotation !== 0 && rotation !== 180;
const objCenter = obj.getCenterPoint(); // Handle TOP edge snapping
const selectedCenter = selectedObj.getCenterPoint(); if (edgeDetection(selectedObj, obj, "top")) {
let newTop = selectedObjBound.top;
// Calculate potential snap points // Handle different rotation combinations for top snapping
const snapPoints = []; if ([0, nintyDeg].includes(obj.angle)) {
if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
// Center point newTop = obj.oCoords.tl.y;
snapPoints.push(objCenter); } else {
newTop = obj.oCoords.tl.y - selectedObjBound.height;
// Edge points }
if (!isRotated || rotation === 0 || rotation === 180) { } else {
// Horizontal edges if ([oneEightyDeg, twoSeventyDeg].includes(selectedObj.angle)) {
snapPoints.push( newTop = obj.oCoords.tl.y - detectedObjBound.height;
new fabric.Point(objCenter.x, obj.getBoundingRect().top) } else {
); newTop =
snapPoints.push( obj.oCoords.tl.y -
new fabric.Point( (selectedObjBound.height + detectedObjBound.height);
objCenter.x, }
obj.getBoundingRect().top + obj.getBoundingRect().height
)
);
} }
if (!isRotated || rotation === 90 || rotation === 270) { // Apply the new position
// Vertical edges selectedObj.setPositionByOrigin(
snapPoints.push( { x: selectedObj.oCoords.tl.x, y: newTop },
new fabric.Point(obj.getBoundingRect().left, objCenter.y) "left",
"top"
); );
snapPoints.push(
new fabric.Point(
obj.getBoundingRect().left + obj.getBoundingRect().width,
objCenter.y
)
);
}
// Find closest snap point
snapPoints.forEach((point) => {
const distance = Math.sqrt(
Math.pow(selectedCenter.x - point.x, 2) +
Math.pow(selectedCenter.y - point.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
closestSnapPoint = point;
}
});
});
// Snap to closest point if found
if (closestSnapPoint) {
const selectedCenter = selectedObj.getCenterPoint();
selectedObj.set({
left: selectedObj.left + (closestSnapPoint.x - selectedCenter.x),
top: selectedObj.top + (closestSnapPoint.y - selectedCenter.y),
});
selectedObj.setCoords(); selectedObj.setCoords();
editorMemo.renderAll(); 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;
@@ -609,26 +660,45 @@ export const DockBuilder = () => {
const onUndoClick = useCallback(() => { const onUndoClick = useCallback(() => {
const undoResult = stack.undo(); const undoResult = stack.undo();
if (undoResult && undoResult.currentState) { if (undoResult.currentCount === -1) {
editorMemo.loadFromJSON(undoResult.currentState, () => { return 0;
editorMemo.renderAll(); }
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(() => {
const redoResult = stack.redo(); const redoResult = stack.redo();
if (redoResult && redoResult.currentState) { if (!redoResult.currentState) {
// currentState is an array, get the first element return 0;
const state = Array.isArray(redoResult.currentState) }
? redoResult.currentState[0]
: redoResult.currentState; localStorage.setItem("dock", redoResult.currentState);
if (state) { editorMemo.loadFromJSON(
editorMemo.loadFromJSON(state, () => { JSON.parse(redoResult.currentState),
() => {
editorMemo.renderAll(); editorMemo.renderAll();
}); },
} (o) => {}
} );
}, [editorMemo]); }, [editorMemo]);
const onPrintScreen = useCallback(() => { const onPrintScreen = useCallback(() => {
@@ -680,33 +750,42 @@ export const DockBuilder = () => {
const CopySelection = () => { const CopySelection = () => {
const activeObject = editorMemo.getActiveObject(); const activeObject = editorMemo.getActiveObject();
if (activeObject) { if (activeObject) {
activeObject.clone(function (cloned) { activeObject.clone(
// Ensure dockData is copied function (cloned) {
if (activeObject.dockData) {
cloned.dockData = { ...activeObject.dockData };
}
clipboard = cloned; clipboard = cloned;
}); },
// Ensure dockData is copied
["dockData"]
);
} }
}; };
const PasteSelection = () => { const PasteSelection = () => {
if (clipboard) { if (clipboard) {
clipboard.clone(function (clonedObj) { clipboard.clone(
// Ensure dockData is copied function (clonedObj) {
if (clipboard.dockData) {
clonedObj.dockData = { ...clipboard.dockData };
}
// Offset the pasted object so it's visible // Offset the pasted object so it's visible
clonedObj.set({ clonedObj.set({
left: (clonedObj.left || 0) + 20, left: clonedObj.left + 20,
top: (clonedObj.top || 0) + 20, top: clonedObj.top + 20,
evented: true, 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.add(clonedObj);
}
editorMemo.setActiveObject(clonedObj); editorMemo.setActiveObject(clonedObj);
editorMemo.requestRenderAll(); editorMemo.requestRenderAll();
}); },
// Ensure dockData is copied
["dockData"]
);
} }
}; };
@@ -806,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