diff --git a/packages/chili-core/src/i18n/en.ts b/packages/chili-core/src/i18n/en.ts index 218a5a1f..333b9a85 100644 --- a/packages/chili-core/src/i18n/en.ts +++ b/packages/chili-core/src/i18n/en.ts @@ -30,6 +30,7 @@ export default { "box.dz": "Height", "body.editableShape": "Editable Shape", "body.meshNode": "Mesh Node", + "body.multiShape": "Multi Shape", "circle.center": "Center", "circle.radius": "Radius", "command.arc": "Arc", diff --git a/packages/chili-core/src/i18n/keys.ts b/packages/chili-core/src/i18n/keys.ts index 5d6809e3..60e46e60 100644 --- a/packages/chili-core/src/i18n/keys.ts +++ b/packages/chili-core/src/i18n/keys.ts @@ -14,6 +14,7 @@ const I18N_KEYS = [ "body.fuse", "body.imported", "body.line", + "body.multiShape", "body.polygon", "body.prism", "body.rect", diff --git a/packages/chili-core/src/i18n/zh-cn.ts b/packages/chili-core/src/i18n/zh-cn.ts index 27c3510b..c87f6ce3 100644 --- a/packages/chili-core/src/i18n/zh-cn.ts +++ b/packages/chili-core/src/i18n/zh-cn.ts @@ -27,6 +27,7 @@ export default { "body.wire": "线框", "body.editableShape": "可编辑的形状", "body.meshNode": "网格节点", + "body.multiShape": "多形状", "box.dx": "长", "box.dy": "宽", "box.dz": "高", diff --git a/packages/chili-core/src/math/xy.ts b/packages/chili-core/src/math/xy.ts index 130cc2a9..5bde6f03 100644 --- a/packages/chili-core/src/math/xy.ts +++ b/packages/chili-core/src/math/xy.ts @@ -4,6 +4,8 @@ import { Precision } from "../foundation"; import { Serializer } from "../serialize"; import { MathUtils } from "./mathUtils"; +export type XYLike = { x: number; y: number }; + @Serializer.register(["x", "y"]) export class XY { static readonly zero = new XY(0, 0); diff --git a/packages/chili-core/src/math/xyz.ts b/packages/chili-core/src/math/xyz.ts index 61de77e4..da1c0076 100644 --- a/packages/chili-core/src/math/xyz.ts +++ b/packages/chili-core/src/math/xyz.ts @@ -4,6 +4,8 @@ import { Precision } from "../foundation"; import { Serializer } from "../serialize"; import { MathUtils } from "./mathUtils"; +export type XYZLike = { x: number; y: number; z: number }; + @Serializer.register(["x", "y", "z"]) export class XYZ { static readonly zero = new XYZ(0, 0, 0); diff --git a/packages/chili-core/src/model/facebaseNode.ts b/packages/chili-core/src/model/facebaseNode.ts index 687b80d4..64a7d635 100644 --- a/packages/chili-core/src/model/facebaseNode.ts +++ b/packages/chili-core/src/model/facebaseNode.ts @@ -2,7 +2,7 @@ import { Property } from "../property"; import { Serializer } from "../serialize"; -import { ParameterShapeNode } from "./node"; +import { ParameterShapeNode } from "./shapeNode"; export abstract class FacebaseNode extends ParameterShapeNode { @Serializer.serialze() diff --git a/packages/chili-core/src/model/index.ts b/packages/chili-core/src/model/index.ts index fd135458..a1b3a320 100644 --- a/packages/chili-core/src/model/index.ts +++ b/packages/chili-core/src/model/index.ts @@ -4,4 +4,5 @@ export * from "./facebaseNode"; export * from "./folderNode"; export * from "./groupNode"; export * from "./node"; +export * from "./shapeNode"; diff --git a/packages/chili-core/src/model/node.ts b/packages/chili-core/src/model/node.ts index 5f19a9f8..d3d984c6 100644 --- a/packages/chili-core/src/model/node.ts +++ b/packages/chili-core/src/model/node.ts @@ -4,17 +4,14 @@ import { IDocument } from "../document"; import { HistoryObservable, IDisposable, - IEqualityComparer, IPropertyChanged, - Id, - PubSub, - Result, + Id } from "../foundation"; -import { I18n, I18nKeys } from "../i18n"; +import { I18nKeys } from "../i18n"; import { BoundingBox, Matrix4 } from "../math"; import { Property } from "../property"; import { Serialized, Serializer } from "../serialize"; -import { IShape, IShapeMeshData, Mesh } from "../shape"; +import { IShapeMeshData, Mesh } from "../shape"; export interface INode extends IPropertyChanged, IDisposable { readonly id: string; @@ -331,110 +328,6 @@ export abstract class GeometryNode extends VisualNode { protected abstract createMesh(): IShapeMeshData; } -export abstract class ShapeNode extends GeometryNode { - protected _shape: Result = Result.err(SHAPE_UNDEFINED); - get shape(): Result { - return this._shape; - } - - protected setShape(shape: Result) { - if (this._shape.isOk && this._shape.value.isEqual(shape.value)) { - return; - } - - if (!shape.isOk) { - PubSub.default.pub("displayError", shape.error); - return; - } - - let oldShape = this._shape; - this._shape = shape; - this._mesh = undefined; - this._boundingBox = undefined; - this._shape.value.matrix = this.transform; - this.emitPropertyChanged("shape", oldShape); - } - - protected override onTransformChanged(newMatrix: Matrix4): void { - if (this.shape.isOk) this.shape.value.matrix = newMatrix; - } - - protected override createMesh(): IShapeMeshData { - if (!this.shape.isOk) { - throw new Error(this.shape.error); - } - return this.shape.value.mesh; - } - - override dispose(): void { - super.dispose(); - this.shape.unchecked()?.dispose(); - } -} - -const SHAPE_UNDEFINED = "Shape not initialized"; -export abstract class ParameterShapeNode extends ShapeNode { - override get shape(): Result { - if (!this._shape.isOk && this._shape.error === SHAPE_UNDEFINED) { - this._shape = this.generateShape(); - } - return this._shape; - } - - protected setPropertyEmitShapeChanged( - property: K, - newValue: this[K], - onPropertyChanged?: (property: K, oldValue: this[K]) => void, - equals?: IEqualityComparer | undefined, - ): boolean { - if (this.setProperty(property, newValue, onPropertyChanged, equals)) { - this.setShape(this.generateShape()); - return true; - } - - return false; - } - - constructor(document: IDocument, materialId?: string, id?: string) { - super(document, undefined as any, materialId, id); - this.setPrivateValue("name", I18n.translate(this.display())); - } - - protected abstract generateShape(): Result; -} - -@Serializer.register(["document", "name", "shape", "materialId", "id"]) -export class EditableShapeNode extends ShapeNode { - override display(): I18nKeys { - return "body.editableShape"; - } - - @Serializer.serialze() - override get shape(): Result { - return this._shape; - } - override set shape(shape: Result) { - this.setShape(shape); - } - - constructor( - document: IDocument, - name: string, - shape: IShape | Result, - materialId?: string, - id?: string, - ) { - super(document, name, materialId, id); - - if (shape instanceof Result) { - this._shape = shape; - } else { - this._shape = Result.ok(shape); - } - this.setPrivateValue("transform", this._shape.value.matrix); - } -} - export namespace NodeSerializer { export function serialize(node: INode) { let nodes: Serialized[] = []; @@ -473,4 +366,4 @@ export namespace NodeSerializer { }); return nodeMap.get(nodes[0].properties["id"]); } -} +} \ No newline at end of file diff --git a/packages/chili-core/src/model/shapeNode.ts b/packages/chili-core/src/model/shapeNode.ts new file mode 100644 index 00000000..58b7dfe6 --- /dev/null +++ b/packages/chili-core/src/model/shapeNode.ts @@ -0,0 +1,220 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +import { VisualConfig } from "../config"; +import { IDocument } from "../document"; +import { Id, IEqualityComparer, PubSub, Result } from "../foundation"; +import { I18n, I18nKeys } from "../i18n"; +import { Matrix4 } from "../math"; +import { Serializer } from "../serialize"; +import { EdgeMeshData, FaceMeshData, IShape, IShapeMeshData, LineType } from "../shape"; +import { GeometryNode } from "./node"; + +export abstract class ShapeNode extends GeometryNode { + protected _shape: Result = Result.err(SHAPE_UNDEFINED); + get shape(): Result { + return this._shape; + } + + protected setShape(shape: Result) { + if (this._shape.isOk && this._shape.value.isEqual(shape.value)) { + return; + } + + if (!shape.isOk) { + PubSub.default.pub("displayError", shape.error); + return; + } + + let oldShape = this._shape; + this._shape = shape; + this._mesh = undefined; + this._boundingBox = undefined; + this._shape.value.matrix = this.transform; + this.emitPropertyChanged("shape", oldShape); + } + + protected override onTransformChanged(newMatrix: Matrix4): void { + if (this.shape.isOk) this.shape.value.matrix = newMatrix; + } + + protected override createMesh(): IShapeMeshData { + if (!this.shape.isOk) { + throw new Error(this.shape.error); + } + return this.shape.value.mesh; + } + + override dispose(): void { + super.dispose(); + this.shape.unchecked()?.dispose(); + } +} + +export class ManyMesh implements IShapeMeshData { + private readonly _edges: EdgeMeshData + private readonly _faces: FaceMeshData + get edges(): EdgeMeshData | undefined { + return this._edges.positions.length > 0 ? this._edges : undefined + } + get faces(): FaceMeshData | undefined { + return this._faces.positions.length > 0 ? this._faces : undefined + } + + constructor(readonly shapes: IShape[]) { + this._edges = this.initEdgeMeshData() + this._faces = this.initFaceMeshData(); + + this.meshShapes() + } + + private initEdgeMeshData(): EdgeMeshData { + return { + lineType: LineType.Solid, + positions: [], + groups: [], + color: VisualConfig.defaultEdgeColor + } + } + + private initFaceMeshData(): FaceMeshData { + return { + indices: [], + normals: [], + positions: [], + uvs: [], + groups: [], + color: VisualConfig.defaultFaceColor + } + } + + private meshShapes() { + for (const shape of this.shapes) { + this.combineEdge(shape.mesh.edges); + this.combineFace(shape.mesh.faces); + } + } + + private combineFace(faceMeshData: FaceMeshData | undefined) { + if (!faceMeshData) { + return + } + + let start = this._faces.positions.length / 3; + this._faces.indices = this._faces.indices.concat(faceMeshData.indices); + this._faces.normals = this._faces.normals.concat(faceMeshData.normals); + this._faces.uvs = this._faces.uvs.concat(faceMeshData.uvs); + this._faces.positions = this._faces.positions.concat(faceMeshData.positions); + this._faces.groups = this._faces.groups.concat(faceMeshData.groups.map(g => { + return { + start: g.start + start, + shape: g.shape, + count: g.count + }; + })); + } + + private combineEdge(edgeMeshData: EdgeMeshData | undefined) { + if (!edgeMeshData) { + return + } + + let start = this._edges.positions.length / 3; + this._edges.positions = this._edges.positions.concat(edgeMeshData.positions); + this._edges.groups = this._edges.groups.concat(edgeMeshData.groups.map(g => { + return { + start: g.start + start, + shape: g.shape, + count: g.count + }; + })); + } + + updateMeshShape() {} + +} + +@Serializer.register(["document", "name", "shapes", "materialId", "id"]) +export class MultiShapeNode extends GeometryNode { + private readonly _shapes: IShape[]; + @Serializer.serialze() + get shapes(): ReadonlyArray { + return this._shapes; + } + + constructor(document: IDocument, name: string, shapes: IShape[], materialId?: string, id: string = Id.generate()) { + super(document, name, materialId, id); + this._shapes = shapes; + } + + protected override createMesh(): IShapeMeshData { + return new ManyMesh(this._shapes) + } + + override display(): I18nKeys { + return "body.multiShape" + } + +} + +const SHAPE_UNDEFINED = "Shape not initialized"; +export abstract class ParameterShapeNode extends ShapeNode { + override get shape(): Result { + if (!this._shape.isOk && this._shape.error === SHAPE_UNDEFINED) { + this._shape = this.generateShape(); + } + return this._shape; + } + + protected setPropertyEmitShapeChanged( + property: K, + newValue: this[K], + onPropertyChanged?: (property: K, oldValue: this[K]) => void, + equals?: IEqualityComparer | undefined, + ): boolean { + if (this.setProperty(property, newValue, onPropertyChanged, equals)) { + this.setShape(this.generateShape()); + return true; + } + + return false; + } + + constructor(document: IDocument, materialId?: string, id?: string) { + super(document, undefined as any, materialId, id); + this.setPrivateValue("name", I18n.translate(this.display())); + } + + protected abstract generateShape(): Result; +} + +@Serializer.register(["document", "name", "shape", "materialId", "id"]) +export class EditableShapeNode extends ShapeNode { + override display(): I18nKeys { + return "body.editableShape"; + } + + @Serializer.serialze() + override get shape(): Result { + return this._shape; + } + override set shape(shape: Result) { + this.setShape(shape); + } + + constructor( + document: IDocument, + name: string, + shape: IShape | Result, + materialId?: string, + id?: string, + ) { + super(document, name, materialId, id); + + if (shape instanceof Result) { + this._shape = shape; + } else { + this._shape = Result.ok(shape); + } + this.setPrivateValue("transform", this._shape.value.matrix); + } +} diff --git a/packages/chili-core/src/shape/meshData.ts b/packages/chili-core/src/shape/meshData.ts index f1ac2b33..42069d3f 100644 --- a/packages/chili-core/src/shape/meshData.ts +++ b/packages/chili-core/src/shape/meshData.ts @@ -39,7 +39,6 @@ export class Mesh { } export interface IShapeMeshData { - get shape(): IShape; get edges(): EdgeMeshData | undefined; get faces(): FaceMeshData | undefined; updateMeshShape(): void; diff --git a/packages/chili-core/src/shape/shapeFactory.ts b/packages/chili-core/src/shape/shapeFactory.ts index be43d88b..8738452a 100644 --- a/packages/chili-core/src/shape/shapeFactory.ts +++ b/packages/chili-core/src/shape/shapeFactory.ts @@ -1,7 +1,7 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { Result } from "../foundation"; -import { Plane, Ray, XYZ } from "../math"; +import { Plane, Ray, XYZ, XYZLike } from "../math"; import { ICompound, IEdge, IFace, IShape, ISolid, IVertex, IWire } from "./shape"; import { IShapeConverter } from "./shapeConverter"; @@ -9,13 +9,13 @@ export interface IShapeFactory { readonly kernelName: string; readonly converter: IShapeConverter; face(...wire: IWire[]): Result; - bezier(points: XYZ[], weights?: number[]): Result; - point(point: XYZ): Result; - line(start: XYZ, end: XYZ): Result; - arc(normal: XYZ, center: XYZ, start: XYZ, angle: number): Result; - circle(normal: XYZ, center: XYZ, radius: number): Result; + bezier(points: XYZLike[], weights?: number[]): Result; + point(point: XYZLike): Result; + line(start: XYZLike, end: XYZLike): Result; + arc(normal: XYZLike, center: XYZLike, start: XYZLike, angle: number): Result; + circle(normal: XYZLike, center: XYZLike, radius: number): Result; rect(plane: Plane, dx: number, dy: number): Result; - polygon(...points: XYZ[]): Result; + polygon(...points: XYZLike[]): Result; box(plane: Plane, dx: number, dy: number, dz: number): Result; wire(...edges: IEdge[]): Result; prism(shape: IShape, vec: XYZ): Result; @@ -25,7 +25,7 @@ export interface IShapeFactory { booleanCommon(shape1: IShape, shape2: IShape): Result; booleanCut(shape1: IShape, shape2: IShape): Result; booleanFuse(shape1: IShape, shape2: IShape): Result; - combine(...shapes: IShape[]): Result; + combine(shapes: IShape[]): Result; makeThickSolidBySimple(shape: IShape, thickness: number): Result; makeThickSolidByJoin(shape: IShape, closingFaces: IShape[], thickness: number): Result; } diff --git a/packages/chili-core/src/visual/visualObject.ts b/packages/chili-core/src/visual/visualObject.ts index 2a5cc4a3..36a3141f 100644 --- a/packages/chili-core/src/visual/visualObject.ts +++ b/packages/chili-core/src/visual/visualObject.ts @@ -1,6 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { BoundingBox, GeometryNode, IDisposable, INode, Matrix4, VisualNode } from "chili-core"; +import { BoundingBox, GeometryNode, IDisposable, Matrix4 } from "chili-core"; export interface IVisualObject extends IDisposable { visible: boolean; diff --git a/packages/chili-three/src/threeView.ts b/packages/chili-three/src/threeView.ts index 6b4d40bc..ee97f22b 100644 --- a/packages/chili-three/src/threeView.ts +++ b/packages/chili-three/src/threeView.ts @@ -7,6 +7,7 @@ import { IShapeFilter, IView, IVisualObject, + MultiShapeNode, Observable, Plane, PubSub, @@ -380,15 +381,24 @@ export class ThreeView extends Observable implements IView { private detectThreeShapes(intersections: Intersection[], shapeFilter?: IShapeFilter): VisualShapeData[] { for (const element of intersections) { const parent = element.object.parent; - if (!(parent instanceof ThreeGeometry) || !(parent.geometryNode instanceof ShapeNode)) continue; + if (!(parent instanceof ThreeGeometry)) continue; - if (shapeFilter && !shapeFilter.allow(parent.geometryNode.shape.value)) { + let shape: IShape | undefined; + if (parent.geometryNode instanceof ShapeNode) { + shape = parent.geometryNode.shape.unchecked(); + } else if (parent.geometryNode instanceof MultiShapeNode) { + shape = this.findShapeAndIndex(parent, element).shape; + } + if (!shape) continue; + + if (shapeFilter && !shapeFilter.allow(shape)) { continue; } + return [ { owner: parent, - shape: parent.geometryNode.shape.value, + shape, point: ThreeHelper.toXYZ(element.pointOnLine ?? element.point), indexes: [], }, @@ -431,6 +441,9 @@ export class ThreeView extends Observable implements IView { indexes: number[]; } { let { shape, index, groups } = this.findShapeAndIndex(parent, element); + if (parent.geometryNode instanceof MultiShapeNode) { + return { shape, directShape: shape, indexes: [index!] }; + } if (!shape) return { shape: undefined, directShape: undefined, indexes: [] }; if (ShapeType.hasShell(shapeType) && shape.shapeType === ShapeType.Face) { let shell = this.getAncestor(ShapeType.Shell, shape, groups!, parent); diff --git a/packages/chili-three/src/threeVisualContext.ts b/packages/chili-three/src/threeVisualContext.ts index 9563396c..1648538f 100644 --- a/packages/chili-three/src/threeVisualContext.ts +++ b/packages/chili-three/src/threeVisualContext.ts @@ -4,6 +4,7 @@ import { CollectionAction, CollectionChangedArgs, DeepObserver, + GeometryNode, GroupNode, IDisposable, INode, @@ -298,8 +299,8 @@ export class ThreeVisualContext implements IVisualContext { let visualObject: (IVisualObject & Object3D) | undefined = undefined; if (node instanceof MeshNode) { visualObject = new ThreeMeshObject(this, node); - } else if (node instanceof ShapeNode) { - visualObject = new ThreeGeometry(node as any, this); + } else if (node instanceof GeometryNode) { + visualObject = new ThreeGeometry(node, this); } else if (node instanceof GroupNode) { visualObject = new GroupVisualObject(node); } diff --git a/packages/chili-three/test/testEdge.ts b/packages/chili-three/test/testEdge.ts index e37b52a7..6e168380 100644 --- a/packages/chili-three/test/testEdge.ts +++ b/packages/chili-three/test/testEdge.ts @@ -77,7 +77,6 @@ export class TestEdge implements IEdge { matrix: Matrix4 = Matrix4.identity(); get mesh(): IShapeMeshData { return { - shape: this, edges: { positions: [this.start.x, this.start.y, this.start.z, this.end.x, this.end.y, this.end.z], color: 0xff0000, diff --git a/packages/chili-wasm/src/factory.ts b/packages/chili-wasm/src/factory.ts index 921610b2..6d420fb2 100644 --- a/packages/chili-wasm/src/factory.ts +++ b/packages/chili-wasm/src/factory.ts @@ -13,19 +13,28 @@ import { Ray, Result, XYZ, + XYZLike } from "chili-core"; import { ShapeResult, TopoDS_Shape } from "../lib/chili-wasm"; import { OccShapeConverter } from "./converter"; import { OcctHelper } from "./helper"; import { OccShape } from "./shape"; -function ensureOccShape(...shapes: IShape[]): TopoDS_Shape[] { - return shapes.map((x) => { - if (!(x instanceof OccShape)) { - throw new Error("The OCC kernel only supports OCC geometries."); - } - return x.shape; - }); +function ensureOccShape(shapes: IShape | IShape[]): TopoDS_Shape[] { + if (Array.isArray(shapes)) { + return shapes.map((x) => { + if (!(x instanceof OccShape)) { + throw new Error("The OCC kernel only supports OCC geometries."); + } + return x.shape; + }); + } + + if (shapes instanceof OccShape) { + return [shapes.shape] + } + + throw new Error("The OCC kernel only supports OCC geometries."); } function convertShapeResult(result: ShapeResult): Result { @@ -43,30 +52,34 @@ export class ShapeFactory implements IShapeFactory { } face(...wire: IWire[]): Result { - let shapes = ensureOccShape(...wire); + let shapes = ensureOccShape(wire); return convertShapeResult(wasm.ShapeFactory.face(shapes)) as Result; } - bezier(points: XYZ[], weights?: number[]): Result { + bezier(points: XYZLike[], weights?: number[]): Result { return convertShapeResult(wasm.ShapeFactory.bezier(points, weights ?? [])) as Result; } - point(point: XYZ): Result { + point(point: XYZLike): Result { return convertShapeResult(wasm.ShapeFactory.point(point)) as Result; } - line(start: XYZ, end: XYZ): Result { + line(start: XYZLike, end: XYZLike): Result { + if (MathUtils.allEqualZero(start.x - end.x, start.y - end.y, start.z - end.z)) { + return Result.err("The start and end points are too close."); + } + return convertShapeResult(wasm.ShapeFactory.line(start, end)) as Result; } - arc(normal: XYZ, center: XYZ, start: XYZ, angle: number): Result { + arc(normal: XYZLike, center: XYZLike, start: XYZLike, angle: number): Result { return convertShapeResult( wasm.ShapeFactory.arc(normal, center, start, MathUtils.degToRad(angle)), ) as Result; } - circle(normal: XYZ, center: XYZ, radius: number): Result { + circle(normal: XYZLike, center: XYZLike, radius: number): Result { return convertShapeResult(wasm.ShapeFactory.circle(normal, center, radius)) as Result; } rect(plane: Plane, dx: number, dy: number): Result { return convertShapeResult(wasm.ShapeFactory.rect(OcctHelper.toAx3(plane), dx, dy)) as Result; } - polygon(...points: XYZ[]): Result { + polygon(...points: XYZLike[]): Result { return convertShapeResult(wasm.ShapeFactory.polygon(points)) as Result; } box(plane: Plane, dx: number, dy: number, dz: number): Result { @@ -75,7 +88,7 @@ export class ShapeFactory implements IShapeFactory { ) as Result; } wire(...edges: IEdge[]): Result { - return convertShapeResult(wasm.ShapeFactory.wire(ensureOccShape(...edges))) as Result; + return convertShapeResult(wasm.ShapeFactory.wire(ensureOccShape(edges))) as Result; } prism(shape: IShape, vec: XYZ): Result { if (vec.length() === 0) { @@ -113,8 +126,8 @@ export class ShapeFactory implements IShapeFactory { wasm.ShapeFactory.booleanFuse(ensureOccShape(shape1), ensureOccShape(shape2)), ); } - combine(...shapes: IShape[]): Result { - return convertShapeResult(wasm.ShapeFactory.combine(ensureOccShape(...shapes))) as Result; + combine(shapes: IShape[]): Result { + return convertShapeResult(wasm.ShapeFactory.combine(ensureOccShape(shapes))) as Result; } makeThickSolidBySimple(shape: IShape, thickness: number): Result { return convertShapeResult( @@ -125,7 +138,7 @@ export class ShapeFactory implements IShapeFactory { return convertShapeResult( wasm.ShapeFactory.makeThickSolidByJoin( ensureOccShape(shape)[0], - ensureOccShape(...closingFaces), + ensureOccShape(closingFaces), thickness, ), );