Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<img src="https://github.com/user-attachments/assets/c8ae2128-621a-4d26-8f9a-1c277e525633" width="23%" align="right">

```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
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions Sources/Cadova/Concrete Layer/Build/Group/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
109 changes: 87 additions & 22 deletions Sources/Cadova/Concrete Layer/Build/Model/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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<D: Dimensionality>(
Expand Down
3 changes: 2 additions & 1 deletion Sources/Cadova/Concrete Layer/Build/Project/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down