Extend Path to match SwiftUI (#172)

* Initial Path element and subpath support

* Fix ambiguous .init

* Add more path commands

* Fix line length

* Fix macOS build

* Add missing file to xcodeproj
This commit is contained in:
Carson Katri 2020-07-11 09:10:25 -04:00 committed by GitHub
parent b9ade79df1
commit f891edd928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 362 additions and 76 deletions

View File

@ -34,6 +34,18 @@ public struct CGPoint: Equatable {
public static var zero: Self {
.init(x: 0, y: 0)
}
func rotate(_ angle: Angle, around origin: Self) -> Self {
let cosAngle = cos(angle.radians)
let sinAngle = sin(angle.radians)
return .init(x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x,
y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y)
}
func offset(by offset: Self) -> Self {
.init(x: x + offset.x,
y: y + offset.y)
}
}
public struct CGSize: Equatable {
@ -54,13 +66,13 @@ public struct CGRect: Equatable {
public let origin: CGPoint
public let size: CGSize
public init(_ origin: CGPoint, _ size: CGSize) {
public init(origin: CGPoint, size: CGSize) {
self.origin = origin
self.size = size
}
public static var zero: Self {
.init(.zero, .zero)
.init(origin: .zero, size: .zero)
}
}
@ -113,7 +125,8 @@ public struct CGAffineTransform: Equatable {
/// Returns an affine transformation matrix constructed from a rotation value you provide.
/// - Parameters:
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
/// A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation.
/// A positive value specifies clockwise rotation and anegative value specifies
/// counterclockwise rotation.
public init(rotationAngle angle: CGFloat) {
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
}
@ -146,7 +159,8 @@ public struct CGAffineTransform: Equatable {
y: (b * point.x) + (d * point.y) + ty)
}
/// Returns an affine transformation matrix constructed by combining two existing affine transforms.
/// Returns an affine transformation matrix constructed by combining two existing affine
/// transforms.
/// - Parameters:
/// - t2: The affine transform to concatenate to this affine transform.
/// - Returns: A new affine transformation matrix. That is, `t = t1*t2`.
@ -178,12 +192,15 @@ public struct CGAffineTransform: Equatable {
/// Returns an affine transformation matrix constructed by rotating an existing affine transform.
/// - Parameters:
/// - angle: The angle, in radians, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation.
/// - angle: The angle, in radians, by which to rotate the affine transform.
/// A positive value specifies clockwise rotation and a negative value specifies
/// counterclockwise rotation.
public func rotated(by angle: CGFloat) -> Self {
Self(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
}
/// Returns an affine transformation matrix constructed by translating an existing affine transform.
/// Returns an affine transformation matrix constructed by translating an existing
/// affine transform.
/// - Parameters:
/// - tx: The value by which to move x values with the affine transform.
/// - ty: The value by which to move y values with the affine transform.

View File

@ -15,14 +15,31 @@
// Created by Carson Katri on 06/28/2020.
//
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif
/// The outline of a 2D shape.
public struct Path: Equatable, LosslessStringConvertible {
public var description: String {
"""
\(storage)
\(elements)
\(transform)
"""
var pathString = [String]()
for element in elements {
switch element {
case let .move(to: pos):
pathString.append("\(pos.x) \(pos.y) m")
case let .line(to: pos):
pathString.append("\(pos.x) \(pos.y) l")
case let .curve(to: pos, control1: c1, control2: c2):
pathString.append("\(c1.x) \(c1.y) \(c2.x) \(c2.y) \(pos.x) \(pos.y) c")
case let .quadCurve(to: pos, control: c):
pathString.append("\(c.x) \(c.y) \(pos.x) \(pos.y) q")
case .closeSubpath:
pathString.append("h")
}
}
return pathString.joined(separator: " ")
}
public enum Storage: Equatable {
@ -48,8 +65,8 @@ public struct Path: Equatable, LosslessStringConvertible {
public var transform: CGAffineTransform = .identity
public struct _SubPath: Equatable {
let path: Path
let transform: CGAffineTransform
public let path: Path
public let transform: CGAffineTransform
}
public var subpaths: [_SubPath] = []
@ -78,7 +95,8 @@ public struct Path: Equatable, LosslessStringConvertible {
cornerRadius: CGFloat,
style: RoundedCornerStyle = .circular) {
storage = .roundedRect(FixedRoundedRect(rect: rect,
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
cornerSize: CGSize(width: cornerRadius,
height: cornerRadius),
style: style))
}
@ -215,21 +233,56 @@ extension Path {
}
public mutating func addRect(_ rect: CGRect, transform: CGAffineTransform = .identity) {
subpaths.append(.init(path: .init(rect), transform: transform))
move(to: rect.origin)
addLine(to: CGPoint(x: rect.size.width, y: 0)
.offset(by: rect.origin))
addLine(to: CGPoint(x: rect.size.width, y: rect.size.height)
.offset(by: rect.origin))
addLine(to: CGPoint(x: 0, y: rect.size.height)
.offset(by: rect.origin))
closeSubpath()
}
public mutating func addRoundedRect(in rect: CGRect,
cornerSize: CGSize,
style: RoundedCornerStyle = .circular,
transform: CGAffineTransform = .identity) {
subpaths.append(.init(path: .init(roundedRect: rect,
cornerSize: cornerSize,
style: style),
transform: transform))
move(to: CGPoint(x: rect.size.width, y: rect.size.height / 2)
.offset(by: rect.origin))
addLine(to: CGPoint(x: rect.size.width, y: rect.size.height - cornerSize.height)
.offset(by: rect.origin))
addQuadCurve(to: CGPoint(x: rect.size.width - cornerSize.width, y: rect.size.height)
.offset(by: rect.origin),
control: CGPoint(x: rect.size.width, y: rect.size.height)
.offset(by: rect.origin))
addLine(to: CGPoint(x: cornerSize.width, y: rect.size.height)
.offset(by: rect.origin))
addQuadCurve(to: CGPoint(x: 0, y: rect.size.height - cornerSize.height)
.offset(by: rect.origin),
control: CGPoint(x: 0, y: rect.size.height)
.offset(by: rect.origin))
addLine(to: CGPoint(x: 0, y: cornerSize.height)
.offset(by: rect.origin))
addQuadCurve(to: CGPoint(x: cornerSize.width, y: 0)
.offset(by: rect.origin),
control: CGPoint.zero
.offset(by: rect.origin))
addLine(to: CGPoint(x: rect.size.width - cornerSize.width, y: 0)
.offset(by: rect.origin))
addQuadCurve(to: CGPoint(x: rect.size.width, y: cornerSize.height)
.offset(by: rect.origin),
control: CGPoint(x: rect.size.width, y: 0)
.offset(by: rect.origin))
closeSubpath()
}
public mutating func addEllipse(in rect: CGRect, transform: CGAffineTransform = .identity) {
subpaths.append(.init(path: .init(ellipseIn: rect), transform: transform))
subpaths.append(.init(path: .init(ellipseIn: .init(origin: rect.origin
.offset(by: .init(x: rect.size.width / 2,
y: rect.size.height / 2)),
size: .init(width: rect.size.width / 2,
height: rect.size.height / 2))),
transform: transform))
}
public mutating func addRects(_ rects: [CGRect], transform: CGAffineTransform = .identity) {
@ -245,31 +298,79 @@ extension Path {
startAngle: Angle,
delta: Angle,
transform: CGAffineTransform = .identity) {
// I don't know how to do this without sin/cos
addArc(center: center,
radius: radius,
startAngle: startAngle,
endAngle: startAngle + delta,
clockwise: false)
}
// There's a great article on bezier curves here:
// https://pomax.github.io/bezierinfo
// FIXME: Handle negative delta
public mutating func addArc(center: CGPoint,
radius: CGFloat,
startAngle: Angle,
endAngle: Angle,
clockwise: Bool,
transform: CGAffineTransform = .identity) {
// I don't know how to do this without sin/cos
if clockwise {
addArc(center: center,
radius: radius,
startAngle: endAngle,
endAngle: endAngle + (.radians(.pi * 2) - endAngle) + startAngle,
clockwise: false)
} else {
let angle = abs(startAngle.radians - endAngle.radians)
if angle > .pi / 2 {
// Split the angle into 90º chunks
let chunk1 = Angle.radians(startAngle.radians + (.pi / 2))
addArc(center: center,
radius: radius,
startAngle: startAngle,
endAngle: chunk1,
clockwise: clockwise)
addArc(center: center,
radius: radius,
startAngle: chunk1,
endAngle: endAngle,
clockwise: clockwise)
} else {
let startPoint = CGPoint(x: radius + center.x,
y: center.y)
let endPoint = CGPoint(x: (radius * cos(angle)) + center.x,
y: (radius * sin(angle)) + center.y)
let l = (4 / 3) * tan(angle / 4)
let c1 = CGPoint(x: radius + center.x, y: (l * radius) + center.y)
let c2 = CGPoint(x: ((cos(angle) + l * sin(angle)) * radius) + center.x,
y: ((sin(angle) - l * cos(angle)) * radius) + center.y)
move(to: startPoint.rotate(startAngle, around: center))
addCurve(to: endPoint.rotate(startAngle, around: center),
control1: c1.rotate(startAngle, around: center),
control2: c2.rotate(startAngle, around: center))
}
}
}
// FIXME: How does this arc method work?
public mutating func addArc(tangent1End p1: CGPoint,
tangent2End p2: CGPoint,
radius: CGFloat,
transform: CGAffineTransform = .identity) {
// I don't know how to do this without sin/cos
}
transform: CGAffineTransform = .identity) {}
public mutating func addPath(_ path: Path, transform: CGAffineTransform = .identity) {
subpaths.append(.init(path: path, transform: transform))
}
public var currentPoint: CGPoint? {
nil
switch elements.last {
case let .move(to: point): return point
case let .line(to: point): return point
case let .curve(to: point, control1: _, control2: _): return point
case let .quadCurve(to: point, control: _): return point
default: return nil
}
}
public func applying(_ transform: CGAffineTransform) -> Path {

View File

@ -52,9 +52,9 @@ extension Rectangle: InsettableShape {
}
public func path(in rect: CGRect) -> Path {
.init(CGRect(rect.origin,
CGSize(width: rect.size.width - (amount / 2),
height: rect.size.height - (amount / 2))))
.init(CGRect(origin: rect.origin,
size: CGSize(width: rect.size.width - (amount / 2),
height: rect.size.height - (amount / 2))))
}
public func inset(by amount: CGFloat) -> Rectangle._Inset {

View File

@ -15,7 +15,7 @@
// Created by Max Desiatov on 06/28/2020.
//
public struct Angle {
public struct Angle: AdditiveArithmetic {
public var radians: Double
public var degrees: Double {
get { radians * (180.0 / .pi) }
@ -41,4 +41,30 @@ public struct Angle {
public static func degrees(_ degrees: Double) -> Angle {
Angle(degrees: degrees)
}
public static let zero: Angle = .radians(0)
public static func + (lhs: Self, rhs: Self) -> Self {
.radians(lhs.radians + rhs.radians)
}
public static func += (lhs: inout Self, rhs: Self) {
// swiftlint:disable:next shorthand_operator
lhs = lhs + rhs
}
public static func - (lhs: Self, rhs: Self) -> Self {
.radians(lhs.radians - rhs.radians)
}
public static func -= (lhs: inout Self, rhs: Self) {
// swiftlint:disable:next shorthand_operator
lhs = lhs - rhs
}
}
extension Angle: Hashable, Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.radians < rhs.radians
}
}

View File

@ -20,7 +20,7 @@ import TokamakCore
public typealias Path = TokamakCore.Path
extension Path: ViewDeferredToRenderer {
// TODO: Support transformations, subpaths, and read through elements to create a path
// TODO: Support transformations
func svgFrom(storage: Storage,
strokeStyle: StrokeStyle = .init(lineWidth: 0,
lineCap: .butt,
@ -30,35 +30,86 @@ extension Path: ViewDeferredToRenderer {
dashPhase: 0)) -> AnyView {
let stroke = [
"stroke-width": "\(strokeStyle.lineWidth)",
"stroke": "black", // TODO: Use the environment variable "foregroundColor"
]
let uniqueKeys = { (first: String, _: String) in first }
switch storage {
case .empty:
return AnyView(EmptyView())
case let .rect(rect):
return AnyView(AnyView(HTML("rect", ["width": "\(max(0, rect.size.width))",
"height": "\(max(0, rect.size.height))"]
.merging(stroke, uniquingKeysWith: uniqueKeys))))
case .ellipse:
return AnyView(HTML("ellipse", ["cx": "50%", "cy": "50%", "rx": "50%", "ry": "50%"]
return AnyView(AnyView(HTML("rect", [
"width": "\(max(0, rect.size.width))",
"height": "\(max(0, rect.size.height))",
"x": "\(rect.origin.x - (rect.size.width / 2))",
"y": "\(rect.origin.y - (rect.size.height / 2))",
].merging(stroke, uniquingKeysWith: uniqueKeys))))
case let .ellipse(rect):
return AnyView(HTML("ellipse", ["cx": "\(rect.origin.x)",
"cy": "\(rect.origin.y)",
"rx": "\(rect.size.width)",
"ry": "\(rect.size.height)"]
.merging(stroke, uniquingKeysWith: uniqueKeys)))
case let .roundedRect(roundedRect):
return AnyView(HTML("rect", [
"width": "\(roundedRect.rect.size.width)",
"height": "\(roundedRect.rect.size.height)",
"rx": "\(roundedRect.cornerSize.width)",
"ry": "\(roundedRect.style == .continuous ? roundedRect.cornerSize.width : roundedRect.cornerSize.height)",
]
.merging(stroke, uniquingKeysWith: uniqueKeys)))
"ry": """
\(roundedRect.style == .continuous ?
roundedRect.cornerSize.width :
roundedRect.cornerSize.height)
""",
"x": "\(roundedRect.rect.origin.x)",
"y": "\(roundedRect.rect.origin.y)",
].merging(stroke, uniquingKeysWith: uniqueKeys)))
case let .stroked(stroked):
return stroked.path.svgFrom(storage: stroked.path.storage, strokeStyle: stroked.style)
return AnyView(stroked.path.svgBody(strokeStyle: stroked.style))
case let .trimmed(trimmed):
return trimmed.path.svgFrom(storage: trimmed.path.storage, strokeStyle: strokeStyle) // TODO: Trim the path
return trimmed.path.svgFrom(storage: trimmed.path.storage,
strokeStyle: strokeStyle) // TODO: Trim the path
}
}
var size: CGSize {
func svgFrom(elements: [Element],
strokeStyle: StrokeStyle = .init(lineWidth: 0,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
dash: [],
dashPhase: 0)) -> AnyView {
var d = [String]()
for element in elements {
switch element {
case let .move(to: pos):
d.append("M\(pos.x),\(pos.y)")
case let .line(to: pos):
d.append("L\(pos.x),\(pos.y)")
case let .curve(to: pos, control1: c1, control2: c2):
d.append("C\(c1.x),\(c1.y),\(c2.x),\(c2.y),\(pos.x),\(pos.y)")
case let .quadCurve(to: pos, control: c1):
d.append("Q\(c1.x),\(c1.y),\(pos.x),\(pos.y)")
case .closeSubpath:
d.append("Z")
}
}
return AnyView(HTML("path", [
"style": "stroke-width: \(strokeStyle.lineWidth);",
"d": d.joined(separator: "\n"),
]))
}
func svgFrom(subpaths: [_SubPath],
strokeStyle: StrokeStyle = .init(lineWidth: 0,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
dash: [],
dashPhase: 0)) -> AnyView {
AnyView(ForEach(Array(subpaths.enumerated()), id: \.offset) { _, path in
path.path.svgBody(strokeStyle: strokeStyle)
})
}
var storageSize: CGSize {
switch storage {
case .empty:
return .zero
@ -73,12 +124,51 @@ extension Path: ViewDeferredToRenderer {
}
}
var elementsSize: CGSize {
// Curves may clip without an explicit size
let positions = elements.compactMap { elem -> CGPoint? in
switch elem {
case let .move(to: pos): return pos
case let .line(to: pos): return pos
case let .curve(to: pos, control1: _, control2: _): return pos
case let .quadCurve(to: pos, control: _): return pos
case .closeSubpath: return nil
}
}
let xPos = positions.map(\.x).sorted(by: <)
let minX = xPos.first ?? 0
let maxX = xPos.last ?? 0
let yPos = positions.map(\.y).sorted(by: <)
let minY = yPos.first ?? 0
let maxY = yPos.last ?? 0
return CGSize(width: abs(maxX - min(0, minX)), height: abs(maxY - min(0, minY)))
}
var size: CGSize {
.init(width: max(storageSize.width, elementsSize.width),
height: max(storageSize.height, elementsSize.height))
}
@ViewBuilder
func svgBody(strokeStyle: StrokeStyle = .init(lineWidth: 0,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
dash: [],
dashPhase: 0)) -> some View {
svgFrom(storage: storage, strokeStyle: strokeStyle)
svgFrom(elements: elements, strokeStyle: strokeStyle)
svgFrom(subpaths: subpaths, strokeStyle: strokeStyle)
}
public var deferredBody: AnyView {
AnyView(HTML("svg", ["style": """
width: \(max(0, size.width));
height: \(max(0, size.height));
overflow: visible;
"""]) {
svgFrom(storage: storage)
svgBody()
})
}
}

View File

@ -17,6 +17,8 @@
import TokamakCore
public typealias Shape = TokamakCore.Shape
// Border modifier
extension _OverlayModifier: DOMViewModifier
where Overlay == _ShapeView<_StrokedShape<TokamakCore.Rectangle._Inset>, Color> {

View File

@ -17,11 +17,27 @@
import TokamakCore
protocol ShapeAttributes {
func attributes(_ style: ShapeStyle) -> [String: String]
}
extension _StrokedShape: ShapeAttributes {
func attributes(_ style: ShapeStyle) -> [String: String] {
if let color = style as? Color {
return ["style": "stroke: \(color); fill: none;"]
} else {
return ["style": "stroke: black; fill: none;"]
}
}
}
extension _ShapeView: ViewDeferredToRenderer {
public var deferredBody: AnyView {
let path = shape.path(in: .zero).deferredBody
if let fillColor = style as? Color {
return AnyView(HTML("div", ["style": "fill: \(fillColor.description)"]) { path })
if let shapeAttributes = shape as? ShapeAttributes {
return AnyView(HTML("div", shapeAttributes.attributes(style)) { path })
} else if let color = style as? Color {
return AnyView(HTML("div", ["style": "fill: \(color);"]) { path })
} else {
return path
}

View File

@ -0,0 +1,57 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#if canImport(SwiftUI)
import SwiftUI
#else
import TokamakDOM
#endif
struct Star: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: .init(x: 40, y: 0))
path.addLine(to: .init(x: 20, y: 76))
path.addLine(to: .init(x: 80, y: 30.4))
path.addLine(to: .init(x: 0, y: 30.4))
path.addLine(to: .init(x: 64, y: 76))
path.addLine(to: .init(x: 40, y: 0))
print(path)
}
}
}
struct PathDemo: View {
var body: some View {
VStack {
Star()
.fill(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1))
Path { path in
path.addRect(.init(origin: .zero, size: .init(width: 20, height: 20)))
path.addEllipse(in: .init(origin: .init(x: 25, y: 0),
size: .init(width: 20, height: 20)))
path.addRoundedRect(in: .init(origin: .init(x: 50, y: 0),
size: .init(width: 20, height: 20)),
cornerSize: .init(width: 4, height: 4))
path.addArc(center: .init(x: 85, y: 10),
radius: 10,
startAngle: .degrees(90),
endAngle: .degrees(180),
clockwise: true)
}
.stroke(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1), lineWidth: 4)
.padding(.vertical)
}
}
}

View File

@ -1,26 +0,0 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import TokamakDOM
struct SVGCircle: View {
var body: some View {
HTML("svg", ["width": "100%", "height": "100%"]) {
HTML("circle", [
"cx": "50%", "cy": "50%", "r": "40%",
"stroke": "green", "stroke-width": "4", "fill": "yellow",
])
}
}
}

View File

@ -43,10 +43,7 @@ struct TokamakDemoView: View {
}
ForEachDemo()
TextDemo()
#if canImport(TokamakDOM)
SVGCircle()
.frame(width: 25, height: 25)
#endif
PathDemo()
TextFieldDemo()
SpacerDemo()
EnvironmentDemo()

View File

@ -25,6 +25,8 @@
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
@ -47,6 +49,7 @@
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -104,6 +107,7 @@
85ED189E24AD425E0085DFA0 /* Counter.swift */,
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */,
B51F214F24B920B400CF2583 /* PathDemo.swift */,
);
name = TokamakDemo;
path = ../Sources/TokamakDemo;
@ -210,6 +214,7 @@
files = (
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
@ -227,6 +232,7 @@
files = (
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,