history timeline widget

This commit is contained in:
Val Erastov 2018-07-05 22:42:25 -07:00
parent a4ef761ffe
commit b8053c5e25
44 changed files with 736 additions and 119 deletions

1
modules/gems/objects.js Normal file
View file

@ -0,0 +1 @@
export const EMPTY_OBJECT = Object.freeze({});

View file

@ -13,8 +13,12 @@ export class StreamBase {
pairwise(first) {
return new PairwiseStream(this, first);
}
scan(initAccumulator) {
return new ScanStream(this, initAccumulator);
}
keep() {
remember() {
let stateStream = new StateStream(undefined);
this.attach(v => stateStream.next(v));
return stateStream;
@ -25,4 +29,4 @@ const {MapStream} = require('./map');
const {FilterStream} = require('./filter');
const {StateStream} = require('./state');
const {PairwiseStream} = require('./pairwise');
const {ScanStream} = require('./scan');

14
modules/lstream/scan.js Normal file
View file

@ -0,0 +1,14 @@
import {StreamBase} from './base';
export class ScanStream extends StreamBase {
constructor(stream, initAccumulator) {
super();
this.stream = stream;
this.acc = initAccumulator;
}
attach(observer) {
return this.stream.attach(v => this.acc = observer(this.acc, v));
}
}

View file

@ -2,10 +2,13 @@ import React from 'react';
import cx from 'classnames';
import ls from './AuxWidget.less';
import AdjustableAbs from "./AdjustableAbs";
import AdjustableAbs from './AdjustableAbs';
export default function AuxWidget({flatTop, flatBottom, children, className, ...props}) {
return <AdjustableAbs className={cx(ls.root, flatTop && ls.flatTop, flatBottom && ls.flatBottom, className)} {...props}>
export default function AuxWidget({flatTop, flatBottom, flatRight, flatLeft, children, className, ...props}) {
return <AdjustableAbs className={cx(ls.root,
flatTop && ls.flatTop, flatBottom && ls.flatBottom,
flatRight && ls.flatRight, flatLeft && ls.flatLeft,
className)} {...props}>
{children}
</AdjustableAbs>
</AdjustableAbs>;
}

View file

@ -1,3 +1,4 @@
@import "./flatEdges.less";
@border-radius: 3px;
.root {
@ -8,9 +9,18 @@
}
.flatBottom {
border-radius: @border-radius @border-radius 0 0;
._flatBottom(@border-radius);
}
.flatTop {
border-radius: 0 0 @border-radius @border-radius;
._flatTop(@border-radius);
}
.flatRight {
._flatRight(@border-radius);
}
.flatLeft {
._flatLeft(@border-radius);
}

View file

@ -0,0 +1,30 @@
import React from 'react';
import ls from './RenderObject.less';
export default function RenderObject({object}) {
return <div className={ls.root}><RenderObjectImpl object={object}/></div>
}
function RenderObjectImpl({object, inner}) {
if (object === undefined || object === null) {
return <span>{object}</span>;
}
if (typeof object === 'object') {
return <div style={{marginLeft: inner?10:0}}>
{Object.keys(object).map(field => <div key={field}>
{field}: <RenderObjectImpl object={object[field]} inner/>
</div>)}
</div>;
} else if (Array.isArray(object)) {
return <div style={{marginLeft: inner?10:0}}>
{Object.map(object).map((item, i) => <div key={i}>
<div><RenderObject object={object[field]} inner/></div>
</div>)}
{Object.keys(object).map(field => <div key={field}>
{field}: <RenderObject object={object[field]} inner/>
</div>)}
</div>;
} else {
return <span>{object}</span>;
}
}

View file

@ -0,0 +1,3 @@
.root {
line-height: 1.5;
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import cx from 'classnames';
import ls from './Widget.less';
import AdjustableAbs from './AdjustableAbs';
import Fa from './Fa';
import SymbolButton from './controls/SymbolButton';
export default function Widget({flatTop, flatBottom, flatRight, flatLeft, children, className, title, onClose, ...props}) {
return <AdjustableAbs className={cx(ls.root,
flatTop && ls.flatTop, flatBottom && ls.flatBottom,
flatRight && ls.flatRight, flatLeft && ls.flatLeft,
className)} {...props}>
<div className={ls.header}>
<div className={ls.title}>{title}</div>
<span className={ls.headerButtons}>
<SymbolButton type='danger' onClick={onClose}><Fa fw icon='close'/></SymbolButton>
</span>
</div>
<div className={ls.content}>
{children}
</div>
</AdjustableAbs>;
}

View file

@ -0,0 +1,50 @@
@import "./flatEdges.less";
@border-radius: 5px;
.root {
color: #fff;
background-color: rgba(40, 40, 40, 0.95);
border: solid 1px #000;
border-radius: @border-radius;
}
.flatBottom {
._flatBottom(@border-radius);
}
.flatTop {
._flatTop(@border-radius);
}
.flatRight {
._flatRight(@border-radius);
}
.flatLeft {
._flatLeft(@border-radius);
}
//IMPL
.root {
padding: 5px 5px 10px 15px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 16px;
}
.headerButtons {
font-size: 20px;
margin-left: 10px;
}
.content {
padding-top: 5px;
}

View file

@ -1,10 +1,11 @@
import React from 'react';
import ls from './Button.less'
import cx from 'classnames';
export default function Button({type, onClick, children}) {
return <button onClick={onClick} className={ls[type]}>{children}</button>
return <button onClick={onClick} className={cx(ls[type], ls.button)}>{children}</button>
}

View file

@ -1,6 +1,10 @@
@import '../../styles/theme.less';
@import '../../styles/mixins.less';
.button {
line-height: 1.5;
}
.neutral, .accent, .danger {
background-color: darken(@color-neutral, 10%);
}

View file

@ -0,0 +1,15 @@
import React from 'react';
import ls from './SymbolButton.less';
export default function SymbolButton({type, onClick, children}) {
return <div className={ls[type]} onClick={onClick}>{children}</div>
}
SymbolButton.defaultProps = {
type: 'neutral',
};

View file

@ -0,0 +1,30 @@
@import '../../styles/theme.less';
.symbolButton {
&:active {
transition: 100ms;
}
cursor: pointer;
}
.neutral {
.symbolButton;
color: #fff;
&:hover {
color: #EFEFEF;
}
&:active {
color: #9cdaf7;
}
}
.danger {
.symbolButton;
color: #fff;
&:hover {
color: @color-danger;
}
&:active {
color: lighten(@color-danger, 20%);
}
}

View file

@ -0,0 +1,20 @@
._flatBottom(@border-radius) {
border-radius: @border-radius @border-radius 0 0;
}
._flatTop(@border-radius) {
border-radius: 0 0 @border-radius @border-radius;
}
._flatRight(@border-radius) {
border-radius: @border-radius 0 0 @border-radius;
}
._flatLeft(@border-radius) {
border-radius: 0 @border-radius @border-radius 0;
}

View file

@ -5,12 +5,18 @@ export default function connect(streamProvider) {
return function (Component) {
return class Connected extends React.Component {
state = {hasError: false};
streamProps = {};
componentWillMount() {
let stream = streamProvider(context.streams, this.props);
this.detacher = stream.attach(data => {
this.streamProps = data;
if (this.state.hasError) {
this.setState({hasError: false});
return;
}
this.forceUpdate();
});
}
@ -20,10 +26,18 @@ export default function connect(streamProvider) {
}
render() {
if (this.state.hasError) {
return null;
}
return <Component {...this.streamProps}
{...this.props} />;
}
componentDidCatch() {
this.setState({hasError: true});
}
};
}
}

View file

@ -1,8 +1,9 @@
import React from 'react';
import context from 'context';
export default function errorBoundary(message, fix) {
export default function errorBoundary(message, fix, resetOn) {
return function(Comp) {
return class extends React.Component {
class ErrorBoundary extends React.Component {
state = {
hasError: false,
@ -17,8 +18,26 @@ export default function errorBoundary(message, fix) {
this.setState({hasError: false, fixAttempt: true});
}
}
if (resetOn) {
let stream = resetOn(context.streams);
if (stream) {
this.attcahing = true;
this.detacher = stream.attach(this.reset);
this.attcahing = false;
}
}
}
reset = () => {
if (this.attcahing) {
return;
}
this.setState({hasError: false, fixAttempt: false});
if (this.detacher) {
this.detacher();
}
};
render() {
if (this.state.hasError) {
return message || null;
@ -26,5 +45,6 @@ export default function errorBoundary(message, fix) {
return <Comp {...this.props} />;
}
}
return ErrorBoundary;
}
}

View file

@ -0,0 +1,7 @@
export function aboveElement(el) {
let r = el.getBoundingClientRect();
return {
x: r.left,
y: r.top
}
}

View file

@ -1,14 +1,13 @@
@import "../theme.less";
@import "../mixins.less";
html {
font: 10px 'Lucida Grande', sans-serif;
html, pre {
font: 11px 'Lucida Grande', sans-serif;
}
body {
background-color: @bg-color;
color: @font-color;
font-size: 11px;
}
iframe {
@ -29,3 +28,6 @@ button {
color: inherit;
}
pre {
line-height: 1.5;
}

22
package-lock.json generated
View file

@ -5312,16 +5312,6 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz",
"integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^2.6.0"
}
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
@ -8426,6 +8416,18 @@
"mkdirp": "~0.5.1",
"sax": "~1.2.1",
"whet.extend": "~0.9.9"
},
"dependencies": {
"js-yaml": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz",
"integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^2.6.0"
}
}
}
},
"table": {

View file

@ -4,7 +4,7 @@ import {EDGE, FACE, SKETCH_OBJECT} from '../scene/entites';
export function activate({streams, services}) {
streams.cadRegistry = {
shellIndex: streams.craft.models.map(models => models.reduce((i, v)=> i.set(v.id, v), new Map())).keep()
shellIndex: streams.craft.models.map(models => models.reduce((i, v)=> i.set(v.id, v), new Map())).remember()
};
streams.cadRegistry.update = streams.cadRegistry.shellIndex;

View file

@ -44,7 +44,7 @@ export function activate({streams, services}) {
for (let i = beginIndex; i <= pointer; i++) {
let request = history[i];
let op = services.operation.registry[request.type];
let op = services.operation.get(request.type);
if (!op) {
console.log(`unknown operation ${request.type}`);
}

View file

@ -0,0 +1,9 @@
import {state} from 'lstream';
import {EMPTY_OBJECT} from 'gems/objects';
export function activate({streams}) {
streams.ui.craft = {
modificationSelection: state(EMPTY_OBJECT)
}
}

View file

@ -1,28 +1,33 @@
import {state} from 'lstream';
export function activate(context) {
let {services} = context;
let registry = {};
context.streams.operation = {
registry: state({})
};
let registry$ = context.streams.operation.registry;
function addOperation(descriptor, actions) {
let {id, label, info, icon, actionParams} = descriptor;
let opAction = {
id: id,
appearance: {
label,
let appearance = {
label,
info,
icon32: icon + '32.png',
icon96: icon + '96.png',
},
};
let opAction = {
id: id,
appearance,
invoke: () => services.wizard.open({type: id}),
...actionParams
};
actions.push(opAction);
registry[id] = Object.assign({}, descriptor, {
registry$.mutate(registry => registry[id] = Object.assign({appearance}, descriptor, {
run: (request, services) => runOperation(request, descriptor, services)
});
}));
}
function registerOperations(operations) {
@ -34,7 +39,7 @@ export function activate(context) {
}
function get(id) {
let op = registry[id];
let op = registry$.value[id];
if (!op) {
throw `operation ${id} is not registered`;
}
@ -43,7 +48,6 @@ export function activate(context) {
services.operation = {
registerOperations,
registry,
get
};
}

View file

@ -0,0 +1,126 @@
import React from 'react';
import ls from './HistoryTimeline.less';
import connect from 'ui/connect';
import decoratorChain from '../../../../../modules/ui/decoratorChain';
import {finishHistoryEditing, removeAndDropDependants} from '../craftHistoryUtils';
import mapContext from '../../../../../modules/ui/mapContext';
import ImgIcon from 'ui/components/ImgIcon';
import {getDescriptor} from './OperationHistory';
import cx from 'classnames';
import Fa from '../../../../../modules/ui/components/Fa';
import {menuAboveElementHint} from '../../dom/menu/menuUtils';
import {combine} from 'lstream';
import {EMPTY_OBJECT} from '../../../../../modules/gems/objects';
import {VIEWER_SELECTOR} from '../../dom/components/View3d';
import {aboveElement} from '../../../../../modules/ui/positionUtils';
function HistoryTimeline({history, pointer, setHistoryPointer, remove, getOperation}) {
return <div className={ls.root}>
<Controls pointer={pointer} eoh={history.length-1} setHistoryPointer={setHistoryPointer}/>
{history.map((m, i) => <React.Fragment key={i}>
<Timesplitter active={i-1 === pointer} onClick={() => setHistoryPointer(i-1)} />
<HistoryItem index={i} modification={m} getOperation={getOperation}
disabled={pointer < i}
inProgress={pointer === i-1} />
</React.Fragment>)}
<Timesplitter eoh active={history.length-1 === pointer} onClick={() => setHistoryPointer(history.length-1)}/>
<InProgressOperation getOperation={getOperation}/>
<AddButton />
</div>;
}
const InProgressOperation = connect(streams => streams.wizard.map(wizard => ({wizard})))(
function InProgressOperation({wizard, getOperation}) {
if (!wizard) {
return null;
}
let {appearance} = getOperation(wizard.type);
return <div className={ls.inProgressItem}>
<ImgIcon url={appearance&&appearance.icon96} size={24} />
</div>;
}
);
function Timesplitter({active, eoh, onClick}) {
return <div className={cx(ls.timesplitter, active&&ls.active, eoh&&ls.eoh)} >
<div className={ls.handle} onClick={onClick}>
<Handle />
</div>
</div>;
}
function Handle() {
const w = 12;
const h = 15;
const m = Math.round(w * 0.5);
const t = Math.round(h * 0.5);
return <svg xmlns="http://www.w3.org/2000/svg" height={h} width={w} >
<polygon className={ls.handlePoly} points={`0,0 ${w},0 ${w},${t} ${m},${h} 0,${t}`}
style={{strokeWidth:0.5}} />
</svg>;
}
function Controls({pointer, eoh, setHistoryPointer}) {
const noB = pointer===-1;
const noF = pointer===eoh;
return <React.Fragment>
<div className={cx(ls.controlBtn, noB&&ls.disabled)} onClick={noB?undefined :() => setHistoryPointer(pointer-1)}>
<Fa icon='step-backward' fw/>
</div>
<div className={cx(ls.controlBtn, noF&&ls.disabled)} onClick={noF?undefined :() => setHistoryPointer(pointer+1)}>
<Fa icon='step-forward' fw/>
</div>
<div className={cx(ls.controlBtn, noF&&ls.disabled)} onClick={noF?undefined :() => setHistoryPointer(eoh)}>
<Fa icon='fast-forward' fw/>
</div>
</React.Fragment>;
}
const HistoryItem = decoratorChain(
connect((streams, props) => streams.ui.craft.modificationSelection.map(s => ({
selected: s.index === props.index,
}))),
mapContext(({streams}) => ({
toggle: (index, modification, el) => streams.ui.craft.modificationSelection.update(s =>
s.index === index ? EMPTY_OBJECT : {index, locationHint: aboveElement(el)})
}))
)
(
function HistoryItem({index, pointer, modification, getOperation, toggle, selected, disabled, inProgress}) {
let {appearance} = getOperation(modification.type);
return <div className={cx(ls.historyItem, selected&&ls.selected, disabled&&ls.disabled, inProgress&&ls.inProgress)}
onClick={e => toggle(index, modification, e.currentTarget)}>
<ImgIcon className={ls.opIcon} url={appearance&&appearance.icon96} size={24} />
<span className={ls.opIndex}>{ index + 1 }</span>
</div>;
});
const AddButton = mapContext(({services}) => ({
showCraftMenu: e => services.action.run('menu.craft', menuAboveElementHint(e.currentTarget))
}))(
function AddButton({showCraftMenu}) {
return <div className={ls.add} onClick={showCraftMenu}>
<Fa icon='plus' fw/>
</div>;
}
);
export default decoratorChain(
connect(streams => combine(streams.craft.modifications, streams.operation.registry)
.map(([modifications, operationRegistry]) => ({
...modifications,
operationRegistry,
getOperation: type => operationRegistry[type]||EMPTY_OBJECT
}))),
mapContext(({streams}) => ({
remove: atIndex => streams.craft.modifications.update(modifications => removeAndDropDependants(modifications, atIndex)),
cancel: () => streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)),
setHistoryPointer: pointer => streams.craft.modifications.update(({history}) => ({history, pointer}))
}))
)(HistoryTimeline);

View file

@ -0,0 +1,143 @@
@import "~ui/styles/theme.less";
.root {
background-color: rgba(0, 0, 0, 0.1);
display: flex;
align-items: stretch;
height: 34px;
//&:hover .timesplitter .handlePoly {
// visibility: visible;
//}
}
.timesplitter {
width: 4px;
height: 100%;
cursor: pointer;
.handle {
margin-top: -15px;
margin-left: -4px;
font-size: 20px;
}
}
.timesplitter.active:not(.eoh) {
background-color: #ff940b;
}
.handlePoly {
fill: #808080;
stroke: #000;
&:hover {
fill: #ff940b;
stroke: #ff3a1e;
}
}
.timesplitter.active .handlePoly {
fill: #ff940b;
stroke: #ff3a1e;
}
.timesplitter.active.eoh .handlePoly {
fill: #BFBFBF;
stroke: #000;
}
//ITEMS
.item {
margin: 2px 0;
padding: 2px;
border: #2e2e2e 1px solid;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
cursor: pointer;
&:active {
transition: 300ms;
}
}
.disabled {
background-color: #828282;
border-color: #a7a7a7;
color: #a7a7a7;
}
.controlBtn {
.item;
margin-left: 2px;
margin-right: 2px;
background-color: #64808b;
&:hover {
background-color: #489;
}
&:active {
background-color: #5dc4da;
}
&.disabled {.disabled;}
}
.historyItem {
.item;
background-color: #737373;
&:hover {
background-color: #4d4d4d;
}
&:active {
background-color: #9c9c9c;
}
&.disabled {.disabled;}
&.selected {
//background-color: #2B2B2B;
border-color: #00ffe4;
//border-width: 2px;
}
&.inProgress {
background-color: @inProgressColor;
}
position: relative;
& .opIndex {
position: absolute;
right: 1px;
bottom: 1px;
text-shadow: 0px -1px 0 #000, 0px 1px 0 #000, 1px 0px 0 #000, -1px 0px 0 #000,
-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
& .opIcon {
margin-left: -6px;
margin-top: -3px;
}
.opInfo {
position: absolute;
bottom: 20px;
}
}
@inProgressColor: #648268;
.inProgressItem {
.item;
background-color: @inProgressColor;
border: #ffffff88 1px dashed;
margin-right: 4px;
cursor: auto;
}
.add {
.controlBtn;
border: #ffffff88 1px dashed;
color: #ffffff88;
&:hover {
border-color: #fff;
}
&.disabled {.disabled;}
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import connect from '../../../../../modules/ui/connect';
import {Section} from '../../../../../modules/ui/components/Section';
import {MShell} from '../../model/mshell';
export default connect(streams => streams.craft.models.map(models => ({models})))
(function ObjectExplorer({models}) {

View file

@ -7,17 +7,18 @@ import ls from './OperationHistory.less';
import cx from 'classnames';
import ButtonGroup from 'ui/components/controls/ButtonGroup';
import Button from 'ui/components/controls/Button';
import {finishHistoryEditing, removeAndDropDependants} from '../../craft/craftHistoryUtils';
import {finishHistoryEditing, removeAndDropDependants} from '../craftHistoryUtils';
import mapContext from 'ui/mapContext';
import decoratorChain from 'ui/decoratorChain';
import {EMPTY_OBJECT} from '../../../../../modules/gems/objects';
function OperationHistory({history, pointer, setHistoryPointer, remove, operationRegistry}) {
function OperationHistory({history, pointer, setHistoryPointer, remove, getOperation}) {
let lastMod = history.length - 1;
return <Stack>
{history.map(({type, params}, index) => {
let {appearance, paramsInfo} = getDescriptor(type, operationRegistry);
let {appearance, paramsInfo} = getOperation(type)||EMPTY_OBJECT;
return <div key={index} onClick={() => setHistoryPointer(index - 1)}
className={cx(ls.item, pointer + 1 === index && ls.selected)}>
{appearance && <ImgIcon url={appearance.icon32} size={16}/>}
@ -35,21 +36,12 @@ function OperationHistory({history, pointer, setHistoryPointer, remove, operatio
</Stack>;
}
const EMPTY_DESCRIPTOR = {};
function getDescriptor(type, registry) {
let descriptor = registry[type];
if (!descriptor) {
descriptor = EMPTY_DESCRIPTOR;
}
return descriptor;
}
export default decoratorChain(
connect(streams => streams.craft.modifications),
mapContext(({streams, services}) => ({
remove: atIndex => streams.craft.modifications.update(modifications => removeAndDropDependants(modifications, atIndex)),
cancel: () => streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)),
operationRegistry: services.operation.registry,
getOperation: services.operation.get,
setHistoryPointer: pointer => streams.craft.modifications.update(({history}) => ({history, pointer}))
}))
)(OperationHistory);

View file

@ -0,0 +1,65 @@
import React from 'react';
import connect from 'ui/connect';
import Widget from 'ui/components/Widget';
import decoratorChain from '../../../../../modules/ui/decoratorChain';
import {combine, merger} from '../../../../../modules/lstream';
import ls from './SelectedModificationInfo.less';
import ImgIcon from 'ui/components/ImgIcon';
import YAML from 'yamljs';
import mapContext from 'ui/mapContext';
import {EMPTY_OBJECT} from '../../../../../modules/gems/objects';
import ButtonGroup from '../../../../../modules/ui/components/controls/ButtonGroup';
import Button from '../../../../../modules/ui/components/controls/Button';
import {removeAndDropDependants} from '../craftHistoryUtils';
import RenderObject from 'ui/components/RenderObject';
function SelectedModificationInfo({ history, index,
operationRegistry,
locationHint: lh,
drop, edit,
close}) {
let m = history[index];
let visible = !!m;
if (!visible) {
return null;
}
let op = operationRegistry[m.type];
if (!op) {
console.warn('unknown operation ' + m.type);
return;
}
let {appearance} = op;
let indexNumber = index + 1;
return <Widget visible={visible}
left={lh && lh.x}
bottom={75}
flatRight={!lh}
title={m.type + ' operation #' + indexNumber}
onClose={close}>
<div className={ls.requestInfo}>
<ImgIcon className={ls.pic} url={appearance && appearance.icon96} size={48}/>
<RenderObject object={m.params}/>
</div>
<div>
<ButtonGroup>
<Button onClick={edit}>EDIT OPERATION</Button>
<Button type='danger' onClick={drop}>DROP OPERATION</Button>
</ButtonGroup>
</div>
</Widget>;
}
export default decoratorChain(
connect(streams => combine(streams.ui.craft.modificationSelection,
streams.operation.registry.map(r => ({operationRegistry: r})),
streams.craft.modifications
).map(merger)),
mapContext((ctx, props) => ({
close: () => ctx.streams.ui.craft.modificationSelection.next(EMPTY_OBJECT),
drop: () => ctx.streams.craft.modifications.update(modifications => removeAndDropDependants(modifications, props.index)),
edit: () => ctx.streams.craft.modifications.update(({history}) => ({history, pointer: props.index - 1}))
}))
)(SelectedModificationInfo);

View file

@ -0,0 +1,11 @@
.requestInfo {
display: flex;
& > * {
margin-right: 5px;
}
margin-bottom: 5px;
}
.pic {
margin-right: 5px;
}

View file

@ -5,7 +5,6 @@ import {finishHistoryEditing, stepOverridingParams} from '../../craftHistoryUtil
import {NOOP} from 'gems/func';
import decoratorChain from 'ui/decoratorChain';
import mapContext from 'ui/mapContext';
import {createPreviewer} from '../../../preview/scenePreviewer';
function HistoryWizard({history, pointer, step, cancel, offset, getOperation, previewerCreator, createValidator}) {
if (pointer === history.length - 1) {

View file

@ -11,31 +11,32 @@ import initializeBySchema from '../../intializeBySchema';
import validateParams from '../../validateParams';
class WizardManager extends React.Component {
render() {
let {wizards, close} = this.props;
return <React.Fragment>
{wizards.map((wizardRef, wizardIndex) => {
let {type} = wizardRef;
let operation = this.props.getOperation(type);
if (!operation) {
throw 'unknown operation ' + type;
}
let params = this.props.initializeOperation(operation);
let validator = this.props.createValidator(operation);
const closeInstance = () => close(wizardRef);
return <Wizard key={wizardIndex}
type={type}
createPreviewer={this.props.previewerCreator(operation)}
form={operation.form}
params={params}
validate={validator}
close={closeInstance}
left={offset(wizardIndex)} />
})}
<HistoryWizard offset={offset(wizards.length)}
createValidator={this.props.createValidator}
render() {
let {wizard, close} = this.props;
if (!wizard) {
return null;
}
let {type} = wizard;
let operation = this.props.getOperation(type);
if (!operation) {
throw 'unknown operation ' + type;
}
let params = this.props.initializeOperation(operation);
let validator = this.props.createValidator(operation);
const closeInstance = () => close(wizard);
return <React.Fragment>
<Wizard type={type}
createPreviewer={this.props.previewerCreator(operation)}
form={operation.form}
params={params}
validate={validator}
close={closeInstance}/>
<HistoryWizard createValidator={this.props.createValidator}
getOperation={this.props.getOperation}
previewerCreator={this.props.previewerCreator}/>
</React.Fragment>;
@ -43,15 +44,15 @@ class WizardManager extends React.Component {
}
function offset(wizardIndex) {
return 70 + (wizardIndex * (250 + 20));
return 70 + (wizardIndex * (250 + 20));
}
export default decoratorChain(
connect(streams => streams.wizards.map(wizards => ({wizards}))),
connect(streams => streams.wizard.map(wizard => ({wizard}))),
mapContext(ctx => ({
close: wizard => ctx.services.wizard.close(wizard),
close: () => ctx.services.wizard.close(),
reset: () => {
ctx.streams.wizards.value = [];
ctx.streams.wizard.value = null;
ctx.streams.craft.modifications.update(modifications => finishHistoryEditing(modifications));
},
getOperation: type => ctx.services.operation.get(type),

View file

@ -2,7 +2,7 @@ import {state} from '../../../../../modules/lstream';
export function activate({streams, services}) {
streams.wizards = state([]);
streams.wizard = state(null);
services.wizard = {
@ -12,11 +12,11 @@ export function activate({streams, services}) {
type
};
streams.wizards.update(opened => [...opened, wizard]);
streams.wizard.value = wizard;
},
close: wizard => {
streams.wizards.update(opened => opened.filter(w => w !== wizard));
streams.wizard = null;
}
}
}

View file

@ -0,0 +1,8 @@
import React from 'react';
import ls from './BottomStack.less';
export default function BottomStack({children}) {
return <div className={ls.root}>
{children}
</div>;
}

View file

@ -0,0 +1,9 @@
.root {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
z-index: 150;
flex-direction: column;
}

View file

@ -1,12 +1,7 @@
@import "~ui/styles/theme.less";
.root {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
z-index: 150;
justify-content: space-between;
background-color: @work-area-control-bar-bg-color;
color: @work-area-control-bar-font-color;

View file

@ -1,10 +1,10 @@
import React, {Fragment} from 'react';
import ObjectExplorer from './ObjectExplorer';
import OperationHistory from './OperationHistory';
import ObjectExplorer from '../../craft/ui/ObjectExplorer';
import OperationHistory from '../../craft/ui/OperationHistory';
import Folder from 'ui/components/Folder';
import Fa from '../../../../../modules/ui/components/Fa';
export default function PartPanel() {
export default function FloatView() {
return <Fragment>
<Folder title={<span> <Fa fw icon='cubes' /> Model</span>}>
<ObjectExplorer/>

View file

@ -7,6 +7,7 @@ import {isMenuAction} from '../menu/menuPlugin';
import {combine, merger} from 'lstream';
import mapContext from 'ui/mapContext';
import decoratorChain from '../../../../../modules/ui/decoratorChain';
import {menuAboveElementHint} from '../menu/menuUtils';
export default function PlugableControlBar() {
return <ControlBar left={<LeftGroup />} right={<RightGroup />}/>;
@ -28,7 +29,7 @@ class ActionButton extends React.Component {
}
if (isMenuAction(actionId)) {
let onClick = props.onClick;
props.onClick = e => onClick(getMenuData(this.el));
props.onClick = e => onClick(menuAboveElementHint(this.el));
}
return <ControlBarButton disabled={!enabled} onElement={el => this.el = el} {...props} >
@ -53,12 +54,3 @@ const ConnectedActionButton = decoratorChain(
)
(ActionButton);
function getMenuData(el) {
//TODO: make more generic
return {
orientationUp: true,
flatBottom: true,
x: el.offsetParent.offsetParent.offsetLeft + el.offsetLeft,
y: el.offsetParent.offsetHeight - el.offsetTop
};
}

View file

@ -1,15 +1,14 @@
import React from 'react';
import PlugableControlBar from './PlugableControlBar';
import ls from './View3d.less';
import Abs from 'ui/components/Abs';
import {
AuxiliaryToolbar, HeadsUpToolbar, PlugableToolbarLeft, PlugableToolbarLeftSecondary,
PlugableToolbarRight
} from './PlugableToolbar';
import {AuxiliaryToolbar, HeadsUpToolbar} from './PlugableToolbar';
import UISystem from './UISystem';
import WizardManager from '../../craft/wizard/components/WizardManager';
import PartPanel from './PartPanel';
import FloatView from './FloatView';
import HistoryTimeline from '../../craft/ui/HistoryTimeline';
import BottomStack from './BottomStack';
import SelectedModificationInfo from '../../craft/ui/SelectedModificationInfo';
export default class View3d extends React.Component {
@ -18,25 +17,29 @@ export default class View3d extends React.Component {
//we don't want the dom to be updated under any circumstances or we loose the WEB-GL container
return false;
}
render() {
return <UISystem className={ls.root} >
return <UISystem className={ls.root}>
<div className={ls.sideBar}>
<PartPanel />
<FloatView />
</div>
<div className={ls.viewer} id='viewer-container'>
<Abs left='0.8em' top='0.8em'>
<HeadsUpToolbar />
<HeadsUpToolbar/>
</Abs>
<Abs right='0.8em' top='0.8em'>
<AuxiliaryToolbar small vertical/>
</Abs>
<PlugableControlBar />
<WizardManager />
<BottomStack>
<HistoryTimeline />
<PlugableControlBar/>
</BottomStack>
<WizardManager/>
</div>
</UISystem>
<SelectedModificationInfo />
</UISystem>;
}
componentWillUnmount() {
throw 'big no-no';
}

View file

@ -0,0 +1,6 @@
export const menuAboveElementHint = el => ({
orientationUp: true,
flatBottom: true,
x: el.offsetParent.offsetParent.offsetLeft + el.offsetLeft,
y: el.offsetParent.offsetHeight - el.offsetTop
});

View file

@ -13,6 +13,7 @@ import * as OperationPlugin from '../craft/operationPlugin';
import * as CraftEnginesPlugin from '../craft/enginesPlugin';
import * as CadRegistryPlugin from '../craft/cadRegistryPlugin';
import * as CraftPlugin from '../craft/craftPlugin';
import * as CraftUiPlugin from '../craft/craftUiPlugin';
import * as StoragePlugin from '../storagePlugin';
import * as ProjectPlugin from '../projectPlugin';
import * as SketcherPlugin from '../sketch/sketcherPlugin';
@ -42,6 +43,7 @@ export default function startApplication(callback) {
CraftEnginesPlugin,
OperationPlugin,
CraftPlugin,
CraftUiPlugin,
CadRegistryPlugin,
tpiPlugin
];

View file

@ -11,7 +11,7 @@ export function activate({services, streams}) {
['ShowSketches', {label: 'sketches'}], ['DeselectAll', {label: null}], ['ToggleCameraMode', {label: null}]
];
streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'INTERSECTION', 'DIFFERENCE', 'UNION'];
streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'FILLET', 'INTERSECTION', 'DIFFERENCE', 'UNION'];
streams.ui.toolbars.auxiliary.value = ['Save', 'StlExport'];
services.action.registerActions(CoreActions);

View file

View file

@ -1,7 +1,6 @@
<html>
<head>
<title>TCAD</title>
<link rel="stylesheet" href="css/toolkit.css?modeler">
<title>Web CAD / Part Designer</title>
<link rel="stylesheet" href="lib/font-awesome/css/font-awesome.min.css?modeler">
<script src="lib/pnltri.js"></script>