diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index cebe2280..3549b96d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,8 +16,6 @@ jobs:
steps:
- uses: SwiftyLab/setup-swift@latest
- with:
- visual-studio-components: Microsoft.VisualStudio.Component.Windows11SDK.22621
- uses: actions/checkout@v4
- name: Install Linux dependencies
diff --git a/README.md b/README.md
index 1c4c2c6e..d2b3ad51 100644
--- a/README.md
+++ b/README.md
@@ -7,17 +7,40 @@ Cadova models are written entirely in Swift, making them easy to version, reuse,
Cadova runs on macOS, Windows, and Linux. To get started, read the [Getting Started guide](https://github.com/tomasf/Cadova/wiki/Getting-Started).
-More documentation is available in the [Wiki](https://github.com/tomasf/Cadova/wiki). Read [What is Cadova?](https://github.com/tomasf/Cadova/wiki/What-is-Cadova%3F) for an introduction. For code examples, see [Examples](https://github.com/tomasf/Cadova/wiki/Examples).
+More documentation is available in the [Wiki](https://github.com/tomasf/Cadova/wiki). Read [What is Cadova?](https://github.com/tomasf/Cadova/wiki/What-is-Cadova%3F) for an introduction.
[](https://github.com/tomasf/Cadova/actions/workflows/main.yml)

-## Related Projects
+## Example
+
+
+```swift
+await Model("Hex key holder") {
+ let height = 20.0
+ let spacing = 8.0
+ Stack(.x, spacing: spacing) {
+ for size in stride(from: 1.5, through: 5.0, by: 0.5) {
+ RegularPolygon(sideCount: 6, widthAcrossFlats: size)
+ }
+ }.measuringBounds { holes, bounds in
+ Stadium(bounds.size + spacing * 2)
+ .extruded(height: height)
+ .subtracting {
+ holes.aligned(at: .centerX)
+ .extruded(height: height)
+ .translated(z: 2)
+ }
+ }
+}
+```
+For more code examples, see [Examples](https://github.com/tomasf/Cadova/wiki/Examples).
+
+# Related Projects
* [Cadova Viewer](https://github.com/tomasf/CadovaViewer) - A native macOS 3MF viewer application
* [Helical](https://github.com/tomasf/Helical) - A Cadova library providing customizable threads, screws, bolts, nuts and related parts.
-Cadova uses [Manifold-Swift](https://github.com/tomasf/manifold-swift), [ThreeMF](https://github.com/tomasf/ThreeMF),
-[freetype-spm](https://github.com/tomasf/freetype-spm) and [FindFont](https://github.com/tomasf/FindFont).
+Cadova uses [Manifold-Swift](https://github.com/tomasf/manifold-swift), [Apus](https://github.com/tomasf/Apus) and [ThreeMF](https://github.com/tomasf/ThreeMF).
## Versioning and Stability
@@ -39,7 +62,7 @@ let package = Package(
name: "<#name#>",
platforms: [.macOS(.v14)],
dependencies: [
- .package(url: "https://github.com/tomasf/Cadova.git", .upToNextMinor(from: "0.2.0")),
+ .package(url: "https://github.com/tomasf/Cadova.git", .upToNextMinor(from: "0.4.1")),
],
targets: [
.executableTarget(
diff --git a/Sources/Cadova/Concrete Layer/Build/Group/Group.swift b/Sources/Cadova/Concrete Layer/Build/Group/Group.swift
index b414f3a8..fb80a2c5 100644
--- a/Sources/Cadova/Concrete Layer/Build/Group/Group.swift
+++ b/Sources/Cadova/Concrete Layer/Build/Group/Group.swift
@@ -121,11 +121,11 @@ public struct Group: Sendable {
var urls: [URL] = []
for model in models {
- if let url = await model.build(
+ if let url = await model.writeToDirectory(
+ outputDirectory,
environment: environment,
context: context,
- options: combinedOptions,
- URL: outputDirectory
+ inheritedOptions: combinedOptions
) {
urls.append(url)
}
diff --git a/Sources/Cadova/Concrete Layer/Build/Model/Model.swift b/Sources/Cadova/Concrete Layer/Build/Model/Model.swift
index c6d2a1ef..12ccf281 100644
--- a/Sources/Cadova/Concrete Layer/Build/Model/Model.swift
+++ b/Sources/Cadova/Concrete Layer/Build/Model/Model.swift
@@ -17,6 +17,11 @@ import Foundation
public struct Model: Sendable {
let name: String
+ public struct InMemoryFile: Sendable {
+ let suggestedFileName: String
+ let contents: Data
+ }
+
private let directives: @Sendable () -> [BuildDirective]
private let options: ModelOptions
@@ -67,30 +72,101 @@ public struct Model: Sendable {
/// }
/// ```
///
- @discardableResult
public init(
_ name: String,
options: ModelOptions...,
+ automaticallyWriteToDisk: Bool = true,
@ModelContentBuilder content: @Sendable @escaping () -> [BuildDirective]
) async {
self.name = name
directives = content
self.options = .init(options)
- if ModelContext.current.isCollectingModels == false {
- if let url = await build() {
- try? Platform.revealFiles([url])
+ if automaticallyWriteToDisk && ModelContext.current.isCollectingModels == false {
+ let _ = await writeToDirectory(revealInSystemFileBrowser: true)
+ }
+ }
+
+ public func writeToFile(
+ _ fileUrl: URL,
+ environment inheritedEnvironment: EnvironmentValues = .defaultEnvironment,
+ context: EvaluationContext? = nil,
+ inheritedOptions: ModelOptions? = nil,
+ revealInSystemFileBrowser: Bool = false
+ ) async throws {
+ guard let file = await buildToData(environment: inheritedEnvironment, context: context ?? .init(),
+ options: inheritedOptions) else { throw CocoaError(.fileWriteUnknown) }
+ do {
+ try file.contents.write(to: fileUrl)
+ logger.info("Wrote model to \(fileUrl.path)")
+ if revealInSystemFileBrowser {
+ try? Platform.revealFiles([fileUrl])
}
+ } catch {
+ logger.error("Failed to save model file to \(fileUrl.path): \(error.descriptiveString)")
+ throw error
+ }
+ }
+
+ public func writeToDirectory(
+ _ directoryUrl: URL? = nil,
+ environment inheritedEnvironment: EnvironmentValues = .defaultEnvironment,
+ context: EvaluationContext? = nil,
+ inheritedOptions: ModelOptions? = nil,
+ revealInSystemFileBrowser: Bool = false
+ ) async -> URL? {
+ let result = await buildToFile(environment: inheritedEnvironment, context: context ?? .init(),
+ options: inheritedOptions, URL: directoryUrl)
+ if revealInSystemFileBrowser, let result {
+ try? Platform.revealFiles([result])
}
+
+ return result
}
- internal func build(
+ public func generateData(
environment inheritedEnvironment: EnvironmentValues = .defaultEnvironment,
- context: EvaluationContext = .init(),
- options inheritedOptions: ModelOptions? = nil,
+ context: EvaluationContext? = nil,
+ inheritedOptions: ModelOptions? = nil
+ ) async -> InMemoryFile? {
+ return await buildToData(environment: inheritedEnvironment, context: context ?? .init(), options: inheritedOptions)
+ }
+
+ private func buildToFile(
+ environment inheritedEnvironment: EnvironmentValues,
+ context: EvaluationContext,
+ options inheritedOptions: ModelOptions?,
URL directory: URL? = nil
) async -> URL? {
logger.info("Generating \"\(name)\"...")
+
+ guard let file = await buildToData(environment: inheritedEnvironment,
+ context: context, options: inheritedOptions) else { return nil }
+
+ let url: URL
+ if let parent = directory {
+ url = parent.appendingPathComponent(file.suggestedFileName, isDirectory: false)
+ } else {
+ url = URL(expandingFilePath: file.suggestedFileName)
+ }
+
+ let fileExisted = FileManager().fileExists(atPath: url.path(percentEncoded: false))
+
+ do {
+ try file.contents.write(to: url)
+ logger.info("Wrote model to \(url.path)")
+ } catch {
+ logger.error("Failed to save model file to \(url.path): \(error.descriptiveString)")
+ }
+
+ return fileExisted ? nil : url
+ }
+
+ private func buildToData(
+ environment inheritedEnvironment: EnvironmentValues,
+ context: EvaluationContext,
+ options inheritedOptions: ModelOptions?
+ ) async -> InMemoryFile? {
let directives = inheritedEnvironment.whileCurrent {
self.directives()
@@ -109,13 +185,6 @@ public struct Model: Sendable {
mutatingEnvironment.modelOptions = localOptions
let environment = mutatingEnvironment
- let baseURL: URL
- if let parent = directory {
- baseURL = parent.appendingPathComponent(name, isDirectory: false)
- } else {
- baseURL = URL(expandingFilePath: name)
- }
-
let geometries3D = directives.compactMap(\.geometry3D)
let geometries2D = directives.compactMap(\.geometry2D)
let provider: OutputDataProvider
@@ -146,17 +215,13 @@ public struct Model: Sendable {
return nil
}
- let url = baseURL.appendingPathExtension(provider.fileExtension)
- let fileExisted = FileManager().fileExists(atPath: url.path(percentEncoded: false))
-
do {
- try await provider.writeOutput(to: url, context: context)
- logger.info("Wrote model to \(url.path)")
+ let fileContents: Data = try await provider.generateOutput(context: context)
+ return InMemoryFile(suggestedFileName: "\(name).\(provider.fileExtension)", contents: fileContents)
} catch {
- logger.error("Failed to save model file to \(url.path): \(error.descriptiveString)")
+ logger.error("Cadova caught an error while generating \(provider.fileExtension) model \"\(name)\":\nš \(error)\n")
+ return nil
}
-
- return fileExisted ? nil : url
}
private func generateResult(
diff --git a/Sources/Cadova/Concrete Layer/Build/Project/Project.swift b/Sources/Cadova/Concrete Layer/Build/Project/Project.swift
index e6f4d193..908b43ab 100644
--- a/Sources/Cadova/Concrete Layer/Build/Project/Project.swift
+++ b/Sources/Cadova/Concrete Layer/Build/Project/Project.swift
@@ -92,7 +92,8 @@ public func Project(
var urls: [URL] = []
for model in models {
- if let modelUrl = await model.build(environment: constantEnvironment, context: context, options: combinedOptions, URL: url) {
+ if let modelUrl = await model.writeToDirectory(url, environment: constantEnvironment, context: context,
+ inheritedOptions: combinedOptions, revealInSystemFileBrowser: false) {
urls.append(modelUrl)
}
}