From 161f3fe2c48ef633084f17991efc80154655a7e0 Mon Sep 17 00:00:00 2001 From: Daniel Steinkogler Date: Mon, 4 Mar 2024 15:05:19 +0000 Subject: [PATCH 01/30] feat(#1122): fillPatterns, labels, icons --- frontend/package-lock.json | 21 + frontend/package.json | 1 + .../Form/DebouncedFillPatternInput.tsx | 81 ++++ frontend/src/config/i18n/de/drawings.json | 4 + frontend/src/config/i18n/en/drawings.json | 9 +- .../drawing/DrawingAttributeEditForm.tsx | 58 ++- .../layers/drawing/DrawingLayer.tsx | 360 +++++++++++++++--- .../drawing/DrawingLayerFillPatterns.tsx | 70 ++++ .../drawing/DrawingLayerIconToolForm.tsx | 58 +++ .../drawing/DrawingLayerLabelToolForm.tsx | 84 ++++ .../drawing/DrawingLayerLeftToolbar.tsx | 8 +- .../drawing/DrawingLayerRightToolbar.tsx | 31 +- ...orm.tsx => DrawingLayerShapesToolForm.tsx} | 21 +- .../layers/drawing/labels/EditableText.tsx | 85 +++++ .../drawing/labels/EditableTextinput.tsx | 72 ++++ .../layers/drawing/shapes/BezierPolygon.tsx | 9 +- .../map_planning/layers/drawing/types.ts | 122 ++++++ .../map_planning/store/MapStoreTypes.ts | 10 +- .../map_planning/store/UntrackedMapStore.ts | 22 +- frontend/src/svg/icons/forbid.svg | 5 + frontend/src/svg/icons/square-filled.svg | 4 + frontend/src/svg/icons/text.svg | 9 + frontend/src/svg/icons/texture.svg | 10 + frontend/src/svg/icons/track.svg | 4 + frontend/src/utils/fillPatterns.ts | 63 +++ 25 files changed, 1093 insertions(+), 128 deletions(-) create mode 100644 frontend/src/components/Form/DebouncedFillPatternInput.tsx create mode 100644 frontend/src/features/map_planning/layers/drawing/DrawingLayerFillPatterns.tsx create mode 100644 frontend/src/features/map_planning/layers/drawing/DrawingLayerIconToolForm.tsx create mode 100644 frontend/src/features/map_planning/layers/drawing/DrawingLayerLabelToolForm.tsx rename frontend/src/features/map_planning/layers/drawing/{DrawingLayerToolForm.tsx => DrawingLayerShapesToolForm.tsx} (91%) create mode 100644 frontend/src/features/map_planning/layers/drawing/labels/EditableText.tsx create mode 100644 frontend/src/features/map_planning/layers/drawing/labels/EditableTextinput.tsx create mode 100644 frontend/src/svg/icons/forbid.svg create mode 100644 frontend/src/svg/icons/square-filled.svg create mode 100644 frontend/src/svg/icons/text.svg create mode 100644 frontend/src/svg/icons/texture.svg create mode 100644 frontend/src/svg/icons/track.svg create mode 100644 frontend/src/utils/fillPatterns.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd900401d..963b649d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,6 +35,7 @@ "react-select": "^5.8.0", "react-select-async-paginate": "^0.7.3", "react-shepherd": "^4.2.0", + "react-tabs": "^6.0.2", "react-toastify": "^9.1.2", "react-tooltip": "^5.22.0", "typewriter-effect": "^2.19.0", @@ -16338,6 +16339,26 @@ "react-dom": "^17.0.2 || 18.x" } }, + "node_modules/react-tabs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", + "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", + "dependencies": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-tabs/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-test-renderer": { "version": "18.2.0", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 6768dd47a..289c5fc9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "react-select": "^5.8.0", "react-select-async-paginate": "^0.7.3", "react-shepherd": "^4.2.0", + "react-tabs": "^6.0.2", "react-toastify": "^9.1.2", "react-tooltip": "^5.22.0", "typewriter-effect": "^2.19.0", diff --git a/frontend/src/components/Form/DebouncedFillPatternInput.tsx b/frontend/src/components/Form/DebouncedFillPatternInput.tsx new file mode 100644 index 000000000..c5890d435 --- /dev/null +++ b/frontend/src/components/Form/DebouncedFillPatternInput.tsx @@ -0,0 +1,81 @@ +import { ComponentPropsWithoutRef } from 'react'; +import { + Controller, + FieldValues, + Path, + SubmitErrorHandler, + SubmitHandler, + useFormContext, +} from 'react-hook-form'; +import { DrawingLayerFillPatterns } from '@/features/map_planning/layers/drawing/DrawingLayerFillPatterns'; +import { FillPatternType } from '@/features/map_planning/layers/drawing/types'; +import { useDebouncedSubmit } from '@/hooks/useDebouncedSubmit'; +import CheckIcon from '@/svg/icons/check.svg?react'; +import CircleDottedIcon from '@/svg/icons/circle-dotted.svg?react'; +import SimpleFormInput from './SimpleFormInput'; + +type DebouncedSimpleFormInputProps = { + /** A function that takes the from values. + * It is called if the settled value of the input is valid */ + onValid: SubmitHandler; + /** A function that takes the form error. + * It is called if the settled value of the input is invalid */ + onInvalid?: SubmitErrorHandler | undefined; + /** The elements unique id. + * Gets passed to react-hook-form to identify which field the input belongs to. */ + id: Path; + /** The test id for the input */ + 'data-testid'?: string; +} & Omit; + +type SimpleFormInputProps = ComponentPropsWithoutRef; + +/** + * A wrapper around {@link SimpleFormInput} that adds a loading spinner or checkmark + * to the right of the input depending on the state of a debounced submit. + * + * # Important + * It is using the {@link useFormContext} hook, so it must be rendered inside a FormProvider. + * + * @param props - {@link DebouncedSimpleFormInputProps} + * @returns + */ +export function DebouncedFillPatternSelector({ + onValid, + onInvalid, + id, + 'data-testid': testId, + ...rest +}: DebouncedSimpleFormInputProps) { + const { handleSubmit, watch, control } = useFormContext(); + + const submitState = useDebouncedSubmit(watch(id), handleSubmit, onValid, onInvalid); + + return ( +
+ ( + + )} + /> + {submitState === 'loading' && ( + + )} + {submitState === 'idle' && ( + + )} +
+ ); +} diff --git a/frontend/src/config/i18n/de/drawings.json b/frontend/src/config/i18n/de/drawings.json index 103ab08ae..bee712a8a 100644 --- a/frontend/src/config/i18n/de/drawings.json +++ b/frontend/src/config/i18n/de/drawings.json @@ -26,4 +26,8 @@ "edit_mode": "Bearbeitungsmodus aktivieren", "error_init_layer": "Entschuldigung, ich habe Schwierigkeiten gezeichnete Elemente von meinem Server zu erhalten. Bitte versuche es später erneut." + "operations": "Operationen zur Bearbeitung", + "edit_mode": "Bearbeitungsmodus aktivieren", + "fillPattern": "Füllmuster", + "select_image": "Bild auswählen" } diff --git a/frontend/src/config/i18n/en/drawings.json b/frontend/src/config/i18n/en/drawings.json index 02ea51857..957675da4 100644 --- a/frontend/src/config/i18n/en/drawings.json +++ b/frontend/src/config/i18n/en/drawings.json @@ -6,28 +6,23 @@ "draw_bezier_polygon_hint": "Use left-click to add points, right-click to finish current polygon.", "edit_bezier_polygon_hint": "Use left-click to add points.", "delete_bezier_polygon_point_hint": "Click on a point to delete it.", - "draw_rectangle_tooltip": "Draw rectangle.", "draw_ellipse_tooltip": "Draw ellipse.", "draw_free_line_tooltip": "Draw free line.", "draw_bezier_polygon_tooltip": "Draw bezier polygon.", "edit_bezier_polygon_tooltip": "Edit bezier polygon.", "delete_bezier_polygon_point_tooltip": "Delete points.", - "delete": "Delete Drawing", "delete_multiple_drawings": "Delete Drawings", - - "add_date": "Added on", "remove_date": "Removed on", "color": "Color", "strokeWidth": "Stroke Width", "fillEnabled": "Fill Enabled", - - "error_invalid_add_remove_date": "Remove date must be after add date.", "operations": "Edit Points", "edit_mode": "Activate Edit Mode", - "error_init_layer": "Sorry, I'm having trouble loading drawn elements from my server. Please try again later." + "fillPattern": "Fill Pattern", + "select_image": "Select Image" } diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingAttributeEditForm.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingAttributeEditForm.tsx index 1cc11b572..50bd03bb1 100644 --- a/frontend/src/features/map_planning/layers/drawing/DrawingAttributeEditForm.tsx +++ b/frontend/src/features/map_planning/layers/drawing/DrawingAttributeEditForm.tsx @@ -6,11 +6,13 @@ import { z } from 'zod'; import { DrawingDto, DrawingShapeType } from '@/api_types/definitions'; import IconButton from '@/components/Button/IconButton'; import SimpleButton, { ButtonVariant } from '@/components/Button/SimpleButton'; +import { DebouncedFillPatternSelector } from '@/components/Form/DebouncedFillPatternInput'; import { DebouncedSimpleFormInput } from '@/components/Form/DebouncedSimpleFormInput'; import EditIcon from '@/svg/icons/edit.svg?react'; import EraserIcon from '@/svg/icons/eraser.svg?react'; import useMapStore from '../../store/MapStore'; import { DrawingLayerStatusPanelContent } from './DrawingLayerStatusPanelContent'; +import { DrawingDto, FillPatternType } from './types'; const DrawingAttributeEditFormSchema = z // The 'empty' value for the API is undefined, so we need to transform the empty string to undefined @@ -19,7 +21,7 @@ const DrawingAttributeEditFormSchema = z removeDate: z.nullable(z.string()).transform((value) => value || undefined), color: z.string(), strokeWidth: z.number().nullable(), - fillEnabled: z.boolean().nullable(), + fillPattern: z.nullable(z.string()).transform((value) => value || undefined), }) .refine((schema) => !schema.removeDate || !schema.addDate || schema.addDate < schema.removeDate, { path: ['dateRelation'], @@ -30,7 +32,7 @@ export type EditDrawingAttributesProps = { onRemoveDateChange: (removeDate: DrawingFormData) => void; onColorChange: (color: DrawingFormData) => void; onStrokeWidthChange: (strokeWidth: DrawingFormData) => void; - onFillEnabledChange: (fillEnabled: DrawingFormData) => void; + onFillPatternChange: (fillPattern: DrawingFormData) => void; onDeleteClick: () => void; isReadOnlyMode: boolean; }; @@ -49,14 +51,14 @@ export type DrawingAttributeEditFormProps = EditDrawingAttributesProps & { removeDateDefaultValue: string; colorDefaultValue: string; strokeWidthDefaultValue?: number; - fillEnabledDefaultValue?: boolean; + fillPatternDefaultValue?: FillPatternType; multipleDrawings?: boolean; showShapeEditButton: boolean; }; export type DrawingFormData = Pick< DrawingDto, - 'addDate' | 'removeDate' | 'scaleX' | 'scaleY' | 'color' | 'strokeWidth' | 'fillEnabled' + 'addDate' | 'removeDate' | 'scaleX' | 'scaleY' | 'color' | 'strokeWidth' | 'fillPattern' >; export function SingleDrawingAttributeForm({ @@ -66,7 +68,7 @@ export function SingleDrawingAttributeForm({ onDeleteClick, onColorChange, onStrokeWidthChange, - onFillEnabledChange, + onFillPatternChange, isReadOnlyMode, }: EditSingleDrawingProps) { return ( @@ -76,13 +78,13 @@ export function SingleDrawingAttributeForm({ removeDateDefaultValue={drawing.removeDate ?? ''} colorDefaultValue={drawing.color ?? ''} strokeWidthDefaultValue={drawing.strokeWidth ?? 0} - fillEnabledDefaultValue={drawing.fillEnabled} + fillPatternDefaultValue={drawing.fillPattern} onAddDateChange={onAddDateChange} onRemoveDateChange={onRemoveDateChange} onDeleteClick={onDeleteClick} onColorChange={onColorChange} onStrokeWidthChange={onStrokeWidthChange} - onFillEnabledChange={onFillEnabledChange} + onFillPatternChange={onFillPatternChange} isReadOnlyMode={isReadOnlyMode} drawingId={drawing.id} showShapeEditButton={drawing.shapeType === DrawingShapeType.BezierPolygon} @@ -98,7 +100,7 @@ export function MultipleDrawingAttributeForm({ onDeleteClick, onColorChange, onStrokeWidthChange, - onFillEnabledChange, + onFillPatternChange, isReadOnlyMode, }: EditMultipleDrawingsProps) { const getCommonAddDate = () => { @@ -128,11 +130,11 @@ export function MultipleDrawingAttributeForm({ }; const getCommonFillEnabled = () => { - const fillEnabled = drawings[0].fillEnabled; + const fillEnabled = drawings[0].fillPattern; const existsCommonFillEnabled = drawings.every( - (drawing) => drawing.fillEnabled === fillEnabled, + (drawing) => drawing.fillPattern === fillEnabled, ); - return existsCommonFillEnabled ? fillEnabled : false; + return existsCommonFillEnabled ? fillEnabled : undefined; }; return ( @@ -142,13 +144,13 @@ export function MultipleDrawingAttributeForm({ removeDateDefaultValue={getCommonRemoveDate() ?? ''} colorDefaultValue={getCommonColor()} strokeWidthDefaultValue={getCommonStrokeWidth()} - fillEnabledDefaultValue={getCommonFillEnabled()} + fillPatternDefaultValue={getCommonFillEnabled()} onAddDateChange={onAddDateChange} onRemoveDateChange={onRemoveDateChange} onDeleteClick={onDeleteClick} onColorChange={onColorChange} onStrokeWidthChange={onStrokeWidthChange} - onFillEnabledChange={onFillEnabledChange} + onFillPatternChange={onFillPatternChange} isReadOnlyMode={isReadOnlyMode} multipleDrawings={true} showShapeEditButton={false} @@ -163,13 +165,13 @@ export function DrawingAttributeEditForm({ removeDateDefaultValue, colorDefaultValue, strokeWidthDefaultValue, - fillEnabledDefaultValue, + fillPatternDefaultValue, onAddDateChange, onRemoveDateChange, onDeleteClick, onColorChange, onStrokeWidthChange, - onFillEnabledChange, + onFillPatternChange, isReadOnlyMode, multipleDrawings = false, showShapeEditButton, @@ -182,13 +184,12 @@ export function DrawingAttributeEditForm({ removeDate: removeDateDefaultValue, color: colorDefaultValue, strokeWidth: strokeWidthDefaultValue, - fillEnabled: fillEnabledDefaultValue, + fillPattern: fillPatternDefaultValue, }, resolver: zodResolver(DrawingAttributeEditFormSchema), }); const showStrokeWidth = strokeWidthDefaultValue !== undefined && strokeWidthDefaultValue > 0; - const showFillEnabled = fillEnabledDefaultValue !== undefined; const drawingLayerSetEditMode = useMapStore((state) => state.drawingLayerSetEditMode); const editMode = useMapStore((state) => state.untrackedState.layers.drawing.editMode); @@ -201,14 +202,14 @@ export function DrawingAttributeEditForm({ removeDate: removeDateDefaultValue, color: colorDefaultValue, strokeWidth: strokeWidthDefaultValue, - fillEnabled: fillEnabledDefaultValue, + fillPattern: fillPatternDefaultValue, }); }, [ addDateDefaultValue, removeDateDefaultValue, colorDefaultValue, strokeWidthDefaultValue, - fillEnabledDefaultValue, + fillPatternDefaultValue, formInfo, ]); @@ -272,18 +273,13 @@ export function DrawingAttributeEditForm({ )} - {showFillEnabled && ( -
- -
- )} +
+ +
{showShapeEditButton && drawingId && (
diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayer.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayer.tsx index 98189fcef..52c5fc91e 100644 --- a/frontend/src/features/map_planning/layers/drawing/DrawingLayer.tsx +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayer.tsx @@ -11,19 +11,30 @@ import { createKeyBindingsAccordingToConfig, } from '@/config/keybindings'; import { useKeyHandlers } from '@/hooks/useKeyHandlers'; +import { getFillPattern } from '@/utils/fillPatterns'; +import { NextcloudKonvaImage } from '../../components/image/NextcloudKonvaImage'; import useMapStore from '../../store/MapStore'; import { useTransformerStore } from '../../store/transformer/TransformerStore'; import { useIsDrawingLayerActive } from '../../utils/layer-utils'; import { CreateDrawingAction, UpdateDrawingAction } from './actions'; import { useDeleteSelectedDrawings } from './hooks/useDeleteSelectedDrawings'; +import { EditableText } from './labels/EditableText'; import BezierPolygon from './shapes/BezierPolygon'; -import { EllipseProperties, FreeLineProperties, RectangleProperties } from './types'; +import { + + EllipseProperties, + FillPatternType, + FreeLineProperties, + ImageProperties, + LabelTextProperties, + RectangleProperties, +} from './types'; type DrawingLayerProps = LayerConfig; type Rectangle = { color: string; - fillEnabled: boolean; + fillPattern: FillPatternType; strokeWidth: number; x1: number; y1: number; @@ -32,7 +43,7 @@ type Rectangle = { }; type Ellipse = { - fillEnabled: boolean; + fillPattern: FillPatternType; strokeWidth: number; color: string; x: number; @@ -44,12 +55,29 @@ type Ellipse = { type Line = { color: string; strokeWidth: number; - fillEnabled: boolean; + fillPattern: FillPatternType; x: number; y: number; points: number[][]; }; +type Text = { + color: string; + text: string; + scaleX: number; + scaleY: number; + x: number; + y: number; +}; + +type Image = { + path: string; + scaleX: number; + scaleY: number; + x: number; + y: number; +}; + function useDrawingLayerKeyListeners() { const { deleteSelectedDrawings } = useDeleteSelectedDrawings(); const isDrawingLayerActive = useIsDrawingLayerActive(); @@ -70,10 +98,18 @@ function DrawingLayer(props: DrawingLayerProps) { const selectedStrokeWidth = useMapStore( (state) => state.untrackedState.layers.drawing.selectedStrokeWidth, ); - const fillEnabled = useMapStore((state) => state.untrackedState.layers.drawing.fillEnabled); + const selectedIcon = useMapStore((state) => state.untrackedState.layers.drawing.selectedIconPath); + + const fillPattern = useMapStore((state) => state.untrackedState.layers.drawing.fillPattern); const isDrawingLayerActive = useIsDrawingLayerActive(); + const mapScale = useMapStore.getState().stageRef.current?.scale(); + + const fillPatternScaleX = mapScale == undefined ? 1 : 1 / mapScale.x; + const fillPatternScaleY = mapScale == undefined ? 1 : 1 / mapScale.y; + const selectDrawings = useMapStore((state) => state.selectDrawings); + useDrawingLayerKeyListeners(); const { ...layerProps } = props; @@ -114,8 +150,28 @@ function DrawingLayer(props: DrawingLayerProps) { }; }); + const textLabels = drawingObjects + .filter((object) => object.type === 'text') + .map((object) => { + return { + ...object, + properties: object.properties as LabelTextProperties, + }; + }); + + const images = drawingObjects + .filter((object) => object.type === 'image') + .map((object) => { + return { + ...object, + properties: object.properties as ImageProperties, + }; + }); + const [newLine, setNewLine] = useState(); const [newBezierLine, setNewBezierLine] = useState(); + const [newLabelText, setNewLabelText] = useState(); + const [newImage, setNewImage] = useState(); const [previewRectangle, setPreviewRectangle] = useState(); const [previewEllipse, setPreviewEllipse] = useState(); @@ -148,7 +204,7 @@ function DrawingLayer(props: DrawingLayerProps) { scaleX: 1, scaleY: 1, color: rectangle.color, - fillEnabled: rectangle.fillEnabled, + fillPattern: rectangle.fillPattern, strokeWidth: rectangle.strokeWidth, properties: { width: rectangle.x2 - rectangle.x1, @@ -176,7 +232,7 @@ function DrawingLayer(props: DrawingLayerProps) { x: Math.round(ellipse.x), y: Math.round(ellipse.y), color: ellipse.color, - fillEnabled: ellipse.fillEnabled, + fillPattern: ellipse.fillPattern, strokeWidth: ellipse.strokeWidth, properties: { radiusX: ellipse.radiusX, @@ -192,24 +248,23 @@ function DrawingLayer(props: DrawingLayerProps) { const createFreeLine = useCallback( (line: Line) => { executeAction( - new CreateDrawingAction([ - { - id: uuid.v4(), - layerId: getSelectedLayerId() ?? -1, - rotation: 0, - addDate: timelineDate, - shapeType: DrawingShapeType.FreeLine, - scaleX: 1, - scaleY: 1, - x: Math.round(line.x), - y: Math.round(line.y), - fillEnabled: line.fillEnabled, - color: line.color, - strokeWidth: line.strokeWidth, - properties: { - points: line.points, - }, - }, + new CreateDrawingAction([{ + id: uuid.v4(), + layerId: getSelectedLayerId() ?? -1, + rotation: 0, + addDate: timelineDate, + type: 'freeLine', + scaleX: 1, + scaleY: 1, + x: Math.round(line.x), + y: Math.round(line.y), + fillPattern: line.fillPattern, + color: line.color, + strokeWidth: line.strokeWidth, + properties: { + points: line.points, + } + } ]), ); }, @@ -230,7 +285,7 @@ function DrawingLayer(props: DrawingLayerProps) { scaleY: 1, x: 0, y: 0, - fillEnabled: line.fillEnabled, + fillPattern: line.fillPattern, color: line.color, strokeWidth: line.strokeWidth, properties: { @@ -243,6 +298,56 @@ function DrawingLayer(props: DrawingLayerProps) { [executeAction, getSelectedLayerId, timelineDate], ); + const createTextLabel = useCallback( + (text: Text) => { + executeAction( + new CreateDrawingAction({ + id: uuid.v4(), + layerId: getSelectedLayerId() ?? -1, + rotation: 0, + addDate: timelineDate, + type: 'text', + scaleX: text.scaleX, + scaleY: text.scaleY, + x: text.x, + y: text.y, + fillPattern: 'none', + color: text.color, + strokeWidth: 0, + properties: { + text: text.text, + }, + }), + ); + }, + [executeAction, getSelectedLayerId, timelineDate], + ); + + const createImage = useCallback( + (image: Image) => { + executeAction( + new CreateDrawingAction({ + id: uuid.v4(), + layerId: getSelectedLayerId() ?? -1, + rotation: 0, + addDate: timelineDate, + type: 'image', + scaleX: image.scaleX, + scaleY: image.scaleY, + x: image.x, + y: image.y, + fillPattern: 'none', + color: '', + strokeWidth: 0, + properties: { + path: image.path, + }, + }), + ); + }, + [executeAction, getSelectedLayerId, timelineDate], + ); + const removeDrawingFromSelection = (e: KonvaEventObject) => { const selectedDrawings = (foundDrawings: DrawingDto[], konvaNode: Konva.Node) => { const drawingNode = konvaNode.getAttr('object'); @@ -300,7 +405,7 @@ function DrawingLayer(props: DrawingLayerProps) { isDrawing.current = true; setNewLine({ strokeWidth: selectedStrokeWidth, - fillEnabled: fillEnabled, + fillPattern: fillPattern, color: selectedColor, x: pos.x, y: pos.y, @@ -310,7 +415,7 @@ function DrawingLayer(props: DrawingLayerProps) { isDrawing.current = true; setPreviewRectangle({ color: selectedColor, - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, x1: pos.x, y1: pos.y, @@ -321,7 +426,7 @@ function DrawingLayer(props: DrawingLayerProps) { isDrawing.current = true; setPreviewEllipse({ color: selectedColor, - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, x: pos.x, y: pos.y, @@ -332,13 +437,32 @@ function DrawingLayer(props: DrawingLayerProps) { if (!newBezierLine) { setNewBezierLine({ color: selectedColor, - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, x: pos.x, y: pos.y, points: [], }); } + } else if (selectedShape == 'text') { + if (newLabelText && newLabelText.text.length > 0) { + createTextLabel(newLabelText); + setNewLabelText(undefined); + } else { + setNewLabelText({ + color: selectedColor, + x: pos.x, + y: pos.y, + scaleX: 1 / (mapScale?.x || 1), + scaleY: 1 / (mapScale?.y || 1), + text: '', + }); + } + } else if (selectedShape == 'image') { + if (newImage) { + createImage(newImage); + setNewImage(undefined); + } } }, [ @@ -346,9 +470,15 @@ function DrawingLayer(props: DrawingLayerProps) { isDrawingLayerActive, selectedShape, selectedStrokeWidth, + fillPattern, selectedColor, - fillEnabled, newBezierLine, + newLabelText, + createTextLabel, + mapScale?.x, + mapScale?.y, + newImage, + createImage, ], ); @@ -363,7 +493,7 @@ function DrawingLayer(props: DrawingLayerProps) { const handleDrawLine = useCallback( (point: Vector2d) => { - if (!newLine) return; + if (!newLine || !isDrawing.current) return; const flatPoints = newLine.points.flat(); @@ -384,9 +514,9 @@ function DrawingLayer(props: DrawingLayerProps) { const handleDrawRectangle = useCallback( (point: Vector2d) => { - if (!previewRectangle) return; + if (!previewRectangle || !isDrawing.current) return; setPreviewRectangle({ - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, color: selectedColor, x1: previewRectangle?.x1, @@ -395,15 +525,15 @@ function DrawingLayer(props: DrawingLayerProps) { y2: point.y, }); }, - [fillEnabled, previewRectangle, selectedColor, selectedStrokeWidth], + [fillPattern, previewRectangle, selectedColor, selectedStrokeWidth], ); const handleDrawSquare = useCallback( (point: Vector2d) => { - if (!previewRectangle) return; + if (!previewRectangle || !isDrawing.current) return; setPreviewRectangle({ - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, color: selectedColor, x1: previewRectangle?.x1, @@ -412,14 +542,14 @@ function DrawingLayer(props: DrawingLayerProps) { y2: previewRectangle?.y1 + (point.x - previewRectangle?.x1), }); }, - [fillEnabled, previewRectangle, selectedColor, selectedStrokeWidth], + [fillPattern, previewRectangle, selectedColor, selectedStrokeWidth], ); const handleDrawEllipse = useCallback( (point: Vector2d) => { - if (!previewEllipse) return; + if (!previewEllipse || !isDrawing.current) return; setPreviewEllipse({ - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, color: selectedColor, x: previewEllipse.x, @@ -428,16 +558,16 @@ function DrawingLayer(props: DrawingLayerProps) { radiusY: Math.abs(point.y - previewEllipse.y), }); }, - [fillEnabled, previewEllipse, selectedColor, selectedStrokeWidth], + [fillPattern, previewEllipse, selectedColor, selectedStrokeWidth], ); const handleDrawCircle = useCallback( (point: Vector2d) => { - if (!previewEllipse) return; + if (!previewEllipse || !isDrawing.current) return; const radius = Math.abs(point.x - previewEllipse.x); setPreviewEllipse({ - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, color: selectedColor, x: previewEllipse.x, @@ -446,12 +576,29 @@ function DrawingLayer(props: DrawingLayerProps) { radiusY: radius, }); }, - [fillEnabled, previewEllipse, selectedColor, selectedStrokeWidth], + [fillPattern, previewEllipse, selectedColor, selectedStrokeWidth], + ); + + const handleDrawImage = useCallback( + (point: Vector2d) => { + if (!selectedIcon) return; + + const newImage: Image = { + path: selectedIcon, + scaleX: 1, + scaleY: 1, + x: point.x, + y: point.y, + }; + + setNewImage(newImage); + }, + [selectedIcon], ); const handleMouseMove = useCallback( (e: KonvaEventObject) => { - if (!isDrawingLayerActive || !isDrawing.current) { + if (!isDrawingLayerActive) { return; } @@ -479,11 +626,15 @@ function DrawingLayer(props: DrawingLayerProps) { handleDrawEllipse(point); } break; + case 'image': + handleDrawImage(point); + break; } }, [ handleDrawCircle, handleDrawEllipse, + handleDrawImage, handleDrawLine, handleDrawRectangle, handleDrawSquare, @@ -503,7 +654,7 @@ function DrawingLayer(props: DrawingLayerProps) { const newRect: Rectangle = { color: selectedColor, - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, x1: Math.min(previewRectangle.x1, previewRectangle.x2), x2: Math.max(previewRectangle.x1, previewRectangle.x2), @@ -513,13 +664,13 @@ function DrawingLayer(props: DrawingLayerProps) { setPreviewRectangle(undefined); createRectangle(newRect); - }, [createRectangle, fillEnabled, previewRectangle, selectedColor, selectedStrokeWidth]); + }, [createRectangle, fillPattern, previewRectangle, selectedColor, selectedStrokeWidth]); const handleEllipseMouseUp = useCallback(() => { if (!previewEllipse) return; const newEllipse: Ellipse = { - fillEnabled: fillEnabled, + fillPattern: fillPattern, strokeWidth: selectedStrokeWidth, color: selectedColor, x: previewEllipse.x, @@ -530,7 +681,7 @@ function DrawingLayer(props: DrawingLayerProps) { setPreviewEllipse(undefined); createEllipse(newEllipse); - }, [createEllipse, fillEnabled, previewEllipse, selectedColor, selectedStrokeWidth]); + }, [createEllipse, fillPattern, previewEllipse, selectedColor, selectedStrokeWidth]); const handleMouseUp = useCallback(() => { if (!isDrawingLayerActive) return; @@ -638,6 +789,9 @@ function DrawingLayer(props: DrawingLayerProps) { ); useEffect(() => { + if (selectedShape != 'image' && newImage) { + setNewImage(undefined); + } if (selectedShape != DrawingShapeType.BezierPolygon && newBezierLine) { createBezierLine(newBezierLine); setNewBezierLine(undefined); @@ -685,7 +839,6 @@ function DrawingLayer(props: DrawingLayerProps) { }; const transformerRef = useRef(null); - return ( <> @@ -701,7 +854,7 @@ function DrawingLayer(props: DrawingLayerProps) { strokeWidth={bezierLine.strokeWidth} onLineClick={handleShapeClicked} color={bezierLine.color} - fillEnabled={bezierLine.fillEnabled} + fillPattern={bezierLine.fillPattern} x={bezierLine.x} y={bezierLine.y} scaleX={bezierLine.scaleX} @@ -740,9 +893,13 @@ function DrawingLayer(props: DrawingLayerProps) { scaleY={line.scaleY} points={line.properties.points.flat()} stroke={line.color} - fill={line.color} - fillEnabled={line.fillEnabled} - closed={line.fillEnabled} + fillPattern={line.fillPattern} + fill={line.fillPattern == 'fill' ? line.color : undefined} + fillPatternScaleX={fillPatternScaleX} + fillPatternScaleY={fillPatternScaleY} + fillPatternImage={getFillPattern(line.fillPattern, line.color)} + fillPatternRepeat="repeat" + closed={line.fillPattern !== 'none'} strokeWidth={line.strokeWidth} tension={0.1} lineCap="round" @@ -791,8 +948,11 @@ function DrawingLayer(props: DrawingLayerProps) { width={rectangle.properties.width} height={rectangle.properties.height} stroke={rectangle.color} - fill={rectangle.color} - fillEnabled={rectangle.fillEnabled} + fill={rectangle.fillPattern == 'fill' ? rectangle.color : undefined} + fillPatternScaleX={fillPatternScaleX} + fillPatternScaleY={fillPatternScaleY} + fillPatternImage={getFillPattern(rectangle.fillPattern, rectangle.color)} + fillPatternRepeat="repeat" strokeWidth={rectangle.strokeWidth} scaleX={rectangle.scaleX} scaleY={rectangle.scaleY} @@ -812,8 +972,11 @@ function DrawingLayer(props: DrawingLayerProps) { height={Math.abs(previewRectangle.y1 - previewRectangle.y2)} stroke={previewRectangle.color} strokeWidth={previewRectangle.strokeWidth} - fill={previewRectangle.color} - fillEnabled={previewRectangle.fillEnabled} + fill={previewRectangle.fillPattern == 'fill' ? previewRectangle.color : undefined} + fillPatternImage={getFillPattern(previewRectangle.fillPattern, previewRectangle.color)} + fillPatternScaleX={fillPatternScaleX} + fillPatternScaleY={fillPatternScaleY} + fillPatternRepeat="repeat" > )} @@ -832,9 +995,13 @@ function DrawingLayer(props: DrawingLayerProps) { scaleY={ellipse.scaleY} radiusX={ellipse.properties.radiusX} radiusY={ellipse.properties.radiusY} + fill={ellipse.fillPattern == 'fill' ? ellipse.color : undefined} + fillPatternScaleX={fillPatternScaleX} + fillPatternScaleY={fillPatternScaleY} + fillPatternImage={getFillPattern(ellipse.fillPattern, ellipse.color)} + fillPatternRepeat="repeat" stroke={ellipse.color} - fill={ellipse.color} - fillEnabled={ellipse.fillEnabled} + fillPattern={ellipse.fillPattern} strokeWidth={ellipse.strokeWidth} onClick={handleShapeClicked} draggable @@ -851,10 +1018,79 @@ function DrawingLayer(props: DrawingLayerProps) { radiusY={previewEllipse.radiusY} stroke={previewEllipse.color} strokeWidth={previewEllipse.strokeWidth} - fill={previewEllipse.color} - fillEnabled={previewEllipse.fillEnabled} + fill={previewEllipse.fillPattern == 'fill' ? previewEllipse.color : undefined} + fillPatternScaleX={fillPatternScaleX} + fillPatternScaleY={fillPatternScaleY} + fillPatternImage={getFillPattern(previewEllipse.fillPattern, previewEllipse.color)} + fillPatternRepeat="repeat" + fillPattern={previewEllipse.fillPattern} + /> + )} + + {textLabels.map((text, i) => ( + + ))} + + {newLabelText && ( + { + setNewLabelText({ ...newLabelText, text: text }); + }} + onClick={handleShapeClicked} + > + )} + + {newImage && ( + )} + + {images.map((image, i) => ( + + ))} ); diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerFillPatterns.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerFillPatterns.tsx new file mode 100644 index 000000000..d56b1c845 --- /dev/null +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerFillPatterns.tsx @@ -0,0 +1,70 @@ +import { useTranslation } from 'react-i18next'; +import IconButton from '@/components/Button/IconButton'; +import NoFillIcon from '@/svg/icons/forbid.svg?react'; +import SquareFilled from '@/svg/icons/square-filled.svg?react'; +import TextureIcon from '@/svg/icons/texture.svg?react'; +import CrosshatchIcon from '@/svg/icons/track.svg?react'; +import { FillPatternType } from './types'; + +interface DrawingLayerFillPatternProps { + selectedPattern: FillPatternType; + onChange: (pattern: FillPatternType) => void; +} + +export function DrawingLayerFillPatterns({ + onChange, + selectedPattern, +}: DrawingLayerFillPatternProps) { + const { t } = useTranslation(['common', 'drawings']); + + return ( + <> +
+ {t('drawings:fillPattern')} +
+ { + onChange('none'); + }} + title={t('drawings:draw_free_line_tooltip')} + > + + + { + onChange('hatch'); + }} + title={t('drawings:draw_free_line_tooltip')} + > + + + { + onChange('crosshatch'); + }} + title={t('drawings:draw_rectangle_tooltip')} + > + + + + { + onChange('fill'); + }} + title={t('drawings:draw_ellipse_tooltip')} + > + + +
+
+ + ); +} diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerIconToolForm.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerIconToolForm.tsx new file mode 100644 index 000000000..550782ebb --- /dev/null +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerIconToolForm.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FileStat } from 'webdav'; +import SimpleButton from '@/components/Button/SimpleButton'; +import useMapStore from '@/features/map_planning/store/MapStore'; +import FileSelectorModal from '@/features/nextcloud_integration/components/FileSelectorModal'; +import { DrawingLayerStatusPanelContent } from './DrawingLayerStatusPanelContent'; + +export function DrawingLayerIconToolForm() { + const { t } = useTranslation(['common', 'drawings']); + + const [showFileSelector, setShowFileSelector] = useState(false); + const setStatusPanelContent = useMapStore((state) => state.setStatusPanelContent); + + const drawingLayerActivateDrawingMode = useMapStore( + (state) => state.drawingLayerActivateDrawingMode, + ); + + const drawingLayerSetSelectedIconPath = useMapStore( + (state) => state.drawingLayerSetSelectedIconPath, + ); + + const handleSelectImage = (item: FileStat) => { + const path = '/Photos/' + item.basename; + drawingLayerActivateDrawingMode('image'); + drawingLayerSetSelectedIconPath(path); + setStatusPanelContent( + , + ); + setShowFileSelector(false); + }; + + return ( + <> +
+
+ + setShowFileSelector(true)}> + {t('drawings:select_image')} + +
+
+ + ); +} diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerLabelToolForm.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerLabelToolForm.tsx new file mode 100644 index 000000000..f153450ae --- /dev/null +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerLabelToolForm.tsx @@ -0,0 +1,84 @@ +import { ReactElement, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import IconButton from '@/components/Button/IconButton'; +import SimpleFormInput from '@/components/Form/SimpleFormInput'; +import useMapStore from '@/features/map_planning/store/MapStore'; +import useDebounceEffect from '@/hooks/useDebounceEffect'; +import TextIcon from '@/svg/icons/text.svg?react'; +import { useTransformerStore } from '../../store/transformer/TransformerStore'; +import { DrawingLayerStatusPanelContent } from './DrawingLayerStatusPanelContent'; +import { DrawingShapeType } from './types'; + +export function DrawingLayerLabelToolForm() { + const { t } = useTranslation(['common', 'drawings']); + + const drawingLayerActivateDrawingMode = useMapStore( + (state) => state.drawingLayerActivateDrawingMode, + ); + + const transformerActions = useTransformerStore().actions; + const selectedShape = useMapStore((state) => state.untrackedState.layers.drawing.shape); + const setStatusPanelContent = useMapStore((state) => state.setStatusPanelContent); + + const activateDrawingMode = (shape: DrawingShapeType) => { + drawingLayerActivateDrawingMode(shape); + transformerActions.clearSelection(); + }; + + return ( + <> +
+
+ { + activateDrawingMode('text'); + setStatusPanelContent( + , + ); + }} + title={t('drawings:draw_bezier_polygon_tooltip')} + > + + +
+
+ +
+ + + + ); +} + +function LabelTestPropertyForm(): ReactElement { + const { t } = useTranslation(['drawings']); + + const setSelectedColor = useMapStore((state) => state.drawingLayerSetSelectedColor); + + const [pickerColor, setPickerColor] = useState('#000000'); + + useDebounceEffect( + () => { + setSelectedColor(pickerColor); + }, + 100, + [pickerColor], + ); + + return ( + <> +
+ setPickerColor(e.target.value)} + value={pickerColor} + /> +
+ + ); +} diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerLeftToolbar.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerLeftToolbar.tsx index e0214f44a..e377944aa 100644 --- a/frontend/src/features/map_planning/layers/drawing/DrawingLayerLeftToolbar.tsx +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerLeftToolbar.tsx @@ -81,11 +81,11 @@ export function DrawingLayerLeftToolbar() { executeAction(new UpdateDrawingAction(updatedDrawings)); }; - const onFillEnabledChange = ({ fillEnabled }: DrawingFormData) => { + const onFillPatternChange = ({ fillPattern }: DrawingFormData) => { if (!selectedDrawings?.length) return; const hasChanged = selectedDrawings.some( - (selectedDrawing) => selectedDrawing.fillEnabled !== fillEnabled, + (selectedDrawing) => selectedDrawing.fillPattern !== fillPattern, ); if (!hasChanged) return; @@ -112,7 +112,7 @@ export function DrawingLayerLeftToolbar() { onRemoveDateChange={onRemoveDateChange} onColorChange={onColorChange} onStrokeWidthChange={onStrokeWidthChange} - onFillEnabledChange={onFillEnabledChange} + onFillPatternChange={onFillPatternChange} onDeleteClick={onDeleteClick} isReadOnlyMode={isReadOnlyMode} /> @@ -123,7 +123,7 @@ export function DrawingLayerLeftToolbar() { onRemoveDateChange={onRemoveDateChange} onColorChange={onColorChange} onStrokeWidthChange={onStrokeWidthChange} - onFillEnabledChange={onFillEnabledChange} + onFillPatternChange={onFillPatternChange} onDeleteClick={onDeleteClick} isReadOnlyMode={isReadOnlyMode} /> diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerRightToolbar.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerRightToolbar.tsx index ea2d50abe..b4289e9ce 100644 --- a/frontend/src/features/map_planning/layers/drawing/DrawingLayerRightToolbar.tsx +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerRightToolbar.tsx @@ -1,9 +1,36 @@ -import { DrawingLayerToolForm } from './DrawingLayerToolForm'; +import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; +import useMapStore from '../../store/MapStore'; +import { DrawingLayerIconToolForm } from './DrawingLayerIconToolForm'; +import { DrawingLayerLabelToolForm } from './DrawingLayerLabelToolForm'; +import { DrawingLayerShapesToolForm } from './DrawingLayerShapesToolForm'; +import 'react-tabs/style/react-tabs.css'; export const DrawingLayerRightToolbar = () => { + const drawingLayerClearSelectedShape = useMapStore( + (state) => state.drawingLayerClearSelectedShape, + ); + return (
- + + + Shapes + Label + Brushes + Icons + + + + + + + + + + + + +
); }; diff --git a/frontend/src/features/map_planning/layers/drawing/DrawingLayerToolForm.tsx b/frontend/src/features/map_planning/layers/drawing/DrawingLayerShapesToolForm.tsx similarity index 91% rename from frontend/src/features/map_planning/layers/drawing/DrawingLayerToolForm.tsx rename to frontend/src/features/map_planning/layers/drawing/DrawingLayerShapesToolForm.tsx index 06cbeab6f..dd2e870d7 100644 --- a/frontend/src/features/map_planning/layers/drawing/DrawingLayerToolForm.tsx +++ b/frontend/src/features/map_planning/layers/drawing/DrawingLayerShapesToolForm.tsx @@ -10,9 +10,10 @@ import PolygonIcon from '@/svg/icons/polygon.svg?react'; import RectangleIcon from '@/svg/icons/rectangle.svg?react'; import LineIcon from '@/svg/icons/wavy-line.svg?react'; import { useTransformerStore } from '../../store/transformer/TransformerStore'; +import { DrawingLayerFillPatterns } from './DrawingLayerFillPatterns'; import { DrawingLayerStatusPanelContent } from './DrawingLayerStatusPanelContent'; -export function DrawingLayerToolForm() { +export function DrawingLayerShapesToolForm() { const { t } = useTranslation(['common', 'drawings']); const drawingLayerActivateDrawingMode = useMapStore( @@ -31,7 +32,6 @@ export function DrawingLayerToolForm() { return ( <>
-

{t('drawings:tools_title')}

state.drawingLayerSetSelectedColor); - const setFill = useMapStore((state) => state.drawingLayerSetFillEnabled); + const setFillPattern = useMapStore((state) => state.drawingLayerSetFillPattern); const setSelectedStrokeWidth = useMapStore((state) => state.drawingLayerSetSelectedStrokeWidth); const selectedStrokeWidth = useMapStore( (state) => state.untrackedState.layers.drawing.selectedStrokeWidth, ); - const fill = useMapStore((state) => state.untrackedState.layers.drawing.fillEnabled); + const fill = useMapStore((state) => state.untrackedState.layers.drawing.fillPattern); const [pickerColor, setPickerColor] = useState('#000000'); @@ -140,14 +140,11 @@ function ShapePropertyForm(props: { selectedShape: DrawingShapeType | null }): R onChange={(e) => setSelectedStrokeWidth(+e.target.value)} value={selectedStrokeWidth} /> - setFill(e.target.checked)} - checked={fill} - /> + +
)} diff --git a/frontend/src/features/map_planning/layers/drawing/labels/EditableText.tsx b/frontend/src/features/map_planning/layers/drawing/labels/EditableText.tsx new file mode 100644 index 000000000..52cda30d7 --- /dev/null +++ b/frontend/src/features/map_planning/layers/drawing/labels/EditableText.tsx @@ -0,0 +1,85 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React from 'react'; +import { Text } from 'react-konva'; +import { DrawingDto } from '../types'; +import { EditableTextInput } from './EditableTextinput'; + +type EditableTextProps = { + id?: string; + x: number; + y: number; + scaleX: number; + scaleY: number; + isEditing: boolean; + onToggleEdit: (e: KonvaEventObject) => void; + onEndEdit?: (e: React.KeyboardEvent) => void; + onClick: (e: KonvaEventObject) => void; + onChange: (text: string) => void; + text: string; + width: number; + height: number; + color?: string; + object?: DrawingDto; +}; + +export function EditableText({ + id, + x, + y, + isEditing, + onToggleEdit, + onChange, + onEndEdit, + onClick, + text, + width, + height, + scaleX, + scaleY, + color, + object, +}: EditableTextProps) { + function handleTextChange(e: React.ChangeEvent) { + onChange(e.currentTarget.value); + } + + if (isEditing) { + return ( + { + if (e.key === 'Enter' && onEndEdit) { + onEndEdit(e); + } + }} + /> + ); + } + return ( + + ); +} diff --git a/frontend/src/features/map_planning/layers/drawing/labels/EditableTextinput.tsx b/frontend/src/features/map_planning/layers/drawing/labels/EditableTextinput.tsx new file mode 100644 index 000000000..e39ff3b0c --- /dev/null +++ b/frontend/src/features/map_planning/layers/drawing/labels/EditableTextinput.tsx @@ -0,0 +1,72 @@ +import { useEffect, useRef } from 'react'; +import { Html } from 'react-konva-utils'; + +function getStyle(width: number, height: number) { + console.log(width, height); + const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + const baseStyle = { + width: '100%', + height: '100%', + border: '1px solid black', + padding: '0px', + margin: '0px', + background: 'none', + outline: 'none', + colour: 'black', + fontSize: '24px', + fontFamily: 'sans-serif', + }; + if (isFirefox) { + return baseStyle; + } + return { + ...baseStyle, + margintop: '-4px', + }; +} + +type EditableTextInputProps = { + x: number; + y: number; + scaleX: number; + scaleY: number; + width: number; + height: number; + value: string; + onChange: (e: React.ChangeEvent) => void; + onKeyDown: (e: React.KeyboardEvent) => void; +}; + +export function EditableTextInput({ + x, + y, + scaleX, + scaleY, + value, + onChange, + onKeyDown, + width, + height, +}: EditableTextInputProps) { + const inputRef = useRef(null); + + const style = getStyle(width, height); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + +