diff --git a/src/components/World/World.js b/src/components/World/World.js index f60490bc..dca75c13 100644 --- a/src/components/World/World.js +++ b/src/components/World/World.js @@ -15,12 +15,13 @@ import { createRenderer } from "./systems/renderer.js"; const COLOR1 = "#dddddd"; const COLOR2 = "#0055aa"; -const PLATFORM_BORDER = 10; -const PLATFORM_EDGE_FOR_UNKNOWNS = 10; -const PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100 class World { constructor(container, vue) { + this.PLATFORM_BORDER = 5; + this.PLATFORM_EDGE_FOR_UNKNOWNS = 10; + this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100 + this.update = this.update.bind(this); // Instances of camera, scene, and renderer @@ -49,9 +50,11 @@ class World { // Terrain Instance const terrain = createTerrain({ color: COLOR1, - height: PLATFORM_SIZE + PLATFORM_BORDER * 2, + height: this.PLATFORM_SIZE + this.PLATFORM_BORDER * 2, width: - PLATFORM_SIZE + PLATFORM_BORDER * 2 + PLATFORM_EDGE_FOR_UNKNOWNS * 2, + this.PLATFORM_SIZE + + this.PLATFORM_BORDER * 2 + + this.PLATFORM_EDGE_FOR_UNKNOWNS * 2, }); this.loop.updatables.push(controls); diff --git a/src/components/World/components/objects/landmarks.js b/src/components/World/components/objects/landmarks.js index 9326a820..40e8f20b 100644 --- a/src/components/World/components/objects/landmarks.js +++ b/src/components/World/components/objects/landmarks.js @@ -1,21 +1,39 @@ import axios from "axios"; +import * as R from "ramda"; import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as TWEEN from "@tweenjs/tween.js"; import { AppString } from "@/constants/app"; +import { accountsDB, db } from "@/db"; +import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import { accessToken } from "@/libs/crypto"; const ANIMATION_DURATION_SECS = 10; const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; export async function loadLandmarks(vue, world, scene, loop) { const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER; try { + await db.open(); + const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const activeDid = settings?.activeDid || ""; + await accountsDB.open(); + const accounts = await accountsDB.accounts.toArray(); + const account = R.find((acc) => acc.did === activeDid, accounts); + const identity = JSON.parse(account?.identity || "undefined"); + const token = await accessToken(identity); + const url = endorserApiServer + "/api/v2/report/claims?claimType=GiveAction"; - const headers = { "Content-Type": "application/json" }; + const headers = { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }; const resp = await axios.get(url, { headers: headers }); if (resp.status === 200) { + console.log("resp.data.data", resp.data.data); const minDate = resp.data.data[resp.data.data.length - 1].issuedAt; const maxDate = resp.data.data[0].issuedAt; const minTimeMillis = new Date(minDate).getTime(); @@ -33,24 +51,27 @@ export async function loadLandmarks(vue, world, scene, loop) { //const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers // modScale = 2; //const modelLoc = "/models/a_bush/scene.gltf", // purple leaves - // modScale = 15, + // modScale = 15; + + // calculate positions for each claim, especially because some are random + const locations = resp.data.data.map((claim) => + locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS) + ); // eslint-disable-next-line @typescript-eslint/no-this-alias - const parentWorld = world; loader.load( modelLoc, function (gltf) { gltf.scene.scale.set(0, 0, 0); - for (const claim of resp.data.data) { + for (let i = 0; i < resp.data.data.length; i++) { + // claim is a GiveServerRecord (see endorserServer.ts) + const claim = resp.data.data[i]; const newPlant = SkeletonUtils.clone(gltf.scene); - const randomness = claim.id.substring(10); - const x = - (100 * BASE32.indexOf(randomness.substring(0, 1))) / 32 - 50; - const z = - (100 * BASE32.indexOf(randomness.substring(8, 9))) / 32 - 50; - newPlant.position.set(x, 0, z); - - parentWorld.scene.add(newPlant); + + const loc = locations[i]; + newPlant.position.set(loc.x, 0, loc.z); + + world.scene.add(newPlant); const timeDelayMillis = fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis); @@ -58,7 +79,7 @@ export async function loadLandmarks(vue, world, scene, loop) { .delay(timeDelayMillis) .to({ x: modScale, y: modScale, z: modScale }, 5000) .start(); - parentWorld.bushes = [...parentWorld.bushes, newPlant]; + world.bushes = [...world.bushes, newPlant]; } }, undefined, @@ -68,16 +89,14 @@ export async function loadLandmarks(vue, world, scene, loop) { ); // calculate when lights shine on appearing claim area - for (const claim of resp.data.data) { + for (let i = 0; i < resp.data.data.length; i++) { // claim is a GiveServerRecord (see endorserServer.ts) + const claim = resp.data.data[i]; - // compute location for this claim - const randomness = claim.id.substring(10); - const x = (100 * BASE32.indexOf(randomness.substring(0, 1))) / 32 - 50; - const z = (100 * BASE32.indexOf(randomness.substring(8, 9))) / 32 - 50; + const loc = locations[i]; const light = createLight(); - light.position.set(x, 20, z); - light.target.position.set(x, 0, z); + light.position.set(loc.x, 20, loc.z); + light.target.position.set(loc.x, 0, loc.z); loop.updatables.push(light); scene.add(light); scene.add(light.target); @@ -119,6 +138,86 @@ export async function loadLandmarks(vue, world, scene, loop) { } } +/** + * + * @param giveClaim + * @returns {x:float, z:float} where -50 <= x & z < 50 + */ +function locForGive(giveClaim, platformWidth, borderWidth) { + let loc; + if (giveClaim?.claim?.recipient?.identifier) { + // this is directly to a person + loc = locForEthrDid(giveClaim.claim.recipient.identifier); + loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }; + } else if (giveClaim?.object?.isPartOf?.identifier) { + // this is probably to a project + const objId = giveClaim.object.isPartOf.identifier; + if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) { + loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length)); + loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }; + } + } + if (!loc) { + // it must be outside our known addresses so let's put it somewhere random + const leftSide = Math.random() < 0.5; + loc = { + x: leftSide + ? -platformWidth / 2 - borderWidth / 2 + : platformWidth / 2 + borderWidth / 2, + z: Math.random() * platformWidth - platformWidth / 2, + }; + } + return loc; +} + +/** + * Generate a deterministic x & z location based on the randomness in a ULID. + * + * We'll use the first 20 bits for the x coordinate and next 20 for the z. + * That's a bit over a trillion locations... should be enough for pretty + * unique locations for this fun kind of application within a network + * -- though they're not guaranteed unique without the full ID. + * (We're leaving 40 bits for other properties.) + * + * @param ulid + * @returns {x: float, z: float} where 0 <= x & z < 100 + */ +function locForUlid(ulid) { + // The random parts of a ULID come after the first 10 characters. + const randomness = ulid.substring(10); + + // That leaves 16 characters of randomness, or 80 bits. + + // We're currently only using 32 possible x and z values + // because the display is pretty low-fidelity at this point. + + // Similar code is below. + const x = (100 * BASE32.indexOf(randomness.substring(0, 1))) / 32; + const z = (100 * BASE32.indexOf(randomness.substring(4, 5))) / 32; + return { x, z }; +} + +/** + * See locForUlid + * @param did + * @returns {x: float, z: float} where 0 <= x & z < 100 + */ +function locForEthrDid(did) { + // "did:ethr:0x..." + if (did.length < 51) { + return { x: 0, z: 0 }; + } else { + const randomness = did.substring(11); + // We'll take the first 4 bits for 16 possible x & z values. + const xOff = parseInt(Number("0x" + randomness.substring(0, 1)), 10); + const x = (xOff * 100) / 16; + // ... and since we're reserving 20 bits total for x, start with character 5. + const zOff = parseInt(Number("0x" + randomness.substring(5, 6)), 10); + const z = (zOff * 100) / 16; + return { x, z }; + } +} + function createLight() { const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0); // eslint-disable-next-line @typescript-eslint/no-empty-function