Skip to content
Snippets Groups Projects
Commit 11c1d7f1 authored by Samuel Jugnet's avatar Samuel Jugnet
Browse files

Merge commit '181ffcca'

Conflicts:
	src/annotationForm/AnnotationDrawing.js
	src/annotationForm/AnnotationFormOverlay/KonvaDrawing/shapes/ParentComponent.js
	src/annotationForm/KonvaDrawing/shapes/ParentComponent.js
parents 86038559 181ffcca
Branches
No related tags found
No related merge requests found
Showing
with 632 additions and 534 deletions
......@@ -8,7 +8,7 @@ import { v4 as uuid } from 'uuid';
import { exportStageSVG } from 'react-konva-to-svg';
import CompanionWindow from 'mirador/dist/es/src/containers/CompanionWindow';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import { VideosReferences } from "mirador/dist/es/src/plugins/VideosReferences";
import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
import Tab from '@mui/material/Tab';
import HighlightAltIcon from '@mui/icons-material/HighlightAlt';
import LayersIcon from '@mui/icons-material/Layers';
......@@ -20,30 +20,24 @@ import WebAnnotation from './WebAnnotation';
import { secondsToHMS } from './utils';
import AnnotationFormContent from './annotationForm/AnnotationFormContent';
import AnnotationFormTime from './annotationForm/AnnotationFormTime';
import AnnotationFormDrawing from './annotationForm/AnnotationFormDrawing';
import { geomFromAnnoTarget, timeFromAnnoTarget } from './AnnotationCreationUtils';
import AnnotationFormOverlay from './annotationForm/AnnotationFormOverlay/AnnotationFormOverlay.js';
const TARGET_VIEW = 'target';
const OVERLAY_VIEW = 'layer';
const TAG_VIEW = 'tag';
const MANIFEST_LINK_VIEW = 'link';
/** Component for creating annotations.
* Display in companion window when a manifest is open and an annoation created or edited */
function AnnotationCreation(props) {
const [toolState, setToolState] = useState({
activeTool: 'cursor',
activeTool: 'edit',
closedMode: 'closed',
colorPopoverOpen: false,
currentColorType: false,
fillColor: 'rgba(255, 0, 0, 0.5)',
image: { id: null },
imageEvent: null,
lineWeightPopoverOpen: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
strokeColor: 'green',
strokeColor: 'rgba(255, 0, 0, 0.5)',
strokeWidth: 3,
});
......@@ -147,8 +141,6 @@ function AnnotationCreation(props) {
};
}, []);
useEffect(() => {
}, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]);
......@@ -248,7 +240,6 @@ function AnnotationCreation(props) {
/** */
const setShapeProperties = (options) => new Promise(() => {
if (options.fill) {
state.fillColor = options.fill;
}
......@@ -282,30 +273,26 @@ function AnnotationCreation(props) {
return svg;
};
/** Set color tool from current shape */
const setColorToolFromCurrentShape = (colorState) => {
setToolState((prevState) => ({
...prevState,
...colorState,
}));
}
};
/** update shapes with shapes from annotationDrawing */
const updateShapes = (newShapes) => {
setShapes(newShapes);
}
};
/** delete shape */
const deleteShape = (shapeId) => {
const newShapes = shapes.filter((shape) => shape.id !== shapeId);
setShapes(newShapes);
}
};
/**
* Validate form and save annotation
......@@ -527,7 +514,7 @@ function AnnotationCreation(props) {
<StyledTabPanel
value={OVERLAY_VIEW}
>
<AnnotationFormDrawing
<AnnotationFormOverlay
toolState={toolState}
updateToolState={setToolState}
handleImgChange={handleImgChange}
......
......@@ -18,3 +18,26 @@ export function geomFromAnnoTarget(annotarget) {
}
return r[1];
}
export const OVERLAY_TOOL = {
CURSOR: 'cursor',
DELETE: 'delete',
EDIT: 'edit',
IMAGE: 'image',
SHAPE: 'shapes',
TEXT: 'text',
};
export const SHAPES_TOOL = {
ARROW: 'arrow',
ELLIPSE: 'ellipse',
FREEHAND: 'freehand',
POLYGON: 'polygon',
RECTANGLE: 'rectangle',
SHAPES: 'shapes',
};
export function isShapesTool(activeTool) {
// Find if active tool in the list of overlay tools. I want a boolean in return
return Object.values(SHAPES_TOOL).find((tool) => tool === activeTool) ;
}
......@@ -11,11 +11,8 @@ import { v4 as uuidv4 } from 'uuid';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
// eslint-disable-next-line import/no-extraneous-dependencies
import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
import ParentComponent from './KonvaDrawing/shapes/ParentComponent';
import Surface from './KonvaDrawing/Surface';
import { act } from '@psychobolt/react-paperjs/dist/index.dev';
import { set } from 'lodash';
import ParentComponent from './AnnotationFormOverlay/KonvaDrawing/shapes/ParentComponent';
import { SHAPES_TOOL } from '../AnnotationCreationUtils';
/** All the stuff to draw on the canvas */
function AnnotationDrawing(props) {
const [shapes, setShapes] = useState([]);
......@@ -76,18 +73,6 @@ function AnnotationDrawing(props) {
const { fillColor, strokeColor, strokeWidth } = props;
/** Debug function facility */
const debug = (command) => {
console.debug('***************************');
console.debug(command);
console.debug('shapes', shapes);
console.debug('shapes taille', shapes.length);
console.debug('currentShape', currentShape);
console.debug('isDrawing', isDrawing);
console.debug('props.activeTool', props.activeTool);
console.debug('-----------------------------');
};
/** */
useEffect(() => {
if (!isDrawing) {
......@@ -123,7 +108,8 @@ function AnnotationDrawing(props) {
fillColor: currentShape.fill,
strokeColor: currentShape.stroke,
strokeWidth: currentShape.strokeWidth,
})
},
);
return () => {
window.removeEventListener('keydown', handleKeyPress);
......@@ -131,25 +117,18 @@ function AnnotationDrawing(props) {
}
}, [currentShape]);
useEffect(() => {
// compare shapes and props.shapes p, if different, update shapes
if (props.shapes.length !== shapes.length) { /// nul a revoir
setShapes(props.shapes);
}
}, [props.shapes]);
/** */
const onShapeClick = async (shp) => {
const shape = shapes.find((s) => s.id === shp.id);
if (props.activeTool === 'delete') {
const newShapes = shapes.filter((s) => s.id !== shape.id);
setShapes(newShapes);
return;
......@@ -162,25 +141,22 @@ function AnnotationDrawing(props) {
fillColor: shape.fill,
strokeColor: shape.stroke,
strokeWidth: shape.strokeWidth,
})
},
);
};
const onTransform = (evt) => {
const modifiedshape = evt.target.attrs;
const shape = shapes.find((s) => s.id === modifiedshape.id);
Object.assign(shape, modifiedshape);
setCurrentShape({ ...shape });
updateCurrentShapeInShapes();
};
const handleDragEnd = (evt) => {
const modifiedshape = evt.currentTarget.attrs;
const shape = shapes.find((s) => s.id === modifiedshape.id);
shape.x = modifiedshape.x;
......@@ -244,7 +220,6 @@ function AnnotationDrawing(props) {
}
};
/** */
const updateCurrentShapeInShapes = () => {
const index = shapes.findIndex((s) => s.id === currentShape.id);
......@@ -265,7 +240,7 @@ function AnnotationDrawing(props) {
pos.y /= props.scale;
let shape = null;
switch (props.activeTool) {
case 'rectangle':
case SHAPES_TOOL.RECTANGLE:
shape = {
fill: props.fillColor,
height: 1,
......@@ -283,18 +258,19 @@ function AnnotationDrawing(props) {
setIsDrawing(true);
setShapes([...shapes, shape]);
setCurrentShape(shape);
case 'ellipse':
break;
case SHAPES_TOOL.ELLIPSE:
shape = {
fill: props.fillColor,
height: 1,
id: uuidv4(),
radiusX: 1,
radiusY: 1,
rotation: 0,
scaleX: 1,
scaleY: 1,
stroke: props.strokeColor,
strokeWidth: props.strokeWidth,
radiusX: 1,
radiusY: 1,
type: props.activeTool,
width: 1,
x: pos.x,
......@@ -305,7 +281,7 @@ function AnnotationDrawing(props) {
setCurrentShape(shape);
break;
case 'text':
case SHAPES_TOOL.TEXT:
shape = {
fill: props.fillColor,
fontSize: 50,
......@@ -314,7 +290,7 @@ function AnnotationDrawing(props) {
scaleX: 1,
scaleY: 1,
text: 'text',
type: 'text',
type: SHAPES_TOOL.TEXT,
x: pos.x,
y: pos.y,
};
......@@ -322,7 +298,7 @@ function AnnotationDrawing(props) {
setShapes([...shapes, shape]);
setCurrentShape(shape);
break;
case 'freehand':
case SHAPES_TOOL.FREEHAND:
// Not totally functionnal
setIsDrawing(true);
shape = {
......@@ -342,14 +318,14 @@ function AnnotationDrawing(props) {
scaleY: 1,
stroke: props.strokeColor,
strokeWidth: props.strokeWidth,
type: 'freehand',
type: SHAPES_TOOL.FREEHAND,
x: 0,
y: 0,
};
setShapes([...shapes, shape]);
setCurrentShape(shape);
break;
case 'polygon':
case SHAPES_TOOL.POLYGON:
setIsDrawing(true);
shape = {
fill: props.fillColor,
......@@ -360,14 +336,14 @@ function AnnotationDrawing(props) {
scaleY: 1,
stroke: props.strokeColor,
strokeWidth: props.strokeWidth,
type: 'polygon',
type: SHAPES_TOOL.POLYGON,
x: 0,
y: 0,
};
setShapes([...shapes, shape]);
setCurrentShape(shape);
break;
case 'arrow':
case SHAPES_TOOL.ARROW:
setIsDrawing(true);
shape = {
fill: props.fillColor,
......@@ -379,12 +355,10 @@ function AnnotationDrawing(props) {
scaleX: 1,
scaleY: 1,
stroke: props.strokeColor,
type: 'arrow',
type: SHAPES_TOOL.ARROW,
};
setShapes([...shapes, shape]);
setCurrentShape(shape);
case 'debug':
debug('debug');
break;
default:
// Handle other cases if any
......@@ -409,7 +383,7 @@ function AnnotationDrawing(props) {
pos.y /= props.scale;
switch (props.activeTool) {
case 'rectangle':
case SHAPES_TOOL.RECTANGLE:
setCurrentShape({
...currentShape,
......@@ -417,7 +391,8 @@ function AnnotationDrawing(props) {
width: pos.x - currentShape.x,
});
updateCurrentShapeInShapes();
case 'ellipse':
break;
case SHAPES_TOOL.ELLIPSE:
// prevent negative radius for ellipse
if (pos.x < currentShape.x) {
......@@ -427,7 +402,6 @@ function AnnotationDrawing(props) {
pos.y = currentShape.y;
}
setCurrentShape({
...currentShape,
height: pos.y - currentShape.y,
......@@ -438,7 +412,7 @@ function AnnotationDrawing(props) {
updateCurrentShapeInShapes();
break;
case 'freehand':
case SHAPES_TOOL.FREEHAND:
const shape = { ...currentShape };
shape.lines.push({
points: [pos.x, pos.y, pos.x, pos.y],
......@@ -448,14 +422,14 @@ function AnnotationDrawing(props) {
setCurrentShape(shape);
updateCurrentShapeInShapes();
break;
case 'polygon':
case SHAPES_TOOL.POLYGON:
const polygonShape = { ...currentShape };
polygonShape.points[2] = pos.x;
polygonShape.points[3] = pos.y;
setCurrentShape(polygonShape);
updateCurrentShapeInShapes();
break;
case 'arrow':
case SHAPES_TOOL.ARROW:
// TODO improve
const arrowShape = {};
// update points
......
import {
Button, ClickAwayListener, Divider, Grid, MenuItem, MenuList, Paper, Popover,
} from '@mui/material';
import Typography from '@mui/material/Typography';
import ToggleButton from '@mui/material/ToggleButton';
import TitleIcon from '@mui/icons-material/Title';
import ImageIcon from '@mui/icons-material/Image';
import DeleteIcon from '@mui/icons-material/Delete';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import RectangleIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CircleIcon from '@mui/icons-material/RadioButtonUnchecked';
import PolygonIcon from '@mui/icons-material/Timeline';
import GestureIcon from '@mui/icons-material/Gesture';
import React, { useEffect } from 'react';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import StrokeColorIcon from '@mui/icons-material/BorderColor';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import LineWeightIcon from '@mui/icons-material/LineWeight';
import FormatColorFillIcon from '@mui/icons-material/FormatColorFill';
import ClosedPolygonIcon from '@mui/icons-material/ChangeHistory';
import OpenPolygonIcon from '@mui/icons-material/ShowChart';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { SketchPicker } from 'react-color';
import { v4 as uuidv4 } from 'uuid';
import CategoryIcon from '@mui/icons-material/Category';
import CursorIcon from '../icons/Cursor';
import ImageFormField from './ImageFormField';
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
'&:first-of-type': {
borderRadius: theme.shape.borderRadius,
},
'&:not(:first-of-type)': {
borderRadius: theme.shape.borderRadius,
},
border: 'none',
margin: theme.spacing(0.5),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(1, 0.5),
}));
const StyledLi = styled('li')(({ theme }) => ({
display:'flex',
wordBreak: 'break-word',
}));
const StyledUl = styled('ul')(({ theme }) => ({
display:'flex',
flexDirection: 'column',
gap: '5px',
listStyle: 'none',
paddingLeft: '0',
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: '5px',
}));
const StyledDivButtonImage = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
marginTop: '5px',
}));
const rgbaToObj = (rgba = 'rgba(255,255,255,0.5)') => {
const rgbaArray = rgba.split(',');
const r = Number(rgbaArray[0].split('(')[1]);
const g = Number(rgbaArray[1]);
const b = Number(rgbaArray[2]);
const a = Number(rgbaArray[3].split(')')[0]);
return {
// eslint-disable-next-line sort-keys
r, g, b, a,
};
};
const objToRgba = (obj = {
// eslint-disable-next-line sort-keys
r: 255, g: 255, b: 255, a: 0.5,
}) => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`;
/** Check if we are using an overlay tool or selecting the overlay view */
function isShapeTool(activeTool) {
switch (activeTool) {
case 'rectangle':
case 'ellipse':
case 'arrow':
case 'polygon':
case 'freehand':
case 'shapes':
return true;
break;
default:
return false;
}
}
/** All the stuff to manage to choose the drawing tool */
function AnnotationFormDrawing({ updateToolState, toolState, handleImgChange, shapes,deleteShape }) {
useEffect(() => {
}, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]);
/** */
const openChooseLineWeight = (e) => {
updateToolState({
...toolState,
lineWeightPopoverOpen: true,
popoverLineWeightAnchorEl: e.currentTarget,
});
};
/** Close color popover window */
const closeChooseColor = (e) => {
updateToolState({
...toolState,
colorPopoverOpen: false,
currentColorType: null,
popoverAnchorEl: null,
});
};
/** Update color : fillColor or strokeColor */
const updateStrokeColor = (color) => {
updateToolState({
...toolState,
[toolState.currentColorType]: objToRgba(color.rgb),
});
};
/** */
const openChooseColor = (e) => {
updateToolState({
...toolState,
colorPopoverOpen: true,
currentColorType: e.currentTarget.value,
popoverAnchorEl: e.currentTarget,
});
};
/** */
const handleCloseLineWeight = (e) => {
updateToolState({
...toolState,
lineWeightPopoverOpen: false,
popoverLineWeightAnchorEl: null,
});
};
/** */
const handleLineWeightSelect = (e) => {
updateToolState({
...toolState,
lineWeightPopoverOpen: false,
popoverLineWeightAnchorEl: null,
strokeWidth: e.currentTarget.value,
});
};
const changeTool = (e, tool) => {
updateToolState({
...toolState,
activeTool: tool,
});
};
const changeClosedMode = (e) => {
updateToolState({
...toolState,
closedMode: e.currentTarget.value,
});
};
/**
*
*/
const addImage = () => {
const data = {
id: image?.id,
uuid: uuidv4(),
};
updateToolState({
...toolState,
image: { id: null },
imageEvent: data,
});
};
const {
activeTool,
closedMode,
image,
lineWeightPopoverOpen,
popoverLineWeightAnchorEl,
fillColor,
strokeColor,
strokeWidth,
colorPopoverOpen,
popoverAnchorEl,
currentColorType,
} = toolState;
return (
<StyledPaper>
<div>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Overlay
</Typography>
</Grid>
<Grid item xs={12}>
<StyledToggleButtonGroup
value={activeTool} // State or props ?
exclusive
onChange={changeTool}
aria-label="tool selection"
size="small"
>
<ToggleButton value="edit" aria-label="select cursor">
<CursorIcon />
</ToggleButton>
<ToggleButton value="shapes" aria-label="select cursor">
<CategoryIcon />
</ToggleButton>
<ToggleButton value="images" aria-label="select cursor">
<ImageIcon />
</ToggleButton>
<ToggleButton value="text" aria-label="select text">
<TitleIcon />
</ToggleButton>
<ToggleButton value="delete" aria-label="select cursor">
<DeleteIcon />
</ToggleButton>
</StyledToggleButtonGroup>
{
activeTool === 'edit' && (
<StyledUl>
{shapes && shapes.map((shape) => (
<StyledLi key={shape.id}>
{shape.id}
<Button onClick={() => deleteShape(shape.id)}>
<DeleteIcon />
</Button>
</StyledLi>
))}
</StyledUl>
)
}
{
isShapeTool(activeTool) && (
<>
<StyledToggleButtonGroup
value={activeTool} // State or props ?
exclusive
onChange={changeTool}
aria-label="tool selection"
size="small"
>
<ToggleButton value="rectangle" aria-label="add a rectangle">
<RectangleIcon />
</ToggleButton>
<ToggleButton value="ellipse" aria-label="add a circle">
<CircleIcon />
</ToggleButton>
<ToggleButton value="arrow" aria-label="add an arrow">
<ArrowOutwardIcon />
</ToggleButton>
<ToggleButton value="polygon" aria-label="add a polygon">
<PolygonIcon />
</ToggleButton>
<ToggleButton value="freehand" aria-label="free hand polygon">
<GestureIcon />
</ToggleButton>
</StyledToggleButtonGroup>
<div>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Style
</Typography>
</Grid>
<Grid item xs={12}>
<ToggleButtonGroup
aria-label="style selection"
size="small"
>
<ToggleButton
value="strokeColor"
aria-label="select color"
onClick={openChooseColor}
>
<StrokeColorIcon style={{ fill: strokeColor }} />
<ArrowDropDownIcon />
</ToggleButton>
<ToggleButton
value="strokeColor"
aria-label="select line weight"
onClick={openChooseLineWeight}
>
<LineWeightIcon />
<ArrowDropDownIcon />
</ToggleButton>
<ToggleButton
value="fillColor"
aria-label="select color"
onClick={openChooseColor}
>
<FormatColorFillIcon style={{ fill: fillColor }} />
<ArrowDropDownIcon />
</ToggleButton>
</ToggleButtonGroup>
<StyledDivider flexItem orientation="vertical" />
{ /* close / open polygon mode only for freehand drawing mode. */
activeTool === 'freehand'
? (
<ToggleButtonGroup
size="small"
value={closedMode}
onChange={changeClosedMode}
>
<ToggleButton value="closed">
<ClosedPolygonIcon />
</ToggleButton>
<ToggleButton value="open">
<OpenPolygonIcon />
</ToggleButton>
</ToggleButtonGroup>
)
: null
}
</Grid>
</Grid>
</div>
</>
)
}
{
activeTool === 'images' ? (
<>
<Grid container>
<ImageFormField xs={8} value={image} onChange={handleImgChange} />
</Grid>
<StyledDivButtonImage>
<Button variant="contained" onClick={addImage}>
<AddPhotoAlternateIcon />
</Button>
</StyledDivButtonImage>
</>
) : (<></>)
}
{
activeTool === 'text' && (
<Typography>
Ajouter un input text
</Typography>
)
}
</Grid>
</Grid>
</div>
<Popover
open={lineWeightPopoverOpen}
anchorEl={popoverLineWeightAnchorEl}
>
<Paper>
<ClickAwayListener onClickAway={handleCloseLineWeight}>
<MenuList autoFocus role="listbox">
{[1, 3, 5, 10, 50].map((option, index) => (
<MenuItem
key={option}
onClick={handleLineWeightSelect}
value={option}
selected={option == strokeWidth}
role="option"
aria-selected={option == strokeWidth}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Popover>
<Popover
open={colorPopoverOpen}
anchorEl={popoverAnchorEl}
onClose={closeChooseColor}
>
<SketchPicker
disableAlpha={false}
color={rgbaToObj(toolState[currentColorType])}
onChangeComplete={updateStrokeColor}
/>
</Popover>
</StyledPaper>
);
}
AnnotationFormDrawing.propTypes = {
handleImgChange: PropTypes.func,
toolState: PropTypes.object,
updateToolState: PropTypes.func,
shapes: PropTypes.object,
deleteShape: PropTypes.func,
};
export default AnnotationFormDrawing;
import {
Button, Grid, Paper,
} from '@mui/material';
import Typography from '@mui/material/Typography';
import ToggleButton from '@mui/material/ToggleButton';
import TitleIcon from '@mui/icons-material/Title';
import ImageIcon from '@mui/icons-material/Image';
import DeleteIcon from '@mui/icons-material/Delete';
import React, { useEffect } from 'react';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import CategoryIcon from '@mui/icons-material/Category';
import CursorIcon from '../../icons/Cursor';
import AnnotationFormOverlayTool from './AnnotationFormOverlayTool';
import { OVERLAY_TOOL } from '../../AnnotationCreationUtils';
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
'&:first-of-type': {
borderRadius: theme.shape.borderRadius,
},
'&:not(:first-of-type)': {
borderRadius: theme.shape.borderRadius,
},
border: 'none',
margin: theme.spacing(0.5),
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: '5px',
}));
/** All the stuff to manage to choose the drawing tool */
function AnnotationFormOverlay({
updateToolState, toolState, shapes, deleteShape,
}) {
useEffect(() => {
}, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]);
const changeTool = (e, tool) => {
updateToolState({
...toolState,
activeTool: tool,
});
};
const {
activeTool,
} = toolState;
return (
<StyledPaper>
<div>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Overlay
</Typography>
</Grid>
<Grid item xs={12}>
<StyledToggleButtonGroup
value={activeTool} // State or props ?
exclusive
onChange={changeTool}
aria-label="tool selection"
size="small"
>
<ToggleButton value={OVERLAY_TOOL.EDIT} aria-label="select cursor">
<CursorIcon />
</ToggleButton>
<ToggleButton value={OVERLAY_TOOL.SHAPE} aria-label="select cursor">
<CategoryIcon />
</ToggleButton>
<ToggleButton value={OVERLAY_TOOL.IMAGE} aria-label="select cursor">
<ImageIcon />
</ToggleButton>
<ToggleButton value={OVERLAY_TOOL.TEXT} aria-label="select text">
<TitleIcon />
</ToggleButton>
<ToggleButton value={OVERLAY_TOOL.DELETE} aria-label="select cursor">
<DeleteIcon />
</ToggleButton>
</StyledToggleButtonGroup>
<AnnotationFormOverlayTool
toolState={toolState}
updateToolState={updateToolState}
shapes={shapes}
deleteShape={deleteShape}
/>
</Grid>
</Grid>
</div>
</StyledPaper>
);
}
AnnotationFormOverlay.propTypes = {
deleteShape: PropTypes.func.isRequired,
shapes: PropTypes.array.isRequired,
toolState: PropTypes.shape({
activeTool: PropTypes.string.isRequired,
closedMode: PropTypes.bool.isRequired,
fillColor: PropTypes.string.isRequired,
image: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
strokeColor: PropTypes.string.isRequired,
strokeWidth: PropTypes.number.isRequired,
updateColor: PropTypes.func.isRequired,
}).isRequired,
updateToolState: PropTypes.func.isRequired,
};
export default AnnotationFormOverlay;
import ToggleButton from '@mui/material/ToggleButton';
import RectangleIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CircleIcon from '@mui/icons-material/RadioButtonUnchecked';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import PolygonIcon from '@mui/icons-material/Timeline';
import GestureIcon from '@mui/icons-material/Gesture';
import PropTypes from 'prop-types';
import React from 'react';
import { styled } from '@mui/material/styles';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { Button } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AnnotationFormOverlayToolOptions from './AnnotationFormOverlayToolOptions';
import { isShapesTool, OVERLAY_TOOL, SHAPES_TOOL } from '../../AnnotationCreationUtils';
const StyledLi = styled('li')(({ theme }) => ({
display: 'flex',
wordBreak: 'break-word',
}));
const StyledUl = styled('ul')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: '5px',
listStyle: 'none',
paddingLeft: '0',
}));
// TODO WIP code duplicated
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
'&:first-of-type': {
borderRadius: theme.shape.borderRadius,
},
'&:not(:first-of-type)': {
borderRadius: theme.shape.borderRadius,
},
border: 'none',
margin: theme.spacing(0.5),
}));
/** All the form part for the overlay view */
function AnnotationFormOverlayTool({
toolState, updateToolState, shapes, deleteShape,
}) {
/** Change the active overlay tool */
const changeTool = (e, tool) => {
updateToolState({
...toolState,
activeTool: tool,
});
};
return (
<>
{toolState.activeTool}
{
toolState.activeTool === OVERLAY_TOOL.EDIT && (
<StyledUl>
{shapes && shapes.map((shape) => (
<StyledLi key={shape.id}>
{shape.id}
<Button onClick={() => deleteShape(shape.id)}>
<DeleteIcon />
</Button>
</StyledLi>
))}
</StyledUl>
)
}
{
isShapesTool(toolState.activeTool) && (
<StyledToggleButtonGroup
value={toolState.activeTool} // State or props ?
exclusive
onChange={changeTool}
aria-label="tool selection"
size="small"
>
<ToggleButton value={SHAPES_TOOL.RECTANGLE} aria-label="add a rectangle">
<RectangleIcon />
</ToggleButton>
<ToggleButton value={SHAPES_TOOL.ELLIPSE} aria-label="add a circle">
<CircleIcon />
</ToggleButton>
<ToggleButton value={SHAPES_TOOL.ARROW} aria-label="add an arrow">
<ArrowOutwardIcon />
</ToggleButton>
<ToggleButton value={SHAPES_TOOL.POLYGON} aria-label="add a polygon">
<PolygonIcon />
</ToggleButton>
<ToggleButton value={SHAPES_TOOL.FREEHAND} aria-label="free hand polygon">
<GestureIcon />
</ToggleButton>
</StyledToggleButtonGroup>
)
}
<AnnotationFormOverlayToolOptions
toolState={toolState}
updateToolState={updateToolState}
/>
</>
);
}
AnnotationFormOverlayTool.propTypes = {
toolState: PropTypes.shape({
activeTool: PropTypes.string.isRequired,
closedMode: PropTypes.bool.isRequired,
fillColor: PropTypes.string.isRequired,
image: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
strokeColor: PropTypes.string.isRequired,
strokeWidth: PropTypes.number.isRequired,
updateColor: PropTypes.func.isRequired,
}).isRequired,
updateToolState: PropTypes.func.isRequired,
};
export default AnnotationFormOverlayTool;
import {
Button,
ClickAwayListener, Divider, Grid, MenuItem, MenuList, Paper, Popover,
} from '@mui/material';
import Typography from '@mui/material/Typography';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import StrokeColorIcon from '@mui/icons-material/BorderColor';
import LineWeightIcon from '@mui/icons-material/LineWeight';
import FormatColorFillIcon from '@mui/icons-material/FormatColorFill';
import React, { useState } from 'react';
import ClosedPolygonIcon from '@mui/icons-material/ChangeHistory';
import OpenPolygonIcon from '@mui/icons-material/ShowChart';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { SketchPicker } from 'react-color';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { v4 as uuidv4 } from 'uuid';
import ImageFormField from './ImageFormField';
import { isShapesTool, OVERLAY_TOOL } from '../../AnnotationCreationUtils';
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(1, 0.5),
}));
const StyledDivButtonImage = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
marginTop: '5px',
}));
/** Utils functions to convert string to object */
const rgbaToObj = (rgba = 'rgba(255,255,255,0.5)') => {
const rgbaArray = rgba.split(',');
return {
// eslint-disable-next-line sort-keys
r: Number(rgbaArray[0].split('(')[1]),
// eslint-disable-next-line sort-keys
g: Number(rgbaArray[1]),
// eslint-disable-next-line sort-keys
b: Number(rgbaArray[2]),
// eslint-disable-next-line sort-keys
a: Number(rgbaArray[3].split(')')[0]),
};
};
/** Convert color object to rgba string */
const objToRgba = (obj = {
// eslint-disable-next-line sort-keys
r: 255, g: 255, b: 255, a: 0.5,
}) => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`;
/** All the tools options for the overlay options */
function AnnotationFormOverlayToolOptions({ updateToolState, toolState }) {
// set toolOptionsValue
const [toolOptions, setToolOptions] = useState({
colorPopoverOpen: false,
currentColorType: null,
lineWeightPopoverOpen: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
});
// Set unused default color to avoid error on render
const currentColor = toolOptions.currentColorType ? rgbaToObj(toolState[toolOptions.currentColorType]) : 'rgba(255, 0, 0, 0.5)';
// Fonction to manage option displaying
/** */
const openChooseLineWeight = (e) => {
setToolOptions({
...toolOptions,
lineWeightPopoverOpen: true,
popoverLineWeightAnchorEl: e.currentTarget,
});
};
/** */
const handleLineWeightSelect = (e) => {
setToolOptions({
...toolOptions,
lineWeightPopoverOpen: false,
popoverLineWeightAnchorEl: null,
});
updateToolState({
...toolState,
strokeWidth: e.currentTarget.value,
});
};
/** Close color popover window */
const closeChooseColor = (e) => {
setToolOptions({
...toolOptions,
colorPopoverOpen: false,
currentColorType: null,
popoverAnchorEl: null,
});
};
/** */
const openChooseColor = (e) => {
console.log('openChooseColor', e.currentTarget.value);
setToolOptions({
...toolOptions,
colorPopoverOpen: true,
currentColorType: e.currentTarget.value,
popoverAnchorEl: e.currentTarget,
});
};
/** */
const handleCloseLineWeight = (e) => {
setToolOptions({
...toolOptions,
lineWeightPopoverOpen: false,
popoverLineWeightAnchorEl: null,
});
};
/** closed mode change */
const changeClosedMode = (e) => {
updateToolState({
...toolState,
closedMode: e.currentTarget.value,
});
};
/** Update color : fillColor or strokeColor */
const updateColor = (color) => {
updateToolState({
...toolState,
[toolOptions.currentColorType]: objToRgba(color.rgb),
});
};
const addImage = () => {
const data = {
id: toolState?.image?.id,
uuid: uuidv4(),
};
updateToolState({
...toolState,
image: { id: null },
imageEvent: data,
});
};
const handleImgChange = (newUrl, imgRef) => {
updateToolState({
...toolState,
image: { ...toolState.image, id: newUrl },
});
};
return (
<div>
{
isShapesTool(toolState.activeTool) && (
<Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Style
</Typography>
</Grid>
<Grid item xs={12}>
<ToggleButtonGroup
aria-label="style selection"
size="small"
>
<ToggleButton
value="strokeColor"
aria-label="select color"
onClick={openChooseColor}
>
<StrokeColorIcon style={{ fill: toolState.strokeColor }} />
<ArrowDropDownIcon />
</ToggleButton>
<ToggleButton
value="strokeColor"
aria-label="select line weight"
onClick={openChooseLineWeight}
>
<LineWeightIcon />
<ArrowDropDownIcon />
</ToggleButton>
<ToggleButton
value="fillColor"
aria-label="select color"
onClick={openChooseColor}
>
<FormatColorFillIcon style={{ fill: toolState.fillColor }} />
<ArrowDropDownIcon />
</ToggleButton>
</ToggleButtonGroup>
<StyledDivider flexItem orientation="vertical" />
{ /* close / open polygon mode only for freehand drawing mode. */
toolState.activeTool === 'freehand'
&& (
<ToggleButtonGroup
size="small"
value={toolState.closedMode}
onChange={changeClosedMode}
>
<ToggleButton value="closed">
<ClosedPolygonIcon />
</ToggleButton>
<ToggleButton value="open">
<OpenPolygonIcon />
</ToggleButton>
</ToggleButtonGroup>
)
}
</Grid>
<Popover
open={toolOptions.lineWeightPopoverOpen}
anchorEl={toolOptions.popoverLineWeightAnchorEl}
>
<Paper>
<ClickAwayListener onClickAway={handleCloseLineWeight}>
<MenuList autoFocus role="listbox">
{[1, 3, 5, 10, 50].map((option, index) => (
<MenuItem
key={option}
onClick={handleLineWeightSelect}
value={option}
selected={option === toolState.strokeWidth}
role="option"
aria-selected={option === toolState.strokeWidth}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Popover>
<Popover
open={toolOptions.colorPopoverOpen}
anchorEl={toolOptions.popoverAnchorEl}
onClose={closeChooseColor}
>
<SketchPicker
disableAlpha={false}
color={currentColor}
onChangeComplete={updateColor}
/>
</Popover>
</Grid>
)
}
{
toolState.activeTool === OVERLAY_TOOL.IMAGE && (
<>
<Grid container>
<ImageFormField xs={8} value={toolState.image} onChange={handleImgChange} />
</Grid>
<StyledDivButtonImage>
<Button variant="contained" onClick={addImage}>
<AddPhotoAlternateIcon />
</Button>
</StyledDivButtonImage>
</>
)
}
{
toolState.activeTool === 'text' && (
// <TextField
// value={toolState.text
// onChange={(ev) => onChange(ev.target.value)}
// error={imgUrl !== '' && !imgIsValid}
// margin="dense"
// label="Image URL"
// type="url"
// fullWidth
// inputRef={inputRef}
// />
' TODO add input'
)
}
</div>
);
}
AnnotationFormOverlayToolOptions.propTypes = {
toolState: PropTypes.shape({
activeTool: PropTypes.string.isRequired,
closedMode: PropTypes.bool.isRequired,
fillColor: PropTypes.string.isRequired,
image: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
strokeColor: PropTypes.string.isRequired,
strokeWidth: PropTypes.number.isRequired,
updateColor: PropTypes.func.isRequired,
}).isRequired,
updateToolState: PropTypes.func.isRequired,
};
export default AnnotationFormOverlayToolOptions;
......@@ -10,10 +10,11 @@ const StyledRoot = styled('div')(({ theme }) => ({
}));
const StyledTextField = styled(TextField)(({ theme }) => ({
marginTop:"0",
marginBottom:"0",
marginBottom: '0',
marginTop: '0',
}));
/** Image input field for the annotation form */
function ImageFormField({ value: image, onChange }) {
const inputRef = useRef(null);
const [imgIsValid, setImgIsValid] = useState(false);
......
......@@ -5,7 +5,7 @@ import { Ellipse, Transformer } from 'react-konva';
function EllipseNode({
onShapeClick, shape, activeTool, isSelected,
onTransform, handleDragEnd
onTransform, handleDragEnd,
}) {
const shapeRef = useRef();
const trRef = useRef();
......@@ -53,13 +53,13 @@ function EllipseNode({
EllipseNode.propTypes = {
fill: PropTypes.string,
fill: PropTypes.string.isRequired,
height: PropTypes.number,
onShapeClick: PropTypes.func.isRequired,
selectedShapeId: PropTypes.string,
shape: PropTypes.object.isRequired,
stroke: PropTypes.string,
strokeWidth: PropTypes.number,
stroke: PropTypes.string.isRequired,
strokeWidth: PropTypes.number.isRequired,
width: PropTypes.number,
x: PropTypes.number,
y: PropTypes.number,
......@@ -71,9 +71,6 @@ EllipseNode.defaultProps = {
y: 100,
width: 100,
height: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 1,
};
export default EllipseNode;
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Layer } from 'react-konva';
import Rectangle from './Rectangle';
import EllipseNode from './EllipseNode';
import TextNode from './TextNode';
import LineNode from './LineNode';
import ArrowNode from './ArrowNode';
import Polygon from './Polygon';
import Freehand from './Freehand';
import ImageShape from './Image';
/** Loads Konva and display in function of their type */
function ParentComponent({
shapes, onShapeClick, selectedShapeId, activeTool,
isMouseOverSave,
scale, width, height, onTransform, handleDragEnd,
isMouseOverSave,trview
shapes, onShapeClick, selectedShapeId, activeTool,
}) {
// TODO Simplify these state
const [selectedShape, setSelectedShape] = useState(null);
......@@ -54,11 +51,11 @@ function ParentComponent({
<Rectangle
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -68,11 +65,11 @@ function ParentComponent({
<TextNode
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -82,11 +79,11 @@ function ParentComponent({
<EllipseNode
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -96,11 +93,11 @@ function ParentComponent({
<Freehand
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -110,11 +107,11 @@ function ParentComponent({
<Polygon
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -124,11 +121,11 @@ function ParentComponent({
<ArrowNode
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
}}
key={i}
/>
......@@ -138,18 +135,16 @@ function ParentComponent({
<ImageShape
{...{
activeTool,
handleDragEnd,
isSelected,
onShapeClick: handleShapeClick,
shape,
onTransform,
handleDragEnd,
shape,
src: shape.src,
}}
key={i}
/>
);
default:
return null;
}
})}
</Layer>
......@@ -157,19 +152,15 @@ function ParentComponent({
}
ParentComponent.propTypes = {
shapes: PropTypes.arrayOf(PropTypes.object).isRequired,
activeTool: PropTypes.string.isRequired,
handleDragEnd: PropTypes.func.isRequired,
height: PropTypes.number.isRequired,
isMouseOverSave: PropTypes.bool.isRequired,
onShapeClick: PropTypes.func.isRequired,
selectedShapeIdProp: PropTypes.string,
activeTool: PropTypes.string,
scale: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
onTransform: PropTypes.func,
handleDragEnd: PropTypes.func,
};
ParentComponent.defaultProps = {
selectedShapeIdProp: null,
onTransform: PropTypes.func.isRequired,
scale: PropTypes.number.isRequired,
selectedShapeId: PropTypes.string.isRequired,
shapes: PropTypes.arrayOf(PropTypes.object).isRequired,
width: PropTypes.number.isRequired,
};
export default ParentComponent;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment