Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Canvas][Layout Engine][WIP][skip ci] Refactor: Rework of Layout Engine state #30827

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a5bd26b
Feat: box select
monfera Feb 12, 2019
33264e1
minor
monfera Feb 14, 2019
6664015
Refactor: preserve gesture state in favor of local selectReduce 1
monfera Feb 14, 2019
8b9b026
Refactor: preserve gesture state in favor of local selectReduce 2 (ju…
monfera Feb 14, 2019
d1f6745
Refactor: preserve gesture state in favor of local selectReduce 3
monfera Feb 14, 2019
3fb4400
Refactor: preserve gesture state in favor of local selectReduce 4 - c…
monfera Feb 14, 2019
3a00242
Refactor: preserve gesture state in favor of local selectReduce 5
monfera Feb 14, 2019
18c4ceb
Refactor: preserve gesture state in favor of local selectReduce 6
monfera Feb 14, 2019
5ebb5b8
Refactor: nicer, simpler shallowEqual (potentially marginally slower)
monfera Feb 14, 2019
7469813
Chore: remove disused `dispatch`
monfera Feb 14, 2019
cc9d154
Fix: No longer break the user session upon encountering some race con…
monfera Feb 14, 2019
6c7eeec
Chore: `shapeAdditions` disused; cleaning up `state.ts`
monfera Feb 14, 2019
7fa8a1b
Refactor: `configuration` is normal part of the scene
monfera Feb 14, 2019
ff632e5
Refactor: state is now in local in the component
monfera Feb 15, 2019
57b9e4d
Refactor: spike to get rid of `select` 1
monfera Feb 15, 2019
67e6db3
Refactor: spike to get rid of `select` 2
monfera Feb 15, 2019
26d878c
Refactor: spike to get rid of `select` 3
monfera Feb 15, 2019
0420084
Refactor: spike to get rid of `select` 4
monfera Feb 15, 2019
66a4eec
Refactor: spike to get rid of `select` 5
monfera Feb 15, 2019
066225f
Refactor: spike to get rid of `select` 6
monfera Feb 15, 2019
2815085
Refactor: spike to get rid of `select` 7
monfera Feb 15, 2019
fec3973
Refactor: spike to more properly type `select`
monfera Feb 15, 2019
9a9f4ae
Doc: types
monfera Feb 16, 2019
0141753
Chore: misc rework
monfera Feb 17, 2019
c1b26c1
Test: TS type specification strength tests (PoC)
monfera Feb 17, 2019
906b924
Fix: properly exclude type strength checks
monfera Feb 18, 2019
52f6597
Refactor: Break up getGrouping 1
monfera Feb 17, 2019
151fd39
Fix: Turn an impure selector fun into a pure function
monfera Feb 17, 2019
d3d5137
Chore: start from the state stream
monfera Feb 17, 2019
9f3d656
Refactor: lifted up a couple of shared nodes
monfera Feb 18, 2019
3c68e54
Chore: minor test enhancement
monfera Feb 18, 2019
efee42c
Refactor: other direction of redux synchronization
monfera Feb 18, 2019
f8b995d
Refactor: other direction of redux synchronization 2
monfera Feb 18, 2019
b3474c9
Refactor: other direction of redux synchronization 3
monfera Feb 19, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"test:ui:runner": "node scripts/functional_test_runner",
"test:server": "grunt test:server",
"test:coverage": "grunt test:coverage",
"typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts",
"checkLicenses": "grunt licenses --dev",
"build": "node scripts/build --all-platforms",
"start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ",
Expand Down Expand Up @@ -407,6 +408,7 @@
"tslint-microsoft-contrib": "^6.0.0",
"tslint-plugin-prettier": "^2.0.0",
"typescript": "^3.0.3",
"typings-tester": "^0.3.2",
"vinyl-fs": "^3.0.2",
"xml2js": "^0.4.19",
"xmlbuilder": "9.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

export const AlignmentGuide = ({ transformMatrix, width, height }) => {
const newStyle = {
Expand All @@ -16,7 +16,7 @@ export const AlignmentGuide = ({ transformMatrix, width, height }) => {
marginTop: -height / 2,
background: 'magenta',
position: 'absolute',
transform: toCSS(transformMatrix),
transform: matrixToCSS(transformMatrix),
};
return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

export const BorderConnection = ({ transformMatrix, width, height }) => {
const newStyle = {
Expand All @@ -15,7 +15,7 @@ export const BorderConnection = ({ transformMatrix, width, height }) => {
marginLeft: -width / 2,
marginTop: -height / 2,
position: 'absolute',
transform: toCSS(transformMatrix),
transform: matrixToCSS(transformMatrix),
};
return <div className="canvasBorder--connection canvasLayoutAnnotation" style={newStyle} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

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

export const BorderResizeHandle = ({ transformMatrix }) => (
<div
className="canvasBorderResizeHandle canvasLayoutAnnotation"
style={{ transform: toCSS(transformMatrix) }}
style={{ transform: matrixToCSS(transformMatrix) }}
/>
);

Expand Down
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 { matrixToCSS } from '../../lib/dom';

export const DragBoxAnnotation = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: matrixToCSS(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 @@ -6,15 +6,15 @@

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

export const HoverAnnotation = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: toCSS(transformMatrix),
transform: matrixToCSS(transformMatrix),
};
return <div className="canvasHoverAnnotation canvasLayoutAnnotation" style={newStyle} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

export const Positionable = ({ children, transformMatrix, width, height }) => {
// Throw if there is more than one child
Expand All @@ -19,7 +19,7 @@ export const Positionable = ({ children, transformMatrix, width, height }) => {
marginLeft: -width / 2,
marginTop: -height / 2,
position: 'absolute',
transform: toCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))),
transform: matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))),
};

const stepChild = React.cloneElement(child, { size: { width, height } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

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

export const RotationHandle = ({ transformMatrix }) => (
<div
className="canvasRotationHandle canvasRotationHandle--connector canvasLayoutAnnotation"
style={{ transform: toCSS(transformMatrix) }}
style={{ transform: matrixToCSS(transformMatrix) }}
>
<div className="canvasRotationHandle--handle" />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

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

export const HoverAnnotation = ({ transformMatrix, text }) => {
const newStyle = {
transform: `${toCSS(transformMatrix)} translate(1em, -1em)`,
transform: `${matrixToCSS(transformMatrix)} translate(1em, -1em)`,
};
return (
<div className="tooltipAnnotation canvasLayoutAnnotation" style={newStyle}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* 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 { shallowEqual } from 'recompose';
import { invert, matrixToAngle, multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix';
import { arrayToMap, identity } from '../../lib/aeroelastic/functional';

export const aeroelasticConfiguration = {
getAdHocChildAnnotationName: 'adHocChildAnnotation',
adHocGroupName: 'adHocGroup',
alignmentGuideName: 'alignmentGuide',
atopZ: 1000,
depthSelect: true,
devColor: 'magenta',
dragBoxAnnotationName: 'dragBoxAnnotation',
dragBoxZ: 1050, // above alignment guides but below the upcoming hover tooltip
groupName: 'group',
groupResize: true,
guideDistance: 3,
hoverAnnotationName: 'hoverAnnotation',
hoverLift: 100,
intraGroupManipulation: false,
intraGroupSnapOnly: false,
minimumElementSize: 0,
persistentGroupName: 'persistentGroup',
resizeAnnotationConnectorOffset: 0,
resizeAnnotationOffset: 0,
resizeAnnotationOffsetZ: 0.1, // causes resize markers to be slightly above the shape plane
resizeAnnotationSize: 10,
resizeConnectorName: 'resizeConnector',
resizeHandleName: 'resizeHandle',
rotateAnnotationOffset: 12,
rotateSnapInPixels: 10,
rotationEpsilon: 0.001,
rotationHandleName: 'rotationHandle',
rotationHandleSize: 14,
rotationTooltipName: 'rotationTooltip',
shortcuts: false,
singleSelect: false,
snapConstraint: true,
tooltipZ: 1100,
};

export const makeUid = () => 1e11 + Math.floor((1e12 - 1e11) * Math.random());

// check for duplication
const deduped = a => a.filter((d, i) => a.indexOf(d) === i);
export const idDuplicateCheck = groups => {
if (deduped(groups.map(g => g.id)).length !== groups.length) {
throw new Error('Duplicate element encountered');
}
};

export const missingParentCheck = groups => {
const idMap = arrayToMap(groups.map(g => g.id));
groups.forEach(g => {
if (g.parent && !idMap[g.parent]) {
g.parent = null;
}
});
};

/**
* elementToShape
*
* converts a `kibana-canvas` element to an `aeroelastic` shape.
*
* Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects,
* such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix
* that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a
* size descriptor. There are two versions of the transform matrix:
* - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix
* - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix
*
* Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the
* server, and to accept such data from the server. The redux and server representations will need to change as more general
* projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot.
*
* While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations.
* For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout
* library, simply for generality.
*/
export const elementToShape = (element, i) => {
const position = element.position;
const a = position.width / 2;
const b = position.height / 2;
const cx = position.left + a;
const cy = position.top + b;
const z = i; // painter's algo: latest item goes to top
// multiplying the angle with -1 as `transform: matrix3d` uses a left-handed coordinate system
const angleRadians = (-position.angle / 180) * Math.PI;
const transformMatrix = multiply(translate(cx, cy, z), rotateZ(angleRadians));
const isGroup = element.id.startsWith('group');
const parent = (element.position && element.position.parent) || null; // reserved for hierarchical (tree shaped) grouping
return {
id: element.id,
type: isGroup ? 'group' : 'rectangleElement',
subtype: isGroup ? 'persistentGroup' : '',
parent,
transformMatrix,
a, // we currently specify half-width, half-height as it leads to
b, // more regular math (like ellipsis radii rather than diameters)
};
};

export const shapeToElement = shape => {
let angle = Math.round((matrixToAngle(shape.transformMatrix) * 180) / Math.PI);
if (1 / angle === -Infinity) {
angle = 0;
}
return {
left: shape.transformMatrix[12] - shape.a,
top: shape.transformMatrix[13] - shape.b,
width: shape.a * 2,
height: shape.b * 2,
angle,
parent: shape.parent || null,
type: shape.type === 'group' ? 'group' : 'element',
};
};

export const updateGlobalPositions = (setPositions, { shapes, gestureEnd }, unsortedElements) => {
const ascending = (a, b) => (a.id < b.id ? -1 : 1);
const relevant = s => s.type !== 'annotation' && s.subtype !== 'adHocGroup';
const elements = unsortedElements.filter(relevant).sort(ascending);
const repositionings = shapes
.filter(relevant)
.sort(ascending)
.map((shape, i) => {
const element = elements[i];
const elemPos = element && element.position;
if (elemPos && gestureEnd) {
// get existing position information from element
const oldProps = {
left: elemPos.left,
top: elemPos.top,
width: elemPos.width,
height: elemPos.height,
angle: Math.round(elemPos.angle),
type: elemPos.type,
parent: elemPos.parent || null,
};

// cast shape into element-like object to compare
const newProps = shapeToElement(shape);

if (1 / newProps.angle === -Infinity) {
newProps.angle = 0;
} // recompose.shallowEqual discerns between 0 and -0

const result = shallowEqual(oldProps, newProps)
? null
: { position: newProps, elementId: shape.id };
return result;
}
})
.filter(identity);
if (repositionings.length) {
setPositions(repositionings);
}
};

// todo check past usage and eliminate
const getRootElementId = (lookup, id) => {
if (!lookup.has(id)) {
return null;
}

const element = lookup.get(id);
return element.parent && element.parent.subtype !== 'adHocGroup'
? getRootElementId(lookup, element.parent)
: element.id;
};

export const componentLayoutLocalState = props => {
// console.log('setting aero state on component!!!');
const shapes = props.elements
.map(elementToShape)
.filter((d, i, a) => !d.id.startsWith('group') || a.find(s => s.parent === d.id));
// todo delete these
idDuplicateCheck(shapes);
missingParentCheck(shapes);
shapes.forEach(shape => {
shape.localTransformMatrix = shape.parent
? multiply(
invert(shapes.find(s => s.id === shape.parent).transformMatrix),
shape.transformMatrix
)
: shape.transformMatrix;
});
// todo move this initial state in a file under `aeroelastic/`
return {
primaryUpdate: null,
currentScene: { shapes, configuration: aeroelasticConfiguration },
};
};
Loading