model attributes

This commit is contained in:
Val Erastov 2022-03-08 20:00:00 -08:00
parent a37d7dc3f8
commit ed5b40878d
46 changed files with 874 additions and 134 deletions

View file

@ -0,0 +1,22 @@
import {state, StateStream} from "lstream/index";
export class LazyStreams<T> {
index = new Map<string, StateStream<T>>();
proto: (id: string) => T;
constructor(proto?: (id: string) => T) {
this.proto = proto || ((id: string) => null);
}
get(id: string) {
let state$: StateStream<T> = this.index[id];
if (state$ == null) {
state$ = state<T>(this.proto(id));
this.index[id] = state$;
}
return state$;
}
}

View file

@ -4,16 +4,18 @@ import cx from 'classnames';
import ButtonGroup from "ui/components/controls/ButtonGroup";
import Button from "ui/components/controls/Button";
export function Dialog({children, className, okText, cancelText, onOK, ...props}: WindowProps & {
export function Dialog({children, compact, className, okText, cancelText, onOK, ...props}: WindowProps & {
cancelText?: string,
okText?: string,
onOK?: () => void
onOK?: () => void,
compact?: boolean,
}) {
return <Window className={cx(className, 'dialog')}
return <Window compact={compact}
className={cx(className, 'dialog')}
footer={
<ButtonGroup className='dialog-buttons padded'>
<Button onClick={props.onClose}>{cancelText || 'Cancel'}</Button>
{!compact && <Button onClick={props.onClose}>{cancelText || 'Cancel'}</Button>}
{onOK && <Button type='accent' onClick={onOK}>{okText || 'OK'}</Button>}
</ButtonGroup>
}

View file

@ -1,42 +1,40 @@
import React from 'react';
import React, {useState} from 'react';
import ls from './Folder.less'
import Fa from "./Fa";
import cx from 'classnames';
import cx from "classnames";
export default class Folder extends React.Component{
constructor() {
super();
this.state = {
closed: null
}
}
export function InnerFolder(props) {
isClosed() {
let {closable, defaultClosed} = this.props;
const [closed, setClosed] = useState(null)
function isClosed(){
let {closable, defaultClosed} = props;
if (!closable) return false;
return closable && (this.state.closed === null ? defaultClosed : this.state.closed)
return closable && (closed === null ? defaultClosed : closed)
}
tweakClose = () => {
this.setState({closed: !this.isClosed()});
};
render() {
let {title, closable, className, children} = this.props;
return <div className={cx(ls.root, className)}>
<Title onClick={closable ? this.tweakClose : null} isClosed={this.isClosed()}>{title}</Title>
{!this.isClosed() && children}
</div>
function tweakClose( ) {
setClosed(!isClosed())
}
const {title, titleClass, closable, children} = props;
return <React.Fragment>
<Title onClick={closable ? tweakClose : null} isClosed={isClosed()} className={titleClass}>{title}</Title>
{!isClosed() && children}
</React.Fragment>
}
export function Title({children, isClosed, onClick}) {
export default function Folder({className, ...props}) {
return <div className={className}>
<InnerFolder {...props} />
</div>
}
export function Title({children, isClosed, onClick, className}) {
const titleCss = onClick ? {
cursor: 'pointer'
} : {};
return <div className={ls.title} onClick={onClick} style={titleCss}>
return <div className={cx(ls.title, className)} onClick={onClick} style={titleCss}>
<span className={ls.handle}><Fa fw icon={isClosed ? 'chevron-right' : 'chevron-down'}/></span>
{' '}{children}
</div>;

View file

@ -1,8 +1,5 @@
@import "~ui/styles/theme.less";
.root {
}
.title {
border-bottom: 1px solid @border-color;
background-color: @bg-base-color;

View file

@ -0,0 +1,72 @@
.objectItem {
@itemRadius: 5px;
@alt-color: #9c9c9c;
.button {
cursor: pointer;
&:hover {
background-color: #0074D9;
}
&:active {
background-color: #000d7f;
}
}
background-color: rgba(0,0,0, 0.6);
border: 1px solid #000;
border-radius: 5px;
margin: 2px @itemRadius;
pointer-events: auto;
display: flex;
align-items: stretch;
.expandHandleInactive {
border-radius: @itemRadius 0 0 @itemRadius;
padding: 3px 3px;
}
.expandHandle {
.expandHandleInactive;
.button;
//background-color: @alt-color;
}
.menuButton {
border-radius: 0 @itemRadius @itemRadius 0;
background-color: @alt-color;
padding: 0 3px;
.button;
}
.objectLabel {
display: flex;
align-items: center;
flex: 1;
padding: 0 5px;
.button;
&.selected {
background: linear-gradient(#59acff, #0074D9);
}
}
.onOffButton {
display: flex;
align-items: center;
padding: 0 4px;
}
.onOffButton.on {
cursor: pointer;
background-color: #005e82;
.button;
}
.onOffButton.off {
cursor: pointer;
background-color: #5f5f5f;
.button;
}
}

View file

@ -0,0 +1,58 @@
import ls from "./GenericExplorer.less";
import React, {useState} from 'react';
import cx from 'classnames';
import {AiFillCaretDown, AiFillCaretRight} from "react-icons/ai";
import {BsDot} from "react-icons/bs";
interface GenericExplorerControlProps {
title: string;
onClick: any;
on: boolean;
children: any;
}
export function GenericExplorerControl(props: GenericExplorerControlProps) {
return <span onClick={props.onClick} title={props.title}
className={cx(ls.onOffButton, ls[ props.on ? 'on' : 'off' ])}>{props.children}</span>
}
interface GenericExplorerNodeProps {
children: any;
controls: any;
label: any;
selected: boolean;
defaultExpanded?: boolean;
expandable: boolean;
select: any;
}
export function GenericExplorerNode(props: GenericExplorerNodeProps) {
const [expanded, setExpanded] = useState(!!props.defaultExpanded);
const expandable = props.expandable;
const toggle = expandable ? (() => setExpanded(expanded => !expanded)) : undefined;
return <>
<div className={ls.objectItem}>
<span className={expandable ? ls.expandHandle : ls.expandHandleInactive} onClick={toggle}>
{expandable ? (expanded ? <AiFillCaretDown/> : <AiFillCaretRight/>)
: <BsDot/>}
</span>
{props.controls}
<span onClick={props.select}
className={cx(ls.objectLabel, props.selected && ls.selected)}>
{props.label}
</span>
<span className={ls.menuButton}>...</span>
</div>
{expanded && <div style={{paddingLeft: 10}}>
{props.children}
</div>}
</>;
}

View file

@ -0,0 +1,25 @@
.titleBar {
background: rgba(0,0,0,0.7);
color: white;
text-transform: uppercase;
padding: 3px 5px;
pointer-events: auto;
flex: 0 0 ;
margin-top: 4px;
}
.scrollableArea {
overflow-y: auto;
flex: 1 1;
pointer-events: auto;
}
.delineation {
background: rgba(0,0,0,0.5);
color: white;
padding: 0;
pointer-events: auto;
flex: 0 0 ;
margin-top: 2px;
}

View file

@ -0,0 +1,26 @@
import React from "react";
import ls from "./SceneInlineSection.less";
interface SceneInlineSectionProps {
title: any;
children: any;
}
export function SceneInlineSection(props: SceneInlineSectionProps) {
return <React.Fragment>
<SceneInlineTitleBar>{props.title}</SceneInlineTitleBar>
<div className={ls.scrollableArea}>
{props.children}
</div>
</React.Fragment>
}
export function SceneInlineTitleBar({children, ...props}) {
return <div className={ls.titleBar} {...props}>{children}</div>;
}
export function SceneInlineDelineation({children, ...props}) {
return <div className={ls.delineation} {...props}>{children}</div>;
}

View file

@ -1,9 +1,10 @@
import React from 'react';
import ls from './Stack.less'
import cx from "classnames";
export default function Stack(props) {
return <div className={ls.root} {...props} />
export default function Stack({className, ...props}) {
return <div className={cx(ls.root, className)} {...props} />
}

View file

@ -6,3 +6,8 @@
padding: 3px 6px;
line-height: 1.7;
}
.root > .root {
padding: 0;
border-bottom: none;
}

View file

@ -12,6 +12,10 @@
pointer-events: auto;
}
.root.compact {
font-size: 11px;
}
.mandatoryBorder {
border: 5px solid @bg-color-4;
}

View file

@ -28,6 +28,7 @@ export interface WindowProps {
onClose: () => void;
props?: JSX.IntrinsicAttributes;
footer?: JSX.Element;
compact?: boolean;
}
export default class Window extends React.Component<WindowProps> {
@ -45,9 +46,9 @@ export default class Window extends React.Component<WindowProps> {
render() {
let {initWidth, initHeight, initLeft, initTop, initRight, initBottom, centerScreen, setFocus, className, resizeCapturingBuffer,
resize, enableResize, children, title, icon, minimizable = false, onClose, controlButtons, footer, ...props} = this.props;
resize, enableResize, children, title, icon, minimizable = false, onClose, controlButtons, footer, compact, ...props} = this.props;
return <div className={cx(ls.root, this.resizeConfig&&ls.mandatoryBorder, className)} {...props} ref={this.keepRef}>
return <div className={cx(ls.root, this.resizeConfig&&ls.mandatoryBorder, compact&&ls.compact, className)} {...props} ref={this.keepRef}>
<div className={ls.bar + ' disable-selection'} onMouseDown={this.startDrag} onMouseUp={this.stopDrag}>
<div className={ls.title}>{icon}<b>{title.toUpperCase()}</b></div>
<div className={ls.controlButtons}>

View file

@ -0,0 +1,50 @@
import React, {useState} from 'react';
import {SketchPicker} from 'react-color';
import Button from "ui/components/controls/Button";
import {CgColorPicker} from "react-icons/cg";
import {Dialog} from "ui/components/Dialog";
import ButtonGroup from "ui/components/controls/ButtonGroup";
import {RiDeleteBack2Line} from "react-icons/ri";
interface ColorControlProps {
value: string;
onChange: any;
dialogTitle?: any
}
export function ColorControl(props: ColorControlProps) {
const [open, setOpen] = useState(false);
const {onChange, value} = props;
const style = value ? { backgroundColor: value } : undefined;
return <React.Fragment>
<ButtonGroup>
<span style={{
display: 'inline-block',
padding: '0 3px',
fontFamily: 'monospace',
...style
}}>{value||<NoValue/>}</span>
<Button onClick={() => onChange(null)}><RiDeleteBack2Line /></Button>
<Button onClick={() => setOpen(true)}><CgColorPicker /></Button>
</ButtonGroup>
{open && <ColorDialog value={value||'white'} onChange={onChange} onClose={() => setOpen(false)} title={props.dialogTitle}/>}
</React.Fragment>
}
function ColorDialog({onChange, value, title, onClose}) {
const change = (color) => {
onChange(color.hex);
}
return <Dialog title={title||'color'} onClose={onClose} compact>
<SketchPicker color={value} onChange={change} onChangeComplete={change}/>
</Dialog>
}
function NoValue() {
return <span style={{fontStyle: 'italic'}}>{'<not set>'}</span>;
}

View file

@ -2,13 +2,14 @@ import {useCallback, useContext, useEffect, useState} from 'react';
import {StreamsContext} from "./streamsContext";
import {ApplicationContext} from "context";
import {Emitter, Stream} from "lstream";
import produce from "immer";
export function useStream<T>(getStream: Stream<T> | ((ctx: ApplicationContext) => Stream<T>)) : T {
const basicStreams = useContext(StreamsContext);
const [state, setState] = useState<{data: T}>();
const stream = resolveStream(getStream, basicStreams);
const stream: Emitter<T> = resolveStream(getStream, basicStreams);
if (!stream) {
console.log(getStream);
@ -41,6 +42,26 @@ export function useStreamWithUpdater<T>(getStream: (ctx: ApplicationContext) =>
}
function resolveStream(getStream, basicStreams) {
export type Patcher<T> = (draft: T) => void;
export function useStreamWithPatcher<T>(getStream: (ctx: ApplicationContext) => Emitter<T>) : [T, (patcher: Patcher<T>) => void] {
const data = useStream(getStream);
const basicStreams = useContext(StreamsContext);
const stream: Emitter<T> = resolveStream(getStream, basicStreams);
const patch = (patcher: Patcher<T>) => {
const newData: T = produce(data, (draft:T) => {
patcher(draft)
})
stream.next(newData);
}
return [data, patch];
}
function resolveStream<T>(getStream, basicStreams): Emitter<T> {
return typeof getStream === 'function' ? getStream(basicStreams) : getStream
}

View file

@ -36,4 +36,15 @@ path {
.warning-text {
color: @on-color-highlight-variant-yellow;
}
}
.bg-color-0 { background-color: @bg-color-0; }
.bg-color-1 { background-color: @bg-color-1; }
.bg-color-2 { background-color: @bg-color-2; }
.bg-color-3 { background-color: @bg-color-3; }
.bg-color-4 { background-color: @bg-color-4; }
.bg-color-5 { background-color: @bg-color-5; }
.bg-color-6 { background-color: @bg-color-6; }
.bg-color-7 { background-color: @bg-color-7; }
.bg-color-8 { background-color: @bg-color-8; }
.bg-color-9 { background-color: @bg-color-9; }

97
package-lock.json generated
View file

@ -22,6 +22,7 @@
"opencascade.js": "^1.1.1",
"prop-types": "15.6.0",
"react": "^16.13.1",
"react-color": "^2.19.3",
"react-dom": "^16.13.1",
"react-icons": "^4.2.0",
"react-toastify": "^5.5.0",
@ -8915,6 +8916,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -9173,6 +9179,11 @@
"node": ">= 8.16.2"
}
},
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -11110,6 +11121,31 @@
"node": ">=0.10.0"
}
},
"node_modules/react-color": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"dependencies": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-color/node_modules/@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@ -11242,6 +11278,14 @@
"loose-envify": "cli.js"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"dependencies": {
"lodash": "^4.0.1"
}
},
"node_modules/read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -13036,6 +13080,14 @@
"node": ">=0.6.0"
}
},
"node_modules/tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -22536,6 +22588,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -22731,6 +22788,11 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-1.0.0.tgz",
"integrity": "sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng=="
},
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -24344,6 +24406,28 @@
}
}
},
"react-color": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
},
"dependencies": {
"@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"requires": {}
}
}
},
"react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@ -24454,6 +24538,14 @@
}
}
},
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"requires": {
"lodash": "^4.0.1"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -25966,6 +26058,11 @@
"setimmediate": "^1.0.4"
}
},
"tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View file

@ -78,6 +78,7 @@
"opencascade.js": "^1.1.1",
"prop-types": "15.6.0",
"react": "^16.13.1",
"react-color": "^2.19.3",
"react-dom": "^16.13.1",
"react-icons": "^4.2.0",
"react-toastify": "^5.5.0",

View file

@ -0,0 +1,32 @@
import {Plugin} from "plugable/pluginSystem";
import {AttributesService} from "cad/attributes/attributesService";
interface AttributesPluginInputContext {
}
export interface AttributesPluginContext {
attributesService: AttributesService;
}
type AttributesPluginWorkingContext = AttributesPluginInputContext&AttributesPluginContext;
declare module 'context' {
interface ApplicationContext extends AttributesPluginContext {}
}
export const AttributesPlugin: Plugin<AttributesPluginInputContext, AttributesPluginContext, AttributesPluginWorkingContext> = {
inputContextSpec: {
},
outputContextSpec: {
attributesService: 'required',
},
activate(ctx: AttributesPluginWorkingContext) {
ctx.attributesService = new AttributesService();
},
}

View file

@ -0,0 +1,19 @@
import {LazyStreams} from "lstream/lazyStreams";
export class AttributesService {
streams = new LazyStreams<ModelAttributes>(id => ({
hidden: false,
label: null,
color: null
}));
}
export interface ModelAttributes {
hidden: boolean;
label: string;
color: string;
}

View file

@ -7,6 +7,7 @@ import {intercept} from "lstream/intercept";
import {CoreContext} from "context";
import {MFace} from "../model/mface";
import {OperationParams} from "cad/craft/schema/schema";
import {clearImplicitModels} from "cad/craft/e0/occCommandInterface";
export function activate(ctx: CoreContext) {
@ -69,6 +70,7 @@ export function activate(ctx: CoreContext) {
}
function runRequest(request): Promise<OperationResult> {
clearImplicitModels();
try {
let op = ctx.operationService.get(request.type);
if (!op) {
@ -90,6 +92,8 @@ export function activate(ctx: CoreContext) {
return result.then ? result : Promise.resolve(result);
} catch (e) {
return Promise.reject(e);
} finally {
clearImplicitModels();
}
}

View file

@ -1,30 +1,40 @@
import React from 'react';
import connect from 'ui/connect';
import {Section} from 'ui/components/Section';
import Fa from 'ui/components/Fa';
import {constant} from 'lstream';
import ls from './ObjectExplorer.less';
import cx from 'classnames';
import {MShell} from '../../model/mshell';
import {MDatum} from '../../model/mdatum';
import {MShell} from 'cad/model/mshell';
import {MDatum} from 'cad/model/mdatum';
import mapContext from 'ui/mapContext';
import decoratorChain from 'ui/decoratorChain';
import {MOpenFaceShell} from "cad/model/mopenFace";
export default connect(streams => streams.craft.models.map(models => ({models})))
(function ObjectExplorer({models}) {
return models.map(m => (m instanceof MShell) ? <ModelSection type='shell' model={m} defaultOpen={true} key={m.id}>
<Section label='faces' defaultOpen={true}>
{
m.faces.map(f => <FaceSection face={f} key={f.id} />)
}
</Section>
<Section label='edges' defaultOpen={true}>
{m.edges.map(e => <EdgeSection edge={e} key={e.id} />)}
</Section>
</ModelSection> : (m instanceof MDatum) ? <ModelSection type='datum' model={m} defaultOpen={true}/> : null);
return <div> {models.map(m => {
if (m instanceof MOpenFaceShell) {
return <OpenFaceSection shell={m}/>
} else if (m instanceof MShell) {
return <ModelSection type='shell' model={m} key={m.id}>
<Section label='faces' defaultOpen={true}>
{
m.faces.map(f => <FaceSection face={f} key={f.id}/>)
}
</Section>
<Section label='edges' defaultOpen={true}>
{m.edges.map(e => <EdgeSection edge={e} key={e.id}/>)}
</Section>
</ModelSection>
} else if (m instanceof MDatum) {
return <ModelSection type='datum' model={m} key={m.id}/>;
} else {
return null;
}
})}
</div>
});
function EdgeSection({edge}) {
@ -36,36 +46,44 @@ function EdgeSection({edge}) {
function FaceSection({face}) {
return <ModelSection type='face' model={face} key={face.id}>
{(face.productionInfo && face.productionInfo.role) && <Section label={<span>role: {face.productionInfo.role}</span>} />}
{(face.productionInfo && face.productionInfo.originatedFromPrimitive) && <Section label={<span>origin: {face.productionInfo.originatedFromPrimitive}</span>} />}
<Section label={face.sketchObjects.length ? 'sketch' : <span className={ls.hint}>{'<no sketch assigned>'}</span>}>
{face.sketchObjects.map(o => <div key={o.id}>{o.id + ':' + o.sketchPrimitive.constructor.name}</div>)}
</Section>
{(face.productionInfo && face.productionInfo.role) &&
<Section label={<span>role: {face.productionInfo.role}</span>}/>}
{(face.productionInfo && face.productionInfo.originatedFromPrimitive) &&
<Section label={<span>origin: {face.productionInfo.originatedFromPrimitive}</span>}/>}
<SketchesList face={face}/>
{face.edges && <Section label='edges' defaultOpen={false}>
{face.edges.map(e => <EdgeSection edge={e} key={e.id}/>)}
</Section>}
</ModelSection>;
}
function SketchesList({face}) {
return <Section
label={face.sketchObjects.length ? 'sketch' : <span className={ls.hint}>{'<no sketch assigned>'}</span>}>
{face.sketchObjects.map(o => <div key={o.id}>{o.id + ':' + o.sketchPrimitive.constructor.name}</div>)}
</Section>;
}
const ModelSection = decoratorChain(
mapContext((ctx, props) => ({
select: () => ctx.services.pickControl.pick(props.model)
})),
connect((streams, props) => (streams.selection[props.type] || constant([])).map(selection => ({selection}))))
(
function ModelSection({model, type, selection, select, ...props}) {
function ModelSection({model, type, typeLabel, selection, select, ...props}) {
let labelClasses = cx(ls.modelLabel, {
[ls.selected]: selection.indexOf(model.id) !== -1
});
let label = <span className={labelClasses}><CommonControls/>
<span onClick={select}>{type} {model.id}</span>
let label = <span className={labelClasses}>
<span onClick={select}>{typeLabel||type} {model.id}</span>
</span>;
return <Section label={label} {...props}/>;
}
);
function CommonControls() {
return <React.Fragment>
<Fa fw icon='crosshairs'/>
</React.Fragment>;
function OpenFaceSection({shell}) {
return <ModelSection type='face' model={shell.face} key={shell.face.id} typeLabel='surface'>
<SketchesList face={shell.face}/>
</ModelSection>;
}

View file

@ -1,9 +1,13 @@
.selected {
background-color: #7d7d7d;
background-color: #f3fb8a;
color: black;
}
.modelLabel {
cursor: pointer;
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.hint {

View file

@ -0,0 +1,152 @@
import React, {useContext, useState} from 'react';
import {MShell} from 'cad/model/mshell';
import {MDatum} from 'cad/model/mdatum';
import {MOpenFaceShell} from "cad/model/mopenFace";
import {useStream, useStreamWithPatcher} from "ui/effects";
import {AppContext} from "cad/dom/components/AppContext";
import {MObject} from "cad/model/mobject";
import {SceneInlineDelineation, SceneInlineSection, SceneInlineTitleBar} from "ui/components/SceneInlineSection";
import {GenericExplorerControl, GenericExplorerNode} from "ui/components/GenericExplorer";
import ls from "cad/craft/ui/ObjectExplorer.less";
import Fa from "ui/components/Fa";
import {AiOutlineEye, AiOutlineEyeInvisible} from "react-icons/ai";
import {ModelAttributes} from "cad/craft/ui/VisibleSwitch";
export function SceneInlineObjectExplorer() {
const models = useStream(ctx => ctx.craftService.models$);
if (!models) {
return null;
}
return <SceneInlineSection title='OBJECTS'> {models.map(m => {
if (m instanceof MOpenFaceShell) {
return <OpenFaceSection shell={m} />
} else if (m instanceof MShell) {
return <ModelSection type='shell' model={m} key={m.id} controlVisibility>
<Section label='faces' defaultOpen={true}>
{
m.faces.map(f => <FaceSection face={f} key={f.id}/>)
}
</Section>
<Section label='edges' defaultOpen={true}>
{m.edges.map(e => <EdgeSection edge={e} key={e.id}/>)}
</Section>
</ModelSection>
} else if (m instanceof MDatum) {
return <ModelSection type='datum' model={m} key={m.id} controlVisibility/>;
} else {
return null;
}
})}
</SceneInlineSection>
}
function EdgeSection({edge}) {
return <ModelSection type='edge' model={edge} key={edge.id}>
{edge.adjacentFaces.map(f => <FaceSection face={f} key={f.id}/>)}
</ModelSection>
}
function FaceSection({face}) {
return <ModelSection type='face' model={face} key={face.id}>
{(face.productionInfo && face.productionInfo.role) &&
<Section label={<span>role: {face.productionInfo.role}</span>}/>}
{(face.productionInfo && face.productionInfo.originatedFromPrimitive) &&
<Section label={<span>origin: {face.productionInfo.originatedFromPrimitive}</span>}/>}
<SketchesList face={face}/>
{face.edges && <Section label='edges' defaultOpen={false}>
{face.edges.map(e => <EdgeSection edge={e} key={e.id}/>)}
</Section>}
</ModelSection>;
}
function SketchesList({face}) {
return <Section
label={face.sketchObjects.length ? 'sketch' : <span className={ls.hint}>{'<no sketch assigned>'}</span>}>
{face.sketchObjects.map(o => <ModelSection
key={o.id}
typeLabel={o.sketchPrimitive.constructor.name}
model={o}
type={'sketchObject'}
expandable={false}
/>)}
</Section>;
}
function ModelSection({model, type, typeLabel, expandable = true, controlVisibility = false, visibilityOf = null, ...props}: {
model: MObject,
visibilityOf?: MObject,
typeLabel?: any,
type: string,
children?: any,
controlVisibility?: boolean,
expandable?: boolean,
}) {
const ctx = useContext(AppContext);
const selection: string[] = useStream(ctx => ctx.streams.selection[type]);
const select = () => ctx.services.pickControl.pick(model);
const selected = selection.indexOf(model.id) !== -1;
let label = <>{typeLabel === undefined ? type:typeLabel} {model.id}</>;
visibilityOf = visibilityOf||model;
return <GenericExplorerNode defaultExpanded={false}
expandable={expandable}
label={label}
selected={selected}
select={select}
controls={
<>
{controlVisibility && <VisibleSwitch modelId={visibilityOf.id}/>}
</>
}>
{props.children}
</GenericExplorerNode>
}
function OpenFaceSection({shell}) {
return <ModelSection type='face' model={shell.face} key={shell.face.id} typeLabel='surface'
controlVisibility
visibilityOf={shell}>
<SketchesList face={shell.face}/>
</ModelSection>;
}
function Section(props) {
const [expanded, setExpanded] = useState(!props.defaultCollapsed);
const tweakClose = () => {
setExpanded(exp => !exp);
};
return <>
<SceneInlineDelineation onClick={tweakClose} style={{cursor: 'pointer'}}>
<Fa fw icon={'caret-' + (expanded ? 'down' : 'right')}/>
<span className={ls.label}>{props.label}</span>
</SceneInlineDelineation>
{expanded && props.children}
</>;
}
export function VisibleSwitch({modelId}) {
let [attrs, patch] = useStreamWithPatcher<ModelAttributes>(ctx => ctx.attributesService.streams.get(modelId));
const onClick = () => {
patch(attr => {
attr.hidden = !attr.hidden
})
}
return <GenericExplorerControl onClick={onClick} title={'test'} on={attrs.hidden}>
{attrs.hidden ? <AiOutlineEyeInvisible /> : <AiOutlineEye />}
</GenericExplorerControl>
}

View file

@ -0,0 +1,34 @@
import React from "react";
import {AiOutlineEye} from "react-icons/ai";
import {state, StateStream} from "lstream";
import {useStreamWithPatcher} from "ui/effects";
class LazyStreams<T> {
index = new Map<string, StateStream<T>>();
proto: (id: string) => T;
constructor(proto?: (id: string) => T) {
this.proto = proto || ((id: string) => null);
}
get(id: string) {
let state$: StateStream<T> = this.index[id];
if (state$ == null) {
state$ = state<T>(this.proto(id));
this.index[id] = state$;
}
return state$;
}
}
export interface ModelAttributes {
hidden: boolean
}
const modelAttrStreams = new LazyStreams<ModelAttributes>(id => ({} as ModelAttributes));

View file

@ -51,7 +51,7 @@ export function attachToForm(Control): any {
state.activeParam = isActive ? fullPathFlatten : null;
});
const value = _.get(params, fullPath);
const value = params[name];
return <React.Fragment>
<Control value={value}
@ -65,10 +65,10 @@ export function attachToForm(Control): any {
export function SubForm(props: {name: ParamsPathSegment, children: any}) {
const formState = useContext(FormStateContext);
const formParams = useContext(FormParamsContext);
const formPath = useContext(FormPathContext);
return <FormParamsContext.Provider value={formState[props.name]}>
return <FormParamsContext.Provider value={formParams[props.name] as any}>
<FormPathContext.Provider value={[...formPath, props.name]}>
{props.children}
</FormPathContext.Provider>

View file

@ -1,20 +1,9 @@
import React from 'react';
import decoratorChain from 'ui/decoratorChain';
import connect from 'ui/connect';
import mapContext from 'ui/mapContext';
import {combine} from 'lstream';
import ObjectExplorer from '../../craft/ui/ObjectExplorer';
import ls from './Explorer.less';
function Explorer() {
export function Explorer() {
return <div className={ls.root}>
<ObjectExplorer />
</div>;
}
export default decoratorChain(
connect(streams => combine(
)),
mapContext(ctx => ({}))
)(Explorer);
}

View file

@ -1,10 +1,10 @@
.root {
overflow: auto;
position: absolute;
top:0;
left:0;
bottom:80px;
max-width: 300px;
//overflow: auto;
//position: absolute;
//top:0;
//left:0;
//bottom:80px;
//max-width: 300px;
}
.root :global(.sectionHeader) {
@ -15,4 +15,7 @@
border-radius: 3px;
pointer-events: initial;
cursor: pointer;
display: flex;
align-items: center;
flex-wrap: nowrap;
}

View file

@ -1,6 +1,14 @@
import React from 'react';
import {useStream} from "ui/effects";
import React, {useContext} from 'react';
import {useStream, useStreamWithPatcher} from "ui/effects";
import {SELECTABLE_ENTITIES} from "../../scene/entityContextPlugin";
import {AppContext} from "cad/dom/components/AppContext";
import Field from "ui/components/controls/Field";
import {InnerFolder} from "ui/components/Folder";
import Label from "ui/components/controls/Label";
import TextControl from "ui/components/controls/TextControl";
import Stack from "ui/components/Stack";
import {ColorControl} from "ui/components/controls/ColorControl";
export function SelectionView() {
@ -11,6 +19,7 @@ export function SelectionView() {
});
return <div className='selection-view'>
{SELECTABLE_ENTITIES.map((entity, i) => {
@ -20,18 +29,44 @@ export function SelectionView() {
if (selection.length === 0) {
return null;
}
return <div>
<b>{entity}</b>
<ul data-entity={entity} key={entity} style={{marginLeft: 10}}>
{selection.map(id => <li>{id}</li>)}
</ul>
</div>
return <React.Fragment key={i}>
<Stack>
{selection.map(id => <SelectedModelView key={id} modelId={id} type={entity}/>)}
</Stack>
</React.Fragment>
})}
</div>
}
interface SelectedModelViewProps {
modelId,
type
}
function SelectedModelView(props: SelectedModelViewProps) {
const ctx = useContext(AppContext);
const model = ctx.cadRegistry.find(props.modelId);
const [attrs, patchAttrs] = useStreamWithPatcher(ctx => ctx.attributesService.streams.get(props.modelId));
const DO_NOTHING = ()=>{};
return <InnerFolder title={props.type + ' ' + model.id} closable titleClass='bg-color-3'>
<Stack className='bg-color-4'>
<Field active={false} name='label' onFocus={DO_NOTHING} onClick={DO_NOTHING}>
<Label>Label</Label>
<TextControl value={attrs.label||''} onChange={val => patchAttrs(attrs => attrs.label = val)}/>
</Field>
<Field active={false} name='label' onFocus={DO_NOTHING} onClick={DO_NOTHING}>
<Label>Color</Label>
<ColorControl
dialogTitle={`Color for ${props.type} ${props.modelId}`}
value={attrs.color} onChange={val => patchAttrs(attrs => attrs.color = val)}/>
</Field>
</Stack>
</InnerFolder>;
}

View file

@ -20,6 +20,7 @@ import SketcherOperationWizard from "../../../sketcher/components/SketcherOperat
import {ToastContainer} from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
import {ContributedComponents} from "./ContributedComponents";
import {SceneInlineObjectExplorer} from "cad/craft/ui/SceneInlineObjectExplorer";
export default class View3d extends React.Component {
@ -41,7 +42,11 @@ export default class View3d extends React.Component {
</div>
<div className={ls.middleSection + ' small-typography'}>
<SketcherMode>
<SketcherMode whenOff={
<div className={ls.overlayingPanel} >
<SceneInlineObjectExplorer />
</div>
}>
<InplaceSketcher>
<div className={ls.overlayingPanel} >
<Scope><SketchObjectExplorer /></Scope>

View file

@ -45,6 +45,7 @@
display: flex;
align-items: flex-start;
flex-grow: 1;
overflow: hidden;
}
.overlayingPanel {

View file

@ -40,6 +40,7 @@ import * as LocationPlugin from "../location/LocationPlugin";
import * as AssemblyPlugin from "../assembly/assemblyPlugin";
import {WorkbenchesLoaderPlugin} from "cad/workbench/workbenchesLoaderPlugin";
import {PluginSystem} from "plugable/pluginSystem";
import {AttributesPlugin} from "cad/attributes/attributesPlugin";
export default function startApplication(callback) {
@ -83,7 +84,8 @@ export default function startApplication(callback) {
AssemblyPlugin,
RemotePartsPlugin,
ViewSyncPlugin,
WizardSelectionPlugin
WizardSelectionPlugin,
AttributesPlugin
];
let allPlugins = [...preUIPlugins, ...plugins];

View file

@ -52,7 +52,7 @@ export class MEdge extends MObject {
tan = he.tangentAtStart();
origin = he.vertexA.point;
} else {
tan = he.tangentAtEnd();
tan = he.tangentAtEnd().negate();
origin = he.vertexB.point;
}
return new Axis(origin, tan);

View file

@ -160,7 +160,7 @@ export class MFace extends MObject {
get productionInfo() {
if (this._productionInfo === undefined) {
this._productionInfo = !this.brepFace.data.productionInfo ? null :
this._productionInfo = !this.brepFace?.data?.productionInfo ? null :
ProductionInfo.fromRawData(this.brepFace.data.productionInfo);
}
return this._productionInfo;

View file

@ -7,6 +7,7 @@ import {MShell} from '../model/mshell';
import {MDatum} from '../model/mdatum';
import DatumView from './views/datumView';
import {View} from './views/view';
import {SketchingView} from "cad/scene/views/faceView";
export function activate(context) {
let {streams} = context;
@ -14,7 +15,8 @@ export function activate(context) {
streams.sketcher.update.attach(mFace => mFace.ext.view.updateSketch());
}
function sceneSynchronizer({services: {cadScene, cadRegistry, viewer, wizard, action, pickControl}}) {
function sceneSynchronizer(ctx) {
const {services: {cadScene, cadRegistry, viewer, wizard, action, pickControl}} = ctx;
return function() {
let wgChildren = cadScene.workGroup.children;
let existent = new Set();
@ -48,6 +50,22 @@ function sceneSynchronizer({services: {cadScene, cadRegistry, viewer, wizard, ac
} else {
console.warn('unsupported model ' + model);
}
const attrStream = ctx.attributesService.streams.get(model.id);
const disposer = attrStream.attach(attrs => {
modelView.rootGroup.visible = !attrs.hidden
viewer.requestRender();
});
modelView.traverse(view => {
if (view instanceof SketchingView) {
const stream = ctx.attributesService.streams.get(view.model.id);
modelView.addDisposer(stream.attach(attr => {
view.setColor(attr.color);
viewer.requestRender();
}))
}
});
modelView.addDisposer(disposer)
SceneGraph.addToGroup(cadScene.workGroup, modelView.rootGroup);
}
}

View file

@ -5,6 +5,6 @@ export class EdgeView extends CurveBasedView {
constructor(edge) {
let brepEdge = edge.brepEdge;
let tess = brepEdge.data.tessellation ? brepEdge.data.tessellation : brepEdge.curve.tessellateToData();
super(edge, tess, 2, 3, 0x2B3856, 0xd1726c);
super(edge, tess, 2, 3, 0x2B3856, 0xc42720);
}
}

View file

@ -69,15 +69,15 @@ export class FaceView extends SketchingView {
}
mark(color) {
this.setColor(color || SELECTION_COLOR);
this.updateColor(color || SELECTION_COLOR);
}
withdraw(color) {
this.setColor(null);
this.updateColor(null);
}
setColor(color) {
setFacesColor(this.meshFaces, color);
updateColor(color) {
setFacesColor(this.meshFaces, color||this.color);
this.geometry.colorsNeedUpdate = true;
}
}
@ -93,4 +93,4 @@ export function setFacesColor(faces, color) {
}
export const NULL_COLOR = new THREE.Color();
export const SELECTION_COLOR = 0xFAFAD2;
export const SELECTION_COLOR = 0xffff80;

View file

@ -28,14 +28,14 @@ export class OpenFaceView extends SketchingView {
super(mFace);
this.material = new THREE.MeshPhongMaterial({
vertexColors: THREE.FaceColors,
color: 0xB0C4DE,
// color: 0xB0C4DE,
shininess: 0,
polygonOffset : true,
polygonOffsetFactor : 1,
polygonOffsetUnits : 2,
side : THREE.DoubleSide,
transparent: true,
opacity: 0.5
opacity: 0.3
});
this.updateBounds();
}
@ -60,7 +60,7 @@ export class OpenFaceView extends SketchingView {
}
updateBounds() {
let markedColor = this.color;
let markedColor = this.markedColor;
this.dropGeometry();
let bounds2d = [];
@ -72,9 +72,11 @@ export class OpenFaceView extends SketchingView {
surface.northEastPoint(), surface.northWestPoint()];
this.createGeometry();
if (markedColor) {
this.mark(markedColor);
}
this.updateColor(markedColor || this.color);
}
traverse(visitor) {
super.traverse(visitor);
}
updateSketch() {
@ -83,19 +85,19 @@ export class OpenFaceView extends SketchingView {
}
mark(color) {
this.setColor(color || SELECTION_COLOR);
this.updateColor(color || SELECTION_COLOR);
}
withdraw(color) {
this.setColor(null);
this.updateColor(null);
}
setColor(color) {
setFacesColor(this.mesh.geometry.faces, color);
updateColor(color) {
setFacesColor(this.mesh.geometry.faces, color||this.color);
this.mesh.geometry.colorsNeedUpdate = true;
}
get color() {
get markedColor() {
let face = this.mesh && this.mesh.geometry && this.mesh.geometry.faces[0];
if (face) {
return face.color === NULL_COLOR ? null : face.color;

View file

@ -71,6 +71,13 @@ export class ShellView extends View {
this.faceViews.forEach(faceView => faceView.setColor(null));
}
traverse(visitor) {
super.traverse(visitor);
this.faceViews.forEach(f => f.traverse(visitor));
this.edgeViews.forEach(e => e.traverse(visitor));
this.vertexViews.forEach(e => e.traverse(visitor));
}
dispose() {
this.mesh.material.dispose();
this.mesh.geometry.dispose();

View file

@ -3,8 +3,8 @@ import {CurveBasedView} from './curveBasedView';
export class SketchObjectView extends CurveBasedView {
constructor(mSketchObject, sketchToWorldTransformation) {
const color = mSketchObject.construction ? 0x777777 : 0xFFFFFF;
const color = mSketchObject.construction ? 0x777777 : 0x0000FF;
const tess = mSketchObject.sketchPrimitive.tessellate(10).map(sketchToWorldTransformation.apply).map(v => v.data());
super(mSketchObject, tess, 1, 2, color, 0x49FFA5);
super(mSketchObject, tess, 3, 4, color, 0x49FFA5);
}
}

View file

@ -1,7 +1,13 @@
import {createFunctionList} from "gems/func";
import {Color} from "three";
export class View {
static MARKER = 'ModelView';
disposers = createFunctionList();
color = new Color();
constructor(model) {
this.model = model;
model.ext.view = this;
@ -15,8 +21,25 @@ export class View {
withdraw(priority) {
}
setColor(color) {
if (!color) {
this.color = new Color();
} else {
this.color.setStyle(color);
}
}
traverse(visitor) {
visitor(this);
}
addDisposer(disposer) {
this.disposers.add(disposer);
}
dispose() {
this.disposers.call();
this.model.ext.view = null;
this.model = null;
};

View file

@ -48,7 +48,7 @@ export class SceneSolid {
export function createSolidMaterial(skin) {
return new THREE.MeshPhongMaterial(Object.assign({
vertexColors: THREE.FaceColors,
color: 0xB0C4DE,
// color: 0xB0C4DE,
shininess: 0,
polygonOffset : true,
polygonOffsetFactor : 1,

View file

@ -1,12 +1,12 @@
import React from 'react';
import {useStream} from "ui/effects";
export default function SketcherMode({children}) {
export default function SketcherMode({children, whenOff}) {
const visible = useStream(ctx => ctx.streams.sketcher.sketchingMode);
if (!visible) {
return null;
return whenOff;
}
return children;

View file

@ -9,6 +9,7 @@ import OperationHistory from '../craft/ui/OperationHistory';
import Expressions from '../expressions/Expressions';
import {SelectionView} from "../dom/components/SelectionView";
import {GrSelect} from "react-icons/gr";
import {Explorer} from "cad/dom/components/Explorer";
export function activate(ctx) {
const {services, streams} = ctx;
@ -28,7 +29,7 @@ export function activate(ctx) {
services.menu.registerMenus(menuConfig);
services.ui.registerFloatView('project', ObjectExplorer, 'Model', 'cubes');
services.ui.registerFloatView('project', Explorer, 'Model', 'cubes');
services.ui.registerFloatView('history', OperationHistory, 'Modifications', 'history');
services.ui.registerFloatView('expressions', Expressions, 'Expressions', 'percent');
services.ui.registerFloatView('selection', SelectionView, 'Selection', GrSelect);

View file

@ -1,4 +1,4 @@
@import "inSceneUI";
@import "inSceneUI-sketcher";
.objectItem {
background-color: rgba(0,0,0, 0.6);

View file

@ -1,4 +1,4 @@
@import "inSceneUI";
@import "inSceneUI-sketcher";
.objectItem {
background-color: rgba(0,0,0, 0.6);