Use consistent hit testing (#689)

This commit is contained in:
DD Liu 2018-09-26 11:19:46 -04:00 committed by GitHub
parent 691417967d
commit 8daea46ad6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 76 deletions

View file

@ -12,22 +12,26 @@ import {sortItemsByZIndex} from './math';
* @return {paper.Item} the hovered item or null if there is none
*/
const getHoveredItem = function (event, hitOptions, subselect) {
// @todo make hit test only hit painting layer
const oldMatch = hitOptions.match;
hitOptions.match = hitResult => {
if (hitResult.item.data && hitResult.item.data.noHover) return false;
return oldMatch ? oldMatch(hitResult) : true;
};
const hitResults = paper.project.hitTestAll(event.point, hitOptions);
if (hitResults.length === 0) {
return null;
}
// sort items by z-index
const items = [];
for (const hitResult of hitResults) {
if (!(hitResult.item.data && hitResult.item.data.noHover) && !hitResult.item.selected) {
items.push(hitResult.item);
// Get highest z-index result
let hitResult;
for (const result of hitResults) {
if (!hitResult || sortItemsByZIndex(hitResult.item, result.item) < 0) {
hitResult = result;
}
}
items.sort(sortItemsByZIndex);
const item = items[items.length - 1];
if (!item) {
const item = hitResult.item;
// If the hovered item is already selected, then there should be no hovered item.
if (!item || item.selected) {
return null;
}

View file

@ -3,8 +3,9 @@ import log from '../../log/log';
import keyMirror from 'keymirror';
import Modes from '../../lib/modes';
import {getHoveredItem} from '../hover';
import {getRootItem, isPGTextItem} from '../item';
import {isBoundsItem} from '../item';
import {hoverBounds, hoverItem} from '../guides';
import {sortItemsByZIndex} from '../math';
import {getSelectedLeafItems, getSelectedSegments} from '../selection';
import MoveTool from './move-tool';
import PointTool from './point-tool';
@ -76,53 +77,84 @@ class ReshapeTool extends paper.Tool {
paper.settings.handleSize = 8;
}
/**
* Returns the hit options to use when conducting hit tests.
* Returns the hit options for segments to use when conducting hit tests. Segments are only visible
* when the shape is selected. Segments take precedence, since they are always over curves and need
* to be grabbable. (Segments are the little circles)
* @return {object} See paper.Item.hitTest for definition of options
*/
getSelectedSegmentHitOptions () {
const hitOptions = {
segments: true,
tolerance: ReshapeTool.TOLERANCE / paper.view.zoom,
match: hitResult => {
if (hitResult.item.data && hitResult.item.data.noHover) return false;
if (!hitResult.item.selected) return false;
return true;
}
};
return hitOptions;
}
/**
* Returns the hit options for handles to use when conducting hit tests. Handles need to be done
* separately because we want to ignore hidden handles, but we don't want hidden handles to negate
* legitimate hits on other things (like if the handle is over part of the fill). (Handles are the diamonds)
* @return {object} See paper.Item.hitTest for definition of options
*/
getHandleHitOptions () {
const hitOptions = {
handles: true,
tolerance: ReshapeTool.TOLERANCE / paper.view.zoom,
match: hitResult => {
if (hitResult.item.data && hitResult.item.data.noHover) return false;
// Only hit test against handles that are visible, that is,
// their segment is selected
if (!hitResult.segment.selected) return false;
// If the entire shape is selected, handles are hidden
if (hitResult.item.fullySelected) return false;
return true;
}
};
return hitOptions;
}
/**
* Returns the hit options for strokes and curves of selected objects, which take precedence over
* unselected things and fills.
* @return {object} See paper.Item.hitTest for definition of options
*/
getSelectedStrokeHitOptions () {
const hitOptions = {
segments: false,
stroke: true,
curves: true,
handles: false,
fill: false,
guide: false,
tolerance: ReshapeTool.TOLERANCE / paper.view.zoom,
match: hitResult => {
if (!hitResult.item.selected) return false;
if (hitResult.item.data && hitResult.item.data.noHover) return false;
return true;
}
};
return hitOptions;
}
/**
* Returns the hit options for fills and unselected strokes/curves to use when conducting hit tests.
* @param {boolean} preselectedOnly True if we should only return results that are already
* selected.
* @return {object} See paper.Item.hitTest for definition of options
*/
getHitOptions (preselectedOnly) {
getUnselectedAndFillHitOptions () {
const hitOptions = {
segments: true,
fill: true,
stroke: true,
curves: true,
handles: true,
fill: true,
guide: false,
tolerance: ReshapeTool.TOLERANCE / paper.view.zoom
tolerance: ReshapeTool.TOLERANCE / paper.view.zoom,
match: hitResult => {
if (hitResult.item.data && hitResult.item.data.noHover) return false;
return true;
}
};
if (preselectedOnly) {
hitOptions.match = item => {
if (!item.item || !item.item.selected) return;
if (item.type === 'handle-out' || item.type === 'handle-in') {
// Only hit test against handles that are visible, that is,
// their segment is selected
if (!item.segment.selected) {
return false;
}
// If the entire shape is selected, handles are hidden
if (item.item.fullySelected) {
return false;
}
}
return true;
};
} else {
hitOptions.match = item => {
if (item.type === 'handle-out' || item.type === 'handle-in') {
// Only hit test against handles that are visible, that is,
// their segment is selected
if (!item.segment.selected) {
return false;
}
// If the entire shape is selected, handles are hidden
if (item.item.fullySelected) {
return false;
}
}
return true;
};
}
return hitOptions;
}
/**
@ -135,6 +167,37 @@ class ReshapeTool extends paper.Tool {
setPrevHoveredItemId (prevHoveredItemId) {
this.prevHoveredItemId = prevHoveredItemId;
}
/**
* Given the point at which the mouse is, return the prioritized hit result, or null if nothing was hit.
* @param {paper.Point} point Point to hit test on canvas
* @return {?paper.HitResult} hitResult
*/
getHitResult (point) {
// Prefer hits on segments to other types of hits, since segments always overlap curves.
let hitResults =
paper.project.hitTestAll(point, this.getSelectedSegmentHitOptions());
if (!hitResults.length) {
hitResults = paper.project.hitTestAll(point, this.getHandleHitOptions());
}
if (!hitResults.length) {
hitResults = paper.project.hitTestAll(point, this.getSelectedStrokeHitOptions());
}
if (!hitResults.length) {
hitResults = paper.project.hitTestAll(point, this.getUnselectedAndFillHitOptions());
}
if (!hitResults.length) {
return null;
}
// Get highest z-index result
let hitResult;
for (const result of hitResults) {
if (!hitResult || sortItemsByZIndex(hitResult.item, result.item) < 0) {
hitResult = result;
}
}
return hitResult;
}
handleMouseDown (event) {
if (event.event.button > 0) return; // only first mouse button
this.active = true;
@ -151,32 +214,12 @@ class ReshapeTool extends paper.Tool {
}
this.lastEvent = event;
// Choose hit result to use ===========================================================
// Prefer hits on already selected items
let hitResults =
paper.project.hitTestAll(event.point, this.getHitOptions(true /* preselectedOnly */));
if (hitResults.length === 0) {
hitResults = paper.project.hitTestAll(event.point, this.getHitOptions());
}
if (hitResults.length === 0) {
const hitResult = this.getHitResult(event.point);
if (!hitResult) {
this._modeMap[ReshapeModes.SELECTION_BOX].onMouseDown(event.modifiers.shift);
return;
}
// Prefer hits on segments to other types of hits, to make sure handles are movable.
let hitResult = hitResults[0];
for (let i = 0; i < hitResults.length; i++) {
if (hitResults[i].type === 'segment') {
hitResult = hitResults[i];
break;
}
}
// Don't allow detail-selection of PGTextItem
if (isPGTextItem(getRootItem(hitResult.item))) {
return;
}
const hitProperties = {
hitResult: hitResult,
clone: event.modifiers.alt,
@ -212,11 +255,22 @@ class ReshapeTool extends paper.Tool {
this.mode = ReshapeModes.FILL;
this._modeMap[this.mode].onMouseDown(hitProperties);
}
// @todo Trigger selection changed. Update styles based on selection.
}
handleMouseMove (event) {
const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */);
const hitResult = this.getHitResult(event.point);
let hoveredItem;
if (hitResult) {
const item = hitResult.item;
if (item.selected) {
hoveredItem = null;
} else if (isBoundsItem(item)) {
hoveredItem = hoverBounds(item);
} else {
hoveredItem = hoverItem(item);
}
}
if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item
(hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item
(hoveredItem && this.prevHoveredItemId &&