@navaramap/three 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "main": "dist/index.umd.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -30,13 +30,13 @@
30
30
  "astronomy-engine": "2.1.19",
31
31
  "stats.js": "0.17.0",
32
32
  "three-stdlib": "2.36.1",
33
- "@navaramap/engine": "0.1.0",
34
- "@navaramap/engine-api": "0.1.0",
35
- "@navaramap/core": "0.1.0",
36
- "@navaramap/three-csm": "0.1.0",
37
- "@navaramap/font": "0.1.0",
38
- "@navaramap/worker": "0.1.0",
39
- "@navaramap/three-api": "0.1.0"
33
+ "@navaramap/core": "0.1.1",
34
+ "@navaramap/engine": "0.1.1",
35
+ "@navaramap/three-api": "0.1.1",
36
+ "@navaramap/three-csm": "0.1.1",
37
+ "@navaramap/worker": "0.1.1",
38
+ "@navaramap/engine-api": "0.1.1",
39
+ "@navaramap/font": "0.1.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "1.62.1",
@@ -71,10 +71,10 @@
71
71
  "tweakpane": "4.0.5",
72
72
  "vite-plugin-glsl": "1.6.0",
73
73
  "vite-plugin-static-copy": "4.1.1",
74
- "@navaramap/three-default-plugin": "0.1.0",
75
- "@navaramap/three-plugins": "0.1.0",
76
- "@navaramap/three-default-descs": "0.1.0",
77
- "@navaramap/three-react": "0.1.0"
74
+ "@navaramap/three-default-descs": "0.1.1",
75
+ "@navaramap/three-default-plugin": "0.1.1",
76
+ "@navaramap/three-react": "0.1.1",
77
+ "@navaramap/three-plugins": "0.1.1"
78
78
  },
79
79
  "scripts": {
80
80
  "start": "vite",
@@ -76,6 +76,8 @@ export abstract class BaseDesc<
76
76
 
77
77
  private _visible?: boolean;
78
78
 
79
+ private _destroyed = false;
80
+
79
81
  constructor(
80
82
  view: ThreeView,
81
83
  ctx: ViewContext,
@@ -92,8 +94,20 @@ export abstract class BaseDesc<
92
94
  /**
93
95
  * Called when the this class is added to the scene. Override this to create the Three.js objects.
94
96
  * This is where you should initialize `this._instance` and add it to the appropriate scene.
97
+ *
98
+ * May return a promise: a descriptor whose implementation loads lazily
99
+ * (e.g. an `await import(...)` of a heavy renderer dependency) can be
100
+ * `async`, awaiting the module before delegating to `super.onCreate()`
101
+ * (after checking {@link destroyed}). `addMesh`/`addLight`/`addEffect`
102
+ * still return their handle synchronously; the instance appears once the
103
+ * promise settles, and a rejection is reported through the descriptor's
104
+ * `error` event (when it declares one). Handle calls issued while the
105
+ * promise is pending are safe: `update()`/`visible` store their fields
106
+ * without touching the missing instance (the deferred `super.onCreate()`
107
+ * applies them), and per-frame/resize hooks are wired only after creation
108
+ * completes.
95
109
  */
96
- abstract onCreate(): void;
110
+ abstract onCreate(): void | Promise<void>;
97
111
 
98
112
  /**
99
113
  * Called when the configuration is updated via `BaseHandle.update()`.
@@ -122,9 +136,19 @@ export abstract class BaseDesc<
122
136
  * Override this to clean up resources. Remember to call `super.onDestroy()`.
123
137
  */
124
138
  onDestroy(): void {
139
+ this._destroyed = true;
125
140
  this._instance = undefined;
126
141
  }
127
142
 
143
+ /**
144
+ * True once {@link onDestroy} has run. An async {@link onCreate} (and the
145
+ * hook wiring that waits on it) checks this after its awaited module
146
+ * resolves, so a descriptor deleted mid-load creates nothing.
147
+ */
148
+ get destroyed(): boolean {
149
+ return this._destroyed;
150
+ }
151
+
128
152
  /**
129
153
  * Gets whether this is currently visible.
130
154
  */
@@ -598,7 +598,6 @@ export abstract class MeshDesc<
598
598
  assertNoTransformConflict(nextGeodetic, nextMatrix, nextMatrixWorld);
599
599
 
600
600
  super.onUpdateConfig(updates);
601
- invariant(this.raw);
602
601
 
603
602
  const spatialChanged =
604
603
  updates.matrix !== undefined ||
@@ -636,6 +635,8 @@ export abstract class MeshDesc<
636
635
  this.applyLit();
637
636
  }
638
637
 
638
+ if (!this.raw) return;
639
+
639
640
  if (spatialChanged) {
640
641
  // With a frame present, the effective transform depends on the
641
642
  // combination of all stored fields — recompose the whole thing.
package/src/index.ts CHANGED
@@ -118,7 +118,12 @@ import {
118
118
  } from "./material";
119
119
  import type { TileMesh } from "./mesh/tile";
120
120
  import { RenderPassOrchestrator } from "./orchestrators/RenderPassOrchestrator";
121
- import { PickHelper } from "./pick/pickHelper";
121
+ import {
122
+ CLICK_PIXEL_TOLERANCE,
123
+ isClickGesture,
124
+ PickHelper,
125
+ TAP_PIXEL_TOLERANCE,
126
+ } from "./pick/pickHelper";
122
127
  import { TerrainPicker } from "./pick/pickTerrain";
123
128
  import { AttributionPlugin, type AttributionPluginOptions } from "./plugins";
124
129
  import { TexturizedSceneByTileCoordinates, type Scenes } from "./scene";
@@ -219,6 +224,12 @@ export * from "./layers";
219
224
  export * from "./passes";
220
225
  export * from "./evaluations";
221
226
  export { SKY_RENDER_ORDER, STARS_RENDER_ORDER } from "./renderOrder";
227
+ export {
228
+ CLICK_PIXEL_TOLERANCE,
229
+ TAP_PIXEL_TOLERANCE,
230
+ PICK_RADIUS,
231
+ TOUCH_PICK_RADIUS,
232
+ } from "./pick/pickHelper";
222
233
  export * from "@navaramap/three-api";
223
234
  export * from "./Color";
224
235
  export { type BlendMode, blendFunction, createReplacer } from "./utils";
@@ -311,8 +322,8 @@ export type Options = {
311
322
  backgroundColor?: CoreColor;
312
323
  /**
313
324
  * Feature picking configuration (default: true). Enables the `featureClick`
314
- * event (click) and the `featureHover` / `featureEnter` / `featureLeave`
315
- * events (mousemove).
325
+ * event (click or touch tap) and the `featureHover` / `featureEnter` /
326
+ * `featureLeave` events (pointer hover).
316
327
  */
317
328
  picking?: boolean;
318
329
  /** When true, renders every frame. When false, renders only on changes or when forceUpdate() is called. */
@@ -404,12 +415,13 @@ export type Options = {
404
415
  } & GlobeOptions;
405
416
 
406
417
  /**
407
- * Mouse event extended with map coordinates at the event location.
418
+ * Pointer event extended with map coordinates at the event location.
419
+ * Fired for every input type; `pointerType` tells them apart.
408
420
  */
409
- export type MapMouseEvent = {
410
- /** World coordinates (ECEF) at the mouse position on the globe surface. */
421
+ export type MapPointerEvent = {
422
+ /** World coordinates (ECEF) at the pointer position on the globe surface. */
411
423
  map: XYZ;
412
- } & MouseEvent;
424
+ } & PointerEvent;
413
425
 
414
426
  /**
415
427
  * Event types emitted by ThreeView. Subscribe using `view.on(eventName, callback)`.
@@ -418,13 +430,13 @@ export type ViewEvents = {
418
430
  /** Emitted when the view is resized. Receives width and height in pixels. */
419
431
  resize: (w: number, h: number) => void;
420
432
  /**
421
- * Emitted when a feature is clicked (click-gesture GPU pick). Receives the
422
- * clicked feature, or null when empty space was clicked. For raw click
423
- * coordinates use the `click` event instead.
433
+ * Emitted when a feature is clicked or tapped (click-gesture GPU pick).
434
+ * Receives the clicked feature, or null when empty space was clicked. For
435
+ * raw click coordinates use the `click` event instead.
424
436
  */
425
437
  featureClick: (info: Nullable<PickedFeature>) => void;
426
438
  /**
427
- * Emitted when the hovered feature changes (mousemove picking). Receives
439
+ * Emitted when the hovered feature changes (hover picking). Receives
428
440
  * the newly hovered feature, or null when the pointer leaves all pickable
429
441
  * features. Hover picking runs a GPU pick per frame while the pointer
430
442
  * moves, and is active only while at least one `featureHover` /
@@ -451,18 +463,26 @@ export type ViewEvents = {
451
463
  postRender: (t: number) => void;
452
464
  /** @private Emitted when terrain height sampling completes. */
453
465
  _sample_terrain_height_received: (ev: TerrainHeightUpdatedEvent) => void;
454
- /** Emitted on mouse down with map coordinates. */
455
- mousedown: (event: MapMouseEvent) => void;
456
- /** Emitted when mouse enters the canvas with map coordinates. */
457
- mouseenter: (event: MapMouseEvent) => void;
458
- /** Emitted when mouse leaves the canvas with map coordinates. */
459
- mouseleave: (event: MapMouseEvent) => void;
460
- /** Emitted on mouse move with map coordinates. */
461
- mousemove: (event: MapMouseEvent) => void;
462
- /** Emitted on mouse up with map coordinates. */
463
- mouseup: (event: MapMouseEvent) => void;
464
- /** Emitted on click with map coordinates. */
465
- click: (event: MapMouseEvent) => void;
466
+ /** Emitted on pointerdown with map coordinates, for every input type. */
467
+ pointerdown: (event: MapPointerEvent) => void;
468
+ /** Emitted when a pointer enters the canvas with map coordinates, for every input type. */
469
+ pointerenter: (event: MapPointerEvent) => void;
470
+ /** Emitted when a pointer leaves the canvas with map coordinates, for every input type. */
471
+ pointerleave: (event: MapPointerEvent) => void;
472
+ /** Emitted on pointermove with map coordinates, for every input type. */
473
+ pointermove: (event: MapPointerEvent) => void;
474
+ /** Emitted on pointerup with map coordinates, for every input type. */
475
+ pointerup: (event: MapPointerEvent) => void;
476
+ /** Emitted when the browser cancels an active pointer (e.g. a system gesture takes over). */
477
+ pointercancel: (event: PointerEvent) => void;
478
+ /**
479
+ * Emitted on a click or touch tap with map coordinates: a primary-pointer
480
+ * press and release within the travel tolerance
481
+ * ({@link CLICK_PIXEL_TOLERANCE}, or {@link TAP_PIXEL_TOLERANCE} for
482
+ * touch). Camera drags and pinches never fire it. `pointerType` tells the
483
+ * input kinds apart.
484
+ */
485
+ click: (event: MapPointerEvent) => void;
466
486
  /** Emitted when the engine becomes idle (no Rust-side events for `idleThreshold` ms). */
467
487
  idle: () => void;
468
488
  };
@@ -1173,9 +1193,9 @@ export default class ThreeView<
1173
1193
  }
1174
1194
 
1175
1195
  /**
1176
- * Convert a mouse event to a MapMouseEvent by adding map coordinates
1196
+ * Extend a pointer event with map coordinates at its location
1177
1197
  */
1178
- private convertMouseEventToMapEvent(event: MouseEvent): MapMouseEvent | null {
1198
+ private convertToMapEvent(event: PointerEvent): MapPointerEvent | null {
1179
1199
  const rect = this._renderer.domElement.getBoundingClientRect();
1180
1200
  const x = event.clientX - rect.left;
1181
1201
  const y = event.clientY - rect.top;
@@ -1188,7 +1208,7 @@ export default class ThreeView<
1188
1208
  y: xyz.y,
1189
1209
  z: xyz.z,
1190
1210
  },
1191
- }) as MapMouseEvent;
1211
+ });
1192
1212
  }
1193
1213
 
1194
1214
  return null;
@@ -1624,47 +1644,75 @@ export default class ThreeView<
1624
1644
 
1625
1645
  this._camera.core = this._core;
1626
1646
 
1627
- this._renderer.domElement.addEventListener("mousedown", (event) => {
1628
- const mapEvent = this.convertMouseEventToMapEvent(event);
1647
+ // The map events are fed from pointer events; touch produces no
1648
+ // compatibility mouse events because the camera-input handlers call
1649
+ // preventDefault() on touch events. `click` is a gesture, not the DOM
1650
+ // event: a primary-pointer press and release within the travel
1651
+ // tolerance, with no pinch and no cancellation. Camera drags never
1652
+ // fire it.
1653
+ let clickStart: Vector2 | undefined;
1654
+
1655
+ this._renderer.domElement.addEventListener("pointerdown", (event) => {
1656
+ // A second finger turns the gesture into a pinch, not a tap.
1657
+ clickStart =
1658
+ event.isPrimary && event.button === 0
1659
+ ? new Vector2(event.clientX, event.clientY)
1660
+ : undefined;
1661
+ const mapEvent = this.convertToMapEvent(event);
1629
1662
  if (mapEvent) {
1630
- this.emit("mousedown", mapEvent);
1663
+ this.emit("pointerdown", mapEvent);
1631
1664
  }
1632
1665
  });
1633
1666
 
1634
- this._renderer.domElement.addEventListener("mouseenter", (event) => {
1635
- const mapEvent = this.convertMouseEventToMapEvent(event);
1667
+ this._renderer.domElement.addEventListener("pointerenter", (event) => {
1668
+ const mapEvent = this.convertToMapEvent(event);
1636
1669
  if (mapEvent) {
1637
- this.emit("mouseenter", mapEvent);
1670
+ this.emit("pointerenter", mapEvent);
1638
1671
  }
1639
1672
  });
1640
1673
 
1641
- this._renderer.domElement.addEventListener("mouseleave", (event) => {
1642
- const mapEvent = this.convertMouseEventToMapEvent(event);
1674
+ this._renderer.domElement.addEventListener("pointerleave", (event) => {
1675
+ const mapEvent = this.convertToMapEvent(event);
1643
1676
  if (mapEvent) {
1644
- this.emit("mouseleave", mapEvent);
1677
+ this.emit("pointerleave", mapEvent);
1645
1678
  }
1646
1679
  });
1647
1680
 
1648
- this._renderer.domElement.addEventListener("mousemove", (event) => {
1649
- const mapEvent = this.convertMouseEventToMapEvent(event);
1681
+ this._renderer.domElement.addEventListener("pointermove", (event) => {
1682
+ const mapEvent = this.convertToMapEvent(event);
1650
1683
  if (mapEvent) {
1651
- this.emit("mousemove", mapEvent);
1684
+ this.emit("pointermove", mapEvent);
1652
1685
  }
1653
1686
  });
1654
1687
 
1655
- this._renderer.domElement.addEventListener("mouseup", (event) => {
1656
- const mapEvent = this.convertMouseEventToMapEvent(event);
1688
+ this._renderer.domElement.addEventListener("pointerup", (event) => {
1689
+ const mapEvent = this.convertToMapEvent(event);
1657
1690
  if (mapEvent) {
1658
- this.emit("mouseup", mapEvent);
1691
+ this.emit("pointerup", mapEvent);
1659
1692
  }
1660
- });
1661
-
1662
- this._renderer.domElement.addEventListener("click", (event) => {
1663
- const mapEvent = this.convertMouseEventToMapEvent(event);
1664
- if (mapEvent) {
1693
+ if (!event.isPrimary || event.button !== 0) return;
1694
+ const start = clickStart;
1695
+ clickStart = undefined;
1696
+ if (!start || !mapEvent) return;
1697
+ const tolerance =
1698
+ event.pointerType === "touch"
1699
+ ? TAP_PIXEL_TOLERANCE
1700
+ : CLICK_PIXEL_TOLERANCE;
1701
+ if (
1702
+ isClickGesture(
1703
+ start,
1704
+ new Vector2(event.clientX, event.clientY),
1705
+ tolerance,
1706
+ )
1707
+ ) {
1665
1708
  this.emit("click", mapEvent);
1666
1709
  }
1667
1710
  });
1711
+
1712
+ this._renderer.domElement.addEventListener("pointercancel", (event) => {
1713
+ clickStart = undefined;
1714
+ this.emit("pointercancel", event);
1715
+ });
1668
1716
  }
1669
1717
 
1670
1718
  /**
@@ -2074,6 +2122,33 @@ export default class ThreeView<
2074
2122
  return new Source(sourceId, s.type, this._core);
2075
2123
  }
2076
2124
 
2125
+ /**
2126
+ * Tracks a possibly-async `onCreate()` (see `BaseDesc.onCreate`): the
2127
+ * `add*` methods still return their handle synchronously, the descriptor's
2128
+ * instance appears when the promise settles, and a rejection is logged and
2129
+ * reported through the descriptor's `error` event (when it declares one).
2130
+ *
2131
+ * `wire` binds the descriptor's lifecycle hooks (per-frame `update`,
2132
+ * `onResize`, `needsUpdate`). It runs immediately for a synchronous
2133
+ * creation, and only after the promise resolves for an async one — the
2134
+ * hooks must not fire while the descriptor has no instance yet.
2135
+ */
2136
+ private _trackAsyncCreate(
2137
+ kind: "mesh" | "light" | "effect",
2138
+ desc: { emit: (event: never, ...args: never[]) => void },
2139
+ result: void | Promise<void>,
2140
+ wire: () => void,
2141
+ ): void {
2142
+ if (!(result instanceof Promise)) {
2143
+ wire();
2144
+ return;
2145
+ }
2146
+ result.then(wire, (error: unknown) => {
2147
+ console.error(`Failed to create ${kind} descriptor:`, error);
2148
+ (desc.emit as (event: string, payload: unknown) => void)("error", error);
2149
+ });
2150
+ }
2151
+
2077
2152
  /**
2078
2153
  * Adds a 3D mesh to the scene.
2079
2154
  * The mesh kind is determined by the nested key (e.g., `{ box: { width: 200 } }`).
@@ -2092,28 +2167,35 @@ export default class ThreeView<
2092
2167
  // Create mesh descriptor instance
2093
2168
  const meshDesc = this.registries.mesh.create(meshType, config);
2094
2169
 
2095
- // Initialize the mesh
2096
- meshDesc.onCreate();
2170
+ // Initialize the mesh; hooks are wired once the instance exists.
2171
+ this._trackAsyncCreate("mesh", meshDesc, meshDesc.onCreate(), () => {
2172
+ // If deleted while an async creation was loading, nothing to wire.
2173
+ if (meshDesc.destroyed) return;
2097
2174
 
2098
- // Set up update listener
2099
- if (meshDesc.update) {
2100
- this.on("preRender", meshDesc.update.bind(meshDesc));
2101
- }
2175
+ // Set up update listener
2176
+ if (meshDesc.update) {
2177
+ this.on("preRender", meshDesc.update.bind(meshDesc));
2178
+ }
2102
2179
 
2103
- if (meshDesc.onResize) {
2104
- this.on("resize", meshDesc.onResize.bind(meshDesc));
2180
+ if (meshDesc.onResize) {
2181
+ this.on("resize", meshDesc.onResize.bind(meshDesc));
2105
2182
 
2106
- const canvasSize = this._getCanvasSize();
2107
- if (canvasSize) {
2108
- meshDesc.onResize(canvasSize.width, canvasSize.height);
2183
+ const canvasSize = this._getCanvasSize();
2184
+ if (canvasSize) {
2185
+ meshDesc.onResize(canvasSize.width, canvasSize.height);
2186
+ }
2109
2187
  }
2110
- }
2111
2188
 
2112
- // Trigger re-render
2113
- meshDesc.on("needsUpdate", this.forceUpdate);
2189
+ // Trigger re-render
2190
+ meshDesc.on("needsUpdate", this.forceUpdate);
2191
+
2192
+ this.forceUpdate();
2193
+ });
2114
2194
 
2115
2195
  const l = new MeshHandle(meshDesc);
2116
2196
 
2197
+ l.on("deleted", this.forceUpdate);
2198
+
2117
2199
  // Store the mesh descriptor
2118
2200
  this.layersManager.add(l);
2119
2201
 
@@ -2139,19 +2221,26 @@ export default class ThreeView<
2139
2221
  // Create light descriptor instance
2140
2222
  const lightDesc = this.registries.light.create(lightType, config);
2141
2223
 
2142
- // Initialize the light
2143
- lightDesc.onCreate();
2224
+ // Initialize the light; hooks are wired once the instance exists.
2225
+ this._trackAsyncCreate("light", lightDesc, lightDesc.onCreate(), () => {
2226
+ // Deleted while an async creation was loading — nothing to wire.
2227
+ if (lightDesc.destroyed) return;
2144
2228
 
2145
- // Set up update listener if the descriptor has an update method
2146
- if (lightDesc.update) {
2147
- this.on("preRender", lightDesc.update.bind(lightDesc));
2148
- }
2229
+ // Set up update listener if the descriptor has an update method
2230
+ if (lightDesc.update) {
2231
+ this.on("preRender", lightDesc.update.bind(lightDesc));
2232
+ }
2149
2233
 
2150
- // Trigger re-render
2151
- lightDesc.on("needsUpdate", this.forceUpdate);
2234
+ // Trigger re-render
2235
+ lightDesc.on("needsUpdate", this.forceUpdate);
2236
+
2237
+ this.forceUpdate();
2238
+ });
2152
2239
 
2153
2240
  const l = new LightHandle(lightDesc);
2154
2241
 
2242
+ l.on("deleted", this.forceUpdate);
2243
+
2155
2244
  // Store the light descriptor
2156
2245
  this.layersManager.add(l);
2157
2246
 
@@ -2194,18 +2283,26 @@ export default class ThreeView<
2194
2283
  // and recompiles shaders when the derived configuration actually changes,
2195
2284
  // so tune effects via `update()` rather than re-adding them.
2196
2285
  this._syncGBuffers();
2197
- l.on("deleted", this._syncGBuffers);
2286
+ l.on("deleted", () => {
2287
+ this._syncGBuffers();
2288
+ this.forceUpdate();
2289
+ });
2198
2290
 
2199
- // Initialize the effect
2200
- effectDesc.onCreate();
2291
+ // Initialize the effect; hooks are wired once the instance exists.
2292
+ this._trackAsyncCreate("effect", effectDesc, effectDesc.onCreate(), () => {
2293
+ // Deleted while an async creation was loading — nothing to wire.
2294
+ if (effectDesc.destroyed) return;
2201
2295
 
2202
- // Set up update listener if the descriptor has an update method
2203
- if (effectDesc.update) {
2204
- this.on("preRender", effectDesc.update.bind(effectDesc));
2205
- }
2296
+ // Set up update listener if the descriptor has an update method
2297
+ if (effectDesc.update) {
2298
+ this.on("preRender", effectDesc.update.bind(effectDesc));
2299
+ }
2206
2300
 
2207
- // Trigger re-render
2208
- effectDesc.on("needsUpdate", this.forceUpdate);
2301
+ // Trigger re-render
2302
+ effectDesc.on("needsUpdate", this.forceUpdate);
2303
+
2304
+ this.forceUpdate();
2305
+ });
2209
2306
 
2210
2307
  // Return handle for imperative access
2211
2308
  return l as EffectHandle<L>;
@@ -52,7 +52,7 @@ export const transformShader = (
52
52
  #include <packing>
53
53
 
54
54
  in float batchId;
55
- out float nvr_vBatchId;
55
+ flat out float nvr_vBatchId;
56
56
  out vec3 vPosition;
57
57
 
58
58
  ${ShowParsVertex}
@@ -93,7 +93,7 @@ uniform float nvr_uPickable;
93
93
  // uEffectIdsMask is declared by overrideMaterialsForMRT (#ifdef USE_SELECTIVE_EFFECT block)
94
94
  ${MODEL_BASE_SHADER_MARKERS.fragment.UNIFORM_END}
95
95
 
96
- in float nvr_vBatchId;
96
+ flat in float nvr_vBatchId;
97
97
 
98
98
  ${ShowParsFragment}
99
99
 
@@ -68,7 +68,7 @@ in float attrBatchId;
68
68
  in vec4 scaleNormalAndCap;
69
69
 
70
70
  uniform vec2 uMinMaxHeight;
71
- out float nvr_vBatchId;
71
+ flat out float nvr_vBatchId;
72
72
 
73
73
  ${useRTE ? RteParsVertex : ""}
74
74
  ${ShowParsVertex}
@@ -162,7 +162,7 @@ uniform float nvr_uPickable;
162
162
  uniform bool uIsTexturized;
163
163
  ${POLYGON_BASE_SHADER_MARKERS.fragment.UNIFORM_END}
164
164
 
165
- in float nvr_vBatchId;
165
+ flat in float nvr_vBatchId;
166
166
 
167
167
  ${ShowParsFragment}
168
168