SceneKit+Snapshot.m 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. //
  2. // SceneKit+Snapshot.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 1/8/20.
  6. //
  7. #import "SceneKit+Snapshot.h"
  8. #import "FHSSnapshotNodes.h"
  9. /// This value is chosen such that this offset can be applied to avoid
  10. /// z-fighting amongst nodes at the same z-position, but small enough
  11. /// that they appear to visually be on the same plane.
  12. CGFloat const kFHSSmallZOffset = 0.05;
  13. CGFloat const kHeaderVerticalInset = 8.0;
  14. #pragma mark SCNGeometry
  15. @interface SCNGeometry (SnapshotPrivate)
  16. @end
  17. @implementation SCNGeometry (SnapshotPrivate)
  18. - (void)addDoubleSidedMaterialWithDiffuseContents:(id)contents {
  19. SCNMaterial *material = [SCNMaterial new];
  20. material.doubleSided = YES;
  21. material.diffuse.contents = contents;
  22. [self insertMaterial:material atIndex:0];
  23. }
  24. @end
  25. #pragma mark SCNNode
  26. @implementation SCNNode (Snapshot)
  27. - (SCNNode *)nearestAncestorSnapshot {
  28. SCNNode *node = self;
  29. while (!node.name && node) {
  30. node = node.parentNode;
  31. }
  32. return node;
  33. }
  34. + (instancetype)shapeNodeWithSize:(CGSize)size materialDiffuse:(id)contents offsetZ:(BOOL)offsetZ {
  35. UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(
  36. 0, 0, size.width, size.height
  37. )];
  38. SCNShape *shape = [SCNShape shapeWithPath:path materialDiffuse:contents];
  39. SCNNode *node = [SCNNode nodeWithGeometry:shape];
  40. if (offsetZ) {
  41. node.position = SCNVector3Make(0, 0, kFHSSmallZOffset);
  42. }
  43. return node;
  44. }
  45. + (instancetype)highlight:(FHSViewSnapshot *)view color:(UIColor *)color {
  46. return [self shapeNodeWithSize:view.frame.size materialDiffuse:color offsetZ:YES];
  47. }
  48. + (instancetype)snapshot:(FHSViewSnapshot *)view {
  49. id image = view.snapshotImage;
  50. return [self shapeNodeWithSize:view.frame.size materialDiffuse:image offsetZ:NO];
  51. }
  52. + (instancetype)lineFrom:(SCNVector3)v1 to:(SCNVector3)v2 color:(UIColor *)lineColor {
  53. SCNVector3 vertices[2] = { v1, v2 };
  54. int32_t _indices[2] = { 0, 1 };
  55. NSData *indices = [NSData dataWithBytes:_indices length:sizeof(_indices)];
  56. SCNGeometrySource *source = [SCNGeometrySource geometrySourceWithVertices:vertices count:2];
  57. SCNGeometryElement *element = [SCNGeometryElement
  58. geometryElementWithData:indices
  59. primitiveType:SCNGeometryPrimitiveTypeLine
  60. primitiveCount:2
  61. bytesPerIndex:sizeof(int32_t)
  62. ];
  63. SCNGeometry *geometry = [SCNGeometry geometryWithSources:@[source] elements:@[element]];
  64. [geometry addDoubleSidedMaterialWithDiffuseContents:lineColor];
  65. return [SCNNode nodeWithGeometry:geometry];
  66. }
  67. - (instancetype)borderWithColor:(UIColor *)color {
  68. struct { SCNVector3 min, max; } bb;
  69. [self getBoundingBoxMin:&bb.min max:&bb.max];
  70. SCNVector3 topLeft = SCNVector3Make(bb.min.x, bb.max.y, kFHSSmallZOffset);
  71. SCNVector3 bottomLeft = SCNVector3Make(bb.min.x, bb.min.y, kFHSSmallZOffset);
  72. SCNVector3 topRight = SCNVector3Make(bb.max.x, bb.max.y, kFHSSmallZOffset);
  73. SCNVector3 bottomRight = SCNVector3Make(bb.max.x, bb.min.y, kFHSSmallZOffset);
  74. SCNNode *top = [SCNNode lineFrom:topLeft to:topRight color:color];
  75. SCNNode *left = [SCNNode lineFrom:bottomLeft to:topLeft color:color];
  76. SCNNode *bottom = [SCNNode lineFrom:bottomLeft to:bottomRight color:color];
  77. SCNNode *right = [SCNNode lineFrom:bottomRight to:topRight color:color];
  78. SCNNode *border = [SCNNode new];
  79. [border addChildNode:top];
  80. [border addChildNode:left];
  81. [border addChildNode:bottom];
  82. [border addChildNode:right];
  83. return border;
  84. }
  85. + (instancetype)header:(FHSViewSnapshot *)view {
  86. SCNText *text = [SCNText labelGeometry:view.title font:[UIFont boldSystemFontOfSize:13.0]];
  87. SCNNode *textNode = [SCNNode nodeWithGeometry:text];
  88. struct { SCNVector3 min, max; } bb;
  89. [textNode getBoundingBoxMin:&bb.min max:&bb.max];
  90. CGFloat textWidth = bb.max.x - bb.min.x;
  91. CGFloat textHeight = bb.max.y - bb.min.y;
  92. CGFloat snapshotWidth = view.frame.size.width;
  93. CGFloat headerWidth = MAX(snapshotWidth, textWidth);
  94. CGRect frame = CGRectMake(0, 0, headerWidth, textHeight + (kHeaderVerticalInset * 2));
  95. SCNNode *headerNode = [SCNNode nodeWithGeometry:[SCNShape
  96. nameHeader:view.headerColor frame:frame corners:8
  97. ]];
  98. [headerNode addChildNode:textNode];
  99. textNode.position = SCNVector3Make(
  100. (frame.size.width / 2.f) - (textWidth / 2.f),
  101. (frame.size.height / 2.f) - (textHeight / 2.f),
  102. kFHSSmallZOffset
  103. );
  104. headerNode.position = SCNVector3Make(
  105. (snapshotWidth / 2.f) - (headerWidth / 2.f),
  106. view.frame.size.height,
  107. kFHSSmallZOffset
  108. );
  109. return headerNode;
  110. }
  111. + (instancetype)snapshot:(FHSViewSnapshot *)view
  112. parent:(FHSViewSnapshot *)parent
  113. parentNode:(SCNNode *)parentNode
  114. root:(SCNNode *)rootNode
  115. depth:(NSInteger *)depthOut
  116. nodesMap:(NSMutableDictionary<NSString *, FHSSnapshotNodes *> *)nodesMap
  117. hideHeaders:(BOOL)hideHeaders {
  118. NSInteger const depth = *depthOut;
  119. // Ignore elements that are not visible.
  120. // These should appear in the list, but not in the 3D view.
  121. if (view.hidden || CGSizeEqualToSize(view.frame.size, CGSizeZero)) {
  122. return nil;
  123. }
  124. // Create a node whose contents are the snapshot of the element
  125. SCNNode *node = [self snapshot:view];
  126. node.name = view.view.identifier;
  127. // Begin building node tree
  128. FHSSnapshotNodes *nodes = [FHSSnapshotNodes snapshot:view depth:depth];
  129. nodes.snapshot = node;
  130. // The node must be added to the root node
  131. // for the coordinate space calculations below to work
  132. [rootNode addChildNode:node];
  133. node.position = ({
  134. // Flip the y-coordinate since SceneKit has a
  135. // flipped version of the UIKit coordinate system
  136. CGRect pframe = parent ? parent.frame : CGRectZero;
  137. CGFloat y = parent ? pframe.size.height - CGRectGetMaxY(view.frame) : 0;
  138. // To simplify calculating the z-axis spacing between the layers, we make
  139. // each snapshot node a direct child of the root rather than embedding
  140. // the nodes in their parent nodes in the same structure as the UI elements
  141. // themselves. With this flattened hierarchy, the z-position can be
  142. // calculated for every node simply by multiplying the spacing by the depth.
  143. //
  144. // `parentSnapshotNode` as referenced here is NOT the actual parent node
  145. // of `node`, it is the node corresponding to the parent of the UI element.
  146. // It is used to convert from frame coordinates, which are relative to
  147. // the bounds of the parent, to coordinates relative to the root node.
  148. SCNVector3 positionRelativeToParent = SCNVector3Make(view.frame.origin.x, y, 0);
  149. SCNVector3 positionRelativeToRoot;
  150. if (parent) {
  151. positionRelativeToRoot = [rootNode convertPosition:positionRelativeToParent fromNode:parentNode];
  152. } else {
  153. positionRelativeToRoot = positionRelativeToParent;
  154. }
  155. positionRelativeToRoot.z = 50 * depth;
  156. positionRelativeToRoot;
  157. });
  158. // Make border node
  159. nodes.border = [node borderWithColor:view.headerColor];
  160. [node addChildNode:nodes.border];
  161. // Make header node
  162. nodes.header = [SCNNode header:view];
  163. [node addChildNode:nodes.header];
  164. if (hideHeaders) {
  165. nodes.header.hidden = YES;
  166. }
  167. nodesMap[view.view.identifier] = nodes;
  168. NSMutableArray<FHSViewSnapshot *> *checkForIntersect = [NSMutableArray new];
  169. NSInteger maxChildDepth = depth;
  170. // Recurse to child nodes; overlapping children have higher depths
  171. for (FHSViewSnapshot *child in view.children) {
  172. NSInteger childDepth = depth + 1;
  173. // Children that intersect a sibling are rendered
  174. // in a separate layer above the previous siblings
  175. for (FHSViewSnapshot *sibling in checkForIntersect) {
  176. if (CGRectIntersectsRect(sibling.frame, child.frame)) {
  177. childDepth = maxChildDepth + 1;
  178. break;
  179. }
  180. }
  181. id didMakeNode = [SCNNode
  182. snapshot:child
  183. parent:view
  184. parentNode:node
  185. root:rootNode
  186. depth:&childDepth
  187. nodesMap:nodesMap
  188. hideHeaders:hideHeaders
  189. ];
  190. if (didMakeNode) {
  191. maxChildDepth = MAX(childDepth, maxChildDepth);
  192. [checkForIntersect addObject:child];
  193. }
  194. }
  195. *depthOut = maxChildDepth;
  196. return node;
  197. }
  198. @end
  199. #pragma mark SCNShape
  200. @implementation SCNShape (Snapshot)
  201. + (instancetype)shapeWithPath:(UIBezierPath *)path materialDiffuse:(id)contents {
  202. SCNShape *shape = [SCNShape shapeWithPath:path extrusionDepth:0];
  203. [shape addDoubleSidedMaterialWithDiffuseContents:contents];
  204. return shape;
  205. }
  206. + (instancetype)nameHeader:(UIColor *)color frame:(CGRect)frame corners:(CGFloat)radius {
  207. UIBezierPath *path = [UIBezierPath
  208. bezierPathWithRoundedRect:frame
  209. byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
  210. cornerRadii:CGSizeMake(radius, radius)
  211. ];
  212. return [SCNShape shapeWithPath:path materialDiffuse:color];
  213. }
  214. @end
  215. #pragma mark SCNText
  216. @implementation SCNText (Snapshot)
  217. + (instancetype)labelGeometry:(NSString *)text font:(UIFont *)font {
  218. NSParameterAssert(text);
  219. SCNText *label = [self new];
  220. label.string = text;
  221. label.font = font;
  222. label.alignmentMode = kCAAlignmentCenter;
  223. label.truncationMode = kCATruncationEnd;
  224. return label;
  225. }
  226. @end