Skip to content

Commit d34807b

Browse files
authored
Attach doodles to elements (#19)
1 parent 94ffb93 commit d34807b

File tree

5 files changed

+125
-33
lines changed

5 files changed

+125
-33
lines changed

src/annotator/guest.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
import * as rangeUtil from './range-util';
1717
import { SelectionObserver } from './selection-observer';
1818
import { normalizeURI } from './util/url';
19+
import { DoodleElementFinder } from '../doodle/doodledOver';
20+
import { xpathFromNode, nodeFromXPath } from './anchoring/xpath';
1921

2022
/**
2123
* @typedef {import('../types/annotator').AnnotationData} AnnotationData
@@ -825,11 +827,29 @@ export default class Guest extends Delegator {
825827
*/
826828
saveCurrentDoodle() {
827829
if (this.doodleCanvasController) {
830+
const controller = this.doodleCanvasController;
831+
const elemFinder = new DoodleElementFinder(controller.container);
832+
833+
this.doodleCanvasController.newLines.forEach(line => {
834+
// Set attached elem info
835+
const attachedElem = elemFinder.resolveLine(line);
836+
const attachedRect = attachedElem.getBoundingClientRect();
837+
line.elem.height = attachedRect.height;
838+
line.elem.width = attachedRect.width;
839+
line.elem.path = xpathFromNode(attachedElem, controller.container);
840+
// Fix line points
841+
const topOffset = attachedRect.top + document.documentElement.scrollTop;
842+
const leftOffset =
843+
attachedRect.left + document.documentElement.scrollLeft;
844+
line.points = line.points.map(pts => [
845+
pts[0] - leftOffset,
846+
pts[1] - topOffset,
847+
]);
848+
});
828849
this.createAnnotation({
829850
$doodle: true,
830851
doodleLines: this.doodleCanvasController.newLines,
831852
});
832-
// removed clearing lines from here because of bug when you try to save unsuccessfully (b/c not logged in) clearing your doodle
833853
}
834854
}
835855

@@ -848,6 +868,7 @@ export default class Guest extends Delegator {
848868
}
849869

850870
//load the lines into our doodleCanvasController.
871+
/** @type {import('../types/api').DoodleLine[]} */
851872
let newLines = [];
852873
for (let targ of doodleAnnotation.target) {
853874
for (let sel of targ.selector) {
@@ -856,6 +877,30 @@ export default class Guest extends Delegator {
856877
}
857878
}
858879
}
880+
// Re-calculate global coordinates for new line
881+
882+
newLines.forEach(line => {
883+
const attachedNode = nodeFromXPath(line.elem.path);
884+
if (
885+
attachedNode === null ||
886+
attachedNode.nodeType !== Node.ELEMENT_NODE
887+
) {
888+
return;
889+
}
890+
/** @type{Element} */
891+
// @ts-ignore we just checked it's an element
892+
const attachedElem = attachedNode;
893+
const attachedRect = attachedElem.getBoundingClientRect();
894+
const topOffset = attachedRect.top + document.documentElement.scrollTop;
895+
const leftOffset =
896+
attachedRect.left + document.documentElement.scrollLeft;
897+
const xMult = attachedRect.width / line.elem.width;
898+
const yMult = attachedRect.height / line.elem.height;
899+
line.points = line.points.map(([x, y]) => {
900+
return [x * xMult + leftOffset, y * yMult + topOffset];
901+
});
902+
});
903+
859904
this.doodleCanvasController.savedDoodles = [
860905
...this.doodleCanvasController.savedDoodles,
861906
{

src/doodle/doodleCanvas.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import propTypes from 'prop-types';
1111
* @prop {string} color - The color of the brush
1212
* @prop {HTMLElement} attachedElement - Which element the DoodleCanvas should cover.
1313
* @prop {Array<import('../types/api').DoodleLine>} lines - An array of lines that compose this doodle.
14-
* @prop {Function} setLines - A function to set the lines
15-
* @prop {Function} onUndo - a function called when undo key commands pressed
16-
* @prop {Function} onRedo - a function called when redo key commands pressed
14+
* @prop {(lines: import('../types/api').DoodleLine[]) => void} setLines - A function to set the lines
15+
* @prop {() => void} onUndo - a function called when undo key commands pressed
16+
* @prop {() => void} onRedo - a function called when redo key commands pressed
1717
*/
1818

1919
/**
@@ -84,6 +84,11 @@ const DoodleCanvas = ({
8484
tool: tool,
8585
color: color,
8686
size: size,
87+
elem: {
88+
path: '',
89+
height: 0,
90+
width: 0,
91+
},
8792
points: [[e.offsetX, e.offsetY]],
8893
},
8994
...lines,
@@ -109,10 +114,9 @@ const DoodleCanvas = ({
109114
const xPos = e.offsetX;
110115
const yPos = e.offsetY;
111116

117+
/** @type {import('../types/annotator').DoodleLine} */
112118
const newLine = {
113-
tool: curLine.tool,
114-
color: curLine.color,
115-
size: curLine.size,
119+
...curLine,
116120
points: [[xPos, yPos], ...curLine.points],
117121
};
118122

src/doodle/doodleController.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ export class DoodleController {
99
*/
1010
constructor(container, options, handleDoodleClick) {
1111
const { tool, size, color } = options;
12+
/** @type {import('../types/api').DoodleLine[]} */
13+
1214
this._lines = [];
15+
/** @type {import('../types/api').Doodle[]} */
16+
1317
this._savedDoodles = [];
18+
/** @type {import('../types/api').DoodleLine[]} */
1419
this._newLines = [];
20+
/** @type {import('../types/api').DoodleLine[]} */
21+
1522
this._redoLines = [];
1623

17-
this._container = container === null ? document.body : container;
24+
this.container = container === null ? document.body : container;
1825
this._tool = tool;
1926
this._size = size;
2027
this._color = color;
@@ -107,12 +114,14 @@ export class DoodleController {
107114

108115
undo() {
109116
if (this._newLines.length) {
117+
// @ts-ignore shift won't return undefined due to length check
110118
this._redoLines.push(this._newLines.shift());
111119
this.render();
112120
}
113121
}
114122
redo() {
115123
if (this._redoLines.length) {
124+
// @ts-ignore pop won't return undefined due to length check
116125
this._newLines = [this._redoLines.pop(), ...this._newLines];
117126
this.render();
118127
}
@@ -125,7 +134,7 @@ export class DoodleController {
125134
render(
126135
<Fragment>
127136
<DoodleCanvas
128-
attachedElement={this._container}
137+
attachedElement={this.container}
129138
size={this._size}
130139
tool={this._tool}
131140
active={this._doodleable}
@@ -138,7 +147,7 @@ export class DoodleController {
138147
<DisplayCanvas
139148
handleDoodleClick={this._handleDoodleClick}
140149
doodles={this.savedDoodles}
141-
container={this._container}
150+
container={this.container}
142151
showDoodles={this._showDoodles}
143152
/>
144153
</Fragment>,

src/doodle/doodledOver.js

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
const IGNORE_PERCENT_THRESHOLD = 0.8; // Ignore elements that cover at least this % of the region
1+
const APPEARANCE_THRESHOLD = 0.75; // How much of a line needs to appear in that element to consider it
2+
3+
const IGNORE_PERCENT_THRESHOLD = 0.5; // Ignore elements that cover at least this % of the region
24
const SPLIT_SIZE = 10; // Split into 2 sub regions (vertically, horizontal shouldn't be common) when one contains this many elements
35

6+
/**
7+
* Gets the area of the element
8+
* @param {Element} elem
9+
* @returns {number}
10+
*/
11+
function getArea(elem) {
12+
return (
13+
elem.getBoundingClientRect().width * elem.getBoundingClientRect().height
14+
);
15+
}
16+
417
/**
518
* Gets the top of the element, in pixels relative to the page.
619
* @param {Element} elem
@@ -55,22 +68,19 @@ class PointResolver {
5568
if (minorElems.length < SPLIT_SIZE) {
5669
this.elements = this.elements.concat(minorElems);
5770
} else {
58-
// Need to go to sub resolvers for minor elements. Just sort + take half in each
59-
const sorted = minorElems.sort(
60-
(a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top
71+
// Need to go to sub resolvers for minor elements.
72+
const midpoint = (top + bottom) / 2;
73+
const firstHalf = elements.filter(e => elemTop(e) < midpoint);
74+
const secondHalf = elements.filter(
75+
e =>
76+
e.getBoundingClientRect().bottom +
77+
document.documentElement.scrollTop >=
78+
midpoint
6179
);
62-
const midway = Math.floor(sorted.length / 2);
80+
6381
this.subResolvers = [
64-
new PointResolver(
65-
sorted.slice(0, midway),
66-
top,
67-
elemTop(sorted[midway])
68-
),
69-
new PointResolver(
70-
sorted.slice(midway, sorted.length),
71-
elemTop(sorted[midway]) + 1,
72-
bottom
73-
),
82+
new PointResolver(firstHalf, top, midpoint),
83+
new PointResolver(secondHalf, midpoint + 1, bottom),
7484
];
7585
}
7686
}
@@ -147,16 +157,33 @@ export class DoodleElementFinder {
147157
/**
148158
* Returns all Elements that the line overlaps
149159
* @param {import("../types/api").DoodleLine} line
150-
* @returns {Element[]}
160+
* @returns {Element}
151161
*/
152162
resolveLine(line) {
153-
// Resolve for each point in the line, removing duplicates.
154-
// In a later sprint we can revisit this and potentially ignore some elements that aren't common
155-
// but for now, this is good enough.
156-
let elems = new Set();
163+
// Map from element to count of times that element has been doodled over
164+
/** @type {Map<Element, number>} */
165+
let elems = new Map();
157166
line.points.forEach(point => {
158-
this.resolvePoint(point[0], point[1]).forEach(elems.add);
167+
this.resolvePoint(point[0], point[1]).forEach(elem => {
168+
if (!elems.has(elem)) {
169+
elems.set(elem, 0);
170+
}
171+
// @ts-ignore .get() can't be undefined, as we just set it.
172+
elems.set(elem, elems.get(elem) + 1);
173+
});
159174
});
160-
return Array.from(elems);
175+
// Sort elements by appearance count, largest to smallest
176+
const sortedElems = Array.from(elems.entries()).sort((a, b) => b[1] - a[1]);
177+
// Get ones that appear enough times, and map back down to only element, removing count
178+
const overAppearanceThreshold = sortedElems
179+
.filter(entry => entry[1] >= APPEARANCE_THRESHOLD * line.points.length)
180+
.map(elem => elem[0]);
181+
if (overAppearanceThreshold.length === 0) {
182+
return sortedElems[0][0];
183+
}
184+
185+
return overAppearanceThreshold.reduce((prev, curr) => {
186+
return getArea(prev) <= getArea(curr) ? prev : curr;
187+
}, overAppearanceThreshold[0]);
161188
}
162189
}

src/types/api.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,18 @@
4545
* @prop {Selector[]} [selector]
4646
*/
4747

48+
/**
49+
* @typedef DoodleElem
50+
* @prop {string} path
51+
* @prop {number} height
52+
* @prop {number} width
53+
*/
4854
/**
4955
* @typedef DoodleLine
5056
* @prop {string} tool
5157
* @prop {string} color
52-
* @prop {string} size
58+
* @prop {number} size
59+
* @prop {DoodleElem} elem
5360
* @prop {Array<Array<number>>} points
5461
*/
5562

0 commit comments

Comments
 (0)