mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-06 08:25:19 +01:00
revert md
This commit is contained in:
parent
07db5af1db
commit
0cbd38d36c
41 changed files with 678 additions and 107 deletions
|
|
@ -35,7 +35,7 @@ export default class CSys {
|
||||||
return new Matrix3x4().setBasisAxises(this.x, this.y, this.z);
|
return new Matrix3x4().setBasisAxises(this.x, this.y, this.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
get outTransformation() {
|
get outTransformation(): Matrix3x4 {
|
||||||
const mx = new Matrix3x4().setBasisAxises(this.x, this.y, this.z);
|
const mx = new Matrix3x4().setBasisAxises(this.x, this.y, this.z);
|
||||||
mx.tx = this.origin.x;
|
mx.tx = this.origin.x;
|
||||||
mx.ty = this.origin.y;
|
mx.ty = this.origin.y;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ export default class Folder extends React.Component{
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Title({children, isClosed, onClick}) {
|
export function Title({children, isClosed, onClick}) {
|
||||||
return <div className={ls.title} onClick={onClick}>
|
const titleCss = onClick ? {
|
||||||
|
cursor: 'pointer'
|
||||||
|
} : {};
|
||||||
|
return <div className={ls.title} onClick={onClick} style={titleCss}>
|
||||||
<span className={ls.handle}><Fa fw icon={isClosed ? 'chevron-right' : 'chevron-down'}/></span>
|
<span className={ls.handle}><Fa fw icon={isClosed ? 'chevron-right' : 'chevron-down'}/></span>
|
||||||
{' '}{children}
|
{' '}{children}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import {Title} from '../Folder';
|
||||||
export class StackSection extends React.Component {
|
export class StackSection extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {title, children} = this.props;
|
const {title, children, isClosed, onTitleClick} = this.props;
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<Title>{title}</Title>
|
<Title isClosed={isClosed} onClick={onTitleClick}>{title}</Title>
|
||||||
{children}
|
{!isClosed && children}
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ import {MFace} from "cad/model/mface";
|
||||||
import {ApplicationContext} from "context";
|
import {ApplicationContext} from "context";
|
||||||
import {MDFCommand} from "cad/mdf/mdf";
|
import {MDFCommand} from "cad/mdf/mdf";
|
||||||
import {EntityKind} from "cad/model/entities";
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import Vector from "math/vector";
|
||||||
|
import {BooleanDefinition} from "cad/craft/schema/common/BooleanDefinition";
|
||||||
|
|
||||||
interface ExtrudeParams {
|
interface ExtrudeParams {
|
||||||
length: number;
|
length: number;
|
||||||
face: MFace;
|
face: MFace;
|
||||||
|
direction?: Vector,
|
||||||
|
boolean: BooleanDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtrudeOperation: MDFCommand<ExtrudeParams> = {
|
const ExtrudeOperation: MDFCommand<ExtrudeParams> = {
|
||||||
|
|
@ -28,17 +32,49 @@ const ExtrudeOperation: MDFCommand<ExtrudeParams> = {
|
||||||
|
|
||||||
const occFaces = occ.utils.sketchToFaces(sketch, face.csys);
|
const occFaces = occ.utils.sketchToFaces(sketch, face.csys);
|
||||||
|
|
||||||
const shapeNames = occ.utils.prism(occFaces, [0, 0, params.length]);
|
const dir = (params.direction && params.direction) || face.normal();
|
||||||
|
|
||||||
const created = shapeNames.map(shapeName => occ.io.getShell(shapeName));
|
const extrusionVector = dir.normalize()._multiply(params.length).data();
|
||||||
|
|
||||||
|
const tools = occFaces.map((faceName, i) => {
|
||||||
|
const shapeName = "Tool" + i;
|
||||||
|
oci.prism(shapeName, faceName, ...extrusionVector)
|
||||||
|
|
||||||
|
// occIterateFaces(oc, shape, face => {
|
||||||
|
// let role;
|
||||||
|
// if (face.IsSame(prismAPI.FirstShape())) {
|
||||||
|
// role = "bottom";
|
||||||
|
// } else if (face.IsSame(prismAPI.LastShape())) {
|
||||||
|
// role = "top";
|
||||||
|
// } else {
|
||||||
|
// role = "sweep";
|
||||||
|
// }
|
||||||
|
// getProductionInfo(face).role = role;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// occIterateEdges(oc, wire, edge => {
|
||||||
|
// const generatedList = prismAPI.Generated(edge);
|
||||||
|
// occIterateListOfShape(oc, generatedList, face => {
|
||||||
|
// console.log(face);
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
return shapeName;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
consumed: [face.parent],
|
created: tools.map(shapeName => occ.io.getShell(shapeName)),
|
||||||
created
|
consumed: []
|
||||||
};
|
}
|
||||||
|
// return occ.utils.applyBooleanModifier(tools, params.boolean);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// useBoolean: {
|
||||||
|
// booleanField: 'boolean',
|
||||||
|
// impliedTargetField: 'face'
|
||||||
|
// },
|
||||||
|
|
||||||
form: [
|
form: [
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
|
@ -57,6 +93,33 @@ const ExtrudeOperation: MDFCommand<ExtrudeParams> = {
|
||||||
preselectionIndex: 0
|
preselectionIndex: 0
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// type: 'vector',
|
||||||
|
// name: 'direction',
|
||||||
|
// label: 'direction'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'boolean',
|
||||||
|
// name: 'boolean',
|
||||||
|
// label: 'boolean',
|
||||||
|
// defaultValue: {
|
||||||
|
// implyItFromField: 'face'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
type: 'vector',
|
||||||
|
name: 'direction',
|
||||||
|
label: 'direction',
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'boolean',
|
||||||
|
label: 'boolean',
|
||||||
|
optional: true,
|
||||||
|
defaultValue: 'NONE'
|
||||||
|
}
|
||||||
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import {SketchGeom} from "cad/sketch/sketchReader";
|
||||||
import {OCCService} from "cad/craft/e0/occService";
|
import {OCCService} from "cad/craft/e0/occService";
|
||||||
import {CoreContext} from "context";
|
import {CoreContext} from "context";
|
||||||
import CSys from "math/csys";
|
import CSys from "math/csys";
|
||||||
|
import {OperationResult} from "cad/craft/craftPlugin";
|
||||||
|
import {MShell} from "cad/model/mshell";
|
||||||
|
import {BooleanDefinition, BooleanKind} from "cad/craft/schema/common/BooleanDefinition";
|
||||||
|
import {bool} from "prop-types";
|
||||||
|
|
||||||
export interface OCCUtils {
|
export interface OCCUtils {
|
||||||
|
|
||||||
|
|
@ -11,8 +15,9 @@ export interface OCCUtils {
|
||||||
|
|
||||||
sketchToFaces(sketch: SketchGeom, csys: CSys): string[];
|
sketchToFaces(sketch: SketchGeom, csys: CSys): string[];
|
||||||
|
|
||||||
prism(faces: string[], dir: Vec3): string[];
|
// applyBoolean(tools: string[], kind: BooleanKind): string[];
|
||||||
|
|
||||||
|
applyBooleanModifier(tools: string[], booleanDef?: BooleanDefinition): OperationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOCCUtils(ctx: CoreContext): OCCUtils {
|
export function createOCCUtils(ctx: CoreContext): OCCUtils {
|
||||||
|
|
@ -32,39 +37,67 @@ export function createOCCUtils(ctx: CoreContext): OCCUtils {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prism(faces: string[], dir: Vec3): string[] {
|
|
||||||
const oci = ctx.occService.commandInterface;
|
|
||||||
return faces.map((faceName, i) => {
|
|
||||||
|
|
||||||
const shapeName = "Shape:" + i;
|
function applyBoolean(tools: string[], target: string[], kind: BooleanKind): string[] {
|
||||||
|
|
||||||
oci.prism(shapeName, faceName, ...dir)
|
|
||||||
|
|
||||||
// occIterateFaces(oc, shape, face => {
|
|
||||||
// let role;
|
|
||||||
// if (face.IsSame(prismAPI.FirstShape())) {
|
|
||||||
// role = "bottom";
|
|
||||||
// } else if (face.IsSame(prismAPI.LastShape())) {
|
|
||||||
// role = "top";
|
|
||||||
// } else {
|
|
||||||
// role = "sweep";
|
|
||||||
// }
|
|
||||||
// getProductionInfo(face).role = role;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// occIterateEdges(oc, wire, edge => {
|
|
||||||
// const generatedList = prismAPI.Generated(edge);
|
|
||||||
// occIterateListOfShape(oc, generatedList, face => {
|
|
||||||
// console.log(face);
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
return shapeName;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyBooleanModifier(tools: string[], booleanDef?: BooleanDefinition): OperationResult {
|
||||||
|
const occ = ctx.occService;
|
||||||
|
const oci = ctx.occService.commandInterface;
|
||||||
|
|
||||||
|
if (!booleanDef || booleanDef.kind === 'NONE') {
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: tools.map(shapeName => occ.io.getShell(shapeName)),
|
||||||
|
consumed: []
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const kind = booleanDef.kind;
|
||||||
|
|
||||||
|
let targets = booleanDef.targets;
|
||||||
|
if (targets.length === 0) {
|
||||||
|
targets = ctx.cadRegistry.shells;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetNames = targets.map((target, i) => {
|
||||||
|
const targetName = 'Target:' + i;
|
||||||
|
ctx.occService.io.pushModel(target, targetName)
|
||||||
|
return targetName;
|
||||||
|
});
|
||||||
|
|
||||||
|
oci.bfuzzyvalue(0.0001);
|
||||||
|
oci.bclearobjects();
|
||||||
|
oci.bcleartools();
|
||||||
|
|
||||||
|
targetNames.forEach(targetName => oci.baddobjects(targetName));
|
||||||
|
tools.forEach(toolName => oci.baddtools(toolName));
|
||||||
|
|
||||||
|
oci.bfillds();
|
||||||
|
oci.bcbuild("BooleanResult");
|
||||||
|
|
||||||
|
// oci.bopcommon("result");
|
||||||
|
//oci.bopfuse("result");
|
||||||
|
//oci.bopcut("result");
|
||||||
|
|
||||||
|
return {
|
||||||
|
consumed: targets,
|
||||||
|
created: tools.map(shapeName => occ.io.getShell(shapeName), targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wiresToFaces, sketchToFaces, prism
|
wiresToFaces, sketchToFaces, applyBooleanModifier
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
5
web/app/cad/craft/e0/OCI.d.ts
vendored
5
web/app/cad/craft/e0/OCI.d.ts
vendored
|
|
@ -54,6 +54,11 @@ interface OCCCommands {
|
||||||
*/
|
*/
|
||||||
BRepIntCS(...args: any[]);
|
BRepIntCS(...args: any[]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Generic webcad engine command
|
||||||
|
*/
|
||||||
|
EngineCommand(...args: any[]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
XProgress [+|-t] [+|-c] [+|-g]
|
XProgress [+|-t] [+|-c] [+|-g]
|
||||||
The options are:
|
The options are:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export const OCI: OCCCommandInterface = new Proxy({}, {
|
||||||
get: function (target, prop: string, receiver) {
|
get: function (target, prop: string, receiver) {
|
||||||
return prop in target ? target[prop] : function() {
|
return prop in target ? target[prop] : function() {
|
||||||
prop = prop.replace(/^_/, '');
|
prop = prop.replace(/^_/, '');
|
||||||
const args = Array.from(arguments).map(a => a + "");
|
const args = Array.from(arguments).map(a => JSON.stringify(a));
|
||||||
console.log("ARGUMENTS:", args);
|
console.log("ARGUMENTS:", args);
|
||||||
const returnCode = CallCommand(prop, [prop, ...args]);
|
const returnCode = CallCommand(prop, [prop, ...args]);
|
||||||
// if (returnCode !== 0) {
|
// if (returnCode !== 0) {
|
||||||
|
|
|
||||||
18
web/app/cad/craft/e0/occEngineInterface.ts
Normal file
18
web/app/cad/craft/e0/occEngineInterface.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import {OCCCommandInterface} from "cad/craft/e0/occCommandInterface";
|
||||||
|
import {MShell} from "cad/model/mshell";
|
||||||
|
import {MObject} from "cad/model/mobject";
|
||||||
|
import {Interrogate} from "cad/craft/e0/interact";
|
||||||
|
import {readShellEntityFromJson} from "cad/scene/wrappers/entityIO";
|
||||||
|
import {createOCCSketchLoader, OCCSketchLoader} from "cad/craft/e0/occSketchLoader";
|
||||||
|
|
||||||
|
export function createOCCEngineInterface(oci: OCCCommandInterface) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
io: {
|
||||||
|
pushModel: (params: {
|
||||||
|
name: string, operand: number,
|
||||||
|
}) => oci.EngineCommand(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import {OCCCommandInterface} from "cad/craft/e0/occCommandInterface";
|
import {OCI} from "cad/craft/e0/occCommandInterface";
|
||||||
import {MShell} from "cad/model/mshell";
|
import {MShell} from "cad/model/mshell";
|
||||||
import {MObject} from "cad/model/mobject";
|
import {MObject} from "cad/model/mobject";
|
||||||
import {Interrogate} from "cad/craft/e0/interact";
|
import {Interrogate} from "cad/craft/e0/interact";
|
||||||
import {readShellEntityFromJson} from "cad/scene/wrappers/entityIO";
|
import {readShellEntityFromJson} from "cad/scene/wrappers/entityIO";
|
||||||
import {createOCCSketchLoader, OCCSketchLoader} from "cad/craft/e0/occSketchLoader";
|
import {createOCCSketchLoader, OCCSketchLoader} from "cad/craft/e0/occSketchLoader";
|
||||||
|
import {CoreContext} from "context";
|
||||||
|
|
||||||
export interface OCCIO {
|
export interface OCCIO {
|
||||||
|
|
||||||
|
|
@ -16,7 +17,7 @@ export interface OCCIO {
|
||||||
sketchLoader: OCCSketchLoader
|
sketchLoader: OCCSketchLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOCCIO(oci: OCCCommandInterface): OCCIO {
|
export function createOCCIO(ctx: CoreContext): OCCIO {
|
||||||
|
|
||||||
function getShell(shapeName: string, consumed: MShell[]): MShell {
|
function getShell(shapeName: string, consumed: MShell[]): MShell {
|
||||||
const shapeJson = Interrogate(shapeName);
|
const shapeJson = Interrogate(shapeName);
|
||||||
|
|
@ -24,11 +25,10 @@ export function createOCCIO(oci: OCCCommandInterface): OCCIO {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushModel(model: MObject, name: string) {
|
function pushModel(model: MObject, name: string) {
|
||||||
|
ctx.occService.engineInterface.pushModel({
|
||||||
}
|
name,
|
||||||
|
operand: model.brepShell.data.externals.ptr
|
||||||
function anchorModel() {
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupRegistry() {
|
function cleanupRegistry() {
|
||||||
|
|
@ -38,7 +38,7 @@ export function createOCCIO(oci: OCCCommandInterface): OCCIO {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getShell, pushModel, cleanupRegistry,
|
getShell, pushModel, cleanupRegistry,
|
||||||
sketchLoader: createOCCSketchLoader(oci)
|
sketchLoader: createOCCSketchLoader(OCI)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import {CoreContext} from "context";
|
||||||
import {OCCCommandInterface, OCI} from "cad/craft/e0/occCommandInterface";
|
import {OCCCommandInterface, OCI} from "cad/craft/e0/occCommandInterface";
|
||||||
import {createOCCIO, OCCIO} from "cad/craft/e0/occIO";
|
import {createOCCIO, OCCIO} from "cad/craft/e0/occIO";
|
||||||
import {createOCCUtils, OCCUtils} from "cad/craft/e0/OCCUtils";
|
import {createOCCUtils, OCCUtils} from "cad/craft/e0/OCCUtils";
|
||||||
|
import {createOCCEngineInterface} from "cad/craft/e0/occEngineInterface";
|
||||||
|
|
||||||
export interface OCCService {
|
export interface OCCService {
|
||||||
|
|
||||||
|
|
@ -9,7 +10,9 @@ export interface OCCService {
|
||||||
|
|
||||||
commandInterface: OCCCommandInterface;
|
commandInterface: OCCCommandInterface;
|
||||||
|
|
||||||
utils: OCCUtils
|
utils: OCCUtils,
|
||||||
|
|
||||||
|
engineInterface: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOCCService(ctx: CoreContext): OCCService {
|
export function createOCCService(ctx: CoreContext): OCCService {
|
||||||
|
|
@ -18,10 +21,12 @@ export function createOCCService(ctx: CoreContext): OCCService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
io: createOCCIO(oci),
|
io: createOCCIO(ctx),
|
||||||
|
|
||||||
commandInterface: oci,
|
commandInterface: oci,
|
||||||
|
|
||||||
|
engineInterface: createOCCEngineInterface(oci),
|
||||||
|
|
||||||
utils: createOCCUtils(ctx)
|
utils: createOCCUtils(ctx)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {IconType} from "react-icons";
|
||||||
import {ActionAppearance} from "../actions/actionSystemPlugin";
|
import {ActionAppearance} from "../actions/actionSystemPlugin";
|
||||||
import {ApplicationContext, CoreContext} from "context";
|
import {ApplicationContext, CoreContext} from "context";
|
||||||
import {OperationResult} from "./craftPlugin";
|
import {OperationResult} from "./craftPlugin";
|
||||||
import {OperationSchema} from "cad/craft/schema/schema";
|
import {OperationFlattenSchema, OperationSchema, schemaIterator} from "cad/craft/schema/schema";
|
||||||
import {FieldWidgetProps, UIDefinition} from "cad/mdf/ui/uiDefinition";
|
import {FieldWidgetProps, UIDefinition} from "cad/mdf/ui/uiDefinition";
|
||||||
|
|
||||||
export function activate(ctx: ApplicationContext) {
|
export function activate(ctx: ApplicationContext) {
|
||||||
|
|
@ -35,7 +35,9 @@ export function activate(ctx: ApplicationContext) {
|
||||||
};
|
};
|
||||||
actions.push(opAction);
|
actions.push(opAction);
|
||||||
|
|
||||||
registry$.mutate(registry => registry[id] = Object.assign({appearance}, descriptor, {
|
const workingSchema = flattenSchema(descriptor.schema);
|
||||||
|
|
||||||
|
registry$.mutate(registry => registry[id] = Object.assign({appearance, workingSchema}, descriptor, {
|
||||||
run: (request, opContext) => runOperation(request, descriptor, opContext)
|
run: (request, opContext) => runOperation(request, descriptor, opContext)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +88,7 @@ export interface Operation<R> extends OperationDescriptor<R>{
|
||||||
icon96: string;
|
icon96: string;
|
||||||
icon: string|IconType;
|
icon: string|IconType;
|
||||||
};
|
};
|
||||||
|
workingSchema: OperationFlattenSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OperationDescriptor<R> {
|
export interface OperationDescriptor<R> {
|
||||||
|
|
@ -117,6 +120,14 @@ export interface OperationGeometryProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flattenSchema(schema: OperationSchema): OperationFlattenSchema {
|
||||||
|
const flatSchema = {} as OperationFlattenSchema;
|
||||||
|
schemaIterator(schema, (path, flattenedPath, schemaField) => {
|
||||||
|
flatSchema[flattenedPath] = schemaField;
|
||||||
|
});
|
||||||
|
return flatSchema;
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'context' {
|
declare module 'context' {
|
||||||
interface CoreContext {
|
interface CoreContext {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
orientation: {
|
orientation: {
|
||||||
type: 'enum',
|
type: 'string',
|
||||||
values: ['XY', 'XZ', 'ZY'],
|
enum: ['XY', 'XZ', 'ZY'],
|
||||||
defaultValue: 'XY'
|
defaultValue: 'XY'
|
||||||
},
|
},
|
||||||
parallelTo: {
|
parallelTo: {
|
||||||
|
|
|
||||||
12
web/app/cad/craft/schema/common/BooleanDefinition.ts
Normal file
12
web/app/cad/craft/schema/common/BooleanDefinition.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import {MObject} from "cad/model/mobject";
|
||||||
|
|
||||||
|
export type BooleanKind = 'NONE' | 'UNION' | 'SUBTRACT' | 'INTERSECT';
|
||||||
|
|
||||||
|
export interface BooleanDefinition {
|
||||||
|
|
||||||
|
kind: BooleanKind;
|
||||||
|
|
||||||
|
targets: MObject[];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +45,12 @@ function materializeParamsImpl(ctx: CoreContext,
|
||||||
const typeDef = TypeRegistry[md.type];
|
const typeDef = TypeRegistry[md.type];
|
||||||
value = typeDef.resolve(ctx, value, md as any, reportError.dot(field), materializeParamsImpl);
|
value = typeDef.resolve(ctx, value, md as any, reportError.dot(field), materializeParamsImpl);
|
||||||
|
|
||||||
|
if (md.resolve !== undefined) {
|
||||||
|
value = md.resolve(
|
||||||
|
ctx, value, md as any, reportError.dot(field), materializeParamsImpl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// if (md.type === Types.NUMBER) {
|
// if (md.type === Types.NUMBER) {
|
||||||
// try {
|
// try {
|
||||||
// const valueType = typeof value;
|
// const valueType = typeof value;
|
||||||
|
|
|
||||||
31
web/app/cad/craft/schema/resolvers/vectorResolver.ts
Normal file
31
web/app/cad/craft/schema/resolvers/vectorResolver.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {Materializer} from "cad/craft/schema/types/index";
|
||||||
|
import {CoreContext} from "context";
|
||||||
|
import {OperationParamsErrorReporter} from "cad/craft/schema/schema";
|
||||||
|
import Vector from "math/vector";
|
||||||
|
import {MObject} from "cad/model/mobject";
|
||||||
|
import {ObjectTypeSchema} from "cad/craft/schema/types/objectType";
|
||||||
|
|
||||||
|
type VectorInput = {
|
||||||
|
vectorEntity: MObject,
|
||||||
|
flip: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VectorResolver(ctx: CoreContext,
|
||||||
|
value: VectorInput,
|
||||||
|
md: ObjectTypeSchema,
|
||||||
|
reportError: OperationParamsErrorReporter,
|
||||||
|
materializer: Materializer): Vector {
|
||||||
|
|
||||||
|
if (!value.vectorEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vector = value.vectorEntity.toDirection();
|
||||||
|
if (!vector) {
|
||||||
|
throw 'unsupported entity type: ' + value.vectorEntity.TYPE;
|
||||||
|
}
|
||||||
|
if (value.flip) {
|
||||||
|
vector = vector.negate();
|
||||||
|
}
|
||||||
|
return vector;
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
import {Types} from "cad/craft/schema/types";
|
|
||||||
import {NumberTypeSchema} from "cad/craft/schema/types/numberType";
|
import {NumberTypeSchema} from "cad/craft/schema/types/numberType";
|
||||||
import {EntityTypeSchema} from "cad/craft/schema/types/entityType";
|
import {EntityTypeSchema} from "cad/craft/schema/types/entityType";
|
||||||
import {ArrayTypeSchema} from "cad/craft/schema/types/arrayType";
|
import {ArrayTypeSchema} from "cad/craft/schema/types/arrayType";
|
||||||
import {ObjectTypeSchema} from "cad/craft/schema/types/objectType";
|
import {ObjectTypeSchema} from "cad/craft/schema/types/objectType";
|
||||||
import {EnumTypeSchema} from "cad/craft/schema/types/enumType";
|
import {StringTypeSchema} from "cad/craft/schema/types/stringType";
|
||||||
|
import {BooleanTypeSchema} from "cad/craft/schema/types/booleanType";
|
||||||
|
|
||||||
export type SchemaField = NumberTypeSchema | EntityTypeSchema | ArrayTypeSchema | ObjectTypeSchema | EnumTypeSchema;
|
export type FlatSchemaField =
|
||||||
|
| ArrayTypeSchema
|
||||||
|
| EntityTypeSchema
|
||||||
|
| NumberTypeSchema
|
||||||
|
| StringTypeSchema
|
||||||
|
| BooleanTypeSchema;
|
||||||
|
|
||||||
|
export type SchemaField = FlatSchemaField | ObjectTypeSchema;
|
||||||
|
|
||||||
export type OperationSchema = {
|
export type OperationSchema = {
|
||||||
[key: string]: SchemaField;
|
[key: string]: SchemaField;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OperationFlattenSchema = {
|
||||||
|
[key: string]: FlatSchemaField;
|
||||||
|
};
|
||||||
|
|
||||||
export interface BaseSchemaField {
|
export interface BaseSchemaField {
|
||||||
defaultValue: Coercable,
|
defaultValue: Coercable,
|
||||||
optional: boolean,
|
optional: boolean,
|
||||||
|
|
@ -30,4 +41,26 @@ export type OperationParamsError = {
|
||||||
|
|
||||||
export type OperationParamsErrorReporter = ((msg: string) => void) & {
|
export type OperationParamsErrorReporter = ((msg: string) => void) & {
|
||||||
dot: (pathPart: string|number) => OperationParamsErrorReporter
|
dot: (pathPart: string|number) => OperationParamsErrorReporter
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function schemaIterator(schema: OperationSchema,
|
||||||
|
callback: (path: string[], flattenedPath: string, field: FlatSchemaField) => void) {
|
||||||
|
|
||||||
|
function inorder(schema: OperationSchema, parentPath: string[]) {
|
||||||
|
|
||||||
|
Object.keys(schema).forEach(key => {
|
||||||
|
const path = [...parentPath, key]
|
||||||
|
const flattenedPath = path.join('/');
|
||||||
|
const schemaField = schema[key];
|
||||||
|
|
||||||
|
|
||||||
|
if (schemaField.type === 'object') {
|
||||||
|
inorder(schemaField.schema, path);
|
||||||
|
} else {
|
||||||
|
callback(path, flattenedPath, schemaField as FlatSchemaField);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
inorder(schema, []);
|
||||||
|
}
|
||||||
20
web/app/cad/craft/schema/types/booleanType.ts
Normal file
20
web/app/cad/craft/schema/types/booleanType.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {Materializer, Type, Types} from "cad/craft/schema/types/index";
|
||||||
|
import {CoreContext} from "context";
|
||||||
|
import {BaseSchemaField, OperationParamsErrorReporter} from "cad/craft/schema/schema";
|
||||||
|
|
||||||
|
export interface BooleanTypeSchema extends BaseSchemaField {
|
||||||
|
|
||||||
|
type: Types.boolean,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BooleanType: Type<any, boolean, BooleanTypeSchema> = {
|
||||||
|
|
||||||
|
resolve(ctx: CoreContext,
|
||||||
|
value: any,
|
||||||
|
md: BooleanTypeSchema,
|
||||||
|
reportError: OperationParamsErrorReporter,
|
||||||
|
materializer: Materializer): boolean {
|
||||||
|
return !!value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,8 @@ import {ArrayType} from "cad/craft/schema/types/arrayType";
|
||||||
import {EntityType} from "cad/craft/schema/types/entityType";
|
import {EntityType} from "cad/craft/schema/types/entityType";
|
||||||
import {NumberType} from "cad/craft/schema/types/numberType";
|
import {NumberType} from "cad/craft/schema/types/numberType";
|
||||||
import {ObjectType} from "cad/craft/schema/types/objectType";
|
import {ObjectType} from "cad/craft/schema/types/objectType";
|
||||||
import {EnumType} from "cad/craft/schema/types/enumType";
|
import {StringType} from "cad/craft/schema/types/stringType";
|
||||||
|
import {BooleanType} from "cad/craft/schema/types/booleanType";
|
||||||
|
|
||||||
export type Materializer = (ctx: CoreContext,
|
export type Materializer = (ctx: CoreContext,
|
||||||
params: OperationParams,
|
params: OperationParams,
|
||||||
|
|
@ -22,16 +23,18 @@ export interface Type<IN, OUT, METADATA extends SchemaField> {
|
||||||
|
|
||||||
export enum Types {
|
export enum Types {
|
||||||
array = 'array',
|
array = 'array',
|
||||||
|
object = 'object',
|
||||||
entity = 'entity',
|
entity = 'entity',
|
||||||
number = 'number',
|
number = 'number',
|
||||||
object = 'object',
|
boolean = 'boolean',
|
||||||
enum = 'enum',
|
string = 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TypeRegistry = {
|
export const TypeRegistry = {
|
||||||
[Types.array]: ArrayType,
|
[Types.array]: ArrayType,
|
||||||
|
[Types.object]: ObjectType,
|
||||||
[Types.entity]: EntityType,
|
[Types.entity]: EntityType,
|
||||||
[Types.number]: NumberType,
|
[Types.number]: NumberType,
|
||||||
[Types.object]: ObjectType,
|
[Types.boolean]: BooleanType,
|
||||||
[Types.enum]: EnumType,
|
[Types.string]: StringType,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export interface ObjectTypeSchema extends BaseSchemaField {
|
||||||
|
|
||||||
schema: OperationSchema;
|
schema: OperationSchema;
|
||||||
|
|
||||||
|
resolver: (input: any) => any;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ObjectType: Type<any, any, ObjectTypeSchema> = {
|
export const ObjectType: Type<any, any, ObjectTypeSchema> = {
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,28 @@ import {Materializer, Type, TypeRegistry, Types} from "cad/craft/schema/types/in
|
||||||
import {CoreContext} from "context";
|
import {CoreContext} from "context";
|
||||||
import {BaseSchemaField, OperationParamsErrorReporter} from "cad/craft/schema/schema";
|
import {BaseSchemaField, OperationParamsErrorReporter} from "cad/craft/schema/schema";
|
||||||
|
|
||||||
export interface EnumTypeSchema extends BaseSchemaField {
|
export interface StringTypeSchema extends BaseSchemaField {
|
||||||
|
|
||||||
type: Types.number,
|
type: Types.number,
|
||||||
|
|
||||||
values: string[]
|
enum?: string[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnumType: Type<any, number, EnumTypeSchema> = {
|
export const StringType: Type<any, string, StringTypeSchema> = {
|
||||||
|
|
||||||
resolve(ctx: CoreContext,
|
resolve(ctx: CoreContext,
|
||||||
value: any,
|
value: any,
|
||||||
md: EnumTypeSchema,
|
md: StringTypeSchema,
|
||||||
reportError: OperationParamsErrorReporter,
|
reportError: OperationParamsErrorReporter,
|
||||||
materializer: Materializer): number {
|
materializer: Materializer): string {
|
||||||
|
|
||||||
if (md.values.indexOf(value) === -1) {
|
value = value + '';
|
||||||
value = md.defaultValue || md.values[0];
|
|
||||||
|
if (md.enum && md.enum.indexOf(value) === -1) {
|
||||||
|
value = md.defaultValue || md.enum[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import materializeParams from '../schema/materializeParams';
|
||||||
import {createFunctionList} from 'gems/func';
|
import {createFunctionList} from 'gems/func';
|
||||||
import {onParamsUpdate} from '../cutExtrude/extrudeOperation';
|
import {onParamsUpdate} from '../cutExtrude/extrudeOperation';
|
||||||
import {propsChangeTracker} from 'lstream/utils';
|
import {propsChangeTracker} from 'lstream/utils';
|
||||||
|
import {OperationSchema, schemaIterator} from "cad/craft/schema/schema";
|
||||||
|
|
||||||
export function activate(ctx) {
|
export function activate(ctx) {
|
||||||
|
|
||||||
|
|
@ -59,9 +60,9 @@ export function activate(ctx) {
|
||||||
let params;
|
let params;
|
||||||
let {changingHistory, noWizardFocus} = opRequest;
|
let {changingHistory, noWizardFocus} = opRequest;
|
||||||
if (changingHistory) {
|
if (changingHistory) {
|
||||||
params = clone(opRequest.params)
|
params = flattenParams(opRequest.params, operation.schema);
|
||||||
} else {
|
} else {
|
||||||
params = initializeBySchema(operation.schema, ctx);
|
params = initializeBySchema(operation.workingSchema, ctx);
|
||||||
if (opRequest.initialOverrides) {
|
if (opRequest.initialOverrides) {
|
||||||
applyOverrides(params, opRequest.initialOverrides);
|
applyOverrides(params, opRequest.initialOverrides);
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +76,8 @@ export function activate(ctx) {
|
||||||
let materializedWorkingRequest$ = workingRequest$.map(req => {
|
let materializedWorkingRequest$ = workingRequest$.map(req => {
|
||||||
let params = {};
|
let params = {};
|
||||||
let errors = [];
|
let errors = [];
|
||||||
materializeParams(ctx, req.params, operation.schema, params, errors, []);
|
let unflatten = unflattenParams(req.params, operation.schema);
|
||||||
|
materializeParams(ctx, unflatten, operation.schema, params, errors);
|
||||||
if (errors.length !== 0) {
|
if (errors.length !== 0) {
|
||||||
return INVALID_REQUEST;
|
return INVALID_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +85,7 @@ export function activate(ctx) {
|
||||||
type: req.type,
|
type: req.type,
|
||||||
params
|
params
|
||||||
};
|
};
|
||||||
}).remember(INVALID_REQUEST).filter(r => r !== INVALID_REQUEST);
|
}).remember(INVALID_REQUEST).filter(r => r !== INVALID_REQUEST).throttle(500);
|
||||||
const state$ = state({});
|
const state$ = state({});
|
||||||
const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params));
|
const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params));
|
||||||
const updateState = mutator => state$.mutate(state => mutator(state));
|
const updateState = mutator => state$.mutate(state => mutator(state));
|
||||||
|
|
@ -129,8 +131,12 @@ export function activate(ctx) {
|
||||||
},
|
},
|
||||||
|
|
||||||
applyWorkingRequest: () => {
|
applyWorkingRequest: () => {
|
||||||
let {type, params} = streams.wizard.wizardContext.value.workingRequest$.value;
|
let wizCtx = streams.wizard.wizardContext.value;
|
||||||
let request = clone({type, params});
|
let {type, params} = wizCtx.workingRequest$.value;
|
||||||
|
let request = {
|
||||||
|
type,
|
||||||
|
params: unflattenParams(params, wizCtx.operation.schema)
|
||||||
|
};
|
||||||
const setError = error => streams.wizard.wizardContext.mutate(ctx => ctx.state$.mutate(state => state.error = error));
|
const setError = error => streams.wizard.wizardContext.mutate(ctx => ctx.state$.mutate(state => state.error = error));
|
||||||
if (streams.wizard.insertOperation.value.type) {
|
if (streams.wizard.insertOperation.value.type) {
|
||||||
ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT, setError );
|
ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT, setError );
|
||||||
|
|
@ -143,6 +149,23 @@ export function activate(ctx) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flattenParams(params, originalSchema) {
|
||||||
|
const flatParams = {};
|
||||||
|
schemaIterator(originalSchema, (path, flattenedPath, schemaField) => {
|
||||||
|
flatParams[flattenedPath] = _.get(params, path);
|
||||||
|
});
|
||||||
|
return flatParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unflattenParams(params, originalSchema) {
|
||||||
|
const unflatParams = {};
|
||||||
|
schemaIterator(originalSchema, (path, flattenedPath, schemaField) => {
|
||||||
|
_.set(unflatParams, path, params[flattenedPath]);
|
||||||
|
});
|
||||||
|
return unflatParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyOverrides(params, initialOverrides) {
|
function applyOverrides(params, initialOverrides) {
|
||||||
Object.assign(params, initialOverrides);
|
Object.assign(params, initialOverrides);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function activate(ctx) {
|
||||||
wizCtx.workingRequest$.attach(({type, params}) => {
|
wizCtx.workingRequest$.attach(({type, params}) => {
|
||||||
const marker = ctx.services.marker;
|
const marker = ctx.services.marker;
|
||||||
marker.startSession();
|
marker.startSession();
|
||||||
let {schema} = wizCtx.operation;
|
let {workingSchema: schema} = wizCtx.operation;
|
||||||
Object.keys(schema).forEach(param => {
|
Object.keys(schema).forEach(param => {
|
||||||
let md = schema[param];
|
let md = schema[param];
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@ function createPickHandlerFromSchema(wizCtx) {
|
||||||
const params = wizCtx.workingRequest$.value.params;
|
const params = wizCtx.workingRequest$.value.params;
|
||||||
const state = wizCtx.state$.value;
|
const state = wizCtx.state$.value;
|
||||||
|
|
||||||
let {schema} = wizCtx.operation;
|
let {workingSchema: schema} = wizCtx.operation;
|
||||||
|
|
||||||
const activeMd = state.activeParam && schema[state.activeParam];
|
const activeMd = state.activeParam && schema[state.activeParam];
|
||||||
const activeCanTakeIt = kind => activeMd.allowedKinds && activeMd.allowedKinds.includes(kind);
|
const activeCanTakeIt = kind => activeMd.allowedKinds && activeMd.allowedKinds.includes(kind);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { ComboBoxOption } from 'ui/components/controls/ComboBoxControl';
|
||||||
import Entity from '../craft/wizard/components/form/Entity';
|
import Entity from '../craft/wizard/components/form/Entity';
|
||||||
import { CheckboxField, NumberField, ComboBoxField, TextField } from '../craft/wizard/components/form/Fields';
|
import { CheckboxField, NumberField, ComboBoxField, TextField } from '../craft/wizard/components/form/Fields';
|
||||||
import { Group } from '../craft/wizard/components/form/Form';
|
import { Group } from '../craft/wizard/components/form/Form';
|
||||||
import { OperationSchema, SchemaField } from './mdf';
|
import {OperationSchema, SchemaField} from "cad/craft/schema/schema";
|
||||||
|
|
||||||
|
|
||||||
export function generateForm(schema: OperationSchema) {
|
export function generateForm(schema: OperationSchema) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ export function loadMDFCommand<R>(mdfCommand: MDFCommand<R>): OperationDescripto
|
||||||
type: 'group',
|
type: 'group',
|
||||||
content: mdfCommand.form
|
content: mdfCommand.form
|
||||||
}
|
}
|
||||||
const formFields = extractFormFields(uiDefinition);
|
const {schema: derivedSchema, formFields} = deriveSchema(uiDefinition);
|
||||||
const derivedSchema = deriveSchema(formFields);
|
|
||||||
return {
|
return {
|
||||||
id: mdfCommand.id,
|
id: mdfCommand.id,
|
||||||
label: mdfCommand.label,
|
label: mdfCommand.label,
|
||||||
|
|
@ -75,13 +74,20 @@ function extractFormFields(uiDefinition: UIDefinition): FieldWidgetProps[] {
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deriveSchema(formFields: FieldWidgetProps[]): OperationSchema {
|
export function deriveSchema(uiDefinition: UIDefinition): {
|
||||||
|
schema: OperationSchema,
|
||||||
|
formFields: FieldWidgetProps[]
|
||||||
|
} {
|
||||||
|
const formFields: FieldWidgetProps[] = extractFormFields(uiDefinition)
|
||||||
const schema = {};
|
const schema = {};
|
||||||
formFields.forEach(f => {
|
formFields.forEach(f => {
|
||||||
let propsToSchema = DynamicComponents[f.type].propsToSchema;
|
let propsToSchema = DynamicComponents[f.type].propsToSchema;
|
||||||
schema[f.name] = propsToSchema(schema, f as any);
|
schema[f.name] = propsToSchema(schema, f as any);
|
||||||
});
|
});
|
||||||
return schema;
|
return {
|
||||||
|
schema,
|
||||||
|
formFields
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {ContainerBasicProps, ContainerWidgetProps} from "cad/mdf/ui/ContainerWidget";
|
|
||||||
|
|
||||||
export interface AccordionWidgetProps extends ContainerBasicProps {
|
|
||||||
|
|
||||||
type: 'accordion';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AccordionWidget(props: AccordionWidgetProps) {
|
|
||||||
return "TBD"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
84
web/app/cad/mdf/ui/BooleanWidget.tsx
Normal file
84
web/app/cad/mdf/ui/BooleanWidget.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import React from "react";
|
||||||
|
import {OperationSchema} from "cad/craft/schema/schema";
|
||||||
|
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
|
||||||
|
import {Types} from "cad/craft/schema/types";
|
||||||
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import {SectionWidgetProps} from "cad/mdf/ui/SectionWidget";
|
||||||
|
import {DynamicComponentWidget} from "cad/mdf/ui/DynamicComponentWidget";
|
||||||
|
import {VectorResolver} from "cad/craft/schema/resolvers/vectorResolver";
|
||||||
|
|
||||||
|
export interface BooleanWidgetProps extends FieldBasicProps {
|
||||||
|
|
||||||
|
type: 'boolean';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTITY_CAPTURE = [EntityKind.SHELL];
|
||||||
|
|
||||||
|
const BOOLEAN_OPTIONS = ['NONE', 'UNION', 'SUBTRACT', 'INTERSECT'];
|
||||||
|
|
||||||
|
const BooleanUIDefinition = (fieldName: string, label: string) => ({
|
||||||
|
|
||||||
|
type: 'section',
|
||||||
|
|
||||||
|
title: label,
|
||||||
|
|
||||||
|
collapsible: true,
|
||||||
|
|
||||||
|
initialCollapse: false,
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
name: fieldName+"/kind",
|
||||||
|
label: 'kind',
|
||||||
|
type: "choice",
|
||||||
|
optional: true,
|
||||||
|
values: BOOLEAN_OPTIONS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fieldName+"/targets",
|
||||||
|
label: 'target',
|
||||||
|
type: "selection",
|
||||||
|
capture: ENTITY_CAPTURE,
|
||||||
|
multi: true,
|
||||||
|
optional: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as SectionWidgetProps);
|
||||||
|
|
||||||
|
|
||||||
|
export function BooleanWidget(props: BooleanWidgetProps) {
|
||||||
|
|
||||||
|
let vectorUIDefinition = BooleanUIDefinition(props.name, props.label);
|
||||||
|
|
||||||
|
return <DynamicComponentWidget {...vectorUIDefinition} />
|
||||||
|
}
|
||||||
|
|
||||||
|
BooleanWidget.propsToSchema = (consumer: OperationSchema, props: BooleanWidgetProps) => {
|
||||||
|
return {
|
||||||
|
type: Types.object,
|
||||||
|
schema: {
|
||||||
|
kind: {
|
||||||
|
label: 'kind',
|
||||||
|
type: Types.string,
|
||||||
|
enum: BOOLEAN_OPTIONS,
|
||||||
|
defaultValue: props.defaultValue || 'NONE',
|
||||||
|
optional: false
|
||||||
|
},
|
||||||
|
|
||||||
|
targets: {
|
||||||
|
label: 'targets',
|
||||||
|
type: Types.array,
|
||||||
|
item: {
|
||||||
|
type: Types.boolean,
|
||||||
|
allowedKinds: ENTITY_CAPTURE,
|
||||||
|
},
|
||||||
|
optional: true,
|
||||||
|
applicable: 'kind !== "NONE"'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...fieldToSchemaGeneric(props),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
27
web/app/cad/mdf/ui/CheckboxWidget.tsx
Normal file
27
web/app/cad/mdf/ui/CheckboxWidget.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from "react";
|
||||||
|
import {OperationSchema} from "cad/craft/schema/schema";
|
||||||
|
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
|
||||||
|
import {Types} from "cad/craft/schema/types";
|
||||||
|
import {CheckboxField} from "cad/craft/wizard/components/form/Fields";
|
||||||
|
|
||||||
|
export interface CheckboxWidgetProps extends FieldBasicProps {
|
||||||
|
|
||||||
|
type: 'checkbox';
|
||||||
|
|
||||||
|
min?: number;
|
||||||
|
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckboxWidget(props: CheckboxWidgetProps) {
|
||||||
|
return <CheckboxField name={props.name} defaultValue={props.defaultValue} label={props.label} />
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckboxWidget.propsToSchema = (consumer: OperationSchema, props: CheckboxWidgetProps) => {
|
||||||
|
return {
|
||||||
|
type: Types.boolean,
|
||||||
|
...fieldToSchemaGeneric(props),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
36
web/app/cad/mdf/ui/ChoiceWidget.tsx
Normal file
36
web/app/cad/mdf/ui/ChoiceWidget.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {ComboBoxField, NumberField} from "cad/craft/wizard/components/form/Fields";
|
||||||
|
import React from "react";
|
||||||
|
import {OperationSchema} from "cad/craft/schema/schema";
|
||||||
|
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
|
||||||
|
import {Types} from "cad/craft/schema/types";
|
||||||
|
import {ComboBoxOption} from "ui/components/controls/ComboBoxControl";
|
||||||
|
|
||||||
|
export interface ChoiceWidgetProps extends FieldBasicProps {
|
||||||
|
|
||||||
|
type: 'choice';
|
||||||
|
|
||||||
|
style?: 'dropdown' | 'radio';
|
||||||
|
|
||||||
|
values: string[];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChoiceWidget(props: ChoiceWidgetProps) {
|
||||||
|
if (!props.style || props.style === 'dropdown') {
|
||||||
|
return <ComboBoxField name={props.name} defaultValue={props.defaultValue} label={props.label} >
|
||||||
|
{props.values.map(value => <ComboBoxOption value={value}>{value}</ComboBoxOption>)}
|
||||||
|
</ComboBoxField>
|
||||||
|
} else {
|
||||||
|
throw 'implement me';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceWidget.propsToSchema = (consumer: OperationSchema, props: ChoiceWidgetProps) => {
|
||||||
|
return {
|
||||||
|
type: Types.string,
|
||||||
|
enum: props.values,
|
||||||
|
...fieldToSchemaGeneric(props),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
31
web/app/cad/mdf/ui/SectionWidget.tsx
Normal file
31
web/app/cad/mdf/ui/SectionWidget.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {ContainerBasicProps, ContainerWidget, ContainerWidgetProps} from "cad/mdf/ui/ContainerWidget";
|
||||||
|
import {Group} from "cad/craft/wizard/components/form/Form";
|
||||||
|
import {StackSection} from "ui/components/controls/FormSection";
|
||||||
|
import Entity from "cad/craft/wizard/components/form/EntityList";
|
||||||
|
import {CheckboxField} from "cad/craft/wizard/components/form/Fields";
|
||||||
|
|
||||||
|
export interface SectionWidgetProps extends ContainerBasicProps {
|
||||||
|
|
||||||
|
type: 'section';
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
collapsible: boolean;
|
||||||
|
|
||||||
|
initialCollapse: boolean;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionWidget(props: SectionWidgetProps) {
|
||||||
|
const [visible, setVisible] = useState(!props.initialCollapse);
|
||||||
|
|
||||||
|
const onTitleClick = props.collapsible ? () => setVisible(visible => !visible) : undefined;
|
||||||
|
|
||||||
|
return <StackSection title={props.title} onTitleClick={onTitleClick} isClosed={!visible}>
|
||||||
|
{visible && <ContainerWidget content={props.content} />}
|
||||||
|
</StackSection>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
75
web/app/cad/mdf/ui/VectorWidget.tsx
Normal file
75
web/app/cad/mdf/ui/VectorWidget.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from "react";
|
||||||
|
import {OperationSchema} from "cad/craft/schema/schema";
|
||||||
|
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
|
||||||
|
import {Types} from "cad/craft/schema/types";
|
||||||
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import {SectionWidgetProps} from "cad/mdf/ui/SectionWidget";
|
||||||
|
import {DynamicComponentWidget} from "cad/mdf/ui/DynamicComponentWidget";
|
||||||
|
import {VectorResolver} from "cad/craft/schema/resolvers/vectorResolver";
|
||||||
|
|
||||||
|
export interface VectorWidgetProps extends FieldBasicProps {
|
||||||
|
|
||||||
|
type: 'vector';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTITY_CAPTURE = [EntityKind.EDGE, EntityKind.SKETCH_OBJECT, EntityKind.DATUM_AXIS, EntityKind.FACE];
|
||||||
|
|
||||||
|
const VectorUIDefinition = (fieldName: string, label: string) => ({
|
||||||
|
|
||||||
|
type: 'section',
|
||||||
|
|
||||||
|
title: label,
|
||||||
|
|
||||||
|
collapsible: true,
|
||||||
|
|
||||||
|
initialCollapse: false,
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
name: fieldName+"/vectorEntity",
|
||||||
|
label: 'vector',
|
||||||
|
type: "selection",
|
||||||
|
capture: ENTITY_CAPTURE,
|
||||||
|
multi: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fieldName+"/flip",
|
||||||
|
label: 'flip',
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as SectionWidgetProps);
|
||||||
|
|
||||||
|
|
||||||
|
export function VectorWidget(props: VectorWidgetProps) {
|
||||||
|
|
||||||
|
let vectorUIDefinition = VectorUIDefinition(props.name, props.label);
|
||||||
|
|
||||||
|
return <DynamicComponentWidget {...vectorUIDefinition} />
|
||||||
|
}
|
||||||
|
|
||||||
|
VectorWidget.propsToSchema = (consumer: OperationSchema, props: VectorWidgetProps) => {
|
||||||
|
return {
|
||||||
|
type: Types.object,
|
||||||
|
schema: {
|
||||||
|
vectorEntity: {
|
||||||
|
label: 'vector',
|
||||||
|
type: Types.entity,
|
||||||
|
allowedKinds: ENTITY_CAPTURE,
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
flip: {
|
||||||
|
label: 'flip',
|
||||||
|
type: Types.boolean,
|
||||||
|
defaultValue: false,
|
||||||
|
optional: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: VectorResolver,
|
||||||
|
...fieldToSchemaGeneric(props),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2,6 +2,11 @@ import {NumberWidget} from "cad/mdf/ui/NumberWidget";
|
||||||
import {SelectionWidget} from "cad/mdf/ui/SelectionWidget";
|
import {SelectionWidget} from "cad/mdf/ui/SelectionWidget";
|
||||||
import {ContainerWidget} from "cad/mdf/ui/ContainerWidget";
|
import {ContainerWidget} from "cad/mdf/ui/ContainerWidget";
|
||||||
import {GroupWidget} from "cad/mdf/ui/GroupWidget";
|
import {GroupWidget} from "cad/mdf/ui/GroupWidget";
|
||||||
|
import {SectionWidget} from "cad/mdf/ui/SectionWidget";
|
||||||
|
import {VectorWidget} from "cad/mdf/ui/VectorWidget";
|
||||||
|
import {CheckboxWidget} from "cad/mdf/ui/CheckboxWidget";
|
||||||
|
import {BooleanWidget} from "cad/mdf/ui/BooleanWidget";
|
||||||
|
import {ChoiceWidget} from "cad/mdf/ui/ChoiceWidget";
|
||||||
|
|
||||||
export const DynamicComponents = {
|
export const DynamicComponents = {
|
||||||
|
|
||||||
|
|
@ -12,4 +17,15 @@ export const DynamicComponents = {
|
||||||
'container': ContainerWidget,
|
'container': ContainerWidget,
|
||||||
|
|
||||||
'group': GroupWidget,
|
'group': GroupWidget,
|
||||||
|
|
||||||
|
'section': SectionWidget,
|
||||||
|
|
||||||
|
'vector': VectorWidget,
|
||||||
|
|
||||||
|
'checkbox': CheckboxWidget,
|
||||||
|
|
||||||
|
'boolean': BooleanWidget,
|
||||||
|
|
||||||
|
'choice': ChoiceWidget,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import {NumberWidgetProps} from "cad/mdf/ui/NumberWidget";
|
import {NumberWidgetProps} from "cad/mdf/ui/NumberWidget";
|
||||||
import {SelectionWidgetProps} from "cad/mdf/ui/SelectionWidget";
|
import {SelectionWidgetProps} from "cad/mdf/ui/SelectionWidget";
|
||||||
import {AccordionWidgetProps} from "cad/mdf/ui/AccordionWidget";
|
import {SectionWidgetProps} from "cad/mdf/ui/SectionWidget";
|
||||||
import {DynamicComponents} from "cad/mdf/ui/componentRegistry";
|
import {DynamicComponents} from "cad/mdf/ui/componentRegistry";
|
||||||
import {ContainerWidgetProps} from "cad/mdf/ui/ContainerWidget";
|
import {ContainerWidgetProps} from "cad/mdf/ui/ContainerWidget";
|
||||||
|
import {GroupWidgetProps} from "cad/mdf/ui/GroupWidget";
|
||||||
|
import {CheckboxWidgetProps} from "cad/mdf/ui/CheckboxWidget";
|
||||||
|
import {VectorWidgetProps} from "cad/mdf/ui/VectorWidget";
|
||||||
|
import {BooleanWidgetProps} from "cad/mdf/ui/BooleanWidget";
|
||||||
|
import {ChoiceWidgetProps} from "cad/mdf/ui/ChoiceWidget";
|
||||||
|
|
||||||
export type FieldWidgetProps = NumberWidgetProps | SelectionWidgetProps;
|
export type FieldWidgetProps = NumberWidgetProps | CheckboxWidgetProps | ChoiceWidgetProps | SelectionWidgetProps | VectorWidgetProps | BooleanWidgetProps;
|
||||||
|
|
||||||
export type BasicWidgetProps = ContainerWidgetProps | AccordionWidgetProps;
|
export type BasicWidgetProps = ContainerWidgetProps | SectionWidgetProps | GroupWidgetProps;
|
||||||
|
|
||||||
export type DynamicWidgetProps = FieldWidgetProps | BasicWidgetProps;
|
export type DynamicWidgetProps = FieldWidgetProps | BasicWidgetProps;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,8 @@ export class MDatumAxis extends MObject {
|
||||||
get parent() {
|
get parent() {
|
||||||
return this.holder;
|
return this.holder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDirection(): Vector {
|
||||||
|
return this.dir;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import {MObject} from './mobject';
|
import {MObject} from './mobject';
|
||||||
import {MBrepShell} from "./mshell";
|
import {MBrepShell} from "./mshell";
|
||||||
import {EntityKind} from "cad/model/entities";
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import {Edge} from "brep/topo/edge";
|
||||||
|
import Vector from "math/vector";
|
||||||
|
|
||||||
export class MEdge extends MObject {
|
export class MEdge extends MObject {
|
||||||
|
|
||||||
static TYPE = EntityKind.EDGE;
|
static TYPE = EntityKind.EDGE;
|
||||||
shell: MBrepShell;
|
shell: MBrepShell;
|
||||||
brepEdge: any;
|
brepEdge: Edge;
|
||||||
|
|
||||||
constructor(id, shell, brepEdge) {
|
constructor(id, shell, brepEdge) {
|
||||||
super(MEdge.TYPE, id);
|
super(MEdge.TYPE, id);
|
||||||
|
|
@ -34,4 +36,9 @@ export class MEdge extends MObject {
|
||||||
get parent() {
|
get parent() {
|
||||||
return this.shell;
|
return this.shell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDirection(): Vector {
|
||||||
|
return this.brepEdge.halfEdge1.tangentAtStart();
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import BBox from "math/bbox";
|
||||||
import {Basis, BasisForPlane} from "math/basis";
|
import {Basis, BasisForPlane} from "math/basis";
|
||||||
import {Face} from "brep/topo/face";
|
import {Face} from "brep/topo/face";
|
||||||
import {EntityKind} from "cad/model/entities";
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import {Matrix3x4} from "math/matrix";
|
||||||
|
|
||||||
export class MFace extends MObject {
|
export class MFace extends MObject {
|
||||||
|
|
||||||
|
|
@ -141,14 +142,14 @@ export class MFace extends MObject {
|
||||||
return EMPTY_ARRAY;
|
return EMPTY_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
get sketchToWorldTransformation() {
|
get sketchToWorldTransformation(): Matrix3x4 {
|
||||||
if (!this._sketchToWorldTransformation) {
|
if (!this._sketchToWorldTransformation) {
|
||||||
this._sketchToWorldTransformation = this.csys.outTransformation;
|
this._sketchToWorldTransformation = this.csys.outTransformation;
|
||||||
}
|
}
|
||||||
return this._sketchToWorldTransformation;
|
return this._sketchToWorldTransformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
get worldToSketchTransformation() {
|
get worldToSketchTransformation(): Matrix3x4 {
|
||||||
if (!this._worldToSketchTransformation) {
|
if (!this._worldToSketchTransformation) {
|
||||||
this._worldToSketchTransformation = this.csys.inTransformation;
|
this._worldToSketchTransformation = this.csys.inTransformation;
|
||||||
}
|
}
|
||||||
|
|
@ -223,4 +224,9 @@ export class MBrepFace extends MFace {
|
||||||
}
|
}
|
||||||
return this.#favorablePoint;
|
return this.#favorablePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDirection(): Vector {
|
||||||
|
return this.normal();
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import {IDENTITY_MATRIX, Matrix3x4} from "math/matrix";
|
import {IDENTITY_MATRIX, Matrix3x4} from "math/matrix";
|
||||||
import {EntityKind} from "cad/model/entities";
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import Vector from "math/vector";
|
||||||
|
|
||||||
export abstract class MObject {
|
export abstract class MObject {
|
||||||
|
|
||||||
|
|
@ -19,6 +20,10 @@ export abstract class MObject {
|
||||||
|
|
||||||
abstract get parent();
|
abstract get parent();
|
||||||
|
|
||||||
|
toDirection() {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
get root(): MObject {
|
get root(): MObject {
|
||||||
let obj = this;
|
let obj = this;
|
||||||
while (obj.parent) {
|
while (obj.parent) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import {MObject} from './mobject';
|
import {MObject} from './mobject';
|
||||||
import {MFace} from "./mface";
|
import {MFace} from "./mface";
|
||||||
import {EntityKind} from "cad/model/entities";
|
import {EntityKind} from "cad/model/entities";
|
||||||
|
import Vector from "math/vector";
|
||||||
|
import {Segment} from "cad/sketch/sketchModel";
|
||||||
|
|
||||||
export class MSketchObject extends MObject {
|
export class MSketchObject extends MObject {
|
||||||
|
|
||||||
|
|
@ -20,4 +22,9 @@ export class MSketchObject extends MObject {
|
||||||
return this.face;
|
return this.face;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDirection(): Vector {
|
||||||
|
const tangent = (this.sketchPrimitive as Segment).tangentAtStart();
|
||||||
|
return this.face.sketchToWorldTransformation.apply(tangent);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ export class InPlaceSketcher {
|
||||||
this.ctx.streams.ui.toolbars.headsUpShowTitles.next(false);
|
this.ctx.streams.ui.toolbars.headsUpShowTitles.next(false);
|
||||||
|
|
||||||
let sketchData = this.ctx.services.storage.get(this.sketchStorageKey);
|
let sketchData = this.ctx.services.storage.get(this.sketchStorageKey);
|
||||||
this.viewer.historyManager.init(sketchData, md);
|
this.viewer.historyManager.init(sketchData);
|
||||||
this.viewer.io.loadSketch(sketchData);
|
this.viewer.io.loadSketch(sketchData);
|
||||||
this.ctx.streams.sketcher.sketchingFace.next(face);
|
this.ctx.streams.sketcher.sketchingFace.next(face);
|
||||||
this.ctx.streams.sketcher.sketcherAppContext.next(this.sketcherAppContext);
|
this.ctx.streams.sketcher.sketcherAppContext.next(this.sketcherAppContext);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class SketchPrimitive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Segment<Segment> extends SketchPrimitive {
|
export class Segment extends SketchPrimitive {
|
||||||
|
|
||||||
a: Vector;
|
a: Vector;
|
||||||
b: Vector;
|
b: Vector;
|
||||||
|
|
@ -105,7 +105,12 @@ export class Segment<Segment> extends SketchPrimitive {
|
||||||
const [A, B] = genForm;
|
const [A, B] = genForm;
|
||||||
oci.point(underName + "_A", A.x, A.y, A.z);
|
oci.point(underName + "_A", A.x, A.y, A.z);
|
||||||
oci.point(underName + "_B", B.x, B.y, B.z);
|
oci.point(underName + "_B", B.x, B.y, B.z);
|
||||||
oci.gcarc(underName, "seg", underName + "_A", underName + "_B")}
|
oci.gcarc(underName, "seg", underName + "_A", underName + "_B")
|
||||||
|
}
|
||||||
|
|
||||||
|
tangentAtStart(): Vector {
|
||||||
|
return this.b.minus(this.a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Arc extends SketchPrimitive {
|
export class Arc extends SketchPrimitive {
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export class GCCircle extends ContractibleObject {
|
||||||
static TYPE = 'GCCircle';
|
static TYPE = 'GCCircle';
|
||||||
|
|
||||||
static newInstance(x, y, r) {
|
static newInstance(x, y, r) {
|
||||||
return GCCircle().init(new GCPoint(x, y), md)
|
return GCCircle().init(new GCPoint(x, y), new GCParam(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
init(c, r) {
|
init(c, r) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class Project {
|
||||||
let sketchId = this.getSketchId();
|
let sketchId = this.getSketchId();
|
||||||
let sketchData = localStorage.getItem(sketchId);
|
let sketchData = localStorage.getItem(sketchId);
|
||||||
if (sketchData != null) {
|
if (sketchData != null) {
|
||||||
this.viewer.historyManager.init(sketchData, md);
|
this.viewer.historyManager.init(sketchData);
|
||||||
this.viewer.io.loadSketch(sketchData);
|
this.viewer.io.loadSketch(sketchData);
|
||||||
}
|
}
|
||||||
this.viewer.repaint();
|
this.viewer.repaint();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue