modeling_face.js

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

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

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

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

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

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

  /**
   * Retrieves the outer wire (boundry) of the face.
   * @returns {Wire} A `Wire` object representing the outer wire (boundry) of the face.
   */
  get outerWire() {
    return new Wire(oc.BRepTools.OuterWire(this.#wrapped));
  }

  /**
   * Retrieves all inner wires (holes) of the face.
   * @returns {Wire[]} An array of `Wire` objects representing the inner wires (holes) of the face.
   */
  get innerWires() {
    const all = explore({shape: this.#wrapped, find: oc.TopAbs_ShapeEnum.TopAbs_WIRE});
    const outer = oc.BRepTools.OuterWire(this.#wrapped);
    return all.filter((wire) => !wire.IsSame(outer)).map((wire) => new Wire(wire));
  }

  /**
   * Computes the normal vector of the face.
   * @returns {Vector | undefined} The normal vector if the face is a plane, otherwise `undefined`.
   */
  get normal() {
    if (!(this.type === Face.TYPE.Plane)) {
      return undefined;
    }

    const normal = this.#asSurface().Plane().Axis().Direction();
    if (this.#wrapped.Orientation_1() !== oc.TopAbs_Orientation.TopAbs_FORWARD) {
      normal.Reverse();
    }
    return new Vector({
      x: normal.X(),
      y: normal.Y(),
      z: normal.Z(),
    });
  }

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

  /**
   * Computes the surface area of the face.
   * @returns {number} The surface area of the face.
   */
  get surfaceArea() {
    return this.#surfaceProperties().Mass();
  }

  /**
   * Computes the x-direction vector of the face.
   * @returns {Vector | undefined} The x-direction vector if the face is a plane, otherwise `undefined`.
   */
  get xDirection() {
    if (!(this.type === Face.TYPE.Plane)) {
      return undefined;
    }

    let direction;
    if (this.#wrapped.Orientation_1() === oc.TopAbs_Orientation.TopAbs_FORWARD) {
      direction = this.#asSurface().Plane().XAxis().Direction();
    } else {
      direction = this.#asSurface().Plane().YAxis().Direction();
    }
    return new Vector({
      x: direction.X(),
      y: direction.Y(),
      z: direction.Z(),
    });
  }

  /**
   * Determines the type of the face's surface.
   * @returns {string} The type of the face's surface (e.g., "Plane", "Cylinder").
   */
  get type() {
    switch (this.#asSurface().GetType()) {
      case oc.GeomAbs_SurfaceType.GeomAbs_Plane:
        return Face.TYPE.Plane;
      case oc.GeomAbs_SurfaceType.GeomAbs_Cylinder:
        return Face.TYPE.Cylinder;
      case oc.GeomAbs_SurfaceType.GeomAbs_Cone:
        return Face.TYPE.Cone;
      case oc.GeomAbs_SurfaceType.GeomAbs_Sphere:
        return Face.TYPE.Sphere;
      case oc.GeomAbs_SurfaceType.GeomAbs_Torus:
        return Face.TYPE.Torus;
      case oc.GeomAbs_SurfaceType.GeomAbs_BezierSurface:
        return Face.TYPE.BezierSurface;
      case oc.GeomAbs_SurfaceType.GeomAbs_BSplineSurface:
        return Face.TYPE.BSplineSurface;
      case oc.GeomAbs_SurfaceType.GeomAbs_SurfaceOfRevolution:
        return Face.TYPE.SurfaceOfRevolution;
      case oc.GeomAbs_SurfaceType.GeomAbs_SurfaceOfExtrusion:
        return Face.TYPE.SurfaceOfExtrusion;
      case oc.GeomAbs_SurfaceType.GeomAbs_OffsetSurface:
        return Face.TYPE.OffsetSurface;
      case oc.GeomAbs_SurfaceType.GeomAbs_OtherSurface:
        return Face.TYPE.OtherSurface;
      default:
        return "undefined";
    }
  }

  /**
   * Determines the orientation of the face.
   * @returns {string} The orientation ("Forward", "Reversed", "Internal", or "External").
   */
  get orientation() {
    switch (this.#wrapped.Orientation_1()) {
      case oc.TopAbs_Orientation.TopAbs_FORWARD:
        return Face.ORIENTATION.Forward;
      case oc.TopAbs_Orientation.TopAbs_REVERSED:
        return Face.ORIENTATION.Reversed;
      case oc.TopAbs_Orientation.TopAbs_INTERNAL:
        return Face.ORIENTATION.Internal;
      case oc.TopAbs_Orientation.TopAbs_EXTERNAL:
        return Face.ORIENTATION.External;
    }
  }

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

  /**
   * Rotates the face 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 {Face} A new `Face` object representing the rotated face.
   */
  rotate({axis, angle}) {
    return new Face(rotate({shape: this.#wrapped, axis: axis.wrapped, angle: angle}));
  }

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

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

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

  /**
   * Cuts the face using one or more tools (other faces).
   * @param {...Face} tools - One or more `Face` objects to use as cutting tools.
   * @returns {Face[]} An array of `Face` 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_FACE}).map((shape) => new Face(shape));
  }

  /**
   * Intersects the face with one or more tools (other faces).
   * @param {...Face} tools - One or more `Face` objects to intersect with.
   * @returns {Face[]} An array of `Face` 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_FACE}).map((shape) => new Face(shape));
  }

  /**
   * Joins the face with one or more tools (other faces).
   * @param {...Face} tools - One or more `Face` objects to join with.
   * @returns {Face[]} An array of `Face` 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_FACE}).map((shape) => new Face(shape));
  }

  /**
   * Applies a fillet (rounded edge) to selected vertices of the face.
   * @param {Object} parameters - Fillet parameters.
   * @param {function(Vertex): boolean} parameters.selector - A function to select vertices for the fillet.
   * @param {number} parameters.radius - The radius of the fillet.
   * @returns {Face} A new `Face` object with the fillet applied.
   */
  fillet({selector, radius}) {
    const fillet = new oc.BRepFilletAPI_MakeFillet2d_2(this.#wrapped);
    this.vertices.filter(selector).forEach((vertex) => fillet.AddFillet(vertex.wrapped, radius));
    const faces = explore({shape: fillet.Shape(), find: oc.TopAbs_ShapeEnum.TopAbs_FACE});
    return new Face(faces[0]);
  }

  /**
   * Applies a chamfer (beveled edge) to selected vertices of the face.
   * @param {Object} parameters - Chamfer parameters.
   * @param {function(Edge): boolean} parameters.selector - A function to select vertices for the chamfer.
   * @param {number} parameters.distance - The distance of the chamfer.
   * @returns {Face} A new `Face` object with the chamfer applied.
   */
  chamfer({selector, distance}) {
    const chamfer = new oc.BRepFilletAPI_MakeFillet2d_2(this.#wrapped);
    this.vertices.filter(selector).forEach((selectedVertex) => {
      const parentEdges = this.edges.filter((edge) => edge.vertices.find((vertex) => vertex.isEqual(selectedVertex)));
      chamfer.AddChamfer_1(parentEdges[0].wrapped, parentEdges[1].wrapped, distance, distance);
    });
    const faces = explore({shape: chamfer.Shape(), find: oc.TopAbs_ShapeEnum.TopAbs_FACE});
    return new Face(faces[0]);
  }

  /**
   * Extrudes the face by a specified distance to create a 3D solid.
   * @param {number} distance - The extrusion distance.
   * @returns {Solid} A new `Solid` object representing the extruded face.
   */
  extrude(distance) {
    const vector = this.normal.wrapped.Normalized().Multiplied(distance);
    const extrude = new oc.BRepPrimAPI_MakePrism_1(this.#wrapped, vector, false, true);
    return new Solid(extrude.Shape());
  }

  /**
   * Revolves the face around a specified axis by a given angle to create a 3D solid.
   * @param {Object} parameters - Revolution parameters.
   * @param {Axis} parameters.axis - The axis to revolve around.
   * @param {number} parameters.angle - The revolution angle in radians.
   * @returns {Solid} A new `Solid` object representing the revolved face.
   */
  revolve({axis, angle}) {
    const revolve = new oc.BRepPrimAPI_MakeRevol_1(this.#wrapped, axis.wrapped, angle, false);
    return new Solid(revolve.Shape());
  }

  /**
   * Offsets the face's outline by a specified distance. Face holes are also offset by a specified distance.
   * @param {Object} parameters - Offset parameters.
   * @param {number} parameters.distance - The offset distance for the face outline.
   * @param {number} [parameters.holeDistance=0] - The offset distance for the face holes.
   * @param {('arc'|'tangent'|'intersection')} [parameters.joinType="tangent"] - The join type for the offset.
   * @returns {Face} A new `Face` object representing the offset face.
   */
  offset({distance, holeDistance = 0, joinType = "tangent"}) {
    const outerOffsetWire = offset({wire: this.outerWire.wrapped, distance, joinType: JOIN_TYPES[joinType]});
    const innerOffsetWires = this.innerWires.map((wire) =>
      offset({wire: wire.wrapped, distance: holeDistance, joinType: JOIN_TYPES[joinType]})
    );
    const offsetFace = new oc.BRepBuilderAPI_MakeFace_15(outerOffsetWire, true).Face();
    if (innerOffsetWires.length > 0) {
      const faceWithHoles = new oc.BRepBuilderAPI_MakeFace_2(offsetFace);
      innerOffsetWires.forEach((innerWire) => {
        faceWithHoles.Add(innerWire);
      });
      return new Face(faceWithHoles.Face());
    } else {
      return new Face(offsetFace);
    }
  }

  /**
   * Reverses the orientation of the face.
   * @returns {Face} A new `Face` object with the reversed orientation.
   */
  reverse() {
    return new Face(this.#wrapped.Reversed());
  }

  /**
   * Checks if the face is parallel to the given direction. Only applicable for planar faces.
   * @param {Vector} direction - The direction vector to test.
   * @returns {boolean|undefined} `true` if the face is parallel to the direction, `false` otherwise, or `undefined` if not a planar face.
   */
  isParallel(direction) {
    return this.normal?.isPerpendicular(direction);
  }

  /**
   * Checks if the face is perpendicular to the given direction. Only applicable for planar faces.
   * @param {Vector} direction - The direction vector to test.
   * @returns {boolean|undefined} `true` if the face is perpendicular to the direction, `false` otherwise, or `undefined` if not a planar face.
   */
  isPerpendicular(direction) {
    return this.normal?.isParallel(direction);
  }

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

  #asSurface() {
    if (!this.#surface) {
      this.#surface = new oc.BRepAdaptor_Surface_2(this.#wrapped, true);
    }
    return this.#surface;
  }

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

  /**
   * Creates a `Face` object from a wire.
   * The wire must be closed and form a valid boundary for the face.
   * @param {Wire} wire - The wire to create the face from.
   * @returns {Face} a new `Face` object created from the given wire.
   */
  static fromWire(wire) {
    return new Face(new oc.BRepBuilderAPI_MakeFace_15(wire.wrapped, true).Face());
  }

  static TYPE = Object.freeze({
    Plane: "Plane",
    Cylinder: "Cylinder",
    Cone: "Cone",
    Sphere: "Sphere",
    Torus: "Torus",
    BezierSurface: "BezierSurface",
    BSplineSurface: "BSplineSurface",
    SurfaceOfRevolution: "SurfaceOfRevolution",
    SurfaceOfExtrusion: "SurfaceOfExtrusion",
    OffsetSurface: "OffsetSurface",
    OtherSurface: "OtherSurface",
  });

  static ORIENTATION = Object.freeze({
    Forward: "Forward",
    Reversed: "Reversed",
    Internal: "Internal",
    External: "External",
  });
}

cachedGetters({
  object: Face,
  properties: [
    "edges",
    "vertices",
    "normal",
    "centerOfMass",
    "surfaceArea",
    "xDirection",
    "type",
    "orientation",
    "outerWire",
    "innerWires",
  ],
});