basic actions for craft history manipulation

This commit is contained in:
Val Erastov 2018-02-16 00:50:38 -08:00
parent c49c21fd17
commit f9c202ba13
9 changed files with 225 additions and 111 deletions

View file

@ -0,0 +1,40 @@
export function addModification({history, pointer}, request) {
let changingHistory = pointer !== history.length - 1;
if (changingHistory) {
history.slice(0, pointer);
history.push(request);
return {
history,
pointer: ++pointer
}
} else {
return {
history: [...history, request],
pointer: ++pointer
}
}
}
export function stepOverridingParams({history, pointer}, params) {
history[pointer + 1] = {
type: history[pointer + 1].type,
params
};
return {
history,
pointer: ++pointer
};
}
export function finishHistoryEditing({history}) {
return ({history, pointer: history.length - 1});
}
export function removeAndDropDependants({history}, indexToRemove) {
history = history.slice(0, indexToRemove);
return {
history,
pointer: history.length - 1
}
}

View file

@ -1,4 +1,5 @@
import {createToken} from "bus";
import {addModification} from './craftHistoryUtils';
export function activate({bus, services}) {
@ -7,45 +8,35 @@ export function activate({bus, services}) {
pointer: -1
});
function getHistory() {
return bus.state[TOKENS.MODIFICATIONS].history;
function isAdditiveChange({history, pointer}, {history:oldHistory, pointer:oldPointer}) {
if (pointer < oldPointer) {
return false;
}
for (let i = 0; i <= oldPointer; i++) {
let modCurr = history[i];
let modPrev = oldHistory[i];
if (modCurr !== modPrev) {
return false;
}
}
return true;
}
bus.subscribe(TOKENS.HISTORY_POINTER, (pointer) => {
let history = getHistory();
if (pointer < history.length) {
resetInternal(history.slice(0, pointer));
bus.setState(TOKENS.MODIFICATIONS, {pointer});
bus.subscribe(TOKENS.MODIFICATIONS, (curr, prev) => {
let beginIndex;
if (isAdditiveChange(curr, prev)) {
beginIndex = prev.pointer + 1;
} else {
services.cadRegistry.reset();
beginIndex = 0;
}
let {history, pointer} = curr;
for (let i = beginIndex; i <= pointer; i++) {
modifyInternal(history[i]);
}
});
function remove(modificationIndex) {
bus.updateState(TOKENS.MODIFICATIONS,
({history, pointer}) => {
return {
history: history.slice(0, modificationIndex),
pointer: Math.min(pointer, modificationIndex - 1)
}
});
}
function resetInternal(modifications) {
services.cadRegistry.reset();
for (let request of modifications) {
modifyInternal(request);
}
}
function reset(modifications) {
resetInternal(modifications);
bus.updateState(TOKENS.MODIFICATIONS,
() => {
return {
history: modifications,
pointer: modifications.length - 1
}
});
}
function modifyInternal(request) {
let op = services.operation.registry[request.type];
@ -57,23 +48,21 @@ export function activate({bus, services}) {
}
function modify(request) {
modifyInternal(request);
bus.updateState(TOKENS.MODIFICATIONS,
({history, pointer}) => {
return {
history: [...history, request],
pointer: pointer++
}
});
bus.updateState(TOKENS.MODIFICATIONS, modifications => addModification(modifications, request));
}
function reset(modifications) {
bus.dispatch(TOKENS.MODIFICATIONS, {
history: modifications,
pointer: modifications.length - 1
});
}
services.craft = {
modify, remove, reset, TOKENS
modify, reset, TOKENS
}
}
export const TOKENS = {
MODIFICATIONS: createToken('craft', 'modifications'),
HISTORY_POINTER: createToken('craft', 'historyPointer'),
MODIFICATIONS: createToken('craft', 'modifications')
};

View file

@ -15,7 +15,7 @@ export function activate({bus, services}) {
});
bus.subscribe(TOKENS.CLOSE, wizard => {
bus.updateState(TOKENS.WIZARDS, opened => opened.filter(w => w === wizard));
bus.updateState(TOKENS.WIZARDS, opened => opened.filter(w => w !== wizard));
});
}

View file

@ -4,27 +4,35 @@ import Stack from 'ui/components/Stack';
import connect from 'ui/connect';
import Fa from 'ui/components/Fa';
import ImgIcon from 'ui/components/ImgIcon';
import ls from './OperationHistory.less'
import ls from './OperationHistory.less';
import cx from 'classnames';
import {TOKENS as CRAFT_TOKENS} from '../../craft/craftPlugin';
import ButtonGroup from '../../../../../modules/ui/components/controls/ButtonGroup';
import Button from '../../../../../modules/ui/components/controls/Button';
import {removeAndDropDependants} from '../../craft/craftHistoryUtils';
function OperationHistory({history, pointer}, {services: {operation: operationService}}) {
function OperationHistory({history, pointer, setHistoryPointer, remove}, {services: {operation: operationService}}) {
let lastMod = history.length - 1;
return <Stack>
{history.map(({type, params}, index) => {
let {appearance, paramsInfo} = getDescriptor(type, operationService.registry);
return <div key={index} className={ls.item}>
return <div key={index} onClick={() => setHistoryPointer(index - 1)}
className={cx(ls.item, pointer + 1 === index && ls.selected)}>
{appearance && <ImgIcon url={appearance.icon32} size={16}/>}
<span>{type} {paramsInfo && paramsInfo(params)} </span>
<span className={ls.buttons}>
<Fa icon='edit' />
<Fa icon='image' />
<Fa icon='remove' />
<Fa icon='remove' className={ls.danger} onClick={() => remove(index)}/>
</span>
</div>;
})}
{pointer !== lastMod && <ButtonGroup>
<Button onClick={() => setHistoryPointer(lastMod)}>Finish History Editing</Button>
</ButtonGroup>}
</Stack>;
}
@ -41,4 +49,9 @@ OperationHistory.contextTypes = {
services: PropTypes.object
};
export default connect(OperationHistory, CRAFT_TOKENS.MODIFICATIONS);
export default connect(OperationHistory, CRAFT_TOKENS.MODIFICATIONS, {
mapActions: ({setState, updateState}) => ({
setHistoryPointer: pointer => setState(CRAFT_TOKENS.MODIFICATIONS, {pointer}),
remove: atIndex => updateState(CRAFT_TOKENS.MODIFICATIONS, modifications => removeAndDropDependants(modifications, atIndex))
})
});

View file

@ -1,8 +1,32 @@
.item {
word-wrap: break-word;
word-break: break-all;
cursor: pointer;
&.selected, &:hover {
background-color: #780000;
}
display: flex;
justify-content: space-between;
align-items: baseline;
& .buttons {
display: none;
}
&:hover .buttons {
display: initial;
}
}
.buttons {
}
display: none;
font-size: 1.3rem;
& > * {
padding: 0 0.3rem;
&.danger:hover {
color: red;
}
}
}
.buttons > i:hover {
color: yellowgreen;
}

View file

@ -0,0 +1,27 @@
import React from 'react';
import connect from '../../../../../../modules/ui/connect';
import {TOKENS as CRAFT_TOKENS} from '../../../craft/craftPlugin';
import Wizard from './Wizard';
import {finishHistoryEditing, stepOverridingParams} from '../../../craft/craftHistoryUtils';
function HistoryWizard({history, pointer, step, cancel, offset}) {
if (pointer === history.length - 1) {
return null;
}
let {type, params: initialState} = history[pointer + 1];
return <Wizard type={type}
onCancel={cancel} onOK={step} close={NOOP}
initialState={initialState} left={offset} />
}
export default connect(HistoryWizard, CRAFT_TOKENS.MODIFICATIONS, {
mapActions: ({updateState}) => ({
step: (params) => updateState(CRAFT_TOKENS.MODIFICATIONS, modifications => stepOverridingParams(modifications, params)),
cancel: () => updateState(CRAFT_TOKENS.MODIFICATIONS, modifications => finishHistoryEditing(modifications)),
}),
mapSelfProps: ({offset}) => ({offset})
});
const NOOP = () => {};

View file

@ -15,45 +15,57 @@ import {CURRENT_SELECTION} from "../../../craft/wizard/wizardPlugin";
import ls from './Wizard.less';
import RadioButtons, {RadioButton} from "ui/components/controls/RadioButtons";
import CadError from '../../../../utils/errors';
import {createPreviewer} from '../../../preview/scenePreviewer';
export default class Wizard extends React.Component {
constructor({initialState, metadata, previewer}, {services: {selection}}) {
constructor({initialState, type}, {services}) {
super();
let {metadata, previewGeomProvider} = services.operation.get(type);
this.metadata = metadata;
this.previewGeomProvider = previewGeomProvider;
this.state = {hasError: false};
this.params = {};
metadata.forEach(([name, type, v]) => {
this.metadata.forEach(([name, type, v]) => {
if (type === 'face' && v === CURRENT_SELECTION) {
let selectedFace = selection.face()[0];
let selectedFace = services.selection.face()[0];
v = selectedFace ? selectedFace.id : '';
}
this.params[name] = v
});
Object.assign(this.params, initialState);
this.preview = previewer(this.params);
this.previewer = createPreviewer(previewGeomProvider, services);
}
componentDidMount() {
this.preview = this.previewer(this.params);
}
componentWillUnmount() {
this.preview.dispose();
}
render() {
let {left, title, metadata} = this.props;
let {type, left} = this.props;
return <Window initWidth={250}
initLeft={left}
title={title}
onClose={this.onClose}
title={type}
onClose={this.cancel}
onKeyDown={this.onKeyDown}
setFocus={this.focusFirstInput}>
<Stack >
{metadata.map(([name, type, , params], index) => {
{this.metadata.map(([name, type, , params], index) => {
return <Field key={index}>
<Label>{uiLabel(name)}</Label>
{this.controlForType(name, type, params)}
</Field>
} )}
<ButtonGroup>
<Button onClick={this.onClose} >Cancel</Button>
<Button onClick={this.cancel} >Cancel</Button>
<Button type='accent' onClick={this.onOK} >OK</Button>
</ButtonGroup>
{this.state.hasError && <div className={ls.errorMessage}>
@ -70,7 +82,7 @@ export default class Wizard extends React.Component {
onKeyDown = e => {
switch (e.keyCode) {
case 27 :
this.onClose();
this.cancel()
break;
case 13 :
this.onOK();
@ -86,34 +98,44 @@ export default class Wizard extends React.Component {
toFocus.focus()
};
onClose = () => {
this.preview.dispose();
this.props.onCancel();
cancel = () => {
if (this.props.onCancel) {
this.props.onCancel();
}
this.props.close();
};
onOK = () => {
try {
this.props.onOK(this.params);
this.onClose();
} catch (error) {
let stateUpdate = {
hasError: true
};
let printError = true;
if (error.TYPE === CadError) {
let {code, userMessage, kind} = error;
printError = !code;
if (code && kind === CadError.KIND.INTERNAL_ERROR) {
console.warn('Operation Error Code: ' + code);
}
Object.assign(stateUpdate, {code, userMessage});
}
if (printError) {
console.error(error);
if (this.props.onOK) {
this.props.onOK(this.params);
} else {
this.context.services.craft.modify({type: this.props.type, params: this.params});
}
this.setState(stateUpdate);
this.props.close();
} catch (error) {
this.handleError(error);
}
};
handleError(error) {
let stateUpdate = {
hasError: true
};
let printError = true;
if (error.TYPE === CadError) {
let {code, userMessage, kind} = error;
printError = !code;
if (code && kind === CadError.KIND.INTERNAL_ERROR) {
console.warn('Operation Error Code: ' + code);
}
Object.assign(stateUpdate, {code, userMessage});
}
if (printError) {
console.error(error);
}
this.setState(stateUpdate);
}
controlForType(name, type, params, tabindex) {
const onChange = val => {

View file

@ -1,30 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, {Fragment} from 'react';
import {TOKENS as WIZARD_TOKENS} from '../../../craft/wizard/wizardPlugin';
import connect from 'ui/connect';
import Wizard from "./Wizard";
import {createPreviewer} from "../../../preview/scenePreviewer";
import Wizard from './Wizard';
import HistoryWizard from './HistoryWizard';
function WizardManager({wizards, close}, {services}) {
return wizards.map( ({type, initialState}, wizardIndex) => {
let {metadata, previewGeomProvider, run} = services.operation.get(type);
function onOK(params) {
services.craft.modify({type, params});
close();
}
let previewer = createPreviewer(previewGeomProvider, services);
return <Wizard key={wizardIndex} previewer={previewer} metadata={metadata}
onOK={onOK}
onCancel={close}
initialState={initialState} title={type} left={70 + wizardIndex * 250 + 20} />
});
function WizardManager({wizards, close}) {
return <Fragment>
{wizards.map((wizardRef, wizardIndex) => {
let {type, initialState} = wizardRef;
const closeInstance = () => close(wizardRef);
return <Wizard key={wizardIndex}
type={type}
close={closeInstance}
initialState={initialState} left={offset(wizardIndex)} />
})}
<HistoryWizard offset={offset(wizards.length)}/>
</Fragment>
}
WizardManager.contextTypes = {
services: PropTypes.object
};
function offset(wizardIndex) {
return 70 + (wizardIndex * (250 + 20));
}
export default connect(WizardManager, WIZARD_TOKENS.WIZARDS, {
mapProps: ([wizards]) => ({wizards}),

View file

@ -2,6 +2,7 @@ export default {
'CUT': 'c',
'EXTRUDE': 'e',
'PLANE': 'p',
'BOX': 'b',
'ZoomIn': '+',
'ZoomOut': '-',
'menu.craft': 'shift+c',