Face
A Face represents a bounded surface region within a 3D solid — the Swift analog of OCCT’s TopoDS_Face. Faces carry a reference to an underlying geometric surface (plane, cylinder, B-spline, etc.) trimmed by a boundary wire. Obtain faces by calling Shape.faces(), by constructing Face(_ shape:) from a face-typed Shape, or via the filtering helpers Shape.upwardFaces() and Shape.horizontalFaces().
Topics
- Initializers · Properties · Surface Properties (v0.18.0) · Shape Extension — Face Analysis · BRepGProp_Face Evaluation (v0.45.0)
Initializers
Face.init?(_ shape:)
Constructs a Face from a Shape that wraps a TopoDS_Face. Returns nil if the shape is null or wraps a non-face topology type.
public convenience init?(_ shape: Shape)
Inverse of Shape.fromFace(_:). Use when you have a face-typed Shape (e.g. from Shape.subShapes(ofType: .face)) and need the typed Face object.
- Parameters:
shape— aShapewrapping aTopoDS_Face. - Returns:
nilifshapeis null orshape.shapeType != .face. - OCCT:
TopoDS::Face— checksShapeType() == TopAbs_FACE, then casts the underlyingTopoDS_Shape. - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! let faceShapes = box.subShapes(ofType: .face) if let face = Face(faceShapes[0]) { print(face.surfaceType) // .plane }
index
This face’s 0-based position in its parent shape’s face enumeration (-1 if standalone).
public let index: Int
Set automatically when a Face is created via Shape.faces() or Shape.face(at:); faces constructed via Face(_ shape:) get index -1.
This is the addressing token every face-index-taking method on Shape expects — face(at:), drafted(faces:…), shelled(thickness:openFaces:), withoutFeatures(faces:), edgesInFace(at:) and the rest. It is only meaningful against the shape it came from: an index taken from one shape and used on another names an unrelated face, or nothing at all. (#541)
- Example:
let faces = Shape.box(width: 10, height: 5, depth: 2)!.faces() for face in faces { print(face.index) // 0, 1, 2, 3, 4, 5 }
Properties
normal
The outward normal vector at the parametric centre of the face.
public var normal: SIMD3<Double>? { get }
Evaluates the surface normal at the midpoint of the face’s UV parameter range. Respects face orientation (TopAbs_REVERSED flips the result).
- Returns: Unit normal vector, or
nilif the normal is undefined at the centre (e.g. degenerate face or singular parametric point). - OCCT:
BRepAdaptor_Surface+BRepLProp_SLProps::Normal— adapts the face, evaluates at(uMid, vMid), at the samePrecision::Confusion()resolution asnormal(atU:v:)since #529. - Note: that resolution reaches
CSLib::Normalas a sine tolerance on the angle between the two parametric directions, not as a length. A surface whose derivatives merely shrink (a cone at its apex, a sphere at its pole) therefore keeps a defined normal; only a nearly singular parameterisation, where the two directions become parallel, returnsnil. - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! for face in box.faces() { if let n = face.normal { print(n) // one of ±X, ±Y, ±Z for a box } }
orientation
The orientation this face carries in the shape it came from.
public var orientation: Shape.Orientation { get }
The flag that decides which side of the surface normal and normal(atU:v:) report: both reverse the surface normal exactly when this is .reversed. OCCT’s own definition is that a face’s orientation names which side of it is material — for a space bounded by a face, the default region lies on the negative side of the surface normal.
A face can appear in one shape twice with opposite orientations, the ordinary result of a split leaving two solids that share a wall. Shape.faces() keeps one entry per distinct face and so carries only one of them; Shape.orientedFaces() keeps both. Two orientedFaces() entries with the same index and different orientation are the two sides of one shared wall. (#614)
- Returns:
.forward,.reversed,.internalor.external. - OCCT:
TopoDS_Shape::Orientation()(TopoDS_Shape.hxx:118). - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! let halves = box.split(atPlane: SIMD3(0, 0, 4), normal: SIMD3(0, 0, 1))! let compound = Shape.compound(halves)! let shared = Dictionary(grouping: compound.orientedFaces(), by: \.index) .filter { $0.value.count > 1 } for (index, sides) in shared { print(index, sides.map(\.orientation)) // [.forward, .reversed] }
outerWire
The outer boundary wire of the face.
public var outerWire: Wire? { get }
Returns the outermost wire (the single outer boundary loop); inner wires (holes) are not returned here.
- Returns: The outer
Wire, ornilif the face has no outer wire or retrieval fails. - OCCT:
BRepTools::OuterWire— returns the outermostTopoDS_Wireof the face. - Example:
if let face = Shape.box(width: 5, height: 5, depth: 5)!.faces().first, let boundary = face.outerWire { print(boundary.length) }
bounds
The axis-aligned bounding box of the face.
public var bounds: (min: SIMD3<Double>, max: SIMD3<Double>) { get }
- Returns: Tuple of min and max corners of the AABB. Returns
(.zero, .zero)on error. - OCCT:
BRepBndLib::Add+Bnd_Box::Get. - Note: This box is enlarged by the face’s mesh deflection whenever the shape has already been meshed (
BRepBndLib::Add’s documenteduseTriangulation=truebehavior); the same face can report a looser box after a call toShape.mesh(linearDeflection:angularDeflection:)than before it, with no other change (#733).AAG’s own floor/wall matching (detectPockets(tolerance:)) reads a tessellation-independent bound internally for this reason. - Example:
let face = Shape.box(width: 10, height: 5, depth: 2)!.faces()[0] let bb = face.bounds // bb.min and bb.max define the face's extents
(Internal, not public API. Face stores its bridge pointer as internal let handle: OCCTFaceRef (Swift default access), released by the OCCT bridge; see Memory Management. exactBounds (internal var) is the counterpart to bounds above, computed from the face’s exact geometry only via OCCTFaceGetBoundsExact, ignoring any triangulation the shape may carry (#733). Used internally by AAG’s floor/wall matching, not exposed publicly. boundsVia(_:) (private func) is the shared implementation bounds and exactBounds both call into, parameterized by which bridge function to invoke, so the six-out-parameter unpacking dance lives in one place.)
isPlanar
Whether the face’s underlying surface is a plane.
public var isPlanar: Bool { get }
- Returns:
trueif the underlyingGeomAbs_SurfaceTypeisGeomAbs_Plane. - OCCT:
BRepAdaptor_Surface::GetType() == GeomAbs_Plane. - Example:
let face = Shape.box(width: 10, height: 10, depth: 10)!.faces()[0] #expect(face.isPlanar) // box faces are planar
isHorizontal(tolerance:)
Whether the face’s normal is pointing up or down (parallel to the Z axis within the given tolerance).
public func isHorizontal(tolerance: Double = 0.01) -> Bool
Pure-Swift: computes abs(normal.z) > cos(tolerance).
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns:
trueif the face’s centre normal is withintoleranceof the ±Z axis;falseifnormalisnil. - Example:
let top = Shape.box(width: 10, height: 10, depth: 5)!.faces() .filter { $0.isHorizontal() } #expect(top.count == 2) // top and bottom of box
isUpwardFacing(tolerance:)
Whether the face’s normal points upward (positive Z component).
public func isUpwardFacing(tolerance: Double = 0.01) -> Bool
Pure-Swift: computes normal.z > cos(tolerance).
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns:
trueif the face is horizontal and upward-facing;falseifnormalisnil. - Example:
let floor = Shape.box(width: 10, height: 10, depth: 5)!.faces() .filter { $0.isUpwardFacing() } #expect(floor.count == 1) // top face
isDownwardFacing(tolerance:)
Whether the face’s normal points downward (negative Z component).
public func isDownwardFacing(tolerance: Double = 0.01) -> Bool
Pure-Swift: computes normal.z < -cos(tolerance).
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns:
trueif the face is horizontal and downward-facing;falseifnormalisnil. - Example:
let ceiling = Shape.box(width: 10, height: 10, depth: 5)!.faces() .filter { $0.isDownwardFacing() } #expect(ceiling.count == 1) // bottom face
isVertical(tolerance:)
Whether the face’s normal is horizontal (perpendicular to Z axis).
public func isVertical(tolerance: Double = 0.01) -> Bool
Pure-Swift: computes abs(normal.z) < sin(tolerance).
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns:
trueif the face’s normal lies in the XY plane withintolerance;falseifnormalisnil. - Example:
let walls = Shape.box(width: 10, height: 10, depth: 5)!.faces() .filter { $0.isVertical() } #expect(walls.count == 4) // four side faces
zLevel
The Z coordinate of a horizontal planar face.
public var zLevel: Double? { get }
Returns nil if the face is not planar, not horizontal (normal within 0.99 of ±Z), or the bridge call fails.
- Returns: The Z coordinate of the face’s plane location, or
nilif the face is non-planar or non-horizontal. - OCCT:
BRepAdaptor_Surface::Plane()→gp_Pln::Location().Z(). - Example:
let box = Shape.box(width: 10, height: 10, depth: 5)! let zLevels = box.faces().compactMap { $0.zLevel } // zLevels contains 0.0 (bottom) and 5.0 (top)
Surface Properties (v0.18.0)
SurfaceType
Classification of the underlying geometric surface type.
public enum SurfaceType: Int32, Sendable {
case plane = 0, cylinder = 1, cone = 2, sphere = 3, torus = 4
case bezierSurface = 5, bsplineSurface = 6
case surfaceOfRevolution = 7, surfaceOfExtrusion = 8
case offsetSurface = 9, other = 10
}
Corresponds to OCCT’s GeomAbs_SurfaceType enumeration, mapped via BRepAdaptor_Surface::GetType().
Surface (Surface.swift) declares its own byte-for-byte identical SurfaceType enum for the same GeomAbs_SurfaceType mapping (its surfaceKind uses OCCTSurfaceGetType where Face.surfaceType below uses OCCTFaceGetSurfaceType): two independent declarations of the same classification, not one shared type.
| Case | GeomAbs_SurfaceType | Meaning |
|---|---|---|
plane | GeomAbs_Plane | Flat plane. |
cylinder | GeomAbs_Cylinder | Cylindrical surface. |
cone | GeomAbs_Cone | Conical surface. |
sphere | GeomAbs_Sphere | Spherical surface. |
torus | GeomAbs_Torus | Toroidal surface. |
bezierSurface | GeomAbs_BezierSurface | Geom_BezierSurface, a single Bezier patch. |
bsplineSurface | GeomAbs_BSplineSurface | Geom_BSplineSurface, a general B-spline surface. |
surfaceOfRevolution | GeomAbs_SurfaceOfRevolution | Geom_SurfaceOfRevolution, a curve swept about an axis. |
surfaceOfExtrusion | GeomAbs_SurfaceOfExtrusion | Geom_SurfaceOfLinearExtrusion, a curve swept along a linear direction. |
offsetSurface | GeomAbs_OffsetSurface | Geom_OffsetSurface, a constant-distance offset of a basis surface. |
other | (anything else) | Fallback for a surface type this classification doesn’t otherwise name, and the value used when the underlying bridge call fails to determine a type. |
(Per-case anchors below, for cross-reference; the table above has the actual meaning of each. Given both Face.SurfaceType and Surface.SurfaceType declare identical case sets independently, each case is anchored under both.)
Surface.SurfaceType.surfaceOfRevolution
PrincipalCurvatures
Result of a principal curvature query at a UV parameter.
public struct PrincipalCurvatures: Sendable {
public let kMin: Double
public let kMax: Double
public let dirMin: SIMD3<Double>
public let dirMax: SIMD3<Double>
}
kMin/kMax— minimum and maximum principal curvatures (reciprocals of principal radii).dirMin/dirMax— unit direction vectors of the principal curvature lines on the surface.
PrincipalCurvatures.kMin
Minimum principal curvature (reciprocal of the maximum principal radius).
PrincipalCurvatures.kMax
Maximum principal curvature (reciprocal of the minimum principal radius).
PrincipalCurvatures.dirMin
Unit direction vector of the minimum-curvature principal line on the surface.
PrincipalCurvatures.dirMax
Unit direction vector of the maximum-curvature principal line on the surface, perpendicular to dirMin.
SurfaceProjection
Result of projecting a 3D point onto the face’s surface.
public struct SurfaceProjection: Sendable {
public let point: SIMD3<Double>
public let u: Double
public let v: Double
public let distance: Double
}
point— closest 3D point on the surface.u,v— UV parameters of that point.distance— Euclidean distance from the query point topoint.
Face.SurfaceProjection.u
U parameter of the closest point.
Face.SurfaceProjection.v
V parameter of the closest point.
uvBounds
The UV parameter bounds of the face as trimmed by its boundary wires.
public var uvBounds: (uMin: Double, uMax: Double, vMin: Double, vMax: Double)? { get }
Uses BRepTools::UVBounds which accounts for the face’s trimming wires. For integration-ready bounds that respect face orientation, use naturalBounds instead.
- Returns: UV extents, or
nilon error. - OCCT:
BRepTools::UVBounds. - Example:
if let uv = face.uvBounds { let uMid = (uv.uMin + uv.uMax) / 2 let vMid = (uv.vMin + uv.vMax) / 2 let center = face.point(atU: uMid, v: vMid) }
surfaceType
The geometric surface type of this face.
public var surfaceType: SurfaceType { get }
Never fails — returns .other if the type cannot be determined.
- OCCT:
BRepAdaptor_Surface::GetType()mapped toSurfaceType. - Example:
let cyl = Shape.cylinder(radius: 5, height: 10)! let side = cyl.faces().first { $0.surfaceType == .cylinder }
area(tolerance:)
The surface area of the face.
public func area(tolerance: Double = 1e-6) -> Double
- Parameters:
tolerance— numerical integration tolerance (default 1e-6). - Returns: Area in squared model units; returns
-1.0on error. - OCCT:
BRepGProp::SurfaceProperties+GProp_GProps::Mass. - Example:
let topFace = Shape.box(width: 10, height: 5, depth: 2)! .faces().filter { $0.isUpwardFacing() }.first! #expect(topFace.area() ≈ 50.0)
point(atU:v:)
Evaluates the 3D point on the surface at UV parameters.
public func point(atU u: Double, v: Double) -> SIMD3<Double>?
- Parameters:
u— U surface parameter;v— V surface parameter. - Returns: 3D point, or
nilif the surface is null or evaluation fails. - OCCT:
BRep_Tool::Surface+Geom_Surface::D0(u, v, pnt). - Example:
if let uv = face.uvBounds { let mid = face.point(atU: (uv.uMin + uv.uMax) / 2, v: (uv.vMin + uv.vMax) / 2) }
normal(atU:v:)
Evaluates the outward surface normal at UV parameters.
public func normal(atU u: Double, v: Double) -> SIMD3<Double>?
Respects face orientation (TopAbs_REVERSED flips the result). Uses order-1 surface properties.
- Parameters:
u— U parameter;v— V parameter. - Returns: Unit normal vector, or
nilif the normal is undefined at(u, v). - OCCT:
BRep_Tool::Surface+GeomLProp_SLProps::Normal(order 1,Precision::Confusion()tolerance). - Example:
if let uv = face.uvBounds, let n = face.normal(atU: uv.uMin, v: uv.vMin) { print(n) }
gaussianCurvature(atU:v:)
The Gaussian curvature of the surface at UV parameters.
public func gaussianCurvature(atU u: Double, v: Double) -> Double?
Gaussian curvature = k₁ × k₂ (product of principal curvatures). Positive on convex/concave surfaces; negative on saddle surfaces; zero on developable surfaces.
- Parameters:
u— U parameter;v— V parameter. - Returns: Gaussian curvature value, or
nilif curvature is undefined at(u, v). - OCCT:
GeomLProp_SLProps::GaussianCurvature(order 2). - Example:
let sphere = Shape.sphere(radius: 5)! if let face = sphere.faces().first, let uv = face.uvBounds { let k = face.gaussianCurvature(atU: (uv.uMin + uv.uMax) / 2, v: (uv.vMin + uv.vMax) / 2) // k ≈ 1/25 (= 1/r²) for a sphere of radius 5 }
meanCurvature(atU:v:)
The mean curvature of the surface at UV parameters.
public func meanCurvature(atU u: Double, v: Double) -> Double?
Mean curvature = (k₁ + k₂) / 2. Zero on minimal surfaces; equal to 1/R on a sphere of radius R.
- Parameters:
u— U parameter;v— V parameter. - Returns: Mean curvature value, or
nilif curvature is undefined at(u, v). - OCCT:
GeomLProp_SLProps::MeanCurvature(order 2). - Example:
if let face = Shape.cylinder(radius: 5, height: 10)!.faces() .first(where: { $0.surfaceType == .cylinder }), let uv = face.uvBounds { let h = face.meanCurvature(atU: (uv.uMin + uv.uMax) / 2, v: (uv.vMin + uv.vMax) / 2) // h ≈ 0.1 (1/(2r)) for a cylinder }
principalCurvatures(atU:v:)
The principal curvatures and their directions at UV parameters.
public func principalCurvatures(atU u: Double, v: Double) -> PrincipalCurvatures?
Returns both principal curvature values (kMin, kMax) and their surface directions. Requires order-2 surface property evaluation.
- Parameters:
u— U parameter;v— V parameter. - Returns:
PrincipalCurvaturesstruct, ornilif curvature is undefined at(u, v). - OCCT:
GeomLProp_SLProps::MinCurvature,MaxCurvature,CurvatureDirections(order 2). - Example:
if let uv = face.uvBounds, let pc = face.principalCurvatures(atU: (uv.uMin + uv.uMax) / 2, v: (uv.vMin + uv.vMax) / 2) { print(pc.kMin, pc.kMax, pc.dirMin, pc.dirMax) }
project(point:)
Projects a 3D point onto the face’s surface, returning the closest point.
public func project(point: SIMD3<Double>) -> SurfaceProjection?
Restricts the projection to the face’s UV parameter range (as returned by BRepTools::UVBounds).
- Parameters:
point— 3D query point. - Returns:
SurfaceProjectionwith the closest 3D point, its UV parameters, and the distance;nilif no projection exists or the face is null. - OCCT:
GeomAPI_ProjectPointOnSurf::NearestPoint/LowerDistanceParameters/LowerDistance. - Example:
let face = Shape.box(width: 10, height: 10, depth: 10)!.faces()[0] if let proj = face.project(point: SIMD3(5, 5, 20)) { print(proj.point, proj.distance) }
allProjections(of:)
Returns all projection results (not just the nearest) for a 3D point onto the face’s surface.
public func allProjections(of point: SIMD3<Double>) -> [SurfaceProjection]
Uses a fixed buffer of up to 32 results. For most surfaces there is only one projection; multiple results arise on periodic or multiply-connected surfaces.
- Parameters:
point— 3D query point. - Returns: Array of
SurfaceProjectionvalues (may be empty if no projections are found). - OCCT:
GeomAPI_ProjectPointOnSurf::NbPoints/Point(i)/Parameters(i)/Distance(i). - Example:
let projections = face.allProjections(of: SIMD3(0, 0, 100)) for p in projections { print(p.point, p.u, p.v, p.distance) }
intersection(with:tolerance:)
Computes the intersection curves between this face and another face.
public func intersection(with other: Face, tolerance: Double = 1e-6) -> Shape?
The result is a Shape containing intersection edges (or a compound thereof). Returns nil if the faces do not intersect or construction fails.
- Parameters:
other— the second face;tolerance— fuzzy intersection tolerance (default 1e-6). - Returns: A
Shapecontaining the intersection curves, ornilif there is no intersection. - OCCT:
BRepAlgoAPI_Section—Approximation(true),ComputePCurveOn1(true),ComputePCurveOn2(true),SetFuzzyValue(tolerance). - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! let faces = box.faces() if faces.count >= 2, let seam = faces[0].intersection(with: faces[1]) { // seam contains the shared edge geometry }
Shape Extension — Face Analysis
These methods are declared as extensions on Shape and operate on the face sub-shapes of a solid.
Shape.faces()
Returns all face sub-shapes of the solid as typed Face objects.
public func faces() -> [Face]
Each returned Face carries its position as Face.index. This is the same enumeration Shape.faceCount counts and Shape.face(at:) indexes: one entry per distinct face, in TopExp_Explorer order. A face reachable from two parents — the wall shared by both halves of a split solid, say — appears once. Returns an empty array if the shape has no faces.
This is the indexing enumeration. Faces are distinguished the way OCCT distinguishes them for an index: by TopoDS_Shape::IsSame, which compares surface and placement and ignores orientation. A face occurring in the shape with both orientations therefore collapses to one entry carrying whichever was reached first — so its Face.normal(atU:v:) points out of one owning solid and into the other. Use Shape.orientedFaces() when the normal’s direction matters. (#614)
- OCCT:
TopExp::MapShapes(shape, TopAbs_FACE, …)via the bridge’s shared sub-shape enumeration. - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! let faces = box.faces() #expect(faces.count == 6) #expect(faces.count == box.faceCount) // Every index handed out here is addressable, and names the same face. #expect(faces.allSatisfy { box.face(at: $0.index) != nil }) // A split solid shares its cut face: 11 distinct faces, 12 occurrences. let halves = box.split(atPlane: SIMD3(0, 0, 4), normal: SIMD3(0, 0, 1))! let compound = Shape.compound(halves)! #expect(compound.faces().count == 11) #expect(compound.orientedFaces().count == 12) - Note: Until #541 this was a separate walk yielding one entry per occurrence in the topology tree, so on a shape with a shared face it was longer than
faceCount, its surplus indices named facesface(at:)could not address, and past the duplicate it named a different face than every index-taking method resolved.
Shape.orientedFaces()
Returns every face occurrence, each carrying the orientation it has in its parent.
public func orientedFaces() -> [Face]
The geometry enumeration. Walk this, not faces(), whenever the direction of a face normal matters: rendering, CAM, per-face area or flux accumulation, anything asking “which way is out”. A face shared by two solids appears twice, once per owner, each time with the orientation that makes Face.normal(atU:v:) point out of that owner.
An entry’s array position is an occurrence number, not a face index — two positions can name the same face. Each returned Face still carries the correct Face.index into faces(), so an occurrence remains addressable by every face-index-taking method on Shape; entries sharing an index are the several sides of one shared face. On a shape whose faces are not shared this returns exactly faces(), same order, same indices.
- OCCT:
TopExp_Explorer(shape, TopAbs_FACE), with each entry’s index resolved through the sameTopExp::MapShapesmapfaces()is built from. This mirrors the kernel’s own split:TopExp::MapShapespublishes no orientation-sensitive overload (TopExp.hxx:57-60) and BREP persistence indexes sub-shapes through theIsSamemap (TopTools_ShapeSet.hxx:192), whileBRepGProp::VolumeProperties— where orientation sets the sign of the volume integral — reads it offex.Current()and keeps oneIsSamemap per orientation so a shared wall’s two sides both survive (BRepGProp.cxx:318-338). - Example:
let box = Shape.box(width: 10, height: 10, depth: 10)! let halves = box.split(atPlane: SIMD3(0, 0, 4), normal: SIMD3(0, 0, 1))! let compound = Shape.compound(halves)! // Outward normals for the whole assembly, shared wall included. for face in compound.orientedFaces() { guard let uv = face.uvBounds else { continue } let n = face.normal(atU: (uv.uMin + uv.uMax) / 2, v: (uv.vMin + uv.vMax) / 2) print(face.index, face.orientation, n ?? .zero) } // The cut face appears twice under one index, once per orientation. let shared = Dictionary(grouping: compound.orientedFaces(), by: \.index) .filter { $0.value.count > 1 } #expect(shared.count == 1) if let sides = shared.first?.value { #expect(Set(sides.map(\.orientation)) == Set([.forward, .reversed])) }
Shape.horizontalFaces(tolerance:)
Returns the subset of faces whose normals point up or down.
public func horizontalFaces(tolerance: Double = 0.01) -> [Face]
Pure-Swift filter over orientedFaces() using Face.isHorizontal(tolerance:). “Horizontal” is a statement about the face normal, so this selects over occurrences: a wall shared by two bodies is horizontal from both sides and contributes one entry per side. Filtering faces() — which cannot carry a shared face’s second orientation — returned 3 on the fixture below where the geometry has 4. (#614)
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns: Faces with normals within
toleranceof the ±Z axis, one per occurrence. - Note: Because a shared face contributes one entry per side, the result can contain two
Facevalues with the sameFace.index. On a shape whose faces are not shared it is identical to filteringfaces(). - Example:
let box = Shape.box(width: 10, height: 10, depth: 5)! let h = box.horizontalFaces() #expect(h.count == 2) // A split solid: the shared wall is horizontal from both sides. let cube = Shape.box(width: 10, height: 10, depth: 10)! let halves = cube.split(atPlane: SIMD3(0, 0, 4), normal: SIMD3(0, 0, 1))! let compound = Shape.compound(halves)! #expect(compound.horizontalFaces().count == 4) // was 3 before #614
Shape.upwardFaces(tolerance:)
Returns the subset of faces whose normals point upward (positive Z).
public func upwardFaces(tolerance: Double = 0.01) -> [Face]
Pure-Swift filter over orientedFaces() using Face.isUpwardFacing(tolerance:). Useful for identifying pocket floors and platform surfaces in CAM. Selects over occurrences for the same reason horizontalFaces() does — “upward-facing” is a statement about the normal. (#614)
- Parameters:
tolerance— angle tolerance in radians (default ~0.57°). - Returns: Upward-facing horizontal faces, one per occurrence.
- Note: Like
horizontalFaces(), this selects over occurrences and so can repeat aFace.index. Dedupe onindex(or usefaces()) if you need one entry per distinct face. It is only for the shared-wall case — two solids whose parents impose opposite orientations — that at most one side can face up, and that follows from the normals being opposed rather than from any guarantee here. When a face is reached twice through parents imposing the same orientation both entries qualify:Shape.compound([box, box]).upwardFaces()returns indices[5, 5]. - Note:
isUpwardFacingtestsn.z > cos(tolerance), so atoleranceof π/2 or more makes the threshold non-positive and admits faces that do not point up at all — including both sides of a vertical shared wall, whose normals haven.z == 0. On a two-solid split,upwardFaces(tolerance: 1.6)returns 10 entries over 9 distinct indices. - Example:
let pocketFloors = myPart.upwardFaces() for face in pocketFloors { print(face.zLevel ?? "non-planar") }
Shape.facesByZLevel(tolerance:)
Groups horizontal faces by their Z height.
public func facesByZLevel(tolerance: Double = 0.01) -> [Double: [Face]]
Calls horizontalFaces(), then groups faces whose zLevel values are within tolerance of each other. Designed for CAM pocket detection and layer-by-layer machining analysis.
- Parameters:
tolerance— Z grouping tolerance; faces within this Z distance are placed in the same group. - Returns: Dictionary mapping representative Z values to arrays of horizontal faces at that level.
- Note: Only planar horizontal faces (those with a non-nil
zLevel) are included; non-planar horizontal faces are silently dropped. - Note: Inherits
horizontalFaces()’s occurrence semantics — a wall shared by two bodies lands in its Z group twice, once per side, where filteringfaces()reported it once and only from whichever side happened to be stored. (#614) - Example:
let stepped = Shape.box(width: 10, height: 10, depth: 5)! // simplified let byZ = stepped.facesByZLevel() for (z, faces) in byZ.sorted(by: { $0.key < $1.key }) { print("Z=\(z): \(faces.count) face(s)") } // A split solid: the cut level carries both owners' copies. let cube = Shape.box(width: 10, height: 10, depth: 10)! let halves = cube.split(atPlane: SIMD3(0, 0, 4), normal: SIMD3(0, 0, 1))! let compound = Shape.compound(halves)! print(compound.facesByZLevel().mapValues(\.count)) // [-5.0: 1, 4.0: 2, 5.0: 1]
BRepGProp_Face Evaluation (v0.45.0)
GPropEvaluation
Result of evaluating a face at UV parameters using BRepGProp_Face.
public struct GPropEvaluation: Sendable {
public let point: SIMD3<Double>
public let normal: SIMD3<Double>
}
point— 3D point on the surface at(u, v).normal— unnormalized surface normal (dS/du × dS/dv). The magnitude equals the local area element (Jacobian determinant), making it suitable for numerical surface integration.
naturalBounds
The natural parametric bounds of the face using BRepGProp_Face.
public var naturalBounds: (uMin: Double, uMax: Double, vMin: Double, vMax: Double)? { get }
Unlike uvBounds (which uses BRepTools::UVBounds), this uses BRepGProp_Face::Bounds, which accounts for face orientation and returns integration-ready bounds.
- Returns: UV bounds, or
nilon error. - OCCT:
BRepGProp_Face::Bounds. - Example:
if let nb = face.naturalBounds { let eval = face.evaluateGProp(u: (nb.uMin + nb.uMax) / 2, v: (nb.vMin + nb.vMax) / 2) }
evaluateGProp(u:v:)
Evaluates the face surface at UV parameters via BRepGProp_Face, returning both the 3D point and the unnormalized surface normal.
public func evaluateGProp(u: Double, v: Double) -> GPropEvaluation?
The returned normal is the cross product dS/du × dS/dv whose magnitude equals the local surface area element. Use this for surface integration (e.g. computing area, flux integrals) rather than for visual normal shading (use normal(atU:v:) for unit normals).
- Parameters:
u— U parameter;v— V parameter. - Returns:
GPropEvaluationwith point and unnormalized normal, ornilon error. - OCCT:
BRepGProp_Face::Normal(u, v, point, normal). - Example:
if let nb = face.naturalBounds, let eval = face.evaluateGProp(u: (nb.uMin + nb.uMax) / 2, v: (nb.vMin + nb.vMax) / 2) { let areaElement = simd_length(eval.normal) // Jacobian at this UV point print(eval.point, eval.normal, areaElement) } - Note: The
normalfield is NOT a unit vector — its magnitude carries the area element. Call.normalizedon it only if you need the direction without the scaling factor.