|
@@ -58,7 +58,6 @@ function makeOverlayHelpIcon(background, helpIcon) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
const versionRegistry = new VersionRegistry();
|
|
|
const graphState = new GraphState();
|
|
@@ -67,31 +66,73 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
let x = 0;
|
|
|
let y = 0;
|
|
|
|
|
|
- function getCallbacks({state, setState}) {
|
|
|
+ const initialState: VersionedModelState = {
|
|
|
+ version: versionRegistry.initialVersion,
|
|
|
+ graph: emptyGraph,
|
|
|
+ historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
|
|
|
+ dependencyGraphL1: emptyGraph,
|
|
|
+ dependencyGraphL0: emptyGraph,
|
|
|
+ }
|
|
|
+
|
|
|
+ // This function may only be called from a functional React component.
|
|
|
+ // It creates the state, and a number of state update functions.
|
|
|
+ function getReducer([state, setState]) {
|
|
|
const setGraph = callback =>
|
|
|
setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
|
|
|
|
|
|
- const addVersionAndDeltas = (newVersion: Version, composite: CompositeDelta) => {
|
|
|
- setState(({version, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => ({
|
|
|
- version: newVersion,
|
|
|
- // add new version to history graph + highlight the new version as the current version:
|
|
|
- historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), version, newVersion),
|
|
|
- // add the composite delta to the L1-graph + highlight it as 'active':
|
|
|
- dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
|
|
|
- // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
|
|
|
- dependencyGraphL0: composite.deltas.reduce(
|
|
|
- (graph, delta) => {
|
|
|
- return addDeltaAndActivate(graph, delta);
|
|
|
- }, dependencyGraphL0),
|
|
|
- ...rest,
|
|
|
- }));
|
|
|
+ const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
|
|
|
+ let composite;
|
|
|
+ try {
|
|
|
+ composite = compositeLevel.createComposite(deltas, description);
|
|
|
+ } catch(e) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const parentVersion = versionRegistry.lookupOptional(parentHash);
|
|
|
+ if (parentVersion !== undefined) {
|
|
|
+ const newVersion = versionRegistry.createVersion(parentVersion, composite);
|
|
|
+
|
|
|
+ setState(({historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
|
|
|
+ return {
|
|
|
+ // add new version to history graph + highlight the new version as the current version:
|
|
|
+ historyGraph: appendToHistoryGraph(historyGraph, newVersion),
|
|
|
+ // add the composite delta to the L1-graph + highlight it as 'active':
|
|
|
+ dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite, false) : dependencyGraphL1, // never add an empty composite
|
|
|
+ // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
|
|
|
+ dependencyGraphL0: composite.deltas.reduce(
|
|
|
+ (graph, delta) => {
|
|
|
+ return addDeltaAndActivate(graph, delta, false);
|
|
|
+ }, dependencyGraphL0),
|
|
|
+ ...rest,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
- const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, embeddings = new Map()) => {
|
|
|
+ const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, newVersionCallback?: (Version)=>void) => {
|
|
|
const composite = compositeLevel.createComposite(deltas, description);
|
|
|
- const version = versionRegistry.createVersion(state.version, composite, embeddings);
|
|
|
- addVersionAndDeltas(version, composite);
|
|
|
- gotoVersion(version);
|
|
|
- return version;
|
|
|
+
|
|
|
+ // update graph state:
|
|
|
+ const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
|
|
|
+ graphState.exec(composite, d3Updater);
|
|
|
+
|
|
|
+ // update rest of state:
|
|
|
+ setState(({version: curVersion, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
|
|
|
+ const newVersion = versionRegistry.createVersion(curVersion, composite);
|
|
|
+ newVersionCallback?.(newVersion);
|
|
|
+ return {
|
|
|
+ version: newVersion,
|
|
|
+ // add new version to history graph + highlight the new version as the current version:
|
|
|
+ historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), curVersion, newVersion),
|
|
|
+ // add the composite delta to the L1-graph + highlight it as 'active':
|
|
|
+ dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
|
|
|
+ // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
|
|
|
+ dependencyGraphL0: composite.deltas.reduce(
|
|
|
+ (graph, delta) => {
|
|
|
+ return addDeltaAndActivate(graph, delta);
|
|
|
+ }, dependencyGraphL0),
|
|
|
+ ...rest,
|
|
|
+ };
|
|
|
+ });
|
|
|
};
|
|
|
const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
|
|
|
const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
|
|
@@ -146,99 +187,124 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
...rest,
|
|
|
}));
|
|
|
};
|
|
|
- return {
|
|
|
- gotoVersion,
|
|
|
- createAndGotoNewVersion,
|
|
|
- addVersionAndDeltas,
|
|
|
- undo,
|
|
|
- redo,
|
|
|
- };
|
|
|
- }
|
|
|
|
|
|
- // this function may only be called from a functional react component!
|
|
|
- function getReactComponents({state, callbacks}: {state: VersionedModelState, callbacks: VersionedModelCallbacks}) {
|
|
|
- const graphStateComponent = makeOverlayHelpIcon(readonly ?
|
|
|
- <Graph graph={state.graph} forces={graphForces} />
|
|
|
- : <EditableGraph
|
|
|
+ const getReactComponents = (callbacks: VersionedModelCallbacks) => {
|
|
|
+ const graphStateComponent = makeOverlayHelpIcon(readonly ?
|
|
|
+ <Graph graph={state.graph} forces={graphForces} />
|
|
|
+ : <EditableGraph
|
|
|
+ graph={state.graph}
|
|
|
+ graphState={graphState}
|
|
|
+ forces={graphForces}
|
|
|
+ generateUUID={generateUUID}
|
|
|
+ primitiveRegistry={primitiveRegistry}
|
|
|
+ setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
|
|
|
+ onUserEdit={callbacks.onUserEdit}
|
|
|
+ />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
|
|
|
+
|
|
|
+ const depGraphL1Component = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
|
|
|
+ HelpIcons.depGraph);
|
|
|
+ const depGraphL0Component = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
|
|
|
+ HelpIcons.depGraph);
|
|
|
+
|
|
|
+ const historyComponent = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.historyGraph} forces={graphForces}
|
|
|
+ mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
|
|
|
+ HelpIcons.historyGraph);
|
|
|
+
|
|
|
+ const rountangleEditor = makeOverlayHelpIcon(
|
|
|
+ <RountangleEditor
|
|
|
graph={state.graph}
|
|
|
- graphState={graphState}
|
|
|
- forces={graphForces}
|
|
|
generateUUID={generateUUID}
|
|
|
primitiveRegistry={primitiveRegistry}
|
|
|
- setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
|
|
|
+ graphState={graphState}
|
|
|
onUserEdit={callbacks.onUserEdit}
|
|
|
- />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
|
|
|
-
|
|
|
- const depGraphL1Component = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
|
|
|
- HelpIcons.depGraph);
|
|
|
- const depGraphL0Component = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
|
|
|
- HelpIcons.depGraph);
|
|
|
-
|
|
|
- const historyComponent = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.historyGraph} forces={graphForces}
|
|
|
- mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
|
|
|
- HelpIcons.historyGraph);
|
|
|
-
|
|
|
- const rountangleEditor = makeOverlayHelpIcon(
|
|
|
- <RountangleEditor
|
|
|
- graph={state.graph}
|
|
|
- generateUUID={generateUUID}
|
|
|
- primitiveRegistry={primitiveRegistry}
|
|
|
- graphState={graphState}
|
|
|
- onUserEdit={callbacks.onUserEdit}
|
|
|
- />,
|
|
|
- HelpIcons.rountangleEditor);
|
|
|
-
|
|
|
- const undoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
|
|
|
- return (
|
|
|
- <div key={fullVersionId(parentVersion)}>
|
|
|
- <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
|
|
|
- UNDO {deltaToUndo.getDescription()}
|
|
|
- </Mantine.Button>
|
|
|
- <Mantine.Space h="xs"/>
|
|
|
- </div>
|
|
|
- );
|
|
|
- });
|
|
|
- const redoButtons = state.version.children.map(([childVersion,deltaToRedo]) => {
|
|
|
- return (
|
|
|
- <div key={fullVersionId(childVersion)}>
|
|
|
- <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconPlayerTrackNext size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
|
|
|
- REDO {deltaToRedo.getDescription()}
|
|
|
- </Mantine.Button>
|
|
|
- <Mantine.Space h="xs"/>
|
|
|
- </div>
|
|
|
+ />,
|
|
|
+ HelpIcons.rountangleEditor);
|
|
|
+
|
|
|
+ const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
|
|
|
+ if (parentsOrChildren.length === 0) {
|
|
|
+ return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
|
|
|
+ }
|
|
|
+ if (parentsOrChildren.length === 1) {
|
|
|
+ return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
|
|
|
+ }
|
|
|
+ return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionDuration={0}>
|
|
|
+ <Mantine.Menu.Target>
|
|
|
+ <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
|
|
|
+ </Mantine.Menu.Target>
|
|
|
+ <Mantine.Menu.Dropdown>
|
|
|
+ {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
|
|
|
+ {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
|
|
|
+ <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
|
|
|
+ </Mantine.Menu.Dropdown>
|
|
|
+ </Mantine.Menu>;
|
|
|
+
|
|
|
+ }
|
|
|
+ const undoButton = makeUndoOrRedoButton(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
|
|
|
+ const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
|
|
|
+
|
|
|
+ const undoRedoButtons = <>
|
|
|
+ {undoButton}
|
|
|
+ {redoButton}
|
|
|
+ </>;
|
|
|
+
|
|
|
+ const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
|
|
|
+ return (
|
|
|
+ <div key={fullVersionId(parentVersion)}>
|
|
|
+ <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
|
|
|
+ UNDO {deltaToUndo.getDescription()}
|
|
|
+ </Mantine.Button>
|
|
|
+ <Mantine.Space h="xs"/>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ });
|
|
|
+ const stackedRedoButtons = state.version.children.map(([childVersion,deltaToRedo]) => {
|
|
|
+ return (
|
|
|
+ <div key={fullVersionId(childVersion)}>
|
|
|
+ <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconPlayerTrackNext size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
|
|
|
+ REDO {deltaToRedo.getDescription()}
|
|
|
+ </Mantine.Button>
|
|
|
+ <Mantine.Space h="xs"/>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ });
|
|
|
+ const stackedUndoRedoButtons = (
|
|
|
+ <Mantine.SimpleGrid cols={2}>
|
|
|
+ <div>{stackedUndoButtons}</div>
|
|
|
+ <div>{stackedRedoButtons}</div>
|
|
|
+ </Mantine.SimpleGrid>
|
|
|
);
|
|
|
- });
|
|
|
- const undoRedoButtons = (
|
|
|
- <Mantine.SimpleGrid cols={2}>
|
|
|
- <div>{undoButtons}</div>
|
|
|
- <div>{redoButtons}</div>
|
|
|
- </Mantine.SimpleGrid>
|
|
|
- );
|
|
|
+
|
|
|
+ return {
|
|
|
+ graphStateComponent,
|
|
|
+ rountangleEditor,
|
|
|
+ depGraphL1Component,
|
|
|
+ depGraphL0Component,
|
|
|
+ historyComponent,
|
|
|
+ undoButton,
|
|
|
+ redoButton,
|
|
|
+ undoRedoButtons,
|
|
|
+ stackedUndoRedoButtons,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
return {
|
|
|
- graphStateComponent,
|
|
|
- rountangleEditor,
|
|
|
- depGraphL1Component,
|
|
|
- depGraphL0Component,
|
|
|
- historyComponent,
|
|
|
- // undoButtons,
|
|
|
- // redoButtons,
|
|
|
- undoRedoButtons,
|
|
|
+ state,
|
|
|
+ getReactComponents,
|
|
|
+ callbacks: {
|
|
|
+ addDeltasAndVersion,
|
|
|
+ gotoVersion,
|
|
|
+ createAndGotoNewVersion,
|
|
|
+ undo,
|
|
|
+ redo,
|
|
|
+ },
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- initialState: {
|
|
|
- version: versionRegistry.initialVersion,
|
|
|
- graph: emptyGraph,
|
|
|
- historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
|
|
|
- dependencyGraphL1: emptyGraph,
|
|
|
- dependencyGraphL0: emptyGraph,
|
|
|
- },
|
|
|
- getCallbacks,
|
|
|
- getReactComponents,
|
|
|
+ initialState,
|
|
|
+ getReducer,
|
|
|
};
|
|
|
}
|