mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Chore: Remove disused shapeAdditions * Refactor: Move configuration into the scene * Chore: Remove disused dispatch * Refactor: move out DOM helper not coupled with layout functions * Chore: make node id generation idempotent * Refactor: Remove selectReduce * Refactor: code alignment with data flow (selector hierarchy) * Refactor: reduced API surface area * Refactor: trivially split state.js * Refactor: simplify `select` * Refactor: extract out workpadPage components * Refactor: rename dag_start * Chore: make todo more salient * Fix: remove chance of collision (two subsequent large random integers may equal) * Chore: split the two captured variables to their own `let`
This commit is contained in:
parent
516afba2ca
commit
a2f35c34d9
19 changed files with 304 additions and 294 deletions
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
|
|
@ -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) }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 HoverAnnotation = ({ transformMatrix, width, height }) => {
|
||||
const newStyle = {
|
||||
|
@ -14,7 +14,7 @@ export const HoverAnnotation = ({ transformMatrix, width, height }) => {
|
|||
height,
|
||||
marginLeft: -width / 2,
|
||||
marginTop: -height / 2,
|
||||
transform: toCSS(transformMatrix),
|
||||
transform: matrixToCSS(transformMatrix),
|
||||
};
|
||||
return <div className="canvasHoverAnnotation canvasLayoutAnnotation" style={newStyle} />;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
@ -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 } });
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -52,100 +52,104 @@ const getRootElementId = (lookup, id) => {
|
|||
: element.id;
|
||||
};
|
||||
|
||||
const animationProps = ({ isSelected, animation }) => {
|
||||
function getClassName() {
|
||||
if (animation) {
|
||||
return animation.name;
|
||||
}
|
||||
return isSelected ? 'canvasPage--isActive' : 'canvasPage--isInactive';
|
||||
}
|
||||
|
||||
function getAnimationStyle() {
|
||||
if (!animation) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
animationDirection: animation.direction,
|
||||
// TODO: Make this configurable
|
||||
animationDuration: '1s',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: getClassName(),
|
||||
animationStyle: getAnimationStyle(),
|
||||
};
|
||||
};
|
||||
|
||||
const layoutProps = ({ updateCount, setUpdateCount, page, elements: pageElements }) => {
|
||||
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(page.id).currentScene;
|
||||
const elementLookup = new Map(pageElements.map(element => [element.id, element]));
|
||||
const recurseGroupTree = shapeId => {
|
||||
return [
|
||||
shapeId,
|
||||
...flatten(
|
||||
shapes
|
||||
.filter(s => s.parent === shapeId && s.type !== 'annotation')
|
||||
.map(s => s.id)
|
||||
.map(recurseGroupTree)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const selectedPrimaryShapeObjects = selectedPrimaryShapes
|
||||
.map(id => shapes.find(s => s.id === id))
|
||||
.filter(shape => shape);
|
||||
|
||||
const selectedPersistentPrimaryShapes = flatten(
|
||||
selectedPrimaryShapeObjects.map(shape =>
|
||||
shape.subtype === 'adHocGroup'
|
||||
? shapes.filter(s => s.parent === shape.id && s.type !== 'annotation').map(s => s.id)
|
||||
: [shape.id]
|
||||
)
|
||||
);
|
||||
const selectedElementIds = flatten(selectedPersistentPrimaryShapes.map(recurseGroupTree));
|
||||
const selectedElements = [];
|
||||
const elements = shapes.map(shape => {
|
||||
let element = null;
|
||||
if (elementLookup.has(shape.id)) {
|
||||
element = elementLookup.get(shape.id);
|
||||
if (selectedElementIds.indexOf(shape.id) > -1) {
|
||||
selectedElements.push({ ...element, id: shape.id });
|
||||
}
|
||||
}
|
||||
// instead of just combining `element` with `shape`, we make property transfer explicit
|
||||
return element ? { ...shape, filter: element.filter } : shape;
|
||||
});
|
||||
return {
|
||||
elements,
|
||||
cursor,
|
||||
selectedElementIds,
|
||||
selectedElements,
|
||||
selectedPrimaryShapes,
|
||||
commit: (...args) => {
|
||||
aeroelastic.commit(page.id, ...args);
|
||||
// TODO: remove this, it's a hack to force react to rerender
|
||||
setUpdateCount(updateCount + 1);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const groupHandlerCreators = {
|
||||
groupElements: ({ commit }) => () =>
|
||||
commit('actionEvent', {
|
||||
event: 'group',
|
||||
}),
|
||||
ungroupElements: ({ commit }) => () =>
|
||||
commit('actionEvent', {
|
||||
event: 'ungroup',
|
||||
}),
|
||||
};
|
||||
|
||||
export const WorkpadPage = compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withProps(({ isSelected, animation }) => {
|
||||
function getClassName() {
|
||||
if (animation) {
|
||||
return animation.name;
|
||||
}
|
||||
return isSelected ? 'canvasPage--isActive' : 'canvasPage--isInactive';
|
||||
}
|
||||
|
||||
function getAnimationStyle() {
|
||||
if (!animation) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
animationDirection: animation.direction,
|
||||
// TODO: Make this configurable
|
||||
animationDuration: '1s',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: getClassName(),
|
||||
animationStyle: getAnimationStyle(),
|
||||
};
|
||||
}),
|
||||
withProps(animationProps),
|
||||
withState('updateCount', 'setUpdateCount', 0), // TODO: remove this, see setUpdateCount below
|
||||
withProps(({ updateCount, setUpdateCount, page, elements: pageElements }) => {
|
||||
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(
|
||||
page.id
|
||||
).currentScene;
|
||||
const elementLookup = new Map(pageElements.map(element => [element.id, element]));
|
||||
const recurseGroupTree = shapeId => {
|
||||
return [
|
||||
shapeId,
|
||||
...flatten(
|
||||
shapes
|
||||
.filter(s => s.parent === shapeId && s.type !== 'annotation')
|
||||
.map(s => s.id)
|
||||
.map(recurseGroupTree)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const selectedPrimaryShapeObjects = selectedPrimaryShapes
|
||||
.map(id => shapes.find(s => s.id === id))
|
||||
.filter(shape => shape);
|
||||
|
||||
const selectedPersistentPrimaryShapes = flatten(
|
||||
selectedPrimaryShapeObjects.map(shape =>
|
||||
shape.subtype === 'adHocGroup'
|
||||
? shapes.filter(s => s.parent === shape.id && s.type !== 'annotation').map(s => s.id)
|
||||
: [shape.id]
|
||||
)
|
||||
);
|
||||
const selectedElementIds = flatten(selectedPersistentPrimaryShapes.map(recurseGroupTree));
|
||||
const selectedElements = [];
|
||||
const elements = shapes.map(shape => {
|
||||
let element = null;
|
||||
if (elementLookup.has(shape.id)) {
|
||||
element = elementLookup.get(shape.id);
|
||||
if (selectedElementIds.indexOf(shape.id) > -1) {
|
||||
selectedElements.push({ ...element, id: shape.id });
|
||||
}
|
||||
}
|
||||
// instead of just combining `element` with `shape`, we make property transfer explicit
|
||||
return element ? { ...shape, filter: element.filter } : shape;
|
||||
});
|
||||
return {
|
||||
elements,
|
||||
cursor,
|
||||
selectedElementIds,
|
||||
selectedElements,
|
||||
selectedPrimaryShapes,
|
||||
commit: (...args) => {
|
||||
aeroelastic.commit(page.id, ...args);
|
||||
// TODO: remove this, it's a hack to force react to rerender
|
||||
setUpdateCount(updateCount + 1);
|
||||
},
|
||||
};
|
||||
}), // Updates states; needs to have both local and global
|
||||
withHandlers({
|
||||
groupElements: ({ commit }) => () =>
|
||||
commit('actionEvent', {
|
||||
event: 'group',
|
||||
}),
|
||||
ungroupElements: ({ commit }) => () =>
|
||||
commit('actionEvent', {
|
||||
event: 'ungroup',
|
||||
}),
|
||||
}),
|
||||
withProps(layoutProps), // Updates states; needs to have both local and global
|
||||
withHandlers(groupHandlerCreators),
|
||||
withHandlers(eventHandlers) // Captures user intent, needs to have reconciled state
|
||||
)(Component);
|
||||
|
||||
|
|
17
x-pack/plugins/canvas/public/lib/aeroelastic/common.js
Normal file
17
x-pack/plugins/canvas/public/lib/aeroelastic/common.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { select } from './select';
|
||||
|
||||
// serves as reminder that we start with the state
|
||||
// todo remove it as we add TS annotations (State)
|
||||
const state = d => d;
|
||||
|
||||
const getScene = state => state.currentScene;
|
||||
export const scene = select(getScene)(state);
|
||||
|
||||
const getPrimaryUpdate = state => state.primaryUpdate;
|
||||
export const primaryUpdate = select(getPrimaryUpdate)(state);
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { select, selectReduce } from './state';
|
||||
import { scene } from './common';
|
||||
import { select } from './select';
|
||||
|
||||
// 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
|
||||
|
@ -26,6 +27,18 @@ const appleKeyboard = Boolean(
|
|||
|
||||
const primaryUpdate = state => state.primaryUpdate;
|
||||
|
||||
const gestureStatePrev = select(
|
||||
scene =>
|
||||
scene.gestureState || {
|
||||
cursor: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
mouseIsDown: false,
|
||||
mouseButtonState: { buttonState: 'up', downX: null, downY: null },
|
||||
}
|
||||
)(scene);
|
||||
|
||||
/**
|
||||
* Gestures - derived selectors for transient state
|
||||
*/
|
||||
|
@ -47,30 +60,26 @@ export const metaHeld = select(appleKeyboard ? e => e.metaKey : e => e.altKey)(k
|
|||
export const optionHeld = select(appleKeyboard ? e => e.altKey : e => e.ctrlKey)(keyFromMouse);
|
||||
export const shiftHeld = select(e => e.shiftKey)(keyFromMouse);
|
||||
|
||||
export const cursorPosition = selectReduce((previous, position) => position || previous, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
})(rawCursorPosition);
|
||||
export const cursorPosition = select(({ cursor }, position) => position || cursor)(
|
||||
gestureStatePrev,
|
||||
rawCursorPosition
|
||||
);
|
||||
|
||||
export const mouseButton = selectReduce(
|
||||
(prev, next) => {
|
||||
if (!next) {
|
||||
return prev;
|
||||
}
|
||||
const { event, uid } = next;
|
||||
if (event === 'mouseDown') {
|
||||
return { down: true, uid };
|
||||
} else {
|
||||
return event === 'mouseUp' ? { down: false, uid } : prev;
|
||||
}
|
||||
},
|
||||
{ down: false, uid: null }
|
||||
)(mouseButtonEvent);
|
||||
export const mouseButton = select(next => {
|
||||
if (!next) {
|
||||
return { down: false, uid: null };
|
||||
}
|
||||
const { event, uid } = next;
|
||||
if (event === 'mouseDown') {
|
||||
return { down: true, uid };
|
||||
} else {
|
||||
return event === 'mouseUp' ? { down: false, uid } : { down: false, uid: null };
|
||||
}
|
||||
})(mouseButtonEvent);
|
||||
|
||||
export const mouseIsDown = selectReduce(
|
||||
(previous, next) => (next ? next.event === 'mouseDown' : previous),
|
||||
false
|
||||
)(mouseButtonEvent);
|
||||
export const mouseIsDown = select(({ mouseIsDown }, next) =>
|
||||
next ? next.event === 'mouseDown' : mouseIsDown
|
||||
)(gestureStatePrev, mouseButtonEvent);
|
||||
|
||||
export const gestureEnd = select(
|
||||
action =>
|
||||
|
@ -115,8 +124,8 @@ const mouseButtonStateTransitions = (state, mouseNowDown, movedAlready) => {
|
|||
}
|
||||
};
|
||||
|
||||
const mouseButtonState = selectReduce(
|
||||
({ buttonState, downX, downY }, mouseNowDown, { x, y }) => {
|
||||
const mouseButtonState = select(
|
||||
({ mouseButtonState: { buttonState, downX, downY } }, mouseNowDown, { x, y }) => {
|
||||
const movedAlready = x !== downX || y !== downY;
|
||||
const newButtonState = mouseButtonStateTransitions(buttonState, mouseNowDown, movedAlready);
|
||||
return {
|
||||
|
@ -124,9 +133,8 @@ const mouseButtonState = selectReduce(
|
|||
downX: newButtonState === 'downed' ? x : downX,
|
||||
downY: newButtonState === 'downed' ? y : downY,
|
||||
};
|
||||
},
|
||||
{ buttonState: 'up', downX: null, downY: null }
|
||||
)(mouseIsDown, cursorPosition);
|
||||
}
|
||||
)(gestureStatePrev, mouseIsDown, cursorPosition);
|
||||
|
||||
export const mouseDowned = select(state => state.buttonState === 'downed')(mouseButtonState);
|
||||
|
||||
|
@ -143,3 +151,9 @@ export const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
|
|||
export const actionEvent = select(action =>
|
||||
action.type === 'actionEvent' ? action.payload : null
|
||||
)(primaryUpdate);
|
||||
|
||||
export const gestureState = select((cursor, mouseIsDown, mouseButtonState) => ({
|
||||
cursor,
|
||||
mouseIsDown,
|
||||
mouseButtonState,
|
||||
}))(cursorPosition, mouseIsDown, mouseButtonState);
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { matrixToCSS } from './dom';
|
||||
import { nextScene } from './layout';
|
||||
import { primaryUpdate } from './layout_functions';
|
||||
import { updater } from './layout';
|
||||
import { multiply, rotateZ, translate } from './matrix';
|
||||
import { createStore, select } from './state';
|
||||
import { createStore } from './store';
|
||||
|
||||
export const layout = { nextScene, primaryUpdate };
|
||||
export const matrix = { multiply, rotateZ, translate };
|
||||
export const state = { createStore, select };
|
||||
export const toCSS = matrixToCSS;
|
||||
|
||||
export const createLayoutStore = (initialState, onChangeCallback) =>
|
||||
createStore(initialState, updater, onChangeCallback);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { select } from './state';
|
||||
import { select } from './select';
|
||||
|
||||
import {
|
||||
actionEvent,
|
||||
|
@ -12,6 +12,7 @@ import {
|
|||
dragging,
|
||||
dragVector,
|
||||
gestureEnd,
|
||||
gestureState,
|
||||
metaHeld,
|
||||
mouseButton,
|
||||
mouseDowned,
|
||||
|
@ -23,12 +24,12 @@ import {
|
|||
import {
|
||||
applyLocalTransforms,
|
||||
cascadeProperties,
|
||||
configuration,
|
||||
draggingShape,
|
||||
getAdHocChildrenAnnotations,
|
||||
getAlignmentGuideAnnotations,
|
||||
getAlterSnapGesture,
|
||||
getAnnotatedShapes,
|
||||
getConfiguration,
|
||||
getConstrainedShapesWithPreexistingAnnotations,
|
||||
getCursor,
|
||||
getDirectSelect,
|
||||
|
@ -39,6 +40,7 @@ import {
|
|||
getGroupedSelectedShapeIds,
|
||||
getGroupedSelectedShapes,
|
||||
getGrouping,
|
||||
getGroupingTuple,
|
||||
getHoverAnnotations,
|
||||
getHoveredShape,
|
||||
getHoveredShapes,
|
||||
|
@ -51,7 +53,6 @@ import {
|
|||
getRestateShapesEvent,
|
||||
getRotationAnnotations,
|
||||
getRotationTooltipAnnotation,
|
||||
getScene,
|
||||
getSelectedPrimaryShapeIds,
|
||||
getSelectedShapeObjects,
|
||||
getSelectedShapes,
|
||||
|
@ -60,21 +61,21 @@ import {
|
|||
getShapes,
|
||||
getSnappedShapes,
|
||||
getTransformIntents,
|
||||
primaryUpdate,
|
||||
resizeAnnotationsFunction,
|
||||
updaterFun,
|
||||
} from './layout_functions';
|
||||
|
||||
/**
|
||||
* Scenegraph update based on events, gestures...
|
||||
*/
|
||||
import { primaryUpdate, scene } from './common';
|
||||
|
||||
export const shapes = select(getShapes)(getScene);
|
||||
export const shapes = select(getShapes)(scene);
|
||||
|
||||
const configuration = select(getConfiguration)(scene);
|
||||
|
||||
const hoveredShapes = select(getHoveredShapes)(configuration, shapes, cursorPosition);
|
||||
|
||||
const hoveredShape = select(getHoveredShape)(hoveredShapes);
|
||||
|
||||
const draggedShape = select(draggingShape)(getScene, hoveredShape, mouseIsDown, mouseDowned);
|
||||
const draggedShape = select(draggingShape)(scene, hoveredShape, mouseIsDown, mouseDowned);
|
||||
|
||||
export const focusedShape = select(getFocusedShape)(draggedShape, hoveredShape);
|
||||
|
||||
|
@ -82,7 +83,7 @@ const alterSnapGesture = select(getAlterSnapGesture)(metaHeld);
|
|||
|
||||
const multiselectModifier = shiftHeld; // todo abstract out keybindings
|
||||
|
||||
const mouseTransformGesturePrev = select(getMouseTransformGesturePrev)(getScene);
|
||||
const mouseTransformGesturePrev = select(getMouseTransformGesturePrev)(scene);
|
||||
|
||||
const mouseTransformState = select(getMouseTransformState)(
|
||||
mouseTransformGesturePrev,
|
||||
|
@ -99,9 +100,9 @@ const restateShapesEvent = select(getRestateShapesEvent)(primaryUpdate);
|
|||
// 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 selectedShapeObjects = select(getSelectedShapeObjects)(scene);
|
||||
|
||||
const selectedShapesPrev = select(getSelectedShapesPrev)(getScene);
|
||||
const selectedShapesPrev = select(getSelectedShapesPrev)(scene);
|
||||
|
||||
const selectionState = select(getSelectionState)(
|
||||
selectedShapesPrev,
|
||||
|
@ -150,7 +151,7 @@ const alignmentGuideAnnotations = select(getAlignmentGuideAnnotations)(
|
|||
|
||||
const hoverAnnotations = select(getHoverAnnotations)(
|
||||
configuration,
|
||||
hoveredShape,
|
||||
select(h => h.slice(0, 1))(hoveredShapes), // todo remove this slicing when box select arrives
|
||||
selectedPrimaryShapeIds,
|
||||
draggedShape
|
||||
);
|
||||
|
@ -183,11 +184,18 @@ const rotationTooltipAnnotation = select(getRotationTooltipAnnotation)(
|
|||
|
||||
const groupAction = select(getGroupAction)(actionEvent);
|
||||
|
||||
const groupingTuple = select(getGroupingTuple)(
|
||||
configuration,
|
||||
constrainedShapesWithPreexistingAnnotations,
|
||||
selectedShapes
|
||||
);
|
||||
|
||||
const grouping = select(getGrouping)(
|
||||
configuration,
|
||||
constrainedShapesWithPreexistingAnnotations,
|
||||
selectedShapes,
|
||||
groupAction
|
||||
groupAction,
|
||||
groupingTuple
|
||||
);
|
||||
|
||||
const groupedSelectedShapes = select(getGroupedSelectedShapes)(grouping);
|
||||
|
@ -231,5 +239,8 @@ export const nextScene = select(getNextScene)(
|
|||
cursor,
|
||||
selectionState,
|
||||
mouseTransformState,
|
||||
groupedSelectedShapes
|
||||
groupedSelectedShapes,
|
||||
gestureState
|
||||
);
|
||||
|
||||
export const updater = select(updaterFun)(nextScene, primaryUpdate);
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getId } from './../../lib/get_id';
|
||||
import { landmarkPoint, shapesAt } from './geometry';
|
||||
|
||||
import {
|
||||
|
@ -41,6 +40,17 @@ import {
|
|||
shallowEqual,
|
||||
} from './functional';
|
||||
|
||||
import { getId as rawGetId } from './../../lib/get_id';
|
||||
|
||||
const idMap = {};
|
||||
const getId = (name, extension) => {
|
||||
// ensures that `axisAlignedBoundingBoxShape` is pure-ish - a new call with the same input will not yield a new id
|
||||
// (while it's possible for the same group to have the same members - ungroup then make the same group again -
|
||||
// it's okay if the newly arising group gets the same id)
|
||||
const key = name + '|' + extension;
|
||||
return idMap[key] || (idMap[key] = rawGetId(name));
|
||||
};
|
||||
|
||||
const resizeVertexTuples = [
|
||||
[-1, -1, 315],
|
||||
[1, -1, 45],
|
||||
|
@ -926,7 +936,7 @@ const idsMatch = selectedShapes => shape => selectedShapes.find(idMatch(shape));
|
|||
const axisAlignedBoundingBoxShape = (config, shapesToBox) => {
|
||||
const axisAlignedBoundingBox = getAABB(shapesToBox);
|
||||
const { a, b, localTransformMatrix, rigTransform } = projectAABB(axisAlignedBoundingBox);
|
||||
const id = getId(config.groupName);
|
||||
const id = getId(config.groupName, shapesToBox.map(s => s.id).join('|'));
|
||||
const aabbShape = {
|
||||
id,
|
||||
type: config.groupName,
|
||||
|
@ -1014,11 +1024,7 @@ const getLeafs = (descendCondition, allShapes, shapes) =>
|
|||
|
||||
const preserveCurrentGroups = (shapes, selectedShapes) => ({ shapes, selectedShapes });
|
||||
|
||||
export const getScene = state => state.currentScene;
|
||||
|
||||
export const configuration = state => {
|
||||
return state.configuration;
|
||||
};
|
||||
export const getConfiguration = scene => scene.configuration;
|
||||
|
||||
export const getShapes = scene => scene.shapes;
|
||||
|
||||
|
@ -1063,7 +1069,7 @@ const multiSelect = (prev, config, hoveredShapes, metaHeld, uid, selectedShapeOb
|
|||
};
|
||||
};
|
||||
|
||||
export const getGrouping = (config, shapes, selectedShapes, groupAction) => {
|
||||
export const getGroupingTuple = (config, shapes, selectedShapes) => {
|
||||
const childOfGroup = shape => shape.parent && shape.parent.startsWith(config.groupName);
|
||||
const isAdHocGroup = shape =>
|
||||
shape.type === config.groupName && shape.subtype === config.adHocGroupName;
|
||||
|
@ -1076,7 +1082,21 @@ export const getGrouping = (config, shapes, selectedShapes, groupAction) => {
|
|||
const isOrBelongsToGroup = shape => isGroup(shape) || childOfGroup(shape);
|
||||
const someSelectedShapesAreGrouped = selectedShapes.some(isOrBelongsToGroup);
|
||||
const selectionOutsideGroup = !someSelectedShapesAreGrouped;
|
||||
return {
|
||||
selectionOutsideGroup,
|
||||
freshSelectedShapes,
|
||||
freshNonSelectedShapes,
|
||||
preexistingAdHocGroups,
|
||||
};
|
||||
};
|
||||
|
||||
export const getGrouping = (config, shapes, selectedShapes, groupAction, tuple) => {
|
||||
const {
|
||||
selectionOutsideGroup,
|
||||
freshSelectedShapes,
|
||||
freshNonSelectedShapes,
|
||||
preexistingAdHocGroups,
|
||||
} = tuple;
|
||||
if (groupAction === 'group') {
|
||||
const selectedAdHocGroupsToPersist = selectedShapes.filter(
|
||||
s => s.subtype === config.adHocGroupName
|
||||
|
@ -1180,11 +1200,6 @@ export const getCursor = (config, shape, draggedPrimaryShape) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Selectors directly from a state object
|
||||
*/
|
||||
export const primaryUpdate = state => state.primaryUpdate;
|
||||
|
||||
export const getSelectedShapesPrev = scene =>
|
||||
scene.selectionState || {
|
||||
shapes: [],
|
||||
|
@ -1306,14 +1321,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,
|
||||
|
@ -1410,7 +1427,8 @@ export const getNextScene = (
|
|||
cursor,
|
||||
selectionState,
|
||||
mouseTransformState,
|
||||
selectedShapes
|
||||
selectedShapes,
|
||||
gestureState
|
||||
) => {
|
||||
const selectedLeafShapes = getLeafs(
|
||||
shape => shape.type === config.groupName,
|
||||
|
@ -1432,7 +1450,13 @@ export const getNextScene = (
|
|||
draggedShape,
|
||||
cursor,
|
||||
selectionState,
|
||||
gestureState,
|
||||
mouseTransformState,
|
||||
selectedShapeObjects: selectedShapes,
|
||||
};
|
||||
};
|
||||
|
||||
export const updaterFun = (nextScene, primaryUpdate) => ({
|
||||
primaryUpdate,
|
||||
currentScene: nextScene,
|
||||
});
|
||||
|
|
16
x-pack/plugins/canvas/public/lib/aeroelastic/select.ts
Normal file
16
x-pack/plugins/canvas/public/lib/aeroelastic/select.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { ActionId, NodeFunction, NodeResult } from './types';
|
||||
|
||||
export const select = (fun: NodeFunction): NodeFunction => (...fns) => {
|
||||
let prevId: ActionId = NaN;
|
||||
let cache: NodeResult = null;
|
||||
const old = (object: NodeResult): boolean =>
|
||||
prevId === (prevId = object.primaryUpdate.payload.uid);
|
||||
return (obj: NodeResult) =>
|
||||
old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as NodeResult)));
|
||||
};
|
|
@ -1,105 +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.
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionId,
|
||||
ChangeCallbackFunction,
|
||||
Meta,
|
||||
NodeFunction,
|
||||
NodeResult,
|
||||
Payload,
|
||||
TypeName,
|
||||
UpdaterFunction,
|
||||
} from './types';
|
||||
|
||||
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 = (): ActionId => 1e11 + Math.floor((1e12 - 1e11) * Math.random());
|
||||
|
||||
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 = [] as NodeResult[];
|
||||
let value = previousValue;
|
||||
let prevValue = previousValue;
|
||||
return (state: NodeResult) => {
|
||||
if (
|
||||
shallowEqual(argumentValues, (argumentValues = inputs.map(input => input(state)))) &&
|
||||
value === prevValue
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
prevValue = value;
|
||||
value = fun(prevValue, ...argumentValues);
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
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 = [] 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))))
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
value = fun(...argumentValues);
|
||||
actionId = lastActionId;
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
export const createStore = (initialState: NodeResult, onChangeCallback: ChangeCallbackFunction) => {
|
||||
let currentState = initialState;
|
||||
let updater: UpdaterFunction = (state: NodeResult): NodeResult => state; // default: no side effect
|
||||
const getCurrentState = () => currentState;
|
||||
// const setCurrentState = newState => (currentState = newState);
|
||||
const setUpdater = (updaterFunction: UpdaterFunction) => {
|
||||
updater = updaterFunction;
|
||||
};
|
||||
|
||||
const commit = (type: TypeName, payload: Payload, meta: Meta = { silent: false }) => {
|
||||
currentState = updater({
|
||||
...currentState,
|
||||
primaryUpdate: {
|
||||
type,
|
||||
payload: { ...payload, uid: makeUid() },
|
||||
},
|
||||
});
|
||||
if (!meta.silent) {
|
||||
onChangeCallback({ type, state: currentState }, meta);
|
||||
}
|
||||
};
|
||||
|
||||
const dispatch = (type: TypeName, payload: Payload) => commit(type, payload);
|
||||
|
||||
return { getCurrentState, setUpdater, commit, dispatch };
|
||||
};
|
42
x-pack/plugins/canvas/public/lib/aeroelastic/store.ts
Normal file
42
x-pack/plugins/canvas/public/lib/aeroelastic/store.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 {
|
||||
ActionId,
|
||||
ChangeCallbackFunction,
|
||||
Meta,
|
||||
NodeResult,
|
||||
Payload,
|
||||
TypeName,
|
||||
UpdaterFunction,
|
||||
} from './types';
|
||||
|
||||
let counter = 0 as ActionId;
|
||||
|
||||
export const createStore = (
|
||||
initialState: NodeResult,
|
||||
updater: UpdaterFunction,
|
||||
onChangeCallback: ChangeCallbackFunction
|
||||
) => {
|
||||
let currentState = initialState;
|
||||
|
||||
const getCurrentState = () => currentState;
|
||||
|
||||
const commit = (type: TypeName, payload: Payload, meta: Meta = { silent: false }) => {
|
||||
currentState = updater({
|
||||
...currentState,
|
||||
primaryUpdate: {
|
||||
type,
|
||||
payload: { ...payload, uid: counter++ },
|
||||
},
|
||||
});
|
||||
if (!meta.silent) {
|
||||
onChangeCallback({ type, state: currentState }, meta);
|
||||
}
|
||||
};
|
||||
|
||||
return { getCurrentState, commit };
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { layout, matrix, state } from './aeroelastic';
|
||||
import { createLayoutStore, matrix } from './aeroelastic';
|
||||
|
||||
const stores = new Map();
|
||||
|
||||
|
@ -16,16 +16,7 @@ export const aeroelastic = {
|
|||
},
|
||||
|
||||
createStore(initialState, onChangeCallback = () => {}, page) {
|
||||
stores.set(page, state.createStore(initialState, onChangeCallback));
|
||||
|
||||
const updateScene = state.select((nextScene, primaryUpdate) => ({
|
||||
shapeAdditions: nextScene.shapes,
|
||||
primaryUpdate,
|
||||
currentScene: nextScene,
|
||||
configuration: nextScene.configuration,
|
||||
}))(layout.nextScene, layout.primaryUpdate);
|
||||
|
||||
stores.get(page).setUpdater(updateScene);
|
||||
stores.set(page, createLayoutStore(initialState, onChangeCallback));
|
||||
},
|
||||
|
||||
removeStore(page) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { transformMatrix3d } from './types';
|
||||
import { transformMatrix3d } from './aeroelastic/types';
|
||||
|
||||
// converts a transform matrix to a CSS string
|
||||
export const matrixToCSS = (transformMatrix: transformMatrix3d): string =>
|
|
@ -261,10 +261,8 @@ export const aeroelastic = ({ dispatch, getState }) => {
|
|||
const createStore = page =>
|
||||
aero.createStore(
|
||||
{
|
||||
shapeAdditions: [],
|
||||
primaryUpdate: null,
|
||||
currentScene: { shapes: [] },
|
||||
configuration: aeroelasticConfiguration,
|
||||
currentScene: { shapes: [], configuration: aeroelasticConfiguration },
|
||||
},
|
||||
onChangeCallback,
|
||||
page
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue