mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-23 17:04:00 +01:00
basic actions for craft history manipulation
This commit is contained in:
parent
c49c21fd17
commit
f9c202ba13
9 changed files with 225 additions and 111 deletions
40
web/app/cad/craft/craftHistoryUtils.js
Normal file
40
web/app/cad/craft/craftHistoryUtils.js
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
27
web/app/cad/dom/components/wizard/HistoryWizard.jsx
Normal file
27
web/app/cad/dom/components/wizard/HistoryWizard.jsx
Normal 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 = () => {};
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export default {
|
|||
'CUT': 'c',
|
||||
'EXTRUDE': 'e',
|
||||
'PLANE': 'p',
|
||||
'BOX': 'b',
|
||||
'ZoomIn': '+',
|
||||
'ZoomOut': '-',
|
||||
'menu.craft': 'shift+c',
|
||||
|
|
|
|||
Loading…
Reference in a new issue