diff options
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/views/TagsView.scss | 4 | ||||
-rw-r--r-- | src/client/views/TagsView.tsx | 61 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx | 33 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 10 | ||||
-rw-r--r-- | src/client/views/search/FaceRecognitionHandler.tsx | 112 | ||||
-rw-r--r-- | src/fields/Doc.ts | 6 |
7 files changed, 117 insertions, 110 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 235f54fe8..af181b031 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -364,6 +364,7 @@ export class DocumentOptions { data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc'); face?: DOCt = new DocInfo('face document'); + faceDescriptor?: List<number>; columnHeaders?: List<SchemaHeaderField>; // headers for stacking views schemaHeaders?: List<SchemaHeaderField>; // headers for schema view dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index f7365a51b..24f9e86bc 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -24,6 +24,10 @@ align-items: center; } +.faceItem { + background-color: lightGreen; +} + .tagsView-suggestions-box { display: flex; flex-wrap: wrap; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 82c85ddf0..da1add55b 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -6,10 +6,10 @@ import React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { NumCast, StrCast } from '../../fields/Types'; +import { DocCast, NumCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -17,6 +17,7 @@ import { undoable } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; import './TagsView.scss'; import { DocumentView } from './nodes/DocumentView'; +import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; /** * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. @@ -158,6 +159,10 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { * @returns */ createTagCollection = () => { + if (!this._props.tagDoc) { + const face = FaceRecognitionHandler.FindUniqueFaceByName(this._props.tag); + return face ? Doc.MakeEmbedding(face) : undefined; + } // Get the documents that contain the tag. const newEmbeddings = TagItem.allDocsWithTag(this._props.tag).map(doc => Doc.MakeEmbedding(doc)); @@ -188,9 +193,13 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { this, e, () => { - const dragData = new DragManager.DocumentDragData([this.createTagCollection()]); - DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); - return true; + const dragCollection = this.createTagCollection(); + if (dragCollection) { + const dragData = new DragManager.DocumentDragData([dragCollection]); + DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); + return true; + } + return false; }, returnFalse, emptyFunction @@ -199,20 +208,20 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { }; render() { - setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection + this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection const tag = this._props.tag.replace(/^#/, ''); const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : ''; return ( - <div className="tagItem" onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this._ref}> + <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this._ref}> {metadata ? ( <span> <b style={{ fontSize: 'smaller' }}>{tag} </b> - {this._props.doc[metadata] as string} + {Field.toString(this._props.doc[metadata])} </span> ) : ( tag )} - {this.props.showRemoveUI && ( + {this.props.showRemoveUI && this._props.tagDoc && ( <IconButton tooltip="Remove tag" onPointerDown={undoable(() => TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)} @@ -234,12 +243,9 @@ interface TagViewProps { */ @observer export class TagsView extends ObservableReactComponent<TagViewProps> { - private _ref: React.RefObject<HTMLDivElement>; - constructor(props: any) { super(props); makeObservable(this); - this._ref = React.createRef(); } @observable _currentInput = ''; @@ -268,18 +274,26 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { * just the tag. * @param tag tag string to add */ - submitTag = undoable((tag: string) => { - const submittedLabel = tag.trim(); - submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); - this._currentInput = ''; // Clear the input box - }, 'added doc label'); + submitTag = undoable( + action((tag: string) => { + const submittedLabel = tag.trim(); + submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); + this._currentInput = ''; // Clear the input box + }), + 'added doc label' + ); /** * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc). * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed. */ render() { - const tagsList = StrListCast(this._props.View.dataDoc.tags); + const tagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags)); + const facesList = new Set<string>( + DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations']) + .filter(d => d.face) + .map(doc => StrCast(DocCast(doc.face)?.title)) + ); return !this._props.View.Document.showTags ? null : ( <div @@ -295,11 +309,14 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { }}> <div className="tagsView-content" style={{ width: '100%' }}> <div className="tagsView-list"> - {!tagsList.length ? null : ( // + {!tagsList.size ? null : ( // <IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} /> )} - {tagsList.map(tag => ( - <TagItem key={tag} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> + {Array.from(tagsList).map((tag, i) => ( + <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> + ))} + {Array.from(facesList).map((tag, i) => ( + <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> ))} </div> {this.isEditing ? ( @@ -330,7 +347,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { color={SnappingManager.userVariantColor} tooltip="Add existing tag" onClick={() => this.submitTag(tag)} - key={i} + key={tag} /> ); })} diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index ea1c22962..c62303dc0 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -9,8 +9,9 @@ import React from 'react'; import { DivHeight, lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { List } from '../../../../fields/List'; -import { ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; @@ -87,23 +88,27 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ?.filter(doc => doc.type === DocumentType.IMG) .forEach(imgDoc => { // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? - if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { + if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).map(fd => new Float32Array(Array.from(fd))); const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this.Document), faceDescriptorsAsFloat32Array); const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1); - const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce( - (prev, face) => { - const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(face))); - return match.distance < prev.dist ? { dist: match.distance, face_match: face } : prev; + const { faceAnno } = FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).reduce( + (prev, faceAnno) => { + const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(faceAnno.faceDescriptor as List<number>))); + return match.distance < prev.dist ? { dist: match.distance, faceAnno } : prev; }, - { dist: 1, face_match: undefined as Opt<List<number>> } + { dist: 1, faceAnno: undefined as Opt<Doc> } ); // assign the face in the image that's closest to the face collection's face - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this.Document); + if (faceAnno) { + FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face)); + FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document); + faceAnno.face = this.Document; + } } }); e.stopPropagation(); @@ -224,7 +229,15 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { } moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); - + addDocument = (doc: Doc | Doc[], annotationKey?: string) => { + const uniqueFaceDoc = doc instanceof Doc ? doc : doc[0]; + const added = uniqueFaceDoc.type === DocumentType.UFACE; + if (added) { + Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); + Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); + } + return added; + }; /** * this changes style provider requests that target the dashboard to requests that target the face collection box which is what's actually being rendered. * This is needed, for instance, to get the default background color from the face collection, not the dashboard. @@ -233,6 +246,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { if (doc === Doc.ActiveDashboard) return this._props.styleProvider?.(this.Document, this._props, property); return this._props.styleProvider?.(doc, this._props, property); }; + render() { return !Doc.ActiveDashboard ? null : ( <div className="faceCollectionBox"> @@ -245,6 +259,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { Document={Doc.ActiveDashboard} fieldKey="myUniqueFaces" moveDocument={this.moveDocument} + addDocument={this.addDocument} isContentActive={returnTrue} isAnyChildContentActive={returnTrue} childHideDecorations={true} diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 6eb3eb784..5d3154e3c 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -51,7 +51,7 @@ export class ImageLabelBoxData { label = label.toUpperCase().trim(); if (label.length > 0) { if (!this._labelGroups.includes(label)) { - this._labelGroups = [...this._labelGroups, label]; + this._labelGroups = [...this._labelGroups, label.startsWith('#') ? label : '#' + label]; } } }; @@ -178,8 +178,12 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const labels = imageInfo.labels.split('\n'); labels.forEach(label => { - label = label.replace(/^\d+\.\s*|-|f\*/, '').trim(); - console.log(label); + label = + '#' + + label + .replace(/^\d+\.\s*|-|f\*/, '') + .replace(/^#/, '') + .trim(); imageInfo.doc[DocData][label] = true; (imageInfo.doc[DocData].tags as List<string>).push(label); }); diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 021802061..4c10307e6 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -1,15 +1,14 @@ import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { ComputedField } from '../../../fields/ScriptField'; +import { DocCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { ComputedField } from '../../../fields/ScriptField'; /** * A singleton class that handles face recognition and manages face Doc collections for each face found. @@ -18,16 +17,17 @@ import { ComputedField } from '../../../fields/ScriptField'; * as a face collection Doc). If the face matches a face collection Doc, then it will be added to that * collection along with the numerical representation of the face, its face descriptor. * - * Image Doc's that are added to one or more face collection Docs will be given these metadata fields: - * <image data field>_faceDescriptors - list of all the numerical face representations found in the image. - * <image data field>_faces - list of unique face Docs corresponding to recognized faces in the image. + * Image Doc's that are added to one or more face collection Docs will be given an annotation rectangle that + * highlights where the face is, and the annotation will have these fields: + * faceDescriptor - the numerical face representations found in the image. + * face - the unique face Docs corresponding to recognized face in the image. + * annotationOn - the image where the face was found * * unique face Doc's are created for each person identified and are stored in the Dashboard's myUniqueFaces field * * Each unique face Doc represents a unique face and collects all matching face images for that person. It has these fields: * face - a string label for the person that was recognized (TODO: currently it's just a 'face#') - * face_descriptors - a list of all the face descriptors for different images of the person - * face_docList - a list of all image Docs that contain a face for the person + * face_annos - a list of face annotations, where each anno has */ export class FaceRecognitionHandler { static _instance: FaceRecognitionHandler; @@ -55,41 +55,11 @@ export class FaceRecognitionHandler { }; /** - * return the metadata field name where unique face Docs are stored - * @param imgDoc image with faces - * @returns name of field - */ - private static ImageDocFaceField = (imgDoc: Doc) => `${Doc.LayoutFieldKey(imgDoc)}_faces`; - - /** * Returns an array of faceDocs for each face recognized in the image * @param imgDoc image with faces * @returns faceDoc array */ - private static ImageDocFaces = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutFieldKey(imgDoc)}_faces`]); - - /** - * initializes an image with an empty list of face descriptors - * @param imgDoc image to initialize - */ - private static initImageDocFaceDescriptors = (imgDoc: Doc) => { - imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`] = new List<List<number>>(); - }; - /** - * returns the face descriptors for each face found on an image Doc - * @param imgDoc - * @returns list of face descriptors - */ - public static ImageDocFaceDescriptors = (imgDoc: Doc) => imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`] as List<List<number>>; - - /** - * Adds a face descriptor for a face found in an image - * @param imgDoc image Doc with face - * @param faceDescriptor descriptor of a face - */ - public static ImageDocAddFaceDescriptor = (imgDoc: Doc, faceDescriptor: List<number>) => { - Cast(imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`], listSpec('number'), null).push(faceDescriptor as unknown as number); - }; + public static ImageDocFaceAnnos = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutFieldKey(imgDoc)}_annotations`]).filter(doc => doc.face); /** * returns a list of all face collection Docs on the current dashboard @@ -98,6 +68,13 @@ export class FaceRecognitionHandler { public static UniqueFaces = () => DocListCast(Doc.ActiveDashboard?.[DocData].myUniqueFaces); /** + * Find a unique face from its name + * @param name name of unique face + * @returns unique face or undefined + */ + public static FindUniqueFaceByName = (name: string) => FaceRecognitionHandler.UniqueFaces().find(faceDoc => faceDoc.title === name); + + /** * Removes a unique face from the set of recognized unique faces * @param faceDoc unique face Doc * @returns @@ -117,28 +94,23 @@ export class FaceRecognitionHandler { * @param faceDoc unique face Doc * @returns face descriptors */ - public static UniqueFaceDescriptors = (faceDoc: Doc) => faceDoc[DocData].face_descriptors as List<List<number>>; + public static UniqueFaceDescriptors = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_annos).map(face => face.faceDescriptor as List<number>); /** * Returns a list of all face image Docs associated with a unique face Doc * @param faceDoc unique face Doc * @returns image Docs */ - public static UniqueFaceImages = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_images); + public static UniqueFaceImages = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_annos).map(face => DocCast(face.annotationOn)); /** * Adds a face image to a unique face Doc, adds the unique face Doc to the images list of reognized faces, * and updates the unique face's set of face image descriptors * @param img - image with faces to add to a face collection Doc - * @param faceDescriptor - the face descriptor for the face in the image to add - * @param faceDoc - unique face Doc + * @param faceAnno - a face annotation */ - public static UniqueFaceAddFaceImage = (img: Doc, faceDescriptor: Opt<List<number>>, faceDoc: Doc) => { - Doc.AddDocToList(faceDoc, 'face_images', img); - if (faceDescriptor) { - Cast(faceDoc.face_descriptors, listSpec('number'), null).push(faceDescriptor as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that - Doc.AddDocToList(img[DocData], FaceRecognitionHandler.ImageDocFaceField(img), faceDoc); - } + public static UniqueFaceAddFaceImage = (faceAnno: Doc, faceDoc: Doc) => { + Doc.AddDocToList(faceDoc, 'face_annos', faceAnno); }; /** @@ -146,11 +118,9 @@ export class FaceRecognitionHandler { * @param img - image with faces to remove * @param faceDoc - unique face Doc */ - public static UniqueFaceRemoveFaceImage = (img: Doc, faceDoc: Doc) => { - Doc.RemoveDocFromList(faceDoc[DocData], 'face_images', img); - const descriptorsEqual = (a: List<number>, b: List<number>) => (a === b ? true : a.length === b.length ? a.every((element, index) => element === b[index]) : false); - faceDoc[DocData].face_descriptors = new List<List<number>>(FaceRecognitionHandler.UniqueFaceDescriptors(faceDoc).filter(fd => !FaceRecognitionHandler.ImageDocFaceDescriptors(img).some(desc => descriptorsEqual(fd, desc)))); - Doc.RemoveDocFromList(img[DocData], FaceRecognitionHandler.ImageDocFaceField(img), faceDoc); + public static UniqueFaceRemoveFaceImage = (faceAnno: Doc, faceDoc: Doc) => { + Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', faceAnno); + faceAnno.face = undefined; }; constructor() { @@ -192,8 +162,7 @@ export class FaceRecognitionHandler { }); const uface = uniqueFaceDoc[DocData]; uface.face = `Face${faceDocNum}`; - uface.face_images = new List<Doc>(); - uface.face_descriptors = new List<List<number>>(); + uface.face_annos = new List<Doc>(); Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); @@ -243,7 +212,6 @@ export class FaceRecognitionHandler { if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); - FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc); FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces img => faceapi @@ -255,22 +223,20 @@ export class FaceRecognitionHandler { const scale = NumCast(imgDoc.data_nativeWidth) / img.width; imgDocFaceDescriptions.forEach((fd, i) => { const faceDescriptor = new List<number>(Array.from(fd.descriptor)); - FaceRecognitionHandler.ImageDocAddFaceDescriptor(imgDoc, faceDescriptor); // add face descriptor to image's list of descriptors const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard); - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, faceDescriptor, matchedUniqueFace); // add image/faceDescriptor to matched unique face - annos.push( - Docs.Create.FreeformDocument([], { - title: ComputedField.MakeFunction(`this.face.face`, undefined, undefined, 'this.face.face = value') as unknown as string, // - annotationOn: imgDoc, - face: matchedUniqueFace[DocData], - backgroundColor: '#55555555', - x: fd.alignedRect.box.left * scale, - y: fd.alignedRect.box.top * scale, - _width: fd.alignedRect.box.width * scale, - _height: fd.alignedRect.box.height * scale, - }) - ); - Doc.SetContainer(annos.lastElement(), imgDoc[DocData]); + const faceAnno = Docs.Create.FreeformDocument([], { + title: ComputedField.MakeFunction(`this.face.face`, undefined, undefined, 'this.face.face = value') as unknown as string, // + annotationOn: imgDoc, + face: matchedUniqueFace[DocData], + faceDescriptor: faceDescriptor, + backgroundColor: 'transparent', + x: fd.alignedRect.box.left * scale, + y: fd.alignedRect.box.top * scale, + _width: fd.alignedRect.box.width * scale, + _height: fd.alignedRect.box.height * scale, + }) + FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, matchedUniqueFace); // add image/faceDescriptor to matched unique face + annos.push(faceAnno); }); imgDoc[DocData].data_annotations = new List<Doc>(annos); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e3178f20c..7abba7679 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -791,9 +791,9 @@ export namespace Doc { linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); - if (Doc.Get(copy, 'title', true)) copy.title = '>:' + doc.title; - // Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); copy.cloneOf = doc; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc.title)); + if (Doc.Get(copy, 'title', true) && !(cfield instanceof ComputedField)) copy.title = '>:' + doc.title; cloneMap.set(doc[Id], copy); return copy; @@ -1424,7 +1424,7 @@ export namespace Doc { export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { DocServer.GetRefFields(docids).then(async fieldlist => { - const list = Array.from(Object.values(fieldlist)) + const list = Array.from(fieldlist.values()) .map(d => DocCast(d)) .filter(d => d); const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list; |