diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 0803ac641a12e53a0df2991192c190ba3cedac74..742c1ae728eb7afd09aa28d0fb33ba6faf842de2 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -1,7 +1,7 @@ import {Component, HostListener, Inject, OnDestroy, OnInit, Optional} from "@angular/core"; import {select, Store} from "@ngrx/store"; import {ARIA_LABELS, CONST} from "common/constants"; -import { Observable, Subscription} from "rxjs"; +import { merge, Observable, Subscription} from "rxjs"; import {getUuid} from "src/util/fn"; import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; import {buffer, debounceTime, distinctUntilChanged, filter, map, switchMapTo, take, takeUntil, tap} from "rxjs/operators"; @@ -22,6 +22,7 @@ import {CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor} from "src/util"; }) export class AnnotationMode implements OnInit, OnDestroy { + public moduleAnnotationTypes: {instance: { name: string, iconClass: string }, onClick: Function} [] = [] public selectedType = 0 public position1: string @@ -61,6 +62,7 @@ export class AnnotationMode implements OnInit, OnDestroy { @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor ) { + this.moduleAnnotationTypes = this.ans.moduleAnnotationTypes if (clickInterceptor) { const { register, deregister } = clickInterceptor const onMouseClick = this.onMouseClick.bind(this) @@ -127,8 +129,20 @@ export class AnnotationMode implements OnInit, OnDestroy { filter((e: any) => e.eventName === 'mousemove') ) - // Trigger mouse click on viewer (avoid dragging) this.subscriptions.push( + /** + * trigger annotation mouse events for modular tools + */ + merge<{ + eventName: 'mousedown' | 'mouseup' | 'mousemove' + event: MouseEvent + }>(...[mouseDown$, mouseUp$, mouseMove$]).subscribe(ev => { + this.ans.tmpAnnotationMouseEvent.next({ + event: ev.event, + eventype: ev.eventName + }) + }), + // Trigger mouse click on viewer (avoid dragging) mouseDown$.pipe( switchMapTo( mouseUp$.pipe( diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html index a67bdb554ce7f162b9fd072739ebe800f906f3f9..9a5738463d1644e9451c755212d3b1ba4eced89d 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html @@ -1,17 +1,17 @@ -<div class="pe-all d-flex flex-column justify-content-content annotation-toolbar"> +<div class="pe-all d-flex flex-column justify-content-content z-index-10 w-3em"> - <mat-card class="d-flex flex-column flex-grow-0 panel-default annotation-toolbar-content p-0"> + <mat-card class="d-flex flex-column flex-grow-0 panel-default p-0"> <div *ngFor="let type of ans.annotationTypes; let i = index; let last = last"> - <button - mat-icon-button - type="button" - (click)="selectAnnotationType(i)" - class="mb-2 mt-2" - [matTooltip]="type.name" - [color]="selectedType === i? 'primary' : 'secondary'"> - <i [ngClass]="type.class"></i> - </button> + <button + mat-icon-button + type="button" + (click)="selectAnnotationType(i)" + class="mb-2 mt-2" + [matTooltip]="type.name" + [color]="selectedType === i? 'primary' : 'secondary'"> + <i [ngClass]="type.class"></i> + </button> <mat-divider *ngIf="last || type.action !== ans.annotationTypes[i+1].action"></mat-divider> </div> <button @@ -23,5 +23,17 @@ color="warn"> <i class="fas fa-times"></i> </button> + <div> + <mat-divider></mat-divider> + <button + *ngFor="let moduleAnnotType of moduleAnnotationTypes" + mat-icon-button + (click)="moduleAnnotType.onClick()" + class="mt-2 mb-2" + [color]="(moduleAnnotType.instance.toolSelected$ | async) ? 'primary' : 'basic'" + type="button"> + <i [class]="moduleAnnotType.instance.iconClass"></i> + </button> + </div> </mat-card> </div> diff --git a/src/atlasComponents/userAnnotations/annotationService.service.ts b/src/atlasComponents/userAnnotations/annotationService.service.ts index 5aea78b5bf62600844c559de96da709d24437264..9315e2b7b7e6cb682b04548782613a26c4c0846f 100644 --- a/src/atlasComponents/userAnnotations/annotationService.service.ts +++ b/src/atlasComponents/userAnnotations/annotationService.service.ts @@ -5,6 +5,7 @@ import {getUuid} from "src/util/fn"; import {Store} from "@ngrx/store"; import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; import * as JSZip from 'jszip'; +import { Observable, Subject } from "rxjs"; const USER_ANNOTATION_LAYER_SPEC = { "type": "annotation", @@ -17,6 +18,9 @@ const USER_ANNOTATION_LAYER_SPEC = { @Injectable() export class AnnotationService { + public tmpAnnotationMouseEvent = new Subject<{ eventype: 'mousedown' | 'mouseup' | 'mousemove', event: MouseEvent }>() + public moduleAnnotationTypes: {instance: {name: string, iconClass: string, toolSelected$: Observable<boolean>}, onClick: Function}[] = [] + // Annotations to display on viewer public pureAnnotationsForViewer = [] diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index c9e2aabecb990e2b8c5885bfb5f26d99b013a0ee..8a712e78e54ea6aa12d0e9f28ecabcfb9e7ff0d4 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -8,6 +8,7 @@ import {AnnotationMode} from "src/atlasComponents/userAnnotations/annotationMode import {AnnotationList} from "src/atlasComponents/userAnnotations/annotationList/annotationList.component"; import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; import {AnnotationMessage} from "src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component"; +import { UserAnnotationToolModule } from "./tools/module"; @NgModule({ imports: [ @@ -17,6 +18,7 @@ import {AnnotationMessage} from "src/atlasComponents/userAnnotations/annotationM FormsModule, ReactiveFormsModule, AngularMaterialModule, + UserAnnotationToolModule, ], declarations: [ AnnotationMode, diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a56d582d4dd917c103bc1e202cc5178d606e257a --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -0,0 +1,20 @@ +import { NgModule } from "@angular/core"; +import { Subject } from "rxjs"; +import { ModularUserAnnotationToolService } from "./service"; +import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; + +@NgModule({ + providers: [ + { + provide: ANNOTATION_EVENT_INJ_TOKEN, + useValue: new Subject() + }, + ModularUserAnnotationToolService + ] +}) + +export class UserAnnotationToolModule { + + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor(_svc: ModularUserAnnotationToolService){} +} diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5d7d41379d93ab37cb8cac30aa03b79a507b9ae --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -0,0 +1,54 @@ +import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, IAnnotationTools, INgAnnotationTypes, TAnnotationEvent, TNgAnnotationEv, TToolType } from "./type"; +import { Observable, Subject } from "rxjs"; + +export class Point extends IAnnotationGeometry { + id: string + x: number + y: number + z: number + + static threshold = 1e-6 + static eql(p1: Point, p2: Point) { + return Math.abs(p1.x - p2.x) < Point.threshold + && Math.abs(p1.y - p2.y) < Point.threshold + && Math.abs(p1.z - p2.z) < Point.threshold + } + constructor(arr: number[] = [], id?: string){ + super({id}) + if (arr.length !== 3) throw new Error(`constructor of points must be length 3`) + this.x = arr[0] + this.y = arr[1] + this.z = arr[2] + } + toJSON(){ + const { id, x, y, z } = this + return { id, x, y, z } + } + toNgAnnotation(): INgAnnotationTypes['point'][]{ + return [{ + id: this.id, + point: [this.x, this.y, this.z], + type: 'point', + }] + } + static fromJSON(json: any) { + const { x, y, z, id } = json + return new Point([x, y, z], id) + } +} + +export class ToolPoint extends AbsToolClass implements IAnnotationTools { + public name = 'Point' + public toolType: TToolType = 'drawing' + public iconClass = 'fas fa-circle' + private managedAnnotations: Point[] = [] + public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() + constructor( + annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> + ){ + super(annotationEv$) + } + ngAnnotationIsRelevant(annotation: TNgAnnotationEv){ + return this.managedAnnotations.some(p => p.id === annotation.pickedAnnotationId) + } +} diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts new file mode 100644 index 0000000000000000000000000000000000000000..83ede9c02caee53817cac234127e41d9b0f6ca03 --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -0,0 +1,202 @@ +import { IAnnotationTools, IAnnotationGeometry, TAnnotationEvent, IAnnotationEvents, AbsToolClass, INgAnnotationTypes, TNgAnnotationEv, TToolType } from "./type"; +import { Point } from './point' +import { OnDestroy } from "@angular/core"; +import { merge, Observable, Subject, Subscription } from "rxjs"; +import { debounceTime, filter, map, switchMapTo, takeUntil } from "rxjs/operators"; + +class Polygon extends IAnnotationGeometry{ + public id: string + + private points: Point[] = [] + private idCounter = 0 + private edges: [number, number][] = [] + + public hasPoint(p: Point) { + return this.points.indexOf(p) >= 0 + } + + public addPoint(p: Point | {x: number, y: number, z: number}, linkTo?: Point): Point { + if (linkTo && !this.hasPoint(linkTo)) { + throw new Error(`linkTo point does not exist for polygon!`) + } + + const pointToBeAdded = p instanceof Point + ? p + : new Point([p.x, p.y, p.z], `${this.id}_${this.idCounter}`) + this.idCounter += 1 + + if (!this.hasPoint(pointToBeAdded)) this.points.push(pointToBeAdded) + if (linkTo) { + const newEdge = [ + this.points.indexOf(linkTo), + this.points.indexOf(pointToBeAdded) + ] as [number, number] + this.edges.push(newEdge) + } + return pointToBeAdded + } + + toJSON(){ + const { id, points, edges } = this + return { id, points, edges } + } + toNgAnnotation(): INgAnnotationTypes['line'][]{ + return this.edges.map((indices, edgeIdx) => { + const pt1 = this.points[indices[0]] + const pt2 = this.points[indices[1]] + return { + id: `${this.id}_${edgeIdx}_0`, + pointA: [pt1.x, pt1.y, pt1.z], + pointB: [pt2.x, pt2.y, pt2.z], + type: 'line', + description: '' + } + }) + } + + parseNgAnnotationObj(pickedAnnotationId: string, pickedOffset: number): { edge: [number, number], edgeIdx: number, point: Point, pointIdx: number } { + const [ id, edgeIdx, _shouldBeZero ] = pickedAnnotationId.split('_') + if (id !== this.id) return null + + if (pickedOffset === 0) { + // focus === edge + + const edgeIdxNumber = Number(edgeIdx) + return { + edgeIdx: edgeIdxNumber, + edge: this.edges[edgeIdxNumber], + pointIdx: null, + point: null + } + } + if (pickedOffset > 2) throw new Error(`polygon should not have picked offset > 2, but is ${pickedOffset}`) + const edgeIdxNumber = Number(edgeIdx) + const edge = this.edges[edgeIdxNumber] + const pointIdx = edge[ pickedOffset - 1 ] + return { + edgeIdx: edgeIdxNumber, + edge, + pointIdx, + point: this.points[pointIdx] + } + } + + static fromJSON(json: any){ + const { id, points, edges } = json + const p = new Polygon() + p.points = points.map(Point.fromJSON) + p.edges = edges + p.id = id + return p + } + + constructor(){ + super() + } +} + +export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDestroy { + public name = 'polygon' + public iconClass = 'fas fa-draw-polygon' + public toolType: TToolType = 'drawing' + + private selectedPoly: Polygon + private lastAddedPoint: Point + + private managedAnnotations: Polygon[] = [] + private subs: Subscription[] = [] + public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() + + private hoveredAnnotation$: Observable<{ + annotation: Polygon + detail: { point: Point } + }> = this.hoverAnnotation$.pipe( + map(ann => { + const { pickedAnnotationId, pickedOffset } = ann.detail + const annotation = this.managedAnnotations.find(poly => poly.parseNgAnnotationObj(pickedAnnotationId, pickedOffset)) + if (!annotation) { + return null + } + return { + annotation, + detail: { + point: annotation.parseNgAnnotationObj(pickedAnnotationId, pickedOffset).point + } + } + }) + ) + + constructor( + annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> + ){ + super(annotationEv$) + + const toolDeselect$ = this.toolSelected$.pipe( + filter(flag => !flag) + ) + const toolSelThenClick$ = this.toolSelected$.pipe( + filter(flag => !!flag), + switchMapTo(this.mouseClick$.pipe( + takeUntil(toolDeselect$) + )) + ) + + this.subs.push( + /** + * on end tool select + */ + toolDeselect$.subscribe(() => { + this.selectedPoly = null + this.lastAddedPoint = null + }), + /** + * on tool selected + * on mouse down + * until tool deselected + */ + toolSelThenClick$.pipe( + ).subscribe(mouseev => { + if (!this.selectedPoly) { + this.selectedPoly = new Polygon() + this.managedAnnotations.push(this.selectedPoly) + } + + const addedPoint = this.selectedPoly.addPoint( + mouseev.detail.ngMouseEvent, + this.lastAddedPoint + ) + this.lastAddedPoint = addedPoint + }), + + /** + * conditions by which ng annotations are refreshed + */ + merge( + toolDeselect$, + toolSelThenClick$ + ).pipe( + debounceTime(16), + ).subscribe(() => { + let out: INgAnnotationTypes['line'][] = [] + for (const managedAnn of this.managedAnnotations) { + out = out.concat(...managedAnn.toNgAnnotation()) + } + this.allNgAnnotations$.next(out) + }), + this.hoverAnnotation$.subscribe(val => { + if (val.detail) { + console.log(val.detail) + } + }) + ) + } + + ngAnnotationIsRelevant(annotation: TNgAnnotationEv): boolean { + // perhaps use more advanced way to track if annotation is a part of polygon? + return this.managedAnnotations.some(poly => poly.id.indexOf(annotation.pickedAnnotationId) >= 0) + } + + ngOnDestroy(){ + if (this.subs.length > 0) this.subs.pop().unsubscribe() + } +} diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..791d2a2017c0e965edd91869569ffd6f2fff190f --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -0,0 +1,242 @@ +import { Injectable } from "@angular/core"; +import { ARIA_LABELS } from 'common/constants' +import { Inject, Optional } from "@angular/core"; +import { select, Store } from "@ngrx/store"; +import { Observable, of, Subject } from "rxjs"; +import { map, scan, switchMap } from "rxjs/operators"; +import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; +import { NehubaViewerUnit } from "src/viewerModule/nehuba"; +import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; +import { AnnotationService } from "../annotationService.service"; +import { ToolPolygon } from "./poly"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, INgAnnotationTypes, TAnnotationEvent } from "./type"; +import { switchMapWaitFor } from "src/util/fn"; + +const IAV_VOXEL_SIZES_NM = { + 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], + 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8': [39062.5, 39062.5, 39062.5], + 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588': [21166.666015625, 20000, 21166.666015625], + 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992': [1000000, 1000000, 1000000,], + 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': [1000000, 1000000, 1000000] +} + +@Injectable({ + providedIn: 'root' +}) + +export class ModularUserAnnotationToolService { + + static VIEWER_MODE = ARIA_LABELS.VIEWER_MODE_ANNOTATING + + static ANNOTATION_LAYER_NAME = 'modular_tool_layer_name' + static USER_ANNOTATION_LAYER_SPEC = { + "type": "annotation", + "tool": "annotateBoundingBox", + "name": ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, + "annotationColor": "#ee00ff", + "annotations": [], + } + + private selectedTmpl: { fullId: string, name: string } + private ngAnnotationLayer: any + private activeToolName: string + private ngAnnotations$ = new Subject<{ + tool: string + annotations: INgAnnotationTypes[keyof INgAnnotationTypes][] + }>() + private registeredTool: { + name: string + iconClass: string + ngOnDestroy?: Function + }[] = [] + private mousePosReal: [number, number, number] + + /** + * @description register new annotation tool + * @param {AbsToolClass} Cls + */ + private registerTool(Cls: new ( + svc: Subject<TAnnotationEvent<keyof IAnnotationEvents>> + ) => AbsToolClass){ + + const newTool = new Cls(this.annotnEvSubj) as AbsToolClass & { ngOnDestroy?: Function } + const { name, iconClass, ngOnDestroy } = newTool + + this.annotnSvc.moduleAnnotationTypes.push({ + instance: newTool, + onClick: () => { + const tool = this.activeToolName === name + ? null + : name + this.activeToolName = tool + this.annotnEvSubj.next({ + type: 'toolSelect', + detail: { name: tool } + } as TAnnotationEvent<'toolSelect'>) + } + }) + + newTool.allNgAnnotations$.subscribe(ann => { + this.ngAnnotations$.next({ + tool: name, + annotations: ann + }) + }) + + this.registeredTool.push({ name, iconClass, ngOnDestroy }) + } + + /** + * + * @description deregister tool. Calls any necessary clean up function + * @param name name of the tool to be deregistered + * @returns void + */ + private deregisterTool(name: string) { + this.annotnSvc.moduleAnnotationTypes = this.annotnSvc.moduleAnnotationTypes.filter(tool => tool.instance.name !== name) + const foundIdx = this.registeredTool.findIndex(spec => spec.name === name) + if (foundIdx >= 0) { + const tool = this.registeredTool.splice(foundIdx, 1)[0] + const { ngOnDestroy } = tool + if (ngOnDestroy) ngOnDestroy.call(tool) + } + } + + + constructor( + private annotnSvc: AnnotationService, + store: Store<any>, + @Inject(ANNOTATION_EVENT_INJ_TOKEN) private annotnEvSubj: Subject<TAnnotationEvent<keyof IAnnotationEvents>>, + @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, + ){ + this.registerTool(ToolPolygon) + + /** + * on new nehubaViewer, unset annotationLayer + */ + nehubaViewer$.subscribe(() => { + this.ngAnnotationLayer = null + }) + + /** + * on new nehubaViewer, listen to mouseState + */ + let cb: () => void + nehubaViewer$.pipe( + switchMap(switchMapWaitFor({ + condition: nv => !!(nv?.nehubaViewer), + })) + ).subscribe(nehubaViewer => { + if (cb) cb() + if (nehubaViewer) { + const mouseState = nehubaViewer.nehubaViewer.ngviewer.mouseState + cb = mouseState.changed.add(() => { + const payload: IAnnotationEvents['hoverAnnotation'] = mouseState.active && !!mouseState.pickedAnnotationId + ? { + pickedAnnotationId: mouseState.pickedAnnotationId, + pickedOffset: mouseState.pickedOffset + } + : null + this.annotnEvSubj.next({ + type: 'hoverAnnotation', + detail: payload + }) + }) + } + }) + + /** + * get mouse real position + */ + nehubaViewer$.pipe( + switchMap(v => v?.mousePosInReal$ || of(null)) + ).subscribe(v => this.mousePosReal = v) + + /** + * listen to mouse event on nehubaViewer, and emit as TAnnotationEvent + */ + this.annotnSvc.tmpAnnotationMouseEvent.subscribe(ev => { + const payload = { + type: ev.eventype, + detail: { + event: ev.event, + ngMouseEvent: { + x: this.mousePosReal[0], + y: this.mousePosReal[1], + z: this.mousePosReal[2] + } + } + } as TAnnotationEvent<'mousedown' | 'mouseup' | 'mousemove'> + this.annotnEvSubj.next(payload) + }) + + + /** + * on annotation update, update annotations + */ + this.ngAnnotations$.pipe( + switchMap(switchMapWaitFor({ + condition: () => !!this.ngAnnotationLayer + })), + scan((acc, curr) => { + console.log(curr) + return { + ...acc, + [curr.tool]: curr.annotations + } + }, {} as { + [key: string]: INgAnnotationTypes[keyof INgAnnotationTypes][] + }), + map(acc => { + const out: INgAnnotationTypes[keyof INgAnnotationTypes][] = [] + for (const key in acc) { + out.push(...acc[key]) + } + return out + }) + ).subscribe(val => { + this.ngAnnotationLayer.layer.localAnnotations.restoreState(val) + }) + + /** + * on viewer mode update, either create layer, or show/hide layer + */ + store.pipe( + select(viewerStateViewerModeSelector) + ).subscribe(viewerMode => { + if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) { + if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(true) + else { + const viewer = (window as any).viewer + const voxelSize = IAV_VOXEL_SIZES_NM[this.selectedTmpl.fullId] + if (!voxelSize) throw new Error(`voxelSize of ${this.selectedTmpl.fullId} cannot be found!`) + const layer = viewer.layerSpecification.getLayer( + ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, + { + ...ModularUserAnnotationToolService.USER_ANNOTATION_LAYER_SPEC, + transform: [ + [1/voxelSize[0], 0, 0, 0], + [0, 1/voxelSize[1], 0, 0], + [0, 0, 1/voxelSize[2], 0], + [0, 0, 0, 1], + ] + } + ) + this.ngAnnotationLayer = viewer.layerManager.addManagedLayer(layer) + } + } else { + if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(false) + } + }) + + /** + * on template select, update selectedtmpl + * required for metadata in annotation geometry and voxel size + */ + store.pipe( + select(viewerStateSelectedTemplatePureSelector) + ).subscribe(tmpl => { + this.selectedTmpl = tmpl + }) + } +} diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d4edf2b75cd119b3acb1e1f123bb835c083f11c --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -0,0 +1,142 @@ +import { InjectionToken } from "@angular/core" +import { Observable } from "rxjs" +import { filter, map, mapTo, switchMap, takeUntil, tap } from 'rxjs/operators' +import { getUuid } from "src/util/fn" + +export type TToolType = 'translation' | 'drawing' | 'deletion' + +type THasId = { + id?: string +} +export abstract class IAnnotationGeometry { + public id: string + + abstract toNgAnnotation(): INgAnnotationTypes[keyof INgAnnotationTypes][] + abstract toJSON(): object + + constructor(spec?: THasId){ + this.id = spec && spec.id || getUuid() + } +} + +export interface IAnnotationTools { + name: string + iconClass: string + toolType: TToolType +} + +export type TNgAnnotationEv = { + pickedAnnotationId: string + pickedOffset: number +} + +export type TNgMouseEvent = { + event: MouseEvent + ngMouseEvent: { + x: number + y: number + z: number + } +} + +export abstract class AbsToolClass { + + public abstract name: string + public abstract iconClass: string + + /** + * @description check if any specific annotation is relevant to the tool. Used for filtering annotations + * @param {TNgAnnotationEv} annotation + * @returns {boolean} if annotation is relevant to this tool + */ + public abstract ngAnnotationIsRelevant(hoverEv: TNgAnnotationEv): boolean + + public abstract allNgAnnotations$: Observable<INgAnnotationTypes[keyof INgAnnotationTypes][]> + + constructor( + protected annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> + ){ + + } + + public toolSelected$ = this.annotationEv$.pipe( + filter(ev => ev.type === 'toolSelect'), + map(ev => (ev as TAnnotationEvent<'toolSelect'>).detail.name === this.name) + ) + + protected mouseDown$ = this.annotationEv$.pipe( + filter(ev => ev.type === 'mousedown') + ) as Observable<TAnnotationEvent<'mousedown'>> + + protected mouseUp$ = this.annotationEv$.pipe( + filter(ev => ev.type === 'mouseup') + ) as Observable<TAnnotationEvent<'mouseup'>> + + protected mouseMove$ = this.annotationEv$.pipe( + filter(ev => ev.type === 'mousemove') + ) as Observable<TAnnotationEvent<'mousemove'>> + + protected mouseClick$ = this.mouseDown$.pipe( + switchMap(ev => this.mouseUp$.pipe( + takeUntil(this.mouseMove$), + mapTo(ev) + )) + ) + + protected hoverAnnotation$ = this.annotationEv$.pipe( + filter(ev => ev.type === 'hoverAnnotation') + ) as Observable<TAnnotationEvent<'hoverAnnotation'>> + + protected hoverAnnotationMouseDown$ = this.hoverAnnotation$.pipe( + tap(console.log), + filter(ev => !!this.ngAnnotationIsRelevant(ev.detail)), + switchMap(hoverAnnotationEv => this.mouseDown$.pipe( + map(mouseDownEv => { + return { + ngAnnotationEv: hoverAnnotationEv.detail, + mouseEvent: mouseDownEv.detail.event + } + }) + )) + ) +} + +export interface IAnnotationEvents { + toolSelect: { + name: string + } + mousemove: TNgMouseEvent + mousedown: TNgMouseEvent + mouseup: TNgMouseEvent + hoverAnnotation: TNgAnnotationEv +} + +export type TAnnotationEvent<T extends keyof IAnnotationEvents> = { + type: T + detail: IAnnotationEvents[T] +} + +export const ANNOTATION_EVENT_INJ_TOKEN = new InjectionToken< + Observable<TAnnotationEvent<keyof IAnnotationEvents>> +>('ANNOTATION_EVENT_INJ_TOKEN') + + +export type TNgAnnotationLine = { + type: 'line' + pointA: [number, number, number] + pointB: [number, number, number] + id: string + description?: string +} + +export type TNgAnnotationPoint = { + type: 'point' + point: [number, number, number] + id: string + description?: string +} + +export interface INgAnnotationTypes { + line: TNgAnnotationLine + point: TNgAnnotationPoint +} diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index fbe0d58637112890ef94b5f53e34b4d68aa20da0..a295e3f09f7c6c710e41a6686dbf6beaf64289c6 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -303,6 +303,10 @@ markdown-dom p width: 0!important; } +.w-3em +{ + width: 3em!important; +} .w-5em { width: 5em!important; diff --git a/src/util/fn.ts b/src/util/fn.ts index 4f673a08e5b2ace779f13d8994d68965687688af..b29be00b988db13f1614af91a171b382d7862570 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -74,11 +74,11 @@ export const arrayOfPrimitiveEqual = <T extends TPrimitive>(o: T[], n: T[]) => interface ISwitchMapWaitFor { interval?: number - condition: () => boolean + condition: (arg?: any) => boolean } -export function switchMapWaitFor(opts: ISwitchMapWaitFor){ - return (arg: unknown) => interval(opts.interval || 16).pipe( - filter(() => opts.condition()), +export function switchMapWaitFor<T>(opts: ISwitchMapWaitFor){ + return (arg: T) => interval(opts.interval || 16).pipe( + filter(() => opts.condition(arg)), take(1), mapTo(arg) )