diff --git a/common/constants.js b/common/constants.js index d9d3e6c4ff1bf4558e08aeb977d988beaed47a54..5a7066902a523a337cd27ab40065d8a64db0df31 100644 --- a/common/constants.js +++ b/common/constants.js @@ -64,10 +64,10 @@ USER_ANNOTATION_LIST: 'user annotations footer', USER_ANNOTATION_IMPORT: 'user annotations import', USER_ANNOTATION_EXPORT: 'user annotations export', - USER_ANNOTATION_EXPORT_SINGLE: 'user annotations export single', + USER_ANNOTATION_EXPORT_SINGLE: 'Export annotation', USER_ANNOTATION_HIDE: 'user annotations hide', USER_ANNOTATION_DELETE: 'user annotations delete', - GOTO_ANNOTATION_ROI: 'Navigate to annotation location of interest.' + GOTO_ANNOTATION_ROI: 'Navigate to annotation location of interest' } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index 141c36903dd66dcdfcef381542fdfe2a4c537446..0dccb54111b0b663098701fe5275fe305cb62e62 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -4,34 +4,34 @@ import {viewerStateChangeNavigation} from "src/services/state/viewerState/action import {Store} from "@ngrx/store"; import {ARIA_LABELS} from "common/constants"; import { ModularUserAnnotationToolService } from "../tools/service"; -import { EXPORT_FORMAT_INJ_TOKEN, TExportFormats } from "../tools/type"; +import { TExportFormats } from "../tools/type"; +import { ComponentStore } from "src/viewerModule/componentStore"; @Component({ selector: 'annotation-list', templateUrl: './annotationList.template.html', styleUrls: ['./annotationList.style.css'], - providers: [{ - provide: EXPORT_FORMAT_INJ_TOKEN, - useFactory: (svc: ModularUserAnnotationToolService) => svc.exportFormat$, - deps: [ - ModularUserAnnotationToolService - ] - }] + providers: [ + ComponentStore, + ] }) export class AnnotationList { public ARIA_LABELS = ARIA_LABELS public identifier = (index: number, item: any) => item.id - public availableFormat: TExportFormats[] = ['json', 'sands', 'string'] - public exportFromat$ = this.annotSvc.exportFormat$ - public selectExportFormat(format: TExportFormats) { - this.exportFromat$.next(format) - } - public managedAnnotations$ = this.annotSvc.managedAnnotations$ - constructor(private store$: Store<any>, public ans: AnnotationService, private annotSvc: ModularUserAnnotationToolService) {} + constructor( + private store$: Store<any>, + public ans: AnnotationService, + private annotSvc: ModularUserAnnotationToolService, + cStore: ComponentStore<{ useFormat: TExportFormats }>, + ) { + cStore.setState({ + useFormat: 'json' + }) + } toggleAnnotationVisibility(annotation) { if (annotation.type === 'polygon') { diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index 5433e233e676d3177b710741a7c7c2eb4e2923fd..2b06e3ade5695a9937016e92e724b2dc69929e23 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -37,22 +37,6 @@ </div> - <!-- select export format --> - <button mat-icon-button [matMenuTriggerFor]="exportFormatMenu"> - <i class="fas fa-code"></i> - </button> - - <mat-menu #exportFormatMenu="matMenu"> - <button *ngFor="let format of availableFormat" - mat-menu-item - (click)="selectExportFormat(format)"> - <span class="iv-custom-comp text" - [ngClass]="{'primary': (exportFromat$ | async) === format}"> - {{ format }} - </span> - </button> - </mat-menu> - <div class="d-flex flex-column"> <small [ngClass]="[ans.annotationFilter !== 'all'? 'text-muted' : '']" class="cursor-pointer" (click)="ans.refreshFinalAnnotationList('all')"> diff --git a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts index e69df14b0f8bf8106cd266a3289b6511d6aee265..299e8ccf558d42e793637ff2a3d95ff3f1e12b77 100644 --- a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts +++ b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts @@ -1,13 +1,14 @@ import { AfterViewInit, Component, ComponentFactoryResolver, Inject, Injector, Input, OnDestroy, Optional, Pipe, PipeTransform, ViewChild, ViewContainerRef } from "@angular/core"; -import { EXPORT_FORMAT_INJ_TOKEN, IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../tools/type"; +import { IAnnotationGeometry, UDPATE_ANNOTATION_TOKEN } from "../tools/type"; import { Point } from '../tools/point' import { Polygon } from '../tools/poly' import { FormControl, FormGroup } from "@angular/forms"; -import { Observable, Subscription } from "rxjs"; +import { Subscription } from "rxjs"; import { select, Store } from "@ngrx/store"; import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors"; import { ModularUserAnnotationToolService } from "../tools/service"; import { MatSnackBar } from "@angular/material/snack-bar"; +import { Line } from "../tools/line"; @Component({ selector: 'single-annotation-unit', @@ -59,7 +60,7 @@ export class SingleAnnotationUnit implements OnDestroy, AfterViewInit{ private snackbar: MatSnackBar, private svc: ModularUserAnnotationToolService, private cfr: ComponentFactoryResolver, - @Optional() @Inject(EXPORT_FORMAT_INJ_TOKEN) private useFormat$: Observable<TExportFormats>, + private injector: Injector, ){ this.subs.push( store.pipe( @@ -81,18 +82,19 @@ export class SingleAnnotationUnit implements OnDestroy, AfterViewInit{ if (this.managedAnnotation && this.editAnnotationVCR) { const editCmp = this.svc.getEditAnnotationCmp(this.managedAnnotation) if (!editCmp) { - this.snackbar.open(`Update component not found!`) + this.snackbar.open(`Update component not found!`, 'Dismiss', { + duration: 3000 + }) throw new Error(`Edit component not found!`) } const cf = this.cfr.resolveComponentFactory(editCmp) + const injector = Injector.create({ providers: [{ provide: UDPATE_ANNOTATION_TOKEN, useValue: this.managedAnnotation - }, { - provide: EXPORT_FORMAT_INJ_TOKEN, - useValue: this.useFormat$ - }] + }], + parent: this.injector }) this.editAnnotationVCR.createComponent(cf, null, injector) } @@ -114,6 +116,7 @@ export class SingleAnnotationNamePipe implements PipeTransform{ if (name) return name if (ann instanceof Polygon) return `Unnamed Polygon` if (ann instanceof Point) return `Unname Point` + if (ann instanceof Line) return `Unnamed Line` return `Unnamed geometry` } } @@ -127,6 +130,7 @@ export class SingleAnnotationClsIconPipe implements PipeTransform{ public transform(ann: IAnnotationGeometry): string{ if (ann instanceof Polygon) return `fas fa-draw-polygon` if (ann instanceof Point) return `fas fa-circle` + if (ann instanceof Line) return `fas fa-slash` return `fas fa-mouse-pointer` } } \ No newline at end of file diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts new file mode 100644 index 0000000000000000000000000000000000000000..e74cbf1b6054b7bbee6bd5492e6d9e0b71ed8ae9 --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -0,0 +1,282 @@ +import { + IAnnotationTools, + IAnnotationGeometry, + TAnnotationEvent, + IAnnotationEvents, + AbsToolClass, + INgAnnotationTypes, + TToolType, + TSandsLine, + getCoord, + TBaseAnnotationGeomtrySpec, + TCallbackFunction, +} from "./type"; +import { Point, TPointJsonSpec } from './point' +import { OnDestroy } from "@angular/core"; +import { merge, Observable, Subject, Subscription } from "rxjs"; +import { filter, switchMapTo, takeUntil } from "rxjs/operators"; +import { getUuid } from "src/util/fn"; + +type TLineJsonSpec = { + '@type': 'siibra-ex/annotation/line' + points: TPointJsonSpec[] +} & TBaseAnnotationGeomtrySpec + +export class Line extends IAnnotationGeometry{ + public id: string + + public points: Point[] = [] + + public hasPoint(p: Point) { + return this.points.indexOf(p) >= 0 + } + + public addLinePoints(p: Point | {x: number, y: number, z: number}): Point { + if (this.checkComplete()) { + throw new Error(`This line is already complete!`) + } + const point = p instanceof Point + ? p + : new Point({ + id: `${this.id}_${getUuid()}`, + "@type": 'siibra-ex/annotatoin/point', + space: this.space, + ...p + }) + if (!this.points[0]) this.points[0] = point + else this.points[1] = point + this.sendUpdateSignal() + return point + } + + getNgAnnotationIds() { + return [this.id] + } + + private checkComplete(){ + return this.points.length >= 2 + } + + toString(): string { + if (!this.checkComplete()) { + return `Line incomplete.` + } + + return `Line from ${this.points[0].toString()} to ${this.points[1].toString()}` + } + + toSands(): TSandsLine { + if (!this.checkComplete()) { + return null + } + const { + x: x0, y: y0, z: z0 + } = this.points[0] + + const { + x: x1, y: y1, z: z1 + } = this.points[1] + + return { + '@id': this.id, + '@type': "tmp/line", + coordinateSpace: { + '@id': this.space["@id"] + }, + coordinatesFrom: [getCoord(x0/1e6), getCoord(y0/1e6), getCoord(z0/1e6)], + coordinatesTo: [getCoord(x1/1e6), getCoord(y1/1e6), getCoord(z1/1e6)], + } + } + + toNgAnnotation(): INgAnnotationTypes['line'][] { + if (!this.checkComplete()) return [] + const pt1 = this.points[0] + const pt2 = this.points[1] + return [{ + id: this.id, + pointA: [pt1.x, pt1.y, pt1.z], + pointB: [pt2.x, pt2.y, pt2.z], + type: 'line', + description: '' + }] + + } + + toJSON(){ + const { id, points } = this + return { id, points } + } + + static fromJSON(json: TLineJsonSpec){ + return new Line(json) + } + + constructor(spec?: TLineJsonSpec){ + super(spec) + const { points = [] } = spec || {} + this.points = points.map(Point.fromJSON) + } + + public translate(x: number, y: number, z: number) { + for (const p of this.points){ + p.translate(x, y, z) + } + this.sendUpdateSignal() + } + + private sendUpdateSignal(){ + this.updateSignal$.next(this.toString()) + } +} + +export const LINE_ICON_CLASS = 'fas fa-slash' + +export class ToolLine extends AbsToolClass implements IAnnotationTools, OnDestroy { + static PREVIEW_ID='tool_line_preview' + public name = 'Line' + public toolType: TToolType = 'drawing' + public iconClass = LINE_ICON_CLASS + + private selectedLine: Line + + subs: Subscription[] = [] + + private forceRefreshAnnotations$ = new Subject() + private managedAnnotations: Line[] = [] + public managedAnnotations$ = new Subject<Line[]>() + public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() + + + onMouseMoveRenderPreview(pos: [number, number, number]) { + if (this.selectedLine && !!this.selectedLine.points[0]) { + const { x, y, z } = this.selectedLine.points[0] + return [{ + id: `${ToolLine.PREVIEW_ID}_0`, + pointA: [ x, y, z ], + pointB: pos, + type: 'line', + description: '' + }] as INgAnnotationTypes['line'][] + } + return [{ + id: `${ToolLine.PREVIEW_ID}_0`, + point: pos, + type: 'point', + description: '' + }] as INgAnnotationTypes['point'][] + } + + + constructor( + annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, + callback?: TCallbackFunction + ){ + super(annotationEv$, callback) + + this.init() + + 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.selectedLine = null + }), + /** + * on tool selected + * on mouse down + * until tool deselected + */ + toolSelThenClick$.pipe( + ).subscribe(mouseev => { + const crd = mouseev.detail.ngMouseEvent + if (!this.selectedLine) { + this.selectedLine = new Line({ + space: this.space, + "@type": 'siibra-ex/annotation/line', + points: [] + }) + this.selectedLine.addLinePoints(crd) + this.managedAnnotations.push(this.selectedLine) + this.managedAnnotations$.next(this.managedAnnotations) + } else { + + // ToDo Tool Should Be Deselected. + this.selectedLine.addLinePoints(crd) + this.selectedLine = null + + if (this.callback) { + this.callback({ type: 'paintingEnd' }) + } + } + }), + + /** + * conditions by which ng annotations are refreshed + */ + merge( + toolDeselect$, + toolSelThenClick$, + this.forceRefreshAnnotations$, + ).pipe( + + ).subscribe(() => { + let out: INgAnnotationTypes['line'][] = [] + for (const managedAnn of this.managedAnnotations) { + out = out.concat(...managedAnn.toNgAnnotation()) + } + + this.allNgAnnotations$.next(out) + }), + + /** + * emit on init, and reset on mouseup$ + * otherwise, pairwise confuses last drag event and first drag event + */ + this.dragHoveredAnnotationsDelta$.subscribe(val => { + + const { ann, deltaX, deltaY, deltaZ } = val + const { pickedAnnotationId, pickedOffset } = ann.detail + + const annotation = this.managedAnnotations.find(an => an.id === pickedAnnotationId) + if (!annotation) { + return null + } + + if (pickedOffset === 2) { + annotation.points[1].translate(deltaX, deltaY, deltaZ) + } else if (pickedOffset === 1) { + annotation.points[0].translate(deltaX, deltaY, deltaZ) + } else { + annotation.translate(deltaX, deltaY, deltaZ) + } + this.forceRefreshAnnotations$.next(null) + }) + ) + } + + ngOnDestroy(){ + this.subs.forEach(s => s.unsubscribe()) + } + + removeAnnotation(id: string){ + const idx = this.managedAnnotations.findIndex(ann => ann.id === id) + if (idx < 0) { + return + } + this.managedAnnotations.splice(idx, 1) + this.managedAnnotations$.next(this.managedAnnotations) + this.forceRefreshAnnotations$.next(null) + } + +} diff --git a/src/atlasComponents/userAnnotations/tools/line/line.component.ts b/src/atlasComponents/userAnnotations/tools/line/line.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e99984bbb5bd5d22b19022c60b76aaa22d939d87 --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/line/line.component.ts @@ -0,0 +1,115 @@ +import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Store } from "@ngrx/store"; +import { Subscription } from "rxjs"; +import { Line, LINE_ICON_CLASS } from "../line"; +import { ToolCmpBase } from "../toolCmp.base"; +import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; +import { Clipboard } from "@angular/cdk/clipboard"; +import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; +import { Point } from "../point"; +import { ARIA_LABELS } from 'common/constants' +import { ComponentStore } from "src/viewerModule/componentStore"; + +@Component({ + selector: 'line-update-cmp', + templateUrl: './line.template.html', + styleUrls: [ + './line.style.css' + ] +}) + +export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ + @Input('update-annotation') + public updateAnnotation: Line + + public ARIA_LABELS = ARIA_LABELS + + public annotationLabel = 'Line' + public LINE_ICON_CLASS = LINE_ICON_CLASS + + @ViewChild('copyTarget', { read: ElementRef, static: false }) + copyTarget: ElementRef + + public useFormat: TExportFormats = 'json' + private sub: Subscription[] = [] + + constructor( + private store: Store<any>, + snackbar: MatSnackBar, + clipboard: Clipboard, + private cStore: ComponentStore<{ useFormat: TExportFormats }>, + @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, + ){ + super(clipboard, snackbar) + if (this.cStore) { + this.sub.push( + this.cStore.select(store => store.useFormat).subscribe((val: TExportFormats) => { + this.useFormat = val + }) + ) + } + + if (updateAnnotation) { + if (updateAnnotation instanceof Line) { + this.updateAnnotation = updateAnnotation + } + } + } + + public viableFormats: TExportFormats[] = ['json', 'sands'] + + setFormat(format: TExportFormats){ + if (this.cStore) { + this.cStore.setState({ + useFormat: format + }) + } + } + + ngOnDestroy(){ + while (this.sub.length > 0) this.sub.pop().unsubscribe() + } + + get copyValue(){ + return this.copyTarget && this.copyTarget.nativeElement.value + } + + gotoRoi(roi?: IAnnotationGeometry){ + if (!this.updateAnnotation && !roi) { + throw new Error(`updateAnnotation undefined`) + } + if (this.updateAnnotation.points.length < 1) { + this.snackbar.open('No points added to polygon yet.', 'Dismiss', { + duration: 3000 + }) + return + } + const { x, y, z } = this.updateAnnotation.points[0] + + this.store.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [x, y, z], + positionReal: true, + animation: {} + } + }) + ) + } + + gotoPoint(point: Point){ + if (!point) throw new Error(`Point is not defined.`) + const { x, y, z } = point + + this.store.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [x, y, z], + positionReal: true, + animation: {} + } + }) + ) + } +} diff --git a/src/atlasComponents/userAnnotations/tools/line/line.style.css b/src/atlasComponents/userAnnotations/tools/line/line.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasComponents/userAnnotations/tools/line/line.template.html b/src/atlasComponents/userAnnotations/tools/line/line.template.html new file mode 100644 index 0000000000000000000000000000000000000000..d65b06ef8c1a6e4c3ed767fcc055fed689b3813f --- /dev/null +++ b/src/atlasComponents/userAnnotations/tools/line/line.template.html @@ -0,0 +1,62 @@ +<!-- actions --> + +<span class="m-2 text-muted"> + Endpoints +</span> + +<mat-chip-list> + <mat-chip *ngFor="let point of (updateAnnotation?.points || []) ; let i = index" + (click)="gotoPoint(point)"> + {{ i }} + </mat-chip> +</mat-chip-list> + +<mat-divider class="m-2"></mat-divider> + +<div class="d-flex"> + + <!-- export --> + <button mat-icon-button + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matMenuTriggerFor]="exportMenu"> + <i class="fas fa-file-export"></i> + </button> +</div> + +<mat-menu #exportMenu="matMenu" xPosition="before"> + <div class="iv-custom-comp card text" + iav-stop="click"> + + <div class="d-flex"> + <button *ngFor="let format of viableFormats" + (click)="setFormat(format)" + mat-flat-button + [color]="useFormat === format ? 'primary' : ''"> + {{ format }} + </button> + </div> + + <div class="iv-custom-comp text"> + <mat-form-field> + <mat-label> + {{ useFormat }} + </mat-label> + <textarea + disabled="true" + matInput + #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> + + <button mat-icon-button + matSuffix + iav-stop="click" + aria-label="Copy to clipboard" + matTooltip="Copy to clipboard." + (click)="copyToClipboard(exportTarget.value)" + color="basic"> + <i class="fas fa-copy"></i> + </button> + </mat-form-field> + </div> + </div> +</mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts index d68658dbe2de34be90eae56127baa6dec4f21ed8..3576af312ea3851571ebc27f1ddac12947497252 100644 --- a/src/atlasComponents/userAnnotations/tools/module.ts +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -3,6 +3,7 @@ import { NgModule } from "@angular/core"; import { Subject } from "rxjs"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; import { UtilModule } from "src/util"; +import { LineUpdateCmp } from "./line/line.component"; import { PointUpdateCmp } from "./point/point.component"; import { PolyUpdateCmp } from "./poly/poly.component"; import { ModularUserAnnotationToolService } from "./service"; @@ -16,11 +17,13 @@ import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; UtilModule, ], declarations: [ + LineUpdateCmp, PolyUpdateCmp, PointUpdateCmp, ToFormattedStringPipe, ], exports: [ + LineUpdateCmp, PolyUpdateCmp, PointUpdateCmp, ], diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 23649b38e1be13f4d4fc0d0cfcbb6fcf3a804322..713c3c6cc4c6c315eb0ed478b4e33f04c37f5e18 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -79,10 +79,8 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr public name = 'Point' public toolType: TToolType = 'drawing' public iconClass = POINT_ICON_CLASS - - private space: TBaseAnnotationGeomtrySpec['space'] - private subs: Subscription[] = [] + public subs: Subscription[] = [] private managedAnnotations: Point[] = [] public managedAnnotations$ = new Subject<Point[]>() public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() @@ -92,7 +90,7 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> ){ super(annotationEv$) - + this.init() const toolDeselect$ = this.toolSelected$.pipe( filter(flag => !flag) ) @@ -105,12 +103,6 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr ) this.subs.push( - /** - * subscribe to space event space info - */ - this.metadataEv$.subscribe(ev => { - this.space = ev.detail.space - }), /** * listen to click ev, add point when it occurrs */ @@ -157,8 +149,15 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr ) } + /** + * @description remove managed annotation via id + * @param id id of annotation + */ removeAnnotation(id: string) { + const idx = this.managedAnnotations.findIndex(ann => id === ann.id) + this.managedAnnotations.splice(idx, 1) + this.managedAnnotations$.next(this.managedAnnotations) } onMouseMoveRenderPreview(pos: [number, number, number]) { @@ -170,10 +169,6 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr }] as INgAnnotationTypes['point'][] } - ngAnnotationIsRelevant(annotation: TNgAnnotationEv){ - return this.managedAnnotations.some(p => p.id === annotation.pickedAnnotationId) - } - ngOnDestroy(){ while (this.subs.length > 0) this.subs.pop().unsubscribe() } diff --git a/src/atlasComponents/userAnnotations/tools/point/point.component.ts b/src/atlasComponents/userAnnotations/tools/point/point.component.ts index 869a2c497bdfe169cf0f39589df4abea04fb546b..9e08437b5b803a17486bf84d6fc0ee0a80598cf3 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.component.ts +++ b/src/atlasComponents/userAnnotations/tools/point/point.component.ts @@ -1,12 +1,12 @@ import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Point, POINT_ICON_CLASS } from "../point"; -import { EXPORT_FORMAT_INJ_TOKEN, IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; +import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; import { Clipboard } from "@angular/cdk/clipboard"; import { ToolCmpBase } from "../toolCmp.base"; import { Store } from "@ngrx/store"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; -import { Observable, Subscription } from "rxjs"; +import { Subscription } from "rxjs"; @Component({ selector: 'point-update-cmp', @@ -39,18 +39,10 @@ export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ private store: Store<any>, snackbar: MatSnackBar, clipboard: Clipboard, - @Optional() @Inject(EXPORT_FORMAT_INJ_TOKEN) useFormat$: Observable<TExportFormats>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ super(clipboard, snackbar) - if (useFormat$) { - this.sub.push( - useFormat$.subscribe(val => { - this.useFormat = val - }) - ) - } if (updateAnnotation) { if (updateAnnotation instanceof Point) { this.updateAnnotation = updateAnnotation diff --git a/src/atlasComponents/userAnnotations/tools/point/point.template.html b/src/atlasComponents/userAnnotations/tools/point/point.template.html index 2fd1334d2f77af8dc16ed69754528891f4ed157b..c5fdf78056fa36c8011f50ce97e2c14d27715951 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.template.html +++ b/src/atlasComponents/userAnnotations/tools/point/point.template.html @@ -6,7 +6,7 @@ <i [class]="POINT_ICON_CLASS"></i> </button> - <form class="flex-grow-1 flex-shrink-1"> + <div class="flex-grow-1 flex-shrink-1"> <mat-form-field class="w-100"> <mat-label> {{ annotationLabel }} @@ -28,5 +28,5 @@ <i class="fas fa-copy"></i> </button> </mat-form-field> - </form> + </div> </div> diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index bbf19d6cdfed0ded3bdfe94fd7cc636ea814a8b1..42780797a569d548d7f6f73d466a88328f068118 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -47,11 +47,11 @@ export class Polygon extends IAnnotationGeometry{ const pointToBeAdded = p instanceof Point ? p : new Point({ - id: `${this.id}_${getUuid()}`, - space: this.space, - '@type': 'siibra-ex/annotatoin/point', - ...p - }) + id: `${this.id}_${getUuid()}`, + space: this.space, + '@type': 'siibra-ex/annotatoin/point', + ...p + }) if (!this.hasPoint(pointToBeAdded)) { this.points.push(pointToBeAdded) @@ -189,15 +189,13 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes public iconClass = POLY_ICON_CLASS public toolType: TToolType = 'drawing' - private space: TBaseAnnotationGeomtrySpec['space'] - private selectedPoly: Polygon private lastAddedPoint: Point private managedAnnotations: Polygon[] = [] public managedAnnotations$ = new Subject<Polygon[]>() - private subs: Subscription[] = [] + public subs: Subscription[] = [] private forceRefreshAnnotations$ = new Subject() public allNgAnnotations$ = new Subject<INgAnnotationTypes[keyof INgAnnotationTypes][]>() @@ -224,7 +222,7 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> ){ super(annotationEv$) - + this.init() const toolDeselect$ = this.toolSelected$.pipe( filter(flag => !flag) ) @@ -342,11 +340,6 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes this.forceRefreshAnnotations$.next(null) } - 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/poly/poly.component.ts b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts index e6c7347301d8dbf82c4ade5f00f38374655e3c39..3621c2d25717a64aa8b3cb85aaafd491dc5bb43b 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts @@ -2,11 +2,11 @@ import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } import { MatSnackBar } from "@angular/material/snack-bar"; import { Polygon, POLY_ICON_CLASS } from "../poly"; import { ToolCmpBase } from "../toolCmp.base"; -import { EXPORT_FORMAT_INJ_TOKEN, IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; +import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; import { Clipboard } from "@angular/cdk/clipboard"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Store } from "@ngrx/store"; -import { Observable, Subscription } from "rxjs"; +import { Subscription } from "rxjs"; @Component({ selector: 'poly-update-cmp', @@ -33,17 +33,9 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ private store: Store<any>, snackbar: MatSnackBar, clipboard: Clipboard, - @Optional() @Inject(EXPORT_FORMAT_INJ_TOKEN) useFormat$: Observable<TExportFormats>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ super(clipboard, snackbar) - if (useFormat$) { - this.sub.push( - useFormat$.subscribe(val => { - this.useFormat = val - }) - ) - } if (updateAnnotation) { if (updateAnnotation instanceof Polygon) { diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index a26ca35b991873e871be7a9663672a64f012f399..52dab491ec667660bf73c783f24d6b97dff5c93c 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -6,7 +6,7 @@ <i [class]="POLY_ICON_CLASS"></i> </button> - <form class="flex-grow-1 flex-shrink-1"> + <div class="flex-grow-1 flex-shrink-1"> <mat-form-field class="w-100"> <mat-label> {{ annotationLabel }} @@ -26,7 +26,7 @@ <i class="fas fa-copy"></i> </button> </mat-form-field> - </form> + </div> </div> <point-update-cmp diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index b4f302ad8e4a97b6ce7fa75a956690e78ab1a388..a48049574a75e15fadc82f9b01a9126ea3968a3b 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -8,11 +8,13 @@ import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; import { Polygon, ToolPolygon } from "./poly"; -import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TExportFormats } from "./type"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TExportFormats, TCallbackFunction } from "./type"; import { switchMapWaitFor } from "src/util/fn"; +import {Line, ToolLine} from "src/atlasComponents/userAnnotations/tools/line"; import { PolyUpdateCmp } from './poly/poly.component' import { Point, ToolPoint } from "./point"; import { PointUpdateCmp } from "./point/point.component"; +import { LineUpdateCmp } from "./line/line.component"; const IAV_VOXEL_SIZES_NM = { 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], @@ -91,14 +93,23 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private registeredTools: { name: string - iconClass: string, - target?: ClassInterface<IAnnotationGeometry>, - editCmp?: ClassInterface<any>, + iconClass: string + target?: ClassInterface<IAnnotationGeometry> + editCmp?: ClassInterface<any> onMouseMoveRenderPreview: (pos: [number, number, number]) => INgAnnotationTypes[keyof INgAnnotationTypes][] onDestoryCallBack: () => void }[] = [] private mousePosReal: [number, number, number] + private handleToolCallback: TCallbackFunction = arg => { + switch (arg.type) { + case 'paintingEnd': { + this.deselectTools() + return + } + } + } + /** * @description register new annotation tool * Some tools (deletion / dragging) may not have target and editCmp @@ -110,12 +121,12 @@ export class ModularUserAnnotationToolService implements OnDestroy{ * }} arg */ private registerTool<T extends AbsToolClass>(arg: { - toolCls: ClassInterface<T>, - target?: ClassInterface<IAnnotationGeometry>, + toolCls: ClassInterface<T> + target?: ClassInterface<IAnnotationGeometry> editCmp?: ClassInterface<any> }){ const { toolCls: Cls, target, editCmp } = arg - const newTool = new Cls(this.annotnEvSubj) as AbsToolClass & { ngOnDestroy?: Function } + const newTool = new Cls(this.annotnEvSubj, arg => this.handleToolCallback(arg)) as AbsToolClass & { ngOnDestroy?: Function } const { name, iconClass, onMouseMoveRenderPreview } = newTool this.moduleAnnotationTypes.push({ @@ -167,7 +178,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } /** - * + * * @description deregister tool. Calls any necessary clean up function * @param name name of the tool to be deregistered * @returns void @@ -181,8 +192,6 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } } - public exportFormat$ = new BehaviorSubject<TExportFormats>('string') - constructor( store: Store<any>, @Inject(INJ_ANNOT_TARGET) annotTarget$: Observable<HTMLElement>, @@ -196,6 +205,12 @@ export class ModularUserAnnotationToolService implements OnDestroy{ editCmp: PointUpdateCmp, }) + this.registerTool({ + toolCls: ToolLine, + target: Line, + editCmp: LineUpdateCmp, + }) + this.registerTool({ toolCls: ToolPolygon, target: Polygon, @@ -210,7 +225,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ switchMap(el => { if (!el) return of(null) return merge(...( - ['mousedown', 'mouseup', 'mousemove'].map(type => + ['mousedown', 'mouseup', 'mousemove'].map(type => fromEvent(el, type, { capture: true }).pipe( map((ev: MouseEvent) => { return { @@ -290,7 +305,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ switchMap(v => v?.mousePosInReal$ || of(null)) ).subscribe(v => this.mousePosReal = v) ) - + /** * on mouse move, render preview annotation */ @@ -316,7 +331,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) ) }) - ).subscribe((ev: { + ).subscribe((ev: { selectedToolName: string ngMouseEvent: {x: number, y: number, z: number} }) => { @@ -332,7 +347,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } const { onMouseMoveRenderPreview } = selectedTool const previewNgAnnotation = onMouseMoveRenderPreview([ngMouseEvent.x, ngMouseEvent.y, ngMouseEvent.z]) - + if (this.previewNgAnnIds.length !== previewNgAnnotation.length) { this.clearAllPreviewAnnotations() } @@ -433,7 +448,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } else { if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(false) } - }) + }) ) /** diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index d90d40ea836b1b5ee3593cac2224aa65fe8844fa..06f715bcbbeb6342f73228b49b89f12da0438265 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -1,5 +1,5 @@ import { InjectionToken } from "@angular/core" -import { merge, Observable, of, Subject } from "rxjs" +import { merge, Observable, of, Subject, Subscription } from "rxjs" import { filter, map, mapTo, pairwise, switchMap, switchMapTo, takeUntil, withLatestFrom } from 'rxjs/operators' import { getUuid } from "src/util/fn" @@ -14,12 +14,8 @@ export abstract class AbsToolClass { public abstract removeAnnotation(id: string): void public abstract managedAnnotations$: Observable<IAnnotationGeometry[]> - /** - * @description to be overwritten by subclass. Check if a given 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 + abstract subs: Subscription[] + protected space: TBaseAnnotationGeomtrySpec['space'] /** * @description to be overwritten by subclass. Emit the latest representation of NgAnnotations from the tool. @@ -27,18 +23,27 @@ export abstract class AbsToolClass { public abstract allNgAnnotations$: Observable<INgAnnotationTypes[keyof INgAnnotationTypes][]> /** - * @description to be overwritten by subclass. Called once every mousemove event, if the tool is active. - * @param {[number, number, number]} mousepos + * @description to be overwritten by subclass. Called once every mousemove event, if the tool is active. + * @param {[number, number, number]} mousepos * @returns {INgAnnotationTypes[keyof INgAnnotationTypes][]} Array of NgAnnotation to be rendered. */ public abstract onMouseMoveRenderPreview(mousepos: [number, number, number]): INgAnnotationTypes[keyof INgAnnotationTypes][] constructor( - protected annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> + protected annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, + protected callback?: TCallbackFunction ){ } + init(){ + this.subs.push( + this.metadataEv$.subscribe(ev => { + this.space = ev.detail.space + }) + ) + } + public toolSelected$ = this.annotationEv$.pipe( filter(ev => ev.type === 'toolSelect'), map(ev => (ev as TAnnotationEvent<'toolSelect'>).detail.name === this.name) @@ -75,10 +80,10 @@ export abstract class AbsToolClass { * on mouseover, then drag annotation * use mousedown as obs src, since hoverAnnotation$ is a bit trigger happy * check if there is a hit on mousedown trigger - * + * * if true - stop mousedown propagation, switchmap to mousemove - * if false - - * + * if false - + * */ protected dragHoveredAnnotation$: Observable<{ startNgX: number @@ -120,9 +125,9 @@ export abstract class AbsToolClass { * otherwise, pairwise confuses last drag event and first drag event */ protected dragHoveredAnnotationsDelta$: Observable<{ - ann: TAnnotationEvent<"hoverAnnotation">, - deltaX: number, - deltaY: number, + ann: TAnnotationEvent<"hoverAnnotation"> + deltaX: number + deltaY: number deltaZ: number }> = merge( of(null), @@ -150,6 +155,15 @@ export abstract class AbsToolClass { export type TToolType = 'translation' | 'drawing' | 'deletion' +export type TCallback = { + paintingEnd: { + callArg: {} + returns: void + } +} + +export type TCallbackFunction = <T extends keyof TCallback>(arg: TCallback[T]['callArg'] & { type: T }) => TCallback[T] | void + export type TBaseAnnotationGeomtrySpec = { id?: string space?: { @@ -322,7 +336,3 @@ export interface ClassInterface<T> { } export type TExportFormats = 'sands' | 'json' | 'string' - -export const EXPORT_FORMAT_INJ_TOKEN = new InjectionToken< - Observable<TExportFormats> ->('EXPORT_FORMAT_INJ_TOKEN') diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index a295e3f09f7c6c710e41a6686dbf6beaf64289c6..e98f09941fcc99b2a721c892167f806adf681137 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -370,6 +370,11 @@ markdown-dom p pointer-events: all; } +.t-a-ease-200 +{ + transition: all ease 200ms; +} + .t-a-ease-500 { transition: all ease 500ms;