Skip to content

Commit ca50cba

Browse files
authored
Add way to find doodled elements, v1 (#8)
* Add way to find doodled elements, v1 Not integrated into rest of the code yet
1 parent 000abf8 commit ca50cba

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed

src/doodle/doodledOver.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const IGNORE_PERCENT_THRESHOLD = 0.8; // Ignore elements that cover at least this % of the region
2+
const SPLIT_SIZE = 10; // Split into 2 sub regions (vertically, horizontal shouldn't be common) when one contains this many elements
3+
4+
/**
5+
* Gets the top of the element, in pixels relative to the page.
6+
* @param {Element} elem
7+
* @returns {number}
8+
*/
9+
function elemTop(elem) {
10+
return elem.getBoundingClientRect().top + document.documentElement.scrollTop;
11+
}
12+
13+
/**
14+
* Calculates how much a given element overlaps the height band given
15+
* @param {number} top
16+
* @param {number} height
17+
* @param {Element} elem
18+
* @returns {number}
19+
*/
20+
function calculateOverlap(top, height, elem) {
21+
// Calculates vertical overlap of a height band + element
22+
// Start by finding elem top/bottom, removing any overflow of the band
23+
const eTop = Math.max(top, elemTop(elem));
24+
const eBottom = Math.min(
25+
top + height,
26+
elem.getBoundingClientRect().bottom + document.documentElement.scrollTop
27+
);
28+
29+
// Return ratios of heights
30+
return (eBottom - eTop) / height;
31+
}
32+
33+
class PointResolver {
34+
/**
35+
*
36+
* @param {Element[]} elements
37+
* @param {number} top
38+
* @param {number} bottom
39+
*/
40+
constructor(elements, top, bottom) {
41+
this.top = top;
42+
this.bottom = bottom;
43+
/** @type {PointResolver[]} */
44+
this.subResolvers = [];
45+
/** @type {Element[]} */
46+
this.elements = elements.filter(
47+
elem =>
48+
calculateOverlap(top, bottom - top, elem) >= IGNORE_PERCENT_THRESHOLD
49+
);
50+
51+
const minorElems = elements.filter(
52+
elem =>
53+
calculateOverlap(top, bottom - top, elem) < IGNORE_PERCENT_THRESHOLD
54+
);
55+
if (minorElems.length < SPLIT_SIZE) {
56+
this.elements = this.elements.concat(minorElems);
57+
} 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
61+
);
62+
const midway = Math.floor(sorted.length / 2);
63+
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+
),
74+
];
75+
}
76+
}
77+
/**
78+
* Returns all elements that the point lies inside of
79+
* @param {number} x x-coord of the point
80+
* @param {number} y y-coord of the point
81+
* @returns {Element[]}
82+
*/
83+
resolvePoint(x, y) {
84+
// If out of our range, then just return nothing.
85+
if (Math.ceil(y) < this.top || Math.floor(y) > this.bottom) {
86+
return [];
87+
}
88+
// Any sub-band should also be allowed to resolve
89+
/** @type {Element[]} */
90+
const subResolved = this.subResolvers.reduce(
91+
// @ts-ignore For some reason, it's trying to give arr the type never[]
92+
(arr, resolver) => arr.concat(resolver.resolvePoint(x, y)),
93+
[]
94+
);
95+
const containingElems = this.elements.filter(elem => {
96+
return (
97+
y > elemTop(elem) && // point lies below the top of elem
98+
y <
99+
elem.getBoundingClientRect().bottom +
100+
document.documentElement.scrollTop && // and above bottom
101+
x >
102+
elem.getBoundingClientRect().left +
103+
document.documentElement.scrollLeft && // to the right of left side
104+
x <
105+
elem.getBoundingClientRect().right +
106+
document.documentElement.scrollLeft
107+
); // to the left of right side
108+
});
109+
return subResolved.concat(containingElems);
110+
}
111+
}
112+
113+
// Hmm, not really a great class name, sadly.
114+
export class DoodleElementFinder {
115+
/**
116+
* @param {HTMLElement | null} root - Element containing the doodle. Defaults to the <body> element of the page.
117+
*/
118+
constructor(root) {
119+
if (root === null) {
120+
root = document.querySelector('body');
121+
}
122+
if (root === null) {
123+
throw Error("Couldn't find body element");
124+
}
125+
const allElements = root.querySelectorAll('*');
126+
this.resolver = new PointResolver(
127+
Array.from(allElements).filter(
128+
elem =>
129+
elem.getBoundingClientRect().width > 0 &&
130+
elem.getBoundingClientRect().height > 0
131+
),
132+
root.getBoundingClientRect().top + document.documentElement.scrollTop,
133+
root.getBoundingClientRect().bottom + document.documentElement.scrollTop
134+
);
135+
}
136+
137+
/**
138+
* Returns all Elements that the given point overlaps
139+
* @param {number} x
140+
* @param {number} y
141+
* @returns {Element[]}
142+
*/
143+
resolvePoint(x, y) {
144+
return this.resolver.resolvePoint(x, y);
145+
}
146+
147+
/**
148+
* Returns all Elements that the line overlaps
149+
* @param {import("../types/api").DoodleLine} line
150+
* @returns {Element[]}
151+
*/
152+
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();
157+
line.points.forEach(point => {
158+
this.resolvePoint(point[0], point[1]).forEach(elems.add);
159+
});
160+
return Array.from(elems);
161+
}
162+
}

0 commit comments

Comments
 (0)