@needle-tools/engine 4.16.6 → 4.16.7-beta.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.
@@ -112,14 +112,14 @@ export async function needleBuildPipeline(command, config, userSettings) {
112
112
  }
113
113
 
114
114
  let shouldRun = false;
115
- const productionArgument = process.argv.indexOf("--production");
116
- if (productionArgument >= 0) {
115
+ // Check env var (set by Unity/Blender integrations) or CLI arg (for manual invocation)
116
+ if (process.env.NEEDLE_BUILD_PRODUCTION === "true" || process.argv.indexOf("--production") >= 0) {
117
117
  shouldRun = true;
118
118
  }
119
119
 
120
120
  if (!shouldRun) {
121
121
  if (command === "build") {
122
- log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\".");
122
+ log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\" or set NEEDLE_BUILD_PRODUCTION=true.");
123
123
  }
124
124
  await new Promise((resolve, _) => setTimeout(resolve, 1000));
125
125
  return null;
@@ -387,12 +387,21 @@ async function invokeBuildPipeline(opts, options = {}) {
387
387
  else {
388
388
  // First check if the user passed in a specific version to use via the vite config
389
389
  let version = opts.buildPipeline?.version;
390
- // If not then see if we have a commandline argument
390
+ let versionSource = version ? "vite" : "";
391
+ // If not, check env var (set by Unity/Blender integrations)
392
+ if (!version && process.env.NEEDLE_BUILD_PIPELINE_VERSION) {
393
+ version = process.env.NEEDLE_BUILD_PIPELINE_VERSION;
394
+ versionSource = "env";
395
+ }
396
+ // Fallback: check CLI arg for backwards compatibility
391
397
  if (!version) {
392
398
  for (let i = 0; i < process.argv.length; i++) {
393
399
  if (process.argv[i] === "--build-pipeline-version" && i < process.argv.length - 1) {
394
400
  const value = process.argv[i + 1]?.replace(/['"]+/g, '').trim();
395
- if (value) version = value;
401
+ if (value) {
402
+ version = value;
403
+ versionSource = "arg";
404
+ }
396
405
  break;
397
406
  }
398
407
  }
@@ -400,8 +409,9 @@ async function invokeBuildPipeline(opts, options = {}) {
400
409
  // Otherwise we default to the stable version on npm
401
410
  if (!version) version = "stable";
402
411
 
412
+ const versionInfo = versionSource ? `'${version}' (${versionSource})` : `'${version}'`;
403
413
  const cmd = `npx --yes @needle-tools/gltf-build-pipeline@${version} transform "${outputDirectory}" \"${tempOutputPath}\"`;
404
- log(`Running compression locally using version '${version}'`);
414
+ log(`Running compression locally using version ${versionInfo}`);
405
415
  proc = exec(cmd, { env: commandEnv });
406
416
  }
407
417
  let pipelineSpinnerIndex = 0;
@@ -1017,6 +1017,8 @@ export class NeedleXRSession implements INeedleXRSession {
1017
1017
  private readonly _xr_update_scripts: INeedleXRSessionEventReceiver[] = [];
1018
1018
  /** scripts that are in the scene but inactive (e.g. disabled parent gameObject) */
1019
1019
  private readonly _inactive_scripts: INeedleXRSessionEventReceiver[] = [];
1020
+ /** tracks scripts that have received onEnterXR — prevents spurious onLeaveXR calls */
1021
+ private readonly _scripts_in_xr = new Set<INeedleXRSessionEventReceiver>();
1020
1022
  private readonly _controllerAdded: ControllerChangedEvt[];
1021
1023
  private readonly _controllerRemoved: ControllerChangedEvt[];
1022
1024
  private readonly _originalCameraWorldPosition?: Vector3 | null;
@@ -1295,8 +1297,10 @@ export class NeedleXRSession implements INeedleXRSession {
1295
1297
  // even if they might already be destroyed e.g. by the WebXR component (it destroys the default controller scripts)
1296
1298
  // they should still receive this callback to be properly cleaned up
1297
1299
  for (const listener of this._xr_scripts) {
1300
+ this._scripts_in_xr.delete(listener);
1298
1301
  listener?.onLeaveXR?.({ xr: this });
1299
1302
  }
1303
+ this._scripts_in_xr.clear();
1300
1304
 
1301
1305
  this.sync?.onExitXR(this);
1302
1306
 
@@ -1498,8 +1502,12 @@ export class NeedleXRSession implements INeedleXRSession {
1498
1502
  }
1499
1503
 
1500
1504
  //performance.mark('NeedleXRSession update scripts start');
1501
- // invoke update on all scripts
1502
- for (const script of this._xr_update_scripts) {
1505
+ // check all XR scripts for destroyed or inactive state
1506
+ // this must cover _xr_scripts (not just _xr_update_scripts) so that scripts
1507
+ // without onUpdateXR are also detected as inactive when removed from the scene
1508
+ // iterate backwards since markInactive/removeScript modifies the array
1509
+ for (let i = this._xr_scripts.length - 1; i >= 0; i--) {
1510
+ const script = this._xr_scripts[i];
1503
1511
  if (script.destroyed === true) {
1504
1512
  this._script_to_remove.push(script);
1505
1513
  continue;
@@ -1508,6 +1516,10 @@ export class NeedleXRSession implements INeedleXRSession {
1508
1516
  this.markInactive(script);
1509
1517
  continue;
1510
1518
  }
1519
+ }
1520
+ // invoke update on scripts that have onUpdateXR
1521
+ for (const script of this._xr_update_scripts) {
1522
+ if (script.destroyed || script.activeAndEnabled === false) continue;
1511
1523
  if (script.onUpdateXR) script.onUpdateXR(args);
1512
1524
  }
1513
1525
  //performance.mark('NeedleXRSession update scripts end');
@@ -1630,9 +1642,11 @@ export class NeedleXRSession implements INeedleXRSession {
1630
1642
  const script = this._inactive_scripts[i];
1631
1643
  if (script.activeAndEnabled) {
1632
1644
  this._inactive_scripts.splice(i, 1);
1633
- this.addScript(script);
1634
- this.invokeCallback_EnterXR(script);
1635
- for (const ctrl of this.controllers) this.invokeCallback_ControllerAdded(script, ctrl);
1645
+ // addScript returns false if already re-added (e.g. via new_scripts_xr processing)
1646
+ if (this.addScript(script)) {
1647
+ this.invokeCallback_EnterXR(script);
1648
+ for (const ctrl of this.controllers) this.invokeCallback_ControllerAdded(script, ctrl);
1649
+ }
1636
1650
  }
1637
1651
  }
1638
1652
  }
@@ -1653,6 +1667,7 @@ export class NeedleXRSession implements INeedleXRSession {
1653
1667
  }
1654
1668
 
1655
1669
  private invokeCallback_EnterXR(script: INeedleXRSessionEventReceiver) {
1670
+ this._scripts_in_xr.add(script);
1656
1671
  if (script.onEnterXR) {
1657
1672
  script.onEnterXR({ xr: this });
1658
1673
  }
@@ -1668,6 +1683,7 @@ export class NeedleXRSession implements INeedleXRSession {
1668
1683
  }
1669
1684
  }
1670
1685
  private invokeCallback_LeaveXR(script: INeedleXRSessionEventReceiver) {
1686
+ if (!this._scripts_in_xr.delete(script)) return;
1671
1687
  if (script.onLeaveXR && !script.destroyed) {
1672
1688
  script.onLeaveXR({ xr: this });
1673
1689
  }
@@ -286,6 +286,8 @@ export class ReflectionProbe extends Behaviour {
286
286
  const current = block.getOverride("envMap")?.value;
287
287
  if (current === this.texture) {
288
288
  block.removeOveride("envMap");
289
+ block.removeOveride("envMapRotation");
290
+ block.removeOveride("envMapIntensity");
289
291
  }
290
292
  }
291
293
  }
@@ -428,6 +428,8 @@ export class VideoPlayer extends Behaviour {
428
428
  }
429
429
  }
430
430
 
431
+ private _playErrors: number = 0;
432
+
431
433
  /** start playing the video source */
432
434
  play() {
433
435
  if (!this._videoElement) this.create(false);
@@ -448,7 +450,8 @@ export class VideoPlayer extends Behaviour {
448
450
  if (debug) console.log("Video Play()", this.clip, this._videoElement, this.time);
449
451
  this._videoElement.currentTime = this.time;
450
452
  this._videoElement.play().catch(err => {
451
- console.log(err);
453
+ if (this._playErrors++ < 10) console.error(err);
454
+ else if (this._playErrors === 10) console.error("Multiple errors playing video, further errors will be suppressed. Use 'debugvideo' param to see all errors.");
452
455
  // https://developer.chrome.com/blog/play-request-was-interrupted/
453
456
  if (debug)
454
457
  console.error("Error playing video", err, "CODE=" + err.code, this.videoElement?.src, this);
@@ -4,7 +4,7 @@ import * as ThreeMeshUI from 'three-mesh-ui'
4
4
  import { Mathf } from "../../engine/engine_math.js";
5
5
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
6
  import { FrameEvent } from "../../engine/engine_setup.js";
7
- import { delayForFrames, getParam } from "../../engine/engine_utils.js";
7
+ import { getParam } from "../../engine/engine_utils.js";
8
8
  import { type NeedleXREventArgs } from "../../engine/xr/api.js";
9
9
  import { Camera } from "../Camera.js";
10
10
  import { GameObject } from "../Component.js";
@@ -236,19 +236,13 @@ export class Canvas extends UIRootComponent implements ICanvas {
236
236
  }
237
237
  }
238
238
 
239
- async onEnterXR(args: NeedleXREventArgs) {
239
+ onEnterXR(args: NeedleXREventArgs) {
240
240
  // workaround for https://linear.app/needle/issue/NE-4114
241
241
  if (this.screenspace) {
242
242
  if (args.xr.isVR || args.xr.isPassThrough) {
243
243
  this.gameObject.visible = false;
244
244
  }
245
245
  }
246
- else {
247
- this.gameObject.visible = false;
248
- await delayForFrames(1).then(()=>{
249
- this.gameObject.visible = true;
250
- });
251
- }
252
246
  }
253
247
  onLeaveXR(args: NeedleXREventArgs): void {
254
248
  if (this.screenspace) {