viewer_sgos / deplacement_dans_env /tooltips_pr_env.js
MikaFil's picture
Create tooltips_pr_env.js
9791406 verified
// tooltips.js
/**
* initializeTooltips(options)
*
* - options.app: the PlayCanvas AppBase instance
* - options.cameraEntity: PlayCanvas camera Entity (utilisant le script orbitCamera)
* - options.modelEntity: the main model entity (for relative positioning, optional)
* - options.tooltipsUrl: URL to fetch JSON array of tooltip definitions
* - options.defaultVisible: boolean: whether tooltips are visible initially
* - options.moveDuration: number (seconds) for smooth camera move to selected tooltip
*
* JSON attendu pour chaque tooltip :
* {
* x, y, z, // position du tooltip (obligatoire)
* title, description, imgUrl, // infos UI (optionnelles)
* camX, camY, camZ // position de caméra cible (optionnelles)
* }
*
* Comportement :
* - Si camX/camY/camZ sont fournis, la caméra se déplacera exactement
* vers (camX, camY, camZ) et s'orientera pour regarder le tooltip.
* - Sinon, on conserve l'ancien comportement : la caméra orbite vers le tooltip
* avec une distance calculée (zoom minimum + taille du tooltip).
*/
export async function initializeTooltips(options) {
const {
app,
cameraEntity,
modelEntity, // non utilisé directement ici mais conservé pour compat
tooltipsUrl,
defaultVisible,
moveDuration = 0.6
} = options;
if (!app || !cameraEntity || !tooltipsUrl) return;
// --- Chargement du JSON de tooltips ---
let tooltipsData;
try {
const resp = await fetch(tooltipsUrl);
tooltipsData = await resp.json();
} catch (e) {
// Échec du fetch/parse JSON -> on abandonne proprement
return;
}
if (!Array.isArray(tooltipsData)) return;
const tooltipEntities = [];
// --- Matériau des sphères (tooltips) ---
const mat = new pc.StandardMaterial();
mat.diffuse = new pc.Color(1, 0.8, 0);
mat.specular = new pc.Color(1, 1, 1);
mat.shininess = 20;
mat.emissive = new pc.Color(0.85, 0.85, 0.85);
mat.emissiveIntensity = 1;
mat.useLighting = false;
mat.update();
// --- Création des entités sphères pour chaque tooltip ---
for (let i = 0; i < tooltipsData.length; i++) {
const tt = tooltipsData[i];
const { x, y, z, title, description, imgUrl, camX, camY, camZ } = tt;
const sphere = new pc.Entity("tooltip-" + i);
sphere.addComponent("model", { type: "sphere" });
sphere.model.material = mat;
// Taille par défaut des "pins"
sphere.setLocalScale(0.05, 0.05, 0.05);
sphere.setLocalPosition(x, y, z);
// On stocke toutes les infos utiles sur l'entité
sphere.tooltipData = {
title,
description,
imgUrl,
// Nouvelle partie : coordonnées de caméra cibles (optionnelles)
camTarget: (Number.isFinite(camX) && Number.isFinite(camY) && Number.isFinite(camZ))
? new pc.Vec3(camX, camY, camZ)
: null
};
app.root.addChild(sphere);
tooltipEntities.push(sphere);
}
// --- Gestion de la visibilité des tooltips ---
function setTooltipsVisibility(visible) {
tooltipEntities.forEach(ent => { ent.enabled = visible; });
}
setTooltipsVisibility(!!defaultVisible);
// Écouteur externe (ex. UI HTML) pour afficher/masquer les tooltips
document.addEventListener("toggle-tooltips", (evt) => {
const { visible } = evt.detail;
setTooltipsVisibility(!!visible);
});
// --- Picking (détection de clic sur un tooltip) ---
let currentTween = null;
app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
// Si une interpolation est en cours, on l'arrête proprement
if (currentTween) {
app.off("update", currentTween);
currentTween = null;
}
const x = event.x, y = event.y;
const from = new pc.Vec3(), to = new pc.Vec3();
const camera = cameraEntity.camera;
// Ray picking écran -> monde
camera.screenToWorld(x, y, camera.nearClip, from);
camera.screenToWorld(x, y, camera.farClip, to);
const dir = new pc.Vec3().sub2(to, from).normalize();
let closestT = Infinity;
let pickedEntity = null;
// Test d'intersection rayon/sphère (simple et suffisant ici)
for (const ent of tooltipEntities) {
if (!ent.enabled) continue;
const center = ent.getPosition();
const worldRadius = 0.5 * ent.getLocalScale().x;
const oc = new pc.Vec3().sub2(center, from);
const tca = oc.dot(dir);
if (tca < 0) continue;
const d2 = oc.lengthSq() - (tca * tca);
if (d2 > worldRadius * worldRadius) continue;
const thc = Math.sqrt(worldRadius * worldRadius - d2);
const t0 = tca - thc;
if (t0 < closestT && t0 >= 0) {
closestT = t0;
pickedEntity = ent;
}
}
if (pickedEntity) {
// Notifier l'UI (titre, description, image)
const { title, description, imgUrl } = pickedEntity.tooltipData;
document.dispatchEvent(new CustomEvent("tooltip-selected", {
detail: { title, description, imgUrl }
}));
// Si on a une position caméra cible, on l'utilise
const desiredCamPos = pickedEntity.tooltipData.camTarget;
tweenCameraToTooltip(pickedEntity, moveDuration, desiredCamPos);
}
});
// --- Helpers math/angles ---
function shortestAngleDiff(target, current) {
// Retourne l'écart angulaire [-180, 180] pour interpoler par le plus court chemin
let delta = target - current;
delta = ((delta + 180) % 360 + 360) % 360 - 180;
return delta;
}
/**
* Calcule {yaw, pitch, distance} pour une caméra à cameraPos regardant pivotPos,
* selon la convention de l'orbitCamera.
*/
function computeOrbitFromPositions(cameraPos, pivotPos) {
const tempEnt = new pc.Entity();
tempEnt.setPosition(cameraPos);
tempEnt.lookAt(pivotPos);
const rotation = tempEnt.getRotation();
// Direction "forward" (de la caméra vers le pivot)
const forward = new pc.Vec3();
rotation.transformVector(pc.Vec3.FORWARD, forward);
// Yaw : rotation horizontale
const rawYaw = Math.atan2(-forward.x, -forward.z) * pc.math.RAD_TO_DEG;
// Pitch : on retire d'abord l'influence du yaw pour isoler la composante verticale
const yawQuat = new pc.Quat().setFromEulerAngles(0, -rawYaw, 0);
const rotNoYaw = new pc.Quat().mul2(yawQuat, rotation);
const fNoYaw = new pc.Vec3();
rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
const rawPitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
// Distance : norme du vecteur pivot - caméra
const toPivot = new pc.Vec3().sub2(pivotPos, cameraPos);
const dist = toPivot.length();
tempEnt.destroy();
return { yaw: rawYaw, pitch: rawPitch, distance: dist };
}
/**
* Animation caméra vers un tooltip.
* - tooltipEnt: entité du tooltip cliqué
* - duration: durée de l'interpolation (s)
* - overrideCamWorldPos (pc.Vec3|null): si fourni, la caméra ira EXACTEMENT à cette position
* tout en regardant le tooltip.
*/
function tweenCameraToTooltip(tooltipEnt, duration, overrideCamWorldPos = null) {
const orbitCam = cameraEntity.script && cameraEntity.script.orbitCamera;
if (!orbitCam) return;
const targetPos = tooltipEnt.getPosition().clone();
// État initial (depuis l'orbitCamera)
const startPivot = orbitCam.pivotPoint.clone();
const startYaw = orbitCam._yaw;
const startPitch = orbitCam._pitch;
const startDist = orbitCam._distance;
// Valeurs finales à déterminer
let endPivot = targetPos.clone();
let endYaw, endPitch, endDist;
if (overrideCamWorldPos) {
// --- Nouveau mode : position caméra imposée par le JSON ---
// On calcule l'orbite (yaw/pitch/dist) qui correspond exactement à cette position
const { yaw, pitch, distance } = computeOrbitFromPositions(overrideCamWorldPos, targetPos);
// Interpolation par le plus court chemin depuis l'état courant
endYaw = startYaw + shortestAngleDiff(yaw, startYaw);
endPitch = startPitch + shortestAngleDiff(pitch, startPitch);
endDist = distance;
} else {
// --- Comportement historique (aucune camX/Y/Z fournie) ---
const worldRadius = 0.5 * tooltipEnt.getLocalScale().x;
const minZoom = orbitCam.distanceMin || 0.1;
const desiredDistance = Math.max(minZoom * 1.2, worldRadius * 4);
// On garde la position caméra actuelle comme point de départ pour calculer les angles
const camWorldPos = cameraEntity.getPosition().clone();
const { yaw, pitch } = computeOrbitFromPositions(camWorldPos, targetPos);
endYaw = startYaw + shortestAngleDiff(yaw, startYaw);
endPitch = startPitch + shortestAngleDiff(pitch, startPitch);
endDist = desiredDistance;
}
// Sauvegarde des origines pour l'interpolation
const orgPivot = startPivot.clone();
const orgYaw = startYaw;
const orgPitch = startPitch;
const orgDist = startDist;
let elapsed = 0;
// Si une interpolation était déjà en cours, on la débranche
if (currentTween) {
app.off("update", currentTween);
currentTween = null;
}
// --- Lerp frame-by-frame ---
function lerpUpdate(dt) {
elapsed += dt;
const t = Math.min(elapsed / duration, 1);
// Pivot (regard) vers le tooltip
const newPivot = new pc.Vec3().lerp(orgPivot, endPivot, t);
orbitCam.pivotPoint.copy(newPivot);
// Yaw, Pitch, Distance (on met aussi les "target" pour rester cohérent avec le script orbitCamera)
const newYaw = pc.math.lerp(orgYaw, endYaw, t);
const newPitch = pc.math.lerp(orgPitch, endPitch, t);
const newDist = pc.math.lerp(orgDist, endDist, t);
orbitCam._targetYaw = newYaw;
orbitCam._yaw = newYaw;
orbitCam._targetPitch = newPitch;
orbitCam._pitch = newPitch;
orbitCam._targetDistance = newDist;
orbitCam._distance = newDist;
// Mise à jour de la position monde de la caméra à partir des paramètres d'orbite
orbitCam._updatePosition();
if (t >= 1) {
app.off("update", lerpUpdate);
currentTween = null;
}
}
currentTween = lerpUpdate;
app.on("update", lerpUpdate);
}
}