Skip to content

Commit b67d79c

Browse files
committed
Optimize getShapesAtPoint with caching and refactor
Introduces a cache object to optimize repeated calls to getShapesAtPoint and refactors hit testing logic into static methods for improved clarity and performance. The changes streamline visibility checks and hit test traversal, reducing allocations and improving maintainability.
1 parent 5716739 commit b67d79c

File tree

2 files changed

+161
-101
lines changed

2 files changed

+161
-101
lines changed

src/group.js

Lines changed: 160 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ import { Sprite } from './effects/sprite.js';
2424
const min = Math.min,
2525
max = Math.max;
2626

27+
const cache = {
28+
getShapesAtPoint: {
29+
results: [],
30+
hitOptions: {},
31+
context: {
32+
x: 0,
33+
y: 0,
34+
visibleOnly: true,
35+
results: null,
36+
},
37+
single: [],
38+
output: [],
39+
empty: [],
40+
},
41+
};
42+
2743
/**
2844
* @name Two.Group
2945
* @class
@@ -363,6 +379,111 @@ export class Group extends Shape {
363379
}
364380
}
365381

382+
static IsVisible = function (element, visibleOnly) {
383+
if (!visibleOnly) {
384+
return true;
385+
}
386+
387+
let current = element;
388+
while (current) {
389+
if (typeof current.visible === 'boolean' && !current.visible) {
390+
return false;
391+
}
392+
if (typeof current.opacity === 'number' && current.opacity <= 0) {
393+
return false;
394+
}
395+
current = current.parent;
396+
}
397+
398+
return true;
399+
};
400+
401+
static VisitForHitTest = function (
402+
group,
403+
context,
404+
includeGroups,
405+
filter,
406+
hitOptions,
407+
tolerance,
408+
stopOnFirst
409+
) {
410+
const children = group && group.children;
411+
if (!children) {
412+
return false;
413+
}
414+
415+
const results = context.results;
416+
for (let i = children.length - 1; i >= 0; i--) {
417+
const child = children[i];
418+
419+
if (!child) {
420+
continue;
421+
}
422+
423+
if (!Group.IsVisible(child, context.visibleOnly)) {
424+
continue;
425+
}
426+
427+
const rect =
428+
typeof child.getBoundingClientRect === 'function'
429+
? child.getBoundingClientRect()
430+
: null;
431+
432+
if (rect && !boundsContains(rect, context.x, context.y, tolerance)) {
433+
continue;
434+
}
435+
436+
if (child instanceof Group) {
437+
if (
438+
includeGroups &&
439+
(!filter || filter(child)) &&
440+
typeof child.contains === 'function' &&
441+
child.contains(context.x, context.y, hitOptions)
442+
) {
443+
results.push(child);
444+
if (stopOnFirst) {
445+
return true;
446+
}
447+
}
448+
if (
449+
Group.VisitForHitTest(
450+
child,
451+
context,
452+
includeGroups,
453+
filter,
454+
hitOptions,
455+
tolerance,
456+
stopOnFirst
457+
)
458+
) {
459+
return true;
460+
}
461+
continue;
462+
}
463+
464+
if (!(child instanceof Shape)) {
465+
continue;
466+
}
467+
468+
if (filter && !filter(child)) {
469+
continue;
470+
}
471+
472+
if (typeof child.contains !== 'function') {
473+
continue;
474+
}
475+
476+
if (child.contains(context.x, context.y, hitOptions)) {
477+
results.push(child);
478+
if (stopOnFirst) {
479+
return true;
480+
}
481+
}
482+
}
483+
484+
return false;
485+
};
486+
366487
/**
367488
* @name Two.Group#copy
368489
* @function
@@ -490,127 +611,66 @@ export class Group extends Shape {
490611

491612
getShapesAtPoint(x, y, options) {
492613
const opts = options || {};
493-
const mode =
494-
opts.mode === 'deepest' || opts.deepest ? 'deepest' : 'all';
614+
const { results, hitOptions, context, single, empty } =
615+
cache.getShapesAtPoint;
616+
617+
results.length = 0;
618+
619+
const mode = opts.mode === 'deepest' || opts.deepest ? 'deepest' : 'all';
495620
const visibleOnly = opts.visibleOnly !== false;
496621
const includeGroups = !!opts.includeGroups;
497622
const filter = typeof opts.filter === 'function' ? opts.filter : null;
498-
const tolerance =
499-
typeof opts.tolerance === 'number' ? opts.tolerance : 0;
500-
501-
const hitOptions = {};
623+
const tolerance = typeof opts.tolerance === 'number' ? opts.tolerance : 0;
502624

503625
if (typeof opts.precision === 'number') {
504626
hitOptions.precision = opts.precision;
627+
} else {
628+
delete hitOptions.precision;
505629
}
506630
if (typeof opts.fill !== 'undefined') {
507631
hitOptions.fill = opts.fill;
632+
} else {
633+
delete hitOptions.fill;
508634
}
509635
if (typeof opts.stroke !== 'undefined') {
510636
hitOptions.stroke = opts.stroke;
637+
} else {
638+
delete hitOptions.stroke;
511639
}
512640
hitOptions.tolerance = tolerance;
513641
hitOptions.ignoreVisibility = !visibleOnly;
514642

515643
const stopOnFirst = mode === 'deepest';
516-
const results = [];
517-
518-
const isVisible = (element) => {
519-
if (!visibleOnly) {
520-
return true;
521-
}
522-
523-
let current = element;
524-
while (current) {
525-
if (typeof current.visible === 'boolean' && !current.visible) {
526-
return false;
527-
}
528-
if (
529-
typeof current.opacity === 'number' &&
530-
current.opacity <= 0
531-
) {
532-
return false;
533-
}
534-
current = current.parent;
535-
}
536-
537-
return true;
538-
};
539-
540-
const visit = (group) => {
541-
const children = group && group.children;
542-
if (!children) {
543-
return false;
544-
}
545-
546-
for (let i = children.length - 1; i >= 0; i--) {
547-
const child = children[i];
548-
549-
if (!child) {
550-
continue;
551-
}
552-
553-
if (!isVisible(child)) {
554-
continue;
555-
}
556-
557-
const rect =
558-
typeof child.getBoundingClientRect === 'function'
559-
? child.getBoundingClientRect()
560-
: null;
561-
562-
if (rect && !boundsContains(rect, x, y, tolerance)) {
563-
continue;
564-
}
565-
566-
if (child instanceof Group) {
567-
if (
568-
includeGroups &&
569-
(!filter || filter(child)) &&
570-
typeof child.contains === 'function' &&
571-
child.contains(x, y, hitOptions)
572-
) {
573-
results.push(child);
574-
if (stopOnFirst) {
575-
return true;
576-
}
577-
}
578-
if (visit(child)) {
579-
return true;
580-
}
581-
continue;
582-
}
583-
584-
if (!(child instanceof Shape)) {
585-
continue;
586-
}
587-
588-
if (filter && !filter(child)) {
589-
continue;
590-
}
591-
592-
if (typeof child.contains !== 'function') {
593-
continue;
594-
}
595-
596-
if (child.contains(x, y, hitOptions)) {
597-
results.push(child);
598-
if (stopOnFirst) {
599-
return true;
600-
}
601-
}
602-
}
603-
604-
return false;
605-
};
606-
607-
visit(this);
644+
context.x = x;
645+
context.y = y;
646+
context.visibleOnly = visibleOnly;
647+
context.results = results;
648+
649+
Group.VisitForHitTest(
650+
this,
651+
context,
652+
includeGroups,
653+
filter,
654+
hitOptions,
655+
tolerance,
656+
stopOnFirst
657+
);
608658

609659
if (stopOnFirst) {
610-
return results.length > 0 ? [results[0]] : [];
660+
if (results.length > 0) {
661+
const first = results[0];
662+
results.length = 0;
663+
single[0] = first;
664+
single.length = 1;
665+
return single;
666+
}
667+
empty.length = 0;
668+
return empty;
611669
}
612670

613-
return results;
671+
const hits = results.slice();
672+
results.length = 0;
673+
return hits;
614674
}
615675

616676
/**

utils/file-sizes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"development":"89KB","production":"48KB"}
1+
{"development":"90KB","production":"48KB"}

0 commit comments

Comments
 (0)