@@ -1,232 +0,0 @@
|
|
1
|
-
class ARButton {
|
2
|
-
|
3
|
-
static createButton( renderer, options = {}, beforeRequestSession ) {
|
4
|
-
|
5
|
-
const button = document.createElement( 'button' );
|
6
|
-
let ARButtonControlsDomOverlay = false;
|
7
|
-
|
8
|
-
function showStartAR( /*device*/ ) {
|
9
|
-
|
10
|
-
options.optionalFeatures = options.optionalFeatures || [];
|
11
|
-
|
12
|
-
if ( options.domOverlay === undefined) {
|
13
|
-
|
14
|
-
var overlay = document.createElement( 'div' );
|
15
|
-
overlay.style.display = 'none';
|
16
|
-
document.body.appendChild( overlay );
|
17
|
-
|
18
|
-
var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
|
19
|
-
svg.setAttribute( 'width', 38 );
|
20
|
-
svg.setAttribute( 'height', 38 );
|
21
|
-
svg.style.position = 'absolute';
|
22
|
-
svg.style.right = '20px';
|
23
|
-
svg.style.top = '20px';
|
24
|
-
svg.addEventListener( 'click', function () {
|
25
|
-
|
26
|
-
currentSession.end();
|
27
|
-
|
28
|
-
} );
|
29
|
-
overlay.appendChild( svg );
|
30
|
-
|
31
|
-
var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
|
32
|
-
path.setAttribute( 'd', 'M 12,12 L 28,28 M 28,12 12,28' );
|
33
|
-
path.setAttribute( 'stroke', '#fff' );
|
34
|
-
path.setAttribute( 'stroke-width', 2 );
|
35
|
-
svg.appendChild( path );
|
36
|
-
|
37
|
-
options.optionalFeatures.push( 'dom-overlay' );
|
38
|
-
options.domOverlay = { root: overlay };
|
39
|
-
ARButtonControlsDomOverlay = true;
|
40
|
-
|
41
|
-
}
|
42
|
-
|
43
|
-
//
|
44
|
-
|
45
|
-
let currentSession = null;
|
46
|
-
let originalDomOverlayParent = null;
|
47
|
-
|
48
|
-
async function onSessionStarted( session ) {
|
49
|
-
|
50
|
-
// Workaround: seems WebXR Viewer has a non-standard behaviour when it comes to DOM Overlay and Canvas;
|
51
|
-
// HTMLElements that are inside the Canvas element are not visible in the DOM Overlay.
|
52
|
-
const isWebXRViewer = /WebXRViewer\//i.test( navigator.userAgent );
|
53
|
-
const overlayElement = options.domOverlay.root;
|
54
|
-
if (isWebXRViewer)
|
55
|
-
{
|
56
|
-
if(options.domOverlay?.root) {
|
57
|
-
originalDomOverlayParent = overlayElement.parentNode;
|
58
|
-
if (originalDomOverlayParent)
|
59
|
-
{
|
60
|
-
console.log("Reparent DOM Overlay to body", overlayElement, overlayElement.style.display);
|
61
|
-
// mozilla webxr does hide elements on session start
|
62
|
-
// this is only necessary if we generated the overlay element
|
63
|
-
overlayElement.style.display = "";
|
64
|
-
overlayElement.style.visibility = "";
|
65
|
-
document.body.appendChild(overlayElement);
|
66
|
-
}
|
67
|
-
}
|
68
|
-
else {
|
69
|
-
console.warn("WebXRViewer: No DOM Overlay found");
|
70
|
-
}
|
71
|
-
}
|
72
|
-
|
73
|
-
session.addEventListener( 'end', onSessionEnded );
|
74
|
-
|
75
|
-
// 'local' is head-relative, 'local-floor' is what the device thinks the floor is (e.g. Hololens knows pretty well)
|
76
|
-
// renderer.xr.setReferenceSpaceType( 'local' );
|
77
|
-
|
78
|
-
await renderer.xr.setSession( session );
|
79
|
-
|
80
|
-
button.textContent = 'STOP AR';
|
81
|
-
|
82
|
-
if (ARButtonControlsDomOverlay)
|
83
|
-
options.domOverlay.root.style.display = '';
|
84
|
-
|
85
|
-
currentSession = session;
|
86
|
-
|
87
|
-
}
|
88
|
-
|
89
|
-
function onSessionEnded( /*event*/ ) {
|
90
|
-
|
91
|
-
currentSession.removeEventListener( 'end', onSessionEnded );
|
92
|
-
|
93
|
-
button.textContent = 'START AR';
|
94
|
-
|
95
|
-
const overlayElement = options.domOverlay.root;
|
96
|
-
// if we reparented the DOM overlay, we're reverting it here
|
97
|
-
if (originalDomOverlayParent)
|
98
|
-
originalDomOverlayParent.appendChild(overlayElement);
|
99
|
-
|
100
|
-
if (ARButtonControlsDomOverlay)
|
101
|
-
overlayElement.style.display = 'none';
|
102
|
-
|
103
|
-
currentSession = null;
|
104
|
-
|
105
|
-
}
|
106
|
-
|
107
|
-
//
|
108
|
-
|
109
|
-
button.style.display = '';
|
110
|
-
|
111
|
-
button.style.cursor = 'pointer';
|
112
|
-
button.style.left = 'calc(50% - 50px)';
|
113
|
-
button.style.width = '100px';
|
114
|
-
|
115
|
-
button.textContent = 'START AR';
|
116
|
-
|
117
|
-
button.onmouseenter = function () {
|
118
|
-
|
119
|
-
button.style.opacity = '1.0';
|
120
|
-
|
121
|
-
};
|
122
|
-
|
123
|
-
button.onmouseleave = function () {
|
124
|
-
|
125
|
-
button.style.opacity = '0.5';
|
126
|
-
|
127
|
-
};
|
128
|
-
|
129
|
-
button.onclick = function () {
|
130
|
-
|
131
|
-
if ( currentSession === null ) {
|
132
|
-
beforeRequestSession?.call(this, options);
|
133
|
-
navigator.xr.requestSession( 'immersive-ar', options ).then( onSessionStarted );
|
134
|
-
|
135
|
-
} else {
|
136
|
-
|
137
|
-
currentSession.end();
|
138
|
-
|
139
|
-
}
|
140
|
-
|
141
|
-
};
|
142
|
-
|
143
|
-
}
|
144
|
-
|
145
|
-
function disableButton() {
|
146
|
-
|
147
|
-
button.disabled = true;
|
148
|
-
|
149
|
-
button.style.display = '';
|
150
|
-
|
151
|
-
button.style.cursor = 'auto';
|
152
|
-
button.style.left = 'calc(50% - 75px)';
|
153
|
-
button.style.width = '150px';
|
154
|
-
|
155
|
-
button.onmouseenter = null;
|
156
|
-
button.onmouseleave = null;
|
157
|
-
|
158
|
-
button.onclick = null;
|
159
|
-
|
160
|
-
}
|
161
|
-
|
162
|
-
function showARNotSupported() {
|
163
|
-
|
164
|
-
disableButton();
|
165
|
-
|
166
|
-
button.textContent = 'AR NOT SUPPORTED';
|
167
|
-
|
168
|
-
}
|
169
|
-
|
170
|
-
function stylizeElement( element ) {
|
171
|
-
|
172
|
-
element.style.position = 'absolute';
|
173
|
-
element.style.bottom = '20px';
|
174
|
-
element.style.padding = '12px 6px';
|
175
|
-
element.style.border = '1px solid #fff';
|
176
|
-
element.style.borderRadius = '4px';
|
177
|
-
element.style.background = 'rgba(0,0,0,0.1)';
|
178
|
-
element.style.color = '#fff';
|
179
|
-
element.style.font = 'normal 13px sans-serif';
|
180
|
-
element.style.textAlign = 'center';
|
181
|
-
element.style.opacity = '0.5';
|
182
|
-
element.style.outline = 'none';
|
183
|
-
element.style.zIndex = '999';
|
184
|
-
|
185
|
-
}
|
186
|
-
|
187
|
-
if ( 'xr' in navigator ) {
|
188
|
-
|
189
|
-
button.id = 'ARButton';
|
190
|
-
button.style.display = 'none';
|
191
|
-
|
192
|
-
stylizeElement( button );
|
193
|
-
|
194
|
-
navigator.xr.isSessionSupported( 'immersive-ar' ).then( function ( supported ) {
|
195
|
-
|
196
|
-
supported ? showStartAR() : showARNotSupported();
|
197
|
-
|
198
|
-
} ).catch( showARNotSupported );
|
199
|
-
|
200
|
-
return button;
|
201
|
-
|
202
|
-
} else {
|
203
|
-
|
204
|
-
const message = document.createElement( 'a' );
|
205
|
-
|
206
|
-
if ( window.isSecureContext === false ) {
|
207
|
-
|
208
|
-
message.href = document.location.href.replace( /^http:/, 'https:' );
|
209
|
-
message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
|
210
|
-
|
211
|
-
} else {
|
212
|
-
|
213
|
-
message.href = 'https://immersiveweb.dev/';
|
214
|
-
message.innerHTML = 'WEBXR NOT AVAILABLE';
|
215
|
-
|
216
|
-
}
|
217
|
-
|
218
|
-
message.style.left = 'calc(50% - 90px)';
|
219
|
-
message.style.width = '180px';
|
220
|
-
message.style.textDecoration = 'none';
|
221
|
-
|
222
|
-
stylizeElement( message );
|
223
|
-
|
224
|
-
return message;
|
225
|
-
|
226
|
-
}
|
227
|
-
|
228
|
-
}
|
229
|
-
|
230
|
-
}
|
231
|
-
|
232
|
-
export { ARButton };
|
@@ -41,12 +41,6 @@
|
|
41
41
|
/** @param {import ('next').NextConfig config } */
|
42
42
|
function nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
|
43
43
|
|
44
|
-
// workaround: fix type finding in production builds
|
45
|
-
if (config.optimization.minimize) {
|
46
|
-
console.log("WARN: 😳 disabling minimization for custom scripts. If you don't use custom scripts in your webproject you can enable minimize in the webpack callback like so: needleNext({webpack: (config) => { config.optimization.minimize = true; return config; }})\n")
|
47
|
-
config.optimization.minimize = false;
|
48
|
-
}
|
49
|
-
|
50
44
|
const meta = getMeta();
|
51
45
|
let useRapier = true;
|
52
46
|
if (userSettings.useRapier === false) useRapier = false;
|
@@ -1,195 +0,0 @@
|
|
1
|
-
class VRButton {
|
2
|
-
|
3
|
-
static createButton( renderer, options ) {
|
4
|
-
|
5
|
-
const button = document.createElement( 'button' );
|
6
|
-
|
7
|
-
function showEnterVR( /*device*/ ) {
|
8
|
-
|
9
|
-
let currentSession = null;
|
10
|
-
|
11
|
-
async function onSessionStarted( session ) {
|
12
|
-
|
13
|
-
session.addEventListener( 'end', onSessionEnded );
|
14
|
-
|
15
|
-
// console.log("Session started, features: ",session)
|
16
|
-
|
17
|
-
await renderer.xr.setSession( session );
|
18
|
-
button.textContent = 'EXIT VR';
|
19
|
-
|
20
|
-
currentSession = session;
|
21
|
-
|
22
|
-
}
|
23
|
-
|
24
|
-
function onSessionEnded( /*event*/ ) {
|
25
|
-
|
26
|
-
currentSession.removeEventListener( 'end', onSessionEnded );
|
27
|
-
|
28
|
-
button.textContent = 'ENTER VR';
|
29
|
-
|
30
|
-
currentSession = null;
|
31
|
-
|
32
|
-
}
|
33
|
-
|
34
|
-
//
|
35
|
-
|
36
|
-
button.style.display = '';
|
37
|
-
|
38
|
-
button.style.cursor = 'pointer';
|
39
|
-
button.style.left = 'calc(50% - 50px)';
|
40
|
-
button.style.width = '100px';
|
41
|
-
|
42
|
-
button.textContent = 'ENTER VR';
|
43
|
-
|
44
|
-
button.onmouseenter = function () {
|
45
|
-
|
46
|
-
button.style.opacity = '1.0';
|
47
|
-
|
48
|
-
};
|
49
|
-
|
50
|
-
button.onmouseleave = function () {
|
51
|
-
|
52
|
-
button.style.opacity = '0.5';
|
53
|
-
|
54
|
-
};
|
55
|
-
|
56
|
-
button.onclick = function () {
|
57
|
-
|
58
|
-
if ( currentSession === null ) {
|
59
|
-
|
60
|
-
// WebXR's requestReferenceSpace only works if the corresponding feature
|
61
|
-
// was requested at session creation time. For simplicity, just ask for
|
62
|
-
// the interesting ones as optional features, but be aware that the
|
63
|
-
// requestReferenceSpace call will fail if it turns out to be unavailable.
|
64
|
-
// ('local' is always available for immersive sessions and doesn't need to
|
65
|
-
// be requested separately.)
|
66
|
-
|
67
|
-
const sessionInit = { optionalFeatures: options.optionalFeatures };
|
68
|
-
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
|
69
|
-
|
70
|
-
} else {
|
71
|
-
|
72
|
-
currentSession.end();
|
73
|
-
|
74
|
-
}
|
75
|
-
|
76
|
-
};
|
77
|
-
|
78
|
-
}
|
79
|
-
|
80
|
-
function disableButton() {
|
81
|
-
|
82
|
-
button.disabled = true;
|
83
|
-
|
84
|
-
button.style.display = '';
|
85
|
-
|
86
|
-
button.style.cursor = 'auto';
|
87
|
-
button.style.left = 'calc(50% - 75px)';
|
88
|
-
button.style.width = '150px';
|
89
|
-
|
90
|
-
button.onmouseenter = null;
|
91
|
-
button.onmouseleave = null;
|
92
|
-
|
93
|
-
button.onclick = null;
|
94
|
-
|
95
|
-
}
|
96
|
-
|
97
|
-
function showWebXRNotFound() {
|
98
|
-
|
99
|
-
disableButton();
|
100
|
-
|
101
|
-
button.textContent = 'VR NOT SUPPORTED';
|
102
|
-
|
103
|
-
}
|
104
|
-
|
105
|
-
function stylizeElement( element ) {
|
106
|
-
|
107
|
-
element.style.position = 'absolute';
|
108
|
-
element.style.bottom = '20px';
|
109
|
-
element.style.padding = '12px 6px';
|
110
|
-
element.style.border = '1px solid #fff';
|
111
|
-
element.style.borderRadius = '4px';
|
112
|
-
element.style.background = 'rgba(0,0,0,0.1)';
|
113
|
-
element.style.color = '#fff';
|
114
|
-
element.style.font = 'normal 13px sans-serif';
|
115
|
-
element.style.textAlign = 'center';
|
116
|
-
element.style.opacity = '0.5';
|
117
|
-
element.style.outline = 'none';
|
118
|
-
element.style.zIndex = '999';
|
119
|
-
|
120
|
-
}
|
121
|
-
|
122
|
-
if ( 'xr' in navigator ) {
|
123
|
-
|
124
|
-
button.id = 'VRButton';
|
125
|
-
button.style.display = 'none';
|
126
|
-
|
127
|
-
stylizeElement( button );
|
128
|
-
|
129
|
-
navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) {
|
130
|
-
|
131
|
-
supported ? showEnterVR() : showWebXRNotFound();
|
132
|
-
|
133
|
-
if(VRButton.xrSessionIsGranted){
|
134
|
-
console.log("XR session is granted - will enter immersive web now")
|
135
|
-
button.click();
|
136
|
-
}
|
137
|
-
|
138
|
-
} );
|
139
|
-
|
140
|
-
return button;
|
141
|
-
|
142
|
-
} else {
|
143
|
-
|
144
|
-
const message = document.createElement( 'a' );
|
145
|
-
|
146
|
-
if ( window.isSecureContext === false ) {
|
147
|
-
|
148
|
-
message.href = document.location.href.replace( /^http:/, 'https:' );
|
149
|
-
message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
|
150
|
-
|
151
|
-
} else {
|
152
|
-
|
153
|
-
message.href = 'https://immersiveweb.dev/';
|
154
|
-
message.innerHTML = 'WEBXR NOT AVAILABLE';
|
155
|
-
|
156
|
-
}
|
157
|
-
|
158
|
-
message.style.left = 'calc(50% - 90px)';
|
159
|
-
message.style.width = '180px';
|
160
|
-
message.style.textDecoration = 'none';
|
161
|
-
|
162
|
-
stylizeElement( message );
|
163
|
-
|
164
|
-
return message;
|
165
|
-
|
166
|
-
}
|
167
|
-
|
168
|
-
}
|
169
|
-
|
170
|
-
|
171
|
-
static xrSessionIsGranted = false;
|
172
|
-
|
173
|
-
static registerSessionGrantedListener() {
|
174
|
-
|
175
|
-
if ( 'xr' in navigator ) {
|
176
|
-
|
177
|
-
// WebXRViewer (based on Firefox) has a bug where addEventListener
|
178
|
-
// throws a silent exception and aborts execution entirely.
|
179
|
-
if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return;
|
180
|
-
|
181
|
-
navigator.xr.addEventListener( 'sessiongranted', () => {
|
182
|
-
|
183
|
-
VRButton.xrSessionIsGranted = true;
|
184
|
-
|
185
|
-
} );
|
186
|
-
|
187
|
-
}
|
188
|
-
|
189
|
-
}
|
190
|
-
|
191
|
-
}
|
192
|
-
|
193
|
-
VRButton.registerSessionGrantedListener();
|
194
|
-
|
195
|
-
export { VRButton };
|
@@ -1,10 +1,13 @@
|
|
1
|
-
import { BrightnessContrastEffect, HueSaturationEffect } from "postprocessing";
|
2
|
-
import { LinearToneMapping, NoToneMapping } from "three";
|
1
|
+
import { BrightnessContrastEffect, HueSaturationEffect, ToneMappingEffect, ToneMappingMode } from "postprocessing";
|
2
|
+
import { ACESFilmicToneMapping, AgXToneMapping, LinearToneMapping, NoToneMapping } from "three";
|
3
3
|
|
4
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
|
+
import { GameObject } from "../../Component.js";
|
5
6
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
7
|
+
import { Volume } from "../Volume.js";
|
6
8
|
import { VolumeParameter } from "../VolumeParameter.js";
|
7
9
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
10
|
+
import { ToneMapping } from "./Tonemapping.js";
|
8
11
|
|
9
12
|
|
10
13
|
export class ColorAdjustments extends PostProcessingEffect {
|
@@ -27,7 +30,7 @@
|
|
27
30
|
|
28
31
|
init() {
|
29
32
|
this.postExposure!.valueProcessor = v => {
|
30
|
-
v = Math.pow(2, v);
|
33
|
+
v = Math.pow(2.0, v);
|
31
34
|
return v;
|
32
35
|
}
|
33
36
|
this.postExposure.defaultValue = 0;
|
@@ -47,36 +50,75 @@
|
|
47
50
|
|
48
51
|
this.saturation.valueProcessor = (v: number) => {
|
49
52
|
if (v < 0) return (v / 100);
|
50
|
-
|
53
|
+
const sat = (v / (100 * Math.PI));
|
54
|
+
return sat;
|
51
55
|
}
|
52
56
|
this.saturation.defaultValue = 0;
|
53
57
|
}
|
54
58
|
|
55
59
|
unapply() {
|
56
|
-
|
60
|
+
// reset to the current value in the toneMappingEffect if that exists, else Linear
|
61
|
+
const currentMode: any = this.toneMappingEffect?.mode.value;
|
62
|
+
const newMode = this.toneMappingEffect?.getThreeToneMapping(currentMode)
|
63
|
+
this.context.renderer.toneMapping = newMode ?? LinearToneMapping;
|
57
64
|
}
|
58
65
|
|
66
|
+
private threeToneMappingToEffectMode(mode: number | undefined) {
|
67
|
+
switch (mode) {
|
68
|
+
case LinearToneMapping: return ToneMappingMode.ACES_FILMIC; // TODO, no Linear mode in postprocessing, see https://github.com/pmndrs/postprocessing/issues/605
|
69
|
+
case ACESFilmicToneMapping: return ToneMappingMode.ACES_FILMIC;
|
70
|
+
case AgXToneMapping: return ToneMappingMode.AGX;
|
71
|
+
default: return ToneMappingMode.ACES_FILMIC; // TODO, no Linear mode in postprocessing, see https://github.com/pmndrs/postprocessing/issues/605
|
72
|
+
}
|
73
|
+
}
|
59
74
|
|
75
|
+
private toneMappingEffect: ToneMapping | null = null;
|
60
76
|
onCreateEffect(): EffectProviderResult {
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
77
|
+
const effects: EffectProviderResult = [];
|
78
|
+
|
79
|
+
if (this.context.renderer.toneMapping !== NoToneMapping && this.postExposure.overrideState)
|
80
|
+
this.context.renderer.toneMapping = NoToneMapping;
|
81
|
+
|
82
|
+
|
83
|
+
// workaround: find the ToneMapping effect in the scene so we can apply the mode
|
84
|
+
this.toneMappingEffect = GameObject.findObjectOfType(Volume)?.sharedProfile?.components.find(c => c.typeName === "ToneMapping") as ToneMapping | null;
|
85
|
+
const currentMode: any = this.toneMappingEffect?.mode.value;
|
86
|
+
const expectedThreeMode = this.toneMappingEffect?.getThreeToneMapping(currentMode);
|
87
|
+
|
88
|
+
// We need this effect if someone uses ACES or AgX tonemapping;
|
89
|
+
// problem is that we CAN'T use this effect for the "Linear" case, the package expects that in this case you remove the effect
|
90
|
+
const tonemapping = new ToneMappingEffect({
|
91
|
+
mode: this.threeToneMappingToEffectMode(expectedThreeMode),
|
92
|
+
// middleGrey: 0.5,
|
93
|
+
// averageLuminance: 1,
|
94
|
+
});
|
95
|
+
|
96
|
+
const apply = (v) => {
|
67
97
|
if (this.postExposure.overrideState)
|
68
98
|
this.context.renderer.toneMappingExposure = v;
|
99
|
+
|
100
|
+
// this is a workaround so that we can apply tonemapping options – no access to the ToneMappingEffect instance from the Tonemapping effect right now...
|
101
|
+
const currentMode = this.toneMappingEffect?.mode.value;
|
102
|
+
tonemapping.mode = this.threeToneMappingToEffectMode(this.toneMappingEffect?.getThreeToneMapping(currentMode));
|
69
103
|
}
|
70
|
-
|
71
|
-
|
104
|
+
|
105
|
+
this.postExposure!.onValueChanged = v => {
|
106
|
+
apply(v);
|
72
107
|
}
|
73
108
|
|
109
|
+
const brightnesscontrast = new BrightnessContrastEffect();
|
110
|
+
this.contrast!.onValueChanged = v => brightnesscontrast.contrast = v;
|
74
111
|
|
75
112
|
const hueSaturationEffect = new HueSaturationEffect();
|
113
|
+
|
114
|
+
effects.push(brightnesscontrast);
|
115
|
+
effects.push(hueSaturationEffect);
|
116
|
+
effects.push(tonemapping);
|
117
|
+
|
76
118
|
this.hueShift!.onValueChanged = v => hueSaturationEffect.hue = v;
|
77
119
|
this.saturation!.onValueChanged = v => hueSaturationEffect.saturation = v;
|
78
120
|
|
79
|
-
return
|
121
|
+
return effects;
|
80
122
|
}
|
81
123
|
|
82
124
|
// apply() {
|
@@ -87,6 +87,7 @@
|
|
87
87
|
export { Image } from "../ui/Image.js";
|
88
88
|
export { InheritVelocityModule } from "../ParticleSystemModules.js";
|
89
89
|
export { InputField } from "../ui/InputField.js";
|
90
|
+
export { Interactable } from "../Interactable.js";
|
90
91
|
export { Light } from "../Light.js";
|
91
92
|
export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
|
92
93
|
export { LODGroup } from "../LODGroup.js";
|
@@ -100,7 +101,7 @@
|
|
100
101
|
export { MeshRenderer } from "../Renderer.js";
|
101
102
|
export { MinMaxCurve } from "../ParticleSystemModules.js";
|
102
103
|
export { MinMaxGradient } from "../ParticleSystemModules.js";
|
103
|
-
export {
|
104
|
+
export { NeedleMenu } from "../NeedleMenu.js";
|
104
105
|
export { NestedGltf } from "../NestedGltf.js";
|
105
106
|
export { Networking } from "../Networking.js";
|
106
107
|
export { NoiseModule } from "../ParticleSystemModules.js";
|
@@ -199,6 +200,7 @@
|
|
199
200
|
export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
|
200
201
|
export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
|
201
202
|
export { WebXR } from "../webxr/WebXR.js";
|
203
|
+
export { WebXRButtonFactory } from "../webxr/WebXRButtons.js";
|
202
204
|
export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
|
203
205
|
export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
|
204
206
|
export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
|
@@ -144,10 +144,10 @@
|
|
144
144
|
|
145
145
|
const logsContainerStyles = `
|
146
146
|
|
147
|
-
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@
|
147
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');
|
148
148
|
|
149
149
|
div[data-needle_engine_debug_overlay] {
|
150
|
-
font-family: 'Roboto', sans-serif;
|
150
|
+
font-family: 'Roboto Flex', sans-serif;
|
151
151
|
font-weight: 400;
|
152
152
|
}
|
153
153
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Layers, Material, Matrix3, Matrix4, Mesh, Object3D, PerspectiveCamera, Quaternion, Texture, Vector2, Vector3, Vector4 } from "three";
|
2
|
-
import ThreeMeshUI from "three-mesh-ui";
|
2
|
+
import ThreeMeshUI, { Text } from "three-mesh-ui";
|
3
3
|
import type { Options } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement";
|
4
4
|
|
5
5
|
import { ContextRegistry } from "../engine_context_registry.js";
|
@@ -57,7 +57,7 @@
|
|
57
57
|
private readonly targetObject = new Object3D();
|
58
58
|
/** this is a point in forward view of the user */
|
59
59
|
private readonly userForwardViewPoint = new Vector3();
|
60
|
-
private readonly oneEuroFilter = new OneEuroFilterXYZ(90, .
|
60
|
+
private readonly oneEuroFilter = new OneEuroFilterXYZ(90, .8);
|
61
61
|
private _lastElementRemoveTime = 0;
|
62
62
|
|
63
63
|
private onBeforeRender = () => {
|
@@ -76,7 +76,7 @@
|
|
76
76
|
const dist = 3.5;
|
77
77
|
const forward = cam.worldForward;
|
78
78
|
forward.y = 0;
|
79
|
-
forward.normalize().multiplyScalar(
|
79
|
+
forward.normalize().multiplyScalar(dist);
|
80
80
|
this.userForwardViewPoint.copy(cam.worldPosition).sub(forward);
|
81
81
|
|
82
82
|
const distFromForwardView = this.targetObject.position.distanceTo(this.userForwardViewPoint);
|
@@ -88,7 +88,7 @@
|
|
88
88
|
|
89
89
|
this.oneEuroFilter.filter(this.targetObject.position, root.position, this.context.time.time);
|
90
90
|
const step = this.context.time.deltaTime;
|
91
|
-
root.quaternion.slerp(this.targetObject.quaternion, step);
|
91
|
+
root.quaternion.slerp(this.targetObject.quaternion, step * 5);
|
92
92
|
|
93
93
|
this.targetObject.removeFromParent();
|
94
94
|
this.context.scene.add(this.getRoot() as any);
|
@@ -174,7 +174,7 @@
|
|
174
174
|
};
|
175
175
|
private readonly _textBuffer: ThreeMeshUI.Text[] = [];
|
176
176
|
private readonly _activeTexts: ThreeMeshUI.Text[] = [];
|
177
|
-
private getText() {
|
177
|
+
private getText(): Text {
|
178
178
|
const root = this.getRoot();
|
179
179
|
if (this._textBuffer.length > 0) {
|
180
180
|
const text = this._textBuffer.pop() as any as ThreeMeshUI.Text;
|
@@ -182,9 +182,9 @@
|
|
182
182
|
setTimeout(() => this.disableDepthTestRecursive(text as any), 100);
|
183
183
|
return text;
|
184
184
|
}
|
185
|
-
if (root.children.length > 20) {
|
186
|
-
const active = this._activeTexts
|
187
|
-
return active
|
185
|
+
if (root.children.length > 20 && this._activeTexts.length > 0) {
|
186
|
+
const active = this._activeTexts.shift();
|
187
|
+
return active!;
|
188
188
|
}
|
189
189
|
const newText = new ThreeMeshUI.Text(this.textOptions);
|
190
190
|
setTimeout(() => this.disableDepthTestRecursive(newText as any), 500);
|
@@ -195,6 +195,7 @@
|
|
195
195
|
obj.traverseVisible((t: Object3D) => {
|
196
196
|
t.renderOrder = 1000;
|
197
197
|
t.layers.set(2);
|
198
|
+
t.position.z = .05;
|
198
199
|
const mat = (t as Mesh).material as Material;
|
199
200
|
if (mat) {
|
200
201
|
mat.depthWrite = false;
|
@@ -365,7 +365,7 @@
|
|
365
365
|
class AddressableSerializer extends TypeSerializer {
|
366
366
|
|
367
367
|
constructor() {
|
368
|
-
super([AssetReference]);
|
368
|
+
super([AssetReference], "AssetReferenceSerializer");
|
369
369
|
}
|
370
370
|
|
371
371
|
onSerialize(data: any, _context: SerializationContext) {
|
@@ -498,7 +498,7 @@
|
|
498
498
|
|
499
499
|
export class ImageReferenceSerializer extends TypeSerializer {
|
500
500
|
constructor() {
|
501
|
-
super([ImageReference]);
|
501
|
+
super([ImageReference], "ImageReferenceSerializer");
|
502
502
|
}
|
503
503
|
|
504
504
|
onSerialize(_data: string, _context: SerializationContext) {
|
@@ -561,7 +561,7 @@
|
|
561
561
|
|
562
562
|
export class FileReferenceSerializer extends TypeSerializer {
|
563
563
|
constructor() {
|
564
|
-
super([FileReference]);
|
564
|
+
super([FileReference], "FileReferenceSerializer");
|
565
565
|
}
|
566
566
|
|
567
567
|
onSerialize(_data: string, _context: SerializationContext) {
|
@@ -9,13 +9,12 @@
|
|
9
9
|
import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
|
10
10
|
import { getParam } from "./engine_utils.js";
|
11
11
|
|
12
|
+
|
12
13
|
const debug = getParam("debuggetcomponent");
|
14
|
+
const debugEnabled = () => {
|
15
|
+
return debug || globalThis["NEEDLE_DEBUG_GETCOMPONENT"] === true;
|
16
|
+
}
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
18
|
function tryGetObject(obj) {
|
20
19
|
if (obj === null || obj === undefined) return obj;
|
21
20
|
if (obj.isObject3D) return obj;
|
@@ -140,32 +139,26 @@
|
|
140
139
|
console.warn(`Accessing components by name is not supported.\nPlease use the component type instead. This may keep working in local development but it will fail when bundling your application.\n\nYou can import other modules your main module to get access to types\nor if you use npmdefs you can make types available globally using globalThis:\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis`, componentType);
|
141
140
|
}
|
142
141
|
}
|
143
|
-
|
144
|
-
|
145
|
-
|
142
|
+
|
143
|
+
if (debugEnabled())
|
144
|
+
console.log("[onGetComponent] FIND", componentType);
|
145
|
+
|
146
|
+
if (componentType === undefined || componentType === null)
|
147
|
+
return null;
|
148
|
+
|
149
|
+
// find component
|
146
150
|
for (let i = 0; i < obj.userData.components.length; i++) {
|
147
151
|
const component = obj.userData.components[i];
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
}
|
154
|
-
}
|
155
|
-
// find in base classes
|
156
|
-
for (let i = 0; i < obj.userData.components.length; i++) {
|
157
|
-
const component = obj.userData.components[i];
|
158
|
-
let parent = Object.getPrototypeOf(component.constructor);
|
159
|
-
do {
|
160
|
-
if (parent === componentType) {
|
161
|
-
if (debug)
|
162
|
-
console.log("MATCH BY PROTOYPE", parent);
|
152
|
+
let prototype = Object.getPrototypeOf(component);
|
153
|
+
while (prototype) {
|
154
|
+
if (prototype === componentType.prototype) {
|
155
|
+
if (debugEnabled())
|
156
|
+
console.log("[onGetComponent] MATCH BY PROTOYPE", prototype);
|
163
157
|
if (arr) arr.push(component);
|
164
158
|
else return component;
|
165
159
|
}
|
166
|
-
|
160
|
+
prototype = Object.getPrototypeOf(prototype);
|
167
161
|
}
|
168
|
-
while (parent);
|
169
162
|
}
|
170
163
|
if (!arr) return null;
|
171
164
|
return arr;
|
@@ -31,6 +31,7 @@
|
|
31
31
|
import * as utils from "./engine_utils.js";
|
32
32
|
import { delay, getParam } from './engine_utils.js';
|
33
33
|
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
34
|
+
import { NeedleMenu, NeedleMenuElement } from './webcomponents/needle-menu.js';
|
34
35
|
|
35
36
|
|
36
37
|
const debug = utils.getParam("debugcontext");
|
@@ -128,6 +129,7 @@
|
|
128
129
|
private static _defaultWebglRendererParameters: WebGLRendererParameters = {
|
129
130
|
antialias: true,
|
130
131
|
alpha: false,
|
132
|
+
powerPreference: "high-performance",
|
131
133
|
};
|
132
134
|
static get DefaultWebGLRendererParameters(): WebGLRendererParameters {
|
133
135
|
return Context._defaultWebglRendererParameters;
|
@@ -257,8 +259,8 @@
|
|
257
259
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
|
258
260
|
*/
|
259
261
|
get xrFrame() { return this._xrFrame }
|
260
|
-
/** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
|
261
|
-
get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera()
|
262
|
+
/** @returns the current WebXR camera while the WebXRManager is active (shorthand for `context.renderer.xr.getCamera()`) */
|
263
|
+
get xrCamera(): WebXRArrayCamera | undefined { return this.renderer.xr.isPresenting ? this.renderer?.xr?.getCamera() : undefined }
|
262
264
|
private _xrFrame: XRFrame | null = null;
|
263
265
|
get arOverlayElement(): HTMLElement {
|
264
266
|
const el = this.domElement as any;
|
@@ -346,6 +348,7 @@
|
|
346
348
|
addressables: Addressables;
|
347
349
|
lightmaps: ILightDataRegistry;
|
348
350
|
players: PlayerViewManager;
|
351
|
+
readonly menu: NeedleMenu;
|
349
352
|
|
350
353
|
get isCreated() { return this._isCreated; }
|
351
354
|
|
@@ -381,6 +384,7 @@
|
|
381
384
|
this.addressables = new Addressables(this);
|
382
385
|
this.lightmaps = new LightDataRegistry(this);
|
383
386
|
this.players = new PlayerViewManager(this);
|
387
|
+
this.menu = new NeedleMenu(this);
|
384
388
|
|
385
389
|
|
386
390
|
const resizeCallback = () => this._sizeChanged = true;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { needleLogoOnlySVG } from "./assets/index.js"
|
2
2
|
import { showBalloonWarning } from "./debug/index.js";
|
3
3
|
import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
|
4
4
|
import { Mathf } from "./engine_math.js";
|
@@ -244,7 +244,7 @@
|
|
244
244
|
setTimeout(() => {
|
245
245
|
logo.style.transform = "translateY(0px)";
|
246
246
|
}, 1);
|
247
|
-
logo.src =
|
247
|
+
logo.src = needleLogoOnlySVG;
|
248
248
|
let isUsingCustomLogo = false;
|
249
249
|
if (hasLicense && this._element) {
|
250
250
|
const customLogo = this._element.getAttribute("loading-logo-src");
|
@@ -95,10 +95,19 @@
|
|
95
95
|
// TODO: do we want to rename this event?
|
96
96
|
this.addEventListener("ready", this.onReady);
|
97
97
|
|
98
|
+
// Workaround for font loading not being supported in ShadowDOM:
|
99
|
+
// Add font import to document header.
|
100
|
+
// Note that this is slower than it could be, ideally the font would be prefetched,
|
101
|
+
// but for that it needs to be in the actual document and not added by JS.
|
102
|
+
const fontLink = document.createElement("link");
|
103
|
+
fontLink.href = "https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,[email protected],100..1000&display=swap";
|
104
|
+
fontLink.rel = "stylesheet";
|
105
|
+
document.head.appendChild(fontLink);
|
106
|
+
|
98
107
|
this.attachShadow({ mode: 'open' });
|
99
108
|
const template = document.createElement('template');
|
100
109
|
template.innerHTML = `<style>
|
101
|
-
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@
|
110
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');
|
102
111
|
|
103
112
|
:host {
|
104
113
|
position: absolute;
|
@@ -897,7 +897,7 @@
|
|
897
897
|
}
|
898
898
|
if (debug) console.log(evt.pointerType, "DOWN", index);
|
899
899
|
if (!this.isInRect(evt)) return;
|
900
|
-
if (this.
|
900
|
+
if (this.isMouseEventFromTouch(evt)) return;
|
901
901
|
|
902
902
|
this.setPointerState(index, this._pointerPressed, true);
|
903
903
|
this.setPointerState(index, this._pointerDown, true);
|
@@ -925,7 +925,7 @@
|
|
925
925
|
const isDown = this.getPointerPressed(index);
|
926
926
|
if (isDown === false && !this.isInRect(evt)) return;
|
927
927
|
if (evt.pointerType === PointerType.Touch && !isDown) return;
|
928
|
-
if (this.
|
928
|
+
if (this.isMouseEventFromTouch(evt)) return;
|
929
929
|
if (debug) console.log(evt.pointerType, "MOVE", index, "hasSpace=" + evt.space != null);
|
930
930
|
|
931
931
|
this.updatePointerPosition(evt);
|
@@ -939,7 +939,7 @@
|
|
939
939
|
if (debug) console.log(evt.pointerType, "UP", index, "was not down");
|
940
940
|
return;
|
941
941
|
}
|
942
|
-
if (this.
|
942
|
+
if (this.isMouseEventFromTouch(evt)) return;
|
943
943
|
if (debug) console.log(evt.pointerType, "UP", index);
|
944
944
|
|
945
945
|
this.setPointerState(index, this._pointerPressed, false);
|
@@ -981,7 +981,7 @@
|
|
981
981
|
this.onDispatchEvent(evt);
|
982
982
|
}
|
983
983
|
|
984
|
-
private
|
984
|
+
private isMouseEventFromTouch(evt: NEPointerEvent) {
|
985
985
|
if (evt.pointerType === PointerType.Mouse) {
|
986
986
|
const lastPointer = this._pointerUpTimestamp.map((v, i) => ({ timestamp: v, index: i })).sort((a, b) => a.timestamp - b.timestamp).at(-1);
|
987
987
|
if (lastPointer && this._pointerTypes[lastPointer.index] === PointerType.Touch) {
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { showBalloonError, showBalloonWarning } from "./debug/index.js";
|
2
2
|
import { GENERATOR, VERSION } from "./engine_constants.js";
|
3
3
|
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
4
4
|
import type { IContext } from "./engine_types.js";
|
5
5
|
import { getParam, isMobileDevice } from "./engine_utils.js";
|
6
|
+
import { LicenseBanner } from "./webcomponents/license-banner.js";
|
6
7
|
|
7
8
|
const debug = getParam("debuglicense");
|
8
9
|
|
@@ -32,6 +33,22 @@
|
|
32
33
|
return hasProLicense() || hasIndieLicense();
|
33
34
|
}
|
34
35
|
|
36
|
+
const _licenseCheckResultChangedCallbacks: ((result: boolean) => void)[] = [];
|
37
|
+
export function onLicenseCheckResultChanged(cb: (result: boolean) => void) {
|
38
|
+
if (hasProLicense() || hasIndieLicense())
|
39
|
+
return cb(true);
|
40
|
+
_licenseCheckResultChangedCallbacks.push(cb);
|
41
|
+
}
|
42
|
+
function invokeLicenseCheckResultChanged(result: boolean) {
|
43
|
+
for (const cb of _licenseCheckResultChangedCallbacks) {
|
44
|
+
try {
|
45
|
+
cb(result);
|
46
|
+
}
|
47
|
+
catch {
|
48
|
+
// ignore
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
35
52
|
|
36
53
|
ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
|
37
54
|
showLicenseInfo(evt.context);
|
@@ -58,6 +75,7 @@
|
|
58
75
|
applicationIsForbidden = false;
|
59
76
|
if (debug) console.log("License check succeeded");
|
60
77
|
NEEDLE_ENGINE_LICENSE_TYPE = "pro";
|
78
|
+
invokeLicenseCheckResultChanged(true);
|
61
79
|
}
|
62
80
|
else if (res?.status === 403) {
|
63
81
|
applicationIsForbidden = true;
|
@@ -134,9 +152,6 @@
|
|
134
152
|
}
|
135
153
|
|
136
154
|
|
137
|
-
const licenseElementIdentifier = "needle-license-element";
|
138
|
-
const licenseDuration = 10000;
|
139
|
-
const licenseDelay = 1200;
|
140
155
|
|
141
156
|
async function onNonCommercialVersionDetected(ctx: IContext) {
|
142
157
|
// if the engine loads faster than the license check, we need to capture the ready event here
|
@@ -148,78 +163,70 @@
|
|
148
163
|
logNonCommercialUse();
|
149
164
|
|
150
165
|
// check if the engine is already ready (meaning has finished loading)
|
151
|
-
if (isReady) {
|
152
|
-
|
153
|
-
}
|
154
|
-
else {
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
}
|
166
|
+
// if (isReady) {
|
167
|
+
// insertNonCommercialUseHint(ctx);
|
168
|
+
// }
|
169
|
+
// else {
|
170
|
+
// ctx.domElement.addEventListener("ready", () => {
|
171
|
+
// insertNonCommercialUseHint(ctx);
|
172
|
+
// });
|
173
|
+
// }
|
159
174
|
}
|
160
175
|
|
161
|
-
|
162
|
-
|
163
|
-
|
176
|
+
// const licenseElementIdentifier = "needle-license-element";
|
177
|
+
// const licenseDuration = 10000;
|
178
|
+
// const licenseDelay = 1200;
|
179
|
+
// function insertNonCommercialUseHint(ctx: IContext) {
|
164
180
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
181
|
+
// return;
|
182
|
+
// const banner = LicenseBanner.create();
|
183
|
+
// ctx.domElement.shadowRoot?.appendChild(banner);
|
184
|
+
// let bannerStyle = `
|
185
|
+
// position: absolute;
|
186
|
+
// bottom: 20px;
|
187
|
+
// right: 20px;
|
188
|
+
// opacity: 0;
|
189
|
+
// transform: translateY(10px);
|
190
|
+
// transition: all .5s ease-in-out 1s;
|
191
|
+
// pointer: cursor;
|
192
|
+
// `;
|
193
|
+
// banner.style.cssText = bannerStyle;
|
194
|
+
// let expectedBannerStyle = banner.style.cssText;
|
195
|
+
// setTimeout(() => {
|
196
|
+
// bannerStyle = bannerStyle.replace("opacity: 0", "opacity: 1");
|
197
|
+
// bannerStyle = bannerStyle.replace("transform: translateY(10px)", "transform: translateY(0)");
|
198
|
+
// banner.style.cssText = bannerStyle;
|
199
|
+
// expectedBannerStyle = banner.style.cssText;
|
200
|
+
// }, 100);
|
171
201
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
imgElement.style.cssText = imageElementCssText
|
185
|
-
}
|
186
|
-
};
|
202
|
+
// // ensure the banner is always visible
|
203
|
+
// const interval = setInterval(() => {
|
204
|
+
// const parent = ctx.domElement.shadowRoot || ctx.domElement;
|
205
|
+
// if (banner.parentNode !== parent) {
|
206
|
+
// parent.appendChild(banner);
|
207
|
+
// }
|
208
|
+
// if (expectedBannerStyle != banner.style.cssText) {
|
209
|
+
// banner.style.cssText = bannerStyle;
|
210
|
+
// expectedBannerStyle = banner.style.cssText;
|
211
|
+
// showBalloonError("This website violates the Needle Engine License! Please contact [email protected]");
|
212
|
+
// }
|
213
|
+
// }, 1000);
|
187
214
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
215
|
+
// if (hasIndieLicense()) {
|
216
|
+
// const removeDelay = licenseDuration + licenseDelay;
|
217
|
+
// setTimeout(() => {
|
218
|
+
// clearInterval(interval);
|
219
|
+
// banner?.remove();
|
220
|
+
// // show the logo every x minutes
|
221
|
+
// const intervalInMinutes = 5;
|
222
|
+
// setTimeout(() => {
|
223
|
+
// if (ctx.domElement.parentNode)
|
224
|
+
// insertNonCommercialUseHint(ctx);
|
225
|
+
// }, 1000 * 60 * intervalInMinutes)
|
226
|
+
// }, removeDelay);
|
227
|
+
// }
|
193
228
|
|
194
|
-
|
195
|
-
textElement.classList.add("text");
|
196
|
-
// if (!isMobileDevice())
|
197
|
-
// textElement.innerHTML = "Made with Needle";
|
198
|
-
licenseElement.appendChild(textElement);
|
199
|
-
|
200
|
-
licenseElement.title = "Needle Engine";
|
201
|
-
if (!hasCommercialLicense())
|
202
|
-
licenseElement.title += " No active license found - Non Commercial Use";
|
203
|
-
licenseElement.addEventListener("click", () => {
|
204
|
-
globalThis.open("https://needle.tools", "_blank");
|
205
|
-
});
|
206
|
-
|
207
|
-
if (hasIndieLicense()) {
|
208
|
-
const removeDelay = licenseDuration + licenseDelay;
|
209
|
-
setTimeout(() => {
|
210
|
-
clearInterval(interval);
|
211
|
-
licenseElement?.remove();
|
212
|
-
style?.remove();
|
213
|
-
// show the logo every x minutes
|
214
|
-
const intervalInMinutes = 5;
|
215
|
-
setTimeout(() => {
|
216
|
-
if (ctx.domElement.parentNode)
|
217
|
-
insertNonCommercialUseHint(ctx);
|
218
|
-
}, 1000 * 60 * intervalInMinutes)
|
219
|
-
}, removeDelay);
|
220
|
-
}
|
221
|
-
|
222
|
-
}
|
229
|
+
// }
|
223
230
|
let lastLogTime = 0;
|
224
231
|
async function logNonCommercialUse(_logo?: string) {
|
225
232
|
const now = Date.now();
|
@@ -249,151 +256,7 @@
|
|
249
256
|
console.log("%c " + licenseText, style);
|
250
257
|
}
|
251
258
|
|
252
|
-
function createLicenseElement() {
|
253
|
-
const licenseElement = document.createElement("div");
|
254
|
-
licenseElement.setAttribute(licenseElementIdentifier, "");
|
255
|
-
licenseElement.style.position = "absolute";
|
256
|
-
licenseElement.style.bottom = "12px";
|
257
|
-
licenseElement.style.right = "15px";
|
258
259
|
|
259
|
-
// licenseElement.style.textShadow = "0 0 2px rgba(200,200,200, 1)";
|
260
|
-
const maxPossibleZIndex = 2147483647;
|
261
|
-
licenseElement.style.zIndex = `${maxPossibleZIndex}`;
|
262
|
-
return licenseElement;
|
263
|
-
}
|
264
|
-
|
265
|
-
// TODO: would be better to put this into a web element and use shadow dom
|
266
|
-
function createLicenseStyle() {
|
267
|
-
const licenseStyle = document.createElement("style");
|
268
|
-
const selector = `div[${licenseElementIdentifier}]`;
|
269
|
-
licenseStyle.innerHTML = `
|
270
|
-
${selector} {
|
271
|
-
font-family: 'Roboto', sans-serif !important;
|
272
|
-
font-weight: 300;
|
273
|
-
transition: all 0.1s ease-in-out !important;
|
274
|
-
pointer-events: all;
|
275
|
-
overflow: hidden;
|
276
|
-
}
|
277
|
-
|
278
|
-
${selector}:hover {
|
279
|
-
cursor: pointer;
|
280
|
-
transition: all 0.3s ease-in-out !important;
|
281
|
-
}
|
282
|
-
|
283
|
-
${selector}, ${selector} > * {
|
284
|
-
display: inline-block !important;
|
285
|
-
visibility: visible !important;
|
286
|
-
border: none !important;
|
287
|
-
text-decoration: none !important;
|
288
|
-
vertical-align: middle !important;
|
289
|
-
}
|
290
|
-
|
291
|
-
@keyframes license-animation-text {
|
292
|
-
1% {
|
293
|
-
opacity: 0;
|
294
|
-
}
|
295
|
-
2.5% {
|
296
|
-
opacity: 1;
|
297
|
-
}
|
298
|
-
100% {
|
299
|
-
opacity: 1;
|
300
|
-
}
|
301
|
-
}
|
302
|
-
${selector} .text {
|
303
|
-
position: relative;
|
304
|
-
display: inline-block;
|
305
|
-
opacity: 0;
|
306
|
-
animation: license-animation-text;
|
307
|
-
animation-duration: ${(licenseDuration / 1000)}s;
|
308
|
-
animation-delay: ${licenseDelay / 1000}s;
|
309
|
-
animation-fill-mode: forwards;
|
310
|
-
line-height: 1em;
|
311
|
-
margin-left: -3px;
|
312
|
-
color: rgba(20,20,20,1);
|
313
|
-
font-size: 14px;
|
314
|
-
font-weight: 800;
|
315
|
-
background-color: rgba(220, 220, 220, .8);
|
316
|
-
// padding: .51em .6em .43em .6em;
|
317
|
-
border-radius: .7em;
|
318
|
-
}
|
319
|
-
|
320
|
-
${selector} .text .non-commercial {
|
321
|
-
font-size: 0.8em;
|
322
|
-
font-weight: 600;
|
323
|
-
text-transform: uppercase;
|
324
|
-
}
|
325
|
-
|
326
|
-
@keyframes logo-animation {
|
327
|
-
0% {
|
328
|
-
transform: translate(0px, 10px);
|
329
|
-
pointer-events: none;
|
330
|
-
}
|
331
|
-
3% {
|
332
|
-
transform: translate(0, 0px);
|
333
|
-
pointer-events: all;
|
334
|
-
opacity: 1;
|
335
|
-
transform: scale(1.1)
|
336
|
-
}
|
337
|
-
12% {
|
338
|
-
transform: scale(1)
|
339
|
-
}
|
340
|
-
100% {
|
341
|
-
opacity: 1;
|
342
|
-
pointer-events: all;
|
343
|
-
transform: scale(1)
|
344
|
-
}
|
345
|
-
}
|
346
|
-
|
347
|
-
${selector} .logo {
|
348
|
-
opacity: 0;
|
349
|
-
pointer-events: none;
|
350
|
-
animation: logo-animation;
|
351
|
-
animation-duration: ${(licenseDuration / 1000)}s;
|
352
|
-
animation-delay: ${licenseDelay / 1000}s;
|
353
|
-
animation-easing: ease-in-out;
|
354
|
-
animation-fill-mode: forwards;
|
355
|
-
}
|
356
|
-
|
357
|
-
${selector} .logo {
|
358
|
-
position: relative !important;
|
359
|
-
display: inline-block !important;
|
360
|
-
border-radius: 50% !important;
|
361
|
-
background-color: transparent !important;
|
362
|
-
padding: 5px !important;
|
363
|
-
transition: all 0.2s ease-in-out !important;
|
364
|
-
}
|
365
|
-
|
366
|
-
${selector}:hover .logo {
|
367
|
-
transition: all 0.2s ease-in-out !important;
|
368
|
-
transform: scale(1.1) !important;
|
369
|
-
cursor: pointer !important;
|
370
|
-
}
|
371
|
-
|
372
|
-
@media (prefers-reduced-motion: reduce) {
|
373
|
-
${selector} .text {
|
374
|
-
animation: none !important;
|
375
|
-
transition: none !important;
|
376
|
-
/* animation end state */
|
377
|
-
opacity: 1;
|
378
|
-
}
|
379
|
-
|
380
|
-
${selector} .logo {
|
381
|
-
animation: none !important;
|
382
|
-
transition: none !important;
|
383
|
-
/* animation end state */
|
384
|
-
opacity: 1;
|
385
|
-
pointer-events: all;
|
386
|
-
transform: scale(1)
|
387
|
-
}
|
388
|
-
${selector} .logo:hover {
|
389
|
-
transition: none !important;
|
390
|
-
}
|
391
|
-
}
|
392
|
-
`
|
393
|
-
return licenseStyle;
|
394
|
-
}
|
395
|
-
|
396
|
-
|
397
260
|
async function sendUsageMessageToAnalyticsBackend() {
|
398
261
|
try {
|
399
262
|
const analyticsUrl = "https://needle-engine-analytics-v2-r26roub2hq-lz.a.run.app";
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AxesHelper, Box3, Camera, type Intersection, Layers, Line,Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
|
1
|
+
import { ArrayCamera, AxesHelper, Box3, Camera, type Intersection, Layers, Line,Mesh, Object3D, PerspectiveCamera,Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
|
2
2
|
|
3
3
|
import { Gizmos } from './engine_gizmos.js';
|
4
4
|
import { Context } from './engine_setup.js';
|
@@ -226,7 +226,13 @@
|
|
226
226
|
if (this.defaultRaycastOptions.results) this.defaultRaycastOptions.results.length = 0;
|
227
227
|
return this.defaultRaycastOptions.results ?? [];
|
228
228
|
}
|
229
|
-
|
229
|
+
const xrCam = this.context.xrCamera;
|
230
|
+
if (this.context.isInXR && xrCam instanceof ArrayCamera && xrCam.cameras.length > 0) {
|
231
|
+
rc.setFromCamera(mp, xrCam.cameras[0] as PerspectiveCamera);
|
232
|
+
}
|
233
|
+
else {
|
234
|
+
rc.setFromCamera(mp, cam);
|
235
|
+
}
|
230
236
|
}
|
231
237
|
let targets = options.targets;
|
232
238
|
if (!targets) {
|
@@ -256,8 +262,9 @@
|
|
256
262
|
rc.layers.disable(2);
|
257
263
|
}
|
258
264
|
|
259
|
-
|
260
|
-
|
265
|
+
if (debugPhysics) {
|
266
|
+
Gizmos.DrawRay(rc.ray.origin, rc.ray.direction, 0xff0000, 2);
|
267
|
+
}
|
261
268
|
|
262
269
|
// shoot
|
263
270
|
results.length = 0;
|
@@ -32,7 +32,7 @@
|
|
32
32
|
|
33
33
|
class ColorSerializer extends TypeSerializer {
|
34
34
|
constructor() {
|
35
|
-
super([Color, RGBAColor])
|
35
|
+
super([Color, RGBAColor], "ColorSerializer")
|
36
36
|
}
|
37
37
|
onDeserialize(data: any): THREE.Color | RGBAColor | void {
|
38
38
|
if (data === undefined || data === null) return;
|
@@ -60,7 +60,7 @@
|
|
60
60
|
}
|
61
61
|
class ObjectSerializer extends TypeSerializer {
|
62
62
|
constructor() {
|
63
|
-
super(Object3D);
|
63
|
+
super(Object3D, "ObjectSerializer");
|
64
64
|
}
|
65
65
|
|
66
66
|
onSerialize(data: any, context: SerializationContext) {
|
@@ -144,8 +144,9 @@
|
|
144
144
|
|
145
145
|
|
146
146
|
class ComponentSerializer extends TypeSerializer {
|
147
|
+
|
147
148
|
constructor() {
|
148
|
-
super([Component, Behaviour]);
|
149
|
+
super([Component, Behaviour], "ComponentSerializer");
|
149
150
|
}
|
150
151
|
|
151
152
|
onSerialize(data: any, _context: SerializationContext) {
|
@@ -19,21 +19,23 @@
|
|
19
19
|
// internal helper class that we can ask for registered type serializers
|
20
20
|
// register your own type by deriving from ITypeSerializer and calling helper.register
|
21
21
|
class SerializationHelper {
|
22
|
-
register(type:
|
23
|
-
if (this.typeMap
|
24
|
-
|
25
|
-
|
22
|
+
register(type: Constructor<any>, ser: ITypeSerializer) {
|
23
|
+
if (this.typeMap.has(type)) {
|
24
|
+
const existing = this.typeMap.get(type);
|
25
|
+
if (existing === ser) return;
|
26
|
+
console.warn("Type " + type + " is already registered", ser, existing);
|
26
27
|
}
|
27
28
|
if (debug)
|
28
|
-
console.log("Register type serializer
|
29
|
-
this.typeMap
|
29
|
+
console.log("Register type serializer", ser.name, ser, type);
|
30
|
+
this.typeMap.set(type, ser);
|
30
31
|
}
|
31
32
|
|
32
|
-
|
33
|
+
/** type > serializer map */
|
34
|
+
private readonly typeMap = new Map<Constructor<any>, ITypeSerializer>();
|
33
35
|
|
34
|
-
getSerializer(type:
|
36
|
+
private getSerializer(type: Constructor<any>): ITypeSerializer | undefined {
|
35
37
|
if (!type) return undefined;
|
36
|
-
return this.typeMap
|
38
|
+
return this.typeMap.get(type)
|
37
39
|
}
|
38
40
|
|
39
41
|
getSerializerForConstructor(type: any, level: number = 0): ITypeSerializer | undefined {
|
@@ -43,51 +45,26 @@
|
|
43
45
|
console.log("invalid type");
|
44
46
|
return undefined;
|
45
47
|
}
|
46
|
-
const name = type.name
|
47
|
-
|
48
|
-
if (debug)
|
49
|
-
console.log("invalid name", name);
|
50
|
-
return undefined;
|
51
|
-
}
|
52
|
-
const res = this.getSerializer(name);
|
48
|
+
const name = type.name;
|
49
|
+
const res = this.getSerializer(type);
|
53
50
|
if (res !== undefined) {
|
54
51
|
if (debug)
|
55
|
-
console.log("FOUND "
|
52
|
+
console.log("FOUND SERIALIZER", (res as TypeSerializer)?.name, type.name, type.constructor.name, "for type: " + name, res, type, this.typeMap);
|
56
53
|
return res;
|
57
54
|
}
|
58
55
|
const parent = Object.getPrototypeOf(type);
|
59
|
-
|
60
|
-
|
61
|
-
if (!hasPrototypeOrConstructor) {
|
62
|
-
if (debug)
|
63
|
-
console.warn("No prototype for " + name, type, type.name, type.prototype, type.constructor.name);
|
64
|
-
// console.log(type.constructor);
|
65
|
-
// console.dir(type);
|
66
|
-
// console.log(Object.getPrototypeOf(type))
|
67
|
-
// console.dir(parent, Object.getPrototypeOf(type));
|
68
|
-
// if(level <= 0){
|
69
|
-
// const t = TypeStore.get(type.name);
|
70
|
-
// console.log(type['__proto__'].name);
|
71
|
-
// if(t) return this.getSerializerForConstructor(t, level + 1);
|
72
|
-
// }
|
73
|
-
return undefined;
|
74
|
-
}
|
75
|
-
const prot = parent.prototype ?? parent.constructor;
|
76
|
-
if (prot !== type) {
|
77
|
-
const resultFromChildren = this.getSerializerForConstructor(prot, ++level);
|
56
|
+
if (parent && parent !== type) {
|
57
|
+
const resultFromChildren = this.getSerializerForConstructor(parent, ++level);
|
78
58
|
if (resultFromChildren) {
|
59
|
+
const prot = parent.constructor || parent.prototype;
|
79
60
|
if (debug)
|
80
|
-
console.log("FOUND " + prot.constructor.name, prot.name, prot, resultFromChildren);
|
61
|
+
console.log("FOUND SERIALIZER(in constructor) " + prot.constructor.name, prot.name, prot, resultFromChildren);
|
81
62
|
// register sub type
|
82
|
-
|
83
|
-
if (typeName === "Function") {
|
84
|
-
console.error("Registering Function is not allowed, something went wrong", type, prot, resultFromChildren);
|
85
|
-
}
|
86
|
-
else
|
87
|
-
this.register(typeName, resultFromChildren);
|
63
|
+
this.register(prot, resultFromChildren);
|
88
64
|
}
|
89
65
|
return resultFromChildren;
|
90
66
|
}
|
67
|
+
if (debug) console.warn("No serializer found for " + name, type, type.name, type.constructor.name);
|
91
68
|
return undefined;
|
92
69
|
}
|
93
70
|
}
|
@@ -96,6 +73,7 @@
|
|
96
73
|
|
97
74
|
|
98
75
|
export interface ITypeSerializer {
|
76
|
+
readonly name?: string;
|
99
77
|
onSerialize(data: any, context: SerializationContext): any;
|
100
78
|
onDeserialize(data: any, context: SerializationContext): any;
|
101
79
|
}
|
@@ -113,6 +91,8 @@
|
|
113
91
|
*/
|
114
92
|
export abstract class TypeSerializer implements ITypeSerializer {
|
115
93
|
|
94
|
+
readonly name?: string;
|
95
|
+
|
116
96
|
// register<T>(c: Constructor<T> | Constructor<T>[])
|
117
97
|
// {
|
118
98
|
// if (Array.isArray(c)) {
|
@@ -125,13 +105,15 @@
|
|
125
105
|
// }
|
126
106
|
// }
|
127
107
|
|
128
|
-
constructor(type: Constructor<any> | Constructor<any>[]) {
|
108
|
+
constructor(type: Constructor<any> | Constructor<any>[], name?: string) {
|
109
|
+
this.name = name;
|
129
110
|
if (Array.isArray(type)) {
|
130
|
-
for (const key of type)
|
131
|
-
helper.register(key
|
111
|
+
for (const key of type) {
|
112
|
+
helper.register(key, this);
|
113
|
+
}
|
132
114
|
}
|
133
115
|
else
|
134
|
-
helper.register(type
|
116
|
+
helper.register(type, this);
|
135
117
|
}
|
136
118
|
abstract onSerialize(data: any, context: SerializationContext): any | void;
|
137
119
|
abstract onDeserialize(data: any, context: SerializationContext): any | void;
|
@@ -570,8 +552,9 @@
|
|
570
552
|
|
571
553
|
// console.log(type.prototype.get("$serializedTypes"));
|
572
554
|
|
555
|
+
|
573
556
|
let instance: any = undefined;
|
574
|
-
if (data && (data.isMaterial || data.isTexture || data.isObject3D)) {
|
557
|
+
if (data && (data.isMaterial || data.isTexture || data.isObject3D || data instanceof AnimationClip)) {
|
575
558
|
// if the data is already a threejs object we dont want to create a new instance
|
576
559
|
// e.g. if we have a serialized class with a serializable(Material)
|
577
560
|
instance = data;
|
@@ -1,6 +1,3 @@
|
|
1
|
-
import { deserializeObject,serializeObject } from "./engine_serialization_core.js";
|
2
|
-
|
3
|
-
export { deserializeObject,serializeObject };
|
4
|
-
|
5
1
|
export * from "./engine_serialization_builtin_serializer.js";
|
2
|
+
export { deserializeObject, SerializationContext,serializeObject } from "./engine_serialization_core.js";
|
6
3
|
export { serializable, serializeable } from "./engine_serialization_decorator.js"
|
@@ -1,5 +1,19 @@
|
|
1
1
|
const logoSvgString = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 187.74"><defs><linearGradient id="a" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="b" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="c" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="d" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="e" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="f" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="rotate(20 4088.49 13316.712)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="g" viewBox="0 0 160 187.74"><path style="fill:url(#a)" d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75z"/><path style="fill:url(#b)" d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98z"/><path style="fill:url(#c)" d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z"/><path style="fill:url(#d)" d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04z"/><path style="fill:#9c3" d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5z"/><path style="fill:url(#e)" d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59z"/><path style="fill:url(#f)" d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z"/><path style="fill:#ffe113" d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1z"/><path style="fill:#f3e600" d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75z"/></symbol></defs><use width="160" height="187.74" xlink:href="#g"/></svg>`;
|
2
|
+
const logoSvgBlob = new Blob([logoSvgString], { type: "image/svg+xml;charset=utf-8" });
|
3
|
+
const logoSvgUrl = URL.createObjectURL(logoSvgBlob);
|
4
|
+
/** Logo Only */
|
5
|
+
export const needleLogoOnlySVG: string = logoSvgUrl;
|
2
6
|
|
3
|
-
|
4
|
-
const svgUrl = URL.createObjectURL(svgBlob);
|
5
|
-
|
7
|
+
|
8
|
+
const madeWithNeedleSvgString = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> <svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 1014 282" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m665.95 132.73v44.88l-10.56-8.4c-0.8-0.64-1.2-1.44-1.2-2.4v-32.4c0-6.48-4.12-9.72-12.36-9.72-2.16 0-4.18 0.4-6.06 1.2s-3.54 1.8-4.98 3-2.56 2.5-3.36 3.9-1.2 2.7-1.2 3.9v40.92l-10.68-8.4c-0.72-0.64-1.08-1.44-1.08-2.4v-53.76l10.92 8.52c0.32 0.24 0.56 0.44 0.72 0.6s0.36 0.32 0.6 0.48c0.96-1.2 2.14-2.28 3.54-3.24s2.92-1.76 4.56-2.4 3.34-1.14 5.1-1.5 3.44-0.54 5.04-0.54c1.44 0 2.92 0.04 4.44 0.12s2.84 0.28 3.96 0.6c4.56 1.12 7.8 3.12 9.72 6s2.88 6.56 2.88 11.04z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m732.38 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m795.93 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m858.57 97.21c0.64 0.48 0.96 1.16 0.96 2.04v74.88c-0.08 1.04-0.12 2.12-0.12 3.24-1.84-1.52-3.56-2.92-5.16-4.2-1.36-1.12-2.66-2.18-3.9-3.18s-2.06-1.66-2.46-1.98c-1.76 2.48-4.26 4.44-7.5 5.88s-7.02 2.16-11.34 2.16c-3.84 0-7.4-0.7-10.68-2.1s-6.14-3.44-8.58-6.12-4.34-5.94-5.7-9.78-2.04-8.16-2.04-12.96c0-4.32 0.78-8.34 2.34-12.06s3.6-6.92 6.12-9.6 5.38-4.78 8.58-6.3 6.48-2.28 9.84-2.28c2.56 0 4.82 0.22 6.78 0.66s3.68 1.06 5.16 1.86 2.78 1.74 3.9 2.82 2.16 2.22 3.12 3.42v-35.04l10.68 8.64zm-27.96 67.92c3.6 0 6.52-0.68 8.76-2.04s3.98-3.06 5.22-5.1 2.1-4.22 2.58-6.54 0.72-4.44 0.72-6.36v-1.2c0-1.12-0.22-2.7-0.66-4.74s-1.28-4.06-2.52-6.06-3-3.7-5.28-5.1-5.22-2.02-8.82-1.86c-3.44 0-6.26 0.74-8.46 2.22s-3.96 3.26-5.28 5.34-2.24 4.2-2.76 6.36-0.78 3.92-0.78 5.28c0 1.84 0.24 3.92 0.72 6.24s1.36 4.48 2.64 6.48 3.04 3.68 5.28 5.04 5.12 2.04 8.64 2.04z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m882.81 97.09c0.64 0.48 0.96 1.12 0.96 1.92l-0.12 41.04v37.08l-10.56-8.4c-0.72-0.64-1.08-1.44-1.08-2.4v-77.88l10.8 8.64z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m950.36 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.8559 0 0 .7642 45.348 36.475)"> <g transform="translate(2.7114)"> <path d="m3.935 173.02c-0.331 0-0.497-0.402-0.497-1.207v-51.002c0-0.738 0.138-1.107 0.414-1.107h1.781c0.277 0 0.415 0.335 0.415 1.006v5.935c0 0.336 0.027 0.553 0.083 0.654 0.055 0.101 0.151-0.017 0.289-0.352 0.912-1.744 1.754-3.236 2.527-4.477 0.773-1.24 1.554-2.179 2.341-2.816s1.65-0.956 2.588-0.956c1.685 0 3.011 0.922 3.977 2.766 0.967 1.845 1.602 3.84 1.905 5.986 0.056 0.268 0.139 0.369 0.249 0.302s0.221-0.235 0.331-0.503c0.939-1.811 1.802-3.353 2.589-4.628 0.787-1.274 1.581-2.246 2.382-2.917s1.671-1.006 2.61-1.006c2.016 0 3.569 1.392 4.66 4.175 1.09 2.783 1.636 6.421 1.636 10.915v37.925c0 0.871-0.18 1.307-0.539 1.307h-1.739c-0.138 0-0.249-0.1-0.332-0.301-0.083-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.338-6.321-1.015-8.3-0.676-1.978-1.76-2.967-3.251-2.967-0.884 0-1.726 0.386-2.527 1.157s-1.519 1.727-2.154 2.867-1.201 2.213-1.699 3.219c-0.248 0.469-0.421 0.905-0.517 1.308-0.097 0.402-0.145 0.972-0.145 1.71v37.221c0 0.871-0.166 1.307-0.497 1.307h-1.74c-0.166 0-0.29-0.1-0.373-0.301-0.083-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.332-6.321-0.994-8.3-0.663-1.978-1.754-2.967-3.273-2.967-1.242 0-2.375 0.704-3.396 2.112-1.022 1.409-2.223 3.555-3.604 6.439v39.031c0 0.805-0.18 1.207-0.539 1.207h-1.698z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m53.642 166.28c-1.077 2.549-2.237 4.477-3.479 5.785-1.243 1.307-2.61 1.961-4.101 1.961-2.154 0-3.853-1.324-5.095-3.973-1.243-2.649-1.864-6.187-1.864-10.613 0-3.488 0.4-6.489 1.201-9.004s1.988-4.51 3.562-5.985c1.574-1.476 3.521-2.414 5.841-2.817l3.686-0.704c0.221-0.067 0.394-0.218 0.518-0.453 0.124-0.234 0.187-0.587 0.187-1.056v-2.917c0-3.89-0.504-6.975-1.512-9.255s-2.354-3.42-4.039-3.42c-1.298 0-2.472 0.72-3.521 2.162s-2.002 3.572-2.858 6.388c-0.083 0.268-0.159 0.453-0.228 0.554-0.069 0.1-0.172 0.083-0.311-0.051l-1.698-1.71c-0.083-0.134-0.138-0.285-0.166-0.453-0.027-0.167 0.014-0.452 0.125-0.855 0.856-3.353 2.009-6.052 3.459-8.098 1.449-2.045 3.224-3.068 5.322-3.068 1.74 0 3.211 0.687 4.412 2.062s2.112 3.37 2.734 5.986c0.621 2.615 0.932 5.7 0.932 9.255v35.712c0 0.536-0.035 0.888-0.104 1.056s-0.2 0.251-0.393 0.251h-1.533c-0.166 0-0.29-0.117-0.373-0.352-0.083-0.234-0.124-0.553-0.124-0.955l-0.083-5.231c-0.055-0.939-0.221-1.006-0.497-0.202zm0.456-19.314c0-1.14-0.194-1.643-0.58-1.509l-3.107 0.603c-1.436 0.202-2.686 0.638-3.749 1.308-1.063 0.671-1.953 1.543-2.671 2.616s-1.257 2.33-1.616 3.772-0.538 3.102-0.538 4.98c0 3.152 0.455 5.616 1.367 7.393 0.911 1.778 2.14 2.666 3.686 2.666 0.939 0 1.85-0.419 2.734-1.257s1.671-1.895 2.361-3.169c0.663-1.408 1.181-2.85 1.553-4.326 0.373-1.475 0.56-2.883 0.56-4.225v-8.852z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m79.034 173.02c-0.166 0-0.297-0.117-0.394-0.352-0.096-0.234-0.145-0.553-0.145-0.955v-4.628c0-0.536-0.041-0.838-0.124-0.905s-0.207 0.1-0.373 0.503c-0.276 0.67-0.69 1.593-1.242 2.766-0.553 1.174-1.271 2.23-2.154 3.169-0.884 0.939-1.961 1.408-3.231 1.408-1.74 0-3.314-0.989-4.722-2.967-1.409-1.979-2.534-4.963-3.376-8.953-0.843-3.991-1.264-8.937-1.264-14.838 0-5.701 0.415-10.68 1.243-14.939s1.988-7.595 3.479-10.009c1.492-2.415 3.204-3.622 5.137-3.622 1.436 0 2.616 0.57 3.541 1.71 0.926 1.14 1.719 2.381 2.382 3.722 0.249 0.47 0.414 0.637 0.497 0.503s0.125-0.536 0.125-1.207v-23.841c0-0.805 0.151-1.208 0.455-1.208h1.864c0.276 0 0.414 0.369 0.414 1.107v72.128c0 0.537-0.041 0.905-0.124 1.107-0.083 0.201-0.235 0.301-0.455 0.301h-1.533zm-0.621-42.049c-0.939-2.213-1.885-3.94-2.838-5.181s-2.009-1.861-3.169-1.861c-1.463 0-2.768 0.889-3.914 2.666s-2.044 4.376-2.693 7.796-0.973 7.578-0.973 12.474c0 5.097 0.338 9.272 1.015 12.524 0.676 3.253 1.567 5.651 2.672 7.193 1.104 1.543 2.305 2.314 3.603 2.314 1.188 0 2.258-0.704 3.211-2.113 0.952-1.408 1.705-3.118 2.257-5.13s0.829-3.957 0.829-5.835v-24.847z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m89.514 149.38c0 3.42 0.345 6.606 1.035 9.557 0.691 2.951 1.609 5.315 2.755 7.092s2.437 2.666 3.873 2.666c1.519 0 2.837-0.738 3.956-2.213 1.118-1.476 2.064-3.655 2.837-6.539 0.083-0.336 0.166-0.52 0.249-0.554 0.083-0.033 0.179 0.017 0.29 0.151l1.408 1.912c0.221 0.268 0.235 0.67 0.041 1.207-0.69 2.548-1.47 4.661-2.34 6.337-0.87 1.677-1.857 2.935-2.962 3.773-1.104 0.838-2.319 1.257-3.645 1.257-2.043 0-3.838-1.14-5.385-3.42-1.546-2.28-2.761-5.482-3.645-9.607-0.884-4.124-1.325-8.836-1.325-14.134 0-5.901 0.455-10.931 1.367-15.089 0.911-4.158 2.14-7.377 3.686-9.658 1.547-2.28 3.3-3.42 5.261-3.42 1.988 0 3.714 1.073 5.178 3.219 1.463 2.146 2.595 5.231 3.396 9.255s1.201 8.886 1.201 14.587c0 0.469-0.02 0.939-0.062 1.408-0.041 0.469-0.214 0.704-0.517 0.704h-16.362c-0.083 0-0.152 0.151-0.207 0.453-0.056 0.302-0.083 0.654-0.083 1.056zm13.752-6.237c0.304 0 0.497-0.1 0.58-0.302 0.083-0.201 0.124-0.57 0.124-1.106 0-3.219-0.283-6.187-0.849-8.903s-1.367-4.896-2.402-6.539c-1.036-1.643-2.272-2.464-3.708-2.464-1.629 0-2.996 0.955-4.101 2.867-1.104 1.911-1.94 4.342-2.506 7.293s-0.849 6.002-0.849 9.154h13.711z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m148.54 119.7c0.165 0 0.283 0.117 0.352 0.352s0.076 0.52 0.02 0.855l-6.254 50.902c-0.028 0.47-0.104 0.788-0.228 0.956s-0.297 0.251-0.518 0.251h-1.615c-0.442 0-0.718-0.402-0.829-1.207l-5.26-40.138c-0.111-0.604-0.201-0.905-0.27-0.905s-0.131 0.301-0.186 0.905l-5.012 40.138c-0.028 0.47-0.097 0.788-0.207 0.956-0.111 0.168-0.277 0.251-0.497 0.251h-1.74c-0.442 0-0.718-0.402-0.829-1.207l-6.503-50.801c-0.055-0.403-0.048-0.721 0.021-0.956s0.2-0.352 0.393-0.352h1.823c0.166 0 0.297 0.067 0.393 0.201 0.097 0.134 0.159 0.403 0.187 0.805l5.302 41.848c0.083 0.671 0.179 0.989 0.29 0.956 0.11-0.034 0.207-0.386 0.29-1.056l5.219-41.949c0.055-0.268 0.124-0.47 0.207-0.604s0.193-0.201 0.331-0.201h1.533c0.138 0 0.262 0.067 0.373 0.201 0.11 0.134 0.179 0.403 0.207 0.805l5.468 41.848c0.083 0.671 0.179 0.989 0.29 0.956 0.11-0.034 0.207-0.386 0.29-1.056l5.053-41.849c0.055-0.335 0.138-0.57 0.249-0.704 0.11-0.134 0.234-0.201 0.373-0.201h1.284z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m156.49 171.51c0 0.604-0.042 1.006-0.125 1.208-0.082 0.201-0.262 0.301-0.538 0.301h-1.533c-0.221 0-0.366-0.083-0.435-0.251s-0.103-0.486-0.103-0.956v-50.902c0-0.805 0.152-1.207 0.456-1.207h1.822c0.304 0 0.456 0.402 0.456 1.207v50.6zm0.165-63.979c0 1.207-0.207 1.811-0.621 1.811h-1.905c-0.221 0-0.366-0.135-0.435-0.403s-0.104-0.67-0.104-1.207v-7.847c0-1.006 0.18-1.509 0.539-1.509h1.988c0.359 0 0.538 0.47 0.538 1.409v7.746z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m168.3 124.83c-0.221 0-0.331 0.269-0.331 0.805v33.801c0 3.42 0.221 5.667 0.663 6.74 0.441 1.073 1.09 1.609 1.946 1.609h3.024c0.138 0 0.242 0.084 0.311 0.252 0.069 0.167 0.103 0.419 0.103 0.754v2.716c0 0.537-0.138 0.906-0.414 1.107-0.248 0.067-0.614 0.134-1.098 0.201-0.483 0.067-0.959 0.118-1.429 0.151-0.469 0.034-0.828 0.05-1.077 0.05-1.712 0-2.934-0.955-3.665-2.867-0.732-1.911-1.098-5.013-1.098-9.305v-35.108c0-0.604-0.124-0.906-0.373-0.906h-3.521c-0.248 0-0.373-0.268-0.373-0.804v-3.521c0-0.537 0.111-0.805 0.332-0.805h3.686c0.166 0 0.263-0.268 0.29-0.805l0.415-16.095c0-0.805 0.124-1.207 0.372-1.207h1.492c0.303 0 0.455 0.436 0.455 1.307v15.995c0 0.537 0.097 0.805 0.29 0.805h5.468c0.221 0 0.331 0.268 0.331 0.805v3.521c0 0.536-0.124 0.804-0.373 0.804h-5.426z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m179.4 173.02c-0.331 0-0.497-0.402-0.497-1.207v-72.329c0-0.738 0.138-1.107 0.414-1.107h1.782c0.276 0 0.414 0.336 0.414 1.006v27.162c0 0.335 0.034 0.536 0.103 0.603s0.159-0.033 0.27-0.302c0.994-1.81 1.898-3.319 2.713-4.526 0.814-1.208 1.629-2.113 2.444-2.717 0.814-0.603 1.691-0.905 2.63-0.905 2.182 0 3.839 1.375 4.971 4.125 1.132 2.749 1.698 6.404 1.698 10.965v37.925c0 0.871-0.166 1.307-0.497 1.307h-1.74c-0.165 0-0.29-0.1-0.373-0.301-0.082-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.366-6.321-1.097-8.3-0.732-1.978-1.899-2.967-3.501-2.967-0.883 0-1.705 0.318-2.464 0.956-0.76 0.637-1.526 1.576-2.299 2.816-0.773 1.241-1.643 2.834-2.61 4.779v39.031c0 0.805-0.179 1.207-0.538 1.207h-1.699z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> </g> <g transform="matrix(.80638 0 0 .80638 452.53 65.421)" fill-rule="nonzero"> <path d="m79.32 36.98v150.76l15.68-13.2 6.59-156.31-22.27 18.75z" fill="url(#f)"/> <path d="m79.32 36.98-22.27-18.75 6.59 156.31 15.68 13.2v-150.76z" fill="url(#e)"/> <path d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z" fill="url(#d)"/> <path d="m25.19 104.83-25.19-14.59 16.97 53.86 16.85 9.77-8.63-49.04z" fill="url(#c)"/> <path d="M43.86,82.5L18.69,67.98L0,90.24L25.18,104.83L43.86,82.5Z" fill="#9c3"/> <path d="m134.82 78.69-9.97 56.5 15.58-9.04 19.57-62.05-25.18 14.59z" fill="url(#b)"/> <path d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z" fill="url(#a)"/> <path d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33 25.19-14.59z" fill="#ffe113"/> <path d="M101.59,18.23L79.32,0L57.05,18.23L79.32,36.98L101.59,18.23Z" fill="#f3e600"/> </g> <defs> <linearGradient id="f" x2="1" gradientTransform="matrix(.84 -162.96 162.96 .84 89.64 184.81)" gradientUnits="userSpaceOnUse"><stop stop-color="#62d399" offset="0"/><stop stop-color="#acd842" offset=".51"/><stop stop-color="#d7db0a" offset=".9"/><stop stop-color="#d7db0a" offset="1"/></linearGradient> <linearGradient id="e" x2="1" gradientTransform="matrix(-1.6,-162.13,162.13,-1.6,69.68,178.9)" gradientUnits="userSpaceOnUse"><stop stop-color="#0ba398" offset="0"/><stop stop-color="#4ca352" offset=".5"/><stop stop-color="#76a30a" offset="1"/></linearGradient> <linearGradient id="d" x2="1" gradientTransform="matrix(-1.9,-67.98,67.98,-1.9,36.6,152.17)" gradientUnits="userSpaceOnUse"><stop stop-color="#36a382" offset="0"/><stop stop-color="#36a382" offset=".19"/><stop stop-color="#49a459" offset=".54"/><stop stop-color="#76a30b" offset="1"/></linearGradient> <linearGradient id="c" x2="1" gradientTransform="matrix(2.18,-62.38,62.38,2.18,15.82,153.24)" gradientUnits="userSpaceOnUse"><stop stop-color="#267880" offset="0"/><stop stop-color="#457a5c" offset=".51"/><stop stop-color="#717516" offset="1"/></linearGradient> <linearGradient id="b" x2="1" gradientTransform="matrix(13.85,-71.96,71.96,13.85,135.08,135.43)" gradientUnits="userSpaceOnUse"><stop stop-color="#b0d939" offset="0"/><stop stop-color="#eadb04" offset="1"/></linearGradient> <linearGradient id="a" x2="1" gradientTransform="matrix(26.159 -64.737 64.737 26.159 107.42 128.14)" gradientUnits="userSpaceOnUse"><stop stop-color="#74af52" offset="0"/><stop stop-color="#74af52" offset=".17"/><stop stop-color="#99be32" offset=".48"/><stop stop-color="#c0c40a" offset="1"/></linearGradient> </defs> </svg>`
|
9
|
+
const madeWithNeedleBlob = new Blob([madeWithNeedleSvgString], { type: "image/svg+xml;charset=utf-8" });
|
10
|
+
const madeWithNeedleUrl = URL.createObjectURL(madeWithNeedleBlob);
|
11
|
+
/** Made With Needle Logo */
|
12
|
+
export const madeWithNeedleSVG: string = madeWithNeedleUrl;
|
13
|
+
|
14
|
+
|
15
|
+
const needleLogoSvgString = `<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 499.26 149.65"><defs><style>.cls-1{fill:url(#Unbenannter_Verlauf_122);}.cls-2{fill:#f3e600;}.cls-3{fill:url(#Unbenannter_Verlauf_113);}.cls-4{fill:url(#gradient-6);}.cls-5{fill:url(#Unbenannter_Verlauf_110);}.cls-6{fill:url(#Unbenannter_Verlauf_101);}.cls-7{fill:#9c3;}.cls-8{fill:#ffe113;}.cls-9{fill:url(#gradient-5);}.cls-10{fill:#02022b;}</style><linearGradient id="Unbenannter_Verlauf_113" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="Unbenannter_Verlauf_110" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="Unbenannter_Verlauf_101" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="Unbenannter_Verlauf_122" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="gradient-6" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="gradient-5" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="translate(4801.15 -595.25) rotate(20)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="needle-icon" viewBox="0 0 160 187.74"><g><polygon class="cls-3" points="79.32 36.98 79.32 187.74 95 174.54 101.59 18.23 79.32 36.98"/><polygon class="cls-5" points="79.32 36.98 57.05 18.23 63.64 174.54 79.32 187.74 79.32 36.98"/><polygon class="cls-6" points="25.19 104.83 33.82 153.87 46.32 138.92 43.86 82.5 25.19 104.83"/><polygon class="cls-1" points="25.19 104.83 0 90.24 16.97 144.1 33.82 153.87 25.19 104.83"/><polygon class="cls-7" points="43.86 82.5 18.69 67.98 0 90.24 25.18 104.83 43.86 82.5"/><polygon class="cls-4" points="134.82 78.69 124.85 135.19 140.43 126.15 160 64.1 134.82 78.69"/><polygon class="cls-9" points="134.82 78.69 116.14 56.36 113.28 121.36 124.85 135.19 134.82 78.69"/><polygon class="cls-8" points="160 64.1 141.31 41.84 116.14 56.36 134.81 78.69 160 64.1"/><polygon class="cls-2" points="101.59 18.23 79.32 0 57.05 18.23 79.32 36.98 101.59 18.23"/></g></symbol></defs><g><path class="cls-10" d="M214.78,77.8v35.52l-10.56-8.4c-.8-.64-1.2-1.44-1.2-2.4v-32.4c0-6.48-4.12-9.72-12.36-9.72-2.16,0-4.18,.4-6.06,1.2-1.88,.8-3.54,1.8-4.98,3-1.44,1.2-2.56,2.5-3.36,3.9-.8,1.4-1.2,2.7-1.2,3.9v40.92l-10.68-8.4c-.72-.64-1.08-1.44-1.08-2.4V48.76l10.92,8.52c.32,.24,.56,.44,.72,.6,.16,.16,.36,.32,.6,.48,.96-1.2,2.14-2.28,3.54-3.24,1.4-.96,2.92-1.76,4.56-2.4,1.64-.64,3.34-1.14,5.1-1.5,1.76-.36,3.44-.54,5.04-.54,1.44,0,2.92,.04,4.44,.12,1.52,.08,2.84,.28,3.96,.6,4.56,1.12,7.8,3.12,9.72,6,1.92,2.88,2.88,6.56,2.88,11.04v9.36Z"/><path class="cls-10" d="M281.21,81.76c0,.88,.02,1.5,.06,1.86,.04,.36-.02,.98-.18,1.86h-7.08c-2.08,0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0,2.88,.56,5.36,1.68,7.44,1.12,2.08,2.6,3.8,4.44,5.16,1.84,1.36,3.94,2.36,6.3,3,2.36,.64,4.74,.96,7.14,.96,3.04,0,5.9-.76,8.58-2.28,2.68-1.52,4.94-3.52,6.78-6,.64,.56,1.54,1.48,2.7,2.76,1.16,1.28,2.94,3.2,5.34,5.76-2.8,3.36-6.22,6.02-10.26,7.98-4.04,1.96-8.42,2.94-13.14,2.94s-8.92-.64-12.84-1.92c-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34,0-4.96,.66-9.42,1.98-13.38,1.32-3.96,3.22-7.32,5.7-10.08,2.48-2.76,5.44-4.9,8.88-6.42,3.44-1.52,7.32-2.28,11.64-2.28,5.76,0,10.52,.88,14.28,2.64,3.76,1.76,6.72,4.16,8.88,7.2,2.16,3.04,3.66,6.54,4.5,10.5,.84,3.96,1.26,8.18,1.26,12.66Zm-29.4-22.8c-2.16,.16-4.16,.72-6,1.68-1.84,.96-3.42,2.2-4.74,3.72-1.32,1.52-2.36,3.28-3.12,5.28-.76,2-1.14,4.12-1.14,6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14Z"/><path class="cls-10" d="M344.76,81.76c0,.88,.02,1.5,.06,1.86,.04,.36-.02,.98-.18,1.86h-7.08c-2.08,0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0,2.88,.56,5.36,1.68,7.44,1.12,2.08,2.6,3.8,4.44,5.16,1.84,1.36,3.94,2.36,6.3,3,2.36,.64,4.74,.96,7.14,.96,3.04,0,5.9-.76,8.58-2.28,2.68-1.52,4.94-3.52,6.78-6,.64,.56,1.54,1.48,2.7,2.76,1.16,1.28,2.94,3.2,5.34,5.76-2.8,3.36-6.22,6.02-10.26,7.98-4.04,1.96-8.42,2.94-13.14,2.94s-8.92-.64-12.84-1.92c-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34,0-4.96,.66-9.42,1.98-13.38,1.32-3.96,3.22-7.32,5.7-10.08,2.48-2.76,5.44-4.9,8.88-6.42,3.44-1.52,7.32-2.28,11.64-2.28,5.76,0,10.52,.88,14.28,2.64,3.76,1.76,6.72,4.16,8.88,7.2,2.16,3.04,3.66,6.54,4.5,10.5,.84,3.96,1.26,8.18,1.26,12.66Zm-29.4-22.8c-2.16,.16-4.16,.72-6,1.68-1.84,.96-3.42,2.2-4.74,3.72-1.32,1.52-2.36,3.28-3.12,5.28-.76,2-1.14,4.12-1.14,6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14Z"/><path class="cls-10" d="M407.4,32.92c.64,.48,.96,1.16,.96,2.04V109.84c-.08,1.04-.12,2.12-.12,3.24-1.84-1.52-3.56-2.92-5.16-4.2-1.36-1.12-2.66-2.18-3.9-3.18-1.24-1-2.06-1.66-2.46-1.98-1.76,2.48-4.26,4.44-7.5,5.88-3.24,1.44-7.02,2.16-11.34,2.16-3.84,0-7.4-.7-10.68-2.1-3.28-1.4-6.14-3.44-8.58-6.12-2.44-2.68-4.34-5.94-5.7-9.78-1.36-3.84-2.04-8.16-2.04-12.96,0-4.32,.78-8.34,2.34-12.06,1.56-3.72,3.6-6.92,6.12-9.6,2.52-2.68,5.38-4.78,8.58-6.3,3.2-1.52,6.48-2.28,9.84-2.28,2.56,0,4.82,.22,6.78,.66,1.96,.44,3.68,1.06,5.16,1.86,1.48,.8,2.78,1.74,3.9,2.82,1.12,1.08,2.16,2.22,3.12,3.42V24.28l10.68,8.64Zm-27.96,67.92c3.6,0,6.52-.68,8.76-2.04,2.24-1.36,3.98-3.06,5.22-5.1,1.24-2.04,2.1-4.22,2.58-6.54,.48-2.32,.72-4.44,.72-6.36v-1.2c0-1.12-.22-2.7-.66-4.74-.44-2.04-1.28-4.06-2.52-6.06-1.24-2-3-3.7-5.28-5.1s-5.22-2.02-8.82-1.86c-3.44,0-6.26,.74-8.46,2.22-2.2,1.48-3.96,3.26-5.28,5.34-1.32,2.08-2.24,4.2-2.76,6.36-.52,2.16-.78,3.92-.78,5.28,0,1.84,.24,3.92,.72,6.24,.48,2.32,1.36,4.48,2.64,6.48,1.28,2,3.04,3.68,5.28,5.04,2.24,1.36,5.12,2.04,8.64,2.04Z"/><path class="cls-10" d="M431.64,32.8c.64,.48,.96,1.12,.96,1.92l-.12,41.04v37.08l-10.56-8.4c-.72-.64-1.08-1.44-1.08-2.4V24.16l10.8,8.64Z"/><path class="cls-10" d="M499.19,81.76c0,.88,.02,1.5,.06,1.86,.04,.36-.02,.98-.18,1.86h-7.08c-2.08,0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0,2.88,.56,5.36,1.68,7.44,1.12,2.08,2.6,3.8,4.44,5.16,1.84,1.36,3.94,2.36,6.3,3,2.36,.64,4.74,.96,7.14,.96,3.04,0,5.9-.76,8.58-2.28,2.68-1.52,4.94-3.52,6.78-6,.64,.56,1.54,1.48,2.7,2.76,1.16,1.28,2.94,3.2,5.34,5.76-2.8,3.36-6.22,6.02-10.26,7.98-4.04,1.96-8.42,2.94-13.14,2.94s-8.92-.64-12.84-1.92c-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34,0-4.96,.66-9.42,1.98-13.38,1.32-3.96,3.22-7.32,5.7-10.08,2.48-2.76,5.44-4.9,8.88-6.42,3.44-1.52,7.32-2.28,11.64-2.28,5.76,0,10.52,.88,14.28,2.64,3.76,1.76,6.72,4.16,8.88,7.2,2.16,3.04,3.66,6.54,4.5,10.5,.84,3.96,1.26,8.18,1.26,12.66Zm-29.4-22.8c-2.16,.16-4.16,.72-6,1.68-1.84,.96-3.42,2.2-4.74,3.72-1.32,1.52-2.36,3.28-3.12,5.28-.76,2-1.14,4.12-1.14,6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14Z"/></g><use width="160" height="187.74" transform="scale(.8)" xlink:href="#needle-icon"/></svg>`;
|
16
|
+
const needleLogoBlob = new Blob([needleLogoSvgString], { type: "image/svg+xml;charset=utf-8" });
|
17
|
+
const needleLogoUrl = URL.createObjectURL(needleLogoBlob);
|
18
|
+
/** Logo + Needle Typo */
|
19
|
+
export const needleLogoSVG: string = needleLogoUrl
|
@@ -8,4 +8,8 @@
|
|
8
8
|
{
|
9
9
|
public isUsed: boolean = true;
|
10
10
|
public usedBy: any = null;
|
11
|
-
}
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
/** @deprecated */
|
15
|
+
export class Interactable extends Behaviour {}
|
@@ -450,7 +450,8 @@
|
|
450
450
|
|
451
451
|
private readonly _needleGamepadButtons: { [key: number | string]: NeedleGamepadButton } = {};
|
452
452
|
/** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
|
453
|
-
private toNeedleGamepadButton(index: number): NeedleGamepadButton {
|
453
|
+
private toNeedleGamepadButton(index: number): NeedleGamepadButton | undefined {
|
454
|
+
if(!this.inputSource.gamepad?.buttons) return undefined
|
454
455
|
const button = this.inputSource.gamepad?.buttons[index];
|
455
456
|
const state = this.states[index];
|
456
457
|
const needleButton = this._needleGamepadButtons[index] || new NeedleGamepadButton();
|
@@ -110,7 +110,7 @@
|
|
110
110
|
|
111
111
|
if (isDesktop() && isDevEnvironment()) {
|
112
112
|
window.addEventListener("keydown", (evt) => {
|
113
|
-
if (evt.key === "x") {
|
113
|
+
if (evt.key === "x" || evt.key === "Escape") {
|
114
114
|
if (NeedleXRSession.active) {
|
115
115
|
NeedleXRSession.stop();
|
116
116
|
}
|
@@ -741,8 +741,6 @@
|
|
741
741
|
this._originalCameraParent = this.context.mainCamera.parent;
|
742
742
|
}
|
743
743
|
|
744
|
-
this.context.mainCameraComponent?.applyClearFlags();
|
745
|
-
|
746
744
|
this._defaultRig = new ImplictXRRig();
|
747
745
|
this.context.scene.add(this._defaultRig.gameObject);
|
748
746
|
this.addRig(this._defaultRig);
|
@@ -779,6 +777,7 @@
|
|
779
777
|
this.context.renderer.xr.enabled = true;
|
780
778
|
// calling update camera here once to prevent the xr camera left/right NaN in the first frame due to false near/far plane values, see NE-4126
|
781
779
|
this.context.renderer.xr.updateCamera(this.context.mainCamera as PerspectiveCamera);
|
780
|
+
this.context.mainCameraComponent?.applyClearFlags();
|
782
781
|
}
|
783
782
|
|
784
783
|
private onInputSourceAdded = (newInputSource: XRInputSource) => {
|
@@ -857,7 +856,10 @@
|
|
857
856
|
|
858
857
|
this.context.xr = null;
|
859
858
|
this.context.renderer.xr.enabled = false;
|
860
|
-
|
859
|
+
// apply the clearflags at the beginning of the next frame
|
860
|
+
this.context.pre_update_oneshot_callbacks.push(()=>{
|
861
|
+
this.context.mainCameraComponent?.applyClearFlags()
|
862
|
+
});
|
861
863
|
|
862
864
|
for (const listener of NeedleXRSession._xrEndListeners) {
|
863
865
|
listener({ xr: this });
|
@@ -117,7 +117,8 @@
|
|
117
117
|
}
|
118
118
|
}
|
119
119
|
|
120
|
-
|
120
|
+
// TODO this is currently not used for post processing effects that are part of Volume stacks,
|
121
|
+
// since these handle that already.
|
121
122
|
onEditorModification(modification: EditorModification): void | boolean | undefined {
|
122
123
|
// Handle a property modification if the property is a VolumeParameter and the modification is just a plain value
|
123
124
|
const key = modification.propertyName;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { N8AOPostPass } from "n8ao";
|
2
|
-
import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
|
2
|
+
import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SMAAEffect, SSAOEffect, ToneMappingEffect, VignetteEffect } from "postprocessing";
|
3
3
|
import { HalfFloatType } from "three";
|
4
4
|
|
5
5
|
import { showBalloonWarning } from "../../engine/debug/index.js";
|
@@ -131,13 +131,14 @@
|
|
131
131
|
// https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
|
132
132
|
renderer[autoclearSetting] = renderer.autoClear;
|
133
133
|
|
134
|
+
const maxSamples = renderer.capabilities.maxSamples;
|
134
135
|
// create composer and set active on context
|
135
136
|
if (!this._composer) {
|
136
137
|
// const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
|
137
138
|
this._composer = new EffectComposer(renderer, {
|
138
139
|
frameBufferType: HalfFloatType,
|
139
140
|
stencilBuffer: true,
|
140
|
-
multisampling: isMobileDevice() ?
|
141
|
+
multisampling: Math.min(isMobileDevice() ? 4 : 8, maxSamples),
|
141
142
|
});
|
142
143
|
}
|
143
144
|
context.composer = this._composer;
|
@@ -152,6 +153,7 @@
|
|
152
153
|
|
153
154
|
// Render to screen pass
|
154
155
|
const screenpass = new RenderPass(scene, cam);
|
156
|
+
screenpass.name = "Render To Screen";
|
155
157
|
screenpass.mainScene = scene;
|
156
158
|
composer.addPass(screenpass);
|
157
159
|
|
@@ -177,6 +179,7 @@
|
|
177
179
|
// create and apply uber pass
|
178
180
|
if (effects.length > 0) {
|
179
181
|
const pass = new EffectPass(cam, ...effects);
|
182
|
+
pass.name = effects.map(e => e.name).join(" ");
|
180
183
|
pass.mainScene = scene;
|
181
184
|
pass.enabled = true;
|
182
185
|
composer.addPass(pass);
|
@@ -185,7 +188,6 @@
|
|
185
188
|
catch(e) {
|
186
189
|
console.error("Error while applying postprocessing effects", e);
|
187
190
|
composer.removeAllPasses();
|
188
|
-
composer.addPass(screenpass);
|
189
191
|
}
|
190
192
|
|
191
193
|
|
@@ -221,14 +223,16 @@
|
|
221
223
|
export const effectsOrder: Array<Constructor<Effect | Pass>> = [
|
222
224
|
NormalPass,
|
223
225
|
DepthDownsamplingPass,
|
226
|
+
SMAAEffect,
|
224
227
|
SSAOEffect,
|
225
228
|
N8AOPostPass,
|
226
229
|
DepthOfFieldEffect,
|
230
|
+
ChromaticAberrationEffect,
|
227
231
|
BloomEffect,
|
228
232
|
SelectiveBloomEffect,
|
229
233
|
VignetteEffect,
|
234
|
+
ToneMappingEffect,
|
230
235
|
HueSaturationEffect,
|
231
|
-
ChromaticAberrationEffect,
|
232
236
|
BrightnessContrastEffect,
|
233
237
|
PixelationEffect,
|
234
238
|
];
|
@@ -89,6 +89,7 @@
|
|
89
89
|
import { Image } from "../../engine-components/ui/Image.js";
|
90
90
|
import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules.js";
|
91
91
|
import { InputField } from "../../engine-components/ui/InputField.js";
|
92
|
+
import { Interactable } from "../../engine-components/Interactable.js";
|
92
93
|
import { Light } from "../../engine-components/Light.js";
|
93
94
|
import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
94
95
|
import { LODGroup } from "../../engine-components/LODGroup.js";
|
@@ -102,7 +103,7 @@
|
|
102
103
|
import { MeshRenderer } from "../../engine-components/Renderer.js";
|
103
104
|
import { MinMaxCurve } from "../../engine-components/ParticleSystemModules.js";
|
104
105
|
import { MinMaxGradient } from "../../engine-components/ParticleSystemModules.js";
|
105
|
-
import {
|
106
|
+
import { NeedleMenu } from "../../engine-components/NeedleMenu.js";
|
106
107
|
import { NestedGltf } from "../../engine-components/NestedGltf.js";
|
107
108
|
import { Networking } from "../../engine-components/Networking.js";
|
108
109
|
import { NoiseModule } from "../../engine-components/ParticleSystemModules.js";
|
@@ -204,6 +205,7 @@
|
|
204
205
|
import { WebARCameraBackground } from "../../engine-components/webxr/WebARCameraBackground.js";
|
205
206
|
import { WebARSessionRoot } from "../../engine-components/webxr/WebARSessionRoot.js";
|
206
207
|
import { WebXR } from "../../engine-components/webxr/WebXR.js";
|
208
|
+
import { WebXRButtonFactory } from "../../engine-components/webxr/WebXRButtons.js";
|
207
209
|
import { WebXRImageTracking } from "../../engine-components/webxr/WebXRImageTracking.js";
|
208
210
|
import { WebXRImageTrackingModel } from "../../engine-components/webxr/WebXRImageTracking.js";
|
209
211
|
import { WebXRPlaneTracking } from "../../engine-components/webxr/WebXRPlaneTracking.js";
|
@@ -303,6 +305,7 @@
|
|
303
305
|
TypeStore.add("Image", Image);
|
304
306
|
TypeStore.add("InheritVelocityModule", InheritVelocityModule);
|
305
307
|
TypeStore.add("InputField", InputField);
|
308
|
+
TypeStore.add("Interactable", Interactable);
|
306
309
|
TypeStore.add("Light", Light);
|
307
310
|
TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
|
308
311
|
TypeStore.add("LODGroup", LODGroup);
|
@@ -316,7 +319,7 @@
|
|
316
319
|
TypeStore.add("MeshRenderer", MeshRenderer);
|
317
320
|
TypeStore.add("MinMaxCurve", MinMaxCurve);
|
318
321
|
TypeStore.add("MinMaxGradient", MinMaxGradient);
|
319
|
-
TypeStore.add("
|
322
|
+
TypeStore.add("NeedleMenu", NeedleMenu);
|
320
323
|
TypeStore.add("NestedGltf", NestedGltf);
|
321
324
|
TypeStore.add("Networking", Networking);
|
322
325
|
TypeStore.add("NoiseModule", NoiseModule);
|
@@ -418,6 +421,7 @@
|
|
418
421
|
TypeStore.add("WebARCameraBackground", WebARCameraBackground);
|
419
422
|
TypeStore.add("WebARSessionRoot", WebARSessionRoot);
|
420
423
|
TypeStore.add("WebXR", WebXR);
|
424
|
+
TypeStore.add("WebXRButtonFactory", WebXRButtonFactory);
|
421
425
|
TypeStore.add("WebXRImageTracking", WebXRImageTracking);
|
422
426
|
TypeStore.add("WebXRImageTrackingModel", WebXRImageTrackingModel);
|
423
427
|
TypeStore.add("WebXRPlaneTracking", WebXRPlaneTracking);
|
@@ -33,21 +33,20 @@
|
|
33
33
|
this._apply(this.mode!.value)
|
34
34
|
}
|
35
35
|
|
36
|
-
|
37
|
-
this.context.renderer.toneMapping =
|
36
|
+
private _apply(v: TonemappingMode) {
|
37
|
+
this.context.renderer.toneMapping = this.getThreeToneMapping(v);
|
38
38
|
}
|
39
39
|
|
40
|
-
|
41
|
-
switch (
|
40
|
+
getThreeToneMapping(mode: TonemappingMode | undefined) {
|
41
|
+
switch (mode) {
|
42
42
|
case TonemappingMode.None:
|
43
|
-
|
44
|
-
break;
|
43
|
+
return LinearToneMapping;
|
45
44
|
case TonemappingMode.Neutral:
|
46
|
-
|
47
|
-
break;
|
45
|
+
return ReinhardToneMapping;
|
48
46
|
case TonemappingMode.ACES:
|
49
|
-
|
50
|
-
|
47
|
+
return ACESFilmicToneMapping;
|
48
|
+
default:
|
49
|
+
return LinearToneMapping; // TODO should be Linear
|
51
50
|
}
|
52
51
|
}
|
53
52
|
|
@@ -8,8 +8,8 @@
|
|
8
8
|
import { Behaviour, GameObject } from "../../Component.js";
|
9
9
|
import { Renderer } from "../../Renderer.js"
|
10
10
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
11
|
-
import {
|
12
|
-
import { XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
11
|
+
import { WebXRButtonFactory } from "../../webxr/WebXRButtons.js";
|
12
|
+
import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
13
13
|
import type { IUSDExporterExtension } from "./Extension.js";
|
14
14
|
import { AnimationExtension } from "./extensions/Animation.js"
|
15
15
|
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
@@ -465,9 +465,9 @@
|
|
465
465
|
|
466
466
|
|
467
467
|
private createQuicklookButton() {
|
468
|
-
const buttoncontainer =
|
468
|
+
const buttoncontainer = WebXRButtonFactory.getOrCreate();
|
469
469
|
const button = buttoncontainer.createQuicklookButton();
|
470
|
-
if(!button.parentNode)
|
470
|
+
if(!button.parentNode) this.context.menu.appendChild(button);
|
471
471
|
return button;
|
472
472
|
}
|
473
473
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { EffectComposer } from "postprocessing";
|
2
2
|
|
3
|
-
import { isDevEnvironment } from "../../engine/debug/index.js";
|
3
|
+
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
4
4
|
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
|
5
5
|
import { serializeable } from "../../engine/engine_serialization_decorator.js";
|
6
6
|
import { getParam } from "../../engine/engine_utils.js";
|
@@ -34,8 +34,8 @@
|
|
34
34
|
console.log("Press P to toggle post processing");
|
35
35
|
window.addEventListener("keydown", (e) => {
|
36
36
|
if (e.key === "p") {
|
37
|
-
console.log("Toggle volume: " + this.name, !this.enabled);
|
38
37
|
this.enabled = !this.enabled;
|
38
|
+
showBalloonMessage("Toggle PostProcessing " + this.name + ": Enabled=" + this.enabled);
|
39
39
|
this.markDirty();
|
40
40
|
}
|
41
41
|
});
|
@@ -13,7 +13,7 @@
|
|
13
13
|
import { XRControllerModel } from "./controllers/XRControllerModel.js";
|
14
14
|
import { XRControllerMovement } from "./controllers/XRControllerMovement.js";
|
15
15
|
import { WebARSessionRoot } from "./WebARSessionRoot.js";
|
16
|
-
import {
|
16
|
+
import { WebXRButtonFactory } from "./WebXRButtons.js";
|
17
17
|
import { XRState, XRStateFlag } from "./XRFlag.js";
|
18
18
|
|
19
19
|
const debug = getParam("debugwebxr");
|
@@ -71,7 +71,6 @@
|
|
71
71
|
|
72
72
|
awake() {
|
73
73
|
NeedleXRSession.getXRSync(this.context);
|
74
|
-
if (debug) window.addEventListener("keydown", evt => { if (evt.key === "x") this.exitXR(); });
|
75
74
|
}
|
76
75
|
|
77
76
|
onEnable(): void {
|
@@ -102,17 +101,11 @@
|
|
102
101
|
this._playerSync.onPlayerSpawned?.removeEventListener(this.onAvatarSpawned);
|
103
102
|
this._playerSync.onPlayerSpawned?.addEventListener(this.onAvatarSpawned);
|
104
103
|
}
|
105
|
-
|
106
|
-
// if a button container exists and is not attached to any parent element we attach it to the shadow root (assuming it was removed during onDisable)
|
107
|
-
if (this._container && !this._container.parentNode) {
|
108
|
-
this.context.domElement.shadowRoot?.appendChild(this._container);
|
109
|
-
}
|
110
104
|
}
|
111
105
|
|
112
106
|
onDisable(): void {
|
113
|
-
// remove the container automatically if it was added to the shadow root
|
114
|
-
this._container?.remove();
|
115
107
|
this._usdzExporter?.destroy();
|
108
|
+
this.removeButtons();
|
116
109
|
}
|
117
110
|
|
118
111
|
private async handleOfferSession() {
|
@@ -183,7 +176,7 @@
|
|
183
176
|
sessionroot.arTouchTransform = this.usePlacementAdjustment;
|
184
177
|
sessionroot.useXRAnchor = this.useXRAnchor;
|
185
178
|
}
|
186
|
-
else if(debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
|
179
|
+
else if (debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
|
187
180
|
}
|
188
181
|
|
189
182
|
// handle VR controls
|
@@ -260,42 +253,62 @@
|
|
260
253
|
|
261
254
|
|
262
255
|
// HTML UI
|
263
|
-
|
264
|
-
|
265
|
-
getButtonsContainer():
|
266
|
-
|
267
|
-
|
256
|
+
|
257
|
+
/** @deprecated use `getButtonsFactory()` or access `WebXRButtonFactory.getOrCreate()` directory */
|
258
|
+
getButtonsContainer(): WebXRButtonFactory {
|
259
|
+
return this.getButtonsFactory();
|
260
|
+
}
|
261
|
+
|
262
|
+
/** Calling this function will get the Needle WebXR button factory (it will be created if it doesnt exist yet)
|
263
|
+
* @returns the Needle WebXR button factory */
|
264
|
+
getButtonsFactory(): WebXRButtonFactory {
|
265
|
+
if (!this._buttonFactory) {
|
266
|
+
this._buttonFactory = WebXRButtonFactory.getOrCreate();
|
268
267
|
}
|
269
|
-
return this.
|
268
|
+
return this._buttonFactory;
|
270
269
|
}
|
271
270
|
|
272
|
-
private
|
271
|
+
private _buttonFactory?: WebXRButtonFactory;
|
272
|
+
|
273
273
|
private handleCreatingHTML() {
|
274
274
|
|
275
275
|
if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
|
276
276
|
// Quicklook / iOS
|
277
277
|
if ((isiOS() && isSafari()) || debugQuicklook) {
|
278
278
|
if (this.useQuicklookExport) {
|
279
|
-
this.
|
279
|
+
this.addButton(this.getButtonsFactory().createQuicklookButton());
|
280
280
|
}
|
281
281
|
}
|
282
282
|
// WebXR
|
283
|
-
if (this.createARButton) this.
|
284
|
-
if (this.createVRButton) this.
|
283
|
+
if (this.createARButton) this.addButton(this.getButtonsFactory().createARButton());
|
284
|
+
if (this.createVRButton) this.addButton(this.getButtonsFactory().createVRButton());
|
285
285
|
}
|
286
286
|
|
287
287
|
if (this.createSendToQuestButton && !isQuest()) {
|
288
288
|
NeedleXRSession.isVRSupported().then(supported => {
|
289
|
-
if (!supported) this.
|
289
|
+
if (!supported) this.addButton(this.getButtonsFactory().createSendToQuestButton());
|
290
290
|
});
|
291
291
|
}
|
292
292
|
|
293
293
|
if (this.createQRCode && !isMobileDevice()) {
|
294
294
|
NeedleXRSession.isXRSupported().then(supported => {
|
295
|
-
if (isDesktop() || !supported) this.
|
295
|
+
if (isDesktop() || !supported) this.addButton(this.getButtonsFactory().createQRCode());
|
296
296
|
});
|
297
297
|
}
|
298
298
|
}
|
299
299
|
|
300
|
+
private readonly _buttons: HTMLElement[] = [];
|
300
301
|
|
302
|
+
private addButton(button: HTMLElement) {
|
303
|
+
this._buttons.push(button);
|
304
|
+
this.context.menu.appendChild(button);
|
305
|
+
}
|
306
|
+
|
307
|
+
private removeButtons() {
|
308
|
+
for (const button of this._buttons) {
|
309
|
+
button.remove();
|
310
|
+
}
|
311
|
+
this._buttons.length = 0;
|
312
|
+
}
|
313
|
+
|
301
314
|
}
|
@@ -1,162 +1,75 @@
|
|
1
1
|
import { isDevEnvironment } from "../../engine/debug/index.js";
|
2
|
-
import { Context } from "../../engine/engine_context.js";
|
3
2
|
import { generateQRCode } from "../../engine/engine_utils.js";
|
4
3
|
import { isMozillaXR } from "../../engine/engine_utils.js";
|
5
4
|
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
6
5
|
import { GameObject } from "../Component.js";
|
7
6
|
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
8
7
|
|
9
|
-
|
8
|
+
// TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested)
|
10
9
|
|
11
|
-
|
10
|
+
export class WebXRButtonFactory {
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
return document.createElement(webXRElementName) as NeedleWebXRHtmlElement;
|
12
|
+
private static _instance: WebXRButtonFactory;
|
13
|
+
private static create() {
|
14
|
+
return new WebXRButtonFactory();
|
17
15
|
}
|
18
16
|
|
19
|
-
static getOrCreate(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
domElement.appendChild(el);
|
25
|
-
};
|
26
|
-
return el as NeedleWebXRHtmlElement;
|
17
|
+
static getOrCreate() {
|
18
|
+
if (!this._instance) {
|
19
|
+
this._instance = this.create();
|
20
|
+
}
|
21
|
+
return this._instance;
|
27
22
|
}
|
28
23
|
|
29
|
-
private
|
24
|
+
private get isSecureConnection() { return window.location.protocol === "https:"; }
|
30
25
|
|
31
|
-
constructor() {
|
32
|
-
super();
|
33
|
-
this.attachShadow({ mode: 'open' });
|
34
|
-
const template = document.createElement('template');
|
35
|
-
template.innerHTML = `<style>
|
36
|
-
:host {
|
37
|
-
position: absolute;
|
38
|
-
display: flex;
|
39
|
-
flex-wrap: wrap;
|
40
|
-
justify-content: center;
|
41
|
-
/** increase z-index (nipplejs has 999 as default) */
|
42
|
-
z-index: 5000;
|
43
|
-
width: 100%;
|
44
|
-
bottom: 100px;
|
45
|
-
left: 50%;
|
46
|
-
transform: translateX(-50%);
|
47
|
-
pointer-events: none;
|
48
|
-
}
|
49
|
-
:host button {
|
50
|
-
font-family: Roboto, sans-serif, Arial;
|
51
|
-
border: none;
|
52
|
-
color: black;
|
53
|
-
background: rgba(255, 255, 255, 1);
|
54
|
-
margin: 5px 5px;
|
55
|
-
padding: 0.5rem .7rem;
|
56
|
-
font-size: 1rem;
|
57
|
-
white-space: nowrap;
|
58
|
-
transition: all 0.2s ease-in-out;
|
59
|
-
border-radius: .2rem;
|
60
|
-
border: rgba(255, 255, 255, 0.2) solid 1px;
|
61
|
-
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
62
|
-
font-weight: normal;
|
63
|
-
pointer-events: all;
|
64
|
-
}
|
65
|
-
:host button:hover {
|
66
|
-
cursor: pointer;
|
67
|
-
background: rgba(255, 255, 255, 1);
|
68
|
-
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1), 0 0 15px rgba(255, 255, 255, 0.2);
|
69
|
-
transition: all 0.1s ease-in-out;
|
70
|
-
}
|
71
|
-
:host button:disabled {
|
72
|
-
background: rgba(200, 200, 200, 1);
|
73
|
-
color: rgba(100, 100, 100, 1);
|
74
|
-
border: rgba(0,0,0,0) 1px solid;
|
75
|
-
box-shadow: none;
|
76
|
-
cursor: initial;
|
77
|
-
}
|
78
|
-
:host button.this-mode-is-requested {
|
79
|
-
font-weight: bold;
|
80
|
-
background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
|
81
|
-
background-size: 200% auto;
|
82
|
-
background-position: 0 100%;
|
83
|
-
animation: AnimationName .7s ease infinite forwards;
|
84
|
-
}
|
85
|
-
:host button.other-mode-is-requested {
|
86
|
-
}
|
87
|
-
|
88
|
-
@keyframes AnimationName {
|
89
|
-
0% { background-position: 0% 0 }
|
90
|
-
100% { background-position: -200% 0 }
|
91
|
-
}
|
92
26
|
|
93
|
-
|
94
|
-
|
95
|
-
display: initial;
|
96
|
-
bottom: 100%;
|
97
|
-
left: 50%;
|
98
|
-
transform: translateX(-50%) translateY(-10px);
|
99
|
-
background-color: white;
|
100
|
-
padding: 1.2rem;
|
101
|
-
border-radius: 0.4rem;
|
102
|
-
pointer-events: all;
|
103
|
-
opacity: 1;
|
104
|
-
transition: opacity 0.2s ease-in-out;
|
105
|
-
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
106
|
-
}
|
27
|
+
get quicklookButton() { return this._quicklookButton }
|
28
|
+
private _quicklookButton?: HTMLButtonElement;
|
107
29
|
|
108
|
-
|
109
|
-
|
110
|
-
}
|
30
|
+
get arButton() { return this._arButton; }
|
31
|
+
private _arButton?: HTMLButtonElement;
|
111
32
|
|
112
|
-
|
113
|
-
|
114
|
-
display: none; /* prevents the QR code from overflowing the body when it's actually disabled but breaks animation */
|
115
|
-
pointer-events: none;
|
116
|
-
}
|
117
|
-
</style>
|
118
|
-
`;
|
33
|
+
get vrButton() { return this._vrButton }
|
34
|
+
private _vrButton?: HTMLButtonElement;
|
119
35
|
|
120
|
-
|
121
|
-
|
122
|
-
this.root.classList.add("needs-https");
|
123
|
-
}
|
124
|
-
if (this.shadowRoot) {
|
125
|
-
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
126
|
-
this.shadowRoot.appendChild(this.root);
|
127
|
-
}
|
128
|
-
}
|
36
|
+
get sendToQuestButton() { return this._sendToQuestButton; }
|
37
|
+
private _sendToQuestButton?: HTMLButtonElement;
|
129
38
|
|
130
|
-
|
39
|
+
get qrButton() { return this._qrButton; }
|
40
|
+
private _qrButton?: HTMLButtonElement;
|
131
41
|
|
132
|
-
|
133
|
-
get quicklookButton() { return this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null; }
|
42
|
+
|
134
43
|
/** get or create the quicklook button
|
135
44
|
* Behaviour of the button:
|
136
45
|
* - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
|
137
46
|
*/
|
138
47
|
createQuicklookButton(): HTMLButtonElement {
|
139
|
-
|
140
|
-
|
48
|
+
if (this._quicklookButton) return this._quicklookButton;
|
49
|
+
|
141
50
|
const button = document.createElement("button");
|
51
|
+
this._quicklookButton = button;
|
52
|
+
|
142
53
|
button.dataset["needle"] = "quicklook-button";
|
143
54
|
button.innerText = "Open in Quicklook";
|
144
55
|
button.addEventListener("click", () => {
|
145
56
|
const usdzExporter = GameObject.findObjectOfType(USDZExporter);
|
146
57
|
if (usdzExporter) {
|
147
|
-
|
58
|
+
button.classList.add("this-mode-is-requested");
|
59
|
+
usdzExporter.exportAsync().then(() => {
|
60
|
+
button.classList.remove("this-mode-is-requested");
|
61
|
+
}).catch(err => {
|
62
|
+
button.classList.remove("this-mode-is-requested");
|
63
|
+
console.error(err);
|
64
|
+
});
|
148
65
|
}
|
149
66
|
else {
|
150
67
|
console.warn("No USDZExporter component found in the scene");
|
151
68
|
}
|
152
69
|
});
|
153
70
|
this.hideElementDuringXRSession(button);
|
154
|
-
this.root?.appendChild(button);
|
155
71
|
return button;
|
156
72
|
}
|
157
|
-
|
158
|
-
/** @returns the WebXR AR button if it was created */
|
159
|
-
get arButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null; }
|
160
73
|
/** get or create the WebXR AR button
|
161
74
|
* @param init optional session init options
|
162
75
|
* Behaviour of the button:
|
@@ -165,10 +78,12 @@
|
|
165
78
|
* - if the device changes and now supports AR, the button will be visible
|
166
79
|
*/
|
167
80
|
createARButton(init?: XRSessionInit): HTMLButtonElement {
|
168
|
-
|
169
|
-
|
81
|
+
if (this._arButton) return this._arButton;
|
82
|
+
|
170
83
|
const mode: XRSessionMode = "immersive-ar";
|
171
84
|
const button = document.createElement("button");
|
85
|
+
this._arButton = button;
|
86
|
+
|
172
87
|
button.classList.add("webxr-button");
|
173
88
|
button.dataset["needle"] = "webxr-ar-button";
|
174
89
|
button.innerText = "Enter AR";
|
@@ -177,7 +92,6 @@
|
|
177
92
|
this.updateSessionSupported(button, mode);
|
178
93
|
this.listenToXRSessionState(button, mode);
|
179
94
|
this.hideElementDuringXRSession(button);
|
180
|
-
this.root?.appendChild(button);
|
181
95
|
|
182
96
|
if (!this.isSecureConnection) {
|
183
97
|
button.disabled = true;
|
@@ -190,8 +104,6 @@
|
|
190
104
|
return button;
|
191
105
|
}
|
192
106
|
|
193
|
-
/** @returns the WebXR VR button if it was created */
|
194
|
-
get vrButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']") as HTMLButtonElement | null; }
|
195
107
|
/** get or create the WebXR VR button
|
196
108
|
* @param init optional session init options
|
197
109
|
* Behaviour of the button:
|
@@ -200,10 +112,11 @@
|
|
200
112
|
* - if the device changes and now supports VR, the button will be visible
|
201
113
|
*/
|
202
114
|
createVRButton(init?: XRSessionInit): HTMLButtonElement {
|
203
|
-
|
204
|
-
|
115
|
+
if (this._vrButton) return this._vrButton;
|
116
|
+
|
205
117
|
const mode: XRSessionMode = "immersive-vr";
|
206
118
|
const button = document.createElement("button");
|
119
|
+
this._vrButton = button;
|
207
120
|
button.classList.add("webxr-button");
|
208
121
|
button.dataset["needle"] = "webxr-vr-button";
|
209
122
|
button.innerText = "Enter VR";
|
@@ -212,7 +125,6 @@
|
|
212
125
|
this.updateSessionSupported(button, mode);
|
213
126
|
this.listenToXRSessionState(button, mode);
|
214
127
|
this.hideElementDuringXRSession(button);
|
215
|
-
this.root?.appendChild(button);
|
216
128
|
|
217
129
|
if (!this.isSecureConnection) {
|
218
130
|
button.disabled = true;
|
@@ -225,17 +137,15 @@
|
|
225
137
|
return button;
|
226
138
|
}
|
227
139
|
|
228
|
-
/** @returns the Send to Quest button */
|
229
|
-
get sendToQuestButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']") as HTMLButtonElement | null; }
|
230
140
|
/** get or create the Send To Quest button
|
231
141
|
* Behaviour of the button:
|
232
142
|
* - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
|
233
143
|
*/
|
234
144
|
createSendToQuestButton(): HTMLButtonElement {
|
235
|
-
|
236
|
-
if (hasButton) return hasButton as HTMLButtonElement;
|
145
|
+
if (this._sendToQuestButton) return this._sendToQuestButton;
|
237
146
|
const baseUrl = `https://oculus.com/open_url/?url=`
|
238
147
|
const button = document.createElement("button");
|
148
|
+
this._sendToQuestButton = button;
|
239
149
|
button.dataset["needle"] = "webxr-sendtoquest-button";
|
240
150
|
button.innerText = "Open on Quest";
|
241
151
|
button.title = "Click to send this page to the Oculus Browser on your Quest";
|
@@ -256,31 +166,72 @@
|
|
256
166
|
}
|
257
167
|
});
|
258
168
|
}
|
259
|
-
this.root?.appendChild(button);
|
260
169
|
return button;
|
261
170
|
}
|
262
171
|
|
263
|
-
|
264
|
-
|
265
|
-
wrapper.style.position = "relative";
|
266
|
-
wrapper.style.display = "inline-block";
|
267
|
-
this.hideElementDuringXRSession(wrapper);
|
172
|
+
createQRCode(): HTMLButtonElement {
|
173
|
+
if (this._qrButton) return this._qrButton;
|
268
174
|
|
269
|
-
const qrCodeContainer = document.createElement("div");
|
270
|
-
qrCodeContainer.classList.add("qr-code-container");
|
271
|
-
qrCodeContainer.classList.add("hidden");
|
272
|
-
generateAndInsertQRCode();
|
273
|
-
|
274
175
|
const qrCodeButton = document.createElement("button");
|
176
|
+
this._qrButton = qrCodeButton;
|
275
177
|
qrCodeButton.innerText = "QR Code";
|
276
178
|
qrCodeButton.title = "Scan this QR code with your phone to open this page";
|
179
|
+
this.hideElementDuringXRSession(qrCodeButton);
|
277
180
|
|
181
|
+
|
182
|
+
const qrCodeContainer = document.createElement("div");
|
183
|
+
qrCodeContainer.style.position = "fixed";
|
184
|
+
qrCodeContainer.style.display = "inline-block";
|
185
|
+
qrCodeContainer.style.padding = "1rem";
|
186
|
+
qrCodeContainer.style.backgroundColor = "white";
|
187
|
+
qrCodeContainer.style.borderRadius = "0.4rem";
|
188
|
+
qrCodeContainer.style.cursor = "pointer";
|
189
|
+
qrCodeContainer.style.zIndex = "1000";
|
190
|
+
qrCodeContainer.style.boxShadow = "0 0 12px rgba(0, 0, 0, 0.2)";
|
191
|
+
|
192
|
+
const qrCodeElement = document.createElement("div");
|
193
|
+
qrCodeElement.classList.add("qr-code-container");
|
194
|
+
qrCodeContainer.appendChild(qrCodeElement);
|
195
|
+
|
278
196
|
qrCodeButton.addEventListener("click", () => {
|
279
|
-
qrCodeContainer.
|
280
|
-
|
281
|
-
// generate the qr code when the button is clicked - this ensures that we get the QRcode with the latest URL
|
282
|
-
generateAndInsertQRCode();
|
197
|
+
if (qrCodeContainer.parentNode) return hideQRCode();
|
198
|
+
showQRCode();
|
283
199
|
});
|
200
|
+
|
201
|
+
/** shows the QRCode near the button */
|
202
|
+
async function showQRCode() {
|
203
|
+
// generate the qr code when the button is clicked
|
204
|
+
// this ensures that we get the QRcode with the latest URL
|
205
|
+
await generateAndInsertQRCode();
|
206
|
+
// TODO: make sure it doesnt overflow the screen
|
207
|
+
// we need to add the qrCodeContainer to the body to get the correct size
|
208
|
+
document.body.appendChild(qrCodeContainer);
|
209
|
+
const containerRect = qrCodeElement.getBoundingClientRect();
|
210
|
+
const buttonRect = qrCodeButton.getBoundingClientRect();
|
211
|
+
qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px";
|
212
|
+
const isButtonInTopHalf = buttonRect.top < containerRect.height;
|
213
|
+
if (isButtonInTopHalf)
|
214
|
+
qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} * .6)`;
|
215
|
+
else
|
216
|
+
qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} * 2.5)`;
|
217
|
+
|
218
|
+
// context click to hide the QR code again, if we dont timeout the event will be triggered immediately
|
219
|
+
setTimeout(() => window.addEventListener("click", hideQRCode, { once: true }));
|
220
|
+
window.addEventListener("resize", hideQRCode);
|
221
|
+
window.addEventListener("scroll", hideQRCode);
|
222
|
+
|
223
|
+
document.body.appendChild(qrCodeContainer);
|
224
|
+
}
|
225
|
+
|
226
|
+
/** hides to QRCode overlay and unsubscribes from events */
|
227
|
+
function hideQRCode() {
|
228
|
+
qrCodeContainer.parentNode?.removeChild(qrCodeContainer);
|
229
|
+
window.removeEventListener("click", hideQRCode);
|
230
|
+
window.removeEventListener("resize", hideQRCode);
|
231
|
+
window.removeEventListener("scroll", hideQRCode);
|
232
|
+
};
|
233
|
+
|
234
|
+
/** generates a QR code and inserts it into the qrCodeElement */
|
284
235
|
async function generateAndInsertQRCode() {
|
285
236
|
const size = 200;
|
286
237
|
const code = await generateQRCode({
|
@@ -288,14 +239,16 @@
|
|
288
239
|
width: size,
|
289
240
|
height: size,
|
290
241
|
});
|
291
|
-
|
292
|
-
|
242
|
+
qrCodeElement.innerHTML = "";
|
243
|
+
qrCodeElement.appendChild(code);
|
293
244
|
}
|
294
245
|
|
295
|
-
|
296
|
-
|
246
|
+
// lazily create the qr button
|
247
|
+
qrCodeButton.addEventListener("pointerenter", () => {
|
248
|
+
generateAndInsertQRCode();
|
249
|
+
}, { once: true });
|
297
250
|
|
298
|
-
|
251
|
+
return qrCodeButton;
|
299
252
|
}
|
300
253
|
|
301
254
|
private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
|
@@ -346,5 +299,6 @@
|
|
346
299
|
}
|
347
300
|
}
|
348
301
|
|
349
|
-
|
350
|
-
|
302
|
+
|
303
|
+
/** @deprecated please use WebXRButtonFactory. This type will be removed in a future update */
|
304
|
+
export type NeedleWebXRHtmlElement = WebXRButtonFactory;
|
@@ -0,0 +1,2 @@
|
|
1
|
+
|
2
|
+
export { NeedleMenu } from "./needle-menu.js";
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import { NeedleLogoElement } from "./logo-element.js";
|
2
|
+
|
3
|
+
const elementName = "needle-license-banner";
|
4
|
+
|
5
|
+
export class LicenseBanner extends HTMLElement {
|
6
|
+
|
7
|
+
static create() {
|
8
|
+
return document.createElement(elementName);
|
9
|
+
}
|
10
|
+
|
11
|
+
constructor() {
|
12
|
+
|
13
|
+
super();
|
14
|
+
this.attachShadow({ mode: 'open' });
|
15
|
+
const template = document.createElement('template');
|
16
|
+
template.innerHTML = `<style>
|
17
|
+
:host {
|
18
|
+
position: relative;
|
19
|
+
width: fit-content;
|
20
|
+
height: fit-content;
|
21
|
+
min-width: 100px;
|
22
|
+
min-height: 30px;
|
23
|
+
background: white;
|
24
|
+
border-radius: .3rem;
|
25
|
+
display: flex;
|
26
|
+
flex-direction: column;
|
27
|
+
justify-content: center;
|
28
|
+
align-items: start;
|
29
|
+
padding: .2rem 1rem .25rem .5rem;
|
30
|
+
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, .1);
|
31
|
+
}
|
32
|
+
`;
|
33
|
+
const content = template.content.cloneNode(true) as HTMLElement;
|
34
|
+
content.title = "Made with Needle Engine";
|
35
|
+
|
36
|
+
this.shadowRoot?.appendChild(content);
|
37
|
+
|
38
|
+
const logo = NeedleLogoElement.create();
|
39
|
+
this.shadowRoot?.appendChild(logo);
|
40
|
+
|
41
|
+
this.addEventListener("click", () => {
|
42
|
+
globalThis.open("https://needle.tools", "_blank");
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
}
|
47
|
+
if (!customElements.get(elementName))
|
48
|
+
customElements.define(elementName, LicenseBanner);
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { madeWithNeedleSVG, needleLogoOnlySVG, needleLogoSVG } from "../assets/index.js";
|
2
|
+
|
3
|
+
const elementName = "needle-logo-element";
|
4
|
+
|
5
|
+
export class NeedleLogoElement extends HTMLElement {
|
6
|
+
|
7
|
+
static get elementName() { return elementName; }
|
8
|
+
|
9
|
+
static create(): NeedleLogoElement {
|
10
|
+
return document.createElement(elementName) as NeedleLogoElement;
|
11
|
+
}
|
12
|
+
|
13
|
+
constructor() {
|
14
|
+
super();
|
15
|
+
this._root = this.attachShadow({ mode: 'closed' });
|
16
|
+
const template = document.createElement('template');
|
17
|
+
template.innerHTML = `<style>
|
18
|
+
:host {
|
19
|
+
position: relative;
|
20
|
+
min-width: fit-content;
|
21
|
+
/* height: 100%; can not have height 100% because of align-items: stretch; in the parent */
|
22
|
+
display: flex;
|
23
|
+
margin: 0 0.3rem;
|
24
|
+
}
|
25
|
+
|
26
|
+
.wrapper {
|
27
|
+
position: relative;
|
28
|
+
display: grid;
|
29
|
+
grid-template-columns: auto auto;
|
30
|
+
padding: .1rem;
|
31
|
+
}
|
32
|
+
.wrapper:hover {
|
33
|
+
cursor: pointer;
|
34
|
+
}
|
35
|
+
img {
|
36
|
+
width: 95px;
|
37
|
+
height: 100%;
|
38
|
+
align-self: end;
|
39
|
+
}
|
40
|
+
span {
|
41
|
+
font-size: 1rem;
|
42
|
+
white-space: nowrap;
|
43
|
+
}
|
44
|
+
</style>
|
45
|
+
<div class="wrapper">
|
46
|
+
<img class="logo" src=${needleLogoSVG} />
|
47
|
+
</div>
|
48
|
+
`;
|
49
|
+
this._root.appendChild(template.content.cloneNode(true));
|
50
|
+
this.wrapper = this._root.querySelector(".wrapper") as HTMLDivElement;
|
51
|
+
this._root.appendChild(this.wrapper);
|
52
|
+
// this.wrapper.classList.add("wrapper");
|
53
|
+
|
54
|
+
// this.wrapper.appendChild(this.logoElement);
|
55
|
+
// this.logoElement.src = logoSVG;
|
56
|
+
|
57
|
+
// this.textElement.textContent = "Needle Engine";
|
58
|
+
// this.wrapper.appendChild(this.textElement);
|
59
|
+
|
60
|
+
this.addEventListener("click", () => {
|
61
|
+
globalThis.open("https://needle.tools", "_blank");
|
62
|
+
});
|
63
|
+
|
64
|
+
// set title
|
65
|
+
this.wrapper.setAttribute("title", "Made with Needle Engine");
|
66
|
+
}
|
67
|
+
|
68
|
+
private readonly _root: ShadowRoot;
|
69
|
+
private readonly wrapper: HTMLDivElement;
|
70
|
+
private readonly logoElement: HTMLImageElement = document.createElement("img");
|
71
|
+
private readonly textElement: HTMLSpanElement = document.createElement("span");
|
72
|
+
|
73
|
+
setLogoVisible(val: boolean) {
|
74
|
+
this.logoElement.style.display = val ? "block" : "none";
|
75
|
+
}
|
76
|
+
|
77
|
+
}
|
78
|
+
if (!customElements.get(elementName))
|
79
|
+
customElements.define(elementName, NeedleLogoElement);
|
@@ -0,0 +1,470 @@
|
|
1
|
+
import { isDevEnvironment } from "../debug/index.js";
|
2
|
+
import type { Context } from "../engine_context.js";
|
3
|
+
import { hasProLicense, onLicenseCheckResultChanged } from "../engine_license.js";
|
4
|
+
import { getParam } from "../engine_utils.js";
|
5
|
+
import { NeedleLogoElement } from "./logo-element.js";
|
6
|
+
|
7
|
+
const elementName = "needle-menu";
|
8
|
+
const debug = getParam("debugmenu");
|
9
|
+
|
10
|
+
declare type MenuButtonOption = {
|
11
|
+
text: string,
|
12
|
+
onClick: () => void,
|
13
|
+
}
|
14
|
+
|
15
|
+
export class NeedleMenu {
|
16
|
+
private readonly _context: Context;
|
17
|
+
private readonly _menu: NeedleMenuElement;
|
18
|
+
|
19
|
+
constructor(context: Context) {
|
20
|
+
this._menu = NeedleMenuElement.getOrCreate(context.domElement);
|
21
|
+
this._context = context;
|
22
|
+
}
|
23
|
+
|
24
|
+
/** Experimental: Change the menu position to be at the top or the bottom of the needle engine webcomponent
|
25
|
+
* @param position "top" or "bottom"
|
26
|
+
*/
|
27
|
+
setPosition(position: "top" | "bottom") {
|
28
|
+
this._menu.setPosition(position);
|
29
|
+
}
|
30
|
+
|
31
|
+
/** When set to false, the Needle Engine logo will be hidden. Hiding the logo requires a needle engine license */
|
32
|
+
showNeedleLogo(visible: boolean) {
|
33
|
+
this._menu.showNeedleLogo(visible);
|
34
|
+
}
|
35
|
+
|
36
|
+
appendChild(child: HTMLElement) {
|
37
|
+
this._menu.appendChild(child);
|
38
|
+
}
|
39
|
+
|
40
|
+
}
|
41
|
+
|
42
|
+
export class NeedleMenuElement extends HTMLElement {
|
43
|
+
|
44
|
+
static create() {
|
45
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#is
|
46
|
+
return document.createElement(elementName, { is: elementName });
|
47
|
+
}
|
48
|
+
|
49
|
+
static getOrCreate(domElement: HTMLElement) {
|
50
|
+
let element = domElement.querySelector(elementName) as NeedleMenuElement | null;
|
51
|
+
if (!element && domElement.shadowRoot) {
|
52
|
+
element = domElement.shadowRoot.querySelector(elementName);
|
53
|
+
}
|
54
|
+
if (!element) {
|
55
|
+
element = NeedleMenuElement.create() as NeedleMenuElement;
|
56
|
+
element._domElement = domElement;
|
57
|
+
if (domElement.shadowRoot)
|
58
|
+
domElement.shadowRoot.appendChild(element);
|
59
|
+
else
|
60
|
+
domElement.appendChild(element);
|
61
|
+
}
|
62
|
+
return element as NeedleMenuElement;
|
63
|
+
}
|
64
|
+
|
65
|
+
private _domElement: HTMLElement | null = null;
|
66
|
+
|
67
|
+
constructor() {
|
68
|
+
super();
|
69
|
+
const template = document.createElement('template');
|
70
|
+
// TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space
|
71
|
+
template.innerHTML = `<style>
|
72
|
+
|
73
|
+
#root {
|
74
|
+
position: absolute;
|
75
|
+
width: auto;
|
76
|
+
max-width: 95%;
|
77
|
+
left: 50%;
|
78
|
+
transform: translateX(-50%);
|
79
|
+
top: 20px;
|
80
|
+
padding: 0.3rem;
|
81
|
+
background: #ffffff5c;
|
82
|
+
display: flex;
|
83
|
+
flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */
|
84
|
+
outline: rgb(0 0 0 / 5%) 1px solid;
|
85
|
+
border: 1px solid rgba(255, 255, 255, .1);
|
86
|
+
border-radius: 1.1999999999999993rem;
|
87
|
+
overflow: clip;
|
88
|
+
box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
|
89
|
+
backdrop-filter: blur(16px);
|
90
|
+
}
|
91
|
+
|
92
|
+
/** using a div here because then we can change the class for placement **/
|
93
|
+
#root.bottom {
|
94
|
+
top: auto;
|
95
|
+
bottom: 30px;
|
96
|
+
}
|
97
|
+
|
98
|
+
.wrapper {
|
99
|
+
position: relative;
|
100
|
+
display: flex;
|
101
|
+
flex-direction: row;
|
102
|
+
justify-content: center;
|
103
|
+
align-items: stretch;
|
104
|
+
gap: 0px;
|
105
|
+
padding: 0 .3rem;
|
106
|
+
}
|
107
|
+
|
108
|
+
.wrapper > *, .options > * {
|
109
|
+
position: relative;
|
110
|
+
border: none;
|
111
|
+
border-radius: 0;
|
112
|
+
outline: 1px solid rgba(0,0,0,0);
|
113
|
+
display: flex;
|
114
|
+
justify-content: center;
|
115
|
+
align-items: center;
|
116
|
+
|
117
|
+
/** basic font settings for all entries **/
|
118
|
+
font-size: 1rem;
|
119
|
+
font-family: 'Roboto Flex', sans-serif;
|
120
|
+
font-optical-sizing: auto;
|
121
|
+
font-weight: normal;
|
122
|
+
font-variation-settings: "wdth" 100;
|
123
|
+
color: rgb(40,40,40);
|
124
|
+
}
|
125
|
+
|
126
|
+
.options > * {
|
127
|
+
padding: .4rem .5rem;
|
128
|
+
}
|
129
|
+
|
130
|
+
:host .options > * {
|
131
|
+
background: transparent;
|
132
|
+
border: none;
|
133
|
+
white-space: nowrap;
|
134
|
+
transition: all 0.1s linear .02s;
|
135
|
+
border-radius: 0.8rem;
|
136
|
+
}
|
137
|
+
:host .options > *:hover {
|
138
|
+
cursor: pointer;
|
139
|
+
color: black;
|
140
|
+
background: rgba(245, 245, 245, .8);
|
141
|
+
outline: rgba(0,0,0,.05) 1px solid;
|
142
|
+
}
|
143
|
+
|
144
|
+
|
145
|
+
/** XR button animation **/
|
146
|
+
:host button.this-mode-is-requested {
|
147
|
+
background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
|
148
|
+
background-size: 200% auto;
|
149
|
+
background-position: 0 100%;
|
150
|
+
animation: AnimationName .7s ease infinite forwards;
|
151
|
+
}
|
152
|
+
:host button.other-mode-is-requested {
|
153
|
+
opacity: .5;
|
154
|
+
}
|
155
|
+
|
156
|
+
@keyframes AnimationName {
|
157
|
+
0% { background-position: 0% 0 }
|
158
|
+
100% { background-position: -200% 0 }
|
159
|
+
}
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
.logo {
|
165
|
+
cursor: pointer;
|
166
|
+
padding-left: 0.6rem;
|
167
|
+
}
|
168
|
+
:host .logo.any-options {
|
169
|
+
border-left: 1px solid rgba(40,40,40,.4);
|
170
|
+
margin-left: 0.3rem;
|
171
|
+
}
|
172
|
+
|
173
|
+
.logo > span {
|
174
|
+
white-space: nowrap;
|
175
|
+
}
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
/** COMPACT */
|
180
|
+
.compact .wrapper, .compact .options {
|
181
|
+
height: auto;
|
182
|
+
max-height: initial;
|
183
|
+
flex-direction: column-reverse;
|
184
|
+
}
|
185
|
+
|
186
|
+
.compact .options > * {
|
187
|
+
font-size: 1.2rem;
|
188
|
+
padding: .6rem .5rem;
|
189
|
+
}
|
190
|
+
.compact .top .options {
|
191
|
+
height: auto;
|
192
|
+
flex-direction: column-reverse;
|
193
|
+
}
|
194
|
+
.compact .bottom .wrapper {
|
195
|
+
height: auto;
|
196
|
+
flex-direction: column;
|
197
|
+
}
|
198
|
+
.compact .logo {
|
199
|
+
padding-left: 0;
|
200
|
+
margin-left: 0.3rem;
|
201
|
+
}
|
202
|
+
.compact.bottom .logo.any-options {
|
203
|
+
border: none;
|
204
|
+
border-bottom: 1px solid rgba(40,40,40,.4);
|
205
|
+
padding-bottom: .4rem;
|
206
|
+
margin-bottom: .5rem;
|
207
|
+
}
|
208
|
+
.compact.top .logo.any-options {
|
209
|
+
border: none;
|
210
|
+
border-top: 1px solid rgba(40,40,40,.4);
|
211
|
+
padding-top: .4rem;
|
212
|
+
margin-top: .5rem;
|
213
|
+
}
|
214
|
+
.compact .options > button {
|
215
|
+
width: 100%;
|
216
|
+
}
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
/* dark mode */
|
221
|
+
/*
|
222
|
+
@media (prefers-color-scheme: dark) {
|
223
|
+
:host {
|
224
|
+
background: rgba(0,0,0, .6);
|
225
|
+
}
|
226
|
+
:host button {
|
227
|
+
color: rgba(200,200,200);
|
228
|
+
}
|
229
|
+
:host button:hover {
|
230
|
+
background: rgba(100,100,100, .8);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
*/
|
234
|
+
|
235
|
+
|
236
|
+
|
237
|
+
</style>
|
238
|
+
|
239
|
+
<div id="root" class="bottom">
|
240
|
+
<div class="wrapper">
|
241
|
+
<div class="options"></div>
|
242
|
+
<div class="logo">
|
243
|
+
<span class="madewith">powered by</span>
|
244
|
+
</div>
|
245
|
+
</div>
|
246
|
+
</div>
|
247
|
+
`;
|
248
|
+
|
249
|
+
// we dont need to expose the shadow root
|
250
|
+
const shadow = this.attachShadow({ mode: 'closed' });
|
251
|
+
const content = template.content.cloneNode(true) as DocumentFragment;
|
252
|
+
shadow?.appendChild(content);
|
253
|
+
this.root = shadow.querySelector("#root") as HTMLDivElement;
|
254
|
+
|
255
|
+
this.wrapper = this.root?.querySelector(".wrapper") as HTMLDivElement;
|
256
|
+
this.options = this.root?.querySelector(".options") as HTMLDivElement;
|
257
|
+
this.logoContainer = this.root?.querySelector(".logo") as HTMLDivElement;
|
258
|
+
|
259
|
+
this.root?.appendChild(this.wrapper);
|
260
|
+
this.wrapper.classList.add("wrapper");
|
261
|
+
|
262
|
+
const logo = NeedleLogoElement.create();
|
263
|
+
logo.style.minHeight = "1rem";
|
264
|
+
this.logoContainer.append(logo);
|
265
|
+
this.logoContainer.addEventListener("click", () => {
|
266
|
+
globalThis.open("https://needle.tools", "_blank");
|
267
|
+
});
|
268
|
+
|
269
|
+
// if the user has a license then we CAN hide the needle logo
|
270
|
+
onLicenseCheckResultChanged(res => {
|
271
|
+
if (res == true && hasProLicense()) {
|
272
|
+
this.logoContainer.style.display = this._userRequestedLogoVisible ? "" : "none";
|
273
|
+
}
|
274
|
+
});
|
275
|
+
|
276
|
+
|
277
|
+
// watch changes
|
278
|
+
const observer = new MutationObserver(mutations => {
|
279
|
+
for (const mutation of mutations) {
|
280
|
+
if (mutation.type === 'childList') {
|
281
|
+
this.onOptionsChildrenChanged(mutation);
|
282
|
+
}
|
283
|
+
}
|
284
|
+
this.onChangeDetected(mutations);
|
285
|
+
});
|
286
|
+
observer.observe(this.root, { childList: true, subtree: true, attributes: true });
|
287
|
+
|
288
|
+
|
289
|
+
|
290
|
+
if (debug) {
|
291
|
+
this.___insertDebugOptions();
|
292
|
+
}
|
293
|
+
}
|
294
|
+
|
295
|
+
connectedCallback() {
|
296
|
+
window.addEventListener("resize", this.handleSizeChange);
|
297
|
+
this._domElement?.addEventListener("resize", this.handleSizeChange);
|
298
|
+
this.handleMenuVisible();
|
299
|
+
}
|
300
|
+
disconnectedCallback() {
|
301
|
+
window.removeEventListener("resize", this.handleSizeChange);
|
302
|
+
this._domElement?.removeEventListener("resize", this.handleSizeChange);
|
303
|
+
}
|
304
|
+
|
305
|
+
private _userRequestedLogoVisible?: boolean = undefined;
|
306
|
+
showNeedleLogo(visible: boolean) {
|
307
|
+
this._userRequestedLogoVisible = visible;
|
308
|
+
if (!hasProLicense()) {
|
309
|
+
if (isDevEnvironment()) console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
|
310
|
+
return;
|
311
|
+
}
|
312
|
+
this.logoContainer.style.display = visible ? "" : "none";
|
313
|
+
}
|
314
|
+
|
315
|
+
setPosition(position: "top" | "bottom") {
|
316
|
+
// ensure the position is of a known type:
|
317
|
+
if (position !== "top" && position !== "bottom") {
|
318
|
+
return console.error("NeedleMenu.setPosition: invalid position", position);
|
319
|
+
}
|
320
|
+
this.root.classList.remove("top", "bottom");
|
321
|
+
this.root.classList.add(position);
|
322
|
+
}
|
323
|
+
|
324
|
+
// private _root: ShadowRoot | null = null;
|
325
|
+
private readonly root: HTMLDivElement;
|
326
|
+
/** wraps the whole content */
|
327
|
+
private readonly wrapper: HTMLDivElement;
|
328
|
+
/** contains the buttons and dynamic elements */
|
329
|
+
private readonly options: HTMLDivElement;
|
330
|
+
/** contains the needle-logo html element */
|
331
|
+
private readonly logoContainer: HTMLDivElement;
|
332
|
+
|
333
|
+
append(...nodes: (string | Node)[]): void {
|
334
|
+
for (const node of nodes) {
|
335
|
+
if (typeof node === "string") {
|
336
|
+
const element = document.createTextNode(node);
|
337
|
+
this.options.appendChild(element);
|
338
|
+
} else {
|
339
|
+
this.options.appendChild(node);
|
340
|
+
}
|
341
|
+
}
|
342
|
+
}
|
343
|
+
appendChild<T extends Node>(node: T): T {
|
344
|
+
return this.options.appendChild(node);
|
345
|
+
}
|
346
|
+
prepend(...nodes: (string | Node)[]): void {
|
347
|
+
for (const node of nodes) {
|
348
|
+
if (typeof node === "string") {
|
349
|
+
const element = document.createTextNode(node);
|
350
|
+
this.options.prepend(element);
|
351
|
+
} else {
|
352
|
+
this.options.prepend(node);
|
353
|
+
}
|
354
|
+
}
|
355
|
+
}
|
356
|
+
|
357
|
+
|
358
|
+
|
359
|
+
/** Called when any change in the web component is detected (including in children and child attributes) */
|
360
|
+
private onChangeDetected(_mut: MutationRecord[]) {
|
361
|
+
// if (debug) console.log("NeedleMenu.onChangeDetected", _mut);
|
362
|
+
this.handleMenuVisible();
|
363
|
+
}
|
364
|
+
|
365
|
+
private onOptionsChildrenChanged(_mut: MutationRecord) {
|
366
|
+
let anyVisibleOptions = false;
|
367
|
+
for (let i = 0; i < this.options.children.length; i++) {
|
368
|
+
const child = this.options.children[i] as HTMLElement;
|
369
|
+
if (child.style.display != "none") {
|
370
|
+
anyVisibleOptions = true;
|
371
|
+
break;
|
372
|
+
}
|
373
|
+
}
|
374
|
+
this.logoContainer.classList.toggle("any-options", anyVisibleOptions);
|
375
|
+
this.handleSizeChange();
|
376
|
+
}
|
377
|
+
|
378
|
+
|
379
|
+
|
380
|
+
|
381
|
+
/** checks if the menu has any content and should be rendered at all
|
382
|
+
* if we dont have any content and logo then we hide the menu
|
383
|
+
*/
|
384
|
+
private handleMenuVisible() {
|
385
|
+
if (debug) console.log("Update VisibleState: Any Content?", this.hasAnyContent);
|
386
|
+
if (this.hasAnyContent) {
|
387
|
+
this.root.style.display = "";
|
388
|
+
} else {
|
389
|
+
this.root.style.display = "none";
|
390
|
+
}
|
391
|
+
}
|
392
|
+
|
393
|
+
/** @returns true if we have any content OR a logo */
|
394
|
+
get hasAnyContent() {
|
395
|
+
return this.options.children.length > 0 || this.logoContainer.style.display != "none";
|
396
|
+
}
|
397
|
+
|
398
|
+
|
399
|
+
private _lastAvailableWidthChange = 0;
|
400
|
+
private _timeoutHandle: number = 0;
|
401
|
+
|
402
|
+
private handleSizeChange = () => {
|
403
|
+
if (!this._domElement) return;
|
404
|
+
|
405
|
+
const width = this._domElement.clientWidth;
|
406
|
+
if (width < 500) {
|
407
|
+
this.root.classList.add("compact");
|
408
|
+
return;
|
409
|
+
}
|
410
|
+
|
411
|
+
const padding = 50;
|
412
|
+
const availableWidth = width - padding * 2;
|
413
|
+
|
414
|
+
// if the available width has not changed significantly then we can skip the rest
|
415
|
+
if (Math.abs(availableWidth - this._lastAvailableWidthChange) < 5) return;
|
416
|
+
this._lastAvailableWidthChange = availableWidth;
|
417
|
+
|
418
|
+
clearTimeout(this._timeoutHandle!);
|
419
|
+
|
420
|
+
this._timeoutHandle = setTimeout(() => {
|
421
|
+
const currentWidth = this.root.clientWidth;
|
422
|
+
const spaceLeft = availableWidth - currentWidth;
|
423
|
+
if (spaceLeft <= 0) {
|
424
|
+
this.root.classList.add("compact")
|
425
|
+
}
|
426
|
+
else if (spaceLeft > 5) {
|
427
|
+
this.root.classList.remove("compact")
|
428
|
+
}
|
429
|
+
}, 200) as unknown as number;
|
430
|
+
|
431
|
+
}
|
432
|
+
|
433
|
+
|
434
|
+
|
435
|
+
private ___insertDebugOptions() {
|
436
|
+
window.addEventListener("keydown", (e) => {
|
437
|
+
if (e.key === "p") {
|
438
|
+
this.setPosition(this.root.classList.contains("top") ? "bottom" : "top");
|
439
|
+
}
|
440
|
+
});
|
441
|
+
const removeOptionsButton = document.createElement("button");
|
442
|
+
removeOptionsButton.textContent = "Hide Buttons";
|
443
|
+
removeOptionsButton.onclick = () => {
|
444
|
+
const optionsChildren = new Array(this.options.children.length);
|
445
|
+
for (let i = 0; i < this.options.children.length; i++) {
|
446
|
+
optionsChildren[i] = this.options.children[i];
|
447
|
+
}
|
448
|
+
for (const child of optionsChildren) {
|
449
|
+
this.options.removeChild(child);
|
450
|
+
}
|
451
|
+
setTimeout(() => {
|
452
|
+
for (const child of optionsChildren) {
|
453
|
+
this.options.appendChild(child);
|
454
|
+
}
|
455
|
+
|
456
|
+
}, 1000)
|
457
|
+
};
|
458
|
+
this.appendChild(removeOptionsButton);
|
459
|
+
const anotherButton = document.createElement("button");
|
460
|
+
anotherButton.textContent = "Toggle Logo";
|
461
|
+
anotherButton.addEventListener("click", () => {
|
462
|
+
this.logoContainer.style.display = this.logoContainer.style.display === "none" ? "" : "none";
|
463
|
+
});
|
464
|
+
this.appendChild(anotherButton);
|
465
|
+
}
|
466
|
+
}
|
467
|
+
|
468
|
+
|
469
|
+
if (!customElements.get(elementName))
|
470
|
+
customElements.define(elementName, NeedleMenuElement);
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { serializable } from '../engine/engine_serialization.js';
|
2
|
+
import { Behaviour } from './Component.js';
|
3
|
+
|
4
|
+
/** Exposes options to editors to customize the Needle menu. */
|
5
|
+
export class NeedleMenu extends Behaviour {
|
6
|
+
|
7
|
+
@serializable()
|
8
|
+
position: "top" | "bottom" = "top";
|
9
|
+
|
10
|
+
/** Show the Needle logo in the menu (requires PRO license) */
|
11
|
+
@serializable()
|
12
|
+
showNeedleLogo: boolean = true;
|
13
|
+
|
14
|
+
onEnable() {
|
15
|
+
this.context.menu.setPosition(this.position);
|
16
|
+
this.context.menu.showNeedleLogo(this.showNeedleLogo);
|
17
|
+
}
|
18
|
+
|
19
|
+
}
|