From 681e6ac1a9630e8cf0a13e534a577cd9e1fd6627 Mon Sep 17 00:00:00 2001 From: Franco Ponticelli Date: Thu, 10 Sep 2026 12:34:39 -0600 Subject: [PATCH] Make reachable public API members public Several types are public and reachable from the public API, but members declared inside them default to internal, so a client can hold the value and not use it. Nesting a type in a public extension makes the type public without extending that to its own members, and a synthesized memberwise initializer is internal even for a public struct. Multiproperties.Layer is the clearest case: layerSequences hands out layers whose property and blendMethod cannot be read, which leaves multiproperties unresolvable outside the module. ObjectType.default is named by Object.type's own documentation but cannot be referenced. Alternative, Mesh.TriangleSet and Texture2DGroup.Coordinate can be read back from a parsed model but not constructed, though the public initializers that take them accept arrays of exactly those types. The new tests import ThreeMF plainly rather than with @testable, so they see the package the way a client does and stop compiling if any of this regresses. --- .../ThreeMF/Resources/Multiproperties.swift | 7 +- .../Resources/Object/Alternatives.swift | 9 ++- .../Resources/Object/Mesh.TriangleSet.swift | 11 +++ Sources/ThreeMF/Resources/Object/Object.swift | 2 +- .../ThreeMF/Resources/Texture2DGroup.swift | 6 +- Tests/Tests/PublicAPIAccessTests.swift | 70 +++++++++++++++++++ 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 Tests/Tests/PublicAPIAccessTests.swift diff --git a/Sources/ThreeMF/Resources/Multiproperties.swift b/Sources/ThreeMF/Resources/Multiproperties.swift index 805f36a..207bf88 100644 --- a/Sources/ThreeMF/Resources/Multiproperties.swift +++ b/Sources/ThreeMF/Resources/Multiproperties.swift @@ -61,8 +61,11 @@ public struct Multiproperties: Resource, XMLElementCodable { public extension Multiproperties { /// One layer of a combination: which property it takes, and how it blends onto what's beneath. struct Layer: Sendable { - let property: PropertyReference - let blendMethod: BlendMethod + /// Which entry of which property group this layer takes. + public let property: PropertyReference + + /// How this layer combines with the ones below it. + public let blendMethod: BlendMethod } /// One combination, as layers from the base upward. diff --git a/Sources/ThreeMF/Resources/Object/Alternatives.swift b/Sources/ThreeMF/Resources/Object/Alternatives.swift index 76b30e7..859e8d1 100644 --- a/Sources/ThreeMF/Resources/Object/Alternatives.swift +++ b/Sources/ThreeMF/Resources/Object/Alternatives.swift @@ -19,7 +19,14 @@ public struct Alternative: XMLElementCodable, Sendable { /// How exact this alternative is, so a consumer can tell it apart from the others. public var modelResolution: ModelResolution? - init(objectID: ResourceID, uuid: UUID? = nil, path: URL? = nil, modelResolution: ModelResolution? = nil) { + /// Creates an alternative representation of an object. + /// - Parameters: + /// - objectID: The id of the object holding this representation. + /// - uuid: The identifier this alternative shares with the object it stands in for. A fresh + /// one is generated when you don't give one. + /// - path: The model part the alternative's object lives in, when it isn't this one. + /// - modelResolution: How exact this alternative is. + public init(objectID: ResourceID, uuid: UUID? = nil, path: URL? = nil, modelResolution: ModelResolution? = nil) { self.objectID = objectID self.uuid = uuid ?? UUID() self.path = path diff --git a/Sources/ThreeMF/Resources/Object/Mesh.TriangleSet.swift b/Sources/ThreeMF/Resources/Object/Mesh.TriangleSet.swift index b4d7542..5b8d512 100644 --- a/Sources/ThreeMF/Resources/Object/Mesh.TriangleSet.swift +++ b/Sources/ThreeMF/Resources/Object/Mesh.TriangleSet.swift @@ -21,6 +21,17 @@ public extension Mesh { /// /// Written as ranges where the indices are contiguous, so a set covering a whole region stays compact. public var triangleIndices: IndexSet + + /// Creates a triangle set. + /// - Parameters: + /// - name: A human-readable name for the set. + /// - identifier: An identifier for the set, unique within its mesh. + /// - triangleIndices: The indices into ``Mesh/triangles`` that belong to this set. + public init(name: String, identifier: String, triangleIndices: IndexSet) { + self.name = name + self.identifier = identifier + self.triangleIndices = triangleIndices + } } } diff --git a/Sources/ThreeMF/Resources/Object/Object.swift b/Sources/ThreeMF/Resources/Object/Object.swift index 53d4794..f7aab03 100644 --- a/Sources/ThreeMF/Resources/Object/Object.swift +++ b/Sources/ThreeMF/Resources/Object/Object.swift @@ -171,7 +171,7 @@ public extension Object { case other /// The type an object has when the file doesn't say: ``ObjectType/model``. - static let `default` = Self.model + public static let `default` = Self.model } } diff --git a/Sources/ThreeMF/Resources/Texture2DGroup.swift b/Sources/ThreeMF/Resources/Texture2DGroup.swift index a2aa11a..6e58060 100644 --- a/Sources/ThreeMF/Resources/Texture2DGroup.swift +++ b/Sources/ThreeMF/Resources/Texture2DGroup.swift @@ -62,7 +62,11 @@ public extension Texture2DGroup { /// The vertical position across the image, from 0 to 1. public let v: Double - init(u: Double, v: Double) { + /// Creates a texture coordinate. + /// - Parameters: + /// - u: The horizontal position across the image, from 0 to 1. + /// - v: The vertical position across the image, from 0 to 1. + public init(u: Double, v: Double) { self.u = u self.v = v } diff --git a/Tests/Tests/PublicAPIAccessTests.swift b/Tests/Tests/PublicAPIAccessTests.swift new file mode 100644 index 0000000..f9209b5 --- /dev/null +++ b/Tests/Tests/PublicAPIAccessTests.swift @@ -0,0 +1,70 @@ +import Testing +import Foundation +import Nodal + +// Deliberately a plain import rather than `@testable`, so this file sees the package exactly as a +// client does. Every symbol below is reachable from the public API; if one of them loses its +// `public` again, this file stops compiling. +import ThreeMF + +struct PublicAPIAccessTests { + @Test func `a multiproperties layer reports the property it takes and how it blends`() throws { + let multiproperties = Multiproperties( + id: 3, + propertyGroupIDs: [1, 2], + blendMethods: [.multiply], + multis: [[0, 1]] + ) + + let layers = try #require(multiproperties.layerSequences.first) + #expect(layers.count == 2) + + #expect(layers[0].property == PropertyReference(groupID: 1, index: 0)) + #expect(layers[1].property == PropertyReference(groupID: 2, index: 1)) + #expect(layers[1].blendMethod == .multiply) + } + + @Test func `an object's type falls back to the documented default`() { + let object = Object(id: 1, content: .mesh(Mesh(vertices: [], triangles: []))) + + // The file said nothing, which per the spec means `.model`. + #expect(object.type == nil) + #expect(object.type ?? .default == .model) + } + + @Test func `a triangle set can be built and handed to a mesh`() { + let set = Mesh.TriangleSet(name: "Top", identifier: "top", triangleIndices: IndexSet(0..<2)) + let mesh = Mesh(vertices: [], triangles: [], triangleSets: [set]) + + #expect(mesh.triangleSets.count == 1) + #expect(mesh.triangleSets[0].name == "Top") + #expect(mesh.triangleSets[0].identifier == "top") + #expect(mesh.triangleSets[0].triangleIndices == IndexSet(0..<2)) + } + + @Test func `texture coordinates can be built and handed to a group`() { + let group = Texture2DGroup(id: 2, texture2DID: 1, coordinates: [ + Texture2DGroup.Coordinate(u: 0, v: 0), + Texture2DGroup.Coordinate(u: 1, v: 0.5), + ]) + + #expect(group.coordinates.count == 2) + #expect(group.coordinates[1].u == 1) + #expect(group.coordinates[1].v == 0.5) + } + + @Test func `an alternative can be built and handed to an object`() { + let uuid = UUID() + let alternative = Alternative(objectID: 7, uuid: uuid, modelResolution: .low) + let object = Object( + id: 1, + alternatives: [alternative], + content: .mesh(Mesh(vertices: [], triangles: [])) + ) + + #expect(object.alternatives.count == 1) + #expect(object.alternatives[0].objectID == 7) + #expect(object.alternatives[0].uuid == uuid) + #expect(object.alternatives[0].modelResolution == .low) + } +}