mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-22 00:14:31 +01:00
implement cut-extrude masking
This commit is contained in:
parent
7df876df50
commit
bf19c9577e
14 changed files with 152 additions and 38 deletions
|
|
@ -7,7 +7,7 @@ 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, top, className, children, onCancel, onOK, infoText, ...props}: {
|
||||
export function GenericWizard({topicId, title, icon, left, top, className, children, onCancel, onOK, infoText, ...props}: {
|
||||
topicId: string,
|
||||
title: string,
|
||||
left?: number,
|
||||
|
|
@ -15,13 +15,15 @@ export function GenericWizard({topicId, title, left, top, className, children, o
|
|||
onCancel: () => any,
|
||||
onOK: () => any,
|
||||
onKeyDown?: (e) => any,
|
||||
infoText?: any
|
||||
infoText?: any,
|
||||
icon?: any
|
||||
} & WindowProps ) {
|
||||
|
||||
return <Window initWidth={250}
|
||||
initLeft={left || 15}
|
||||
initTop={top}
|
||||
title={(title||'').toUpperCase()}
|
||||
icon={icon}
|
||||
className={cx('mid-typography', className)}
|
||||
controlButtons={<>
|
||||
<WindowControlButton title='help' onClick={(e) => DocumentationTopic$.next({
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@
|
|||
.title {
|
||||
padding: 0.3em 0 0.3em 0.5em;
|
||||
font-size: 1.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controlButtons {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default class Window extends React.Component<WindowProps> {
|
|||
|
||||
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.title}>{icon} <b>{title.toUpperCase()}</b></div>
|
||||
<div className={ls.controlButtons}>
|
||||
{controlButtons}
|
||||
{minimizable && <WindowControlButton onClick={onClose}>_</WindowControlButton>}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,8 @@ import {BooleanDefinition} from "cad/craft/schema/common/BooleanDefinition";
|
|||
import {UnitVector} from "math/vector";
|
||||
import {OperationDescriptor} from "cad/craft/operationPlugin";
|
||||
import {MObject} from "cad/model/mobject";
|
||||
import {Edge} from "brep/topo/edge";
|
||||
import {FaceRef} from "cad/craft/e0/OCCUtils";
|
||||
import {GetRef} from "cad/craft/e0/interact";
|
||||
import {
|
||||
FromMObjectProductionAnalyzer,
|
||||
FromSketchProductionAnalyzer, NULL_ANALYZER,
|
||||
ProductionAnalyzer, PushPullFaceProductionAnalyzer
|
||||
} from "cad/craft/production/productionAnalyzer";
|
||||
import {FromSketchProductionAnalyzer, PushPullFaceProductionAnalyzer} from "cad/craft/production/productionAnalyzer";
|
||||
|
||||
|
||||
interface ExtrudeParams {
|
||||
|
|
@ -28,10 +22,24 @@ interface ExtrudeParams {
|
|||
export const ExtrudeOperation: OperationDescriptor<ExtrudeParams> = {
|
||||
id: 'EXTRUDE',
|
||||
label: 'Extrude',
|
||||
dynamicLabel: params => {
|
||||
switch (params.boolean?.kind) {
|
||||
case 'SUBTRACT': return 'Extrude-Cut';
|
||||
case 'INTERSECT': return 'Extrude-Intersect';
|
||||
case 'UNION': return 'Extrude-Fuse';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
dynamicIcon: params => {
|
||||
switch (params.boolean?.kind) {
|
||||
case 'SUBTRACT': return 'img/cad/cut';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
icon: 'img/cad/extrude',
|
||||
info: 'extrudes 2D sketch',
|
||||
paramsInfo: ({length}) => `(${r(length)})`,
|
||||
run: (params: ExtrudeParams, ctx: ApplicationContext) => {
|
||||
run: (params: ExtrudeParams, ctx: ApplicationContext, rawParams: any) => {
|
||||
|
||||
let occ = ctx.occService;
|
||||
const oci = occ.commandInterface;
|
||||
|
|
@ -43,7 +51,15 @@ export const ExtrudeOperation: OperationDescriptor<ExtrudeParams> = {
|
|||
params.profiles
|
||||
|
||||
}
|
||||
const dir: UnitVector = (params.direction || face.normal()).normalize();
|
||||
let dir: UnitVector;
|
||||
if (params.direction) {
|
||||
dir = params.direction.normalize();
|
||||
} else {
|
||||
dir = face.normal().normalize();
|
||||
if (rawParams.direction?.flip) {
|
||||
dir._negate();
|
||||
}
|
||||
}
|
||||
let extrusionVector = dir._multiply(params.length);
|
||||
|
||||
let sketchId = face.id;
|
||||
|
|
@ -131,4 +147,21 @@ export const ExtrudeOperation: OperationDescriptor<ExtrudeParams> = {
|
|||
}
|
||||
|
||||
],
|
||||
|
||||
masking: [
|
||||
{
|
||||
id: 'CUT2',
|
||||
label: 'Cut',
|
||||
icon: 'img/cad/cut',
|
||||
info: 'makes a cut based on 2D sketch',
|
||||
maskingParams: {
|
||||
direction: {
|
||||
flip: true
|
||||
},
|
||||
boolean: {
|
||||
kind: 'SUBTRACT'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ export const ModelerWorkspace: WorkbenchConfig = {
|
|||
PatternLinearOperation,
|
||||
PatternRadialOperation,
|
||||
],
|
||||
actions: [GetVolume],
|
||||
actions: [],
|
||||
ui: {
|
||||
toolbar: [
|
||||
'DATUM_CREATE', 'PLANE', 'EditFace', '-',
|
||||
"EXTRUDE", "REVOLVE", "LOFT", "SWEEP", "-",
|
||||
"EXTRUDE", "CUT2", "REVOLVE", "LOFT", "SWEEP", "-",
|
||||
"BOOLEAN", "-",
|
||||
"SHELL_TOOL", "FILLET_TOOL", "SCALE_BODY","-",
|
||||
"MIRROR_BODY", "PATTERN_LINEAR", "PATTERN_RADIAL", "-",
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function activate(ctx: CoreContext) {
|
|||
}));
|
||||
}
|
||||
|
||||
const result = op.run(params, ctx);
|
||||
const result = op.run(params, ctx, request.params);
|
||||
// @ts-ignore
|
||||
return result.then ? result : Promise.resolve(result);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
export function roundValueForPresentation(value) {
|
||||
return value.toPrecision ? value.toPrecision(4).replace(/\.0$/, '') : value;
|
||||
}
|
||||
40
web/app/cad/craft/operationHelper.ts
Normal file
40
web/app/cad/craft/operationHelper.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import {Operation} from "cad/craft/operationPlugin";
|
||||
import {resolveIcon} from "cad/craft/ui/iconResolver";
|
||||
|
||||
export function roundValueForPresentation(value) {
|
||||
return value.toPrecision ? value.toPrecision(4).replace(/\.0$/, '') : value;
|
||||
}
|
||||
|
||||
export function operationIconToActionIcon(icon, appearance) {
|
||||
if (typeof icon === 'string') {
|
||||
appearance.icon32 = icon + '32.png';
|
||||
appearance.icon96 = icon + '96.png';
|
||||
} else {
|
||||
appearance.icon = resolveIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAppearance<R>(op: Operation<R>, params: R) {
|
||||
let appearance = op.appearance;
|
||||
if (!op.dynamicLabel && !op.dynamicIcon) {
|
||||
return appearance;
|
||||
}
|
||||
appearance = {...appearance};
|
||||
|
||||
if (op.dynamicLabel) {
|
||||
const label = op.dynamicLabel(params);
|
||||
if (label) {
|
||||
appearance.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (op.dynamicIcon) {
|
||||
const icon = op.dynamicIcon(params);
|
||||
if (icon) {
|
||||
operationIconToActionIcon(icon, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
return appearance;
|
||||
}
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import {FlattenPath, ParamsPath} from "cad/craft/wizard/wizardTypes";
|
|||
import {IconDeclaration} from "cad/icons/IconDeclaration";
|
||||
import {resolveIcon} from "cad/craft/ui/iconResolver";
|
||||
import {loadDeclarativeForm} from "cad/mdf/declarativeFormLoader";
|
||||
import {operationIconToActionIcon} from "cad/craft/operationHelper";
|
||||
|
||||
export function activate(ctx: ApplicationContext) {
|
||||
|
||||
|
|
@ -30,23 +31,23 @@ export function activate(ctx: ApplicationContext) {
|
|||
form = loadedForm;
|
||||
}
|
||||
|
||||
if (!label) {
|
||||
label = id;
|
||||
}
|
||||
|
||||
let appearance: ActionAppearance = {
|
||||
label,
|
||||
info
|
||||
};
|
||||
if (typeof icon === 'string') {
|
||||
appearance.icon32 = icon + '32.png';
|
||||
appearance.icon96 = icon + '96.png';
|
||||
} else {
|
||||
appearance.icon = resolveIcon(icon);
|
||||
}
|
||||
|
||||
operationIconToActionIcon(icon, appearance);
|
||||
|
||||
let opAction = {
|
||||
id: id,
|
||||
appearance,
|
||||
invoke: () => ctx.services.wizard.open(id),
|
||||
...actionParams
|
||||
};
|
||||
actions.push(opAction);
|
||||
|
||||
let schemaIndex = createSchemaIndex(schema);
|
||||
|
||||
|
|
@ -58,6 +59,29 @@ export function activate(ctx: ApplicationContext) {
|
|||
};
|
||||
|
||||
registry$.mutate(registry => registry[id] = operation);
|
||||
actions.push(opAction);
|
||||
|
||||
if (descriptor.masking) {
|
||||
descriptor.masking.forEach(masking => {
|
||||
|
||||
|
||||
const appearance = {
|
||||
...opAction.appearance,
|
||||
label: masking.label,
|
||||
info: masking.info,
|
||||
};
|
||||
|
||||
operationIconToActionIcon(masking.icon, appearance);
|
||||
|
||||
actions.push({
|
||||
...opAction,
|
||||
id: masking.id,
|
||||
appearance,
|
||||
invoke: () => ctx.services.wizard.open(id, masking.maskingParams),
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerOperations(operations) {
|
||||
|
|
@ -98,19 +122,30 @@ export interface Operation<R> extends OperationDescriptor<R>{
|
|||
schema: OperationSchema;
|
||||
}
|
||||
|
||||
type OpIcon = IconDeclaration | IconType | string | ((props: any) => JSX.Element);
|
||||
|
||||
export interface OperationDescriptor<R> {
|
||||
id: string;
|
||||
label: string;
|
||||
info: string;
|
||||
icon: IconDeclaration | IconType | string | ((props: any) => JSX.Element);
|
||||
icon: OpIcon;
|
||||
actionParams?: any;
|
||||
run: (request: R, opContext: CoreContext) => OperationResult | Promise<OperationResult>;
|
||||
run: (params: {}, ctx: CoreContext, rawParams: R) => OperationResult | Promise<OperationResult>;
|
||||
paramsInfo: (params: R) => string,
|
||||
previewGeomProvider?: (params: R) => OperationGeometryProvider,
|
||||
previewer?: any,
|
||||
form: FormDefinition | React.FunctionComponent,
|
||||
schema?: OperationSchema,
|
||||
onParamsUpdate?: (params, name, value) => void,
|
||||
masking?: {
|
||||
id: string,
|
||||
label: string;
|
||||
info: string,
|
||||
icon: OpIcon;
|
||||
maskingParams: any;
|
||||
}[],
|
||||
dynamicLabel?: (params: R) => string,
|
||||
dynamicIcon?: (params: R) => OpIcon,
|
||||
}
|
||||
|
||||
export interface OperationService {
|
||||
|
|
|
|||
|
|
@ -460,7 +460,6 @@ export class PushPullFaceProductionAnalyzer extends FromMObjectProductionAnalyze
|
|||
|
||||
const edgeMap = new Map();
|
||||
for (let he of this.baseFace.edges) {
|
||||
debugger
|
||||
const twin = he.twin();
|
||||
if (twin) {
|
||||
edgeMap.set(twin.loop.face.data.id, twin.edge);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {menuAboveElementHint} from '../../dom/menu/menuUtils';
|
|||
import {combine} from 'lstream';
|
||||
import {EMPTY_OBJECT} from 'gems/objects';
|
||||
import {aboveElement} from 'ui/positionUtils';
|
||||
import {resolveAppearance} from "cad/craft/operationHelper";
|
||||
|
||||
@connect(streams => combine(streams.craft.modifications, streams.operation.registry, streams.wizard.insertOperation)
|
||||
.map(([modifications, operationRegistry, insertOperationReq]) => ({
|
||||
|
|
@ -133,8 +134,9 @@ const HistoryItem = decoratorChain(
|
|||
)
|
||||
(
|
||||
function HistoryItem({index, pointer, modification, getOperation, toggle, selected, disabled, inProgress}) {
|
||||
let {appearance} = getOperation(modification.type);
|
||||
return <div className={cx(ls.historyItem, selected&&ls.selected, disabled&&ls.disabled, inProgress&&ls.inProgress)}
|
||||
const operation = getOperation(modification.type);
|
||||
const appearance = resolveAppearance(operation, modification.params);
|
||||
return <div className={cx(ls.historyItem, selected&&ls.selected, disabled&&ls.disabled, inProgress&&ls.inProgress)}
|
||||
onClick={e => toggle(index, modification, e.currentTarget)}>
|
||||
<ImgIcon className={ls.opIcon} url={appearance&&appearance.icon96} size={24} />
|
||||
<span className={ls.opIndex}>{ index + 1 }</span>
|
||||
|
|
@ -142,7 +144,7 @@ function HistoryItem({index, pointer, modification, getOperation, toggle, select
|
|||
});
|
||||
|
||||
const AddButton = mapContext((ctx) => ({
|
||||
showCraftMenu: e => ctx.actionService.action.run('menu.craft', menuAboveElementHint(e.currentTarget))
|
||||
showCraftMenu: e => ctx.actionService.action.run(params, ctx, 'menu.craft')
|
||||
}))(
|
||||
function AddButton({showCraftMenu}) {
|
||||
return <div className={ls.add} onClick={showCraftMenu}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Button from 'ui/components/controls/Button';
|
|||
import {removeAndDropDependants, removeFeature} from '../craftHistoryUtils';
|
||||
import RenderObject from 'ui/components/RenderObject';
|
||||
import {AppContext} from "cad/dom/components/AppContext";
|
||||
import {resolveAppearance} from "cad/craft/operationHelper";
|
||||
|
||||
function SelectedModificationInfo({ history, index,
|
||||
operationRegistry,
|
||||
|
|
@ -41,13 +42,14 @@ function SelectedModificationInfo({ history, index,
|
|||
console.warn('unknown operation ' + m.type);
|
||||
return;
|
||||
}
|
||||
let {appearance} = op;
|
||||
const appearance = resolveAppearance(op, m.params);
|
||||
|
||||
let indexNumber = index + 1;
|
||||
return <Widget visible={visible}
|
||||
left={lh && lh.x}
|
||||
bottom={95}
|
||||
flatRight={!lh}
|
||||
title={m.type + ' operation #' + indexNumber}
|
||||
title={appearance.label.toUpperCase() + ' operation #' + indexNumber}
|
||||
onClose={close}>
|
||||
<div className={ls.requestInfo}>
|
||||
<ImgIcon className={ls.pic} url={appearance && appearance.icon96} size={48}/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {FormEditContext, FormParamsContext, FormPathContext, FormStateContext} f
|
|||
import {GenericWizard} from "ui/components/GenericWizard";
|
||||
import {useStream} from "ui/effects";
|
||||
import {AppContext} from "cad/dom/components/AppContext";
|
||||
import {resolveAppearance} from "cad/craft/operationHelper";
|
||||
import ImgIcon from "ui/components/ImgIcon";
|
||||
|
||||
interface WizardProps {
|
||||
noFocus: boolean;
|
||||
|
|
@ -73,15 +75,16 @@ export default function Wizard(props: WizardProps) {
|
|||
};
|
||||
|
||||
let {left} = props;
|
||||
let wizardService = ctx.wizardService;
|
||||
|
||||
let title = (operation.label || operation.id).toUpperCase();
|
||||
let appearance = resolveAppearance(operation, workingRequest.params);
|
||||
let title = appearance.label.toUpperCase();
|
||||
let icon = <ImgIcon url={appearance.icon32} size={16}/>;
|
||||
|
||||
let Form = operation.form;
|
||||
|
||||
return <GenericWizard
|
||||
left={left}
|
||||
title={title}
|
||||
icon={icon}
|
||||
onClose={cancel}
|
||||
onKeyDown={onKeyDown}
|
||||
setFocus={focusFirstInput}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function sceneSynchronizer(ctx) {
|
|||
} else if (model instanceof MDatum) {
|
||||
modelView = new DatumView(ctx, model, wizard.open,
|
||||
datum => pickControl.pick(datum),
|
||||
e => action.run('menu.datum', e),
|
||||
e => action.run(params, ctx, 'menu.datum'),
|
||||
wizard.isInProgress);
|
||||
} else {
|
||||
console.warn('unsupported model ' + model);
|
||||
|
|
|
|||
Loading…
Reference in a new issue