123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- //
- // SceneKit+Snapshot.m
- // FLEX
- //
- // Created by Tanner Bennett on 1/8/20.
- //
- #import "SceneKit+Snapshot.h"
- #import "FHSSnapshotNodes.h"
- /// This value is chosen such that this offset can be applied to avoid
- /// z-fighting amongst nodes at the same z-position, but small enough
- /// that they appear to visually be on the same plane.
- CGFloat const kFHSSmallZOffset = 0.05;
- CGFloat const kHeaderVerticalInset = 8.0;
- #pragma mark SCNGeometry
- @interface SCNGeometry (SnapshotPrivate)
- @end
- @implementation SCNGeometry (SnapshotPrivate)
- - (void)addDoubleSidedMaterialWithDiffuseContents:(id)contents {
- SCNMaterial *material = [SCNMaterial new];
- material.doubleSided = YES;
- material.diffuse.contents = contents;
- [self insertMaterial:material atIndex:0];
- }
- @end
- #pragma mark SCNNode
- @implementation SCNNode (Snapshot)
- - (SCNNode *)nearestAncestorSnapshot {
- SCNNode *node = self;
- while (!node.name && node) {
- node = node.parentNode;
- }
- return node;
- }
- + (instancetype)shapeNodeWithSize:(CGSize)size materialDiffuse:(id)contents offsetZ:(BOOL)offsetZ {
- UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(
- 0, 0, size.width, size.height
- )];
- SCNShape *shape = [SCNShape shapeWithPath:path materialDiffuse:contents];
- SCNNode *node = [SCNNode nodeWithGeometry:shape];
-
- if (offsetZ) {
- node.position = SCNVector3Make(0, 0, kFHSSmallZOffset);
- }
- return node;
- }
- + (instancetype)highlight:(FHSViewSnapshot *)view color:(UIColor *)color {
- return [self shapeNodeWithSize:view.frame.size materialDiffuse:color offsetZ:YES];
- }
- + (instancetype)snapshot:(FHSViewSnapshot *)view {
- id image = view.snapshotImage;
- return [self shapeNodeWithSize:view.frame.size materialDiffuse:image offsetZ:NO];
- }
- + (instancetype)lineFrom:(SCNVector3)v1 to:(SCNVector3)v2 color:(UIColor *)lineColor {
- SCNVector3 vertices[2] = { v1, v2 };
- int32_t _indices[2] = { 0, 1 };
- NSData *indices = [NSData dataWithBytes:_indices length:sizeof(_indices)];
-
- SCNGeometrySource *source = [SCNGeometrySource geometrySourceWithVertices:vertices count:2];
- SCNGeometryElement *element = [SCNGeometryElement
- geometryElementWithData:indices
- primitiveType:SCNGeometryPrimitiveTypeLine
- primitiveCount:2
- bytesPerIndex:sizeof(int32_t)
- ];
- SCNGeometry *geometry = [SCNGeometry geometryWithSources:@[source] elements:@[element]];
- [geometry addDoubleSidedMaterialWithDiffuseContents:lineColor];
- return [SCNNode nodeWithGeometry:geometry];
- }
- - (instancetype)borderWithColor:(UIColor *)color {
- struct { SCNVector3 min, max; } bb;
- [self getBoundingBoxMin:&bb.min max:&bb.max];
- SCNVector3 topLeft = SCNVector3Make(bb.min.x, bb.max.y, kFHSSmallZOffset);
- SCNVector3 bottomLeft = SCNVector3Make(bb.min.x, bb.min.y, kFHSSmallZOffset);
- SCNVector3 topRight = SCNVector3Make(bb.max.x, bb.max.y, kFHSSmallZOffset);
- SCNVector3 bottomRight = SCNVector3Make(bb.max.x, bb.min.y, kFHSSmallZOffset);
- SCNNode *top = [SCNNode lineFrom:topLeft to:topRight color:color];
- SCNNode *left = [SCNNode lineFrom:bottomLeft to:topLeft color:color];
- SCNNode *bottom = [SCNNode lineFrom:bottomLeft to:bottomRight color:color];
- SCNNode *right = [SCNNode lineFrom:bottomRight to:topRight color:color];
- SCNNode *border = [SCNNode new];
- [border addChildNode:top];
- [border addChildNode:left];
- [border addChildNode:bottom];
- [border addChildNode:right];
- return border;
- }
- + (instancetype)header:(FHSViewSnapshot *)view {
- SCNText *text = [SCNText labelGeometry:view.title font:[UIFont boldSystemFontOfSize:13.0]];
- SCNNode *textNode = [SCNNode nodeWithGeometry:text];
- struct { SCNVector3 min, max; } bb;
- [textNode getBoundingBoxMin:&bb.min max:&bb.max];
- CGFloat textWidth = bb.max.x - bb.min.x;
- CGFloat textHeight = bb.max.y - bb.min.y;
- CGFloat snapshotWidth = view.frame.size.width;
- CGFloat headerWidth = MAX(snapshotWidth, textWidth);
- CGRect frame = CGRectMake(0, 0, headerWidth, textHeight + (kHeaderVerticalInset * 2));
- SCNNode *headerNode = [SCNNode nodeWithGeometry:[SCNShape
- nameHeader:view.headerColor frame:frame corners:8
- ]];
- [headerNode addChildNode:textNode];
- textNode.position = SCNVector3Make(
- (frame.size.width / 2.f) - (textWidth / 2.f),
- (frame.size.height / 2.f) - (textHeight / 2.f),
- kFHSSmallZOffset
- );
- headerNode.position = SCNVector3Make(
- (snapshotWidth / 2.f) - (headerWidth / 2.f),
- view.frame.size.height,
- kFHSSmallZOffset
- );
- return headerNode;
- }
- + (instancetype)snapshot:(FHSViewSnapshot *)view
- parent:(FHSViewSnapshot *)parent
- parentNode:(SCNNode *)parentNode
- root:(SCNNode *)rootNode
- depth:(NSInteger *)depthOut
- nodesMap:(NSMutableDictionary<NSString *, FHSSnapshotNodes *> *)nodesMap
- hideHeaders:(BOOL)hideHeaders {
- NSInteger const depth = *depthOut;
- // Ignore elements that are not visible.
- // These should appear in the list, but not in the 3D view.
- if (view.hidden || CGSizeEqualToSize(view.frame.size, CGSizeZero)) {
- return nil;
- }
- // Create a node whose contents are the snapshot of the element
- SCNNode *node = [self snapshot:view];
- node.name = view.view.identifier;
- // Begin building node tree
- FHSSnapshotNodes *nodes = [FHSSnapshotNodes snapshot:view depth:depth];
- nodes.snapshot = node;
- // The node must be added to the root node
- // for the coordinate space calculations below to work
- [rootNode addChildNode:node];
- node.position = ({
- // Flip the y-coordinate since SceneKit has a
- // flipped version of the UIKit coordinate system
- CGRect pframe = parent ? parent.frame : CGRectZero;
- CGFloat y = parent ? pframe.size.height - CGRectGetMaxY(view.frame) : 0;
- // To simplify calculating the z-axis spacing between the layers, we make
- // each snapshot node a direct child of the root rather than embedding
- // the nodes in their parent nodes in the same structure as the UI elements
- // themselves. With this flattened hierarchy, the z-position can be
- // calculated for every node simply by multiplying the spacing by the depth.
- //
- // `parentSnapshotNode` as referenced here is NOT the actual parent node
- // of `node`, it is the node corresponding to the parent of the UI element.
- // It is used to convert from frame coordinates, which are relative to
- // the bounds of the parent, to coordinates relative to the root node.
- SCNVector3 positionRelativeToParent = SCNVector3Make(view.frame.origin.x, y, 0);
- SCNVector3 positionRelativeToRoot;
- if (parent) {
- positionRelativeToRoot = [rootNode convertPosition:positionRelativeToParent fromNode:parentNode];
- } else {
- positionRelativeToRoot = positionRelativeToParent;
- }
- positionRelativeToRoot.z = 50 * depth;
- positionRelativeToRoot;
- });
- // Make border node
- nodes.border = [node borderWithColor:view.headerColor];
- [node addChildNode:nodes.border];
- // Make header node
- nodes.header = [SCNNode header:view];
- [node addChildNode:nodes.header];
- if (hideHeaders) {
- nodes.header.hidden = YES;
- }
- nodesMap[view.view.identifier] = nodes;
- NSMutableArray<FHSViewSnapshot *> *checkForIntersect = [NSMutableArray new];
- NSInteger maxChildDepth = depth;
- // Recurse to child nodes; overlapping children have higher depths
- for (FHSViewSnapshot *child in view.children) {
- NSInteger childDepth = depth + 1;
- // Children that intersect a sibling are rendered
- // in a separate layer above the previous siblings
- for (FHSViewSnapshot *sibling in checkForIntersect) {
- if (CGRectIntersectsRect(sibling.frame, child.frame)) {
- childDepth = maxChildDepth + 1;
- break;
- }
- }
- id didMakeNode = [SCNNode
- snapshot:child
- parent:view
- parentNode:node
- root:rootNode
- depth:&childDepth
- nodesMap:nodesMap
- hideHeaders:hideHeaders
- ];
- if (didMakeNode) {
- maxChildDepth = MAX(childDepth, maxChildDepth);
- [checkForIntersect addObject:child];
- }
- }
- *depthOut = maxChildDepth;
- return node;
- }
- @end
- #pragma mark SCNShape
- @implementation SCNShape (Snapshot)
- + (instancetype)shapeWithPath:(UIBezierPath *)path materialDiffuse:(id)contents {
- SCNShape *shape = [SCNShape shapeWithPath:path extrusionDepth:0];
- [shape addDoubleSidedMaterialWithDiffuseContents:contents];
- return shape;
- }
- + (instancetype)nameHeader:(UIColor *)color frame:(CGRect)frame corners:(CGFloat)radius {
- UIBezierPath *path = [UIBezierPath
- bezierPathWithRoundedRect:frame
- byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
- cornerRadii:CGSizeMake(radius, radius)
- ];
- return [SCNShape shapeWithPath:path materialDiffuse:color];
- }
- @end
- #pragma mark SCNText
- @implementation SCNText (Snapshot)
- + (instancetype)labelGeometry:(NSString *)text font:(UIFont *)font {
- NSParameterAssert(text);
- SCNText *label = [self new];
- label.string = text;
- label.font = font;
- label.alignmentMode = kCAAlignmentCenter;
- label.truncationMode = kCATruncationEnd;
- return label;
- }
- @end
|