Skip to content

Commit

Permalink
Feat: box select
Browse files Browse the repository at this point in the history
  • Loading branch information
monfera committed Feb 14, 2019
1 parent 0e7a735 commit ef748d0
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import { toCSS } from '../../lib/aeroelastic';

export const DragBoxAnnotation = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: toCSS(transformMatrix),
};
return <div className="canvasDragBoxAnnotation canvasLayoutAnnotation" style={newStyle} />;
};

DragBoxAnnotation.propTypes = {
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.canvasDragBoxAnnotation {
position: absolute;
background: none;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: dashed 1px $euiColorDarkShade;
pointer-events: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { pure } from 'recompose';
import { DragBoxAnnotation as Component } from './dragbox_annotation';

export const DragBoxAnnotation = pure(Component);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Shortcuts } from 'react-shortcuts';
import { ElementWrapper } from '../element_wrapper';
import { AlignmentGuide } from '../alignment_guide';
import { HoverAnnotation } from '../hover_annotation';
import { DragBoxAnnotation } from '../dragbox_annotation';
import { TooltipAnnotation } from '../tooltip_annotation';
import { RotationHandle } from '../rotation_handle';
import { BorderConnection } from '../border_connection';
Expand Down Expand Up @@ -146,6 +147,8 @@ export class WorkpadPage extends PureComponent {
case 'adHocChildAnnotation': // now sharing aesthetics but may diverge in the future
case 'hoverAnnotation': // fixme: with the upcoming TS work, use enumerative types here
return <HoverAnnotation {...props} />;
case 'dragBoxAnnotation':
return <DragBoxAnnotation {...props} />;
case 'rotationHandle':
return <RotationHandle {...props} />;
case 'resizeHandle':
Expand Down
117 changes: 85 additions & 32 deletions x-pack/plugins/canvas/public/lib/aeroelastic/geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { invert, mvMultiply, normalize, ORIGIN } from './matrix';
import {
componentProduct,
invert,
mvMultiply,
normalize,
ORIGIN,
RIGHT,
UP,
TOP_LEFT,
BOTTOM_LEFT,
TOP_RIGHT,
BOTTOM_RIGHT,
} from './matrix';
import { dotProduct } from './matrix2d';

/**
* Pure calculations with geometry awareness - a set of rectangles with known size (a, b) and projection (transform matrix)
*/

const cornerScreenPositions = (transformMatrix, a, b) =>
// for unknown perf gain, this could be cached per shape
[TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT].map(corner =>
mvMultiply(transformMatrix, componentProduct(corner, [a, b, 0, 1]))
);

export const insideAABB = ({ x, y, a, b }) => (transformMatrix, aa, bb) => {
const corners = cornerScreenPositions(transformMatrix, aa, bb);
// console.log('insideAABB x, y, a, b:', x, y, a, b, corners[0]);
const result = corners.every(([xx, yy]) => {
const res = x - a <= xx && xx <= x + a && y - b <= yy && yy <= y + b;
// console.log('Corner:', res, 'xx/yy:', xx, yy);
return res;
});
// console.log('insideAABB result:', result);
// console.log('\n');
return result;
};

/**
*
* a * x0 + b * x1 = x
Expand All @@ -33,39 +65,60 @@ import { invert, mvMultiply, normalize, ORIGIN } from './matrix';
* b = (y - a * y0) / y1
*
*/

const planeTuple = transformMatrix => {
// for unknown perf gain, this could be cached per shape
const centerPoint = normalize(mvMultiply(transformMatrix, ORIGIN));
const rightPoint = normalize(mvMultiply(transformMatrix, RIGHT));
const upPoint = normalize(mvMultiply(transformMatrix, UP));
const x0 = rightPoint[0] - centerPoint[0];
const y0 = rightPoint[1] - centerPoint[1];
const x1 = upPoint[0] - centerPoint[0];
const y1 = upPoint[1] - centerPoint[1];
const rightSlope = y1 ? rightPoint[2] - centerPoint[2] : 0; // handle degenerate case: y1 === 0 (infinite slope)
const upSlope = y1 ? upPoint[2] - centerPoint[2] : 0; // handle degenerate case: y1 === 0 (infinite slope)
const inverseProjection = invert(transformMatrix);
const A1 = 1 / (x0 - (y0 / y1) * x1);
const A2 = -((A1 * x1) / y1);
const A0 = -A1 * centerPoint[0] - A2 * centerPoint[1];
const invY1 = -1 / y1;
const z0 = centerPoint[2] + rightSlope * A0 + upSlope * invY1 * (centerPoint[1] + A0 * y0);
const zx = A1 * (rightSlope + upSlope * y0 * invY1);
const zy = -upSlope * invY1 + A2 * (rightSlope + upSlope * y0 * invY1);
const planeVector = [zx, zy, z0];
return { inverseProjection, planeVector };
};

const rectangleAtPoint = ({ transformMatrix, a, b }, x, y) => {
const { inverseProjection, planeVector } = planeTuple(transformMatrix);

// Determine z (depth) by composing the x, y vector out of local unit x and unit y vectors; by knowing the
// scalar multipliers for the unit x and unit y vectors, we can determine z from their respective 'slope' (gradient)
const screenNormalVector = [x, y, 1];
const z = dotProduct(planeVector, screenNormalVector);

// We go full tilt with the inverse transform approach because that's general enough to handle any non-pathological
// composition of transforms. Eg. this is a description of the idea: https://math.stackexchange.com/a/1685315
// Hmm maybe we should reuse the above right and up unit vectors to establish whether we're within the (a, b) 'radius'
// rather than using matrix inversion. Bound to be cheaper.

const intersection = normalize(mvMultiply(inverseProjection, [x, y, z, 1]));
const [sx, sy] = intersection;
const inside = Math.abs(sx) <= a && Math.abs(sy) <= b;

// z is needed downstream, to tell which one is the closest shape hit by an x, y ray (shapes can be tilted in z)
// it looks weird to even return items where inside === false, but it could be useful for hotspots outside the rectangle

return {
z,
intersection,
inside,
};
};

// set of shapes under a specific point
const shapesAtPoint = (shapes, x, y) =>
shapes.map((shape, index) => {
const { transformMatrix, a, b } = shape;

// Determine z (depth) by composing the x, y vector out of local unit x and unit y vectors; by knowing the
// scalar multipliers for the unit x and unit y vectors, we can determine z from their respective 'slope' (gradient)
const centerPoint = normalize(mvMultiply(transformMatrix, ORIGIN));
const rightPoint = normalize(mvMultiply(transformMatrix, [1, 0, 0, 1]));
const upPoint = normalize(mvMultiply(transformMatrix, [0, 1, 0, 1]));
const x0 = rightPoint[0] - centerPoint[0];
const y0 = rightPoint[1] - centerPoint[1];
const x1 = upPoint[0] - centerPoint[0];
const y1 = upPoint[1] - centerPoint[1];
const A = (x - centerPoint[0] - ((y - centerPoint[1]) / y1) * x1) / (x0 - (y0 / y1) * x1);
const B = (y - centerPoint[1] - A * y0) / y1;
const rightSlope = rightPoint[2] - centerPoint[2];
const upSlope = upPoint[2] - centerPoint[2];
const z = centerPoint[2] + (y1 ? rightSlope * A + upSlope * B : 0); // handle degenerate case: y1 === 0 (infinite slope)

// We go full tilt with the inverse transform approach because that's general enough to handle any non-pathological
// composition of transforms. Eg. this is a description of the idea: https://math.stackexchange.com/a/1685315
// Hmm maybe we should reuse the above right and up unit vectors to establish whether we're within the (a, b) 'radius'
// rather than using matrix inversion. Bound to be cheaper.

const inverseProjection = invert(transformMatrix);
const intersection = normalize(mvMultiply(inverseProjection, [x, y, z, 1]));
const [sx, sy] = intersection;

// z is needed downstream, to tell which one is the closest shape hit by an x, y ray (shapes can be tilted in z)
// it looks weird to even return items where inside === false, but it could be useful for hotspots outside the rectangle
return { z, intersection, inside: Math.abs(sx) <= a && Math.abs(sy) <= b, shape, index };
});
shapes.map((shape, index) => ({ ...rectangleAtPoint(shape, x, y), shape, index }));

// Z-order the possibly several shapes under the same point.
// Since CSS X points to the right, Y to the bottom (not the top!) and Z toward the viewer, it's a left-handed coordinate
Expand Down
20 changes: 18 additions & 2 deletions x-pack/plugins/canvas/public/lib/aeroelastic/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import {
getConstrainedShapesWithPreexistingAnnotations,
getCursor,
getDirectSelect,
getDragBox,
getDragBoxAnnotation,
getDragBoxSelected,
getDraggedPrimaryShape,
getFocusedShape,
getGroupAction,
Expand Down Expand Up @@ -96,13 +99,17 @@ const transformGestures = mouseTransformGesture;

const restateShapesEvent = select(getRestateShapesEvent)(primaryUpdate);

const dragBox = select(getDragBox)(dragging, draggedShape, dragVector);

// directSelect is an API entry point (via the `shapeSelect` action) that lets the client directly specify what thing
const directSelect = select(getDirectSelect)(primaryUpdate);

const selectedShapeObjects = select(getSelectedShapeObjects)(getScene);

const selectedShapesPrev = select(getSelectedShapesPrev)(getScene);

const boxSelected = select(getDragBoxSelected)(dragBox, shapes);

const selectionState = select(getSelectionState)(
selectedShapesPrev,
configuration,
Expand All @@ -112,6 +119,7 @@ const selectionState = select(getSelectionState)(
metaHeld,
multiselectModifier,
directSelect,
boxSelected,
shapes
);

Expand Down Expand Up @@ -150,7 +158,12 @@ const alignmentGuideAnnotations = select(getAlignmentGuideAnnotations)(

const hoverAnnotations = select(getHoverAnnotations)(
configuration,
hoveredShape,
select((h, b) =>
h
.slice(0, 1)
.concat(b)
.filter((d, i, a) => a.indexOf(d) === i)
)(hoveredShapes, boxSelected),
selectedPrimaryShapeIds,
draggedShape
);
Expand Down Expand Up @@ -204,14 +217,17 @@ const resizeAnnotations = select(resizeAnnotationsFunction)(configuration, group

const rotationAnnotations = select(getRotationAnnotations)(configuration, grouping);

const dragBoxAnnotation = select(getDragBoxAnnotation)(configuration, dragBox);

const annotatedShapes = select(getAnnotatedShapes)(
grouping,
alignmentGuideAnnotations,
hoverAnnotations,
rotationAnnotations,
resizeAnnotations,
rotationTooltipAnnotation,
adHocChildrenAnnotations
adHocChildrenAnnotations,
dragBoxAnnotation
);

const globalTransformShapes = select(cascadeProperties)(annotatedShapes);
Expand Down
60 changes: 49 additions & 11 deletions x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { getId } from './../../lib/get_id';
import { landmarkPoint, shapesAt } from './geometry';
import { landmarkPoint, shapesAt, insideAABB } from './geometry';

import {
compositeComponent,
Expand Down Expand Up @@ -1202,6 +1202,7 @@ export const getSelectionState = (
metaHeld,
multiselect,
directSelect,
boxSelected,
allShapes
) => {
const uidUnchanged = uid === prev.uid;
Expand Down Expand Up @@ -1306,14 +1307,16 @@ export const getAdHocChildrenAnnotations = (config, { shapes }) => {
.map(borderAnnotation(config.getAdHocChildAnnotationName, config.hoverLift));
};

export const getHoverAnnotations = (config, shape, selectedPrimaryShapeIds, draggedShape) => {
return shape &&
shape.type !== 'annotation' &&
selectedPrimaryShapeIds.indexOf(shape.id) === -1 &&
!draggedShape
? [borderAnnotation(config.hoverAnnotationName, config.hoverLift)(shape)]
: [];
};
export const getHoverAnnotations = (config, shapes, selectedPrimaryShapeIds, draggedShape) =>
shapes
.filter(
shape =>
shape &&
shape.type !== 'annotation' &&
selectedPrimaryShapeIds.indexOf(shape.id) === -1 &&
!draggedShape
)
.map(borderAnnotation(config.hoverAnnotationName, config.hoverLift));

export const getSnappedShapes = (
config,
Expand Down Expand Up @@ -1376,14 +1379,48 @@ export const getRotationAnnotations = (config, { shapes, selectedShapes }) => {
.filter(identity);
};

export const getDragBox = (dragging, draggedShape, { x0, y0, x1, y1 }) =>
dragging &&
!draggedShape && {
x: (x0 + x1) / 2,
y: (y0 + y1) / 2,
a: Math.abs(x1 - x0) / 2,
b: Math.abs(y1 - y0) / 2,
};

export const getDragBoxSelected = (box, shapes) => {
if (!box) {
return [];
}
const filter = insideAABB(box);
return shapes.filter(s => s.type !== 'annotation' && filter(s.transformMatrix, s.a, s.b));
};

export const getDragBoxAnnotation = (config, box) =>
box
? [
{
id: config.dragBoxAnnotationName,
type: 'annotation',
subtype: config.dragBoxAnnotationName,
interactive: false,
parent: null,
localTransformMatrix: translate(box.x, box.y, config.dragBoxZ),
a: box.a,
b: box.b,
},
]
: [];

export const getAnnotatedShapes = (
{ shapes },
alignmentGuideAnnotations,
hoverAnnotations,
rotationAnnotations,
resizeAnnotations,
rotationTooltipAnnotation,
adHocChildrenAnnotations
adHocChildrenAnnotations,
dragBoxAnnotation
) => {
// fixme update it to a simple concatenator, no need for enlisting the now pretty long subtype list
const annotations = [].concat(
Expand All @@ -1392,7 +1429,8 @@ export const getAnnotatedShapes = (
rotationAnnotations,
resizeAnnotations,
rotationTooltipAnnotation,
adHocChildrenAnnotations
adHocChildrenAnnotations,
dragBoxAnnotation
);
// remove preexisting annotations
const contentShapes = shapes.filter(shape => shape.type !== 'annotation');
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const NANMATRIX = [
] as transformMatrix3d;

export const ORIGIN = [0, 0, 0, 1] as vector3d;
export const RIGHT = [1, 0, 0, 1] as vector3d;
export const UP = [0, 1, 0, 1] as vector3d;
export const TOP_LEFT = [-1, 1, 0, 1] as vector3d;
export const TOP_RIGHT = [1, 1, 0, 1] as vector3d;
export const BOTTOM_LEFT = [-1, -1, 0, 1] as vector3d;
export const BOTTOM_RIGHT = [1, -1, 0, 1] as vector3d;

export const translate = (x: number, y: number, z: number): transformMatrix3d =>
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1] as transformMatrix3d;
Expand Down Expand Up @@ -269,6 +275,9 @@ export const subtract = (
p - P,
] as transformMatrix3d;

export const componentProduct = ([a, b, c, d]: vector3d, [A, B, C, D]: vector3d): vector3d =>
[a * A, b * B, c * C, d * D] as vector3d;

export const reduceTransforms = (transforms: transformMatrix3d[]): transformMatrix3d =>
transforms.length === 1
? transforms[0]
Expand Down
Loading

0 comments on commit ef748d0

Please sign in to comment.