dsh-notch 0.3.1 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +58 -47
- package/README.md +60 -55
- package/cordis.patch.yml +3 -0
- package/desktop/README.md +29 -0
- package/desktop/notch-lifecycle.mjs +120 -0
- package/desktop/restart-probe.mjs +61 -0
- package/docs/desktop-017rc2.md +23 -0
- package/docs/elastic-edge-dock.md +127 -0
- package/docs/motion-audit.md +1 -1
- package/docs/releases/v0.3.2.md +38 -0
- package/docs/releases/v0.3.3.md +9 -0
- package/docs/subagent-compatibility.md +51 -0
- package/docs/verification/elastic-algorithm-timing.json +337 -0
- package/docs/verification/elastic-attention.json +44 -0
- package/docs/verification/elastic-frame-timing.json +240 -0
- package/docs/verification/elastic-tension.json +59 -0
- package/docs/verification/elastic-tip-taper.json +181 -0
- package/dshx.yml +2 -0
- package/lib/client.js +112 -0
- package/lib/client.js.map +1 -0
- package/lib/index.mjs +990 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +66 -10
- package/src/board.ts +178 -35
- package/src/client/index.ts +104 -0
- package/src/dsh-notch.ts +19 -2
- package/src/http.ts +17 -2
- package/src/session-state.ts +15 -1
- package/src/store.ts +3 -3
- package/src/types.ts +3 -0
- package/BASELINE.md +0 -13
- package/DESIGN.md +0 -34
- package/cordis.yml +0 -5
- package/macos/Package.swift +0 -17
- package/macos/STATUS-MOTION.md +0 -49
- package/macos/Sources/Client.swift +0 -139
- package/macos/Sources/IdleRobot.swift +0 -423
- package/macos/Sources/NotchMarkdown.swift +0 -266
- package/macos/Sources/Panel.swift +0 -153
- package/macos/Sources/Resources/Idle/balance.json +0 -1
- package/macos/Sources/Resources/Idle/blink.json +0 -1
- package/macos/Sources/Resources/Idle/cube-in.json +0 -1
- package/macos/Sources/Resources/Idle/dance.json +0 -1
- package/macos/Sources/Resources/Idle/hop.json +0 -1
- package/macos/Sources/Resources/Idle/nod.json +0 -1
- package/macos/Sources/Resources/Idle/satellite-in.json +0 -1
- package/macos/Sources/Resources/Idle/satellite-out.json +0 -1
- package/macos/Sources/Resources/Idle/scan.json +0 -1
- package/macos/Sources/Resources/Idle/sleep.json +0 -1
- package/macos/Sources/Resources/Idle/sneeze.json +0 -1
- package/macos/Sources/Resources/Idle/stretch.json +0 -1
- package/macos/Sources/Resources/Idle/tilt.json +0 -1
- package/macos/Sources/ReviewStudio.swift +0 -337
- package/macos/Sources/RootView.swift +0 -1093
- package/macos/Sources/StatusOrbit.swift +0 -642
- package/macos/Sources/main.swift +0 -153
- package/macos/Tests/ExpandedHeightProbe.swift +0 -108
- package/macos/Tests/GeometryProbe.swift +0 -98
- package/macos/Tests/IdleProbe.swift +0 -294
- package/macos/Tests/MotionProbe.swift +0 -106
- package/macos/Tests/OutcomeProbe.swift +0 -133
- package/macos/Tests/ScrollbarProbe.swift +0 -91
- package/macos/Tests/expanded-height.sh +0 -13
- package/macos/Tests/geometry.sh +0 -14
- package/macos/Tests/idle.sh +0 -14
- package/macos/Tests/motion.sh +0 -14
- package/macos/Tests/outcome.sh +0 -14
- package/macos/Tests/scrollbar.sh +0 -13
- package/tests/answer-lifecycle.test.mjs +0 -23
- package/tests/board.test.mjs +0 -7
- package/tests/browse-sync.test.mjs +0 -20
- package/tests/mirror.test.mjs +0 -4
- package/tests/peer-range.test.mjs +0 -21
- package/tests/session-state.test.mjs +0 -54
- package/tests/unread-hold.test.mjs +0 -90
- package/tools/desktop-shell/README.md +0 -56
- package/tools/desktop-shell/child-log.mjs +0 -6
- package/tools/desktop-shell/child-log.test.mjs +0 -14
- package/tools/desktop-shell/package-recovery.py +0 -78
- package/tools/desktop-shell/recovery-probe.cjs +0 -50
- package/tools/desktop-shell/renderer-recovery.mjs +0 -95
- package/tools/desktop-shell/renderer-recovery.test.mjs +0 -24
- package/tools/diagnostics/completion-watch.py +0 -101
- package/tools/diagnostics/completion-watch.test.py +0 -17
- package/tools/diagnostics/renderer-diagnostics.mjs +0 -56
- package/tools/diagnostics/renderer-diagnostics.test.mjs +0 -32
- package/tools/idle/LICENSE.open-bot-motion +0 -21
- package/tools/idle/export.mjs +0 -40
- package/tools/idle/index.html +0 -4
- package/tools/idle/motions.js +0 -83
- package/tools/idle/original.js +0 -1673
- package/tools/idle/renderer.js +0 -1673
- package/tools/recording/Client.swift +0 -76
- package/tools/recording/DemoCatalog.swift +0 -174
- package/tools/recording/README.md +0 -23
- package/tools/recording/build.sh +0 -41
- package/tools/recording/main.swift +0 -100
- package/tools/review/ReviewStudio.swift +0 -236
- package/tools/review/audit.sh +0 -19
- package/tools/review/build.sh +0 -7
- package/tools/review/main.swift +0 -158
- package/tools/review/render-audit.swift +0 -32
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
|
-
import AppKit
|
|
3
|
-
import CoreText
|
|
4
|
-
|
|
5
|
-
struct OrbitLayout: Equatable {
|
|
6
|
-
var top: Double = 0
|
|
7
|
-
var middle: Double = 0
|
|
8
|
-
var bottom: Double = 0
|
|
9
|
-
var decision: Double = 0
|
|
10
|
-
var total: Double { top+middle+bottom+decision }
|
|
11
|
-
var height: CGFloat { 20+28*max(0,total-1) }
|
|
12
|
-
var middleY: CGFloat { 10+28*(top+max(0,decision+middle-1)) }
|
|
13
|
-
var bottomY: CGFloat { height-10 }
|
|
14
|
-
static func mix(_ a:Self,_ b:Self,_ t:Double)->Self {
|
|
15
|
-
Self(top:a.top+(b.top-a.top)*t,middle:a.middle+(b.middle-a.middle)*t,bottom:a.bottom+(b.bottom-a.bottom)*t,decision:a.decision+(b.decision-a.decision)*t)
|
|
16
|
-
}
|
|
17
|
-
static func flight(from a:Self,to b:Self,progress:Double,flight:StatusFlight)->Self {
|
|
18
|
-
let growth=OrbitMotionFrame.ease(progress/0.48)
|
|
19
|
-
var result=mix(a,b,growth)
|
|
20
|
-
if !flight.returnsToRunning {
|
|
21
|
-
let source=OrbitMotionFrame(progress:progress,failed:flight.failed,returns:false,angle:flight.angle).sourceOpacity
|
|
22
|
-
// Existing result: close the vacated slot as ink leaves it. A new result
|
|
23
|
-
// first needs room for its drawn circle, then releases the source slot.
|
|
24
|
-
let remaining=flight.destinationBefore > 0 ? source:1-OrbitMotionFrame.ease((progress-0.72)/0.28)
|
|
25
|
-
result.middle=max(a.middle,1)*remaining
|
|
26
|
-
}
|
|
27
|
-
return result
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
enum StatusOutcome {
|
|
32
|
-
case success, failure, decision
|
|
33
|
-
var rgb: (Double, Double, Double) {
|
|
34
|
-
switch self {
|
|
35
|
-
case .success: (0.204, 0.780, 0.349)
|
|
36
|
-
case .failure: (1.0, 0.251, 0.0)
|
|
37
|
-
case .decision: (0.949, 1.0, 0.078)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
var color: Color { let c=rgb; return Color(red:c.0,green:c.1,blue:c.2) }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
struct StatusFlight: Identifiable {
|
|
44
|
-
let id = UUID()
|
|
45
|
-
let outcome: StatusOutcome
|
|
46
|
-
var failed: Bool { outcome == .failure }
|
|
47
|
-
var decision: Bool { outcome == .decision }
|
|
48
|
-
let startedAt: Date
|
|
49
|
-
let busyBefore: Int
|
|
50
|
-
let destinationBefore: Int
|
|
51
|
-
let returnsToRunning: Bool
|
|
52
|
-
let angle: Double
|
|
53
|
-
init(failed: Bool, startedAt: Date, busyBefore: Int, destinationBefore: Int, returnsToRunning: Bool = true, angle:Double? = nil) {
|
|
54
|
-
self.init(outcome:failed ? .failure:.success,startedAt:startedAt,busyBefore:busyBefore,destinationBefore:destinationBefore,returnsToRunning:returnsToRunning,angle:angle)
|
|
55
|
-
}
|
|
56
|
-
init(outcome: StatusOutcome, startedAt: Date, busyBefore: Int, destinationBefore: Int, returnsToRunning: Bool = true, angle:Double? = nil) {
|
|
57
|
-
self.outcome = outcome
|
|
58
|
-
self.startedAt = startedAt
|
|
59
|
-
self.busyBefore = busyBefore
|
|
60
|
-
self.destinationBefore = destinationBefore
|
|
61
|
-
self.returnsToRunning = returnsToRunning
|
|
62
|
-
self.angle=angle ?? startedAt.timeIntervalSinceReferenceDate*2 * .pi/3
|
|
63
|
-
}
|
|
64
|
-
static let duration: TimeInterval = 0.95
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// A reply returns the same task to the running lamp. It owns a clock so
|
|
68
|
-
/// polling and panel folding cannot reset its ink, count, or layout progress.
|
|
69
|
-
struct DecisionReturn: Identifiable {
|
|
70
|
-
let id = UUID()
|
|
71
|
-
let startedAt: Date
|
|
72
|
-
let busyBefore: Int
|
|
73
|
-
let from: OrbitLayout
|
|
74
|
-
let angle: Double
|
|
75
|
-
var travelling: Bool { from.middle > 0.001 || from.top > 0.001 }
|
|
76
|
-
var duration: Double { travelling ? StatusFlight.duration:DecisionMorph.resumeDuration }
|
|
77
|
-
func progress(at now:Date)->Double { min(1,max(0,now.timeIntervalSince(startedAt)/duration)) }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
struct DecisionReturnFrame {
|
|
81
|
-
let progress: Double
|
|
82
|
-
let distance: CGFloat
|
|
83
|
-
let tailLength: CGFloat
|
|
84
|
-
let tint: Double
|
|
85
|
-
let fill: Double
|
|
86
|
-
let strokeOpacity: Double
|
|
87
|
-
let countMix: Double
|
|
88
|
-
let collapse: Double
|
|
89
|
-
let baseRingOpacity: Double
|
|
90
|
-
init(progress:Double,angle:Double) {
|
|
91
|
-
let p=min(1,max(0,progress));self.progress=p
|
|
92
|
-
let route=OrbitBrushRoute(failed:false,angle:angle)
|
|
93
|
-
let u=min(1,max(0,(p-0.14)/0.86))
|
|
94
|
-
let span=route.total-route.resultDrawn
|
|
95
|
-
let terminal=9.5*DecisionSpin.runningVelocity*StatusFlight.duration*0.86/span
|
|
96
|
-
let paced=(3*u*u-2*u*u*u)+terminal*(u*u*u-u*u)
|
|
97
|
-
distance=route.resultDrawn+span*paced
|
|
98
|
-
let leaving=OrbitMotionFrame.ease((distance-route.resultDrawn)/max(1,route.returned-route.resultDrawn))
|
|
99
|
-
let joined=OrbitMotionFrame.ease((distance-route.returned)/max(1,route.total-route.returned))
|
|
100
|
-
tailLength=(2 * .pi*9.5)+(18-2 * .pi*9.5)*leaving+(route.departure-18)*joined
|
|
101
|
-
tint=1-leaving
|
|
102
|
-
fill=1-OrbitMotionFrame.ease(p/0.28)
|
|
103
|
-
strokeOpacity=OrbitMotionFrame.ease(p/0.14)
|
|
104
|
-
countMix=joined
|
|
105
|
-
collapse=OrbitMotionFrame.ease((p-0.35)/0.65)
|
|
106
|
-
// The existing ring keeps rotating while the incoming tip approaches it;
|
|
107
|
-
// the travelling trail becomes that same ring at the final phase/velocity.
|
|
108
|
-
baseRingOpacity=1-OrbitMotionFrame.ease((leaving-0.55)/0.45)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
struct DecisionReturnStroke: Shape {
|
|
113
|
-
let frame:DecisionReturnFrame
|
|
114
|
-
let angle:Double
|
|
115
|
-
let gap:CGFloat
|
|
116
|
-
func path(in rect:CGRect)->Path {
|
|
117
|
-
let route=OrbitBrushRoute(failed:false,angle:angle,gap:gap)
|
|
118
|
-
let original=OrbitBrushRoute(failed:false,angle:angle)
|
|
119
|
-
let source=[original.arrival,original.resultDrawn,original.returned,original.total]
|
|
120
|
-
let target=[route.arrival,route.resultDrawn,route.returned,route.total]
|
|
121
|
-
func mapped(_ d:CGFloat)->CGFloat {
|
|
122
|
-
for i in 1..<source.count where d <= source[i] {
|
|
123
|
-
return target[i-1]+(target[i]-target[i-1])*(d-source[i-1])/max(0.00001,source[i]-source[i-1])
|
|
124
|
-
}
|
|
125
|
-
return route.total
|
|
126
|
-
}
|
|
127
|
-
return route.path(in:mapped(frame.distance-frame.tailLength)...mapped(frame.distance))
|
|
128
|
-
.offsetBy(dx:rect.midX,dy:rect.midY)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
struct OrbitMotionFrame {
|
|
133
|
-
let progress: Double
|
|
134
|
-
let failed: Bool
|
|
135
|
-
let returns: Bool
|
|
136
|
-
let distance: CGFloat
|
|
137
|
-
let tailLength: CGFloat
|
|
138
|
-
let tint: Double
|
|
139
|
-
let resultOpacity: Double
|
|
140
|
-
let sourceOpacity: Double
|
|
141
|
-
let arrived: Bool
|
|
142
|
-
let returning: Bool
|
|
143
|
-
let returned: Bool
|
|
144
|
-
static func ease(_ value: Double) -> Double {
|
|
145
|
-
let t = min(1, max(0, value))
|
|
146
|
-
return t * t * (3 - 2 * t)
|
|
147
|
-
}
|
|
148
|
-
private static func pacedHead(_ t:Double,_ values:[CGFloat])->CGFloat {
|
|
149
|
-
let times=[0.0,0.18,0.56,0.94,1.0]
|
|
150
|
-
func slope(_ i:Int)->CGFloat {
|
|
151
|
-
guard i > 0 && i < 4 else { return 0 }
|
|
152
|
-
let a=(values[i]-values[i-1])/(times[i]-times[i-1])
|
|
153
|
-
let b=(values[i+1]-values[i])/(times[i+1]-times[i])
|
|
154
|
-
return a*b > 0 ? 2*a*b/(a+b):0
|
|
155
|
-
}
|
|
156
|
-
let i=(0..<4).first { t <= times[$0+1] } ?? 3
|
|
157
|
-
let dt=times[i+1]-times[i],u=min(1,max(0,(t-times[i])/dt)),u2=u*u,u3=u2*u
|
|
158
|
-
return (2*u3-3*u2+1)*values[i]+(u3-2*u2+u)*dt*slope(i)+(-2*u3+3*u2)*values[i+1]+(u3-u2)*dt*slope(i+1)
|
|
159
|
-
}
|
|
160
|
-
init(progress: Double, failed: Bool, returns: Bool, angle: Double = 0) {
|
|
161
|
-
self.progress = min(1, max(0, progress))
|
|
162
|
-
self.failed = failed
|
|
163
|
-
self.returns = returns
|
|
164
|
-
let route = OrbitBrushRoute(failed: failed, angle: angle)
|
|
165
|
-
let end = returns ? route.total : route.resultDrawn + 18
|
|
166
|
-
let u=self.progress
|
|
167
|
-
let terminal=9.5*DecisionSpin.runningVelocity*StatusFlight.duration/(end-route.departure)
|
|
168
|
-
let paced=(3*u*u-2*u*u*u)+terminal*(u-3*u*u+2*u*u*u)
|
|
169
|
-
let head = returns ? route.departure + (end - route.departure) * paced : Self.pacedHead(self.progress,[route.departure,route.sourceExit,route.arrival,route.resultDrawn,route.resultDrawn+18])
|
|
170
|
-
distance = min(head, returns ? route.total : route.resultDrawn)
|
|
171
|
-
let outbound = Self.ease((head - route.sourceExit) / (route.arrival - route.sourceExit))
|
|
172
|
-
let inbound = Self.ease((head - route.resultDrawn + 8) / (route.returned - route.resultDrawn + 16))
|
|
173
|
-
let tintStart = max(route.departure, route.sourceExit - 8)
|
|
174
|
-
let outgoingTint = Self.ease((head - tintStart) / (route.arrival + 8 - tintStart))
|
|
175
|
-
tint = returns && head > route.resultDrawn - 8 ? 1 - inbound : outgoingTint
|
|
176
|
-
resultOpacity = Self.ease((head - route.arrival - 0.55 * (route.resultDrawn - route.arrival)) / (0.35 * (route.resultDrawn - route.arrival)))
|
|
177
|
-
arrived = resultOpacity > 0
|
|
178
|
-
returning = returns && head > route.resultDrawn
|
|
179
|
-
returned = returns && head >= route.returned
|
|
180
|
-
sourceOpacity = returns ? 1 : 1 - outbound
|
|
181
|
-
if head < route.arrival { tailLength = route.departure + (18 - route.departure) * outbound }
|
|
182
|
-
else if !returns && head > route.resultDrawn { tailLength = max(0, 18 - (head - route.resultDrawn)) }
|
|
183
|
-
else if head > route.returned {
|
|
184
|
-
let t = (head - route.returned) / (route.total - route.returned)
|
|
185
|
-
tailLength = 18 + (route.departure - 18) * (3*t*t-2*t*t*t)
|
|
186
|
-
} else { tailLength = 18 }
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/// An arc-length trail: the head draws forward and the tail follows on the same route.
|
|
191
|
-
struct OrbitBrushRoute {
|
|
192
|
-
private(set) var points: [CGPoint] = []
|
|
193
|
-
private(set) var lengths: [CGFloat] = []
|
|
194
|
-
private(set) var departure: CGFloat = 0
|
|
195
|
-
private(set) var sourceExit: CGFloat = 0
|
|
196
|
-
private(set) var arrival: CGFloat = 0
|
|
197
|
-
private(set) var resultDrawn: CGFloat = 0
|
|
198
|
-
private(set) var returned: CGFloat = 0
|
|
199
|
-
var total: CGFloat { lengths.last ?? 0 }
|
|
200
|
-
|
|
201
|
-
init(failed: Bool, angle: Double, gap: CGFloat = 28) {
|
|
202
|
-
let r: CGFloat = 9.5
|
|
203
|
-
let direction: CGFloat = failed ? 1 : -1
|
|
204
|
-
let y = direction * gap
|
|
205
|
-
arc(center: .zero, from: angle, sweep: 1.4 * .pi, radius: r)
|
|
206
|
-
departure = total
|
|
207
|
-
let exitAngle = failed ? Double.pi / 2 : -Double.pi / 2
|
|
208
|
-
let sourceHead = angle + 1.4 * .pi
|
|
209
|
-
let extra = (exitAngle - sourceHead).truncatingRemainder(dividingBy: 2 * .pi)
|
|
210
|
-
arc(center: .zero, from: sourceHead, sweep: extra < 0 ? extra + 2 * .pi : extra, radius: r)
|
|
211
|
-
sourceExit = total
|
|
212
|
-
let start = points.last!
|
|
213
|
-
curve(from: start, c1: CGPoint(x: start.x - 4 * sin(exitAngle), y: start.y + 4 * cos(exitAngle)),
|
|
214
|
-
c2: CGPoint(x: -r, y: y + 10), to: CGPoint(x: -r, y: y))
|
|
215
|
-
arrival = total
|
|
216
|
-
arc(center: CGPoint(x: 0, y: y), from: .pi, sweep: 2 * .pi, radius: r)
|
|
217
|
-
resultDrawn = total
|
|
218
|
-
let finalAngle = angle + StatusFlight.duration * 2 * .pi / 3
|
|
219
|
-
let end = CGPoint(x: r * cos(exitAngle), y: r * sin(exitAngle))
|
|
220
|
-
curve(from: points.last!, c1: CGPoint(x: -r, y: y - 10),
|
|
221
|
-
c2: CGPoint(x: end.x + 4 * sin(exitAngle), y: end.y - 4 * cos(exitAngle)), to: end)
|
|
222
|
-
returned = total
|
|
223
|
-
let resume = (finalAngle - exitAngle).truncatingRemainder(dividingBy: 2 * .pi)
|
|
224
|
-
arc(center: .zero, from: exitAngle, sweep: (resume < 0 ? resume + 2 * .pi : resume) + 1.4 * .pi, radius: r)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func range(for frame: OrbitMotionFrame) -> ClosedRange<CGFloat> {
|
|
228
|
-
max(0, frame.distance - frame.tailLength)...frame.distance
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
func path(in range: ClosedRange<CGFloat>) -> Path {
|
|
232
|
-
var path = Path()
|
|
233
|
-
guard range.upperBound - range.lowerBound > 0.01 else { return path }
|
|
234
|
-
var began = false
|
|
235
|
-
for i in 1..<points.count {
|
|
236
|
-
let a = lengths[i - 1], b = lengths[i]
|
|
237
|
-
guard b > a, b >= range.lowerBound, a <= range.upperBound else { continue }
|
|
238
|
-
let lo = max(a, range.lowerBound), hi = min(b, range.upperBound)
|
|
239
|
-
func point(_ d: CGFloat) -> CGPoint {
|
|
240
|
-
let t = (d - a) / (b - a)
|
|
241
|
-
return CGPoint(x: points[i-1].x + (points[i].x-points[i-1].x)*t,
|
|
242
|
-
y: points[i-1].y + (points[i].y-points[i-1].y)*t)
|
|
243
|
-
}
|
|
244
|
-
if !began { path.move(to: point(lo)); began = true }
|
|
245
|
-
path.addLine(to: point(hi))
|
|
246
|
-
}
|
|
247
|
-
return path
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private mutating func append(_ point: CGPoint) {
|
|
251
|
-
let distance = points.last.map { hypot(point.x - $0.x, point.y - $0.y) } ?? 0
|
|
252
|
-
lengths.append(total + distance)
|
|
253
|
-
points.append(point)
|
|
254
|
-
}
|
|
255
|
-
private mutating func arc(center: CGPoint, from: Double, sweep: Double, radius: CGFloat) {
|
|
256
|
-
for i in 0...96 {
|
|
257
|
-
let a = from + sweep * Double(i) / 96
|
|
258
|
-
append(CGPoint(x: center.x + radius * cos(a), y: center.y + radius * sin(a)))
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
private mutating func curve(from: CGPoint, c1: CGPoint, c2: CGPoint, to: CGPoint) {
|
|
262
|
-
for i in 1...64 {
|
|
263
|
-
let t = CGFloat(i) / 64, u = 1 - t
|
|
264
|
-
append(CGPoint(x: u*u*u*from.x + 3*u*u*t*c1.x + 3*u*t*t*c2.x + t*t*t*to.x,
|
|
265
|
-
y: u*u*u*from.y + 3*u*u*t*c1.y + 3*u*t*t*c2.y + t*t*t*to.y))
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
struct OrbitStroke: Shape {
|
|
271
|
-
let frame: OrbitMotionFrame
|
|
272
|
-
let angle: Double
|
|
273
|
-
var gap: CGFloat = 28
|
|
274
|
-
func path(in rect: CGRect) -> Path {
|
|
275
|
-
let route = OrbitBrushRoute(failed: frame.failed, angle: angle, gap:gap)
|
|
276
|
-
let canonical=OrbitBrushRoute(failed:frame.failed,angle:angle)
|
|
277
|
-
let source=[CGFloat(0),canonical.departure,canonical.sourceExit,canonical.arrival,canonical.resultDrawn,canonical.returned,canonical.total]
|
|
278
|
-
let dest=[CGFloat(0),route.departure,route.sourceExit,route.arrival,route.resultDrawn,route.returned,route.total]
|
|
279
|
-
func map(_ d:CGFloat)->CGFloat {
|
|
280
|
-
for i in 1..<source.count where d <= source[i] {
|
|
281
|
-
let w=(d-source[i-1])/max(0.00001,source[i]-source[i-1])
|
|
282
|
-
return dest[i-1]+w*(dest[i]-dest[i-1])
|
|
283
|
-
}
|
|
284
|
-
return route.total
|
|
285
|
-
}
|
|
286
|
-
let range=canonical.range(for:frame)
|
|
287
|
-
return route.path(in: map(range.lowerBound)...map(range.upperBound)).offsetBy(dx: rect.midX, dy: rect.midY)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
struct DecisionSpin {
|
|
292
|
-
let began:Date
|
|
293
|
-
let angle:Double
|
|
294
|
-
let initialVelocity:Double
|
|
295
|
-
let finalVelocity:Double
|
|
296
|
-
var delay:Double = 0
|
|
297
|
-
static let duration=0.62
|
|
298
|
-
static let runningVelocity=2 * Double.pi/3
|
|
299
|
-
func velocity(at now:Date)->Double {
|
|
300
|
-
let u=min(1,max(0,(now.timeIntervalSince(began)-delay)/Self.duration))
|
|
301
|
-
return initialVelocity+(finalVelocity-initialVelocity)*(3*u*u-2*u*u*u)
|
|
302
|
-
}
|
|
303
|
-
func position(at now:Date)->Double {
|
|
304
|
-
let elapsed=max(0,now.timeIntervalSince(began)-delay),t=min(elapsed,Self.duration),u=t/Self.duration
|
|
305
|
-
return angle+initialVelocity*t+(finalVelocity-initialVelocity)*Self.duration*(u*u*u-0.5*u*u*u*u)+finalVelocity*max(0,elapsed-Self.duration)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
struct DecisionMorph {
|
|
309
|
-
let amount:Double
|
|
310
|
-
private var q:Double { min(1,max(0,amount)) }
|
|
311
|
-
private var p:Double { 1-q }
|
|
312
|
-
static let resumeDuration=0.98
|
|
313
|
-
private static let clearFraction=0.28/resumeDuration
|
|
314
|
-
var draw:Double {
|
|
315
|
-
let u=min(1,max(0,(p-Self.clearFraction)/(1-Self.clearFraction)))
|
|
316
|
-
let terminal=DecisionSpin.runningVelocity*(Self.resumeDuration-0.28)/(2 * Double.pi*0.70)
|
|
317
|
-
// Integrate a monotone smoothstep velocity: acceleration also reaches zero at handoff.
|
|
318
|
-
return (2-terminal)*u+(2*terminal-2)*(u*u*u-0.5*u*u*u*u)
|
|
319
|
-
}
|
|
320
|
-
var fill:Double { 1-IdleInterpolation.smooth((p-0.02)/(Self.clearFraction-0.02)) }
|
|
321
|
-
var flip:Double { 1-IdleInterpolation.smooth(p/Self.clearFraction) }
|
|
322
|
-
var trim:Double { 0.70*draw }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
struct DecisionClosing {
|
|
326
|
-
static let duration=0.82
|
|
327
|
-
let amount:Double
|
|
328
|
-
var trim:Double { 0.70+0.30*IdleInterpolation.smooth(amount/0.5) }
|
|
329
|
-
var fill:Double { IdleInterpolation.smooth((amount-0.50)/0.38) }
|
|
330
|
-
var tint:Double { IdleInterpolation.smooth((amount-0.24)/0.55) }
|
|
331
|
-
var flip:Double { IdleInterpolation.smooth((amount-0.62)/0.38) }
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/// Center visible ink, excluding font side bearings and baseline line-box padding.
|
|
335
|
-
@MainActor enum CenteredStatusGlyph {
|
|
336
|
-
private static var cache:[String:Path]=[:]
|
|
337
|
-
static func path(_ text:String)->Path {
|
|
338
|
-
if let cached=cache[text] { return cached }
|
|
339
|
-
let base=NSFont.systemFont(ofSize:10,weight:.bold)
|
|
340
|
-
let font=NSFont(descriptor:base.fontDescriptor.withDesign(.rounded) ?? base.fontDescriptor,size:10) ?? base
|
|
341
|
-
let ct=CTFontCreateWithName(font.fontName as CFString,10,nil)
|
|
342
|
-
let chars=Array(text.utf16)
|
|
343
|
-
var glyphs=[CGGlyph](repeating:0,count:chars.count)
|
|
344
|
-
CTFontGetGlyphsForCharacters(ct,chars,&glyphs,chars.count)
|
|
345
|
-
var advances=[CGSize](repeating:.zero,count:glyphs.count)
|
|
346
|
-
CTFontGetAdvancesForGlyphs(ct,.horizontal,glyphs,&advances,glyphs.count)
|
|
347
|
-
let combined=CGMutablePath();var x=0.0
|
|
348
|
-
for (i,glyph) in glyphs.enumerated() {
|
|
349
|
-
if let outline=CTFontCreatePathForGlyph(ct,glyph,nil) {
|
|
350
|
-
combined.addPath(outline,transform:CGAffineTransform(translationX:x,y:0))
|
|
351
|
-
}
|
|
352
|
-
x+=advances[i].width
|
|
353
|
-
}
|
|
354
|
-
let bounds=combined.boundingBoxOfPath
|
|
355
|
-
guard !bounds.isEmpty else { return Path() }
|
|
356
|
-
let scale=min(1,12/bounds.width)
|
|
357
|
-
let transform=CGAffineTransform(a:scale,b:0,c:0,d:-scale,tx:-bounds.midX*scale,ty:bounds.midY*scale)
|
|
358
|
-
var result=Path();result.addPath(Path(combined),transform:transform)
|
|
359
|
-
if text == "!" {
|
|
360
|
-
// The long stem carries more ink than the dot: align optical mass, not just bounds.
|
|
361
|
-
let ink=result.boundingRect,step=0.04
|
|
362
|
-
var sumY=0.0,count=0.0
|
|
363
|
-
for y in stride(from:ink.minY+step/2,to:ink.maxY,by:step) {
|
|
364
|
-
for x in stride(from:ink.minX+step/2,to:ink.maxX,by:step) {
|
|
365
|
-
if result.contains(CGPoint(x:x,y:y)) { sumY+=y;count+=1 }
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if count > 0 { result=result.applying(CGAffineTransform(translationX:0,y:-sumY/count)) }
|
|
369
|
-
}
|
|
370
|
-
cache[text]=result;return result
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/// Two halves hinge at the glyph's equator, like a split-flap calendar.
|
|
375
|
-
struct DecisionFlipGlyph:View {
|
|
376
|
-
let number:Int
|
|
377
|
-
let progress:Double
|
|
378
|
-
let color:Color
|
|
379
|
-
var body:some View {
|
|
380
|
-
let numeral=CenteredStatusGlyph.path("\(number)")
|
|
381
|
-
let mark=CenteredStatusGlyph.path("!")
|
|
382
|
-
Canvas { context,size in
|
|
383
|
-
func half(_ text:String,top:Bool,scale:Double,exposed:Double? = nil) {
|
|
384
|
-
guard scale > 0.001 else { return }
|
|
385
|
-
var c=context
|
|
386
|
-
c.translateBy(x:size.width/2,y:size.height/2)
|
|
387
|
-
if let exposed {
|
|
388
|
-
let y=top ? -size.height/2:exposed
|
|
389
|
-
let height=top ? size.height/2-exposed:size.height/2-exposed
|
|
390
|
-
c.clip(to:Path(CGRect(x:-size.width/2,y:y,width:size.width,height:max(0,height))))
|
|
391
|
-
}
|
|
392
|
-
c.scaleBy(x:1,y:scale)
|
|
393
|
-
c.clip(to:Path(CGRect(x:-size.width/2,y:top ? -size.height/2:0,width:size.width,height:size.height/2)))
|
|
394
|
-
c.fill(text == "!" ? mark:numeral,with:.color(color))
|
|
395
|
-
}
|
|
396
|
-
if progress < 0.5 {
|
|
397
|
-
let scale=max(0,cos(.pi*progress))
|
|
398
|
-
half("!",top:true,scale:1,exposed:size.height/2*scale)
|
|
399
|
-
half("\(number)",top:false,scale:1)
|
|
400
|
-
half("\(number)",top:true,scale:scale)
|
|
401
|
-
} else {
|
|
402
|
-
let scale=max(0,cos(.pi*(1-progress)))
|
|
403
|
-
half("!",top:true,scale:1)
|
|
404
|
-
half("\(number)",top:false,scale:1,exposed:size.height/2*scale)
|
|
405
|
-
half("!",top:false,scale:scale)
|
|
406
|
-
}
|
|
407
|
-
}.frame(width:19,height:19)
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
struct DecisionDissolveGlyph:View {
|
|
412
|
-
let number:Int
|
|
413
|
-
let progress:Double
|
|
414
|
-
let color:Color
|
|
415
|
-
var body:some View {
|
|
416
|
-
let numeral=CenteredStatusGlyph.path("\(number)"),mark=CenteredStatusGlyph.path("!")
|
|
417
|
-
let t=min(1,max(0,progress))
|
|
418
|
-
Canvas { context,size in
|
|
419
|
-
context.translateBy(x:size.width/2,y:size.height/2)
|
|
420
|
-
var old=context;old.opacity *= 1-t;old.fill(numeral,with:.color(color))
|
|
421
|
-
var new=context;new.opacity *= t;new.fill(mark,with:.color(color))
|
|
422
|
-
}.frame(width:19,height:19)
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/// Disappear before the two 19-point disks (plus stroke allowance) can touch.
|
|
427
|
-
struct StatusSeparation {
|
|
428
|
-
static func opacity(weight:Double,distance:Double)->Double {
|
|
429
|
-
weight*IdleInterpolation.smooth((distance-20)/8)
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
struct WorkingDecisionGlyph:View {
|
|
434
|
-
let amount:Double
|
|
435
|
-
let number:Int
|
|
436
|
-
let angle:Double
|
|
437
|
-
var closing:Bool = false
|
|
438
|
-
var body:some View {
|
|
439
|
-
let m=DecisionMorph(amount:amount)
|
|
440
|
-
let closed=DecisionClosing(amount:amount)
|
|
441
|
-
let blue=NotchTokens.deepSeekBlue
|
|
442
|
-
ZStack {
|
|
443
|
-
if closing {
|
|
444
|
-
let ink=Color(red:0.302+(0.949-0.302)*closed.tint,green:0.420+0.580*closed.tint,blue:0.996+(0.078-0.996)*closed.tint)
|
|
445
|
-
Circle().fill(ink).scaleEffect(closed.fill)
|
|
446
|
-
Circle().trim(from:0,to:closed.trim).stroke(ink,style:StrokeStyle(lineWidth:1.5,lineCap:.round)).rotationEffect(.radians(angle))
|
|
447
|
-
DecisionDissolveGlyph(number:number,progress:closed.flip,color:Color(red:(0.302+(0.949-0.302)*closed.tint)*(1-closed.fill),green:(0.420+0.580*closed.tint)*(1-closed.fill),blue:(0.996+(0.078-0.996)*closed.tint)*(1-closed.fill)))
|
|
448
|
-
} else {
|
|
449
|
-
Circle().fill(NotchTokens.amber).opacity(m.fill)
|
|
450
|
-
Circle().stroke(NotchTokens.amber,lineWidth:1.5).opacity(m.fill)
|
|
451
|
-
Circle().trim(from:0,to:m.trim)
|
|
452
|
-
.stroke(blue,style:StrokeStyle(lineWidth:1.5,lineCap:.round))
|
|
453
|
-
.opacity(min(1,m.draw/0.03)).rotationEffect(.radians(angle))
|
|
454
|
-
DecisionDissolveGlyph(number:number,progress:m.flip,color:Color(red:0.302*(1-m.fill),green:0.420*(1-m.fill),blue:0.996*(1-m.fill)))
|
|
455
|
-
}
|
|
456
|
-
}.frame(width:19,height:19)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/// The robot's final point travels to the rim, becomes the pen, then resolves text.
|
|
461
|
-
struct StatusBirth {
|
|
462
|
-
let progress:Double
|
|
463
|
-
static let blueDrawDuration=(RobotDeparture.duration-0.666)*0.76
|
|
464
|
-
static let bluePenVelocity=2 * Double.pi*0.70/blueDrawDuration
|
|
465
|
-
var blueDraw:Double { min(1,max(0,(progress-0.24)/0.76)) }
|
|
466
|
-
var travel:Double { IdleInterpolation.smooth(progress/0.24) }
|
|
467
|
-
var draw:Double { IdleInterpolation.smooth((progress-0.24)/0.48) }
|
|
468
|
-
var fill:Double { IdleInterpolation.smooth((progress-0.72)/0.18) }
|
|
469
|
-
var text:Double { IdleInterpolation.smooth((progress-0.86)/0.14) }
|
|
470
|
-
}
|
|
471
|
-
struct StatusBirthGlyph:View {
|
|
472
|
-
let progress:Double
|
|
473
|
-
let decision:Bool
|
|
474
|
-
let number:Int
|
|
475
|
-
let angle:Double
|
|
476
|
-
var body:some View {
|
|
477
|
-
let m=StatusBirth(progress:progress)
|
|
478
|
-
let ink=decision ? NotchTokens.amber:NotchTokens.deepSeekBlue
|
|
479
|
-
let drawn=decision ? m.draw:m.blueDraw
|
|
480
|
-
let length=(decision ? 1.0:0.70)*drawn
|
|
481
|
-
ZStack {
|
|
482
|
-
if decision { Circle().fill(ink).scaleEffect(m.fill) }
|
|
483
|
-
Circle().trim(from:0,to:length)
|
|
484
|
-
.stroke(ink,style:StrokeStyle(lineWidth:1.5,lineCap:.round))
|
|
485
|
-
.rotationEffect(.radians(angle)).opacity(min(1,drawn/0.02))
|
|
486
|
-
Circle().fill(ink).frame(width:1.5,height:1.5)
|
|
487
|
-
.offset(x:cos(angle)*9.5*m.travel,y:sin(angle)*9.5*m.travel)
|
|
488
|
-
.opacity(1-min(1,drawn/0.02))
|
|
489
|
-
DecisionFlipGlyph(number:number,progress:decision ? 1:0,color:decision ? Color.black:ink)
|
|
490
|
-
.opacity(decision ? m.text:m.blueDraw)
|
|
491
|
-
}.frame(width:19,height:19)
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
struct StatusOrbitView: View {
|
|
496
|
-
@ObservedObject var model: BoardModel
|
|
497
|
-
@Environment(\.accessibilityReduceMotion) private var systemReduceMotion
|
|
498
|
-
var reduceMotionOverride: Bool? = nil
|
|
499
|
-
/// Fixed presentation time for deterministic native frame capture.
|
|
500
|
-
var renderDate: Date? = nil
|
|
501
|
-
var workReveal:Double = 1
|
|
502
|
-
private var reduceMotion: Bool { reduceMotionOverride ?? systemReduceMotion }
|
|
503
|
-
|
|
504
|
-
private var layout: OrbitLayout { model.orbitLayout }
|
|
505
|
-
private var showTop: Bool { layout.top > 0.0001 }
|
|
506
|
-
private var showBottom: Bool { layout.bottom > 0.0001 }
|
|
507
|
-
private var showMiddle: Bool { layout.middle > 0.0001 }
|
|
508
|
-
private var totalHeight: CGFloat { layout.height }
|
|
509
|
-
private var originY: CGFloat { layout.middleY }
|
|
510
|
-
private var bottomY: CGFloat { layout.bottomY }
|
|
511
|
-
|
|
512
|
-
private func clearance(_ weight:Double,_ y:Double,_ present:Bool)->Double {
|
|
513
|
-
guard !present else { return weight }
|
|
514
|
-
var neighbors:[Double]=[]
|
|
515
|
-
if model.busyCount > 0 { neighbors.append(layout.middleY) }
|
|
516
|
-
if model.completedUnreadCount > 0 { neighbors.append(10+28*layout.decision) }
|
|
517
|
-
if !model.failedRows.isEmpty { neighbors.append(layout.bottomY) }
|
|
518
|
-
if model.needsAction { neighbors.append(10) }
|
|
519
|
-
guard let distance=neighbors.map({abs($0-y)}).min() else { return weight }
|
|
520
|
-
return StatusSeparation.opacity(weight:weight,distance:distance)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
var body: some View {
|
|
524
|
-
TimelineView(.animation(minimumInterval: 1.0 / 60.0, paused: reduceMotion || (model.busyCount == 0 && model.statusFlight == nil && !model.decisionSpinActive))) { context in
|
|
525
|
-
let now = renderDate ?? context.date
|
|
526
|
-
let flight = reduceMotion ? nil : model.statusFlight
|
|
527
|
-
let progress = flight.map { now.timeIntervalSince($0.startedAt) / StatusFlight.duration } ?? 1
|
|
528
|
-
let motion = flight.map { OrbitMotionFrame(progress: progress, failed: $0.failed, returns: $0.returnsToRunning, angle: $0.angle) }
|
|
529
|
-
let angle = model.decisionAngle(at:now)
|
|
530
|
-
let reply = reduceMotion ? nil:model.decisionReturn
|
|
531
|
-
let replyProgress = reply?.progress(at:now) ?? 1
|
|
532
|
-
let replyFrame = reply.map { DecisionReturnFrame(progress:replyProgress,angle:$0.angle) }
|
|
533
|
-
let localReply = reply.map { !$0.travelling } ?? false
|
|
534
|
-
let isolated = layout.top < 0.0001 && layout.bottom < 0.0001
|
|
535
|
-
let morphing = localReply || (reply == nil && flight == nil && isolated && abs(layout.middle+layout.decision-1) < 0.001)
|
|
536
|
-
ZStack(alignment: .topLeading) {
|
|
537
|
-
if morphing {
|
|
538
|
-
Group {
|
|
539
|
-
if workReveal < 1 {
|
|
540
|
-
StatusBirthGlyph(progress:workReveal,decision:model.needsAction,number:max(1,model.busyCount),angle:reduceMotion ? 0:angle)
|
|
541
|
-
} else {
|
|
542
|
-
WorkingDecisionGlyph(amount:localReply ? 1-replyProgress:layout.decision,number:max(1,max(model.busyCount,model.retainedBusyCount)),angle:reduceMotion ? 0:angle,closing:reply == nil && model.closingDecision)
|
|
543
|
-
}
|
|
544
|
-
}.position(x:15,y:10)
|
|
545
|
-
}
|
|
546
|
-
if layout.decision > 0.0001 && !morphing {
|
|
547
|
-
ZStack {
|
|
548
|
-
Circle().fill(NotchTokens.amber)
|
|
549
|
-
DecisionFlipGlyph(number:1,progress:1,color:.black)
|
|
550
|
-
}.frame(width:19,height:19)
|
|
551
|
-
.scaleEffect(workReveal)
|
|
552
|
-
.opacity(flight?.decision == true && flight?.destinationBefore == 0 ? (motion?.resultOpacity ?? 1):1)
|
|
553
|
-
.opacity(reply?.travelling == true ? (replyFrame?.fill ?? 1):clearance(layout.decision,10,model.needsAction))
|
|
554
|
-
.position(x:15,y:10)
|
|
555
|
-
}
|
|
556
|
-
if showTop {
|
|
557
|
-
let count = flight?.outcome == .success && motion?.arrived == false ? flight!.destinationBefore : max(model.completedUnreadCount,model.retainedSuccessCount)
|
|
558
|
-
statusDisk(count: count, color: NotchTokens.greenComplete, opacity: flight?.outcome == .success && flight?.destinationBefore == 0 ? (motion?.resultOpacity ?? 1) : 1)
|
|
559
|
-
.position(x: 15, y: 10+28*layout.decision)
|
|
560
|
-
.opacity(clearance(layout.top,10+28*layout.decision,model.completedUnreadCount > 0))
|
|
561
|
-
}
|
|
562
|
-
if showMiddle && !morphing {
|
|
563
|
-
let count = flight != nil && motion?.returned == false ? flight!.busyBefore : model.orbitBusyCount
|
|
564
|
-
ZStack {
|
|
565
|
-
Circle().fill(Color.black).frame(width: 15, height: 15)
|
|
566
|
-
.opacity(flight?.returnsToRunning == false ? (motion?.sourceOpacity ?? 1):1)
|
|
567
|
-
if flight == nil && model.busyCount > 0 && (reply == nil || reply!.busyBefore > 0) {
|
|
568
|
-
Circle().trim(from: 0, to: 0.70)
|
|
569
|
-
.stroke(NotchTokens.deepSeekBlue, style: StrokeStyle(lineWidth: 1.5, lineCap: .round))
|
|
570
|
-
.rotationEffect(.radians(reduceMotion ? 0 : angle))
|
|
571
|
-
.opacity(replyFrame?.baseRingOpacity ?? 1)
|
|
572
|
-
}
|
|
573
|
-
if let reply, let replyFrame {
|
|
574
|
-
DecisionFlipGlyph(number:reply.busyBefore,progress:0,color:NotchTokens.deepSeekBlue)
|
|
575
|
-
.opacity(reply.busyBefore > 0 ? 1-replyFrame.countMix:0)
|
|
576
|
-
DecisionFlipGlyph(number:model.busyCount,progress:0,color:NotchTokens.deepSeekBlue)
|
|
577
|
-
.opacity(replyFrame.countMix)
|
|
578
|
-
} else {
|
|
579
|
-
DecisionFlipGlyph(number:count,progress:0,color:NotchTokens.deepSeekBlue)
|
|
580
|
-
.opacity(count > 0 ? (motion?.sourceOpacity ?? 1) : 0)
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
.frame(width: 19, height: 19)
|
|
584
|
-
.scaleEffect(workReveal).opacity(layout.middle)
|
|
585
|
-
.position(x: 15, y: originY)
|
|
586
|
-
}
|
|
587
|
-
if showBottom {
|
|
588
|
-
let count = flight?.failed == true && motion?.arrived == false ? flight!.destinationBefore : max(model.failedRows.count,model.retainedFailureCount)
|
|
589
|
-
statusDisk(count: count, color: NotchTokens.redFail, opacity: flight?.failed == true && flight?.destinationBefore == 0 ? (motion?.resultOpacity ?? 1) : 1)
|
|
590
|
-
.position(x: 15, y: bottomY)
|
|
591
|
-
.opacity(clearance(layout.bottom,layout.bottomY,!model.failedRows.isEmpty))
|
|
592
|
-
}
|
|
593
|
-
if let reply, let frame=replyFrame, reply.travelling {
|
|
594
|
-
let yellow=StatusOutcome.decision.rgb
|
|
595
|
-
let ink=Color(red:0.302+(yellow.0-0.302)*frame.tint,
|
|
596
|
-
green:0.420+(yellow.1-0.420)*frame.tint,
|
|
597
|
-
blue:0.996+(yellow.2-0.996)*frame.tint)
|
|
598
|
-
DecisionReturnStroke(frame:frame,angle:reply.angle,gap:max(0,originY-10))
|
|
599
|
-
.stroke(ink,style:StrokeStyle(lineWidth:1.5,lineCap:.round))
|
|
600
|
-
.frame(width:30,height:20).position(x:15,y:originY)
|
|
601
|
-
.opacity(frame.strokeOpacity).allowsHitTesting(false).zIndex(-1)
|
|
602
|
-
}
|
|
603
|
-
if let flight, let motion {
|
|
604
|
-
let target = flight.outcome.rgb
|
|
605
|
-
let color = Color(red: 0.302 + (target.0 - 0.302) * motion.tint,
|
|
606
|
-
green: 0.420 + (target.1 - 0.420) * motion.tint,
|
|
607
|
-
blue: 0.996 + (target.2 - 0.996) * motion.tint)
|
|
608
|
-
// Colour belongs to the visible trail: its tail stays the result colour
|
|
609
|
-
// as the head returns towards the source rim, even behind the solid disk.
|
|
610
|
-
let resultColor = flight.outcome.color
|
|
611
|
-
let direction: CGFloat = flight.failed ? 1 : -1
|
|
612
|
-
let returnInk = LinearGradient(stops: [
|
|
613
|
-
.init(color: resultColor, location: 0),
|
|
614
|
-
.init(color: resultColor, location: 0.52),
|
|
615
|
-
.init(color: NotchTokens.deepSeekBlue, location: 1),
|
|
616
|
-
], startPoint: UnitPoint(x: 0.5, y: 0.5 + direction * 28 / 20),
|
|
617
|
-
endPoint: UnitPoint(x: 0.5, y: 0.5 + direction * 9.5 / 20))
|
|
618
|
-
let ink = motion.returning ? AnyShapeStyle(returnInk) : AnyShapeStyle(color)
|
|
619
|
-
OrbitStroke(frame: motion, angle: flight.angle, gap:flight.decision ? max(0,originY-10):28*(flight.failed ? layout.middle : layout.top))
|
|
620
|
-
.stroke(ink, style: StrokeStyle(lineWidth: 1.5, lineCap: .round))
|
|
621
|
-
.frame(width: 30, height: 20)
|
|
622
|
-
.position(x: 15, y: originY)
|
|
623
|
-
.allowsHitTesting(false)
|
|
624
|
-
.zIndex(-1)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
.frame(width: 30, height: totalHeight)
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
.accessibilityElement(children: .ignore)
|
|
631
|
-
.accessibilityLabel("运行中 \(model.busyCount),完成 \(model.completedUnreadCount),失败 \(model.failedRows.count),等待决定 \(model.needsAction ? 1:0)")
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
private func statusDisk(count: Int, color: Color, opacity: Double) -> some View {
|
|
635
|
-
ZStack {
|
|
636
|
-
Circle().fill(color)
|
|
637
|
-
DecisionFlipGlyph(number:count,progress:0,color:color == NotchTokens.greenComplete ? Color.black:Color.white)
|
|
638
|
-
}
|
|
639
|
-
.frame(width: 19, height: 19)
|
|
640
|
-
.opacity(count > 0 ? opacity : 0)
|
|
641
|
-
}
|
|
642
|
-
}
|