@navaramap/three 0.0.2 → 0.0.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.
Files changed (34) hide show
  1. package/dist/assets/{fontWorker-8oWvyvwL.js → fontWorker-wz61edjR.js} +2 -2
  2. package/dist/assets/{fontWorker-8oWvyvwL.js.map → fontWorker-wz61edjR.js.map} +1 -1
  3. package/dist/assets/{index-D9G7kIEj.js → index-ywHSGImj.js} +3 -3
  4. package/dist/assets/{index-D9G7kIEj.js.map → index-ywHSGImj.js.map} +1 -1
  5. package/dist/assets/{navara_wasm_font_worker_bg-CkZP9l2i.wasm → navara_wasm_font_worker_bg-Du9nb2Pn.wasm} +0 -0
  6. package/dist/assets/{navara_wasm_worker_bg-Q8zrkLCR.wasm → navara_wasm_worker_bg-Xadu67fw.wasm} +0 -0
  7. package/dist/index.d.ts +133 -2
  8. package/dist/index.js +4309 -4288
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.umd.cjs +223 -210
  11. package/dist/index.umd.cjs.map +1 -1
  12. package/package.json +12 -12
  13. package/src/atmosphere.ts +43 -1
  14. package/src/event/feature.ts +43 -13
  15. package/src/event/features/polygon.ts +2 -1
  16. package/src/input.ts +48 -15
  17. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +1 -0
  18. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +2 -0
  19. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +2 -0
  20. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +2 -0
  21. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +5 -1
  22. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +2 -0
  23. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +2 -0
  24. package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +2 -0
  25. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +2 -0
  26. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +2 -0
  27. package/src/material/overrideMaterialsForMRT.ts +7 -2
  28. package/src/mesh/batchedSdfText.ts +234 -70
  29. package/src/mesh/instanced.ts +1 -1
  30. package/src/mesh/polygon.ts +11 -4
  31. package/src/mesh/polyline.ts +10 -2
  32. package/src/mesh/sdfText.ts +6 -1
  33. package/src/solar.test.ts +33 -0
  34. package/src/solar.ts +31 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
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/font": "0.0.2",
34
- "@navaramap/core": "0.0.2",
35
- "@navaramap/engine-api": "0.0.2",
36
- "@navaramap/engine": "0.0.2",
37
- "@navaramap/three_api": "0.0.2",
38
- "@navaramap/three_csm": "0.0.2",
39
- "@navaramap/worker": "0.0.2"
33
+ "@navaramap/core": "0.0.3",
34
+ "@navaramap/engine": "0.0.3",
35
+ "@navaramap/font": "0.0.3",
36
+ "@navaramap/three_api": "0.0.3",
37
+ "@navaramap/engine-api": "0.0.3",
38
+ "@navaramap/worker": "0.0.3",
39
+ "@navaramap/three_csm": "0.0.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "1.61.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_descs": "0.0.2",
75
- "@navaramap/three_default_plugin": "0.0.2",
76
- "@navaramap/three_plugins": "0.0.2",
77
- "@navaramap/three_react": "0.0.2"
74
+ "@navaramap/three_default_descs": "0.0.3",
75
+ "@navaramap/three_default_plugin": "0.0.3",
76
+ "@navaramap/three_plugins": "0.0.3",
77
+ "@navaramap/three_react": "0.0.3"
78
78
  },
79
79
  "scripts": {
80
80
  "start": "vite",
package/src/atmosphere.ts CHANGED
@@ -14,7 +14,13 @@ import invariant from "tiny-invariant";
14
14
 
15
15
  import { type ThreeViewCamera } from "./camera";
16
16
  import { ATMOSPHERE_ASSETS_URL, STBN_URL } from "./constants";
17
- import { shiftDateToElevation, shiftDateToHourAngle } from "./solar";
17
+ import {
18
+ dateForSolarTime,
19
+ getSolarTime as solarTimeAt,
20
+ getSunElevation as sunElevationAt,
21
+ shiftDateToElevation,
22
+ shiftDateToHourAngle,
23
+ } from "./solar";
18
24
 
19
25
  /**
20
26
  * Events emitted by the {@link Atmosphere} class.
@@ -258,6 +264,42 @@ export class Atmosphere extends EventHandler<AtmosphereEvents> {
258
264
  return dotProduct < 0;
259
265
  }
260
266
 
267
+ /**
268
+ * Returns the sun's elevation angle in degrees above the local horizon at
269
+ * `location` for the current {@link date}. Positive is above the horizon,
270
+ * negative below (night); includes atmospheric refraction.
271
+ *
272
+ * @example
273
+ * const elevation = view.atmosphere.getSunElevation(view.camera.positionGeographic);
274
+ * if (elevation < 0) console.log("The sun has set.");
275
+ */
276
+ getSunElevation(location: { lat: number; lng: number }): number {
277
+ return sunElevationAt(this.date, location.lat, location.lng);
278
+ }
279
+
280
+ /**
281
+ * Returns the local apparent solar time at `lng` for the current
282
+ * {@link date}, in hours in the range [0, 24) where 12 is solar noon.
283
+ * Accounts for the equation of time.
284
+ *
285
+ * @example
286
+ * const hours = view.atmosphere.getSolarTime({ lng: 139.69 }); // e.g. 6.3 = 06:18
287
+ */
288
+ getSolarTime(location: { lng: number }): number {
289
+ return solarTimeAt(this.date, location.lng);
290
+ }
291
+
292
+ /**
293
+ * Sets {@link date} so the local apparent solar time at `lng` equals `hours`
294
+ * (0–24), keeping the same solar day. Inverse of {@link getSolarTime}.
295
+ *
296
+ * @example
297
+ * view.atmosphere.setSolarTime({ lng: 139.69 }, 6.3); // sunrise over Tokyo
298
+ */
299
+ setSolarTime(location: { lng: number }, hours: number): void {
300
+ this.date = dateForSolarTime(this.date, location.lng, hours);
301
+ }
302
+
261
303
  /**
262
304
  * Gets the current date used for calculating sun/moon positions.
263
305
  */
@@ -118,6 +118,20 @@ export async function processRenderableFeatureAdded(
118
118
 
119
119
  const featureLayerId = ev.layer_id;
120
120
 
121
+ // Captured once: the render-completion report is deferred until the mesh is
122
+ // in its render/bake scene (see below), by which point `ev` may be gone.
123
+ const bits = ev.bits;
124
+ const renderedType =
125
+ point || billboard || text
126
+ ? "point"
127
+ : model
128
+ ? "model"
129
+ : polyline
130
+ ? "polyline"
131
+ : polygon
132
+ ? "polygon"
133
+ : undefined;
134
+
121
135
  const useParallel = checkFeatureParallel(feature);
122
136
 
123
137
  if (useParallel) {
@@ -127,15 +141,6 @@ export async function processRenderableFeatureAdded(
127
141
 
128
142
  const obj = await renderFeature(ctx, feature, tileHandle, featureLayerId)
129
143
  ?.then((r) => {
130
- const type = (() => {
131
- if (point || billboard || text) return "point";
132
- else if (model) return "model";
133
- else if (polyline) return "polyline";
134
- else if (polygon) return "polygon";
135
- })();
136
- if (type) {
137
- featureHandler.markFeatureIsRendered(type, ev.bits);
138
- }
139
144
  // The glTF/Draco decode happened on the JS side, so report the actual
140
145
  // decoded GPU size back to the ledger (its compressed-payload estimate
141
146
  // undercounts Draco content). Draco decode inflates geometry markedly.
@@ -164,7 +169,12 @@ export async function processRenderableFeatureAdded(
164
169
  }
165
170
  });
166
171
 
167
- if (!obj) return;
172
+ if (!obj) {
173
+ // renderFeature produced no mesh (e.g. empty geometry); still report
174
+ // rendered so the tile's LOD/activation can advance, as before.
175
+ if (renderedType) featureHandler.markFeatureIsRendered(renderedType, bits);
176
+ return;
177
+ }
168
178
 
169
179
  // Sprite should be handled by mesh itself.
170
180
  const transform = (polyline ?? polygon ?? model)?.transform;
@@ -185,7 +195,10 @@ export async function processRenderableFeatureAdded(
185
195
 
186
196
  meshes.set(id, obj);
187
197
 
188
- if (isDraped && tileHandle) {
198
+ // `!= null` (not truthiness): the root vector tile's handle is 0, which is
199
+ // falsy — a truthy check silently dropped root-tile draped meshes from the
200
+ // texturized scene cache, leaving small datasets that render at z0 blank.
201
+ if (isDraped && tileHandle != null) {
189
202
  // Insert the bakeable draped mesh into the per-tile cache now (it is fully built at
190
203
  // this point). Readiness/ancestor-fallback is decided entirely on the Rust side from
191
204
  // the ECS lifecycle, so no `scene_ready` is reported. Insert even when invisible —
@@ -213,6 +226,14 @@ export async function processRenderableFeatureAdded(
213
226
  });
214
227
  }
215
228
 
229
+ // Report render-completion to Rust only now that the mesh is in its render
230
+ // (MRT) or bake (texturized) scene. For draped features this must follow the
231
+ // `texturizedSceneByTileCoordinates.add` above: the Rust drape resolve flips a
232
+ // terrain tile's vector source to this tile once its features report rendered,
233
+ // so reporting before the offscreen scene existed left the terrain tile's
234
+ // vector drape blank until a camera-move re-traverse.
235
+ if (renderedType) featureHandler.markFeatureIsRendered(renderedType, bits);
236
+
216
237
  if (obj instanceof PolygonMesh && polygon && polygon.outline_geometry) {
217
238
  const outline = await renderPolygonOutline(ctx, polygon);
218
239
  outline.renderOrder = FEATURE_RENDER_ORDER;
@@ -320,7 +341,7 @@ export async function processRenderableFeatureChanged(
320
341
  if (
321
342
  ((obj instanceof PolygonMesh && obj.clampToGround) ||
322
343
  (obj instanceof PolylineMesh && obj.draped)) &&
323
- tileHandle
344
+ tileHandle != null // the root tile's handle is 0 (falsy)
324
345
  ) {
325
346
  texturizedSceneByTileCoordinates.markDirty(tileHandle, layerId);
326
347
  }
@@ -345,7 +366,16 @@ export async function processRenderableFeatureChanged(
345
366
 
346
367
  obj.updateMatrix();
347
368
 
348
- if (obj.visible) {
369
+ // Re-emit `featureUpdated` (which re-runs the layer's registered evaluator
370
+ // over this whole feature set) only when the set is actually displayed at the
371
+ // current LOD. `obj.visible` is not that signal for draped meshes: they stay
372
+ // visible while built so they remain stable bakeable drape sources (see
373
+ // PolygonMesh._update), so gating on it alone re-evaluated every built draped
374
+ // tile on every activation flip during zoom. The evaluator's per-feature
375
+ // values persist in the batch data texture, and user-driven restyles reach
376
+ // inactive sets through `Layer.forceUpdate`, so skipping inactive sets here
377
+ // is safe.
378
+ if (obj.visible && active) {
349
379
  handleFeatureUpdatedEventByLayerId(
350
380
  viewEvents,
351
381
  layersManager,
@@ -19,5 +19,6 @@ export function processPolygonChanged(
19
19
  active: boolean,
20
20
  tileHandle: TileHandle | undefined,
21
21
  ) {
22
- obj._update(m.material, active, !!tileHandle);
22
+ // `!= null`, not truthiness: the root vector tile's handle is 0.
23
+ obj._update(m.material, active, tileHandle != null);
23
24
  }
package/src/input.ts CHANGED
@@ -15,6 +15,21 @@ export function registerInputEvents(
15
15
 
16
16
  let lastTerrainDistance: number | undefined;
17
17
 
18
+ // Mousedown and mousemove must share the same coordinate space: the engine
19
+ // re-anchors its cursor position from the mousedown coordinates so the first
20
+ // drag delta is never computed against a stale position (e.g. after the
21
+ // window lost focus while the OS cursor kept moving).
22
+ const normalizePosition = (event: MouseEvent) => {
23
+ const rect = element.getBoundingClientRect();
24
+ const width = element.clientWidth;
25
+ const height = element.clientHeight;
26
+ const aspectRatio = width / height;
27
+ return {
28
+ x: ((event.clientX - rect.left) / width) * aspectRatio,
29
+ y: (event.clientY - rect.top) / height,
30
+ };
31
+ };
32
+
18
33
  const mousedown = (event: MouseEvent) => {
19
34
  mouseEvent = {
20
35
  type: "mousedown",
@@ -22,7 +37,11 @@ export function registerInputEvents(
22
37
  };
23
38
  const terrainDistance = getTerrainDistance?.() ?? undefined;
24
39
  lastTerrainDistance = terrainDistance;
25
- core.input({ ...mouseEvent, terrain_distance: terrainDistance });
40
+ core.input({
41
+ ...mouseEvent,
42
+ ...normalizePosition(event),
43
+ terrain_distance: terrainDistance,
44
+ });
26
45
  };
27
46
  const mouseup = () => {
28
47
  core.input({
@@ -33,13 +52,9 @@ export function registerInputEvents(
33
52
  };
34
53
 
35
54
  const mousemove = (event: MouseEvent) => {
36
- const width = element.clientWidth;
37
- const height = element.clientHeight;
38
- const aspectRatio = width / height;
39
55
  core.input({
40
56
  type: "mousemove",
41
- x: (event.clientX / width) * aspectRatio,
42
- y: event.clientY / height,
57
+ ...normalizePosition(event),
43
58
  });
44
59
  };
45
60
 
@@ -109,36 +124,54 @@ export function registerInputEvents(
109
124
  key: event?.key,
110
125
  });
111
126
  };
112
- // This is used to emit a key up event without any params.
113
- const keyupEmpty = () => {
114
- core.input({
115
- type: "keyup",
116
- });
127
+
128
+ // Releases every pressed button and modifier key. When the window loses
129
+ // focus or visibility the matching mouseup/keyup fires in another window, so
130
+ // without this the engine would keep acting on stale pressed state after the
131
+ // user comes back.
132
+ const releaseInputs = () => {
133
+ for (const button of [0, 1, 2]) {
134
+ core.input({ type: "mouseup", button });
135
+ }
136
+ mouseEvent = undefined;
137
+ core.input({ type: "keyup", key_code: "ControlLeft", key: "Control" });
138
+ core.input({ type: "keyup", key_code: "ControlRight", key: "Control" });
139
+ };
140
+ const visibilitychange = () => {
141
+ if (document.visibilityState === "hidden") {
142
+ releaseInputs();
143
+ }
117
144
  };
118
145
 
119
146
  element.addEventListener("mousedown", mousedown);
120
147
  element.addEventListener("mouseup", mouseup);
121
- element.addEventListener("mouseleave", mouseup);
122
- element.addEventListener("mouseleave", keyupEmpty);
148
+ element.addEventListener("mouseleave", releaseInputs);
123
149
  element.addEventListener("mousemove", mousemove);
124
150
  element.addEventListener("touchstart", touchstart);
125
151
  element.addEventListener("touchend", touchend);
152
+ // The browser cancels active touches on app switches and system gestures;
153
+ // treat them as ended so no stale touch stays in the engine.
154
+ element.addEventListener("touchcancel", touchend);
126
155
  element.addEventListener("touchmove", touchmove);
127
156
  element.addEventListener("wheel", wheel);
128
157
  document.addEventListener("keydown", keydown);
129
158
  document.addEventListener("keyup", keyup);
159
+ window.addEventListener("blur", releaseInputs);
160
+ document.addEventListener("visibilitychange", visibilitychange);
130
161
 
131
162
  return () => {
132
163
  element.removeEventListener("mousedown", mousedown);
133
164
  element.removeEventListener("mouseup", mouseup);
134
- element.removeEventListener("mouseleave", mouseup);
135
- element.removeEventListener("mouseleave", keyupEmpty);
165
+ element.removeEventListener("mouseleave", releaseInputs);
136
166
  element.removeEventListener("mousemove", mousemove);
137
167
  element.removeEventListener("touchstart", touchstart);
138
168
  element.removeEventListener("touchend", touchend);
169
+ element.removeEventListener("touchcancel", touchend);
139
170
  element.removeEventListener("touchmove", touchmove);
140
171
  element.removeEventListener("wheel", wheel);
141
172
  document.removeEventListener("keydown", keydown);
142
173
  document.removeEventListener("keyup", keyup);
174
+ window.removeEventListener("blur", releaseInputs);
175
+ document.removeEventListener("visibilitychange", visibilitychange);
143
176
  };
144
177
  }
@@ -10,6 +10,7 @@ import type { InstancedSpriteBaseState } from "./types";
10
10
 
11
11
  vi.mock("@navaramap/three_api", () => ({
12
12
  degreeToRadian: (degree: number) => (degree * Math.PI) / 180,
13
+ RTE_ONE_UNIFORM: { value: 1.0 },
13
14
  }));
14
15
 
15
16
  vi.mock("@navaramap/engine-api", () => ({
@@ -1,4 +1,5 @@
1
1
  import { encodePosition } from "@navaramap/engine-api";
2
+ import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
2
3
  import {
3
4
  type Matrix4,
4
5
  type Texture,
@@ -82,6 +83,7 @@ export const createBaseMutates = (
82
83
  uniforms.uRTCCenterView = refs.uRTCCenterView;
83
84
  uniforms.uEyeRTELow = refs.uEyeRTELow;
84
85
  uniforms.uEyeRTEHigh = refs.uEyeRTEHigh;
86
+ uniforms.u_rteOne = RTE_ONE_UNIFORM;
85
87
  uniforms.uScale = refs.uScale;
86
88
  uniforms.uCenter = refs.uCenter;
87
89
  uniforms.uSizeInMeters = refs.uSizeInMeters;
@@ -77,6 +77,8 @@ export type InstancedSpriteBaseRefs = {
77
77
  uRTCCenterView: UniformValue<Vector3>;
78
78
  uEyeRTELow: UniformValue<Vector3>;
79
79
  uEyeRTEHigh: UniformValue<Vector3>;
80
+ /** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
81
+ u_rteOne?: UniformValue<number>;
80
82
  uScale: UniformValue<number>;
81
83
  uCenter: UniformValue<Vector2>;
82
84
  uSizeInMeters: UniformValue<boolean>;
@@ -1,3 +1,4 @@
1
+ import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
1
2
  import type { Matrix4, Texture, Vector3 } from "three";
2
3
  import { Matrix4 as ThreeMatrix4, Vector3 as ThreeVector3 } from "three";
3
4
 
@@ -78,6 +79,7 @@ export const createBaseMutates = (useRTE: boolean): PolygonBaseMutates => {
78
79
  uniforms.u_cameraPositionHigh = refs.u_cameraPositionHigh;
79
80
  uniforms.u_cameraPositionLow = refs.u_cameraPositionLow;
80
81
  uniforms.modelViewMatrixRTE = refs.modelViewMatrixRTE;
82
+ uniforms.u_rteOne = RTE_ONE_UNIFORM;
81
83
  }
82
84
 
83
85
  // Height uniforms
@@ -115,7 +115,11 @@ ${ShadowMapDepthVertex}
115
115
  #include <envmap_vertex>
116
116
 
117
117
  vPosition = absTransformed.xyz;
118
- vViewPosition = -absMvPosition.xyz;
118
+ // mvPosition comes from the RTE projection (modelViewMatrixRTE * camera-relative
119
+ // position), so it is precise at any distance. Deriving vViewPosition from
120
+ // modelViewMatrix * absTransformed instead would suffer f32 cancellation at
121
+ // ECEF magnitude and make view-dependent shading wobble as the camera moves.
122
+ vViewPosition = -mvPosition.xyz;
119
123
  `,
120
124
  useRTE,
121
125
  )
@@ -108,6 +108,8 @@ export type PolygonBaseRefs = {
108
108
  modelViewMatrixRTE?: UniformValue<Matrix4>;
109
109
  u_cameraPositionHigh?: UniformValue<Vector3>;
110
110
  u_cameraPositionLow?: UniformValue<Vector3>;
111
+ /** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
112
+ u_rteOne?: UniformValue<number>;
111
113
  };
112
114
 
113
115
  export type PolygonBaseUniforms = Partial<PolygonBaseRefs>;
@@ -1,3 +1,4 @@
1
+ import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
1
2
  import {
2
3
  Color as ThreeColor,
3
4
  Matrix4 as ThreeMatrix4,
@@ -101,6 +102,7 @@ export const createBaseMutates = (useRTE: boolean): PolylineBaseMutates => {
101
102
  uniforms.u_cameraPositionHigh = refs.u_cameraPositionHigh;
102
103
  uniforms.u_cameraPositionLow = refs.u_cameraPositionLow;
103
104
  uniforms.modelViewMatrixRTE = refs.modelViewMatrixRTE;
105
+ uniforms.u_rteOne = RTE_ONE_UNIFORM;
104
106
  }
105
107
 
106
108
  // Batch texture
@@ -114,6 +114,8 @@ export type PolylineBaseRefs = {
114
114
  modelViewMatrixRTE?: UniformValue<Matrix4>;
115
115
  u_cameraPositionHigh?: UniformValue<Vector3>;
116
116
  u_cameraPositionLow?: UniformValue<Vector3>;
117
+ /** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
118
+ u_rteOne?: UniformValue<number>;
117
119
  };
118
120
 
119
121
  export type PolylineBaseUniforms = Partial<PolylineBaseRefs>;
@@ -1,4 +1,5 @@
1
1
  import { encodePosition } from "@navaramap/engine-api";
2
+ import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
2
3
  import { Color, DataTexture, type Matrix4, Vector2, Vector3 } from "three";
3
4
 
4
5
  import type { UniformValue } from "../../../types";
@@ -119,6 +120,7 @@ export const createBaseMutates = (
119
120
  uniforms.uRTCCenterView = refs.uRTCCenterView;
120
121
  uniforms.uEyeRTELow = refs.uEyeRTELow;
121
122
  uniforms.uEyeRTEHigh = refs.uEyeRTEHigh;
123
+ uniforms.u_rteOne = RTE_ONE_UNIFORM;
122
124
  uniforms.nvr_uBatchId = refs.nvr_uBatchId;
123
125
  uniforms.nvr_uPickable = refs.nvr_uPickable;
124
126
  uniforms.uAtlas = refs.uAtlas;
@@ -111,6 +111,8 @@ export type SdfTextBaseRefs = {
111
111
  uRTCCenterView: UniformValue<Vector3>;
112
112
  uEyeRTELow: UniformValue<Vector3>;
113
113
  uEyeRTEHigh: UniformValue<Vector3>;
114
+ /** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
115
+ u_rteOne?: UniformValue<number>;
114
116
  nvr_uBatchId: UniformValue<number>;
115
117
  nvr_uPickable: UniformValue<number>;
116
118
  uAtlas: UniformValue<DataTexture | null>;
@@ -431,8 +431,13 @@ export function setupMaterialForMRT(
431
431
  material: ShaderMaterial,
432
432
  options?: SetupMaterialForMRTOptions,
433
433
  ): void {
434
- if (material instanceof LineMaterial) {
435
- injectGBufferToLineMaterial(material);
434
+ // Detect LineMaterial by `type`, not only `instanceof`: callers may construct
435
+ // it from `three/examples/jsm/lines` while this module's `LineMaterial` comes
436
+ // from `three-stdlib`, so a bare `instanceof` misses those cross-copy
437
+ // instances and would wrongly route them through the ShaderMaterial path
438
+ // (whose normal-buffer write references an undeclared `normal`).
439
+ if (material instanceof LineMaterial || material.type === "LineMaterial") {
440
+ injectGBufferToLineMaterial(material as LineMaterial);
436
441
  return;
437
442
  }
438
443
  injectGBufferToShaderMaterial(material, options?.normal);