@@ -232,6 +232,8 @@ struct TeethAndTimeView: View {
232232 @EnvironmentObject private var model: LiteFlowViewModel
233233 @State private var stage = 0.0
234234 @State private var showDemoSample = false
235+ @State private var previewArch : DentalPreviewArch = . both
236+ @State private var zoom = 1.0
235237
236238 private var hasSelectedStl : Bool {
237239 model. scans. contains { $0. modality. lowercased ( ) == " stl " }
@@ -244,10 +246,30 @@ struct TeethAndTimeView: View {
244246 DentalScenePreview (
245247 stage: stage,
246248 scans: model. previewScans,
249+ previewArch: previewArch,
250+ zoom: zoom,
247251 hasSelectedStl: hasSelectedStl
248252 )
249253 . frame ( height: 340 )
250254 . clipShape ( RoundedRectangle ( cornerRadius: 12 ) )
255+ if hasSelectedStl {
256+ Picker ( " Arch " , selection: $previewArch) {
257+ ForEach ( DentalPreviewArch . allCases, id: \. self) { arch in
258+ Text ( arch. title) . tag ( arch)
259+ }
260+ }
261+ . pickerStyle ( . segmented)
262+ HStack ( spacing: 12 ) {
263+ Text ( " Zoom " )
264+ . font ( . caption)
265+ . foregroundStyle ( . secondary)
266+ Slider ( value: $zoom, in: 0.75 ... 2.4 , step: 0.05 )
267+ Text ( String ( format: " %.1fx " , zoom) )
268+ . font ( . caption. monospacedDigit ( ) )
269+ . foregroundStyle ( . secondary)
270+ . frame ( width: 42 , alignment: . trailing)
271+ }
272+ }
251273 if !hasSelectedStl {
252274 VStack ( spacing: 8 ) {
253275 Button {
@@ -482,12 +504,14 @@ struct PrintAndSendView: View {
482504struct DentalScenePreview: View {
483505 var stage : Double
484506 var scans : [ PreviewScan ]
507+ var previewArch : DentalPreviewArch = . both
508+ var zoom : Double = 1
485509 var hasSelectedStl : Bool = false
486510
487511 var body : some View {
488512 ZStack ( alignment: . bottomLeading) {
489513 SceneView (
490- scene: DentalPreviewScene . make ( stage: stage, scans: scans) ,
514+ scene: DentalPreviewScene . make ( stage: stage, scans: scans, previewArch : previewArch , zoom : zoom ) ,
491515 options: [ . allowsCameraControl, . autoenablesDefaultLighting]
492516 )
493517 Text ( previewCaption)
@@ -513,30 +537,63 @@ struct DentalScenePreview: View {
513537 }
514538}
515539
540+ enum DentalPreviewArch: CaseIterable {
541+ case both
542+ case upper
543+ case lower
544+
545+ var title : String {
546+ switch self {
547+ case . both: return " Both "
548+ case . upper: return " Upper "
549+ case . lower: return " Lower "
550+ }
551+ }
552+
553+ func includes( _ scan: PreviewScan ) -> Bool {
554+ guard scan. modality. lowercased ( ) == " stl " else { return false }
555+ let name = scan. fileName. lowercased ( )
556+ switch self {
557+ case . both:
558+ return true
559+ case . upper:
560+ return name. contains ( " upper " ) || name. contains ( " maxillary " )
561+ case . lower:
562+ return name. contains ( " lower " ) || name. contains ( " mandibular " )
563+ }
564+ }
565+ }
566+
516567private enum DentalPreviewScene {
517- static func make( stage: Double, scans: [ PreviewScan] ) - > SCNScene {
568+ static func make( stage: Double, scans: [ PreviewScan] , previewArch : DentalPreviewArch , zoom : Double ) - > SCNScene {
518569 let scene = SCNScene ( )
519570 scene. background. contents = UIColor . systemBackground
520571
521572 let camera = SCNCamera ( )
522- camera. fieldOfView = 45
573+ camera. fieldOfView = 36
523574 let cameraNode = SCNNode ( )
524575 cameraNode. camera = camera
525- cameraNode. position = SCNVector3 ( 0 , 0 , 9 )
576+ cameraNode. position = SCNVector3 ( 0 , 0 , Float ( 7.2 / Swift . max ( 0.75 , zoom ) ) )
526577 scene. rootNode. addChildNode ( cameraNode)
527578
579+ let ambient = SCNLight ( )
580+ ambient. type = . ambient
581+ ambient. intensity = 560
582+ let ambientNode = SCNNode ( )
583+ ambientNode. light = ambient
584+ scene. rootNode. addChildNode ( ambientNode)
585+
528586 let light = SCNLight ( )
529- light. type = . omni
530- light. intensity = 900
587+ light. type = . directional
588+ light. intensity = 1_350
531589 let lightNode = SCNNode ( )
532590 lightNode. light = light
533- lightNode. position = SCNVector3 ( 0 , 3 , 6 )
591+ lightNode. eulerAngles = SCNVector3 ( - 0.6 , 0.25 , 0 )
592+ lightNode. position = SCNVector3 ( 0 , 4 , 6 )
534593 scene. rootNode. addChildNode ( lightNode)
535594
536- let stlNodes = scans
537- . filter { $0. modality. lowercased ( ) == " stl " }
538- . prefix ( 2 )
539- . compactMap ( STLSceneMesh . node)
595+ let selectedScans = selectedScans ( from: scans, previewArch: previewArch)
596+ let stlNodes = selectedScans. compactMap ( STLSceneMesh . node)
540597 if stlNodes. isEmpty {
541598 addSampleDentalCast ( to: scene, stage: stage)
542599 } else {
@@ -551,18 +608,66 @@ private enum DentalPreviewScene {
551608 return scene
552609 }
553610
611+ private static func selectedScans( from scans: [ PreviewScan ] , previewArch: DentalPreviewArch ) -> [ PreviewScan ] {
612+ let stlScans = Array ( scans. filter { $0. modality. lowercased ( ) == " stl " } . prefix ( 2 ) )
613+ guard previewArch != . both else { return stlScans }
614+
615+ let namedMatches = stlScans. filter ( previewArch. includes)
616+ if !namedMatches. isEmpty {
617+ return namedMatches
618+ }
619+ guard stlScans. count > 1 else { return stlScans }
620+ return previewArch == . upper ? [ stlScans [ 0 ] ] : [ stlScans [ 1 ] ]
621+ }
622+
554623 private static func centerAndScale( _ node: SCNNode ) {
555- let bounds = node. boundingBox
624+ guard let bounds = recursiveBounds ( for : node) else { return }
556625 let min = bounds. min
557626 let maxPoint = bounds. max
558627 let center = SCNVector3 ( ( min. x + maxPoint. x) / 2 , ( min. y + maxPoint. y) / 2 , ( min. z + maxPoint. z) / 2 )
559628 let span = Swift . max ( maxPoint. x - min. x, Swift . max ( maxPoint. y - min. y, maxPoint. z - min. z) )
560- let scale = span > 0 ? 4.8 / span : 1
561- node. position = SCNVector3 ( - center. x * scale, - center. y * scale, - center. z * scale)
629+ let scale = span > 0 ? 6.2 / span : 1
630+ for child in node. childNodes {
631+ child. position = SCNVector3 (
632+ child. position. x - center. x,
633+ child. position. y - center. y,
634+ child. position. z - center. z
635+ )
636+ }
637+ node. position = SCNVector3Zero
562638 node. scale = SCNVector3 ( scale, scale, scale)
563639 node. eulerAngles. x = - . pi / 2
564640 }
565641
642+ private static func recursiveBounds( for node: SCNNode ) -> ( min: SCNVector3 , max: SCNVector3 ) ? {
643+ var result : ( min: SCNVector3 , max: SCNVector3 ) ?
644+ for child in node. childNodes {
645+ let bounds = child. boundingBox
646+ let corners = [
647+ SCNVector3 ( bounds. min. x, bounds. min. y, bounds. min. z) ,
648+ SCNVector3 ( bounds. min. x, bounds. min. y, bounds. max. z) ,
649+ SCNVector3 ( bounds. min. x, bounds. max. y, bounds. min. z) ,
650+ SCNVector3 ( bounds. min. x, bounds. max. y, bounds. max. z) ,
651+ SCNVector3 ( bounds. max. x, bounds. min. y, bounds. min. z) ,
652+ SCNVector3 ( bounds. max. x, bounds. min. y, bounds. max. z) ,
653+ SCNVector3 ( bounds. max. x, bounds. max. y, bounds. min. z) ,
654+ SCNVector3 ( bounds. max. x, bounds. max. y, bounds. max. z) ,
655+ ]
656+ for corner in corners {
657+ let converted = child. convertPosition ( corner, to: node)
658+ if let current = result {
659+ result = (
660+ min: SCNVector3 ( Swift . min ( current. min. x, converted. x) , Swift . min ( current. min. y, converted. y) , Swift . min ( current. min. z, converted. z) ) ,
661+ max: SCNVector3 ( Swift . max ( current. max. x, converted. x) , Swift . max ( current. max. y, converted. y) , Swift . max ( current. max. z, converted. z) )
662+ )
663+ } else {
664+ result = ( converted, converted)
665+ }
666+ }
667+ }
668+ return result
669+ }
670+
566671 private static func addSampleDentalCast( to scene: SCNScene , stage: Double ) {
567672 let parent = SCNNode ( )
568673 parent. eulerAngles. x = - 0.08
@@ -643,7 +748,7 @@ private enum DentalPreviewScene {
643748}
644749
645750private enum STLSceneMesh {
646- private static let trianglePreviewLimit = 80_000
751+ private static let trianglePreviewLimit = 120_000
647752 private static var geometryCache : [ String : SCNGeometry ] = [ : ]
648753 private static let cacheLock = NSLock ( )
649754
@@ -666,9 +771,15 @@ private enum STLSceneMesh {
666771 bytesPerIndex: MemoryLayout< Int32> . size
667772 )
668773 let geometry = SCNGeometry ( sources: [ source] , elements: [ element] )
669- geometry. firstMaterial? . diffuse. contents = UIColor ( red: 0.94 , green: 0.88 , blue: 0.74 , alpha: 1.0 )
670- geometry. firstMaterial? . specular. contents = UIColor . white
671- geometry. firstMaterial? . isDoubleSided = true
774+ let material = SCNMaterial ( )
775+ material. diffuse. contents = UIColor ( red: 0.99 , green: 0.96 , blue: 0.82 , alpha: 1.0 )
776+ material. ambient. contents = UIColor ( red: 0.62 , green: 0.56 , blue: 0.36 , alpha: 1.0 )
777+ material. specular. contents = UIColor ( white: 0.9 , alpha: 1.0 )
778+ material. emission. contents = UIColor ( red: 0.06 , green: 0.05 , blue: 0.02 , alpha: 1.0 )
779+ material. shininess = 0.45
780+ material. lightingModel = . blinn
781+ material. isDoubleSided = true
782+ geometry. materials = [ material]
672783 storeGeometry ( geometry, for: cacheKey)
673784 return SCNNode ( geometry: geometry)
674785 }
0 commit comments