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. [![Swift](https://github.com/tomasf/Cadova/actions/workflows/main.yml/badge.svg)](https://github.com/tomasf/Cadova/actions/workflows/main.yml) ![Platforms](https://img.shields.io/badge/Platforms-macOS_|_Linux_|_Windows-cc9529?logo=swift&logoColor=white) -## 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) } }