-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTriplanarMPRViewer.swift
More file actions
171 lines (154 loc) · 5.91 KB
/
Copy pathTriplanarMPRViewer.swift
File metadata and controls
171 lines (154 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//
// TriplanarMPRViewer.swift
// MTK Examples
//
// MPR-only example using public VolumeViewport contracts.
//
import SwiftUI
import MTKCore
import MTKUI
/// Example purpose: triplanar MPR setup through the public MPR viewport contract.
///
/// ADR concepts demonstrated:
/// `VolumeDataset -> VolumeViewport -> MetalViewportView` for axial, coronal,
/// and sagittal review. Use `ClinicalViewportSession` when the UI needs the
/// reference shared 2x2 clinical layout.
///
/// Interactive MPR presentation remains Metal-native as `MTLTexture` until
/// `PresentationPass` presents into `MTKView`. This example does not use
/// SceneKit or `CGImage` for display.
struct TriplanarMPRViewerExample: View {
@State private var axialViewport: VolumeViewport?
@State private var coronalViewport: VolumeViewport?
@State private var sagittalViewport: VolumeViewport?
@State private var didConfigureExample = false
@State private var errorMessage: String?
var body: some View {
Group {
if let axialViewport, let coronalViewport, let sagittalViewport {
TriplanarMPRContent(
axialViewport: axialViewport,
coronalViewport: coronalViewport,
sagittalViewport: sagittalViewport
)
} else if let errorMessage {
ContentUnavailableView(
"Failed to Prepare Triplanar Viewer",
systemImage: "exclamationmark.triangle",
description: Text(errorMessage)
)
} else {
ProgressView("Preparing triplanar MPR viewports...")
}
}
.task {
await configureExampleIfNeeded()
}
}
@MainActor
private func configureExampleIfNeeded() async {
guard !didConfigureExample else { return }
didConfigureExample = true
errorMessage = nil
do {
let dataset = makeSampleDataset()
let axial = try VolumeViewport(axis: .axial, normalizedSlicePosition: 0.35)
let coronal = try VolumeViewport(axis: .coronal, normalizedSlicePosition: 0.50)
let sagittal = try VolumeViewport(axis: .sagittal, normalizedSlicePosition: 0.65)
for viewport in [axial, coronal, sagittal] {
await viewport.applyDataset(dataset)
await viewport.setWindowLevel(window: 400, level: 40)
}
axialViewport = axial
coronalViewport = coronal
sagittalViewport = sagittal
} catch {
axialViewport = nil
coronalViewport = nil
sagittalViewport = nil
didConfigureExample = false
errorMessage = error.localizedDescription
}
}
private func makeSampleDataset() -> VolumeDataset {
let width = 384
let height = 384
let depth = 220
let voxelCount = width * height * depth
let bytesPerVoxel = VolumePixelFormat.int16Signed.bytesPerVoxel
let voxels = Data(repeating: 0, count: voxelCount * bytesPerVoxel)
return VolumeDataset(
data: voxels,
dimensions: VolumeDimensions(width: width, height: height, depth: depth),
spacing: VolumeSpacing(x: 0.00075, y: 0.00075, z: 0.0010),
pixelFormat: .int16Signed,
intensityRange: (-1024)...3071,
recommendedWindow: -160...240
)
}
}
@MainActor
private struct TriplanarMPRContent: View {
@ObservedObject var axialViewport: VolumeViewport
@ObservedObject var coronalViewport: VolumeViewport
@ObservedObject var sagittalViewport: VolumeViewport
var body: some View {
VStack(spacing: 16) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
TriplanarMPRPane(title: "Axial", viewport: axialViewport)
TriplanarMPRPane(title: "Coronal", viewport: coronalViewport)
TriplanarMPRPane(title: "Sagittal", viewport: sagittalViewport)
}
.padding(.horizontal)
sliceControls
}
}
private var sliceControls: some View {
VStack(spacing: 12) {
slider(title: "Axial Slice", viewport: axialViewport)
slider(title: "Coronal Slice", viewport: coronalViewport)
slider(title: "Sagittal Slice", viewport: sagittalViewport)
}
.padding(.horizontal)
}
private func slider(title: String, viewport: VolumeViewport) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.caption.weight(.semibold))
Slider(
value: Binding(
get: { Double(viewport.normalizedSlicePosition) },
set: { newValue in
Task { await viewport.setSlicePosition(Float(newValue)) }
}
),
in: 0...1
)
}
}
}
@MainActor
private struct TriplanarMPRPane: View {
let title: String
@ObservedObject var viewport: VolumeViewport
var body: some View {
ZStack(alignment: .topLeading) {
MetalViewportView(surface: viewport.surface)
Text(title)
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.black.opacity(0.7), in: RoundedRectangle(cornerRadius: 8))
.foregroundStyle(.white)
.padding(8)
}
.aspectRatio(1, contentMode: .fit)
.background(Color.black.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
/*
This example is intentionally MPR-only. It does not create a 3D pane, does not
use SceneKit, and does not instantiate render graph or engine internals.
`CGImage` remains export-only and is not part of the interactive display path.
*/