@popaya/pgsg-viewer 0.1.9 → 0.1.11
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/dist/{chunk-3HV5SAZ7.js → chunk-Z7DBLRJH.js} +8 -2
- package/dist/chunk-Z7DBLRJH.js.map +1 -0
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{pgsg-three-viewer-TOCXQMZQ.js → pgsg-three-viewer-KBOYGHT3.js} +721 -377
- package/dist/pgsg-three-viewer-KBOYGHT3.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +418 -16
- package/dist/react/index.js.map +1 -1
- package/package.json +24 -2
- package/dist/chunk-3HV5SAZ7.js.map +0 -1
- package/dist/pgsg-three-viewer-TOCXQMZQ.js.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/viewers/three/pgsg-three-viewer.ts
|
|
2
|
-
import * as
|
|
3
|
-
import { GLTFLoader } from "three/
|
|
2
|
+
import * as THREE5 from "three";
|
|
3
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
4
|
+
import { TransformControls } from "three/addons/controls/TransformControls.js";
|
|
5
|
+
import * as RAPIER2 from "@dimforge/rapier3d-compat";
|
|
4
6
|
import nipplejs from "nipplejs";
|
|
5
7
|
|
|
6
8
|
// src/viewers/spark/SparkSplatRenderer.ts
|
|
@@ -40,119 +42,142 @@ var SparkSplatRenderer = class {
|
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
// src/viewers/three/controls/
|
|
45
|
+
// src/viewers/three/controls/RapierWalkCapsuleController.ts
|
|
44
46
|
import * as THREE from "three";
|
|
45
|
-
|
|
47
|
+
import * as RAPIER from "@dimforge/rapier3d-compat";
|
|
48
|
+
var RapierWalkController = class {
|
|
46
49
|
dom;
|
|
47
50
|
camera;
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
world;
|
|
52
|
+
body;
|
|
53
|
+
collider;
|
|
50
54
|
radius;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
halfHeight;
|
|
56
|
+
eyeHeight;
|
|
57
|
+
moveSpeed;
|
|
58
|
+
jumpSpeed;
|
|
54
59
|
lookSensitivity;
|
|
60
|
+
usePointerLock;
|
|
55
61
|
yaw = 0;
|
|
56
62
|
pitch = 0;
|
|
57
|
-
velocity = new THREE.Vector3();
|
|
58
|
-
onGround = false;
|
|
59
|
-
lastGroundY = null;
|
|
60
63
|
keys = /* @__PURE__ */ new Set();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
moveVec = new THREE.Vector2(0, 0);
|
|
65
|
+
// joystick: x=forward, y=right
|
|
66
|
+
groundProbeExtra;
|
|
67
|
+
groundSnapMax;
|
|
68
|
+
// ---- bound handlers (so dispose works) ----
|
|
69
|
+
onClickBound = () => this.onClick();
|
|
70
|
+
onMouseMoveBound = (e) => this.onMouseMove(e);
|
|
71
|
+
onKeyDownBound = (e) => this.onKeyDown(e);
|
|
72
|
+
onKeyUpBound = (e) => this.onKeyUp(e);
|
|
73
|
+
onBlurBound = () => this.onBlur();
|
|
70
74
|
constructor(opts) {
|
|
71
75
|
this.dom = opts.dom;
|
|
72
76
|
this.camera = opts.camera;
|
|
73
|
-
this.
|
|
74
|
-
|
|
77
|
+
this.world = opts.world;
|
|
78
|
+
this.radius = opts.radius ?? 0.2;
|
|
79
|
+
this.halfHeight = opts.halfHeight ?? 0.6;
|
|
75
80
|
this.eyeHeight = opts.eyeHeight ?? 1.6;
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.walkSpeed = opts.walkSpeed ?? 2.5;
|
|
79
|
-
this.runMultiplier = opts.runMultiplier ?? 2;
|
|
81
|
+
this.moveSpeed = opts.moveSpeed ?? 3;
|
|
82
|
+
this.jumpSpeed = opts.jumpSpeed ?? 6;
|
|
80
83
|
this.lookSensitivity = opts.lookSensitivity ?? 22e-4;
|
|
81
|
-
this.usePointerLock = opts.usePointerLock ??
|
|
84
|
+
this.usePointerLock = opts.usePointerLock ?? true;
|
|
85
|
+
this.groundProbeExtra = opts.groundProbeExtra ?? 0.25;
|
|
86
|
+
this.groundSnapMax = opts.groundSnapMax ?? 0.65;
|
|
82
87
|
const euler = new THREE.Euler().setFromQuaternion(
|
|
83
88
|
this.camera.quaternion,
|
|
84
89
|
"YXZ"
|
|
85
90
|
);
|
|
86
91
|
this.pitch = euler.x;
|
|
87
92
|
this.yaw = euler.y;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const start = this.camera.position.clone();
|
|
94
|
+
const centerY = start.y - this.eyeHeight + (this.halfHeight + this.radius);
|
|
95
|
+
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(start.x, centerY, start.z).lockRotations().setLinearDamping(6).setCcdEnabled(true);
|
|
96
|
+
this.body = this.world.createRigidBody(bodyDesc);
|
|
97
|
+
const colDesc = RAPIER.ColliderDesc.capsule(this.halfHeight, this.radius).setFriction(0.8).setRestitution(0);
|
|
98
|
+
this.collider = this.world.createCollider(colDesc, this.body);
|
|
99
|
+
this.collider.setActiveEvents?.(RAPIER.ActiveEvents.COLLISION_EVENTS);
|
|
100
|
+
this.bindInput();
|
|
93
101
|
}
|
|
94
102
|
dispose() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.unbind();
|
|
99
|
-
if (this.usePointerLock) document.exitPointerLock();
|
|
103
|
+
this.unbindInput();
|
|
104
|
+
if (this.body) {
|
|
105
|
+
this.world.removeRigidBody(this.body);
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
// ---------------------------
|
|
109
|
+
// External movement (joystick)
|
|
110
|
+
// x = forward/back (-1..1), y = right/left (-1..1)
|
|
111
|
+
// ---------------------------
|
|
102
112
|
setMovementVector(x, y) {
|
|
103
|
-
this.
|
|
113
|
+
this.moveVec.set(x, y);
|
|
104
114
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// --------------------------------------------------
|
|
118
|
-
// Input binding (SAFE for pointer lock)
|
|
119
|
-
// --------------------------------------------------
|
|
120
|
-
bind() {
|
|
121
|
-
this.dom.addEventListener("click", this.onClick);
|
|
122
|
-
window.addEventListener("mousemove", this.onMouseMove);
|
|
123
|
-
window.addEventListener("keydown", this.onKeyDown);
|
|
124
|
-
window.addEventListener("keyup", this.onKeyUp);
|
|
125
|
-
window.addEventListener("blur", this.onBlur);
|
|
126
|
-
this.dom.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
127
|
-
}
|
|
128
|
-
bindTouch() {
|
|
129
|
-
this.dom.addEventListener("touchstart", this.onTouchStart);
|
|
130
|
-
this.dom.addEventListener("touchmove", this.onTouchMove);
|
|
115
|
+
setRotation(yaw, pitch) {
|
|
116
|
+
this.yaw = yaw;
|
|
117
|
+
this.pitch = THREE.MathUtils.clamp(
|
|
118
|
+
pitch,
|
|
119
|
+
-Math.PI / 2 + 0.01,
|
|
120
|
+
Math.PI / 2 - 0.01
|
|
121
|
+
);
|
|
122
|
+
this.camera.quaternion.setFromEuler(
|
|
123
|
+
new THREE.Euler(this.pitch, this.yaw, 0, "YXZ")
|
|
124
|
+
);
|
|
131
125
|
}
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
window.removeEventListener("keydown", this.onKeyDown);
|
|
136
|
-
window.removeEventListener("keyup", this.onKeyUp);
|
|
137
|
-
window.removeEventListener("blur", this.onBlur);
|
|
126
|
+
resetVelocity() {
|
|
127
|
+
this.body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
128
|
+
this.body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
138
129
|
}
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
130
|
+
setPositionFromCamera() {
|
|
131
|
+
const p = this.camera.position;
|
|
132
|
+
const centerY = p.y - this.eyeHeight + (this.halfHeight + this.radius);
|
|
133
|
+
this.body.setTranslation({ x: p.x, y: centerY, z: p.z }, true);
|
|
134
|
+
this.resetVelocity();
|
|
142
135
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
136
|
+
// Snap the capsule so its feet rest on the nearest floor below
|
|
137
|
+
snapToGround() {
|
|
138
|
+
const p = this.body.translation();
|
|
139
|
+
const origin = { x: p.x, y: p.y + 2, z: p.z };
|
|
140
|
+
const dir = { x: 0, y: -1, z: 0 };
|
|
141
|
+
const ray = new RAPIER.Ray(origin, dir);
|
|
142
|
+
const maxDist = 50;
|
|
143
|
+
const hit = this.castRayWithNormal(ray, maxDist);
|
|
144
|
+
if (!hit) return;
|
|
145
|
+
const toi = hit.timeOfImpact;
|
|
146
|
+
const hitY = origin.y - toi;
|
|
147
|
+
const newCenterY = hitY + (this.halfHeight + this.radius) + 0.02;
|
|
148
|
+
this.body.setTranslation({ x: p.x, y: newCenterY, z: p.z }, true);
|
|
149
|
+
this.resetVelocity();
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------
|
|
152
|
+
// Input binding
|
|
153
|
+
// ---------------------------
|
|
154
|
+
bindInput() {
|
|
149
155
|
if (this.usePointerLock) {
|
|
150
|
-
|
|
156
|
+
this.dom.addEventListener("click", this.onClickBound);
|
|
157
|
+
window.addEventListener("mousemove", this.onMouseMoveBound);
|
|
151
158
|
}
|
|
159
|
+
window.addEventListener("keydown", this.onKeyDownBound);
|
|
160
|
+
window.addEventListener("keyup", this.onKeyUpBound);
|
|
161
|
+
window.addEventListener("blur", this.onBlurBound);
|
|
162
|
+
}
|
|
163
|
+
unbindInput() {
|
|
164
|
+
this.dom.removeEventListener("click", this.onClickBound);
|
|
165
|
+
window.removeEventListener("mousemove", this.onMouseMoveBound);
|
|
166
|
+
window.removeEventListener("keydown", this.onKeyDownBound);
|
|
167
|
+
window.removeEventListener("keyup", this.onKeyUpBound);
|
|
168
|
+
window.removeEventListener("blur", this.onBlurBound);
|
|
169
|
+
}
|
|
170
|
+
onClick() {
|
|
171
|
+
if (!this.usePointerLock) return;
|
|
172
|
+
if (document.pointerLockElement !== this.dom) this.dom.requestPointerLock();
|
|
173
|
+
}
|
|
174
|
+
onBlur() {
|
|
152
175
|
this.keys.clear();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
this.moveVec.set(0, 0);
|
|
177
|
+
}
|
|
178
|
+
onMouseMove(e) {
|
|
179
|
+
if (!this.usePointerLock) return;
|
|
180
|
+
if (document.pointerLockElement !== this.dom) return;
|
|
156
181
|
this.yaw -= e.movementX * this.lookSensitivity;
|
|
157
182
|
this.pitch -= e.movementY * this.lookSensitivity;
|
|
158
183
|
this.pitch = THREE.MathUtils.clamp(
|
|
@@ -163,171 +188,86 @@ var WalkCapsuleController = class {
|
|
|
163
188
|
this.camera.quaternion.setFromEuler(
|
|
164
189
|
new THREE.Euler(this.pitch, this.yaw, 0, "YXZ")
|
|
165
190
|
);
|
|
166
|
-
}
|
|
167
|
-
onKeyDown
|
|
191
|
+
}
|
|
192
|
+
onKeyDown(e) {
|
|
168
193
|
this.keys.add(e.key.toLowerCase());
|
|
169
|
-
}
|
|
170
|
-
onKeyUp
|
|
194
|
+
}
|
|
195
|
+
onKeyUp(e) {
|
|
171
196
|
this.keys.delete(e.key.toLowerCase());
|
|
172
|
-
};
|
|
173
|
-
onTouchStart = (e) => {
|
|
174
|
-
const t = e.touches[0];
|
|
175
|
-
this.lastTouchX = t.clientX;
|
|
176
|
-
this.lastTouchY = t.clientY;
|
|
177
|
-
};
|
|
178
|
-
onTouchMove = (e) => {
|
|
179
|
-
const t = e.touches[0];
|
|
180
|
-
const deltaX = t.clientX - this.lastTouchX;
|
|
181
|
-
const deltaY = t.clientY - this.lastTouchY;
|
|
182
|
-
this.lastTouchX = t.clientX;
|
|
183
|
-
this.lastTouchY = t.clientY;
|
|
184
|
-
this.yaw -= deltaX * 2e-3;
|
|
185
|
-
this.pitch -= deltaY * 2e-3;
|
|
186
|
-
this.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.pitch));
|
|
187
|
-
this.camera.quaternion.setFromEuler(
|
|
188
|
-
new THREE.Euler(this.pitch, this.yaw, 0, "YXZ")
|
|
189
|
-
);
|
|
190
|
-
};
|
|
191
|
-
startGyro() {
|
|
192
|
-
this.useGyro = true;
|
|
193
|
-
window.addEventListener("deviceorientation", (e) => {
|
|
194
|
-
if (!e.alpha || !e.beta) return;
|
|
195
|
-
this.gyroAlpha = THREE.MathUtils.degToRad(e.alpha);
|
|
196
|
-
this.gyroBeta = THREE.MathUtils.degToRad(e.beta);
|
|
197
|
-
this.gyroGamma = THREE.MathUtils.degToRad(e.gamma ?? 0);
|
|
198
|
-
});
|
|
199
197
|
}
|
|
200
|
-
//
|
|
201
|
-
// Update
|
|
202
|
-
//
|
|
203
|
-
update(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
new THREE.Euler(this.pitch, this.yaw, 0, "YXZ")
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
if (!Number.isFinite(this.camera.position.y)) return;
|
|
216
|
-
dt = Math.min(dt, 0.05);
|
|
217
|
-
const forward = new THREE.Vector3();
|
|
218
|
-
this.camera.getWorldDirection(forward);
|
|
219
|
-
forward.y = 0;
|
|
220
|
-
if (forward.lengthSq() < 0.01) {
|
|
221
|
-
forward.set(0, 0, -1).applyQuaternion(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, this.yaw, 0)));
|
|
222
|
-
}
|
|
223
|
-
forward.normalize();
|
|
224
|
-
const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
|
198
|
+
// ---------------------------
|
|
199
|
+
// Update
|
|
200
|
+
// ---------------------------
|
|
201
|
+
update(_dt) {
|
|
202
|
+
const forward = new THREE.Vector3(0, 0, -1).applyAxisAngle(
|
|
203
|
+
new THREE.Vector3(0, 1, 0),
|
|
204
|
+
this.yaw
|
|
205
|
+
);
|
|
206
|
+
const right = new THREE.Vector3(1, 0, 0).applyAxisAngle(
|
|
207
|
+
new THREE.Vector3(0, 1, 0),
|
|
208
|
+
this.yaw
|
|
209
|
+
);
|
|
225
210
|
const wish = new THREE.Vector3();
|
|
226
211
|
if (this.keys.has("w")) wish.add(forward);
|
|
227
212
|
if (this.keys.has("s")) wish.sub(forward);
|
|
228
213
|
if (this.keys.has("d")) wish.add(right);
|
|
229
214
|
if (this.keys.has("a")) wish.sub(right);
|
|
230
|
-
if (this.
|
|
231
|
-
wish.addScaledVector(forward, this.
|
|
232
|
-
wish.addScaledVector(right, this.
|
|
233
|
-
}
|
|
234
|
-
if (wish.lengthSq() > 0) wish.normalize();
|
|
235
|
-
const speed = this.walkSpeed * (this.keys.has("shift") ? this.runMultiplier : 1);
|
|
236
|
-
this.velocity.x = wish.x * speed;
|
|
237
|
-
this.velocity.z = wish.z * speed;
|
|
238
|
-
this.velocity.y -= this.gravity * dt;
|
|
239
|
-
const nextPos = this.camera.position.clone();
|
|
240
|
-
nextPos.x += this.velocity.x * dt;
|
|
241
|
-
nextPos.z += this.velocity.z * dt;
|
|
242
|
-
nextPos.y += this.velocity.y * dt;
|
|
243
|
-
const resolved = this.resolveGroundAndCollisions(nextPos);
|
|
244
|
-
this.camera.position.copy(resolved);
|
|
245
|
-
}
|
|
246
|
-
snapToGround() {
|
|
247
|
-
const origin = this.camera.position.clone();
|
|
248
|
-
origin.y += 5;
|
|
249
|
-
this.raycaster.set(origin, new THREE.Vector3(0, -1, 0));
|
|
250
|
-
this.raycaster.far = 50;
|
|
251
|
-
const hits = this.raycaster.intersectObjects(this.colliders, true);
|
|
252
|
-
for (const hit of hits) {
|
|
253
|
-
if (!hit.face) continue;
|
|
254
|
-
const normalMatrix = new THREE.Matrix3().getNormalMatrix(
|
|
255
|
-
hit.object.matrixWorld
|
|
256
|
-
);
|
|
257
|
-
const worldNormal = hit.face.normal.clone().applyMatrix3(normalMatrix).normalize();
|
|
258
|
-
if (worldNormal.y > 0.5) {
|
|
259
|
-
this.lastGroundY = hit.point.y;
|
|
260
|
-
this.camera.position.y = this.lastGroundY + this.eyeHeight;
|
|
261
|
-
this.velocity.y = 0;
|
|
262
|
-
this.onGround = true;
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
215
|
+
if (this.moveVec.lengthSq() > 1e-6) {
|
|
216
|
+
wish.addScaledVector(forward, this.moveVec.x);
|
|
217
|
+
wish.addScaledVector(right, this.moveVec.y);
|
|
265
218
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
286
|
-
const worldNormal = hit.face.normal.clone().applyMatrix3(normalMatrix).normalize();
|
|
287
|
-
if (worldNormal.y > 0.5) {
|
|
288
|
-
groundHit = hit;
|
|
289
|
-
break;
|
|
219
|
+
if (wish.lengthSq() > 1e-8) wish.normalize();
|
|
220
|
+
const vel = this.body.linvel();
|
|
221
|
+
const targetVX = wish.x * this.moveSpeed;
|
|
222
|
+
const targetVZ = wish.z * this.moveSpeed;
|
|
223
|
+
this.body.setLinvel(
|
|
224
|
+
{
|
|
225
|
+
x: targetVX,
|
|
226
|
+
y: vel.y,
|
|
227
|
+
z: targetVZ
|
|
228
|
+
},
|
|
229
|
+
true
|
|
230
|
+
// wake up
|
|
231
|
+
);
|
|
232
|
+
if (this.keys.has(" ")) {
|
|
233
|
+
if (this.isGrounded()) {
|
|
234
|
+
this.body.setLinvel(
|
|
235
|
+
{ x: targetVX, y: this.jumpSpeed, z: targetVZ },
|
|
236
|
+
true
|
|
237
|
+
);
|
|
290
238
|
}
|
|
291
239
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
240
|
+
const p = this.body.translation();
|
|
241
|
+
const feetY = p.y - (this.halfHeight + this.radius);
|
|
242
|
+
this.camera.position.set(p.x, feetY + this.eyeHeight, p.z);
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------
|
|
245
|
+
// Grounding
|
|
246
|
+
// ---------------------------
|
|
247
|
+
isGrounded() {
|
|
248
|
+
const p = this.body.translation();
|
|
249
|
+
const origin = { x: p.x, y: p.y, z: p.z };
|
|
250
|
+
const dir = { x: 0, y: -1, z: 0 };
|
|
251
|
+
const ray = new RAPIER.Ray(origin, dir);
|
|
252
|
+
const maxDist = this.halfHeight + this.radius + this.groundProbeExtra;
|
|
253
|
+
const hit = this.castRayWithNormal(ray, maxDist);
|
|
254
|
+
if (!hit) return false;
|
|
255
|
+
const ny = hit.normal?.y ?? 0;
|
|
256
|
+
return ny > 0.35;
|
|
257
|
+
}
|
|
258
|
+
// Ray cast helper with “exclude self” when possible
|
|
259
|
+
castRayWithNormal(ray, maxDist) {
|
|
260
|
+
const anyRapier = RAPIER;
|
|
261
|
+
try {
|
|
262
|
+
if (anyRapier.QueryFilter) {
|
|
263
|
+
const filter = new anyRapier.QueryFilter();
|
|
264
|
+
if (filter.excludeCollider) filter.excludeCollider(this.collider);
|
|
265
|
+
else if (filter.excludeRigidBody) filter.excludeRigidBody(this.body);
|
|
266
|
+
return this.world.castRayAndGetNormal(ray, maxDist, true, filter);
|
|
312
267
|
}
|
|
268
|
+
} catch {
|
|
313
269
|
}
|
|
314
|
-
|
|
315
|
-
center.y -= this.eyeHeight * 0.9;
|
|
316
|
-
const dirs = [
|
|
317
|
-
new THREE.Vector3(1, 0, 0),
|
|
318
|
-
new THREE.Vector3(-1, 0, 0),
|
|
319
|
-
new THREE.Vector3(0, 0, 1),
|
|
320
|
-
new THREE.Vector3(0, 0, -1)
|
|
321
|
-
];
|
|
322
|
-
for (const d of dirs) {
|
|
323
|
-
this.raycaster.set(center, d);
|
|
324
|
-
this.raycaster.far = this.radius + 0.25;
|
|
325
|
-
const wallHits = this.raycaster.intersectObjects(this.colliders, true);
|
|
326
|
-
if (!wallHits.length) continue;
|
|
327
|
-
const overlap = this.radius + 0.25 - wallHits[0].distance;
|
|
328
|
-
if (overlap > 0) camPos.addScaledVector(d, -overlap);
|
|
329
|
-
}
|
|
330
|
-
return camPos;
|
|
270
|
+
return this.world.castRayAndGetNormal(ray, maxDist, true);
|
|
331
271
|
}
|
|
332
272
|
};
|
|
333
273
|
|
|
@@ -904,78 +844,255 @@ var MeasurementController = class {
|
|
|
904
844
|
}
|
|
905
845
|
};
|
|
906
846
|
|
|
847
|
+
// src/viewers/three/controls/FloorplanController.ts
|
|
848
|
+
import * as THREE4 from "three";
|
|
849
|
+
var FloorplanController = class {
|
|
850
|
+
dom;
|
|
851
|
+
camera;
|
|
852
|
+
target;
|
|
853
|
+
height;
|
|
854
|
+
panSpeed;
|
|
855
|
+
zoomSpeed;
|
|
856
|
+
minHeight;
|
|
857
|
+
maxHeight;
|
|
858
|
+
isDragging = false;
|
|
859
|
+
lastX = 0;
|
|
860
|
+
lastY = 0;
|
|
861
|
+
// touch
|
|
862
|
+
touchMode = "none";
|
|
863
|
+
lastDist = 0;
|
|
864
|
+
constructor(opts) {
|
|
865
|
+
this.dom = opts.dom;
|
|
866
|
+
this.camera = opts.camera;
|
|
867
|
+
this.target = opts.target.clone();
|
|
868
|
+
this.height = opts.height;
|
|
869
|
+
this.panSpeed = opts.panSpeed ?? 0.01;
|
|
870
|
+
this.zoomSpeed = opts.zoomSpeed ?? 1.1;
|
|
871
|
+
this.minHeight = opts.minHeight ?? opts.height * 0.25;
|
|
872
|
+
this.maxHeight = opts.maxHeight ?? opts.height * 3;
|
|
873
|
+
this.applyCamera();
|
|
874
|
+
this.bind();
|
|
875
|
+
}
|
|
876
|
+
dispose() {
|
|
877
|
+
this.unbind();
|
|
878
|
+
}
|
|
879
|
+
update(_dt) {
|
|
880
|
+
this.applyCamera();
|
|
881
|
+
}
|
|
882
|
+
setTarget(t) {
|
|
883
|
+
this.target.copy(t);
|
|
884
|
+
this.applyCamera();
|
|
885
|
+
}
|
|
886
|
+
applyCamera() {
|
|
887
|
+
this.height = THREE4.MathUtils.clamp(
|
|
888
|
+
this.height,
|
|
889
|
+
this.minHeight,
|
|
890
|
+
this.maxHeight
|
|
891
|
+
);
|
|
892
|
+
this.camera.position.set(
|
|
893
|
+
this.target.x,
|
|
894
|
+
this.target.y + this.height,
|
|
895
|
+
this.target.z
|
|
896
|
+
);
|
|
897
|
+
this.camera.up.set(0, 0, -1);
|
|
898
|
+
this.camera.lookAt(this.target);
|
|
899
|
+
}
|
|
900
|
+
bind() {
|
|
901
|
+
this.dom.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
902
|
+
this.dom.addEventListener("mousedown", this.onMouseDown);
|
|
903
|
+
window.addEventListener("mousemove", this.onMouseMove);
|
|
904
|
+
window.addEventListener("mouseup", this.onMouseUp);
|
|
905
|
+
this.dom.addEventListener("wheel", this.onWheel, { passive: false });
|
|
906
|
+
this.dom.addEventListener("touchstart", this.onTouchStart, {
|
|
907
|
+
passive: false
|
|
908
|
+
});
|
|
909
|
+
this.dom.addEventListener("touchmove", this.onTouchMove, {
|
|
910
|
+
passive: false
|
|
911
|
+
});
|
|
912
|
+
this.dom.addEventListener("touchend", this.onTouchEnd);
|
|
913
|
+
}
|
|
914
|
+
unbind() {
|
|
915
|
+
this.dom.removeEventListener("mousedown", this.onMouseDown);
|
|
916
|
+
window.removeEventListener("mousemove", this.onMouseMove);
|
|
917
|
+
window.removeEventListener("mouseup", this.onMouseUp);
|
|
918
|
+
this.dom.removeEventListener("wheel", this.onWheel);
|
|
919
|
+
this.dom.removeEventListener("touchstart", this.onTouchStart);
|
|
920
|
+
this.dom.removeEventListener("touchmove", this.onTouchMove);
|
|
921
|
+
this.dom.removeEventListener("touchend", this.onTouchEnd);
|
|
922
|
+
}
|
|
923
|
+
onMouseDown = (e) => {
|
|
924
|
+
if (e.button !== 0) return;
|
|
925
|
+
this.isDragging = true;
|
|
926
|
+
this.lastX = e.clientX;
|
|
927
|
+
this.lastY = e.clientY;
|
|
928
|
+
};
|
|
929
|
+
onMouseUp = () => {
|
|
930
|
+
this.isDragging = false;
|
|
931
|
+
};
|
|
932
|
+
onMouseMove = (e) => {
|
|
933
|
+
if (!this.isDragging) return;
|
|
934
|
+
const dx = e.clientX - this.lastX;
|
|
935
|
+
const dy = e.clientY - this.lastY;
|
|
936
|
+
this.lastX = e.clientX;
|
|
937
|
+
this.lastY = e.clientY;
|
|
938
|
+
this.target.x -= dx * this.panSpeed * this.height;
|
|
939
|
+
this.target.z += dy * this.panSpeed * this.height;
|
|
940
|
+
this.applyCamera();
|
|
941
|
+
};
|
|
942
|
+
onWheel = (e) => {
|
|
943
|
+
e.preventDefault();
|
|
944
|
+
const factor = Math.pow(this.zoomSpeed, e.deltaY / 100);
|
|
945
|
+
this.height *= factor;
|
|
946
|
+
this.applyCamera();
|
|
947
|
+
};
|
|
948
|
+
onTouchStart = (e) => {
|
|
949
|
+
if (e.touches.length === 1) {
|
|
950
|
+
this.touchMode = "pan";
|
|
951
|
+
this.lastX = e.touches[0].clientX;
|
|
952
|
+
this.lastY = e.touches[0].clientY;
|
|
953
|
+
} else if (e.touches.length === 2) {
|
|
954
|
+
this.touchMode = "pinch";
|
|
955
|
+
this.lastDist = this.touchDistance(e.touches[0], e.touches[1]);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
onTouchMove = (e) => {
|
|
959
|
+
e.preventDefault();
|
|
960
|
+
if (this.touchMode === "pan" && e.touches.length === 1) {
|
|
961
|
+
const x = e.touches[0].clientX;
|
|
962
|
+
const y = e.touches[0].clientY;
|
|
963
|
+
const dx = x - this.lastX;
|
|
964
|
+
const dy = y - this.lastY;
|
|
965
|
+
this.lastX = x;
|
|
966
|
+
this.lastY = y;
|
|
967
|
+
this.target.x -= dx * this.panSpeed * this.height;
|
|
968
|
+
this.target.z += dy * this.panSpeed * this.height;
|
|
969
|
+
this.applyCamera();
|
|
970
|
+
}
|
|
971
|
+
if (this.touchMode === "pinch" && e.touches.length === 2) {
|
|
972
|
+
const dist = this.touchDistance(e.touches[0], e.touches[1]);
|
|
973
|
+
const ratio = this.lastDist / Math.max(1e-6, dist);
|
|
974
|
+
this.lastDist = dist;
|
|
975
|
+
this.height *= ratio;
|
|
976
|
+
this.applyCamera();
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
onTouchEnd = () => {
|
|
980
|
+
this.touchMode = "none";
|
|
981
|
+
};
|
|
982
|
+
touchDistance(a, b) {
|
|
983
|
+
const dx = a.clientX - b.clientX;
|
|
984
|
+
const dy = a.clientY - b.clientY;
|
|
985
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
907
989
|
// src/viewers/three/pgsg-three-viewer.ts
|
|
908
990
|
var PGSGThreeViewer = class {
|
|
909
991
|
constructor(options) {
|
|
910
992
|
this.options = options;
|
|
911
993
|
this.container = options.container;
|
|
912
|
-
this.
|
|
913
|
-
if (!this.container) {
|
|
994
|
+
if (!this.container)
|
|
914
995
|
throw new Error("[PGSGThreeViewer] Container is required");
|
|
915
|
-
}
|
|
916
996
|
}
|
|
917
|
-
scene = new
|
|
997
|
+
scene = new THREE5.Scene();
|
|
918
998
|
camera;
|
|
919
999
|
renderer;
|
|
920
1000
|
splats;
|
|
1001
|
+
gltfLoader = new GLTFLoader();
|
|
1002
|
+
pendingPlacementUrl;
|
|
1003
|
+
ghostModel;
|
|
1004
|
+
raycaster = new THREE5.Raycaster();
|
|
1005
|
+
mouse = new THREE5.Vector2();
|
|
1006
|
+
placementRotation = 0;
|
|
1007
|
+
transformControls;
|
|
921
1008
|
container;
|
|
922
1009
|
resizeObserver = null;
|
|
923
1010
|
controls;
|
|
924
1011
|
running = false;
|
|
925
|
-
orbitTarget = new
|
|
1012
|
+
orbitTarget = new THREE5.Vector3();
|
|
926
1013
|
lastTime = performance.now();
|
|
927
|
-
colliders = [];
|
|
928
1014
|
persistentMeasurements = [];
|
|
929
1015
|
measurementController = null;
|
|
930
|
-
|
|
931
|
-
|
|
1016
|
+
world;
|
|
1017
|
+
// Roots
|
|
1018
|
+
worldRoot = new THREE5.Object3D();
|
|
1019
|
+
// identity
|
|
1020
|
+
cvRoot = new THREE5.Object3D();
|
|
1021
|
+
// ONLY place where we convert OpenCV->OpenGL
|
|
1022
|
+
splatPivot = new THREE5.Object3D();
|
|
932
1023
|
colliderMesh;
|
|
933
|
-
|
|
1024
|
+
// For raycasting / measurement only
|
|
1025
|
+
colliderMeshes = [];
|
|
1026
|
+
currentMode = "orbit";
|
|
1027
|
+
acc = 0;
|
|
1028
|
+
fixedDt = 1 / 60;
|
|
1029
|
+
GLOBAL_SCALE = 0.7;
|
|
934
1030
|
async load() {
|
|
935
1031
|
this.initRenderer();
|
|
936
1032
|
this.initCamera();
|
|
937
1033
|
this.initScene();
|
|
938
1034
|
this.bindResize();
|
|
939
1035
|
this.resize();
|
|
1036
|
+
this.transformControls = new TransformControls(
|
|
1037
|
+
this.camera,
|
|
1038
|
+
this.renderer.domElement
|
|
1039
|
+
);
|
|
1040
|
+
this.scene.add(this.transformControls);
|
|
1041
|
+
console.log(this.transformControls instanceof THREE5.Object3D);
|
|
1042
|
+
this.transformControls.addEventListener("dragging-changed", (event) => {
|
|
1043
|
+
if (!this.controls) return;
|
|
1044
|
+
this.controls.enabled = !event.value;
|
|
1045
|
+
});
|
|
1046
|
+
this.transformControls.addEventListener("mouseDown", () => {
|
|
1047
|
+
document.exitPointerLock?.();
|
|
1048
|
+
});
|
|
1049
|
+
await RAPIER2.init();
|
|
1050
|
+
this.world = new RAPIER2.World({
|
|
1051
|
+
x: 0,
|
|
1052
|
+
y: -9.81 * this.GLOBAL_SCALE,
|
|
1053
|
+
z: 0
|
|
1054
|
+
});
|
|
940
1055
|
this.scene.add(this.worldRoot);
|
|
1056
|
+
this.cvRoot.scale.set(1, -1, -1);
|
|
1057
|
+
this.worldRoot.add(this.cvRoot);
|
|
941
1058
|
this.splats = new SparkSplatRenderer();
|
|
942
|
-
const { mesh
|
|
1059
|
+
const { mesh } = await this.splats.load(this.options.source);
|
|
943
1060
|
this.splatPivot.clear();
|
|
944
1061
|
this.splatPivot.add(mesh);
|
|
945
|
-
this.
|
|
946
|
-
this.worldRoot.rotation.x = Math.PI;
|
|
947
|
-
this.worldRoot.updateMatrixWorld(true);
|
|
1062
|
+
this.cvRoot.add(this.splatPivot);
|
|
948
1063
|
if (this.options.colliderSource) {
|
|
949
1064
|
await this.loadCollider(this.options.colliderSource);
|
|
950
1065
|
}
|
|
951
|
-
this.
|
|
1066
|
+
this.cvRoot.updateMatrixWorld(true);
|
|
1067
|
+
this.placeInitialCamera();
|
|
952
1068
|
if (this.isMobile()) {
|
|
953
1069
|
this.initJoystick();
|
|
954
|
-
this.initModeToggle();
|
|
955
1070
|
this.switchToWalk();
|
|
956
|
-
this.updateModeLabel("WALK");
|
|
957
|
-
const walk = this.controls;
|
|
958
|
-
window.addEventListener(
|
|
959
|
-
"keydown",
|
|
960
|
-
(e) => {
|
|
961
|
-
if (e.key === "g") {
|
|
962
|
-
walk.enableGyro();
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
{ once: true }
|
|
966
|
-
);
|
|
967
1071
|
} else {
|
|
968
1072
|
this.switchToMeasure();
|
|
969
1073
|
}
|
|
970
1074
|
window.addEventListener("keydown", this.onKeyToggle);
|
|
1075
|
+
window.addEventListener("keydown", (e) => {
|
|
1076
|
+
if (!this.transformControls) return;
|
|
1077
|
+
if (e.key === "r") this.transformControls.setMode("rotate");
|
|
1078
|
+
if (e.key === "t") this.transformControls.setMode("translate");
|
|
1079
|
+
if (e.key === "s") this.transformControls.setMode("scale");
|
|
1080
|
+
});
|
|
1081
|
+
this.renderer.domElement.addEventListener("click", this.onSceneClick);
|
|
971
1082
|
}
|
|
972
1083
|
start() {
|
|
973
1084
|
if (this.running) return;
|
|
974
1085
|
this.running = true;
|
|
975
|
-
this.renderer.setAnimationLoop((
|
|
1086
|
+
this.renderer.setAnimationLoop(() => {
|
|
976
1087
|
const now = performance.now();
|
|
977
1088
|
const delta = (now - this.lastTime) / 1e3;
|
|
978
1089
|
this.lastTime = now;
|
|
1090
|
+
this.acc += delta;
|
|
1091
|
+
while (this.acc >= this.fixedDt) {
|
|
1092
|
+
this.world.timestep = this.fixedDt;
|
|
1093
|
+
this.world.step();
|
|
1094
|
+
this.acc -= this.fixedDt;
|
|
1095
|
+
}
|
|
979
1096
|
this.controls?.update?.(delta);
|
|
980
1097
|
this.renderer.render(this.scene, this.camera);
|
|
981
1098
|
});
|
|
@@ -994,55 +1111,121 @@ var PGSGThreeViewer = class {
|
|
|
994
1111
|
destroy() {
|
|
995
1112
|
this.running = false;
|
|
996
1113
|
this.renderer.setAnimationLoop(null);
|
|
1114
|
+
this.renderer.domElement.removeEventListener("click", this.onSceneClick);
|
|
997
1115
|
this.resizeObserver?.disconnect();
|
|
998
1116
|
this.resizeObserver = null;
|
|
999
1117
|
this.controls?.dispose();
|
|
1000
1118
|
this.renderer.dispose();
|
|
1119
|
+
window.removeEventListener("keydown", this.onKeyToggle);
|
|
1001
1120
|
}
|
|
1002
1121
|
setCameraMode(mode) {
|
|
1003
|
-
|
|
1122
|
+
this.currentMode = mode;
|
|
1004
1123
|
if (this.controls instanceof MeasurementController) {
|
|
1005
1124
|
this.persistentMeasurements = this.controls.measurements;
|
|
1006
1125
|
this.controls.setVisible(false);
|
|
1007
1126
|
}
|
|
1008
1127
|
switch (mode) {
|
|
1009
1128
|
case "measure":
|
|
1010
|
-
document.exitPointerLock();
|
|
1129
|
+
document.exitPointerLock?.();
|
|
1011
1130
|
this.switchToMeasure();
|
|
1012
|
-
if (this.controls instanceof MeasurementController)
|
|
1131
|
+
if (this.controls instanceof MeasurementController)
|
|
1013
1132
|
this.controls.setVisible(true);
|
|
1014
|
-
}
|
|
1015
1133
|
break;
|
|
1016
1134
|
case "orbit":
|
|
1017
|
-
document.exitPointerLock();
|
|
1135
|
+
document.exitPointerLock?.();
|
|
1018
1136
|
this.switchToOrbit();
|
|
1019
1137
|
break;
|
|
1020
1138
|
case "walk":
|
|
1021
1139
|
this.switchToWalk();
|
|
1022
1140
|
break;
|
|
1023
|
-
|
|
1024
|
-
this.
|
|
1141
|
+
case "floorplan":
|
|
1142
|
+
this.switchToFloorplan();
|
|
1025
1143
|
break;
|
|
1026
1144
|
}
|
|
1027
1145
|
}
|
|
1028
1146
|
toggleCameraMode() {
|
|
1029
|
-
if (this.controls instanceof
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1147
|
+
if (this.controls instanceof RapierWalkController) this.switchToOrbit();
|
|
1148
|
+
else this.switchToWalk();
|
|
1149
|
+
}
|
|
1150
|
+
getMeasurements() {
|
|
1151
|
+
return this.measurementController?.getMeasurements?.() ?? [];
|
|
1152
|
+
}
|
|
1153
|
+
clearMeasurements() {
|
|
1154
|
+
if (!this.measurementController) return;
|
|
1155
|
+
this.measurementController.clearAll();
|
|
1156
|
+
this.persistentMeasurements = [];
|
|
1157
|
+
}
|
|
1158
|
+
finalizePolyline() {
|
|
1159
|
+
if (this.controls instanceof MeasurementController)
|
|
1160
|
+
this.controls.finalizePolyline();
|
|
1036
1161
|
}
|
|
1162
|
+
deleteMeasurement(index) {
|
|
1163
|
+
this.measurementController?.deleteMeasurement?.(index);
|
|
1164
|
+
}
|
|
1165
|
+
async enablePlacementMode(url) {
|
|
1166
|
+
this.pendingPlacementUrl = url;
|
|
1167
|
+
this.placementRotation = 0;
|
|
1168
|
+
console.log("[PGSG Viewer] Placement mode enabled");
|
|
1169
|
+
const gltf = await this.gltfLoader.loadAsync(url);
|
|
1170
|
+
this.ghostModel = gltf.scene;
|
|
1171
|
+
const box = new THREE5.Box3().setFromObject(this.ghostModel);
|
|
1172
|
+
const size = new THREE5.Vector3();
|
|
1173
|
+
box.getSize(size);
|
|
1174
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
1175
|
+
const scale = 1.5 / maxDim;
|
|
1176
|
+
this.ghostModel.scale.multiplyScalar(scale);
|
|
1177
|
+
this.ghostModel.traverse((child) => {
|
|
1178
|
+
if (!child.isMesh) return;
|
|
1179
|
+
child.material = child.material.clone();
|
|
1180
|
+
child.material.transparent = true;
|
|
1181
|
+
child.material.opacity = 0.4;
|
|
1182
|
+
});
|
|
1183
|
+
this.scene.add(this.ghostModel);
|
|
1184
|
+
this.renderer.domElement.addEventListener(
|
|
1185
|
+
"mousemove",
|
|
1186
|
+
this.onPlacementMove
|
|
1187
|
+
);
|
|
1188
|
+
this.renderer.domElement.addEventListener("wheel", this.onPlacementWheel);
|
|
1189
|
+
this.renderer.domElement.addEventListener("click", this.onSceneClick);
|
|
1190
|
+
window.addEventListener("keydown", this.onPlacementCancel);
|
|
1191
|
+
}
|
|
1192
|
+
async addModelFromUrl(url, position) {
|
|
1193
|
+
return new Promise((resolve, reject) => {
|
|
1194
|
+
this.gltfLoader.load(
|
|
1195
|
+
url,
|
|
1196
|
+
(gltf) => {
|
|
1197
|
+
const model = gltf.scene;
|
|
1198
|
+
const box = new THREE5.Box3().setFromObject(model);
|
|
1199
|
+
const size = new THREE5.Vector3();
|
|
1200
|
+
box.getSize(size);
|
|
1201
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
1202
|
+
const scale = 1.5 / maxDim;
|
|
1203
|
+
model.scale.multiplyScalar(scale);
|
|
1204
|
+
if (position) {
|
|
1205
|
+
model.position.copy(position);
|
|
1206
|
+
}
|
|
1207
|
+
const floorBox = new THREE5.Box3().setFromObject(model);
|
|
1208
|
+
model.position.y -= floorBox.min.y;
|
|
1209
|
+
this.cvRoot.add(model);
|
|
1210
|
+
resolve(model);
|
|
1211
|
+
},
|
|
1212
|
+
void 0,
|
|
1213
|
+
reject
|
|
1214
|
+
);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
// ---------------------------
|
|
1218
|
+
// Init
|
|
1219
|
+
// ---------------------------
|
|
1037
1220
|
initRenderer() {
|
|
1038
1221
|
if (this.renderer) return;
|
|
1039
|
-
this.renderer = new
|
|
1222
|
+
this.renderer = new THREE5.WebGLRenderer({
|
|
1040
1223
|
antialias: this.options.renderer?.antialias ?? false,
|
|
1041
1224
|
alpha: false,
|
|
1042
1225
|
powerPreference: "high-performance"
|
|
1043
1226
|
});
|
|
1044
1227
|
this.renderer.setClearColor(1118481, 1);
|
|
1045
|
-
this.renderer.outputColorSpace =
|
|
1228
|
+
this.renderer.outputColorSpace = THREE5.SRGBColorSpace;
|
|
1046
1229
|
this.renderer.setSize(
|
|
1047
1230
|
this.container.clientWidth,
|
|
1048
1231
|
this.container.clientHeight
|
|
@@ -1050,21 +1233,17 @@ var PGSGThreeViewer = class {
|
|
|
1050
1233
|
this.renderer.setPixelRatio(
|
|
1051
1234
|
this.options.renderer?.pixelRatio ?? window.devicePixelRatio
|
|
1052
1235
|
);
|
|
1236
|
+
this.renderer.localClippingEnabled = true;
|
|
1053
1237
|
this.container.appendChild(this.renderer.domElement);
|
|
1054
|
-
this.renderer.domElement.addEventListener("webglcontextlost", (e) => {
|
|
1055
|
-
e.preventDefault();
|
|
1056
|
-
console.log("[PGSG Viewer] WebGL context lost");
|
|
1057
|
-
});
|
|
1058
1238
|
}
|
|
1059
1239
|
initScene() {
|
|
1060
|
-
this.scene = new
|
|
1061
|
-
this.scene.background = new
|
|
1062
|
-
const
|
|
1063
|
-
const light = new THREE4.HemisphereLight(16777215, 2236962, 1);
|
|
1240
|
+
this.scene = new THREE5.Scene();
|
|
1241
|
+
this.scene.background = new THREE5.Color(1118481);
|
|
1242
|
+
const light = new THREE5.HemisphereLight(16777215, 2236962, 1);
|
|
1064
1243
|
this.scene.add(light);
|
|
1065
1244
|
}
|
|
1066
1245
|
initCamera() {
|
|
1067
|
-
this.camera = new
|
|
1246
|
+
this.camera = new THREE5.PerspectiveCamera(
|
|
1068
1247
|
this.options.camera?.fov ?? 60,
|
|
1069
1248
|
this.container.clientWidth / this.container.clientHeight,
|
|
1070
1249
|
this.options.camera?.near ?? 0.01,
|
|
@@ -1073,109 +1252,105 @@ var PGSGThreeViewer = class {
|
|
|
1073
1252
|
this.camera.position.set(2, 2, 5);
|
|
1074
1253
|
this.camera.lookAt(0, 0, 0);
|
|
1075
1254
|
}
|
|
1076
|
-
initJoystick() {
|
|
1077
|
-
const zone = document.createElement("div");
|
|
1078
|
-
zone.style.position = "absolute";
|
|
1079
|
-
zone.style.left = "20px";
|
|
1080
|
-
zone.style.bottom = "20px";
|
|
1081
|
-
zone.style.width = "150px";
|
|
1082
|
-
zone.style.height = "150px";
|
|
1083
|
-
zone.style.zIndex = "1000";
|
|
1084
|
-
this.container.appendChild(zone);
|
|
1085
|
-
const joystick = nipplejs.create({
|
|
1086
|
-
zone,
|
|
1087
|
-
mode: "static",
|
|
1088
|
-
position: { left: "50px", bottom: "50px" },
|
|
1089
|
-
color: "white"
|
|
1090
|
-
});
|
|
1091
|
-
joystick.on("move", (_, data) => {
|
|
1092
|
-
const angle = data.angle.radian;
|
|
1093
|
-
const force = data.force;
|
|
1094
|
-
if (this.controls instanceof WalkCapsuleController || this.controls instanceof OrbitCameraController) {
|
|
1095
|
-
this.controls.setMovementVector(
|
|
1096
|
-
Math.sin(angle) * force,
|
|
1097
|
-
Math.cos(angle) * force
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
joystick.on("end", () => {
|
|
1102
|
-
if (this.controls instanceof WalkCapsuleController || this.controls instanceof OrbitCameraController) {
|
|
1103
|
-
this.controls.setMovementVector(0, 0);
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
initModeToggle() {
|
|
1108
|
-
}
|
|
1109
|
-
updateModeLabel(mode) {
|
|
1110
|
-
if (!this.modeButton) return;
|
|
1111
|
-
this.modeButton.innerText = mode;
|
|
1112
|
-
}
|
|
1113
1255
|
bindResize() {
|
|
1114
1256
|
this.resizeObserver?.disconnect();
|
|
1115
1257
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
1116
1258
|
this.resizeObserver.observe(this.container);
|
|
1117
1259
|
}
|
|
1260
|
+
isMobile() {
|
|
1261
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0 || window.innerWidth < 768;
|
|
1262
|
+
}
|
|
1263
|
+
// ---------------------------
|
|
1264
|
+
// Collider (visual + rapier)
|
|
1265
|
+
// ---------------------------
|
|
1118
1266
|
async loadCollider(url) {
|
|
1119
1267
|
console.log("[PGSG Viewer] Loading collider:", url);
|
|
1120
1268
|
const loader = new GLTFLoader();
|
|
1121
1269
|
const gltf = await loader.loadAsync(url);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
this.colliderMesh
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1270
|
+
const colliderRoot = new THREE5.Object3D();
|
|
1271
|
+
colliderRoot.name = "colliderRoot";
|
|
1272
|
+
colliderRoot.add(gltf.scene);
|
|
1273
|
+
this.colliderMesh = colliderRoot;
|
|
1274
|
+
this.cvRoot.add(colliderRoot);
|
|
1275
|
+
this.cvRoot.updateMatrixWorld(true);
|
|
1276
|
+
colliderRoot.updateWorldMatrix(true, true);
|
|
1277
|
+
this.colliderMeshes.length = 0;
|
|
1278
|
+
colliderRoot.traverse((child) => {
|
|
1279
|
+
if (!child?.isMesh) return;
|
|
1280
|
+
const geom = child.geometry.clone();
|
|
1281
|
+
child.updateWorldMatrix(true, false);
|
|
1282
|
+
geom.applyMatrix4(child.matrixWorld);
|
|
1283
|
+
const vertices = new Float32Array(geom.attributes.position.array);
|
|
1284
|
+
let indices;
|
|
1285
|
+
if (geom.index) indices = new Uint32Array(geom.index.array);
|
|
1286
|
+
else {
|
|
1287
|
+
const count = geom.attributes.position.count;
|
|
1288
|
+
indices = new Uint32Array(count);
|
|
1289
|
+
for (let i = 0; i < count; i++) indices[i] = i;
|
|
1133
1290
|
}
|
|
1291
|
+
const rb = this.world.createRigidBody(RAPIER2.RigidBodyDesc.fixed());
|
|
1292
|
+
const cd = RAPIER2.ColliderDesc.trimesh(vertices, indices);
|
|
1293
|
+
cd.setFriction(0.9);
|
|
1294
|
+
cd.setRestitution(0);
|
|
1295
|
+
this.world.createCollider(cd, rb);
|
|
1296
|
+
this.colliderMeshes.push(child);
|
|
1134
1297
|
});
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
isMobile() {
|
|
1139
|
-
return "ontouchstart" in window || navigator.maxTouchPoints > 0 || window.innerWidth < 768;
|
|
1298
|
+
const rawBox = new THREE5.Box3().setFromObject(colliderRoot);
|
|
1299
|
+
console.log("OpenGL bounds Y:", rawBox.min.y, rawBox.max.y);
|
|
1300
|
+
console.log("[PGSG Viewer] Colliders loaded:", this.colliderMeshes.length);
|
|
1140
1301
|
}
|
|
1302
|
+
// ---------------------------
|
|
1303
|
+
// Mode switching
|
|
1304
|
+
// ---------------------------
|
|
1141
1305
|
onKeyToggle = (e) => {
|
|
1142
1306
|
if (e.key !== "v") return;
|
|
1143
|
-
if (this.controls instanceof
|
|
1144
|
-
|
|
1145
|
-
} else {
|
|
1146
|
-
this.switchToWalk();
|
|
1147
|
-
}
|
|
1307
|
+
if (this.controls instanceof RapierWalkController) this.switchToOrbit();
|
|
1308
|
+
else this.switchToWalk();
|
|
1148
1309
|
};
|
|
1149
1310
|
switchToMeasure() {
|
|
1150
|
-
this.controls?.dispose();
|
|
1311
|
+
this.controls?.dispose?.();
|
|
1151
1312
|
this.measurementController = new MeasurementController({
|
|
1152
1313
|
dom: this.renderer.domElement,
|
|
1153
1314
|
camera: this.camera,
|
|
1154
1315
|
scene: this.scene,
|
|
1155
|
-
colliders: this.
|
|
1316
|
+
colliders: this.colliderMeshes
|
|
1156
1317
|
});
|
|
1157
|
-
this.measurementController.restoreMeasurements(
|
|
1318
|
+
this.measurementController.restoreMeasurements?.(
|
|
1319
|
+
this.persistentMeasurements
|
|
1320
|
+
);
|
|
1158
1321
|
this.controls = this.measurementController;
|
|
1159
1322
|
}
|
|
1160
1323
|
switchToWalk() {
|
|
1161
|
-
this.controls?.dispose();
|
|
1162
|
-
const walk = new
|
|
1324
|
+
this.controls?.dispose?.();
|
|
1325
|
+
const walk = new RapierWalkController({
|
|
1163
1326
|
camera: this.camera,
|
|
1164
1327
|
dom: this.renderer.domElement,
|
|
1165
|
-
|
|
1328
|
+
world: this.world,
|
|
1166
1329
|
eyeHeight: 1.6,
|
|
1167
|
-
radius: 0.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1330
|
+
radius: 0.2,
|
|
1331
|
+
halfHeight: 0.6,
|
|
1332
|
+
moveSpeed: 3 * this.GLOBAL_SCALE,
|
|
1333
|
+
jumpSpeed: 6 * this.GLOBAL_SCALE,
|
|
1171
1334
|
usePointerLock: !this.isMobile()
|
|
1172
1335
|
});
|
|
1173
1336
|
this.controls = walk;
|
|
1174
|
-
|
|
1337
|
+
const euler = new THREE5.Euler().setFromQuaternion(
|
|
1338
|
+
this.camera.quaternion,
|
|
1339
|
+
"YXZ"
|
|
1340
|
+
);
|
|
1341
|
+
walk.setRotation(euler.y, euler.x);
|
|
1342
|
+
walk.setPositionFromCamera();
|
|
1343
|
+
walk.resetVelocity();
|
|
1344
|
+
walk.snapToGround?.();
|
|
1175
1345
|
}
|
|
1176
1346
|
switchToOrbit() {
|
|
1177
|
-
this.controls?.dispose();
|
|
1178
|
-
|
|
1347
|
+
this.controls?.dispose?.();
|
|
1348
|
+
this.camera.up.set(0, 1, 0);
|
|
1349
|
+
this.camera.updateMatrixWorld(true);
|
|
1350
|
+
const box = new THREE5.Box3().setFromObject(this.cvRoot);
|
|
1351
|
+
const center = box.getCenter(new THREE5.Vector3());
|
|
1352
|
+
this.orbitTarget.copy(center);
|
|
1353
|
+
const distance = Math.max(this.camera.position.distanceTo(center), 1);
|
|
1179
1354
|
this.controls = new OrbitCameraController(
|
|
1180
1355
|
this.camera,
|
|
1181
1356
|
this.renderer.domElement,
|
|
@@ -1186,30 +1361,199 @@ var PGSGThreeViewer = class {
|
|
|
1186
1361
|
}
|
|
1187
1362
|
);
|
|
1188
1363
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1364
|
+
switchToFloorplan() {
|
|
1365
|
+
this.controls?.dispose?.();
|
|
1366
|
+
const box = new THREE5.Box3().setFromObject(this.cvRoot);
|
|
1367
|
+
const center = box.getCenter(new THREE5.Vector3());
|
|
1368
|
+
const size = box.getSize(new THREE5.Vector3());
|
|
1369
|
+
const height = Math.max(size.x, size.z) * 1.2;
|
|
1370
|
+
this.orbitTarget.copy(center);
|
|
1371
|
+
this.controls = new FloorplanController({
|
|
1372
|
+
dom: this.renderer.domElement,
|
|
1373
|
+
camera: this.camera,
|
|
1374
|
+
target: center,
|
|
1375
|
+
height,
|
|
1376
|
+
panSpeed: 15e-4,
|
|
1377
|
+
zoomSpeed: 1.15
|
|
1378
|
+
});
|
|
1194
1379
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1380
|
+
// ---------------------------
|
|
1381
|
+
// Spawn
|
|
1382
|
+
// ---------------------------
|
|
1383
|
+
placeInitialCamera() {
|
|
1384
|
+
const targetObj = this.colliderMesh ?? this.cvRoot;
|
|
1385
|
+
const box = new THREE5.Box3().setFromObject(targetObj);
|
|
1386
|
+
const center = box.getCenter(new THREE5.Vector3());
|
|
1387
|
+
const size = box.getSize(new THREE5.Vector3());
|
|
1388
|
+
const startY = box.max.y + 10;
|
|
1389
|
+
const maxDist = box.max.y - box.min.y + 50;
|
|
1390
|
+
const samples = [];
|
|
1391
|
+
const sx = Math.max(0.2, size.x * 0.15);
|
|
1392
|
+
const sz = Math.max(0.2, size.z * 0.15);
|
|
1393
|
+
for (const dx of [-sx, 0, sx]) {
|
|
1394
|
+
for (const dz of [-sz, 0, sz]) {
|
|
1395
|
+
samples.push({ x: center.x + dx, z: center.z + dz });
|
|
1396
|
+
}
|
|
1199
1397
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
this.
|
|
1398
|
+
let bestFloorY = null;
|
|
1399
|
+
let bestXZ = null;
|
|
1400
|
+
for (const s of samples) {
|
|
1401
|
+
const hits = this.raycastDownMulti(s.x, s.z, startY, maxDist, 10);
|
|
1402
|
+
if (hits.length === 0) continue;
|
|
1403
|
+
let candidate = null;
|
|
1404
|
+
for (let i = 1; i < hits.length; i++) {
|
|
1405
|
+
const h = hits[i];
|
|
1406
|
+
if (Math.abs(h.normalY) < 0.55) continue;
|
|
1407
|
+
candidate = h;
|
|
1408
|
+
}
|
|
1409
|
+
if (!candidate) continue;
|
|
1410
|
+
if (bestFloorY == null || candidate.y > bestFloorY) {
|
|
1411
|
+
bestFloorY = candidate.y;
|
|
1412
|
+
bestXZ = s;
|
|
1413
|
+
}
|
|
1204
1414
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
this.
|
|
1415
|
+
const eye = 1.6;
|
|
1416
|
+
if (bestFloorY != null && bestXZ) {
|
|
1417
|
+
this.camera.position.set(bestXZ.x, bestFloorY + eye, bestXZ.z + 0.5);
|
|
1418
|
+
this.orbitTarget.set(bestXZ.x, bestFloorY, bestXZ.z);
|
|
1419
|
+
} else {
|
|
1420
|
+
this.camera.position.set(center.x, box.max.y + eye, center.z + 2);
|
|
1421
|
+
this.orbitTarget.copy(center);
|
|
1209
1422
|
}
|
|
1423
|
+
this.camera.up.set(0, 1, 0);
|
|
1424
|
+
this.camera.lookAt(this.orbitTarget);
|
|
1425
|
+
this.camera.updateMatrixWorld(true);
|
|
1426
|
+
}
|
|
1427
|
+
getWorldNormalSafe(hit) {
|
|
1428
|
+
if (!hit.face) return null;
|
|
1429
|
+
const m3 = new THREE5.Matrix3().getNormalMatrix(hit.object.matrixWorld);
|
|
1430
|
+
return hit.face.normal.clone().applyMatrix3(m3).normalize();
|
|
1431
|
+
}
|
|
1432
|
+
// ---------------------------
|
|
1433
|
+
// Mobile joystick
|
|
1434
|
+
// ---------------------------
|
|
1435
|
+
initJoystick() {
|
|
1436
|
+
const zone = document.createElement("div");
|
|
1437
|
+
zone.style.position = "absolute";
|
|
1438
|
+
zone.style.left = "20px";
|
|
1439
|
+
zone.style.bottom = "20px";
|
|
1440
|
+
zone.style.width = "150px";
|
|
1441
|
+
zone.style.height = "150px";
|
|
1442
|
+
zone.style.zIndex = "1000";
|
|
1443
|
+
this.container.appendChild(zone);
|
|
1444
|
+
const joystick = nipplejs.create({
|
|
1445
|
+
zone,
|
|
1446
|
+
mode: "static",
|
|
1447
|
+
position: { left: "50px", bottom: "50px" },
|
|
1448
|
+
color: "white"
|
|
1449
|
+
});
|
|
1450
|
+
joystick.on("move", (_, data) => {
|
|
1451
|
+
const angle = data.angle.radian;
|
|
1452
|
+
const force = data.force;
|
|
1453
|
+
if (this.controls instanceof RapierWalkController || this.controls instanceof OrbitCameraController) {
|
|
1454
|
+
this.controls.setMovementVector(
|
|
1455
|
+
Math.sin(angle) * force,
|
|
1456
|
+
Math.cos(angle) * force
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
joystick.on("end", () => {
|
|
1461
|
+
if (this.controls instanceof RapierWalkController || this.controls instanceof OrbitCameraController) {
|
|
1462
|
+
this.controls.setMovementVector(0, 0);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1210
1465
|
}
|
|
1466
|
+
raycastDownMulti(x, z, startY, maxDist, maxHits = 8) {
|
|
1467
|
+
const hits = [];
|
|
1468
|
+
let originY = startY;
|
|
1469
|
+
let remaining = maxDist;
|
|
1470
|
+
for (let i = 0; i < maxHits; i++) {
|
|
1471
|
+
const ray = new RAPIER2.Ray({ x, y: originY, z }, { x: 0, y: -1, z: 0 });
|
|
1472
|
+
const hit = this.world.castRayAndGetNormal(ray, remaining, true);
|
|
1473
|
+
if (!hit) break;
|
|
1474
|
+
const y = originY - hit.timeOfImpact;
|
|
1475
|
+
const normalY = hit.normal?.y ?? 0;
|
|
1476
|
+
hits.push({ y, normalY });
|
|
1477
|
+
const eps = 0.02;
|
|
1478
|
+
originY = y - eps;
|
|
1479
|
+
remaining = remaining - hit.timeOfImpact - eps;
|
|
1480
|
+
if (remaining <= 0) break;
|
|
1481
|
+
}
|
|
1482
|
+
return hits;
|
|
1483
|
+
}
|
|
1484
|
+
onSceneClick = async (event) => {
|
|
1485
|
+
if (!this.pendingPlacementUrl || !this.ghostModel) return;
|
|
1486
|
+
const position = this.ghostModel.position.clone();
|
|
1487
|
+
const placed = await this.addModelFromUrl(
|
|
1488
|
+
this.pendingPlacementUrl,
|
|
1489
|
+
position
|
|
1490
|
+
);
|
|
1491
|
+
if (placed && this.transformControls) {
|
|
1492
|
+
placed.rotation.y = this.placementRotation;
|
|
1493
|
+
this.transformControls.attach(placed);
|
|
1494
|
+
}
|
|
1495
|
+
this.scene.remove(this.ghostModel);
|
|
1496
|
+
this.ghostModel = void 0;
|
|
1497
|
+
this.pendingPlacementUrl = void 0;
|
|
1498
|
+
this.renderer.domElement.removeEventListener(
|
|
1499
|
+
"mousemove",
|
|
1500
|
+
this.onPlacementMove
|
|
1501
|
+
);
|
|
1502
|
+
this.renderer.domElement.removeEventListener(
|
|
1503
|
+
"wheel",
|
|
1504
|
+
this.onPlacementWheel
|
|
1505
|
+
);
|
|
1506
|
+
this.renderer.domElement.removeEventListener("click", this.onSceneClick);
|
|
1507
|
+
window.removeEventListener("keydown", this.onPlacementCancel);
|
|
1508
|
+
};
|
|
1509
|
+
onPlacementMove = (event) => {
|
|
1510
|
+
if (!this.pendingPlacementUrl || !this.ghostModel) return;
|
|
1511
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
1512
|
+
this.mouse.set(
|
|
1513
|
+
(event.clientX - rect.left) / rect.width * 2 - 1,
|
|
1514
|
+
-((event.clientY - rect.top) / rect.height) * 2 + 1
|
|
1515
|
+
);
|
|
1516
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
1517
|
+
const intersects = this.raycaster.intersectObjects(
|
|
1518
|
+
this.colliderMeshes,
|
|
1519
|
+
true
|
|
1520
|
+
);
|
|
1521
|
+
if (!intersects.length) return;
|
|
1522
|
+
const point = intersects[0].point;
|
|
1523
|
+
this.ghostModel.position.copy(point);
|
|
1524
|
+
};
|
|
1525
|
+
onPlacementWheel = (event) => {
|
|
1526
|
+
if (!this.ghostModel) return;
|
|
1527
|
+
event.preventDefault();
|
|
1528
|
+
const step = Math.PI / 12;
|
|
1529
|
+
if (event.deltaY > 0) {
|
|
1530
|
+
this.placementRotation += step;
|
|
1531
|
+
} else {
|
|
1532
|
+
this.placementRotation -= step;
|
|
1533
|
+
}
|
|
1534
|
+
this.ghostModel.rotation.y = this.placementRotation;
|
|
1535
|
+
};
|
|
1536
|
+
onPlacementCancel = (event) => {
|
|
1537
|
+
if (event.key !== "Escape") return;
|
|
1538
|
+
if (!this.ghostModel) return;
|
|
1539
|
+
console.log("[PGSG Viewer] Placement cancelled");
|
|
1540
|
+
this.scene.remove(this.ghostModel);
|
|
1541
|
+
this.ghostModel = void 0;
|
|
1542
|
+
this.pendingPlacementUrl = void 0;
|
|
1543
|
+
this.renderer.domElement.style.cursor = "default";
|
|
1544
|
+
this.renderer.domElement.removeEventListener(
|
|
1545
|
+
"mousemove",
|
|
1546
|
+
this.onPlacementMove
|
|
1547
|
+
);
|
|
1548
|
+
this.renderer.domElement.removeEventListener("click", this.onSceneClick);
|
|
1549
|
+
this.renderer.domElement.removeEventListener(
|
|
1550
|
+
"wheel",
|
|
1551
|
+
this.onPlacementWheel
|
|
1552
|
+
);
|
|
1553
|
+
window.removeEventListener("keydown", this.onPlacementCancel);
|
|
1554
|
+
};
|
|
1211
1555
|
};
|
|
1212
1556
|
export {
|
|
1213
1557
|
PGSGThreeViewer
|
|
1214
1558
|
};
|
|
1215
|
-
//# sourceMappingURL=pgsg-three-viewer-
|
|
1559
|
+
//# sourceMappingURL=pgsg-three-viewer-KBOYGHT3.js.map
|