React transition

This commit is contained in:
Val Erastov 2018-01-10 00:21:05 -08:00
parent 95d4b96bc7
commit fdc52ec85d
181 changed files with 1871 additions and 506 deletions

View file

@ -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('.');
}

View 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});
}
}

View 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>;
}
}

View 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>
}

View 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;
}

View 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>
}

View 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}/>
}

View 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>
}

View 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>
}
}

View 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;
}

View 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} />
};

View 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
};

View 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;
}

View file

View 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;
}

View 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>;
}

View 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>;
}

View 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;
}
}

View 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';
}

View file

@ -0,0 +1,8 @@
.root {
position: absolute;
}
.bar {
display: flex;
justify-content: space-between;
}

View file

@ -0,0 +1,9 @@
import React from 'react';
import ls from './Button.less'
export default function Button({text}) {
return <span>{text}</span>
}

View file

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

View 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>;
}

View 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>
}

View 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
};

82
modules/ui/connect.jsx Normal file
View 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
View file

@ -0,0 +1,7 @@
let COUNTER = 0;
const PREFIX = 'id_';
export default function genId() {
return `${PREFIX}_${COUNTER++}`;
}

View file

@ -0,0 +1 @@
@import "constants";

View 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;
}

View 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: ;

View file

View 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",

View file

@ -1,3 +0,0 @@
export * from './core-actions'
export * from './operation-actions'
export * from './history-actions'

View file

@ -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&currency_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'
};

View file

@ -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')
}

View file

@ -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)
}

View file

@ -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>
}
}

View file

@ -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
);
}

View file

@ -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']
};

View file

@ -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 {

View file

@ -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'

View file

@ -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;

View file

@ -1,3 +1,4 @@
:global
.brep-debugger {
& .strike {

View file

@ -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 = [];

View file

@ -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'

View file

@ -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 {

View file

@ -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';
}

View file

@ -0,0 +1,8 @@
export function toIdAndOverrides(ref) {
if (Array.isArray(ref)) {
return ref;
} else {
return [ref, undefined]
}
}

View 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),
};

View 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&currency_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'
}
}
]

View 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')
}

View 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;

View file

@ -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) => {

View 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>
}

View 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;
}
}

View file

@ -0,0 +1,10 @@
import React from 'react';
export default class ObjectExplorer extends React.Component {
render() {
return <div >
ObjectExplorer
</div>
}
}

View file

@ -0,0 +1,10 @@
import React from 'react';
export default class OperationHistory extends React.Component {
render() {
return <div >
OperationHistory
</div>
}
}

View 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
};
}

View 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);

View 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
};
}

View 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;
}

View 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 />;
}

View 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;
}

View 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}));

View 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')
};

View 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
);
}

View 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