/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey * http://juerglehni.com/ & https://puckey.studio/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ QUnit.module('HitResult'); test('Hit-testing options', function() { var defaultOptions = { type: null, tolerance: paper.settings.hitTolerance, fill: true, stroke: true, segments: true, handles: false, ends: false, position: false, center: false, bounds: false, guides: false, selected: false }; equals(HitResult.getOptions(), defaultOptions, 'Default options'); }); function testHitResult(hitResult, expeced, message) { equals(!!hitResult, !!expeced, message ? message : expeced ? 'A HitResult should be returned.' : 'No HitResult should be returned.'); if (hitResult && expeced) { if (expeced.type) { equals(hitResult.type, expeced.type, 'hitResult.type == \'' + expeced.type + '\''); } if (expeced.item) { equals(hitResult.item == expeced.item, true, 'hitResult.item == ' + expeced.item); } if (expeced.name) { equals(hitResult.name, expeced.name, 'hitResult.name == \'' + expeced.name + '\''); } if (expeced.point) { equals(hitResult.point.toString(), expeced.point.toString(), 'hitResult.point == \'' + expeced.point + '\''); } if (expeced.segment) { equals(hitResult.segment == expeced.segment, true, 'hitResult.segment == ' + expeced.segment); } } } test('hitting a filled shape', function() { var path = new Path.Circle([50, 50], 50); var hitResult = path.hitTest([75, 75]); testHitResult(path.hitTest([75, 75]), null, 'Since the path is not filled, the hit-test should return null.'); path.fillColor = 'red'; testHitResult(path.hitTest([75, 75]), { type: 'fill', item: path }); }); test('the item on top should be returned', function() { var path = new Path.Circle([50, 50], 50); path.fillColor = 'red'; // The cloned path is lying above the path: var copy = path.clone(); testHitResult(paper.project.hitTest([75, 75]), { type: 'fill', item: copy }); }); test('hitting a stroked path', function() { var path = new Path([0, 0], [50, 0]); // We are hit-testing with an offset of 5pt on a path with a stroke width // of 10: testHitResult(paper.project.hitTest([25, 5]), null, 'Since the path is not stroked yet, the hit-test should return null.'); path.strokeColor = 'black'; path.strokeWidth = 10; testHitResult(path.hitTest([25, 5]), { type: 'stroke', item: path }); }); test('hitting a selected path', function() { var path = new Path.Circle([50, 50], 50); path.fillColor = 'red'; testHitResult(paper.project.hitTest([75, 75], { selected: true }), null, 'Since the path is not stroked yet, the hit-test should return null.'); path.selected = true; testHitResult(paper.project.hitTest([75, 75]), { type: 'fill', item: path }); }); test('hitting path segments', function() { var path = new Path([0, 0], [10, 10], [20, 0]); testHitResult(paper.project.hitTest([10, 10]), { type: 'segment', item: path }); }); test('hitting the center and position of a path', function() { var path = new Path([0, 0], [100, 100], [200, 0]); path.closed = true; var center = path.bounds.center, position = path.position, positionResult = { type: 'position', item: path, point: position }, centerResult = { type: 'center', item: path, point: center }; testHitResult(paper.project.hitTest(position, { center: true }), centerResult); var offset = new Point(1, 1); testHitResult(paper.project.hitTest(position.add(offset), { tolerance: offset.length, center: true }), centerResult, 'position with tolerance'); testHitResult(paper.project.hitTest(position, { position: true }), positionResult); testHitResult(paper.project.hitTest(center, { position: true }), positionResult); path.pivot = [100, 100]; testHitResult(paper.project.hitTest(center, { position: true }), null, 'with pivot, the position should not be in the center'); testHitResult(paper.project.hitTest(path.position, { position: true }), { type: 'position', item: path, point: path.position }); }); test('hitting path handles (1)', function() { var path = new Path.Circle(new Point(), 10); path.firstSegment.handleIn = [-50, 0]; path.firstSegment.handleOut = [50, 0]; var firstPoint = path.firstSegment.point; testHitResult(paper.project.hitTest(firstPoint.add(50, 0), { handles: true }), { type: 'handle-out', item: path }); testHitResult(paper.project.hitTest(firstPoint.add(-50, 0), { handles: true }), { type: 'handle-in', item: path }); }); test('hitting path handles (2)', function() { var path = new Path(new Segment({ point: [0, 0], handleIn: [-50, -50], handleOut: [50, 50] })); testHitResult(paper.project.hitTest([50, 50], { handles: true }), { type: 'handle-out', item: path }); testHitResult(paper.project.hitTest([-50, -50], { handles: true }), { type: 'handle-in', item: path }); }); test('hit-testing stroke on segment point of a path', function() { var path = new Path([0, 0], [50, 50], [100, 0]); path.strokeColor = 'black'; path.closed = true; var error = null; try { var hitResult = paper.project.hitTest(path.firstSegment.point, { stroke: true }); } catch (e) { error = e; } var description = 'This hit-test should not throw an error'; if (error) description += ': ' + error; equals(error == null, true, description); }); test('hit-testing a point that is extremely close to a curve', function() { var path = new Path.Rectangle([0, 0], [100, 100]); // A point whose x value is extremely close to 0: var point = new Point(2.842 / Math.pow(10, 14), 0); var error; try { var hitResult = path.hitTest(point, { stroke: true }); } catch(e) { error = e; } var description = 'This hit-test should not throw an error'; if (error) description += ': ' + error; equals(error == null, true, description); }); test('hitting path ends', function() { var path = new Path([0, 0], [50, 50], [100, 0]); path.closed = true; equals(function() { return !paper.project.hitTest(path.firstSegment.point, { ends: true }); }, true, 'No HitResult should be returned, because the path is closed.'); path.closed = false; testHitResult(paper.project.hitTest(path.lastSegment.point, { ends: true }), { type: 'segment', item: path, segment: path.lastSegment }); equals(function() { return !paper.project.hitTest(path.segments[1].point, { ends: true }); }, true, 'No HitResult should be returned, since the second segment is not an end'); }); test('When a path is closed, the end of a path cannot be hit.', function() { var path = new Path([0, 0], [50, 50], [100, 0]); path.closed = true; var hitResult = paper.project.hitTest([0, 0], { ends: true }); equals(function() { return !hitResult; }, true, 'When a path is closed, the end of a path cannot be hit.'); }); test('hitting path bounding box', function() { var path = new Path.Circle({ center: [100, 100], radius: 50, fillColor: 'red' }); testHitResult(paper.project.hitTest(path.bounds.topLeft, { bounds: true }), { type: 'bounds', item: path, name: 'top-left', point: path.bounds.topLeft }); }); test('hitting raster bounding box', function() { var path = new Path.Circle({ center: [100, 100], radius: 50, fillColor: 'red' }); var raster = path.rasterize(72); path.remove(); testHitResult(paper.project.hitTest(raster.bounds.topLeft, { bounds: true }), { type: 'bounds', item: raster, name: 'top-left', point: path.bounds.topLeft }); }); test('hitting guides', function() { var path = new Path.Circle({ center: [100, 100], radius: 50, fillColor: 'red' }); var copy = path.clone(); var result = paper.project.hitTest(path.position); equals(result && result.item, copy, 'The copy should be returned, because it is on top.'); path.guide = true; var result = paper.project.hitTest(path.position, { guides: true, fill: true }); equals(result && result.item, path, 'The path should be returned, because it is a guide.'); }); test('hitting raster items', function() { // Create a path, rasterize it and then remove the path: var path = new Path.Rectangle(new Point(), new Size(320, 240)); path.fillColor = 'red'; var raster = path.rasterize(72); var hitResult = paper.project.hitTest(new Point(160, 120)); equals(function() { return hitResult && hitResult.item == raster; }, true, 'Hit raster item before moving'); // Move the raster: raster.translate(100, 100); var hitResult = paper.project.hitTest(new Point(160, 120)); equals(function() { return hitResult && hitResult.item == raster; }, true, 'Hit raster item after moving'); }); test('hitting path with a text item in the project', function() { var path = new Path.Rectangle(new Point(50, 50), new Point(100, 100)); path.fillColor = 'blue'; var hitResult = paper.project.hitTest(new Point(75, 75)); equals(function() { return hitResult && hitResult.item == path; }, true, 'Hit path item before adding text item'); var text1 = new PointText(30, 30); text1.content = "Text 1"; var hitResult = paper.project.hitTest(new Point(75, 75)); equals(function() { return !!hitResult; }, true, 'A hitresult should be returned.'); equals(function() { return !!hitResult && hitResult.item == path; }, true, 'We should have hit the path'); }); test('hit-testing of items that come after a transformed group.', function() { paper.project.currentStyle.fillColor = 'black'; var point1 = new Point(100, 100); var point2 = new Point(140, 100); var delta = new Point(250, 0); var path1 = new Path.Circle(point1, 20); path1.name = 'path1'; var path2 = new Path.Circle(point2, 20); path2.name = 'path2'; var group = new Group(path2); group.name = 'group'; var hitResult = paper.project.hitTest(point1); equals(function() { return hitResult && hitResult.item; }, path1, 'Hit testing project for point1 should give us path1.'); hitResult = paper.project.hitTest(point2); equals(function() { return hitResult && hitResult.item; }, path2, 'Hit testing project for point2 should give us path2.'); hitResult = paper.project.hitTest(point2); equals(function() { return hitResult && hitResult.item; }, path2, 'Hit testing project for point2 should give us path2.'); group.translate(delta); hitResult = paper.project.hitTest(point1); equals(function() { return hitResult && hitResult.item; }, path1, 'After translating group, hit-testing project for point1 should give us path1.'); hitResult = paper.project.hitTest(point2.add(delta)); equals(function() { return hitResult && hitResult.item; }, path2, 'After translating group, hit-testing project for point2 + delta should give us path2.'); hitResult = path1.hitTest(point1); equals(function() { return hitResult && hitResult.item; }, path1, 'After translating group, hit-testing path1 for point1 should give us path1.'); group.moveBelow(path1); hitResult = paper.project.hitTest(point1); equals(function() { return hitResult && hitResult.item; }, path1, 'After moving group before path1, hit-testing project for point1 should give us path1.'); hitResult = paper.project.hitTest(point2.add(delta)); equals(function() { return hitResult && hitResult.item; }, path2, 'After moving group before path1, hit-testing project for point2 + delta should give us path2.'); hitResult = path1.hitTest(point1); equals(function() { return hitResult && hitResult.item; }, path1, 'After moving group before path1, hit-testing path1 for point1 should give us path1.'); }); test('hit-testing of placed symbols.', function() { var point = new Point(100, 100); var path = new Path.Circle([0, 0], 20); path.fillColor = 'black'; var definition = new SymbolDefinition(path); var placedItem = definition.place(point); var hitResult = placedItem.hitTest(point); equals(function() { return hitResult && hitResult.item == placedItem; }, true, 'hitResult.item should be placedItem'); }); test('hit-testing the corner of a rectangle with miter stroke.', function() { var rect = new Path.Rectangle({ rectangle: [100, 100, 300, 200], fillColor: '#f00', strokeColor: 'blue', strokeJoin: 'miter', strokeWidth: 20 }); equals(function() { return rect.hitTest(rect.strokeBounds.topRight) != null; }, true); }); test('hit-testing invisible items.', function() { var point = new Point(0, 0); var circle1 = new Path.Circle({ center: point.subtract([25, 0]), radius: 50, fillColor: 'red' }); var circle2 = new Path.Circle({ center: point.add([25, 0]), radius: 50, fillColor: 'blue' }); equals(function() { return paper.project.hitTest(point).item === circle2; }, true); circle2.visible = false; equals(function() { return paper.project.hitTest(point).item === circle1; }, true); }); test('hit-testing guides.', function() { var point = new Point(0, 0); var circle1 = new Path.Circle({ center: point.subtract([25, 0]), radius: 50, fillColor: 'red' }); var circle2 = new Path.Circle({ center: point.add([25, 0]), radius: 50, fillColor: 'blue' }); var strokePoint = circle2.bounds.leftCenter; equals(function() { return paper.project.hitTest(strokePoint).item === circle2; }, true); circle2.guide = true; equals(function() { return paper.project.hitTest(strokePoint).item === circle1; }, true); equals(function() { var result = paper.project.hitTest(strokePoint, { guides: true, fill: true }); return result && result.item === circle2; }, true); }); test('hit-testing fills with tolerance', function() { var path = new Path.Rectangle({ from: [50, 50], to: [200, 200], fillColor: 'red' }); var tolerance = 10; var point = path.bounds.bottomRight.add(tolerance / Math.SQRT2); equals(function() { var result = paper.project.hitTest(point, { tolerance: tolerance, fill: true }); return result && result.item === path; }, true); var point = new Point(20, 20); var size = new Size(40, 40); var hitPoint = new Point(10, 10); var options = { fill: true, tolerance: 20 }; var shapeRect = new Shape.Rectangle(point, size); shapeRect.fillColor = 'black'; var pathRect = new Path.Rectangle(point, size); pathRect.fillColor = 'black'; equals(function() { var hit = shapeRect.hitTest(hitPoint, options); return hit && hit.type === 'fill'; }, true); equals(function() { var hit = pathRect.hitTest(hitPoint, options); return hit && hit.type === 'fill'; }, true); }); test('hit-testing compound-paths', function() { var center = new Point(100, 100); var path1 = new Path.Circle({ center: center, radius: 100 }); var path2 = new Path.Circle({ center: center, radius: 50 }); var compoundPath = new CompoundPath({ children: [path1, path2], fillColor: 'blue', fillRule: 'evenodd' }); // When hit-testing a side, we should get a result on the torus equals(function() { var result = paper.project.hitTest(center.add([75, 0]), { fill: true }); return result && result.item === compoundPath; }, true); // When hit-testing the center, we should not get a result on the torus equals(function() { var result = paper.project.hitTest(center, { fill: true }); return result === null; }, true); // When asking specifically for paths, she should get the top-most path in // the center (the one that cuts out the hole) equals(function() { var result = paper.project.hitTest(center, { class: Path, fill: true }); return result && result.item === path2; }, true); }); test('hit-testing clipped items', function() { var rect = new Path.Rectangle({ point: [50, 150], size: [100, 50], fillColor: 'red' }); var circle = new Path.Circle({ center: [100, 200], radius: 20, fillColor: 'green' }); var group = new Group({ children: [rect, circle] }); group.clipped = true; var point1 = new Point(100, 190); var point2 = new Point(100, 210); equals(function() { var result = paper.project.hitTest(point1); return result && result.item === circle; }, true); equals(function() { var result = paper.project.hitTest(point2); return result === null; }, true); }); test('hit-testing with a match function', function() { var point = new Point(100, 100), red = new Color('red'), green = new Color('green'), blue = new Color('blue'); var c1 = new Path.Circle({ center: point, radius: 50, fillColor: red }); var c2 = new Path.Circle({ center: point, radius: 50, fillColor: green }); var c3 = new Path.Circle({ center: point, radius: 50, fillColor: blue }); equals(function() { var result = paper.project.hitTest(point, { fill: true, match: function(res) { return res.item.fillColor == red; } }); return result && result.item === c1; }, true); equals(function() { var result = paper.project.hitTest(point, { fill: true, match: function(res) { return res.item.fillColor == green; } }); return result && result.item === c2; }, true); equals(function() { var result = paper.project.hitTest(point, { fill: true, match: function(res) { return res.item.fillColor == blue; } }); return result && result.item === c3; }, true); }); test('hit-testing for all items', function() { var c1 = new Path.Circle({ center: [100, 100], radius: 40, fillColor: 'red' }); var c2 = new Path.Circle({ center: [120, 120], radius: 40, fillColor: 'green' }); var c3 = new Path.Circle({ center: [140, 140], radius: 40, fillColor: 'blue' }); equals(function() { var result = paper.project.hitTestAll([60, 60]); return result.length === 0; }, true); equals(function() { var result = paper.project.hitTestAll([80, 80]); return result.length === 1 && result[0].item === c1; }, true); equals(function() { var result = paper.project.hitTestAll([100, 100]); return result.length === 2 && result[0].item === c2 && result[1].item === c1; }, true); equals(function() { var result = paper.project.hitTestAll([120, 120]); return result.length === 3 && result[0].item === c3 && result[1].item === c2 && result[2].item === c1; }, true); equals(function() { var result = paper.project.hitTestAll([140, 140]); return result.length === 2 && result[0].item === c3 && result[1].item === c2; }, true); equals(function() { var result = paper.project.hitTestAll([160, 160]); return result.length === 1 && result[0].item === c3; }, true); equals(function() { var result = paper.project.hitTestAll([180, 180]); return result.length === 0; }, true); }); test('hit-testing shapes with strokes and rounded corners (#1207)', function() { var rect = new Shape.Rectangle({ size: [300, 180], strokeWidth: 30, strokeColor: 'black', fillColor: 'blue', radius: 90 }); var path = rect.toPath(); path.visible = false; // Test a few shape stroke hit-test edge cases that are right between the // rounded corners and the straight parts. testHitResult(project.hitTest([90, -10]), { type: 'stroke' }); testHitResult(project.hitTest([90, 190]), { type: 'stroke' }); testHitResult(project.hitTest([-10, 90]), { type: 'stroke' }); // Test at regular intervals along the stroke, and step away from the center // in both directions to hit-test for (var pos = 0; pos < path.length; pos += 10) { var loc = path.getLocationAt(pos), step = loc.normal.multiply(5); testHitResult(project.hitTest(loc.point.add(step)), { type: 'stroke' }); testHitResult(project.hitTest(loc.point.subtract(step)), { type: 'stroke' }); } }); test('hit-testing scaled items with different settings of view.zoom and item.strokeScaling (#1195)', function() { function testItem(ctor, zoom, strokeScaling) { var item = new ctor.Rectangle({ point: [100, 100], size: [100, 100], fillColor: 'red', strokeColor: 'black', strokeWidth: 10, strokeScaling: strokeScaling, applyMatrix: true }); item.scale(2); view.zoom = zoom; var tolerance = 10, options = { tolerance: tolerance, fill: true, stroke: true }, bounds = item.strokeBounds, point = bounds.leftCenter, name = ctor.name + '.Rectangle, strokeScaling = ' + strokeScaling + ', zoom = ' + zoom; testHitResult(project.hitTest(point.subtract(tolerance + 1, 0), options), null, name + ' outside of stroke' ); testHitResult(project.hitTest(point.subtract(tolerance, 0), options), { type: 'stroke' }, name + ' on stroke within tolerance' ); testHitResult(project.hitTest(point, options), { type: 'stroke' }, name + ' on stroke' ); item.remove(); } testItem(Shape, 1, false); testItem(Shape, 1, true); testItem(Shape, 2, false); testItem(Shape, 2, true); testItem(Path, 1, false); testItem(Path, 1, true); testItem(Path, 2, false); testItem(Path, 2, true); }); test('hit-testing items scaled to 0', function() { var item = new Shape.Rectangle({ point: [0, 0], size: [100, 100], fillColor: 'red', selected: true }); item.scale(0); testHitResult(project.hitTest(item.position), null, 'should not throw an exception.'); }); // TODO: project.hitTest(point, {type: AnItemType});