modeling_solid.js

import oc from "../opencascade/initializer";
import explore from "../utils/explore";
import cachedGetters from "../utils/cache";
import {Face, Edge, Vertex} from "./index";
import {Axis, Plane, Vector} from "../math";
import {cut, intersect, join} from "../operations/booleans";
import {translate, rotate, mirror, scale} from "../operations/transformations";
import {split} from "../operations/modifications";
import {DEFAULT_PRECISION, JOIN_TYPES} from "../constants";

/**
 * Represents a solid in 3D space.
 * @memberof modeling
 * @alias Solid
 */
export class Solid {
  #wrapped;
  #properties;

  /**
   * @hideconstructor
   */
  constructor(wrapped) {
    this.#wrapped = oc.TopoDS.Solid_1(wrapped);
  }

  /**
   * Returns the wrapped OpenCascade object.
   * @private
   */
  get wrapped() {
    return this.#wrapped;
  }

  /**
   * Retrieves all faces of the solid.
   * @returns {Face[]} An array of `Face` objects representing the solid's faces.
   */
  get faces() {
    return explore({shape: this.#wrapped, find: oc.TopAbs_ShapeEnum.TopAbs_FACE}).map((shape) => new Face(shape));
  }

  /**
   * Retrieves all edges of the solid.
   * @returns {Edge[]} An array of `Edge` objects representing the solid's edges.
   */
  get edges() {
    return explore({shape: this.#wrapped, find: oc.TopAbs_ShapeEnum.TopAbs_EDGE}).map((shape) => new Edge(shape));
  }

  /**
   * Retrieves all vertices of the solid.
   * @returns {Vertex[]} An array of `Vertex` objects representing the solid's vertices.
   */
  get vertices() {
    return explore({shape: this.#wrapped, find: oc.TopAbs_ShapeEnum.TopAbs_VERTEX}).map((shape) => new Vertex(shape));
  }

  /**
   * Computes the center of mass of the solid.
   * @returns {Vector} A `Vector` object representing the center of mass of the solid.
   */
  get centerOfMass() {
    const centerOfMass = this.#surfaceProperties().CentreOfMass();
    return new Vector({
      x: centerOfMass.X(),
      y: centerOfMass.Y(),
      z: centerOfMass.Z(),
    });
  }

  /**
   * Computes the volume of the solid.
   * @returns {number} The volume of the solid.
   */
  get volume() {
    return this.#surfaceProperties().Mass();
  }

  /**
   * Translates the solid by a specified offset.
   * @param {Vector} offset - The translation offset.
   * @returns {Solid} A new `Solid` object representing the translated solid.
   */
  translate(offset) {
    return new Solid(translate({shape: this.#wrapped, offset: offset.wrapped}));
  }

  /**
   * Rotates the solid around a specified axis by a given angle.
   * @param {Object} parameters - Rotation parameters.
   * @param {Axis} parameters.axis - The axis to rotate around.
   * @param {number} parameters.angle - The rotation angle in radians.
   * @returns {Solid} A new `Solid` object representing the rotated solid.
   */
  rotate({axis, angle}) {
    return new Solid(rotate({shape: this.#wrapped, axis: axis.wrapped, angle: angle}));
  }

  /**
   * Mirrors the solid across a specified plane.
   * @param {Plane} plane - The plane to mirror across.
   * @returns {Solid} A new `Solid` object representing the mirrored solid.
   */
  mirror(plane) {
    return new Solid(mirror({shape: this.#wrapped, axis: plane.wrapped.Position().Ax2()}));
  }

  /**
   * Scales the solid by a specified factor.
   * @param {number} factor - The scaling factor.
   * @returns {Solid} A new `Solid` object representing the scaled solid.
   */
  scale(factor) {
    return new Solid(scale({shape: this.#wrapped, factor}));
  }

  /**
   * Splits the solid into multiple parts using a specified tool.
   * @param {Plane} tool - The tool used to split the solid.
   * @returns {Solid[]} An array of `Solid` objects representing the split parts.
   */
  split(tool) {
    const face = new oc.BRepBuilderAPI_MakeFace_3(tool.wrapped).Face();
    return explore({shape: split({shape: this.#wrapped, tool: face}), find: oc.TopAbs_ShapeEnum.TopAbs_SOLID}).map(
      (shape) => new Solid(shape)
    );
  }

  /**
   * Cuts the solid using one or more tools (other solids).
   * @param {...Solid} tools - One or more `Solid` objects to use as cutting tools.
   * @returns {Solid[]} An array of `Solid` objects representing the result.
   */
  cut(...tools) {
    const compound = cut({
      shape: this.#wrapped,
      tools: tools.map((tool) => tool.wrapped),
    });
    return explore({shape: compound, find: oc.TopAbs_ShapeEnum.TopAbs_SOLID}).map((shape) => new Solid(shape));
  }

  /**
   * Intersects the solid with one or more tools (other solids).
   * @param {...Solid} tools - One or more `Solid` objects to intersect with.
   * @returns {Solid[]} An array of `Solid` objects representing the result.
   */
  intersect(...tools) {
    const compound = intersect({
      shape: this.#wrapped,
      tools: tools.map((tool) => tool.wrapped),
    });
    return explore({shape: compound, find: oc.TopAbs_ShapeEnum.TopAbs_SOLID}).map((shape) => new Solid(shape));
  }

  /**
   * Joins the solid with one or more tools (other solids).
   * @param {...Solid} tools - One or more `Solid` objects to join with.
   * @returns {Solid[]} An array of `Solid` objects representing the result.
   */
  join(...tools) {
    const compound = join({
      shape: this.#wrapped,
      tools: tools.map((tool) => tool.wrapped),
    });
    return explore({shape: compound, find: oc.TopAbs_ShapeEnum.TopAbs_SOLID}).map((shape) => new Solid(shape));
  }

  /**
   * Applies a fillet (rounded edge) to selected edges of the solid.
   * @param {Object} parameters - Fillet parameters.
   * @param {function(Edge): boolean} parameters.selector - A function to select edges for the fillet.
   * @param {number} parameters.radius - The radius of the fillet.
   * @returns {Solid} A new `Solid` object with the fillet applied.
   */
  fillet({selector, radius}) {
    const fillet = new oc.BRepFilletAPI_MakeFillet(this.#wrapped, oc.ChFi3d_FilletShape.ChFi3d_Rational);
    this.edges.filter(selector).forEach((edge) => fillet.Add_2(radius, edge.wrapped));
    const compound = fillet.Shape();
    const solids = explore({shape: compound, find: oc.TopAbs_ShapeEnum.TopAbs_SOLID});
    return new Solid(solids[0]);
  }

  /**
   * Applies a chamfer (beveled edge) to selected edges of the solid.
   * @param {Object} parameters - Chamfer parameters.
   * @param {function(Edge): boolean} parameters.selector - A function to select edges for the chamfer.
   * @param {number} parameters.distance - The distance of the chamfer.
   * @returns {Solid} A new `Solid` object with the chamfer applied.
   */
  chamfer({selector, distance}) {
    const chamfer = new oc.BRepFilletAPI_MakeChamfer(this.#wrapped);
    this.edges.filter(selector).forEach((edge) => chamfer.Add_2(distance, edge.wrapped));
    const compound = chamfer.Shape();
    const solids = explore({shape: compound, find: oc.TopAbs_ShapeEnum.TopAbs_SOLID});
    return new Solid(solids[0]);
  }

  /**
   * Creates a hollow version of the solid by offsetting its faces by a specified thickness.
   * @param {Object} parameters - Shell parameters.
   * @param {function(Face): boolean} parameters.selector - A function to select faces for the shell.
   * @param {number} parameters.thickness - The thickness of the shell. Positive value shells outwards, negative value shells inwards.
   * @param {('arc'|'tangent'|'intersection')} [parameters.joinType="intersection"] - The join type for the shell.
   * @returns {Solid} A new `Solid` object representing the shelled solid.
   */
  shell({selector, thickness, joinType = "intersection"}) {
    const wrappedFaces = new oc.TopTools_ListOfShape_1();
    this.faces.filter(selector).forEach((face) => wrappedFaces.Append_1(face.wrapped));

    const shell = new oc.BRepOffsetAPI_MakeThickSolid();
    shell.MakeThickSolidByJoin(
      this.#wrapped,
      wrappedFaces,
      thickness,
      DEFAULT_PRECISION,
      oc.BRepOffset_Mode.BRepOffset_Skin,
      false,
      false,
      JOIN_TYPES[joinType],
      true,
      new oc.Message_ProgressRange_1()
    );
    return new Solid(shell.Shape());
  }

  /**
   * Computes the hash code for the solid.
   * @returns {number} The hash code.
   * @private
   */
  hashCode() {
    return oc.OCJS.HashCode(this.#wrapped);
  }

  #surfaceProperties() {
    if (!this.#properties) {
      this.#properties = new oc.GProp_GProps_1();
      oc.BRepGProp.VolumeProperties_1(this.#wrapped, this.#properties, true, false, false);
    }
    return this.#properties;
  }

  /**
   * Creates a new solid from a set of faces.
   * @param {...Face} faces - One or more `Face` objects to create the solid from.
   * @returns {Solid} A new `Solid` object representing the created solid.
   */
  static fromFaces(...faces) {
    const sew = new oc.BRepBuilderAPI_Sewing(DEFAULT_PRECISION, true, true, true, false);
    faces.forEach((face) => sew.Add(face.wrapped));
    sew.Perform(new oc.Message_ProgressRange_1());

    const builder = new oc.BRep_Builder();
    const solid = new oc.TopoDS_Solid();
    builder.MakeSolid(solid);
    builder.Add(solid, sew.SewedShape());

    return new Solid(solid);
  }
}

cachedGetters({object: Solid, properties: ["faces", "edges", "vertices", "centerOfMass", "volume"]});