Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 69 additions & 0 deletions Sources/Cadova/Concrete Layer/ModelGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Foundation

public struct ModelFileGenerator {
private let evaluationContext = EvaluationContext()

public init() {}

public func build(
named name: String? = nil,
options: ModelOptions...,
@ModelContentBuilder content: @Sendable @escaping () -> [BuildDirective]
) async throws -> ModelFile {
let directives = content()
let options = ModelOptions(options).adding(modelName: name, directives: directives)
let environment = EnvironmentValues.defaultEnvironment.adding(directives: directives, modelOptions: options)

let (dataProvider, warnings) = try await directives.build(with: options, in: environment, context: evaluationContext)
return ModelFile(dataProvider: dataProvider, evaluationContext: evaluationContext, modelName: name,
buildWarnings: warnings)
}
}

public struct ModelFile {
private let dataProvider: OutputDataProvider
private let evaluationContext: EvaluationContext
private let modelName: String?

internal init(dataProvider: OutputDataProvider, evaluationContext: EvaluationContext, modelName: String?,
buildWarnings: [BuildWarning]) {
self.dataProvider = dataProvider
self.evaluationContext = evaluationContext
self.modelName = modelName
self.buildWarnings = buildWarnings
}

public let buildWarnings: [BuildWarning]

public var fileExtension: String { dataProvider.fileExtension }

public var suggestedFileName: String {
let invalidCharacters: String
#if os(Windows)
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
invalidCharacters = "<>:\"/\\|?*"
#elseif os(Linux)
invalidCharacters = "/"
#else
// Assume an Apple platform.
// ':' is technically allowed, but for legacy reasons is displayed as '/' in the Finder.
invalidCharacters = ":/"
#endif

var disallowedCharacterSet: CharacterSet = CharacterSet(charactersIn: Unicode.Scalar(0)..<Unicode.Scalar(32))
disallowedCharacterSet.insert(charactersIn: invalidCharacters)

var sanitizedFileName: String = (modelName ?? "Model")
sanitizedFileName.unicodeScalars.removeAll(where: { disallowedCharacterSet.contains($0) })
if sanitizedFileName.isEmpty { sanitizedFileName = "Model" }
return "\(sanitizedFileName).\(fileExtension)"
}

public func data() async throws -> Data {
try await dataProvider.generateOutput(context: evaluationContext)
}

public func write(to fileURL: URL) async throws {
try await dataProvider.writeOutput(to: fileURL, context: evaluationContext)
}
}
45 changes: 45 additions & 0 deletions Tests/Tests/ModelFileGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import Testing
@testable import Cadova

struct ModelFileGeneratorTests {

@Test func `model generator creates valid files`() async throws {

let generator = ModelFileGenerator()
let defaultNameModelFile = try await generator.build(options: .format3D(.threeMF)) {
Box(x: 10, y: 10, z: 5)
}

#expect(defaultNameModelFile.fileExtension == "3mf")
#expect(defaultNameModelFile.suggestedFileName == "Model.3mf")

let namedModelFile = try await generator.build(named: "My Cool Model", options: .format3D(.threeMF)) {
Box(x: 10, y: 10, z: 5)
}

#expect(namedModelFile.fileExtension == "3mf")
#expect(namedModelFile.suggestedFileName == "My Cool Model.3mf")

let illegallyNamedModelFile = try await generator.build(named: "/////", options: .format3D(.threeMF)) {
Box(x: 10, y: 10, z: 5)
}

#expect(illegallyNamedModelFile.fileExtension == "3mf")
#expect(illegallyNamedModelFile.suggestedFileName == "Model.3mf")

let partialIllegallyNamedModelFile = try await generator.build(named: "//My Cool Model//", options: .format3D(.threeMF)) {
Box(x: 10, y: 10, z: 5)
}

#expect(partialIllegallyNamedModelFile.fileExtension == "3mf")
#expect(partialIllegallyNamedModelFile.suggestedFileName == "My Cool Model.3mf")

let results = [defaultNameModelFile, namedModelFile, illegallyNamedModelFile, partialIllegallyNamedModelFile]

for result in results {
let data = try await result.data()
#expect(!data.isEmpty)
}
}
}