diff options
5 files changed, 244 insertions, 0 deletions
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss new file mode 100644 index 000000000..253f48f77 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss @@ -0,0 +1,24 @@ +/* MeshTransformGrid.scss */ +.meshTransformGrid { + position: relative; + width: 100%; + height: 100%; + pointer-events: none; /* Prevents interaction with the grid itself */ + opacity: 5%; +} + +.grid-line { + position: absolute; + background-color: rgba(255, 255, 255, 0.6); /* Light grid lines */ +} + +.control-point { + position: absolute; + width: 12px; + height: 12px; + background-color: rgba(255, 255, 255, 1); /* White control points */ + border-radius: 50%; + cursor: pointer; + z-index: 10; + pointer-events: auto; /* Allows dragging of control points */ +} diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx new file mode 100644 index 000000000..ee5c597e9 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import './MeshTransformGrid.scss'; + +interface MeshTransformGridProps { + imageRef: React.RefObject<HTMLImageElement>; // Reference to the image element + gridXSize: number; // Number of X subdivisions + gridYSize: number; // Number of Y subdivisions + isInteractive: boolean; // Whether control points are interactive (can be dragged) +} + +const MeshTransformGrid: React.FC<MeshTransformGridProps> = ({ imageRef, gridXSize, gridYSize, isInteractive }) => { + const [controlPoints, setControlPoints] = useState<any[]>([]); + + // Set up control points based on image size and grid sizes + useEffect(() => { + if (imageRef.current) { + const { width, height, left, top } = imageRef.current.getBoundingClientRect(); + const newControlPoints = []; + + for (let i = 0; i <= gridYSize; i++) { + for (let j = 0; j <= gridXSize; j++) { + newControlPoints.push({ + id: `${i}-${j}`, + x: (j * width) / gridXSize + left, + y: (i * height) / gridYSize + top, + }); + } + } + + setControlPoints(newControlPoints); + } + }, [imageRef, gridXSize, gridYSize]); + + // Handle dragging of control points + const handleDrag = (e: React.MouseEvent, pointId: string) => { + if (!isInteractive) return; // Prevent dragging if grid is not interactive + + const { clientX, clientY } = e; + const updatedPoints = controlPoints.map((point) => { + if (point.id === pointId) { + return { ...point, x: clientX, y: clientY }; + } + return point; + }); + setControlPoints(updatedPoints); + }; + + // Render grid lines between control points + const renderGridLines = () => { + const lines = []; + for (let i = 0; i < controlPoints.length; i++) { + const point = controlPoints[i]; + const nextPoint = controlPoints[i + 1]; + + // Horizontal lines + if (nextPoint && i % (gridXSize + 1) !== gridXSize) { + lines.push({ + start: { x: point.x, y: point.y }, + end: { x: nextPoint.x, y: nextPoint.y }, + }); + } + + // Vertical lines + if (i + gridXSize + 1 < controlPoints.length) { + const downPoint = controlPoints[i + gridXSize + 1]; + lines.push({ + start: { x: point.x, y: point.y }, + end: { x: downPoint.x, y: downPoint.y }, + }); + } + } + return lines.map((line, index) => ( + <div + key={index} + className="grid-line" + style={{ + position: 'absolute', + left: `${line.start.x}px`, + top: `${line.start.y}px`, + width: `${Math.abs(line.end.x - line.start.x)}px`, + height: `${Math.abs(line.end.y - line.start.y)}px`, + border: '1px solid rgba(255, 255, 255, 0.6)', + }} + /> + )); + }; + + return ( + <div className="meshTransformGrid"> + {renderGridLines()} + + {controlPoints.map((point) => ( + <div + key={point.id} + className="control-point" + style={{ + left: `${point.x}px`, + top: `${point.y}px`, + transform: 'translate(-50%, -50%)', + }} + draggable={isInteractive} // Only allow dragging if interactive + onDrag={(e) => handleDrag(e, point.id)} + /> + ))} + </div> + ); +}; + +export default MeshTransformGrid; diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss new file mode 100644 index 000000000..a72b2de6a --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss @@ -0,0 +1,21 @@ +/* MeshTransformButton.scss */ +.meshTransformBtnContainer { + position: relative; + width: 100%; + height: 100%; +} + +.grid-line { + position: absolute; + background-color: rgba(255, 255, 255, 0.6); +} + +.control-point { + position: absolute; + width: 12px; + height: 12px; + background-color: rgba(255, 255, 255, 1); + border-radius: 50%; + cursor: pointer; + z-index: 10; +} diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx new file mode 100644 index 000000000..eb68410b0 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx @@ -0,0 +1,90 @@ +import './MeshTransformButton.scss'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { Button, IconButton, Type } from '@dash/components'; +import { AiOutlineInfo } from 'react-icons/ai'; +import { SettingsManager } from '../../../../util/SettingsManager'; +import MeshTransformGrid from './imageMesh'; + +interface ButtonContainerProps { + onClick: () => Promise<void>; + loading: boolean; + onReset: () => void; + btnText: string; + imageWidth: number; + imageHeight: number; + gridXSize: number; // X subdivisions + gridYSize: number; // Y subdivisions +} + +export function MeshTransformButton({ + loading, + onClick: startMeshTransform, + onReset, + btnText, + imageWidth, + imageHeight, + gridXSize, + gridYSize +}: ButtonContainerProps) { + const [showGrid, setShowGrid] = React.useState(false); + const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points + const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element + + const handleGridToggle = () => { + if (showGrid) { + setShowGrid(false); // Hide the grid + setIsGridInteractive(false); // Disable control points manipulation + } else { + setShowGrid(true); // Show the grid + setIsGridInteractive(true); // Enable control points manipulation + } + }; + + return ( + <div className="meshTransformBtnContainer"> + <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} /> + {loading ? ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} + iconPlacement="right" + onClick={() => { + if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation + }} + /> + ) : ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + onClick={() => { + if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation + }} + /> + )} + + {/* The IconButton will toggle the grid */} + <IconButton + type={Type.SEC} + color={SettingsManager.userVariantColor} + tooltip="Toggle Grid" + icon={<AiOutlineInfo size="16px" />} + onClick={handleGridToggle} // Toggle the grid when clicked + /> + + {/* Only show the grid if `showGrid` is true */} + {showGrid && ( + <MeshTransformGrid + imageRef={imageRef} + gridXSize={gridXSize} + gridYSize={gridYSize} + isInteractive={isGridInteractive} // Pass the interactive flag to control point manipulation + /> + )} + <img ref={imageRef} src="your-image-source.jpg" alt="Mesh" style={{ width: imageWidth, height: imageHeight }} /> + </div> + ); +} |