Skip to content

Commit 5716739

Browse files
committed
Delegate getShapesAtPoint to Group and add tests
Moved the getShapesAtPoint logic from Two to Group, allowing scene groups to perform hit testing directly. Updated Two#getShapesAtPoint to delegate to the root scene group. Added and expanded tests to verify correct delegation and option handling. Updated type definitions to include Group#getShapesAtPoint.
1 parent 374f782 commit 5716739

File tree

4 files changed

+163
-125
lines changed

4 files changed

+163
-125
lines changed

src/group.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Events } from './events.js';
22
import { _ } from './utils/underscore.js';
33
import { getEffectFromObject } from './utils/shape.js';
4+
import { boundsContains } from './utils/hit-test.js';
45

56
import { Shape } from './shape.js';
67
import { Children } from './children.js';
@@ -487,6 +488,131 @@ export class Group extends Shape {
487488
return this;
488489
}
489490

491+
getShapesAtPoint(x, y, options) {
492+
const opts = options || {};
493+
const mode =
494+
opts.mode === 'deepest' || opts.deepest ? 'deepest' : 'all';
495+
const visibleOnly = opts.visibleOnly !== false;
496+
const includeGroups = !!opts.includeGroups;
497+
const filter = typeof opts.filter === 'function' ? opts.filter : null;
498+
const tolerance =
499+
typeof opts.tolerance === 'number' ? opts.tolerance : 0;
500+
501+
const hitOptions = {};
502+
503+
if (typeof opts.precision === 'number') {
504+
hitOptions.precision = opts.precision;
505+
}
506+
if (typeof opts.fill !== 'undefined') {
507+
hitOptions.fill = opts.fill;
508+
}
509+
if (typeof opts.stroke !== 'undefined') {
510+
hitOptions.stroke = opts.stroke;
511+
}
512+
hitOptions.tolerance = tolerance;
513+
hitOptions.ignoreVisibility = !visibleOnly;
514+
515+
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);
608+
609+
if (stopOnFirst) {
610+
return results.length > 0 ? [results[0]] : [];
611+
}
612+
613+
return results;
614+
}
615+
490616
/**
491617
* @name Two.Group#corner
492618
* @function

src/two.js

Lines changed: 7 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import * as math from './utils/math.js';
1010
import { Commands } from './utils/path-commands.js';
1111
import { _ } from './utils/underscore.js';
1212
import { xhr } from './utils/xhr.js';
13-
import { boundsContains } from './utils/hit-test.js';
1413

1514
// Core Classes
1615

@@ -549,131 +548,16 @@ export default class Two {
549548
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
550549
* @returns {Two.Shape[]} Ordered list of shapes under the specified point, front to back.
551550
* @description Returns shapes underneath the provided coordinates. Coordinates are expected in world space (matching the renderer output).
551+
* @nota-bene Delegates to {@link Two.Group#getShapesAtPoint} on the root scene.
552552
*/
553553
getShapesAtPoint(x, y, options) {
554-
const opts = options || {};
555-
const mode =
556-
opts.mode === 'deepest' || opts.deepest ? 'deepest' : 'all';
557-
const visibleOnly = opts.visibleOnly !== false;
558-
const includeGroups = !!opts.includeGroups;
559-
const filter =
560-
typeof opts.filter === 'function' ? opts.filter : null;
561-
const tolerance =
562-
typeof opts.tolerance === 'number' ? opts.tolerance : 0;
563-
const hitOptions = {};
564-
565-
if (typeof opts.precision === 'number') {
566-
hitOptions.precision = opts.precision;
567-
}
568-
if (typeof opts.fill !== 'undefined') {
569-
hitOptions.fill = opts.fill;
570-
}
571-
if (typeof opts.stroke !== 'undefined') {
572-
hitOptions.stroke = opts.stroke;
573-
}
574-
hitOptions.tolerance = tolerance;
575-
hitOptions.ignoreVisibility = !visibleOnly;
576-
577-
const stopOnFirst = mode === 'deepest';
578-
const results = [];
579-
580-
const isVisible = (element) => {
581-
if (!visibleOnly) {
582-
return true;
583-
}
584-
585-
let current = element;
586-
while (current) {
587-
if (typeof current.visible === 'boolean' && !current.visible) {
588-
return false;
589-
}
590-
if (
591-
typeof current.opacity === 'number' &&
592-
current.opacity <= 0
593-
) {
594-
return false;
595-
}
596-
current = current.parent;
597-
}
598-
599-
return true;
600-
};
601-
602-
const visit = (group) => {
603-
if (!group || !group.children) {
604-
return false;
605-
}
606-
607-
const children = group.children;
608-
for (let i = children.length - 1; i >= 0; i--) {
609-
const child = children[i];
610-
611-
if (!child) {
612-
continue;
613-
}
614-
615-
if (!isVisible(child)) {
616-
continue;
617-
}
618-
619-
const rect =
620-
typeof child.getBoundingClientRect === 'function'
621-
? child.getBoundingClientRect()
622-
: null;
623-
624-
if (rect && !boundsContains(rect, x, y, tolerance)) {
625-
continue;
626-
}
627-
628-
if (child instanceof Group) {
629-
if (includeGroups && typeof child.contains === 'function') {
630-
if (!filter || filter(child)) {
631-
if (child.contains(x, y, hitOptions)) {
632-
results.push(child);
633-
if (stopOnFirst) {
634-
return true;
635-
}
636-
}
637-
}
638-
}
639-
640-
if (visit(child)) {
641-
return true;
642-
}
643-
644-
continue;
645-
}
646-
647-
if (!(child instanceof Shape)) {
648-
continue;
649-
}
650-
651-
if (filter && !filter(child)) {
652-
continue;
653-
}
654-
655-
if (typeof child.contains !== 'function') {
656-
continue;
657-
}
658-
659-
if (child.contains(x, y, hitOptions)) {
660-
results.push(child);
661-
if (stopOnFirst) {
662-
return true;
663-
}
664-
}
665-
}
666-
667-
return false;
668-
};
669-
670-
visit(this.scene);
671-
672-
if (stopOnFirst) {
673-
return results.length > 0 ? [results[0]] : [];
554+
if (
555+
this.scene &&
556+
typeof this.scene.getShapesAtPoint === 'function'
557+
) {
558+
return this.scene.getShapesAtPoint(x, y, options);
674559
}
675-
676-
return results;
560+
return [];
677561
}
678562

679563
/**

tests/suite/hit-test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ QUnit.test('Shape.contains evaluates fill and stroke geometry', function (assert
4444
});
4545

4646
QUnit.test('Two#getShapesAtPoint respects options', function (assert) {
47-
assert.expect(4);
47+
assert.expect(6);
4848

4949
var two = new Two({ width: 400, height: 400, autostart: false });
5050

@@ -68,6 +68,12 @@ QUnit.test('Two#getShapesAtPoint respects options', function (assert) {
6868
var circleHits = two.getShapesAtPoint(100, 100);
6969
assert.ok(circleHits.indexOf(circle) > -1, 'Circle reported at its center.');
7070

71+
var sceneHits = two.scene.getShapesAtPoint(100, 100);
72+
assert.ok(
73+
sceneHits.indexOf(circle) > -1,
74+
'Scene group reports circle at its center.'
75+
);
76+
7177
var deepest = two.getShapesAtPoint(50, 4, { mode: 'deepest' });
7278
assert.deepEqual(deepest, [line], 'Deepest mode returns top-most shape only.');
7379

@@ -83,4 +89,12 @@ QUnit.test('Two#getShapesAtPoint respects options', function (assert) {
8389
allHits.indexOf(hidden) > -1,
8490
'Hidden shapes included when visibleOnly is false.'
8591
);
92+
93+
var sceneAll = two.scene.getShapesAtPoint(200, 200, {
94+
visibleOnly: false,
95+
});
96+
assert.ok(
97+
sceneAll.indexOf(hidden) > -1,
98+
'Scene group honours visibleOnly option.'
99+
);
86100
});

types.d.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,20 @@ declare module 'two.js/src/group' {
17511751
* to a new renderer.
17521752
*/
17531753
dispose(): Group;
1754+
/**
1755+
* @name Two.Group#getShapesAtPoint
1756+
* @function
1757+
* @param {Number} x - X coordinate in world space.
1758+
* @param {Number} y - Y coordinate in world space.
1759+
* @param {SceneHitTestOptions} [options]
1760+
* @returns {Shape[]} Ordered list of intersecting shapes, front to back.
1761+
* @description Traverse the group hierarchy and return shapes that contain the specified point.
1762+
*/
1763+
getShapesAtPoint(
1764+
x: number,
1765+
y: number,
1766+
options?: SceneHitTestOptions
1767+
): Shape[];
17541768
/**
17551769
* @name Two.Group#corner
17561770
* @function
@@ -1871,7 +1885,7 @@ declare module 'two.js/src/group' {
18711885
import { Children } from 'two.js/src/children';
18721886
import { Gradient } from 'two.js/src/effects/gradient';
18731887
import { Texture } from 'two.js/src/effects/texture';
1874-
import { BoundingBox } from 'two.js';
1888+
import { BoundingBox, SceneHitTestOptions } from 'two.js';
18751889
}
18761890
declare module 'two.js/src/renderers/canvas' {
18771891
/**

0 commit comments

Comments
 (0)