@react-native-gpui/svg 0.0.0-stage → 15.15.0-alpha.1

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.
@@ -0,0 +1,477 @@
1
+ //! Turning a node into the image it is drawn as, and when to do it.
2
+ //!
3
+ //! A drawing is found by its [`Key`] in the shared cache, which builds no
4
+ //! document. Only a miss builds the four documents (one per coverage sample) and
5
+ //! rasterises them. A small image with nothing already on screen for its node is
6
+ //! drawn right away, so a row that scrolls in has its icon on the first frame.
7
+ //! Anything else is drawn on a background thread while the node keeps showing
8
+ //! its previous image, if it has one, and is built again when the new one is
9
+ //! ready.
10
+
11
+ use crate::cache::{self, Cache};
12
+ use crate::key::{node_key, Key};
13
+ use crate::{svg_document, Sample, SAMPLE_OFFSETS};
14
+ use gpui::{App, Window};
15
+ use react_native_gpui::extensions::{rendering_node, ComponentNode, NodeHandle};
16
+ use rn_gpui_sys::ffi::RectF;
17
+ use std::collections::{HashMap, HashSet};
18
+ use std::sync::{Arc, Mutex, OnceLock};
19
+ use std::time::Instant;
20
+
21
+ /// Images up to this many device pixels are drawn on the main thread when their
22
+ /// node has nothing to show yet: about 48 CSS pixels square at a scale of 2.
23
+ const SYNC_MAX_PIXELS: i64 = 96 * 96;
24
+
25
+ /// Below this many device pixels the four samples are drawn one after the other:
26
+ /// each takes less time than starting a thread for it.
27
+ const PARALLEL_MIN_PIXELS: i64 = 128 * 128;
28
+
29
+ /// `RN_GPUI_SVG_TRACE=1` logs every drawing that is rasterised: its size, how
30
+ /// long each coverage sample took and how long they took together. With
31
+ /// `RN_GPUI_SVG_SYNC=1` every drawing is rasterised on the main thread.
32
+ fn setting(name: &str) -> bool {
33
+ std::env::var(name).is_ok_and(|value| value == "1")
34
+ }
35
+
36
+ fn trace() -> bool {
37
+ static TRACE: OnceLock<bool> = OnceLock::new();
38
+ *TRACE.get_or_init(|| setting("RN_GPUI_SVG_TRACE"))
39
+ }
40
+
41
+ fn force_sync() -> bool {
42
+ static SYNC: OnceLock<bool> = OnceLock::new();
43
+ *SYNC.get_or_init(|| setting("RN_GPUI_SVG_SYNC"))
44
+ }
45
+
46
+ /// The image each node showed last, so a node whose drawing changes keeps
47
+ /// showing it until the new one is ready.
48
+ fn shown() -> &'static Mutex<HashMap<i32, Arc<gpui::RenderImage>>> {
49
+ static SHOWN: OnceLock<Mutex<HashMap<i32, Arc<gpui::RenderImage>>>> = OnceLock::new();
50
+ SHOWN.get_or_init(|| Mutex::new(HashMap::new()))
51
+ }
52
+
53
+ /// Drawings being rasterised, with the nodes waiting for each.
54
+ fn pending() -> &'static Mutex<HashMap<Key, Vec<NodeHandle>>> {
55
+ static PENDING: OnceLock<Mutex<HashMap<Key, Vec<NodeHandle>>>> = OnceLock::new();
56
+ PENDING.get_or_init(|| Mutex::new(HashMap::new()))
57
+ }
58
+
59
+ /// Drawings the renderer could not rasterise, which are not tried again.
60
+ fn failed() -> &'static Mutex<HashSet<Key>> {
61
+ static FAILED: OnceLock<Mutex<HashSet<Key>>> = OnceLock::new();
62
+ FAILED.get_or_init(|| Mutex::new(HashSet::new()))
63
+ }
64
+
65
+ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
66
+ mutex
67
+ .lock()
68
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
69
+ }
70
+
71
+ fn has_filter(node: &ComponentNode) -> bool {
72
+ node.component == "RNSVGFilter" || node.children.iter().any(has_filter)
73
+ }
74
+
75
+ /// The image in the cache for `key`, or the result of `make`, which is stored.
76
+ pub fn get_or_make(
77
+ cache: &Mutex<Cache>,
78
+ key: Key,
79
+ make: impl FnOnce() -> Option<Arc<gpui::RenderImage>>,
80
+ ) -> Option<Arc<gpui::RenderImage>> {
81
+ if let Some(image) = lock(cache).get(&key) {
82
+ return Some(image);
83
+ }
84
+ let image = make()?;
85
+ lock(cache).insert(key, image.clone());
86
+ Some(image)
87
+ }
88
+
89
+ /// The image `node` is drawn as now, or what it shows meanwhile.
90
+ pub fn image_for(
91
+ node: &ComponentNode,
92
+ frame: RectF,
93
+ scale: f32,
94
+ window: &mut Window,
95
+ cx: &mut App,
96
+ ) -> Option<Arc<gpui::RenderImage>> {
97
+ let key = node_key(node, frame, scale);
98
+ let remember = |image: &Arc<gpui::RenderImage>| {
99
+ lock(shown()).insert(node.tag, image.clone());
100
+ };
101
+ if let Some(image) = lock(cache::global()).get(&key) {
102
+ remember(&image);
103
+ return Some(image);
104
+ }
105
+ let previous = lock(shown()).get(&node.tag).cloned();
106
+ if lock(failed()).contains(&key) {
107
+ return previous;
108
+ }
109
+ let pixels = (frame.w * scale).round() as i64 * (frame.h * scale).round() as i64;
110
+ let small = pixels <= SYNC_MAX_PIXELS && !has_filter(node);
111
+ if force_sync() || (previous.is_none() && small) {
112
+ let renderer = cx.svg_renderer();
113
+ let image = get_or_make(cache::global(), key, || {
114
+ sampled_image(&renderer, node, frame, scale)
115
+ });
116
+ match &image {
117
+ Some(image) => remember(image),
118
+ None => {
119
+ lock(failed()).insert(key);
120
+ }
121
+ }
122
+ return image;
123
+ }
124
+ request(key, node.clone(), frame, scale, window, cx);
125
+ previous
126
+ }
127
+
128
+ /// Rasterises `node` on a background thread, once for every node that asks for
129
+ /// the same drawing, and builds the nodes again when it is done.
130
+ fn request(
131
+ key: Key,
132
+ node: ComponentNode,
133
+ frame: RectF,
134
+ scale: f32,
135
+ window: &mut Window,
136
+ cx: &mut App,
137
+ ) {
138
+ let Some(handle) = rendering_node() else {
139
+ return;
140
+ };
141
+ {
142
+ let mut pending = lock(pending());
143
+ if let Some(waiting) = pending.get_mut(&key) {
144
+ waiting.push(handle);
145
+ return;
146
+ }
147
+ pending.insert(key, vec![handle]);
148
+ }
149
+ let renderer = cx.svg_renderer();
150
+ let work = cx
151
+ .background_executor()
152
+ .spawn(async move { sampled_image(&renderer, &node, frame, scale) });
153
+ window
154
+ .spawn(cx, async move |cx| {
155
+ let image = work.await;
156
+ match &image {
157
+ Some(image) => lock(cache::global()).insert(key, image.clone()),
158
+ None => {
159
+ lock(failed()).insert(key);
160
+ }
161
+ }
162
+ let waiting = lock(pending()).remove(&key).unwrap_or_default();
163
+ let _ = cx.update(|_, cx| {
164
+ for handle in &waiting {
165
+ handle.notify(cx);
166
+ }
167
+ });
168
+ })
169
+ .detach();
170
+ }
171
+
172
+ /// Draws the document at the window's scale, once per coverage sample without
173
+ /// antialiasing, and averages the samples. The samples are rasterised side by
174
+ /// side and combined in a fixed order, so the pixels do not depend on timing.
175
+ pub fn sampled_image(
176
+ renderer: &gpui::SvgRenderer,
177
+ node: &ComponentNode,
178
+ frame: RectF,
179
+ scale: f32,
180
+ ) -> Option<Arc<gpui::RenderImage>> {
181
+ let width = (frame.w * scale).round() as i32;
182
+ let height = (frame.h * scale).round() as i32;
183
+ if width < 1 || height < 1 {
184
+ return None;
185
+ }
186
+ let size = gpui::size(gpui::DevicePixels(width), gpui::DevicePixels(height));
187
+ let started = Instant::now();
188
+ let render = |(dx, dy): (f32, f32)| -> Option<(Vec<u8>, f64)> {
189
+ let sample_started = Instant::now();
190
+ let document = svg_document(node, frame, Some(Sample { dx, dy, scale }));
191
+ let parsed = renderer.parse_svg(document.as_bytes()).ok()?;
192
+ let rendered = renderer
193
+ .render_parsed(&parsed, gpui::SvgSize::ExactSize(size))
194
+ .ok()?;
195
+ rendered.as_bytes(0).map(|bytes| {
196
+ (
197
+ bytes.to_vec(),
198
+ sample_started.elapsed().as_secs_f64() * 1000.0,
199
+ )
200
+ })
201
+ };
202
+ let samples: Vec<Option<(Vec<u8>, f64)>> =
203
+ if i64::from(width) * i64::from(height) < PARALLEL_MIN_PIXELS {
204
+ SAMPLE_OFFSETS
205
+ .iter()
206
+ .map(|offset| render(*offset))
207
+ .collect()
208
+ } else {
209
+ std::thread::scope(|scope| {
210
+ let workers: Vec<_> = SAMPLE_OFFSETS
211
+ .iter()
212
+ .map(|offset| scope.spawn(move || render(*offset)))
213
+ .collect();
214
+ workers
215
+ .into_iter()
216
+ .map(|worker| worker.join().ok().flatten())
217
+ .collect()
218
+ })
219
+ };
220
+ if trace() {
221
+ let times: Vec<String> = samples
222
+ .iter()
223
+ .map(|sample| {
224
+ sample
225
+ .as_ref()
226
+ .map_or("failed".into(), |(_, ms)| format!("{ms:.1}"))
227
+ })
228
+ .collect();
229
+ eprintln!(
230
+ "[svg] {width}x{height} samples_ms=[{}] wall_ms={:.1}",
231
+ times.join(", "),
232
+ started.elapsed().as_secs_f64() * 1000.0
233
+ );
234
+ }
235
+ let samples: Vec<Option<Vec<u8>>> = samples
236
+ .into_iter()
237
+ .map(|sample| sample.map(|(bytes, _)| bytes))
238
+ .collect();
239
+ average(&samples, width as u32, height as u32)
240
+ }
241
+
242
+ /// Averages the samples: BGRA with straight alpha, colour weighted by alpha.
243
+ fn average(samples: &[Option<Vec<u8>>], width: u32, height: u32) -> Option<Arc<gpui::RenderImage>> {
244
+ let length = width as usize * height as usize * 4;
245
+ let mut sum = vec![0f32; length];
246
+ for bytes in samples {
247
+ let bytes = bytes.as_ref()?;
248
+ if bytes.len() != length {
249
+ return None;
250
+ }
251
+ for (total, pixel) in sum.chunks_exact_mut(4).zip(bytes.chunks_exact(4)) {
252
+ let alpha = f32::from(pixel[3]);
253
+ total[0] += f32::from(pixel[0]) * alpha;
254
+ total[1] += f32::from(pixel[1]) * alpha;
255
+ total[2] += f32::from(pixel[2]) * alpha;
256
+ total[3] += alpha;
257
+ }
258
+ }
259
+ let count = samples.len() as f32;
260
+ let mut out = Vec::with_capacity(sum.len());
261
+ for total in sum.chunks_exact(4) {
262
+ if total[3] <= 0.0 {
263
+ out.extend_from_slice(&[0, 0, 0, 0]);
264
+ continue;
265
+ }
266
+ let blue = (total[0] / total[3]).round().clamp(0.0, 255.0);
267
+ let green = (total[1] / total[3]).round().clamp(0.0, 255.0);
268
+ let red = (total[2] / total[3]).round().clamp(0.0, 255.0);
269
+ // Half coverage is 127.5 of 255, which the alpha channel cannot hold.
270
+ // Chrome blends the exact half, and the result then rounds away from
271
+ // the ink's own side: toward light under dark ink and toward dark
272
+ // under light ink, so the alpha rounds the other way to match.
273
+ let alpha = total[3] / count;
274
+ let alpha = if (alpha.fract() - 0.5).abs() < 1e-3 {
275
+ let luma = 0.299 * red + 0.587 * green + 0.114 * blue;
276
+ if luma < 128.0 {
277
+ alpha.floor()
278
+ } else {
279
+ alpha.ceil()
280
+ }
281
+ } else {
282
+ alpha.round()
283
+ };
284
+ out.extend_from_slice(&[
285
+ blue as u8,
286
+ green as u8,
287
+ red as u8,
288
+ alpha.clamp(0.0, 255.0) as u8,
289
+ ]);
290
+ }
291
+ let buffer = image::ImageBuffer::from_raw(width, height, out)?;
292
+ Some(Arc::new(gpui::RenderImage::new(
293
+ smallvec::SmallVec::from_const([image::Frame::new(buffer)]),
294
+ )))
295
+ }
296
+
297
+ #[cfg(test)]
298
+ mod tests {
299
+ use super::*;
300
+ use std::sync::atomic::{AtomicUsize, Ordering};
301
+
302
+ fn image() -> Arc<gpui::RenderImage> {
303
+ let buffer = image::ImageBuffer::from_raw(2, 2, vec![0u8; 16]).expect("a buffer");
304
+ Arc::new(gpui::RenderImage::new(smallvec::SmallVec::from_const([
305
+ image::Frame::new(buffer),
306
+ ])))
307
+ }
308
+
309
+ #[test]
310
+ fn a_hit_makes_nothing_and_a_miss_makes_once() {
311
+ let cache = Mutex::new(Cache::new(1 << 20));
312
+ let made = AtomicUsize::new(0);
313
+ let make = || {
314
+ made.fetch_add(1, Ordering::SeqCst);
315
+ Some(image())
316
+ };
317
+ let key = crate::key::test_key(1);
318
+ let first = get_or_make(&cache, key, make).unwrap();
319
+ assert_eq!(made.load(Ordering::SeqCst), 1);
320
+ // The second lookup is a hit: the closure, which is where a document
321
+ // string would be built, is not called.
322
+ let second = get_or_make(&cache, key, || panic!("a hit must not build anything")).unwrap();
323
+ assert!(Arc::ptr_eq(&first, &second));
324
+ assert_eq!(made.load(Ordering::SeqCst), 1);
325
+ }
326
+
327
+ #[test]
328
+ fn a_drawing_that_cannot_be_made_is_not_cached() {
329
+ let cache = Mutex::new(Cache::new(1 << 20));
330
+ let key = crate::key::test_key(2);
331
+ assert!(get_or_make(&cache, key, || None).is_none());
332
+ assert_eq!(lock(&cache).len(), 0);
333
+ }
334
+
335
+ #[test]
336
+ fn averaging_four_equal_samples_gives_the_sample() {
337
+ let pixels = vec![10u8, 20, 30, 255, 0, 0, 0, 0];
338
+ let samples = vec![
339
+ Some(pixels.clone()),
340
+ Some(pixels.clone()),
341
+ Some(pixels.clone()),
342
+ Some(pixels),
343
+ ];
344
+ let averaged = average(&samples, 2, 1).unwrap();
345
+ assert_eq!(
346
+ averaged.as_bytes(0).unwrap(),
347
+ &[10, 20, 30, 255, 0, 0, 0, 0]
348
+ );
349
+ }
350
+
351
+ #[test]
352
+ fn a_missing_sample_gives_no_image() {
353
+ assert!(average(&[Some(vec![0; 4]), None], 1, 1).is_none());
354
+ }
355
+
356
+ fn node(
357
+ component: &str,
358
+ props: serde_json::Value,
359
+ children: Vec<ComponentNode>,
360
+ ) -> ComponentNode {
361
+ ComponentNode {
362
+ component: component.into(),
363
+ tag: 1,
364
+ frame: RectF {
365
+ x: 0.0,
366
+ y: 0.0,
367
+ w: 24.0,
368
+ h: 24.0,
369
+ },
370
+ scale: (1.0, 1.0),
371
+ props: Arc::new(props),
372
+ parent: None,
373
+ children,
374
+ }
375
+ }
376
+
377
+ fn drawings() -> Vec<ComponentNode> {
378
+ use serde_json::json;
379
+ let fill = json!({"type": 0, "payload": 0xff336699_u32});
380
+ let shape = |component: &str, props: serde_json::Value| {
381
+ let mut full = json!({"fill": fill, "propList": ["fill"]});
382
+ full.as_object_mut()
383
+ .unwrap()
384
+ .extend(props.as_object().unwrap().clone());
385
+ node(component, full, vec![])
386
+ };
387
+ let root = |children| {
388
+ node(
389
+ "RNSVGSvgView",
390
+ json!({"vbWidth": 24, "vbHeight": 24, "align": "xMidYMid", "meetOrSlice": 0}),
391
+ children,
392
+ )
393
+ };
394
+ let blurred = node(
395
+ "RNSVGGroup",
396
+ json!({"filter": "f", "propList": []}),
397
+ vec![shape(
398
+ "RNSVGRect",
399
+ json!({"x": 6, "y": 6, "width": 12, "height": 12}),
400
+ )],
401
+ );
402
+ let filter = node(
403
+ "RNSVGDefs",
404
+ json!({}),
405
+ vec![node(
406
+ "RNSVGFilter",
407
+ json!({"name": "f", "x": 0, "y": 0, "width": 24, "height": 24, "filterUnits": "userSpaceOnUse", "primitiveUnits": "userSpaceOnUse"}),
408
+ vec![node(
409
+ "RNSVGFeGaussianBlur",
410
+ json!({"stdDeviationX": 2, "stdDeviationY": 2}),
411
+ vec![],
412
+ )],
413
+ )],
414
+ );
415
+ vec![
416
+ root(vec![shape("RNSVGPath", json!({"d": "M2 2L22 2L12 22Z"}))]),
417
+ root(vec![shape(
418
+ "RNSVGCircle",
419
+ json!({"cx": 12, "cy": 12, "r": 7.3}),
420
+ )]),
421
+ root(vec![shape(
422
+ "RNSVGRect",
423
+ json!({"x": 3.3, "y": 4.1, "width": 15, "height": 9, "rx": 3, "matrix": [0.94, -0.34, 0.34, 0.94, 0, 0]}),
424
+ )]),
425
+ root(vec![filter, blurred]),
426
+ ]
427
+ }
428
+
429
+ /// The four samples one after the other, as the module drew them before.
430
+ fn sequential(
431
+ renderer: &gpui::SvgRenderer,
432
+ node: &ComponentNode,
433
+ frame: RectF,
434
+ scale: f32,
435
+ ) -> Option<Arc<gpui::RenderImage>> {
436
+ let width = (frame.w * scale).round() as i32;
437
+ let height = (frame.h * scale).round() as i32;
438
+ let size = gpui::size(gpui::DevicePixels(width), gpui::DevicePixels(height));
439
+ let samples: Vec<Option<Vec<u8>>> = SAMPLE_OFFSETS
440
+ .iter()
441
+ .map(|&(dx, dy)| {
442
+ let document = svg_document(node, frame, Some(Sample { dx, dy, scale }));
443
+ let parsed = renderer.parse_svg(document.as_bytes()).ok()?;
444
+ let rendered = renderer
445
+ .render_parsed(&parsed, gpui::SvgSize::ExactSize(size))
446
+ .ok()?;
447
+ rendered.as_bytes(0).map(<[u8]>::to_vec)
448
+ })
449
+ .collect();
450
+ average(&samples, width as u32, height as u32)
451
+ }
452
+
453
+ #[test]
454
+ fn rasterising_the_samples_side_by_side_gives_the_same_pixels() {
455
+ let renderer = gpui::SvgRenderer::new(Arc::new(()));
456
+ // 24 and 48 point frames are drawn in turn, and 80 at a scale of 2 is
457
+ // large enough to be drawn in parallel.
458
+ for (index, drawing) in drawings().iter().enumerate() {
459
+ for (side, scale) in [(24.0, 1.0), (24.0, 2.0), (80.0, 2.0)] {
460
+ let frame = RectF {
461
+ x: 0.0,
462
+ y: 0.0,
463
+ w: side,
464
+ h: side,
465
+ };
466
+ let together = sampled_image(&renderer, drawing, frame, scale).expect("drawn");
467
+ let apart = sequential(&renderer, drawing, frame, scale).expect("drawn");
468
+ assert_eq!(
469
+ together.as_bytes(0).unwrap(),
470
+ apart.as_bytes(0).unwrap(),
471
+ "drawing {index} at scale {scale}"
472
+ );
473
+ assert!(together.as_bytes(0).unwrap().iter().any(|byte| *byte != 0));
474
+ }
475
+ }
476
+ }
477
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@react-native-gpui/svg",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "15.15.0-alpha.1",
4
+ "description": "Adapter for react-native-svg (MIT): draws SVG elements with the desktop renderer",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/streetwriters/react-native-gpui-modules.git",
9
+ "directory": "modules/svg"
10
+ },
11
+ "homepage": "https://github.com/streetwriters/react-native-gpui-modules/tree/main/modules/svg#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/streetwriters/react-native-gpui-modules/issues"
14
+ },
15
+ "files": [
16
+ "desktop",
17
+ "react-native.config.js",
18
+ "README.md",
19
+ "LICENSE",
20
+ "CHANGELOG.md",
21
+ "!**/*.test.js",
22
+ "!**/*.test.mjs",
23
+ "!**/*.test.ts",
24
+ "!**/*.test.cjs"
25
+ ],
26
+ "peerDependencies": {
27
+ "react-native": "0.87.x",
28
+ "react-native-svg": "15.15.x"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "tag": "latest"
33
+ }
34
+ }
@@ -0,0 +1,51 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ desktop: {
5
+ cmake: 'desktop',
6
+ crate: 'rn-gpui-svg',
7
+ componentCodegenPackage: 'react-native-svg',
8
+ componentSourcePackage: 'react-native-svg',
9
+ componentDescriptors: [
10
+ 'RNSVGCircleComponentDescriptor',
11
+ 'RNSVGClipPathComponentDescriptor',
12
+ 'RNSVGDefsComponentDescriptor',
13
+ 'RNSVGFeBlendComponentDescriptor',
14
+ 'RNSVGFeColorMatrixComponentDescriptor',
15
+ 'RNSVGFeCompositeComponentDescriptor',
16
+ 'RNSVGFeFloodComponentDescriptor',
17
+ 'RNSVGFeGaussianBlurComponentDescriptor',
18
+ 'RNSVGFeMergeComponentDescriptor',
19
+ 'RNSVGFeOffsetComponentDescriptor',
20
+ 'RNSVGFilterComponentDescriptor',
21
+ 'RNSVGEllipseComponentDescriptor',
22
+ 'RNSVGForeignObjectComponentDescriptor',
23
+ 'RNSVGGroupComponentDescriptor',
24
+ 'RNSVGLinearGradientComponentDescriptor',
25
+ 'RNSVGLineComponentDescriptor',
26
+ 'RNSVGMarkerComponentDescriptor',
27
+ 'RNSVGMaskComponentDescriptor',
28
+ 'RNSVGPathComponentDescriptor',
29
+ 'RNSVGPatternComponentDescriptor',
30
+ 'RNSVGRadialGradientComponentDescriptor',
31
+ 'RNSVGRectComponentDescriptor',
32
+ 'RNSVGSymbolComponentDescriptor',
33
+ 'RNSVGTextComponentDescriptor',
34
+ 'RNSVGTextPathComponentDescriptor',
35
+ 'RNSVGTSpanComponentDescriptor',
36
+ 'RNSVGUseComponentDescriptor',
37
+ 'RNSVGSvgViewComponentDescriptor',
38
+ ],
39
+ componentHeaders: [
40
+ 'react/renderer/components/rnsvg/ComponentDescriptors.h',
41
+ 'react/renderer/components/rnsvg/RNSVGComponentDescriptors.h',
42
+ ],
43
+ componentIncludeDirs: ['common/cpp'],
44
+ componentSources: [
45
+ 'common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp',
46
+ 'common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp',
47
+ ],
48
+ },
49
+ },
50
+ },
51
+ };