mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-06 08:25:19 +01:00
objects highlight
This commit is contained in:
parent
ed5b40878d
commit
3dea461392
43 changed files with 657 additions and 354 deletions
8
modules/gems/indexed.ts
Normal file
8
modules/gems/indexed.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import {Index} from "gems/indexType";
|
||||
|
||||
export function createIndex<T>(arr: T[], indexProp: (item) => string): Index<T> {
|
||||
return arr.reduce((index, item) => {
|
||||
index[indexProp(item)] = item;
|
||||
return index;
|
||||
}, {})
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import {ORIGIN} from "math/vector";
|
|||
|
||||
export default class ScalableLine extends Mesh {
|
||||
|
||||
constructor(tessellation, width, color, opacity, smooth, ambient) {
|
||||
super(createGeometry(tessellation, smooth), createMaterial(color, opacity, ambient));
|
||||
constructor(tessellation, width, color, opacity, smooth, ambient, offset) {
|
||||
super(createGeometry(tessellation, smooth), createMaterial(color, opacity, ambient, offset));
|
||||
this.width = width;
|
||||
this.morphTargetInfluences = [0];
|
||||
}
|
||||
|
|
@ -35,12 +35,19 @@ export default class ScalableLine extends Mesh {
|
|||
}
|
||||
}
|
||||
|
||||
function createMaterial(color, opacity, ambient) {
|
||||
function createMaterial(color, opacity, ambient, offset) {
|
||||
let materialParams = {
|
||||
vertexColors: FaceColors,
|
||||
morphTargets: true,
|
||||
color,
|
||||
};
|
||||
if (offset) {
|
||||
Object.assign(materialParams, {
|
||||
polygonOffset: true,
|
||||
polygonOffsetFactor: -2.0,
|
||||
polygonOffsetUnits: -1.0,
|
||||
});
|
||||
}
|
||||
if (!ambient) {
|
||||
materialParams.shininess = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,7 +217,31 @@ export default class SceneSetUp {
|
|||
if (logInfoOut !== null) {
|
||||
logInfoOut.ray = raycaster.ray
|
||||
}
|
||||
return raycaster.intersectObjects( objects, true );
|
||||
|
||||
const intersects = [];
|
||||
|
||||
function intersectObject(object) {
|
||||
|
||||
object.raycast( raycaster, intersects );
|
||||
|
||||
const children = object.children;
|
||||
|
||||
if (object.visible) {
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
intersectObject(children[ i ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objects.forEach(intersectObject);
|
||||
|
||||
intersects.sort((a, b) => {
|
||||
if (Math.abs(a.distance - b.distance) < 0.01 && (a.object.raycastPriority || b.object.raycastPriority)) {
|
||||
return b.object.raycastPriority||0 - a.object.raycastPriority||0;
|
||||
}
|
||||
return a.distance - b.distance;
|
||||
})
|
||||
return intersects;
|
||||
}
|
||||
|
||||
customRaycast(from3, to3, objects) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ interface GenericExplorerNodeProps {
|
|||
defaultExpanded?: boolean;
|
||||
expandable: boolean;
|
||||
select: any;
|
||||
onMouseEnter?: any;
|
||||
onMouseLeave?: any;
|
||||
}
|
||||
|
||||
export function GenericExplorerNode(props: GenericExplorerNodeProps) {
|
||||
|
|
@ -36,7 +38,7 @@ export function GenericExplorerNode(props: GenericExplorerNodeProps) {
|
|||
const toggle = expandable ? (() => setExpanded(expanded => !expanded)) : undefined;
|
||||
|
||||
return <>
|
||||
<div className={ls.objectItem}>
|
||||
<div className={ls.objectItem} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}>
|
||||
<span className={expandable ? ls.expandHandle : ls.expandHandleInactive} onClick={toggle}>
|
||||
{expandable ? (expanded ? <AiFillCaretDown/> : <AiFillCaretRight/>)
|
||||
: <BsDot/>}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import Stack from "ui/components/Stack";
|
|||
import ButtonGroup from "ui/components/controls/ButtonGroup";
|
||||
import Button from "ui/components/controls/Button";
|
||||
|
||||
export function GenericWizard({topicId, title, left, className, children, onCancel, onOK, infoText, ...props}: {
|
||||
export function GenericWizard({topicId, title, left, top, className, children, onCancel, onOK, infoText, ...props}: {
|
||||
topicId: string,
|
||||
title: string,
|
||||
left?: number,
|
||||
top?: number,
|
||||
onCancel: () => any,
|
||||
onOK: () => any,
|
||||
onKeyDown?: (e) => any,
|
||||
infoText: any
|
||||
infoText?: any
|
||||
} & WindowProps ) {
|
||||
|
||||
return <Window initWidth={250}
|
||||
initLeft={left || 15}
|
||||
initTop={top}
|
||||
title={(title||'').toUpperCase()}
|
||||
className={cx('mid-typography', className)}
|
||||
controlButtons={<>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ export function MenuItem({icon, label, hotKey, style, disabled, onClick, childre
|
|||
hotKey = null;
|
||||
}
|
||||
}
|
||||
let clickHandler = disabled ? undefined : () => {
|
||||
let clickHandler = disabled ? undefined : (e) => {
|
||||
closeAllUpPopups();
|
||||
onClick();
|
||||
onClick(e);
|
||||
};
|
||||
return <div className={cx(ls.item, disabled && ls.disabled)}
|
||||
onMouseDown={e => e.stopPropagation()} style={style} onClick={clickHandler} {...props}>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import React from 'react';
|
|||
import ls from './Stack.less'
|
||||
import cx from "classnames";
|
||||
|
||||
export default function Stack({className, ...props}) {
|
||||
type StackProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export default function Stack({className, ...props}: StackProps) {
|
||||
return <div className={cx(ls.root, className)} {...props} />
|
||||
}
|
||||
|
||||
|
|
@ -2,16 +2,17 @@ import React from 'react';
|
|||
|
||||
import cx from 'classnames';
|
||||
|
||||
export default function Button({type, onClick, className, ...props}: {
|
||||
export default function Button({type, onClick, className, compact, ...props}: {
|
||||
type?: string,
|
||||
onClick: () => void,
|
||||
className? : string,
|
||||
children?: any,
|
||||
compact?: boolean
|
||||
} & JSX.IntrinsicAttributes) {
|
||||
|
||||
type = type || 'neutral';
|
||||
|
||||
return <button onClick={onClick} className={cx(type, className)} {...props} />
|
||||
return <button onClick={onClick} className={cx(type, compact&&'compact', className)} {...props} />
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import React, {useContext} from 'react';
|
||||
import {FieldId} from "ui/components/controls/Field";
|
||||
|
||||
export default class CheckboxControl extends React.Component {
|
||||
export default function CheckboxControl(props) {
|
||||
let {onChange, value} = props;
|
||||
const fieldId = useContext(FieldId);
|
||||
return <input id={fieldId}
|
||||
type='checkbox'
|
||||
checked={value}
|
||||
onChange={e => onChange(e.target.checked)}/>
|
||||
|
||||
render() {
|
||||
let {onChange, value} = this.props;
|
||||
return <input type='checkbox'
|
||||
checked={value}
|
||||
onChange={e => onChange(e.target.checked)} />
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,14 @@ export function ColorControl(props: ColorControlProps) {
|
|||
return <React.Fragment>
|
||||
<ButtonGroup>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
padding: '0 3px',
|
||||
fontFamily: 'monospace',
|
||||
...style
|
||||
}}>{value||<NoValue/>}</span>
|
||||
<Button onClick={() => onChange(null)}><RiDeleteBack2Line /></Button>
|
||||
<Button onClick={() => setOpen(true)}><CgColorPicker /></Button>
|
||||
<Button compact onClick={() => onChange(null)}><RiDeleteBack2Line /></Button>
|
||||
<Button compact onClick={() => setOpen(true)}><CgColorPicker /></Button>
|
||||
</ButtonGroup>
|
||||
{open && <ColorDialog value={value||'white'} onChange={onChange} onClose={() => setOpen(false)} title={props.dialogTitle}/>}
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import React from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import ls from './Field.less'
|
||||
import cx from 'classnames';
|
||||
|
||||
export const FieldId = React.createContext(-1);
|
||||
|
||||
let ID_GENERATOR = 0;
|
||||
|
||||
export default function Field({active, name, ...props}) {
|
||||
return <div className={cx(ls.root, active&&ls.active)} data-field-name={name} {...props} />
|
||||
|
||||
const fieldId = useMemo(() => 'Field_' + (ID_GENERATOR++), []);
|
||||
|
||||
return <FieldId.Provider value={fieldId}>
|
||||
<div className={cx(ls.root, active&&ls.active)} data-field-name={name} {...props} />
|
||||
</FieldId.Provider>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.active {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
import React, {useContext} from 'react';
|
||||
import {FieldId} from "ui/components/controls/Field";
|
||||
|
||||
export default function Label({children}) {
|
||||
return <span>{children}</span>
|
||||
const fieldId = useContext(FieldId);
|
||||
return <label htmlFor={fieldId}>{children}</label>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@
|
|||
"marked": "^1.0.0",
|
||||
"mousetrap": "1.6.1",
|
||||
"numeric": "1.2.6",
|
||||
"opencascade.js": "^1.1.1",
|
||||
"prop-types": "15.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-color": "^2.19.3",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ export function ActionButtonBehavior({children, actionId}) {
|
|||
|
||||
return children({
|
||||
'data-action-id': actionId,
|
||||
onClick: e => actionService.run(actionId, e),
|
||||
onClick: e => {
|
||||
canceled = true;
|
||||
actionService.run(actionId, e);
|
||||
},
|
||||
onMouseEnter: e => {
|
||||
updateCoords(e);
|
||||
canceled = false;
|
||||
|
|
|
|||
|
|
@ -29,3 +29,13 @@ export function requiresSolidSelection(amount) {
|
|||
update: checkForSelectedSolids(amount)
|
||||
}
|
||||
}
|
||||
|
||||
export const RequiresAnyModelSelection = {
|
||||
listens: ctx => ctx.streams.selection.all,
|
||||
update: (state, selection) => {
|
||||
state.enabled = selection.length > 0;
|
||||
if (!state.enabled) {
|
||||
state.hint = 'requires at least one model selected';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,10 +155,12 @@ export interface ActionService {
|
|||
hint$: StateStream<Hint>;
|
||||
}
|
||||
|
||||
declare module 'context' {
|
||||
interface CoreContext {
|
||||
|
||||
actionService: ActionService;
|
||||
}
|
||||
export interface ActionSystemPlugin {
|
||||
actionService: ActionService;
|
||||
}
|
||||
|
||||
|
||||
declare module 'context' {
|
||||
interface CoreContext extends ActionSystemPlugin {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import {Plugin} from "plugable/pluginSystem";
|
||||
import {AttributesService} from "cad/attributes/attributesService";
|
||||
import {contributeComponent} from "cad/dom/components/ContributedComponents";
|
||||
import {DisplayOptionsDialogManager} from "cad/attributes/ui/DisplayOptionsDialog";
|
||||
import {ActionSystemPlugin} from "cad/actions/actionSystemPlugin";
|
||||
import {RequiresAnyModelSelection} from "cad/actions/actionHelpers";
|
||||
import {IoColorPalette} from "react-icons/io5";
|
||||
import {FaTable} from "react-icons/fa";
|
||||
import {ApplicationContext} from "context";
|
||||
|
||||
interface AttributesPluginInputContext {
|
||||
}
|
||||
type AttributesPluginInputContext = ActionSystemPlugin;
|
||||
|
||||
export interface AttributesPluginContext {
|
||||
attributesService: AttributesService;
|
||||
|
|
@ -17,6 +23,7 @@ declare module 'context' {
|
|||
export const AttributesPlugin: Plugin<AttributesPluginInputContext, AttributesPluginContext, AttributesPluginWorkingContext> = {
|
||||
|
||||
inputContextSpec: {
|
||||
actionService: 'required',
|
||||
},
|
||||
|
||||
outputContextSpec: {
|
||||
|
|
@ -25,6 +32,31 @@ export const AttributesPlugin: Plugin<AttributesPluginInputContext, AttributesPl
|
|||
|
||||
activate(ctx: AttributesPluginWorkingContext) {
|
||||
ctx.attributesService = new AttributesService();
|
||||
contributeComponent(DisplayOptionsDialogManager);
|
||||
|
||||
ctx.actionService.registerActions([
|
||||
{
|
||||
id: 'ModelDisplayOptions',
|
||||
appearance: {
|
||||
icon: IoColorPalette,
|
||||
label: 'display options...',
|
||||
info: 'open a dialog with display options for model(s)',
|
||||
},
|
||||
invoke: (ctx: ApplicationContext, e) => ctx.attributesService
|
||||
.openDisplayOptionsEditor(ctx.entityContextService.selectedIds, e),
|
||||
...RequiresAnyModelSelection,
|
||||
},
|
||||
{
|
||||
id: 'ModelAttributesEditor',
|
||||
appearance: {
|
||||
icon: FaTable,
|
||||
label: 'edit attributes...',
|
||||
info: 'open a dialog with attributes editor for the model',
|
||||
},
|
||||
invoke: ({services}) => services.sketcher.sketchFace(services.selection.face.single),
|
||||
...RequiresAnyModelSelection,
|
||||
},
|
||||
])
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import {LazyStreams} from "lstream/lazyStreams";
|
||||
import {state} from "lstream";
|
||||
|
||||
export interface ModelAttributes {
|
||||
hidden: boolean;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export class AttributesService {
|
||||
|
||||
|
|
@ -8,12 +15,28 @@ export class AttributesService {
|
|||
color: null
|
||||
}));
|
||||
|
||||
displayOptionsEditors$ = state<EditorSet>({});
|
||||
|
||||
attributesEditors$ = state<EditorSet>({});
|
||||
|
||||
openDisplayOptionsEditor(modelIds: string[], e: any) {
|
||||
this.displayOptionsEditors$.mutate(editors => {
|
||||
const copy = [...modelIds].sort();
|
||||
editors[copy.join(':')] = {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
models: copy
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface ModelAttributes {
|
||||
hidden: boolean;
|
||||
label: string;
|
||||
color: string;
|
||||
export type EditorSet = {
|
||||
[key: string]: {
|
||||
x: number,
|
||||
y: number,
|
||||
models: string[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
76
web/app/cad/attributes/ui/DisplayOptionsDialog.tsx
Normal file
76
web/app/cad/attributes/ui/DisplayOptionsDialog.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import React, {useContext} from "react";
|
||||
import {useStreamWithPatcher, useStreamWithUpdater} from "ui/effects";
|
||||
import Stack from "ui/components/Stack";
|
||||
import Field from "ui/components/controls/Field";
|
||||
import Label from "ui/components/controls/Label";
|
||||
import {ColorControl} from "ui/components/controls/ColorControl";
|
||||
import CheckboxControl from "ui/components/controls/CheckboxControl";
|
||||
import {AppContext} from "cad/dom/components/AppContext";
|
||||
import {ModelAttributes} from "cad/attributes/attributesService";
|
||||
import {GenericWizard} from "ui/components/GenericWizard";
|
||||
|
||||
export function DisplayOptionsDialogManager() {
|
||||
|
||||
const [editors, update] = useStreamWithUpdater(ctx => ctx.attributesService.displayOptionsEditors$);
|
||||
|
||||
function close(key: string) {
|
||||
update(editor => {
|
||||
delete editor[key];
|
||||
return editor;
|
||||
});
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{Object.keys(editors).map(key => {
|
||||
const request = editors[key];
|
||||
return <GenericWizard key={key} title='display options'
|
||||
onCancel={() => close(key)}
|
||||
onOK={() => close(key)}
|
||||
onClose={() => close(key)}
|
||||
topicId='entity-display-options'
|
||||
left={request.x}
|
||||
top={request.y}>
|
||||
<DisplayOptionsView modelIds={request.models} />
|
||||
</GenericWizard>
|
||||
})}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
||||
export interface DisplayOptionsViewProps {
|
||||
modelIds: string[];
|
||||
}
|
||||
|
||||
export function DisplayOptionsView(props: DisplayOptionsViewProps) {
|
||||
|
||||
const ctx = useContext(AppContext);
|
||||
const streamsAndPatchers: [ModelAttributes, any][] = [];
|
||||
|
||||
for (let modelId of props.modelIds) {
|
||||
const streamAndPatcher = useStreamWithPatcher(ctx => ctx.attributesService.streams.get(modelId));
|
||||
streamsAndPatchers.push(streamAndPatcher);
|
||||
}
|
||||
|
||||
function patchAttrs(mutator) {
|
||||
for (let [model, patch] of streamsAndPatchers) {
|
||||
patch(mutator);
|
||||
}
|
||||
}
|
||||
|
||||
const [[proto]] = streamsAndPatchers;
|
||||
const attrs = proto;
|
||||
|
||||
const DO_NOTHING = ()=>{};
|
||||
return <Stack className='bg-color-4'>
|
||||
<Field active={false} name='label' onFocus={DO_NOTHING} onClick={DO_NOTHING}>
|
||||
<Label>Visible</Label>
|
||||
<CheckboxControl value={!attrs.hidden} onChange={val => patchAttrs(attrs => attrs.hidden = !val)}/>
|
||||
</Field>
|
||||
<Field active={false} name='label' onFocus={DO_NOTHING} onClick={DO_NOTHING}>
|
||||
<Label>Color</Label>
|
||||
<ColorControl
|
||||
dialogTitle={`Color for `}
|
||||
value={attrs.color} onChange={val => patchAttrs(attrs => attrs.color = val)}/>
|
||||
</Field>
|
||||
</Stack>;
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ export function SceneInlineObjectExplorer() {
|
|||
|
||||
return <SceneInlineSection title='OBJECTS'> {models.map(m => {
|
||||
if (m instanceof MOpenFaceShell) {
|
||||
return <OpenFaceSection shell={m} />
|
||||
return <OpenFaceSection shell={m} key={m.id} />
|
||||
} else if (m instanceof MShell) {
|
||||
return <ModelSection type='shell' model={m} key={m.id} controlVisibility>
|
||||
<Section label='faces' defaultOpen={true}>
|
||||
|
|
@ -101,6 +101,8 @@ function ModelSection({model, type, typeLabel, expandable = true, controlVisibil
|
|||
label={label}
|
||||
selected={selected}
|
||||
select={select}
|
||||
onMouseEnter={() => ctx.highlightService.highlight(model.id)}
|
||||
onMouseLeave={() => ctx.highlightService.unHighlight(model.id)}
|
||||
controls={
|
||||
<>
|
||||
{controlVisibility && <VisibleSwitch modelId={visibilityOf.id}/>}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
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));
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, {useContext} from 'react';
|
||||
import ls from './EntityList.less';
|
||||
import Label from 'ui/components/controls/Label';
|
||||
import Field from 'ui/components/controls/Field';
|
||||
|
|
@ -6,12 +6,17 @@ import Fa from 'ui/components/Fa';
|
|||
import {attachToForm} from './Form';
|
||||
import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
|
||||
import {EMPTY_ARRAY, removeInPlace} from 'gems/iterables';
|
||||
import {AppContext} from "cad/dom/components/AppContext";
|
||||
|
||||
@attachToForm
|
||||
export default class EntityList extends React.Component {
|
||||
|
||||
deselect = (entityId) => {
|
||||
let {value, onChange} = this.props;
|
||||
function EntityList(props) {
|
||||
|
||||
const ctx = useContext(AppContext);
|
||||
|
||||
let {name, label, active, setActive, value, placeholder, readOnly, entityRenderer = e => e} = props;
|
||||
|
||||
const deselect = (entityId) => {
|
||||
let {value, onChange} = props;
|
||||
if (Array.isArray(value)) {
|
||||
onChange(removeInPlace(value, entityId));
|
||||
} else {
|
||||
|
|
@ -19,26 +24,27 @@ export default class EntityList extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let {name, label, active, setActive, value, placeholder, readOnly, onEntityEnter, onEntityLeave, entityRenderer = e => e} = this.props;
|
||||
if (!Array.isArray(value)) {
|
||||
value = value ? asArray(value) : EMPTY_ARRAY;
|
||||
}
|
||||
return <Field active={active} name={name} onClick={setActive}>
|
||||
<Label>{label||camelCaseSplitToStr(name)}:</Label>
|
||||
<div>{value.length === 0 ?
|
||||
<span className={ls.emptySelection}>{placeholder || '<not selected>'}</span> :
|
||||
value.map((entity, i) => <span className={ls.entityRef} key={i}
|
||||
onMouseEnter={() => onEntityEnter&&onEntityEnter(entity)}
|
||||
onMouseLeave={() => onEntityLeave&&onEntityLeave(entity)}>
|
||||
{entityRenderer(entity)}
|
||||
{!readOnly && <span className={ls.rm} onClick={() => this.deselect(entity)}> <Fa icon='times'/></span>}
|
||||
</span>)}
|
||||
</div>
|
||||
</Field>;
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
value = value ? asArray(value) : EMPTY_ARRAY;
|
||||
}
|
||||
return <Field active={active} name={name} onClick={setActive}>
|
||||
<Label>{label||camelCaseSplitToStr(name)}:</Label>
|
||||
<div>{value.length === 0 ?
|
||||
<span className={ls.emptySelection}>{placeholder || '<not selected>'}</span> :
|
||||
value.map((entity, i) => <span className={ls.entityRef} key={i}
|
||||
onMouseEnter={() => ctx.highlightService.highlight(entity)}
|
||||
onMouseLeave={() => ctx.highlightService.unHighlight(entity)}>
|
||||
{entityRenderer(entity)}
|
||||
{!readOnly && <span className={ls.rm} onClick={() => deselect(entity)}> <Fa icon='times'/></span>}
|
||||
</span>)}
|
||||
</div>
|
||||
</Field>;
|
||||
|
||||
}
|
||||
|
||||
export default attachToForm(EntityList);
|
||||
|
||||
function asArray(val) {
|
||||
_arr[0] = val;
|
||||
return _arr;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import {useStream} from 'ui/effects';
|
||||
import {state} from 'lstream';
|
||||
import {Scope} from "../../../sketcher/components/Scope";
|
||||
import {Scope} from "sketcher/components/Scope";
|
||||
|
||||
const CONTRIBUTED_COMPONENTS$ = state([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,29 +23,34 @@ function ActionMenu({actions, keymap, ...menuState}) {
|
|||
</Menu>;
|
||||
}
|
||||
|
||||
function ActionMenuItem({label, cssIcons, icon32, icon96, enabled, hotKey, visible, actionId, ...props}) {
|
||||
function ActionMenuItem({label, cssIcons, icon, icon32, icon96, enabled, hotKey, visible, actionId, ...props}) {
|
||||
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`,
|
||||
backgroundPositionX: 5,
|
||||
backgroundPositionY: 4,
|
||||
};
|
||||
if (!enabled) {
|
||||
style.filter = 'grayscale(90%)';
|
||||
let renderedIcon, style;
|
||||
if (icon) {
|
||||
const Icon = icon;
|
||||
renderedIcon = <Icon />;
|
||||
} else {
|
||||
if (icon32 || icon96) {
|
||||
let size = 16;
|
||||
renderedIcon = <Filler width={size} height='1.18em'/>;
|
||||
style = {
|
||||
backgroundImage: `url(${icon32 || icon96})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: `${size}px ${size}px`,
|
||||
backgroundPositionX: 5,
|
||||
backgroundPositionY: 4,
|
||||
};
|
||||
if (!enabled) {
|
||||
style.filter = 'grayscale(90%)';
|
||||
}
|
||||
} else if (cssIcons) {
|
||||
renderedIcon = <Fa fw fa={cssIcons} />;
|
||||
}
|
||||
} else if (cssIcons) {
|
||||
icon = <Fa fw fa={cssIcons} />;
|
||||
}
|
||||
|
||||
return <MenuItem {...{label, icon, style, disabled: !enabled, hotKey, ...props}} />;
|
||||
return <MenuItem icon={renderedIcon} {...{label, style, disabled: !enabled, hotKey, ...props}} />;
|
||||
}
|
||||
|
||||
const ConnectedActionMenu = connect((streams, props) =>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import * as SketcherPlugin from '../sketch/sketcherPlugin';
|
|||
import * as SketcherStoragePlugin from '../sketch/sketchStoragePlugin';
|
||||
import * as ExportPlugin from '../exportPlugin';
|
||||
import * as ExposurePlugin from '../exposure/exposurePlugin';
|
||||
import * as ViewSyncPlugin from '../scene/viewSyncPlugin';
|
||||
import {ViewSyncPlugin} from '../scene/viewSyncPlugin';
|
||||
import * as EntityContextPlugin from '../scene/entityContextPlugin';
|
||||
import * as OCCTPlugin from '../craft/e0/occtPlugin';
|
||||
|
||||
|
|
@ -41,6 +41,7 @@ import * as AssemblyPlugin from "../assembly/assemblyPlugin";
|
|||
import {WorkbenchesLoaderPlugin} from "cad/workbench/workbenchesLoaderPlugin";
|
||||
import {PluginSystem} from "plugable/pluginSystem";
|
||||
import {AttributesPlugin} from "cad/attributes/attributesPlugin";
|
||||
import {HighlightPlugin} from "cad/scene/highlightPlugin";
|
||||
|
||||
export default function startApplication(callback) {
|
||||
|
||||
|
|
@ -85,7 +86,8 @@ export default function startApplication(callback) {
|
|||
RemotePartsPlugin,
|
||||
ViewSyncPlugin,
|
||||
WizardSelectionPlugin,
|
||||
AttributesPlugin
|
||||
AttributesPlugin,
|
||||
HighlightPlugin
|
||||
];
|
||||
|
||||
let allPlugins = [...preUIPlugins, ...plugins];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {printRaycastDebugInfo, RayCastDebugInfo} from "./rayCastDebug";
|
||||
import {LOG_FLAGS} from "../../logFlags";
|
||||
|
||||
export function activate(context) {
|
||||
const {services, streams} = context;
|
||||
export function activate(ctx) {
|
||||
const {services, streams} = ctx;
|
||||
const domElement = services.viewer.sceneSetup.domElement();
|
||||
const event = {
|
||||
viewer: services.viewer
|
||||
|
|
@ -11,8 +11,22 @@ export function activate(context) {
|
|||
domElement.addEventListener('mousedown', mousedown, false);
|
||||
domElement.addEventListener('mouseup', mouseup, false);
|
||||
domElement.addEventListener('mousemove', mousemove, false);
|
||||
domElement.addEventListener('contextmenu', (e) => ctx.actionService.run('menu.contextual', {
|
||||
x: e.offsetX,
|
||||
y: e.offsetY
|
||||
}), false);
|
||||
|
||||
let performRaycast = e => services.viewer.raycast(e, services.cadScene.workGroup.children, RayCastDebugInfo);
|
||||
|
||||
let performRaycast = e => {
|
||||
const hits = services.viewer.raycast(e, services.cadScene.workGroup.children, RayCastDebugInfo);
|
||||
hits.sort((a, b) => {
|
||||
if (Math.abs(a.distance - b.distance) < 0.01 && (a.object.raycastPriority || b.object.raycastPriority)) {
|
||||
return b.object.raycastPriority||0 - a.object.raycastPriority||0;
|
||||
}
|
||||
return a.distance - b.distance;
|
||||
})
|
||||
return hits;
|
||||
}
|
||||
|
||||
let toDrag = null;
|
||||
let pressed = new Set();
|
||||
|
|
@ -102,35 +116,6 @@ export function activate(context) {
|
|||
} else {
|
||||
let hits = performRaycast(e);
|
||||
dispatchMousemove(e, hits)
|
||||
event.hits = hits;
|
||||
|
||||
valid.clear();
|
||||
for (let hit of hits) {
|
||||
valid.add(hit.object);
|
||||
if (!hit.object.passMouseEvent || !hit.object.passMouseEvent(event)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
entered.forEach(el => {
|
||||
if (!valid.has(el) && el.onMouseLeave) {
|
||||
el.onMouseLeave(event);
|
||||
}
|
||||
});
|
||||
|
||||
valid.forEach(el => {
|
||||
if (!entered.has(el) && el.onMouseEnter) {
|
||||
el.onMouseEnter(event);
|
||||
}
|
||||
if (el.onMouseMove) {
|
||||
el.onMouseMove(event);
|
||||
}
|
||||
});
|
||||
|
||||
let t = valid;
|
||||
valid = entered;
|
||||
entered = t;
|
||||
valid.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +132,8 @@ export function activate(context) {
|
|||
}
|
||||
|
||||
entered.forEach(el => {
|
||||
if (!valid.has(el) && el.onMouseLeave) {
|
||||
//need to check parent in case of object removed
|
||||
if (!valid.has(el) && el.onMouseLeave && el.parent) {
|
||||
el.onMouseLeave(event);
|
||||
}
|
||||
});
|
||||
|
|
@ -167,7 +153,7 @@ export function activate(context) {
|
|||
valid.clear();
|
||||
}
|
||||
|
||||
context.services.modelMouseEventSystem = {
|
||||
ctx.services.modelMouseEventSystem = {
|
||||
dispatchMousedown, dispatchMouseup, dispatchMousemove
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as mask from 'gems/mask'
|
||||
import {getAttribute, setAttribute} from 'scene/objectData';
|
||||
import {getAttribute} from 'scene/objectData';
|
||||
import {FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL, DATUM_AXIS, LOOP} from '../../model/entities';
|
||||
import {LOG_FLAGS} from '../../logFlags';
|
||||
import * as vec from 'math/vec';
|
||||
import {LOG_FLAGS} from 'cad/logFlags';
|
||||
import {initRayCastDebug, printRaycastDebugInfo, RayCastDebugInfo} from "./rayCastDebug";
|
||||
|
||||
export interface PickControlService {
|
||||
|
|
@ -157,7 +156,9 @@ export function activate(context) {
|
|||
function handleSolidPick(e) {
|
||||
let pickResults = services.viewer.raycast(e, services.cadScene.workGroup.children);
|
||||
traversePickResults(e, pickResults, PICK_KIND.FACE, (sketchFace) => {
|
||||
context.locationService.edit(sketchFace.shell);
|
||||
const shell = sketchFace.shell;
|
||||
services.marker.markExclusively(shell.TYPE, shell.id);
|
||||
context.locationService.edit(shell);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import {state, StateStream} from 'lstream';
|
||||
import {combine, state, StateStream, Stream} from 'lstream';
|
||||
|
||||
import {addToListInMap} from 'gems/iterables';
|
||||
import {EMPTY_ARRAY} from 'gems/iterables';
|
||||
import {DATUM, FACE, SHELL, SKETCH_OBJECT, EDGE, LOOP} from '../model/entities';
|
||||
import {combine} from "lstream";
|
||||
import {addToListInMap, EMPTY_ARRAY} from 'gems/iterables';
|
||||
import {DATUM, EDGE, FACE, LOOP, SHELL, SKETCH_OBJECT} from '../model/entities';
|
||||
import {MObject} from "cad/model/mobject";
|
||||
|
||||
export const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL];
|
||||
|
|
@ -14,7 +12,7 @@ export function defineStreams(ctx) {
|
|||
ctx.streams.selection[entity] = state([]);
|
||||
});
|
||||
ctx.streams.selection.all = combine(...(Object.values(ctx.streams.selection) as StateStream<string[]>[]))
|
||||
.map(selection => [].concat(...selection)).throttle();
|
||||
.map((selection:string[]) => [].concat(...selection)).remember();
|
||||
}
|
||||
|
||||
export function activate(ctx) {
|
||||
|
|
@ -58,6 +56,9 @@ export function activate(ctx) {
|
|||
})
|
||||
|
||||
ctx.entityContextService = {
|
||||
get selectedIds() {
|
||||
return ctx.streams.selection.all.value
|
||||
},
|
||||
selectedEntities: ctx.streams.selection.all.map(ids => ids.map(ctx.cadRegistry.find)).remember()
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +67,7 @@ declare module 'context' {
|
|||
interface CoreContext {
|
||||
|
||||
entityContextService: {
|
||||
selectedIds: string[],
|
||||
selectedEntities: StateStream<MObject[]>
|
||||
};
|
||||
}
|
||||
|
|
|
|||
56
web/app/cad/scene/highlightPlugin.ts
Normal file
56
web/app/cad/scene/highlightPlugin.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import {Plugin} from "plugable/pluginSystem";
|
||||
import {combine, stream} from "lstream";
|
||||
import Viewer from "cad/scene/viewer";
|
||||
|
||||
export class HighlightService {
|
||||
|
||||
highlightEvents = stream<string>();
|
||||
unHighlightEvents = stream<string>();
|
||||
|
||||
constructor(viewer: Viewer) {
|
||||
combine(this.highlightEvents, this.unHighlightEvents)
|
||||
.throttle()
|
||||
.attach(() => viewer.requestRender())
|
||||
}
|
||||
|
||||
highlight(id: string) {
|
||||
this.highlightEvents.next(id);
|
||||
}
|
||||
|
||||
unHighlight(id: string) {
|
||||
this.unHighlightEvents.next(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface HighlightPluginInputContext {
|
||||
viewer: Viewer;
|
||||
}
|
||||
|
||||
export interface HighlightPluginContext {
|
||||
highlightService: HighlightService;
|
||||
}
|
||||
|
||||
type HighlightPluginWorkingContext = HighlightPluginInputContext&HighlightPluginContext;
|
||||
|
||||
declare module 'context' {
|
||||
interface ApplicationContext extends HighlightPluginContext {}
|
||||
}
|
||||
|
||||
export const HighlightPlugin: Plugin<HighlightPluginInputContext, HighlightPluginContext, HighlightPluginWorkingContext> = {
|
||||
|
||||
inputContextSpec: {
|
||||
viewer: 'required',
|
||||
},
|
||||
|
||||
outputContextSpec: {
|
||||
highlightService: 'required',
|
||||
},
|
||||
|
||||
activate(ctx: HighlightPluginWorkingContext) {
|
||||
ctx.highlightService = new HighlightService(ctx.viewer);
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -52,12 +52,12 @@ function createMarker(findEntity, requestRender) {
|
|||
return;
|
||||
}
|
||||
marked.set(id, mObj);
|
||||
mObj.ext.view && mObj.ext.view.mark(color);
|
||||
mObj.ext.view && mObj.ext.view.mark('selection');
|
||||
}
|
||||
|
||||
function doWithdraw(obj) {
|
||||
marked.delete(obj.id);
|
||||
obj.ext.view && obj.ext.view.withdraw();
|
||||
obj.ext.view && obj.ext.view.withdraw('selection');
|
||||
}
|
||||
|
||||
function onUpdate() {
|
||||
|
|
@ -67,7 +67,7 @@ function createMarker(findEntity, requestRender) {
|
|||
|
||||
function clear() {
|
||||
if (marked.size !== 0) {
|
||||
marked.forEach(m => m.ext.view && m.ext.view.withdraw());
|
||||
marked.forEach(m => m.ext.view && m.ext.view.withdraw('selection'));
|
||||
marked.clear();
|
||||
onUpdate();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,39 @@ import DatumView from './views/datumView';
|
|||
import {View} from './views/view';
|
||||
import {SketchingView} from "cad/scene/views/faceView";
|
||||
|
||||
export function activate(context) {
|
||||
let {streams} = context;
|
||||
streams.cadRegistry.update.attach(sceneSynchronizer(context));
|
||||
streams.sketcher.update.attach(mFace => mFace.ext.view.updateSketch());
|
||||
export const ViewSyncPlugin = {
|
||||
|
||||
inputContextSpec: {
|
||||
highlightService: 'required',
|
||||
attributesService: 'required',
|
||||
},
|
||||
|
||||
outputContextSpec: {
|
||||
},
|
||||
|
||||
activate(ctx) {
|
||||
let {streams} = ctx;
|
||||
ctx.highlightService.highlightEvents.attach(id => {
|
||||
const model = ctx.cadRegistry.find(id);
|
||||
model?.ext?.view?.mark('highlight');
|
||||
});
|
||||
ctx.highlightService.unHighlightEvents.attach(id => {
|
||||
const model = ctx.cadRegistry.find(id);
|
||||
model?.ext?.view?.withdraw('highlight');
|
||||
});
|
||||
|
||||
streams.cadRegistry.update.attach(sceneSynchronizer(ctx));
|
||||
streams.sketcher.update.attach(mFace => mFace.ext.view.updateSketch());
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function sceneSynchronizer(ctx) {
|
||||
const {services: {cadScene, cadRegistry, viewer, wizard, action, pickControl}} = ctx;
|
||||
let xxx = 0;
|
||||
return function() {
|
||||
console.log("sceneSynchronizer update" + (xxx++))
|
||||
|
||||
let wgChildren = cadScene.workGroup.children;
|
||||
let existent = new Set();
|
||||
for (let i = wgChildren.length - 1; i >= 0; --i) {
|
||||
|
|
@ -25,12 +49,12 @@ function sceneSynchronizer(ctx) {
|
|||
let shellView = getAttribute(obj, View.MARKER);
|
||||
if (shellView) {
|
||||
let exists = cadRegistry.modelIndex.has(shellView.model.id);
|
||||
if (!exists) {
|
||||
// if (!exists) {
|
||||
SceneGraph.removeFromGroup(cadScene.workGroup, obj);
|
||||
shellView.dispose();
|
||||
} else {
|
||||
existent.add(shellView.model.id);
|
||||
}
|
||||
// } else {
|
||||
// existent.add(shellView.model.id);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,12 +62,11 @@ function sceneSynchronizer(ctx) {
|
|||
if (!existent.has(model.id)) {
|
||||
let modelView;
|
||||
if (model instanceof MOpenFaceShell) {
|
||||
modelView = new OpenFaceShellView(model);
|
||||
modelView = new OpenFaceShellView(ctx, model);
|
||||
} else if (model instanceof MShell) {
|
||||
modelView = new ShellView(model, undefined, viewer);
|
||||
modelView = new ShellView(ctx, model, undefined,);
|
||||
} else if (model instanceof MDatum) {
|
||||
modelView = new DatumView(model, viewer,
|
||||
wizard.open,
|
||||
modelView = new DatumView(ctx, model, wizard.open,
|
||||
datum => pickControl.pick(datum),
|
||||
e => action.run('menu.datum', e),
|
||||
wizard.isInProgress);
|
||||
|
|
@ -69,6 +92,7 @@ function sceneSynchronizer(ctx) {
|
|||
SceneGraph.addToGroup(cadScene.workGroup, modelView.rootGroup);
|
||||
}
|
||||
}
|
||||
|
||||
viewer.requestRender();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,12 @@ import {setAttribute} from 'scene/objectData';
|
|||
import ScalableLine from 'scene/objects/scalableLine';
|
||||
|
||||
export class CurveBasedView extends View {
|
||||
constructor(model, tessellation, visualWidth, markerWidth, color, defaultMarkColor) {
|
||||
super(model);
|
||||
constructor(ctx, model, tessellation, visualWidth, markerWidth, color, defaultMarkColor, offset, markTable) {
|
||||
super(ctx, model, undefined, markTable);
|
||||
this.rootGroup = SceneGraph.createGroup();
|
||||
this.representation = new ScalableLine(tessellation, visualWidth, color, undefined, false, true);
|
||||
this.marker = new ScalableLine(tessellation, markerWidth, defaultMarkColor, undefined, false, true);
|
||||
this.picker = new ScalableLine(tessellation, 10, 0xFA8072, undefined, false, true);
|
||||
this.representation = new ScalableLine(tessellation, visualWidth, color, undefined, false, true, offset);
|
||||
this.marker = new ScalableLine(tessellation, markerWidth, defaultMarkColor, undefined, false, true, offset);
|
||||
this.picker = new ScalableLine(tessellation, 10, 0xFA8072, undefined, false, true, offset);
|
||||
this.marker.visible = false;
|
||||
this.picker.material.visible = false;
|
||||
|
||||
|
|
@ -19,14 +19,25 @@ export class CurveBasedView extends View {
|
|||
this.rootGroup.add(this.representation);
|
||||
this.rootGroup.add(this.marker);
|
||||
this.rootGroup.add(this.picker);
|
||||
this.picker.onMouseEnter = () => {
|
||||
this.ctx.highlightService.highlight(this.model.id);
|
||||
}
|
||||
this.picker.onMouseLeave = () => {
|
||||
this.ctx.highlightService.unHighlight(this.model.id);
|
||||
}
|
||||
}
|
||||
|
||||
mark(color) {
|
||||
this.marker.visible = true;
|
||||
}
|
||||
|
||||
withdraw(color) {
|
||||
this.marker.visible = false;
|
||||
updateVisuals() {
|
||||
const markColor = this.markColor;
|
||||
if (!markColor) {
|
||||
this.marker.visible = false;
|
||||
this.representation.visible = true;
|
||||
} else {
|
||||
this.marker.material.color.set(markColor);
|
||||
this.marker.visible = true;
|
||||
this.representation.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import {CSYS_SIZE_MODEL} from '../../craft/datum/csysObject';
|
|||
|
||||
export default class DatumView extends View {
|
||||
|
||||
constructor(datum, viewer, beginOperation, selectDatum, showDatumMenu, isReadOnly) {
|
||||
super(datum);
|
||||
constructor(ctx, datum, beginOperation, selectDatum, showDatumMenu, isReadOnly) {
|
||||
super(ctx, datum);
|
||||
|
||||
const viewer = ctx.viewer;
|
||||
|
||||
class MenuButton extends Mesh {
|
||||
|
||||
|
|
@ -137,9 +139,9 @@ export default class DatumView extends View {
|
|||
setAttribute(this.rootGroup, DATUM, this);
|
||||
setAttribute(this.rootGroup, View.MARKER, this);
|
||||
|
||||
this.xAxisView = new DatumAxisView(this.model.xAxis, dv.csysObj.xAxis);
|
||||
this.yAxisView = new DatumAxisView(this.model.yAxis, dv.csysObj.yAxis);
|
||||
this.zAxisView = new DatumAxisView(this.model.zAxis, dv.csysObj.zAxis);
|
||||
this.xAxisView = new DatumAxisView(ctx, this.model.xAxis, dv.csysObj.xAxis);
|
||||
this.yAxisView = new DatumAxisView(ctx, this.model.yAxis, dv.csysObj.yAxis);
|
||||
this.zAxisView = new DatumAxisView(ctx, this.model.zAxis, dv.csysObj.zAxis);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
@ -178,8 +180,8 @@ class AffordanceBox extends Mesh {
|
|||
|
||||
class DatumAxisView extends View {
|
||||
|
||||
constructor(model, axisArrow) {
|
||||
super(model);
|
||||
constructor(ctx, model, axisArrow) {
|
||||
super(ctx, model);
|
||||
this.axisArrow = axisArrow;
|
||||
setAttribute(this.axisArrow.handle, DATUM_AXIS, this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
import {CurveBasedView} from './curveBasedView';
|
||||
|
||||
const MarkerTable = [
|
||||
{
|
||||
type: 'selection',
|
||||
priority: 10,
|
||||
colors: [0xc42720],
|
||||
},
|
||||
{
|
||||
type: 'highlight',
|
||||
priority: 1,
|
||||
colors: [0xffebcd, 0xFF00FF],
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgeView extends CurveBasedView {
|
||||
|
||||
constructor(edge) {
|
||||
constructor(ctx, edge) {
|
||||
let brepEdge = edge.brepEdge;
|
||||
let tess = brepEdge.data.tessellation ? brepEdge.data.tessellation : brepEdge.curve.tessellateToData();
|
||||
super(edge, tess, 2, 3, 0x2B3856, 0xc42720);
|
||||
super(ctx, edge, tess, 2, 4, 0x2B3856, 0xc42720, false, MarkerTable);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@ import * as SceneGraph from 'scene/sceneGraph';
|
|||
import {SketchObjectView} from './sketchObjectView';
|
||||
import {View} from './view';
|
||||
import {SketchLoopView} from './sketchLoopView';
|
||||
import {createSolidMaterial} from "cad/scene/wrappers/sceneObject";
|
||||
import {SketchMesh} from "cad/scene/views/shellView";
|
||||
import {Geometry} from "three";
|
||||
|
||||
export class SketchingView extends View {
|
||||
|
||||
constructor(face) {
|
||||
super(face);
|
||||
constructor(ctx, face, parent) {
|
||||
super(ctx, face, parent);
|
||||
this.sketchGroup = SceneGraph.createGroup();
|
||||
this.sketchObjectViews = [];
|
||||
this.sketchLoopViews = [];
|
||||
|
|
@ -24,17 +27,25 @@ export class SketchingView extends View {
|
|||
|
||||
const sketchTr = this.model.sketchToWorldTransformation;
|
||||
for (let sketchObject of this.model.sketchObjects) {
|
||||
let sov = new SketchObjectView(sketchObject, sketchTr);
|
||||
let sov = new SketchObjectView(this.ctx, sketchObject, sketchTr);
|
||||
SceneGraph.addToGroup(this.sketchGroup, sov.rootGroup);
|
||||
this.sketchObjectViews.push(sov);
|
||||
}
|
||||
this.model.sketchLoops.forEach(mLoop => {
|
||||
let loopView = new SketchLoopView(mLoop);
|
||||
let loopView = new SketchLoopView(this.ctx, mLoop);
|
||||
SceneGraph.addToGroup(this.sketchGroup, loopView.rootGroup);
|
||||
this.sketchLoopViews.push(loopView);
|
||||
});
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
updateVisuals() {
|
||||
this.mesh.material.color.set(this.markColor||this.parent.markColor||this.color||this.parent.color||NULL_COLOR);
|
||||
}
|
||||
|
||||
disposeSketch() {
|
||||
this.sketchObjectViews.forEach(o => o.dispose());
|
||||
this.sketchLoopViews.forEach(o => o.dispose());
|
||||
|
|
@ -51,34 +62,40 @@ export class SketchingView extends View {
|
|||
|
||||
export class FaceView extends SketchingView {
|
||||
|
||||
constructor(face, geometry) {
|
||||
super(face);
|
||||
this.geometry = geometry;
|
||||
constructor(ctx, face, parent, skin) {
|
||||
super(ctx, face, parent);
|
||||
const geom = new Geometry();
|
||||
geom.dynamic = true;
|
||||
this.geometry = geom;
|
||||
|
||||
this.material = createSolidMaterial(skin);
|
||||
this.meshFaces = [];
|
||||
let off = geometry.faces.length;
|
||||
|
||||
const off = geom.faces.length;
|
||||
if (face.brepFace.data.tessellation) {
|
||||
tessDataToGeom(face.brepFace.data.tessellation.data, geometry)
|
||||
tessDataToGeom(face.brepFace.data.tessellation.data, geom)
|
||||
} else {
|
||||
brepFaceToGeom(face.brepFace, geometry);
|
||||
brepFaceToGeom(face.brepFace, geom);
|
||||
}
|
||||
for (let i = off; i < geometry.faces.length; i++) {
|
||||
const meshFace = geometry.faces[i];
|
||||
for (let i = off; i < geom.faces.length; i++) {
|
||||
const meshFace = geom.faces[i];
|
||||
this.meshFaces.push(meshFace);
|
||||
setAttribute(meshFace, FACE, this);
|
||||
}
|
||||
geom.mergeVertices();
|
||||
this.mesh = new SketchMesh(geom, this.material);
|
||||
this.mesh.onMouseEnter = () => {
|
||||
this.ctx.highlightService.highlight(this.model.id);
|
||||
}
|
||||
this.mesh.onMouseLeave = () => {
|
||||
this.ctx.highlightService.unHighlight(this.model.id);
|
||||
}
|
||||
this.rootGroup.add(this.mesh);
|
||||
}
|
||||
|
||||
mark(color) {
|
||||
this.updateColor(color || SELECTION_COLOR);
|
||||
}
|
||||
|
||||
withdraw(color) {
|
||||
this.updateColor(null);
|
||||
}
|
||||
|
||||
updateColor(color) {
|
||||
setFacesColor(this.meshFaces, color||this.color);
|
||||
this.geometry.colorsNeedUpdate = true;
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,4 +110,4 @@ export function setFacesColor(faces, color) {
|
|||
}
|
||||
|
||||
export const NULL_COLOR = new THREE.Color();
|
||||
export const SELECTION_COLOR = 0xffff80;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import {setAttribute} from 'scene/objectData';
|
||||
import {FACE, SHELL} from '../../model/entities';
|
||||
import {NULL_COLOR, SELECTION_COLOR, setFacesColor, SketchingView} from './faceView';
|
||||
import {SketchingView} from './faceView';
|
||||
import {View} from './view';
|
||||
import {SketchMesh} from './shellView';
|
||||
|
||||
export class OpenFaceShellView extends View {
|
||||
|
||||
constructor(shell) {
|
||||
super(shell);
|
||||
this.openFace = new OpenFaceView(shell.face);
|
||||
constructor(ctx, shell) {
|
||||
super(ctx, shell);
|
||||
this.openFace = new OpenFaceView(ctx, shell.face, this);
|
||||
setAttribute(this.rootGroup, SHELL, this);
|
||||
setAttribute(this.rootGroup, View.MARKER, this);
|
||||
}
|
||||
|
|
@ -24,8 +24,8 @@ export class OpenFaceShellView extends View {
|
|||
|
||||
export class OpenFaceView extends SketchingView {
|
||||
|
||||
constructor(mFace) {
|
||||
super(mFace);
|
||||
constructor(ctx, mFace, parent) {
|
||||
super(ctx, mFace, parent);
|
||||
this.material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: THREE.FaceColors,
|
||||
// color: 0xB0C4DE,
|
||||
|
|
@ -57,10 +57,15 @@ export class OpenFaceView extends SketchingView {
|
|||
geometry.computeFaceNormals();
|
||||
this.mesh = new SketchMesh(geometry, this.material);
|
||||
this.rootGroup.add(this.mesh);
|
||||
this.mesh.onMouseEnter = () => {
|
||||
this.ctx.highlightService.highlight(this.model.id);
|
||||
}
|
||||
this.mesh.onMouseLeave = () => {
|
||||
this.ctx.highlightService.unHighlight(this.model.id);
|
||||
}
|
||||
}
|
||||
|
||||
updateBounds() {
|
||||
let markedColor = this.markedColor;
|
||||
this.dropGeometry();
|
||||
|
||||
let bounds2d = [];
|
||||
|
|
@ -72,7 +77,7 @@ export class OpenFaceView extends SketchingView {
|
|||
surface.northEastPoint(), surface.northWestPoint()];
|
||||
|
||||
this.createGeometry();
|
||||
this.updateColor(markedColor || this.color);
|
||||
this.updateVisuals();
|
||||
}
|
||||
|
||||
traverse(visitor) {
|
||||
|
|
@ -84,27 +89,6 @@ export class OpenFaceView extends SketchingView {
|
|||
this.updateBounds();
|
||||
}
|
||||
|
||||
mark(color) {
|
||||
this.updateColor(color || SELECTION_COLOR);
|
||||
}
|
||||
|
||||
withdraw(color) {
|
||||
this.updateColor(null);
|
||||
}
|
||||
|
||||
updateColor(color) {
|
||||
setFacesColor(this.mesh.geometry.faces, color||this.color);
|
||||
this.mesh.geometry.colorsNeedUpdate = true;
|
||||
}
|
||||
|
||||
get markedColor() {
|
||||
let face = this.mesh && this.mesh.geometry && this.mesh.geometry.faces[0];
|
||||
if (face) {
|
||||
return face.color === NULL_COLOR ? null : face.color;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.dropGeometry();
|
||||
this.material.dispose();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import {View} from './view';
|
||||
import * as SceneGraph from 'scene/sceneGraph';
|
||||
import {getAttribute, setAttribute} from 'scene/objectData';
|
||||
import {createSolidMaterial} from '../wrappers/sceneObject';
|
||||
import {FaceView, SELECTION_COLOR} from './faceView';
|
||||
import {EdgeView} from './edgeView';
|
||||
import {FACE, SHELL} from '../../model/entities';
|
||||
import {FACE, LOOP, SHELL} from '../../model/entities';
|
||||
import {Mesh} from 'three';
|
||||
import {VertexView} from "./vertexView";
|
||||
import {MSketchLoop} from "cad/model/mloop";
|
||||
|
||||
export class ShellView extends View {
|
||||
|
||||
constructor(shell, skin, viewer) {
|
||||
super(shell);
|
||||
constructor(ctx, shell, skin) {
|
||||
super(ctx, shell);
|
||||
|
||||
this.material = createSolidMaterial(skin);
|
||||
this.rootGroup = SceneGraph.createGroup();
|
||||
this.edgeGroup = SceneGraph.createGroup();
|
||||
this.vertexGroup = SceneGraph.createGroup();
|
||||
|
|
@ -27,29 +26,20 @@ export class ShellView extends View {
|
|||
setAttribute(this.rootGroup, SHELL, this);
|
||||
setAttribute(this.rootGroup, View.MARKER, this);
|
||||
|
||||
const geometry = new THREE.Geometry();
|
||||
geometry.dynamic = true;
|
||||
this.mesh = new SketchMesh(geometry, this.material);
|
||||
// this.mesh.visible = false;
|
||||
this.rootGroup.add(this.mesh);
|
||||
|
||||
|
||||
const geom = this.mesh.geometry;
|
||||
for (let face of shell.faces) {
|
||||
const faceView = new FaceView(face, geom);
|
||||
const faceView = new FaceView(ctx, face, this, skin);
|
||||
this.faceViews.push(faceView);
|
||||
this.rootGroup.add(faceView.rootGroup);
|
||||
}
|
||||
geom.mergeVertices();
|
||||
|
||||
for (let edge of shell.edges) {
|
||||
const edgeView = new EdgeView(edge);
|
||||
const edgeView = new EdgeView(ctx, edge);
|
||||
SceneGraph.addToGroup(this.edgeGroup, edgeView.rootGroup);
|
||||
this.edgeViews.push(edgeView);
|
||||
}
|
||||
|
||||
for (let vertex of shell.vertices) {
|
||||
const vertexView = new VertexView(vertex, viewer);
|
||||
const vertexView = new VertexView(ctx, vertex);
|
||||
SceneGraph.addToGroup(this.vertexGroup, vertexView.rootGroup);
|
||||
this.vertexViews.push(vertexView);
|
||||
}
|
||||
|
|
@ -58,29 +48,24 @@ export class ShellView extends View {
|
|||
this.model.location$.attach(loc => {
|
||||
loc.setToMatrix4x4(this.rootGroup.matrix);
|
||||
this.rootGroup.matrixWorldNeedsUpdate = true;
|
||||
viewer.requestRender();
|
||||
ctx.viewer.requestRender();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
mark(color) {
|
||||
this.faceViews.forEach(faceView => faceView.setColor(color || SELECTION_COLOR));
|
||||
}
|
||||
|
||||
withdraw(color) {
|
||||
this.faceViews.forEach(faceView => faceView.setColor(null));
|
||||
}
|
||||
|
||||
traverse(visitor) {
|
||||
super.traverse(visitor);
|
||||
traverse(visitor, includeSelf = true) {
|
||||
super.traverse(visitor, includeSelf);
|
||||
this.faceViews.forEach(f => f.traverse(visitor));
|
||||
this.edgeViews.forEach(e => e.traverse(visitor));
|
||||
this.vertexViews.forEach(e => e.traverse(visitor));
|
||||
}
|
||||
|
||||
updateVisuals(color) {
|
||||
super.updateVisuals(color);
|
||||
this.faceViews.forEach(f => f.updateVisuals(color));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.mesh.material.dispose();
|
||||
this.mesh.geometry.dispose();
|
||||
for (let faceView of this.faceViews) {
|
||||
faceView.dispose();
|
||||
}
|
||||
|
|
@ -100,22 +85,4 @@ export class SketchMesh extends Mesh {
|
|||
super(geometry, material);
|
||||
}
|
||||
|
||||
passRayCast(hits) {
|
||||
for (let hit of hits) {
|
||||
if (hit.object === this && hit.face) {
|
||||
let faceView = getAttribute(hit.face, FACE);
|
||||
if (faceView) {
|
||||
if (faceView.sketchLoopViews.find(v => hits.find(h => v.mesh.geometry.faces.indexOf(h.face) !== -1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
passMouseEvent(e) {
|
||||
return this.passRayCast(e.hits);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -9,21 +9,40 @@ import Vector from 'math/vector';
|
|||
import {LOOP} from '../../model/entities';
|
||||
import {setAttribute} from 'scene/objectData';
|
||||
|
||||
export class SketchLoopView extends MarkTracker(View) {
|
||||
constructor(mLoop) {
|
||||
super(mLoop);
|
||||
const HIGHLIGHT_COLOR = 0xDBFFD9;
|
||||
const SELECT_COLOR = 0xCCEFCA;
|
||||
|
||||
const MarkerTable = [
|
||||
{
|
||||
type: 'selection',
|
||||
priority: 10,
|
||||
colors: [SELECT_COLOR],
|
||||
},
|
||||
{
|
||||
type: 'highlight',
|
||||
priority: 1,
|
||||
colors: [HIGHLIGHT_COLOR],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export class SketchLoopView extends View {
|
||||
|
||||
constructor(ctx, mLoop) {
|
||||
super(ctx, mLoop, MarkerTable);
|
||||
this.rootGroup = SceneGraph.createGroup();
|
||||
|
||||
const geometry = new Geometry();
|
||||
geometry.dynamic = true;
|
||||
this.mesh = new Mesh(geometry, createSolidMaterial({
|
||||
color: SKETCH_LOOP_DEFAULT_HIGHLIGHT_COLOR,
|
||||
// color: HIGHLIGHT_COLOR,
|
||||
side: DoubleSide,
|
||||
// transparent: true,
|
||||
depthTest: true,
|
||||
depthWrite: false,
|
||||
// depthTest: true,
|
||||
// depthWrite: false,
|
||||
polygonOffset: true,
|
||||
polygonOffsetFactor: -4,
|
||||
polygonOffsetFactor: -1.0, // should less than offset of loop lines
|
||||
polygonOffsetUnits: -1.0,
|
||||
visible: false
|
||||
}));
|
||||
let surface = mLoop.face.surface;
|
||||
|
|
@ -45,27 +64,23 @@ export class SketchLoopView extends MarkTracker(View) {
|
|||
}
|
||||
|
||||
this.rootGroup.add(this.mesh);
|
||||
this.mesh.onMouseEnter = (e) => {
|
||||
this.mark(SKETCH_LOOP_DEFAULT_HIGHLIGHT_COLOR, 5);
|
||||
e.viewer.requestRender();
|
||||
};
|
||||
this.mesh.onMouseLeave = (e) => {
|
||||
this.withdraw(5);
|
||||
e.viewer.requestRender();
|
||||
};
|
||||
this.mesh.onMouseEnter = () => {
|
||||
this.ctx.highlightService.highlight(this.model.id);
|
||||
}
|
||||
this.mesh.onMouseLeave = () => {
|
||||
this.ctx.highlightService.unHighlight(this.model.id);
|
||||
}
|
||||
this.mesh.raycastPriority = 10;
|
||||
}
|
||||
|
||||
mark(color = SKETCH_LOOP_DEFAULT_SELECT_COLOR, priority = 10) {
|
||||
super.mark(color, priority);
|
||||
}
|
||||
|
||||
markImpl(color) {
|
||||
this.mesh.material.visible = true;
|
||||
this.mesh.material.color.setHex(color)
|
||||
}
|
||||
|
||||
withdrawImpl() {
|
||||
this.mesh.material.visible = false;
|
||||
updateVisuals() {
|
||||
const markColor = this.markColor;
|
||||
if (!markColor) {
|
||||
this.mesh.material.visible = false;
|
||||
} else {
|
||||
this.mesh.material.color.set(markColor);
|
||||
this.mesh.material.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
@ -74,6 +89,3 @@ export class SketchLoopView extends MarkTracker(View) {
|
|||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
const SKETCH_LOOP_DEFAULT_HIGHLIGHT_COLOR = 0xDBFFD9;
|
||||
const SKETCH_LOOP_DEFAULT_SELECT_COLOR = 0xCCEFCA;
|
||||
|
|
@ -2,9 +2,9 @@ import {CurveBasedView} from './curveBasedView';
|
|||
|
||||
export class SketchObjectView extends CurveBasedView {
|
||||
|
||||
constructor(mSketchObject, sketchToWorldTransformation) {
|
||||
constructor(ctx, mSketchObject, sketchToWorldTransformation) {
|
||||
const color = mSketchObject.construction ? 0x777777 : 0x0000FF;
|
||||
const tess = mSketchObject.sketchPrimitive.tessellate(10).map(sketchToWorldTransformation.apply).map(v => v.data());
|
||||
super(mSketchObject, tess, 3, 4, color, 0x49FFA5);
|
||||
super(ctx, mSketchObject, tess, 3, 4, color, 0x49FFA5, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import {ConstantScaleGroup} from "scene/scaleHelper";
|
|||
|
||||
export class VertexView extends View {
|
||||
|
||||
constructor(vertex, viewer) {
|
||||
super(vertex);
|
||||
this.rootGroup = new VertexObject(viewer, 50, 100, () => this.rootGroup.position);
|
||||
constructor(ctx, vertex) {
|
||||
super(ctx, vertex);
|
||||
this.rootGroup = new VertexObject(ctx.viewer, 50, 100, () => this.rootGroup.position);
|
||||
|
||||
this.rootGroup.position.x = vertex.brepVertex.point.x;
|
||||
this.rootGroup.position.y = vertex.brepVertex.point.y;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,73 @@
|
|||
import {createFunctionList} from "gems/func";
|
||||
import {Color} from "three";
|
||||
import {createIndex} from "gems/indexed";
|
||||
|
||||
const MarkerTable = [
|
||||
{
|
||||
type: 'selection',
|
||||
priority: 10,
|
||||
colors: [0xffff80],
|
||||
},
|
||||
{
|
||||
type: 'highlight',
|
||||
priority: 1,
|
||||
colors: [0xffebcd, 0xffdf00],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export class View {
|
||||
|
||||
static MARKER = 'ModelView';
|
||||
|
||||
disposers = createFunctionList();
|
||||
color = new Color();
|
||||
|
||||
constructor(model) {
|
||||
constructor(ctx, model, parent, markerTable = MarkerTable) {
|
||||
this.ctx = ctx;
|
||||
this.model = model;
|
||||
this.parent = parent;
|
||||
model.ext.view = this;
|
||||
this.marks = [];
|
||||
this.markerTable = createIndex(markerTable, i => i.type);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
}
|
||||
|
||||
get markColor() {
|
||||
if (this.marks.length !== 0) {
|
||||
const baseMark = this.marks[0];
|
||||
return baseMark.colors[Math.min(baseMark.colors.length, this.marks.length) - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setVisible(value) {
|
||||
}
|
||||
|
||||
mark(color, priority) {
|
||||
}
|
||||
|
||||
withdraw(priority) {
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
if (!color) {
|
||||
this.color = new Color();
|
||||
} else {
|
||||
this.color.setStyle(color);
|
||||
mark(type = 'selection') {
|
||||
const marker = this.markerTable[type];
|
||||
const found = this.marks.find(c => c.type === marker.type);
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
this.marks.push(marker);
|
||||
this.marks.sort((c1, c2) => c1.priority - c2.priority);
|
||||
this.updateVisuals();
|
||||
}
|
||||
|
||||
traverse(visitor) {
|
||||
visitor(this);
|
||||
withdraw(type) {
|
||||
this.marks = this.marks.filter(c => c.type !== type)
|
||||
this.updateVisuals();
|
||||
}
|
||||
|
||||
updateVisuals() {
|
||||
}
|
||||
|
||||
traverse(visitor, includeSelf = true) {
|
||||
if (includeSelf) {
|
||||
visitor(this);
|
||||
}
|
||||
}
|
||||
|
||||
addDisposer(disposer) {
|
||||
|
|
@ -48,8 +84,8 @@ export class View {
|
|||
|
||||
export const MarkTracker = ViewClass => class extends ViewClass {
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
constructor(ctx, model) {
|
||||
super(ctx, model);
|
||||
this.marks = new Map();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,5 +57,11 @@ export default [
|
|||
// actions: ['DATUM_MOVE', 'DATUM_ROTATE', 'DATUM_REBASE', '-', 'PLANE_FROM_DATUM', 'BOX', 'SPHERE', 'TORUS',
|
||||
// 'CONE', 'CYLINDER']
|
||||
},
|
||||
|
||||
{
|
||||
id: 'contextual',
|
||||
label: 'contextual',
|
||||
cssIcons: ['magic'],
|
||||
info: 'contextual actions',
|
||||
actions: ['ModelDisplayOptions', 'ModelAttributesEditor']
|
||||
}
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue