Needle Engine

Changes between version 3.37.10-alpha.6 and 3.37.10-alpha.7
Files changed (3) hide show
  1. src/engine-components/CameraUtils.ts +2 -2
  2. src/engine/engine_utils_screenshot.ts +68 -6
  3. src/engine/webcomponents/needle menu/needle-menu.ts +43 -14
src/engine-components/CameraUtils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Object3D } from "three";
1
+ import { PerspectiveCamera } from "three";
2
2
 
3
3
  import { getCameraController } from "../engine/engine_camera.js";
4
4
  import { addNewComponent, getOrAddComponent } from "../engine/engine_components.js";
@@ -17,7 +17,7 @@
17
17
  if (debug) console.warn("Creating missing camera")
18
18
  const scene = evt.context.scene;
19
19
 
20
- const cameraObject = new Object3D();
20
+ const cameraObject = new PerspectiveCamera();
21
21
  cameraObject.name = "Default Fallback Camera"
22
22
  scene.add(cameraObject);
23
23
 
src/engine/engine_utils_screenshot.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { Camera,PerspectiveCamera } from "three";
1
+ import { Camera, Color, PerspectiveCamera } from "three";
2
2
 
3
3
  import { ContextRegistry } from "./engine_context_registry.js";
4
4
  import { Context } from "./engine_setup.js";
5
5
 
6
- declare type ImageMimeType = "image/webp" | "image/png";
6
+ declare type ScreenshotImageMimeType = "image/webp" | "image/png";
7
7
 
8
8
  /**
9
- * Take a screenshot from the current scene
9
+ * Take a screenshot from the current scene.
10
+ * **NOTE**: Use {@link screenshot2} for more options.
11
+ *
10
12
  * @param context The context to take the screenshot from
11
13
  * @param width The width of the screenshot
12
14
  * @param height The height of the screenshot
@@ -19,8 +21,62 @@
19
21
  * saveImage(dataUrl, "screenshot.png");
20
22
  * ```
21
23
  */
22
- export function screenshot(context?: Context, width?: number, height?: number, mimeType: ImageMimeType = "image/webp", camera?: Camera | null): string | null {
24
+ export function screenshot(context?: Context, width?: number, height?: number, mimeType: ScreenshotImageMimeType = "image/webp", camera?: Camera | null): string | null {
25
+ return screenshot2({ context, width, height, mimeType, camera });
26
+ }
23
27
 
28
+
29
+ declare type ScreenshotOptions = {
30
+ /**
31
+ * The context to take the screenshot from. If not provided, the current context will be used.
32
+ */
33
+ context?: Pick<Context, "renderer" | "mainCamera" | "renderNow" | "updateAspect" | "updateSize">,
34
+ /**
35
+ * The width of the screenshot - if not provided, the width of the current renderer will be used.
36
+ */
37
+ width?: number,
38
+ /**
39
+ * The height of the screenshot - if not provided, the height of the current renderer will be used.
40
+ */
41
+ height?: number,
42
+ /**
43
+ * The mime type of the image
44
+ */
45
+ mimeType?: ScreenshotImageMimeType,
46
+ /**
47
+ * The camera to use for the screenshot. If not provided, the main camera of the context will be used.
48
+ */
49
+ camera?: Camera | null,
50
+ /**
51
+ * If true, the background will be transparent.
52
+ */
53
+ transparent?: boolean
54
+ };
55
+
56
+ /**
57
+ * Take a screenshot from the current scene.
58
+ * @param {ScreenshotOptions} opts
59
+ * @returns The data url of the screenshot. Returns null if the screenshot could not be taken.
60
+ * ```ts
61
+ * const res = screenshot2({
62
+ * width: 1024,
63
+ * height: 1024,
64
+ * mimeType: "image/webp",
65
+ * transparent: true,
66
+ * })
67
+ * // use saveImage to download the image
68
+ * saveImage(res, "screenshot.webp");
69
+ * ```
70
+ */
71
+ export function screenshot2(opts: ScreenshotOptions = {
72
+ mimeType: "image/png",
73
+ transparent: false,
74
+ }): string | null {
75
+
76
+ if (!opts) opts = {}
77
+
78
+ let { context, width, height, mimeType, camera } = opts;
79
+
24
80
  if (!context) {
25
81
  context = ContextRegistry.Current as Context;
26
82
  if (!context) {
@@ -51,7 +107,13 @@
51
107
  context.renderer.domElement.style.width = width + "px";
52
108
  context.renderer.domElement.style.height = height + "px";
53
109
 
110
+ const previousClearColor = context.renderer.getClearColor(new Color());
111
+
54
112
  try {
113
+ if (opts?.transparent) {
114
+ context.renderer.setClearColor(0x000000, 0);
115
+ }
116
+
55
117
  const canvas = context.renderer.domElement;
56
118
 
57
119
  // set the desired output size
@@ -64,14 +126,14 @@
64
126
  // render now
65
127
  context.renderNow();
66
128
 
67
- // const webPMimeType = "image/webp";
68
- // const pngMimeType = "image/png";
129
+ console.log("Screenshot taken", { width, height, mimeType, camera });
69
130
  const dataUrl = canvas.toDataURL(mimeType);
70
131
  return dataUrl;
71
132
  }
72
133
  finally {
73
134
  context.renderer.setSize(prevWidth, prevHeight, false);
74
135
  context.updateSize();
136
+ context.renderer.setClearColor(previousClearColor);
75
137
  }
76
138
 
77
139
  return null;
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isDevEnvironment } from "../../debug/index.js";
2
2
  import type { Context } from "../../engine_context.js";
3
3
  import { hasProLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
4
+ import { isLocalNetwork } from "../../engine_networking_utils.js";
4
5
  import { getParam } from "../../engine_utils.js";
5
6
  import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
6
7
  import { ButtonsFactory } from "../buttons.js";
@@ -108,6 +109,14 @@
108
109
  this._menu.setPosition(position);
109
110
  }
110
111
 
112
+ /**
113
+ * Call to show or hide the menu.
114
+ * NOTE: Hiding the menu is a PRO feature and requires a needle engine license. Hiding the menu will not work in production without a license.
115
+ */
116
+ setVisible(visible: boolean) {
117
+ this._menu.setVisible(visible);
118
+ }
119
+
111
120
  /** When set to false, the Needle Engine logo will be hidden. Hiding the logo requires a needle engine license */
112
121
  showNeedleLogo(visible: boolean) {
113
122
  this._menu.showNeedleLogo(visible);
@@ -458,6 +467,20 @@
458
467
 
459
468
  // watch changes
460
469
  let showInterval = -1;
470
+ let didWarnAboutWrongLicense = 0;
471
+ const forceVisible = (parent) => {
472
+ if (context?.isInAR && context.arOverlayElement) {
473
+ if (parent != context.arOverlayElement) {
474
+ context.arOverlayElement.appendChild(this);
475
+ }
476
+ }
477
+ else if (this.parentNode != this._domElement?.shadowRoot)
478
+ this._domElement?.shadowRoot?.appendChild(this);
479
+ this.style.display = "flex";
480
+ this.style.visibility = "visible";
481
+ this.style.opacity = "1";
482
+ }
483
+
461
484
  const rootObserver = new MutationObserver(mutations => {
462
485
  this.onChangeDetected(mutations);
463
486
 
@@ -465,19 +488,20 @@
465
488
  const requiredParent = this?.parentNode;
466
489
  if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || requiredParent != this._domElement?.shadowRoot) {
467
490
  if (!hasProLicense()) {
468
- clearInterval(showInterval);
469
- showInterval = setInterval(() => {
470
- if (context?.isInAR && context.arOverlayElement) {
471
- if (requiredParent != context.arOverlayElement) {
472
- context.arOverlayElement.appendChild(this);
473
- }
491
+ // if a user doesn't have a local pro license *but* for development the menu is hidden then we show a warning
492
+ if (isLocalNetwork()) {
493
+ // set visible once so that the check above is not triggered again
494
+ if (didWarnAboutWrongLicense === 0)
495
+ forceVisible(requiredParent);
496
+ // warn only once
497
+ if (didWarnAboutWrongLicense++ === 1) {
498
+ console.warn(`Needle Menu Warning: You need a PRO license to hide the Needle Engine menu → The menu will be visible in your deployed website if you don't have a PRO license. See https://needle.tools/pricing for details.`);
474
499
  }
475
- else if (this.parentNode != this._domElement?.shadowRoot)
476
- this._domElement?.shadowRoot?.appendChild(this);
477
- this.style.display = "flex";
478
- this.style.visibility = "visible";
479
- this.style.opacity = "1";
480
- }, 5)
500
+ }
501
+ else {
502
+ clearInterval(showInterval);
503
+ showInterval = setInterval(() => forceVisible(requiredParent), 5)
504
+ }
481
505
  }
482
506
  }
483
507
  });
@@ -509,8 +533,9 @@
509
533
  this._userRequestedLogoVisible = visible;
510
534
  if (!visible) {
511
535
  if (!hasProLicense() || debugNonCommercial) {
512
- if (isDevEnvironment()) console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
513
- return;
536
+ console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
537
+ const localNetwork = isLocalNetwork()
538
+ if (!localNetwork) return;
514
539
  }
515
540
  }
516
541
  this.logoContainer.style.display = visible ? "" : "none";
@@ -525,6 +550,10 @@
525
550
  this.root.classList.add(position);
526
551
  }
527
552
 
553
+ setVisible(visible: boolean) {
554
+ this.style.display = visible ? "flex" : "none";
555
+ }
556
+
528
557
  // private _root: ShadowRoot | null = null;
529
558
  private readonly root: HTMLDivElement;
530
559
  /** wraps the whole content */