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) + } +}