brilliantsole 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/brilliantsole.cjs +5630 -494
- package/build/brilliantsole.cjs.map +1 -1
- package/build/brilliantsole.js +21293 -3088
- package/build/brilliantsole.js.map +1 -1
- package/build/brilliantsole.ls.js +23153 -6240
- package/build/brilliantsole.ls.js.map +1 -1
- package/build/brilliantsole.min.js +1 -1
- package/build/brilliantsole.min.js.map +1 -1
- package/build/brilliantsole.module.d.ts +1158 -74
- package/build/brilliantsole.module.js +21259 -3089
- package/build/brilliantsole.module.js.map +1 -1
- package/build/brilliantsole.module.min.d.ts +1158 -74
- package/build/brilliantsole.module.min.js +1 -1
- package/build/brilliantsole.module.min.js.map +1 -1
- package/build/brilliantsole.node.module.d.ts +869 -70
- package/build/brilliantsole.node.module.js +5608 -495
- package/build/brilliantsole.node.module.js.map +1 -1
- package/build/dts/BS.d.ts +20 -1
- package/build/dts/Device.d.ts +135 -13
- package/build/dts/DeviceManager.d.ts +3 -3
- package/build/dts/DisplayManager.d.ts +320 -0
- package/build/dts/FileTransferManager.d.ts +10 -4
- package/build/dts/connection/BaseConnectionManager.d.ts +2 -2
- package/build/dts/connection/bluetooth/BluetoothUUID.d.ts +12 -0
- package/build/dts/devicePair/DevicePair.d.ts +5 -5
- package/build/dts/sensor/SensorConfigurationManager.d.ts +2 -1
- package/build/dts/server/BaseClient.d.ts +4 -4
- package/build/dts/server/udp/UDPUtils.d.ts +1 -1
- package/build/dts/utils/ArrayBufferUtils.d.ts +1 -0
- package/build/dts/utils/BitmapUtils.d.ts +17 -0
- package/build/dts/utils/ColorUtils.d.ts +5 -0
- package/build/dts/utils/DisplayBitmapUtils.d.ts +47 -0
- package/build/dts/utils/DisplayCanvasHelper.d.ts +270 -0
- package/build/dts/utils/DisplayContextCommand.d.ts +300 -0
- package/build/dts/utils/DisplayContextState.d.ts +51 -0
- package/build/dts/utils/DisplayContextStateHelper.d.ts +9 -0
- package/build/dts/utils/DisplayManagerInterface.d.ts +173 -0
- package/build/dts/utils/DisplaySpriteSheetUtils.d.ts +72 -0
- package/build/dts/utils/DisplayUtils.d.ts +70 -0
- package/build/dts/utils/MathUtils.d.ts +16 -0
- package/build/dts/utils/PathUtils.d.ts +4 -0
- package/build/dts/utils/RangeHelper.d.ts +7 -0
- package/build/dts/utils/SpriteSheetUtils.d.ts +20 -0
- package/build/index.d.ts +1156 -72
- package/build/index.node.d.ts +867 -68
- package/examples/3d-generic/index.html +5 -0
- package/examples/3d-generic/script.js +1 -0
- package/examples/basic/index.html +335 -0
- package/examples/basic/script.js +1303 -3
- package/examples/camera/utils.js +1 -1
- package/examples/display-3d/index.html +195 -0
- package/examples/display-3d/script.js +1235 -0
- package/examples/display-canvas/aframe.js +42950 -0
- package/examples/display-canvas/index.html +245 -0
- package/examples/display-canvas/script.js +2312 -0
- package/examples/display-image/index.html +189 -0
- package/examples/display-image/script.js +1093 -0
- package/examples/display-spritesheet/index.html +960 -0
- package/examples/display-spritesheet/script.js +4243 -0
- package/examples/display-text/index.html +195 -0
- package/examples/display-text/script.js +1418 -0
- package/examples/display-wireframe/index.html +204 -0
- package/examples/display-wireframe/script.js +1167 -0
- package/examples/glasses-gestures/index.html +6 -1
- package/examples/glasses-gestures/script.js +10 -8
- package/examples/microphone/index.html +3 -1
- package/examples/punch/index.html +4 -1
- package/examples/server/script.js +0 -1
- package/package.json +10 -2
- package/src/BS.ts +92 -1
- package/src/CameraManager.ts +6 -2
- package/src/Device.ts +544 -13
- package/src/DisplayManager.ts +2989 -0
- package/src/FileTransferManager.ts +79 -26
- package/src/InformationManager.ts +8 -7
- package/src/MicrophoneManager.ts +10 -3
- package/src/TfliteManager.ts +4 -2
- package/src/WifiManager.ts +4 -1
- package/src/connection/BaseConnectionManager.ts +2 -0
- package/src/connection/bluetooth/bluetoothUUIDs.ts +36 -1
- package/src/devicePair/DevicePairPressureSensorDataManager.ts +1 -1
- package/src/scanner/NobleScanner.ts +1 -1
- package/src/sensor/SensorConfigurationManager.ts +16 -8
- package/src/server/udp/UDPServer.ts +4 -4
- package/src/server/udp/UDPUtils.ts +1 -1
- package/src/server/websocket/WebSocketClient.ts +50 -1
- package/src/utils/ArrayBufferUtils.ts +23 -5
- package/src/utils/AudioUtils.ts +1 -1
- package/src/utils/ColorUtils.ts +66 -0
- package/src/utils/DisplayBitmapUtils.ts +695 -0
- package/src/utils/DisplayCanvasHelper.ts +4222 -0
- package/src/utils/DisplayContextCommand.ts +1566 -0
- package/src/utils/DisplayContextState.ts +138 -0
- package/src/utils/DisplayContextStateHelper.ts +48 -0
- package/src/utils/DisplayManagerInterface.ts +1356 -0
- package/src/utils/DisplaySpriteSheetUtils.ts +782 -0
- package/src/utils/DisplayUtils.ts +529 -0
- package/src/utils/EventDispatcher.ts +59 -14
- package/src/utils/MathUtils.ts +88 -2
- package/src/utils/ObjectUtils.ts +6 -1
- package/src/utils/PathUtils.ts +192 -0
- package/src/utils/RangeHelper.ts +15 -3
- package/src/utils/Timer.ts +1 -1
- package/src/utils/environment.ts +15 -6
- package/examples/microphone/gender.js +0 -54
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
import * as BS from "../../build/brilliantsole.module.js";
|
|
2
|
+
/** @typedef {import("../utils/three/three.module.min").Vector2} TVector2 */
|
|
3
|
+
/** @typedef {import("../utils/three/three.module.min").Vector3} TVector3 */
|
|
4
|
+
/** @typedef {import("../utils/three/three.module.min").Quaternion} TQuaternion */
|
|
5
|
+
/** @typedef {import("../utils/three/three.module.min").Euler} TEuler */
|
|
6
|
+
|
|
7
|
+
// DEVICE
|
|
8
|
+
const device = new BS.Device();
|
|
9
|
+
window.device = device;
|
|
10
|
+
window.BS = BS;
|
|
11
|
+
|
|
12
|
+
const rotationDevice = new BS.Device();
|
|
13
|
+
window.rotationDevice = rotationDevice;
|
|
14
|
+
|
|
15
|
+
// CONNECT
|
|
16
|
+
|
|
17
|
+
const toggleConnectionButton = document.getElementById("toggleConnection");
|
|
18
|
+
toggleConnectionButton.addEventListener("click", () =>
|
|
19
|
+
device.toggleConnection()
|
|
20
|
+
);
|
|
21
|
+
device.addEventListener("connectionStatus", () => {
|
|
22
|
+
let disabled = false;
|
|
23
|
+
let innerText = device.connectionStatus;
|
|
24
|
+
switch (device.connectionStatus) {
|
|
25
|
+
case "notConnected":
|
|
26
|
+
innerText = "connect";
|
|
27
|
+
break;
|
|
28
|
+
case "connected":
|
|
29
|
+
innerText = "disconnect";
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
toggleConnectionButton.disabled = disabled;
|
|
33
|
+
toggleConnectionButton.innerText = innerText;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const toggleRotationConnectionButton = document.getElementById(
|
|
37
|
+
"toggleRotationConnection"
|
|
38
|
+
);
|
|
39
|
+
toggleRotationConnectionButton.addEventListener("click", () =>
|
|
40
|
+
rotationDevice.toggleConnection()
|
|
41
|
+
);
|
|
42
|
+
rotationDevice.addEventListener("connectionStatus", () => {
|
|
43
|
+
let disabled = false;
|
|
44
|
+
let innerText = rotationDevice.connectionStatus;
|
|
45
|
+
switch (rotationDevice.connectionStatus) {
|
|
46
|
+
case "notConnected":
|
|
47
|
+
innerText = "connect to rotator";
|
|
48
|
+
break;
|
|
49
|
+
case "connected":
|
|
50
|
+
innerText = "disconnect from rotator";
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
toggleRotationConnectionButton.disabled = disabled;
|
|
54
|
+
toggleRotationConnectionButton.innerText = innerText;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// CANVAS
|
|
58
|
+
/** @type {HTMLCanvasElement} */
|
|
59
|
+
const displayCanvas = document.getElementById("display");
|
|
60
|
+
|
|
61
|
+
// DISPLAY CANVAS HELPER
|
|
62
|
+
const displayCanvasHelper = new BS.DisplayCanvasHelper();
|
|
63
|
+
// displayCanvasHelper.setBrightness("veryLow");
|
|
64
|
+
displayCanvasHelper.canvas = displayCanvas;
|
|
65
|
+
window.displayCanvasHelper = displayCanvasHelper;
|
|
66
|
+
|
|
67
|
+
device.addEventListener("connected", () => {
|
|
68
|
+
if (device.isDisplayAvailable) {
|
|
69
|
+
displayCanvasHelper.device = device;
|
|
70
|
+
} else {
|
|
71
|
+
console.error("device doesn't have a display");
|
|
72
|
+
device.disconnect();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// BRIGHTNESS
|
|
77
|
+
/** @type {HTMLSelectElement} */
|
|
78
|
+
const setDisplayBrightnessSelect = document.getElementById(
|
|
79
|
+
"setDisplayBrightnessSelect"
|
|
80
|
+
);
|
|
81
|
+
/** @type {HTMLOptGroupElement} */
|
|
82
|
+
const setDisplayBrightnessSelectOptgroup =
|
|
83
|
+
setDisplayBrightnessSelect.querySelector("optgroup");
|
|
84
|
+
BS.DisplayBrightnesses.forEach((displayBrightness) => {
|
|
85
|
+
setDisplayBrightnessSelectOptgroup.appendChild(new Option(displayBrightness));
|
|
86
|
+
});
|
|
87
|
+
setDisplayBrightnessSelect.addEventListener("input", () => {
|
|
88
|
+
displayCanvasHelper.setBrightness(setDisplayBrightnessSelect.value);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
setDisplayBrightnessSelect.value = displayCanvasHelper.brightness;
|
|
92
|
+
|
|
93
|
+
// COLORS
|
|
94
|
+
|
|
95
|
+
/** @type {HTMLTemplateElement} */
|
|
96
|
+
const displayColorTemplate = document.getElementById("displayColorTemplate");
|
|
97
|
+
const displayColorsContainer = document.getElementById("displayColors");
|
|
98
|
+
const setDisplayColor = BS.ThrottleUtils.throttle(
|
|
99
|
+
(colorIndex, colorString) => {
|
|
100
|
+
console.log({ colorIndex, colorString });
|
|
101
|
+
displayCanvasHelper.setColor(colorIndex, colorString, true);
|
|
102
|
+
},
|
|
103
|
+
100,
|
|
104
|
+
true
|
|
105
|
+
);
|
|
106
|
+
/** @type {HTMLInputElement[]} */
|
|
107
|
+
const displayColorInputs = [];
|
|
108
|
+
const setupColors = () => {
|
|
109
|
+
displayColorsContainer.innerHTML = "";
|
|
110
|
+
for (
|
|
111
|
+
let colorIndex = 0;
|
|
112
|
+
colorIndex < displayCanvasHelper.numberOfColors;
|
|
113
|
+
colorIndex++
|
|
114
|
+
) {
|
|
115
|
+
const displayColorContainer = displayColorTemplate.content
|
|
116
|
+
.cloneNode(true)
|
|
117
|
+
.querySelector(".displayColor");
|
|
118
|
+
|
|
119
|
+
const colorIndexSpan = displayColorContainer.querySelector(".colorIndex");
|
|
120
|
+
colorIndexSpan.innerText = `color #${colorIndex}`;
|
|
121
|
+
const colorInput = displayColorContainer.querySelector("input");
|
|
122
|
+
displayColorInputs[colorIndex] = colorInput;
|
|
123
|
+
colorInput.addEventListener("input", () => {
|
|
124
|
+
setDisplayColor(colorIndex, colorInput.value);
|
|
125
|
+
});
|
|
126
|
+
displayColorsContainer.appendChild(displayColorContainer);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
setupColors();
|
|
130
|
+
displayCanvasHelper.addEventListener("numberOfColors", () => setupColors());
|
|
131
|
+
displayCanvasHelper.addEventListener("color", (event) => {
|
|
132
|
+
const { colorHex, colorIndex } = event.message;
|
|
133
|
+
displayColorInputs[colorIndex].value = colorHex;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// CAMERA
|
|
137
|
+
const cameraEntity = document.getElementById("camera");
|
|
138
|
+
const cameraRigEntity = document.getElementById("cameraRig");
|
|
139
|
+
|
|
140
|
+
// MODEL
|
|
141
|
+
const modelEntity = document.getElementById("model");
|
|
142
|
+
const testEntity = document.getElementById("test");
|
|
143
|
+
let isTall = true;
|
|
144
|
+
const normalizeEntity = (entity) => {
|
|
145
|
+
const object3D = entity.getObject3D("mesh");
|
|
146
|
+
|
|
147
|
+
if (!object3D) return;
|
|
148
|
+
|
|
149
|
+
const box = new THREE.Box3().setFromObject(object3D);
|
|
150
|
+
const size = new THREE.Vector3();
|
|
151
|
+
box.getSize(size);
|
|
152
|
+
// isTall = size.y >= size.x;
|
|
153
|
+
console.log({ isTall });
|
|
154
|
+
const center = new THREE.Vector3();
|
|
155
|
+
box.getCenter(center);
|
|
156
|
+
|
|
157
|
+
object3D.position.sub(center);
|
|
158
|
+
|
|
159
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
160
|
+
const fitHeightDistance =
|
|
161
|
+
1 / (2 * Math.tan(THREE.MathUtils.degToRad(50) / 2));
|
|
162
|
+
const fitScreenScale = fitHeightDistance / maxDim;
|
|
163
|
+
|
|
164
|
+
entity.object3D.scale.setScalar(fitScreenScale * 2.0);
|
|
165
|
+
};
|
|
166
|
+
let numberOfSteps = 10;
|
|
167
|
+
let waitTime = 5;
|
|
168
|
+
modelEntity.addEventListener("model-loaded", async () => {
|
|
169
|
+
const modelEntity = document.querySelector("#model");
|
|
170
|
+
modelEntity.object3D.scale.set(1, 1, 1);
|
|
171
|
+
modelEntity.object3D.rotation.set(0, 0, 0);
|
|
172
|
+
await waitForFrame();
|
|
173
|
+
|
|
174
|
+
normalizeEntity(modelEntity);
|
|
175
|
+
|
|
176
|
+
if (!jitRender) {
|
|
177
|
+
await generateSpriteSheet(numberOfSteps, waitTime);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
testEntity.addEventListener("model-loaded", async () => {
|
|
181
|
+
const testEntity = document.querySelector("#model");
|
|
182
|
+
testEntity.object3D.scale.set(1, 1, 1);
|
|
183
|
+
testEntity.object3D.rotation.set(0, 0, 0);
|
|
184
|
+
await waitForFrame();
|
|
185
|
+
|
|
186
|
+
normalizeEntity(testEntity);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const sceneEl = document.querySelector("a-scene");
|
|
190
|
+
const croppedCanvas = document.createElement("canvas");
|
|
191
|
+
const croppedCtx = croppedCanvas.getContext("2d");
|
|
192
|
+
const tempCanvas = document.createElement("canvas");
|
|
193
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
194
|
+
async function captureModelSnapshot(alphaThreshold = 10, reposition = true) {
|
|
195
|
+
const position = cameraRigEntity.object3D.position.clone();
|
|
196
|
+
if (reposition) {
|
|
197
|
+
cameraRigEntity.object3D.position.set(0, 0, 3);
|
|
198
|
+
cameraRigEntity.setAttribute("look-controls", { enabled: false });
|
|
199
|
+
await waitForFrame();
|
|
200
|
+
cameraRigEntity.object3D.rotation.set(0, 0, 0);
|
|
201
|
+
await waitForFrame();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const renderer = sceneEl.renderer;
|
|
205
|
+
const camera = sceneEl.camera;
|
|
206
|
+
const object3D = modelEntity.getObject3D("mesh");
|
|
207
|
+
if (!object3D) return null;
|
|
208
|
+
|
|
209
|
+
// --- Step 1: project model’s bounding box into screen space ---
|
|
210
|
+
const box = new THREE.Box3().setFromObject(object3D);
|
|
211
|
+
const vertices = [
|
|
212
|
+
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
|
|
213
|
+
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
|
|
214
|
+
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
|
|
215
|
+
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
|
|
216
|
+
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
|
|
217
|
+
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
|
|
218
|
+
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
|
|
219
|
+
new THREE.Vector3(box.max.x, box.max.y, box.max.z),
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const min = new THREE.Vector2(Infinity, Infinity);
|
|
223
|
+
const max = new THREE.Vector2(-Infinity, -Infinity);
|
|
224
|
+
|
|
225
|
+
vertices.forEach((v) => {
|
|
226
|
+
v.project(camera);
|
|
227
|
+
const x = (v.x * 0.5 + 0.5) * renderer.domElement.width;
|
|
228
|
+
const y = (1 - (v.y * 0.5 + 0.5)) * renderer.domElement.height;
|
|
229
|
+
min.x = Math.min(min.x, x);
|
|
230
|
+
min.y = Math.min(min.y, y);
|
|
231
|
+
max.x = Math.max(max.x, x);
|
|
232
|
+
max.y = Math.max(max.y, y);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const width = Math.max(1, Math.floor(max.x - min.x));
|
|
236
|
+
const height = Math.max(1, Math.floor(max.y - min.y));
|
|
237
|
+
|
|
238
|
+
// --- Step 2: force renderer transparent ---
|
|
239
|
+
const prevClearColor = new THREE.Color();
|
|
240
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
241
|
+
renderer.getClearColor(prevClearColor);
|
|
242
|
+
|
|
243
|
+
renderer.setClearColor(0x000000, 0);
|
|
244
|
+
|
|
245
|
+
// Hide any <a-sky> entities
|
|
246
|
+
const skies = sceneEl.querySelectorAll("a-sky");
|
|
247
|
+
skies.forEach((sky) => (sky.object3D.visible = false));
|
|
248
|
+
|
|
249
|
+
// --- Step 3: render scene ---
|
|
250
|
+
renderer.render(sceneEl.object3D, camera);
|
|
251
|
+
|
|
252
|
+
// --- Step 4: copy to temporary canvas ---
|
|
253
|
+
tempCanvas.width = renderer.domElement.width;
|
|
254
|
+
tempCanvas.height = renderer.domElement.height;
|
|
255
|
+
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
|
256
|
+
tempCtx.drawImage(renderer.domElement, 0, 0);
|
|
257
|
+
|
|
258
|
+
// --- Step 5: crop initial bounding box ---
|
|
259
|
+
croppedCanvas.width = width;
|
|
260
|
+
croppedCanvas.height = height;
|
|
261
|
+
croppedCtx.clearRect(0, 0, width, height);
|
|
262
|
+
croppedCtx.drawImage(
|
|
263
|
+
tempCanvas,
|
|
264
|
+
min.x,
|
|
265
|
+
min.y,
|
|
266
|
+
width,
|
|
267
|
+
height,
|
|
268
|
+
0,
|
|
269
|
+
0,
|
|
270
|
+
width,
|
|
271
|
+
height
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// --- Step 6: pixel-level trim for zero-padding ---
|
|
275
|
+
const imageData = croppedCtx.getImageData(0, 0, width, height);
|
|
276
|
+
const data = imageData.data;
|
|
277
|
+
|
|
278
|
+
let top = height,
|
|
279
|
+
bottom = 0,
|
|
280
|
+
left = width,
|
|
281
|
+
right = 0;
|
|
282
|
+
for (let y = 0; y < height; y++) {
|
|
283
|
+
for (let x = 0; x < width; x++) {
|
|
284
|
+
const idx = (y * width + x) * 4;
|
|
285
|
+
const alpha = data[idx + 3];
|
|
286
|
+
if (alpha > alphaThreshold) {
|
|
287
|
+
if (x < left) left = x;
|
|
288
|
+
if (x > right) right = x;
|
|
289
|
+
if (y < top) top = y;
|
|
290
|
+
if (y > bottom) bottom = y;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Handle case where model is invisible
|
|
296
|
+
if (right < left || bottom < top) {
|
|
297
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
298
|
+
skies.forEach((sky) => (sky.object3D.visible = true));
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const finalW = right - left + 1;
|
|
303
|
+
const finalH = bottom - top + 1;
|
|
304
|
+
const finalCanvas = document.createElement("canvas");
|
|
305
|
+
const finalCtx = finalCanvas.getContext("2d");
|
|
306
|
+
finalCanvas.width = finalW;
|
|
307
|
+
finalCanvas.height = finalH;
|
|
308
|
+
finalCtx.clearRect(0, 0, finalW, finalH);
|
|
309
|
+
|
|
310
|
+
const finalImageData = finalCtx.createImageData(finalW, finalH);
|
|
311
|
+
for (let y = 0; y < finalH; y++) {
|
|
312
|
+
const srcStart = ((y + top) * width + left) * 4;
|
|
313
|
+
const destStart = y * finalW * 4;
|
|
314
|
+
finalImageData.data.set(
|
|
315
|
+
data.subarray(srcStart, srcStart + finalW * 4),
|
|
316
|
+
destStart
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
finalCtx.putImageData(finalImageData, 0, 0);
|
|
320
|
+
|
|
321
|
+
// --- Step 7: restore renderer and skies ---
|
|
322
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
323
|
+
skies.forEach((sky) => (sky.object3D.visible = true));
|
|
324
|
+
|
|
325
|
+
if (reposition) {
|
|
326
|
+
cameraRigEntity.setAttribute("look-controls", { enabled: true });
|
|
327
|
+
cameraRigEntity.object3D.position.copy(position);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return finalCanvas; // tight transparent PNG
|
|
331
|
+
}
|
|
332
|
+
window.captureModelSnapshot = captureModelSnapshot;
|
|
333
|
+
|
|
334
|
+
async function captureEntityRotations(
|
|
335
|
+
numberOfSteps = 10,
|
|
336
|
+
waitTime = 100,
|
|
337
|
+
skipPicture = false
|
|
338
|
+
) {
|
|
339
|
+
const captures = [];
|
|
340
|
+
const step = 360 / numberOfSteps;
|
|
341
|
+
const minPitch = -80; // stop short of bottom pole
|
|
342
|
+
const maxPitch = 80; // stop short of top pole
|
|
343
|
+
|
|
344
|
+
//Single top pole (+90°)
|
|
345
|
+
if (isTall) {
|
|
346
|
+
modelEntity.object3D.rotation.set(THREE.MathUtils.degToRad(90), 0, 0);
|
|
347
|
+
} else {
|
|
348
|
+
modelEntity.object3D.rotation.set(Math.PI, 0, 0);
|
|
349
|
+
}
|
|
350
|
+
if (!skipPicture) {
|
|
351
|
+
await waitForFrame();
|
|
352
|
+
await BS.wait(waitTime);
|
|
353
|
+
const canvas = await captureModelSnapshot();
|
|
354
|
+
captures.push({
|
|
355
|
+
euler: modelEntity.object3D.rotation.clone(),
|
|
356
|
+
quaternion: modelEntity.object3D.quaternion.clone(),
|
|
357
|
+
canvas,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Loop over pitch, skipping true poles
|
|
362
|
+
for (let pitch = maxPitch; pitch >= minPitch; pitch -= step) {
|
|
363
|
+
for (let i = 0; i < numberOfSteps; i++) {
|
|
364
|
+
const yaw = i * step;
|
|
365
|
+
|
|
366
|
+
// Rotate entity
|
|
367
|
+
if (isTall) {
|
|
368
|
+
modelEntity.object3D.rotation.set(
|
|
369
|
+
THREE.MathUtils.degToRad(pitch), // X = pitch
|
|
370
|
+
THREE.MathUtils.degToRad(yaw), // Y = yaw
|
|
371
|
+
THREE.MathUtils.degToRad(0), // Z = roll (upright),
|
|
372
|
+
"ZXY"
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
modelEntity.object3D.rotation.set(
|
|
376
|
+
THREE.MathUtils.degToRad(pitch + 90), // X = pitch
|
|
377
|
+
THREE.MathUtils.degToRad(0), // Y = yaw
|
|
378
|
+
THREE.MathUtils.degToRad(yaw), // Z = roll (upright),
|
|
379
|
+
"XYZ"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Wait for next frame so rotation is applied
|
|
384
|
+
await waitForFrame();
|
|
385
|
+
await BS.wait(waitTime);
|
|
386
|
+
|
|
387
|
+
if (!skipPicture) {
|
|
388
|
+
// Take picture
|
|
389
|
+
const canvas = await captureModelSnapshot();
|
|
390
|
+
captures.push({
|
|
391
|
+
euler: modelEntity.object3D.rotation.clone(),
|
|
392
|
+
quaternion: modelEntity.object3D.quaternion.clone(),
|
|
393
|
+
canvas,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Single bottom pole (-90°)
|
|
400
|
+
if (isTall) {
|
|
401
|
+
modelEntity.object3D.rotation.set(THREE.MathUtils.degToRad(-90), 0, 0);
|
|
402
|
+
} else {
|
|
403
|
+
modelEntity.object3D.rotation.set(0, 0, 0);
|
|
404
|
+
}
|
|
405
|
+
if (!skipPicture) {
|
|
406
|
+
await waitForFrame();
|
|
407
|
+
await BS.wait(waitTime);
|
|
408
|
+
const canvas = await captureModelSnapshot();
|
|
409
|
+
captures.push({
|
|
410
|
+
euler: modelEntity.object3D.rotation.clone(),
|
|
411
|
+
quaternion: modelEntity.object3D.quaternion.clone(),
|
|
412
|
+
canvas,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
modelEntity.object3D.rotation.set(0, 0, 0);
|
|
417
|
+
|
|
418
|
+
return captures;
|
|
419
|
+
}
|
|
420
|
+
window.captureEntityRotations = captureEntityRotations;
|
|
421
|
+
|
|
422
|
+
let createSinglePalette = false;
|
|
423
|
+
let skipVoidColor = false;
|
|
424
|
+
|
|
425
|
+
/** @type {import("../utils/three/three.module.min")} */
|
|
426
|
+
const THREE = window.THREE;
|
|
427
|
+
|
|
428
|
+
/** @type {{sprite: BS.DisplaySprite, euler: TEuler, quaternion: TQuaternion, image: HTMLImageElement}[]} */
|
|
429
|
+
const spriteCaptures = [];
|
|
430
|
+
window.spriteCaptures = spriteCaptures;
|
|
431
|
+
const generateSpriteSheet = async (numberOfSteps, waitTime) => {
|
|
432
|
+
generateSpriteSheetButton.disabled = true;
|
|
433
|
+
const captures = await captureEntityRotations(numberOfSteps, waitTime);
|
|
434
|
+
|
|
435
|
+
let maxHeight = -Infinity;
|
|
436
|
+
let maxWidth = -Infinity;
|
|
437
|
+
captures.forEach(({ canvas }) => {
|
|
438
|
+
maxHeight = Math.max(maxHeight, canvas.height);
|
|
439
|
+
maxWidth = Math.max(maxWidth, canvas.width);
|
|
440
|
+
});
|
|
441
|
+
const scalar = drawInputHeight / maxHeight;
|
|
442
|
+
const previewScalar = maxSpritePreviewHeight / maxHeight;
|
|
443
|
+
|
|
444
|
+
spriteCaptures.forEach(({ image }) => image.remove());
|
|
445
|
+
spriteCaptures.length = 0;
|
|
446
|
+
|
|
447
|
+
spriteSheet = { name: "model", sprites: [] };
|
|
448
|
+
if (createSinglePalette) {
|
|
449
|
+
// FILL - create palette from default front angle
|
|
450
|
+
}
|
|
451
|
+
for (let i in captures) {
|
|
452
|
+
const { canvas, euler, quaternion } = captures[i];
|
|
453
|
+
|
|
454
|
+
const height = canvas.height * scalar;
|
|
455
|
+
const width = canvas.width * scalar;
|
|
456
|
+
|
|
457
|
+
const resizedCanvas = BS.resizeImage(canvas, width, height);
|
|
458
|
+
|
|
459
|
+
const { sprite, blob } = await BS.canvasToSprite(
|
|
460
|
+
resizedCanvas,
|
|
461
|
+
i,
|
|
462
|
+
skipVoidColor
|
|
463
|
+
? Math.min(
|
|
464
|
+
BS.pixelDepthToNumberOfColors(pixelDepth),
|
|
465
|
+
displayCanvasHelper.numberOfColors - 1
|
|
466
|
+
)
|
|
467
|
+
: BS.pixelDepthToNumberOfColors(pixelDepth),
|
|
468
|
+
createSinglePalette ? "palette" : i,
|
|
469
|
+
!createSinglePalette,
|
|
470
|
+
spriteSheet,
|
|
471
|
+
skipVoidColor ? 1 : 0
|
|
472
|
+
);
|
|
473
|
+
//console.log(sprite, blob);
|
|
474
|
+
|
|
475
|
+
const previewHeight = canvas.height * previewScalar;
|
|
476
|
+
const previewWidth = canvas.width * previewScalar;
|
|
477
|
+
|
|
478
|
+
const image = new Image();
|
|
479
|
+
image.width = previewWidth;
|
|
480
|
+
image.height = previewHeight;
|
|
481
|
+
image.src = URL.createObjectURL(blob);
|
|
482
|
+
document.body.appendChild(image);
|
|
483
|
+
spriteCaptures.push({
|
|
484
|
+
sprite,
|
|
485
|
+
euler,
|
|
486
|
+
quaternion,
|
|
487
|
+
image,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
checkSpriteSheetSize();
|
|
492
|
+
|
|
493
|
+
generateSpriteSheetButton.disabled = false;
|
|
494
|
+
if (uploadWholeSpriteSheet) {
|
|
495
|
+
await displayCanvasHelper.uploadSpriteSheet(spriteSheet);
|
|
496
|
+
await displayCanvasHelper.selectSpriteSheet(spriteSheet.name);
|
|
497
|
+
}
|
|
498
|
+
await draw();
|
|
499
|
+
};
|
|
500
|
+
let maxSpritePreviewHeight = 50;
|
|
501
|
+
window.generateSpriteSheet = generateSpriteSheet;
|
|
502
|
+
|
|
503
|
+
const generateSpriteSheetButton = document.getElementById(
|
|
504
|
+
"generateSpriteSheet"
|
|
505
|
+
);
|
|
506
|
+
generateSpriteSheetButton.addEventListener("click", () => {
|
|
507
|
+
generateSpriteSheet(numberOfSteps, waitTime);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/** @type {HTMLInputElement} */
|
|
511
|
+
const modelInput = document.getElementById("modelInput");
|
|
512
|
+
modelInput.addEventListener("input", () => {
|
|
513
|
+
const file = modelInput.files[0];
|
|
514
|
+
if (!file) return;
|
|
515
|
+
const url = URL.createObjectURL(file);
|
|
516
|
+
loadModel(url);
|
|
517
|
+
modelInput.value = "";
|
|
518
|
+
});
|
|
519
|
+
const loadModel = (url) => {
|
|
520
|
+
console.log("model url", url);
|
|
521
|
+
modelEntity.setAttribute("gltf-model", `url(${url})`);
|
|
522
|
+
testEntity.setAttribute("gltf-model", `url(${url})`);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
window.addEventListener(
|
|
526
|
+
"keydown",
|
|
527
|
+
function (e) {
|
|
528
|
+
if (e.target.nodeName == "INPUT") {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const keysToPrevent = [
|
|
533
|
+
"ArrowUp",
|
|
534
|
+
"ArrowDown",
|
|
535
|
+
"ArrowLeft",
|
|
536
|
+
"ArrowRight",
|
|
537
|
+
" ",
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
if (keysToPrevent.includes(e.key)) {
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
{ passive: false }
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const waitForFrame = async () => await new Promise(requestAnimationFrame);
|
|
548
|
+
const lookAtEntity = document.getElementById("lookAt");
|
|
549
|
+
async function getRelativeModelQuaternion() {
|
|
550
|
+
lookAtEntity.object3D.position.copy(modelEntity.object3D.position);
|
|
551
|
+
lookAtEntity.object3D.lookAt(cameraRigEntity.object3D.position);
|
|
552
|
+
await waitForFrame();
|
|
553
|
+
|
|
554
|
+
const lookAtEntityQuaternion = lookAtEntity.object3D.quaternion
|
|
555
|
+
.clone()
|
|
556
|
+
.invert();
|
|
557
|
+
const modelEntityQuaternion = new THREE.Quaternion();
|
|
558
|
+
modelEntity.object3D.getWorldQuaternion(modelEntityQuaternion);
|
|
559
|
+
|
|
560
|
+
lookAtEntityQuaternion.multiply(modelEntityQuaternion);
|
|
561
|
+
testEntity.object3D.quaternion.copy(lookAtEntityQuaternion);
|
|
562
|
+
return lookAtEntityQuaternion;
|
|
563
|
+
}
|
|
564
|
+
window.getRelativeModelQuaternion = getRelativeModelQuaternion;
|
|
565
|
+
|
|
566
|
+
async function getModelScreenBoundingBox(alphaThreshold = 10) {
|
|
567
|
+
sceneEl.object3D.updateMatrixWorld(true);
|
|
568
|
+
|
|
569
|
+
const renderer = sceneEl.renderer;
|
|
570
|
+
const canvas = renderer.domElement;
|
|
571
|
+
const camera = sceneEl.camera;
|
|
572
|
+
|
|
573
|
+
// drawing buffer (physical pixels)
|
|
574
|
+
const dbW = canvas.width,
|
|
575
|
+
dbH = canvas.height;
|
|
576
|
+
|
|
577
|
+
// CSS pixel size (use this for UI overlays)
|
|
578
|
+
const size = renderer.getSize(new THREE.Vector2());
|
|
579
|
+
const cssW = size.x,
|
|
580
|
+
cssH = size.y;
|
|
581
|
+
|
|
582
|
+
const object3D = modelEntity.getObject3D("mesh");
|
|
583
|
+
if (!object3D) return null;
|
|
584
|
+
|
|
585
|
+
// --- Project 8 corners of world-space AABB ---
|
|
586
|
+
const box = new THREE.Box3().setFromObject(object3D);
|
|
587
|
+
if (!isFinite(box.min.x)) return null;
|
|
588
|
+
|
|
589
|
+
const corners = [
|
|
590
|
+
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
|
|
591
|
+
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
|
|
592
|
+
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
|
|
593
|
+
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
|
|
594
|
+
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
|
|
595
|
+
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
|
|
596
|
+
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
|
|
597
|
+
new THREE.Vector3(box.max.x, box.max.y, box.max.z),
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
let minX = Infinity,
|
|
601
|
+
minY = Infinity;
|
|
602
|
+
let maxX = -Infinity,
|
|
603
|
+
maxY = -Infinity;
|
|
604
|
+
|
|
605
|
+
for (const v of corners) {
|
|
606
|
+
v.project(camera); // NDC
|
|
607
|
+
const x = (v.x * 0.5 + 0.5) * dbW; // -> drawing buffer pixels
|
|
608
|
+
const y = (1 - (v.y * 0.5 + 0.5)) * dbH;
|
|
609
|
+
minX = Math.min(minX, x);
|
|
610
|
+
minY = Math.min(minY, y);
|
|
611
|
+
maxX = Math.max(maxX, x);
|
|
612
|
+
maxY = Math.max(maxY, y);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Unclamped AABB (may be <0 or >dbW/dbH)
|
|
616
|
+
const unclamped = { minX, minY, maxX, maxY };
|
|
617
|
+
|
|
618
|
+
// Clamp for safe readPixels (ROI)
|
|
619
|
+
const sx0 = Math.max(0, Math.floor(minX));
|
|
620
|
+
const sy0 = Math.max(0, Math.floor(minY));
|
|
621
|
+
const sx1 = Math.min(dbW, Math.ceil(maxX));
|
|
622
|
+
const sy1 = Math.min(dbH, Math.ceil(maxY));
|
|
623
|
+
const roiW = Math.max(0, sx1 - sx0);
|
|
624
|
+
const roiH = Math.max(0, sy1 - sy0);
|
|
625
|
+
if (roiW === 0 || roiH === 0) return null;
|
|
626
|
+
|
|
627
|
+
// Transparent render pass
|
|
628
|
+
const prevClearColor = new THREE.Color();
|
|
629
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
630
|
+
renderer.getClearColor(prevClearColor);
|
|
631
|
+
renderer.setClearColor(0x000000, 0);
|
|
632
|
+
|
|
633
|
+
const skies = sceneEl.querySelectorAll("a-sky");
|
|
634
|
+
const prevSkyVis = [];
|
|
635
|
+
skies.forEach(
|
|
636
|
+
(sky, i) => (
|
|
637
|
+
(prevSkyVis[i] = sky.object3D.visible), (sky.object3D.visible = false)
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
if (typeof waitForFrame === "function") await waitForFrame();
|
|
642
|
+
renderer.render(sceneEl.object3D, camera);
|
|
643
|
+
|
|
644
|
+
// Read pixels in ROI (WebGL is bottom-left origin)
|
|
645
|
+
const gl = renderer.getContext();
|
|
646
|
+
const pixels = new Uint8Array(roiW * roiH * 4);
|
|
647
|
+
gl.readPixels(sx0, dbH - sy1, roiW, roiH, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
648
|
+
|
|
649
|
+
// Alpha trim within ROI
|
|
650
|
+
let left = roiW,
|
|
651
|
+
right = -1,
|
|
652
|
+
bottom = 0,
|
|
653
|
+
topBL = roiH;
|
|
654
|
+
for (let rowBL = 0; rowBL < roiH; rowBL++) {
|
|
655
|
+
const rowOffset = rowBL * roiW * 4;
|
|
656
|
+
for (let col = 0; col < roiW; col++) {
|
|
657
|
+
const a = pixels[rowOffset + col * 4 + 3];
|
|
658
|
+
if (a > alphaThreshold) {
|
|
659
|
+
if (col < left) left = col;
|
|
660
|
+
if (col > right) right = col;
|
|
661
|
+
if (rowBL < topBL) topBL = rowBL;
|
|
662
|
+
if (rowBL > bottom) bottom = rowBL;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Restore renderer + skies ASAP
|
|
668
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
669
|
+
skies.forEach((sky, i) => (sky.object3D.visible = prevSkyVis[i]));
|
|
670
|
+
|
|
671
|
+
if (right < left || bottom < topBL) return null;
|
|
672
|
+
|
|
673
|
+
// Convert ROI (bottom-left origin) to screen (top-left origin)
|
|
674
|
+
const topRowTopOrigin = roiH - 1 - bottom;
|
|
675
|
+
const bottomRowTopOrigin = roiH - 1 - topBL;
|
|
676
|
+
|
|
677
|
+
const tightMinX = sx0 + left;
|
|
678
|
+
const tightMaxX = sx0 + right;
|
|
679
|
+
const tightMinY = sy0 + topRowTopOrigin;
|
|
680
|
+
const tightMaxY = sy0 + bottomRowTopOrigin;
|
|
681
|
+
|
|
682
|
+
// Helper: normalize a box
|
|
683
|
+
const toNorm = (box, denomW, denomH) => ({
|
|
684
|
+
minX: box.minX / denomW,
|
|
685
|
+
minY: box.minY / denomH,
|
|
686
|
+
maxX: box.maxX / denomW,
|
|
687
|
+
maxY: box.maxY / denomH,
|
|
688
|
+
// no +1: use continuous extents for normalized sizes
|
|
689
|
+
width: (box.maxX - box.minX) / denomW,
|
|
690
|
+
height: (box.maxY - box.minY) / denomH,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Build outputs in both spaces (choose 'css' for HTML overlays)
|
|
694
|
+
const tightBox = {
|
|
695
|
+
minX: tightMinX,
|
|
696
|
+
minY: tightMinY,
|
|
697
|
+
maxX: tightMaxX,
|
|
698
|
+
maxY: tightMaxY,
|
|
699
|
+
};
|
|
700
|
+
const aabbBox = unclamped;
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
// Normalized to CSS size (recommended for DOM overlays)
|
|
704
|
+
css: {
|
|
705
|
+
tight: toNorm(tightBox, cssW, cssH),
|
|
706
|
+
aabb: toNorm(aabbBox, cssW, cssH), // can go <0 or >1 if off-screen
|
|
707
|
+
},
|
|
708
|
+
// Normalized to drawing buffer (rarely what you want for UI)
|
|
709
|
+
buffer: {
|
|
710
|
+
tight: toNorm(tightBox, dbW, dbH),
|
|
711
|
+
aabb: toNorm(aabbBox, dbW, dbH),
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
window.getModelScreenBoundingBox = getModelScreenBoundingBox;
|
|
717
|
+
|
|
718
|
+
async function getClosestModelSprite() {
|
|
719
|
+
const _quaternion = await getRelativeModelQuaternion();
|
|
720
|
+
const _euler = new THREE.Euler().setFromQuaternion(_quaternion);
|
|
721
|
+
let rotation;
|
|
722
|
+
if (isTall) {
|
|
723
|
+
_euler.reorder("ZXY");
|
|
724
|
+
rotation = -_euler.z;
|
|
725
|
+
_euler.z = 0;
|
|
726
|
+
} else {
|
|
727
|
+
_euler.reorder("XYZ");
|
|
728
|
+
rotation = -_euler.y;
|
|
729
|
+
_euler.y = 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
_quaternion.setFromEuler(_euler);
|
|
733
|
+
// console.log("euler", _euler);
|
|
734
|
+
|
|
735
|
+
let closestIndex = -1;
|
|
736
|
+
let closestAngle = Infinity;
|
|
737
|
+
spriteCaptures.forEach(({ quaternion, euler }, index) => {
|
|
738
|
+
//euler.z = _euler.z;
|
|
739
|
+
const angle = Math.abs(quaternion.angleTo(_quaternion));
|
|
740
|
+
if (angle < closestAngle) {
|
|
741
|
+
closestAngle = angle;
|
|
742
|
+
closestIndex = index;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
// console.log({ closestAngle, closestIndex });
|
|
746
|
+
|
|
747
|
+
const { sprite, image } = spriteCaptures[closestIndex];
|
|
748
|
+
|
|
749
|
+
if (selectedImage != image) {
|
|
750
|
+
if (selectedImage) {
|
|
751
|
+
selectedImage.classList.remove("selected");
|
|
752
|
+
}
|
|
753
|
+
selectedImage = image;
|
|
754
|
+
selectedImage.classList.add("selected");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
sprite,
|
|
759
|
+
rotation,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
let selectedImage;
|
|
763
|
+
window.getClosestModelSprite = getClosestModelSprite;
|
|
764
|
+
|
|
765
|
+
// DRAGOVER
|
|
766
|
+
window.addEventListener("dragover", (e) => {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
window.addEventListener("drop", (e) => {
|
|
771
|
+
e.preventDefault();
|
|
772
|
+
const file = e.dataTransfer.files[0];
|
|
773
|
+
console.log(file);
|
|
774
|
+
if (isModelFilename(file?.name)) {
|
|
775
|
+
const url = URL.createObjectURL(file);
|
|
776
|
+
loadModel(url);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// REDRAW ON CHANGE
|
|
782
|
+
const redrawOnChangeInput = document.getElementById("redrawOnChange");
|
|
783
|
+
redrawOnChangeInput.addEventListener("input", () => {
|
|
784
|
+
setRedrawOnChange(redrawOnChangeInput.checked);
|
|
785
|
+
});
|
|
786
|
+
let redrawOnChange = redrawOnChangeInput.checked;
|
|
787
|
+
const setRedrawOnChange = (newRedrawOnChange) => {
|
|
788
|
+
redrawOnChange = newRedrawOnChange;
|
|
789
|
+
console.log({ redrawOnChange });
|
|
790
|
+
redrawOnChangeInput.checked = redrawOnChange;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// AUTODRAW
|
|
794
|
+
const autoDrawInput = document.getElementById("autoDraw");
|
|
795
|
+
autoDrawInput.addEventListener("input", () => {
|
|
796
|
+
setAutoDraw(autoDrawInput.checked);
|
|
797
|
+
});
|
|
798
|
+
let autoDraw = autoDrawInput.checked;
|
|
799
|
+
const setAutoDraw = (newAutoDraw) => {
|
|
800
|
+
autoDraw = newAutoDraw;
|
|
801
|
+
console.log({ autoDraw });
|
|
802
|
+
autoDrawInput.checked = autoDraw;
|
|
803
|
+
if (autoDraw) {
|
|
804
|
+
draw();
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// PASTE
|
|
809
|
+
function isValidUrl(string) {
|
|
810
|
+
try {
|
|
811
|
+
new URL(string);
|
|
812
|
+
return true;
|
|
813
|
+
} catch (_) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
window.addEventListener("paste", (event) => {
|
|
818
|
+
const string = event.clipboardData.getData("text");
|
|
819
|
+
if (!isValidUrl(string)) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
// eg https://modelviewer.dev/shared-assets/models/NeilArmstrong.glb
|
|
823
|
+
if (isModelFilename(string)) {
|
|
824
|
+
console.log("pasted model url");
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
const modelFileExtensions = [".glb", ".gltf"];
|
|
828
|
+
const isModelFilename = (string) =>
|
|
829
|
+
modelFileExtensions.some((extension) => string.endsWith(extension));
|
|
830
|
+
window.addEventListener("paste", (event) => {
|
|
831
|
+
const items = event.clipboardData.items;
|
|
832
|
+
for (let i = 0; i < items.length; i++) {
|
|
833
|
+
const item = items[i];
|
|
834
|
+
if (item.kind == "file") {
|
|
835
|
+
const file = item.getAsFile();
|
|
836
|
+
if (isModelFilename(file.name)) {
|
|
837
|
+
const url = URL.createObjectURL(file);
|
|
838
|
+
loadModel(url);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// DRAW PARAMS
|
|
846
|
+
|
|
847
|
+
const drawXContainer = document.getElementById("drawX");
|
|
848
|
+
const drawXInput = drawXContainer.querySelector("input");
|
|
849
|
+
const drawXSpan = drawXContainer.querySelector("span.value");
|
|
850
|
+
let drawX = Number(drawXInput.value);
|
|
851
|
+
|
|
852
|
+
drawXInput.addEventListener("input", () => {
|
|
853
|
+
setDrawX(Number(drawXInput.value));
|
|
854
|
+
if (redrawOnChange) {
|
|
855
|
+
draw();
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
const setDrawX = (newDrawX) => {
|
|
859
|
+
drawX = newDrawX;
|
|
860
|
+
drawXInput.value = drawX;
|
|
861
|
+
// console.log({ drawX });
|
|
862
|
+
drawXSpan.innerText = Math.round(drawX);
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const drawYContainer = document.getElementById("drawY");
|
|
866
|
+
const drawYInput = drawYContainer.querySelector("input");
|
|
867
|
+
const drawYSpan = drawYContainer.querySelector("span.value");
|
|
868
|
+
let drawY = Number(drawYInput.value);
|
|
869
|
+
|
|
870
|
+
drawYInput.addEventListener("input", () => {
|
|
871
|
+
setDrawY(Number(drawYInput.value));
|
|
872
|
+
if (redrawOnChange) {
|
|
873
|
+
draw();
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
const setDrawY = (newDrawY) => {
|
|
877
|
+
drawY = newDrawY;
|
|
878
|
+
drawYInput.value = drawY;
|
|
879
|
+
// console.log({ drawY });
|
|
880
|
+
drawYSpan.innerText = Math.round(drawY);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const drawRotationContainer = document.getElementById("drawRotation");
|
|
884
|
+
const drawRotationInput = drawRotationContainer.querySelector("input");
|
|
885
|
+
const drawRotationSpan = drawRotationContainer.querySelector("span.value");
|
|
886
|
+
let drawRotation = Number(drawRotationInput.value);
|
|
887
|
+
|
|
888
|
+
drawRotationInput.addEventListener("input", () => {
|
|
889
|
+
// console.log({ drawRotation });
|
|
890
|
+
setDrawRotation(Number(drawRotationInput.value));
|
|
891
|
+
if (redrawOnChange) {
|
|
892
|
+
draw();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
const setDrawRotation = (newDrawRotation) => {
|
|
896
|
+
drawRotation = newDrawRotation;
|
|
897
|
+
drawRotationSpan.innerText = Math.round(newDrawRotation);
|
|
898
|
+
drawRotationInput.value = drawRotation;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const drawInputHeightContainer = document.getElementById("drawInputHeight");
|
|
902
|
+
const drawInputHeightInput = drawInputHeightContainer.querySelector("input");
|
|
903
|
+
const drawInputHeightSpan =
|
|
904
|
+
drawInputHeightContainer.querySelector("span.value");
|
|
905
|
+
let drawInputHeight = Number(drawInputHeightInput.value);
|
|
906
|
+
|
|
907
|
+
drawInputHeightInput.addEventListener("input", () => {
|
|
908
|
+
drawInputHeight = Number(drawInputHeightInput.value);
|
|
909
|
+
//console.log({ drawInputHeight });
|
|
910
|
+
drawInputHeightSpan.innerText = drawInputHeight;
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const drawOutputHeightContainer = document.getElementById("drawOutputHeight");
|
|
914
|
+
const drawOutputHeightInput = drawOutputHeightContainer.querySelector("input");
|
|
915
|
+
const drawOutputHeightSpan =
|
|
916
|
+
drawOutputHeightContainer.querySelector("span.value");
|
|
917
|
+
let drawOutputHeight = Number(drawOutputHeightInput.value);
|
|
918
|
+
drawOutputHeightInput.addEventListener("input", () => {
|
|
919
|
+
setDrawOutputHeight(Number(drawOutputHeightInput.value));
|
|
920
|
+
if (redrawOnChange) {
|
|
921
|
+
draw();
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
const setDrawOutputHeight = (newDrawOutputHeight) => {
|
|
925
|
+
drawOutputHeight = newDrawOutputHeight;
|
|
926
|
+
// console.log({ drawOutputHeight });
|
|
927
|
+
|
|
928
|
+
drawOutputHeightSpan.innerText = Math.round(drawOutputHeight);
|
|
929
|
+
drawOutputHeightInput.value = drawOutputHeight;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// PIXEL DEPTH
|
|
933
|
+
|
|
934
|
+
let pixelDepth = BS.DisplayPixelDepths[1];
|
|
935
|
+
const setPixelDepth = (newPixelDepth) => {
|
|
936
|
+
pixelDepth = newPixelDepth;
|
|
937
|
+
// console.log({ pixelDepth });
|
|
938
|
+
};
|
|
939
|
+
const pixelDepthSelect = document.getElementById("pixelDepth");
|
|
940
|
+
const pixelDepthOptgroup = pixelDepthSelect.querySelector("optgroup");
|
|
941
|
+
pixelDepthSelect.addEventListener("input", () => {
|
|
942
|
+
setPixelDepth(pixelDepthSelect.value);
|
|
943
|
+
});
|
|
944
|
+
BS.DisplayPixelDepths.forEach((pixelDepth) => {
|
|
945
|
+
pixelDepthOptgroup.appendChild(
|
|
946
|
+
new Option(
|
|
947
|
+
`${BS.pixelDepthToNumberOfColors(pixelDepth)} colors`,
|
|
948
|
+
pixelDepth
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
});
|
|
952
|
+
pixelDepthSelect.value = pixelDepth;
|
|
953
|
+
|
|
954
|
+
// DRAW
|
|
955
|
+
let defaultMaxFileLength = 10 * 1024; // 10kb
|
|
956
|
+
let isDrawing = false;
|
|
957
|
+
/** @type {BS.DisplaySpriteSheet} */
|
|
958
|
+
let spriteSheet;
|
|
959
|
+
let uploadWholeSpriteSheet = true;
|
|
960
|
+
let jitRender = false;
|
|
961
|
+
let drawWhenReady = false;
|
|
962
|
+
|
|
963
|
+
window.bbScalar = 1;
|
|
964
|
+
const draw = async () => {
|
|
965
|
+
if (isDrawing) {
|
|
966
|
+
console.log("busy drawing");
|
|
967
|
+
drawWhenReady = true;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (!spriteSheet && !jitRender) {
|
|
971
|
+
console.error("no sprite sheet");
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
isDrawing = true;
|
|
975
|
+
|
|
976
|
+
const boundingBox = await getModelScreenBoundingBox();
|
|
977
|
+
if (boundingBox) {
|
|
978
|
+
let { minX, maxX, maxY, minY, width, height } = boundingBox.buffer.aabb;
|
|
979
|
+
const widthScalar = displayCanvasHelper.width;
|
|
980
|
+
const heightScalar = displayCanvasHelper.height;
|
|
981
|
+
|
|
982
|
+
height *= heightScalar * bbScalar;
|
|
983
|
+
width *= widthScalar * bbScalar;
|
|
984
|
+
|
|
985
|
+
setDrawX((widthScalar * (minX + maxX)) / 2);
|
|
986
|
+
setDrawY((heightScalar * (minY + maxY)) / 2);
|
|
987
|
+
|
|
988
|
+
// await displayCanvasHelper.setRotation(0);
|
|
989
|
+
// await displayCanvasHelper.drawRect(drawX, drawY, width, height);
|
|
990
|
+
|
|
991
|
+
if (jitRender) {
|
|
992
|
+
setDrawOutputHeight(height);
|
|
993
|
+
let spriteScale = drawOutputHeight / drawInputHeight;
|
|
994
|
+
await displayCanvasHelper.setSpriteScale(spriteScale);
|
|
995
|
+
|
|
996
|
+
const canvas = await captureModelSnapshot(10, false);
|
|
997
|
+
const width = canvas.width * (drawInputHeight / canvas.height);
|
|
998
|
+
|
|
999
|
+
const resizedCanvas = BS.resizeImage(canvas, width, drawInputHeight);
|
|
1000
|
+
const maxFileLength = displayCanvasHelper.device?.isConnected
|
|
1001
|
+
? displayCanvasHelper.device.maxFileLength
|
|
1002
|
+
: defaultMaxFileLength;
|
|
1003
|
+
|
|
1004
|
+
spriteSheet = await BS.canvasToSpriteSheet(
|
|
1005
|
+
resizedCanvas,
|
|
1006
|
+
"model",
|
|
1007
|
+
skipVoidColor
|
|
1008
|
+
? Math.min(
|
|
1009
|
+
BS.pixelDepthToNumberOfColors(pixelDepth),
|
|
1010
|
+
displayCanvasHelper.numberOfColors - 1
|
|
1011
|
+
)
|
|
1012
|
+
: BS.pixelDepthToNumberOfColors(pixelDepth),
|
|
1013
|
+
"palette",
|
|
1014
|
+
maxFileLength
|
|
1015
|
+
);
|
|
1016
|
+
checkSpriteSheetSize();
|
|
1017
|
+
await displayCanvasHelper.drawSpriteFromSpriteSheet(
|
|
1018
|
+
drawX,
|
|
1019
|
+
drawY,
|
|
1020
|
+
spriteSheet.sprites[0].name,
|
|
1021
|
+
spriteSheet,
|
|
1022
|
+
"palette"
|
|
1023
|
+
);
|
|
1024
|
+
} else {
|
|
1025
|
+
const { sprite, rotation } = await getClosestModelSprite();
|
|
1026
|
+
|
|
1027
|
+
setDrawRotation(rotation);
|
|
1028
|
+
await displayCanvasHelper.setRotation(drawRotation, true);
|
|
1029
|
+
|
|
1030
|
+
const innerBox = innerBoxSize(
|
|
1031
|
+
width,
|
|
1032
|
+
height,
|
|
1033
|
+
rotation,
|
|
1034
|
+
sprite.height / sprite.width
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
let spriteScale = innerBox.height / sprite.height;
|
|
1038
|
+
await displayCanvasHelper.setSpriteScale(spriteScale);
|
|
1039
|
+
|
|
1040
|
+
if (uploadWholeSpriteSheet) {
|
|
1041
|
+
console.log("drawing sprite");
|
|
1042
|
+
await displayCanvasHelper.drawSprite(drawX, drawY, sprite.name);
|
|
1043
|
+
if (!createSinglePalette) {
|
|
1044
|
+
await displayCanvasHelper.selectSpriteSheetPalette(sprite.name);
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
console.log("uploadng whole sprite");
|
|
1048
|
+
await displayCanvasHelper.drawSpriteFromSpriteSheet(
|
|
1049
|
+
drawX,
|
|
1050
|
+
drawY,
|
|
1051
|
+
sprite.name,
|
|
1052
|
+
spriteSheet,
|
|
1053
|
+
createSinglePalette ? "model" : sprite.name
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
await displayCanvasHelper.show();
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
function innerBoxSize(W, H, theta, aspectRatio) {
|
|
1062
|
+
const cos = Math.cos(theta);
|
|
1063
|
+
const sin = Math.sin(theta);
|
|
1064
|
+
const absCos = Math.abs(cos);
|
|
1065
|
+
const absSin = Math.abs(sin);
|
|
1066
|
+
|
|
1067
|
+
const w1 = W / (absCos + aspectRatio * absSin);
|
|
1068
|
+
const w2 = H / (absSin + aspectRatio * absCos);
|
|
1069
|
+
|
|
1070
|
+
const w = Math.min(w1, w2);
|
|
1071
|
+
const h = aspectRatio * w;
|
|
1072
|
+
|
|
1073
|
+
return { width: w, height: h };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const saveSpriteSheet = () => {
|
|
1077
|
+
console.log("saveSpriteSheet");
|
|
1078
|
+
const spritesheetString = JSON.stringify(spriteSheet, null, 2);
|
|
1079
|
+
const blob = new Blob([spritesheetString], { type: "application/json" });
|
|
1080
|
+
const url = URL.createObjectURL(blob);
|
|
1081
|
+
|
|
1082
|
+
const a = document.createElement("a");
|
|
1083
|
+
a.href = url;
|
|
1084
|
+
a.download = `spritesheet-${spriteSheet.name}.json`;
|
|
1085
|
+
a.click();
|
|
1086
|
+
|
|
1087
|
+
URL.revokeObjectURL(url);
|
|
1088
|
+
};
|
|
1089
|
+
window.saveSpriteSheet = saveSpriteSheet;
|
|
1090
|
+
|
|
1091
|
+
const redrawButton = document.getElementById("draw");
|
|
1092
|
+
redrawButton.addEventListener("click", () => {
|
|
1093
|
+
draw();
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
displayCanvasHelper.addEventListener("ready", () => {
|
|
1097
|
+
isDrawing = false;
|
|
1098
|
+
if (drawWhenReady || autoDraw) {
|
|
1099
|
+
drawWhenReady = false;
|
|
1100
|
+
draw();
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// PROGRESS
|
|
1105
|
+
|
|
1106
|
+
/** @type {HTMLProgressElement} */
|
|
1107
|
+
const fileTransferProgress = document.getElementById("fileTransferProgress");
|
|
1108
|
+
|
|
1109
|
+
device.addEventListener("fileTransferProgress", (event) => {
|
|
1110
|
+
const progress = event.message.progress;
|
|
1111
|
+
//console.log({ progress });
|
|
1112
|
+
fileTransferProgress.value = progress == 1 ? 0 : progress;
|
|
1113
|
+
});
|
|
1114
|
+
device.addEventListener("fileTransferStatus", () => {
|
|
1115
|
+
if (device.fileTransferStatus == "idle") {
|
|
1116
|
+
fileTransferProgress.value = 0;
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
const checkSpriteSheetSizeButton = document.getElementById(
|
|
1121
|
+
"checkSpriteSheetSize"
|
|
1122
|
+
);
|
|
1123
|
+
const checkSpriteSheetSize = () => {
|
|
1124
|
+
const arrayBuffer = displayCanvasHelper.serializeSpriteSheet(spriteSheet);
|
|
1125
|
+
checkSpriteSheetSizeButton.innerText = `size: ${(
|
|
1126
|
+
arrayBuffer.byteLength / 1024
|
|
1127
|
+
).toFixed(2)}kb`;
|
|
1128
|
+
if (displayCanvasHelper.device?.isConnected) {
|
|
1129
|
+
checkSpriteSheetSizeButton.innerText += ` (max ${(
|
|
1130
|
+
displayCanvasHelper.device.maxFileLength / 1024
|
|
1131
|
+
).toFixed(2)}kb)`;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
checkSpriteSheetSizeButton.addEventListener("click", () => {
|
|
1135
|
+
checkSpriteSheetSize();
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// ROTATOR
|
|
1139
|
+
const toggleRotationInput = document.getElementById("toggleRotation");
|
|
1140
|
+
toggleRotationInput.addEventListener("input", () => {
|
|
1141
|
+
setToggleRotation(toggleRotationInput.checked);
|
|
1142
|
+
});
|
|
1143
|
+
let rotationEnabled = toggleRotationInput.checked;
|
|
1144
|
+
const setToggleRotation = (newRotationEnabled) => {
|
|
1145
|
+
rotationEnabled = newRotationEnabled;
|
|
1146
|
+
console.log({ rotationEnabled });
|
|
1147
|
+
toggleRotationInput.checked = rotationEnabled;
|
|
1148
|
+
if (rotationDevice.isConnected) {
|
|
1149
|
+
updateSensorConfig();
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
rotationDevice.addEventListener("connected", () => {
|
|
1153
|
+
updateSensorConfig();
|
|
1154
|
+
});
|
|
1155
|
+
const updateSensorConfig = () => {
|
|
1156
|
+
if (rotationDevice.isConnected) {
|
|
1157
|
+
let sensorRate = rotationEnabled ? 20 : 0;
|
|
1158
|
+
rotationDevice.setSensorConfiguration({
|
|
1159
|
+
gameRotation: sensorRate,
|
|
1160
|
+
linearAcceleration: sensorRate,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
/** @type {TQuaternion} */
|
|
1166
|
+
const _quaternion = new THREE.Quaternion();
|
|
1167
|
+
/** @type {TQuaternion} */
|
|
1168
|
+
const targetQuaternion = new THREE.Quaternion();
|
|
1169
|
+
/**
|
|
1170
|
+
* @param {BS.Quaternion} quaternion
|
|
1171
|
+
* @param {boolean} applyOffset
|
|
1172
|
+
*/
|
|
1173
|
+
const updateQuaternion = (quaternion, applyOffset = false) => {
|
|
1174
|
+
_quaternion.copy(quaternion);
|
|
1175
|
+
targetQuaternion.copy(_quaternion);
|
|
1176
|
+
if (applyOffset) {
|
|
1177
|
+
targetQuaternion.premultiply(offsetQuaternion);
|
|
1178
|
+
}
|
|
1179
|
+
targetRotationEntity.object3D.quaternion.slerp(
|
|
1180
|
+
targetQuaternion,
|
|
1181
|
+
window.interpolationSmoothing
|
|
1182
|
+
);
|
|
1183
|
+
};
|
|
1184
|
+
rotationDevice.addEventListener("gameRotation", (event) => {
|
|
1185
|
+
let gameRotation = event.message.gameRotation;
|
|
1186
|
+
updateQuaternion(gameRotation, true);
|
|
1187
|
+
});
|
|
1188
|
+
rotationDevice.addEventListener("rotation", (event) => {
|
|
1189
|
+
const rotation = event.message.rotation;
|
|
1190
|
+
updateQuaternion(rotation, true);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
window.sensorRate = 20;
|
|
1194
|
+
window.interpolationSmoothing = 0.4;
|
|
1195
|
+
window.positionScalar = 0.2;
|
|
1196
|
+
|
|
1197
|
+
/** @type {TVector3} */
|
|
1198
|
+
const _position = new THREE.Vector3();
|
|
1199
|
+
|
|
1200
|
+
/** @param {BS.Vector3} position */
|
|
1201
|
+
const updatePosition = (position) => {
|
|
1202
|
+
_position.copy(position).multiplyScalar(window.positionScalar);
|
|
1203
|
+
targetPositionEntity.object3D.position.lerp(
|
|
1204
|
+
_position,
|
|
1205
|
+
window.interpolationSmoothing
|
|
1206
|
+
);
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
rotationDevice.addEventListener("acceleration", (event) => {
|
|
1210
|
+
const acceleration = event.message.acceleration;
|
|
1211
|
+
updatePosition(acceleration);
|
|
1212
|
+
});
|
|
1213
|
+
rotationDevice.addEventListener("gravity", () => {
|
|
1214
|
+
const gravity = event.message.gravity;
|
|
1215
|
+
updatePosition(gravity);
|
|
1216
|
+
});
|
|
1217
|
+
rotationDevice.addEventListener("linearAcceleration", (event) => {
|
|
1218
|
+
const linearAcceleration = event.message.linearAcceleration;
|
|
1219
|
+
updatePosition(linearAcceleration);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
/** @type {TQuaternion} */
|
|
1223
|
+
const offsetQuaternion = new THREE.Quaternion();
|
|
1224
|
+
const resetOrientation = () => {
|
|
1225
|
+
offsetQuaternion.copy(_quaternion).invert();
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
const targetPositionEntity = document.getElementById("position");
|
|
1229
|
+
const targetRotationEntity = document.getElementById("rotation");
|
|
1230
|
+
|
|
1231
|
+
/** @type {HTMLButtonElement} */
|
|
1232
|
+
const resetOrientationButton = document.getElementById("resetOrientation");
|
|
1233
|
+
resetOrientationButton.addEventListener("click", () => {
|
|
1234
|
+
resetOrientation();
|
|
1235
|
+
});
|