diff --git a/.vscode/settings.json b/.vscode/settings.json index 2a15253..56f3e77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "*.css": "tailwindcss" }, "cSpell.words": [ - "heroicons" + "heroicons", + "wailsjs" ] } \ No newline at end of file diff --git a/frontend/components/DocumentCanvas/AreaCanvas.tsx b/frontend/components/DocumentCanvas/AreaCanvas.tsx new file mode 100644 index 0000000..da0b65a --- /dev/null +++ b/frontend/components/DocumentCanvas/AreaCanvas.tsx @@ -0,0 +1,69 @@ +'use client' + +import React, { useEffect, useRef } from 'react' +import { useProject } from '../../context/Project/provider' + +type Props = { + width: number, + height: number + zoomLevel: number +} + + +const AreaCanvas = (props: Props) => { + const { getSelectedDocument, selectedAreaId, } = useProject() + const canvas = useRef(null) + + const areas = getSelectedDocument()?.areas + const { width, height, zoomLevel } = props + + + const applyAreasToCanvas = (zoomLevel: number) => { + if (!areas || !areas.length) return + + const canvasContext = canvas.current!.getContext('2d')! + + areas.forEach(a => { + canvasContext.beginPath() + if (a.id !== selectedAreaId) { + canvasContext.setLineDash([4]) + canvasContext.lineWidth = 2 + canvasContext.strokeStyle = '#010101' + } else { + canvasContext.setLineDash([]) + canvasContext.lineWidth = 3 + canvasContext.strokeStyle = '#dc8dec' + } + const width = (a.endX - a.startX) * zoomLevel + const height = (a.endY - a.startY) * zoomLevel + const x = a.startX * zoomLevel + const y = a.startY * zoomLevel + canvasContext.roundRect(x, y, width, height, 4) + canvasContext.stroke() + canvasContext.closePath() + }) + } + + const clearCanvas = () => { + const canvasInstance = canvas.current! + const context = canvasInstance.getContext('2d')! + context.clearRect(0, 0, canvasInstance.width, canvasInstance.height) + } + + const updateSize = (size: { width: number, height: number }) => { + const canvasInstance = canvas.current! + const { width, height } = size + canvasInstance.width = width + canvasInstance.height = height + } + + useEffect(() => { + clearCanvas() + updateSize({ width, height }) + applyAreasToCanvas(zoomLevel) + }, [width, height, zoomLevel, areas]) + + return +} + +export default AreaCanvas diff --git a/frontend/components/DocumentCanvas/AreaTextPreview.tsx b/frontend/components/DocumentCanvas/AreaTextPreview.tsx new file mode 100644 index 0000000..0aaa9ea --- /dev/null +++ b/frontend/components/DocumentCanvas/AreaTextPreview.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { ipc } from '../../wailsjs/wailsjs/go/models' +import classNames from '../../utils/classNames' + +type Props = { + areas: ipc.Area[] + processedArea?: ipc.ProcessedArea + zoomLevel: number + setWordToEdit: (props: { word: ipc.ProcessedWord, areaId: string }) => void +} + +const AreaTextPreview = ({ areas, processedArea, zoomLevel, setWordToEdit }: Props) => { + if (!processedArea) return <> + + + return
+ { + processedArea.lines?.map(l => l.words).flat().map((w, i) => { + const width = Math.floor((w.boundingBox.x1 - w.boundingBox.x0) * zoomLevel) + 2 + const height = Math.floor((w.boundingBox.y1 - w.boundingBox.y0) * zoomLevel) + 2 + return setWordToEdit({ word: w, areaId: processedArea.id })}> + {w.fullText} + + }) + } +
+} + +export default AreaTextPreview diff --git a/frontend/components/DocumentCanvas/EditProcessedWord.tsx b/frontend/components/DocumentCanvas/EditProcessedWord.tsx new file mode 100644 index 0000000..c3ee198 --- /dev/null +++ b/frontend/components/DocumentCanvas/EditProcessedWord.tsx @@ -0,0 +1,82 @@ +import React, { useRef } from 'react' +import { ipc } from '../../wailsjs/wailsjs/go/models' +import classNames from '../../utils/classNames' +import onEnterHandler from '../../utils/onEnterHandler' +import { useProject } from '../../context/Project/provider' + +type Props = { + zoomLevel: number + processedArea?: ipc.ProcessedArea + wordToEdit?: ipc.ProcessedWord + setWordToEdit: (props?: { word: ipc.ProcessedWord, areaId: string }) => void + setHoveredProcessedArea: (area?: ipc.ProcessedArea) => void +} + +const EditProcessedWord = ({ setWordToEdit, zoomLevel, wordToEdit, processedArea, setHoveredProcessedArea }: Props) => { + const { + requestUpdateProcessedWordById, + getProcessedAreaById, + } = useProject() + const editWordInput = useRef(null) + + + if (!wordToEdit || !processedArea) return <> + + const width = Math.floor((wordToEdit.boundingBox.x1 - wordToEdit.boundingBox.x0) * zoomLevel) + 2 + const height = Math.floor(((wordToEdit.boundingBox.y1 - wordToEdit.boundingBox.y0) * zoomLevel) * 2) + 4 + + const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => { + requestUpdateProcessedWordById(wordId, newWordValue) + .then(res => { + getProcessedAreaById(processedArea.id || '').then(response => { + setHoveredProcessedArea(response) + }) + }) + .catch(console.error) + setWordToEdit(undefined) + } + + return
setWordToEdit(undefined)} + > +
+ {wordToEdit.fullText} +
+ + e.currentTarget.select()} + onBlur={(e) => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value)} + onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value))} + /> +
+} + +export default EditProcessedWord \ No newline at end of file diff --git a/frontend/components/DocumentCanvas/ImageCanvas.tsx b/frontend/components/DocumentCanvas/ImageCanvas.tsx new file mode 100644 index 0000000..685995b --- /dev/null +++ b/frontend/components/DocumentCanvas/ImageCanvas.tsx @@ -0,0 +1,55 @@ +'use client' + +import React, { useEffect, useRef } from 'react' +import loadImage from '../../useCases/loadImage' + +type Props = { + zoomLevel: number, + imagePath?: string, + setSize: (size: { width: number, height: number }) => void +} + +const ImageCanvas = (props: Props) => { + const canvas = useRef(null) + const { imagePath, zoomLevel, setSize } = props + + const applyImageToCanvas = async (path: string) => { + const canvasContext = canvas.current!.getContext('2d')! + + let image: HTMLImageElement + try { + image = await loadImage(path) + } catch (err) { + return + } + + const width = image.naturalWidth * zoomLevel + const height = image.naturalHeight * zoomLevel + + updateSize({ width, height }) + + canvasContext.drawImage(image, 0, 0, width, height) + } + + const clearCanvas = () => { + const canvasInstance = canvas.current! + const context = canvasInstance.getContext('2d')! + context.clearRect(0, 0, canvasInstance.width, canvasInstance.height) + } + + const updateSize = (size: { width: number, height: number }) => { + const canvasInstance = canvas.current! + const { width, height } = size + canvasInstance.width = width + canvasInstance.height = height + setSize(size) + } + + useEffect(() => { + if (imagePath) applyImageToCanvas(imagePath) + }, [imagePath, zoomLevel]) + + return +} + +export default ImageCanvas diff --git a/frontend/components/DocumentCanvas/UiCanvas.tsx b/frontend/components/DocumentCanvas/UiCanvas.tsx new file mode 100644 index 0000000..b97b448 --- /dev/null +++ b/frontend/components/DocumentCanvas/UiCanvas.tsx @@ -0,0 +1,186 @@ +'use client' + +import React, { WheelEvent, useEffect, useRef, useState } from 'react' +import { useProject } from '../../context/Project/provider' +import { ipc } from '../../wailsjs/wailsjs/go/models' +import createUiCanvasInteractions from './createUiCanvasInteractions' +import processImageArea from '../../useCases/processImageArea' +import AreaTextPreview from './AreaTextPreview' +import EditProcessedWord from './EditProcessedWord' + +type Props = { + width: number, + height: number + zoomDetails: { currentZoomLevel: number, zoomStep: number, maxZoomLevel: number } + setZoomLevel: (value: number) => void +} + +let interactions: ReturnType | null = null + +let downClickX = 0 +let downClickY = 0 +let isDrawing = false + +const UiCanvas = (props: Props) => { + const { + getSelectedDocument, + getProcessedAreaById, + requestAddArea, + setSelectedAreaId, + } = useProject() + const canvas = useRef(null) + const [hoverOverAreaId, setHoverOverAreaId] = useState('') + const [wordToEdit, setWordToEdit] = useState<{ word: ipc.ProcessedWord, areaId: string } | undefined>() + const [hoveredProcessedArea, setHoveredProcessedArea] = useState() + + const areas = getSelectedDocument()?.areas || [] + const { width, height, zoomDetails, setZoomLevel } = props + const { currentZoomLevel } = zoomDetails + + const applyUiCanvasUpdates = () => { + const canvasContext = canvas.current!.getContext('2d')! + + if (!areas || !areas.length) return + + const hoverArea = areas.find(a => a.id === hoverOverAreaId) + if (!hoverArea) return + + canvasContext.beginPath() + canvasContext.setLineDash([]) + canvasContext.lineWidth = 6 + canvasContext.strokeStyle = '#dc8dec' + const width = (hoverArea.endX - hoverArea.startX) * currentZoomLevel + const height = (hoverArea.endY - hoverArea.startY) * currentZoomLevel + const x = hoverArea.startX * currentZoomLevel + const y = hoverArea.startY * currentZoomLevel + canvasContext.roundRect(x, y, width, height, 4) + canvasContext.stroke() + canvasContext.closePath() + } + + const clearCanvas = () => { + const canvasInstance = canvas.current! + const context = canvasInstance.getContext('2d')! + context.clearRect(0, 0, canvasInstance.width, canvasInstance.height) + } + + const handleMouseDown = (e: React.MouseEvent) => { + if (e.nativeEvent.shiftKey) { + downClickX = e.nativeEvent.offsetX + downClickY = e.nativeEvent.offsetY + isDrawing = true + } + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (isDrawing) interactions?.onActivelyDrawArea({ + startX: downClickX, + startY: downClickY, + endX: e.nativeEvent.offsetX, + endY: e.nativeEvent.offsetY, + }) + else interactions?.onHoverOverArea( + e.clientX, + e.clientY, + currentZoomLevel, + areas, + (areaId) => { + if (areaId === hoverOverAreaId) return + + setHoverOverAreaId(areaId || '') + getProcessedAreaById(areaId || '').then(response => { + setHoveredProcessedArea(response) + }) + } + ) + } + + const handleMouseUp = async (e: React.MouseEvent) => { + if (isDrawing) { + const coordinates = { + startMouseX: downClickX, + startMouseY: downClickY, + endMouseX: e.nativeEvent.offsetX, + endMouseY: e.nativeEvent.offsetY, + } + interactions?.onFinishDrawArea(coordinates, currentZoomLevel, + async (startX, startY, endX, endY) => { + const canvasInstance = canvas.current + if (!canvasInstance) return + + const selectedDocumentId = getSelectedDocument()?.id + if (selectedDocumentId) { + const addedArea = await requestAddArea(selectedDocumentId, { startX, startY, endX, endY }) + setSelectedAreaId(addedArea.id) + processImageArea(selectedDocumentId, addedArea.id) + } + + const context = canvasInstance.getContext('2d') + context?.clearRect(0, 0, canvasInstance.width, canvasInstance.height) + isDrawing = false + downClickX = 0 + downClickY = 0 + } + ) + } + } + + const handleWheelEvent = (e: WheelEvent) => { + if (e.ctrlKey) interactions?.onZoom(e.deltaY, zoomDetails, setZoomLevel) + } + + const updateSize = (size: { width: number, height: number }) => { + const canvasInstance = canvas.current! + const { width, height } = size + canvasInstance.width = width + canvasInstance.height = height + } + + + useEffect(() => { + if (!interactions && canvas.current) { + interactions = createUiCanvasInteractions(canvas.current) + } + }, [canvas.current]) + + useEffect(() => { + clearCanvas() + updateSize({ width, height }) + applyUiCanvasUpdates() + }, [width, height, currentZoomLevel, areas]) + + + useEffect(() => { + clearCanvas() + applyUiCanvasUpdates() + }, [hoverOverAreaId]) + + + return <> + + + + + + +} + +export default UiCanvas diff --git a/frontend/components/DocumentCanvas/createUiCanvasInteractions.ts b/frontend/components/DocumentCanvas/createUiCanvasInteractions.ts new file mode 100644 index 0000000..a0454f5 --- /dev/null +++ b/frontend/components/DocumentCanvas/createUiCanvasInteractions.ts @@ -0,0 +1,101 @@ +import isInBounds from '../../utils/isInBounds' +import { ipc } from '../../wailsjs/wailsjs/go/models' + + +type MouseCoordinates = { + startMouseX: number, startMouseY: number, endMouseX: number, endMouseY: number +} + +type RectangleCoordinates = { + startX: number, startY: number, endX: number, endY: number +} + +type AddAreaToStoreCallback = + (startX: number, startY: number, endX: number, endY: number) + => Promise + +type SetZoomCallback = (newZoomLevel: number) => void + +type ZoomDetails = { + currentZoomLevel: number, + maxZoomLevel: number, + zoomStep: number +} + +type HoverOverAreaCallback = (areaId?: string) => void + +/** + * @param uiCanvas + * @returns Various methods to be called during events on the `UiCanvas`. + * Dependencies must be injected, such as state change callbacks. + */ +const createUiCanvasInteractions = (uiCanvas: HTMLCanvasElement) => { + const uiCanvasContext = uiCanvas.getContext('2d')! + + return { + onActivelyDrawArea: (coordinates: RectangleCoordinates) => { + const { startX, startY, endX, endY } = coordinates + + uiCanvasContext.clearRect(0, 0, uiCanvas.width, uiCanvas.height) + uiCanvasContext.beginPath() + const width = endX - startX + const height = endY - startY + uiCanvasContext.rect(startX, startY, width, height) + uiCanvasContext.strokeStyle = '#000' + uiCanvasContext.lineWidth = 2 + uiCanvasContext.stroke() + }, + onFinishDrawArea: (coordinates: MouseCoordinates, zoomLevel: number, addAreaToStoreCallback: AddAreaToStoreCallback) => { + let { startMouseX, endMouseX, startMouseY, endMouseY } = coordinates + + let startX: number, endX: number + if (startMouseX < endMouseX) { + startX = Math.floor(startMouseX / zoomLevel) + endX = Math.floor(endMouseX / zoomLevel) + } else { + startX = Math.floor(endMouseX / zoomLevel) + endX = Math.floor(startMouseX / zoomLevel) + } + + let startY: number, endY: number + if (startMouseY < endMouseY) { + startY = Math.floor(startMouseY / zoomLevel) + endY = Math.floor(endMouseY / zoomLevel) + } else { + startY = Math.floor(endMouseY / zoomLevel) + endY = Math.floor(startMouseY / zoomLevel) + } + + addAreaToStoreCallback(startX, startY, endX, endY) + }, + onZoom: (wheelDelta: number, zoomDetails: ZoomDetails, setZoomCallBack: SetZoomCallback) => { + const { currentZoomLevel, maxZoomLevel, zoomStep } = zoomDetails + + const shouldAttemptToZoomIn = (wheelDelta < 0) && currentZoomLevel < maxZoomLevel + if (shouldAttemptToZoomIn) setZoomCallBack(currentZoomLevel + zoomStep) + else if (currentZoomLevel > (zoomStep * 2)) setZoomCallBack(currentZoomLevel - zoomStep) + }, + onHoverOverArea: (mouseX: number, mouseY: number, zoomLevel: number, areas: ipc.Area[], callback: HoverOverAreaCallback) => { + if (!areas.length) return + + const domRect = uiCanvas.getBoundingClientRect() + const x = mouseX - domRect.left + const y = mouseY - domRect.top + const point = { x, y } + + const areaContainingCoords = areas.find(a => { + const bounds = { + startX: a.startX, + startY: a.startY, + endX: a.endX, + endY: a.endY + } + return isInBounds(point, bounds, zoomLevel) + }) + + callback(areaContainingCoords?.id) + }, + } +} + +export default createUiCanvasInteractions diff --git a/frontend/components/DocumentCanvas/index.tsx b/frontend/components/DocumentCanvas/index.tsx new file mode 100644 index 0000000..d7ba515 --- /dev/null +++ b/frontend/components/DocumentCanvas/index.tsx @@ -0,0 +1,58 @@ +'use client' + +import React, { useState } from 'react' +import { useProject, } from '../../context/Project/provider' +import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline' +import classNames from '../../utils/classNames' +import LanguageSelect from '../workspace/LanguageSelect' +import ImageCanvas from './ImageCanvas' +import AreaCanvas from './AreaCanvas' +import UiCanvas from './UiCanvas' + +const zoomStep = 0.025 +const maxZoomLevel = 4 + +const DocumentCanvas = () => { + const { getSelectedDocument } = useProject() + const selectedDocument = getSelectedDocument() + + const [zoomLevel, setZoomLevel] = useState(1) + const [size, setSize] = useState({ width: 0, height: 0 }) + const { width, height } = size + + return
+
+
+

{selectedDocument?.name}

+ +
+
+ + { setZoomLevel(e.currentTarget.valueAsNumber) }} + /> + +
+
+
+ + + + +
+
+} + +export default DocumentCanvas diff --git a/frontend/components/workspace/DocumentRenderer.tsx b/frontend/components/workspace/DocumentRenderer.tsx deleted file mode 100644 index 80326b9..0000000 --- a/frontend/components/workspace/DocumentRenderer.tsx +++ /dev/null @@ -1,412 +0,0 @@ -'use client' - -import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline' -import React, { useEffect, useRef, useState, WheelEvent } from 'react' -import { useProject } from '../../context/Project/provider' -import loadImage from '../../useCases/loadImage' -import processImageArea from '../../useCases/processImageArea' -import classNames from '../../utils/classNames' -import LanguageSelect from './LanguageSelect' -import isInBounds from '../../utils/isInBounds' -import { ipc } from '../../wailsjs/wailsjs/go/models' -import onEnterHandler from '../../utils/onEnterHandler' - -const zoomStep = 0.025 -const maxZoomLevel = 4 - -const DocumentRenderer = () => { - const { - getSelectedDocument, - requestAddArea, - selectedAreaId, - setSelectedAreaId, - getProcessedAreasByDocumentId, - requestUpdateProcessedWordById - } = useProject() - const selectedDocument = getSelectedDocument() - const areas = selectedDocument?.areas - const documentCanvas = useRef(null) - const areaCanvas = useRef(null) - const uiCanvas = useRef(null) - const drawingCanvas = useRef(null) - const editWordInput = useRef(null) - - const [zoomLevel, setZoomLevel] = useState(1) - const [hoverOverAreaId, setHoverOverAreaId] = useState('') - const [hoveredProcessedArea, setHoveredProcessedArea] = useState() - const [wordToEdit, setWordToEdit] = useState<{ word: ipc.ProcessedWord, areaId: string } | undefined>() - - let downClickX = 0 - let downClickY = 0 - let isDrawing = false - - const applyCanvasSizes = (size: { width: number, height: number }) => { - const documentCanvasInstance = documentCanvas.current - if (!documentCanvasInstance) return - documentCanvasInstance.width = size.width - documentCanvasInstance.height = size.height - - const areaCanvasInstance = areaCanvas.current - if (!areaCanvasInstance) return - areaCanvasInstance.width = size.width - areaCanvasInstance.height = size.height - - const uiCanvasInstance = uiCanvas.current - if (!uiCanvasInstance) return - uiCanvasInstance.width = size.width - uiCanvasInstance.height = size.height - - const drawingCanvasInstance = drawingCanvas.current - if (!drawingCanvasInstance) return - drawingCanvasInstance.width = size.width - drawingCanvasInstance.height = size.height - } - - const applyDocumentToCanvas = async (path: string) => { - let image: HTMLImageElement - try { - image = await loadImage(path) - } catch (err) { - return - } - - const width = image.naturalWidth * zoomLevel - const height = image.naturalHeight * zoomLevel - - applyCanvasSizes({ width, height }) - - const documentCanvasInstance = documentCanvas.current - if (!documentCanvasInstance) return - - const context = documentCanvasInstance.getContext('2d') - if (!context) return - context.drawImage(image, 0, 0, width, height) - - if (areas) applyAreasToCanvas() - applyUiCanvasUpdates() - } - - const applyAreasToCanvas = () => { - const areaCanvasInstance = areaCanvas.current - if (!areaCanvasInstance) return - const context = areaCanvasInstance.getContext('2d')! - if (!context) return - - context.clearRect(0, 0, areaCanvasInstance.width, areaCanvasInstance.height) - - if (!areas || !areas.length) return - - areas.forEach(a => { - context.beginPath() - if (a.id !== selectedAreaId) { - context.setLineDash([4]) - context.lineWidth = 2 - context.strokeStyle = '#010101' - } else { - context.setLineDash([]) - context.lineWidth = 3 - context.strokeStyle = '#dc8dec' - } - const width = (a.endX - a.startX) * zoomLevel - const height = (a.endY - a.startY) * zoomLevel - const x = a.startX * zoomLevel - const y = a.startY * zoomLevel - context.roundRect(x, y, width, height, 4) - context.stroke() - context.closePath() - }) - } - - const applyUiCanvasUpdates = () => { - const uiCanvasInstance = uiCanvas.current - if (!uiCanvasInstance) return - const context = uiCanvasInstance.getContext('2d')! - if (!context) return - - context.clearRect(0, 0, uiCanvasInstance.width, uiCanvasInstance.height) - - if (!areas || !areas.length) return - - const hoverArea = areas.find(a => a.id === hoverOverAreaId) - if (!hoverArea) return - - context.beginPath() - context.setLineDash([]) - context.lineWidth = 6 - context.strokeStyle = '#dc8dec' - const width = (hoverArea.endX - hoverArea.startX) * zoomLevel - const height = (hoverArea.endY - hoverArea.startY) * zoomLevel - const x = hoverArea.startX * zoomLevel - const y = hoverArea.startY * zoomLevel - context.roundRect(x, y, width, height, 4) - context.stroke() - context.closePath() - } - - const getProcessedAreaById = async (areaId: string) => { - try { - if (!selectedDocument || !selectedDocument.id || !areaId) return - const processedAreas = await getProcessedAreasByDocumentId(selectedDocument.id) - const foundProcessedArea = processedAreas.find(a => a.id === areaId) - console.log(foundProcessedArea) - return foundProcessedArea - } catch (err) { - console.error(err) - return - } - } - - const handleHoverOverArea = (e: React.MouseEvent) => { - const domRect = e.currentTarget.getBoundingClientRect() - const x = e.clientX - domRect.left - const y = e.clientY - domRect.top - const point = { x, y } - - const areaContainingCoords = areas?.find(a => { - const bounds = { startX: a.startX, startY: a.startY, endX: a.endX, endY: a.endY } - return isInBounds(point, bounds, zoomLevel) - }) - - if (areaContainingCoords?.id === hoverOverAreaId) return - - setHoverOverAreaId(areaContainingCoords?.id || '') - getProcessedAreaById(areaContainingCoords?.id || '').then(response => { - setHoveredProcessedArea(response) - }) - } - - const handleMouseDrawArea = (x: number, y: number) => { - const drawingCanvasInstance = drawingCanvas.current - if (!drawingCanvasInstance) return - const context = drawingCanvasInstance.getContext('2d') - if (!context) return - - context.clearRect(0, 0, drawingCanvasInstance.width, drawingCanvasInstance.height) - context.beginPath() - const width = x - downClickX - const height = y - downClickY - context.rect(downClickX, downClickY, width, height) - context.strokeStyle = '#000' - context.lineWidth = 2 - context.stroke() - } - - const handleMouseDown = (e: React.MouseEvent) => { - console.log(e) - if (e.nativeEvent.shiftKey) { - const drawingCanvasInstance = drawingCanvas.current - if (!drawingCanvasInstance) return - - downClickX = e.nativeEvent.offsetX - downClickY = e.nativeEvent.offsetY - isDrawing = true - } - } - - const handleMouseUp = async (e: React.MouseEvent) => { - if (isDrawing) { - const drawingCanvasInstance = drawingCanvas.current - if (!drawingCanvasInstance) return - - const mouseX = e.nativeEvent.offsetX - const mouseY = e.nativeEvent.offsetY - - let startX: number, endX: number - if (downClickX < mouseX) { - startX = Math.floor(downClickX / zoomLevel) - endX = Math.floor(mouseX / zoomLevel) - } else { - startX = Math.floor(mouseX / zoomLevel) - endX = Math.floor(downClickX / zoomLevel) - } - - let startY: number, endY: number - if (downClickY < mouseY) { - startY = Math.floor(downClickY / zoomLevel) - endY = Math.floor(mouseY / zoomLevel) - } else { - startY = Math.floor(mouseY / zoomLevel) - endY = Math.floor(downClickY / zoomLevel) - } - - if (selectedDocument?.id) { - const addedArea = await requestAddArea(selectedDocument.id, { startX, startY, endX, endY }) - setSelectedAreaId(addedArea.id) - processImageArea(selectedDocument.id, addedArea.id) - } - - const context = drawingCanvasInstance.getContext('2d') - context?.clearRect(0, 0, drawingCanvasInstance.width, drawingCanvasInstance.height) - isDrawing = false - downClickX = 0 - downClickY = 0 - } - } - - const handleMouseMove = (e: React.MouseEvent) => { - let mouseX = e.nativeEvent.offsetX - let mouseY = e.nativeEvent.offsetY - - if (isDrawing) handleMouseDrawArea(mouseX, mouseY) - else handleHoverOverArea(e) - } - - const handleWheelEvent = (e: WheelEvent) => { - if (!e.ctrlKey) return - - const shouldAttemptToZoomIn = (e.deltaY < 0) && zoomLevel < maxZoomLevel - if (shouldAttemptToZoomIn) setZoomLevel(zoomLevel + zoomStep) - else if (zoomLevel > (zoomStep * 2)) setZoomLevel(zoomLevel - zoomStep) - } - - const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => { - console.log(newWordValue) - requestUpdateProcessedWordById(wordId, newWordValue) - .then(res => { - console.log('res', res) - getProcessedAreaById(hoverOverAreaId || '').then(response => { - setHoveredProcessedArea(response) - }) - }) - .catch(console.error) - setWordToEdit(undefined) - } - - useEffect(() => { - if (selectedDocument?.path) applyDocumentToCanvas(selectedDocument.path) - }) - - useEffect(() => { - applyUiCanvasUpdates() - }, [hoverOverAreaId]) - - const renderAreaPreview = () => { - if (!areas || !areas.length || !hoveredProcessedArea) return <> - - const hoverArea = areas.find(a => a.id === hoverOverAreaId) - if (!hoverArea) return <> - - - return
- { - hoveredProcessedArea.lines?.map(l => l.words).flat().map((w, i) => { - const width = Math.floor((w.boundingBox.x1 - w.boundingBox.x0) * zoomLevel) + 2 - const height = Math.floor((w.boundingBox.y1 - w.boundingBox.y0) * zoomLevel) + 2 - return setWordToEdit({ word: w, areaId: hoverArea.id })}> - {w.fullText} - - }) - } -
- } - - const renderEditWord = () => { - if (!wordToEdit) return <> - - const { word, areaId } = wordToEdit - const width = Math.floor((word.boundingBox.x1 - word.boundingBox.x0) * zoomLevel) + 2 - const height = Math.floor(((word.boundingBox.y1 - word.boundingBox.y0) * zoomLevel) * 2) + 4 - return
setWordToEdit(undefined)} - > -
- {word.fullText} -
- - e.currentTarget.select()} - onBlur={(e) => handleWordCorrectionSubmit(word.id, e.currentTarget.value)} - onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(word.id, e.currentTarget.value))} - /> -
- } - - - return
-
-
-

{getSelectedDocument()?.name}

- -
-
- - { setZoomLevel(e.currentTarget.valueAsNumber) }} - /> - -
-
-
- - - - - {renderAreaPreview()} - {renderEditWord()} -
-
-} - -export default DocumentRenderer diff --git a/frontend/components/workspace/Main.tsx b/frontend/components/workspace/Main.tsx index 64c57fa..ac4654f 100644 --- a/frontend/components/workspace/Main.tsx +++ b/frontend/components/workspace/Main.tsx @@ -3,7 +3,7 @@ import { useNavigation } from '../../context/Navigation/provider' import { workspaces } from '../../context/Navigation/types' import { useProject } from '../../context/Project/provider' -import DocumentRenderer from './DocumentRenderer' +import DocumentCanvas from '../DocumentCanvas' import NoSelectedDocument from './NoSelectedDocument' import TextEditor from './TextEditor' @@ -13,7 +13,7 @@ const MainWorkspace = () => { const renderSelectedWorkSpace = () => { if (selectedWorkspace === workspaces.TEXTEDITOR) return - else return !selectedDocumentId ? : + else return !selectedDocumentId ? : } return
diff --git a/frontend/context/Project/makeDefaultProject.ts b/frontend/context/Project/makeDefaultProject.ts index 6a48af3..0f60498 100644 --- a/frontend/context/Project/makeDefaultProject.ts +++ b/frontend/context/Project/makeDefaultProject.ts @@ -31,6 +31,7 @@ const makeDefaultProject = (): ProjectContextType => ({ getGroupById: (groupId) => undefined, requestSelectProjectByName: (projectName) => Promise.resolve(false), requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false), + getProcessedAreaById: (areaId) => Promise.resolve(undefined), }) export default makeDefaultProject diff --git a/frontend/context/Project/provider.tsx b/frontend/context/Project/provider.tsx index 3a200ae..06cd07a 100644 --- a/frontend/context/Project/provider.tsx +++ b/frontend/context/Project/provider.tsx @@ -189,6 +189,18 @@ export function ProjectProvider({ children, projectProps }: Props) { return successfulResponse } + const getProcessedAreaById = async (areaId: string) => { + try { + if (!selectedDocumentId || !areaId) return + const processedAreas = await getProcessedAreasByDocumentId(selectedDocumentId) + const foundProcessedArea = processedAreas.find(a => a.id === areaId) + return foundProcessedArea + } catch (err) { + console.error(err) + return Promise.resolve(undefined) + } + } + useEffect(() => { if (!documents.length && !groups.length) updateDocuments() @@ -231,6 +243,7 @@ export function ProjectProvider({ children, projectProps }: Props) { getGroupById, requestSelectProjectByName, requestUpdateProcessedWordById, + getProcessedAreaById, } return diff --git a/frontend/context/Project/types.ts b/frontend/context/Project/types.ts index 115c70b..d9286c1 100644 --- a/frontend/context/Project/types.ts +++ b/frontend/context/Project/types.ts @@ -63,4 +63,5 @@ export type ProjectContextType = { getGroupById: (groupId: string) => ipc.Group | undefined requestSelectProjectByName: (projectName: string) => Promise requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise + getProcessedAreaById: (areaId: string) => Promise } & ProjectProps \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6ee6ff5..4ea7378 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "postcss": "^8.4.19", "server-only": "^0.0.1", "tailwindcss": "^3.2.4", + "typedoc": "^0.24.7", "typescript": "^4.9.4" } }, @@ -971,6 +972,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3050,6 +3057,12 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -3148,6 +3161,24 @@ "node": ">=10" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -4601,6 +4632,18 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4984,6 +5027,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedoc": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz", + "integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "4.9.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", @@ -5204,6 +5292,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/wasm-feature-detect": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz", @@ -5954,6 +6054,12 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, + "ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7428,6 +7534,12 @@ "minimist": "^1.2.0" } }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -7502,6 +7614,18 @@ "yallist": "^4.0.0" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + }, "mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -8385,6 +8509,18 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "shiki": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", + "dev": true, + "requires": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -8664,6 +8800,38 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" }, + "typedoc": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz", + "integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "typescript": { "version": "4.9.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", @@ -8809,6 +8977,18 @@ "unist-util-stringify-position": "^3.0.0" } }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "wasm-feature-detect": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f274cd9..ab9c759 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "postcss": "^8.4.19", "server-only": "^0.0.1", "tailwindcss": "^3.2.4", + "typedoc": "^0.24.7", "typescript": "^4.9.4" } } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index be11901..66810ab 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -e8bab26469d3f6b725d33cef9359048b \ No newline at end of file +2415a78ef8f325df057b22f577cbbe50 \ No newline at end of file