web-3Dviever-glb/index.html Repository web-3Dviever-glb Original path web-3Dviever-glb/index.htmlRole SOURCE Size 53908 bytes Lines 1890 SHA-256 245ec511830c888022123d3efca5a44eded70f64927cbfaa8ab3645e1463f2beDisplayed range 501–1000 Previous file/page · Project index · Next file/page
zoomHoldDelay = window.setTimeout(() => {
zoomHoldInterval = window.setInterval(() => {
zoomCameraByFactor(factor);
}, 85);
}, 260);
});
button.addEventListener("click", (event) => {
/* Klik z klawiatury nie generuje pointerdown. */
if (event.detail === 0) {
zoomCameraByFactor(factor);
}
});
for (const eventName of [
"pointerup",
"pointercancel",
"pointerleave",
"lostpointercapture"
]) {
button.addEventListener(eventName, stopZoomHold);
}
}
bindZoomButton(zoomInButton, MOBILE_ZOOM_FACTOR);
bindZoomButton(zoomOutButton, 1 / MOBILE_ZOOM_FACTOR);
window.addEventListener("pointerup", stopZoomHold);
window.addEventListener("blur", stopZoomHold);
function moveCameraHorizontally(rightDelta, forwardDelta) {
cancelFlight();
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
if (forward.lengthSq() === 0) {
return;
}
forward.normalize();
const right = new THREE.Vector3(
forward.z,
0,
-forward.x
);
const moveVector = new THREE.Vector3()
.addScaledVector(right, rightDelta)
.addScaledVector(forward, forwardDelta);
camera.position.add(moveVector);
controls.target.add(moveVector);
controls.update();
}
window.addEventListener("keydown", (event) => {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement ||
event.metaKey ||
event.ctrlKey ||
event.altKey
) {
return;
}
switch (event.key) {
case "ArrowLeft":
event.preventDefault();
moveCameraHorizontally(KEYBOARD_MOVE_STEP, 0);
break;
case "ArrowRight":
event.preventDefault();
moveCameraHorizontally(-KEYBOARD_MOVE_STEP, 0);
break;
case "ArrowUp":
event.preventDefault();
moveCameraHorizontally(0, KEYBOARD_MOVE_STEP);
break;
case "ArrowDown":
event.preventDefault();
moveCameraHorizontally(0, -KEYBOARD_MOVE_STEP);
break;
case "r":
case "R":
resetCamera();
break;
}
});
/*
* Oba przyciski myszy: kamera nie zatacza okręgu wokół celu.
* Jej pozycja pozostaje nieruchoma, a zmienia się wyłącznie kierunek
* patrzenia — jak rozglądanie się głową stojąc w jednym miejscu.
*/
const inPlaceLook = {
active: false,
suppressUntilAllButtonsReleased: false,
lastX: 0,
lastY: 0,
targetDistance: 1
};
const lookSpherical = new THREE.Spherical();
const lookDirection = new THREE.Vector3();
function beginInPlaceLook(event) {
if (loadButton.disabled || !loadedObject) {
return;
}
cancelFlight();
inPlaceLook.active = true;
inPlaceLook.suppressUntilAllButtonsReleased = true;
inPlaceLook.lastX = event.clientX;
inPlaceLook.lastY = event.clientY;
inPlaceLook.targetDistance = Math.max(
camera.position.distanceTo(controls.target),
0.05
);
controls.enabled = false;
}
function updateInPlaceLook(event) {
const deltaX = event.clientX - inPlaceLook.lastX;
const deltaY = event.clientY - inPlaceLook.lastY;
inPlaceLook.lastX = event.clientX;
inPlaceLook.lastY = event.clientY;
camera.getWorldDirection(lookDirection);
lookSpherical.setFromVector3(lookDirection);
lookSpherical.theta += deltaX * IN_PLACE_LOOK_SENSITIVITY;
lookSpherical.phi += deltaY * IN_PLACE_LOOK_SENSITIVITY;
lookSpherical.phi = THREE.MathUtils.clamp(
lookSpherical.phi,
0.02,
Math.PI - 0.02
);
lookDirection.setFromSpherical(lookSpherical).normalize();
controls.target
.copy(camera.position)
.addScaledVector(lookDirection, inPlaceLook.targetDistance);
camera.lookAt(controls.target);
}
function finishInPlaceLook() {
inPlaceLook.active = false;
inPlaceLook.suppressUntilAllButtonsReleased = false;
controls.enabled = !loadButton.disabled;
controls.update();
}
renderer.domElement.addEventListener(
"mousedown",
(event) => {
if ((event.buttons & 3) === 3) {
beginInPlaceLook(event);
event.preventDefault();
event.stopImmediatePropagation();
}
},
true
);
renderer.domElement.addEventListener(
"pointermove",
(event) => {
if (event.pointerType !== "mouse") {
return;
}
if (
!inPlaceLook.suppressUntilAllButtonsReleased &&
(event.buttons & 3) === 3
) {
beginInPlaceLook(event);
}
if (!inPlaceLook.suppressUntilAllButtonsReleased) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
if ((event.buttons & 3) === 3) {
inPlaceLook.active = true;
updateInPlaceLook(event);
} else {
inPlaceLook.active = false;
}
},
true
);
window.addEventListener(
"pointerup",
(event) => {
if (
event.pointerType === "mouse" &&
inPlaceLook.suppressUntilAllButtonsReleased &&
event.buttons === 0
) {
finishInPlaceLook();
}
},
true
);
window.addEventListener("blur", () => {
if (inPlaceLook.suppressUntilAllButtonsReleased) {
finishInPlaceLook();
}
});
renderer.domElement.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
function cancelFlight() {
if (!flightAnimation) {
return;
}
flightAnimation = null;
controls.enabled = !loadButton.disabled;
}
controls.addEventListener("start", cancelFlight);
function easeInOutCubic(value) {
return value < 0.5
? 4 * value * value * value
: 1 - Math.pow(-2 * value + 2, 3) / 2;
}
function startFlightTo(point) {
if (!loadedObject) {
return;
}
const startPosition = camera.position.clone();
const startTarget = controls.target.clone();
const currentOffset = startPosition.clone().sub(startTarget);
const currentDistance = Math.max(currentOffset.length(), 0.001);
const directionFromTarget = currentOffset.normalize();
const minimumDistance = Math.max(loadedModelRadius * 0.025, 0.18);
const maximumDistance = Math.max(loadedModelRadius * 0.35, 0.8);
const endDistance = THREE.MathUtils.clamp(
currentDistance * 0.38,
minimumDistance,
maximumDistance
);
const endTarget = point.clone();
const endPosition = endTarget.clone().addScaledVector(
directionFromTarget,
endDistance
);
flightAnimation = {
startTime: performance.now(),
duration: DOUBLE_CLICK_FLIGHT_MS,
startPosition,
endPosition,
startTarget,
endTarget
};
controls.enabled = false;
}
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
function pickPointAt(clientX, clientY) {
if (!loadedObject) {
return null;
}
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
raycaster.params.Points.threshold = Math.max(
loadedModelRadius * 0.012,
0.025
);
try {
const hits = raycaster.intersectObject(loadedObject, true);
if (hits.length > 0) {
return hits[0].point.clone();
}
} catch (error) {
console.warn("Raycast modelu nie jest dostępny:", error);
}
/*
* Spark nie gwarantuje raycastu dla każdego wariantu splatów.
* W takim przypadku przelot trafia w punkt na płaszczyźnie
* przechodzącej przez aktualny cel kamery.
*/
if (loadedKind === "splat") {
const normal = new THREE.Vector3();
camera.getWorldDirection(normal);
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
normal,
controls.target
);
return raycaster.ray.intersectPlane(plane, new THREE.Vector3());
}
return null;
}
function flyToScreenPoint(clientX, clientY) {
const point = pickPointAt(clientX, clientY);
if (point) {
startFlightTo(point);
}
}
let lastTouchDoubleTapAt = -Infinity;
renderer.domElement.addEventListener("dblclick", (event) => {
event.preventDefault();
/* Nie uruchamiaj drugiego lotu, jeśli przeglądarka po double-tap
* wygenerowała dodatkowo syntetyczny dblclick. */
if (performance.now() - lastTouchDoubleTapAt < 600) {
return;
}
flyToScreenPoint(event.clientX, event.clientY);
});
/*
* Mobilny odpowiednik dwukliku: dwa krótkie stuknięcia jednym palcem.
* Gest z dwoma palcami nigdy nie jest interpretowany jako double-tap.
*/
const activeTouchPointers = new Map();
let touchGestureHadMultiplePointers = false;
let previousTap = null;
renderer.domElement.addEventListener("pointerdown", (event) => {
if (event.pointerType !== "touch") {
return;
}
if (activeTouchPointers.size === 0) {
touchGestureHadMultiplePointers = false;
}
activeTouchPointers.set(event.pointerId, {
startX: event.clientX,
startY: event.clientY,
startTime: performance.now(),
moved: false
});
if (activeTouchPointers.size > 1) {
touchGestureHadMultiplePointers = true;
}
});
renderer.domElement.addEventListener("pointermove", (event) => {
if (event.pointerType !== "touch") {
return;
}
const touch = activeTouchPointers.get(event.pointerId);
if (!touch) {
return;
}
if (
Math.hypot(
event.clientX - touch.startX,
event.clientY - touch.startY
) > TAP_MAX_MOVE_PX
) {
touch.moved = true;
}
});
function finishTouchPointer(event, allowTap) {
const touch = activeTouchPointers.get(event.pointerId);
if (!touch) {
return;
}
const now = performance.now();
const canBeTap =
allowTap &&
!touchGestureHadMultiplePointers &&
!touch.moved &&
now - touch.startTime < 520;
activeTouchPointers.delete(event.pointerId);
if (canBeTap) {
const currentTap = {
time: now,
x: event.clientX,
y: event.clientY
};
if (
previousTap &&
now - previousTap.time <= DOUBLE_TAP_MAX_DELAY_MS &&
Math.hypot(
currentTap.x - previousTap.x,
currentTap.y - previousTap.y
) <= DOUBLE_TAP_MAX_DISTANCE_PX
) {
previousTap = null;
lastTouchDoubleTapAt = now;
/* Pozwól OrbitControls najpierw zakończyć pointerup. */
window.setTimeout(() => {
flyToScreenPoint(currentTap.x, currentTap.y);
}, 0);
} else {
previousTap = currentTap;
}
}
if (activeTouchPointers.size === 0) {
touchGestureHadMultiplePointers = false;
}
}
renderer.domElement.addEventListener("pointerup", (event) => {
if (event.pointerType === "touch") {
finishTouchPointer(event, true);
}
});
renderer.domElement.addEventListener("pointercancel", (event) => {
if (event.pointerType === "touch") {
finishTouchPointer(event, false);
}
});
function updateFlightAnimation(now) {
if (!flightAnimation) {
return;
}
const rawProgress = Math.min(
(now - flightAnimation.startTime) / flightAnimation.duration,
1
);
const progress = easeInOutCubic(rawProgress);
camera.position.lerpVectors(
flightAnimation.startPosition,
flightAnimation.endPosition,
progress
);
controls.target.lerpVectors(
flightAnimation.startTarget,
flightAnimation.endTarget,
progress
);
controls.update();
if (rawProgress >= 1) {
flightAnimation = null;
controls.enabled = !loadButton.disabled;
}
}
function fitCameraToObject(object, viewDirection = new THREE.Vector3(0, 0, -1)) {
object.updateMatrixWorld(true);
const box = new THREE.Box3().setFromObject(object);
if (box.isEmpty()) {
homeCameraPosition.set(0, 0, -3.2);
homeCameraTarget.set(0, 0, -0.05);
loadedModelRadius = 1;
resetCamera();
return;
}
const sphere = box.getBoundingSphere(new THREE.Sphere());
const radius = Math.max(sphere.radius, 0.001);
loadedModelRadius = radius;
const verticalFov = THREE.MathUtils.degToRad(camera.fov);