mirror of
https://github.com/xibyte/jsketcher
synced 2026-01-01 21:34:35 +01:00
React transition
This commit is contained in:
parent
95d4b96bc7
commit
fdc52ec85d
181 changed files with 1871 additions and 506 deletions
|
|
@ -5,19 +5,16 @@ export default class Bus {
|
|||
this.state = {};
|
||||
this.keepStateFor = new Set();
|
||||
this.lock = new Set();
|
||||
this.stateConnections = new Map();
|
||||
}
|
||||
|
||||
subscribe(key, callback) {
|
||||
let listenerList = this.listeners[key];
|
||||
subscribe(token, callback) {
|
||||
let listenerList = this.listeners[token];
|
||||
if (listenerList === undefined) {
|
||||
listenerList = [];
|
||||
this.listeners[key] = listenerList;
|
||||
this.listeners[token] = listenerList;
|
||||
}
|
||||
listenerList.push(callback);
|
||||
|
||||
if (this.keepStateFor.has(key)) {
|
||||
callback(this.state[key]);
|
||||
}
|
||||
return callback;
|
||||
};
|
||||
|
||||
|
|
@ -30,7 +27,49 @@ export default class Bus {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokensToStates(tokens) {
|
||||
return tokens.map( token => this.state[token] );
|
||||
}
|
||||
|
||||
connectToState(tokens, callback) {
|
||||
if (!Array.isArray(tokens)) {
|
||||
tokens = [tokens];
|
||||
}
|
||||
|
||||
let connection = () => {
|
||||
callback(this.tokensToStates(tokens));
|
||||
};
|
||||
tokens.forEach(token => {
|
||||
if (!this.keepStateFor.has(token)) {
|
||||
throw `unable to connect to stateless token ${token}. state for a token should be initialized before connecting`;
|
||||
}
|
||||
this.subscribe(token, connection);
|
||||
});
|
||||
this.stateConnections.set(connection, tokens);
|
||||
return connection;
|
||||
}
|
||||
|
||||
disconnectFromState(connection) {
|
||||
this.stateConnections.get(connection).forEach(token => this.unSubscribe(token, connection));
|
||||
this.stateConnections.delete(connection);
|
||||
}
|
||||
|
||||
updateStates(tokens, updater) {
|
||||
let updated = updater(this.tokensToStates(tokens));
|
||||
for (let i = 0; i < tokens.length; ++i) {
|
||||
this.dispatch(tokens[i], updated[i]);
|
||||
}
|
||||
}
|
||||
|
||||
updateState(token, updater) {
|
||||
this.dispatch(token, updater(this.state[token]));
|
||||
}
|
||||
|
||||
setState(token, partialState) {
|
||||
this.dispatch(token, Object.assign({}, this.state[token], partialState));
|
||||
}
|
||||
|
||||
dispatch(key, data) {
|
||||
if (this.lock.has(key)) {
|
||||
console.warn('recursive dispatch');
|
||||
|
|
@ -57,16 +96,14 @@ export default class Bus {
|
|||
}
|
||||
};
|
||||
|
||||
enableState(forEvent, initValue) {
|
||||
this.keepStateFor.add(forEvent);
|
||||
this.state[forEvent] = initValue;
|
||||
enableState(forToken, initValue) {
|
||||
this.keepStateFor.add(forToken);
|
||||
this.state[forToken] = initValue;
|
||||
}
|
||||
|
||||
disableState(forEvent) {
|
||||
this.keepStateFor.delete(forEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function createToken(...fqn) {
|
||||
return fqn.join('.');
|
||||
}
|
||||
|
||||
|
|
|
|||
25
modules/ui/WindowSystem.jsx
Normal file
25
modules/ui/WindowSystem.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class WindowSystem extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
windows: []
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.windows;
|
||||
}
|
||||
|
||||
addWindow(window) {
|
||||
this.setState({windows: [...this.state.windows, window]});
|
||||
}
|
||||
|
||||
removeWindow(window) {
|
||||
let windows = [...this.state.windows];
|
||||
windows.splice(windows.indexOf(window), 1);
|
||||
this.setState({windows});
|
||||
}
|
||||
}
|
||||
51
modules/ui/components/Abs.jsx
Normal file
51
modules/ui/components/Abs.jsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class Abs extends React.Component {
|
||||
|
||||
fit() {
|
||||
if (!this.el) {
|
||||
return;
|
||||
}
|
||||
let w = this.el.offsetWidth;
|
||||
let h = this.el.offsetHeight;
|
||||
let holder = this.el.parentNode;
|
||||
|
||||
const fit = (prop, dim, holderDim) => {
|
||||
let pos = this.props[prop];
|
||||
if (pos !== undefined) {
|
||||
if (pos + dim > holderDim) {
|
||||
pos = holderDim - dim;
|
||||
this.el.style[prop] = pos + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fit('left', w, holder.offsetWidth);
|
||||
fit('right', w, holder.offsetWidth);
|
||||
fit('top', h, holder.offsetHeight);
|
||||
fit('bottom', h, holder.offsetHeight);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fit();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.fit();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.el = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
let {left, top, right, bottom, children, style, ...props} = this.props;
|
||||
return <div ref={el => this.el = el}
|
||||
style={{position: 'absolute', left, top, right, bottom, zIndex: 999, ...style}} {...props}>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
11
modules/ui/components/AuxWidget.jsx
Normal file
11
modules/ui/components/AuxWidget.jsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import Abs from "./Abs";
|
||||
import cx from 'classnames';
|
||||
|
||||
import ls from './AuxWidget.less';
|
||||
|
||||
export default function AuxWidget({flatTop, flatBottom, children, className, ...props}) {
|
||||
return <Abs className={cx(ls.root, flatTop && ls.flatTop, flatBottom && ls.flatBottom, className)} {...props }>
|
||||
{children}
|
||||
</Abs>
|
||||
}
|
||||
16
modules/ui/components/AuxWidget.less
Normal file
16
modules/ui/components/AuxWidget.less
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
@border-radius: 3px;
|
||||
|
||||
.root {
|
||||
color: #fff;
|
||||
background-color: rgba(40, 40, 40, 0.95);
|
||||
border: solid 1px #000;
|
||||
border-radius: @border-radius;
|
||||
}
|
||||
|
||||
.flatBottom {
|
||||
border-radius: @border-radius @border-radius 0 0;
|
||||
}
|
||||
|
||||
.flatTop {
|
||||
border-radius: 0 0 @border-radius @border-radius;
|
||||
}
|
||||
16
modules/ui/components/Card.jsx
Normal file
16
modules/ui/components/Card.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function Card({visible, children, ...props}) {
|
||||
return <div style={
|
||||
{
|
||||
display: visible ? 'block' : 'none',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
}
|
||||
} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
16
modules/ui/components/Fa.jsx
Normal file
16
modules/ui/components/Fa.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
export default function Fa({icon, fw, fa, stack, className, ...props}) {
|
||||
let faCss = fa ? fa.map(s => 'fa-' + s) : [];
|
||||
if (icon) {
|
||||
icon = 'fa-' + icon;
|
||||
}
|
||||
if (fw) {
|
||||
faCss.push('fa-fw');
|
||||
}
|
||||
if (stack) {
|
||||
faCss.push('fa-stack-' + stack);
|
||||
}
|
||||
return <i className={ cx('fa', icon, faCss, className) } {...props}/>
|
||||
}
|
||||
12
modules/ui/components/Filler.jsx
Normal file
12
modules/ui/components/Filler.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function Filler({width, height, children, style, ...props}) {
|
||||
return <span style={{
|
||||
display: 'inline-block',
|
||||
width,
|
||||
height,
|
||||
...style
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
}
|
||||
35
modules/ui/components/Folder.jsx
Normal file
35
modules/ui/components/Folder.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
|
||||
import ls from './Folder.less'
|
||||
import Fa from "./Fa";
|
||||
|
||||
export default class Folder extends React.Component{
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
closed: null
|
||||
}
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
let {closable, defaultClosed} = this.props;
|
||||
if (!closable) return false;
|
||||
return closable && (this.state.closed === null ? defaultClosed : this.state.closed)
|
||||
}
|
||||
|
||||
tweakClose = () => {
|
||||
this.setState({closed: !this.isClosed()});
|
||||
};
|
||||
|
||||
render() {
|
||||
let {title, closable, children} = this.props;
|
||||
return <div className={ls.root}>
|
||||
<div className={ls.title} onClick={closable ? this.tweakClose : null}>
|
||||
<Fa fw icon={this.isClosed() ? 'chevron-right' : 'chevron-down'}/>
|
||||
{title}
|
||||
</div>
|
||||
{!this.isClosed() && children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
16
modules/ui/components/Folder.less
Normal file
16
modules/ui/components/Folder.less
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.root {
|
||||
color: #eee;
|
||||
font: 11px 'Lucida Grande', sans-serif;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
overflow: hidden;
|
||||
padding: 0 4px 0 5px;
|
||||
border-bottom: 1px solid #2c2c2c;
|
||||
|
||||
padding-left: 16px;
|
||||
|
||||
}
|
||||
13
modules/ui/components/ImgIcon.jsx
Normal file
13
modules/ui/components/ImgIcon.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function ImgIcon({url, size, style, ...props}) {
|
||||
return <span style={{
|
||||
display: 'inline-block',
|
||||
backgroundImage: 'url('+url+')',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: `${size}px ${size}px`,
|
||||
width: size + 'px',
|
||||
height: size + 'px',
|
||||
...style
|
||||
}} {...props} />
|
||||
};
|
||||
50
modules/ui/components/Menu.jsx
Normal file
50
modules/ui/components/Menu.jsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ls from './Menu.less';
|
||||
import AuxWidget from "./AuxWidget";
|
||||
import cx from 'classnames';
|
||||
|
||||
export default function Menu({children, visible, x, y, orientationUp, style, ...props}) {
|
||||
return <AuxWidget
|
||||
className={cx(ls.root, 'disable-selection')}
|
||||
style={{
|
||||
display: visible ? 'block' : 'none',
|
||||
...style
|
||||
}}
|
||||
left={x}
|
||||
top={orientationUp ? undefined : y}
|
||||
bottom={orientationUp ? y : undefined}
|
||||
{...props}>
|
||||
{children}
|
||||
</AuxWidget>;
|
||||
}
|
||||
|
||||
export function MenuSeparator() {
|
||||
return <div className={ls.separator} />
|
||||
}
|
||||
|
||||
|
||||
export function MenuItem({icon, label, hotKey, style, disabled, onClick}, {closeAllUpPopups}) {
|
||||
|
||||
if (hotKey) {
|
||||
hotKey = hotKey.replace(/\s/g, '');
|
||||
if (hotKey.length > 15) {
|
||||
hotKey = null;
|
||||
}
|
||||
}
|
||||
let clickHandler = disabled ? undefined : () => {
|
||||
closeAllUpPopups();
|
||||
onClick();
|
||||
};
|
||||
return <div className={cx(ls.item, disabled && ls.disabled)}
|
||||
onMouseDown={e => e.stopPropagation()} style={style} onClick={clickHandler}>
|
||||
{icon}
|
||||
<span className={ls.label}>{label}</span>
|
||||
{hotKey && <span className={ls.hotKey}>{hotKey}</span>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
MenuItem.contextTypes = {
|
||||
closeAllUpPopups: PropTypes.func
|
||||
};
|
||||
48
modules/ui/components/Menu.less
Normal file
48
modules/ui/components/Menu.less
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
@import '../styles/theme';
|
||||
|
||||
.root {
|
||||
padding: 0.45em 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 0.45em 0.55em 0.45em 0.45em;
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&:hover {
|
||||
background-color: #0074D9;
|
||||
}
|
||||
&:active {
|
||||
background-color: #000d7f;
|
||||
}
|
||||
|
||||
&.disabled:hover, &.disbaled:active {
|
||||
background-color: #545454;
|
||||
}
|
||||
|
||||
|
||||
& .hotKey {
|
||||
color: @font-color-suppressed;
|
||||
font-size: 0.8em;
|
||||
padding-left: 1.5em;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
& .label {
|
||||
padding-left: 0.45em;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: solid 1px #777;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: @font-color-suppressed;
|
||||
}
|
||||
|
||||
0
modules/ui/components/Row.jsx
Normal file
0
modules/ui/components/Row.jsx
Normal file
35
modules/ui/components/TabSwitcher.less
Normal file
35
modules/ui/components/TabSwitcher.less
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@import "../styles/theme";
|
||||
|
||||
.root {
|
||||
color: @font-color-suppressed;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@border: 1px solid @border-color;
|
||||
|
||||
padding: 0.3em 0.3em 0.4em 0.6em;
|
||||
cursor: pointer;
|
||||
border-right: @border;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
color: @font-color;
|
||||
}
|
||||
&.active {
|
||||
background-color: @bg-color-alt;
|
||||
color: @font-color;
|
||||
}
|
||||
&:first-child {
|
||||
border-left: @border;
|
||||
}
|
||||
}
|
||||
|
||||
.expand:hover {
|
||||
color: green;
|
||||
}
|
||||
.close:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
21
modules/ui/components/TabSwticher.jsx
Normal file
21
modules/ui/components/TabSwticher.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import Fa from 'ui/components/Fa';
|
||||
import cx from 'classnames';
|
||||
|
||||
import ls from './TabSwitcher.less';
|
||||
|
||||
export default function TabSwitcher({children, className}) {
|
||||
|
||||
return <div className={cx(ls.root, className, 'disable-selection')}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
export function Tab({id, label, active, closable, onSwitch}) {
|
||||
return <span className={cx(ls.tab, active && ls.active)} onClick={() => onSwitch(id)}>
|
||||
{label}
|
||||
{closable && <Fragment>
|
||||
<Fa fw icon='expand' className={ls.expand} /> <Fa fw icon='close' className={ls.close} />
|
||||
</Fragment>}
|
||||
</span>;
|
||||
}
|
||||
16
modules/ui/components/Toolbar.jsx
Normal file
16
modules/ui/components/Toolbar.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import ls from './Toolbar.less';
|
||||
|
||||
export default function Toolbar({children, className, ...props}) {
|
||||
return <div className={cx(`${ls.root} disable-selection compact-font`, className)} {...props}>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function ToolbarButton({children, disabled}) {
|
||||
return <div className={cx(ls.button, {disabled})}>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
36
modules/ui/components/Toolbar.less
Normal file
36
modules/ui/components/Toolbar.less
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@import "../styles/theme";
|
||||
|
||||
.root {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
padding: 0.3em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8em;
|
||||
padding: 0.3em 0.1em;
|
||||
color: #555;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
color: #aaa;
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
48
modules/ui/components/Window.jsx
Normal file
48
modules/ui/components/Window.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ls from './Window.less'
|
||||
import Fa from "./Fa";
|
||||
|
||||
export default class Window extends React.Component {
|
||||
|
||||
constructor({initWidth}) {
|
||||
super();
|
||||
this.state = {
|
||||
width: initWidth
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {children, title, minimizable } = this.props;
|
||||
return <div className={ls.root} style={this.getStyle()}>
|
||||
<div className={ls.bar}>
|
||||
{title}
|
||||
<div className={ls.controlButtons}>
|
||||
{minimizable && <span className={ls.button}>_</span>}
|
||||
<span className={ls.button}><Fa icon='close' /></span>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
getStyle() {
|
||||
return {
|
||||
width: toPx(this.state.width),
|
||||
height: toPx(this.state.height),
|
||||
left: toPx(this.state.left),
|
||||
top: toPx(this.state.top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window.defaultProps = {
|
||||
minimizable: false,
|
||||
};
|
||||
|
||||
function toPx(val) {
|
||||
return val === undefined ? undefined : val + 'px';
|
||||
}
|
||||
|
||||
8
modules/ui/components/Window.less
Normal file
8
modules/ui/components/Window.less
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.root {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
9
modules/ui/components/controls/Button.jsx
Normal file
9
modules/ui/components/controls/Button.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import ls from './Button.less'
|
||||
|
||||
export default function Button({text}) {
|
||||
|
||||
return <span>{text}</span>
|
||||
|
||||
}
|
||||
0
modules/ui/components/controls/Button.less
Normal file
0
modules/ui/components/controls/Button.less
Normal file
9
modules/ui/components/controls/ButtonGroup.jsx
Normal file
9
modules/ui/components/controls/ButtonGroup.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import ls from './ButtonGroup.less'
|
||||
|
||||
export default function ButtonGroup({children}) {
|
||||
|
||||
return <div>{children}</div>
|
||||
|
||||
}
|
||||
0
modules/ui/components/controls/ButtonGroup.less
Normal file
0
modules/ui/components/controls/ButtonGroup.less
Normal file
12
modules/ui/components/controls/Field.jsx
Normal file
12
modules/ui/components/controls/Field.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import ls from './Label.less'
|
||||
|
||||
export default function Field({children}) {
|
||||
|
||||
return <div className={ls.root}>
|
||||
{children[0]} {children[1]}
|
||||
</div>;
|
||||
|
||||
|
||||
}
|
||||
0
modules/ui/components/controls/Field.less
Normal file
0
modules/ui/components/controls/Field.less
Normal file
8
modules/ui/components/controls/Label.jsx
Normal file
8
modules/ui/components/controls/Label.jsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ls from './Label.less'
|
||||
|
||||
export default function Label({children}) {
|
||||
return <span>{children}</span>
|
||||
}
|
||||
0
modules/ui/components/controls/Label.less
Normal file
0
modules/ui/components/controls/Label.less
Normal file
61
modules/ui/components/controls/NumberControl.jsx
Normal file
61
modules/ui/components/controls/NumberControl.jsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ls from './NumberControl.less'
|
||||
|
||||
export default class NumberControl extends React.Component{
|
||||
|
||||
render() {
|
||||
let {initValue, } = this.props;
|
||||
|
||||
return <div className={ls.root}>
|
||||
<input type='text' defaultValue={initValue}
|
||||
ref={(input) => this.input = input}
|
||||
onWheel={this.onWheel}
|
||||
onChange={e => onChange(e.target.value)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
onWheel = (e) => {
|
||||
let {baseStep, round, min, max, onChange, accelerator} = this.props;
|
||||
let delta = 0;
|
||||
if ( e.wheelDelta ) { // WebKit / Opera / Explorer 9
|
||||
delta = e.wheelDelta;
|
||||
} else if ( e.detail ) { // Firefox
|
||||
delta = - e.detail;
|
||||
}
|
||||
let val = e.target.value;
|
||||
if (!val) val = 0;
|
||||
let step = baseStep * (e.shiftKey ? accelerator : 1);
|
||||
val = parseFloat(val) + (delta < 0 ? -step : step);
|
||||
if (min !== undefined && val < min) {
|
||||
val = min;
|
||||
}
|
||||
if (max !== undefined && val > max) {
|
||||
val = max;
|
||||
}
|
||||
if (round !== 0) {
|
||||
val = val.toFixed(round);
|
||||
}
|
||||
this.input.value = val;
|
||||
onChange(val);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
NumberControl.propTypes = {
|
||||
baseStep: PropTypes.number,
|
||||
round: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
accelerator: PropTypes.number,
|
||||
initValue: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
NumberControl.defaultProps = {
|
||||
baseStep: 1,
|
||||
round: 0,
|
||||
accelerator: 100
|
||||
};
|
||||
0
modules/ui/components/controls/NumberControl.less
Normal file
0
modules/ui/components/controls/NumberControl.less
Normal file
82
modules/ui/connect.jsx
Normal file
82
modules/ui/connect.jsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function connect(tokens, WrappedComponent, staticProps, mapper, dispatchMapper) {
|
||||
|
||||
if (!Array.isArray(tokens)) {
|
||||
tokens = [tokens];
|
||||
}
|
||||
|
||||
mapper = createMapper(mapper);
|
||||
|
||||
dispatchMapper = dispatchMapper || function(dispatch) {
|
||||
return dispatch;
|
||||
};
|
||||
|
||||
return class StateConnector extends React.Component {
|
||||
|
||||
constructor(context) {
|
||||
super();
|
||||
this.mounted = false;
|
||||
this.stateProps = {};
|
||||
this.dispatchProps = dispatchMapper(this.dispatch);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.externalStateConnection = this.context.bus.connectToState(tokens, this.setExternalState);
|
||||
this.externalStateConnection();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.context.bus.disconnectFromState(this.externalStateConnection);
|
||||
}
|
||||
|
||||
setExternalState = (state) => {
|
||||
this.stateProps = mapper(state);
|
||||
if (this.mounted) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
dispatch = (event, data) => {
|
||||
this.context.bus.dispatch(event, data);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.stateProps} {...this.dispatchProps} {...staticProps} />
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
bus: PropTypes.object
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createMapper(mapper) {
|
||||
if (!mapper) {
|
||||
return function (state) {
|
||||
let props = {};
|
||||
state.forEach(stateItem => Object.assign(props, stateItem));
|
||||
return props;
|
||||
};
|
||||
} else if (Array.isArray(mapper)) {
|
||||
return function (state) {
|
||||
let props = {};
|
||||
for (let i = 0; i < state.length; i++) {
|
||||
let stateItem = state[i];
|
||||
let mapperItem = mapper[i];
|
||||
Object.assign(props, mapperItem ? mapperItem(stateItem) : stateItem)
|
||||
}
|
||||
return props;
|
||||
};
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
|
||||
|
||||
|
||||
7
modules/ui/genId.js
Normal file
7
modules/ui/genId.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
let COUNTER = 0;
|
||||
const PREFIX = 'id_';
|
||||
|
||||
export default function genId() {
|
||||
return `${PREFIX}_${COUNTER++}`;
|
||||
}
|
||||
1
modules/ui/styles/index.less
Normal file
1
modules/ui/styles/index.less
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import "constants";
|
||||
28
modules/ui/styles/init/main.less
Normal file
28
modules/ui/styles/init/main.less
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
@import "../theme";
|
||||
|
||||
body {
|
||||
background-color: @bg-color;
|
||||
color: @font-color;
|
||||
font: 11px 'Lucida Grande', sans-serif;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
:global(.disable-selection) {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
:global(.compact-font) {
|
||||
font-family: 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
20
modules/ui/styles/theme.less
Normal file
20
modules/ui/styles/theme.less
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@bg-color: #000;
|
||||
@bg-color-alt: #1a1a1a;
|
||||
|
||||
@font-color: #eee;
|
||||
@font-color-minor: #ccc;
|
||||
|
||||
@font-color-suppressed: #aaa;
|
||||
@font-color-disabled: #888;
|
||||
|
||||
@border-color: #2c2c2c;
|
||||
|
||||
@work-area-color: #808080;
|
||||
|
||||
@work-area-control-bar-bg-color: rgba(0, 0, 0, 0.5);
|
||||
@work-area-control-bar-bg-color-active: #555;
|
||||
@work-area-control-bar-font-color: @font-color-minor;
|
||||
|
||||
|
||||
//@work-area-toolbar-bg-color: ;
|
||||
//@work-area-toolbar-font-color: ;
|
||||
0
modules/ui/wizard/declarativeWizard.js
Normal file
0
modules/ui/wizard/declarativeWizard.js
Normal file
|
|
@ -48,6 +48,7 @@
|
|||
"dependencies": {
|
||||
"react": "16.2.0",
|
||||
"react-dom": "16.2.0",
|
||||
"prop-types": "15.6.0",
|
||||
"clipper-lib": "6.2.1",
|
||||
"diff-match-patch": "1.0.0",
|
||||
"earcut": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './core-actions'
|
||||
export * from './operation-actions'
|
||||
export * from './history-actions'
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import * as ActionHelpers from './action-helpers'
|
||||
|
||||
export const EditFace = {
|
||||
cssIcons: ['file-picture-o'],
|
||||
label: 'sketch',
|
||||
icon96: 'img/3d/face-edit96.png',
|
||||
info: 'open sketcher for a face/plane',
|
||||
listens: ['selection_face'],
|
||||
update: ActionHelpers.checkForSelectedFaces(1),
|
||||
invoke: (app) => app.editFace()
|
||||
};
|
||||
|
||||
export const Save = {
|
||||
cssIcons: ['floppy-o'],
|
||||
label: 'save',
|
||||
info: 'save project to storage',
|
||||
invoke: (app) => app.save()
|
||||
};
|
||||
|
||||
export const StlExport = {
|
||||
cssIcons: ['upload', 'flip-vertical'],
|
||||
label: 'STL Export',
|
||||
info: 'export model to STL file',
|
||||
invoke: (app) => app.stlExport()
|
||||
};
|
||||
|
||||
export const RefreshSketches = {
|
||||
cssIcons: ['refresh'],
|
||||
label: 'Refresh Sketches',
|
||||
info: 'refresh all visible sketches',
|
||||
invoke: (app) => app.refreshSketches()
|
||||
};
|
||||
|
||||
export const DeselectAll = {
|
||||
cssIcons: ['square-o'],
|
||||
label: 'deselect all',
|
||||
info: 'deselect everything',
|
||||
invoke: (app) => app.viewer.selectionMgr.deselectAll()
|
||||
};
|
||||
|
||||
export const ToggleCameraMode = {
|
||||
cssIcons: ['video-camera'],
|
||||
label: 'toggle camera',
|
||||
info: 'switch camera mode between perspective and orthographic',
|
||||
invoke: (app) => {
|
||||
app.context.services.viewer.toggleCamera();
|
||||
app.context.services.viewer.render();
|
||||
}
|
||||
};
|
||||
|
||||
export const Info = {
|
||||
cssIcons: ['info-circle'],
|
||||
label: 'info',
|
||||
info: 'opens help dialog',
|
||||
invoke: (app) => app.showInfo()
|
||||
};
|
||||
|
||||
export const Donate = {
|
||||
cssIcons: ['paypal'],
|
||||
label: 'donate',
|
||||
info: 'open paypal donate page',
|
||||
invoke: (app, e) => window.open('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=WADW7V7CC32CY&lc=US&item_name=web%2dcad%2eorg¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted', '_blank')
|
||||
};
|
||||
|
||||
export const GitHub = {
|
||||
cssIcons: ['github'],
|
||||
label: 'GitHub',
|
||||
info: 'open GitHub project page',
|
||||
invoke: (app, e) => window.open('https://github.com/xibyte/jsketcher', '_blank')
|
||||
};
|
||||
|
||||
export const ShowSketches = {
|
||||
type: 'binary',
|
||||
property: 'showSketches',
|
||||
cssIcons: ['image'],
|
||||
label: 'show sketches',
|
||||
info: 'toggle whether to show sketches on a solid face'
|
||||
};
|
||||
|
||||
export const LookAtSolid = {
|
||||
cssIcons: ['crosshairs'],
|
||||
label: 'look at solid',
|
||||
info: 'position camera at the solid at zoom to fit it',
|
||||
invoke: (app, e) => app.lookAtSolid(app.inputManager.context.attr('data-id'))
|
||||
};
|
||||
|
||||
export const noIcon = {
|
||||
label: 'no icon'
|
||||
};
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
export const SetHistoryPointer = {
|
||||
label: 'set history',
|
||||
info: 'set history pointer to this modification item',
|
||||
invoke: (app) => {
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
app.craft.historyPointer = mIndex;
|
||||
}
|
||||
};
|
||||
|
||||
export const OpenHistoryWizard = {
|
||||
label: 'edit operation',
|
||||
info: 'open wizard to change parameters of this operation',
|
||||
invoke: (app) => {
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
if (mIndex != app.craft.historyPointer) {
|
||||
app.craft.historyPointer = mIndex;
|
||||
} else {
|
||||
const modification = app.craft.history[mIndex];
|
||||
app.ui.createWizardForOperation(modification);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const EditOperationSketch = {
|
||||
cssIcons: ['image'],
|
||||
label: 'sketch',
|
||||
info: 'edit the sketch assigned to this operation',
|
||||
invoke: (app) => {
|
||||
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
const modification = app.craft.history[mIndex];
|
||||
if (!modification.face) {
|
||||
return;
|
||||
}
|
||||
if (mIndex != app.craft.historyPointer) {
|
||||
app.craft.historyPointer = mIndex;
|
||||
}
|
||||
const face = app.findFace(modification.face);
|
||||
app.sketchFace(face);
|
||||
}
|
||||
};
|
||||
|
||||
export const RemoveModification = {
|
||||
label: 'remove modification',
|
||||
info: 'remove this modification',
|
||||
invoke: (app) => {
|
||||
if (!confirm("This modification and all following modifications will be removed. Continue?")) {
|
||||
return;
|
||||
}
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
app.craft.remove(mIndex);
|
||||
}
|
||||
};
|
||||
|
||||
function modificationIndex(app) {
|
||||
return app.inputManager.context.data('modification')
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import * as Operations from '../craft/operations'
|
||||
import * as ActionHelpers from './action-helpers'
|
||||
|
||||
function mergeInfo(opName, action) {
|
||||
const op = Operations[opName];
|
||||
action.label = op.label;
|
||||
action.icon32 = op.icon + '32.png';
|
||||
action.icon96 = op.icon + '96.png';
|
||||
action.invoke = (app) => app.ui.initOperation(opName);
|
||||
return action;
|
||||
}
|
||||
|
||||
export const CUT = mergeInfo('CUT', {
|
||||
info: 'makes a cut based on 2D sketch'
|
||||
});
|
||||
|
||||
export const EXTRUDE = mergeInfo('EXTRUDE', {
|
||||
info: 'extrudes 2D sketch'
|
||||
});
|
||||
|
||||
export const REVOLVE = mergeInfo('REVOLVE', {
|
||||
info: 'revolve 2D sketch'
|
||||
});
|
||||
|
||||
export const SHELL = mergeInfo('SHELL', {
|
||||
info: 'makes shell using borders'
|
||||
});
|
||||
|
||||
export const BOX = mergeInfo('BOX', {
|
||||
info: 'creates new object box'
|
||||
});
|
||||
|
||||
export const PLANE = mergeInfo('PLANE', {
|
||||
info: 'creates new object plane'
|
||||
});
|
||||
|
||||
export const SPHERE = mergeInfo('SPHERE', {
|
||||
info: 'creates new object sphere'
|
||||
});
|
||||
|
||||
export const INTERSECTION = mergeInfo('INTERSECTION', {
|
||||
info: 'intersection operation on two solids'
|
||||
});
|
||||
|
||||
export const DIFFERENCE = mergeInfo('DIFFERENCE', {
|
||||
info: 'difference operation on two solids'
|
||||
});
|
||||
|
||||
export const UNION = mergeInfo('UNION', {
|
||||
info: 'union operation on two solids'
|
||||
});
|
||||
|
||||
export const IMPORT_STL = mergeInfo('IMPORT_STL', {
|
||||
info: 'import stl from external location'
|
||||
});
|
||||
|
||||
requiresFaceSelection(CUT, 1);
|
||||
requiresFaceSelection(EXTRUDE, 1);
|
||||
requiresFaceSelection(REVOLVE, 1);
|
||||
|
||||
requiresSolidSelection(INTERSECTION, 2);
|
||||
requiresSolidSelection(DIFFERENCE, 2);
|
||||
requiresSolidSelection(UNION, 2);
|
||||
|
||||
function requiresFaceSelection(action, amount) {
|
||||
action.listens = ['selection_face'];
|
||||
action.update = ActionHelpers.checkForSelectedFaces(amount)
|
||||
}
|
||||
|
||||
function requiresSolidSelection(action, amount) {
|
||||
action.listens = ['selection_face'];
|
||||
action.update = ActionHelpers.checkForSelectedSolids(amount)
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React, {Fragment} from 'react';
|
||||
|
||||
export default class WebApplication extends React.Component {
|
||||
|
||||
render() {
|
||||
return <Fragment>
|
||||
<div className='app-tab-view' id='view-3d'>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}>
|
||||
<div id='right-panel'/>
|
||||
<div id='viewer-container'/>
|
||||
<div id='control-bar'>
|
||||
<div className='left-group'>
|
||||
</div>
|
||||
<div className='right-group'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tab-switcher'/>
|
||||
<a id='downloader' style={{display: 'none'}}/>
|
||||
</Fragment>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import WebApplication from './WebApplication';
|
||||
|
||||
export default function startReact(callback) {
|
||||
return ReactDOM.render(
|
||||
<WebApplication />,
|
||||
document.getElementById('app'),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
export const file = {
|
||||
label: 'file',
|
||||
cssIcons: ['file'],
|
||||
actions: ['Save', 'StlExport', '-', 'IMPORT_STL']
|
||||
};
|
||||
|
||||
export const craft = {
|
||||
label: 'craft',
|
||||
cssIcons: ['magic'],
|
||||
info: 'set of available craft operations on a solid',
|
||||
actions: ['EXTRUDE', 'CUT', 'REVOLVE', 'SHELL']
|
||||
};
|
||||
|
||||
export const primitives = {
|
||||
label: 'add',
|
||||
cssIcons: ['cube', 'plus'],
|
||||
info: 'set of available solid creation operations',
|
||||
actions: ['PLANE', 'BOX', 'SPHERE']
|
||||
};
|
||||
|
||||
export const boolean = {
|
||||
label: 'bool',
|
||||
cssIcons: ['pie-chart'],
|
||||
info: 'set of available boolean operations',
|
||||
actions: ['INTERSECTION', 'DIFFERENCE', 'UNION']
|
||||
};
|
||||
|
||||
export const main = {
|
||||
label: 'start',
|
||||
cssIcons: ['rocket'],
|
||||
info: 'common set of actions',
|
||||
actions: ['EXTRUDE', 'CUT', 'SHELL', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-',
|
||||
'EditFace', '-', 'DeselectAll', 'RefreshSketches']
|
||||
};
|
||||
|
||||
export const SolidContext = {
|
||||
label: 'solid-context',
|
||||
info: 'solid context actions',
|
||||
actions: ['LookAtSolid']
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ import {Face} from './topo/face';
|
|||
import {Loop} from './topo/loop';
|
||||
import {Edge} from './topo/edge';
|
||||
import {Vertex} from './topo/vertex';
|
||||
import {normalOfCCWSeq} from '../3d/cad-utils';
|
||||
import {normalOfCCWSeq} from '../cad/cad-utils';
|
||||
import BBox from "../math/bbox";
|
||||
|
||||
export default class BrepBuilder {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {NurbsSurface, NurbsCurve} from './geom/impl/nurbs'
|
|||
import {Plane} from './geom/impl/plane'
|
||||
import {Point} from './geom/point'
|
||||
import {BasisForPlane, Matrix3} from '../math/l3space'
|
||||
import * as cad_utils from '../3d/cad-utils'
|
||||
import * as cad_utils from '../cad/cad-utils'
|
||||
import * as math from '../math/math'
|
||||
import mergeNullFace from './null-face-merge'
|
||||
import {invert} from './operations/boolean'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {Point} from './geom/point'
|
|||
import {Plane} from './geom/impl/plane'
|
||||
import {createPrism, enclose} from './brep-enclose'
|
||||
import {AXIS, Matrix3} from '../math/l3space'
|
||||
import {Circle} from '../3d/craft/sketch/sketch-model'
|
||||
import {Circle} from '../cad/craft/sketch/sketch-model'
|
||||
|
||||
export function box(w, h, d, tr) {
|
||||
const wh = w * 0.5;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
:global
|
||||
.brep-debugger {
|
||||
|
||||
& .strike {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {Face} from '../topo/face';
|
|||
import {Vertex} from '../topo/vertex';
|
||||
import Vector from 'math/vector';
|
||||
import {isCCW} from '../../math/math';
|
||||
import PIP from '../../3d/tess/pip';
|
||||
import PIP from '../../cad/tess/pip';
|
||||
|
||||
export function evolveFace(originFace, loops) {
|
||||
let out = [];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {TriangulateFace} from '../../3d/tess/triangulation'
|
||||
import {TriangulateFace} from '../../cad/tess/triangulation'
|
||||
import {Shell} from '../topo/shell'
|
||||
import {HalfEdge} from '../topo/edge'
|
||||
import {Loop} from '../topo/loop'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {TopoObject} from './topo-object'
|
||||
import {Loop} from './loop'
|
||||
import PIP from '../../3d/tess/pip';
|
||||
import PIP from '../../cad/tess/pip';
|
||||
import {NurbsCurve} from "../geom/impl/nurbs";
|
||||
import {eqSqTol, veq, veqNeg} from "../geom/tolerance";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export function checkForSelectedFaces(amount) {
|
||||
return (state, app) => {
|
||||
state.enabled = app.getFaceSelection().length >= amount;
|
||||
return (state, context) => {
|
||||
state.enabled = context.services.selection.face().length >= amount;
|
||||
if (!state.enabled) {
|
||||
state.hint = amount === 1 ? 'requires a face to be selected' : 'requires ' + amount + ' faces to be selected';
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ export function checkForSelectedFaces(amount) {
|
|||
}
|
||||
|
||||
export function checkForSelectedSolids(amount) {
|
||||
return (state, app) => {
|
||||
state.enabled = app.getFaceSelection().length >= amount;
|
||||
return (state, context) => {
|
||||
state.enabled = context.services.selection.face().length >= amount;
|
||||
if (!state.enabled) {
|
||||
state.hint = amount === 1 ? 'requires a solid to be selected' : 'requires ' + amount + ' solids to be selected';
|
||||
}
|
||||
8
web/app/cad/actions/actionRef.jsx
Normal file
8
web/app/cad/actions/actionRef.jsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export function toIdAndOverrides(ref) {
|
||||
if (Array.isArray(ref)) {
|
||||
return ref;
|
||||
} else {
|
||||
return [ref, undefined]
|
||||
}
|
||||
}
|
||||
63
web/app/cad/actions/actionSystemPlugin.js
Normal file
63
web/app/cad/actions/actionSystemPlugin.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import {createToken} from 'bus';
|
||||
|
||||
export function activate(context) {
|
||||
|
||||
let {bus} = context;
|
||||
|
||||
function run(id, data) {
|
||||
bus.dispatch(TOKENS.actionRun(id), data);
|
||||
}
|
||||
|
||||
function register(action) {
|
||||
bus.enableState(TOKENS.actionAppearance(action.id), action.appearance);
|
||||
|
||||
let stateToken = TOKENS.actionState(action.id);
|
||||
let initialState = {
|
||||
hint: '',
|
||||
enabled: true,
|
||||
visible: true
|
||||
};
|
||||
if (action.update) {
|
||||
action.update(initialState, context);
|
||||
}
|
||||
bus.enableState(stateToken, initialState);
|
||||
|
||||
if (action.update && action.listens) {
|
||||
|
||||
const stateUpdater = () => {
|
||||
bus.updateState(stateToken, (actionState) => {
|
||||
actionState.hint = '';
|
||||
actionState.enabled = true;
|
||||
actionState.visible = true;
|
||||
action.update(actionState, context);
|
||||
return actionState;
|
||||
});
|
||||
};
|
||||
|
||||
for (let event of action.listens) {
|
||||
bus.subscribe(event, stateUpdater);
|
||||
}
|
||||
}
|
||||
bus.subscribe(TOKENS.actionRun(action.id), (data) => action.invoke(context, data));
|
||||
}
|
||||
|
||||
function registerAction(action) {
|
||||
register(action);
|
||||
}
|
||||
|
||||
function registerActions(actions) {
|
||||
actions.forEach(action => register(action));
|
||||
}
|
||||
|
||||
context.services.action = {run, registerAction, registerActions}
|
||||
}
|
||||
|
||||
export const TOKENS = {
|
||||
ACTION_STATE_NS: 'action.state',
|
||||
ACTION_APPEARANCE_NS: 'action.appearance',
|
||||
ACTION_RUN_NS: 'action.run',
|
||||
|
||||
actionState: (actionId) => createToken(TOKENS.ACTION_STATE_NS, actionId),
|
||||
actionAppearance: (actionId) => createToken(TOKENS.ACTION_APPEARANCE_NS, actionId),
|
||||
actionRun: (actionId) => createToken(TOKENS.ACTION_RUN_NS, actionId),
|
||||
};
|
||||
130
web/app/cad/actions/coreActions.js
Normal file
130
web/app/cad/actions/coreActions.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import * as ActionHelpers from './action-helpers'
|
||||
|
||||
export default [
|
||||
{
|
||||
id: 'EditFace',
|
||||
appearance: {
|
||||
cssIcons: ['file-picture-o'],
|
||||
label: 'sketch',
|
||||
icon96: 'img/cad/face-edit96.png',
|
||||
info: 'open sketcher for a face/plane',
|
||||
},
|
||||
listens: ['selection_face'],
|
||||
update: ActionHelpers.checkForSelectedFaces(1),
|
||||
invoke: (context) => context.services.sketcher.editFace(),
|
||||
},
|
||||
|
||||
{
|
||||
id: 'Save',
|
||||
appearance: {
|
||||
cssIcons: ['floppy-o'],
|
||||
label: 'save',
|
||||
info: 'save project to storage',
|
||||
},
|
||||
invoke: (context) => context.services.project.save()
|
||||
},
|
||||
|
||||
{
|
||||
id: 'StlExport',
|
||||
appearance: {
|
||||
cssIcons: ['upload', 'flip-vertical'],
|
||||
label: 'STL Export',
|
||||
info: 'export model to STL file',
|
||||
},
|
||||
invoke: (context) => context.services.project.stlExport()
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
id: 'RefreshSketches',
|
||||
appearance: {
|
||||
cssIcons: ['refresh'],
|
||||
label: 'Refresh Sketches',
|
||||
info: 'refresh all visible sketches',
|
||||
},
|
||||
invoke: (context) => context.services.sketcher.refreshSketches()
|
||||
},
|
||||
|
||||
{
|
||||
id: 'DeselectAll',
|
||||
appearance: {
|
||||
cssIcons: ['square-o'],
|
||||
label: 'deselect all',
|
||||
info: 'deselect everything',
|
||||
},
|
||||
invoke: (context) => context.services.selection.deselectAll()
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ToggleCameraMode',
|
||||
appearance: {
|
||||
cssIcons: ['video-camera'],
|
||||
label: 'toggle camera',
|
||||
info: 'switch camera mode between perspective and orthographic',
|
||||
},
|
||||
invoke: (app) => {
|
||||
let viewer = app.context.services.viewer;
|
||||
viewer.toggleCamera();
|
||||
viewer.render();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'Info',
|
||||
appearance: {
|
||||
cssIcons: ['info-circle'],
|
||||
label: 'info',
|
||||
info: 'opens help dialog',
|
||||
},
|
||||
invoke: (context) => context.services.help.showInfo()
|
||||
},
|
||||
|
||||
{
|
||||
id: 'Donate',
|
||||
appearance: {
|
||||
cssIcons: ['paypal'],
|
||||
label: 'donate',
|
||||
info: 'open paypal donate page',
|
||||
},
|
||||
invoke: (context) => window.open('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=WADW7V7CC32CY&lc=US&item_name=web%2dcad%2eorg¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted', '_blank')
|
||||
},
|
||||
|
||||
{
|
||||
id: 'GitHub',
|
||||
appearance: {
|
||||
cssIcons: ['github'],
|
||||
label: 'GitHub',
|
||||
info: 'open GitHub project page',
|
||||
},
|
||||
invoke: (context) => window.open('https://github.com/xibyte/jsketcher', '_blank')
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ShowSketches',
|
||||
type: 'binary',
|
||||
property: 'showSketches',
|
||||
appearance: {
|
||||
cssIcons: ['image'],
|
||||
label: 'show sketches',
|
||||
info: 'toggle whether to show sketches on a solid face'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'LookAtSolid',
|
||||
appearance: {
|
||||
cssIcons: ['crosshairs'],
|
||||
label: 'look at solid',
|
||||
info: 'position camera at the solid at zoom to fit it',
|
||||
},
|
||||
invoke: (context) => app.lookAtSolid(app.inputManager.context.attr('data-id'))
|
||||
},
|
||||
|
||||
{
|
||||
id: 'noIcon',
|
||||
appearance: {
|
||||
label: 'no icon'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
68
web/app/cad/actions/historyActions.js
Normal file
68
web/app/cad/actions/historyActions.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
export default [
|
||||
{
|
||||
id: 'SetHistoryPointer',
|
||||
appearance: {
|
||||
label: 'set history',
|
||||
info: 'set history pointer to this modification item',
|
||||
},
|
||||
invoke: (app) => {
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
app.craft.historyPointer = mIndex;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OpenHistoryWizard',
|
||||
appearance: {
|
||||
label: 'edit operation',
|
||||
info: 'open wizard to change parameters of this operation',
|
||||
},
|
||||
invoke: (app) => {
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
if (mIndex != app.craft.historyPointer) {
|
||||
app.craft.historyPointer = mIndex;
|
||||
} else {
|
||||
const modification = app.craft.history[mIndex];
|
||||
app.ui.createWizardForOperation(modification);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'EditOperationSketch',
|
||||
appearance: {
|
||||
cssIcons: ['image'],
|
||||
label: 'sketch',
|
||||
info: 'edit the sketch assigned to this operation',
|
||||
},
|
||||
invoke: (app) => {
|
||||
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
const modification = app.craft.history[mIndex];
|
||||
if (!modification.face) {
|
||||
return;
|
||||
}
|
||||
if (mIndex != app.craft.historyPointer) {
|
||||
app.craft.historyPointer = mIndex;
|
||||
}
|
||||
const face = app.findFace(modification.face);
|
||||
app.sketchFace(face);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'RemoveModification',
|
||||
appearance: {
|
||||
label: 'remove modification',
|
||||
info: 'remove this modification',
|
||||
},
|
||||
invoke: (app) => {
|
||||
if (!confirm("This modification and all following modifications will be removed. Continue?")) {
|
||||
return;
|
||||
}
|
||||
const mIndex = parseInt(modificationIndex(app));
|
||||
app.craft.remove(mIndex);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function modificationIndex(app) {
|
||||
return app.inputManager.context.data('modification')
|
||||
}
|
||||
105
web/app/cad/actions/operationActions.js
Normal file
105
web/app/cad/actions/operationActions.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import * as Operations from '../craft/operations'
|
||||
import * as ActionHelpers from './action-helpers'
|
||||
|
||||
const OPERATION_ACTIONS = [
|
||||
{
|
||||
id: 'CUT',
|
||||
appearance: {
|
||||
info: 'makes a cut based on 2D sketch',
|
||||
},
|
||||
...requiresFaceSelection(1)
|
||||
},
|
||||
{
|
||||
id: 'EXTRUDE',
|
||||
appearance: {
|
||||
info: 'extrudes 2D sketch',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'REVOLVE',
|
||||
appearance: {
|
||||
info: 'revolve 2D sketch',
|
||||
},
|
||||
...requiresFaceSelection(1)
|
||||
},
|
||||
{
|
||||
id: 'SHELL',
|
||||
appearance: {
|
||||
info: 'makes shell using borders',
|
||||
},
|
||||
...requiresFaceSelection(1)
|
||||
},
|
||||
{
|
||||
id: 'BOX',
|
||||
appearance: {
|
||||
info: 'creates new object box'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PLANE',
|
||||
appearance: {
|
||||
info: 'creates new object plane'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SPHERE',
|
||||
appearance: {
|
||||
info: 'creates new object sphere'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INTERSECTION',
|
||||
appearance: {
|
||||
info: 'intersection operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'DIFFERENCE',
|
||||
appearance: {
|
||||
info: 'difference operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'UNION',
|
||||
appearance: {
|
||||
info: 'union operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'IMPORT_STL',
|
||||
appearance: {
|
||||
info: 'import stl from external location'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function mergeInfo(action) {
|
||||
const op = Operations[action.id];
|
||||
action.invoke = app => app.ui.initOperation(action.id);
|
||||
Object.assign(action.appearance, {
|
||||
label: op.label,
|
||||
icon32: op.icon + '32.png',
|
||||
icon96: op.icon + '96.png',
|
||||
});
|
||||
}
|
||||
|
||||
OPERATION_ACTIONS.forEach(action => mergeInfo(action));
|
||||
|
||||
function requiresFaceSelection(amount) {
|
||||
return {
|
||||
listens: ['selection_face'],
|
||||
update: ActionHelpers.checkForSelectedFaces(amount)
|
||||
}
|
||||
}
|
||||
|
||||
function requiresSolidSelection(amount) {
|
||||
return {
|
||||
listens: ['selection_face'],
|
||||
update: ActionHelpers.checkForSelectedSolids(amount)
|
||||
}
|
||||
}
|
||||
|
||||
export default OPERATION_ACTIONS;
|
||||
|
|
@ -1,39 +1,39 @@
|
|||
import {MESH_OPERATIONS} from './mesh/workbench'
|
||||
import {Extrude, Cut} from './brep/cut-extrude'
|
||||
import {Revolve} from './brep/revolve'
|
||||
import {BREPSceneSolid} from '../scene/wrappers/brepSceneObject'
|
||||
import {PlaneSceneObject} from '../scene/wrappers/planeSceneObject'
|
||||
import {box} from '../../brep/brep-primitives'
|
||||
// import {MESH_OPERATIONS} from './mesh/workbench'
|
||||
// import {Extrude, Cut} from './brep/cut-extrude'
|
||||
// import {Revolve} from './brep/revolve'
|
||||
// import {BREPSceneSolid} from '../scene/wrappers/brepSceneObject'
|
||||
// import {PlaneSceneObject} from '../scene/wrappers/planeSceneObject'
|
||||
// import {box} from '../../brep/brep-primitives'
|
||||
|
||||
export const CUT = {
|
||||
icon: 'img/3d/cut',
|
||||
icon: 'img/cad/cut',
|
||||
label: 'Cut',
|
||||
info: (p) => '(' + r(p.value) + ')',
|
||||
action: (app, params) => Cut(app, params)
|
||||
};
|
||||
|
||||
export const EXTRUDE = {
|
||||
icon: 'img/3d/extrude',
|
||||
icon: 'img/cad/extrude',
|
||||
label: 'Extrude',
|
||||
info: (p) => '(' + r(p.value) + ')',
|
||||
action: (app, params) => Extrude(app, params)
|
||||
};
|
||||
|
||||
export const REVOLVE = {
|
||||
icon: 'img/3d/revolve',
|
||||
icon: 'img/cad/revolve',
|
||||
label: 'Revolve',
|
||||
info: (p) => '(' + p.angle + ')',
|
||||
action: (app, params) => Revolve(app, params)
|
||||
};
|
||||
|
||||
export const SHELL = {
|
||||
icon: 'img/3d/shell',
|
||||
icon: 'img/cad/shell',
|
||||
label: 'Shell',
|
||||
info: (p) => '(' + p.d + ')'
|
||||
};
|
||||
|
||||
export const BOX = {
|
||||
icon: 'img/3d/cube',
|
||||
icon: 'img/cad/cube',
|
||||
label: 'Box',
|
||||
info: (p) => '(' + p.width + ', ' + p.height + ', ' + p.depth + ')',
|
||||
action: (app, request) => {
|
||||
|
|
@ -45,7 +45,7 @@ export const BOX = {
|
|||
};
|
||||
|
||||
export const PLANE = {
|
||||
icon: 'img/3d/plane',
|
||||
icon: 'img/cad/plane',
|
||||
label: 'Plane',
|
||||
info: (p) => '(' + p.depth + ')',
|
||||
action: (app, request) => {
|
||||
|
|
@ -57,7 +57,7 @@ export const PLANE = {
|
|||
};
|
||||
|
||||
export const SPHERE = {
|
||||
icon: 'img/3d/sphere',
|
||||
icon: 'img/cad/sphere',
|
||||
label: 'Sphere',
|
||||
info: (p) => '(' + p.radius + ')',
|
||||
action: (app, request) => {
|
||||
|
|
@ -66,25 +66,25 @@ export const SPHERE = {
|
|||
};
|
||||
|
||||
export const INTERSECTION = {
|
||||
icon: 'img/3d/intersection',
|
||||
icon: 'img/cad/intersection',
|
||||
label: 'Intersection',
|
||||
info: (p) => null
|
||||
};
|
||||
|
||||
export const DIFFERENCE = {
|
||||
icon: 'img/3d/difference',
|
||||
icon: 'img/cad/difference',
|
||||
label: 'Difference',
|
||||
info: (p) => null
|
||||
};
|
||||
|
||||
export const UNION = {
|
||||
icon: 'img/3d/union',
|
||||
icon: 'img/cad/union',
|
||||
label: 'Union',
|
||||
info: (p) => null
|
||||
};
|
||||
|
||||
export const IMPORT_STL = {
|
||||
icon: 'img/3d/stl',
|
||||
icon: 'img/cad/stl',
|
||||
label: 'STL Import',
|
||||
info: (p) => '(' + p.url.substring(p.url.lastIndexOf('/') + 1 ) + ')',
|
||||
action: (app, request) => {
|
||||
23
web/app/cad/dom/components/ControlBar.jsx
Normal file
23
web/app/cad/dom/components/ControlBar.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
import ls from './ControlBar.less';
|
||||
import cx from 'classnames';
|
||||
|
||||
export default function ControlBar({left, right}) {
|
||||
|
||||
return <div className={ls.root}>
|
||||
<div className={ls.left}>
|
||||
{right}
|
||||
</div>
|
||||
<div className={ls.right}>
|
||||
{left}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ControlBarButton({onClick, onElement, disabled, children}) {
|
||||
return <span className={cx(ls.button, 'disable-selection', {disabled})}
|
||||
onClick={disabled || onClick} ref={onElement}>
|
||||
{children}
|
||||
</span>
|
||||
}
|
||||
36
web/app/cad/dom/components/ControlBar.less
Normal file
36
web/app/cad/dom/components/ControlBar.less
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@import "~ui/styles/theme";
|
||||
|
||||
.root {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: @work-area-control-bar-bg-color;
|
||||
color: @work-area-control-bar-font-color;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 0.3em 0.3em 0.4em 0.6em;
|
||||
|
||||
@border: 1px solid @border-color;
|
||||
&.left {
|
||||
border-right: @border;
|
||||
}
|
||||
&.right {
|
||||
border-left: @border;
|
||||
}
|
||||
&:hover {
|
||||
background-color: @work-area-control-bar-bg-color-active;
|
||||
}
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
color: @font-color-suppressed;
|
||||
&:hover {
|
||||
background-color: @work-area-control-bar-bg-color;
|
||||
}
|
||||
}
|
||||
10
web/app/cad/dom/components/ObjectExplorer.jsx
Normal file
10
web/app/cad/dom/components/ObjectExplorer.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class ObjectExplorer extends React.Component {
|
||||
|
||||
render() {
|
||||
return <div >
|
||||
ObjectExplorer
|
||||
</div>
|
||||
}
|
||||
}
|
||||
10
web/app/cad/dom/components/OperationHistory.jsx
Normal file
10
web/app/cad/dom/components/OperationHistory.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class OperationHistory extends React.Component {
|
||||
|
||||
render() {
|
||||
return <div >
|
||||
OperationHistory
|
||||
</div>
|
||||
}
|
||||
}
|
||||
56
web/app/cad/dom/components/PlugableControlBar.jsx
Normal file
56
web/app/cad/dom/components/PlugableControlBar.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import ControlBar, {ControlBarButton} from './ControlBar';
|
||||
import connect from 'ui/connect';
|
||||
import Fa from 'ui/components/Fa';
|
||||
import {TOKENS as UI_TOKENS} from '../uiEntryPointsPlugin';
|
||||
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
|
||||
import {toIdAndOverrides} from "../../actions/actionRef";
|
||||
|
||||
|
||||
export default function PlugableControlBar() {
|
||||
return <ControlBar left={<LeftGroup />} right={<RightGroup />}/>;
|
||||
}
|
||||
|
||||
function ButtonGroup({actions}) {
|
||||
return actions.map(actionRef => {
|
||||
let [id, overrides] = toIdAndOverrides(actionRef);
|
||||
let actionRunToken = ACTION_TOKENS.actionRun(id);
|
||||
let Comp = connect([ACTION_TOKENS.actionAppearance(id), ACTION_TOKENS.actionState(id)],
|
||||
ActionButton, {actionId: id},
|
||||
([appearance, state]) => Object.assign({}, appearance, state, overrides),
|
||||
dispatch => ({runAction: (data) => dispatch(actionRunToken, data)})
|
||||
);
|
||||
return <Comp key={id}/>;
|
||||
});
|
||||
}
|
||||
|
||||
function isMenuAction(actionId) {
|
||||
return actionId.startsWith('menu.');
|
||||
}
|
||||
|
||||
class ActionButton extends React.Component {
|
||||
|
||||
render() {
|
||||
let {label, cssIcons, runAction, enabled, visible, actionId} = this.props;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
const onClick = e => runAction(isMenuAction(actionId) ? getMenuData(this.el) : undefined);
|
||||
return <ControlBarButton {...{onClick, disabled: !enabled}} onElement={el => this.el = el}>
|
||||
{cssIcons && <Fa fa={cssIcons} fw/>} {label}
|
||||
</ControlBarButton>;
|
||||
}
|
||||
}
|
||||
|
||||
const LeftGroup = connect(UI_TOKENS.CONTROL_BAR_LEFT, ButtonGroup, undefined, ([actions]) => ({actions}));
|
||||
const RightGroup = connect(UI_TOKENS.CONTROL_BAR_RIGHT, ButtonGroup, undefined, ([actions]) => ({actions}));
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
44
web/app/cad/dom/components/PlugableToolbar.jsx
Normal file
44
web/app/cad/dom/components/PlugableToolbar.jsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import connect from 'ui/connect';
|
||||
import Fa from 'ui/components/Fa';
|
||||
import {TOKENS as UI_TOKENS} from '../uiEntryPointsPlugin';
|
||||
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
|
||||
import Toolbar, {ToolbarButton} from "../../../../../modules/ui/components/Toolbar";
|
||||
import ImgIcon from "../../../../../modules/ui/components/ImgIcon";
|
||||
import {toIdAndOverrides} from "../../actions/actionRef";
|
||||
|
||||
|
||||
function ConfigurableToolbar({actions, small}) {
|
||||
|
||||
return <Toolbar>
|
||||
{actions.map(actionRef => {
|
||||
let [id, overrides] = toIdAndOverrides(actionRef);
|
||||
let Comp = connect(
|
||||
[ACTION_TOKENS.actionAppearance(id), ACTION_TOKENS.actionState(id)],
|
||||
ActionButton, {small, },
|
||||
([appearance, state]) => Object.assign({}, appearance, state, overrides));
|
||||
return <Comp key={id}/>
|
||||
})}
|
||||
</Toolbar>
|
||||
}
|
||||
|
||||
function ActionButton({label, icon96, cssIcons, small, enabled, visible, onClick}) {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let icon = small ? <Fa fa={cssIcons} fw/> : <ImgIcon url={icon96} size={48} />;
|
||||
|
||||
return <ToolbarButton {...{onClick, disabled: !enabled}}>
|
||||
{icon}
|
||||
{!small && <div>{label}</div>}
|
||||
</ToolbarButton>
|
||||
}
|
||||
|
||||
export function createPlugableToolbar(configToken, small) {
|
||||
return connect(configToken, ConfigurableToolbar, {small}, ([actions]) => ({actions}) );
|
||||
}
|
||||
|
||||
export const PlugableToolbarLeft = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_LEFT);
|
||||
export const PlugableToolbarLeftSecondary = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_LEFT_SECONDARY);
|
||||
export const PlugableToolbarRight = createPlugableToolbar(UI_TOKENS.TOOLBAR_BAR_RIGHT, true);
|
||||
63
web/app/cad/dom/components/View3d.jsx
Normal file
63
web/app/cad/dom/components/View3d.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PlugableControlBar from './PlugableControlBar';
|
||||
|
||||
import ls from './View3d.less';
|
||||
import ObjectExplorer from './ObjectExplorer';
|
||||
import OperationHistory from './OperationHistory';
|
||||
import Toolbar, {ToolbarButton} from 'ui/components/Toolbar';
|
||||
import ImgIcon from 'ui/components/ImgIcon';
|
||||
import Fa from 'ui/components/Fa';
|
||||
import Abs from 'ui/components/Abs';
|
||||
import {PlugableToolbarLeft, PlugableToolbarLeftSecondary, PlugableToolbarRight} from "./PlugableToolbar";
|
||||
import MenuHolder from "../menu/MenuHolder";
|
||||
import {TOKENS as MENU_TOKENS} from '../menu/menuPlugin';
|
||||
|
||||
|
||||
export default class View3d extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
return <div className={ls.root} onMouseDown={this.closeAllUpPopups}>
|
||||
<MenuHolder />
|
||||
<div className={ls.sideBar}>
|
||||
<ObjectExplorer/>
|
||||
<OperationHistory/>
|
||||
</div>
|
||||
<div className={ls.viewer} id='viewer-container'>
|
||||
{/*<div className={ls.viewer} */}
|
||||
<div>
|
||||
</div>
|
||||
<Abs left='2em' top='2em' className={ls.leftToolbarGroup}>
|
||||
<PlugableToolbarLeft />
|
||||
<PlugableToolbarLeftSecondary />
|
||||
</Abs>
|
||||
<Abs right='2em' top='2em'>
|
||||
<PlugableToolbarRight className={ls.smallToolbar}/>
|
||||
</Abs>
|
||||
<PlugableControlBar />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
closeAllUpPopups = () => {
|
||||
let openedMenus = this.context.bus.state[MENU_TOKENS.OPENED];
|
||||
if (openedMenus && openedMenus.length !== 0) {
|
||||
this.context.bus.dispatch(MENU_TOKENS.CLOSE_ALL);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
closeAllUpPopups: this.closeAllUpPopups
|
||||
}
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
bus: PropTypes.object
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
closeAllUpPopups: PropTypes.func
|
||||
};
|
||||
}
|
||||
23
web/app/cad/dom/components/View3d.less
Normal file
23
web/app/cad/dom/components/View3d.less
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
|
||||
.leftToolbarGroup > * {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
.smallToolbar > * {
|
||||
font-size: 2em;
|
||||
}
|
||||
88
web/app/cad/dom/components/WebApplication.jsx
Normal file
88
web/app/cad/dom/components/WebApplication.jsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'ui/styles/init/minireset.css';
|
||||
import 'ui/styles/init/main.less';
|
||||
import '../../../../css/app3d-legacy.less';
|
||||
|
||||
import View3d from './View3d';
|
||||
|
||||
import WindowSystem from 'ui/WindowSystem';
|
||||
import Window from "ui/components/Window";
|
||||
import Folder from "ui/components/Folder";
|
||||
import Field from "ui/components/controls/Field";
|
||||
import Label from "ui/components/controls/Label";
|
||||
import NumberControl from "ui/components/controls/NumberControl";
|
||||
import ButtonGroup from "ui/components/controls/ButtonGroup";
|
||||
import Button from "ui/components/controls/Button";
|
||||
|
||||
import ls from './WebApplication.less';
|
||||
import TabSwitcher, {Tab} from 'ui/components/TabSwticher';
|
||||
import Card from 'ui/components/Card';
|
||||
|
||||
const DEFAULT_VIEW = {id: 'view3d', label: '3D View', Component: View3d};
|
||||
|
||||
export default class WebApplication extends React.Component {
|
||||
|
||||
constructor({bus}) {
|
||||
super();
|
||||
this.bus = bus;
|
||||
this.views = [DEFAULT_VIEW, {id: 'XXX', label: '3D View2', Component: Fragment}];
|
||||
this.state = {
|
||||
activeView: DEFAULT_VIEW
|
||||
};
|
||||
}
|
||||
|
||||
switchTab = (viewId) => {
|
||||
this.setState({activeView: this.views.find(v => v.id === viewId)});
|
||||
};
|
||||
|
||||
render() {
|
||||
let activeView = this.state.activeView;
|
||||
return <div className={ls.root}>
|
||||
|
||||
<div className={ls.content}>
|
||||
{this.views.map(({id, Component}) => <Card key={id} visible={id === activeView.id}>
|
||||
<Component />
|
||||
</Card>)}
|
||||
</div>
|
||||
|
||||
<TabSwitcher className={ls.contentSwitcher}>
|
||||
{this.views.map(({label, id}) => <Tab id={id} label={label} active={id === activeView.id}
|
||||
key={id}
|
||||
closable={id !== DEFAULT_VIEW}
|
||||
onSwitch={this.switchTab} />)}
|
||||
</TabSwitcher>
|
||||
<a id='downloader' style={{display: 'none'}}/>
|
||||
{/*<WindowSystem /> */}
|
||||
{/*<Window initWith={250} >*/}
|
||||
{/*<Folder title="Test">*/}
|
||||
{/*<Field>*/}
|
||||
{/*<Label>Width</Label>*/}
|
||||
{/*<NumberControl initValue={5} onChange={val => console.log(val)}/>*/}
|
||||
{/*</Field>*/}
|
||||
{/*<Field>*/}
|
||||
{/*<Label>Width</Label>*/}
|
||||
{/*<NumberControl initValue={6} onChange={val => console.log(val)}/>*/}
|
||||
{/*</Field>*/}
|
||||
{/*<ButtonGroup>*/}
|
||||
{/*<Button text='Cancel' />*/}
|
||||
{/*<Button text='OK' />*/}
|
||||
{/*</ButtonGroup>*/}
|
||||
{/*</Folder>*/}
|
||||
{/*</Window>*/}
|
||||
</div>
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {bus: this.bus};
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
bus: PropTypes.object
|
||||
};
|
||||
}
|
||||
|
||||
function render(Component) {
|
||||
return <Component />;
|
||||
}
|
||||
21
web/app/cad/dom/components/WebApplication.less
Normal file
21
web/app/cad/dom/components/WebApplication.less
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@import "~ui/styles/theme";
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contentSwitcher {
|
||||
border-top: 1px solid @border-color;
|
||||
}
|
||||
66
web/app/cad/dom/menu/MenuHolder.jsx
Normal file
66
web/app/cad/dom/menu/MenuHolder.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import connect from 'ui/connect';
|
||||
import {TOKENS as MENU_TOKENS} from './menuPlugin';
|
||||
import {TOKENS as ACTION_TOKENS} from '../../actions/actionSystemPlugin';
|
||||
import Menu, {MenuItem, MenuSeparator} from "../../../../../modules/ui/components/Menu";
|
||||
import Fa from "../../../../../modules/ui/components/Fa";
|
||||
import Filler from "../../../../../modules/ui/components/Filler";
|
||||
import {TOKENS as KeyboardTokens} from "../../keyboard/keyboardPlugin";
|
||||
|
||||
function MenuHolder({menus}) {
|
||||
return menus.map(({id, actions}) => {
|
||||
let menuToken = MENU_TOKENS.menuState(id);
|
||||
return React.createElement(connect([menuToken, KeyboardTokens.KEYMAP],
|
||||
ActionMenu, {actions}, [,keymap => ({keymap})]), {key: id});
|
||||
});
|
||||
}
|
||||
|
||||
function ActionMenu({actions, keymap, ...props}) {
|
||||
return <Menu {...props}>
|
||||
{actions.map((action, index)=> {
|
||||
if (action === '-') {
|
||||
return <MenuSeparator key={index} />
|
||||
}
|
||||
const runToken = ACTION_TOKENS.actionRun(action);
|
||||
return React.createElement(
|
||||
connect([ACTION_TOKENS.actionState(action), ACTION_TOKENS.actionAppearance(action)],
|
||||
ActionMenuItem,
|
||||
{hotKey: keymap[action]}, undefined,
|
||||
dispatch => ({
|
||||
onClick: () => dispatch(runToken)
|
||||
})),
|
||||
{key: action});
|
||||
})}
|
||||
</Menu>;
|
||||
}
|
||||
|
||||
function ActionMenuItem({label, cssIcons, icon32, icon96, onClick, enabled, hotKey, visible}) {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
let icon, style;
|
||||
if (icon32 || icon96) {
|
||||
let size = 16;
|
||||
icon = <Filler width={size} height='1.18em'/>;
|
||||
style = {
|
||||
backgroundImage: `url(${icon32 || icon96})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: `${size}px ${size}px`,
|
||||
backgroundPositionY: 6,
|
||||
backgroundPositionX: 5,
|
||||
};
|
||||
if (!enabled) {
|
||||
style.filter = 'grayscale(90%)';
|
||||
}
|
||||
} else if (cssIcons) {
|
||||
icon = <Fa fw fa={cssIcons} />;
|
||||
}
|
||||
|
||||
return <MenuItem {...{label, icon, style, disabled: !enabled, hotKey, onClick}} />;
|
||||
}
|
||||
|
||||
|
||||
export default connect(MENU_TOKENS.MENUS, MenuHolder, undefined, ([menus]) => ({menus}));
|
||||
|
||||
|
||||
|
||||
50
web/app/cad/dom/menu/menuPlugin.js
Normal file
50
web/app/cad/dom/menu/menuPlugin.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import {createToken} from 'bus';
|
||||
|
||||
export function activate({bus, services}) {
|
||||
|
||||
bus.enableState(TOKENS.MENUS, []);
|
||||
bus.enableState(TOKENS.OPENED, []);
|
||||
|
||||
function registerMenus(menus) {
|
||||
let menusToAdd = [];
|
||||
let showMenuActions = [];
|
||||
menus.forEach(({id, actions, ...appearance}) => {
|
||||
let stateToken = TOKENS.menuState(id);
|
||||
bus.enableState(stateToken, {
|
||||
visible: false,
|
||||
orientationUp: false,
|
||||
x: undefined,
|
||||
y: undefined
|
||||
});
|
||||
if (!appearance) {
|
||||
appearance.label = id;
|
||||
}
|
||||
showMenuActions.push({
|
||||
id: 'menu.' + id,
|
||||
appearance,
|
||||
invoke: (ctx, hints) => bus.updateStates([stateToken, TOKENS.OPENED],
|
||||
([state, opened]) => [Object.assign(state, {visible: true}, hints), [id, ...opened]]
|
||||
)
|
||||
});
|
||||
|
||||
menusToAdd.push({id, actions});
|
||||
});
|
||||
services.action.registerActions(showMenuActions);
|
||||
bus.updateState(TOKENS.MENUS, menus => [...menus, ...menusToAdd]);
|
||||
}
|
||||
|
||||
bus.subscribe(TOKENS.CLOSE_ALL, () => {
|
||||
bus.state[TOKENS.OPENED].forEach(openedMenu => bus.setState(TOKENS.menuState(openedMenu), {visible: false}));
|
||||
bus.updateState(TOKENS.OPENED, () => []);
|
||||
});
|
||||
|
||||
services.menu = { registerMenus }
|
||||
}
|
||||
|
||||
export const TOKENS = {
|
||||
menuState: id => createToken('menu', 'state', id),
|
||||
MENUS: createToken('menus'),
|
||||
CLOSE_ALL: createToken('menus', 'closeAll'),
|
||||
OPENED: createToken('menus', 'opened')
|
||||
};
|
||||
|
||||
11
web/app/cad/dom/startReact.jsx
Normal file
11
web/app/cad/dom/startReact.jsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import WebApplication from './components/WebApplication';
|
||||
|
||||
export default function startReact(bus, callback) {
|
||||
return ReactDOM.render(
|
||||
<WebApplication bus={bus} />,
|
||||
document.getElementById('app'),
|
||||
callback
|
||||
);
|
||||
}
|
||||
25
web/app/cad/dom/uiEntryPointsPlugin.js
Normal file
25
web/app/cad/dom/uiEntryPointsPlugin.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import {createToken} from 'bus';
|
||||
|
||||
|
||||
export function activate({bus}) {
|
||||
|
||||
bus.enableState(TOKENS.CONTROL_BAR_LEFT, []);
|
||||
bus.enableState(TOKENS.CONTROL_BAR_RIGHT, []);
|
||||
|
||||
bus.enableState(TOKENS.TOOLBAR_BAR_LEFT, []);
|
||||
bus.enableState(TOKENS.TOOLBAR_BAR_LEFT_SECONDARY, []);
|
||||
bus.enableState(TOKENS.TOOLBAR_BAR_RIGHT, []);
|
||||
|
||||
}
|
||||
|
||||
const NS = 'ui.config';
|
||||
|
||||
export const TOKENS = {
|
||||
CONTROL_BAR_LEFT: createToken(NS, 'controlBar.left'),
|
||||
CONTROL_BAR_RIGHT: createToken(NS, 'controlBar.right'),
|
||||
|
||||
TOOLBAR_BAR_LEFT: createToken(NS, 'toolbar.left'),
|
||||
TOOLBAR_BAR_LEFT_SECONDARY: createToken(NS, 'toolbar.left.secondary'),
|
||||
TOOLBAR_BAR_RIGHT: createToken(NS, 'toolbar.right'),
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue