Spaces:
Running
Running
| //points.js | |
| /** | |
| * initializePoints(options) | |
| * | |
| * - options.app: the PlayCanvas App instance | |
| * - options.cameraEntity: the PlayCanvas camera Entity | |
| * - options.modelEntity: the main model entity (for any relative positioning; optional) | |
| * - options.pointsUrl: URL to fetch JSON array of points | |
| * - options.defaultVisible: boolean: whether points are visible initially | |
| * - options.moveDuration: number (seconds) for smooth camera move to selected point | |
| */ | |
| export async function initializePoints(options) { | |
| const { | |
| app, | |
| cameraEntity, | |
| modelEntity, | |
| pointsUrl, | |
| defaultVisible, | |
| moveDuration = 0.6 | |
| } = options; | |
| if (!app || !cameraEntity || !pointsUrl) { | |
| console.error("points.js → missing required initialization options"); | |
| return; | |
| } | |
| // Load JSON of points: | |
| let pointsData; | |
| try { | |
| const resp = await fetch(pointsUrl); | |
| pointsData = await resp.json(); | |
| } catch (e) { | |
| console.error("points.js → failed fetching points.json:", e); | |
| return; | |
| } | |
| if (!Array.isArray(pointsData)) { | |
| console.error("points.js → points.json must be an array"); | |
| return; | |
| } | |
| const pointEntities = []; | |
| // Create a material for info-point spheres | |
| 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.update(); | |
| // Build each sphere + attach custom data | |
| for (let i = 0; i < pointsData.length; i++) { | |
| const pt = pointsData[i]; | |
| const { x, y, z, text, imageUrl } = pt; | |
| const sphere = new pc.Entity("point-" + i); | |
| sphere.addComponent("model", { type: "sphere" }); | |
| sphere.model.material = mat; | |
| // Scale small (primitive sphere radius = 0.5) | |
| sphere.setLocalScale(0.05, 0.05, 0.05); | |
| sphere.setLocalPosition(x, y, z); | |
| sphere.pointData = { text, imageUrl }; | |
| app.root.addChild(sphere); | |
| pointEntities.push(sphere); | |
| } | |
| // Show/hide all point spheres | |
| function setPointsVisibility(visible) { | |
| pointEntities.forEach(ent => { | |
| ent.enabled = visible; | |
| }); | |
| } | |
| setPointsVisibility(!!defaultVisible); | |
| // Respond to toggle-points event from interface.js | |
| document.addEventListener("toggle-points", (evt) => { | |
| const { visible } = evt.detail; | |
| setPointsVisibility(!!visible); | |
| }); | |
| // Keep track of any in-flight camera tween so we can cancel it | |
| let currentTween = null; | |
| // On mouse down (or touch equivalent), perform manual ray‐sphere intersection | |
| app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => { | |
| // If a tween is running, cancel it immediately | |
| if (currentTween) { | |
| app.off("update", currentTween); | |
| currentTween = null; | |
| } | |
| const x = event.x; | |
| const y = event.y; | |
| const from = new pc.Vec3(); | |
| const to = new pc.Vec3(); | |
| const camera = cameraEntity.camera; | |
| cameraEntity.camera.screenToWorld(x, y, camera.nearClip, from); | |
| cameraEntity.camera.screenToWorld(x, y, camera.farClip, to); | |
| const dir = new pc.Vec3().sub2(to, from).normalize(); | |
| let closestT = Infinity; | |
| let pickedEntity = null; | |
| for (const ent of pointEntities) { | |
| 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) { | |
| const { text, imageUrl } = pickedEntity.pointData; | |
| document.dispatchEvent(new CustomEvent("point-selected", { | |
| detail: { text, imageUrl } | |
| })); | |
| tweenCameraToPoint(pickedEntity, moveDuration); | |
| } | |
| }); | |
| // Also close tooltip if user interacts (mouse or touch) on the canvas | |
| const canvasId = app.graphicsDevice.canvas.id; | |
| const htmlCanvas = document.getElementById(canvasId); | |
| if (htmlCanvas) { | |
| htmlCanvas.addEventListener("mousedown", () => { | |
| document.dispatchEvent(new CustomEvent("hide-tooltip")); | |
| }); | |
| htmlCanvas.addEventListener("touchstart", () => { | |
| document.dispatchEvent(new CustomEvent("hide-tooltip")); | |
| }); | |
| } | |
| // Tween helper: smoothly move and reorient camera to focus the chosen point entity | |
| function tweenCameraToPoint(pointEnt, duration) { | |
| const orbitCam = cameraEntity.script.orbitCamera; | |
| if (!orbitCam) return; | |
| // Compute target pivot exactly at the sphere center | |
| const targetPos = pointEnt.getPosition().clone(); | |
| // Compute current state | |
| const startPivot = orbitCam.pivotPoint.clone(); | |
| const startYaw = orbitCam._yaw; | |
| const startPitch = orbitCam._pitch; | |
| const startDist = orbitCam._distance; | |
| // Compute direction & candidate distance: | |
| const worldRadius = 0.5 * pointEnt.getLocalScale().x; | |
| const minZoom = orbitCam.distanceMin; | |
| const desiredDistance = Math.max(minZoom * 1.2, worldRadius * 4); | |
| // Compute target yaw/pitch from camera pointing at targetPos | |
| // Reuse reset logic: place a temp entity at camera’s current position, have it look at target | |
| const camWorldPos = cameraEntity.getPosition().clone(); | |
| const tempEnt = new pc.Entity(); | |
| tempEnt.setPosition(camWorldPos); | |
| tempEnt.lookAt(targetPos); | |
| const rotation = tempEnt.getRotation(); | |
| const forward = new pc.Vec3(); | |
| rotation.transformVector(pc.Vec3.FORWARD, forward); | |
| const tgtYaw = Math.atan2(-forward.x, -forward.z) * pc.math.RAD_TO_DEG; | |
| const yawQuat = new pc.Quat().setFromEulerAngles(0, -tgtYaw, 0); | |
| const rotNoYaw = new pc.Quat().mul2(yawQuat, rotation); | |
| const fNoYaw = new pc.Vec3(); | |
| rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw); | |
| const tgtPitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG; | |
| tempEnt.destroy(); | |
| // Target state: | |
| const endPivot = targetPos.clone(); | |
| const endYaw = tgtYaw; | |
| const endPitch = tgtPitch; | |
| const endDist = desiredDistance; | |
| let elapsed = 0; | |
| const orgPivot = startPivot.clone(); | |
| const orgYaw = startYaw; | |
| const orgPitch = startPitch; | |
| const orgDist = startDist; | |
| // If another tween is running, cancel it | |
| if (currentTween) { | |
| app.off("update", currentTween); | |
| currentTween = null; | |
| } | |
| // Per-frame update | |
| function lerpUpdate(dt) { | |
| elapsed += dt; | |
| const t = Math.min(elapsed / duration, 1); | |
| // Interpolate pivot (vector lerp) | |
| const newPivot = new pc.Vec3().lerp(orgPivot, endPivot, t); | |
| orbitCam.pivotPoint.copy(newPivot); | |
| // Interpolate yaw/pitch/distance (simple lerp) | |
| 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; | |
| orbitCam._updatePosition(); | |
| if (t >= 1) { | |
| app.off("update", lerpUpdate); | |
| currentTween = null; | |
| } | |
| } | |
| currentTween = lerpUpdate; | |
| app.on("update", lerpUpdate); | |
| } | |
| } | |