@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 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, ios/, src/, web/
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 # ios/app.js, the bundle the app ships
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 ../ios/app.js # the bundle Xcode ships
80
- npx swiftbrowser-native dev src/index.tsx --out ../ios/app.js # http://localhost:5173/, Fast Refresh on save
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 ../ios/app.js --worker render=src/render.worker.ts
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 `ios/NativeFunctions.swift` for them):
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.1",
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.1",
67
- "@swiftbrowser/web": "0.26.1",
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
- if t < dt {
1662
- let x = t / dt
1663
- return x + x - x * x - 1
1664
- }
1665
- if t > 1 - dt {
1666
- let x = (t - 1) / dt
1667
- return x * x + x + x + 1
1668
- }
1669
- return 0
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
- for fk in 0..<fc {
1857
- for l in 0..<lanes {
1858
- let el = fk * V + l
1859
- let p = s[l]
1860
- let dt = a[el] / sr
1861
- var v: Float
1862
- switch op {
1863
- case 40: v = p
1864
- case 41: v = Foundation.sin(2 * Float.pi * p)
1865
- case 42:
1866
- v = 2 * p - 1
1867
- if dt > 0 { v -= Self.blep(p, dt) }
1868
- case 43:
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
- v = p < w ? 1 : -1
1871
- if dt > 0 {
1872
- let q = p + 1 - w
1873
- v += Self.blep(p, dt) - Self.blep(q - q.rounded(.down), dt)
1874
- }
1875
- default: v = 1 - 4 * Swift.abs(p - 0.5)
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
- for fk in 0..<fc {
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 el = fk * V + l
1996
- let cutoff = Swift.min(Swift.max(b[el], 10), 0.49 * sr)
1997
- let resonance = Swift.min(Swift.max(c[el], 0), 0.98)
1998
- if !(s[2 * L + l] == cutoff && s[3 * L + l] == resonance) {
1999
- s[2 * L + l] = cutoff
2000
- s[3 * L + l] = resonance
2001
- let g = Foundation.tan(Float.pi * cutoff / sr)
2002
- let k = 2 - 2 * resonance
2003
- let a1 = 1 / (1 + g * (g + k))
2004
- s[4 * L + l] = a1
2005
- s[5 * L + l] = g * a1
2006
- s[6 * L + l] = g * g * a1
2007
- s[7 * L + l] = k
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 data = spec.path, !data.isEmpty, let parsed = Path(svg: data) {
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 or size class (sent once at launch and on change).
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: 50265117 bytes
4
- sha256: f7cb92276a436e8c765a5eca556c7436d02752c411256b0d6ff4e2ca33950635
3
+ size: 50282598 bytes
4
+ sha256: 36221c0c44b7bb993ff03090d3f241303c9b224b90b1b1dc398137d25926ba14