@swiftbrowser/native 0.26.1 → 0.26.3
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.md +7 -7
- package/package.json +3 -3
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +149 -59
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +6 -3
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +12 -1
- package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +11 -1
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +3 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +5 -2
- package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +67 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/README.md
CHANGED
|
@@ -37,10 +37,10 @@ build against the exact version of the renderer this JavaScript was written
|
|
|
37
37
|
for. `swiftbrowser native init` lays a project out that way:
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
|
-
npx @swiftbrowser/cli native init MyApp # MyApp.xcodeproj,
|
|
40
|
+
npx @swiftbrowser/cli native init MyApp # MyApp.xcodeproj (iOS and macOS), apple/, src/, web/
|
|
41
41
|
cd MyApp && npm install # React, @swiftbrowser/react, @swiftbrowser/web and this package
|
|
42
|
-
npm run build #
|
|
43
|
-
open MyApp.xcodeproj # run it on a simulator
|
|
42
|
+
npm run build # apple/app.js, the bundle the app ships
|
|
43
|
+
open MyApp.xcodeproj # run it on a simulator, an iPhone or this Mac
|
|
44
44
|
npm run dev:web # the same app as a web page
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -76,8 +76,8 @@ run(<App />);
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
```sh
|
|
79
|
-
npx swiftbrowser-native build src/index.tsx --out ../
|
|
80
|
-
npx swiftbrowser-native dev src/index.tsx --out ../
|
|
79
|
+
npx swiftbrowser-native build src/index.tsx --out ../apple/app.js # the bundle Xcode ships
|
|
80
|
+
npx swiftbrowser-native dev src/index.tsx --out ../apple/app.js # http://localhost:5173/, Fast Refresh on save
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
On the Swift side, `NativeApp().load()` runs the bundled `app.js` and
|
|
@@ -152,7 +152,7 @@ the app's scene phase here (`active`, `inactive`, `background`), as
|
|
|
152
152
|
## Workers
|
|
153
153
|
|
|
154
154
|
```sh
|
|
155
|
-
npx swiftbrowser-native build src/index.tsx --out ../
|
|
155
|
+
npx swiftbrowser-native build src/index.tsx --out ../apple/app.js --worker render=src/render.worker.ts
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
```ts
|
|
@@ -410,7 +410,7 @@ and a page can register its own element under the same name
|
|
|
410
410
|
also from @swiftbrowser/react), so the same element draws on both.
|
|
411
411
|
|
|
412
412
|
Swift that is not a view registers as a function, before the bundle loads
|
|
413
|
-
(`swiftbrowser native init` writes an `
|
|
413
|
+
(`swiftbrowser native init` writes an `apple/NativeFunctions.swift` for them):
|
|
414
414
|
|
|
415
415
|
```swift
|
|
416
416
|
NativeFunctions.register("purchase") { call in // async
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
|
|
6
6
|
"repository": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@babel/core": "^7.29.7",
|
|
66
|
-
"@swiftbrowser/react": "0.26.
|
|
67
|
-
"@swiftbrowser/web": "0.26.
|
|
66
|
+
"@swiftbrowser/react": "0.26.3",
|
|
67
|
+
"@swiftbrowser/web": "0.26.3",
|
|
68
68
|
"esbuild": "^0.28.2",
|
|
69
69
|
"react-refresh": "^0.17.0"
|
|
70
70
|
},
|
|
@@ -198,6 +198,11 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
198
198
|
/// Re-rating: per register whether it may change from frame to frame, and the sample stream's instructions kept.
|
|
199
199
|
private let changing: UnsafeMutablePointer<UInt8>
|
|
200
200
|
private let keptSample: UnsafeMutablePointer<Int32>
|
|
201
|
+
/// Per register whether it may change from frame to frame within a block, as the streams were last made: what the
|
|
202
|
+
/// sample stream (as it stays after re-rating) and the master stage write, state, the beat, a region's results and
|
|
203
|
+
/// a choice. A filter whose cutoff and resonance do not works out its coefficients once a chunk.
|
|
204
|
+
private let framewise: UnsafeMutablePointer<UInt8>
|
|
205
|
+
private let registerCount: Int
|
|
201
206
|
|
|
202
207
|
private let laneNote: UnsafeMutablePointer<Int32>
|
|
203
208
|
private let laneGate: UnsafeMutablePointer<Float>
|
|
@@ -310,6 +315,8 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
310
315
|
pinnedMask = mask()
|
|
311
316
|
routedMask = mask()
|
|
312
317
|
changing = mask()
|
|
318
|
+
framewise = mask()
|
|
319
|
+
self.registerCount = registerCount
|
|
313
320
|
for p in program.parameters { parameterMask[p.register] = 1 }
|
|
314
321
|
for r in stateRegisters { stateMask[r] = 1 }
|
|
315
322
|
for c in program.choices { renamedMask[c.register] = 1 }
|
|
@@ -505,7 +512,7 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
505
512
|
routeRegisters.deallocate()
|
|
506
513
|
routeCounts.deallocate()
|
|
507
514
|
routeState.deallocate()
|
|
508
|
-
for pointer in [parameterMask, stateMask, renamedMask, pinnedMask, routedMask, changing] { pointer.deallocate() }
|
|
515
|
+
for pointer in [parameterMask, stateMask, renamedMask, pinnedMask, routedMask, changing, framewise] { pointer.deallocate() }
|
|
509
516
|
constantValues.deallocate()
|
|
510
517
|
parameterLowest.deallocate()
|
|
511
518
|
parameterHighest.deallocate()
|
|
@@ -617,6 +624,11 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
617
624
|
sampleSerial = reschedule(sampleCount, from: keptSample, into: sampleCode)
|
|
618
625
|
masterCount = masters
|
|
619
626
|
masterSerial = reschedule(masters, from: orderedMaster, into: masterCode)
|
|
627
|
+
let stride = Int32(width)
|
|
628
|
+
for r in 0..<registerCount { framewise[r] = stateMask[r] | renamedMask[r] }
|
|
629
|
+
framewise[6] = 1
|
|
630
|
+
for i in 0..<sampleCount { framewise[Int(keptSample[i * 12 + 1] / stride)] = 1 }
|
|
631
|
+
for i in 0..<masters { framewise[Int(orderedMaster[i * 12 + 1] / stride)] = 1 }
|
|
620
632
|
return true
|
|
621
633
|
}
|
|
622
634
|
|
|
@@ -1656,17 +1668,49 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
1656
1668
|
}
|
|
1657
1669
|
}
|
|
1658
1670
|
|
|
1671
|
+
/// PolyBLEP's correction near a wrap (`t` the phase, `dt` its step; 0 for a step of 0 or less): both sides worked
|
|
1672
|
+
/// out and one chosen, with no branch, so a loop of them vectorizes.
|
|
1659
1673
|
@inline(__always)
|
|
1660
1674
|
private static func blep(_ t: Float, _ dt: Float) -> Float {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1675
|
+
let x0 = t / dt
|
|
1676
|
+
let x1 = (t - 1) / dt
|
|
1677
|
+
let rising = x0 + x0 - x0 * x0 - 1
|
|
1678
|
+
let falling = x1 * x1 + x1 + x1 + 1
|
|
1679
|
+
return dt > 0 ? (t < dt ? rising : t > 1 - dt ? falling : 0) : 0
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/// sin(2π p) for a phase `p`: folded into a quarter cycle, then Taylor's polynomial to x¹¹, in floats with no
|
|
1683
|
+
/// branch, so a loop of them vectorizes. Within 2e-7 of sin; machine.ts takes JavaScript's own.
|
|
1684
|
+
@inline(__always)
|
|
1685
|
+
static func sin2pi(_ p: Float) -> Float {
|
|
1686
|
+
let q = p - (p + 0.5).rounded(.down)
|
|
1687
|
+
let h = q > 0.25 ? 0.5 - q : q < -0.25 ? -0.5 - q : q
|
|
1688
|
+
let x = h * (2 * Float.pi)
|
|
1689
|
+
let z = x * x
|
|
1690
|
+
var s: Float = -2.5052108e-8
|
|
1691
|
+
s = s * z + 2.7557319e-6
|
|
1692
|
+
s = s * z - 1.9841270e-4
|
|
1693
|
+
s = s * z + 8.3333333e-3
|
|
1694
|
+
s = s * z - 0.16666667
|
|
1695
|
+
return x + x * z * s
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/// tan(x) for 0 … π/2 (a filter's prewarp): Cephes' tanf polynomial up to π/4, and 1 / tan(π/2 − x) past it, in
|
|
1699
|
+
/// floats with no branch, so a loop of them vectorizes. Within 1.5e-6 of tan (a cutoff 0.0002% off at the top of
|
|
1700
|
+
/// the range), where machine.ts takes JavaScript's own, faster there than any polynomial in emulated floats.
|
|
1701
|
+
@inline(__always)
|
|
1702
|
+
static func tan(_ x: Float) -> Float {
|
|
1703
|
+
let upper = x > 0.78539816
|
|
1704
|
+
let y = upper ? 1.5707964 - x : x
|
|
1705
|
+
let z = y * y
|
|
1706
|
+
var p: Float = 9.38540185543e-3
|
|
1707
|
+
p = p * z + 3.11992232697e-3
|
|
1708
|
+
p = p * z + 2.44301354525e-2
|
|
1709
|
+
p = p * z + 5.34112807005e-2
|
|
1710
|
+
p = p * z + 1.33387994085e-1
|
|
1711
|
+
p = p * z + 3.33331568548e-1
|
|
1712
|
+
let t = p * z * y + y
|
|
1713
|
+
return upper ? 1 / t : t
|
|
1670
1714
|
}
|
|
1671
1715
|
|
|
1672
1716
|
/// A table's, shape's or sample's index from its register: in range, or the first.
|
|
@@ -1851,32 +1895,63 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
1851
1895
|
}
|
|
1852
1896
|
}
|
|
1853
1897
|
case 40, 41, 42, 43, 44:
|
|
1854
|
-
// Oscillators: the output from the phase now, then the phase steps by freq / sampleRate.
|
|
1898
|
+
// Oscillators: the output from the phase now, then the phase steps by freq / sampleRate. A loop for
|
|
1899
|
+
// each, with no branch in it and its index arithmetic unchecked (an overflow check would keep it from
|
|
1900
|
+
// vectorizing), so each runs across the lanes at once.
|
|
1855
1901
|
let s = S + Int(code[at + 7])
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1902
|
+
@inline(__always) func step(_ l: Int, _ p: Float, _ dt: Float) {
|
|
1903
|
+
let next = p + dt
|
|
1904
|
+
s[l] = next >= 0 && next < 1 ? next : next - next.rounded(.down)
|
|
1905
|
+
}
|
|
1906
|
+
switch op {
|
|
1907
|
+
case 40:
|
|
1908
|
+
for fk in 0..<fc {
|
|
1909
|
+
for l in 0..<lanes {
|
|
1910
|
+
let el = fk &* V &+ l
|
|
1911
|
+
let p = s[l]
|
|
1912
|
+
d[el] = p
|
|
1913
|
+
step(l, p, a[el] / sr)
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
case 41:
|
|
1917
|
+
for fk in 0..<fc {
|
|
1918
|
+
for l in 0..<lanes {
|
|
1919
|
+
let el = fk &* V &+ l
|
|
1920
|
+
let p = s[l]
|
|
1921
|
+
d[el] = Self.sin2pi(p)
|
|
1922
|
+
step(l, p, a[el] / sr)
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
case 42:
|
|
1926
|
+
for fk in 0..<fc {
|
|
1927
|
+
for l in 0..<lanes {
|
|
1928
|
+
let el = fk &* V &+ l
|
|
1929
|
+
let p = s[l]
|
|
1930
|
+
let dt = a[el] / sr
|
|
1931
|
+
d[el] = 2 * p - 1 - Self.blep(p, dt)
|
|
1932
|
+
step(l, p, dt)
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
case 43:
|
|
1936
|
+
for fk in 0..<fc {
|
|
1937
|
+
for l in 0..<lanes {
|
|
1938
|
+
let el = fk &* V &+ l
|
|
1939
|
+
let p = s[l]
|
|
1940
|
+
let dt = a[el] / sr
|
|
1869
1941
|
let w = Swift.min(Swift.max(b[el], 0), 1)
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1942
|
+
let q = p + 1 - w
|
|
1943
|
+
d[el] = (p < w ? 1 : -1) + Self.blep(p, dt) - Self.blep(q - q.rounded(.down), dt)
|
|
1944
|
+
step(l, p, dt)
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
default:
|
|
1948
|
+
for fk in 0..<fc {
|
|
1949
|
+
for l in 0..<lanes {
|
|
1950
|
+
let el = fk &* V &+ l
|
|
1951
|
+
let p = s[l]
|
|
1952
|
+
d[el] = 1 - 4 * Swift.abs(p - 0.5)
|
|
1953
|
+
step(l, p, a[el] / sr)
|
|
1876
1954
|
}
|
|
1877
|
-
d[el] = v
|
|
1878
|
-
let next = p + dt
|
|
1879
|
-
s[l] = next >= 0 && next < 1 ? next : next - next.rounded(.down)
|
|
1880
1955
|
}
|
|
1881
1956
|
}
|
|
1882
1957
|
case 45:
|
|
@@ -1990,34 +2065,49 @@ public final class DSPMachine: @unchecked Sendable {
|
|
|
1990
2065
|
case 47, 48, 49:
|
|
1991
2066
|
// A state-variable filter (Simper's form): resonance 0 … 1.
|
|
1992
2067
|
let s = S + Int(code[at + 7])
|
|
1993
|
-
|
|
2068
|
+
// Its coefficients with the polynomial tan, in loops with no branch and their index arithmetic
|
|
2069
|
+
// unchecked, so they vectorize: once a chunk where the cutoff and resonance are set once a block, every
|
|
2070
|
+
// frame where they move (an envelope's cutoff), which costs less than keeping them for when they do not.
|
|
2071
|
+
@inline(__always) func coefficients(_ cutoffIn: Float, _ resonanceIn: Float) -> (a1: Float, a2: Float, a3: Float, k: Float) {
|
|
2072
|
+
let cutoff = Swift.min(Swift.max(cutoffIn, 10), 0.49 * sr)
|
|
2073
|
+
let resonance = Swift.min(Swift.max(resonanceIn, 0), 0.98)
|
|
2074
|
+
let g = Self.tan(Float.pi * cutoff / sr)
|
|
2075
|
+
let k = 2 - 2 * resonance
|
|
2076
|
+
let a1 = 1 / (1 + g * (g + k))
|
|
2077
|
+
return (a1, g * a1, g * g * a1, k)
|
|
2078
|
+
}
|
|
2079
|
+
@inline(__always) func step(_ l: Int, _ el: Int, _ a1: Float, _ a2: Float, _ a3: Float, _ k: Float) {
|
|
2080
|
+
let ic1 = s[l]
|
|
2081
|
+
let ic2 = s[L &+ l]
|
|
2082
|
+
let x = a[el]
|
|
2083
|
+
let v3 = x - ic2
|
|
2084
|
+
let v1 = a1 * ic1 + a2 * v3
|
|
2085
|
+
let v2 = ic2 + a2 * ic1 + a3 * v3
|
|
2086
|
+
s[l] = 2 * v1 - ic1
|
|
2087
|
+
s[L &+ l] = 2 * v2 - ic2
|
|
2088
|
+
d[el] = op == 47 ? v2 : op == 49 ? v1 : x - k * v1 - v2
|
|
2089
|
+
}
|
|
2090
|
+
let held = !serial && framewise[Int(code[at + 3]) / width] == 0 && framewise[Int(code[at + 4]) / width] == 0
|
|
2091
|
+
if held {
|
|
1994
2092
|
for l in 0..<lanes {
|
|
1995
|
-
let
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2093
|
+
let (a1, a2, a3, k) = coefficients(b[l], c[l])
|
|
2094
|
+
s[4 &* L &+ l] = a1
|
|
2095
|
+
s[5 &* L &+ l] = a2
|
|
2096
|
+
s[6 &* L &+ l] = a3
|
|
2097
|
+
s[7 &* L &+ l] = k
|
|
2098
|
+
}
|
|
2099
|
+
for fk in 0..<fc {
|
|
2100
|
+
for l in 0..<lanes {
|
|
2101
|
+
step(l, fk &* V &+ l, s[4 &* L &+ l], s[5 &* L &+ l], s[6 &* L &+ l], s[7 &* L &+ l])
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
} else {
|
|
2105
|
+
for fk in 0..<fc {
|
|
2106
|
+
for l in 0..<lanes {
|
|
2107
|
+
let el = fk &* V &+ l
|
|
2108
|
+
let (a1, a2, a3, k) = coefficients(b[el], c[el])
|
|
2109
|
+
step(l, el, a1, a2, a3, k)
|
|
2008
2110
|
}
|
|
2009
|
-
let a1 = s[4 * L + l]
|
|
2010
|
-
let a2 = s[5 * L + l]
|
|
2011
|
-
let a3 = s[6 * L + l]
|
|
2012
|
-
let ic1 = s[l]
|
|
2013
|
-
let ic2 = s[L + l]
|
|
2014
|
-
let x = a[el]
|
|
2015
|
-
let v3 = x - ic2
|
|
2016
|
-
let v1 = a1 * ic1 + a2 * v3
|
|
2017
|
-
let v2 = ic2 + a2 * ic1 + a3 * v3
|
|
2018
|
-
s[l] = 2 * v1 - ic1
|
|
2019
|
-
s[L + l] = 2 * v2 - ic2
|
|
2020
|
-
d[el] = op == 47 ? v2 : op == 49 ? v1 : x - s[7 * L + l] * v1 - v2
|
|
2021
2111
|
}
|
|
2022
2112
|
}
|
|
2023
2113
|
case 50:
|
|
@@ -37,6 +37,8 @@ public final class NativeApp {
|
|
|
37
37
|
public private(set) var colorScheme: String?
|
|
38
38
|
public private(set) var dynamicTypeSize: String?
|
|
39
39
|
public private(set) var horizontalSizeClass: String?
|
|
40
|
+
/// The platform the app runs on, `ios` or `macos` (docs/notes/macos.md), once the root view has said.
|
|
41
|
+
public private(set) var platform: String?
|
|
40
42
|
/// URLs the app was opened with before its script could take them (a
|
|
41
43
|
/// universal link that launched it): delivered once it loads.
|
|
42
44
|
private var pendingURLs: [URL] = []
|
|
@@ -312,17 +314,18 @@ public final class NativeApp {
|
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
/// Tells the app the device environment; sent once at load and on change.
|
|
315
|
-
public func setEnvironment(colorScheme: String? = nil, dynamicTypeSize: String? = nil, horizontalSizeClass: String? = nil) {
|
|
317
|
+
public func setEnvironment(colorScheme: String? = nil, dynamicTypeSize: String? = nil, horizontalSizeClass: String? = nil, platform: String? = nil) {
|
|
316
318
|
var changed = false
|
|
317
319
|
if let colorScheme, colorScheme != self.colorScheme { self.colorScheme = colorScheme; changed = true }
|
|
318
320
|
if let dynamicTypeSize, dynamicTypeSize != self.dynamicTypeSize { self.dynamicTypeSize = dynamicTypeSize; changed = true }
|
|
319
321
|
if let horizontalSizeClass, horizontalSizeClass != self.horizontalSizeClass { self.horizontalSizeClass = horizontalSizeClass; changed = true }
|
|
322
|
+
if let platform, platform != self.platform { self.platform = platform; changed = true }
|
|
320
323
|
if changed { sendEnvironment() }
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
private func sendEnvironment() {
|
|
324
|
-
guard isLoaded, colorScheme != nil || dynamicTypeSize != nil || horizontalSizeClass != nil else { return }
|
|
325
|
-
dispatch(.environment(colorScheme: colorScheme, dynamicTypeSize: dynamicTypeSize, horizontalSizeClass: horizontalSizeClass))
|
|
327
|
+
guard isLoaded, colorScheme != nil || dynamicTypeSize != nil || horizontalSizeClass != nil || platform != nil else { return }
|
|
328
|
+
dispatch(.environment(colorScheme: colorScheme, dynamicTypeSize: dynamicTypeSize, horizontalSizeClass: horizontalSizeClass, platform: platform))
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
/// The scene moved between the foreground, inactive and the background.
|
|
@@ -79,7 +79,9 @@ extension AnyShape {
|
|
|
79
79
|
case "ellipse": self = AnyShape(Ellipse())
|
|
80
80
|
case "roundedRectangle", "containerRelative": self = AnyShape(RoundedRectangle(cornerRadius: spec.cornerRadius, style: .continuous))
|
|
81
81
|
case "path":
|
|
82
|
-
if let
|
|
82
|
+
if let affine = spec.affinePath {
|
|
83
|
+
self = AnyShape(AffinePathShape(affine: affine))
|
|
84
|
+
} else if let data = spec.path, !data.isEmpty, let parsed = Path(svg: data) {
|
|
83
85
|
self = AnyShape(PathShape(path: parsed, size: spec.size.map { CGSize(width: $0.width, height: $0.height) }))
|
|
84
86
|
} else {
|
|
85
87
|
// No path yet (the size is not reported), or none to read: nothing to draw.
|
|
@@ -117,6 +119,15 @@ struct NoPathShape: Shape {
|
|
|
117
119
|
func path(in rect: CGRect) -> Path { Path() }
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
/// A custom shape whose path is drawn for the rectangle it is laid out in,
|
|
123
|
+
/// from its first layout (`AffinePath`).
|
|
124
|
+
struct AffinePathShape: Shape {
|
|
125
|
+
let affine: AffinePath
|
|
126
|
+
func path(in rect: CGRect) -> Path {
|
|
127
|
+
(Path(svg: affine.data(width: rect.width, height: rect.height)) ?? Path()).offsetBy(dx: rect.minX, dy: rect.minY)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
struct PathShape: Shape {
|
|
121
132
|
let path: Path
|
|
122
133
|
let size: CGSize?
|
|
@@ -84,9 +84,19 @@ public struct NativeRootView: View {
|
|
|
84
84
|
app.setEnvironment(
|
|
85
85
|
colorScheme: colorScheme == .dark ? "dark" : "light",
|
|
86
86
|
dynamicTypeSize: dynamicTypeSize.protocolName,
|
|
87
|
-
horizontalSizeClass: sizeClassName
|
|
87
|
+
horizontalSizeClass: sizeClassName,
|
|
88
|
+
platform: Self.platformName
|
|
88
89
|
)
|
|
89
90
|
}
|
|
91
|
+
|
|
92
|
+
/// The platform the app is drawn on (docs/notes/macos.md): the OS, as the protocol names it.
|
|
93
|
+
static var platformName: String {
|
|
94
|
+
#if os(macOS)
|
|
95
|
+
"macos"
|
|
96
|
+
#else
|
|
97
|
+
"ios"
|
|
98
|
+
#endif
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/// Presents the first of `presented` over `content` (a sheet, a full-screen
|
|
@@ -376,6 +376,8 @@ public struct ShapeProps: Equatable, Sendable {
|
|
|
376
376
|
public var path: String?
|
|
377
377
|
/// The size `path` was drawn for.
|
|
378
378
|
public var size: SizeSpec?
|
|
379
|
+
/// A path for any size, in place of `path` and `size`.
|
|
380
|
+
public var affinePath: AffinePath?
|
|
379
381
|
public init(props: JSONObject) {
|
|
380
382
|
shape = props.string("shape") ?? "rectangle"
|
|
381
383
|
cornerRadius = props.double("cornerRadius") ?? 0
|
|
@@ -386,6 +388,7 @@ public struct ShapeProps: Equatable, Sendable {
|
|
|
386
388
|
trimTo = props.object("trim")?.double("to")
|
|
387
389
|
path = props.string("path")
|
|
388
390
|
size = SizeSpec(json: props.value("size"))
|
|
391
|
+
affinePath = AffinePath(json: props.value("affinePath"))
|
|
389
392
|
}
|
|
390
393
|
}
|
|
391
394
|
|
|
@@ -163,18 +163,21 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
163
163
|
with("registered", id: id, ["name": .string(name), "value": value])
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/// The device appearance, text size
|
|
166
|
+
/// The device appearance, text size, size class or platform (`ios` or
|
|
167
|
+
/// `macos`, docs/notes/macos.md; sent once at launch and on change).
|
|
167
168
|
public static func environment(
|
|
168
169
|
colorScheme: String? = nil,
|
|
169
170
|
dynamicTypeSize: String? = nil,
|
|
170
171
|
horizontalSizeClass: String? = nil,
|
|
171
|
-
scenePhase: String? = nil
|
|
172
|
+
scenePhase: String? = nil,
|
|
173
|
+
platform: String? = nil
|
|
172
174
|
) -> RendererEvent {
|
|
173
175
|
var fields: JSONObject = [:]
|
|
174
176
|
if let colorScheme { fields["colorScheme"] = .string(colorScheme) }
|
|
175
177
|
if let dynamicTypeSize { fields["dynamicTypeSize"] = .string(dynamicTypeSize) }
|
|
176
178
|
if let horizontalSizeClass { fields["horizontalSizeClass"] = .string(horizontalSizeClass) }
|
|
177
179
|
if let scenePhase { fields["scenePhase"] = .string(scenePhase) }
|
|
180
|
+
if let platform { fields["platform"] = .string(platform) }
|
|
178
181
|
return RendererEvent(type: "environment", fields: fields)
|
|
179
182
|
}
|
|
180
183
|
|
|
@@ -258,3 +258,70 @@ private struct PathScanner {
|
|
|
258
258
|
return byte == UInt8(ascii: "1")
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
/// A custom shape's path for any size (`affinePath`): SVG path data whose
|
|
263
|
+
/// numbers are their values for an empty rectangle, the `i`th growing
|
|
264
|
+
/// `width[i]` a point of the shape's width and `height[i]` a point of its
|
|
265
|
+
/// height. @swiftbrowser/react sends one for a path whose every number is
|
|
266
|
+
/// such a function of the size, so the shape is drawn at the size it is laid
|
|
267
|
+
/// out at with no size reported.
|
|
268
|
+
public struct AffinePath: Equatable, Sendable {
|
|
269
|
+
public var path: String
|
|
270
|
+
public var width: [Double]
|
|
271
|
+
public var height: [Double]
|
|
272
|
+
|
|
273
|
+
public init(path: String, width: [Double], height: [Double]) {
|
|
274
|
+
self.path = path
|
|
275
|
+
self.width = width
|
|
276
|
+
self.height = height
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public init?(json: JSONValue) {
|
|
280
|
+
guard let path = json["path"].string else { return nil }
|
|
281
|
+
self.path = path
|
|
282
|
+
width = (json["width"].array ?? []).map { $0.double ?? 0 }
|
|
283
|
+
height = (json["height"].array ?? []).map { $0.double ?? 0 }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// The path data for a `width` by `height` rectangle, its points rounded
|
|
287
|
+
/// to hundredths as @swiftbrowser/react's `Path` rounds them.
|
|
288
|
+
public func data(width w: Double, height h: Double) -> String {
|
|
289
|
+
let bytes = Array(path.utf8)
|
|
290
|
+
var out = ""
|
|
291
|
+
out.reserveCapacity(bytes.count + bytes.count / 4)
|
|
292
|
+
var index = 0
|
|
293
|
+
var at = 0
|
|
294
|
+
func isDigit(_ i: Int) -> Bool { i < bytes.count && bytes[i] >= 0x30 && bytes[i] <= 0x39 }
|
|
295
|
+
while at < bytes.count {
|
|
296
|
+
let byte = bytes[at]
|
|
297
|
+
// A number: an optional minus, digits with a point among or before them, an exponent.
|
|
298
|
+
let starts = isDigit(at) || (byte == 0x2E && isDigit(at + 1)) || (byte == 0x2D && (isDigit(at + 1) || (at + 2 < bytes.count && bytes[at + 1] == 0x2E && isDigit(at + 2))))
|
|
299
|
+
guard starts else {
|
|
300
|
+
out.unicodeScalars.append(Unicode.Scalar(byte))
|
|
301
|
+
at += 1
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
var end = at + (byte == 0x2D ? 1 : 0)
|
|
305
|
+
while isDigit(end) { end += 1 }
|
|
306
|
+
if end < bytes.count && bytes[end] == 0x2E {
|
|
307
|
+
end += 1
|
|
308
|
+
while isDigit(end) { end += 1 }
|
|
309
|
+
}
|
|
310
|
+
if end < bytes.count && (bytes[end] == 0x65 || bytes[end] == 0x45) {
|
|
311
|
+
var exponent = end + 1
|
|
312
|
+
if exponent < bytes.count && (bytes[exponent] == 0x2B || bytes[exponent] == 0x2D) { exponent += 1 }
|
|
313
|
+
if isDigit(exponent) {
|
|
314
|
+
end = exponent
|
|
315
|
+
while isDigit(end) { end += 1 }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let value = (Double(String(decoding: bytes[at..<end], as: UTF8.self)) ?? 0)
|
|
319
|
+
+ w * (index < width.count ? width[index] : 0) + h * (index < height.count ? height[index] : 0)
|
|
320
|
+
let rounded = (value * 100).rounded() / 100
|
|
321
|
+
out += rounded == 0 ? "0" : rounded == rounded.rounded() && Swift.abs(rounded) < 1e15 ? String(Int(rounded)) : String(rounded)
|
|
322
|
+
index += 1
|
|
323
|
+
at = end
|
|
324
|
+
}
|
|
325
|
+
return out
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/SwiftBrowserNative.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 50282598 bytes
|
|
4
|
+
sha256: 36221c0c44b7bb993ff03090d3f241303c9b224b90b1b1dc398137d25926ba14
|