Skip to content

Commit 22edd3a

Browse files
committed
Add support for the 3MF Production extension
Also add ModelLoader.
1 parent 13af455 commit 22edd3a

18 files changed

Lines changed: 607 additions & 79 deletions

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ let package = Package(
1010
],
1111
dependencies: [
1212
.package(url: "https://github.com/tomasf/Zip.git", from: "2.1.0"),
13-
.package(url: "https://github.com/tomasf/Nodal.git", from: "0.3.1")
13+
.package(url: "https://github.com/tomasf/Nodal.git", from: "0.3.2")
1414
],
1515
targets: [
1616
.target(

Sources/ThreeMF/Extensions.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Nodal
23

34
internal extension Dictionary {
45
static func +(lhs: Self, rhs: Self) -> Self {
@@ -15,3 +16,36 @@ internal extension Collection {
1516
isEmpty ? nil : self
1617
}
1718
}
19+
20+
extension Collection where Element: Sendable {
21+
func asyncMap<T: Sendable>(_ transform: @Sendable @escaping (Element) async throws -> T) async rethrows -> [T] {
22+
try await withThrowingTaskGroup(of: (Int, T).self) { group in
23+
for (index, element) in self.enumerated() {
24+
group.addTask {
25+
let value = try await transform(element)
26+
return (index, value)
27+
}
28+
}
29+
30+
var results = Array<T?>(repeating: nil, count: self.count)
31+
for try await (index, result) in group {
32+
results[index] = result
33+
}
34+
35+
return results.map { $0! }
36+
}
37+
}
38+
}
39+
40+
extension URL: XMLValueCodable {
41+
public var xmlStringValue: String {
42+
relativePath
43+
}
44+
45+
public init(xmlStringValue string: String) throws {
46+
guard let url = URL(string: string) else {
47+
throw XMLValueError.invalidFormat(expected: "URI", found: string)
48+
}
49+
self = url
50+
}
51+
}

Sources/ThreeMF/Model/Build.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
import Nodal
3+
4+
public struct Build: Sendable, XMLElementCodable {
5+
public var items: [Item]
6+
public var uuid: UUID?
7+
8+
public init(items: [Item], uuid: UUID? = nil) {
9+
self.items = items
10+
self.uuid = uuid
11+
}
12+
13+
public func encode(to element: Node) {
14+
element.setValue(uuid ?? .uuidIfProduction, forAttribute: Production.UUID)
15+
element.encode(items, elementName: Core.item)
16+
}
17+
18+
public init(from element: Node) throws {
19+
uuid = try element.value(forAttribute: Production.UUID)
20+
items = try element.decode(elementName: Core.item)
21+
}
22+
}

Sources/ThreeMF/Model/Item.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,34 @@ public struct Item: Sendable, XMLElementCodable {
77
public var partNumber: String?
88
public var metadata: [Metadata]
99
public var customAttributes: [ExpandedName: String]
10+
public var path: URL?
11+
public var uuid: UUID?
1012

1113
public init(
1214
objectID: ResourceID,
1315
transform: Matrix3D? = nil,
1416
partNumber: String? = nil,
1517
metadata: [Metadata] = [],
16-
customAttributes: [ExpandedName: String] = [:]
18+
customAttributes: [ExpandedName: String] = [:],
19+
path: URL? = nil,
20+
uuid: UUID? = nil
1721
) {
1822
self.objectID = objectID
1923
self.transform = transform
2024
self.partNumber = partNumber
2125
self.metadata = metadata
2226
self.customAttributes = customAttributes
27+
self.path = path
28+
self.uuid = uuid
2329
}
2430

2531
public func encode(to element: Node) {
2632
element.setValue(objectID, forAttribute: .objectID)
2733
element.setValue(transform, forAttribute: .transform)
2834
element.setValue(partNumber, forAttribute: .partNumber)
35+
element.setValue(uuid ?? .uuidIfProduction, forAttribute: Production.UUID)
36+
element.setValue(path, forAttribute: Production.path)
37+
2938
element.encode(metadata, elementName: Core.metadata, containedIn: Core.metadataGroup)
3039
for (name, value) in customAttributes {
3140
element.setValue(value, forAttribute: name)
@@ -36,9 +45,11 @@ public struct Item: Sendable, XMLElementCodable {
3645
objectID = try element.value(forAttribute: .objectID)
3746
transform = try element.value(forAttribute: .transform)
3847
partNumber = try element.value(forAttribute: .partNumber)
48+
uuid = try element.value(forAttribute: Production.UUID)
49+
path = try element.value(forAttribute: Production.path)
3950
metadata = try element.decode(elementName: Core.metadata, containedIn: Core.metadataGroup)
4051

41-
let knownAttributes: Set<ExpandedName> = [.objectID, .transform, .partNumber, Core.metadataGroup]
52+
let knownAttributes: Set<ExpandedName> = [.objectID, .transform, .partNumber, Core.metadataGroup, Production.UUID, Production.path]
4253
customAttributes = element.namespacedAttributes.filter { !knownAttributes.contains($0.key) }
4354
}
4455
}

Sources/ThreeMF/Model/Model.swift

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import Foundation
22
import Nodal
33

4+
@TaskLocal internal var requiredExtensions: Set<Namespace> = []
5+
6+
internal extension UUID {
7+
static var uuidIfProduction: UUID? {
8+
requiredExtensions.contains(.production) ? UUID() : nil
9+
}
10+
}
11+
412
public struct Model: Sendable, XMLElementCodable {
513
public var unit: Unit?
614
public var xmlLanguageCode: String?
@@ -12,7 +20,7 @@ public struct Model: Sendable, XMLElementCodable {
1220

1321
public var metadata: [Metadata]
1422
public var resources: ResourceContainer
15-
public var buildItems: [Item]
23+
public var build: Build
1624

1725
public init(
1826
unit: Unit? = nil,
@@ -23,7 +31,7 @@ public struct Model: Sendable, XMLElementCodable {
2331
customNamespaces: [String: String] = [:], // Prefix: URI
2432
metadata: [Metadata] = [],
2533
resources: [any Resource] = [],
26-
buildItems: [Item] = []
34+
build: Build
2735
) {
2836
self.unit = unit
2937
self.xmlLanguageCode = xmlLanguageCode
@@ -34,18 +42,35 @@ public struct Model: Sendable, XMLElementCodable {
3442

3543
self.metadata = metadata
3644
self.resources = ResourceContainer(resources: resources)
37-
self.buildItems = buildItems
45+
self.build = build
46+
}
47+
48+
public init(
49+
unit: Unit? = nil,
50+
xmlLanguageCode: String? = nil,
51+
languageCode: String? = nil,
52+
requiredExtensions: Set<Namespace> = [],
53+
recommendedExtensions: Set<Namespace> = [],
54+
customNamespaces: [String: String] = [:], // Prefix: URI
55+
metadata: [Metadata] = [],
56+
resources: [any Resource] = [],
57+
buildItems: [Item] = []
58+
) {
59+
let build = Build(items: buildItems)
60+
self.init(unit: unit, xmlLanguageCode: xmlLanguageCode, languageCode: languageCode, requiredExtensions: requiredExtensions, recommendedExtensions: recommendedExtensions, customNamespaces: customNamespaces, metadata: metadata, resources: resources, build: build)
3861
}
3962

4063
public func encode(to element: Node) {
41-
element.setValue(unit, forAttribute: .unit)
42-
element.setValue(xmlLanguageCode, forAttribute: XML.lang)
43-
element.setValue(languageCode, forAttribute: .language)
44-
element.setValue(requiredExtensions.compactMap(\.outputPrefix).nonEmpty, forAttribute: .requiredExtensions)
45-
element.setValue(recommendedExtensions.compactMap(\.outputPrefix).nonEmpty, forAttribute: .recommendedExtensions)
46-
element.encode(metadata, elementName: Core.metadata)
47-
element.encode(resources, elementName: Core.resources)
48-
element.encode(buildItems, elementName: Core.item, containedIn: Core.build)
64+
$requiredExtensions.withValue(requiredExtensions) {
65+
element.setValue(unit, forAttribute: .unit)
66+
element.setValue(xmlLanguageCode, forAttribute: XML.lang)
67+
element.setValue(languageCode, forAttribute: .language)
68+
element.setValue(requiredExtensions.compactMap(\.outputPrefix).nonEmpty, forAttribute: .requiredExtensions)
69+
element.setValue(recommendedExtensions.compactMap(\.outputPrefix).nonEmpty, forAttribute: .recommendedExtensions)
70+
element.encode(metadata, elementName: Core.metadata)
71+
element.encode(resources, elementName: Core.resources)
72+
element.encode(build, elementName: Core.build)
73+
}
4974
}
5075

5176
public init(from element: Node) throws {
@@ -70,6 +95,6 @@ public struct Model: Sendable, XMLElementCodable {
7095

7196
metadata = try element.decode(elementName: Core.metadata)
7297
resources = try element.decode(elementName: Core.resources)
73-
buildItems = try element.decode(elementName: Core.item, containedIn: Core.build)
98+
build = try element.decode(elementName: Core.build)
7499
}
75100
}

Sources/ThreeMF/Namespace/Attributes.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,7 @@ extension AttributeName where Self == ExpandedName {
8585
static var startIndex: Self { attribute("startindex") }
8686
static var endIndex: Self { attribute("endindex") }
8787
static var identifier: Self { attribute("identifier") }
88+
89+
// MARK: - Alternatives
90+
static var modelResolution: Self { attribute("modelresolution") }
8891
}

Sources/ThreeMF/Namespace/Namespace.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@ public struct Namespace: Hashable, Sendable {
1212
}
1313

1414
internal extension Namespace {
15-
static var known: Set<Self> {
16-
[.core, .triangleSets, .mirroring, .materials, .volumetric, .implicit, .boolean, .displacement, .slice]
17-
}
15+
static var known: Set<Self> {[
16+
.core,
17+
.triangleSets,
18+
.mirroring,
19+
.materials,
20+
.volumetric,
21+
.implicit,
22+
.boolean,
23+
.displacement,
24+
.slice,
25+
.production,
26+
.alternatives
27+
]}
1828

1929
static func knownNamespace(for uri: String) -> Self? {
2030
return Self.known.first(where: { $0.uri == uri })
@@ -50,6 +60,16 @@ public extension Namespace {
5060
outputPrefix: "m"
5161
)
5262

63+
static let production = Self(
64+
uri: "http://schemas.microsoft.com/3dmanufacturing/production/2015/06",
65+
outputPrefix: "p"
66+
)
67+
68+
static let alternatives = Self(
69+
uri: "http://schemas.microsoft.com/3dmanufacturing/production/alternatives/2021/04",
70+
outputPrefix: "pa"
71+
)
72+
5373
static let mirroring = Self(
5474
uri: "http://schemas.microsoft.com/3dmanufacturing/mirroring/2021/07",
5575
outputPrefix: "mm"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import Nodal
3+
4+
internal struct Production: NamespaceSpecification {
5+
static let namespace = Namespace.production
6+
7+
static let path = attributeName("path")
8+
static let UUID = attributeName("UUID")
9+
}
10+
11+
internal struct Alternatives: NamespaceSpecification {
12+
static let namespace = Namespace.alternatives
13+
14+
static let alternatives = elementName("alternatives")
15+
static let alternative = elementName("alternative")
16+
17+
static let modelResolution = attributeName("modelresolution")
18+
}
19+
20+
public enum ModelResolution: String, Sendable, Hashable, XMLValueCodable {
21+
case full = "fullres"
22+
case low = "lowres"
23+
case obfuscated
24+
}

0 commit comments

Comments
 (0)