diff --git a/package-lock.json b/package-lock.json index fc8b6f3..f3a2101 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "autoprefixer": "^10.4.20", "fabric": "^6.1.0", "postcss-cli": "^11.0.0", + "re-resizable": "^6.9.17", "react": "^18.3.1", "react-dom": "^18.3.1", "react-query": "^3.39.3", @@ -16756,6 +16757,15 @@ "react-dom": ">=16.9.0" } }, + "node_modules/re-resizable": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.17.tgz", + "integrity": "sha512-OBqd1BwVXpEJJn/yYROG+CbeqIDBWIp6wathlpB0kzZWWZIY1gPTsgK2yJEui5hOvkCdC2mcexF2V3DZVfLq2g==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", diff --git a/package.json b/package.json index e1ea614..b7c54e3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "autoprefixer": "^10.4.20", "fabric": "^6.1.0", "postcss-cli": "^11.0.0", + "re-resizable": "^6.9.17", "react": "^18.3.1", "react-dom": "^18.3.1", "react-query": "^3.39.3", diff --git a/src/App.js b/src/App.js index 70e2331..9cb75f1 100644 --- a/src/App.js +++ b/src/App.js @@ -5,7 +5,7 @@ import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/ic import Sidebar from './sidebar/sidebar' import WidgetsContainer from './sidebar/widgetsContainer' import UploadsContainer from './sidebar/uploadsContainer' -import Canvas from './canvas/mainClass' +import Canvas from './canvas/canvas' function App() { diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js new file mode 100644 index 0000000..3e6ccd4 --- /dev/null +++ b/src/canvas/canvas.js @@ -0,0 +1,269 @@ +import React from "react" +import * as fabric from 'fabric' +import { FullscreenOutlined, ReloadOutlined } from "@ant-design/icons" +import { Button, Tooltip } from "antd" + +import Widget from "./widgets/base" +import Cursor from "./constants/cursor" + + +class Canvas extends React.Component { + + constructor(props) { + super(props) + + this.canvasRef = React.createRef() + this.canvasContainerRef = React.createRef() + + /** + * @type {Widget[]} + */ + this.widgets = [] + + this.modes = { + DEFAULT: '', + PAN: 'pan', + } + this.currentMode = this.modes.DEFAULT + + this.mousePressed = false + this.mousePos = { + x: 0, + y: 0 + } + + this.state = { + widgets: [], + zoom: 1, + isPanning: false, + currentTranslate: { x: 0, y: 0 }, + } + + this.resetTransforms = this.resetTransforms.bind(this) + + // this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this) + } + + componentDidMount() { + this.initEvents() + + // this.widgets.push(new Widget()) + + this.setState({widgets: [new Widget()]}) + + } + + componentWillUnmount() { + + } + + mouseDownEvent(event){ + + this.mousePressed = true + + this.mousePos.x = event.e.clientX + this.mousePos.y = event.e.clientY + + } + + mouseMoveEvent(event){ + + } + + mouseUpEvent(event){ + this.mousePressed = false + } + + /** + * + * @returns {import("./widgets/base").Widget[]} + */ + getWidgets(){ + + return this.state.widgets + } + + /** + * returns list of active objects on the canvas + * @returns Widget[] + */ + getActiveObjects(){ + + return this.getWidgets().filter((widget) => { + return widget.isSelected + }) + + } + + initEvents(){ + + this.canvasContainerRef.current.addEventListener("mousedown", (event) => { + this.mousePressed = true + this.mousePos = { x: event.clientX, y: event.clientY } + + if (this.state.widgets.length > 0){ + + this.currentMode = this.modes.PAN + this.setCursor(Cursor.GRAB) + + } + + }) + + this.canvasContainerRef.current.addEventListener("mouseup", () => { + this.mousePressed = false + this.currentMode = this.modes.DEFAULT + this.setCursor(Cursor.DEFAULT) + }) + + this.canvasContainerRef.current.addEventListener("mousemove", (event) => { + // console.log("event: ", event) + if (this.mousePressed && this.currentMode === this.modes.PAN) { + const deltaX = event.clientX - this.mousePos.x + const deltaY = event.clientY - this.mousePos.y + + this.setState(prevState => ({ + currentTranslate: { + x: prevState.currentTranslate.x + deltaX, + y: prevState.currentTranslate.y + deltaY, + } + }), this.applyTransform) + + this.mousePos = { x: event.clientX, y: event.clientY } + + this.setCursor(Cursor.GRAB) + } + + + + }) + + this.canvasContainerRef.current.addEventListener("selection:created", () => { + console.log("selected") + this.currentMode = this.modes.DEFAULT + }) + + this.canvasContainerRef.current.addEventListener('wheel', (event) => { + this.wheelZoom(event) + }) + + } + + applyTransform(){ + const { currentTranslate, zoom } = this.state + this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` + } + + wheelZoom(event){ + let delta = event.deltaY + let zoom = this.state.zoom * 0.999 ** delta + this.setZoom(zoom, {x: event.offsetX, y: event.offsetY}) + } + + /** + * fits the canvas size to fit the widgets bounding box + */ + fitCanvasToBoundingBox(){ + // this.canvasRef.current.style.width = this.canvasContainerRef.current.clientWidth + // this.canvasRef.current.style.height = this.canvasContainerRef.current.clientHeight + } + + setCursor(cursor){ + this.canvasContainerRef.current.style.cursor = cursor + } + + setZoom(zoom, pos={x:0, y:0}){ + + const { currentTranslate } = this.state + + // Calculate the new translation to zoom into the mouse position + const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) + const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) + + const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) + const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) + + this.setState({ + zoom: zoom, + currentTranslate: { + x: newTranslateX, + y: newTranslateY + } + }, this.applyTransform) + + } + + getCanvasObjectsBoundingBox(padding = 0) { + const objects = this.fabricCanvas.getObjects() + if (objects.length === 0) { + return { left: 0, top: 0, width: this.fabricCanvas.width, height: this.fabricCanvas.height } + } + + const boundingBox = objects.reduce((acc, obj) => { + const objBoundingBox = obj.getBoundingRect(true) + acc.left = Math.min(acc.left, objBoundingBox.left) + acc.top = Math.min(acc.top, objBoundingBox.top) + acc.right = Math.max(acc.right, objBoundingBox.left + objBoundingBox.width) + acc.bottom = Math.max(acc.bottom, objBoundingBox.top + objBoundingBox.height) + return acc + }, { + left: Infinity, + top: Infinity, + right: -Infinity, + bottom: -Infinity + }) + + // Adding padding + boundingBox.left -= padding + boundingBox.top -= padding + boundingBox.right += padding + boundingBox.bottom += padding + + return { + left: boundingBox.left, + top: boundingBox.top, + width: boundingBox.right - boundingBox.left, + height: boundingBox.bottom - boundingBox.top + } + } + + resetTransforms() { + this.setState({ + zoom: 1, + currentTranslate: { x: 0, y: 0 } + }, this.applyTransform) + } + + render() { + return ( +
+ +
+ + +
+ +
+
+
+ { + this.state.widgets.map((wid, index) => { + return {React.cloneElement(wid, {ref:wid.elementRef})} + }) + } +
+
+
+
+ ) + } +} + +export default Canvas diff --git a/src/canvas/constants/cursor.js b/src/canvas/constants/cursor.js new file mode 100644 index 0000000..b99a07a --- /dev/null +++ b/src/canvas/constants/cursor.js @@ -0,0 +1,16 @@ + +const Cursor = { + POINTER: 'pointer', + DEFAULT: 'default', + CROSSHAIR: 'crosshair', + EW_RESIZE: 'e-resize', // east resize + NS_RESIZE: 'n-resize', // east resize + NW_RESIZE: 'nw-resize', // east resize + SE_RESIZE: 'se-resize', + SW_RESIZE: 'sw-resize', + GRAB: 'grab', + GRABBING: 'grabbing', +} + + +export default Cursor \ No newline at end of file diff --git a/src/canvas/constants/layouts.js b/src/canvas/constants/layouts.js new file mode 100644 index 0000000..82db7bd --- /dev/null +++ b/src/canvas/constants/layouts.js @@ -0,0 +1,7 @@ +const Layouts = { + PACK: "flex", + GRID: "grid", + PLACE: "absolute" +} + +export default Layouts \ No newline at end of file diff --git a/src/canvas/constants/tools.js b/src/canvas/constants/tools.js new file mode 100644 index 0000000..486300a --- /dev/null +++ b/src/canvas/constants/tools.js @@ -0,0 +1,8 @@ + +const Tools = { + COLOR_PICKER: "color_picker", + EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown +} + + +export default Tools \ No newline at end of file diff --git a/src/canvas/fabricCanvas.js b/src/canvas/fabricCanvas.js deleted file mode 100644 index b743ee1..0000000 --- a/src/canvas/fabricCanvas.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' -import * as fabric from 'fabric' - -/** - * NOTE: Not in use - */ -function FabricJSCanvas({ canvasOptions, className = '', onCanvasContextUpdate }) { - - const canvasRef = useRef(null) - - const fabricCanvasRef = useRef(null) - - useEffect(() => { - - const options = {} - let canvas = null - if (canvasRef.current) { - canvas = new fabric.Canvas(canvasRef.current, options) - const parent = canvasRef.current.parentNode.parentNode - canvas.setDimensions({ width: parent.clientWidth, height: parent.clientHeight }) - canvas.calcOffset() - - fabricCanvasRef.current = canvas - canvasRef.current.parentNode.style.width = "100%" - canvasRef.current.parentNode.style.height = "100%" - - console.log("Parent: ", canvasRef.current.parentNode.parentNode) - - canvasRef.current.parentNode.parentNode.addEventListener("resize", updateCanvasDimensions) - window.addEventListener("resize", updateCanvasDimensions) - - // make the fabric.Canvas instance available to your app - if (onCanvasContextUpdate) - onCanvasContextUpdate(canvas) - } - - return () => { - window.removeEventListener("resize", updateCanvasDimensions) - canvasRef.current.parentNode.parentNode.removeEventListener("resize", updateCanvasDimensions) - - if (onCanvasContextUpdate) - onCanvasContextUpdate(null) - - canvas.dispose() - } - }, [canvasRef]) - - - const updateCanvasDimensions = useCallback(() => { - if (!canvasRef.current || !fabricCanvasRef.current) - return - // console.log("Updating canvas") - const parent = canvasRef.current.parentNode.parentNode - - fabricCanvasRef.current.setDimensions({ width: parent.clientWidth, height: parent.clientHeight }) - fabricCanvasRef.current.calcOffset() - - fabricCanvasRef.current.renderAll() - - }, [fabricCanvasRef, canvasRef]) - - - - return -} - - -export default FabricJSCanvas \ No newline at end of file diff --git a/src/canvas/main.js b/src/canvas/main.js index 56f1abe..f42ee45 100644 --- a/src/canvas/main.js +++ b/src/canvas/main.js @@ -1,7 +1,12 @@ import { useCallback, useRef, useEffect, useMemo } from "react" import * as fabric from 'fabric' - +/** + * + * This is a functional based component of the canvas which was discarded because it makes code + * ugly and hard to interface with fabric.js, Fabric 6 has breaking changes such as dispose, is async + * which interferes wih useEffect + */ function Canvas(){ diff --git a/src/canvas/mainClass.js b/src/canvas/mainClass.js index cc68e2f..814fe96 100644 --- a/src/canvas/mainClass.js +++ b/src/canvas/mainClass.js @@ -4,6 +4,10 @@ import { FullscreenOutlined } from "@ant-design/icons" import { Button, Tooltip } from "antd" +/** + * @deprecated - Fabric.js cannot be used as its limited to drawing, elements cannot be added to canvas + */ + class Canvas extends React.Component { constructor(props) { diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js new file mode 100644 index 0000000..e1d9645 --- /dev/null +++ b/src/canvas/widgets/base.js @@ -0,0 +1,178 @@ +import React from "react" +import { NotImplementedError } from "../../utils/errors" + +import Tools from "../constants/tools" +import Layouts from "../constants/layouts" +import Cursor from "../constants/cursor" + + +function UID(){ + return Date.now().toString(36) + Math.random().toString(36).substr(2) +} + +/** + * Base class to be extended + */ +class Widget extends React.Component{ + + constructor(props, _type="widget"){ + + super(props) + + // const _type="widget" + this.type = _type + // this id has to be unique inside the canvas, it will be set automatically and should never be changed + this.__id = `${_type}_${UID()}` + this._zIndex = 0 + + this._selected = false + this._disableResize = false + this._disableSelection = false + + this.cursor = Cursor.POINTER + + this.icon = "" // antd icon name representing this widget + + this.elementRef = React.createRef() + + this.props = { + styling: { + backgroundColor: { + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "" + }, + foregroundColor: { + tool: Tools.COLOR_PICKER, + value: "" + }, + }, + layout: "show", // enables layout use "hide" to hide layout dropdown, takes the layout from this.layout + events: { + event1: { + tool: Tools.EVENT_HANDLER, + value: "" + } + } + } + + this.functions = { + "load": {"args1": "number", "args2": "string"} + } + + + this.layout = Layouts.PACK + this.boundingRect = { + x: 0, + y: 0, + height: 100, + width: 100 + } + + } + + + setComponentAdded(added=true){ + + + // this.elementRef = document.querySelector(`[data-id="${this.__id}"]`) + + } + + componentDidMount(){ + this.elementRef?.addEventListener("click", this.mousePress) + } + + componentWillUnmount(){ + this.elementRef?.removeEventListener("click", this.mousePress) + } + + mousePress(event){ + + if (!this._disableSelection){ + this._selected = true + + const widgetSelected = new CustomEvent("selection:created", { + detail: { + event, + id: this.__id, + element: this + }, + }) + console.log("dispatched") + document.dispatchEvent(widgetSelected) + } + } + + select(){ + this._selected = true + } + + deSelect(){ + this._selected = false + } + + getProps(){ + return this.props + } + + getWidgetFunctions(){ + return this.functions + } + + getId(){ + return this.__id + } + + renderContent(){ + // throw new NotImplementedError("render method has to be implemented") + return ( +
+ +
+ ) + } + + /** + * This is an internal methods don't override + * @returns {HTMLElement} + */ + render(){ + + let style = { + cursor: this.cursor, + top: "40px", + left: "40px", + width: this.boundingRect.width, + height: this.boundingRect.height + } + + let selectionStyle = { + x: "-5px", + y: "-5px", + width: this.boundingRect.width + 5, + height: this.boundingRect.height + 5 + } + + return ( + +
+ + {this.render()} +
+ +
+ +
+ +
+
+ ) + + } + +} + + +export default Widget \ No newline at end of file diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..08e625a --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,8 @@ + +export class NotImplementedError extends Error { + constructor(message) { + super(message) + this.message = message + } +} +