mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Perf: avoid matrix transpose in canvas layout that served only initial docu purpose, don't cons extra arrays * Chore: switch to ES2015 import/export and direct function access * Chore: TS conversion for some of the layout engine files * Chore: rework TS linting for all files under `aeroelastic` (even the `.js` ones)
This commit is contained in:
parent
f98ba0d697
commit
ccbc60b653
22 changed files with 2098 additions and 2207 deletions
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const AlignmentGuide = ({ transformMatrix, width, height }) => {
|
||||
const newStyle = {
|
||||
|
@ -16,7 +16,7 @@ export const AlignmentGuide = ({ transformMatrix, width, height }) => {
|
|||
marginTop: -height / 2,
|
||||
background: 'magenta',
|
||||
position: 'absolute',
|
||||
transform: aero.dom.matrixToCSS(transformMatrix),
|
||||
transform: toCSS(transformMatrix),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const BorderConnection = ({ transformMatrix, width, height }) => {
|
||||
const newStyle = {
|
||||
|
@ -15,7 +15,7 @@ export const BorderConnection = ({ transformMatrix, width, height }) => {
|
|||
marginLeft: -width / 2,
|
||||
marginTop: -height / 2,
|
||||
position: 'absolute',
|
||||
transform: aero.dom.matrixToCSS(transformMatrix),
|
||||
transform: toCSS(transformMatrix),
|
||||
};
|
||||
return <div className="canvasBorder--connection canvasLayoutAnnotation" style={newStyle} />;
|
||||
};
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const BorderResizeHandle = ({ transformMatrix }) => (
|
||||
<div
|
||||
className="canvasBorderResizeHandle canvasLayoutAnnotation"
|
||||
style={{ transform: aero.dom.matrixToCSS(transformMatrix) }}
|
||||
style={{ transform: toCSS(transformMatrix) }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const HoverAnnotation = ({ transformMatrix, width, height }) => {
|
||||
const newStyle = {
|
||||
|
@ -14,7 +14,7 @@ export const HoverAnnotation = ({ transformMatrix, width, height }) => {
|
|||
height,
|
||||
marginLeft: -width / 2,
|
||||
marginTop: -height / 2,
|
||||
transform: aero.dom.matrixToCSS(transformMatrix),
|
||||
transform: toCSS(transformMatrix),
|
||||
};
|
||||
return <div className="canvasHoverAnnotation canvasLayoutAnnotation" style={newStyle} />;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const Positionable = ({ children, transformMatrix, width, height }) => {
|
||||
// Throw if there is more than one child
|
||||
|
@ -19,7 +19,7 @@ export const Positionable = ({ children, transformMatrix, width, height }) => {
|
|||
marginLeft: -width / 2,
|
||||
marginTop: -height / 2,
|
||||
position: 'absolute',
|
||||
transform: aero.dom.matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))),
|
||||
transform: toCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))),
|
||||
};
|
||||
|
||||
const stepChild = React.cloneElement(child, { size: { width, height } });
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import aero from '../../lib/aeroelastic';
|
||||
import { toCSS } from '../../lib/aeroelastic';
|
||||
|
||||
export const RotationHandle = ({ transformMatrix }) => (
|
||||
<div
|
||||
className="canvasRotationHandle canvasRotationHandle--connector canvasLayoutAnnotation"
|
||||
style={{ transform: aero.dom.matrixToCSS(transformMatrix) }}
|
||||
style={{ transform: toCSS(transformMatrix) }}
|
||||
>
|
||||
<div className="canvasRotationHandle--handle" />
|
||||
</div>
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock config
|
||||
*/
|
||||
|
||||
const adHocGroupName = 'adHocGroup';
|
||||
const alignmentGuideName = 'alignmentGuide';
|
||||
const atopZ = 1000;
|
||||
const depthSelect = true;
|
||||
const devColor = 'magenta';
|
||||
const groupName = 'group';
|
||||
const groupResize = true;
|
||||
const guideDistance = 3;
|
||||
const hoverAnnotationName = 'hoverAnnotation';
|
||||
const hoverLift = 100;
|
||||
const intraGroupManipulation = false;
|
||||
const intraGroupSnapOnly = false;
|
||||
const persistentGroupName = 'persistentGroup';
|
||||
const resizeAnnotationOffset = 0;
|
||||
const resizeAnnotationOffsetZ = 0.1; // causes resize markers to be slightly above the shape plane
|
||||
const resizeAnnotationSize = 10;
|
||||
const resizeAnnotationConnectorOffset = 0; //resizeAnnotationSize //+ 2
|
||||
const resizeConnectorName = 'resizeConnector';
|
||||
const rotateAnnotationOffset = 12;
|
||||
const rotationEpsilon = 0.001;
|
||||
const rotationHandleName = 'rotationHandle';
|
||||
const rotationHandleSize = 14;
|
||||
const resizeHandleName = 'resizeHandle';
|
||||
const rotateSnapInPixels = 10;
|
||||
const shortcuts = false;
|
||||
const singleSelect = false;
|
||||
const snapConstraint = true;
|
||||
const minimumElementSize = 0; // guideDistance / 2 + 1;
|
||||
|
||||
module.exports = {
|
||||
adHocGroupName,
|
||||
alignmentGuideName,
|
||||
atopZ,
|
||||
depthSelect,
|
||||
devColor,
|
||||
groupName,
|
||||
groupResize,
|
||||
guideDistance,
|
||||
hoverAnnotationName,
|
||||
hoverLift,
|
||||
intraGroupManipulation,
|
||||
intraGroupSnapOnly,
|
||||
minimumElementSize,
|
||||
persistentGroupName,
|
||||
resizeAnnotationOffset,
|
||||
resizeAnnotationOffsetZ,
|
||||
resizeAnnotationSize,
|
||||
resizeAnnotationConnectorOffset,
|
||||
resizeConnectorName,
|
||||
resizeHandleName,
|
||||
rotateAnnotationOffset,
|
||||
rotationEpsilon,
|
||||
rotateSnapInPixels,
|
||||
rotationHandleName,
|
||||
rotationHandleSize,
|
||||
shortcuts,
|
||||
singleSelect,
|
||||
snapConstraint,
|
||||
};
|
|
@ -4,14 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { transformMatrix3d } from './types';
|
||||
|
||||
// converts a transform matrix to a CSS string
|
||||
const matrixToCSS = transformMatrix =>
|
||||
export const matrixToCSS = (transformMatrix: transformMatrix3d): string =>
|
||||
transformMatrix ? 'matrix3d(' + transformMatrix.join(',') + ')' : 'translate3d(0,0,0)';
|
||||
|
||||
// converts to string, and adds `px` if non-zero
|
||||
const px = value => (value === 0 ? '0' : value + 'px');
|
||||
|
||||
module.exports = {
|
||||
matrixToCSS,
|
||||
px,
|
||||
};
|
||||
export const px = (value: number): string => (value === 0 ? '0' : value + 'px');
|
|
@ -12,7 +12,7 @@
|
|||
* @param {*[][]} arrays
|
||||
* @returns *[]
|
||||
*/
|
||||
const flatten = arrays => [].concat(...arrays);
|
||||
export const flatten = arrays => [].concat(...arrays);
|
||||
|
||||
/**
|
||||
* identity
|
||||
|
@ -20,7 +20,7 @@ const flatten = arrays => [].concat(...arrays);
|
|||
* @param d
|
||||
* @returns d
|
||||
*/
|
||||
const identity = d => d;
|
||||
export const identity = d => d;
|
||||
|
||||
/**
|
||||
* map
|
||||
|
@ -32,19 +32,7 @@ const identity = d => d;
|
|||
* @param {Function} fun
|
||||
* @returns {function(*): *}
|
||||
*/
|
||||
const map = fun => array => array.map(value => fun(value));
|
||||
|
||||
/**
|
||||
* log
|
||||
*
|
||||
* @param d
|
||||
* @param {Function} printerFun
|
||||
* @returns d
|
||||
*/
|
||||
const log = (d, printerFun = identity) => {
|
||||
console.log(printerFun(d));
|
||||
return d;
|
||||
};
|
||||
export const map = fun => array => array.map(value => fun(value));
|
||||
|
||||
/**
|
||||
* disjunctiveUnion
|
||||
|
@ -54,7 +42,7 @@ const log = (d, printerFun = identity) => {
|
|||
* @param {*[]} set2
|
||||
* @returns *[]
|
||||
*/
|
||||
const disjunctiveUnion = (keyFun, set1, set2) =>
|
||||
export const disjunctiveUnion = (keyFun, set1, set2) =>
|
||||
set1
|
||||
.filter(s1 => !set2.find(s2 => keyFun(s2) === keyFun(s1)))
|
||||
.concat(set2.filter(s2 => !set1.find(s1 => keyFun(s1) === keyFun(s2))));
|
||||
|
@ -65,9 +53,9 @@ const disjunctiveUnion = (keyFun, set1, set2) =>
|
|||
* @param {number} b
|
||||
* @returns {number} the mean of the two parameters
|
||||
*/
|
||||
const mean = (a, b) => (a + b) / 2;
|
||||
export const mean = (a, b) => (a + b) / 2;
|
||||
|
||||
const shallowEqual = (a, b) => {
|
||||
export const shallowEqual = (a, b) => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
@ -82,31 +70,21 @@ const shallowEqual = (a, b) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const not = fun => (...args) => !fun(...args);
|
||||
export const not = fun => (...args) => !fun(...args);
|
||||
|
||||
const removeDuplicates = (idFun, a) =>
|
||||
export const removeDuplicates = (idFun, a) =>
|
||||
a.filter((d, i) => a.findIndex(s => idFun(s) === idFun(d)) === i);
|
||||
|
||||
const arrayToMap = a => Object.assign({}, ...a.map(d => ({ [d]: true })));
|
||||
export const arrayToMap = a => Object.assign({}, ...a.map(d => ({ [d]: true })));
|
||||
|
||||
const subMultitree = (pk, fk, elements, roots) => {
|
||||
export const subMultitree = (pk, fk, elements, inputRoots) => {
|
||||
const getSubgraphs = roots => {
|
||||
const children = flatten(roots.map(r => elements.filter(e => fk(e) === pk(r))));
|
||||
return [...roots, ...(children.length && getSubgraphs(children, elements))];
|
||||
if (children.length) {
|
||||
return [...roots, ...getSubgraphs(children, elements)];
|
||||
} else {
|
||||
return roots;
|
||||
}
|
||||
};
|
||||
return getSubgraphs(roots);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
arrayToMap,
|
||||
disjunctiveUnion,
|
||||
flatten,
|
||||
subMultitree,
|
||||
identity,
|
||||
log,
|
||||
map,
|
||||
mean,
|
||||
not,
|
||||
removeDuplicates,
|
||||
shallowEqual,
|
||||
return getSubgraphs(inputRoots);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const matrix = require('./matrix');
|
||||
import { invert, mvMultiply, normalize, ORIGIN } from './matrix';
|
||||
|
||||
/**
|
||||
* Pure calculations with geometry awareness - a set of rectangles with known size (a, b) and projection (transform matrix)
|
||||
|
@ -40,9 +40,9 @@ const shapesAtPoint = (shapes, x, y) =>
|
|||
|
||||
// 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 = matrix.normalize(matrix.mvMultiply(transformMatrix, matrix.ORIGIN));
|
||||
const rightPoint = matrix.normalize(matrix.mvMultiply(transformMatrix, [1, 0, 0, 1]));
|
||||
const upPoint = matrix.normalize(matrix.mvMultiply(transformMatrix, [0, 1, 0, 1]));
|
||||
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];
|
||||
|
@ -58,8 +58,8 @@ const shapesAtPoint = (shapes, x, y) =>
|
|||
// 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 = matrix.invert(transformMatrix);
|
||||
const intersection = matrix.normalize(matrix.mvMultiply(inverseProjection, [x, y, z, 1]));
|
||||
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)
|
||||
|
@ -76,18 +76,13 @@ const shapesAtPoint = (shapes, x, y) =>
|
|||
//
|
||||
// If it were a right handed coordinate system, AND Y still pointed down, then Z should increase away from the
|
||||
// viewer. But that's not the case. So we maximize the Z value to tell what's on top.
|
||||
const shapesAt = (shapes, { x, y }) =>
|
||||
export const shapesAt = (shapes, { x, y }) =>
|
||||
shapesAtPoint(shapes, x, y)
|
||||
.filter(shape => shape.inside)
|
||||
.sort((shape1, shape2) => shape2.z - shape1.z || shape2.index - shape1.index) // stable sort: DOM insertion order!!!
|
||||
.map(shape => shape.shape); // decreasing order, ie. from front (closest to viewer) to back
|
||||
|
||||
const getExtremum = (transformMatrix, a, b) =>
|
||||
matrix.normalize(matrix.mvMultiply(transformMatrix, [a, b, 0, 1]));
|
||||
const getExtremum = (transformMatrix, a, b) => normalize(mvMultiply(transformMatrix, [a, b, 0, 1]));
|
||||
|
||||
const landmarkPoint = (a, b, transformMatrix, k, l) => getExtremum(transformMatrix, k * a, l * b);
|
||||
|
||||
module.exports = {
|
||||
landmarkPoint,
|
||||
shapesAt,
|
||||
};
|
||||
export const landmarkPoint = (a, b, transformMatrix, k, l) =>
|
||||
getExtremum(transformMatrix, k * a, l * b);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const { select, selectReduce } = require('./state');
|
||||
import { select, selectReduce } from './state';
|
||||
|
||||
// Only needed to shuffle some modifier keys for Apple keyboards as per vector editing software conventions,
|
||||
// so it's OK that user agent strings are not reliable; in case it's spoofed, it'll just work with a slightly
|
||||
|
@ -43,15 +43,16 @@ const keyFromMouse = select(({ type, payload: { altKey, metaKey, shiftKey, ctrlK
|
|||
type === 'cursorPosition' || type === 'mouseEvent' ? { altKey, metaKey, shiftKey, ctrlKey } : {}
|
||||
)(primaryUpdate);
|
||||
|
||||
const metaHeld = select(appleKeyboard ? e => e.metaKey : e => e.altKey)(keyFromMouse);
|
||||
const optionHeld = select(appleKeyboard ? e => e.altKey : e => e.ctrlKey)(keyFromMouse);
|
||||
const shiftHeld = select(e => e.shiftKey)(keyFromMouse);
|
||||
export const metaHeld = select(appleKeyboard ? e => e.metaKey : e => e.altKey)(keyFromMouse);
|
||||
export const optionHeld = select(appleKeyboard ? e => e.altKey : e => e.ctrlKey)(keyFromMouse);
|
||||
export const shiftHeld = select(e => e.shiftKey)(keyFromMouse);
|
||||
|
||||
const cursorPosition = selectReduce((previous, position) => position || previous, { x: 0, y: 0 })(
|
||||
rawCursorPosition
|
||||
);
|
||||
export const cursorPosition = selectReduce((previous, position) => position || previous, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
})(rawCursorPosition);
|
||||
|
||||
const mouseButton = selectReduce(
|
||||
export const mouseButton = selectReduce(
|
||||
(prev, next) => {
|
||||
if (!next) {
|
||||
return prev;
|
||||
|
@ -66,12 +67,12 @@ const mouseButton = selectReduce(
|
|||
{ down: false, uid: null }
|
||||
)(mouseButtonEvent);
|
||||
|
||||
const mouseIsDown = selectReduce(
|
||||
export const mouseIsDown = selectReduce(
|
||||
(previous, next) => (next ? next.event === 'mouseDown' : previous),
|
||||
false
|
||||
)(mouseButtonEvent);
|
||||
|
||||
const gestureEnd = select(
|
||||
export const gestureEnd = select(
|
||||
action =>
|
||||
action &&
|
||||
(action.type === 'actionEvent' ||
|
||||
|
@ -85,39 +86,39 @@ const gestureEnd = select(
|
|||
* Edit: http://stable.ascii-flow.appspot.com/#567671116534197027/776257435
|
||||
*
|
||||
*
|
||||
* mouseIsDown
|
||||
* initial state: 'up' +-----------> 'downed'
|
||||
* ^ ^ + +
|
||||
* | | !mouseIsDown | |
|
||||
* !mouseIsDown | +-----------------+ | mouseIsDown && movedAlready
|
||||
* | |
|
||||
* +----+ 'dragging' <----+
|
||||
* mouseNowDown
|
||||
* initial state: 'up' +------------> 'downed'
|
||||
* ^ ^ + +
|
||||
* | | !mouseNowDown | |
|
||||
* !mouseNowDown | +------------------+ | mouseNowDown && movedAlready
|
||||
* | |
|
||||
* +----+ 'dragging' <-----+
|
||||
* + ^
|
||||
* | |
|
||||
* +------+
|
||||
* mouseIsDown
|
||||
* mouseNowDown
|
||||
*
|
||||
*/
|
||||
const mouseButtonStateTransitions = (state, mouseIsDown, movedAlready) => {
|
||||
const mouseButtonStateTransitions = (state, mouseNowDown, movedAlready) => {
|
||||
switch (state) {
|
||||
case 'up':
|
||||
return mouseIsDown ? 'downed' : 'up';
|
||||
return mouseNowDown ? 'downed' : 'up';
|
||||
case 'downed':
|
||||
if (mouseIsDown) {
|
||||
if (mouseNowDown) {
|
||||
return movedAlready ? 'dragging' : 'downed';
|
||||
} else {
|
||||
return 'up';
|
||||
}
|
||||
|
||||
case 'dragging':
|
||||
return mouseIsDown ? 'dragging' : 'up';
|
||||
return mouseNowDown ? 'dragging' : 'up';
|
||||
}
|
||||
};
|
||||
|
||||
const mouseButtonState = selectReduce(
|
||||
({ buttonState, downX, downY }, mouseIsDown, { x, y }) => {
|
||||
({ buttonState, downX, downY }, mouseNowDown, { x, y }) => {
|
||||
const movedAlready = x !== downX || y !== downY;
|
||||
const newButtonState = mouseButtonStateTransitions(buttonState, mouseIsDown, movedAlready);
|
||||
const newButtonState = mouseButtonStateTransitions(buttonState, mouseNowDown, movedAlready);
|
||||
return {
|
||||
buttonState: newButtonState,
|
||||
downX: newButtonState === 'downed' ? x : downX,
|
||||
|
@ -127,11 +128,11 @@ const mouseButtonState = selectReduce(
|
|||
{ buttonState: 'up', downX: null, downY: null }
|
||||
)(mouseIsDown, cursorPosition);
|
||||
|
||||
const mouseDowned = select(state => state.buttonState === 'downed')(mouseButtonState);
|
||||
export const mouseDowned = select(state => state.buttonState === 'downed')(mouseButtonState);
|
||||
|
||||
const dragging = select(state => state.buttonState === 'dragging')(mouseButtonState);
|
||||
export const dragging = select(state => state.buttonState === 'dragging')(mouseButtonState);
|
||||
|
||||
const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
|
||||
export const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
|
||||
down: buttonState !== 'up',
|
||||
x0: downX,
|
||||
y0: downY,
|
||||
|
@ -139,20 +140,6 @@ const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
|
|||
y1: y,
|
||||
}))(mouseButtonState, cursorPosition);
|
||||
|
||||
const actionEvent = select(action => (action.type === 'actionEvent' ? action.payload : null))(
|
||||
primaryUpdate
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
actionEvent,
|
||||
dragging,
|
||||
dragVector,
|
||||
cursorPosition,
|
||||
gestureEnd,
|
||||
metaHeld,
|
||||
mouseButton,
|
||||
mouseDowned,
|
||||
mouseIsDown,
|
||||
optionHeld,
|
||||
shiftHeld,
|
||||
};
|
||||
export const actionEvent = select(action =>
|
||||
action.type === 'actionEvent' ? action.payload : null
|
||||
)(primaryUpdate);
|
||||
|
|
|
@ -4,18 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const dom = require('./dom');
|
||||
const geometry = require('./geometry');
|
||||
const gestures = require('./gestures');
|
||||
const layout = require('./layout');
|
||||
const matrix = require('./matrix');
|
||||
const state = require('./state');
|
||||
import { matrixToCSS } from './dom';
|
||||
import { nextScene } from './layout';
|
||||
import { primaryUpdate } from './layout_functions';
|
||||
import { multiply, rotateZ, translate } from './matrix';
|
||||
import { createStore, select } from './state';
|
||||
|
||||
module.exports = {
|
||||
dom,
|
||||
geometry,
|
||||
gestures,
|
||||
layout,
|
||||
matrix,
|
||||
state,
|
||||
};
|
||||
export const layout = { nextScene, primaryUpdate };
|
||||
export const matrix = { multiply, rotateZ, translate };
|
||||
export const state = { createStore, select };
|
||||
export const toCSS = matrixToCSS;
|
||||
|
|
File diff suppressed because it is too large
Load diff
1418
x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
Normal file
1418
x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,345 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }]*/
|
||||
|
||||
/**
|
||||
* transpose
|
||||
*
|
||||
* Turns a row major ordered vector representation of a 4 x 4 matrix into a column major ordered vector representation, or
|
||||
* the other way around.
|
||||
*
|
||||
* Must pass a row major ordered vector if the goal is to obtain a column major ordered vector.
|
||||
*
|
||||
* We're using row major order in the _source code_ as this results in the correct visual shape of the matrix, but
|
||||
* `transform3d` needs column major order.
|
||||
*
|
||||
* This is what the matrix is: Eg. this is the equivalent matrix of `translate3d(${x}px, ${y}px, ${z}px)`:
|
||||
*
|
||||
* a e i m 1 0 0 x
|
||||
* b f j n 0 1 0 y
|
||||
* c g k o 0 0 1 z
|
||||
* d h l p 0 0 0 1
|
||||
*
|
||||
* but it's _not_ represented as a 2D array or array of arrays. CSS3 `transform3d` expects it as this vector:
|
||||
*
|
||||
* [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]
|
||||
*
|
||||
* so it's clear that the first _column vector_ corresponds to a, b, c, d but in source code, we must write a, e, i, m in
|
||||
* the first row if we want to visually resemble the above 4x4 matrix, ie. if we don't want that us programmers transpose
|
||||
* matrices in our heads.
|
||||
*
|
||||
*/
|
||||
const transpose = ([a, e, i, m, b, f, j, n, c, g, k, o, d, h, l, p]) => [
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g,
|
||||
h,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
l,
|
||||
m,
|
||||
n,
|
||||
o,
|
||||
p,
|
||||
];
|
||||
|
||||
const ORIGIN = [0, 0, 0, 1];
|
||||
|
||||
const NULLVECTOR = [0, 0, 0, 0];
|
||||
|
||||
const NULLMATRIX = transpose([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
const UNITMATRIX = transpose([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
||||
|
||||
// currently these functions expensively transpose; in a future version we can have way more efficient matrix operations
|
||||
// (eg. pre-transpose)
|
||||
const translate = (x, y, z) => transpose([1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1]);
|
||||
|
||||
const scale = (x, y, z) => transpose([x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]);
|
||||
|
||||
const shear = (x, y) => transpose([1, x, 0, 0, y, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
||||
|
||||
const perspective = d => transpose([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1 / d, 1]);
|
||||
|
||||
/**
|
||||
* rotate
|
||||
*
|
||||
* @param {number} x the x coordinate of the vector around which to rotate
|
||||
* @param {number} y the y coordinate of the vector around which to rotate
|
||||
* @param {number} z the z coordinate of the vector around which to rotate
|
||||
* @param {number} a rotation angle in radians
|
||||
* @returns {number[][]} a 4x4 transform matrix in column major order
|
||||
*/
|
||||
const rotate = (x, y, z, a) => {
|
||||
// it looks like the formula but inefficient; common terms could be precomputed, transpose can be avoided.
|
||||
// an optimizing compiler eg. Google Closure Advanced could perform most of the optimizations and JIT also watches out
|
||||
// for eg. common expressions
|
||||
|
||||
const sinA = Math.sin(a);
|
||||
const coshAi = 1 - Math.cos(a);
|
||||
|
||||
return transpose([
|
||||
1 + coshAi * (x * x - 1),
|
||||
z * sinA + x * y * coshAi,
|
||||
-y * sinA + x * y * coshAi,
|
||||
0,
|
||||
-z * sinA + x * y * coshAi,
|
||||
1 + coshAi * (y * y - 1),
|
||||
x * sinA + y * x * coshAi,
|
||||
0,
|
||||
y * sinA + x * z * coshAi,
|
||||
-x * sinA + y * z * coshAi,
|
||||
1 + coshAi * (z * z - 1),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* rotate_ functions
|
||||
*
|
||||
* @param {number} a
|
||||
* @returns {number[][]}
|
||||
*
|
||||
* Should be replaced with more efficient direct versions rather than going through the generic `rotate3d` function.
|
||||
*/
|
||||
const rotateX = a => rotate(1, 0, 0, a);
|
||||
const rotateY = a => rotate(0, 1, 0, a);
|
||||
const rotateZ = a => rotate(0, 0, 1, a);
|
||||
|
||||
/**
|
||||
* multiply
|
||||
*
|
||||
* Matrix multiplies two matrices of column major format, returning the result in the same format
|
||||
*
|
||||
*
|
||||
* A E I M
|
||||
* B F J N
|
||||
* C G K O
|
||||
* D H L P
|
||||
*
|
||||
* a e i m . . . .
|
||||
* b f j n . . . .
|
||||
* c g k o . . . .
|
||||
* d h l p . . . d * M + h * N + l * O + p * P
|
||||
*
|
||||
*/
|
||||
const mult = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p],
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]
|
||||
) => [
|
||||
a * A + e * B + i * C + m * D,
|
||||
b * A + f * B + j * C + n * D,
|
||||
c * A + g * B + k * C + o * D,
|
||||
d * A + h * B + l * C + p * D,
|
||||
|
||||
a * E + e * F + i * G + m * H,
|
||||
b * E + f * F + j * G + n * H,
|
||||
c * E + g * F + k * G + o * H,
|
||||
d * E + h * F + l * G + p * H,
|
||||
|
||||
a * I + e * J + i * K + m * L,
|
||||
b * I + f * J + j * K + n * L,
|
||||
c * I + g * J + k * K + o * L,
|
||||
d * I + h * J + l * K + p * L,
|
||||
|
||||
a * M + e * N + i * O + m * P,
|
||||
b * M + f * N + j * O + n * P,
|
||||
c * M + g * N + k * O + o * P,
|
||||
d * M + h * N + l * O + p * P,
|
||||
];
|
||||
|
||||
const multiply = (...elements) =>
|
||||
elements.slice(1).reduce((prev, next) => mult(prev, next), elements[0]);
|
||||
|
||||
/**
|
||||
* mvMultiply
|
||||
*
|
||||
* Multiplies a matrix and a vector
|
||||
*
|
||||
*
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
* D
|
||||
*
|
||||
* a e i m .
|
||||
* b f j n .
|
||||
* c g k o .
|
||||
* d h l p d * A + h * B + l * C + p * D
|
||||
*
|
||||
*/
|
||||
const mvMultiply = ([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p], [A, B, C, D]) => [
|
||||
a * A + e * B + i * C + m * D,
|
||||
b * A + f * B + j * C + n * D,
|
||||
c * A + g * B + k * C + o * D,
|
||||
d * A + h * B + l * C + p * D,
|
||||
];
|
||||
|
||||
const normalize = ([A, B, C, D]) => (D === 1 ? [A, B, C, D] : [A / D, B / D, C / D, 1]);
|
||||
|
||||
/**
|
||||
* invert
|
||||
*
|
||||
* Inverts the matrix
|
||||
*
|
||||
* a e i m
|
||||
* b f j n
|
||||
* c g k o
|
||||
* d h l p
|
||||
*/
|
||||
const invert = ([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]) => {
|
||||
const inv = [
|
||||
f * k * p - f * l * o - j * g * p + j * h * o + n * g * l - n * h * k,
|
||||
-b * k * p + b * l * o + j * c * p - j * d * o - n * c * l + n * d * k,
|
||||
b * g * p - b * h * o - f * c * p + f * d * o + n * c * h - n * d * g,
|
||||
-b * g * l + b * h * k + f * c * l - f * d * k - j * c * h + j * d * g,
|
||||
-e * k * p + e * l * o + i * g * p - i * h * o - m * g * l + m * h * k,
|
||||
a * k * p - a * l * o - i * c * p + i * d * o + m * c * l - m * d * k,
|
||||
-a * g * p + a * h * o + e * c * p - e * d * o - m * c * h + m * d * g,
|
||||
a * g * l - a * h * k - e * c * l + e * d * k + i * c * h - i * d * g,
|
||||
e * j * p - e * l * n - i * f * p + i * h * n + m * f * l - m * h * j,
|
||||
-a * j * p + a * l * n + i * b * p - i * d * n - m * b * l + m * d * j,
|
||||
a * f * p - a * h * n - e * b * p + e * d * n + m * b * h - m * d * f,
|
||||
-a * f * l + a * h * j + e * b * l - e * d * j - i * b * h + i * d * f,
|
||||
-e * j * o + e * k * n + i * f * o - i * g * n - m * f * k + m * g * j,
|
||||
a * j * o - a * k * n - i * b * o + i * c * n + m * b * k - m * c * j,
|
||||
-a * f * o + a * g * n + e * b * o - e * c * n - m * b * g + m * c * f,
|
||||
a * f * k - a * g * j - e * b * k + e * c * j + i * b * g - i * c * f,
|
||||
];
|
||||
|
||||
const det = a * inv[0] + b * inv[4] + c * inv[8] + d * inv[12];
|
||||
|
||||
if (det === 0) {
|
||||
return false; // no solution
|
||||
} else {
|
||||
const recDet = 1 / det;
|
||||
|
||||
for (let index = 0; index < 16; index++) {
|
||||
inv[index] *= recDet;
|
||||
}
|
||||
|
||||
return inv;
|
||||
}
|
||||
};
|
||||
|
||||
const translateComponent = a => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, a[12], a[13], a[14], 1];
|
||||
|
||||
const compositeComponent = ([a, b, c, d, e, f, g, h, i, j, k, l, _m, _n, _o, p]) => [
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g,
|
||||
h,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
l,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
p,
|
||||
];
|
||||
|
||||
const add = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p],
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]
|
||||
) => [
|
||||
a + A,
|
||||
b + B,
|
||||
c + C,
|
||||
d + D,
|
||||
e + E,
|
||||
f + F,
|
||||
g + G,
|
||||
h + H,
|
||||
i + I,
|
||||
j + J,
|
||||
k + K,
|
||||
l + L,
|
||||
m + M,
|
||||
n + N,
|
||||
o + O,
|
||||
p + P,
|
||||
];
|
||||
|
||||
const subtract = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p],
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]
|
||||
) => [
|
||||
a - A,
|
||||
b - B,
|
||||
c - C,
|
||||
d - D,
|
||||
e - E,
|
||||
f - F,
|
||||
g - G,
|
||||
h - H,
|
||||
i - I,
|
||||
j - J,
|
||||
k - K,
|
||||
l - L,
|
||||
m - M,
|
||||
n - N,
|
||||
o - O,
|
||||
p - P,
|
||||
];
|
||||
|
||||
const reduceTransforms = transforms =>
|
||||
transforms.length === 1
|
||||
? transforms[0]
|
||||
: transforms.slice(1).reduce((prev, next) => multiply(prev, next), transforms[0]);
|
||||
|
||||
// applies an arbitrary number of transforms - left to right - to a preexisting transform matrix
|
||||
const applyTransforms = (transforms, previousTransformMatrix) =>
|
||||
transforms.reduce((prev, next) => multiply(prev, next), previousTransformMatrix);
|
||||
|
||||
const clamp = (low, high, value) => Math.min(high, Math.max(low, value));
|
||||
|
||||
const matrixToAngle = transformMatrix => {
|
||||
// clamping is needed, otherwise inevitable floating point inaccuracies can cause NaN
|
||||
const z0 = Math.acos(clamp(-1, 1, transformMatrix[0]));
|
||||
const z1 = Math.asin(clamp(-1, 1, transformMatrix[1]));
|
||||
return z1 > 0 ? z0 : -z0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ORIGIN,
|
||||
NULLVECTOR,
|
||||
NULLMATRIX,
|
||||
UNITMATRIX,
|
||||
transpose,
|
||||
translate,
|
||||
shear,
|
||||
rotateX,
|
||||
rotateY,
|
||||
rotateZ,
|
||||
scale,
|
||||
perspective,
|
||||
matrixToAngle,
|
||||
multiply,
|
||||
mvMultiply,
|
||||
invert,
|
||||
normalize,
|
||||
applyTransforms,
|
||||
reduceTransforms,
|
||||
translateComponent,
|
||||
compositeComponent,
|
||||
add,
|
||||
subtract,
|
||||
};
|
285
x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts
Normal file
285
x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts
Normal file
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Column major order:
|
||||
*
|
||||
* Instead of a row major ordered vector representation of a 4 x 4 matrix, we use column major ordered vectors.
|
||||
*
|
||||
* This is what the matrix is: Eg. this is the equivalent matrix of `translate3d(${x}px, ${y}px, ${z}px)`:
|
||||
*
|
||||
* a e i m 1 0 0 x
|
||||
* b f j n 0 1 0 y
|
||||
* c g k o 0 0 1 z
|
||||
* d h l p 0 0 0 1
|
||||
*
|
||||
* but it's _not_ represented as a 2D array or array of arrays. CSS3 `transform3d` expects it as this vector:
|
||||
*
|
||||
* [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]
|
||||
*
|
||||
* so it's clear that the first _column vector_ corresponds to a, b, c, d.
|
||||
*
|
||||
*/
|
||||
|
||||
import { transformMatrix3d, vector3d } from './types';
|
||||
|
||||
const NANMATRIX = [
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
] as transformMatrix3d;
|
||||
|
||||
export const ORIGIN = [0, 0, 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;
|
||||
|
||||
export const scale = (x: number, y: number, z: number): transformMatrix3d =>
|
||||
[x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1] as transformMatrix3d;
|
||||
|
||||
export const rotateZ = (a: number): transformMatrix3d => {
|
||||
const sinA = Math.sin(a);
|
||||
const cosA = Math.cos(a);
|
||||
return [cosA, -sinA, 0, 0, sinA, cosA, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] as transformMatrix3d;
|
||||
};
|
||||
|
||||
/**
|
||||
* multiply
|
||||
*
|
||||
* Matrix multiplies two matrices of column major format, returning the result in the same format
|
||||
*
|
||||
*
|
||||
* A E I M
|
||||
* B F J N
|
||||
* C G K O
|
||||
* D H L P
|
||||
*
|
||||
* a e i m . . . .
|
||||
* b f j n . . . .
|
||||
* c g k o . . . .
|
||||
* d h l p . . . d * M + h * N + l * O + p * P
|
||||
*
|
||||
*/
|
||||
const mult = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]: transformMatrix3d,
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]: transformMatrix3d
|
||||
): transformMatrix3d =>
|
||||
[
|
||||
a * A + e * B + i * C + m * D,
|
||||
b * A + f * B + j * C + n * D,
|
||||
c * A + g * B + k * C + o * D,
|
||||
d * A + h * B + l * C + p * D,
|
||||
|
||||
a * E + e * F + i * G + m * H,
|
||||
b * E + f * F + j * G + n * H,
|
||||
c * E + g * F + k * G + o * H,
|
||||
d * E + h * F + l * G + p * H,
|
||||
|
||||
a * I + e * J + i * K + m * L,
|
||||
b * I + f * J + j * K + n * L,
|
||||
c * I + g * J + k * K + o * L,
|
||||
d * I + h * J + l * K + p * L,
|
||||
|
||||
a * M + e * N + i * O + m * P,
|
||||
b * M + f * N + j * O + n * P,
|
||||
c * M + g * N + k * O + o * P,
|
||||
d * M + h * N + l * O + p * P,
|
||||
] as transformMatrix3d;
|
||||
|
||||
export const multiply = (
|
||||
first: transformMatrix3d,
|
||||
...rest: transformMatrix3d[]
|
||||
): transformMatrix3d => rest.reduce((prev, next) => mult(prev, next), first);
|
||||
|
||||
/**
|
||||
* mvMultiply
|
||||
*
|
||||
* Multiplies a matrix and a vector
|
||||
*
|
||||
*
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
* D
|
||||
*
|
||||
* a e i m .
|
||||
* b f j n .
|
||||
* c g k o .
|
||||
* d h l p d * A + h * B + l * C + p * D
|
||||
*
|
||||
*/
|
||||
export const mvMultiply = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]: transformMatrix3d,
|
||||
[A, B, C, D]: vector3d
|
||||
): vector3d =>
|
||||
[
|
||||
a * A + e * B + i * C + m * D,
|
||||
b * A + f * B + j * C + n * D,
|
||||
c * A + g * B + k * C + o * D,
|
||||
d * A + h * B + l * C + p * D,
|
||||
] as vector3d;
|
||||
|
||||
export const normalize = ([A, B, C, D]: vector3d): vector3d =>
|
||||
D === 1 ? ([A, B, C, D] as vector3d) : ([A / D, B / D, C / D, 1] as vector3d);
|
||||
|
||||
/**
|
||||
* invert
|
||||
*
|
||||
* Inverts the matrix
|
||||
*
|
||||
* a e i m
|
||||
* b f j n
|
||||
* c g k o
|
||||
* d h l p
|
||||
*/
|
||||
export const invert = ([
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g,
|
||||
h,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
l,
|
||||
m,
|
||||
n,
|
||||
o,
|
||||
p,
|
||||
]: transformMatrix3d): transformMatrix3d => {
|
||||
const inv = [
|
||||
f * k * p - f * l * o - j * g * p + j * h * o + n * g * l - n * h * k,
|
||||
-b * k * p + b * l * o + j * c * p - j * d * o - n * c * l + n * d * k,
|
||||
b * g * p - b * h * o - f * c * p + f * d * o + n * c * h - n * d * g,
|
||||
-b * g * l + b * h * k + f * c * l - f * d * k - j * c * h + j * d * g,
|
||||
-e * k * p + e * l * o + i * g * p - i * h * o - m * g * l + m * h * k,
|
||||
a * k * p - a * l * o - i * c * p + i * d * o + m * c * l - m * d * k,
|
||||
-a * g * p + a * h * o + e * c * p - e * d * o - m * c * h + m * d * g,
|
||||
a * g * l - a * h * k - e * c * l + e * d * k + i * c * h - i * d * g,
|
||||
e * j * p - e * l * n - i * f * p + i * h * n + m * f * l - m * h * j,
|
||||
-a * j * p + a * l * n + i * b * p - i * d * n - m * b * l + m * d * j,
|
||||
a * f * p - a * h * n - e * b * p + e * d * n + m * b * h - m * d * f,
|
||||
-a * f * l + a * h * j + e * b * l - e * d * j - i * b * h + i * d * f,
|
||||
-e * j * o + e * k * n + i * f * o - i * g * n - m * f * k + m * g * j,
|
||||
a * j * o - a * k * n - i * b * o + i * c * n + m * b * k - m * c * j,
|
||||
-a * f * o + a * g * n + e * b * o - e * c * n - m * b * g + m * c * f,
|
||||
a * f * k - a * g * j - e * b * k + e * c * j + i * b * g - i * c * f,
|
||||
] as transformMatrix3d;
|
||||
|
||||
const det = a * inv[0] + b * inv[4] + c * inv[8] + d * inv[12];
|
||||
|
||||
if (det === 0) {
|
||||
return NANMATRIX; // no real solution
|
||||
} else {
|
||||
const recDet = 1 / det;
|
||||
|
||||
for (let index = 0; index < 16; index++) {
|
||||
inv[index] *= recDet;
|
||||
}
|
||||
|
||||
return inv;
|
||||
}
|
||||
};
|
||||
|
||||
export const translateComponent = (a: transformMatrix3d): transformMatrix3d =>
|
||||
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, a[12], a[13], a[14], 1] as transformMatrix3d;
|
||||
|
||||
export const compositeComponent = ([
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g,
|
||||
h,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
l,
|
||||
m,
|
||||
n,
|
||||
o,
|
||||
p,
|
||||
]: transformMatrix3d): transformMatrix3d =>
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, 0, 0, 0, p] as transformMatrix3d;
|
||||
|
||||
export const add = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]: transformMatrix3d,
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]: transformMatrix3d
|
||||
): transformMatrix3d =>
|
||||
[
|
||||
a + A,
|
||||
b + B,
|
||||
c + C,
|
||||
d + D,
|
||||
e + E,
|
||||
f + F,
|
||||
g + G,
|
||||
h + H,
|
||||
i + I,
|
||||
j + J,
|
||||
k + K,
|
||||
l + L,
|
||||
m + M,
|
||||
n + N,
|
||||
o + O,
|
||||
p + P,
|
||||
] as transformMatrix3d;
|
||||
|
||||
export const subtract = (
|
||||
[a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]: transformMatrix3d,
|
||||
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]: transformMatrix3d
|
||||
): transformMatrix3d =>
|
||||
[
|
||||
a - A,
|
||||
b - B,
|
||||
c - C,
|
||||
d - D,
|
||||
e - E,
|
||||
f - F,
|
||||
g - G,
|
||||
h - H,
|
||||
i - I,
|
||||
j - J,
|
||||
k - K,
|
||||
l - L,
|
||||
m - M,
|
||||
n - N,
|
||||
o - O,
|
||||
p - P,
|
||||
] as transformMatrix3d;
|
||||
|
||||
export const reduceTransforms = (transforms: transformMatrix3d[]): transformMatrix3d =>
|
||||
transforms.length === 1
|
||||
? transforms[0]
|
||||
: transforms.slice(1).reduce((prev, next) => multiply(prev, next), transforms[0]);
|
||||
|
||||
const clamp = (low: number, high: number, value: number): number =>
|
||||
Math.min(high, Math.max(low, value));
|
||||
|
||||
export const matrixToAngle = (transformMatrix: transformMatrix3d): number => {
|
||||
// clamping is needed, otherwise inevitable floating point inaccuracies can cause NaN
|
||||
const z0 = Math.acos(clamp(-1, 1, transformMatrix[0]));
|
||||
const z1 = Math.asin(clamp(-1, 1, transformMatrix[1]));
|
||||
return z1 > 0 ? z0 : -z0;
|
||||
};
|
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* transpose
|
||||
*
|
||||
* Turns a row major ordered vector representation of a 4 x 4 matrix into a column major ordered vector representation, or
|
||||
* the other way around.
|
||||
*
|
||||
* Must pass a row major ordered vector if the goal is to obtain a column major ordered vector.
|
||||
*
|
||||
* We're using row major order in the _source code_ as this results in the correct visual shape of the matrix, but
|
||||
* `transform3d` needs column major order.
|
||||
*
|
||||
* This is what the matrix is: Eg. this is the equivalent matrix of `translate(${x}px, ${y}px)`:
|
||||
*
|
||||
* a d g 1 0 x
|
||||
* b e h 0 1 y
|
||||
* c f i 0 0 1
|
||||
*
|
||||
* but it's _not_ represented as a 2D array or array of arrays.
|
||||
*
|
||||
* [a, b, c, d, e, f, g, h, i]
|
||||
*
|
||||
*/
|
||||
const transpose = ([a, d, g, b, e, h, c, f, i]) => [a, b, c, d, e, f, g, h, i];
|
||||
|
||||
const ORIGIN = [0, 0, 1];
|
||||
|
||||
const NULLVECTOR = [0, 0, 0];
|
||||
|
||||
const NULLMATRIX = transpose([0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
const UNITMATRIX = transpose([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
||||
|
||||
// currently these functions expensively transpose; in a future version we can have way more efficient matrix operations
|
||||
// (eg. pre-transpose)
|
||||
const translate = (x, y) => transpose([1, 0, x, 0, 1, y, 0, 0, 1]);
|
||||
|
||||
const scale = (x, y) => transpose([x, 0, 0, 0, y, 0, 0, 0, 1]);
|
||||
|
||||
const shear = (x, y) => transpose([1, x, 0, y, 1, 0, 0, 0, 1]);
|
||||
|
||||
/**
|
||||
* multiply
|
||||
*
|
||||
* Matrix multiplies two matrices of column major format, returning the result in the same format
|
||||
*
|
||||
*
|
||||
* A D G
|
||||
* B E H
|
||||
* C F I
|
||||
*
|
||||
* a d g . . .
|
||||
* b e h . . .
|
||||
* c f i . . c * G + f * H + i * I
|
||||
*
|
||||
*/
|
||||
const mult = ([a, b, c, d, e, f, g, h, i], [A, B, C, D, E, F, G, H, I]) => [
|
||||
a * A + d * B + g * C,
|
||||
b * A + e * B + h * C,
|
||||
c * A + f * B + i * C,
|
||||
|
||||
a * D + d * E + g * F,
|
||||
b * D + e * E + h * F,
|
||||
c * D + f * E + i * F,
|
||||
|
||||
a * G + d * H + g * I,
|
||||
b * G + e * H + h * I,
|
||||
c * G + f * H + i * I,
|
||||
];
|
||||
|
||||
const multiply = (...elements) =>
|
||||
elements.slice(1).reduce((prev, next) => mult(prev, next), elements[0]);
|
||||
|
||||
/**
|
||||
* mvMultiply
|
||||
*
|
||||
* Multiplies a matrix and a vector
|
||||
*
|
||||
*
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
*
|
||||
* a d g .
|
||||
* b e h .
|
||||
* c f i c * A + f * B + i * C
|
||||
*
|
||||
*/
|
||||
const mvMultiply = ([a, b, c, d, e, f, g, h, i], [A, B, C]) => [
|
||||
a * A + d * B + g * C,
|
||||
b * A + e * B + h * C,
|
||||
c * A + f * B + i * C,
|
||||
];
|
||||
|
||||
const normalize = ([A, B, C]) => (C === 1 ? [A, B, C] : [A / C, B / C, 1]);
|
||||
|
||||
const add = ([a, b, c, d, e, f, g, h, i], [A, B, C, D, E, F, G, H, I]) => [
|
||||
a + A,
|
||||
b + B,
|
||||
c + C,
|
||||
d + D,
|
||||
e + E,
|
||||
f + F,
|
||||
g + G,
|
||||
h + H,
|
||||
i + I,
|
||||
];
|
||||
|
||||
const subtract = ([a, b, c, d, e, f, g, h, i], [A, B, C, D, E, F, G, H, I]) => [
|
||||
a - A,
|
||||
b - B,
|
||||
c - C,
|
||||
d - D,
|
||||
e - E,
|
||||
f - F,
|
||||
g - G,
|
||||
h - H,
|
||||
i - I,
|
||||
];
|
||||
|
||||
const reduceTransforms = transforms =>
|
||||
transforms.length === 1
|
||||
? transforms[0]
|
||||
: transforms.slice(1).reduce((prev, next) => multiply(prev, next), transforms[0]);
|
||||
|
||||
// applies an arbitrary number of transforms - left to right - to a preexisting transform matrix
|
||||
const applyTransforms = (transforms, previousTransformMatrix) =>
|
||||
transforms.reduce((prev, next) => multiply(prev, next), previousTransformMatrix);
|
||||
|
||||
/**
|
||||
*
|
||||
* componentProduct
|
||||
*
|
||||
*/
|
||||
const componentProduct = ([a, b, c], [A, B, C]) => [a * A, b * B, c * C];
|
||||
|
||||
module.exports = {
|
||||
ORIGIN,
|
||||
NULLVECTOR,
|
||||
NULLMATRIX,
|
||||
UNITMATRIX,
|
||||
transpose,
|
||||
translate,
|
||||
shear,
|
||||
scale,
|
||||
multiply,
|
||||
mvMultiply,
|
||||
normalize,
|
||||
applyTransforms,
|
||||
reduceTransforms,
|
||||
add,
|
||||
subtract,
|
||||
componentProduct,
|
||||
};
|
93
x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts
Normal file
93
x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { transformMatrix2d, vector2d } from './types';
|
||||
|
||||
export const ORIGIN = [0, 0, 1] as vector2d;
|
||||
|
||||
export const UNITMATRIX = [1, 0, 0, 0, 1, 0, 0, 0, 1] as transformMatrix2d;
|
||||
|
||||
export const translate = (x: number, y: number): transformMatrix2d =>
|
||||
[1, 0, 0, 0, 1, 0, x, y, 1] as transformMatrix2d;
|
||||
|
||||
export const scale = (x: number, y: number): transformMatrix2d =>
|
||||
[x, 0, 0, 0, y, 0, 0, 0, 1] as transformMatrix2d;
|
||||
|
||||
/**
|
||||
* multiply
|
||||
*
|
||||
* Matrix multiplies two matrices of column major format, returning the result in the same format
|
||||
*
|
||||
*
|
||||
* A D G
|
||||
* B E H
|
||||
* C F I
|
||||
*
|
||||
* a d g . . .
|
||||
* b e h . . .
|
||||
* c f i . . c * G + f * H + i * I
|
||||
*
|
||||
*/
|
||||
const mult = (
|
||||
[a, b, c, d, e, f, g, h, i]: transformMatrix2d,
|
||||
[A, B, C, D, E, F, G, H, I]: transformMatrix2d
|
||||
): transformMatrix2d =>
|
||||
[
|
||||
a * A + d * B + g * C,
|
||||
b * A + e * B + h * C,
|
||||
c * A + f * B + i * C,
|
||||
|
||||
a * D + d * E + g * F,
|
||||
b * D + e * E + h * F,
|
||||
c * D + f * E + i * F,
|
||||
|
||||
a * G + d * H + g * I,
|
||||
b * G + e * H + h * I,
|
||||
c * G + f * H + i * I,
|
||||
] as transformMatrix2d;
|
||||
|
||||
export const multiply = (
|
||||
first: transformMatrix2d,
|
||||
...rest: transformMatrix2d[]
|
||||
): transformMatrix2d => rest.reduce((prev, next) => mult(prev, next), first);
|
||||
|
||||
/**
|
||||
* mvMultiply
|
||||
*
|
||||
* Multiplies a matrix and a vector
|
||||
*
|
||||
*
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
*
|
||||
* a d g .
|
||||
* b e h .
|
||||
* c f i c * A + f * B + i * C
|
||||
*
|
||||
*/
|
||||
export const mvMultiply = (
|
||||
[a, b, c, d, e, f, g, h, i]: transformMatrix2d,
|
||||
[A, B, C]: vector2d
|
||||
): vector2d => [a * A + d * B + g * C, b * A + e * B + h * C, c * A + f * B + i * C] as vector2d;
|
||||
|
||||
export const normalize = ([A, B, C]: vector2d): vector2d =>
|
||||
C === 1 ? ([A, B, C] as vector2d) : ([A / C, B / C, 1] as vector2d);
|
||||
|
||||
export const add = (
|
||||
[a, b, c, d, e, f, g, h, i]: transformMatrix2d,
|
||||
[A, B, C, D, E, F, G, H, I]: transformMatrix2d
|
||||
): transformMatrix2d =>
|
||||
[a + A, b + B, c + C, d + D, e + E, f + F, g + G, h + H, i + I] as transformMatrix2d;
|
||||
|
||||
export const subtract = (
|
||||
[a, b, c, d, e, f, g, h, i]: transformMatrix2d,
|
||||
[A, B, C, D, E, F, G, H, I]: transformMatrix2d
|
||||
): transformMatrix2d =>
|
||||
[a - A, b - B, c - C, d - D, e - E, f - F, g - G, h - H, i - I] as transformMatrix2d;
|
||||
|
||||
export const componentProduct = ([a, b, c]: vector2d, [A, B, C]: vector2d): vector2d =>
|
||||
[a * A, b * B, c * C] as vector2d;
|
|
@ -4,47 +4,66 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const { shallowEqual } = require('./functional');
|
||||
import {
|
||||
ActionId,
|
||||
ChangeCallbackFunction,
|
||||
Meta,
|
||||
NodeFunction,
|
||||
NodeResult,
|
||||
Payload,
|
||||
TypeName,
|
||||
UpdaterFunction,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* PoC action dispatch
|
||||
*/
|
||||
export const shallowEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeUid = () => 1e11 + Math.floor((1e12 - 1e11) * Math.random());
|
||||
const makeUid = (): ActionId => 1e11 + Math.floor((1e12 - 1e11) * Math.random());
|
||||
|
||||
const selectReduce = (fun, previousValue, mapFun = d => d, logFun) => (...inputs) => {
|
||||
export const selectReduce = (fun: NodeFunction, previousValue: NodeResult): NodeFunction => (
|
||||
...inputs: NodeFunction[]
|
||||
): NodeResult => {
|
||||
// last-value memoizing version of this single line function:
|
||||
// (fun, previousValue) => (...inputs) => state => previousValue = fun(previousValue, ...inputs.map(input => input(state)))
|
||||
let argumentValues = [];
|
||||
let argumentValues = [] as NodeResult[];
|
||||
let value = previousValue;
|
||||
let prevValue = previousValue;
|
||||
let mappedValue;
|
||||
return state => {
|
||||
return (state: NodeResult) => {
|
||||
if (
|
||||
shallowEqual(argumentValues, (argumentValues = inputs.map(input => input(state)))) &&
|
||||
value === prevValue
|
||||
) {
|
||||
return mappedValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
prevValue = value;
|
||||
value = fun(prevValue, ...argumentValues);
|
||||
if (logFun) {
|
||||
logFun(value, argumentValues);
|
||||
}
|
||||
mappedValue = mapFun(value);
|
||||
return mappedValue;
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
const select = (fun, logFun) => (...inputs) => {
|
||||
export const select = (fun: NodeFunction): NodeFunction => (
|
||||
...inputs: NodeFunction[]
|
||||
): NodeResult => {
|
||||
// last-value memoizing version of this single line function:
|
||||
// fun => (...inputs) => state => fun(...inputs.map(input => input(state)))
|
||||
let argumentValues = [];
|
||||
let value;
|
||||
let actionId;
|
||||
return state => {
|
||||
const lastActionId = state.primaryUpdate.payload.uid;
|
||||
let argumentValues = [] as NodeResult[];
|
||||
let value: NodeResult;
|
||||
let actionId: ActionId;
|
||||
return (state: NodeResult) => {
|
||||
const lastActionId: ActionId = state.primaryUpdate.payload.uid;
|
||||
if (
|
||||
actionId === lastActionId ||
|
||||
shallowEqual(argumentValues, (argumentValues = inputs.map(input => input(state))))
|
||||
|
@ -54,21 +73,20 @@ const select = (fun, logFun) => (...inputs) => {
|
|||
|
||||
value = fun(...argumentValues);
|
||||
actionId = lastActionId;
|
||||
if (logFun) {
|
||||
logFun(value, argumentValues);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
const createStore = (initialState, onChangeCallback = () => {}) => {
|
||||
export const createStore = (initialState: NodeResult, onChangeCallback: ChangeCallbackFunction) => {
|
||||
let currentState = initialState;
|
||||
let updater = state => state; // default: no side effect
|
||||
let updater: UpdaterFunction = (state: NodeResult): NodeResult => state; // default: no side effect
|
||||
const getCurrentState = () => currentState;
|
||||
// const setCurrentState = newState => (currentState = newState);
|
||||
const setUpdater = updaterFunction => (updater = updaterFunction);
|
||||
const setUpdater = (updaterFunction: UpdaterFunction) => {
|
||||
updater = updaterFunction;
|
||||
};
|
||||
|
||||
const commit = (type, payload, meta = {}) => {
|
||||
const commit = (type: TypeName, payload: Payload, meta: Meta = { silent: false }) => {
|
||||
currentState = updater({
|
||||
...currentState,
|
||||
primaryUpdate: {
|
||||
|
@ -81,14 +99,7 @@ const createStore = (initialState, onChangeCallback = () => {}) => {
|
|||
}
|
||||
};
|
||||
|
||||
const dispatch = (type, payload) => setTimeout(() => commit(type, payload));
|
||||
const dispatch = (type: TypeName, payload: Payload) => commit(type, payload);
|
||||
|
||||
return { getCurrentState, setUpdater, commit, dispatch };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createStore,
|
||||
select,
|
||||
selectReduce,
|
||||
makeUid,
|
||||
};
|
30
x-pack/plugins/canvas/public/lib/aeroelastic/types.ts
Normal file
30
x-pack/plugins/canvas/public/lib/aeroelastic/types.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
type f64 = number; // eventual AssemblyScript compatibility; doesn't hurt with vanilla TS either
|
||||
type f = f64; // shorthand
|
||||
|
||||
export type vector2d = [f, f, f] & ReadonlyArray<f> & { __nominal: 'vector2d' };
|
||||
export type vector3d = [f, f, f, f] & ReadonlyArray<f> & { __nominal: 'vector3d' };
|
||||
|
||||
export type transformMatrix2d = [f, f, f, f, f, f, f, f, f] &
|
||||
ReadonlyArray<f> & { __nominal: 'transformMatrix2d' };
|
||||
export type transformMatrix3d = [f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f] &
|
||||
ReadonlyArray<f> & { __nominal: 'transformMatrix3d' };
|
||||
|
||||
export interface Meta {
|
||||
silent: boolean;
|
||||
}
|
||||
export type ActionId = number;
|
||||
export type TypeName = string;
|
||||
export type NodeResult = any;
|
||||
export type Payload = any;
|
||||
export type NodeFunction = (...args: any[]) => any;
|
||||
export type UpdaterFunction = (arg: NodeResult) => NodeResult;
|
||||
export type ChangeCallbackFunction = (
|
||||
{ type, state }: { type: TypeName; state: NodeResult },
|
||||
meta: Meta
|
||||
) => void;
|
|
@ -4,12 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import aero from './aeroelastic';
|
||||
import { layout, matrix, state } from './aeroelastic';
|
||||
|
||||
const stores = new Map();
|
||||
|
||||
export const aeroelastic = {
|
||||
matrix: aero.matrix,
|
||||
matrix,
|
||||
|
||||
clearStores() {
|
||||
stores.clear();
|
||||
|
@ -20,14 +20,14 @@ export const aeroelastic = {
|
|||
throw new Error('Only a single aeroelastic store per page should exist');
|
||||
}
|
||||
|
||||
stores.set(page, aero.state.createStore(initialState, onChangeCallback));
|
||||
stores.set(page, state.createStore(initialState, onChangeCallback));
|
||||
|
||||
const updateScene = aero.state.select((nextScene, primaryUpdate) => ({
|
||||
const updateScene = state.select((nextScene, primaryUpdate) => ({
|
||||
shapeAdditions: nextScene.shapes,
|
||||
primaryUpdate,
|
||||
currentScene: nextScene,
|
||||
configuration: nextScene.configuration,
|
||||
}))(aero.layout.nextScene, aero.layout.primaryUpdate);
|
||||
}))(layout.nextScene, layout.primaryUpdate);
|
||||
|
||||
stores.get(page).setUpdater(updateScene);
|
||||
},
|
||||
|
|
|
@ -8,7 +8,6 @@ import { shallowEqual } from 'recompose';
|
|||
import { aeroelastic as aero } from '../../lib/aeroelastic_kibana';
|
||||
import { matrixToAngle } from '../../lib/aeroelastic/matrix';
|
||||
import { arrayToMap, identity } from '../../lib/aeroelastic/functional';
|
||||
import defaultConfiguration from '../../lib/aeroelastic/config';
|
||||
import {
|
||||
addElement,
|
||||
removeElements,
|
||||
|
@ -24,7 +23,38 @@ import { appReady } from '../actions/app';
|
|||
import { setWorkpad } from '../actions/workpad';
|
||||
import { getNodes, getPages, getSelectedPage, getSelectedElement } from '../selectors/workpad';
|
||||
|
||||
const isGroupId = id => id.startsWith(defaultConfiguration.groupName);
|
||||
const aeroelasticConfiguration = {
|
||||
adHocGroupName: 'adHocGroup',
|
||||
alignmentGuideName: 'alignmentGuide',
|
||||
atopZ: 1000,
|
||||
depthSelect: true,
|
||||
devColor: 'magenta',
|
||||
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,
|
||||
shortcuts: false,
|
||||
singleSelect: false,
|
||||
snapConstraint: true,
|
||||
};
|
||||
|
||||
const isGroupId = id => id.startsWith(aeroelasticConfiguration.groupName);
|
||||
|
||||
/**
|
||||
* elementToShape
|
||||
|
@ -229,7 +259,7 @@ export const aeroelastic = ({ dispatch, getState }) => {
|
|||
shapeAdditions: [],
|
||||
primaryUpdate: null,
|
||||
currentScene: { shapes: [] },
|
||||
configuration: defaultConfiguration,
|
||||
configuration: aeroelasticConfiguration,
|
||||
},
|
||||
onChangeCallback,
|
||||
page
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue