feat: zoom doc and delete area

This commit is contained in:
Joshua Shoemaker 2023-03-22 00:53:21 -05:00
parent f0d0c609b3
commit 49db0aff66
9 changed files with 200 additions and 79 deletions

View File

@ -1,19 +1,26 @@
'use client'
import React, { useEffect, useRef } from 'react'
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'
const zoomStep = 0.05
const maxZoomLevel = 4
const DocumentRenderer = () => {
const { getSelectedDocument, requestAddArea } = useProject()
const { getSelectedDocument, requestAddArea, selectedAreaId, setSelectedAreaId } = useProject()
const selectedDocument = getSelectedDocument()
const areas = selectedDocument?.areas
const documentCanvas = useRef<HTMLCanvasElement>(null)
const areaCanvas = useRef<HTMLCanvasElement>(null)
const drawingCanvas = useRef<HTMLCanvasElement>(null)
const [zoomLevel, setZoomLevel] = useState(1)
let downClickX = 0
let downClickY = 0
let isMouseDown = false
@ -43,14 +50,17 @@ const DocumentRenderer = () => {
return
}
applyCanvasSizes({ width: image.naturalWidth, height: image.naturalHeight })
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, image.width, image.height)
context.drawImage(image, 0, 0, width, height)
if (areas) applyAreasToCanvas()
}
@ -58,7 +68,7 @@ const DocumentRenderer = () => {
const applyAreasToCanvas = () => {
const areaCanvasInstance = areaCanvas.current
if (!areaCanvasInstance) return
const context = areaCanvasInstance.getContext('2d')
const context = areaCanvasInstance.getContext('2d')!
if (!context) return
context.clearRect(0, 0, areaCanvasInstance.width, areaCanvasInstance.height)
@ -66,14 +76,23 @@ const DocumentRenderer = () => {
if (!areas || !areas.length) return
areas.forEach(a => {
const width = a.endX - a.startX
const height = a.endY - a.startY
const x = a.startX
const y = a.startY
context.rect(x, y, width, height)
context.lineWidth = 2
context.strokeStyle = '#dc8dec'
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()
})
}
@ -95,25 +114,26 @@ const DocumentRenderer = () => {
let startX: number, endX: number
if (downClickX < mouseX) {
startX = downClickX
endX = mouseX
startX = Math.floor(downClickX / zoomLevel)
endX = Math.floor(mouseX / zoomLevel)
} else {
startX = mouseX
endX = downClickX
startX = Math.floor(mouseX / zoomLevel)
endX = Math.floor(downClickX / zoomLevel)
}
let startY: number, endY: number
if (downClickY < mouseY) {
startY = downClickY
endY = mouseY
startY = Math.floor(downClickY / zoomLevel)
endY = Math.floor(mouseY / zoomLevel)
} else {
startY = mouseY
endY = downClickY
startY = Math.floor(mouseY / zoomLevel)
endY = Math.floor(downClickY / zoomLevel)
}
if (selectedDocument?.id) {
const addedArea = await requestAddArea(selectedDocument.id, { startX, startY, endX, endY })
processImageArea(selectedDocument.id, addedArea)
setSelectedAreaId(addedArea.id)
processImageArea(selectedDocument.id, addedArea.id)
}
const context = drawingCanvasInstance.getContext('2d')
@ -145,9 +165,16 @@ const DocumentRenderer = () => {
}
}
const handleWheelEvent = (e: WheelEvent<HTMLDivElement>) => {
if (!e.ctrlKey) return
const shouldAttemptToZoomIn = (e.deltaY < 0) && zoomLevel < maxZoomLevel
if (shouldAttemptToZoomIn) setZoomLevel(zoomLevel + zoomStep)
else if (zoomLevel > (zoomStep * 2)) setZoomLevel(zoomLevel - zoomStep)
}
useEffect(() => {
if (selectedDocument?.path) applyDocumentToCanvas(selectedDocument.path)
applyAreasToCanvas()
})
return <div className='relative'>
@ -155,26 +182,42 @@ const DocumentRenderer = () => {
<h1 className="text-2xl font-semibold text-gray-900">
{getSelectedDocument()?.name}
</h1>
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
<div>
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
<div className='flex justify-evenly items-center mt-2 mb-0'>
<MagnifyingGlassMinusIcon className='w-4 h-4' />
<input
id="zoomRange" type="range" min={zoomStep} max={maxZoomLevel} step={zoomStep}
value={zoomLevel} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
onChange={(e) => { setZoomLevel(e.currentTarget.valueAsNumber) }}
/>
<MagnifyingGlassPlusIcon className='w-4 h-4' />
</div>
</div>
</div>
<div className="relative mt-2">
<div
onWheelCapture={handleWheelEvent}
className={classNames('relative mt-2 overflow-scroll',
'w-[calc(100vw-320px)] h-[calc(100vh-240px)] border-4',
'border-dashed border-gray-200')}>
<canvas
className="absolute border-4 border-dashed border-gray-200"
className="absolute"
ref={documentCanvas}
/>
<canvas
className="absolute border-4 border-transparent"
className="absolute "
ref={areaCanvas}
/>
<canvas
className="absolute border-4 border-transparent"
className="absolute"
ref={drawingCanvas}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
/>
</div>
</div>
</div >
}
export default DocumentRenderer

View File

@ -70,6 +70,7 @@ function Sidebar() {
getSelectedDocument,
getAreaById,
requestUpdateArea,
requestDeleteAreaById,
requestAddDocument,
requestAddDocumentGroup,
selectedAreaId,
@ -128,34 +129,24 @@ function Sidebar() {
setIsEditAreaNameInputShowing(false)
}
// ________________
const onAreaDragOver = (areaId: string) => {
setDragOverAreaId(areaId)
}
const onAreaDragStart = (areaId: string) => {
// setDragStartAreaId(areaId)
setSelectedAreaId(areaId)
}
const onAreaDropEnd = (areaId: string) => {
const areaDroppedOn = navigation.map(n => n.documents).flat().map(d => d.areas).flat().find(a => a.id === dragOverAreaId)
if (!areaDroppedOn) return
requestChangeAreaOrder(areaId, areaDroppedOn.order)
setDragOverAreaId('')
}
// ________________
const handleAreaDeleteButtonClick = (areaId: string) => {
requestDeleteAreaById(areaId)
}
const onDocumentClickHandler = (itemId: string) => {
setSelectedDocumentId(itemId)
@ -362,7 +353,7 @@ function Sidebar() {
name="documentName"
id="documentName"
autoFocus
className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
className="h-8 w-[calc(100%-18px)] text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 inline-block rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={d.name}
onBlur={onDocumentInputBlur}
onKeyDown={(event) => {
@ -397,12 +388,12 @@ function Sidebar() {
)}>
{selectedDocumentId === d.id && isEditDocumentNameInputShowing
? <input // TODO: this
? <input
type="text"
name="documentName"
id="documentName"
autoFocus
className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
className="h-8 w-[calc(100%-18px)] text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 inline-block rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={d.name}
onBlur={onDocumentInputBlur}
onKeyDown={(event) => {
@ -434,7 +425,7 @@ function Sidebar() {
id="areaName"
autoFocus
className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder={a.name || `Area ${index + 1}`}
placeholder={a.name || `Area ${index}`}
onBlur={onAreaInputBlur}
onKeyDown={(event) => {
onEnterHandler(event,
@ -442,23 +433,29 @@ function Sidebar() {
}}
ref={editAreaNameTextInput}
/>
: <a
role='button'
onClick={() => onAreaClick(a.id)}
onDoubleClick={() => onAreaDoubleClick(a.id)}
: <div
draggable
onDragOver={() => onAreaDragOver(a.id)}
onDragStart={() => onAreaDragStart(a.id)}
onDragEnd={() => onAreaDropEnd(a.id)}
className={classNames('text-gray-300 hover:bg-gray-700 hover:text-white',
'group w-full flex items-center pr-2 py-2 text-left font-medium pl-8 text-xs',
'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none',
selectedAreaId === a.id ? 'underline' : '',
dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : ''
)}
>
{a.name || `Area ${a.order}`}
</a>
className={classNames('flex justify-between items-center cursor-pointer',
selectedAreaId === a.id ? 'bg-indigo-500 text-gray-200' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : '',
selectedAreaId === a.id && dragOverAreaId === a.id ? 'bg-indigo-300' : '',
)}>
<a
role='button'
onClick={() => onAreaClick(a.id)}
onDoubleClick={() => onAreaDoubleClick(a.id)}
className={classNames('group w-full pr-2 py-2 text-left font-medium pl-8 text-xs',
'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none',
)}>
{a.name || `Area ${a.order}`}
</a>
<XMarkIcon
className='w-5 h-5 mr-2 text-white hover:bg-white hover:text-gray-700 rounded-full p-0.5'
onClick={() => handleAreaDeleteButtonClick(a.id)} />
</div>
}
</li>
))}

View File

@ -13,6 +13,7 @@ const makeDefaultProject = (): ProjectContextType => ({
requestAddProcessedArea: (processesArea) => Promise.resolve(new ipc.ProcessedArea()),
requestAddArea: (documentId, area) => Promise.resolve(new ipc.Area()),
requestUpdateArea: (updatedArea) => Promise.resolve(new ipc.Area()),
requestDeleteAreaById: (areaId) => Promise.resolve(false),
requestAddDocument: (groupId, documentName) => Promise.resolve(new ipc.Document()),
requestAddDocumentGroup: (groupName: string) => Promise.resolve(new ipc.Group()),
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise.resolve(new ipc.UserMarkdown()),

View File

@ -9,6 +9,7 @@ import {
RequestChooseUserAvatar,
RequestUpdateDocument,
RequestChangeAreaOrder,
RequestDeleteAreaById,
} from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UpdateDocumentRequest, UserProps } from './types'
@ -30,7 +31,6 @@ export function ProjectProvider({ children, projectProps }: Props) {
const updateDocuments = async () => {
GetDocuments().then(response => {
console.log(response)
if (response.documents.length) setDocuments(response.documents)
if (response.groups.length) setGroups(response.groups)
Promise.resolve(response)
@ -66,6 +66,12 @@ export function ProjectProvider({ children, projectProps }: Props) {
documents.map(d => d.areas).flat().find(a => a.id === areaId)
)
const requestDeleteAreaById = async (areaId: string): Promise<boolean> => {
const wasSuccessfulDeletion = await RequestDeleteAreaById(areaId)
if (wasSuccessfulDeletion) updateDocuments()
return wasSuccessfulDeletion
}
const getSelectedDocument = () => documents.find(d => d.id === selectedDocumentId)
const getProcessedAreasByDocumentId = async (documentId: string) => {
@ -132,7 +138,6 @@ export function ProjectProvider({ children, projectProps }: Props) {
}
const requestChangeAreaOrder = async (areaId: string, newOrder: number) => {
console.log('requestChangeAreaOrder')
const response = await RequestChangeAreaOrder(areaId, newOrder)
await updateDocuments()
return response
@ -152,6 +157,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
requestAddDocument,
requestAddDocumentGroup,
requestUpdateArea,
requestDeleteAreaById,
selectedAreaId,
setSelectedAreaId,
selectedDocumentId,

View File

@ -43,6 +43,7 @@ export type ProjectContextType = {
requestAddProcessedArea: (processedArea: ipc.ProcessedArea) => Promise<ipc.ProcessedArea>
requestAddArea: (documentId: string, area: AddAreaProps) => Promise<ipc.Area>
requestUpdateArea: (area: AreaProps) => Promise<ipc.Area>
requestDeleteAreaById: (areaId: string) => Promise<boolean>
requestAddDocument: (groupId: string, documentName: string) => Promise<ipc.Document>
requestAddDocumentGroup: (groupName: string) => Promise<ipc.Group>
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise<ipc.UserMarkdown>

View File

@ -1,32 +1,35 @@
import { createScheduler, createWorker } from 'tesseract.js'
import { GetDocumentById, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
import { GetAreaById, GetDocumentById, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../wailsjs/wailsjs/go/models'
import loadImage from './loadImage'
const processImageArea = async (documentId: string, area: ipc.Area) => {
const processImageArea = async (documentId: string, areaId: string) => {
const foundDocument = await GetDocumentById(documentId)
if (!foundDocument.path || !foundDocument.areas?.length) return
const foundArea = await GetAreaById(areaId)
if (!foundDocument.path || !foundDocument.areas?.length || !foundArea.id) return
const processLanguage = foundDocument.defaultLanguage.processCode
const { path } = foundDocument
const imageData = await loadImage(path)
const scheduler = createScheduler()
const worker = await createWorker()
await worker.loadLanguage('eng') // TODO: change this when multilangiage system is implementd
await worker.initialize('eng') // TODO: same here
await worker.loadLanguage(processLanguage)
await worker.initialize(processLanguage)
scheduler.addWorker(worker)
const result = await scheduler.addJob('recognize', imageData, {
rectangle: {
left: area.startX,
top: area.startY,
width: area.endX - area.startX,
height: area.endY - area.startY,
left: foundArea.startX,
top: foundArea.startY,
width: foundArea.endX - foundArea.startX,
height: foundArea.endY - foundArea.startY,
}
})
const addProcessesAreaRequest = await RequestAddProcessedArea(new ipc.ProcessedArea({
id: area.id,
id: foundArea.id,
documentId,
fullText: result.data.text,
lines: result.data.lines.map((l: any) => new ipc.ProcessedLine({

View File

@ -4,6 +4,8 @@ import {ipc} from '../models';
export function CreateNewProject(arg1:string):Promise<ipc.Session>;
export function GetAreaById(arg1:string):Promise<ipc.Area>;
export function GetCurrentSession():Promise<ipc.Session>;
export function GetCurrentUser():Promise<ipc.User>;
@ -30,6 +32,8 @@ export function RequestChangeAreaOrder(arg1:string,arg2:number):Promise<ipc.Docu
export function RequestChooseUserAvatar():Promise<string>;
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
export function RequestUpdateArea(arg1:ipc.Area):Promise<ipc.Area>;
export function RequestUpdateCurrentUser(arg1:ipc.User):Promise<ipc.User>;

View File

@ -6,6 +6,10 @@ export function CreateNewProject(arg1) {
return window['go']['ipc']['Channel']['CreateNewProject'](arg1);
}
export function GetAreaById(arg1) {
return window['go']['ipc']['Channel']['GetAreaById'](arg1);
}
export function GetCurrentSession() {
return window['go']['ipc']['Channel']['GetCurrentSession']();
}
@ -58,6 +62,10 @@ export function RequestChooseUserAvatar() {
return window['go']['ipc']['Channel']['RequestChooseUserAvatar']();
}
export function RequestDeleteAreaById(arg1) {
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
}
export function RequestUpdateArea(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1);
}

View File

@ -32,12 +32,13 @@ func (c *Channel) GetDocumentById(id string) Document {
})
}
response := Document{
Id: foundDocument.Id,
Name: foundDocument.Name,
GroupId: foundDocument.GroupId,
Path: foundDocument.Path,
ProjectId: foundDocument.ProjectId,
Areas: jsonAreas,
Id: foundDocument.Id,
Name: foundDocument.Name,
GroupId: foundDocument.GroupId,
Path: foundDocument.Path,
ProjectId: foundDocument.ProjectId,
Areas: jsonAreas,
DefaultLanguage: Language(foundDocument.DefaultLanguage),
}
return response
}
@ -193,6 +194,32 @@ func (c *Channel) RequestAddDocumentGroup(name string) Group {
return response
}
func (c *Channel) GetAreaById(areaId string) Area {
foundDocument := document.GetDocumentCollection().GetDocumentByAreaId(areaId)
if len(foundDocument.Areas) == 0 {
return Area{}
}
var foundArea document.Area
for i, a := range foundDocument.Areas {
if a.Id == areaId {
foundArea = foundDocument.Areas[i]
}
}
return Area{
Id: foundArea.Id,
Name: foundArea.Name,
StartX: foundArea.StartX,
EndX: foundArea.EndX,
StartY: foundArea.StartY,
EndY: foundArea.EndY,
Order: foundArea.Order,
Language: Language(foundArea.Language),
}
}
func (c *Channel) RequestAddArea(documentId string, area Area) Area {
foundDocument := document.GetDocumentCollection().GetDocumentById(documentId)
@ -233,7 +260,7 @@ func (c *Channel) RequestUpdateArea(updatedArea Area) Area {
return Area{}
}
areaToUpdate := documentOfArea.GetAreaById((updatedArea.Id))
areaToUpdate := documentOfArea.GetAreaById(updatedArea.Id)
if areaToUpdate.Id == "" {
return Area{}
@ -259,6 +286,37 @@ func (c *Channel) RequestUpdateArea(updatedArea Area) Area {
}
}
func (c *Channel) RequestDeleteAreaById(areaId string) bool {
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(areaId)
if documentOfArea.Id == "" {
return false
}
areaToDeleteIndex := -1
for i, a := range documentOfArea.Areas {
if a.Id == areaId {
areaToDeleteIndex = i
break
}
}
if areaToDeleteIndex < 0 {
return false
}
// func remove(s []int, i int) []int {
// s[i] = s[len(s)-1]
// return s[:len(s)-1]
// }
documentOfArea.Areas[areaToDeleteIndex] = documentOfArea.Areas[len(documentOfArea.Areas)-1]
documentOfArea.Areas = documentOfArea.Areas[:len(documentOfArea.Areas)-1]
return true
}
func (c *Channel) RequestUpdateDocument(updatedDocument Document) Document {
documentToUpdate := document.GetDocumentCollection().GetDocumentById(updatedDocument.Id)