Skip to content

Commit 1877ae4

Browse files
committed
Add static build method, documentation to ModelFileGenerator
1 parent 53e3593 commit 1877ae4

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed

Sources/Cadova/Concrete Layer/Build/Model/Model.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
/// A model that can be exported to a file.
3+
/// A model that will be exported to a file in the current working directory.
44
///
55
/// Use `Model` to build geometry and write it to disk in formats like 3MF, STL, or SVG.
66
/// The model is created and exported in a single step using an async initializer.
@@ -14,13 +14,15 @@ import Foundation
1414
/// Models can also be grouped within a ``Project`` to share environment settings and metadata
1515
/// across multiple output files.
1616
///
17+
/// For fine-grained control of file output, see ``ModelFileGenerator``.
18+
///
1719
public struct Model: Sendable, ModelBuildable {
1820
let name: String
1921

2022
private let directives: @Sendable () -> [BuildDirective]
2123
private let options: ModelOptions
2224

23-
/// Creates and exports a model based on the provided geometry.
25+
/// Creates and exports a model to the current working directory based on the provided geometry.
2426
///
2527
/// Use this initializer to construct and write a 3D or 2D model to disk. The model is
2628
/// generated from a geometry tree you define using the result builder. Supported output

Sources/Cadova/Concrete Layer/ModelGenerator.swift

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,112 @@
11
import Foundation
22

3+
/// A model that will be rendered into a standard file format.
4+
///
5+
/// Use `ModelFileGenerator` to build geometry into standard file formats like 3MF, STL, or SVG.
6+
/// The model is rendered into a `ModelFile`, from which data can be accessed in memory or written
7+
/// to disk.
8+
///
9+
/// ```swift
10+
/// let modelFile = try await ModelFileGenerator.build(named: "my-part") {
11+
/// Box(x: 10, y: 10, z: 5)
12+
/// }
13+
///
14+
/// let fileName = modelFile.suggestedFileName
15+
/// let fileData = try await modelFile.data()
16+
/// ```
17+
///
18+
/// For command-line apps, see ``Model`` for a pre-built convenient workflow for outputting to
19+
/// the current working directory.
320
public struct ModelFileGenerator {
4-
private let evaluationContext = EvaluationContext()
21+
22+
/// Render a one-shot model to a model file.
23+
///
24+
/// For more details, see the documentation for the ``ModelFileGenerator.build()``
25+
/// instance method.
26+
///
27+
/// - Note: If you intend to render geometries more than once, create an instance of
28+
/// ``ModelFileGenerator`` and re-use ``build()`` on that instance instead.
29+
/// Instances maintain a cache for improved performance over multiple builds.
30+
public static func build(
31+
named name: String? = nil,
32+
options: ModelOptions...,
33+
@ModelContentBuilder content: @Sendable @escaping () -> [BuildDirective]
34+
) async throws -> ModelFile {
35+
return try await ModelFileGenerator().build(named: name, options: options, content: content)
36+
}
537

38+
private let evaluationContext = EvaluationContext()
39+
40+
/// Creates a ``ModelFileGenerator`` instance. Instances maintain a cache, allowing improved
41+
/// performance when performing multiple, subsequent builds.
642
public init() {}
743

44+
/// Renders a model to a standard file format based on the provided geometry.
45+
///
46+
/// Use this function to construct a 3D or 2D model to a file which can then be used in memory
47+
/// or written to disk. The model is generated from a geometry tree you define using the result
48+
/// builder. Supported output formats include 3MF, STL, and SVG, and can be customized via `ModelOptions`.
49+
///
50+
/// The model will be rendered into a ``ModelFile`` object which can be used to access the file's
51+
/// contents, get a suggested file name, and write the file to disk.
52+
///
53+
/// In addition to geometry, the model’s result builder also accepts:
54+
/// - `Metadata(...)`: Attaches metadata (e.g. title, author, license) that is merged into the model’s options.
55+
/// - `Environment { … }` or `Environment(\.keyPath, value)`: Applies environment customizations for this model.
56+
///
57+
/// Precedence and merging rules:
58+
/// - `Environment` directives inside the model’s builder form the base.
59+
/// - `Metadata` inside the model’s builder is merged into the model’s options.
60+
///
61+
/// - Parameters:
62+
/// - name: The base name of the model, which will be used to generate a suggested file name.
63+
/// - options: One or more `ModelOptions` used to customize output format, compression, metadata, etc.
64+
/// - content: A result builder that builds the model geometry, and may also include `Environment` and `Metadata`.
65+
///
66+
/// - Returns: Returns the constructed file in the form of a ``ModelFile`` object.
67+
///
68+
/// ### Examples
69+
///
70+
/// ```swift
71+
/// let fileData: Data = try await ModelFileGenerator.build(named: "simple") {
72+
/// Box(x: 10, y: 10, z: 5)
73+
/// }.data()
74+
/// ```
75+
///
76+
/// ```swift
77+
/// let modelGenerator = ModelFileGenerator()
78+
/// let file: ModelFile = try await modelGenerator.build(named: "complex", options: .format3D(.threeMF)) {
79+
/// // Model-local metadata and environment
80+
/// Metadata(title: "Complex", description: "A more complex example of using ModelFileGenerator")
81+
///
82+
/// Environment {
83+
/// $0.segmentation = .adaptive(minAngle: 10°, minSize: 0.5)
84+
/// }
85+
///
86+
/// Box(x: 100, y: 3, z: 20)
87+
/// .deformed(by: BezierPath2D {
88+
/// curve(controlX: 50, controlY: 50, endX: 100, endY: 0)
89+
/// })
90+
/// }
91+
///
92+
/// let url = try await presentSaveDialog(defaultName: file.suggestedFileName)
93+
/// try await file.write(to: url)
94+
/// ```
895
public func build(
996
named name: String? = nil,
1097
options: ModelOptions...,
1198
@ModelContentBuilder content: @Sendable @escaping () -> [BuildDirective]
1299
) async throws -> ModelFile {
100+
return try await build(named: name, options: options, content: content)
101+
}
102+
103+
internal func build(
104+
named name: String? = nil,
105+
options: [ModelOptions],
106+
@ModelContentBuilder content: @Sendable @escaping () -> [BuildDirective]
107+
) async throws -> ModelFile {
108+
// This is here because in Swift, a varadic parameter can't be passed along as a varadic
109+
// parameter (i.e., the static method can't call the instance method without this).
13110
let directives = content()
14111
let options = ModelOptions(options).adding(modelName: name, directives: directives)
15112
let environment = EnvironmentValues.defaultEnvironment.adding(directives: directives, modelOptions: options)
@@ -20,6 +117,7 @@ public struct ModelFileGenerator {
20117
}
21118
}
22119

120+
/// A representation of a model in the form of a standard file format (3MF, STL, SVG, etc).
23121
public struct ModelFile {
24122
private let dataProvider: OutputDataProvider
25123
private let evaluationContext: EvaluationContext
@@ -32,11 +130,15 @@ public struct ModelFile {
32130
self.modelName = modelName
33131
self.buildWarnings = buildWarnings
34132
}
35-
133+
134+
/// Any warnings generated during the build process.
36135
public let buildWarnings: [BuildWarning]
37-
136+
137+
/// The file's file extension, such as `3mf`, `stl`, etc.
38138
public var fileExtension: String { dataProvider.fileExtension }
39-
139+
140+
/// The file's suggested name, including extension, based on the model name given when built.
141+
/// Illegal file name characters will be removed based on the current platform.
40142
public var suggestedFileName: String {
41143
let invalidCharacters: String
42144
#if os(Windows)
@@ -58,11 +160,13 @@ public struct ModelFile {
58160
if sanitizedFileName.isEmpty { sanitizedFileName = "Model" }
59161
return "\(sanitizedFileName).\(fileExtension)"
60162
}
61-
163+
164+
/// Generates the file's contents as in-memory data.
62165
public func data() async throws -> Data {
63166
try await dataProvider.generateOutput(context: evaluationContext)
64167
}
65-
168+
169+
/// Writes the file's contents to the given location on disk.
66170
public func write(to fileURL: URL) async throws {
67171
try await dataProvider.writeOutput(to: fileURL, context: evaluationContext)
68172
}

0 commit comments

Comments
 (0)