[Canvas] Simplified aero state (#32980) (#33344)

* 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:
Robert Monfera 2019-03-15 20:44:27 +01:00 committed by GitHub
parent 516afba2ca
commit a2f35c34d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 304 additions and 294 deletions

View file

@ -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

View file

@ -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} />;
};

View file

@ -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) }}
/>
);

View file

@ -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} />;
};

View file

@ -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 } });

View file

@ -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>

View file

@ -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}>

View file

@ -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);

View 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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,
});

View 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)));
};

View file

@ -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 };
};

View 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 };
};

View file

@ -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) {

View file

@ -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 =>

View file

@ -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